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
} from 'lucide-react'
import { useRouter } from 'next/navigation'
import Image from 'next/image'
import { useLanguage } from '@/lib/i18n'
import { useState } from 'react'
@@ -44,7 +45,7 @@ export function LandingPage() {
return (
<div className="min-h-screen bg-paper text-ink font-sans selection:bg-ochre/30 selection:text-ink">
{/* 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="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>
@@ -104,7 +105,7 @@ export function LandingPage() {
{/* 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">
<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 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 [isHidden, setIsHidden] = useState(false)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [showLeaveDialog, setShowLeaveDialog] = useState(false)
const [showCollaboratorDialog, setShowCollaboratorDialog] = useState(false)
const [collaborators, setCollaborators] = useState<any[]>([])
const [owner, setOwner] = useState<any>(null)
@@ -408,14 +409,13 @@ export const NoteCard = memo(function NoteCard({
}
const handleLeaveShare = async () => {
if (confirm(t('notes.confirmLeaveShare'))) {
try {
await leaveSharedNote(note.id)
setIsDeleting(true) // Hide the note from view
} catch (error) {
console.error('Failed to leave share:', error)
}
try {
await leaveSharedNote(note.id)
setIsDeleting(true)
} catch (error) {
console.error('Failed to leave share:', error)
}
setShowLeaveDialog(false)
}
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"
onClick={(e) => {
e.stopPropagation()
handleLeaveShare()
setShowLeaveDialog(true)
}}
>
<LogOut className="h-3 w-3 me-1" />
@@ -885,6 +885,24 @@ export const NoteCard = memo(function NoteCard({
</AlertDialogFooter>
</AlertDialogContent>
</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>
)
})

View File

@@ -17,6 +17,7 @@ import {
} from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useNotebooks } from '@/context/notebooks-context'
import { useLanguage } from '@/lib/i18n'
import type { Note } from '@/lib/types'
interface SearchMatch {
@@ -58,6 +59,7 @@ function escapeRegExp(s: string) {
export function SearchModal({ isOpen, onClose }: SearchModalProps) {
const router = useRouter()
const { notebooks } = useNotebooks()
const { t } = useLanguage()
const [query, setQuery] = useState('')
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"
role="dialog"
aria-modal="true"
aria-label="Recherche"
aria-label={t('searchModal.ariaLabel')}
onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
>
<motion.div
@@ -432,13 +434,13 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
type="text"
value={query}
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"
/>
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1">
<button
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 ${
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
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 ${
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 */}
{savedQueries.length > 0 && (
<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">
{savedQueries.map(sq => (
<button
@@ -507,12 +509,12 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
</div>
<span className="text-[11px] font-medium text-concrete">
{isLoading
? 'Recherche en cours…'
? t('searchModal.searching')
: filteredMatches.length > 0
? `${filteredMatches.length} occurrence${filteredMatches.length > 1 ? 's' : ''} dans ${docMatchesCount} note${docMatchesCount > 1 ? 's' : ''}`
: query.trim()
? 'Aucun résultat'
: 'Tapez pour rechercher'}
? t('searchModal.noResults')
: t('searchModal.typeToSearch')}
</span>
</div>
<div className="flex items-center gap-4">
@@ -531,7 +533,7 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
onChange={e => setSearchInTrash(e.target.checked)}
className="rounded border-border/60 text-blueprint focus:ring-blueprint w-3 h-3"
/>
<span>Corbeille incluse</span>
<span>{t('searchModal.trashIncluded')}</span>
</label>
</div>
</div>
@@ -547,13 +549,13 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
<div className="flex items-center gap-1.5 mb-1.5">
<Sparkles size={12} className="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>
</div>
{overviewLoading ? (
<div className="flex items-center gap-2">
<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>
) : (
<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">
<Search size={20} className="opacity-30 animate-pulse" />
<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>
</div>
)}
@@ -644,7 +646,7 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
{activeMatch.noteTitle}
</h4>
<p className="text-[8px] uppercase tracking-wider text-concrete font-bold mt-0.5">
APERÇU CONTEXTUEL
{t('searchModal.documentPreview')}
</p>
</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"
>
<CornerDownRight size={13} />
<span>Ouvrir dans l'éditeur</span>
<span>{t('searchModal.openInEditor')}</span>
</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">
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">
<HelpCircle size={24} className="opacity-25" />
<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">
Sélectionnez un résultat pour explorer son contenu.
</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">
<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>
naviguer
{t('searchModal.hintNavigate')}
</span>
<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>
ouvrir
{t('searchModal.hintOpen')}
</span>
<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>
fermer
{t('searchModal.hintClose')}
</span>
</div>
<div className="flex items-center gap-1.5 text-[9px] font-bold uppercase tracking-wider text-concrete/50">
<Command size={10} />
<span>Memento Search</span>
<span>{t('searchModal.title')}</span>
</div>
</div>
</motion.div>

View File

@@ -3932,5 +3932,27 @@
"openSite": "Open site",
"updateSite": "Update 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",
"updateSite": "Mettre à jour 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"
}
}