Kundenrezensionen jetzt vollständig durch Kund:innen selbst, nicht mehr über Admin

Bisher musste VanVan Bewertungen manuell im CMS eintragen. Jetzt können
angemeldete Kund:innen Rezensionen direkt auf der Produktseite abgeben
(inkl. Herzen-Sternebewertung und Text). Die Durchschnittsbewertung wird
automatisch berechnet und überall live eingebunden: Produktkarten
(shop-weit), Produktdetailseite, Shop-Filter/Sortierung nach Bewertung
und die Kundenstimmen-Sektion auf der Startseite.

Neu dazu: ein Kontobereich fürs Login (Demo-Login per E-Mail), Profilfoto-
Upload, wählbarer Benutzername, und ein Header-Kontomenü, das nur bei
Anmeldung erscheint. Jede Bestellung im Konto-Dashboard hat einen
"Rezension schreiben"-Link, der direkt zur Produktseite führt und dort
per Bestellnummer verknüpft wird; im Profil ist dann sofort sichtbar,
welche Bestellungen schon bewertet wurden.

Technisch als reines Frontend/localStorage-System umgesetzt (kein Backend
in dieser Phase), analog zum bestehenden Warenkorb-Muster. Admin-CMS-Feld
für manuelle Bewertungen entfernt, da Rezensionen jetzt ausschließlich von
Kund:innen kommen (auch aus rechtlichen Gründen: keine Fake-Bewertungen).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
qcigano
2026-08-03 04:17:04 +02:00
co-authored by Claude Sonnet 5
parent d27a858d8f
commit 6f10e41386
28 changed files with 2338 additions and 334 deletions
+56 -20
View File
@@ -8,6 +8,7 @@ import type { Locale } from "./config";
export const ui = {
de: {
nav: { shop: "Shop", sale: "Sale", creative: "Über die Kreative", faq: "FAQ", contact: "Kontakt", account: "Mein Konto", login: "Anmelden" },
accountMenu: { myAccount: "Mein Konto", logout: "Abmelden" },
topbar: "Versandkostenfrei ab 75 € innerhalb DE/AT/CH/LU",
footer: {
tagline: "Handgemacht mit Liebe Kreativität zum Verschenken und Selbstbehalten.",
@@ -38,6 +39,10 @@ export const ui = {
photoNote: "echte Fotos ergänzt VanVan später",
reviewsTitle: "Kundenrezensionen", noReviews: "Noch keine Bewertungen für dieses Produkt — sei die/der Erste!",
reviewCount: (n: number) => `${n} Bewertung${n === 1 ? "" : "en"}`,
writeReviewTitle: "🩵 Deine Rezension", yourRating: "Deine Bewertung", yourReview: "Was hat dir gefallen? (optional)",
yourName: "Dein Name", submitReview: "Rezension veröffentlichen", reviewSaved: "Danke für deine Rezension! ✓",
reviewNeedsRating: "Bitte wähle 15 Herzen aus.", reviewFromOrder: (nr: string) => `Du bewertest deine Bestellung ${nr}.`,
loginHintReview: "Angemeldet als", notLoggedInReview: "Du bist nicht angemeldet — deine Rezension wird mit dem eingegebenen Namen veröffentlicht.",
},
shop: {
title: "Alle Produkte", lead: "Stöbere durch alle handgemachten Stücke filtere und sortiere nach deinen Wünschen.",
@@ -198,15 +203,16 @@ export const ui = {
},
account: {
eyebrow: "Mein Konto", title: "Anmelden oder als Gast bestellen",
todoNote: "Phase 1 reine Oberfläche ohne echtes Login/Registrierung. Bestellhistorie, Sendungsverfolgung, Wunschliste und \"Zuletzt angesehen\" folgen in Phase 2 mit echter Datenbank-Anbindung.",
loginTitle: "Anmelden", email: "E-Mail", password: "Passwort", loginButton: "Anmelden (folgt in Phase 2)", forgotPassword: "Passwort vergessen?",
todoNote: "Phase 1 Demo-Anmeldung ohne echte Passwortprüfung: Jede E-Mail-Adresse funktioniert, das Passwort-Feld ist reine Vorschau. Die Bestellungen im Konto sind Beispieldaten. Eine echte Anmeldung mit Datenbank kommt in Phase 2.",
loginTitle: "Anmelden", email: "E-Mail", password: "Passwort", loginButton: "Anmelden", forgotPassword: "Passwort vergessen?",
newTitle: "Neu hier?", newText: "Lege ein Konto an, um Bestellhistorie, Wunschliste und Sendungsverfolgung an einem Ort zu haben oder bestelle einfach als Gast, ganz ohne Konto.",
guestButton: "Als Gast weiter einkaufen",
previewLink: "👁️ Vorschau: So sieht der Bereich für angemeldete Kund:innen aus",
loginError: "Bitte gib eine E-Mail-Adresse ein.",
},
accountDash: {
eyebrow: "Mein Konto", greeting: "Hallo Marie 🩵", lead: "Schön, dass du wieder da bist. Hier findest du alle deine Bestellungen, deine Wunschliste und deine Daten auf einen Blick.",
previewNote: "VORSCHAU-ANSICHT (Phase 1): So sieht der persönliche Bereich aus, sobald ein:e Kund:in angemeldet ist. Die hier gezeigten Bestellungen und Daten sind reine Beispieldaten zur Gestaltung — es gibt noch kein echtes Login und keine echte Datenbank. Beides kommt in Phase 2 (Cloudflare Worker + D1), danach zeigt diese Seite die echten Daten der jeweils angemeldeten Person.",
eyebrow: "Mein Konto", greeting: (name: string) => `Hallo ${name} 🩵`, lead: "Schön, dass du wieder da bist. Hier findest du alle deine Bestellungen, deine Wunschliste und deine Daten auf einen Blick.",
previewNote: "Phase 1: Deine Anmeldung, dein Profil und deine Rezensionen werden schon jetzt echt auf diesem Gerät gespeichert. Nur die Bestellungen unten sind Beispieldaten zur Gestaltung — echte Bestellungen kommen in Phase 2 mit Zahlungsanbindung und Datenbank (Cloudflare Worker + D1).",
notLoggedIn: "Du bist nicht angemeldet — du wirst zur Anmeldung weitergeleitet …",
logout: "Abmelden",
statOrders: "Bestellungen", statWishlist: "Wunschliste", statOpen: "Offene Sendungen",
ordersTitle: "📦 Meine Bestellungen",
@@ -216,10 +222,14 @@ export const ui = {
wishlistTitle: "🩵 Meine Wunschliste", wishlistEmpty: "Deine Wunschliste ist noch leer — stöbere im Shop und speichere deine Lieblingsstücke mit dem Herz-Symbol.",
recentTitle: "🕘 Zuletzt angesehen",
profileTitle: "👤 Meine Daten", profileName: "Name", profileEmail: "E-Mail", profileAddress: "Lieferadresse", profileEdit: "Daten bearbeiten",
reviewBtn: "🩵 Rezension schreiben", reviewedBadge: "✓ Bewertet",
usernameLabel: "Benutzername", usernameSave: "Speichern", usernameSaved: "Gespeichert ✓",
photoLabel: "Profilfoto", photoChange: "Foto ändern", photoRemove: "Entfernen",
},
},
en: {
nav: { shop: "Shop", sale: "Sale", creative: "About the Maker", faq: "FAQ", contact: "Contact", account: "My Account", login: "Sign in" },
accountMenu: { myAccount: "My Account", logout: "Sign out" },
topbar: "Free shipping from €75 within DE/AT/CH/LU",
footer: {
tagline: "Handmade with love creativity to gift and to keep.",
@@ -250,6 +260,10 @@ export const ui = {
photoNote: "real photos will be added by VanVan later",
reviewsTitle: "Customer reviews", noReviews: "No reviews yet for this product — be the first!",
reviewCount: (n: number) => `${n} review${n === 1 ? "" : "s"}`,
writeReviewTitle: "🩵 Your review", yourRating: "Your rating", yourReview: "What did you like? (optional)",
yourName: "Your name", submitReview: "Publish review", reviewSaved: "Thanks for your review! ✓",
reviewNeedsRating: "Please choose 15 hearts.", reviewFromOrder: (nr: string) => `You're reviewing your order ${nr}.`,
loginHintReview: "Signed in as", notLoggedInReview: "You're not signed in — your review will be published under the name you enter.",
},
shop: {
title: "All products", lead: "Browse all handmade pieces filter and sort to your liking.",
@@ -410,15 +424,16 @@ export const ui = {
},
account: {
eyebrow: "My Account", title: "Sign in or check out as a guest",
todoNote: "Phase 1 interface only, no real login/registration yet. Order history, tracking, wishlist and \"recently viewed\" follow in Phase 2 with a real database connection.",
loginTitle: "Sign in", email: "Email", password: "Password", loginButton: "Sign in (coming in Phase 2)", forgotPassword: "Forgot password?",
todoNote: "Phase 1 demo sign-in without real password checking: any email address works, the password field is a preview only. Orders shown in the account are example data. A real login with a database is coming in Phase 2.",
loginTitle: "Sign in", email: "Email", password: "Password", loginButton: "Sign in", forgotPassword: "Forgot password?",
newTitle: "New here?", newText: "Create an account to keep order history, wishlist and tracking in one place or simply check out as a guest, no account needed.",
guestButton: "Continue shopping as guest",
previewLink: "👁️ Preview: what signed-in customers see",
loginError: "Please enter an email address.",
},
accountDash: {
eyebrow: "My Account", greeting: "Hello Marie 🩵", lead: "Good to see you again. Here you'll find all your orders, your wishlist and your details at a glance.",
previewNote: "PREVIEW (Phase 1): This is what the personal area looks like once a customer is signed in. The orders and details shown here are example data for design purposes only — there is no real login or database yet. Both are coming in Phase 2 (Cloudflare Worker + D1), after which this page will show the signed-in person's real data.",
eyebrow: "My Account", greeting: (name: string) => `Hello ${name} 🩵`, lead: "Good to see you again. Here you'll find all your orders, your wishlist and your details at a glance.",
previewNote: "Phase 1: your sign-in, your profile and your reviews are already genuinely saved on this device. Only the orders below are example data for design purposes — real orders arrive in Phase 2 with payment integration and a database (Cloudflare Worker + D1).",
notLoggedIn: "You're not signed in — redirecting you to sign in …",
logout: "Sign out",
statOrders: "Orders", statWishlist: "Wishlist", statOpen: "Open shipments",
ordersTitle: "📦 My orders",
@@ -428,10 +443,14 @@ export const ui = {
wishlistTitle: "🩵 My wishlist", wishlistEmpty: "Your wishlist is still empty — browse the shop and save your favourites with the heart icon.",
recentTitle: "🕘 Recently viewed",
profileTitle: "👤 My details", profileName: "Name", profileEmail: "Email", profileAddress: "Delivery address", profileEdit: "Edit details",
reviewBtn: "🩵 Write a review", reviewedBadge: "✓ Reviewed",
usernameLabel: "Username", usernameSave: "Save", usernameSaved: "Saved ✓",
photoLabel: "Profile photo", photoChange: "Change photo", photoRemove: "Remove",
},
},
ch: {
nav: { shop: "Shop", sale: "Aktion", creative: "Über d'Kreativi", faq: "FAQ", contact: "Kontakt", account: "Mis Konto", login: "Aamälde" },
accountMenu: { myAccount: "Mis Konto", logout: "Abmälde" },
topbar: "Gratisversand ab 75 € innerhalb DE/AT/CH/LU",
footer: {
tagline: "Vo Hang gmacht mit Härz Sächeli zum Verschänke und Sälber Bhalte.",
@@ -462,6 +481,10 @@ export const ui = {
photoNote: "echti Fotis macht VanVan später drby",
reviewsTitle: "Kundebewärtige", noReviews: "No kei Bewärtige für das Produkt — sei du dr Erschti!",
reviewCount: (n: number) => `${n} Bewärtig${n === 1 ? "" : "e"}`,
writeReviewTitle: "🩵 Dini Bewärtig", yourRating: "Dini Bewärtig", yourReview: "Was hat dir gfalle? (optional)",
yourName: "Din Name", submitReview: "Bewärtig veröffentliche", reviewSaved: "Danke für dini Bewärtig! ✓",
reviewNeedsRating: "Bitte wähl 15 Herzli us.", reviewFromOrder: (nr: string) => `Du bewärtisch dini Bstellig ${nr}.`,
loginHintReview: "Aagmäldet als", notLoggedInReview: "Du bisch nid aagmäldet — dini Bewärtig wird mit em iigäbene Name veröffentlicht.",
},
shop: {
title: "Alli Produkt", lead: "Lueg dr alli vo Hang gmachte Sächeli a filtere und sortiere, wie's dir passt.",
@@ -622,15 +645,16 @@ export const ui = {
},
account: {
eyebrow: "Mis Konto", title: "Aamälde oder als Gast bstelle",
todoNote: "Phase 1 nume d'Oberflächi ohni echts Login/Registrierig. Bstellhistorie, Sändigsverfolgig, Wunschlischte und \"Zletscht aagluegt\" chöme i Phase 2.",
loginTitle: "Aamälde", email: "E-Mail", password: "Passwort", loginButton: "Aamälde (chunnt i Phase 2)", forgotPassword: "Passwort vergässe?",
todoNote: "Phase 1 Demo-Aamäldig ohni echti Passwortprüefig: jedi E-Mail-Adrässe funktioniert, s'Passwort-Fäld isch nume Vorschau. D'Bstellige im Konto sind Bispieldate. E echti Aamäldig mit Datebank chunnt i Phase 2.",
loginTitle: "Aamälde", email: "E-Mail", password: "Passwort", loginButton: "Aamälde", forgotPassword: "Passwort vergässe?",
newTitle: "Neu da?", newText: "Leg es Konto a, für Bstellhistorie, Wunschlischte und Sändigsverfolgig a einem Ort z'ha oder bstell eifach als Gast, ganz ohni Konto.",
guestButton: "Als Gast wyter yichaufe",
previewLink: "👁️ Vorschau: so gseht dr Bereich für aagmäldeti Kund:inne us",
loginError: "Bitte gib e E-Mail-Adrässe i.",
},
accountDash: {
eyebrow: "Mis Konto", greeting: "Hoi Marie 🩵", lead: "Schön, bisch wieder da. Da findsch alli dini Bstellige, dini Wunschlischte und dini Date uf ein Blick.",
previewNote: "VORSCHAU (Phase 1): So gseht dr persönlich Bereich us, sobald öpper aagmäldet isch. D'Bstellige und Date wo da zeigt werde, sind reini Bispieldate für d'Gstaltig — es git no kei echts Login und kei echti Datebank. Beides chunnt i Phase 2 (Cloudflare Worker + D1), denn zeigt die Syte d'echte Date vo dr aagmäldete Person.",
eyebrow: "Mis Konto", greeting: (name: string) => `Hoi ${name} 🩵`, lead: "Schön, bisch wieder da. Da findsch alli dini Bstellige, dini Wunschlischte und dini Date uf ein Blick.",
previewNote: "Phase 1: dini Aamäldig, dis Profil und dini Bewärtige werde scho jetzt echt uf däm Gerät gspeicheret. Nume d'Bstellige unde sind Bispieldate für d'Gstaltig — echti Bstellige chöme i Phase 2 mit Zahligsaabindig und Datebank (Cloudflare Worker + D1).",
notLoggedIn: "Du bisch nid aagmäldet — du wirsch zur Aamäldig wytergleitet …",
logout: "Abmälde",
statOrders: "Bstellige", statWishlist: "Wunschlischte", statOpen: "Offni Sändige",
ordersTitle: "📦 Mini Bstellige",
@@ -640,10 +664,14 @@ export const ui = {
wishlistTitle: "🩵 Mini Wunschlischte", wishlistEmpty: "Dini Wunschlischte isch no leer — lueg im Shop verbi und spicher dini Lieblingssächeli mit em Härz-Symbol.",
recentTitle: "🕘 Zletscht aagluegt",
profileTitle: "👤 Mini Date", profileName: "Name", profileEmail: "E-Mail", profileAddress: "Lieferadrässe", profileEdit: "Date bearbeite",
reviewBtn: "🩵 Bewärtig schrybe", reviewedBadge: "✓ Bewärtet",
usernameLabel: "Benutzername", usernameSave: "Spichere", usernameSaved: "Gspicheret ✓",
photoLabel: "Profilfoto", photoChange: "Foto ändere", photoRemove: "Entferne",
},
},
fr: {
nav: { shop: "Boutique", sale: "Soldes", creative: "À propos de la créatrice", faq: "FAQ", contact: "Contact", account: "Mon compte", login: "Se connecter" },
accountMenu: { myAccount: "Mon compte", logout: "Se déconnecter" },
topbar: "Livraison gratuite dès 75 € en DE/AT/CH/LU",
footer: {
tagline: "Fait main avec amour de la créativité à offrir ou à garder.",
@@ -674,6 +702,10 @@ export const ui = {
photoNote: "VanVan ajoutera de vraies photos plus tard",
reviewsTitle: "Avis clients", noReviews: "Pas encore d'avis pour ce produit — soyez le/la premier(ère) !",
reviewCount: (n: number) => `${n} avis`,
writeReviewTitle: "🩵 Votre avis", yourRating: "Votre note", yourReview: "Qu'avez-vous aimé ? (facultatif)",
yourName: "Votre nom", submitReview: "Publier l'avis", reviewSaved: "Merci pour votre avis ! ✓",
reviewNeedsRating: "Merci de choisir 1 à 5 cœurs.", reviewFromOrder: (nr: string) => `Vous évaluez votre commande ${nr}.`,
loginHintReview: "Connecté(e) en tant que", notLoggedInReview: "Vous n'êtes pas connecté(e) — votre avis sera publié sous le nom que vous indiquez.",
},
shop: {
title: "Tous les produits", lead: "Parcourez toutes les pièces faites main filtrez et triez selon vos envies.",
@@ -834,15 +866,16 @@ export const ui = {
},
account: {
eyebrow: "Mon compte", title: "Se connecter ou commander en tant qu'invité",
todoNote: "Phase 1 interface uniquement, pas encore de vraie connexion/inscription. L'historique des commandes, le suivi, la liste de souhaits et \"vu récemment\" suivront en phase 2.",
loginTitle: "Se connecter", email: "E-mail", password: "Mot de passe", loginButton: "Se connecter (disponible en phase 2)", forgotPassword: "Mot de passe oublié ?",
todoNote: "Phase 1 connexion de démonstration sans vérification réelle du mot de passe : n'importe quelle adresse e-mail fonctionne, le champ mot de passe n'est qu'un aperçu. Les commandes affichées dans le compte sont des exemples. Une vraie connexion avec base de données arrivera en phase 2.",
loginTitle: "Se connecter", email: "E-mail", password: "Mot de passe", loginButton: "Se connecter", forgotPassword: "Mot de passe oublié ?",
newTitle: "Nouveau ici ?", newText: "Créez un compte pour retrouver l'historique des commandes, la liste de souhaits et le suivi au même endroit ou commandez simplement en tant qu'invité, sans compte.",
guestButton: "Continuer mes achats en tant qu'invité",
previewLink: "👁️ Aperçu : l'espace vu par les client(e)s connecté(e)s",
loginError: "Veuillez saisir une adresse e-mail.",
},
accountDash: {
eyebrow: "Mon compte", greeting: "Bonjour Marie 🩵", lead: "Ravie de vous revoir. Retrouvez ici toutes vos commandes, votre liste de souhaits et vos données en un coup d'œil.",
previewNote: "APERÇU (phase 1) : voici à quoi ressemble l'espace personnel une fois le/la client(e) connecté(e). Les commandes et données affichées ici sont des exemples destinés à la conception — il n'existe encore ni connexion réelle ni base de données. Les deux arriveront en phase 2 (Cloudflare Worker + D1) ; cette page affichera alors les données réelles de la personne connectée.",
eyebrow: "Mon compte", greeting: (name: string) => `Bonjour ${name} 🩵`, lead: "Ravie de vous revoir. Retrouvez ici toutes vos commandes, votre liste de souhaits et vos données en un coup d'œil.",
previewNote: "Phase 1 : votre connexion, votre profil et vos avis sont déjà réellement enregistrés sur cet appareil. Seules les commandes ci-dessous sont des exemples destinés à la conception — les vraies commandes arriveront en phase 2 avec le paiement et une base de données (Cloudflare Worker + D1).",
notLoggedIn: "Vous n'êtes pas connecté(e) — redirection vers la connexion …",
logout: "Se déconnecter",
statOrders: "Commandes", statWishlist: "Liste de souhaits", statOpen: "Envois en cours",
ordersTitle: "📦 Mes commandes",
@@ -852,6 +885,9 @@ export const ui = {
wishlistTitle: "🩵 Ma liste de souhaits", wishlistEmpty: "Votre liste de souhaits est encore vide — parcourez la boutique et enregistrez vos coups de cœur avec l'icône en forme de cœur.",
recentTitle: "🕘 Vus récemment",
profileTitle: "👤 Mes données", profileName: "Nom", profileEmail: "E-mail", profileAddress: "Adresse de livraison", profileEdit: "Modifier mes données",
reviewBtn: "🩵 Écrire un avis", reviewedBadge: "✓ Avis publié",
usernameLabel: "Nom d'utilisateur", usernameSave: "Enregistrer", usernameSaved: "Enregistré ✓",
photoLabel: "Photo de profil", photoChange: "Changer la photo", photoRemove: "Supprimer",
},
},
} as const;