Add front-end fixes

This commit is contained in:
myrmidex 2025-11-09 13:09:22 +01:00
parent 30f4bc6fa6
commit afa4cf27b7
57 changed files with 805 additions and 210 deletions

View file

@ -1,5 +1,4 @@
import { FC } from "react" import { Link } from "react-router"
import Link from "next/link"
import useRoutes from "@/hooks/useRoutes" import useRoutes from "@/hooks/useRoutes"
import { UserType } from "@/types/UserType" import { UserType } from "@/types/UserType"
import { DishType } from "@/types/DishType" import { DishType } from "@/types/DishType"
@ -9,7 +8,7 @@ interface Props {
users: UserType[] users: UserType[]
} }
const OnboardingBanner: FC<Props> = ({ dishes, users }) => { const OnboardingBanner = ({ dishes, users }: Props) => {
const routes = useRoutes(); const routes = useRoutes();
const steps = [ const steps = [
@ -36,7 +35,7 @@ const OnboardingBanner: FC<Props> = ({ dishes, users }) => {
<div className="w-full text-center text-2xl my-5" key={index}> <div className="w-full text-center text-2xl my-5" key={index}>
{ {
step.count === 0 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 className="line-through">{ step.label }</div>
} }
</div> </div>

View file

@ -1,4 +1,4 @@
import React, { FC, useState } from "react"; import React, { useState } from "react";
import { DishType } from "@/types/DishType"; import { DishType } from "@/types/DishType";
import { UserType } from "@/types/UserType"; import { UserType } from "@/types/UserType";
import { useFetchUsers } from "@/hooks/useFetchUsers"; import { useFetchUsers } from "@/hooks/useFetchUsers";
@ -12,7 +12,7 @@ interface Props {
reloadDish: () => void; reloadDish: () => void;
} }
const AddUserToDishForm: FC<Props> = ({ dish, reloadDish }) => { const AddUserToDishForm = ({ dish, reloadDish }: Props) => {
const [showAdd, setShowAdd] = useState<boolean>(false); const [showAdd, setShowAdd] = useState<boolean>(false);
const [selectedUser, setSelectedUser] = useState<string>("-1"); const [selectedUser, setSelectedUser] = useState<string>("-1");
const { users, isLoading: isUsersLoading } = useFetchUsers(); const { users, isLoading: isUsersLoading } = useFetchUsers();

View file

@ -1,15 +1,15 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useRouter } from "next/navigation"; import { useNavigate } from "react-router";
import { createDish } from "@/utils/api/dishApi"; import { createDish } from "~/utils/api/dishApi";
import PageTitle from "@/components/ui/PageTitle"; import PageTitle from "~/components/ui/PageTitle";
import Alert from "@/components/ui/Alert"; import Alert from "~/components/ui/Alert";
import SolidButton from "@/components/ui/Buttons/SolidButton"; import SolidButton from "~/components/ui/Buttons/SolidButton";
import OutlineLinkButton from "@/components/ui/Buttons/OutlineLinkButton"; import OutlineLinkButton from "~/components/ui/Buttons/OutlineLinkButton";
import { ChevronLeftIcon } from "@heroicons/react/16/solid"; import { ChevronLeftIcon } from "@heroicons/react/16/solid";
import Hr from "@/components/ui/Hr" import Hr from "~/components/ui/Hr"
const CreateDishForm = () => { const CreateDishForm = () => {
const router = useRouter() const navigate = useNavigate()
const [name, setName] = useState<string>(""); const [name, setName] = useState<string>("");
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -35,7 +35,7 @@ const CreateDishForm = () => {
try { try {
const result = await createDish(name); const result = await createDish(name);
if (result) { if (result) {
router.push('/dishes') navigate('/dishes')
} }
} catch (error: unknown) { } catch (error: unknown) {
setError(error instanceof Error ? error.message : "An unexpected error occurred."); setError(error instanceof Error ? error.message : "An unexpected error occurred.");

View file

@ -1,10 +1,10 @@
import {DishType} from "@/types/DishType"; import {DishType} from "~/types/DishType";
import {PencilIcon, TrashIcon} from '@heroicons/react/24/solid' import {PencilIcon, TrashIcon} from '@heroicons/react/24/solid'
import Link from "next/link"; import { Link } from "react-router";
import useRoutes from "@/hooks/useRoutes"; import useRoutes from "~/hooks/useRoutes";
import {UserType} from "@/types/UserType"; import {UserType} from "~/types/UserType";
import Card from "@/components/layout/Card"; import Card from "~/components/layout/Card";
const Dish = ({ dish }: { dish: DishType}) => { const Dish = ({ dish }: { dish: DishType}) => {
const routes = useRoutes(); const routes = useRoutes();
@ -21,12 +21,12 @@ const Dish = ({ dish }: { dish: DishType}) => {
} }
</div> </div>
<div className="flex-none flex gap-2"> <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"> <div className="border border-foreground p-2 rounded">
<PencilIcon width="14" /> <PencilIcon width="14" />
</div> </div>
</Link> </Link>
<Link href={routes.dish.delete(dish)}> <Link to={routes.dish.delete(dish)}>
<div className="border border-red-500 p-2 rounded"> <div className="border border-red-500 p-2 rounded">
<TrashIcon width="14" className="text-red-500" /> <TrashIcon width="14" className="text-red-500" />
</div> </div>

View file

@ -1,5 +1,4 @@
import {UserType} from "@/types/UserType"; import {UserType} from "@/types/UserType";
import {FC} from "react";
import {DishType} from "@/types/DishType"; import {DishType} from "@/types/DishType";
interface Props { interface Props {
@ -7,7 +6,7 @@ interface Props {
dish: DishType, dish: DishType,
} }
const DishCard: FC<Props> = ({ user, dish }: Props) => { const DishCard = ({ user, dish }: Props) => {
return ( return (
<div className="w-full flex py-2 text-xl font-bold text-primary"> <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"> <div className="user-badge flex-none w-16 text-center pr-5">

View file

@ -1,20 +1,20 @@
import React, {FC, useState} from "react"; import React, {useState} from "react";
import {useRouter} from "next/navigation"; import { useNavigate } from "react-router";
import Alert from "@/components/ui/Alert"; import Alert from "~/components/ui/Alert";
import {updateDish} from "@/utils/api/dishApi"; import {updateDish} from "~/utils/api/dishApi";
import {DishType} from "@/types/DishType"; import {DishType} from "~/types/DishType";
import useRoutes from "@/hooks/useRoutes"; import useRoutes from "~/hooks/useRoutes";
import Spinner from "@/components/Spinner"; import Spinner from "~/components/Spinner";
import Button from "@/components/ui/Button" import Button from "~/components/ui/Button"
interface Props { interface Props {
dish: DishType dish: DishType
} }
const EditDishForm: FC<Props> = ({ dish }) => { const EditDishForm = ({ dish }: Props) => {
const [name, setName] = useState<string>(dish.name); const [name, setName] = useState<string>(dish.name);
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const router = useRouter() const navigate = useNavigate()
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const routes = useRoutes(); const routes = useRoutes();
@ -38,7 +38,7 @@ const EditDishForm: FC<Props> = ({ dish }) => {
try { try {
const result = await updateDish(dish.id, name); const result = await updateDish(dish.id, name);
if (result) { if (result) {
router.push(routes.dish.index()) navigate(routes.dish.index())
} }
} catch (error: unknown) { } catch (error: unknown) {
setError(error instanceof Error ? error.message : "An unexpected error occurred"); setError(error instanceof Error ? error.message : "An unexpected error occurred");

View file

@ -1,9 +1,9 @@
import React, {FC} from "react"; import React from "react";
import SectionTitle from "@/components/ui/SectionTitle"; import SectionTitle from "@/components/ui/SectionTitle";
import {syncUserDishRecurrences} from "@/utils/api/usersApi"; import {syncUserDishRecurrences} from "@/utils/api/usersApi";
import Spinner from "@/components/Spinner"; import Spinner from "@/components/Spinner";
import {UserDishType} from "@/types/ScheduledUserDishType"; import {UserDishType} from "@/types/ScheduledUserDishType";
import {RecurrenceType} from "@/types/ScheduleType"; import {RecurrenceType} from "@/types/RecurrenceType";
import SolidButton from "@/components/ui/Buttons/SolidButton"; import SolidButton from "@/components/ui/Buttons/SolidButton";
interface Props { interface Props {
@ -11,7 +11,7 @@ interface Props {
onSubmit: () => void 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 weeklyRecurrence = userDish.recurrences.find((recurrence) => recurrence.type === 'App\\Models\\WeeklyRecurrence')
const minimumRecurrence = userDish.recurrences.find((recurrence) => recurrence.type === 'App\\Models\\MinimumRecurrence') const minimumRecurrence = userDish.recurrences.find((recurrence) => recurrence.type === 'App\\Models\\MinimumRecurrence')

View file

@ -1,11 +1,10 @@
import {FC} from "react"; import {RecurrenceType} from "@/types/RecurrenceType";
import {RecurrenceType} from "@/types/ScheduleType";
interface Props { interface Props {
recurrences: RecurrenceType[]; recurrences: RecurrenceType[];
} }
const RecurrenceLabels: FC<Props> = ({recurrences}) => { const RecurrenceLabels = ({recurrences}: Props) => {
const weeklyRecurrences = recurrences.filter(recurrence => recurrence.type === 'App\\Models\\WeeklyRecurrence'); const weeklyRecurrences = recurrences.filter(recurrence => recurrence.type === 'App\\Models\\WeeklyRecurrence');
const minimumRecurrences = recurrences.filter(recurrence => recurrence.type === 'App\\Models\\MinimumRecurrence'); const minimumRecurrences = recurrences.filter(recurrence => recurrence.type === 'App\\Models\\MinimumRecurrence');

View file

@ -1,4 +1,4 @@
import React, { FC } from "react"; import React from "react";
import { DishType } from "@/types/DishType"; import { DishType } from "@/types/DishType";
import { UserType } from "@/types/UserType"; import { UserType } from "@/types/UserType";
import UserDishCard from "@/components/features/dishes/UserDishCard"; import UserDishCard from "@/components/features/dishes/UserDishCard";
@ -10,7 +10,7 @@ interface Props {
reloadDish: () => void; reloadDish: () => void;
} }
const SyncUsersForm: FC<Props> = ({ dish, reloadDish }) => { const SyncUsersForm = ({ dish, reloadDish }: Props) => {
return ( return (
<div className="my-2 w-full flex flex-col"> <div className="my-2 w-full flex flex-col">
<SectionTitle className="mt-2">Users</SectionTitle> <SectionTitle className="mt-2">Users</SectionTitle>

View file

@ -1,14 +1,14 @@
import React, {FC, useEffect} from "react"; import React, {useEffect} from "react";
import {DishType} from "@/types/DishType"; import {DishType} from "~/types/DishType";
import {UserType} from "@/types/UserType"; import {UserType} from "~/types/UserType";
import Link from "next/link"; import { Link } from "react-router";
import {PencilIcon, TrashIcon} from "@heroicons/react/24/solid"; import {PencilIcon, TrashIcon} from "@heroicons/react/24/solid";
import {removeUserFromDish} from "@/utils/api/dishApi"; import {removeUserFromDish} from "~/utils/api/dishApi";
import EditDishUserCardEditForm from "@/components/features/dishes/EditDishUserCardEditForm"; import EditDishUserCardEditForm from "~/components/features/dishes/EditDishUserCardEditForm";
import {getUserDishForUserAndDish} from "@/utils/api/usersApi"; import {getUserDishForUserAndDish} from "~/utils/api/usersApi";
import Spinner from "@/components/Spinner"; import Spinner from "~/components/Spinner";
import RecurrenceLabels from "@/components/features/dishes/RecurrenceLabels"; import RecurrenceLabels from "~/components/features/dishes/RecurrenceLabels";
import {UserDishType} from "@/types/ScheduledUserDishType"; import {UserDishType} from "~/types/ScheduledUserDishType";
interface Props { interface Props {
dish: DishType dish: DishType
@ -16,7 +16,7 @@ interface Props {
reloadDish: () => void reloadDish: () => void
} }
const UserDishCard: FC<Props> = ({dish, user, reloadDish}) => { const UserDishCard = ({dish, user, reloadDish}: Props) => {
const [userDish, setUserDish] = React.useState<UserDishType|null>(null); const [userDish, setUserDish] = React.useState<UserDishType|null>(null);
const [userDishLoading, setUserDishLoading] = React.useState(true); const [userDishLoading, setUserDishLoading] = React.useState(true);
const [isEditMode, setIsEditMode] = React.useState(false); const [isEditMode, setIsEditMode] = React.useState(false);
@ -56,14 +56,14 @@ const UserDishCard: FC<Props> = ({dish, user, reloadDish}) => {
</div> </div>
<div className="flex-none w-8"> <div className="flex-none w-8">
<Link onClick={() => setIsEditMode(!isEditMode)} href="#"> <Link onClick={() => setIsEditMode(!isEditMode)} to="#">
<div className="border border-foreground p-2 rounded"> <div className="border border-foreground p-2 rounded">
<PencilIcon width="14"/> <PencilIcon width="14"/>
</div> </div>
</Link> </Link>
</div> </div>
<div className="flex-none w-8"> <div className="flex-none w-8">
<Link onClick={handleRemove} href="#"> <Link onClick={handleRemove} to="#">
<div className="border border-red-500 p-2 rounded"> <div className="border border-red-500 p-2 rounded">
<TrashIcon width="14" className="text-red-500"/> <TrashIcon width="14" className="text-red-500"/>
</div> </div>

View file

@ -1,6 +1,6 @@
import Link from "next/link"; import { Link } from "react-router";
import React, {FC} from "react"; import React from "react";
import useRoutes from "@/hooks/useRoutes"; import useRoutes from "~/hooks/useRoutes";
import classNames from "classnames"; import classNames from "classnames";
interface Props { interface Props {
@ -21,7 +21,7 @@ const linkStyles = classNames(
'space-grotesk', 'text-xl' 'space-grotesk', 'text-xl'
) )
const MobileDropdownMenu: FC<Props> = ({ isOpen, setIsOpen, handleLogout }) => { const MobileDropdownMenu = ({ isOpen, setIsOpen, handleLogout }: Props) => {
const routes = useRoutes(); const routes = useRoutes();
if (!isOpen) return null; if (!isOpen) return null;
@ -29,35 +29,35 @@ const MobileDropdownMenu: FC<Props> = ({ isOpen, setIsOpen, handleLogout }) => {
return ( return (
<div className={divStyles}> <div className={divStyles}>
<Link <Link
href={routes.home()} to={routes.home()}
className={linkStyles} className={linkStyles}
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
> >
Home Home
</Link> </Link>
<Link <Link
href={routes.dish.index()} to={routes.dish.index()}
className={linkStyles} className={linkStyles}
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
> >
Dishes Dishes
</Link> </Link>
<Link <Link
href={routes.user.index()} to={routes.user.index()}
className={linkStyles} className={linkStyles}
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
> >
Users Users
</Link> </Link>
<Link <Link
href={routes.schedule.history()} to={routes.schedule.history()}
className={linkStyles} className={linkStyles}
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
> >
History History
</Link> </Link>
<Link <Link
href={routes.auth.login()} to={routes.auth.login()}
onClick={handleLogout} onClick={handleLogout}
className={linkStyles}> className={linkStyles}>
Logout Logout

View file

@ -1,6 +1,3 @@
"use client"
import {FC} from "react";
import ScheduleDayCard from "@/components/features/schedule/dayCard/ScheduleDayCard"; import ScheduleDayCard from "@/components/features/schedule/dayCard/ScheduleDayCard";
import { FilledScheduleType, ScheduleType } from "@/types/ScheduleType"; import { FilledScheduleType, ScheduleType } from "@/types/ScheduleType";
import {useFetchUsers} from "@/hooks/useFetchUsers"; import {useFetchUsers} from "@/hooks/useFetchUsers";
@ -52,7 +49,7 @@ interface Props {
schedule: ScheduleType[]; schedule: ScheduleType[];
} }
const ScheduleCalendar: FC<Props> = ({ schedule }: Props) => { const ScheduleCalendar = ({ schedule }: Props) => {
const {users, isLoading: areUsersLoading} = useFetchUsers(); const {users, isLoading: areUsersLoading} = useFetchUsers();
if (areUsersLoading) return <Spinner /> if (areUsersLoading) return <Spinner />

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import React, { FC, useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { ScheduleType } from "@/types/ScheduleType"; import { ScheduleType } from "@/types/ScheduleType";
import Spinner from "@/components/Spinner"; import Spinner from "@/components/Spinner";
import PageTitle from "@/components/ui/PageTitle"; import PageTitle from "@/components/ui/PageTitle";
@ -20,7 +20,7 @@ interface Props {
date: string; date: string;
} }
const ScheduleEditForm: FC<Props> = ({ date }) => { const ScheduleEditForm = ({ date }: Props) => {
const [schedule, setSchedule] = useState<ScheduleType>() const [schedule, setSchedule] = useState<ScheduleType>()
const [userDishes, setUserDishes] = useState<UserDishType[]>([]) const [userDishes, setUserDishes] = useState<UserDishType[]>([])
const [isScheduleLoading, setIsScheduleLoading] = useState(true); const [isScheduleLoading, setIsScheduleLoading] = useState(true);

View file

@ -1,4 +1,4 @@
import { type FC, useState} from "react"; import { useState} from "react";
import Modal from "@/components/ui/Modal"; import Modal from "@/components/ui/Modal";
import ScheduleRegenerateForm from "@/components/features/schedule/ScheduleRegenerateForm"; import ScheduleRegenerateForm from "@/components/features/schedule/ScheduleRegenerateForm";
import {ArrowPathIcon} from "@heroicons/react/16/solid"; import {ArrowPathIcon} from "@heroicons/react/16/solid";
@ -7,7 +7,7 @@ interface ScheduleRegenerateButtonProps {
onModalClose?: () => void; onModalClose?: () => void;
} }
const ScheduleRegenerateButton: FC<ScheduleRegenerateButtonProps> = ({ onModalClose }) => { const ScheduleRegenerateButton = ({ onModalClose }: ScheduleRegenerateButtonProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleCloseModal = () => { const handleCloseModal = () => {

View file

@ -1,6 +1,6 @@
import {DialogTitle} from "@headlessui/react"; import {DialogTitle} from "@headlessui/react";
import Toggle from "@/components/ui/Toggle"; import Toggle from "@/components/ui/Toggle";
import {FC, useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {generateSchedule} from "@/utils/api/scheduleApi"; import {generateSchedule} from "@/utils/api/scheduleApi";
import Alert from "@/components/ui/Alert"; import Alert from "@/components/ui/Alert";
import SolidButton from "@/components/ui/Buttons/SolidButton"; import SolidButton from "@/components/ui/Buttons/SolidButton";
@ -9,7 +9,7 @@ interface ScheduleRegenerateFormProps {
closeModal: () => void; closeModal: () => void;
} }
const ScheduleRegenerateForm: FC<ScheduleRegenerateFormProps> = ({closeModal}) => { const ScheduleRegenerateForm = ({closeModal}: ScheduleRegenerateFormProps) => {
const [overwrite, setOverwrite] = useState(false); const [overwrite, setOverwrite] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");

View file

@ -1,5 +1,3 @@
"use client"
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import ScheduleCalendar from "@/components/features/schedule/ScheduleCalendar"; import ScheduleCalendar from "@/components/features/schedule/ScheduleCalendar";

View file

@ -1,4 +1,4 @@
import {FC, FormEvent, useMemo, useState} from "react"; import {FormEvent, useMemo, useState} from "react";
import {DishType} from "@/types/DishType"; import {DishType} from "@/types/DishType";
import {ScheduledUserDishType} from "@/types/ScheduledUserDishType"; import {ScheduledUserDishType} from "@/types/ScheduledUserDishType";
import {updateScheduledUserDish} from "@/utils/api/scheduledUserDishesApi"; import {updateScheduledUserDish} from "@/utils/api/scheduledUserDishesApi";
@ -10,7 +10,7 @@ interface Props {
allDishes: DishType[] 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 [selectedUserDishId, setSelectedUserDishId] = useState<number>(scheduledUserDish.user_dish ? scheduledUserDish.user_dish.id : 0)
const [errorMessage, setErrorMessage] = useState<string>("") const [errorMessage, setErrorMessage] = useState<string>("")
const [isSuccess, setIsSuccess] = useState(false); const [isSuccess, setIsSuccess] = useState(false);

View file

@ -1,5 +1,5 @@
import {DateTime} from "luxon"; import {DateTime} from "luxon";
import React, {FC} from "react"; import React from "react";
import classNames from "classnames"; import classNames from "classnames";
interface Props { interface Props {
@ -7,7 +7,7 @@ interface Props {
className?: string; 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 isToday = DateTime.fromISO(date).toFormat("yyyy-LL-dd") == DateTime.now().toFormat("yyyy-LL-dd")
const textStyle = classNames("inline font-bold", { const textStyle = classNames("inline font-bold", {

View file

@ -1,11 +1,11 @@
import React, {FC} from "react"; import React from "react";
import {UserType} from "@/types/UserType"; import {UserType} from "~/types/UserType";
import ScheduleDayCardUserDish from "@/components/features/schedule/dayCard/ScheduleDayCardUserDish"; import ScheduleDayCardUserDish from "~/components/features/schedule/dayCard/ScheduleDayCardUserDish";
import { FilledScheduleType, ScheduleType } from "@/types/ScheduleType"; import { FilledScheduleType, ScheduleType } from "~/types/ScheduleType";
import Link from "next/link"; import { Link } from "react-router";
import {PencilSquareIcon} from "@heroicons/react/24/outline"; import {PencilSquareIcon} from "@heroicons/react/24/outline";
import useRoutes from "@/hooks/useRoutes"; import useRoutes from "~/hooks/useRoutes";
import DateBadge from "@/components/features/schedule/dayCard/DateBadge"; import DateBadge from "~/components/features/schedule/dayCard/DateBadge";
import { DateTime } from "luxon" import { DateTime } from "luxon"
import classNames from "classnames" import classNames from "classnames"
@ -14,7 +14,7 @@ interface Props {
users: UserType[]; users: UserType[];
} }
const ScheduleDayCard: FC<Props> = ({schedule, users}) => { const ScheduleDayCard = ({schedule, users}: Props) => {
const routes = useRoutes() const routes = useRoutes()
const isToday = DateTime.fromISO(schedule.date).toFormat("yyyy-LL-dd") == DateTime.now().toFormat("yyyy-LL-dd") 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 <Link
className="w-full flex text-sm pr-3 pt-2" className="w-full flex text-sm pr-3 pt-2"
aria-label="Edit dish" 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 <PencilSquareIcon className="h-5 w-5 mr-2 ml-auto" /> Edit
</Link> </Link>

View file

@ -1,4 +1,4 @@
import React, { FC } from "react"; import React from "react";
import { ScheduledUserDishType } from "@/types/ScheduledUserDishType"; import { ScheduledUserDishType } from "@/types/ScheduledUserDishType";
import { UserType } from "@/types/UserType"; import { UserType } from "@/types/UserType";
import { FilledScheduleType, ScheduleType } from "@/types/ScheduleType"; import { FilledScheduleType, ScheduleType } from "@/types/ScheduleType";
@ -8,7 +8,7 @@ interface Props {
user: UserType; user: UserType;
} }
const ScheduleDayCardUserDish: FC<Props> = ({ schedule, user }) => { const ScheduleDayCardUserDish = ({ schedule, user }: Props) => {
const getDish = (user: UserType) => { const getDish = (user: UserType) => {
const scheduled_dishes = schedule.scheduled_user_dishes.filter((scheduled_user_dish: ScheduledUserDishType) => ( const scheduled_dishes = schedule.scheduled_user_dishes.filter((scheduled_user_dish: ScheduledUserDishType) => (
scheduled_user_dish.user_dish?.user.id == user.id scheduled_user_dish.user_dish?.user.id == user.id

View file

@ -1,22 +1,22 @@
import React, {FC, useState} from "react"; import React, {useState} from "react";
import {useRouter} from "next/navigation"; import { useNavigate } from "react-router";
import useRoutes from "@/hooks/useRoutes"; import useRoutes from "~/hooks/useRoutes";
import {updateUser} from "@/utils/api/usersApi"; import {updateUser} from "~/utils/api/usersApi";
import PageTitle from "@/components/ui/PageTitle"; import PageTitle from "~/components/ui/PageTitle";
import Link from "next/link"; import { Link } from "react-router";
import Alert from "@/components/ui/Alert"; import Alert from "~/components/ui/Alert";
import {UserType} from "@/types/UserType"; import {UserType} from "~/types/UserType";
import SolidButton from "@/components/ui/Buttons/SolidButton"; import SolidButton from "~/components/ui/Buttons/SolidButton";
interface Props { interface Props {
user: UserType; user: UserType;
} }
const EditUserForm: FC<Props> = ({ user }) => { const EditUserForm = ({ user }: Props) => {
const [name, setName] = useState<string>(user.name); const [name, setName] = useState<string>(user.name);
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const router = useRouter(); const navigate = useNavigate();
const routes = useRoutes(); const routes = useRoutes();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
@ -30,14 +30,14 @@ const EditUserForm: FC<Props> = ({ user }) => {
updateUser(user, name) updateUser(user, name)
.then(() => { .then(() => {
router.push(routes.user.index()) navigate(routes.user.index())
}) })
} }
return ( return (
<div className="w-full flex flex-col items-center"> <div className="w-full flex flex-col items-center">
<PageTitle>Create User</PageTitle> <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"> <form onSubmit={handleSubmit} className="w-full max-w-sm mt-4 border-secondary border-2 rounded p-4">
{ {

View file

@ -1,45 +1,26 @@
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import React, { useEffect, useState } from 'react'; import React, { useEffect } from 'react';
import { useLocation, useNavigate } from "react-router" 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 }) { export default function AuthGuard({ children }: { children: React.ReactNode }) {
const { isAuthenticated } = useAuth(); // Access the authentication state from AuthContext const { isAuthenticated } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const location = useLocation(); const location = useLocation();
const publicRoutes = ['/login', '/register']; const publicRoutes = ['/login', '/register'];
const isPublic = publicRoutes.includes(location.pathname); const isPublic = publicRoutes.includes(location.pathname);
useEffect(() => { useEffect(() => {
// Determine behavior based on auth state and route type // Handle redirects based on auth state
if (isAuthenticated === null) { if (isAuthenticated && isPublic) {
// Await authentication resolution (e.g., token check)
setLoading(true);
} else if (isAuthenticated && isPublic) {
// Redirect authenticated users away from public pages // Redirect authenticated users away from public pages
navigate('/', { replace: true }); navigate('/', { replace: true });
} else if (!isAuthenticated && !isPublic) { } else if (!isAuthenticated && !isPublic) {
// Redirect unauthenticated users trying to access protected pages // Redirect unauthenticated users trying to access protected pages
navigate('/login', { replace: true }); navigate('/login', { replace: true });
} else {
// Otherwise, stop loading since the state is resolved
setLoading(false);
} }
}, [isAuthenticated, location.pathname, isPublic, navigate]); }, [isAuthenticated, location.pathname, isPublic, navigate]);
// Show a spinner while authentication state is loading // Render children for all routes - redirects will happen via useEffect
if (loading) {
return <LoadingSpinner />;
}
// Render children only when the authentication state and path are valid
return <>{children}</>; return <>{children}</>;
} }

View file

@ -1,10 +1,10 @@
import React, {FC} from "react"; import React from "react";
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
} }
const Card: FC<Props> = ({ children }) => { const Card = ({ children }: Props) => {
return ( return (
<div className="w-full border-2 border-secondary p-2 pl-4 my-2 rounded flex"> <div className="w-full border-2 border-secondary p-2 pl-4 my-2 rounded flex">
{ children } { children }

View file

@ -1,29 +1,26 @@
"use client";
import React, { useState } from "react"; import React, { useState } from "react";
import Link from "next/link"; import { Link, useNavigate } from "react-router";
import useRoutes from "@/hooks/useRoutes"; import useRoutes from "~/hooks/useRoutes";
import MobileDropdownMenu from "@/components/features/navbar/MobileDropdownMenu"; import MobileDropdownMenu from "~/components/features/navbar/MobileDropdownMenu";
import {useRouter} from "next/navigation"; import { useAuth } from "~/context/AuthContext";
import {useAuth} from "@/context/AuthContext";
const NavBar = () => { const NavBar = () => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const routes = useRoutes(); const routes = useRoutes();
const router = useRouter(); const navigate = useNavigate();
const {isAuthenticated, logout} = useAuth(); const {isAuthenticated, logout} = useAuth();
const handleLogout = (e: React.MouseEvent) => { const handleLogout = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
logout(); logout();
router.replace('/login'); navigate('/login', { replace: true });
}; };
return ( return (
<nav className="border-b-2 border-secondary shadow-sm z-50 mb-8"> <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"> <div className="relative flex justify-between items-center px-4 lg:px-8 py-2 lg:container lg:mx-auto">
{/* Logo */} {/* 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 DISH PLANNER
</Link> </Link>
@ -53,19 +50,19 @@ const NavBar = () => {
{/* Desktop Menu */} {/* Desktop Menu */}
<div className="hidden md:flex space-x-6"> <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 Home
</Link> </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 Dishes
</Link> </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 Users
</Link> </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 History
</Link> </Link>
<Link href={routes.auth.login()} <Link to={routes.auth.login()}
onClick={handleLogout} onClick={handleLogout}
className="text-primary text-right hover:background-secondary"> className="text-primary text-right hover:background-secondary">
Logout Logout

View file

@ -1,5 +1,4 @@
import React from "react" import React from "react"
import type { FC } from "react"
import classNames from "classnames"; import classNames from "classnames";
interface Props { interface Props {
@ -8,7 +7,7 @@ interface Props {
type: 'error' | 'warning' | 'info' | 'success'; type: 'error' | 'warning' | 'info' | 'success';
} }
const Alert: FC<Props> = ({ children, className, type } ) => { const Alert = ({ children, className, type }: Props) => {
let bgColor = 'bg-blue-200' let bgColor = 'bg-blue-200'
let fgColor = 'bg-blue-800' let fgColor = 'bg-blue-800'

View file

@ -1,13 +1,13 @@
import Link from "next/link"; import { Link } from "react-router";
import React, { FC, ReactElement, ReactNode } from "react"; import React from "react";
import classNames from "classnames"; import classNames from "classnames";
interface ButtonProps { interface ButtonProps {
appearance?: 'solid' | 'outline' | 'text'; appearance?: 'solid' | 'outline' | 'text';
children: ReactNode; children: React.ReactNode;
className?: string; className?: string;
href?: string; href?: string;
icon?: ReactNode; icon?: React.ReactNode;
onClick?: () => void; onClick?: () => void;
disabled?: boolean; disabled?: boolean;
size?: 'small' | 'medium' | 'large'; size?: 'small' | 'medium' | 'large';
@ -15,10 +15,10 @@ interface ButtonProps {
variant?: 'primary' | 'secondary' | 'accent'; 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, size = 'medium', type,
variant = 'primary' variant = 'primary'
}) => { }: ButtonProps) => {
const styles = classNames( const styles = classNames(
"flex items-center space-x-1", "flex items-center space-x-1",
"justify-center font-size-18 py-2 px-4 rounded flex", "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 = const iconElement =
React.isValidElement(icon) && React.isValidElement(icon) &&
React.cloneElement(icon as ReactElement<{ className?: string }>, { React.cloneElement(icon as React.ReactElement<{ className?: string }>, {
className: iconClassNames, className: iconClassNames,
}); });
if (href !== undefined) { if (href !== undefined) {
return ( return (
<Link href={href} className={styles}> <Link to={href} className={styles}>
{ icon && iconElement} { icon && iconElement}
{ children} { children}
</Link> </Link>

View file

@ -1,4 +1,4 @@
import React, { type FC } from "react"; import React from "react";
import classNames from "classnames"; import classNames from "classnames";
interface Props { interface Props {
@ -10,7 +10,7 @@ interface Props {
type: 'submit' | 'button'; 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( const style = classNames(
"justify-center border-2 border-accent font-size-18 text-accent-blue py-2 px-4 rounded flex", "justify-center border-2 border-accent font-size-18 text-accent-blue py-2 px-4 rounded flex",
{ 'text-xs': size === "small" }, { 'text-xs': size === "small" },

View file

@ -1,4 +1,4 @@
import React, { type FC, type ReactElement } from "react"; import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import { Link } from "react-router" import { Link } from "react-router"
@ -11,7 +11,7 @@ interface Props {
variant?: "primary" | "secondary"; 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( const linkClassNames = classNames(
"underline font-default pt-3 pb-3 px-4 rounded mb-0 flex", "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 = const iconElement =
React.isValidElement(icon) && React.isValidElement(icon) &&
React.cloneElement(icon as ReactElement<{ className?: string }>, { React.cloneElement(icon as React.ReactElement<{ className?: string }>, {
className: iconClassNames, className: iconClassNames,
}); });

View file

@ -1,4 +1,4 @@
import React, { type FC } from "react"; import React from "react";
import classNames from "classnames"; import classNames from "classnames";
interface Props { interface Props {
@ -10,7 +10,7 @@ interface Props {
type: 'submit' | 'button'; 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( const style = classNames(
"py-2 px-4 bg-primary text-white text-xl p-2 rounded hover:bg-secondary mb-0", "py-2 px-4 bg-primary text-white text-xl p-2 rounded hover:bg-secondary mb-0",
{ {

View file

@ -1,4 +1,4 @@
import React, { type FC, type ReactElement } from "react"; import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import { Link } from "react-router" import { Link } from "react-router"
@ -11,7 +11,7 @@ interface Props {
variant?: "primary" | "secondary"; 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( const style = classNames(
"py-2 px-4 text-xl p-2 rounded hover:bg-secondary mb-0 text-center flex", "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 = const iconElement =
React.isValidElement(icon) && React.isValidElement(icon) &&
React.cloneElement(icon as ReactElement<{ className?: string }>, { React.cloneElement(icon as React.ReactElement<{ className?: string }>, {
className: iconClassNames, className: iconClassNames,
}); });

View file

@ -1,11 +1,10 @@
import { FC } from "react"
import classNames from "classnames" import classNames from "classnames"
interface HrProps { interface HrProps {
className?: string; className?: string;
} }
const Hr: FC<HrProps> = ({ className }) => { const Hr = ({ className }: HrProps) => {
const styles = classNames("my-4 border-secondary", className) const styles = classNames("my-4 border-secondary", className)
return <hr className={styles}/> return <hr className={styles}/>

View file

@ -1,12 +1,12 @@
import React, {FC, ReactNode} from "react"; import React from "react";
interface LabelProps { interface LabelProps {
href?: string; href?: string;
children: ReactNode; children: React.ReactNode;
onClick?: () => void; 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" const styles = "items-center space-x-1 background-accent p-2 rounded"
if (href !== undefined) { if (href !== undefined) {

View file

@ -1,4 +1,4 @@
import { type FC, type JSX } from "react"; import { type JSX } from "react";
import classNames from "classnames"; import classNames from "classnames";
import Button from "@/components/ui/Button" import Button from "@/components/ui/Button"
@ -11,14 +11,14 @@ interface ModalProps {
setModalOpen: (open: boolean) => void; setModalOpen: (open: boolean) => void;
} }
const Modal: FC<ModalProps> = ({ const Modal = ({
buttonLabel, buttonLabel,
buttonClassName, buttonClassName,
modalChildren, modalChildren,
modalOpen, modalOpen,
buttonChildren, buttonChildren,
setModalOpen, setModalOpen,
}) => { }: ModalProps) => {
const buttonStyles = classNames(buttonClassName, 'anta-regular'); const buttonStyles = classNames(buttonClassName, 'anta-regular');
const closeModal = () => { const closeModal = () => {

View file

@ -1,4 +1,3 @@
import { type FC } from "react";
import classNames from "classnames"; import classNames from "classnames";
interface Props { interface Props {
@ -6,7 +5,7 @@ interface Props {
className?: string, className?: string,
} }
const PageTitle: FC<Props> = ({ children, className }) => { const PageTitle = ({ children, className }: Props) => {
const styles = classNames( const styles = classNames(
'ml-4 text-2xl font-default uppercase w-full text-accent-blue font-bold', 'ml-4 text-2xl font-default uppercase w-full text-accent-blue font-bold',
className, className,

View file

@ -1,11 +1,11 @@
import React, {FC, useState} from "react"; import React, {useState} from "react";
interface Props { interface Props {
value: number; value: number;
setValue: (value: number) => void; 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 [openInput, setOpenInput] = useState<'category' | 'number'>([7, 365].includes(value) ? 'category' : 'number')
const toggleInput = (e: React.MouseEvent<HTMLButtonElement>) => { const toggleInput = (e: React.MouseEvent<HTMLButtonElement>) => {

View file

@ -1,11 +1,10 @@
import {FC} from "react";
interface ToggleProps { interface ToggleProps {
checked: boolean; checked: boolean;
onChange: (checked: boolean) => void; onChange: (checked: boolean) => void;
} }
const Toggle: FC<ToggleProps> = ({ checked, onChange }) => { const Toggle = ({ checked, onChange }: ToggleProps) => {
const handleChange = () => { const handleChange = () => {
onChange(checked); onChange(checked);
} }

View file

@ -1,30 +1,25 @@
"use client"
import React, { createContext, useContext, useEffect, useState } from 'react'; import React, { createContext, useContext, useEffect, useState } from 'react';
interface AuthContextProps { interface AuthContextProps {
isAuthenticated: boolean | null; isAuthenticated: boolean;
login: () => void; login: () => void;
logout: () => void; logout: () => void;
} }
const AuthContext = createContext<AuthContextProps>({ const AuthContext = createContext<AuthContextProps>({
isAuthenticated: null, isAuthenticated: false,
login: () => {}, login: () => {},
logout: () => {}, logout: () => {},
}); });
export const AuthProvider = ({ children }: { children: React.ReactNode }) => { 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(() => { useEffect(() => {
// Check token on client after mount
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (token) { setIsAuthenticated(!!token);
// You could add any token validation logic here
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
}
}, []); }, []);
const login = () => { const login = () => {

View file

@ -12,6 +12,7 @@ import "./app.css";
import React from "react" import React from "react"
import { AuthProvider } from "~/context/AuthContext" import { AuthProvider } from "~/context/AuthContext"
import AuthGuard from "~/components/layout/AuthGuard" import AuthGuard from "~/components/layout/AuthGuard"
import NavBar from "~/components/layout/NavBar"
export const links: Route.LinksFunction = () => [ export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" }, { 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"> <body className="antialiased w-full min-h-screen bg-gray-600">
<AuthProvider> <AuthProvider>
<AuthGuard> <AuthGuard>
<NavBar />
<div className="m-5 lg:container lg:mx-auto">{ children }</div> <div className="m-5 lg:container lg:mx-auto">{ children }</div>
</AuthGuard> </AuthGuard>
</AuthProvider> </AuthProvider>

View file

@ -4,4 +4,20 @@ export default [
index("routes/home.tsx"), index("routes/home.tsx"),
route("login", "./components/features/auth/LoginForm.tsx"), route("login", "./components/features/auth/LoginForm.tsx"),
route("register", "./components/features/auth/RegisterForm.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; ] satisfies RouteConfig;

View 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>
);
}

View 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 />;
}

View 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} />
))
)}
</>
);
}

View file

@ -1,13 +1,13 @@
import type { Route } from "./+types/home"; import type { Route } from "./+types/home";
import { Welcome } from "../welcome/welcome"; import UpcomingDishes from "~/components/features/schedule/UpcomingDishes";
export function meta({}: Route.MetaArgs) { export function meta({}: Route.MetaArgs) {
return [ return [
{ title: "New React Router App" }, { title: "Dish Planner - Schedule" },
{ name: "description", content: "Welcome to React Router!" }, { name: "description", content: "View and manage your upcoming dish schedule" },
]; ];
} }
export default function Home() { export default function Home() {
return <Welcome />; return <UpcomingDishes />;
} }

View 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;

View 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 />;
}

View 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;

View 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;

View 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;

View file

@ -0,0 +1,4 @@
export type RecurrenceType = {
type: "App\\Models\\WeeklyRecurrence" | "App\\Models\\MinimumRecurrence";
value: number;
}

View file

@ -1,11 +1,6 @@
import type { ScheduledUserDishType, UserDishType } from "@/types/ScheduledUserDishType"; import type { ScheduledUserDishType, UserDishType } from "@/types/ScheduledUserDishType";
import type { UserType } from "@/types/UserType"; import type { UserType } from "@/types/UserType";
export type RecurrenceType = {
type: "App\\Models\\WeeklyRecurrence" | "App\\Models\\MinimumRecurrence";
value: number;
}
export type ScheduleType = { export type ScheduleType = {
id: number; id: number;
date: string; date: string;

View file

@ -1,6 +1,6 @@
import type { UserType } from "@/types/UserType"; import type { UserType } from "@/types/UserType";
import type { DishType } from "@/types/DishType"; import type { DishType } from "@/types/DishType";
import type { RecurrenceType } from "@/types/ScheduleType"; import type { RecurrenceType } from "@/types/RecurrenceType";
export type UserDishType = { export type UserDishType = {
id: number; id: number;
@ -9,12 +9,6 @@ export type UserDishType = {
recurrences: RecurrenceType[]; recurrences: RecurrenceType[];
} }
export type UserDishWithoutUserType = {
id: number;
dish: DishType;
recurrences: RecurrenceType[];
}
export type ScheduledUserDishType = { export type ScheduledUserDishType = {
id: number; id: number;
user_dish: UserDishType; user_dish: UserDishType;

View file

@ -1,5 +1,5 @@
import {UserType} from "@/types/UserType"; import {UserType} from "@/types/UserType";
import {RecurrenceType} from "@/types/ScheduleType"; import {RecurrenceType} from "@/types/RecurrenceType";
export type DishType = { export type DishType = {
user: UserType; user: UserType;

View 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[];
}

View file

@ -1,4 +1,4 @@
import type { UserDishWithoutUserType } from "@/types/ScheduledUserDishType"; import type { UserDishWithoutUserType } from "@/types/UserDishWithoutUserType";
export type UserType = { export type UserType = {
id: number; id: number;

View file

@ -1,4 +1,4 @@
import {RecurrenceType} from "@/types/ScheduleType"; import {RecurrenceType} from "@/types/RecurrenceType";
import {apiRequest} from "@/utils/api/apiRequest"; import {apiRequest} from "@/utils/api/apiRequest";
import {UserType} from "@/types/UserType"; import {UserType} from "@/types/UserType";

View file

@ -6,11 +6,14 @@
"": { "": {
"name": "my-react-router-app", "name": "my-react-router-app",
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.9",
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@react-router/node": "^7.5.3", "@react-router/node": "^7.5.3",
"@react-router/serve": "^7.5.3", "@react-router/serve": "^7.5.3",
"@types/luxon": "^3.7.1",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"isbot": "^5.1.27", "isbot": "^5.1.27",
"luxon": "^3.7.2",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router": "^7.5.3" "react-router": "^7.5.3"
@ -935,6 +938,79 @@
"node": ">=18" "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": { "node_modules/@heroicons/react": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
@ -1107,6 +1183,73 @@
"node": ">=14" "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": { "node_modules/@react-router/dev": {
"version": "7.6.0", "version": "7.6.0",
"resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.6.0.tgz", "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.6.0.tgz",
@ -1237,6 +1380,36 @@
"react-router": "7.6.0" "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": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.40.2", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
@ -1517,6 +1690,15 @@
"win32" "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": { "node_modules/@tailwindcss/node": {
"version": "4.1.6", "version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.6.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.6.tgz",
@ -1794,6 +1976,33 @@
"vite": "^5.2.0 || ^6" "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": { "node_modules/@types/estree": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@ -1801,6 +2010,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/node": {
"version": "20.17.46", "version": "20.17.46",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.46.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.46.tgz",
@ -2119,6 +2334,15 @@
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT" "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": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -3283,6 +3507,15 @@
"yallist": "^3.0.2" "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": { "node_modules/magic-string": {
"version": "0.30.17", "version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
@ -4363,6 +4596,12 @@
"node": ">=8" "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": { "node_modules/tailwindcss": {
"version": "4.1.6", "version": "4.1.6",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz", "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": { "node_modules/type-is": {
"version": "1.6.18", "version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@ -4548,6 +4793,15 @@
"browserslist": ">= 4.21.0" "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": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",

View file

@ -9,11 +9,14 @@
"typecheck": "react-router typegen && tsc" "typecheck": "react-router typegen && tsc"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.9",
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@react-router/node": "^7.5.3", "@react-router/node": "^7.5.3",
"@react-router/serve": "^7.5.3", "@react-router/serve": "^7.5.3",
"@types/luxon": "^3.7.1",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"isbot": "^5.1.27", "isbot": "^5.1.27",
"luxon": "^3.7.2",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router": "^7.5.3" "react-router": "^7.5.3"