Files
Momento/memento-mobile/app/note/create.tsx
Antigravity 0fa8978395
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m32s
CI / Deploy production (on server) (push) Has been skipped
feat: mobile app complet + flashcards fixes + drag handle améliorations
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>
2026-05-29 18:49:40 +00:00

178 lines
6.9 KiB
TypeScript

import { useState, useRef } from 'react'
import {
View, Text, TextInput, TouchableOpacity, ScrollView,
KeyboardAvoidingView, Platform, ActivityIndicator,
Alert, StyleSheet,
} from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { useRouter, useLocalSearchParams } from 'expo-router'
import { Sparkles, X } from 'lucide-react-native'
import { apiFetch } from '@/lib/api'
import { ENDPOINTS } from '@/lib/config'
import { C } from '@/lib/theme'
import { AISheet } from '@/components/AISheet'
import { TitleSheet } from '@/components/TitleSheet'
import { MicButton } from '@/components/MicButton'
import { useAudioRecorder } from '@/lib/useAudioRecorder'
export default function CreateNoteScreen() {
const router = useRouter()
const { notebookId } = useLocalSearchParams<{ notebookId?: string }>()
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [saving, setSaving] = useState(false)
const [aiSheetOpen, setAiSheetOpen] = useState(false)
const [titleSheetOpen, setTitleSheetOpen] = useState(false)
const contentRef = useRef<TextInput>(null)
// Audio
const { state: audioState, startRecording, stopAndTranscribe, cancelRecording } = useAudioRecorder(
(text) => setContent((prev) => prev ? prev + ' ' + text : text)
)
const handleMic = () => {
if (audioState === 'idle' || audioState === 'error') startRecording()
else if (audioState === 'recording') stopAndTranscribe()
else cancelRecording()
}
const handleSave = async () => {
if (!title.trim()) {
Alert.alert('Titre requis', 'Donnez un titre à votre note.')
return
}
setSaving(true)
try {
const res = await apiFetch(ENDPOINTS.createNote, {
method: 'POST',
body: JSON.stringify({ title: title.trim(), content, notebookId }),
})
if (!res.ok) {
const d = await res.json().catch(() => ({}))
throw new Error(d.error ?? 'Erreur serveur')
}
const { note } = await res.json()
router.replace({ pathname: '/note/[id]', params: { id: note.id } })
} catch (e: any) {
Alert.alert('Erreur', e.message)
} finally {
setSaving(false)
}
}
return (
<SafeAreaView style={s.safe}>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={{ flex: 1 }}>
{/* Header */}
<View style={s.header}>
<TouchableOpacity onPress={() => router.back()} style={s.cancelBtn}>
<X size={20} color={C.concrete} />
</TouchableOpacity>
<Text style={s.headerTitle}>Nouvelle note</Text>
<TouchableOpacity
onPress={handleSave}
disabled={saving || !title.trim()}
style={[s.saveBtn, (!title.trim() || saving) && s.saveBtnDisabled]}
>
{saving
? <ActivityIndicator size="small" color={C.white} />
: <Text style={s.saveBtnText}>Enregistrer</Text>}
</TouchableOpacity>
</View>
<ScrollView style={{ flex: 1 }} keyboardShouldPersistTaps="handled">
{/* Titre */}
<View style={s.titleRow}>
<TextInput
value={title}
onChangeText={setTitle}
placeholder="Titre de la note…"
style={s.titleInput}
placeholderTextColor={C.border}
returnKeyType="next"
onSubmitEditing={() => contentRef.current?.focus()}
autoFocus
/>
{content.trim().length >= 10 && (
<TouchableOpacity onPress={() => setTitleSheetOpen(true)} style={s.sparkleBtn} activeOpacity={0.8}>
<Sparkles size={16} color={C.brand} />
</TouchableOpacity>
)}
</View>
<View style={s.divider} />
{/* Contenu */}
<TextInput
ref={contentRef}
value={content}
onChangeText={setContent}
placeholder="Commencez à écrire…"
style={s.contentInput}
placeholderTextColor={C.concrete}
multiline
textAlignVertical="top"
scrollEnabled={false}
/>
</ScrollView>
{/* Barre outils bas */}
<View style={s.toolbar}>
<MicButton state={audioState} onPress={handleMic} />
{audioState === 'recording' && (
<Text style={s.recordingHint}> Enregistrement Appuyez pour arrêter</Text>
)}
{audioState !== 'recording' && (
<TouchableOpacity
onPress={() => setAiSheetOpen(true)}
disabled={!content.trim()}
style={[s.aiBtn, !content.trim() && s.aiBtnDisabled]}
activeOpacity={0.8}
>
<Sparkles size={15} color={C.white} />
<Text style={s.aiBtnText}>Améliorer avec l'IA</Text>
</TouchableOpacity>
)}
</View>
</KeyboardAvoidingView>
{/* Modaux propres */}
<AISheet
visible={aiSheetOpen}
onClose={() => setAiSheetOpen(false)}
text={content}
onApply={(improved) => setContent(improved)}
/>
<TitleSheet
visible={titleSheetOpen}
onClose={() => setTitleSheetOpen(false)}
content={content}
onSelect={(t) => setTitle(t)}
/>
</SafeAreaView>
)
}
const s = StyleSheet.create({
safe: { flex: 1, backgroundColor: C.paper },
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: C.border },
cancelBtn: { padding: 4, marginRight: 4 },
headerTitle: { flex: 1, fontSize: 15, fontWeight: '600', color: C.ink, textAlign: 'center' },
saveBtn: { backgroundColor: C.brand, paddingHorizontal: 14, paddingVertical: 7, borderRadius: 10 },
saveBtnDisabled: { opacity: 0.35 },
saveBtnText: { color: C.white, fontWeight: '700', fontSize: 13 },
titleRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 8, gap: 8 },
titleInput: { flex: 1, fontSize: 26, fontWeight: '800', color: C.ink, letterSpacing: -0.5, lineHeight: 32 },
sparkleBtn: { padding: 8, borderRadius: 10, backgroundColor: '#f3ece4' },
divider: { height: 1, backgroundColor: C.border, marginHorizontal: 20, marginBottom: 16 },
contentInput: { flex: 1, fontSize: 16, color: C.ink, lineHeight: 26, paddingHorizontal: 20, paddingBottom: 120, minHeight: 300 },
toolbar: { flexDirection: 'row', alignItems: 'center', gap: 10, paddingHorizontal: 16, paddingVertical: 12, borderTopWidth: 1, borderTopColor: C.border, backgroundColor: C.paper },
recordingHint: { flex: 1, fontSize: 13, color: '#e11d48', fontWeight: '500' },
aiBtn: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 6, backgroundColor: C.ink, paddingVertical: 11, paddingHorizontal: 14, borderRadius: 12, justifyContent: 'center' },
aiBtnDisabled: { opacity: 0.35 },
aiBtnText: { color: C.white, fontWeight: '600', fontSize: 14 },
})