chore: clean up repo for public release

- Remove BMAD framework, IDE configs, dev screenshots, test files,
  internal docs, and backup files
- Rename keep-notes/ to memento-note/
- Update all references from keep-notes to memento-note
- Add Apache 2.0 license with Commons Clause (non-commercial restriction)
- Add clean .gitignore and .env.docker.example
This commit is contained in:
Sepehr Ramezani
2026-04-20 22:48:06 +02:00
parent 402e88b788
commit e4d4e23dc7
3981 changed files with 407 additions and 530622 deletions

View File

@@ -0,0 +1,33 @@
'use client'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { ArrowRight, Sparkles } from 'lucide-react'
import Link from 'next/link'
import { useLanguage } from '@/lib/i18n'
export function AISettingsLinkCard() {
const { t } = useLanguage()
return (
<Card className="mt-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-amber-500" />
{t('nav.aiSettings')}
</CardTitle>
<CardDescription>
{t('nav.configureAI')}
</CardDescription>
</CardHeader>
<CardContent>
<Link href="/settings/ai">
<Button variant="outline" className="w-full justify-between group">
<span>{t('nav.manageAISettings')}</span>
<ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" />
</Button>
</Link>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,155 @@
'use client'
import { useState } from 'react'
import { SettingsNav, SettingsSection, SettingToggle, SettingInput, SettingSelect } from '@/components/settings'
import { updateAISettings } from '@/app/actions/ai-settings'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
export default function ProfileSettingsPage() {
const { t } = useLanguage()
// Mock user data - in real implementation, load from server
const [user, setUser] = useState({
name: 'John Doe',
email: 'john@example.com'
})
const [language, setLanguage] = useState('auto')
const [showRecentNotes, setShowRecentNotes] = useState(false)
const handleNameChange = async (value: string) => {
setUser(prev => ({ ...prev, name: value }))
// TODO: Implement profile update
}
const handleEmailChange = async (value: string) => {
setUser(prev => ({ ...prev, email: value }))
// TODO: Implement email update
}
const handleLanguageChange = async (value: string) => {
setLanguage(value)
try {
await updateAISettings({ preferredLanguage: value as any })
} catch (error) {
console.error('Error updating language:', error)
toast.error(t('aiSettings.error'))
}
}
const handleRecentNotesChange = async (enabled: boolean) => {
setShowRecentNotes(enabled)
try {
await updateAISettings({ showRecentNotes: enabled })
} catch (error) {
console.error('Error updating recent notes setting:', error)
toast.error(t('aiSettings.error'))
}
}
return (
<div className="container mx-auto py-10 px-4 max-w-6xl">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar Navigation */}
<aside className="lg:col-span-1">
<SettingsNav />
</aside>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">{t('profile.title')}</h1>
<p className="text-gray-600 dark:text-gray-400">
{t('profile.description')}
</p>
</div>
{/* Profile Information */}
<SettingsSection
title={t('profile.accountSettings')}
icon={<span className="text-2xl">👤</span>}
description={t('profile.description')}
>
<SettingInput
label={t('profile.displayName')}
description={t('profile.displayName')}
value={user.name}
onChange={handleNameChange}
placeholder={t('auth.namePlaceholder')}
/>
<SettingInput
label={t('profile.email')}
description={t('profile.email')}
value={user.email}
type="email"
onChange={handleEmailChange}
placeholder={t('auth.emailPlaceholder')}
/>
</SettingsSection>
{/* Preferences */}
<SettingsSection
title={t('settings.language')}
icon={<span className="text-2xl"></span>}
description={t('profile.languagePreferencesDescription')}
>
<SettingSelect
label={t('profile.preferredLanguage')}
description={t('profile.languageDescription')}
value={language}
options={[
{ value: 'auto', label: t('profile.autoDetect') },
{ value: 'en', label: 'English' },
{ value: 'fr', label: 'Français' },
{ value: 'es', label: 'Español' },
{ value: 'de', label: 'Deutsch' },
{ value: 'fa', label: 'فارسی' },
{ value: 'it', label: 'Italiano' },
{ value: 'pt', label: 'Português' },
{ value: 'ru', label: 'Русский' },
{ value: 'zh', label: '中文' },
{ value: 'ja', label: '日本語' },
{ value: 'ko', label: '한국어' },
{ value: 'ar', label: 'العربية' },
{ value: 'hi', label: 'हिन्दी' },
{ value: 'nl', label: 'Nederlands' },
{ value: 'pl', label: 'Polski' },
]}
onChange={handleLanguageChange}
/>
<SettingToggle
label={t('profile.showRecentNotes')}
description={t('profile.showRecentNotesDescription')}
checked={showRecentNotes}
onChange={handleRecentNotesChange}
/>
</SettingsSection>
{/* AI Settings Link */}
<div className="p-6 border rounded-lg bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-950 dark:to-pink-950">
<div className="flex items-center gap-4">
<div className="text-4xl"></div>
<div className="flex-1">
<h3 className="font-semibold text-lg mb-1">{t('aiSettings.title')}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
{t('aiSettings.description')}
</p>
</div>
<button
onClick={() => window.location.href = '/settings/ai'}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
{t('nav.configureAI')}
</button>
</div>
</div>
</main>
</div>
</div>
)
}

View File

@@ -0,0 +1,44 @@
import { auth } from '@/auth'
import { redirect } from 'next/navigation'
import { ProfileForm } from './profile-form'
import prisma from '@/lib/prisma'
import { ProfilePageHeader } from '@/components/profile-page-header'
import { AISettingsLinkCard } from './ai-settings-link-card'
export default async function ProfilePage() {
const session = await auth()
if (!session?.user?.id) {
redirect('/login')
}
// Parallel queries
const [user, aiSettings] = await Promise.all([
prisma.user.findUnique({
where: { id: session.user.id },
select: { name: true, email: true, role: true }
}),
prisma.userAISettings.findUnique({
where: { userId: session.user.id }
})
])
if (!user) {
redirect('/login')
}
const userAISettings = {
preferredLanguage: aiSettings?.preferredLanguage || 'auto',
showRecentNotes: aiSettings?.showRecentNotes ?? false
}
return (
<div className="max-w-2xl">
<ProfilePageHeader />
<ProfileForm user={user} userAISettings={userAISettings} />
{/* AI Settings Link */}
<AISettingsLinkCard />
</div>
)
}

View File

@@ -0,0 +1,179 @@
'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { updateProfile, changePassword, updateFontSize, updateShowRecentNotes } from '@/app/actions/profile'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
export function ProfileForm({ user, userAISettings }: { user: any; userAISettings?: any }) {
const router = useRouter()
const [fontSize, setFontSize] = useState(userAISettings?.fontSize || 'medium')
const [isUpdatingFontSize, setIsUpdatingFontSize] = useState(false)
const [showRecentNotes, setShowRecentNotes] = useState(userAISettings?.showRecentNotes ?? false)
const [isUpdatingRecentNotes, setIsUpdatingRecentNotes] = useState(false)
const { t } = useLanguage()
const FONT_SIZES = [
{ value: 'small', label: t('profile.fontSizeSmall'), size: '14px' },
{ value: 'medium', label: t('profile.fontSizeMedium'), size: '16px' },
{ value: 'large', label: t('profile.fontSizeLarge'), size: '18px' },
{ value: 'extra-large', label: t('profile.fontSizeExtraLarge'), size: '20px' },
]
const handleFontSizeChange = async (size: string) => {
setIsUpdatingFontSize(true)
try {
const result = await updateFontSize(size)
if (result?.error) {
toast.error(t('profile.fontSizeUpdateFailed'))
} else {
setFontSize(size)
// Apply font size immediately
applyFontSize(size)
toast.success(t('profile.fontSizeUpdateSuccess'))
}
} catch (error) {
toast.error(t('profile.fontSizeUpdateFailed'))
} finally {
setIsUpdatingFontSize(false)
}
}
const applyFontSize = (size: string) => {
// Base font size in pixels (16px is standard)
const fontSizeMap = {
'small': '14px', // ~87% of 16px
'medium': '16px', // 100% (standard)
'large': '18px', // ~112% of 16px
'extra-large': '20px' // 125% of 16px
}
const fontSizeFactorMap = {
'small': 0.95,
'medium': 1.0,
'large': 1.1,
'extra-large': 1.25
}
const fontSizeValue = fontSizeMap[size as keyof typeof fontSizeMap] || '16px'
const fontSizeFactor = fontSizeFactorMap[size as keyof typeof fontSizeFactorMap] || 1.0
document.documentElement.style.setProperty('--user-font-size', fontSizeValue)
document.documentElement.style.setProperty('--user-font-size-factor', fontSizeFactor.toString())
localStorage.setItem('user-font-size', size)
}
// Apply saved font size on mount
useEffect(() => {
const savedFontSize = localStorage.getItem('user-font-size') || userAISettings?.fontSize || 'medium'
applyFontSize(savedFontSize as string)
}, [])
const handleShowRecentNotesChange = async (enabled: boolean) => {
setIsUpdatingRecentNotes(true)
const previousValue = showRecentNotes
try {
const result = await updateShowRecentNotes(enabled)
if (result?.error) {
toast.error(result.error)
} else {
setShowRecentNotes(enabled)
toast.success(t('profile.recentNotesUpdateSuccess') || 'Paramètre mis à jour')
// Force full page reload to ensure settings are reloaded
window.location.href = '/settings/profile'
}
} catch (error: any) {
setShowRecentNotes(previousValue)
toast.error(error?.message || 'Erreur')
} finally {
setIsUpdatingRecentNotes(false)
}
}
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>{t('profile.title')}</CardTitle>
<CardDescription>{t('profile.description')}</CardDescription>
</CardHeader>
<form action={async (formData) => {
const result = await updateProfile({ name: formData.get('name') as string })
if (result?.error) {
toast.error(t('profile.updateFailed'))
} else {
toast.success(t('profile.updateSuccess'))
}
}}>
<CardContent className="space-y-4">
<div className="space-y-2">
<label htmlFor="name" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{t('profile.displayName')}</label>
<Input id="name" name="name" defaultValue={user.name} />
</div>
<div className="space-y-2">
<label htmlFor="email" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{t('profile.email')}</label>
<Input id="email" value={user.email} disabled className="bg-muted" />
</div>
</CardContent>
<CardFooter>
<Button type="submit">{t('general.save')}</Button>
</CardFooter>
</form>
</Card>
<Card>
<CardHeader>
<CardTitle>{t('profile.changePassword')}</CardTitle>
<CardDescription>{t('profile.changePasswordDescription')}</CardDescription>
</CardHeader>
<form action={async (formData) => {
const result = await changePassword(formData)
if (result?.error) {
const msg = '_form' in result.error
? result.error._form[0]
: result.error.currentPassword?.[0] || result.error.newPassword?.[0] || result.error.confirmPassword?.[0] || t('profile.passwordChangeFailed')
toast.error(msg)
} else {
toast.success(t('profile.passwordChangeSuccess'))
// Reset form manually or redirect
const form = document.querySelector('form#password-form') as HTMLFormElement
form?.reset()
}
}} id="password-form">
<CardContent className="space-y-4">
<div className="space-y-2">
<label htmlFor="currentPassword" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{t('profile.currentPassword')}</label>
<Input id="currentPassword" name="currentPassword" type="password" required />
</div>
<div className="space-y-2">
<label htmlFor="newPassword" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{t('profile.newPassword')}</label>
<Input id="newPassword" name="newPassword" type="password" required minLength={6} />
</div>
<div className="space-y-2">
<label htmlFor="confirmPassword" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{t('profile.confirmPassword')}</label>
<Input id="confirmPassword" name="confirmPassword" type="password" required minLength={6} />
</div>
</CardContent>
<CardFooter>
<Button type="submit">{t('profile.updatePassword')}</Button>
</CardFooter>
</form>
</Card>
</div>
)
}