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
116 lines
3.8 KiB
TypeScript
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 },
|
|
})
|