fix(ux-audit): landing next/image + confirm AlertDialog + search-modal i18n
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:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user