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 "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>
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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.");
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 = () => {
|
||||||
|
|
|
||||||
|
|
@ -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("");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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", {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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}</>;
|
||||||
}
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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" },
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}/>
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 = () => {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>) => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = () => {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
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 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 />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 { 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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
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 = {
|
export type UserType = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
||||||
254
frontend/package-lock.json
generated
254
frontend/package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue