Update presentation page to dark theme and add language selector

- Update /presentation page to use dark theme (PageHeader, Footer, bg-cyber-darker)
- Add LanguageSelector component to PageHeader for all pages
- Update AuthorPresentationEditor to use dark theme styling
- Update ArticleField and ArticleFormButtons to use dark theme
- Add locale persistence in localStorage
- Update _app.tsx to load saved locale from localStorage
- All pages now support FR/EN language switching
This commit is contained in:
Nicolas Cantu 2025-12-27 23:17:50 +01:00
parent b052900e5d
commit d3cae85b3d
8 changed files with 110 additions and 37 deletions

View File

@ -86,7 +86,7 @@ export function ArticleField(props: ArticleFieldProps) {
const { id, label, value, onChange, required = false, type = 'text', rows, placeholder, helpText, min } =
props
const inputClass =
'w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500'
'w-full px-3 py-2 bg-cyber-darker border border-neon-cyan/20 rounded-lg text-cyber-accent placeholder-cyber-accent/50 focus:ring-2 focus:ring-neon-cyan/50 focus:border-neon-cyan/50 focus:outline-none transition-colors'
const input =
type === 'textarea' ? (
@ -114,11 +114,11 @@ export function ArticleField(props: ArticleFieldProps) {
return (
<div>
<label htmlFor={id} className="block text-sm font-medium text-gray-700 mb-1">
{label} {required && '*'}
<label htmlFor={id} className="block text-sm font-medium text-cyber-accent mb-1">
{label} {required && <span className="text-neon-cyan">*</span>}
</label>
{input}
{helpText && <p className="text-xs text-gray-500 mt-1">{helpText}</p>}
{helpText && <p className="text-xs text-cyber-accent/70 mt-1">{helpText}</p>}
</div>
)
}

View File

@ -1,4 +1,5 @@
import React from 'react'
import { t } from '@/lib/i18n'
interface ArticleFormButtonsProps {
loading: boolean
@ -11,17 +12,17 @@ export function ArticleFormButtons({ loading, onCancel }: ArticleFormButtonsProp
<button
type="submit"
disabled={loading}
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
className="flex-1 px-4 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Publication...' : 'Publier la publication'}
{loading ? t('publish.publishing') : t('publish.button')}
</button>
{onCancel && (
<button
type="button"
onClick={onCancel}
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg font-medium transition-colors"
className="px-4 py-2 bg-cyber-dark hover:bg-cyber-dark/80 text-cyber-accent rounded-lg font-medium transition-colors border border-cyber-accent/30 hover:border-neon-cyan/50"
>
Cancel
{t('common.back')}
</button>
)}
</div>

View File

@ -3,6 +3,7 @@ import { useNostrConnect } from '@/hooks/useNostrConnect'
import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
import { ArticleField } from './ArticleField'
import { ArticleFormButtons } from './ArticleFormButtons'
import { t } from '@/lib/i18n'
interface AuthorPresentationDraft {
presentation: string
@ -14,9 +15,9 @@ const ADDRESS_PATTERN = /^(1|3|bc1)[a-zA-Z0-9]{25,62}$/
function NotConnected() {
return (
<div className="border rounded-lg p-6 bg-gray-50">
<p className="text-center text-gray-600 mb-4">
Connectez-vous avec Nostr pour créer votre article de présentation
<div className="border border-neon-cyan/20 rounded-lg p-6 bg-cyber-dark/50">
<p className="text-center text-cyber-accent mb-4">
{t('presentation.notConnected')}
</p>
</div>
)
@ -24,10 +25,10 @@ function NotConnected() {
function SuccessNotice() {
return (
<div className="border rounded-lg p-6 bg-green-50 border-green-200">
<h3 className="text-lg font-semibold text-green-800 mb-2">Article de présentation créé !</h3>
<p className="text-green-700">
Votre article de présentation a é créé avec succès. Vous pouvez maintenant publier des articles.
<div className="border border-neon-green/50 rounded-lg p-6 bg-neon-green/10">
<h3 className="text-lg font-semibold text-neon-green mb-2">{t('presentation.success')}</h3>
<p className="text-cyber-accent">
{t('presentation.successMessage')}
</p>
</div>
)
@ -38,8 +39,8 @@ function ValidationError({ message }: { message: string | null }) {
return null
}
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-sm text-red-800">{message}</p>
<div className="bg-red-500/10 border border-red-500/50 rounded-lg p-3">
<p className="text-sm text-red-400">{message}</p>
</div>
)
}
@ -143,13 +144,12 @@ function PresentationForm({
onSubmit={(e) => {
void handleSubmit(e)
}}
className="border rounded-lg p-6 bg-white space-y-4"
className="border border-neon-cyan/20 rounded-lg p-6 bg-cyber-dark space-y-4"
>
<div className="mb-6">
<h2 className="text-2xl font-bold mb-2">Créer votre article de présentation</h2>
<p className="text-gray-600 text-sm">
Cet article est obligatoire pour publier sur zapwall4Science. Il contient votre présentation, la description de
votre contenu et votre adresse Bitcoin pour le sponsoring.
<h2 className="text-2xl font-bold mb-2 text-neon-cyan font-mono">{t('presentation.title')}</h2>
<p className="text-cyber-accent text-sm">
{t('presentation.description')}
</p>
</div>

View File

@ -0,0 +1,53 @@
import { useState, useEffect } from 'react'
import { setLocale, getLocale, type Locale } from '@/lib/i18n'
const LOCALE_STORAGE_KEY = 'zapwall-locale'
export function LanguageSelector() {
const [currentLocale, setCurrentLocale] = useState<Locale>(getLocale())
useEffect(() => {
// Load saved locale from localStorage
const savedLocale = typeof window !== 'undefined' ? (localStorage.getItem(LOCALE_STORAGE_KEY) as Locale | null) : null
if (savedLocale && (savedLocale === 'fr' || savedLocale === 'en')) {
setLocale(savedLocale)
setCurrentLocale(savedLocale)
}
}, [])
const handleLocaleChange = (locale: Locale) => {
setLocale(locale)
setCurrentLocale(locale)
if (typeof window !== 'undefined') {
localStorage.setItem(LOCALE_STORAGE_KEY, locale)
}
// Force page reload to update all translations
window.location.reload()
}
return (
<div className="flex items-center gap-2">
<button
onClick={() => handleLocaleChange('fr')}
className={`px-2 py-1 text-xs font-medium rounded transition-colors ${
currentLocale === 'fr'
? 'bg-neon-cyan/20 text-neon-cyan border border-neon-cyan/50'
: 'text-cyber-accent hover:text-neon-cyan border border-transparent hover:border-neon-cyan/30'
}`}
>
FR
</button>
<button
onClick={() => handleLocaleChange('en')}
className={`px-2 py-1 text-xs font-medium rounded transition-colors ${
currentLocale === 'en'
? 'bg-neon-cyan/20 text-neon-cyan border border-neon-cyan/50'
: 'text-cyber-accent hover:text-neon-cyan border border-transparent hover:border-neon-cyan/30'
}`}
>
EN
</button>
</div>
)
}

View File

@ -1,5 +1,6 @@
import Link from 'next/link'
import { ConditionalPublishButton } from './ConditionalPublishButton'
import { LanguageSelector } from './LanguageSelector'
import { t } from '@/lib/i18n'
export function PageHeader() {
@ -10,6 +11,7 @@ export function PageHeader() {
{t('home.title')}
</Link>
<div className="flex items-center gap-4">
<LanguageSelector />
<Link
href="/docs"
className="px-4 py-2 text-cyber-accent hover:text-neon-cyan text-sm font-medium transition-colors border border-cyber-accent/30 hover:border-neon-cyan/50 rounded hover:shadow-glow-cyan"

View File

@ -8,6 +8,10 @@ export function useI18n(locale: Locale = 'fr') {
useEffect(() => {
const load = async () => {
try {
// Get saved locale from localStorage or use provided locale
const savedLocale = typeof window !== 'undefined' ? (localStorage.getItem('zapwall-locale') as Locale | null) : null
const initialLocale = savedLocale && (savedLocale === 'fr' || savedLocale === 'en') ? savedLocale : locale
// Load translations from files in public directory
const frResponse = await fetch('/locales/fr.txt')
const enResponse = await fetch('/locales/en.txt')
@ -22,8 +26,8 @@ export function useI18n(locale: Locale = 'fr') {
await loadTranslations('en', enText)
}
setLocale(locale)
setCurrentLocale(locale)
setLocale(initialLocale)
setCurrentLocale(initialLocale)
setLoaded(true)
} catch (e) {
console.error('Error loading translations:', e)

View File

@ -1,12 +1,28 @@
import '@/styles/globals.css'
import type { AppProps } from 'next/app'
import { useI18n } from '@/hooks/useI18n'
import { getLocale } from '@/lib/i18n'
function I18nProvider({ children }: { children: React.ReactNode }) {
const { loaded } = useI18n('fr') // Default to French, can be made dynamic based on user preference or browser locale
// Get saved locale from localStorage or default to French
const getInitialLocale = (): 'fr' | 'en' => {
if (typeof window === 'undefined') {
return 'fr'
}
const savedLocale = localStorage.getItem('zapwall-locale') as 'fr' | 'en' | null
if (savedLocale === 'fr' || savedLocale === 'en') {
return savedLocale
}
// Try to detect browser locale
const browserLocale = navigator.language.split('-')[0]
return browserLocale === 'en' ? 'en' : 'fr'
}
const initialLocale = getInitialLocale()
const { loaded } = useI18n(initialLocale)
if (!loaded) {
return <div>Loading...</div>
return <div className="min-h-screen bg-cyber-darker flex items-center justify-center text-neon-cyan">Loading...</div>
}
return <>{children}</>

View File

@ -1,7 +1,8 @@
import { useEffect, useCallback } from 'react'
import { useRouter } from 'next/router'
import Head from 'next/head'
import { ConnectButton } from '@/components/ConnectButton'
import { PageHeader } from '@/components/PageHeader'
import { Footer } from '@/components/Footer'
import { AuthorPresentationEditor } from '@/components/AuthorPresentationEditor'
import { useNostrConnect } from '@/hooks/useNostrConnect'
import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
@ -36,26 +37,22 @@ function PresentationLayout() {
content={t('presentation.description')}
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm">
<div className="max-w-4xl mx-auto px-4 py-4 flex justify-between items-center">
<h1 className="text-2xl font-bold text-gray-900">zapwall.fr</h1>
<ConnectButton />
</div>
</header>
<main className="min-h-screen bg-cyber-darker">
<PageHeader />
<div className="max-w-4xl mx-auto px-4 py-8">
<div className="mb-6">
<h2 className="text-3xl font-bold">{t('presentation.title')}</h2>
<p className="text-gray-600 mt-2">
<h2 className="text-3xl font-bold text-neon-cyan font-mono mb-2">{t('presentation.title')}</h2>
<p className="text-cyber-accent mt-2">
{t('presentation.description')}
</p>
</div>
<AuthorPresentationEditor />
</div>
<Footer />
</main>
</>
)