Add front-end fixes
This commit is contained in:
parent
30f4bc6fa6
commit
afa4cf27b7
57 changed files with 805 additions and 210 deletions
|
|
@ -1,5 +1,4 @@
|
|||
import { FC } from "react"
|
||||
import Link from "next/link"
|
||||
import { Link } from "react-router"
|
||||
import useRoutes from "@/hooks/useRoutes"
|
||||
import { UserType } from "@/types/UserType"
|
||||
import { DishType } from "@/types/DishType"
|
||||
|
|
@ -9,7 +8,7 @@ interface Props {
|
|||
users: UserType[]
|
||||
}
|
||||
|
||||
const OnboardingBanner: FC<Props> = ({ dishes, users }) => {
|
||||
const OnboardingBanner = ({ dishes, users }: Props) => {
|
||||
const routes = useRoutes();
|
||||
|
||||
const steps = [
|
||||
|
|
@ -36,7 +35,7 @@ const OnboardingBanner: FC<Props> = ({ dishes, users }) => {
|
|||
<div className="w-full text-center text-2xl my-5" key={index}>
|
||||
{
|
||||
step.count === 0
|
||||
? <Link href={ step.href } className="underline">{ step.label }</Link>
|
||||
? <Link to={ step.href } className="underline">{ step.label }</Link>
|
||||
: <div className="line-through">{ step.label }</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { FC, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { DishType } from "@/types/DishType";
|
||||
import { UserType } from "@/types/UserType";
|
||||
import { useFetchUsers } from "@/hooks/useFetchUsers";
|
||||
|
|
@ -12,7 +12,7 @@ interface Props {
|
|||
reloadDish: () => void;
|
||||
}
|
||||
|
||||
const AddUserToDishForm: FC<Props> = ({ dish, reloadDish }) => {
|
||||
const AddUserToDishForm = ({ dish, reloadDish }: Props) => {
|
||||
const [showAdd, setShowAdd] = useState<boolean>(false);
|
||||
const [selectedUser, setSelectedUser] = useState<string>("-1");
|
||||
const { users, isLoading: isUsersLoading } = useFetchUsers();
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { createDish } from "@/utils/api/dishApi";
|
||||
import PageTitle from "@/components/ui/PageTitle";
|
||||
import Alert from "@/components/ui/Alert";
|
||||
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
||||
import OutlineLinkButton from "@/components/ui/Buttons/OutlineLinkButton";
|
||||
import { useNavigate } from "react-router";
|
||||
import { createDish } from "~/utils/api/dishApi";
|
||||
import PageTitle from "~/components/ui/PageTitle";
|
||||
import Alert from "~/components/ui/Alert";
|
||||
import SolidButton from "~/components/ui/Buttons/SolidButton";
|
||||
import OutlineLinkButton from "~/components/ui/Buttons/OutlineLinkButton";
|
||||
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
||||
import Hr from "@/components/ui/Hr"
|
||||
import Hr from "~/components/ui/Hr"
|
||||
|
||||
const CreateDishForm = () => {
|
||||
const router = useRouter()
|
||||
const navigate = useNavigate()
|
||||
const [name, setName] = useState<string>("");
|
||||
const [error, setError] = useState<string>("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -35,7 +35,7 @@ const CreateDishForm = () => {
|
|||
try {
|
||||
const result = await createDish(name);
|
||||
if (result) {
|
||||
router.push('/dishes')
|
||||
navigate('/dishes')
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
setError(error instanceof Error ? error.message : "An unexpected error occurred.");
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import {DishType} from "@/types/DishType";
|
||||
import {DishType} from "~/types/DishType";
|
||||
|
||||
import {PencilIcon, TrashIcon} from '@heroicons/react/24/solid'
|
||||
import Link from "next/link";
|
||||
import useRoutes from "@/hooks/useRoutes";
|
||||
import {UserType} from "@/types/UserType";
|
||||
import Card from "@/components/layout/Card";
|
||||
import { Link } from "react-router";
|
||||
import useRoutes from "~/hooks/useRoutes";
|
||||
import {UserType} from "~/types/UserType";
|
||||
import Card from "~/components/layout/Card";
|
||||
|
||||
const Dish = ({ dish }: { dish: DishType}) => {
|
||||
const routes = useRoutes();
|
||||
|
|
@ -21,12 +21,12 @@ const Dish = ({ dish }: { dish: DishType}) => {
|
|||
}
|
||||
</div>
|
||||
<div className="flex-none flex gap-2">
|
||||
<Link href={routes.dish.edit(dish)}>
|
||||
<Link to={routes.dish.edit(dish)}>
|
||||
<div className="border border-foreground p-2 rounded">
|
||||
<PencilIcon width="14" />
|
||||
</div>
|
||||
</Link>
|
||||
<Link href={routes.dish.delete(dish)}>
|
||||
<Link to={routes.dish.delete(dish)}>
|
||||
<div className="border border-red-500 p-2 rounded">
|
||||
<TrashIcon width="14" className="text-red-500" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import {UserType} from "@/types/UserType";
|
||||
import {FC} from "react";
|
||||
import {DishType} from "@/types/DishType";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -7,7 +6,7 @@ interface Props {
|
|||
dish: DishType,
|
||||
}
|
||||
|
||||
const DishCard: FC<Props> = ({ user, dish }: Props) => {
|
||||
const DishCard = ({ user, dish }: Props) => {
|
||||
return (
|
||||
<div className="w-full flex py-2 text-xl font-bold text-primary">
|
||||
<div className="user-badge flex-none w-16 text-center pr-5">
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import React, {FC, useState} from "react";
|
||||
import {useRouter} from "next/navigation";
|
||||
import Alert from "@/components/ui/Alert";
|
||||
import {updateDish} from "@/utils/api/dishApi";
|
||||
import {DishType} from "@/types/DishType";
|
||||
import useRoutes from "@/hooks/useRoutes";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import Button from "@/components/ui/Button"
|
||||
import React, {useState} from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import Alert from "~/components/ui/Alert";
|
||||
import {updateDish} from "~/utils/api/dishApi";
|
||||
import {DishType} from "~/types/DishType";
|
||||
import useRoutes from "~/hooks/useRoutes";
|
||||
import Spinner from "~/components/Spinner";
|
||||
import Button from "~/components/ui/Button"
|
||||
|
||||
interface Props {
|
||||
dish: DishType
|
||||
}
|
||||
|
||||
const EditDishForm: FC<Props> = ({ dish }) => {
|
||||
const EditDishForm = ({ dish }: Props) => {
|
||||
const [name, setName] = useState<string>(dish.name);
|
||||
const [error, setError] = useState<string>("");
|
||||
const router = useRouter()
|
||||
const navigate = useNavigate()
|
||||
const [loading, setLoading] = useState(false);
|
||||
const routes = useRoutes();
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ const EditDishForm: FC<Props> = ({ dish }) => {
|
|||
try {
|
||||
const result = await updateDish(dish.id, name);
|
||||
if (result) {
|
||||
router.push(routes.dish.index())
|
||||
navigate(routes.dish.index())
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
setError(error instanceof Error ? error.message : "An unexpected error occurred");
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React, {FC} from "react";
|
||||
import React from "react";
|
||||
import SectionTitle from "@/components/ui/SectionTitle";
|
||||
import {syncUserDishRecurrences} from "@/utils/api/usersApi";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import {UserDishType} from "@/types/ScheduledUserDishType";
|
||||
import {RecurrenceType} from "@/types/ScheduleType";
|
||||
import {RecurrenceType} from "@/types/RecurrenceType";
|
||||
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -11,7 +11,7 @@ interface Props {
|
|||
onSubmit: () => void
|
||||
}
|
||||
|
||||
const EditDishUserCardEditForm: FC<Props> = ({ userDish, onSubmit}) => {
|
||||
const EditDishUserCardEditForm = ({ userDish, onSubmit}: Props) => {
|
||||
const weeklyRecurrence = userDish.recurrences.find((recurrence) => recurrence.type === 'App\\Models\\WeeklyRecurrence')
|
||||
const minimumRecurrence = userDish.recurrences.find((recurrence) => recurrence.type === 'App\\Models\\MinimumRecurrence')
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import {FC} from "react";
|
||||
import {RecurrenceType} from "@/types/ScheduleType";
|
||||
import {RecurrenceType} from "@/types/RecurrenceType";
|
||||
|
||||
interface Props {
|
||||
recurrences: RecurrenceType[];
|
||||
}
|
||||
|
||||
const RecurrenceLabels: FC<Props> = ({recurrences}) => {
|
||||
const RecurrenceLabels = ({recurrences}: Props) => {
|
||||
const weeklyRecurrences = recurrences.filter(recurrence => recurrence.type === 'App\\Models\\WeeklyRecurrence');
|
||||
const minimumRecurrences = recurrences.filter(recurrence => recurrence.type === 'App\\Models\\MinimumRecurrence');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { FC } from "react";
|
||||
import React from "react";
|
||||
import { DishType } from "@/types/DishType";
|
||||
import { UserType } from "@/types/UserType";
|
||||
import UserDishCard from "@/components/features/dishes/UserDishCard";
|
||||
|
|
@ -10,7 +10,7 @@ interface Props {
|
|||
reloadDish: () => void;
|
||||
}
|
||||
|
||||
const SyncUsersForm: FC<Props> = ({ dish, reloadDish }) => {
|
||||
const SyncUsersForm = ({ dish, reloadDish }: Props) => {
|
||||
return (
|
||||
<div className="my-2 w-full flex flex-col">
|
||||
<SectionTitle className="mt-2">Users</SectionTitle>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import React, {FC, useEffect} from "react";
|
||||
import {DishType} from "@/types/DishType";
|
||||
import {UserType} from "@/types/UserType";
|
||||
import Link from "next/link";
|
||||
import React, {useEffect} from "react";
|
||||
import {DishType} from "~/types/DishType";
|
||||
import {UserType} from "~/types/UserType";
|
||||
import { Link } from "react-router";
|
||||
import {PencilIcon, TrashIcon} from "@heroicons/react/24/solid";
|
||||
import {removeUserFromDish} from "@/utils/api/dishApi";
|
||||
import EditDishUserCardEditForm from "@/components/features/dishes/EditDishUserCardEditForm";
|
||||
import {getUserDishForUserAndDish} from "@/utils/api/usersApi";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import RecurrenceLabels from "@/components/features/dishes/RecurrenceLabels";
|
||||
import {UserDishType} from "@/types/ScheduledUserDishType";
|
||||
import {removeUserFromDish} from "~/utils/api/dishApi";
|
||||
import EditDishUserCardEditForm from "~/components/features/dishes/EditDishUserCardEditForm";
|
||||
import {getUserDishForUserAndDish} from "~/utils/api/usersApi";
|
||||
import Spinner from "~/components/Spinner";
|
||||
import RecurrenceLabels from "~/components/features/dishes/RecurrenceLabels";
|
||||
import {UserDishType} from "~/types/ScheduledUserDishType";
|
||||
|
||||
interface Props {
|
||||
dish: DishType
|
||||
|
|
@ -16,7 +16,7 @@ interface Props {
|
|||
reloadDish: () => void
|
||||
}
|
||||
|
||||
const UserDishCard: FC<Props> = ({dish, user, reloadDish}) => {
|
||||
const UserDishCard = ({dish, user, reloadDish}: Props) => {
|
||||
const [userDish, setUserDish] = React.useState<UserDishType|null>(null);
|
||||
const [userDishLoading, setUserDishLoading] = React.useState(true);
|
||||
const [isEditMode, setIsEditMode] = React.useState(false);
|
||||
|
|
@ -56,14 +56,14 @@ const UserDishCard: FC<Props> = ({dish, user, reloadDish}) => {
|
|||
</div>
|
||||
|
||||
<div className="flex-none w-8">
|
||||
<Link onClick={() => setIsEditMode(!isEditMode)} href="#">
|
||||
<Link onClick={() => setIsEditMode(!isEditMode)} to="#">
|
||||
<div className="border border-foreground p-2 rounded">
|
||||
<PencilIcon width="14"/>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-none w-8">
|
||||
<Link onClick={handleRemove} href="#">
|
||||
<Link onClick={handleRemove} to="#">
|
||||
<div className="border border-red-500 p-2 rounded">
|
||||
<TrashIcon width="14" className="text-red-500"/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Link from "next/link";
|
||||
import React, {FC} from "react";
|
||||
import useRoutes from "@/hooks/useRoutes";
|
||||
import { Link } from "react-router";
|
||||
import React from "react";
|
||||
import useRoutes from "~/hooks/useRoutes";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -21,7 +21,7 @@ const linkStyles = classNames(
|
|||
'space-grotesk', 'text-xl'
|
||||
)
|
||||
|
||||
const MobileDropdownMenu: FC<Props> = ({ isOpen, setIsOpen, handleLogout }) => {
|
||||
const MobileDropdownMenu = ({ isOpen, setIsOpen, handleLogout }: Props) => {
|
||||
const routes = useRoutes();
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
|
@ -29,35 +29,35 @@ const MobileDropdownMenu: FC<Props> = ({ isOpen, setIsOpen, handleLogout }) => {
|
|||
return (
|
||||
<div className={divStyles}>
|
||||
<Link
|
||||
href={routes.home()}
|
||||
to={routes.home()}
|
||||
className={linkStyles}
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<Link
|
||||
href={routes.dish.index()}
|
||||
to={routes.dish.index()}
|
||||
className={linkStyles}
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Dishes
|
||||
</Link>
|
||||
<Link
|
||||
href={routes.user.index()}
|
||||
to={routes.user.index()}
|
||||
className={linkStyles}
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Users
|
||||
</Link>
|
||||
<Link
|
||||
href={routes.schedule.history()}
|
||||
to={routes.schedule.history()}
|
||||
className={linkStyles}
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
History
|
||||
</Link>
|
||||
<Link
|
||||
href={routes.auth.login()}
|
||||
to={routes.auth.login()}
|
||||
onClick={handleLogout}
|
||||
className={linkStyles}>
|
||||
Logout
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
"use client"
|
||||
|
||||
import {FC} from "react";
|
||||
import ScheduleDayCard from "@/components/features/schedule/dayCard/ScheduleDayCard";
|
||||
import { FilledScheduleType, ScheduleType } from "@/types/ScheduleType";
|
||||
import {useFetchUsers} from "@/hooks/useFetchUsers";
|
||||
|
|
@ -52,7 +49,7 @@ interface Props {
|
|||
schedule: ScheduleType[];
|
||||
}
|
||||
|
||||
const ScheduleCalendar: FC<Props> = ({ schedule }: Props) => {
|
||||
const ScheduleCalendar = ({ schedule }: Props) => {
|
||||
const {users, isLoading: areUsersLoading} = useFetchUsers();
|
||||
|
||||
if (areUsersLoading) return <Spinner />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ScheduleType } from "@/types/ScheduleType";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import PageTitle from "@/components/ui/PageTitle";
|
||||
|
|
@ -20,7 +20,7 @@ interface Props {
|
|||
date: string;
|
||||
}
|
||||
|
||||
const ScheduleEditForm: FC<Props> = ({ date }) => {
|
||||
const ScheduleEditForm = ({ date }: Props) => {
|
||||
const [schedule, setSchedule] = useState<ScheduleType>()
|
||||
const [userDishes, setUserDishes] = useState<UserDishType[]>([])
|
||||
const [isScheduleLoading, setIsScheduleLoading] = useState(true);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type FC, useState} from "react";
|
||||
import { useState} from "react";
|
||||
import Modal from "@/components/ui/Modal";
|
||||
import ScheduleRegenerateForm from "@/components/features/schedule/ScheduleRegenerateForm";
|
||||
import {ArrowPathIcon} from "@heroicons/react/16/solid";
|
||||
|
|
@ -7,7 +7,7 @@ interface ScheduleRegenerateButtonProps {
|
|||
onModalClose?: () => void;
|
||||
}
|
||||
|
||||
const ScheduleRegenerateButton: FC<ScheduleRegenerateButtonProps> = ({ onModalClose }) => {
|
||||
const ScheduleRegenerateButton = ({ onModalClose }: ScheduleRegenerateButtonProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleCloseModal = () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {DialogTitle} from "@headlessui/react";
|
||||
import Toggle from "@/components/ui/Toggle";
|
||||
import {FC, useEffect, useState} from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import {generateSchedule} from "@/utils/api/scheduleApi";
|
||||
import Alert from "@/components/ui/Alert";
|
||||
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
||||
|
|
@ -9,7 +9,7 @@ interface ScheduleRegenerateFormProps {
|
|||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const ScheduleRegenerateForm: FC<ScheduleRegenerateFormProps> = ({closeModal}) => {
|
||||
const ScheduleRegenerateForm = ({closeModal}: ScheduleRegenerateFormProps) => {
|
||||
const [overwrite, setOverwrite] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { DateTime } from "luxon";
|
||||
import ScheduleCalendar from "@/components/features/schedule/ScheduleCalendar";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {FC, FormEvent, useMemo, useState} from "react";
|
||||
import {FormEvent, useMemo, useState} from "react";
|
||||
import {DishType} from "@/types/DishType";
|
||||
import {ScheduledUserDishType} from "@/types/ScheduledUserDishType";
|
||||
import {updateScheduledUserDish} from "@/utils/api/scheduledUserDishesApi";
|
||||
|
|
@ -10,7 +10,7 @@ interface Props {
|
|||
allDishes: DishType[]
|
||||
}
|
||||
|
||||
const UserDishEditCard: FC<Props> = ({ scheduledUserDish, allDishes }) => {
|
||||
const UserDishEditCard = ({ scheduledUserDish, allDishes }: Props) => {
|
||||
const [selectedUserDishId, setSelectedUserDishId] = useState<number>(scheduledUserDish.user_dish ? scheduledUserDish.user_dish.id : 0)
|
||||
const [errorMessage, setErrorMessage] = useState<string>("")
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import {DateTime} from "luxon";
|
||||
import React, {FC} from "react";
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -7,7 +7,7 @@ interface Props {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
const DateBadge: FC<Props> = ({ className, date }) => {
|
||||
const DateBadge = ({ className, date }: Props) => {
|
||||
const isToday = DateTime.fromISO(date).toFormat("yyyy-LL-dd") == DateTime.now().toFormat("yyyy-LL-dd")
|
||||
|
||||
const textStyle = classNames("inline font-bold", {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import React, {FC} from "react";
|
||||
import {UserType} from "@/types/UserType";
|
||||
import ScheduleDayCardUserDish from "@/components/features/schedule/dayCard/ScheduleDayCardUserDish";
|
||||
import { FilledScheduleType, ScheduleType } from "@/types/ScheduleType";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import {UserType} from "~/types/UserType";
|
||||
import ScheduleDayCardUserDish from "~/components/features/schedule/dayCard/ScheduleDayCardUserDish";
|
||||
import { FilledScheduleType, ScheduleType } from "~/types/ScheduleType";
|
||||
import { Link } from "react-router";
|
||||
import {PencilSquareIcon} from "@heroicons/react/24/outline";
|
||||
import useRoutes from "@/hooks/useRoutes";
|
||||
import DateBadge from "@/components/features/schedule/dayCard/DateBadge";
|
||||
import useRoutes from "~/hooks/useRoutes";
|
||||
import DateBadge from "~/components/features/schedule/dayCard/DateBadge";
|
||||
import { DateTime } from "luxon"
|
||||
import classNames from "classnames"
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ interface Props {
|
|||
users: UserType[];
|
||||
}
|
||||
|
||||
const ScheduleDayCard: FC<Props> = ({schedule, users}) => {
|
||||
const ScheduleDayCard = ({schedule, users}: Props) => {
|
||||
const routes = useRoutes()
|
||||
const isToday = DateTime.fromISO(schedule.date).toFormat("yyyy-LL-dd") == DateTime.now().toFormat("yyyy-LL-dd")
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ const ScheduleDayCard: FC<Props> = ({schedule, users}) => {
|
|||
<Link
|
||||
className="w-full flex text-sm pr-3 pt-2"
|
||||
aria-label="Edit dish"
|
||||
href={routes.schedule.date.edit(schedule.date)}
|
||||
to={routes.schedule.date.edit(schedule.date)}
|
||||
>
|
||||
<PencilSquareIcon className="h-5 w-5 mr-2 ml-auto" /> Edit
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { FC } from "react";
|
||||
import React from "react";
|
||||
import { ScheduledUserDishType } from "@/types/ScheduledUserDishType";
|
||||
import { UserType } from "@/types/UserType";
|
||||
import { FilledScheduleType, ScheduleType } from "@/types/ScheduleType";
|
||||
|
|
@ -8,7 +8,7 @@ interface Props {
|
|||
user: UserType;
|
||||
}
|
||||
|
||||
const ScheduleDayCardUserDish: FC<Props> = ({ schedule, user }) => {
|
||||
const ScheduleDayCardUserDish = ({ schedule, user }: Props) => {
|
||||
const getDish = (user: UserType) => {
|
||||
const scheduled_dishes = schedule.scheduled_user_dishes.filter((scheduled_user_dish: ScheduledUserDishType) => (
|
||||
scheduled_user_dish.user_dish?.user.id == user.id
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
import React, {FC, useState} from "react";
|
||||
import {useRouter} from "next/navigation";
|
||||
import useRoutes from "@/hooks/useRoutes";
|
||||
import {updateUser} from "@/utils/api/usersApi";
|
||||
import PageTitle from "@/components/ui/PageTitle";
|
||||
import Link from "next/link";
|
||||
import Alert from "@/components/ui/Alert";
|
||||
import {UserType} from "@/types/UserType";
|
||||
import SolidButton from "@/components/ui/Buttons/SolidButton";
|
||||
import React, {useState} from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import useRoutes from "~/hooks/useRoutes";
|
||||
import {updateUser} from "~/utils/api/usersApi";
|
||||
import PageTitle from "~/components/ui/PageTitle";
|
||||
import { Link } from "react-router";
|
||||
import Alert from "~/components/ui/Alert";
|
||||
import {UserType} from "~/types/UserType";
|
||||
import SolidButton from "~/components/ui/Buttons/SolidButton";
|
||||
|
||||
interface Props {
|
||||
user: UserType;
|
||||
}
|
||||
|
||||
const EditUserForm: FC<Props> = ({ user }) => {
|
||||
const EditUserForm = ({ user }: Props) => {
|
||||
|
||||
const [name, setName] = useState<string>(user.name);
|
||||
const [error, setError] = useState<string>('');
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
const routes = useRoutes();
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
|
|
@ -30,14 +30,14 @@ const EditUserForm: FC<Props> = ({ user }) => {
|
|||
|
||||
updateUser(user, name)
|
||||
.then(() => {
|
||||
router.push(routes.user.index())
|
||||
navigate(routes.user.index())
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col items-center">
|
||||
<PageTitle>Create User</PageTitle>
|
||||
<Link href={routes.user.index()} className="mr-auto">Back to users</Link>
|
||||
<Link to={routes.user.index()} className="mr-auto">Back to users</Link>
|
||||
|
||||
<form onSubmit={handleSubmit} className="w-full max-w-sm mt-4 border-secondary border-2 rounded p-4">
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,45 +1,26 @@
|
|||
import { useAuth } from '@/context/AuthContext';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from "react-router"
|
||||
|
||||
// Optional Loading spinner component to display while loading
|
||||
const LoadingSpinner = () => (
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
<div className="animate-spin w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full"></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||
const { isAuthenticated } = useAuth(); // Access the authentication state from AuthContext
|
||||
const { isAuthenticated } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const location = useLocation();
|
||||
|
||||
const publicRoutes = ['/login', '/register'];
|
||||
const isPublic = publicRoutes.includes(location.pathname);
|
||||
|
||||
useEffect(() => {
|
||||
// Determine behavior based on auth state and route type
|
||||
if (isAuthenticated === null) {
|
||||
// Await authentication resolution (e.g., token check)
|
||||
setLoading(true);
|
||||
} else if (isAuthenticated && isPublic) {
|
||||
// Handle redirects based on auth state
|
||||
if (isAuthenticated && isPublic) {
|
||||
// Redirect authenticated users away from public pages
|
||||
navigate('/', { replace: true });
|
||||
} else if (!isAuthenticated && !isPublic) {
|
||||
// Redirect unauthenticated users trying to access protected pages
|
||||
navigate('/login', { replace: true });
|
||||
} else {
|
||||
// Otherwise, stop loading since the state is resolved
|
||||
setLoading(false);
|
||||
}
|
||||
}, [isAuthenticated, location.pathname, isPublic, navigate]);
|
||||
|
||||
// Show a spinner while authentication state is loading
|
||||
if (loading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
// Render children only when the authentication state and path are valid
|
||||
// Render children for all routes - redirects will happen via useEffect
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import React, {FC} from "react";
|
||||
import React from "react";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Card: FC<Props> = ({ children }) => {
|
||||
const Card = ({ children }: Props) => {
|
||||
return (
|
||||
<div className="w-full border-2 border-secondary p-2 pl-4 my-2 rounded flex">
|
||||
{ children }
|
||||
|
|
|
|||
|
|
@ -1,29 +1,26 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import useRoutes from "@/hooks/useRoutes";
|
||||
import MobileDropdownMenu from "@/components/features/navbar/MobileDropdownMenu";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useAuth} from "@/context/AuthContext";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import useRoutes from "~/hooks/useRoutes";
|
||||
import MobileDropdownMenu from "~/components/features/navbar/MobileDropdownMenu";
|
||||
import { useAuth } from "~/context/AuthContext";
|
||||
|
||||
const NavBar = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const routes = useRoutes();
|
||||
const router = useRouter();
|
||||
const navigate = useNavigate();
|
||||
const {isAuthenticated, logout} = useAuth();
|
||||
|
||||
const handleLogout = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
logout();
|
||||
router.replace('/login');
|
||||
navigate('/login', { replace: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="border-b-2 border-secondary shadow-sm z-50 mb-8">
|
||||
<div className="relative flex justify-between items-center px-4 lg:px-8 py-2 lg:container lg:mx-auto">
|
||||
{/* Logo */}
|
||||
<Link href={routes.home()} className="pt-2 text-2xl font-syncopate text-primary">
|
||||
<Link to={routes.home()} className="pt-2 text-2xl font-syncopate text-primary">
|
||||
DISH PLANNER
|
||||
</Link>
|
||||
|
||||
|
|
@ -53,19 +50,19 @@ const NavBar = () => {
|
|||
|
||||
{/* Desktop Menu */}
|
||||
<div className="hidden md:flex space-x-6">
|
||||
<Link href={routes.home()} className="text-accent-blue hover:background-secondary">
|
||||
<Link to={routes.home()} className="text-accent-blue hover:background-secondary">
|
||||
Home
|
||||
</Link>
|
||||
<Link href={routes.dish.index()} className="text-accent-blue hover:background-secondary">
|
||||
<Link to={routes.dish.index()} className="text-accent-blue hover:background-secondary">
|
||||
Dishes
|
||||
</Link>
|
||||
<Link href={routes.user.index()} className="text-accent-blue hover:background-secondary">
|
||||
<Link to={routes.user.index()} className="text-accent-blue hover:background-secondary">
|
||||
Users
|
||||
</Link>
|
||||
<Link href={routes.schedule.history()} className="text-accent-blue hover:background-secondary">
|
||||
<Link to={routes.schedule.history()} className="text-accent-blue hover:background-secondary">
|
||||
History
|
||||
</Link>
|
||||
<Link href={routes.auth.login()}
|
||||
<Link to={routes.auth.login()}
|
||||
onClick={handleLogout}
|
||||
className="text-primary text-right hover:background-secondary">
|
||||
Logout
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React from "react"
|
||||
import type { FC } from "react"
|
||||
import classNames from "classnames";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -8,7 +7,7 @@ interface Props {
|
|||
type: 'error' | 'warning' | 'info' | 'success';
|
||||
}
|
||||
|
||||
const Alert: FC<Props> = ({ children, className, type } ) => {
|
||||
const Alert = ({ children, className, type }: Props) => {
|
||||
let bgColor = 'bg-blue-200'
|
||||
let fgColor = 'bg-blue-800'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import Link from "next/link";
|
||||
import React, { FC, ReactElement, ReactNode } from "react";
|
||||
import { Link } from "react-router";
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface ButtonProps {
|
||||
appearance?: 'solid' | 'outline' | 'text';
|
||||
children: ReactNode;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
href?: string;
|
||||
icon?: ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
|
|
@ -15,10 +15,10 @@ interface ButtonProps {
|
|||
variant?: 'primary' | 'secondary' | 'accent';
|
||||
}
|
||||
|
||||
const Button: FC<ButtonProps> = ({ appearance, children, className, disabled, href, icon, onClick,
|
||||
const Button = ({ appearance, children, className, disabled, href, icon, onClick,
|
||||
size = 'medium', type,
|
||||
variant = 'primary'
|
||||
}) => {
|
||||
}: ButtonProps) => {
|
||||
const styles = classNames(
|
||||
"flex items-center space-x-1",
|
||||
"justify-center font-size-18 py-2 px-4 rounded flex",
|
||||
|
|
@ -40,13 +40,13 @@ const Button: FC<ButtonProps> = ({ appearance, children, className, disabled, hr
|
|||
|
||||
const iconElement =
|
||||
React.isValidElement(icon) &&
|
||||
React.cloneElement(icon as ReactElement<{ className?: string }>, {
|
||||
React.cloneElement(icon as React.ReactElement<{ className?: string }>, {
|
||||
className: iconClassNames,
|
||||
});
|
||||
|
||||
if (href !== undefined) {
|
||||
return (
|
||||
<Link href={href} className={styles}>
|
||||
<Link to={href} className={styles}>
|
||||
{ icon && iconElement}
|
||||
{ children}
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { type FC } from "react";
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -10,7 +10,7 @@ interface Props {
|
|||
type: 'submit' | 'button';
|
||||
}
|
||||
|
||||
const OutlineButton: FC<Props> = ({ children, className, disabled = false, onClick, size, type }) => {
|
||||
const OutlineButton = ({ children, className, disabled = false, onClick, size, type }: Props) => {
|
||||
const style = classNames(
|
||||
"justify-center border-2 border-accent font-size-18 text-accent-blue py-2 px-4 rounded flex",
|
||||
{ 'text-xs': size === "small" },
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { type FC, type ReactElement } from "react";
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { Link } from "react-router"
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ interface Props {
|
|||
variant?: "primary" | "secondary";
|
||||
}
|
||||
|
||||
const OutlineLinkButton: FC<Props> = ({ children, className, href, icon, size = "medium", variant }) => {
|
||||
const OutlineLinkButton = ({ children, className, href, icon, size = "medium", variant }: Props) => {
|
||||
const linkClassNames = classNames(
|
||||
"underline font-default pt-3 pb-3 px-4 rounded mb-0 flex",
|
||||
{
|
||||
|
|
@ -34,7 +34,7 @@ const OutlineLinkButton: FC<Props> = ({ children, className, href, icon, size =
|
|||
|
||||
const iconElement =
|
||||
React.isValidElement(icon) &&
|
||||
React.cloneElement(icon as ReactElement<{ className?: string }>, {
|
||||
React.cloneElement(icon as React.ReactElement<{ className?: string }>, {
|
||||
className: iconClassNames,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { type FC } from "react";
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -10,7 +10,7 @@ interface Props {
|
|||
type: 'submit' | 'button';
|
||||
}
|
||||
|
||||
const SolidButton: FC<Props> = ({ children, className, disabled = false, onClick, size, type }) => {
|
||||
const SolidButton = ({ children, className, disabled = false, onClick, size, type }: Props) => {
|
||||
const style = classNames(
|
||||
"py-2 px-4 bg-primary text-white text-xl p-2 rounded hover:bg-secondary mb-0",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { type FC, type ReactElement } from "react";
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { Link } from "react-router"
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ interface Props {
|
|||
variant?: "primary" | "secondary";
|
||||
}
|
||||
|
||||
const SolidLinkButton: FC<Props> = ({ children, className, href, icon, size = "medium", variant }) => {
|
||||
const SolidLinkButton = ({ children, className, href, icon, size = "medium", variant }: Props) => {
|
||||
const style = classNames(
|
||||
"py-2 px-4 text-xl p-2 rounded hover:bg-secondary mb-0 text-center flex",
|
||||
{
|
||||
|
|
@ -29,7 +29,7 @@ const SolidLinkButton: FC<Props> = ({ children, className, href, icon, size = "m
|
|||
|
||||
const iconElement =
|
||||
React.isValidElement(icon) &&
|
||||
React.cloneElement(icon as ReactElement<{ className?: string }>, {
|
||||
React.cloneElement(icon as React.ReactElement<{ className?: string }>, {
|
||||
className: iconClassNames,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { FC } from "react"
|
||||
import classNames from "classnames"
|
||||
|
||||
interface HrProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Hr: FC<HrProps> = ({ className }) => {
|
||||
const Hr = ({ className }: HrProps) => {
|
||||
const styles = classNames("my-4 border-secondary", className)
|
||||
|
||||
return <hr className={styles}/>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import React, {FC, ReactNode} from "react";
|
||||
import React from "react";
|
||||
|
||||
interface LabelProps {
|
||||
href?: string;
|
||||
children: ReactNode;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const Label: FC<LabelProps> = ({ href, children, onClick }) => {
|
||||
const Label = ({ href, children, onClick }: LabelProps) => {
|
||||
const styles = "items-center space-x-1 background-accent p-2 rounded"
|
||||
|
||||
if (href !== undefined) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type FC, type JSX } from "react";
|
||||
import { type JSX } from "react";
|
||||
import classNames from "classnames";
|
||||
import Button from "@/components/ui/Button"
|
||||
|
||||
|
|
@ -11,14 +11,14 @@ interface ModalProps {
|
|||
setModalOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const Modal: FC<ModalProps> = ({
|
||||
const Modal = ({
|
||||
buttonLabel,
|
||||
buttonClassName,
|
||||
modalChildren,
|
||||
modalOpen,
|
||||
buttonChildren,
|
||||
setModalOpen,
|
||||
}) => {
|
||||
}: ModalProps) => {
|
||||
const buttonStyles = classNames(buttonClassName, 'anta-regular');
|
||||
|
||||
const closeModal = () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { type FC } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -6,7 +5,7 @@ interface Props {
|
|||
className?: string,
|
||||
}
|
||||
|
||||
const PageTitle: FC<Props> = ({ children, className }) => {
|
||||
const PageTitle = ({ children, className }: Props) => {
|
||||
const styles = classNames(
|
||||
'ml-4 text-2xl font-default uppercase w-full text-accent-blue font-bold',
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import React, {FC, useState} from "react";
|
||||
import React, {useState} from "react";
|
||||
|
||||
interface Props {
|
||||
value: number;
|
||||
setValue: (value: number) => void;
|
||||
}
|
||||
|
||||
const RecurrenceInput: FC<Props> = ({ value, setValue}) => {
|
||||
const RecurrenceInput = ({ value, setValue}: Props) => {
|
||||
const [openInput, setOpenInput] = useState<'category' | 'number'>([7, 365].includes(value) ? 'category' : 'number')
|
||||
|
||||
const toggleInput = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import {FC} from "react";
|
||||
|
||||
interface ToggleProps {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
const Toggle: FC<ToggleProps> = ({ checked, onChange }) => {
|
||||
const Toggle = ({ checked, onChange }: ToggleProps) => {
|
||||
const handleChange = () => {
|
||||
onChange(checked);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,25 @@
|
|||
"use client"
|
||||
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
interface AuthContextProps {
|
||||
isAuthenticated: boolean | null;
|
||||
isAuthenticated: boolean;
|
||||
login: () => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextProps>({
|
||||
isAuthenticated: null,
|
||||
isAuthenticated: false,
|
||||
login: () => {},
|
||||
logout: () => {},
|
||||
});
|
||||
|
||||
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||
// Start with false during SSR, will be updated on client
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check token on client after mount
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
// You could add any token validation logic here
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
setIsAuthenticated(!!token);
|
||||
}, []);
|
||||
|
||||
const login = () => {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import "./app.css";
|
|||
import React from "react"
|
||||
import { AuthProvider } from "~/context/AuthContext"
|
||||
import AuthGuard from "~/components/layout/AuthGuard"
|
||||
import NavBar from "~/components/layout/NavBar"
|
||||
|
||||
export const links: Route.LinksFunction = () => [
|
||||
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||
|
|
@ -38,6 +39,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||
<body className="antialiased w-full min-h-screen bg-gray-600">
|
||||
<AuthProvider>
|
||||
<AuthGuard>
|
||||
<NavBar />
|
||||
<div className="m-5 lg:container lg:mx-auto">{ children }</div>
|
||||
</AuthGuard>
|
||||
</AuthProvider>
|
||||
|
|
|
|||
|
|
@ -4,4 +4,20 @@ export default [
|
|||
index("routes/home.tsx"),
|
||||
route("login", "./components/features/auth/LoginForm.tsx"),
|
||||
route("register", "./components/features/auth/RegisterForm.tsx"),
|
||||
|
||||
// Dishes routes
|
||||
route("dishes", "routes/dishes.tsx"),
|
||||
route("dishes/create", "routes/dishes.create.tsx"),
|
||||
route("dishes/:id/edit", "routes/dishes.$id.edit.tsx"),
|
||||
|
||||
// Users routes
|
||||
route("users", "routes/users.tsx"),
|
||||
route("users/create", "routes/users.create.tsx"),
|
||||
route("users/:id/edit", "routes/users.$id.edit.tsx"),
|
||||
|
||||
// Schedule routes
|
||||
route("schedule/:date/edit", "routes/schedule.$date.edit.tsx"),
|
||||
|
||||
// History route
|
||||
route("scheduled-user-dishes/history", "routes/scheduled-user-dishes.history.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
|
|
|
|||
66
frontend/app/routes/dishes.$id.edit.tsx
Normal file
66
frontend/app/routes/dishes.$id.edit.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import type { Route } from "./+types/dishes.$id.edit";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import PageTitle from "~/components/ui/PageTitle";
|
||||
import EditDishForm from "~/components/features/dishes/EditDishForm";
|
||||
import { DishType } from "~/types/DishType";
|
||||
import Spinner from "~/components/Spinner";
|
||||
import { fetchDish } from "~/utils/api/dishApi";
|
||||
import SyncUsersForm from "~/components/features/dishes/SyncUsersForm";
|
||||
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
||||
import useRoutes from "~/hooks/useRoutes";
|
||||
import OutlineLinkButton from "~/components/ui/Buttons/OutlineLinkButton";
|
||||
import Hr from "~/components/ui/Hr";
|
||||
import { useParams } from "react-router";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Dish Planner - Edit Dish" },
|
||||
{ name: "description", content: "Edit dish details" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function EditDishPage() {
|
||||
const params = useParams();
|
||||
const id = Number(params.id);
|
||||
const [dish, setDish] = useState<DishType | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const routes = useRoutes();
|
||||
|
||||
const loadDish = useCallback(async () => {
|
||||
try {
|
||||
const fetchedDish = await fetchDish(id);
|
||||
setDish(fetchedDish);
|
||||
} catch (error) {
|
||||
console.error("Error fetching dish:", error);
|
||||
throw new Error("No token found in localStorage.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
loadDish();
|
||||
}, [loadDish]);
|
||||
|
||||
if (isLoading || dish === null) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex mb-3">
|
||||
<PageTitle>Edit Dish</PageTitle>
|
||||
<OutlineLinkButton href={routes.dish.index()}>
|
||||
<ChevronLeftIcon className="w-5 h-5 mr-1" />
|
||||
<p className="text-sm">BACK</p>
|
||||
</OutlineLinkButton>
|
||||
</div>
|
||||
|
||||
<EditDishForm dish={dish} />
|
||||
|
||||
<Hr className="mt-8" />
|
||||
|
||||
<SyncUsersForm dish={dish} reloadDish={loadDish} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
frontend/app/routes/dishes.create.tsx
Normal file
13
frontend/app/routes/dishes.create.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { Route } from "./+types/dishes.create";
|
||||
import CreateDishForm from "~/components/features/dishes/CreateDishForm";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Dish Planner - Create Dish" },
|
||||
{ name: "description", content: "Create a new dish" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function CreateDishPage() {
|
||||
return <CreateDishForm />;
|
||||
}
|
||||
64
frontend/app/routes/dishes.tsx
Normal file
64
frontend/app/routes/dishes.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import type { Route } from "./+types/dishes";
|
||||
import PageTitle from "~/components/ui/PageTitle";
|
||||
import { DishType } from "~/types/DishType";
|
||||
import Dish from "~/components/features/dishes/Dish";
|
||||
import { PlusIcon } from "@heroicons/react/24/solid";
|
||||
import { useEffect, useState } from "react";
|
||||
import useRoutes from "~/hooks/useRoutes";
|
||||
import { listDishes } from "~/utils/api/dishApi";
|
||||
import Button from "~/components/ui/Button";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Dish Planner - Dishes" },
|
||||
{ name: "description", content: "Manage your dishes" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function DishesIndexPage() {
|
||||
const routes = useRoutes();
|
||||
|
||||
const [dishes, setDishes] = useState<DishType[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
listDishes()
|
||||
.then((dishes: DishType[]) => setDishes(dishes))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
if (loading) return <p>Loading...</p>;
|
||||
|
||||
if (!dishes) {
|
||||
return <h1>Loading...</h1>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex mb-3">
|
||||
<div className="flex-none mr-4 pt-1">
|
||||
<PageTitle>Dishes</PageTitle>
|
||||
</div>
|
||||
<div className="flex-grow flex justify-end">
|
||||
<Button
|
||||
href={routes.dish.create()}
|
||||
icon={<PlusIcon />}
|
||||
size="small"
|
||||
variant="primary"
|
||||
appearance="text"
|
||||
>
|
||||
Add Dish
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{dishes.length === 0 ? (
|
||||
<h1 className="text-secondary">No dishes found :(</h1>
|
||||
) : (
|
||||
dishes.map((dish: DishType, index: number) => (
|
||||
<Dish key={index} dish={dish} />
|
||||
))
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import type { Route } from "./+types/home";
|
||||
import { Welcome } from "../welcome/welcome";
|
||||
import UpcomingDishes from "~/components/features/schedule/UpcomingDishes";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "New React Router App" },
|
||||
{ name: "description", content: "Welcome to React Router!" },
|
||||
{ title: "Dish Planner - Schedule" },
|
||||
{ name: "description", content: "View and manage your upcoming dish schedule" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return <Welcome />;
|
||||
return <UpcomingDishes />;
|
||||
}
|
||||
|
|
|
|||
19
frontend/app/routes/schedule.$date.edit.tsx
Normal file
19
frontend/app/routes/schedule.$date.edit.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { Route } from "./+types/schedule.$date.edit";
|
||||
import ScheduleEditForm from "~/components/features/schedule/ScheduleEditForm";
|
||||
import { useParams } from "react-router";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Dish Planner - Edit Schedule" },
|
||||
{ name: "description", content: "Edit schedule for a specific date" },
|
||||
];
|
||||
}
|
||||
|
||||
const ScheduleEditPage = () => {
|
||||
const params = useParams();
|
||||
const date = params.date as string;
|
||||
|
||||
return <ScheduleEditForm date={date} />;
|
||||
};
|
||||
|
||||
export default ScheduleEditPage;
|
||||
13
frontend/app/routes/scheduled-user-dishes.history.tsx
Normal file
13
frontend/app/routes/scheduled-user-dishes.history.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { Route } from "./+types/scheduled-user-dishes.history";
|
||||
import HistoricalDishes from "~/components/features/schedule/HistoricalDishes";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Dish Planner - History" },
|
||||
{ name: "description", content: "View historical dishes" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function HistoryPage() {
|
||||
return <HistoricalDishes />;
|
||||
}
|
||||
32
frontend/app/routes/users.$id.edit.tsx
Normal file
32
frontend/app/routes/users.$id.edit.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import type { Route } from "./+types/users.$id.edit";
|
||||
import { useEffect, useState } from "react";
|
||||
import { UserType } from "~/types/UserType";
|
||||
import { showUser } from "~/utils/api/usersApi";
|
||||
import Spinner from "~/components/Spinner";
|
||||
import EditUserForm from "~/components/features/users/EditUserForm";
|
||||
import { useParams } from "react-router";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Dish Planner - Edit User" },
|
||||
{ name: "description", content: "Edit user details" },
|
||||
];
|
||||
}
|
||||
|
||||
const UpdateUsersPage = () => {
|
||||
const params = useParams();
|
||||
const id = Number(params.id);
|
||||
const [user, setUser] = useState<UserType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
showUser(id).then((user: UserType) => setUser(user));
|
||||
}, [id]);
|
||||
|
||||
if (!user) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return <EditUserForm user={user} />;
|
||||
};
|
||||
|
||||
export default UpdateUsersPage;
|
||||
74
frontend/app/routes/users.create.tsx
Normal file
74
frontend/app/routes/users.create.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import type { Route } from "./+types/users.create";
|
||||
import PageTitle from "~/components/ui/PageTitle";
|
||||
import useRoutes from "~/hooks/useRoutes";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useState } from "react";
|
||||
import Alert from "~/components/ui/Alert";
|
||||
import { createUser } from "~/utils/api/usersApi";
|
||||
import { Link } from "react-router";
|
||||
import SolidButton from "~/components/ui/Buttons/SolidButton";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Dish Planner - Create User" },
|
||||
{ name: "description", content: "Create a new user" },
|
||||
];
|
||||
}
|
||||
|
||||
const CreateUsersPage = () => {
|
||||
const [name, setName] = useState<string>("");
|
||||
const [error, setError] = useState<string>("");
|
||||
const navigate = useNavigate();
|
||||
const routes = useRoutes();
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!name.trim()) {
|
||||
setError("Name cannot be empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
createUser(name).then(() => {
|
||||
navigate(routes.user.index());
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col items-center">
|
||||
<PageTitle>Create User</PageTitle>
|
||||
<Link to={routes.user.index()} className="py-2 mr-auto">
|
||||
Back to users
|
||||
</Link>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="w-full max-w-sm mt-4 border-secondary border-2 rounded p-4"
|
||||
>
|
||||
{error != "" && (
|
||||
<Alert type="error" className="mt-4">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<label htmlFor="name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder=""
|
||||
name="name"
|
||||
id="name"
|
||||
autoFocus={true}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full p-2 border rounded bg-primary border-secondary bg-gray-600 text-secondary"
|
||||
/>
|
||||
|
||||
<SolidButton type="submit" className="mt-4">
|
||||
Create
|
||||
</SolidButton>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUsersPage;
|
||||
77
frontend/app/routes/users.tsx
Normal file
77
frontend/app/routes/users.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import type { Route } from "./+types/users";
|
||||
import PageTitle from "~/components/ui/PageTitle";
|
||||
import { useFetchUsers } from "~/hooks/useFetchUsers";
|
||||
import Spinner from "~/components/Spinner";
|
||||
import useRoutes from "~/hooks/useRoutes";
|
||||
import { Link } from "react-router";
|
||||
import { PencilIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/solid";
|
||||
import React from "react";
|
||||
import { deleteUser } from "~/utils/api/usersApi";
|
||||
import { UserType } from "~/types/UserType";
|
||||
import Card from "~/components/layout/Card";
|
||||
import OutlineLinkButton from "~/components/ui/Buttons/OutlineLinkButton";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Dish Planner - Users" },
|
||||
{ name: "description", content: "Manage your users" },
|
||||
];
|
||||
}
|
||||
|
||||
const UsersPage = () => {
|
||||
const { users, isLoading } = useFetchUsers();
|
||||
const routes = useRoutes();
|
||||
|
||||
const handleDelete = (user: UserType) => {
|
||||
deleteUser(user).then(() => window.location.reload());
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const usersList = () => {
|
||||
return users.map((user) => (
|
||||
<Card key={user.id}>
|
||||
<div className="flex-grow text-xl font-bold pt-1">{user.name}</div>
|
||||
<div className="flex-none flex gap-2">
|
||||
<div className="flex-none w-8">
|
||||
<Link to={routes.user.edit(user)}>
|
||||
<div className="border border-foreground p-2 rounded">
|
||||
<PencilIcon width="14" />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-none w-8">
|
||||
<Link to="#" onClick={() => handleDelete(user)}>
|
||||
<div className="border border-red-500 p-2 rounded">
|
||||
<TrashIcon width="14" className="text-red-500" />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex w-full">
|
||||
<div className="w-1/2 flex-none pt-2">
|
||||
<PageTitle>Users</PageTitle>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow flex justify-end">
|
||||
<OutlineLinkButton href={routes.user.create()} variant="primary">
|
||||
<PlusIcon className="w-4 h-4 mt-1 mr-1" />
|
||||
<p>Add User</p>
|
||||
</OutlineLinkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{users && users.length > 0 ? usersList() : <div>No users</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersPage;
|
||||
4
frontend/app/types/RecurrenceType.ts
Normal file
4
frontend/app/types/RecurrenceType.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export type RecurrenceType = {
|
||||
type: "App\\Models\\WeeklyRecurrence" | "App\\Models\\MinimumRecurrence";
|
||||
value: number;
|
||||
}
|
||||
|
|
@ -1,11 +1,6 @@
|
|||
import type { ScheduledUserDishType, UserDishType } from "@/types/ScheduledUserDishType";
|
||||
import type { UserType } from "@/types/UserType";
|
||||
|
||||
export type RecurrenceType = {
|
||||
type: "App\\Models\\WeeklyRecurrence" | "App\\Models\\MinimumRecurrence";
|
||||
value: number;
|
||||
}
|
||||
|
||||
export type ScheduleType = {
|
||||
id: number;
|
||||
date: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { UserType } from "@/types/UserType";
|
||||
import type { DishType } from "@/types/DishType";
|
||||
import type { RecurrenceType } from "@/types/ScheduleType";
|
||||
import type { RecurrenceType } from "@/types/RecurrenceType";
|
||||
|
||||
export type UserDishType = {
|
||||
id: number;
|
||||
|
|
@ -9,12 +9,6 @@ export type UserDishType = {
|
|||
recurrences: RecurrenceType[];
|
||||
}
|
||||
|
||||
export type UserDishWithoutUserType = {
|
||||
id: number;
|
||||
dish: DishType;
|
||||
recurrences: RecurrenceType[];
|
||||
}
|
||||
|
||||
export type ScheduledUserDishType = {
|
||||
id: number;
|
||||
user_dish: UserDishType;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import {UserType} from "@/types/UserType";
|
||||
import {RecurrenceType} from "@/types/ScheduleType";
|
||||
import {RecurrenceType} from "@/types/RecurrenceType";
|
||||
|
||||
export type DishType = {
|
||||
user: UserType;
|
||||
|
|
|
|||
8
frontend/app/types/UserDishWithoutUserType.ts
Normal file
8
frontend/app/types/UserDishWithoutUserType.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import type { DishType } from "@/types/DishType";
|
||||
import type { RecurrenceType } from "@/types/RecurrenceType";
|
||||
|
||||
export type UserDishWithoutUserType = {
|
||||
id: number;
|
||||
dish: DishType;
|
||||
recurrences: RecurrenceType[];
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { UserDishWithoutUserType } from "@/types/ScheduledUserDishType";
|
||||
import type { UserDishWithoutUserType } from "@/types/UserDishWithoutUserType";
|
||||
|
||||
export type UserType = {
|
||||
id: number;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {RecurrenceType} from "@/types/ScheduleType";
|
||||
import {RecurrenceType} from "@/types/RecurrenceType";
|
||||
import {apiRequest} from "@/utils/api/apiRequest";
|
||||
import {UserType} from "@/types/UserType";
|
||||
|
||||
|
|
|
|||
254
frontend/package-lock.json
generated
254
frontend/package-lock.json
generated
|
|
@ -6,11 +6,14 @@
|
|||
"": {
|
||||
"name": "my-react-router-app",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.9",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@react-router/node": "^7.5.3",
|
||||
"@react-router/serve": "^7.5.3",
|
||||
"@types/luxon": "^3.7.1",
|
||||
"classnames": "^2.5.1",
|
||||
"isbot": "^5.1.27",
|
||||
"luxon": "^3.7.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router": "^7.5.3"
|
||||
|
|
@ -935,6 +938,79 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.3",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.26.28",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
|
||||
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.2",
|
||||
"@floating-ui/utils": "^0.2.8",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
|
||||
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@headlessui/react": {
|
||||
"version": "2.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz",
|
||||
"integrity": "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.16",
|
||||
"@react-aria/focus": "^3.20.2",
|
||||
"@react-aria/interactions": "^3.25.0",
|
||||
"@tanstack/react-virtual": "^3.13.9",
|
||||
"use-sync-external-store": "^1.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@heroicons/react": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
|
||||
|
|
@ -1107,6 +1183,73 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/focus": {
|
||||
"version": "3.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz",
|
||||
"integrity": "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/interactions": "^3.25.6",
|
||||
"@react-aria/utils": "^3.31.0",
|
||||
"@react-types/shared": "^3.32.1",
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"clsx": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/interactions": {
|
||||
"version": "3.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.6.tgz",
|
||||
"integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/ssr": "^3.9.10",
|
||||
"@react-aria/utils": "^3.31.0",
|
||||
"@react-stately/flags": "^3.1.2",
|
||||
"@react-types/shared": "^3.32.1",
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/ssr": {
|
||||
"version": "3.9.10",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
|
||||
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/utils": {
|
||||
"version": "3.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.31.0.tgz",
|
||||
"integrity": "sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/ssr": "^3.9.10",
|
||||
"@react-stately/flags": "^3.1.2",
|
||||
"@react-stately/utils": "^3.10.8",
|
||||
"@react-types/shared": "^3.32.1",
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"clsx": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-router/dev": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.6.0.tgz",
|
||||
|
|
@ -1237,6 +1380,36 @@
|
|||
"react-router": "7.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-stately/flags": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
|
||||
"integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-stately/utils": {
|
||||
"version": "3.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz",
|
||||
"integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-types/shared": {
|
||||
"version": "3.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz",
|
||||
"integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
|
||||
|
|
@ -1517,6 +1690,15 @@
|
|||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.6.tgz",
|
||||
|
|
@ -1794,6 +1976,33 @@
|
|||
"vite": "^5.2.0 || ^6"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
|
||||
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.13.12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
||||
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
|
|
@ -1801,6 +2010,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
|
||||
"integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.17.46",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.46.tgz",
|
||||
|
|
@ -2119,6 +2334,15 @@
|
|||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
|
@ -3283,6 +3507,15 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
|
||||
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
|
|
@ -4363,6 +4596,12 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz",
|
||||
|
|
@ -4455,6 +4694,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
|
|
@ -4548,6 +4793,15 @@
|
|||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -9,11 +9,14 @@
|
|||
"typecheck": "react-router typegen && tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.9",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@react-router/node": "^7.5.3",
|
||||
"@react-router/serve": "^7.5.3",
|
||||
"@types/luxon": "^3.7.1",
|
||||
"classnames": "^2.5.1",
|
||||
"isbot": "^5.1.27",
|
||||
"luxon": "^3.7.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router": "^7.5.3"
|
||||
|
|
|
|||
Loading…
Reference in a new issue