From d6b3eb2fe5ab471f5fdf1867e67535d96424188f Mon Sep 17 00:00:00 2001 From: mcinquin Date: Mon, 14 Sep 2026 10:36:26 +0200 Subject: [PATCH] feat(emoji): add a list of emoji --- .../dashboard/teams/[teamId]/create/page.tsx | 126 ++++++++++- src/lib/phrase-emojis.ts | 208 ++++++++++++++++++ src/lib/schemas/phrase.ts | 2 +- 3 files changed, 327 insertions(+), 9 deletions(-) create mode 100644 src/lib/phrase-emojis.ts diff --git a/src/app/dashboard/teams/[teamId]/create/page.tsx b/src/app/dashboard/teams/[teamId]/create/page.tsx index 89d269c..7f886f3 100644 --- a/src/app/dashboard/teams/[teamId]/create/page.tsx +++ b/src/app/dashboard/teams/[teamId]/create/page.tsx @@ -1,9 +1,10 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; import { useParams, useRouter } from "next/navigation"; import Link from "next/link"; import { motion, AnimatePresence } from "framer-motion"; +import { EMOJI_CATEGORIES } from "@/lib/phrase-emojis"; interface Phrase { id: string; @@ -25,9 +26,15 @@ export default function CreateCardPage() { const [selectedIds, setSelectedIds] = useState>(new Set()); const [newPhraseText, setNewPhraseText] = useState(""); const [newPhraseEmoji, setNewPhraseEmoji] = useState(""); + const [emojiPickerOpen, setEmojiPickerOpen] = useState(false); + const [emojiCategoryId, setEmojiCategoryId] = useState(EMOJI_CATEGORIES[0].id); const [addingPhrase, setAddingPhrase] = useState(false); const [creating, setCreating] = useState(false); const [error, setError] = useState(""); + const emojiPickerRef = useRef(null); + const activeEmojiCategory = + EMOJI_CATEGORIES.find((category) => category.id === emojiCategoryId) ?? + EMOJI_CATEGORIES[0]; const totalCells = rows * cols; const centerPos = freeCenter && rows % 2 !== 0 && cols % 2 !== 0 @@ -46,6 +53,30 @@ export default function CreateCardPage() { fetchPhrases(); }, [fetchPhrases]); + useEffect(() => { + if (!emojiPickerOpen) return; + + function handlePointerDown(event: MouseEvent | TouchEvent) { + const target = event.target as Node; + if (emojiPickerRef.current && !emojiPickerRef.current.contains(target)) { + setEmojiPickerOpen(false); + } + } + + function handleEscape(event: KeyboardEvent) { + if (event.key === "Escape") setEmojiPickerOpen(false); + } + + document.addEventListener("mousedown", handlePointerDown); + document.addEventListener("touchstart", handlePointerDown); + document.addEventListener("keydown", handleEscape); + return () => { + document.removeEventListener("mousedown", handlePointerDown); + document.removeEventListener("touchstart", handlePointerDown); + document.removeEventListener("keydown", handleEscape); + }; + }, [emojiPickerOpen]); + function togglePhrase(id: string) { setSelectedIds((prev) => { const next = new Set(prev); @@ -84,6 +115,7 @@ export default function CreateCardPage() { setSelectedIds((prev) => new Set(Array.from(prev).concat(phrase.id))); setNewPhraseText(""); setNewPhraseEmoji(""); + setEmojiPickerOpen(false); } else { const data = await res.json().catch(() => ({})); setError(data.error ?? "Impossible d'ajouter la phrase"); @@ -274,13 +306,91 @@ export default function CreateCardPage() { {/* Add custom phrase */}
- setNewPhraseEmoji(e.target.value)} - maxLength={2} - /> +
+ + + + {emojiPickerOpen && ( + +
+

+ Choisir un emoji +

+ {newPhraseEmoji && ( + + )} +
+ +
+ {EMOJI_CATEGORIES.map((category) => ( + + ))} +
+ +

+ {activeEmojiCategory.label} +

+ +
+ {activeEmojiCategory.emojis.map((emoji) => ( + + ))} +
+
+ )} +
+
category.emojis +); diff --git a/src/lib/schemas/phrase.ts b/src/lib/schemas/phrase.ts index 429bc31..31ad89f 100644 --- a/src/lib/schemas/phrase.ts +++ b/src/lib/schemas/phrase.ts @@ -8,7 +8,7 @@ export const createPhraseSchema = z.object({ .max(200, "Texte trop long (max 200 caractรจres)"), emoji: z.preprocess( (val) => (val === null || val === "" ? undefined : val), - z.string().trim().max(4, "Emoji invalide").optional() + z.string().trim().max(16, "Emoji invalide").optional() ), });