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:
parent
b052900e5d
commit
d3cae85b3d
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 été 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>
|
||||
|
||||
|
||||
53
components/LanguageSelector.tsx
Normal file
53
components/LanguageSelector.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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}</>
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user