diff --git a/.gitignore b/.gitignore index 0e041e7..9eb836b 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ next-env.d.ts /dist/ -.env \ No newline at end of file +.env +algorithm-notes.md diff --git a/Dockerfile b/Dockerfile index 02e758e..9ce9d1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ FROM node:22.4.1-alpine as base WORKDIR /usr/src/app # Copy package.json and install dependencies -COPY package*.json ./ +COPY package.json package-lock.json ./ RUN npm ci # Copy prisma directory diff --git a/components.json b/components.json new file mode 100644 index 0000000..8a1bf45 --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} \ No newline at end of file diff --git a/components/AddCustomExercise.tsx b/components/AddCustomExercise.tsx index 5f20684..3501084 100644 --- a/components/AddCustomExercise.tsx +++ b/components/AddCustomExercise.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { FaEllipsisV, FaPlus, FaDumbbell, FaEdit, FaTrashAlt } from 'react-icons/fa'; import { useExerciseOptions } from '../utils/useExerciseOptions'; import { fetchCustomExercises, createCustomExercise, updateCustomExercise, deleteCustomExercise } from '../pages/api/customExercises'; diff --git a/components/AdminLoadExercises.tsx b/components/AdminLoadExercises.tsx index daac75b..0d68aeb 100644 --- a/components/AdminLoadExercises.tsx +++ b/components/AdminLoadExercises.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import axios from 'axios'; import { useSession } from 'next-auth/react'; diff --git a/components/CurrentWorkout.tsx b/components/CurrentWorkout.tsx index b2560ac..76c92cc 100644 --- a/components/CurrentWorkout.tsx +++ b/components/CurrentWorkout.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState, useEffect, ChangeEvent } from 'react'; +import { useState, useEffect, ChangeEvent } from 'react'; import { useSession } from 'next-auth/react'; import Calendar from 'react-calendar'; import 'react-calendar/dist/Calendar.css'; @@ -49,6 +49,7 @@ const CurrentWorkout: React.FC = () => { // const [workoutExerciseOrder, setWorkoutExerciseOrder] = useState(Array.from(workoutExercises.keys())); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + const [localNotes, setLocalNotes] = useState(workoutData.notes); useEffect(() => { setIsMounted(true); @@ -114,6 +115,10 @@ const CurrentWorkout: React.FC = () => { saveWorkoutData(); }, [workoutData, selectedDate, isMounted, session]); + useEffect(() => { + setLocalNotes(workoutData.notes); + }, [workoutData.notes]); + const handleAddSet = async (exerciseName: string, type: 'regular' | 'dropset' = 'regular') => { setIsLoading(true); try { @@ -654,10 +659,13 @@ const CurrentWorkout: React.FC = () => { diff --git a/components/CurrentWorkout_old.tsx b/components/CurrentWorkout_old.tsx deleted file mode 100644 index e0bb369..0000000 --- a/components/CurrentWorkout_old.tsx +++ /dev/null @@ -1,652 +0,0 @@ -'use client'; - -import React, { useState, useEffect, ChangeEvent } from 'react'; -import { useSession } from 'next-auth/react'; -import Calendar from 'react-calendar'; -import 'react-calendar/dist/Calendar.css'; -import '../styles/globals.css'; -import Dropdown from 'react-dropdown'; -import Select, { SingleValue } from 'react-select'; -import 'react-dropdown/style.css'; -import { fetchExercises, Exercise } from '../utils/exerciseService'; -import { useExerciseOptions } from '../utils/useExerciseOptions'; -import '../styles/DotDropdownMenu.css'; -import Stopwatch from '../components/Stopwatch'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faStopwatch, faSquare, faCheckSquare, faCalendarAlt } from '@fortawesome/free-solid-svg-icons'; -import LastStatsModal from '../components/LastStatsModal'; -import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; - -import { fetchCustomExercises } from '../pages/api/customExercises'; -import { fetchPresetExercises } from '../pages/api/presetExercises'; -import { fetchWorkoutData, updateWorkoutData, WorkoutData, Set } from '../pages/api/workoutRoutes'; - -type ExerciseMap = Map; - -const CurrentWorkout: React.FC = () => { - const { data: session } = useSession(); - const [exercises, setExercises] = useState([]); - const [customExercises, setCustomExercises] = useState([]); - const [selectedDate, setSelectedDate] = useState(() => new Date(Date.UTC(new Date().getFullYear(), new Date().getMonth(), new Date().getDate()))); - // const [workoutExercises, setWorkoutExercises] = useState(new Map()); - const [workoutData, setWorkoutData] = useState({ - exercises: {}, - exerciseOrder: [], - notes: '', - }); - - const { equipmentTypes, primaryMuscles, exercisesByMuscle } = useExerciseOptions(); - const [notes, setNotes] = useState(''); - const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(true); - const [isCalendarVisible, setIsCalendarVisible] = useState(false); - const [isMounted, setIsMounted] = useState(false); - const [mesocycleStartDate, setMesocycleStartDate] = useState(new Date()); - const [duration, setDuration] = useState(4); - const [isStopwatchVisible, setIsStopwatchVisible] = useState(false); - const [isLastStatsModalOpen, setIsLastStatsModalOpen] = useState(false); - const [currentExercise, setCurrentExercise] = useState(''); - const [lastStats, setLastStats] = useState<{ date: string, sets: Set[] } | null>(null); - // const [workoutExerciseOrder, setWorkoutExerciseOrder] = useState(Array.from(workoutExercises.keys())); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - setIsMounted(true); - const fetchMesocycleData = async () => { - try { - const mesocycleData = await getMesocycleData(); // Ignore for now - setMesocycleStartDate(new Date(mesocycleData.startDate)); - setDuration(mesocycleData.duration); - } catch (error) { - console.error('Failed to fetch mesocycle data:', error); - } - }; - fetchMesocycleData(); - }, []); - - useEffect(() => { - const loadExercises = async () => { - try { - const [fetchedPresetExercises, fetchedCustomExercises] = await Promise.all([ - fetchPresetExercises(), - fetchCustomExercises() - ]); - setExercises(fetchedPresetExercises); - setCustomExercises(fetchedCustomExercises); - } catch (error) { - console.error('Failed to load exercises:', error); - } - }; - loadExercises(); - }, []); - - useEffect(() => { - const fetchWorkoutDataForDay = async () => { - if (isMounted && session?.user?.id) { - const dateKey = selectedDate.toISOString().split('T')[0]; - try { - const data = await fetchWorkoutData(dateKey, session.user.id); - setWorkoutData(data); - } catch (error) { - console.error('Failed to fetch workout data:', error); - setWorkoutData({ - exercises: {}, - exerciseOrder: [], - notes: '', - }); - } - } - }; - fetchWorkoutDataForDay(); - }, [isMounted, selectedDate, session]); - - useEffect(() => { - const saveWorkoutData = async () => { - if (isMounted && session?.user?.id) { - const dateKey = selectedDate.toISOString().split('T')[0]; - try { - await updateWorkoutData(dateKey, session.user.id, workoutData); - } catch (error) { - console.error('Failed to save workout data:', error); - } - } - }; - saveWorkoutData(); - }, [workoutData, selectedDate, isMounted, session]); - - const handleAddSet = async (exerciseName: string, type: 'regular' | 'dropset' = 'regular') => { - setIsLoading(true); - try { - setWorkoutData(prev => { - const exerciseSets = prev.exercises[exerciseName] || []; - const lastSet = exerciseSets[exerciseSets.length - 1] || { weight: 0, reps: 0, logged: false }; - const newSet: Set = { weight: lastSet.weight, reps: lastSet.reps, logged: false, type }; - return { - ...prev, - exercises: { - ...prev.exercises, - [exerciseName]: [...exerciseSets, newSet], - }, - }; - }); - } catch (error) { - console.error('Failed to add set:', error); - } finally { - setIsLoading(false); - } - }; - - const handleRemoveSet = async (exerciseName: string, setIndex: number) => { - setIsLoading(true); - try { - setWorkoutData(prev => { - const exercises = { ...prev.exercises }; - const sets = exercises[exerciseName] || []; - sets.splice(setIndex, 1); - if (sets.length === 0) { - delete exercises[exerciseName]; - return { - ...prev, - exercises, - exerciseOrder: prev.exerciseOrder.filter(name => name !== exerciseName) - }; - } else { - exercises[exerciseName] = sets; - return { ...prev, exercises }; - } - }); - } catch (error) { - console.error('Failed to remove set:', error); - } finally { - setIsLoading(false); - } - }; - - const handleInputChange = async (exerciseName: string, setIndex: number, field: keyof Set, value: number) => { - setIsLoading(true); - try { - setWorkoutData(prev => { - const exercises = { ...prev.exercises }; - const sets = [...(exercises[exerciseName] || [])]; - sets[setIndex] = { ...sets[setIndex], [field]: value }; - exercises[exerciseName] = sets; - return { ...prev, exercises }; - }); - } catch (error) { - console.error('Failed to update set:', error); - } finally { - setIsLoading(false); - } - }; - - const handleLogSet = async (exerciseName: string, setIndex: number) => { - setIsLoading(true); - try { - setWorkoutData(prev => { - const exercises = { ...prev.exercises }; - const sets = [...(exercises[exerciseName] || [])]; - sets[setIndex] = { ...sets[setIndex], logged: !sets[setIndex].logged }; - exercises[exerciseName] = sets; - return { ...prev, exercises }; - }); - } catch (error) { - console.error('Failed to log set:', error); - } finally { - setIsLoading(false); - } - }; - - const addWorkoutExercise = async (exerciseName: string) => { - await handleAsyncOperation(async () => { - setWorkoutData(prev => { - if (!prev.exercises[exerciseName]) { - return { - ...prev, - exercises: { - ...prev.exercises, - [exerciseName]: [{ weight: 0, reps: 0, logged: false }], - }, - exerciseOrder: [...prev.exerciseOrder, exerciseName], - }; - } - return prev; - }); - }, "Failed to add workout exercise"); - }; - - const handleRemoveExercise = async (exerciseName: string) => { - setIsLoading(true); - try { - setWorkoutData(prev => { - const { [exerciseName]: _, ...remainingExercises } = prev.exercises; - return { - ...prev, - exercises: remainingExercises, - exerciseOrder: prev.exerciseOrder.filter(name => name !== exerciseName) - }; - }); - } catch (error) { - console.error('Failed to remove exercise:', error); - } finally { - setIsLoading(false); - } - }; - - const generateWeightOptions = () => { - const options = []; - for (let i = 1; i <= 10; i++) { - options.push(i); - } - for (let i = 10.25; i <= 300; i += 0.25) { - options.push(parseFloat(i.toFixed(2))); - } - return options; - }; - - const generateRepsOptions = (max: number) => { - return Array.from({ length: max }, (_, i) => i + 1); - }; - - const options = [ - { value: 'addSet', label: 'Add Set' }, - { value: 'addDropset', label: 'Add Dropset' }, - { value: 'removeSet', label: 'Remove Set' }, - { value: 'removeExercise', label: 'Remove Exercise' }, - { value: 'seeLastStats', label: 'Last Session' }, - ]; - - const [dropdownValue, setDropdownValue] = useState(undefined); - - const handleSelect = async (exerciseName: string, option: any) => { - switch (option.value) { - case 'addSet': - handleAddSet(exerciseName); - break; - case 'addDropset': - handleAddSet(exerciseName, 'dropset'); - break; - case 'removeSet': - handleRemoveSet(exerciseName, (workoutData.exercises[exerciseName] || []).length - 1); - break; - case 'removeExercise': - handleRemoveExercise(exerciseName); - break; - case 'seeLastStats': - setCurrentExercise(exerciseName); - const stats = await findLastSessionStats(exerciseName); - setLastStats(stats); - setIsLastStatsModalOpen(true); - break; - default: - break; - } - setDropdownValue(undefined); - }; - - const CustomControl = () => { - return ( -
- -
- ); - }; - - const allExercises = [...exercises, ...customExercises].sort((a, b) => a.name.localeCompare(b.name)); - const exerciseOptions = allExercises.map(exercise => ({ value: exercise.name, label: exercise.name })); - - type ValuePiece = Date | null; - type Value = ValuePiece | [ValuePiece, ValuePiece]; - - const handleDateChange = async (value: Value, event: React.MouseEvent): Promise => { - if (!value || Array.isArray(value) || !session?.user?.id) return; - const normalisedDate = new Date(Date.UTC(value.getFullYear(), value.getMonth(), value.getDate())); - setSelectedDate(normalisedDate); - - if (isMounted) { - const dateKey = normalisedDate.toISOString().split('T')[0]; - try { - const data = await fetchWorkoutData(dateKey, session.user.id); - setWorkoutData(data); - } catch (error) { - console.error('Failed to fetch workout data:', error); - setWorkoutData({ - exercises: {}, - exerciseOrder: [], - notes: '', - }); - } - } - setIsCalendarVisible(false); - }; - - const calculateDayAndWeek = (selectedDate: Date, startDate: Date, duration: number) => { - const start = new Date(startDate); - const selected = new Date(selectedDate); - const dayDiff = Math.floor((selected.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1; - - let weekNumber = Math.floor(dayDiff / 7) + 1; - let dayNumber = (dayDiff % 7) + 1; - - if (dayDiff < 0 || weekNumber - 1 > duration) { - weekNumber = 0; - dayNumber = 0; - } - - return { week: weekNumber, day: dayNumber }; - }; - - const { week, day } = calculateDayAndWeek(selectedDate, mesocycleStartDate, duration); - - const findLastSessionStats = async (exerciseName: string): Promise<{ date: string, sets: Set[] } | null> => { - if (!session?.user?.id) return null; - const currentDate = new Date(selectedDate); - currentDate.setDate(currentDate.getDate() - 1); - - for (let i = 0; i < 365; i++) { - const dateKey = currentDate.toISOString().split('T')[0]; - try { - const data = await fetchWorkoutData(dateKey, session.user.id); - if (data.exercises[exerciseName]) { - return { date: dateKey, sets: data.exercises[exerciseName] }; - } - } catch (error) { - console.error('Failed to fetch workout data:', error); - } - currentDate.setDate(currentDate.getDate() - 1); - } - - return null; - }; - - const onDragEnd = async (result: any) => { - if (!result.destination) { - return; - } - - const items = Array.from(workoutData.exerciseOrder); - const [reorderedItem] = items.splice(result.source.index, 1); - items.splice(result.destination.index, 0, reorderedItem); - - setWorkoutData(prev => ({ ...prev, exerciseOrder: items })); - }; - - const handleAsyncOperation = async (operation: () => Promise, errorMessage: string) => { - setError(null); - setIsLoading(true); - try { - await operation(); - } catch (err) { - console.error(errorMessage, err); - setError(errorMessage); - } finally { - setIsLoading(false); - } - }; - - return ( -
-
-
-

- {/* TODO: Add Mesocycle name */} - Day {day} Week {week} -

-
-
-

- {selectedDate.toDateString()} -

-
-
- {Object.values(workoutData.exercises).every(sets => sets.every(set => set.logged)) && ( - - )} - - -
-
- - {isCalendarVisible && ( -
- -
- )} - - {isStopwatchVisible && ( -
- -
- )} - -
- ({ value: option.toString(), label: option.toString() }))} - onChange={(option: SingleValue<{ value: string; label: string }>) => - handleAsyncOperation( - async () => await handleInputChange(exerciseName, setIndex, 'weight', Number(option?.value)), - "Failed to update weight" - ) - } - placeholder="ENTER" - className="w-full text-black" - classNamePrefix="react-select" - value={{ value: set.weight.toString(), label: set.weight.toString() }} - components={{ DropdownIndicator: () => null, IndicatorSeparator: () => null }} - styles={{ - control: (provided, state) => ({ - ...provided, - height: '40px', - minHeight: '40px', - justifyContent: 'center', - borderColor: state.isFocused ? 'purple' : '#e0e0e0', - '&:hover': { - borderColor: '#4A148C', - }, - }), - valueContainer: (provided) => ({ - ...provided, - justifyContent: 'center', - }), - singleValue: (provided) => ({ - ...provided, - textAlign: 'center', - }) - }} - /> -
-
- -
- - setIsLastStatsModalOpen(false)} - exerciseName={currentExercise} - lastStats={lastStats} - /> -
-
-
- ); -}; - -export default CurrentWorkout; \ No newline at end of file diff --git a/components/CurrentWorkout_prototype_new.tsx b/components/CurrentWorkout_prototype_new.tsx deleted file mode 100644 index ac5b26c..0000000 --- a/components/CurrentWorkout_prototype_new.tsx +++ /dev/null @@ -1,607 +0,0 @@ -'use client'; - -import React, { useState, useEffect, ChangeEvent } from 'react'; -import { useSession } from 'next-auth/react'; -import Calendar from 'react-calendar'; -import 'react-calendar/dist/Calendar.css'; -import '../styles/globals.css'; -import Dropdown from 'react-dropdown'; -import Select, { SingleValue } from 'react-select'; -import 'react-dropdown/style.css'; -import { fetchExercises, Exercise } from '../utils/exerciseService'; -import { useExerciseOptions } from '../utils/useExerciseOptions'; -import '../styles/DotDropdownMenu.css'; -import Stopwatch from '../components/Stopwatch'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faStopwatch, faSquare, faCheckSquare, faCalendarAlt, faEllipsisV, faDumbbell, faPlus } from '@fortawesome/free-solid-svg-icons'; -import LastStatsModal from '../components/LastStatsModal'; -import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; - -import { fetchCustomExercises } from '../pages/api/customExercises'; -import { fetchPresetExercises } from '../pages/api/presetExercises'; -import { fetchWorkoutData, updateWorkoutData, WorkoutData, Set } from '../pages/api/workoutRoutes'; - -type ExerciseMap = Map; - -const CurrentWorkout: React.FC = () => { - const { data: session } = useSession(); - const [exercises, setExercises] = useState([]); - const [customExercises, setCustomExercises] = useState([]); - const [selectedDate, setSelectedDate] = useState(() => new Date(Date.UTC(new Date().getFullYear(), new Date().getMonth(), new Date().getDate()))); - // const [workoutExercises, setWorkoutExercises] = useState(new Map()); - const [workoutData, setWorkoutData] = useState({ - exercises: {}, - exerciseOrder: [], - notes: '', - }); - - const { equipmentTypes, primaryMuscles, exercisesByMuscle } = useExerciseOptions(); - const [notes, setNotes] = useState(''); - const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(true); - const [isCalendarVisible, setIsCalendarVisible] = useState(false); - const [isMounted, setIsMounted] = useState(false); - const [mesocycleStartDate, setMesocycleStartDate] = useState(new Date()); - const [duration, setDuration] = useState(4); - const [isStopwatchVisible, setIsStopwatchVisible] = useState(false); - const [isLastStatsModalOpen, setIsLastStatsModalOpen] = useState(false); - const [currentExercise, setCurrentExercise] = useState(''); - const [lastStats, setLastStats] = useState<{ date: string, sets: Set[] } | null>(null); - // const [workoutExerciseOrder, setWorkoutExerciseOrder] = useState(Array.from(workoutExercises.keys())); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - setIsMounted(true); - const fetchMesocycleData = async () => { - try { - const mesocycleData = await getMesocycleData(); // Ignore for now - setMesocycleStartDate(new Date(mesocycleData.startDate)); - setDuration(mesocycleData.duration); - } catch (error) { - console.error('Failed to fetch mesocycle data:', error); - } - }; - fetchMesocycleData(); - }, []); - - useEffect(() => { - const loadExercises = async () => { - try { - const [fetchedPresetExercises, fetchedCustomExercises] = await Promise.all([ - fetchPresetExercises(), - fetchCustomExercises() - ]); - setExercises(fetchedPresetExercises); - setCustomExercises(fetchedCustomExercises); - } catch (error) { - console.error('Failed to load exercises:', error); - } - }; - loadExercises(); - }, []); - - useEffect(() => { - const fetchWorkoutDataForDay = async () => { - if (isMounted && session?.user?.id) { - const dateKey = selectedDate.toISOString().split('T')[0]; - try { - const data = await fetchWorkoutData(dateKey, session.user.id); - setWorkoutData(data); - } catch (error) { - console.error('Failed to fetch workout data:', error); - setWorkoutData({ - exercises: {}, - exerciseOrder: [], - notes: '', - }); - } - } - }; - fetchWorkoutDataForDay(); - }, [isMounted, selectedDate, session]); - - useEffect(() => { - const saveWorkoutData = async () => { - if (isMounted && session?.user?.id) { - const dateKey = selectedDate.toISOString().split('T')[0]; - try { - await updateWorkoutData(dateKey, session.user.id, workoutData); - } catch (error) { - console.error('Failed to save workout data:', error); - } - } - }; - saveWorkoutData(); - }, [workoutData, selectedDate, isMounted, session]); - - const handleAddSet = async (exerciseName: string, type: 'regular' | 'dropset' = 'regular') => { - setIsLoading(true); - try { - setWorkoutData(prev => { - const exerciseSets = prev.exercises[exerciseName] || []; - const lastSet = exerciseSets[exerciseSets.length - 1] || { weight: 0, reps: 0, logged: false }; - const newSet: Set = { weight: lastSet.weight, reps: lastSet.reps, logged: false, type }; - return { - ...prev, - exercises: { - ...prev.exercises, - [exerciseName]: [...exerciseSets, newSet], - }, - }; - }); - } catch (error) { - console.error('Failed to add set:', error); - } finally { - setIsLoading(false); - } - }; - - const handleRemoveSet = async (exerciseName: string, setIndex: number) => { - setIsLoading(true); - try { - setWorkoutData(prev => { - const exercises = { ...prev.exercises }; - const sets = exercises[exerciseName] || []; - sets.splice(setIndex, 1); - if (sets.length === 0) { - delete exercises[exerciseName]; - return { - ...prev, - exercises, - exerciseOrder: prev.exerciseOrder.filter(name => name !== exerciseName) - }; - } else { - exercises[exerciseName] = sets; - return { ...prev, exercises }; - } - }); - } catch (error) { - console.error('Failed to remove set:', error); - } finally { - setIsLoading(false); - } - }; - - const handleInputChange = async (exerciseName: string, setIndex: number, field: keyof Set, value: number) => { - setIsLoading(true); - try { - setWorkoutData(prev => { - const exercises = { ...prev.exercises }; - const sets = [...(exercises[exerciseName] || [])]; - sets[setIndex] = { ...sets[setIndex], [field]: value }; - exercises[exerciseName] = sets; - return { ...prev, exercises }; - }); - } catch (error) { - console.error('Failed to update set:', error); - } finally { - setIsLoading(false); - } - }; - - const handleLogSet = async (exerciseName: string, setIndex: number) => { - setIsLoading(true); - try { - setWorkoutData(prev => { - const exercises = { ...prev.exercises }; - const sets = [...(exercises[exerciseName] || [])]; - sets[setIndex] = { ...sets[setIndex], logged: !sets[setIndex].logged }; - exercises[exerciseName] = sets; - return { ...prev, exercises }; - }); - } catch (error) { - console.error('Failed to log set:', error); - } finally { - setIsLoading(false); - } - }; - - const addWorkoutExercise = async (exerciseName: string) => { - await handleAsyncOperation(async () => { - setWorkoutData(prev => { - if (!prev.exercises[exerciseName]) { - return { - ...prev, - exercises: { - ...prev.exercises, - [exerciseName]: [{ weight: 0, reps: 0, logged: false }], - }, - exerciseOrder: [...prev.exerciseOrder, exerciseName], - }; - } - return prev; - }); - }, "Failed to add workout exercise"); - }; - - const handleRemoveExercise = async (exerciseName: string) => { - setIsLoading(true); - try { - setWorkoutData(prev => { - const { [exerciseName]: _, ...remainingExercises } = prev.exercises; - return { - ...prev, - exercises: remainingExercises, - exerciseOrder: prev.exerciseOrder.filter(name => name !== exerciseName) - }; - }); - } catch (error) { - console.error('Failed to remove exercise:', error); - } finally { - setIsLoading(false); - } - }; - - const generateWeightOptions = () => { - const options = []; - for (let i = 1; i <= 10; i++) { - options.push(i); - } - for (let i = 10.25; i <= 300; i += 0.25) { - options.push(parseFloat(i.toFixed(2))); - } - return options; - }; - - const generateRepsOptions = (max: number) => { - return Array.from({ length: max }, (_, i) => i + 1); - }; - - const options = [ - { value: 'addSet', label: 'Add Set' }, - { value: 'addDropset', label: 'Add Dropset' }, - { value: 'removeSet', label: 'Remove Set' }, - { value: 'removeExercise', label: 'Remove Exercise' }, - { value: 'seeLastStats', label: 'Last Session' }, - ]; - - const [dropdownValue, setDropdownValue] = useState(undefined); - - const handleSelect = async (exerciseName: string, option: any) => { - switch (option.value) { - case 'addSet': - handleAddSet(exerciseName); - break; - case 'addDropset': - handleAddSet(exerciseName, 'dropset'); - break; - case 'removeSet': - handleRemoveSet(exerciseName, (workoutData.exercises[exerciseName] || []).length - 1); - break; - case 'removeExercise': - handleRemoveExercise(exerciseName); - break; - case 'seeLastStats': - setCurrentExercise(exerciseName); - const stats = await findLastSessionStats(exerciseName); - setLastStats(stats); - setIsLastStatsModalOpen(true); - break; - default: - break; - } - setDropdownValue(undefined); - }; - - const CustomControl = () => { - return ( -
- -
- ); - }; - - const allExercises = [...exercises, ...customExercises].sort((a, b) => a.name.localeCompare(b.name)); - const exerciseOptions = allExercises.map(exercise => ({ value: exercise.name, label: exercise.name })); - - type ValuePiece = Date | null; - type Value = ValuePiece | [ValuePiece, ValuePiece]; - - const handleDateChange = async (value: Value, event: React.MouseEvent): Promise => { - if (!value || Array.isArray(value) || !session?.user?.id) return; - const normalisedDate = new Date(Date.UTC(value.getFullYear(), value.getMonth(), value.getDate())); - setSelectedDate(normalisedDate); - - if (isMounted) { - const dateKey = normalisedDate.toISOString().split('T')[0]; - try { - const data = await fetchWorkoutData(dateKey, session.user.id); - setWorkoutData(data); - } catch (error) { - console.error('Failed to fetch workout data:', error); - setWorkoutData({ - exercises: {}, - exerciseOrder: [], - notes: '', - }); - } - } - setIsCalendarVisible(false); - }; - - const calculateDayAndWeek = (selectedDate: Date, startDate: Date, duration: number) => { - const start = new Date(startDate); - const selected = new Date(selectedDate); - const dayDiff = Math.floor((selected.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1; - - let weekNumber = Math.floor(dayDiff / 7) + 1; - let dayNumber = (dayDiff % 7) + 1; - - if (dayDiff < 0 || weekNumber - 1 > duration) { - weekNumber = 0; - dayNumber = 0; - } - - return { week: weekNumber, day: dayNumber }; - }; - - const { week, day } = calculateDayAndWeek(selectedDate, mesocycleStartDate, duration); - - const findLastSessionStats = async (exerciseName: string): Promise<{ date: string, sets: Set[] } | null> => { - if (!session?.user?.id) return null; - const currentDate = new Date(selectedDate); - currentDate.setDate(currentDate.getDate() - 1); - - for (let i = 0; i < 365; i++) { - const dateKey = currentDate.toISOString().split('T')[0]; - try { - const data = await fetchWorkoutData(dateKey, session.user.id); - if (data.exercises[exerciseName]) { - return { date: dateKey, sets: data.exercises[exerciseName] }; - } - } catch (error) { - console.error('Failed to fetch workout data:', error); - } - currentDate.setDate(currentDate.getDate() - 1); - } - - return null; - }; - - const onDragEnd = async (result: any) => { - if (!result.destination) { - return; - } - - const items = Array.from(workoutData.exerciseOrder); - const [reorderedItem] = items.splice(result.source.index, 1); - items.splice(result.destination.index, 0, reorderedItem); - - setWorkoutData(prev => ({ ...prev, exerciseOrder: items })); - }; - - const handleAsyncOperation = async (operation: () => Promise, errorMessage: string) => { - setError(null); - setIsLoading(true); - try { - await operation(); - } catch (err) { - console.error(errorMessage, err); - setError(errorMessage); - } finally { - setIsLoading(false); - } - }; - - return ( -
-
-
-
-

- Day {day} Week {week} -

-
- {Object.values(workoutData.exercises).every(sets => sets.every(set => set.logged)) && ( - - )} - - -
-
-

- {selectedDate.toDateString()} -

-
- - {isCalendarVisible && ( -
- -
- )} - - {isStopwatchVisible && ( -
- -
- )} - -
- ({ value: option.toString(), label: option.toString() }))} - onChange={(option: SingleValue<{ value: string; label: string }>) => - handleAsyncOperation( - async () => await handleInputChange(exerciseName, setIndex, 'weight', Number(option?.value)), - "Failed to update weight" - ) - } - value={{ value: set.weight.toString(), label: set.weight.toString() }} - className="w-full" - classNamePrefix="react-select" - // ... (keep the existing styles) - /> - - - -
- - setIsLastStatsModalOpen(false)} - exerciseName={currentExercise} - lastStats={lastStats} - /> -
-
- ); -}; - -export default CurrentWorkout; \ No newline at end of file diff --git a/components/CustomExerciseModal.tsx b/components/CustomExerciseModal.tsx index b770643..343d76a 100644 --- a/components/CustomExerciseModal.tsx +++ b/components/CustomExerciseModal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import Select, { MultiValue, SingleValue } from 'react-select'; interface SelectOption { diff --git a/components/FullscreenToggle.tsx b/components/FullscreenToggle.tsx new file mode 100644 index 0000000..ffe0376 --- /dev/null +++ b/components/FullscreenToggle.tsx @@ -0,0 +1,38 @@ +import { useState, useCallback, useEffect } from 'react'; +import { Maximize, Minimize } from 'lucide-react'; + +const FullscreenToggle: React.FC = () => { + const [isFullscreen, setIsFullscreen] = useState(false); + + const toggleFullscreen = useCallback((): void => { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen().catch((e: Error) => { + console.error(`Error attempting to enable full-screen mode: ${e.message}`); + }); + } else { + if (document.exitFullscreen) { + document.exitFullscreen(); + } + } + }, []); + + useEffect(() => { + const onFullscreenChange = (): void => { + setIsFullscreen(Boolean(document.fullscreenElement)); + }; + + document.addEventListener('fullscreenchange', onFullscreenChange); + return () => document.removeEventListener('fullscreenchange', onFullscreenChange); + }, []); + + return ( + + ); +}; + +export default FullscreenToggle; diff --git a/components/Layout.tsx b/components/Layout.tsx index 0e88615..98fed59 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { useSession } from 'next-auth/react'; import { useRouter } from 'next/router'; import Sidebar from './Sidebar'; diff --git a/components/Mesocycles.tsx b/components/Mesocycles.tsx index f922c85..bb7d3c5 100644 --- a/components/Mesocycles.tsx +++ b/components/Mesocycles.tsx @@ -1,52 +1,60 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { FaEllipsisV, FaPlus, FaDumbbell, FaCalendarAlt } from 'react-icons/fa'; import ConfigureMesocycleModal from './ConfigureMesocycleModal'; import '../styles/globals.css'; - -interface Day { - name: string; - exercises: { muscleGroup: string, exercise: string | null }[]; -} - -interface Mesocycle { - name: string; - templateName: string; - days: [string, { muscleGroup: string; exercise: string | null }[]][]; -} +import { fetchMesocycles, deleteMesocycle, MesocycleData } from '../pages/api/mesocycleRoutes'; const Mesocycles: React.FC = () => { - const [mesocycles, setMesocycles] = useState([]); - const [selectedMesocycle, setSelectedMesocycle] = useState(null); + const [mesocycles, setMesocycles] = useState([]); + const [selectedMesocycle, setSelectedMesocycle] = useState(null); const [duration, setDuration] = useState(4); const [showModal, setShowModal] = useState(false); const [dropdownVisible, setDropdownVisible] = useState(null); const [setsPerExercise, setSetsPerExercise] = useState>(new Map()); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); const router = useRouter(); useEffect(() => { - const mesocyclesFromStorage = localStorage.getItem('mesocycles'); - if (mesocyclesFromStorage) { - setMesocycles(JSON.parse(mesocyclesFromStorage)); - } + const loadMesocycles = async () => { + try { + setIsLoading(true); + const fetchedMesocycles = await fetchMesocycles(); + setMesocycles(fetchedMesocycles); + } catch (error) { + setError('Failed to load mesocycles. Please try again.'); + console.error('Error fetching mesocycles:', error); + } finally { + setIsLoading(false); + } + }; + + loadMesocycles(); }, []); - const handleDeleteMesocycle = (name: string) => { - const updatedMesocycles = mesocycles.filter(mesocycle => mesocycle.name !== name); - setMesocycles(updatedMesocycles); - localStorage.setItem('mesocycles', JSON.stringify(updatedMesocycles)); + const handleDeleteMesocycle = async (name: string) => { + try { + await deleteMesocycle(name); + // Update state or refetch mesocycles + setMesocycles(prevMesocycles => prevMesocycles.filter(m => m.name !== name)); + } catch (error) { + console.error('Failed to delete mesocycle:', error); + // Display error message to the user + alert(error instanceof Error ? error.message : 'Failed to delete mesocycle'); + } }; - const handleSelectMesocycle = (mesocycle: Mesocycle) => { + const handleSelectMesocycle = (mesocycle: MesocycleData) => { setSelectedMesocycle(mesocycle); setShowModal(true); const initialSets = new Map(); mesocycle.days.forEach(day => { - day[1].forEach(exercise => { - if (exercise.exercise) { - initialSets.set(exercise.exercise, setsPerExercise.get(exercise.exercise) || 3); + day.exercises.forEach(exercise => { + if (exercise.exerciseId) { + initialSets.set(exercise.exerciseId.toString(), setsPerExercise.get(exercise.exerciseId.toString()) || 3); } }); }); @@ -57,9 +65,9 @@ const Mesocycles: React.FC = () => { const setsPerMuscleGroup: { [key: string]: number } = {}; selectedMesocycle?.days.forEach(day => { - day[1].forEach(exercise => { - if (exercise.exercise && setsPerExercise.has(exercise.exercise)) { - const sets = setsPerExercise.get(exercise.exercise) || 0; + day.exercises.forEach(exercise => { + if (exercise.exerciseId && setsPerExercise.has(exercise.exerciseId.toString())) { + const sets = setsPerExercise.get(exercise.exerciseId.toString()) || 0; setsPerMuscleGroup[exercise.muscleGroup] = (setsPerMuscleGroup[exercise.muscleGroup] || 0) + sets; } }); @@ -68,6 +76,7 @@ const Mesocycles: React.FC = () => { return setsPerMuscleGroup; }; + // TODO - Loading Mesocycles const handleLoadMesocycle = () => { if (selectedMesocycle) { localStorage.setItem('currentMesocycle', JSON.stringify({ mesocycle: selectedMesocycle, duration, setsPerExercise: Array.from(setsPerExercise.entries()) })); @@ -113,16 +122,16 @@ const Mesocycles: React.FC = () => { } }; - const handleDropdownSelect = (option: any, name: string) => { + const handleDropdownSelect = (option: { value: string }, mesocycleName: string) => { if (option.value === 'load') { - const mesocycle = mesocycles.find(m => m.name === name); + const mesocycle = mesocycles.find(m => m.name === mesocycleName); if (mesocycle) handleSelectMesocycle(mesocycle); } else if (option.value === 'delete') { - handleDeleteMesocycle(name); + handleDeleteMesocycle(mesocycleName); } setDropdownVisible(null); }; - + return (
@@ -142,7 +151,7 @@ const Mesocycles: React.FC = () => {

No saved mesocycles

- Create Your First Mesocycle + Create Your First MesocycleData
diff --git a/components/NewMesocycle.tsx b/components/NewMesocycle.tsx index 0b99c9a..2941c12 100644 --- a/components/NewMesocycle.tsx +++ b/components/NewMesocycle.tsx @@ -1,496 +1,535 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; +import axios from 'axios'; import { useRouter } from 'next/router'; import Select, { SingleValue } from 'react-select'; -import 'react-dropdown/style.css'; -import '../styles/DotDropdownMenu.css'; import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; -import { FaTrash, FaPen, FaCopy } from 'react-icons/fa'; +import { FaTrash, FaPen, FaCopy, FaEllipsisV, FaPlus, FaDumbbell } from 'react-icons/fa'; import { useExerciseOptions } from '../utils/useExerciseOptions'; -import Dropdown from 'react-dropdown'; -import '../styles/globals.css'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from '@/components/ui/dropdown-menu'; import SelectMuscleGroupModal from '../components/SelectMuscleGroupModal'; import CopyExercisesModal from '../components/CopyExercisesModal'; import SelectTemplateModal from '../components/SelectTemplateModal'; +import { fetchMesocycleTemplates, createMesocycle, MesocycleData } from '../pages/api/mesocycleRoutes'; interface Day { - name: string; - exercises: { muscleGroup: string, exercise: string | null }[]; + name: string; + exercises: { muscleGroup: string; exercise: string | null }[]; } const dayOptions = [ - { label: 'Monday', value: 'Monday' }, - { label: 'Tuesday', value: 'Tuesday' }, - { label: 'Wednesday', value: 'Wednesday' }, - { label: 'Thursday', value: 'Thursday' }, - { label: 'Friday', value: 'Friday' }, - { label: 'Saturday', value: 'Saturday' }, - { label: 'Sunday', value: 'Sunday' }, -]; - -const dropdownOptions = [ - { value: 'addDay', label: 'Add Day' }, - { value: 'selectTemplate', label: 'Select Template' }, - { value: 'resetTemplate', label: 'Reset Template' }, + { label: 'Monday', value: 'Monday' }, + { label: 'Tuesday', value: 'Tuesday' }, + { label: 'Wednesday', value: 'Wednesday' }, + { label: 'Thursday', value: 'Thursday' }, + { label: 'Friday', value: 'Friday' }, + { label: 'Saturday', value: 'Saturday' }, + { label: 'Sunday', value: 'Sunday' }, ]; const NewMesocycle: React.FC = () => { - const { exercisesByMuscle } = useExerciseOptions(); - const [templates, setTemplates] = useState<{ label: string, value: string, days: Day[] }[]>([]); - const [selectedTemplate, setSelectedTemplate] = useState<{ label: string, value: string, days: Day[] } | null>(null); - const [days, setDays] = useState([]); - const [showModal, setShowModal] = useState(false); - const [currentDayIndex, setCurrentDayIndex] = useState(null); - const [showCopyModal, setShowCopyModal] = useState<{ show: boolean, sourceDayIndex: number | null }>({ show: false, sourceDayIndex: null }); - const [copyTargetDayIndex, setCopyTargetDayIndex] = useState(null); - const [mesocycleName, setMesocycleName] = useState('New Mesocycle'); - const [isEditingName, setIsEditingName] = useState(false); - const [templateName, setTemplateName] = useState(''); - const [showDropdownModal, setShowDropdownModal] = useState(false); - const [dropdownModalContent, setDropdownModalContent] = useState(null); - const [dropdownVisible, setDropdownVisible] = useState(null); - const [tempSelectedTemplate, setTempSelectedTemplate] = useState<{ label: string, value: string, days: Day[] } | null>(null); - const router = useRouter(); - - const CustomControl = () => { - return ( -
- -
- ); - }; - - useEffect(() => { - const fetchTemplates = async () => { - try { - const response = await fetch('/data/mesocycle-templates.json'); - if (response.ok) { - const data = await response.json(); - console.log('Fetched templates:', data); - setTemplates(data); - } else { - console.error('Failed to fetch templates.json', response.statusText); - } - } catch (error) { - console.error('Error fetching templates:', error); - } - }; - fetchTemplates(); - }, []); - - useEffect(() => { - const savedTemplate = localStorage.getItem('selectedTemplate'); - const savedDays = localStorage.getItem('savedDays'); - if (savedTemplate) { - const parsedTemplate = JSON.parse(savedTemplate); - setSelectedTemplate(parsedTemplate); - setTemplateName(parsedTemplate.label); - } - if (savedDays) { - setDays(JSON.parse(savedDays)); - } - }, []); - - useEffect(() => { - if (selectedTemplate) { - localStorage.setItem('savedDays', JSON.stringify(days)); - localStorage.setItem('selectedTemplate', JSON.stringify(selectedTemplate)); - } - }, [days, selectedTemplate]); - - const handleTemplateChange = (option: SingleValue<{ label: string, value: string, days: Day[] }>) => { - if (option) { - setTempSelectedTemplate(option); - } - }; - - const handleConfirmTemplate = () => { - if (tempSelectedTemplate) { - setSelectedTemplate(tempSelectedTemplate); - setTemplateName(tempSelectedTemplate.label); - setDays(tempSelectedTemplate.days); - setShowDropdownModal(false); - } - }; - - const handleDayNameChange = (dayIndex: number, option: SingleValue<{ label: string, value: string }>) => { - const newDays = days.map((day, i) => { - if (i === dayIndex) { - return { ...day, name: option?.value || day.name }; - } - return day; - }); - setDays(newDays); - }; - - const handleAddDay = () => { - if (days.length >= 7) { - alert("You can only have up to 7 days in a mesocycle."); - return; - } - const newDay: Day = { name: `Day ${days.length + 1}`, exercises: [] }; - setDays([...days, newDay]); - }; - - const handleRemoveDay = (index: number) => { - const newDays = days.filter((_, i) => i !== index); - setDays(newDays); - }; - - const handleAddExercise = (dayIndex: number) => { - setCurrentDayIndex(dayIndex); - setShowModal(true); - }; - - const handleRemoveExercise = (dayIndex: number, exerciseIndex: number) => { - const newDays = days.map((day, i) => { - if (i === dayIndex) { - return { - ...day, - exercises: day.exercises.filter((_, j) => j !== exerciseIndex) - }; - } - return day; - }); - setDays(newDays); - }; - - const handleExerciseChange = (dayIndex: number, exerciseIndex: number, selectedExercise: SingleValue<{ value: string, label: string }>) => { - const newDays = days.map((day, i) => { - if (i === dayIndex) { - const newExercises = day.exercises.map((exercise, j) => { - if (j === exerciseIndex) { - return { ...exercise, exercise: selectedExercise ? selectedExercise.value : null }; - } - return exercise; - }); - return { ...day, exercises: newExercises }; - } - return day; - }); - setDays(newDays); - }; - - const handleCreateMesocycle = () => { - const mesocycle = new Map(days.map(day => [day.name, day.exercises])); - const savedMesocycles = localStorage.getItem('mesocycles'); - const mesocycles = savedMesocycles ? JSON.parse(savedMesocycles) : []; - mesocycles.push({ name: mesocycleName, templateName: selectedTemplate?.label, days: Array.from(mesocycle.entries()) }); - localStorage.setItem('mesocycles', JSON.stringify(mesocycles)); - alert('Mesocycle created and saved!'); - console.log("Mesocycle: ", mesocycle); - router.push('/mesocycles'); - }; - - const handleCopyExercises = (sourceDayIndex: number, destinationDayIndex: number) => { - const sourceExercises = days[sourceDayIndex].exercises; - const newDays = days.map((day, i) => { - if (i === destinationDayIndex) { - return { ...day, exercises: [...sourceExercises] }; - } - return day; - }); - setDays(newDays); - setShowCopyModal({ show: false, sourceDayIndex: null }); - setCopyTargetDayIndex(null); - }; - - const handleAddAndCopyExercises = (sourceDayIndex: number, targetDayName: string) => { - const sourceExercises = days[sourceDayIndex].exercises; - const newDay: Day = { name: targetDayName, exercises: [...sourceExercises] }; - setDays([...days, newDay]); - setShowCopyModal({ show: false, sourceDayIndex: null }); - setCopyTargetDayIndex(null); - }; - - const onDragEnd = (result: any) => { - const { source, destination } = result; - - if (!destination) { - return; - } - - if (source.droppableId === destination.droppableId && source.index === destination.index) { - return; - } - - const sourceDayIndex = parseInt(source.droppableId, 10); - const destinationDayIndex = parseInt(destination.droppableId, 10); - - const sourceExercises = Array.from(days[sourceDayIndex].exercises); - const [movedExercise] = sourceExercises.splice(source.index, 1); - - const newDays = Array.from(days); - if (destination.droppableId === 'remove-exercise') { - newDays[sourceDayIndex].exercises = sourceExercises; - } else if (sourceDayIndex === destinationDayIndex) { - sourceExercises.splice(destination.index, 0, movedExercise); - newDays[sourceDayIndex].exercises = sourceExercises; + const { exercisesByMuscle } = useExerciseOptions(); + const [templates, setTemplates] = useState<{ label: string; value: string; days: Day[] }[]>([]); + const [selectedTemplate, setSelectedTemplate] = useState<{ label: string; value: string; days: Day[] } | null>(null); + const [days, setDays] = useState([]); + const [showModal, setShowModal] = useState(false); + const [currentDayIndex, setCurrentDayIndex] = useState(null); + const [showCopyModal, setShowCopyModal] = useState<{ show: boolean; sourceDayIndex: number | null }>({ + show: false, + sourceDayIndex: null, + }); + const [copyTargetDayIndex, setCopyTargetDayIndex] = useState(null); + const [mesocycleName, setMesocycleName] = useState('New Mesocycle'); + const [isEditingName, setIsEditingName] = useState(false); + const [showTemplateModal, setShowTemplateModal] = useState(false); + const [templateName, setTemplateName] = useState(''); + const [showDropdownModal, setShowDropdownModal] = useState(false); + const [dropdownModalContent, setDropdownModalContent] = useState(null); + const [dropdownVisible, setDropdownVisible] = useState(null); + const [tempSelectedTemplate, setTempSelectedTemplate] = useState<{ label: string, value: string, days: Day[] } | null>(null); + const [tempCopyTargetDay, setTempCopyTargetDay] = useState<{ label: string; value: string } | null>(null); + const router = useRouter(); + + useEffect(() => { + const fetchTemplates = async () => { + try { + const response = await fetch('/data/mesocycle-templates.json'); // TODO: Load JSON into database + if (response.ok) { + const data = await response.json(); + console.log('Fetched templates:', data); + setTemplates(data); } else { - const destinationExercises = Array.from(days[destinationDayIndex].exercises); - destinationExercises.splice(destination.index, 0, movedExercise); - newDays[sourceDayIndex].exercises = sourceExercises; - newDays[destinationDayIndex].exercises = destinationExercises; - } - - setDays(newDays); - }; - - const getFilteredDayOptions = (currentDay: string) => { - return dayOptions.filter(dayOption => !days.some(day => day.name === dayOption.value && day.name !== currentDay)); - }; - - const handleMuscleGroupSelect = (option: SingleValue<{ value: string, label: string }>) => { - if (currentDayIndex !== null && option) { - const newDays = days.map((day, i) => { - if (i === currentDayIndex) { - return { - ...day, - exercises: [...day.exercises, { muscleGroup: option.value, exercise: null }] - }; - } - return day; - }); - setDays(newDays); - setShowModal(false); - } - }; - - const sortedMuscleGroups = Object.keys(exercisesByMuscle).sort().map(muscle => ({ value: muscle, label: muscle })); - - const handleNameEdit = () => { - setIsEditingName(true); - }; - - const handleNameChange = (e: React.ChangeEvent) => { - setMesocycleName(e.target.value); - }; - - const handleNameBlur = () => { - setIsEditingName(false); - localStorage.setItem('mesocycleName', mesocycleName); - }; - - useEffect(() => { - const savedName = localStorage.getItem('mesocycleName'); - if (savedName) { - setMesocycleName(savedName); + console.error('Failed to fetch templates.json', response.statusText); } - }, []); - - const handleOpenCopyModal = (sourceDayIndex: number) => { - setShowCopyModal({ show: true, sourceDayIndex }); + } catch (error) { + console.error('Error fetching templates:', error); + } }; + fetchTemplates(); + }, []); + + useEffect(() => { + const savedTemplate = localStorage.getItem('selectedTemplate'); + const savedDays = localStorage.getItem('savedDays'); + if (savedTemplate) { + const parsedTemplate = JSON.parse(savedTemplate); + setSelectedTemplate(parsedTemplate); + setTemplateName(parsedTemplate.label); + } + if (savedDays) { + setDays(JSON.parse(savedDays)); + } + }, []); + + useEffect(() => { + if (selectedTemplate) { + localStorage.setItem('savedDays', JSON.stringify(days)); + localStorage.setItem('selectedTemplate', JSON.stringify(selectedTemplate)); + } + }, [days, selectedTemplate]); + + const handleTemplateChange = (option: SingleValue<{ label: string, value: string, days: Day[] }>) => { + if (option) { + setTempSelectedTemplate(option); + } + }; + + const handleConfirmTemplate = () => { + if (tempSelectedTemplate) { + setSelectedTemplate(tempSelectedTemplate); + setTemplateName(tempSelectedTemplate.label); + setDays(tempSelectedTemplate.days); + setShowTemplateModal(false); + setTempSelectedTemplate(null); + } + }; + + const handleDayNameChange = (dayIndex: number, option: SingleValue<{ label: string, value: string }>) => { + const newDays = days.map((day, i) => { + if (i === dayIndex) { + return { ...day, name: option?.value || day.name }; + } + return day; + }); + setDays(newDays); + }; + + const handleAddDay = () => { + if (days.length >= 7) { + alert("You can only have up to 7 days in a mesocycle."); + return; + } + const newDay: Day = { name: `Day ${days.length + 1}`, exercises: [] }; + setDays([...days, newDay]); + }; + + const handleRemoveDay = (index: number) => { + const newDays = days.filter((_, i) => i !== index); + setDays(newDays); + }; + + const handleAddExercise = (dayIndex: number) => { + setCurrentDayIndex(dayIndex); + setShowModal(true); + }; + + const handleExerciseChange = (dayIndex: number, exerciseIndex: number, selectedExercise: SingleValue<{ value: string, label: string }>) => { + const newDays = days.map((day, i) => { + if (i === dayIndex) { + const newExercises = day.exercises.map((exercise, j) => { + if (j === exerciseIndex) { + return { ...exercise, exercise: selectedExercise ? selectedExercise.value : null }; + } + return exercise; + }); + return { ...day, exercises: newExercises }; + } + return day; + }); + setDays(newDays); + }; + + const handleCreateMesocycle = async () => { + const hasEmptyExercises = days.some(day => + day.exercises.some(exercise => !exercise.exercise) + ); - const handleSelectCopyTargetDay = (option: SingleValue<{ label: string, value: string }>) => { - const targetDayIndex = days.findIndex(day => day.name === option?.value); - if (targetDayIndex !== -1) { - setCopyTargetDayIndex(targetDayIndex); + if (hasEmptyExercises) { + alert("Please fill in all exercise fields before creating the mesocycle."); + return; + } + + try { + const mesocycleData: MesocycleData = { + name: mesocycleName, + templateName: selectedTemplate?.label ?? null, + days: days.map(day => ({ + name: day.name, + exercises: day.exercises.map(exercise => { + const muscleExercises = exercisesByMuscle[exercise.muscleGroup] || []; + const foundExercise = muscleExercises.find(e => e.name === exercise.exercise); + + return { + muscleGroup: exercise.muscleGroup, + exerciseId: foundExercise?.id ?? null, + }; + }), + })), + }; + + console.log('Sending mesocycle data:', mesocycleData); + const createdMesocycle = await createMesocycle(mesocycleData); + console.log('Mesocycle created:', createdMesocycle); + router.push('/mesocycles'); + } catch (error: unknown) { + if (error instanceof Error) { + console.error('Failed to create mesocycle:', error.message); + + if (axios.isAxiosError(error) && error.response) { + console.error('Error response:', error.response.data); + alert(`An error occurred: ${error.response.data.error || 'Unknown error'}`); } else { - setCopyTargetDayIndex(null); - if (showCopyModal.sourceDayIndex !== null && option) { - handleAddAndCopyExercises(showCopyModal.sourceDayIndex, option.value); - } + alert(`An error occurred: ${error.message}`); } - }; - - const handleConfirmCopy = () => { - if (showCopyModal.sourceDayIndex !== null && copyTargetDayIndex !== null) { - handleCopyExercises(showCopyModal.sourceDayIndex, copyTargetDayIndex); - } - }; - - const resetTemplate = () => { - const newDays = days.map(day => ({ + } else { + console.error('An unknown error occurred'); + alert('An unknown error occurred. Please try again.'); + } + } + }; + + const handleCopyExercises = (sourceDayIndex: number, destinationDayIndex: number) => { + const sourceExercises = days[sourceDayIndex].exercises; + const newDays = days.map((day, i) => { + if (i === destinationDayIndex) { + return { ...day, exercises: [...sourceExercises] }; + } + return day; + }); + setDays(newDays); + setShowCopyModal({ show: false, sourceDayIndex: null }); + setCopyTargetDayIndex(null); + }; + + const handleAddAndCopyExercises = (sourceDayIndex: number, targetDayName: string) => { + const sourceExercises = days[sourceDayIndex].exercises; + const newDay: Day = { name: targetDayName, exercises: [...sourceExercises] }; + setDays([...days, newDay]); + setShowCopyModal({ show: false, sourceDayIndex: null }); + setCopyTargetDayIndex(null); + }; + + const onDragEnd = (result: any) => { + const { source, destination } = result; + + if (!destination) { + return; + } + + if (source.droppableId === destination.droppableId && source.index === destination.index) { + return; + } + + const sourceDayIndex = parseInt(source.droppableId, 10); + const destinationDayIndex = parseInt(destination.droppableId, 10); + + const sourceExercises = Array.from(days[sourceDayIndex].exercises); + const [movedExercise] = sourceExercises.splice(source.index, 1); + + const newDays = Array.from(days); + if (destination.droppableId === 'remove-exercise') { + newDays[sourceDayIndex].exercises = sourceExercises; + } else if (sourceDayIndex === destinationDayIndex) { + sourceExercises.splice(destination.index, 0, movedExercise); + newDays[sourceDayIndex].exercises = sourceExercises; + } else { + const destinationExercises = Array.from(days[destinationDayIndex].exercises); + destinationExercises.splice(destination.index, 0, movedExercise); + newDays[sourceDayIndex].exercises = sourceExercises; + newDays[destinationDayIndex].exercises = destinationExercises; + } + + setDays(newDays); + }; + + const getFilteredDayOptions = (currentDay: string) => { + return dayOptions.filter(dayOption => !days.some(day => day.name === dayOption.value && day.name !== currentDay)); + }; + + const handleMuscleGroupSelect = (option: SingleValue<{ value: string, label: string }>) => { + if (currentDayIndex !== null && option) { + const newDays = days.map((day, i) => { + if (i === currentDayIndex) { + return { ...day, - exercises: day.exercises.map(exercise => ({ ...exercise, exercise: null })) - })); - setDays(newDays); - setSelectedTemplate(null); - setTemplateName(''); - setDropdownModalContent(null); - }; - - const handleDropdownSelect = (option: any) => { - setDropdownVisible(null); - switch (option?.value) { - case 'addDay': - handleAddDay(); - break; - case 'selectTemplate': - setDropdownModalContent('selectTemplate'); - setShowDropdownModal(true); - break; - case 'resetTemplate': - resetTemplate(); - break; - default: - break; + exercises: [...day.exercises, { muscleGroup: option.value, exercise: null }] + }; } - }; + return day; + }); + setDays(newDays); + setShowModal(false); + } + }; + + const sortedMuscleGroups = Object.keys(exercisesByMuscle).sort().map(muscle => ({ value: muscle, label: muscle })); + + useEffect(() => { + const savedName = localStorage.getItem('mesocycleName'); + if (savedName) { + setMesocycleName(savedName); + } + }, []); + + const handleOpenCopyModal = (sourceDayIndex: number) => { + setShowCopyModal({ show: true, sourceDayIndex }); + }; + + const handleSelectCopyTargetDay = (option: SingleValue<{ label: string, value: string }>) => { + if (option) { + setTempCopyTargetDay(option); + } + }; + + const handleConfirmCopy = () => { + if (showCopyModal.sourceDayIndex !== null && tempCopyTargetDay) { + const targetDayIndex = days.findIndex(day => day.name === tempCopyTargetDay.value); + if (targetDayIndex !== -1) { + handleCopyExercises(showCopyModal.sourceDayIndex, targetDayIndex); + } else { + handleAddAndCopyExercises(showCopyModal.sourceDayIndex, tempCopyTargetDay.value); + } + setShowCopyModal({ show: false, sourceDayIndex: null }); + setTempCopyTargetDay(null); + } + }; + + const resetTemplate = () => { + const newDays = days.map(day => ({ + ...day, + exercises: day.exercises.map(exercise => ({ ...exercise, exercise: null })) + })); + setDays(newDays); + setSelectedTemplate(null); + setTemplateName(''); + setDropdownModalContent(null); + }; + + const handleDropdownSelect = (option: any) => { + setDropdownVisible(null); + switch (option?.value) { + case 'addDay': + handleAddDay(); + break; + case 'selectTemplate': + setDropdownModalContent('selectTemplate'); + setShowDropdownModal(true); + break; + case 'resetTemplate': + resetTemplate(); + break; + default: + break; + } + }; + return ( +
+
+
+
+ {isEditingName ? ( + setMesocycleName(e.target.value)} + onBlur={() => setIsEditingName(false)} + autoFocus + className="text-3xl font-bold text-gray-800" + /> + ) : ( +

+ {mesocycleName} + setIsEditingName(true)} + /> +

+ )} +
+ + + + + + + Add Day + + setShowTemplateModal(true)}> + Select Template + + + Reset Template + + + +
- return ( -
-
-
- {isEditingName ? ( - +
+ {days.map((day, dayIndex) => ( + + {(provided) => ( + +
+
+ null, - DropdownIndicator: () => null - }} - options={getFilteredDayOptions(day.name)} - onChange={(option) => handleDayNameChange(dayIndex, option)} - value={dayOptions.find(d => d.value === day.name)} - className="text-black w-2/3" - classNamePrefix="react-select" - /> - - -
- {day.exercises.map((exercise, exerciseIndex) => ( - - {(provided) => ( -
-
- {exercise.muscleGroup.toUpperCase()} -
- ({ + value: ex.name, + label: ex.name, + })) || [] + } + onChange={(option) => handleExerciseChange(dayIndex, exerciseIndex, option)} + placeholder="Select exercise" + className="text-black mt-2" + classNamePrefix="react-select" + value={ + exercise.exercise + ? { value: exercise.exercise, label: exercise.exercise } + : null + } + /> +
+ )} +
+ ))} + {provided.placeholder}
-
- - - setShowModal(false)} - onSelect={handleMuscleGroupSelect} - muscleGroups={sortedMuscleGroups} - /> - - setShowCopyModal({ show: false, sourceDayIndex: null })} - onCopy={handleConfirmCopy} - onSelectTargetDay={handleSelectCopyTargetDay} - dayOptions={dayOptions.filter(day => !days.some(d => d.name === day.value))} - selectedDay={ - copyTargetDayIndex !== null - ? { label: days[copyTargetDayIndex].name, value: days[copyTargetDayIndex].name } - : null - } - /> - - setShowDropdownModal(false)} - templates={templates} - onSelectTemplate={handleTemplateChange} - selectedTemplate={tempSelectedTemplate} - onConfirm={handleConfirmTemplate} - /> - -
- -
+
+ +
+ + )} + + ))} +
+ + + {days.length === 0 && ( +
+ +

No days added to your mesocycle

+ +
+ )} + +
+
- ); +
+ + setShowModal(false)} + onSelect={handleMuscleGroupSelect} + muscleGroups={sortedMuscleGroups} + /> + + { + setShowTemplateModal(false); + setTempSelectedTemplate(null); + }} + templates={templates} + onSelectTemplate={handleTemplateChange} + selectedTemplate={tempSelectedTemplate} + onConfirm={handleConfirmTemplate} + /> + + { + setShowCopyModal({ show: false, sourceDayIndex: null }); + setTempCopyTargetDay(null); + }} + onCopy={handleConfirmCopy} + onSelectTargetDay={handleSelectCopyTargetDay} + dayOptions={dayOptions.filter((day) => !days.some((d) => d.name === day.value))} + selectedDay={tempCopyTargetDay} + /> +
+ ); }; -export default NewMesocycle; \ No newline at end of file +export default NewMesocycle; diff --git a/components/SectionWrapper.tsx b/components/SectionWrapper.tsx index 0ef04ff..ba164e6 100644 --- a/components/SectionWrapper.tsx +++ b/components/SectionWrapper.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import { ReactNode } from 'react'; interface SectionWrapperProps { children: ReactNode; diff --git a/components/SidebarLayout.tsx b/components/SidebarLayout.tsx index 081ec4a..9b287a8 100644 --- a/components/SidebarLayout.tsx +++ b/components/SidebarLayout.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import Sidebar from './Sidebar'; interface SidebarLayoutProps { diff --git a/components/SignInPage.tsx b/components/SignInPage.tsx index 81b5d44..831c3e8 100644 --- a/components/SignInPage.tsx +++ b/components/SignInPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, FormEvent } from 'react'; +import { useState, FormEvent } from 'react'; import { signIn } from 'next-auth/react'; import { useRouter } from 'next/router'; import Head from 'next/head'; diff --git a/components/SignUpPage.tsx b/components/SignUpPage.tsx index 38fc762..d66ac45 100644 --- a/components/SignUpPage.tsx +++ b/components/SignUpPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, FormEvent } from 'react'; +import { useState, FormEvent } from 'react'; import { useRouter } from 'next/router'; import Head from 'next/head'; import Link from 'next/link'; diff --git a/components/Stopwatch.tsx b/components/Stopwatch.tsx index 92d6b56..07c69e6 100644 --- a/components/Stopwatch.tsx +++ b/components/Stopwatch.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; const Stopwatch: React.FC = () => { const [isRunning, setIsRunning] = useState(false); diff --git a/components/withAuth.tsx b/components/withAuth.tsx index cb8bc9b..dd4e533 100644 --- a/components/withAuth.tsx +++ b/components/withAuth.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { useSession } from 'next-auth/react'; import { useRouter } from 'next/router'; diff --git a/package-lock.json b/package-lock.json index 6f9e8ad..e99cee8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,15 +14,19 @@ "@fortawesome/react-fontawesome": "^0.2.2", "@next-auth/prisma-adapter": "^1.0.7", "@prisma/client": "^5.18.0", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-slot": "^1.1.0", "axios": "^1.7.4", "bcrypt": "^5.1.1", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", "fast-glob": "^3.3.2", "lucide-react": "^0.263.1", "micromatch": "^4.0.7", "next": "^14.2.4", "next-auth": "^4.24.7", "next-transpile-modules": "^10.0.1", - "prisma": "^5.18.0", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", "react-calendar": "^5.0.0", @@ -32,6 +36,8 @@ "react-modal": "^3.16.1", "react-select": "^5.8.0", "styled-components": "^6.1.11", + "tailwind-merge": "^2.5.2", + "tailwindcss-animate": "^1.0.7", "typescript": "^5.5.2" }, "devDependencies": { @@ -47,6 +53,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "postcss": "^8.4.39", "postcss-loader": "^8.1.1", + "prisma": "^5.19.1", "style-loader": "^4.0.0", "tailwindcss": "^3.4.10", "ts-node": "^10.9.2" @@ -56,7 +63,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -345,7 +351,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -358,7 +364,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -567,6 +573,19 @@ "@floating-ui/utils": "^0.2.4" } }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", + "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@floating-ui/utils": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz", @@ -673,7 +692,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -691,7 +709,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -704,7 +721,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -1004,7 +1020,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -1024,55 +1039,583 @@ "prisma": "*" }, "peerDependenciesMeta": { - "prisma": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.19.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.19.1.tgz", + "integrity": "sha512-lAG6A6QnG2AskAukIEucYJZxxcSqKsMK74ZFVfCTOM/7UiyJQi48v6TQ47d6qKG3LbMslqOvnTX25dj/qvclGg==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.19.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.19.1.tgz", + "integrity": "sha512-kR/PoxZDrfUmbbXqqb8SlBBgCjvGaJYMCOe189PEYzq9rKqitQ2fvT/VJ8PDSe8tTNxhc2KzsCfCAL+Iwm/7Cg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.19.1", + "@prisma/engines-version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3", + "@prisma/fetch-engine": "5.19.1", + "@prisma/get-platform": "5.19.1" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3.tgz", + "integrity": "sha512-xR6rt+z5LnNqTP5BBc+8+ySgf4WNMimOKXRn6xfNRDSpHvbOEmd7+qAOmzCrddEc4Cp8nFC0txU14dstjH7FXA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.19.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.19.1.tgz", + "integrity": "sha512-pCq74rtlOVJfn4pLmdJj+eI4P7w2dugOnnTXpRilP/6n5b2aZiA4ulJlE0ddCbTPkfHmOL9BfaRgA8o+1rfdHw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.19.1", + "@prisma/engines-version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3", + "@prisma/get-platform": "5.19.1" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.19.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.19.1.tgz", + "integrity": "sha512-sCeoJ+7yt0UjnR+AXZL7vXlg5eNxaFOwC23h0KvW1YIXUoa7+W2ZcAUhoEQBmJTW4GrFqCuZ8YSP0mkDa4k3Zg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.19.1" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", + "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", + "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", + "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.1.tgz", + "integrity": "sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", + "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", + "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz", + "integrity": "sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x" + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.1.tgz", + "integrity": "sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", + "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", + "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", + "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", + "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { "optional": true } } }, - "node_modules/@prisma/debug": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.18.0.tgz", - "integrity": "sha512-f+ZvpTLidSo3LMJxQPVgAxdAjzv5OpzAo/eF8qZqbwvgi2F5cTOI9XCpdRzJYA0iGfajjwjOKKrVq64vkxEfUw==", - "license": "Apache-2.0" - }, - "node_modules/@prisma/engines": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.18.0.tgz", - "integrity": "sha512-ofmpGLeJ2q2P0wa/XaEgTnX/IsLnvSp/gZts0zjgLNdBhfuj2lowOOPmDcfKljLQUXMvAek3lw5T01kHmCG8rg==", - "hasInstallScript": true, - "license": "Apache-2.0", + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "license": "MIT", "dependencies": { - "@prisma/debug": "5.18.0", - "@prisma/engines-version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169", - "@prisma/fetch-engine": "5.18.0", - "@prisma/get-platform": "5.18.0" + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@prisma/engines-version": { - "version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169.tgz", - "integrity": "sha512-a/+LpJj8vYU3nmtkg+N3X51ddbt35yYrRe8wqHTJtYQt7l1f8kjIBcCs6sHJvodW/EK5XGvboOiwm47fmNrbgg==", - "license": "Apache-2.0" - }, - "node_modules/@prisma/fetch-engine": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.18.0.tgz", - "integrity": "sha512-I/3u0x2n31rGaAuBRx2YK4eB7R/1zCuayo2DGwSpGyrJWsZesrV7QVw7ND0/Suxeo/vLkJ5OwuBqHoCxvTHpOg==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "license": "MIT", "dependencies": { - "@prisma/debug": "5.18.0", - "@prisma/engines-version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169", - "@prisma/get-platform": "5.18.0" + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@prisma/get-platform": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.18.0.tgz", - "integrity": "sha512-Tk+m7+uhqcKDgnMnFN0lRiH7Ewea0OEsZZs9pqXa7i3+7svS3FSCqDBCaM9x5fmhhkufiG0BtunJVDka+46DlA==", - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "5.18.0" - } + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "license": "MIT" }, "node_modules/@swc/counter": { "version": "0.1.3", @@ -1094,28 +1637,28 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/bcrypt": { @@ -1182,7 +1725,7 @@ "version": "20.14.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -1224,7 +1767,7 @@ "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/react": "*" @@ -1485,7 +2028,7 @@ "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1519,7 +2062,7 @@ "version": "8.3.3", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -1581,7 +2124,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1597,14 +2139,12 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -1638,7 +2178,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -1648,6 +2187,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", @@ -1924,7 +2475,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2040,7 +2590,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -2096,7 +2645,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -2121,7 +2669,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -2150,6 +2697,27 @@ "node": ">=6.0" } }, + "node_modules/class-variance-authority": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", + "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "2.0.0" + }, + "funding": { + "url": "https://joebell.co.uk" + } + }, + "node_modules/class-variance-authority/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -2175,7 +2743,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2188,7 +2755,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/color-support": { @@ -2216,7 +2782,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -2280,14 +2845,13 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2367,7 +2931,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -2520,18 +3083,23 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, "license": "Apache-2.0" }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -2541,7 +3109,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, "license": "MIT" }, "node_modules/doctrine": { @@ -2571,7 +3138,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { @@ -2585,7 +3151,6 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/enhanced-resolve": { @@ -3232,7 +3797,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -3307,7 +3871,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -3423,6 +3986,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -3457,7 +4029,6 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -3478,7 +4049,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -3499,7 +4069,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -3509,7 +4078,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -3776,6 +4344,15 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -3832,7 +4409,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -4186,7 +4762,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -4207,7 +4782,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -4256,7 +4830,6 @@ "version": "1.21.6", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", - "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -4373,7 +4946,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -4435,7 +5007,6 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, "license": "ISC" }, "node_modules/lucide-react": { @@ -4475,7 +5046,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/map-age-cleaner": { @@ -4588,7 +5159,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -4641,7 +5211,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -4849,7 +5418,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4897,7 +5465,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -5132,7 +5699,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -5188,7 +5754,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5204,7 +5769,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -5248,7 +5812,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5258,7 +5821,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -5278,7 +5840,6 @@ "version": "8.4.39", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -5307,7 +5868,6 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -5325,7 +5885,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -5345,7 +5904,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -5381,7 +5939,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -5394,7 +5951,6 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -5502,7 +6058,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -5528,7 +6083,6 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -5583,19 +6137,23 @@ "license": "MIT" }, "node_modules/prisma": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.18.0.tgz", - "integrity": "sha512-+TrSIxZsh64OPOmaSgVPH7ALL9dfU0jceYaMJXsNrTkFHO7/3RANi5K2ZiPB1De9+KDxCWn7jvRq8y8pvk+o9g==", + "version": "5.19.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.19.1.tgz", + "integrity": "sha512-c5K9MiDaa+VAAyh1OiYk76PXOme9s3E992D7kvvIOhCrNsBQfy2mP2QAQtX0WNj140IgG++12kwZpYB9iIydNQ==", + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/engines": "5.18.0" + "@prisma/engines": "5.19.1" }, "bin": { "prisma": "build/index.js" }, "engines": { "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" } }, "node_modules/prop-types": { @@ -5815,6 +6373,53 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", + "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.4", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-select": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz", @@ -5842,6 +6447,29 @@ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", "license": "MIT" }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -5862,7 +6490,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -5886,7 +6513,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -6206,7 +6832,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -6219,7 +6844,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6248,7 +6872,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -6319,7 +6942,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -6338,7 +6960,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6353,14 +6974,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/string-width/node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -6373,7 +6992,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -6481,7 +7099,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6621,7 +7238,6 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -6665,11 +7281,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz", + "integrity": "sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.4.10", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", - "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -6703,6 +7328,15 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -6813,7 +7447,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -6823,7 +7456,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -6869,14 +7501,13 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, "license": "Apache-2.0" }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -6920,7 +7551,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tslib": { @@ -7065,7 +7696,7 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/update-browserslist-db": { @@ -7109,6 +7740,27 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", + "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-isomorphic-layout-effect": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", @@ -7132,6 +7784,28 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7151,7 +7825,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/warning": { @@ -7284,7 +7958,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -7422,7 +8095,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -7441,7 +8113,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -7459,14 +8130,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -7481,7 +8150,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7494,7 +8162,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7507,7 +8174,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -7544,7 +8210,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" diff --git a/package.json b/package.json index 1f6c456..9f8d51f 100644 --- a/package.json +++ b/package.json @@ -17,15 +17,19 @@ "@fortawesome/react-fontawesome": "^0.2.2", "@next-auth/prisma-adapter": "^1.0.7", "@prisma/client": "^5.18.0", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-slot": "^1.1.0", "axios": "^1.7.4", "bcrypt": "^5.1.1", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", "fast-glob": "^3.3.2", "lucide-react": "^0.263.1", "micromatch": "^4.0.7", "next": "^14.2.4", "next-auth": "^4.24.7", "next-transpile-modules": "^10.0.1", - "prisma": "^5.18.0", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", "react-calendar": "^5.0.0", @@ -35,6 +39,8 @@ "react-modal": "^3.16.1", "react-select": "^5.8.0", "styled-components": "^6.1.11", + "tailwind-merge": "^2.5.2", + "tailwindcss-animate": "^1.0.7", "typescript": "^5.5.2" }, "devDependencies": { @@ -50,6 +56,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "postcss": "^8.4.39", "postcss-loader": "^8.1.1", + "prisma": "^5.19.1", "style-loader": "^4.0.0", "tailwindcss": "^3.4.10", "ts-node": "^10.9.2" diff --git a/pages/_app.tsx b/pages/_app.tsx index 4c53923..9ec22f5 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -4,19 +4,23 @@ import '../styles/globals.css'; import SidebarLayout from '../components/SidebarLayout'; import Layout from '../components/Layout' import Head from 'next/head'; +import FullscreenToggle from '../components/FullscreenToggle'; function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) { return ( - - - - - - - - - - +
+ + + + + + + + + + + +
); } diff --git a/pages/api/admin/load-exercises.ts b/pages/api/admin/load-exercises.ts index c1c9a02..c616026 100644 --- a/pages/api/admin/load-exercises.ts +++ b/pages/api/admin/load-exercises.ts @@ -52,7 +52,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const session = await getServerSession(req, res, authOptions); if (!session || session.user?.email !== 'admin@email.com') { - return res.status(401).json({ message: 'Unauthorized' }); + return res.status(401).json({ message: 'Unauthorised' }); } await loadExercises(); diff --git a/pages/api/custom-exercises/[id].ts b/pages/api/custom-exercises/[id].ts index 8d87747..47000dc 100644 --- a/pages/api/custom-exercises/[id].ts +++ b/pages/api/custom-exercises/[id].ts @@ -13,7 +13,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (!session || !session.user?.id) { console.log('No session found or user ID missing, returning 401'); - return res.status(401).json({ error: 'Unauthorized' }); + return res.status(401).json({ error: 'Unauthorised' }); } const userId = session.user.id; diff --git a/pages/api/custom-exercises/index.ts b/pages/api/custom-exercises/index.ts index 9e0f830..c053294 100644 --- a/pages/api/custom-exercises/index.ts +++ b/pages/api/custom-exercises/index.ts @@ -15,7 +15,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (!session || !session.user?.id) { console.log('No session found or user ID missing, returning 401'); - return res.status(401).json({ error: 'Unauthorized' }); + return res.status(401).json({ error: 'Unauthorised' }); } const userId = session.user.id; diff --git a/pages/api/mesocycle-templates/index.ts b/pages/api/mesocycle-templates/index.ts new file mode 100644 index 0000000..17740a9 --- /dev/null +++ b/pages/api/mesocycle-templates/index.ts @@ -0,0 +1,27 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import prisma from '../../../lib/prisma'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === 'GET') { + try { + const templates = await prisma.mesocycleTemplate.findMany({ + include: { + days: { + include: { + exercises: true, + }, + orderBy: { + order: 'asc', + }, + }, + }, + }); + res.status(200).json(templates); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch mesocycle templates' }); + } + } else { + res.setHeader('Allow', ['GET']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } +} diff --git a/pages/api/mesocycleRoutes.ts b/pages/api/mesocycleRoutes.ts new file mode 100644 index 0000000..0c53d5e --- /dev/null +++ b/pages/api/mesocycleRoutes.ts @@ -0,0 +1,74 @@ +import axios from 'axios'; + +const api = axios.create({ + baseURL: '/api', + withCredentials: true, +}); + +export interface MesocycleData { + name: string; + templateName: string | null; + days: Array<{ + name: string; + exercises: Array<{ + muscleGroup: string; + exerciseId: number | null; + }>; + }>; +} + +export const fetchMesocycleTemplates = async () => { + try { + const response = await api.get('/mesocycle-templates'); + return response.data; + } catch (error) { + console.error('Error fetching mesocycle templates:', error); + throw error; + } +}; + +export const fetchMesocycles = async () => { + try { + const response = await api.get('/mesocycles'); + return response.data; + } catch (error) { + console.error('Error fetching mesocycles:', error); + throw error; + } +}; + +export const createMesocycle = async (mesocycleData: MesocycleData) => { + try { + const response = await api.post('/mesocycles', mesocycleData); + return response.data; + } catch (error) { + console.error('Error creating mesocycle:', error); + throw error; + } +}; + +export const updateMesocycle = async (id: number, mesocycleData: MesocycleData) => { + try { + const response = await api.put(`/mesocycles/${id}`, mesocycleData); + return response.data; + } catch (error) { + console.error('Error updating mesocycle:', error); + throw error; + } +}; + +export const deleteMesocycle = async (name: string): Promise => { + try { + console.log(`Attempting to delete mesocycle: ${name}`); + await api.delete(`/mesocycles/${encodeURIComponent(name)}`); + console.log('Mesocycle deleted successfully'); + } catch (error) { + console.error('Error deleting mesocycle:', error); + if (axios.isAxiosError(error) && error.response) { + console.error('Error response:', error.response?.data); + console.error('Error status:', error.response?.status); + throw new Error(error.response?.data?.error || 'Failed to delete mesocycle'); + } + throw error; + } +}; diff --git a/pages/api/mesocycles/[name].ts b/pages/api/mesocycles/[name].ts new file mode 100644 index 0000000..46c89f3 --- /dev/null +++ b/pages/api/mesocycles/[name].ts @@ -0,0 +1,88 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "../auth/[...nextauth]"; +import prisma from '../../../lib/prisma'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const session = await getServerSession(req, res, authOptions); + if (!session || !session.user) { + return res.status(401).json({ error: 'Unauthorised' }); + } + + const { name } = req.query; + + if (typeof name !== 'string') { + return res.status(400).json({ error: 'Invalid mesocycle name' }); + } + + if (req.method === 'PUT') { + try { + const { name: newName, templateName, days } = req.body; + const updatedMesocycle = await prisma.mesocycle.update({ + where: { + name_userId: { + name: name, + userId: session.user.id + } + }, + data: { + name: newName, + templateName, + days: { + deleteMany: {}, + create: days.map((day: any, index: number) => ({ + name: day.name, + order: index, + exercises: { + create: day.exercises.map((exercise: any, exerciseIndex: number) => ({ + muscleGroup: exercise.muscleGroup, + exerciseId: exercise.exerciseId, + order: exerciseIndex, + })), + }, + })), + }, + }, + include: { + days: { + include: { + exercises: true, + }, + orderBy: { + order: 'asc', + }, + }, + }, + }); + res.status(200).json(updatedMesocycle); + } catch (error) { + console.error('Error updating mesocycle:', error); + res.status(500).json({ error: 'Failed to update mesocycle', details: error instanceof Error ? error.message : 'Unknown error' }); + } + } else if (req.method === 'DELETE') { + try { + const deletedMesocycle = await prisma.mesocycle.delete({ + where: { + name_userId: { + name: name, + userId: session.user.id + } + }, + }); + + res.status(204).end(); + } catch (error) { + console.error('Error in DELETE /mesocycles/:name:', error); + if (error instanceof Error && error.name === 'PrismaClientKnownRequestError' && (error as any).code === 'P2025') { + return res.status(404).json({ error: 'Mesocycle not found' }); + } + res.status(500).json({ + error: 'An error occurred while deleting the mesocycle', + details: error instanceof Error ? error.message : 'Unknown error' + }); + } + } else { + res.setHeader('Allow', ['PUT', 'DELETE']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } +} diff --git a/pages/api/mesocycles.ts b/pages/api/mesocycles/index.ts similarity index 67% rename from pages/api/mesocycles.ts rename to pages/api/mesocycles/index.ts index dbbeeb0..17bd158 100644 --- a/pages/api/mesocycles.ts +++ b/pages/api/mesocycles/index.ts @@ -1,40 +1,26 @@ import { NextApiRequest, NextApiResponse } from 'next'; -import prisma from '../../lib/prisma'; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "../auth/[...nextauth]"; +import prisma from '../../../lib/prisma'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method === 'GET') { - try { - const mesocycles = await prisma.mesocycle.findMany({ - include: { - days: { - include: { - exercises: { - include: { - exercise: true, - }, - }, - }, - orderBy: { - order: 'asc', - }, - }, - }, - }); - res.status(200).json(mesocycles); - } catch (error) { - res.status(500).json({ error: 'Failed to fetch mesocycles' }); - } - } else if (req.method === 'POST') { + const session = await getServerSession(req, res, authOptions); + if (!session || !session.user) { + return res.status(401).json({ error: 'Unauthorised' }); + } + + if (req.method === 'POST') { try { const { name, templateName, days } = req.body; const mesocycle = await prisma.mesocycle.create({ data: { name, templateName, + userId: session.user.id, days: { - create: days.map((day: any, dayIndex: number) => ({ + create: days.map((day: any, index: number) => ({ name: day.name, - order: dayIndex, + order: index, exercises: { create: day.exercises.map((exercise: any, exerciseIndex: number) => ({ muscleGroup: exercise.muscleGroup, @@ -48,11 +34,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) include: { days: { include: { - exercises: { - include: { - exercise: true, - }, - }, + exercises: true, }, }, }, @@ -61,8 +43,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } catch (error) { res.status(500).json({ error: 'Failed to create mesocycle' }); } + } else if (req.method === 'GET') { + try { + const mesocycles = await prisma.mesocycle.findMany({ + where: { userId: session.user.id }, + include: { + days: { + include: { + exercises: true, + }, + }, + }, + }); + res.status(200).json(mesocycles); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch mesocycles' }); + } } else { - res.setHeader('Allow', ['GET', 'POST']); + res.setHeader('Allow', ['POST', 'GET']); res.status(405).end(`Method ${req.method} Not Allowed`); } -} \ No newline at end of file +} diff --git a/pages/api/workouts/[date].ts b/pages/api/workouts/[date].ts index a44b74a..7794b23 100644 --- a/pages/api/workouts/[date].ts +++ b/pages/api/workouts/[date].ts @@ -13,7 +13,7 @@ export default async function handler( const session = await getServerSession(req, res, authOptions) if (!session || !session.user) { - return res.status(401).json({ message: 'Unauthorized' }) + return res.status(401).json({ message: 'Unauthorised' }) } const { date } = req.query diff --git a/prisma/migrations/20240913105431_mesocycle_cascade_deletes/migration.sql b/prisma/migrations/20240913105431_mesocycle_cascade_deletes/migration.sql new file mode 100644 index 0000000..2d21bab --- /dev/null +++ b/prisma/migrations/20240913105431_mesocycle_cascade_deletes/migration.sql @@ -0,0 +1,40 @@ +/* + Warnings: + + - You are about to drop the `MesocycleExercise` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "Day" DROP CONSTRAINT "Day_mesocycleId_fkey"; + +-- DropForeignKey +ALTER TABLE "MesocycleExercise" DROP CONSTRAINT "MesocycleExercise_dayId_fkey"; + +-- DropForeignKey +ALTER TABLE "MesocycleExercise" DROP CONSTRAINT "MesocycleExercise_exerciseId_fkey"; + +-- DropTable +DROP TABLE "MesocycleExercise"; + +-- CreateTable +CREATE TABLE "DayExercise" ( + "id" SERIAL NOT NULL, + "dayId" INTEGER NOT NULL, + "exerciseId" INTEGER NOT NULL, + "muscleGroup" TEXT NOT NULL, + "order" INTEGER NOT NULL, + + CONSTRAINT "DayExercise_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "DayExercise_dayId_order_key" ON "DayExercise"("dayId", "order"); + +-- AddForeignKey +ALTER TABLE "Day" ADD CONSTRAINT "Day_mesocycleId_fkey" FOREIGN KEY ("mesocycleId") REFERENCES "Mesocycle"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DayExercise" ADD CONSTRAINT "DayExercise_dayId_fkey" FOREIGN KEY ("dayId") REFERENCES "Day"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DayExercise" ADD CONSTRAINT "DayExercise_exerciseId_fkey" FOREIGN KEY ("exerciseId") REFERENCES "Exercise"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20240913110530_mesocycle_name_user_id_combo/migration.sql b/prisma/migrations/20240913110530_mesocycle_name_user_id_combo/migration.sql new file mode 100644 index 0000000..8a7c70c --- /dev/null +++ b/prisma/migrations/20240913110530_mesocycle_name_user_id_combo/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[name,userId]` on the table `Mesocycle` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "Mesocycle_name_userId_key" ON "Mesocycle"("name", "userId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 320d1df..d5734e3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -69,7 +69,7 @@ model Exercise { category String? images String[] workoutExercises WorkoutExercise[] - mesocycleExercises MesocycleExercise[] + dayExercises DayExercise[] } model CustomExercise { @@ -149,24 +149,26 @@ model Mesocycle { days Day[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - userId String // Changed from Int to String + userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([name, userId]) // use a combination of Mesocycle name and userID for Mesocycles } model Day { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) name String - mesocycle Mesocycle @relation(fields: [mesocycleId], references: [id]) + mesocycle Mesocycle @relation(fields: [mesocycleId], references: [id], onDelete: Cascade) mesocycleId Int order Int - exercises MesocycleExercise[] + exercises DayExercise[] @@unique([mesocycleId, order]) } -model MesocycleExercise { +model DayExercise { id Int @id @default(autoincrement()) - day Day @relation(fields: [dayId], references: [id]) + day Day @relation(fields: [dayId], references: [id], onDelete: Cascade) dayId Int exercise Exercise @relation(fields: [exerciseId], references: [id]) exerciseId Int @@ -174,4 +176,4 @@ model MesocycleExercise { order Int @@unique([dayId, order]) -} \ No newline at end of file +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..0270f64 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..77e9fb7 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..0e4dccf --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,203 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..a92b8e0 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/styles/globals.css b/styles/globals.css index ce02f63..d17ad24 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -217,3 +217,68 @@ body { padding-right: 8px; } +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + diff --git a/tailwind.config.js b/tailwind.config.js index f663c76..e9a0c4b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,11 +1,62 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: [ + darkMode: ['class'], + content: [ './pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', ], theme: { - extend: {}, + extend: { + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + } + } }, - plugins: [], + plugins: [require("tailwindcss-animate")], }; \ No newline at end of file