Files
Momento/memento-mobile/components/TitleSheet.tsx
Antigravity a9e43d7594
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 6m16s
CI / Deploy production (on server) (push) Successful in 22s
fix(audit): mobile i18n + offline share + silent catches + mobile fixes
Mobile i18n (nouveau système):
- lib/i18n.ts: traductions FR/EN/FA + détection locale + RTL (I18nManager)
- (tabs)/_layout.tsx: titres tabs via t()
- 50+ clés traduites par langue

Mobile fixes:
- TitleSheet: dependency array [visible] → [visible, content]
- Partage: inclut extrait contenu + lien public si publié
- Édition: message i18n 'utilisez la version web'

Web fixes:
- 28 silent catches → console.error('[silent-catch]', e)
- Web Clipper: host_permissions déjà restreints dans dist-chrome-store
2026-07-05 16:41:12 +00:00

116 lines
3.8 KiB
TypeScript

/**
* TitleSheet — suggère 3 titres IA dans un bottom sheet propre
*/
import { useState, useEffect } from 'react'
import {
View, Text, TouchableOpacity, ActivityIndicator, StyleSheet,
} from 'react-native'
import { Sparkles } from 'lucide-react-native'
import { BottomSheet } from '@/components/BottomSheet'
import { apiFetch } from '@/lib/api'
import { ENDPOINTS } from '@/lib/config'
import { C } from '@/lib/theme'
interface Props {
visible: boolean
onClose: () => void
content: string
onSelect: (title: string) => void
}
export function TitleSheet({ visible, onClose, content, onSelect }: Props) {
const [loading, setLoading] = useState(false)
const [suggestions, setSuggestions] = useState<string[]>([])
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (visible && content.trim()) {
fetchTitles()
}
}, [visible, content])
const fetchTitles = async () => {
setLoading(true)
setError(null)
setSuggestions([])
try {
const res = await apiFetch(ENDPOINTS.aiTitle, {
method: 'POST',
body: JSON.stringify({ content }),
})
const d = await res.json().catch(() => ({}))
if (!res.ok) {
setError(d.error === 'quota_exceeded' ? 'Quota dépassé' : 'Erreur serveur')
return
}
const raw: unknown[] = d.suggestions ?? []
setSuggestions(raw.map((s) => (typeof s === 'string' ? s : (s as any).title ?? '')).filter(Boolean))
} catch {
setError('Erreur réseau')
} finally {
setLoading(false)
}
}
const handleSelect = (t: string) => {
onSelect(t)
onClose()
}
return (
<BottomSheet visible={visible} onClose={onClose} title="Titres suggérés">
{loading && (
<View style={s.center}>
<ActivityIndicator color={C.brand} size="large" />
<Text style={s.hint}>Génération des titres</Text>
</View>
)}
{error && !loading && (
<View style={s.center}>
<Text style={s.errorText}>{error}</Text>
<TouchableOpacity onPress={fetchTitles} style={s.retryBtn}>
<Text style={s.retryText}>Réessayer</Text>
</TouchableOpacity>
</View>
)}
{!loading && !error && suggestions.length > 0 && (
<View style={s.list}>
{suggestions.map((t, i) => (
<TouchableOpacity key={i} onPress={() => handleSelect(t)} style={s.row} activeOpacity={0.7}>
<View style={s.numBadge}>
<Text style={s.numText}>{i + 1}</Text>
</View>
<Text style={s.titleText}>{t}</Text>
<Sparkles size={14} color={C.brand} />
</TouchableOpacity>
))}
</View>
)}
{!loading && !error && suggestions.length === 0 && (
<View style={s.center}>
<Text style={s.hint}>Aucune suggestion disponible</Text>
</View>
)}
</BottomSheet>
)
}
const s = StyleSheet.create({
center: { alignItems: 'center', paddingVertical: 32, gap: 12 },
hint: { fontSize: 14, color: C.concrete },
errorText: { fontSize: 14, color: '#e11d48' },
retryBtn: { paddingHorizontal: 20, paddingVertical: 10, borderRadius: 10, backgroundColor: C.border },
retryText: { fontSize: 13, color: C.ink, fontWeight: '600' },
list: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 8 },
row: {
flexDirection: 'row', alignItems: 'center', gap: 12,
paddingHorizontal: 12, paddingVertical: 14,
borderRadius: 14, marginBottom: 8,
backgroundColor: C.white, borderWidth: 1, borderColor: C.border,
marginHorizontal: 8,
},
numBadge: { width: 26, height: 26, borderRadius: 13, backgroundColor: '#f3ece4', alignItems: 'center', justifyContent: 'center' },
numText: { fontSize: 12, fontWeight: '700', color: C.brand },
titleText: { flex: 1, fontSize: 14, fontWeight: '500', color: C.ink },
})