fix(ux-audit): landing next/image + confirm AlertDialog + search-modal i18n
Some checks failed
CI / Deploy production (on server) (push) Has been cancelled
CI / Lint, Unit Tests & Build (push) Has been cancelled

Landing page:
- <img> → next/image avec width/height (CLS fix)
- padding nav responsive px-4 sm:px-8

note-card:
- confirm() natif → AlertDialog (non-bloquant, stylable)
- showLeaveDialog state + dialog component

search-modal (20 chaînes FR → i18n):
- useLanguage ajouté
- 20 strings FR hardcoded → t('searchModal.*')
- Clés ajoutées dans en.json + fr.json
This commit is contained in:
Antigravity
2026-07-04 21:49:24 +00:00
parent e72ca26f97
commit 03a3fb7411
5 changed files with 94 additions and 29 deletions

View File

@@ -7,6 +7,7 @@ import {
Box Box
} from 'lucide-react' } from 'lucide-react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import Image from 'next/image'
import { useLanguage } from '@/lib/i18n' import { useLanguage } from '@/lib/i18n'
import { useState } from 'react' import { useState } from 'react'
@@ -44,7 +45,7 @@ export function LandingPage() {
return ( return (
<div className="min-h-screen bg-paper text-ink font-sans selection:bg-ochre/30 selection:text-ink"> <div className="min-h-screen bg-paper text-ink font-sans selection:bg-ochre/30 selection:text-ink">
{/* Navigation */} {/* Navigation */}
<nav className="fixed top-0 left-0 right-0 z-[100] bg-paper/80 backdrop-blur-md border-b border-border px-8 py-4 flex items-center justify-between"> <nav className="fixed top-0 left-0 right-0 z-[100] bg-paper/80 backdrop-blur-md border-b border-border px-4 sm:px-8 py-4 flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-ink flex items-center justify-center rounded-xl shadow-lg rotate-3 group hover:rotate-0 transition-transform cursor-pointer"> <div className="w-10 h-10 bg-ink flex items-center justify-center rounded-xl shadow-lg rotate-3 group hover:rotate-0 transition-transform cursor-pointer">
<span className="text-paper font-serif text-2xl font-bold">M</span> <span className="text-paper font-serif text-2xl font-bold">M</span>
@@ -104,7 +105,7 @@ export function LandingPage() {
{/* App Preview Mockup */} {/* App Preview Mockup */}
<motion.div initial={{ opacity: 0, y: 100 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 1, delay: 0.2, ease: [0.23, 1, 0.32, 1] }} className="mt-24 relative"> <motion.div initial={{ opacity: 0, y: 100 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 1, delay: 0.2, ease: [0.23, 1, 0.32, 1] }} className="mt-24 relative">
<div className="relative mx-auto max-w-5xl aspect-[16/10] bg-white rounded-[32px] shadow-[0_40px_100px_-20px_rgba(0,0,0,0.15)] border border-border p-4 overflow-hidden group"> <div className="relative mx-auto max-w-5xl aspect-[16/10] bg-white rounded-[32px] shadow-[0_40px_100px_-20px_rgba(0,0,0,0.15)] border border-border p-4 overflow-hidden group">
<img src="/images/workspace-hero.jpg" alt="Memento Workspace" className="w-full h-full object-cover rounded-2xl filter saturate-[0.8]" /> <Image src="/images/workspace-hero.jpg" alt="Memento Workspace" width={1200} height={750} className="w-full h-full object-cover rounded-2xl filter saturate-[0.8]" priority />
<div className="absolute inset-0 bg-ink/10 group-hover:bg-ink/0 transition-colors duration-500" /> <div className="absolute inset-0 bg-ink/10 group-hover:bg-ink/0 transition-colors duration-500" />
<div className="absolute top-10 right-10 w-64 bg-paper/90 backdrop-blur-xl border border-border p-6 rounded-2xl shadow-2xl"> <div className="absolute top-10 right-10 w-64 bg-paper/90 backdrop-blur-xl border border-border p-6 rounded-2xl shadow-2xl">

View File

@@ -185,6 +185,7 @@ export const NoteCard = memo(function NoteCard({
const [isDeleting, setIsDeleting] = useState(false) const [isDeleting, setIsDeleting] = useState(false)
const [isHidden, setIsHidden] = useState(false) const [isHidden, setIsHidden] = useState(false)
const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [showLeaveDialog, setShowLeaveDialog] = useState(false)
const [showCollaboratorDialog, setShowCollaboratorDialog] = useState(false) const [showCollaboratorDialog, setShowCollaboratorDialog] = useState(false)
const [collaborators, setCollaborators] = useState<any[]>([]) const [collaborators, setCollaborators] = useState<any[]>([])
const [owner, setOwner] = useState<any>(null) const [owner, setOwner] = useState<any>(null)
@@ -408,14 +409,13 @@ export const NoteCard = memo(function NoteCard({
} }
const handleLeaveShare = async () => { const handleLeaveShare = async () => {
if (confirm(t('notes.confirmLeaveShare'))) { try {
try { await leaveSharedNote(note.id)
await leaveSharedNote(note.id) setIsDeleting(true)
setIsDeleting(true) // Hide the note from view } catch (error) {
} catch (error) { console.error('Failed to leave share:', error)
console.error('Failed to leave share:', error)
}
} }
setShowLeaveDialog(false)
} }
const handleRemoveFusedBadge = async (e: React.MouseEvent) => { const handleRemoveFusedBadge = async (e: React.MouseEvent) => {
@@ -657,7 +657,7 @@ export const NoteCard = memo(function NoteCard({
className="h-6 px-2 text-xs text-gray-500 hover:text-red-600 dark:hover:text-red-400" className="h-6 px-2 text-xs text-gray-500 hover:text-red-600 dark:hover:text-red-400"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
handleLeaveShare() setShowLeaveDialog(true)
}} }}
> >
<LogOut className="h-3 w-3 me-1" /> <LogOut className="h-3 w-3 me-1" />
@@ -885,6 +885,24 @@ export const NoteCard = memo(function NoteCard({
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
{/* Leave Share Confirmation Dialog */}
<AlertDialog open={showLeaveDialog} onOpenChange={setShowLeaveDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('notes.leaveShare')}</AlertDialogTitle>
<AlertDialogDescription>
{t('notes.confirmLeaveShare')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={handleLeaveShare}>
{t('notes.leaveShare')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Card> </Card>
) )
}) })

View File

@@ -17,6 +17,7 @@ import {
} from 'lucide-react' } from 'lucide-react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useNotebooks } from '@/context/notebooks-context' import { useNotebooks } from '@/context/notebooks-context'
import { useLanguage } from '@/lib/i18n'
import type { Note } from '@/lib/types' import type { Note } from '@/lib/types'
interface SearchMatch { interface SearchMatch {
@@ -58,6 +59,7 @@ function escapeRegExp(s: string) {
export function SearchModal({ isOpen, onClose }: SearchModalProps) { export function SearchModal({ isOpen, onClose }: SearchModalProps) {
const router = useRouter() const router = useRouter()
const { notebooks } = useNotebooks() const { notebooks } = useNotebooks()
const { t } = useLanguage()
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const [useRegex, setUseRegex] = useState(false) const [useRegex, setUseRegex] = useState(false)
@@ -413,7 +415,7 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
className="fixed inset-0 bg-black/40 dark:bg-black/60 backdrop-blur-sm flex items-center justify-center z-[200] p-4 sm:p-6 select-none" className="fixed inset-0 bg-black/40 dark:bg-black/60 backdrop-blur-sm flex items-center justify-center z-[200] p-4 sm:p-6 select-none"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="Recherche" aria-label={t('searchModal.ariaLabel')}
onClick={(e) => { if (e.target === e.currentTarget) onClose() }} onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
> >
<motion.div <motion.div
@@ -432,13 +434,13 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
type="text" type="text"
value={query} value={query}
onChange={e => setQuery(e.target.value)} onChange={e => setQuery(e.target.value)}
placeholder="Rechercher dans toutes vos notes…" placeholder={t('searchModal.placeholder')}
className="w-full text-sm pl-10 pr-28 py-2.5 rounded-xl border border-border/70 dark:border-zinc-800 bg-white/85 dark:bg-[#1C1C1C] text-ink dark:text-dark-ink placeholder-concrete/50 outline-none focus:border-blueprint transition-colors" className="w-full text-sm pl-10 pr-28 py-2.5 rounded-xl border border-border/70 dark:border-zinc-800 bg-white/85 dark:bg-[#1C1C1C] text-ink dark:text-dark-ink placeholder-concrete/50 outline-none focus:border-blueprint transition-colors"
/> />
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1"> <div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1">
<button <button
onClick={() => setCaseSensitive(c => !c)} onClick={() => setCaseSensitive(c => !c)}
title="Respecter la casse" title={t('searchModal.caseSensitive')}
className={`px-1.5 py-1 text-[9.5px] font-bold rounded-md transition-colors select-none ${ className={`px-1.5 py-1 text-[9.5px] font-bold rounded-md transition-colors select-none ${
caseSensitive ? 'text-blueprint bg-blueprint/8' : 'text-concrete hover:bg-black/5 dark:hover:bg-white/5' caseSensitive ? 'text-blueprint bg-blueprint/8' : 'text-concrete hover:bg-black/5 dark:hover:bg-white/5'
}`} }`}
@@ -447,7 +449,7 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
</button> </button>
<button <button
onClick={() => setUseRegex(r => !r)} onClick={() => setUseRegex(r => !r)}
title="Mode regex" title={t('searchModal.regexMode')}
className={`px-1.5 py-1 text-[9.5px] font-bold rounded-md transition-colors select-none ${ className={`px-1.5 py-1 text-[9.5px] font-bold rounded-md transition-colors select-none ${
useRegex ? 'text-blueprint bg-blueprint/8' : 'text-concrete hover:bg-black/5 dark:hover:bg-white/5' useRegex ? 'text-blueprint bg-blueprint/8' : 'text-concrete hover:bg-black/5 dark:hover:bg-white/5'
}`} }`}
@@ -463,7 +465,7 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
{/* Saved queries */} {/* Saved queries */}
{savedQueries.length > 0 && ( {savedQueries.length > 0 && (
<div className="flex items-center gap-2 text-[10px] text-concrete"> <div className="flex items-center gap-2 text-[10px] text-concrete">
<span className="uppercase text-[9px] font-bold">Favoris :</span> <span className="uppercase text-[9px] font-bold">{t('searchModal.favorites')}</span>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{savedQueries.map(sq => ( {savedQueries.map(sq => (
<button <button
@@ -507,12 +509,12 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
</div> </div>
<span className="text-[11px] font-medium text-concrete"> <span className="text-[11px] font-medium text-concrete">
{isLoading {isLoading
? 'Recherche en cours…' ? t('searchModal.searching')
: filteredMatches.length > 0 : filteredMatches.length > 0
? `${filteredMatches.length} occurrence${filteredMatches.length > 1 ? 's' : ''} dans ${docMatchesCount} note${docMatchesCount > 1 ? 's' : ''}` ? `${filteredMatches.length} occurrence${filteredMatches.length > 1 ? 's' : ''} dans ${docMatchesCount} note${docMatchesCount > 1 ? 's' : ''}`
: query.trim() : query.trim()
? 'Aucun résultat' ? t('searchModal.noResults')
: 'Tapez pour rechercher'} : t('searchModal.typeToSearch')}
</span> </span>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -531,7 +533,7 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
onChange={e => setSearchInTrash(e.target.checked)} onChange={e => setSearchInTrash(e.target.checked)}
className="rounded border-border/60 text-blueprint focus:ring-blueprint w-3 h-3" className="rounded border-border/60 text-blueprint focus:ring-blueprint w-3 h-3"
/> />
<span>Corbeille incluse</span> <span>{t('searchModal.trashIncluded')}</span>
</label> </label>
</div> </div>
</div> </div>
@@ -547,13 +549,13 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
<div className="flex items-center gap-1.5 mb-1.5"> <div className="flex items-center gap-1.5 mb-1.5">
<Sparkles size={12} className="text-brand-accent" /> <Sparkles size={12} className="text-brand-accent" />
<span className="text-[9px] font-bold uppercase tracking-widest text-brand-accent"> <span className="text-[9px] font-bold uppercase tracking-widest text-brand-accent">
{overviewLoading ? 'Analyse...' : 'Réponse IA'} {overviewLoading ? t('searchModal.aiAnalysis') : t('searchModal.aiResponse')}
</span> </span>
</div> </div>
{overviewLoading ? ( {overviewLoading ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Loader2 size={12} className="animate-spin text-brand-accent" /> <Loader2 size={12} className="animate-spin text-brand-accent" />
<span className="text-[11px] text-muted-foreground">Synthèse des résultats...</span> <span className="text-[11px] text-muted-foreground">t('searchModal.resultsSummary')</span>
</div> </div>
) : ( ) : (
<p className="text-[12px] leading-relaxed text-foreground/80">{overview}</p> <p className="text-[12px] leading-relaxed text-foreground/80">{overview}</p>
@@ -614,7 +616,7 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
<div className="h-full flex flex-col items-center justify-center text-center p-6 text-concrete pt-24 space-y-2"> <div className="h-full flex flex-col items-center justify-center text-center p-6 text-concrete pt-24 space-y-2">
<Search size={20} className="opacity-30 animate-pulse" /> <Search size={20} className="opacity-30 animate-pulse" />
<p className="text-[11px] font-medium italic opacity-70"> <p className="text-[11px] font-medium italic opacity-70">
{query.trim() ? 'Aucune note ne correspond.' : 'Tapez pour obtenir des résultats instantanés.'} {query.trim() ? t('searchModal.noMatch') : t('searchModal.typeForResults')}
</p> </p>
</div> </div>
)} )}
@@ -644,7 +646,7 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
{activeMatch.noteTitle} {activeMatch.noteTitle}
</h4> </h4>
<p className="text-[8px] uppercase tracking-wider text-concrete font-bold mt-0.5"> <p className="text-[8px] uppercase tracking-wider text-concrete font-bold mt-0.5">
APERÇU CONTEXTUEL {t('searchModal.documentPreview')}
</p> </p>
</div> </div>
@@ -662,7 +664,7 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
className="px-5 py-2.5 bg-ink text-white dark:bg-white dark:text-black hover:opacity-90 text-xs font-semibold rounded-xl flex items-center gap-2 transition-all shadow-sm" className="px-5 py-2.5 bg-ink text-white dark:bg-white dark:text-black hover:opacity-90 text-xs font-semibold rounded-xl flex items-center gap-2 transition-all shadow-sm"
> >
<CornerDownRight size={13} /> <CornerDownRight size={13} />
<span>Ouvrir dans l'éditeur</span> <span>{t('searchModal.openInEditor')}</span>
</button> </button>
<span className="text-[10px] text-concrete font-bold font-mono bg-paper dark:bg-white/5 border border-border/30 px-2 py-1 rounded"> <span className="text-[10px] text-concrete font-bold font-mono bg-paper dark:bg-white/5 border border-border/30 px-2 py-1 rounded">
ID: {activeMatch.noteId.slice(0, 8)} ID: {activeMatch.noteId.slice(0, 8)}
@@ -673,7 +675,7 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
<div className="flex-1 flex flex-col items-center justify-center text-center p-6 text-concrete space-y-3"> <div className="flex-1 flex flex-col items-center justify-center text-center p-6 text-concrete space-y-3">
<HelpCircle size={24} className="opacity-25" /> <HelpCircle size={24} className="opacity-25" />
<div className="space-y-1"> <div className="space-y-1">
<p className="text-[11.5px] font-bold">Aperçu du document</p> <p className="text-[11.5px] font-bold">{t('searchModal.documentPreview')}</p>
<p className="text-[10px] italic opacity-60"> <p className="text-[10px] italic opacity-60">
Sélectionnez un résultat pour explorer son contenu. Sélectionnez un résultat pour explorer son contenu.
</p> </p>
@@ -688,20 +690,20 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
<div className="flex items-center gap-5 text-[9.5px] font-bold text-concrete/70"> <div className="flex items-center gap-5 text-[9.5px] font-bold text-concrete/70">
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<kbd className="bg-slate-200 dark:bg-zinc-800 px-1 py-0.5 rounded text-ink dark:text-white text-[9px]"></kbd> <kbd className="bg-slate-200 dark:bg-zinc-800 px-1 py-0.5 rounded text-ink dark:text-white text-[9px]"></kbd>
naviguer {t('searchModal.hintNavigate')}
</span> </span>
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<kbd className="bg-slate-200 dark:bg-zinc-800 px-1 py-0.5 rounded text-ink dark:text-white text-[9px]">Entrée</kbd> <kbd className="bg-slate-200 dark:bg-zinc-800 px-1 py-0.5 rounded text-ink dark:text-white text-[9px]">Entrée</kbd>
ouvrir {t('searchModal.hintOpen')}
</span> </span>
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<kbd className="bg-slate-200 dark:bg-zinc-800 px-1 py-0.5 rounded text-ink dark:text-white text-[9px]">Échap</kbd> <kbd className="bg-slate-200 dark:bg-zinc-800 px-1 py-0.5 rounded text-ink dark:text-white text-[9px]">Échap</kbd>
fermer {t('searchModal.hintClose')}
</span> </span>
</div> </div>
<div className="flex items-center gap-1.5 text-[9px] font-bold uppercase tracking-wider text-concrete/50"> <div className="flex items-center gap-1.5 text-[9px] font-bold uppercase tracking-wider text-concrete/50">
<Command size={10} /> <Command size={10} />
<span>Memento Search</span> <span>{t('searchModal.title')}</span>
</div> </div>
</div> </div>
</motion.div> </motion.div>

View File

@@ -3932,5 +3932,27 @@
"openSite": "Open site", "openSite": "Open site",
"updateSite": "Update site", "updateSite": "Update site",
"unpublish": "Unpublish site" "unpublish": "Unpublish site"
},
"searchModal": {
"search_ariaLabel": "Search",
"search_placeholder": "Search across all your notes…",
"search_caseSensitive": "Case sensitive",
"search_regexMode": "Regex mode",
"search_trashIncluded": "Trash included",
"search_openInEditor": "Open in editor",
"search_title": "Memento Search",
"search_favorites": "Favorites:",
"search_searching": "Searching…",
"search_noResults": "No results",
"search_typeToSearch": "Type to search",
"search_aiAnalysis": "Analyzing…",
"search_aiResponse": "AI Response",
"search_resultsSummary": "Results summary…",
"search_documentPreview": "Document preview",
"search_noMatch": "No note matches.",
"search_typeForResults": "Type to get instant results.",
"search_hintNavigate": "navigate",
"search_hintOpen": "open",
"search_hintClose": "close"
} }
} }

View File

@@ -3936,5 +3936,27 @@
"openSite": "Ouvrir le site", "openSite": "Ouvrir le site",
"updateSite": "Mettre à jour le site", "updateSite": "Mettre à jour le site",
"unpublish": "Dépublier le site" "unpublish": "Dépublier le site"
},
"searchModal": {
"search_ariaLabel": "Recherche",
"search_placeholder": "Rechercher dans toutes vos notes…",
"search_caseSensitive": "Respecter la casse",
"search_regexMode": "Mode regex",
"search_trashIncluded": "Corbeille incluse",
"search_openInEditor": "Ouvrir dans l'éditeur",
"search_title": "Memento Search",
"search_favorites": "Favoris :",
"search_searching": "Recherche en cours…",
"search_noResults": "Aucun résultat",
"search_typeToSearch": "Tapez pour rechercher",
"search_aiAnalysis": "Analyse...",
"search_aiResponse": "Réponse IA",
"search_resultsSummary": "Synthèse des résultats...",
"search_documentPreview": "Aperçu du document",
"search_noMatch": "Aucune note ne correspond.",
"search_typeForResults": "Tapez pour obtenir des résultats instantanés.",
"search_hintNavigate": "naviguer",
"search_hintOpen": "ouvrir",
"search_hintClose": "fermer"
} }
} }