Localization and AI chat widget

This commit is contained in:
HugeFrog24
2026-02-14 01:19:52 +01:00
parent c39800a021
commit 39b463d1a1
31 changed files with 158 additions and 110 deletions

View File

@@ -28,7 +28,6 @@ services:
- SMTP_PORT=587 - SMTP_PORT=587
- SMTP_USER=smtp-user@example.com - SMTP_USER=smtp-user@example.com
- SMTP_PASS=your-smtp-password - SMTP_PASS=your-smtp-password
- SMTP_FROM_NAME="Your Gallery Name"
- SMTP_SECURE=false - SMTP_SECURE=false
- SMTP_REQUIRE_TLS=true - SMTP_REQUIRE_TLS=true
- JWT_SECRET=your-super-secret-jwt-key-change-this-in-production - JWT_SECRET=your-super-secret-jwt-key-change-this-in-production

View File

@@ -2,11 +2,15 @@
"Categories": { "Categories": {
"paintings": { "paintings": {
"title": "Gemälde & Digitale Kunst", "title": "Gemälde & Digitale Kunst",
"description": "Traumhafte Gemälde, die Fantasie und Emotion verbinden" "description": "Traumhafte Gemälde, die Fantasie und Emotion verbinden",
"gallery": "Galerie der Gemälde und digitalen Kunst",
"artwork": "Gemälde"
}, },
"origami": { "origami": {
"title": "Illustrationen", "title": "Illustrationen",
"description": "Handgezeichnete Tuscheillustrationen, inspiriert von Märchen" "description": "Handgezeichnete Tuscheillustrationen, inspiriert von Märchen",
"gallery": "Illustrationen-Galerie",
"artwork": "Illustration"
} }
}, },
"Artworks": { "Artworks": {

View File

@@ -2,11 +2,15 @@
"Categories": { "Categories": {
"paintings": { "paintings": {
"title": "Paintings & Digital Art", "title": "Paintings & Digital Art",
"description": "Dreamlike paintings blending fantasy and emotion" "description": "Dreamlike paintings blending fantasy and emotion",
"gallery": "Paintings & Digital Art gallery",
"artwork": "Paintings & Digital Art artwork"
}, },
"origami": { "origami": {
"title": "Illustrations", "title": "Illustrations",
"description": "Hand-drawn and ink illustrations inspired by fairy tales" "description": "Hand-drawn and ink illustrations inspired by fairy tales",
"gallery": "Illustrations gallery",
"artwork": "Illustration"
} }
}, },
"Artworks": { "Artworks": {

View File

@@ -2,11 +2,15 @@
"Categories": { "Categories": {
"paintings": { "paintings": {
"title": "Pinturas y Arte Digital", "title": "Pinturas y Arte Digital",
"description": "Pinturas oníricas que mezclan fantasía y emoción" "description": "Pinturas oníricas que mezclan fantasía y emoción",
"gallery": "Galería de pinturas y arte digital",
"artwork": "Obra de pintura y arte digital"
}, },
"origami": { "origami": {
"title": "Ilustraciones", "title": "Ilustraciones",
"description": "Ilustraciones a mano y tinta inspiradas en cuentos de hadas" "description": "Ilustraciones a mano y tinta inspiradas en cuentos de hadas",
"gallery": "Galería de ilustraciones",
"artwork": "Ilustración"
} }
}, },
"Artworks": { "Artworks": {

View File

@@ -2,11 +2,15 @@
"Categories": { "Categories": {
"paintings": { "paintings": {
"title": "ნახატები და ციფრული ხელოვნება", "title": "ნახატები და ციფრული ხელოვნება",
"description": "ოცნებისებური ნახატები, რომლებიც ფანტაზიასა და ემოციას აერთიანებს" "description": "ოცნებისებური ნახატები, რომლებიც ფანტაზიასა და ემოციას აერთიანებს",
"gallery": "ნახატებისა და ციფრული ხელოვნების გალერეა",
"artwork": "ნახატი"
}, },
"origami": { "origami": {
"title": "ილუსტრაციები", "title": "ილუსტრაციები",
"description": "ხელით დახატული და მელნის ილუსტრაციები, ზღაპრებით შთაგონებული" "description": "ხელით დახატული და მელნის ილუსტრაციები, ზღაპრებით შთაგონებული",
"gallery": "ილუსტრაციების გალერეა",
"artwork": "ილუსტრაცია"
} }
}, },
"Artworks": { "Artworks": {

View File

@@ -2,11 +2,15 @@
"Categories": { "Categories": {
"paintings": { "paintings": {
"title": "Картины и цифровое искусство", "title": "Картины и цифровое искусство",
"description": "Мечтательные картины, сочетающие фантазию и эмоции" "description": "Мечтательные картины, сочетающие фантазию и эмоции",
"gallery": "Галерея картин и цифрового искусства",
"artwork": "Картина"
}, },
"origami": { "origami": {
"title": "Иллюстрации", "title": "Иллюстрации",
"description": "Рисунки тушью и акварелью, вдохновлённые сказками" "description": "Рисунки тушью и акварелью, вдохновлённые сказками",
"gallery": "Галерея иллюстраций",
"artwork": "Иллюстрация"
} }
}, },
"Artworks": { "Artworks": {

View File

@@ -2,11 +2,15 @@
"Categories": { "Categories": {
"paintings": { "paintings": {
"title": "Tablolar ve Dijital Sanat", "title": "Tablolar ve Dijital Sanat",
"description": "Fantezi ve duyguyu harmanlayan rüya gibi tablolar" "description": "Fantezi ve duyguyu harmanlayan rüya gibi tablolar",
"gallery": "Tablo ve dijital sanat galerisi",
"artwork": "Tablo eseri"
}, },
"origami": { "origami": {
"title": "İllüstrasyonlar", "title": "İllüstrasyonlar",
"description": "Peri masallarından ilham alan el çizimi ve mürekkep illüstrasyonları" "description": "Peri masallarından ilham alan el çizimi ve mürekkep illüstrasyonları",
"gallery": "İllüstrasyon galerisi",
"artwork": "İllüstrasyon"
} }
}, },
"Artworks": { "Artworks": {

View File

@@ -2,19 +2,27 @@
"Categories": { "Categories": {
"origami": { "origami": {
"title": "Origami-Kreationen", "title": "Origami-Kreationen",
"description": "Kunstvolle Papierfaltkunst, die Präzision und Kreativität zeigt" "description": "Kunstvolle Papierfaltkunst, die Präzision und Kreativität zeigt",
"gallery": "Origami-Kreationen-Galerie",
"artwork": "Origami-Kreation"
}, },
"crochet": { "crochet": {
"title": "Häkelarbeiten", "title": "Häkelarbeiten",
"description": "Handgefertigte Häkelstücke, die traditionelle Techniken mit modernem Design verbinden" "description": "Handgefertigte Häkelstücke, die traditionelle Techniken mit modernem Design verbinden",
"gallery": "Häkelarbeiten-Galerie",
"artwork": "Häkelarbeit"
}, },
"paintings": { "paintings": {
"title": "Gemälde", "title": "Gemälde",
"description": "Originale Gemälde, die verschiedene Stile und Techniken erkunden" "description": "Originale Gemälde, die verschiedene Stile und Techniken erkunden",
"gallery": "Gemälde-Galerie",
"artwork": "Gemälde"
}, },
"fingernails": { "fingernails": {
"title": "Nagelkunst", "title": "Nagelkunst",
"description": "Kreative und detaillierte Nagelkunst-Designs" "description": "Kreative und detaillierte Nagelkunst-Designs",
"gallery": "Nagelkunst-Galerie",
"artwork": "Nagelkunst-Werk"
} }
}, },
"Artworks": { "Artworks": {

View File

@@ -2,19 +2,27 @@
"Categories": { "Categories": {
"origami": { "origami": {
"title": "Origami Creations", "title": "Origami Creations",
"description": "Intricate paper folding art pieces showcasing precision and creativity" "description": "Intricate paper folding art pieces showcasing precision and creativity",
"gallery": "Origami Creations gallery",
"artwork": "Origami Creations artwork"
}, },
"crochet": { "crochet": {
"title": "Crochet Items", "title": "Crochet Items",
"description": "Handcrafted crochet pieces combining traditional techniques with modern design" "description": "Handcrafted crochet pieces combining traditional techniques with modern design",
"gallery": "Crochet Items gallery",
"artwork": "Crochet Items artwork"
}, },
"paintings": { "paintings": {
"title": "Paintings", "title": "Paintings",
"description": "Original paintings exploring various styles and techniques" "description": "Original paintings exploring various styles and techniques",
"gallery": "Paintings gallery",
"artwork": "Paintings artwork"
}, },
"fingernails": { "fingernails": {
"title": "Fingernail Art", "title": "Fingernail Art",
"description": "Creative and detailed nail art designs" "description": "Creative and detailed nail art designs",
"gallery": "Fingernail Art gallery",
"artwork": "Fingernail Art artwork"
} }
}, },
"Artworks": { "Artworks": {

View File

@@ -2,19 +2,27 @@
"Categories": { "Categories": {
"origami": { "origami": {
"title": "Creaciones de Origami", "title": "Creaciones de Origami",
"description": "Piezas de arte de plegado de papel intrincadas que muestran precisión y creatividad" "description": "Piezas de arte de plegado de papel intrincadas que muestran precisión y creatividad",
"gallery": "Galería de creaciones de origami",
"artwork": "Obra de origami"
}, },
"crochet": { "crochet": {
"title": "Artículos de Ganchillo", "title": "Artículos de Ganchillo",
"description": "Piezas de ganchillo hechas a mano que combinan técnicas tradicionales con diseño moderno" "description": "Piezas de ganchillo hechas a mano que combinan técnicas tradicionales con diseño moderno",
"gallery": "Galería de artículos de ganchillo",
"artwork": "Obra de ganchillo"
}, },
"paintings": { "paintings": {
"title": "Pinturas", "title": "Pinturas",
"description": "Pinturas originales que exploran varios estilos y técnicas" "description": "Pinturas originales que exploran varios estilos y técnicas",
"gallery": "Galería de pinturas",
"artwork": "Obra de pintura"
}, },
"fingernails": { "fingernails": {
"title": "Arte de Uñas", "title": "Arte de Uñas",
"description": "Diseños creativos y detallados de arte de uñas" "description": "Diseños creativos y detallados de arte de uñas",
"gallery": "Galería de arte de uñas",
"artwork": "Obra de arte de uñas"
} }
}, },
"Artworks": { "Artworks": {

View File

@@ -2,19 +2,27 @@
"Categories": { "Categories": {
"origami": { "origami": {
"title": "ორიგამის ნამუშევრები", "title": "ორიგამის ნამუშევრები",
"description": "რთული ქაღალდის კეცვის ხელოვნების ნაწარმოებები, რომლებიც გამოირჩევა სიზუსტითა და შემოქმედებითობით" "description": "რთული ქაღალდის კეცვის ხელოვნების ნაწარმოებები, რომლებიც გამოირჩევა სიზუსტითა და შემოქმედებითობით",
"gallery": "ორიგამის გალერეა",
"artwork": "ორიგამის ნამუშევარი"
}, },
"crochet": { "crochet": {
"title": "ქსოვის ნაწარმები", "title": "ქსოვის ნაწარმები",
"description": "ხელნაკეთი ქსოვის ნაწარმოები, რომლებიც აერთიანებს ტრადიციულ ტექნიკებს თანამედროვე დიზაინთან" "description": "ხელნაკეთი ქსოვის ნაწარმოები, რომლებიც აერთიანებს ტრადიციულ ტექნიკებს თანამედროვე დიზაინთან",
"gallery": "ქსოვის გალერეა",
"artwork": "ქსოვის ნაწარმი"
}, },
"paintings": { "paintings": {
"title": "ნახატები", "title": "ნახატები",
"description": "ორიგინალური ნახატები, რომლებიც იკვლევს სხვადასხვა სტილსა და ტექნიკას" "description": "ორიგინალური ნახატები, რომლებიც იკვლევს სხვადასხვა სტილსა და ტექნიკას",
"gallery": "ნახატების გალერეა",
"artwork": "ნახატი"
}, },
"fingernails": { "fingernails": {
"title": "ფრჩხილების ხელოვნება", "title": "ფრჩხილების ხელოვნება",
"description": "შემოქმედებითი და დეტალური ფრჩხილების დიზაინები" "description": "შემოქმედებითი და დეტალური ფრჩხილების დიზაინები",
"gallery": "ფრჩხილების ხელოვნების გალერეა",
"artwork": "ფრჩხილების ნამუშევარი"
} }
}, },
"Artworks": { "Artworks": {

View File

@@ -2,19 +2,27 @@
"Categories": { "Categories": {
"origami": { "origami": {
"title": "Оригами", "title": "Оригами",
"description": "Сложные произведения искусства складывания бумаги, демонстрирующие точность и творчество" "description": "Сложные произведения искусства складывания бумаги, демонстрирующие точность и творчество",
"gallery": "Галерея оригами",
"artwork": "Произведение оригами"
}, },
"crochet": { "crochet": {
"title": "Вязаные изделия", "title": "Вязаные изделия",
"description": "Изделия ручной работы, сочетающие традиционные техники с современным дизайном" "description": "Изделия ручной работы, сочетающие традиционные техники с современным дизайном",
"gallery": "Галерея вязаных изделий",
"artwork": "Вязаное изделие"
}, },
"paintings": { "paintings": {
"title": "Картины", "title": "Картины",
"description": "Оригинальные картины, исследующие различные стили и техники" "description": "Оригинальные картины, исследующие различные стили и техники",
"gallery": "Галерея картин",
"artwork": "Картина"
}, },
"fingernails": { "fingernails": {
"title": "Дизайн ногтей", "title": "Дизайн ногтей",
"description": "Креативные и детализированные дизайны для ногтей" "description": "Креативные и детализированные дизайны для ногтей",
"gallery": "Галерея дизайна ногтей",
"artwork": "Произведение дизайна ногтей"
} }
}, },
"Artworks": { "Artworks": {

View File

@@ -2,19 +2,27 @@
"Categories": { "Categories": {
"origami": { "origami": {
"title": "Origami Kreasyonları", "title": "Origami Kreasyonları",
"description": "Hassasiyet ve yaratıcılığı sergileyen karmaşık kağıt katlama sanat eserleri" "description": "Hassasiyet ve yaratıcılığı sergileyen karmaşık kağıt katlama sanat eserleri",
"gallery": "Origami kreasyonları galerisi",
"artwork": "Origami eseri"
}, },
"crochet": { "crochet": {
"title": "Örgü Ürünleri", "title": "Örgü Ürünleri",
"description": "Geleneksel teknikleri modern tasarımla birleştiren el yapımı örgü parçaları" "description": "Geleneksel teknikleri modern tasarımla birleştiren el yapımı örgü parçaları",
"gallery": "Örgü ürünleri galerisi",
"artwork": "Örgü eseri"
}, },
"paintings": { "paintings": {
"title": "Resimler", "title": "Resimler",
"description": "Çeşitli stil ve teknikleri keşfeden orijinal resimler" "description": "Çeşitli stil ve teknikleri keşfeden orijinal resimler",
"gallery": "Resim galerisi",
"artwork": "Resim eseri"
}, },
"fingernails": { "fingernails": {
"title": "Tırnak Sanatı", "title": "Tırnak Sanatı",
"description": "Yaratıcı ve detaylı tırnak sanatı tasarımları" "description": "Yaratıcı ve detaylı tırnak sanatı tasarımları",
"gallery": "Tırnak sanatı galerisi",
"artwork": "Tırnak sanatı eseri"
} }
}, },
"Artworks": { "Artworks": {

View File

@@ -34,7 +34,8 @@
"category": "Kategorie", "category": "Kategorie",
"viewMoreArtworks": "Weitere Kunstwerke ansehen", "viewMoreArtworks": "Weitere Kunstwerke ansehen",
"shareArtwork": "Kunstwerk teilen", "shareArtwork": "Kunstwerk teilen",
"categoryArtwork": "{category} Kunstwerk" "notFoundTitle": "Kunstwerk nicht gefunden",
"notFoundDescription": "Das angeforderte Kunstwerk konnte nicht gefunden werden."
}, },
"Sort": { "Sort": {
"title": "Titel", "title": "Titel",
@@ -42,9 +43,6 @@
"sortByTitle": "Nach Titel sortieren", "sortByTitle": "Nach Titel sortieren",
"sortByYear": "Nach Jahr sortieren" "sortByYear": "Nach Jahr sortieren"
}, },
"Gallery": {
"categoryGallery": "{category} Galerie"
},
"Theme": { "Theme": {
"changeThemeColors": "Theme-Farben ändern", "changeThemeColors": "Theme-Farben ändern",
"themeOptions": "Theme-Optionen", "themeOptions": "Theme-Optionen",

View File

@@ -34,7 +34,8 @@
"category": "Category", "category": "Category",
"viewMoreArtworks": "View More Artworks", "viewMoreArtworks": "View More Artworks",
"shareArtwork": "Share Artwork", "shareArtwork": "Share Artwork",
"categoryArtwork": "{category} Artwork" "notFoundTitle": "Artwork Not Found",
"notFoundDescription": "The requested artwork could not be found."
}, },
"Sort": { "Sort": {
"title": "Title", "title": "Title",
@@ -42,9 +43,6 @@
"sortByTitle": "Sort by title", "sortByTitle": "Sort by title",
"sortByYear": "Sort by year" "sortByYear": "Sort by year"
}, },
"Gallery": {
"categoryGallery": "{category} gallery"
},
"Theme": { "Theme": {
"changeThemeColors": "Change theme colors", "changeThemeColors": "Change theme colors",
"themeOptions": "Theme options", "themeOptions": "Theme options",

View File

@@ -34,7 +34,8 @@
"category": "Categoría", "category": "Categoría",
"viewMoreArtworks": "Ver Más Obras", "viewMoreArtworks": "Ver Más Obras",
"shareArtwork": "Compartir Obra", "shareArtwork": "Compartir Obra",
"categoryArtwork": "Obra de {category}" "notFoundTitle": "Obra no encontrada",
"notFoundDescription": "No se pudo encontrar la obra solicitada."
}, },
"Sort": { "Sort": {
"title": "Título", "title": "Título",
@@ -42,9 +43,6 @@
"sortByTitle": "Ordenar por título", "sortByTitle": "Ordenar por título",
"sortByYear": "Ordenar por año" "sortByYear": "Ordenar por año"
}, },
"Gallery": {
"categoryGallery": "galería de {category}"
},
"Theme": { "Theme": {
"changeThemeColors": "Cambiar colores del tema", "changeThemeColors": "Cambiar colores del tema",
"themeOptions": "Opciones de tema", "themeOptions": "Opciones de tema",

View File

@@ -34,7 +34,8 @@
"category": "კატეგორია", "category": "კატეგორია",
"viewMoreArtworks": "მეტი ნამუშევრის ნახვა", "viewMoreArtworks": "მეტი ნამუშევრის ნახვა",
"shareArtwork": "ნამუშევრის გაზიარება", "shareArtwork": "ნამუშევრის გაზიარება",
"categoryArtwork": "{category} ნამუშევარი" "notFoundTitle": "ნამუშევარი ვერ მოიძებნა",
"notFoundDescription": "მოთხოვნილი ნამუშევარი ვერ მოიძებნა."
}, },
"Sort": { "Sort": {
"title": "სათაური", "title": "სათაური",
@@ -42,9 +43,6 @@
"sortByTitle": "დალაგება სათაურით", "sortByTitle": "დალაგება სათაურით",
"sortByYear": "დალაგება წლით" "sortByYear": "დალაგება წლით"
}, },
"Gallery": {
"categoryGallery": "{category} გალერეა"
},
"Theme": { "Theme": {
"changeThemeColors": "თემის ფერების შეცვლა", "changeThemeColors": "თემის ფერების შეცვლა",
"themeOptions": "თემის პარამეტრები", "themeOptions": "თემის პარამეტრები",

View File

@@ -34,7 +34,8 @@
"category": "Категория", "category": "Категория",
"viewMoreArtworks": "Посмотреть больше произведений", "viewMoreArtworks": "Посмотреть больше произведений",
"shareArtwork": "Поделиться произведением", "shareArtwork": "Поделиться произведением",
"categoryArtwork": "Произведение {category}" "notFoundTitle": "Произведение не найдено",
"notFoundDescription": "Запрашиваемое произведение не найдено."
}, },
"Sort": { "Sort": {
"title": "Название", "title": "Название",
@@ -42,9 +43,6 @@
"sortByTitle": "Сортировать по названию", "sortByTitle": "Сортировать по названию",
"sortByYear": "Сортировать по году" "sortByYear": "Сортировать по году"
}, },
"Gallery": {
"categoryGallery": "Галерея {category}"
},
"Theme": { "Theme": {
"changeThemeColors": "Изменить цвета темы", "changeThemeColors": "Изменить цвета темы",
"themeOptions": "Настройки темы", "themeOptions": "Настройки темы",

View File

@@ -34,7 +34,8 @@
"category": "Kategori", "category": "Kategori",
"viewMoreArtworks": "Daha Fazla Eser Görüntüle", "viewMoreArtworks": "Daha Fazla Eser Görüntüle",
"shareArtwork": "Eseri Paylaş", "shareArtwork": "Eseri Paylaş",
"categoryArtwork": "{category} Eseri" "notFoundTitle": "Eser Bulunamadı",
"notFoundDescription": "İstenen eser bulunamadı."
}, },
"Sort": { "Sort": {
"title": "Başlık", "title": "Başlık",
@@ -42,9 +43,6 @@
"sortByTitle": "Başlığa göre sırala", "sortByTitle": "Başlığa göre sırala",
"sortByYear": "Yıla göre sırala" "sortByYear": "Yıla göre sırala"
}, },
"Gallery": {
"categoryGallery": "{category} galerisi"
},
"Theme": { "Theme": {
"changeThemeColors": "Tema renklerini değiştir", "changeThemeColors": "Tema renklerini değiştir",
"themeOptions": "Tema seçenekleri", "themeOptions": "Tema seçenekleri",

View File

@@ -1,8 +1,8 @@
"use client"; "use client";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "@/i18n/navigation";
import { useTranslations, useLocale } from "next-intl"; import { useTranslations } from "next-intl";
interface LoginState { interface LoginState {
step: "email" | "otp"; step: "email" | "otp";
@@ -17,7 +17,6 @@ interface LoginState {
export default function AdminLogin() { export default function AdminLogin() {
const router = useRouter(); const router = useRouter();
const locale = useLocale();
const t = useTranslations("admin"); const t = useTranslations("admin");
const tCommon = useTranslations("Common"); const tCommon = useTranslations("Common");
const [state, setState] = useState<LoginState>({ const [state, setState] = useState<LoginState>({
@@ -113,7 +112,7 @@ export default function AdminLogin() {
// Redirect to localized admin dashboard // Redirect to localized admin dashboard
setTimeout(() => { setTimeout(() => {
router.push(`/${locale}/admin`); router.push("/admin");
}, 1000); }, 1000);
} else { } else {
updateState({ updateState({
@@ -140,7 +139,7 @@ export default function AdminLogin() {
if (state.configLoading) { if (state.configLoading) {
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"> <div className="min-h-[calc(100vh-var(--header-height,160px))] flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8"> <div className="max-w-md w-full space-y-8">
<div className="text-center"> <div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-accent-600 mx-auto"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-accent-600 mx-auto"></div>
@@ -154,7 +153,7 @@ export default function AdminLogin() {
} }
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"> <div className="min-h-[calc(100vh-var(--header-height,160px))] flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8"> <div className="max-w-md w-full space-y-8">
<div> <div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white"> <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "@/i18n/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { PersonalMessage } from "@/types/config"; import { PersonalMessage } from "@/types/config";
import { ArtistProfileWithTranslations } from "@/types/admin"; import { ArtistProfileWithTranslations } from "@/types/admin";

View File

@@ -45,8 +45,8 @@ export async function generateMetadata({
if (!artwork) { if (!artwork) {
return { return {
title: "Artwork Not Found", title: t("ArtworkDetail.notFoundTitle"),
description: "The requested artwork could not be found.", description: t("ArtworkDetail.notFoundDescription"),
}; };
} }

View File

@@ -1,8 +1,9 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { routing } from "@/i18n/routing";
// Minimal fallback — the next-intl middleware handles root redirect with // Minimal fallback — the next-intl middleware handles root redirect with
// accept-language negotiation and cookie detection. This page should // accept-language negotiation and cookie detection. This page should
// never be reached in normal operation. // never be reached in normal operation.
export default function RootPage() { export default function RootPage() {
redirect("/en"); redirect(`/${routing.defaultLocale}`);
} }

View File

@@ -1,19 +1,18 @@
"use client"; "use client";
import { Artwork } from "@/types/artwork"; import { Artwork } from "@/types/artwork";
import Link from "next/link"; import { Link } from "@/i18n/navigation";
import { useTranslations, useLocale } from "next-intl"; import { useTranslations } from "next-intl";
interface ArtworkCardProps { interface ArtworkCardProps {
artwork: Artwork; artwork: Artwork;
} }
export default function ArtworkCard({ artwork }: ArtworkCardProps) { export default function ArtworkCard({ artwork }: ArtworkCardProps) {
const locale = useLocale();
const t = useTranslations("Artwork"); const t = useTranslations("Artwork");
return ( return (
<Link href={`/${locale}/artwork/${artwork.id}`} className="block w-full"> <Link href={`/artwork/${artwork.id}`} className="block w-full">
<article <article
className="group relative bg-white dark:bg-gray-800 rounded-md overflow-hidden shadow-sm transition-all duration-300 hover:shadow-md hover:-translate-y-0.5 w-full cursor-pointer" className="group relative bg-white dark:bg-gray-800 rounded-md overflow-hidden shadow-sm transition-all duration-300 hover:shadow-md hover:-translate-y-0.5 w-full cursor-pointer"
aria-labelledby={`title-${artwork.id}`} aria-labelledby={`title-${artwork.id}`}

View File

@@ -2,7 +2,7 @@
import { Artwork } from "@/types/artwork"; import { Artwork } from "@/types/artwork";
import { ArrowLeftIcon } from "@heroicons/react/24/outline"; import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/navigation"; import { useRouter } from "@/i18n/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
interface ArtworkDetailViewProps { interface ArtworkDetailViewProps {
@@ -47,9 +47,7 @@ export default function ArtworkDetailView({ artwork }: ArtworkDetailViewProps) {
{/* Image caption */} {/* Image caption */}
<div className="text-center"> <div className="text-center">
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
{t("ArtworkDetail.categoryArtwork", { {t(`Categories.${artwork.category}.artwork`)}
category: t(`Categories.${artwork.category}.title`),
})}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -6,16 +6,16 @@ import { useTranslations } from "next-intl";
interface ArtworkGridProps { interface ArtworkGridProps {
artworks: Artwork[]; artworks: Artwork[];
category: string; categoryId: string;
} }
export default function ArtworkGrid({ artworks, category }: ArtworkGridProps) { export default function ArtworkGrid({ artworks, categoryId }: ArtworkGridProps) {
const t = useTranslations(); const t = useTranslations();
return ( return (
<div <div
className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8 gap-2" className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8 gap-2"
aria-label={t("Gallery.categoryGallery", { category })} aria-label={t(`Categories.${categoryId}.gallery`)}
> >
{artworks.map((artwork) => ( {artworks.map((artwork) => (
<ArtworkCard key={artwork.id} artwork={artwork} /> <ArtworkCard key={artwork.id} artwork={artwork} />

View File

@@ -7,9 +7,9 @@ import {
UserIcon, UserIcon,
ChevronDownIcon, ChevronDownIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import Link from "next/link"; import { useSearchParams } from "next/navigation";
import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { Link, useRouter, usePathname } from "@/i18n/navigation";
import { useTranslations, useLocale } from "next-intl"; import { useTranslations } from "next-intl";
import LanguageSwitcher from "./LanguageSwitcher"; import LanguageSwitcher from "./LanguageSwitcher";
import { useHeaderHeightCSS } from "@/hooks/useHeaderHeight"; import { useHeaderHeightCSS } from "@/hooks/useHeaderHeight";
@@ -41,7 +41,6 @@ export default function Header({
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const locale = useLocale();
const t = useTranslations(); const t = useTranslations();
// Determine if we're in controlled mode (SearchHeader behavior) or autonomous mode (GlobalHeader behavior) // Determine if we're in controlled mode (SearchHeader behavior) or autonomous mode (GlobalHeader behavior)
@@ -122,17 +121,15 @@ export default function Header({
// Autonomous mode: handle navigation ourselves // Autonomous mode: handle navigation ourselves
setInternalLoading(true); setInternalLoading(true);
try { try {
// Check if we're on the localized home page // Check if we're on the home page
const isOnHomePage = pathname === `/${locale}`; const isOnHomePage = pathname === "/";
if (!isOnHomePage) { if (!isOnHomePage) {
// Navigate to localized home with search // Navigate to home with search
if (trimmedSearch) { if (trimmedSearch) {
router.push( router.push(`/?search=${encodeURIComponent(trimmedSearch)}`);
`/${locale}?search=${encodeURIComponent(trimmedSearch)}`,
);
} else { } else {
router.push(`/${locale}`); router.push("/");
} }
} else { } else {
// If we're on the home page, update search params // If we're on the home page, update search params
@@ -142,7 +139,7 @@ export default function Header({
} else { } else {
params.delete("search"); params.delete("search");
} }
router.push(`/${locale}?${params.toString()}`); router.push(`/?${params.toString()}`);
} }
} finally { } finally {
setInternalLoading(false); setInternalLoading(false);
@@ -159,7 +156,7 @@ export default function Header({
setAdminEmail(null); setAdminEmail(null);
setIsUserMenuOpen(false); setIsUserMenuOpen(false);
setIsLoggingOut(false); setIsLoggingOut(false);
router.push(`/${locale}/admin/login`); router.push("/admin/login");
} }
}; };
@@ -196,7 +193,7 @@ export default function Header({
{isUserMenuOpen && ( {isUserMenuOpen && (
<div className="absolute right-0 mt-2 w-48 rounded-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1 z-50"> <div className="absolute right-0 mt-2 w-48 rounded-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1 z-50">
<Link <Link
href={`/${locale}/admin`} href="/admin"
onClick={() => setIsUserMenuOpen(false)} onClick={() => setIsUserMenuOpen(false)}
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
> >
@@ -215,7 +212,7 @@ export default function Header({
</div> </div>
) : ( ) : (
<Link <Link
href={`/${locale}/admin/login`} href="/admin/login"
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-accent-600 dark:hover:text-accent-400 transition-colors" className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-accent-600 dark:hover:text-accent-400 transition-colors"
> >
<UserIcon className="h-4 w-4" /> <UserIcon className="h-4 w-4" />
@@ -228,7 +225,7 @@ export default function Header({
{/* Second row: Title and Description */} {/* Second row: Title and Description */}
<div className="text-center"> <div className="text-center">
<Link href={`/${locale}`} className="inline-block"> <Link href="/" className="inline-block">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-1 hover:text-accent-600 transition-colors"> <h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-1 hover:text-accent-600 transition-colors">
{t("Site.name", { artistName: t("Artist.name") })} {t("Site.name", { artistName: t("Artist.name") })}
</h1> </h1>

View File

@@ -48,7 +48,7 @@ export default function SectionContainer({ section }: SectionContainerProps) {
description={section.description} description={section.description}
onSort={handleSort} onSort={handleSort}
/> />
<ArtworkGrid artworks={sortedArtworks} category={section.title} /> <ArtworkGrid artworks={sortedArtworks} categoryId={section.id} />
</section> </section>
); );
} }

View File

@@ -7,17 +7,15 @@ import { getTenantId, tenantMessagePath } from "@/lib/tenant";
export default getRequestConfig(async ({ requestLocale }) => { export default getRequestConfig(async ({ requestLocale }) => {
// The locale is resolved by the next-intl middleware from the URL prefix, // The locale is resolved by the next-intl middleware from the URL prefix,
// locale cookie, or accept-language negotiation. It should always be present. // locale cookie, or accept-language negotiation. It should always be present.
const requested = await requestLocale; const locale = await requestLocale;
if (!requested || !routing.locales.includes(requested as (typeof routing.locales)[number])) { if (!locale || !routing.locales.includes(locale as (typeof routing.locales)[number])) {
throw new Error( throw new Error(
`[i18n/request] Invalid or missing locale "${requested}". ` + `[i18n/request] Invalid or missing locale "${locale}". ` +
"The next-intl middleware should have resolved this before reaching here.", "The next-intl middleware should have resolved this before reaching here.",
); );
} }
const locale = requested;
// Resolve the tenant from the proxy-injected header. // Resolve the tenant from the proxy-injected header.
// Falls back to the default tenant during static prerendering. // Falls back to the default tenant during static prerendering.
const tenantId = await getTenantId(); const tenantId = await getTenantId();

View File

@@ -54,7 +54,6 @@ const SMTP_CONFIG = {
const ADMIN_EMAIL = process.env.ADMIN_EMAIL!; const ADMIN_EMAIL = process.env.ADMIN_EMAIL!;
const JWT_SECRET = process.env.JWT_SECRET!; const JWT_SECRET = process.env.JWT_SECRET!;
const SMTP_FROM_NAME = process.env.SMTP_FROM_NAME;
// Create nodemailer transporter // Create nodemailer transporter
const transporter = nodemailer.createTransport(SMTP_CONFIG); const transporter = nodemailer.createTransport(SMTP_CONFIG);
@@ -92,10 +91,9 @@ export async function sendOTPEmail(
try { try {
const emailTranslations = await getEmailTranslations(locale); const emailTranslations = await getEmailTranslations(locale);
const localizedSiteName = await getLocalizedSiteName(locale); const localizedSiteName = await getLocalizedSiteName(locale);
const senderName = SMTP_FROM_NAME || localizedSiteName;
const mailOptions = { const mailOptions = {
from: `${senderName} <${SMTP_CONFIG.auth.user}>`, from: `${localizedSiteName} <${SMTP_CONFIG.auth.user}>`,
to: email, to: email,
subject: emailTranslations.subject, subject: emailTranslations.subject,
html: ` html: `

View File

@@ -62,9 +62,8 @@ export function proxy(request: NextRequest) {
new URL(`/${locale}/admin/login`, request.url), new URL(`/${locale}/admin/login`, request.url),
); );
} }
return NextResponse.next({ // Authenticated — fall through to handleI18nRouting so next-intl
request: { headers: requestHeaders }, // resolves the locale. Without this, requestLocale is undefined.
});
} }
// Skip locale handling for API routes and static assets (anything with a // Skip locale handling for API routes and static assets (anything with a