diff --git a/ui/Console.tsx b/components/ui/Console.tsx
similarity index 100%
rename from ui/Console.tsx
rename to components/ui/Console.tsx
diff --git a/ui/Dialog.tsx b/components/ui/Dialog.tsx
similarity index 92%
rename from ui/Dialog.tsx
rename to components/ui/Dialog.tsx
index eff2215..2fd3084 100644
--- a/ui/Dialog.tsx
+++ b/components/ui/Dialog.tsx
@@ -1,7 +1,7 @@
import { cn } from '@/lib/utils';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import type { ReactNode } from 'react';
-import Icon from './Icon';
+import { Icon, Icons } from '@/components/ui/Icon';
// import Tooltip from './Tooltip';
type DialogProps = {
@@ -42,7 +42,7 @@ export default function Dialog({
{title}
-
+
)}
diff --git a/ui/ErrorMessage.tsx b/components/ui/ErrorMessage.tsx
similarity index 100%
rename from ui/ErrorMessage.tsx
rename to components/ui/ErrorMessage.tsx
diff --git a/components/ui/Form.tsx b/components/ui/Form.tsx
new file mode 100644
index 0000000..97b2aa6
--- /dev/null
+++ b/components/ui/Form.tsx
@@ -0,0 +1,161 @@
+import type * as LabelPrimitive from '@radix-ui/react-label';
+import { Slot } from '@radix-ui/react-slot';
+import * as React from 'react';
+import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
+import { Controller, FormProvider, useFormContext } from 'react-hook-form';
+
+import { Label } from '@/components/ui/Label';
+import { cn } from '@/lib/utils';
+
+const Form = FormProvider;
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath
= FieldPath
+> = {
+ name: TName;
+};
+
+const FormFieldContext = React.createContext({} as FormFieldContextValue);
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ );
+};
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext);
+ const itemContext = React.useContext(FormItemContext);
+ const { getFieldState, formState } = useFormContext();
+
+ const fieldState = getFieldState(fieldContext.name, formState);
+
+ if (!fieldContext) {
+ throw new Error('useFormField should be used within ');
+ }
+
+ const { id } = itemContext;
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState
+ };
+};
+
+type FormItemContextValue = {
+ id: string;
+};
+
+const FormItemContext = React.createContext({} as FormItemContextValue);
+
+const FormItem = React.forwardRef>(
+ ({ className, ...props }, ref) => {
+ const id = React.useId();
+
+ return (
+
+
+
+ );
+ }
+);
+FormItem.displayName = 'FormItem';
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField();
+
+ return (
+
+ );
+});
+FormLabel.displayName = 'FormLabel';
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
+
+ return (
+
+ );
+});
+FormControl.displayName = 'FormControl';
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField();
+
+ return (
+
+ );
+});
+FormDescription.displayName = 'FormDescription';
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField();
+ const body = error ? String(error?.message) : children;
+
+ if (!body) {
+ return null;
+ }
+
+ return (
+
+ {body}
+
+ );
+});
+FormMessage.displayName = 'FormMessage';
+
+export {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ useFormField
+};
diff --git a/components/ui/Icon.tsx b/components/ui/Icon.tsx
new file mode 100644
index 0000000..2fca13a
--- /dev/null
+++ b/components/ui/Icon.tsx
@@ -0,0 +1,16 @@
+import { cn } from "@/lib/utils";
+
+export function Icon({ name, className }: { name: string; className?: string }) {
+ return ;
+}
+
+export const Icons = {
+ gitHub: () => (
+
+
+
+ )
+};
diff --git a/components/ui/Input.tsx b/components/ui/Input.tsx
new file mode 100644
index 0000000..9090c86
--- /dev/null
+++ b/components/ui/Input.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+export type InputProps = React.InputHTMLAttributes;
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+
+Input.displayName = 'Input';
+
+export { Input };
diff --git a/components/ui/Label.tsx b/components/ui/Label.tsx
new file mode 100644
index 0000000..5341821
--- /dev/null
+++ b/components/ui/Label.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/ui/Leaderboard.tsx b/components/ui/Leaderboard.tsx
similarity index 89%
rename from ui/Leaderboard.tsx
rename to components/ui/Leaderboard.tsx
index a0dd8f3..bff199a 100644
--- a/ui/Leaderboard.tsx
+++ b/components/ui/Leaderboard.tsx
@@ -2,12 +2,10 @@
import { AnimatePresence, motion } from 'framer-motion';
+import Podium from '@/components/ui/events/podium/Podium';
import { useLeaderboardEvent } from '@/lib/hooks/use-leaderboard';
import { cn } from '@/lib/utils';
-import Podium from '@/ui/events/podium/Podium';
import { Timer } from './Timer';
-import { type ScoreEvent } from '@/lib/leaderboard';
-import useSWRSubscription, { type SWRSubscription } from 'swr/subscription';
const SCORE_COLORS = ['text-yellow-400', 'text-gray-400', 'text-orange-400'];
@@ -74,7 +72,7 @@ export default function Leaderboard({ token }: { token: string }) {
@@ -83,7 +81,7 @@ export default function Leaderboard({ token }: { token: string }) {
{group.name}
-
+
{group.players && group.players.length > 1
? group.players
.map((player) => player.pseudo || 'Anonyme')
@@ -101,11 +99,11 @@ export default function Leaderboard({ token }: { token: string }) {
*/}
Essai{tries > 1 ? 's' : ''}
- {tries}
+ {tries}
Score
-
+
{group.players.reduce((a, b) => a + b.score, 0)}
@@ -116,7 +114,7 @@ export default function Leaderboard({ token }: { token: string }) {
[...Array(20).keys()].map((i) => (
{!puzzle.score ? (
-
+
) : (
@@ -151,11 +122,7 @@ export default function Puzzle({ token, id }: { token: string; id: number }) {
Score : {puzzle.score}
-
router.push(getURL(`/dashboard/puzzles`))}
- >
+ router.push(getURL(`/dashboard/puzzles`))}>
Retour aux puzzles
@@ -163,3 +130,63 @@ export default function Puzzle({ token, id }: { token: string; id: number }) {
);
}
+
+const InputFormSchema = z.object({
+ answer: z.string().nonempty().trim(),
+ code_file: z.any().nullable()
+});
+
+function InputForm() {
+ const form = useForm>({
+ resolver: zodResolver(InputFormSchema),
+ defaultValues: {
+ answer: '',
+ code_file: null
+ }
+ });
+
+ function onSubmit(data: z.infer) {
+ console.log(data);
+ form.reset();
+ }
+
+ return (
+
+
+ );
+}
diff --git a/ui/Puzzles.tsx b/components/ui/Puzzles.tsx
similarity index 96%
rename from ui/Puzzles.tsx
rename to components/ui/Puzzles.tsx
index 5246b30..0cff12b 100644
--- a/ui/Puzzles.tsx
+++ b/components/ui/Puzzles.tsx
@@ -5,12 +5,12 @@ import { type ChangeEvent, useContext, useEffect, useMemo, useState } from 'reac
import { useForm } from 'react-hook-form';
import { useSWRConfig } from 'swr';
-import AppLink from './AppLink';
-import Button from './Button';
-import Dialog from './Dialog';
-import Icon from './Icon';
-import Input from './Input';
-import Select from './Select';
+import AppLink from '@/components/ui/AppLink';
+import { Button } from '@/components/ui/Button';
+import Dialog from '@/components/ui/Dialog';
+import { Icon, Icons } from '@/components/ui/Icon';
+import { Input } from '@/components/ui/Input';
+import Select from '@/components/ui/Select';
import { UserContext } from '@/context/user';
import { useGroups } from '@/lib/hooks/use-groups';
@@ -111,7 +111,7 @@ export default function Puzzles({ token }: { token: string }) {
onOpenChange={() => handleClick(chapter.id)}
trigger={
-
+ {/* */}
Rejoindre un groupe
}
@@ -124,7 +124,7 @@ export default function Puzzles({ token }: { token: string }) {
{chapter.startDate && chapter.endDate ? (
-
+ {/* */}
{new Date(chapter.startDate).toLocaleDateString('fr-FR', {
day: 'numeric',
@@ -291,10 +291,10 @@ function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
))}
)}
-
+ /> */}
)}
@@ -493,7 +493,7 @@ function GroupForm({ chapter, token }: { chapter: Chapter; token: string }) {