feat: mobile app complet + flashcards fixes + drag handle améliorations
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m32s
CI / Deploy production (on server) (push) Has been skipped

Mobile app:
- Révision flashcards : liste decks, session flip-card SM-2, couleurs harmonisées web
- Génération flashcards depuis note (FlashcardSheet + route /api/mobile/flashcards/generate)
- Audio Whisper : hook useAudioRecorder reécrit, MicButton avec erreurs
- IA : AISheet (améliorer/clarifier/résumer), TitleSheet (titre automatique)
- Suppression note (soft delete + confirmation Alert)
- Note du jour : titre lisible + HTML (plus JSON TipTap brut)
- Parser TipTap→HTML côté mobile (tipTapToHtml)
- Icône 🎓 dans header note → génération flashcards
- Endpoint flashcardGenerate dans config.ts

Web fixes:
- Bug flashcards groupées par carnet → deck par note (migration + schema)
- Bug filtre 'cartes dues' ignoré (suppression fallback buildSessionQueue)
- Suppression UI création deck manuelle (inutile)
- Fix setViewType is not defined dans home-client.tsx

Drag handle menu:
- Fix : clearNodes() avant transformation (heading→liste/code/citation)
- Ajout : option 'Texte' (paragraphe) dans Transformer en
- Ajout : Monter / Descendre le bloc
- Ajout : Copier le contenu du bloc
- Fix : sous-menu hover stable (délai 200ms)
- Fix : Supprimer en rouge via classe --danger (plus :first-child)
- i18n : nouvelles clés dans 15 locales

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Antigravity
2026-05-29 18:49:40 +00:00
parent 1121b8c345
commit 0fa8978395
54 changed files with 2648 additions and 245 deletions

View File

@@ -9,7 +9,6 @@ import {
ChevronLeft,
ChevronRight,
Calendar,
Plus,
BarChart3,
Loader2,
BookOpen,
@@ -78,8 +77,7 @@ function shuffleCards<T>(items: T[]): T[] {
}
function buildSessionQueue(cards: FlashcardItem[], dueOnly: boolean): FlashcardItem[] {
let toReview = dueOnly ? cards.filter((c) => c.due) : cards
if (toReview.length === 0) toReview = cards
const toReview = dueOnly ? cards.filter((c) => c.due) : cards
return shuffleCards(toReview)
}
@@ -316,8 +314,6 @@ export function RevisionView() {
const [sessionDurationSeconds, setSessionDurationSeconds] = useState(0)
const sessionTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
const [newDeckName, setNewDeckName] = useState('')
const [creatingDeck, setCreatingDeck] = useState(false)
// Gap 8 — deck deletion state
const [deletingDeckId, setDeletingDeckId] = useState<string | null>(null)
@@ -402,6 +398,17 @@ export function RevisionView() {
return
}
const shuffled = buildSessionQueue(cardsInput, mode === 'due')
// Mode "due" mais aucune carte n'est due → afficher état "à jour" directement
if (shuffled.length === 0) {
setSessionCards([])
setCurrentIndex(0)
setIsFlipped(false)
setSessionGrades({})
setIsSessionActive(true)
setIsSessionFinished(true)
setActiveDeckId(deckId)
return
}
setSessionCards(shuffled)
setCurrentIndex(0)
setIsFlipped(false)
@@ -561,27 +568,6 @@ export function RevisionView() {
}
}
const createDeck = async () => {
const name = newDeckName.trim()
if (!name) return
setCreatingDeck(true)
try {
const res = await fetch('/api/flashcards/decks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
})
const data = await res.json()
if (res.ok) {
setNewDeckName('')
await loadDecks()
toast.success(t('flashcards.deckCreated'))
if (data.deck?.id) setActiveDeckId(data.deck.id)
}
} finally {
setCreatingDeck(false)
}
}
// Gap 8 — delete a deck
const handleDeleteDeck = async (deckId: string) => {
@@ -910,24 +896,6 @@ export function RevisionView() {
</div>
)}
<div className="flex flex-wrap gap-2 items-center">
<input
value={newDeckName}
onChange={(e) => setNewDeckName(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') void createDeck() }}
placeholder={t('flashcards.newDeckPlaceholder')}
className="flex-1 min-w-[180px] px-3 py-2 rounded-lg border border-border text-sm bg-transparent"
/>
<button
type="button"
onClick={createDeck}
disabled={creatingDeck || !newDeckName.trim()}
className="px-3 py-2 rounded-lg border border-border text-xs font-bold flex items-center gap-1 disabled:opacity-50"
>
{creatingDeck ? <Loader2 size={14} className="animate-spin" /> : <Plus size={14} />}
{t('flashcards.createDeck')}
</button>
</div>
{loadingDecks ? (
<div className="flex justify-center py-16"><Loader2 className="animate-spin text-concrete" /></div>