add string translations in german
This commit is contained in:
parent
365fcf0cae
commit
5bf3f8694c
Binary file not shown.
@ -19,18 +19,18 @@ export const catalogProducts = [
|
||||
{
|
||||
id: `${perfume.slug}-sample`,
|
||||
slug: perfume.slug,
|
||||
name: `${perfume.name} Sample`,
|
||||
name: `${perfume.name} Probe`,
|
||||
kind: "sample",
|
||||
size_label: "2ml",
|
||||
size_label: "2 ml Probe",
|
||||
price_cents: parsePriceCents(perfume.prices.sample),
|
||||
discovery_credit_cents: 0,
|
||||
},
|
||||
{
|
||||
id: `${perfume.slug}-full`,
|
||||
slug: perfume.slug,
|
||||
name: `${perfume.name} Full Size`,
|
||||
name: `${perfume.name} 50 ml Flakon`,
|
||||
kind: "full_size",
|
||||
size_label: "50ml",
|
||||
size_label: "50 ml Flakon",
|
||||
price_cents: parsePriceCents(perfume.prices.full),
|
||||
discovery_credit_cents: 0,
|
||||
},
|
||||
|
||||
@ -25,7 +25,7 @@ const readBody = async (req) =>
|
||||
req.on("data", (chunk) => {
|
||||
raw += chunk;
|
||||
if (raw.length > 1_000_000) {
|
||||
reject(new Error("Request body too large"));
|
||||
reject(new Error("Anfrage ist zu gross."));
|
||||
req.destroy();
|
||||
}
|
||||
});
|
||||
@ -37,7 +37,7 @@ const readBody = async (req) =>
|
||||
try {
|
||||
resolve(JSON.parse(raw));
|
||||
} catch {
|
||||
reject(new Error("Invalid JSON"));
|
||||
reject(new Error("Ungültiges JSON."));
|
||||
}
|
||||
});
|
||||
req.on("error", reject);
|
||||
@ -130,7 +130,7 @@ const authenticate = (req) => {
|
||||
const requireAuth = (req, res) => {
|
||||
const auth = authenticate(req);
|
||||
if (!auth) {
|
||||
json(res, 401, { error: "Please log in to continue." });
|
||||
json(res, 401, { error: "Bitte melde dich an, um fortzufahren." });
|
||||
return null;
|
||||
}
|
||||
return auth;
|
||||
@ -175,7 +175,7 @@ const getAvailableDiscounts = (userId, rows) => {
|
||||
type: "discovery",
|
||||
creditId: discovery.id,
|
||||
amount_cents: Math.min(discovery.amount_cents, fullTotal),
|
||||
label: "Discovery Set credit",
|
||||
label: "Discovery-Set-Gutschrift",
|
||||
});
|
||||
}
|
||||
|
||||
@ -194,7 +194,7 @@ const getAvailableDiscounts = (userId, rows) => {
|
||||
slug: row.slug,
|
||||
product_id: row.product_id,
|
||||
amount_cents: Math.min(sample.amount_cents, row.price_cents * row.quantity),
|
||||
label: `${row.name.replace(" Full Size", "")} sample credit`,
|
||||
label: `Proben-Gutschrift für ${row.name.replace(" 50 ml Flakon", "")}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -355,13 +355,13 @@ const register = async (req, res) => {
|
||||
const surname = String(body.surname || "").trim();
|
||||
|
||||
if (!firstName || !surname || !email || !password) {
|
||||
json(res, 400, { error: "First name, surname, email, and password are required." });
|
||||
json(res, 400, { error: "Vorname, Nachname, E-Mail und Passwort sind erforderlich." });
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = db.prepare("SELECT id FROM users WHERE email = ?").get(email);
|
||||
if (existing) {
|
||||
json(res, 409, { error: "An account with this email already exists." });
|
||||
json(res, 409, { error: "Mit dieser E-Mail existiert bereits ein Konto." });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -389,7 +389,7 @@ const login = async (req, res) => {
|
||||
const user = db.prepare("SELECT * FROM users WHERE email = ?").get(email);
|
||||
|
||||
if (!user || !verifyPassword(password, user.password_salt, user.password_hash)) {
|
||||
json(res, 401, { error: "Invalid email or password." });
|
||||
json(res, 401, { error: "E-Mail oder Passwort ist ungültig." });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -415,7 +415,7 @@ const patchProfile = async (req, res, user) => {
|
||||
});
|
||||
|
||||
if (!firstName || !surname) {
|
||||
json(res, 400, { error: "First name and surname are required." });
|
||||
json(res, 400, { error: "Vorname und Nachname sind erforderlich." });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -459,7 +459,7 @@ const addCartItem = async (req, res, user) => {
|
||||
const quantity = Math.max(1, Number(body.quantity || 1));
|
||||
const product = db.prepare("SELECT * FROM products WHERE id = ?").get(productId);
|
||||
if (!product) {
|
||||
json(res, 404, { error: "Product not found." });
|
||||
json(res, 404, { error: "Produkt nicht gefunden." });
|
||||
return;
|
||||
}
|
||||
const timestamp = now();
|
||||
@ -472,7 +472,7 @@ const addCartItem = async (req, res, user) => {
|
||||
).run(user.id, productId, quantity, timestamp, timestamp);
|
||||
json(res, 200, {
|
||||
cart: getCart(user.id),
|
||||
message: `${quantity} x ${product.name} added.`,
|
||||
message: `${quantity} x ${product.name} wurde in den Warenkorb gelegt.`,
|
||||
});
|
||||
};
|
||||
|
||||
@ -480,7 +480,7 @@ const patchCartItem = async (req, res, user, productId) => {
|
||||
const body = await readBody(req);
|
||||
const quantity = Number(body.quantity);
|
||||
if (!Number.isFinite(quantity)) {
|
||||
json(res, 400, { error: "Quantity is required." });
|
||||
json(res, 400, { error: "Die Menge ist erforderlich." });
|
||||
return;
|
||||
}
|
||||
if (quantity <= 0) {
|
||||
@ -502,7 +502,7 @@ const checkout = async (req, res, user) => {
|
||||
const body = await readBody(req);
|
||||
const rows = getCartRows(user.id);
|
||||
if (rows.length === 0) {
|
||||
json(res, 400, { error: "Your cart is empty." });
|
||||
json(res, 400, { error: "Dein Warenkorb ist leer." });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -514,11 +514,11 @@ const checkout = async (req, res, user) => {
|
||||
};
|
||||
const paymentMethod = String(body.payment_method || body.paymentMethod || "").trim();
|
||||
if (!addressFields.street_name || !addressFields.house_number || !addressFields.zip_code || !addressFields.city) {
|
||||
json(res, 400, { error: "Street name, house number, ZIP code, and city are required." });
|
||||
json(res, 400, { error: "Strasse, Hausnummer, PLZ und Ort sind erforderlich." });
|
||||
return;
|
||||
}
|
||||
if (!["Bill", "Card", "Twint", "PayPal"].includes(paymentMethod)) {
|
||||
json(res, 400, { error: "Choose a payment method." });
|
||||
json(res, 400, { error: "Wähle eine Zahlungsmethode." });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -611,7 +611,7 @@ const subscribeProduct = async (req, res, user) => {
|
||||
const type = String(body.type || "restock").trim();
|
||||
const product = db.prepare("SELECT * FROM products WHERE id = ?").get(productId);
|
||||
if (!product) {
|
||||
json(res, 404, { error: "Product not found." });
|
||||
json(res, 404, { error: "Produkt nicht gefunden." });
|
||||
return;
|
||||
}
|
||||
db.prepare(
|
||||
@ -620,7 +620,7 @@ const subscribeProduct = async (req, res, user) => {
|
||||
).run(user.id, productId, type, now());
|
||||
json(res, 200, {
|
||||
ok: true,
|
||||
message: `${product.name} ${type} subscription saved.`,
|
||||
message: `${product.name}: Benachrichtigung gespeichert.`,
|
||||
subscriptions: getProductSubscriptions(user.id),
|
||||
});
|
||||
};
|
||||
@ -719,13 +719,13 @@ const route = async (req, res) => {
|
||||
return deleteCartItem(res, user, decodeURIComponent(itemMatch[1]));
|
||||
}
|
||||
|
||||
json(res, 404, { error: "Route not found." });
|
||||
json(res, 404, { error: "Route nicht gefunden." });
|
||||
};
|
||||
|
||||
createServer((req, res) => {
|
||||
route(req, res).catch((error) => {
|
||||
console.error(error);
|
||||
json(res, 500, { error: "Server error." });
|
||||
json(res, 500, { error: "Serverfehler." });
|
||||
});
|
||||
}).listen(PORT, () => {
|
||||
console.log(`Shop API listening on http://localhost:${PORT}`);
|
||||
|
||||
@ -75,17 +75,17 @@ function ProductPurchasePanel({
|
||||
subscribeToProduct,
|
||||
}) {
|
||||
const selectedProductId = `${perfume.slug}-${selectedSize === "sample" ? "sample" : "full"}`;
|
||||
const selectedProductLabel = selectedSize === "sample" ? "Sample" : "Full Size";
|
||||
const selectedProductLabel = selectedSize === "sample" ? "Probe" : "50 ml Flakon";
|
||||
const sizeOptions = [
|
||||
{
|
||||
key: "full",
|
||||
title: "Full Size 50ml",
|
||||
title: "50 ml Flakon",
|
||||
price: perfume.prices.full,
|
||||
note: "Nachkauf, 500+ Anwendungen",
|
||||
},
|
||||
{
|
||||
key: "sample",
|
||||
title: "Sample 2ml",
|
||||
title: "Probe 2 ml",
|
||||
price: perfume.prices.sample,
|
||||
note: "Zum Testen, ca. 20 Anwendungen",
|
||||
},
|
||||
@ -125,7 +125,7 @@ function ProductPurchasePanel({
|
||||
addToCart(
|
||||
selectedProductId,
|
||||
1,
|
||||
`${perfume.name} ${selectedProductLabel} added.`
|
||||
`${perfume.name} ${selectedProductLabel} wurde in den Warenkorb gelegt.`
|
||||
).catch(() => {})
|
||||
}
|
||||
>
|
||||
@ -137,7 +137,7 @@ function ProductPurchasePanel({
|
||||
type="button"
|
||||
onClick={() => subscribeToProduct(selectedProductId, "restock").catch(() => {})}
|
||||
>
|
||||
Restock Update abonnieren
|
||||
Verfügbarkeits-Update abonnieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -145,7 +145,7 @@ function ProductPurchasePanel({
|
||||
<div>
|
||||
<strong>Discovery Set wird angerechnet</strong>
|
||||
<p>
|
||||
Sample- und Set-Guthaben werden beim Full-Size-Kauf automatisch
|
||||
Proben- und Set-Guthaben werden beim 50-ml-Kauf automatisch
|
||||
abgezogen.
|
||||
</p>
|
||||
{discountPreviewCents > 0 && (
|
||||
@ -478,10 +478,10 @@ function ProductTestingCTA({ perfume, addToCart }) {
|
||||
<section className="detail-bottom-cta" data-reveal-group data-on-accent>
|
||||
<div>
|
||||
<span className="eyebrow" data-reveal="fade">Lieber erst testen?</span>
|
||||
<h2 data-reveal="lines">Sample oder Discovery Set.</h2>
|
||||
<h2 data-reveal="lines">Probe oder Discovery Set.</h2>
|
||||
<p data-reveal="fade">
|
||||
Bestelle ein 2ml Sample für CHF 12 oder das komplette Discovery Set mit
|
||||
allen 6 Düften für CHF 48. Beide werden beim späteren Full-Size-Kauf
|
||||
Bestelle eine 2-ml-Probe für CHF 12 oder das komplette Discovery Set mit
|
||||
allen 6 Düften für CHF 48. Beide werden beim späteren 50-ml-Kauf
|
||||
vollständig angerechnet.
|
||||
</p>
|
||||
</div>
|
||||
@ -491,17 +491,17 @@ function ProductTestingCTA({ perfume, addToCart }) {
|
||||
type="button"
|
||||
className="atmos-btn atmos-btn--primary"
|
||||
onClick={() =>
|
||||
addToCart(`${perfume.slug}-sample`, 1, `${perfume.name} Sample added.`).catch(
|
||||
addToCart(`${perfume.slug}-sample`, 1, `${perfume.name} Probe wurde in den Warenkorb gelegt.`).catch(
|
||||
() => {}
|
||||
)
|
||||
}
|
||||
>
|
||||
Sample bestellen — {perfume.prices.sample}
|
||||
Probe bestellen — {perfume.prices.sample}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="atmos-btn atmos-btn--secondary"
|
||||
onClick={() => addToCart("discovery-set", 1, "Discovery Set added.").catch(() => {})}
|
||||
onClick={() => addToCart("discovery-set", 1, "Discovery Set wurde in den Warenkorb gelegt.").catch(() => {})}
|
||||
>
|
||||
Discovery Set — CHF 48
|
||||
</button>
|
||||
|
||||
@ -7,7 +7,7 @@ function SharedNavbar({ variant = "hero", active = "", brandMode = "logo" }) {
|
||||
const { cart, openCart, openProfile, user } = useShop();
|
||||
const { isLight, toggleTheme } = useTheme();
|
||||
const cartCount = cart.total_quantity || 0;
|
||||
const cartLabel = cartCount > 0 ? `Cart ${cartCount}` : "Cart";
|
||||
const cartLabel = cartCount > 0 ? `Warenkorb ${cartCount}` : "Warenkorb";
|
||||
const cartAriaLabel =
|
||||
cartCount > 0
|
||||
? `Warenkorb mit ${cartCount} ${cartCount === 1 ? "Artikel" : "Artikeln"} öffnen`
|
||||
@ -58,7 +58,7 @@ function SharedNavbar({ variant = "hero", active = "", brandMode = "logo" }) {
|
||||
aria-haspopup="dialog"
|
||||
aria-label={user ? "Profil öffnen" : "Anmelden oder Profil öffnen"}
|
||||
>
|
||||
Profile
|
||||
Profil
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -5,19 +5,33 @@ import { useShop } from "../shop/useShop";
|
||||
import "./ShopDrawer.css";
|
||||
|
||||
const paymentMethods = [
|
||||
{ key: "Bill", badge: "BILL", label: "Rechnung" },
|
||||
{ key: "Card", badge: "CARD", label: "Karte" },
|
||||
{ key: "Bill", badge: "RE", label: "Rechnung" },
|
||||
{ key: "Card", badge: "KA", label: "Karte" },
|
||||
{ key: "Twint", badge: "TW", label: "Twint" },
|
||||
{ key: "PayPal", badge: "PP", label: "PayPal" },
|
||||
];
|
||||
|
||||
const notificationLabels = [
|
||||
["drops_enabled", "New Drops"],
|
||||
["restocks_enabled", "Restocks"],
|
||||
["small_batch_enabled", "Small Batch Releases"],
|
||||
["discovery_enabled", "Discovery Set Updates"],
|
||||
["drops_enabled", "Neue Drops"],
|
||||
["restocks_enabled", "Verfügbarkeits-Updates"],
|
||||
["small_batch_enabled", "Small-Batch-Veröffentlichungen"],
|
||||
["discovery_enabled", "Discovery-Set-Updates"],
|
||||
];
|
||||
|
||||
const discoveryStatusLabels = {
|
||||
"No Discount atm": "Noch kein Rabatt",
|
||||
"Discount already used": "Rabatt bereits genutzt",
|
||||
"Discount available": "Rabatt verfügbar",
|
||||
};
|
||||
|
||||
const creditStatusLabels = {
|
||||
available: "verfügbar",
|
||||
used: "genutzt",
|
||||
};
|
||||
|
||||
const formatDiscoveryStatus = (status) => discoveryStatusLabels[status] || status;
|
||||
const formatCreditStatus = (status) => creditStatusLabels[status] || status;
|
||||
|
||||
function Field({
|
||||
label,
|
||||
value,
|
||||
@ -78,7 +92,7 @@ function AuthPanel() {
|
||||
return (
|
||||
<form className="drawer-stack" onSubmit={submit}>
|
||||
<section className="drawer-section">
|
||||
<span className="drawer-eyebrow">{mode === "login" ? "Login" : "Registrieren"}</span>
|
||||
<span className="drawer-eyebrow">{mode === "login" ? "Anmelden" : "Registrieren"}</span>
|
||||
<h2 className="drawer-heading">
|
||||
{mode === "login" ? "Willkommen zurück." : "Konto erstellen."}
|
||||
</h2>
|
||||
@ -130,7 +144,7 @@ function AuthPanel() {
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
>
|
||||
{mode === "login" ? "Login" : "Registrieren"}
|
||||
{mode === "login" ? "Anmelden" : "Registrieren"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -138,7 +152,7 @@ function AuthPanel() {
|
||||
type="button"
|
||||
onClick={() => setMode((current) => (current === "login" ? "register" : "login"))}
|
||||
>
|
||||
{mode === "login" ? "Konto erstellen" : "Bestehenden Account nutzen"}
|
||||
{mode === "login" ? "Konto erstellen" : "Bestehendes Konto nutzen"}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
@ -270,7 +284,7 @@ function CartPanel() {
|
||||
|
||||
<section className="drawer-section drawer-totals">
|
||||
<div className="drawer-totals__row">
|
||||
<span>Subtotal</span>
|
||||
<span>Zwischensumme</span>
|
||||
<strong>{formatChf(cart.subtotal_cents)}</strong>
|
||||
</div>
|
||||
<div className="drawer-totals__row">
|
||||
@ -285,14 +299,14 @@ function CartPanel() {
|
||||
<strong>{formatChf(discount.amount_cents)}</strong>
|
||||
{" — "}
|
||||
{discount.type === "discovery"
|
||||
? "Discovery Set Credit für eine Full Size"
|
||||
: `Sample Credit für ${discount.slug}`}
|
||||
? "Discovery-Set-Gutschrift für einen 50-ml-Flakon"
|
||||
: `Proben-Gutschrift für ${discount.slug}`}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="drawer-totals__row drawer-totals__row--total">
|
||||
<span>Total</span>
|
||||
<span>Gesamt</span>
|
||||
<strong>{formatChf(cart.total_cents)}</strong>
|
||||
</div>
|
||||
</section>
|
||||
@ -370,14 +384,14 @@ function ProfilePanel() {
|
||||
<section className="drawer-section drawer-profile-head">
|
||||
<div>
|
||||
<span className="drawer-eyebrow">Profil</span>
|
||||
<h2 className="drawer-heading">Hi, {user.first_name}.</h2>
|
||||
<h2 className="drawer-heading">Hallo, {user.first_name}.</h2>
|
||||
</div>
|
||||
<button
|
||||
className="atmos-btn atmos-btn--outline atmos-btn--sm"
|
||||
type="button"
|
||||
onClick={logout}
|
||||
>
|
||||
Logout
|
||||
Abmelden
|
||||
</button>
|
||||
</section>
|
||||
|
||||
@ -441,13 +455,13 @@ function ProfilePanel() {
|
||||
</section>
|
||||
|
||||
<section className="drawer-section">
|
||||
<span className="drawer-eyebrow">Discount Status</span>
|
||||
<div className="drawer-status-box">{user.discoveryStatus}</div>
|
||||
<span className="drawer-eyebrow">Rabattstatus</span>
|
||||
<div className="drawer-status-box">{formatDiscoveryStatus(user.discoveryStatus)}</div>
|
||||
{user.sampleCredits?.length > 0 && (
|
||||
<div className="drawer-credit-list">
|
||||
{user.sampleCredits.map((credit) => (
|
||||
<span key={`${credit.slug}-${credit.created_at}`}>
|
||||
{credit.slug}: {credit.status}
|
||||
{credit.slug}: {formatCreditStatus(credit.status)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@ -455,7 +469,7 @@ function ProfilePanel() {
|
||||
</section>
|
||||
|
||||
<section className="drawer-section">
|
||||
<span className="drawer-eyebrow">Drop / Restock Preferences</span>
|
||||
<span className="drawer-eyebrow">Drop- und Verfügbarkeits-Einstellungen</span>
|
||||
<div className="drawer-toggle-grid">
|
||||
{notificationLabels.map(([key, label]) => {
|
||||
const active = !!notifications[key];
|
||||
@ -468,7 +482,7 @@ function ProfilePanel() {
|
||||
onClick={() => togglePreference(key)}
|
||||
>
|
||||
<span>{label}</span>
|
||||
<strong>{active ? "Active" : "Inactive"}</strong>
|
||||
<strong>{active ? "Aktiv" : "Inaktiv"}</strong>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@ -499,13 +513,13 @@ function ProfilePanel() {
|
||||
</section>
|
||||
|
||||
<section className="drawer-section">
|
||||
<span className="drawer-eyebrow">Small Batch Access</span>
|
||||
<span className="drawer-eyebrow">Small-Batch-Zugang</span>
|
||||
<div className="drawer-status-box">
|
||||
{loyalty.unlocked ? "Freigeschaltet" : "Noch gesperrt"}
|
||||
</div>
|
||||
<div className="drawer-requirements">
|
||||
<RequirementRow label="Discovery Set" met={loyalty.hasDiscoverySet} />
|
||||
<RequirementRow label="Full Size" met={loyalty.hasFullSize} />
|
||||
<RequirementRow label="50 ml Flakon" met={loyalty.hasFullSize} />
|
||||
<RequirementRow label="Bestellungen" met={loyalty.purchases >= 3}>
|
||||
{loyalty.purchases}/3
|
||||
</RequirementRow>
|
||||
@ -559,7 +573,7 @@ function ReadBlock({ label, value }) {
|
||||
function ShopDrawer() {
|
||||
const { closePanel, panelOpen, panelType, user } = useShop();
|
||||
|
||||
const drawerLabel = !user ? "Account" : panelType === "cart" ? "Warenkorb" : "Profil";
|
||||
const drawerLabel = !user ? "Konto" : panelType === "cart" ? "Warenkorb" : "Profil";
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -24,7 +24,7 @@ const SUPPORT_TOPICS = {
|
||||
title: "Fragen zum Discovery Set",
|
||||
keywords: ["discovery", "set", "sample", "testen", "proben"],
|
||||
reply:
|
||||
"Das Discovery Set ist dafür gedacht, mehrere Düfte in Ruhe auf der Haut zu testen, bevor du dich für eine Full Size entscheidest. So bekommst du ein genaueres Gefühl für Verlauf, Atmosphäre und Tragbarkeit der einzelnen Kompositionen.",
|
||||
"Das Discovery Set ist dafür gedacht, mehrere Düfte in Ruhe auf der Haut zu testen, bevor du dich für einen 50-ml-Flakon entscheidest. So bekommst du ein genaueres Gefühl für Verlauf, Atmosphäre und Tragbarkeit der einzelnen Kompositionen.",
|
||||
},
|
||||
duftwahl: {
|
||||
id: "duftwahl",
|
||||
|
||||
@ -9,20 +9,20 @@ const DISCOVERY_SET_IMAGE = "/atmos-discovery-set-thumbnail.webp";
|
||||
|
||||
const discoveryPanelFacts = [
|
||||
{ label: "Umfang", value: "6 × 2ml" },
|
||||
{ label: "Gutschrift", value: "CHF 48 werden beim späteren Full-Size-Kauf berücksichtigt." },
|
||||
{ label: "Gutschrift", value: "CHF 48 werden beim späteren 50-ml-Kauf berücksichtigt." },
|
||||
];
|
||||
|
||||
const discoveryBenefits = [
|
||||
{
|
||||
title: "6 × 2ml Samples aller Signature-Düfte",
|
||||
title: "6 × 2-ml-Proben aller Signature-Düfte",
|
||||
text: "Kalter Beton, Schwarzes Benzin, Verbranntes Chrom, Blasse Seide, Weisse Asche und Nasser Marmor.",
|
||||
},
|
||||
{
|
||||
title: "CHF 48 Gutschein automatisch im Set",
|
||||
text: "Nur das erste Discovery Set erstellt den einmaligen Rabatt. Er wird bei einem späteren Full-Size-Kauf automatisch angerechnet.",
|
||||
text: "Nur das erste Discovery Set erstellt den einmaligen Rabatt. Er wird bei einem späteren 50-ml-Kauf automatisch angerechnet.",
|
||||
},
|
||||
{
|
||||
title: "Jedes Sample = ca. 20 Anwendungen",
|
||||
title: "Jede Probe = ca. 20 Anwendungen",
|
||||
text: "Genug, um jeden Duft mehrere Tage im Alltag und in unterschiedlichen Situationen zu testen.",
|
||||
},
|
||||
{
|
||||
@ -45,7 +45,7 @@ const discoverySteps = [
|
||||
{
|
||||
number: "03",
|
||||
title: "Entscheiden",
|
||||
text: "Full-Size bestellen. CHF 48 werden automatisch angerechnet, sofern der Rabatt noch nicht genutzt wurde.",
|
||||
text: "50-ml-Flakon bestellen. CHF 48 werden automatisch angerechnet, sofern der Rabatt noch nicht genutzt wurde.",
|
||||
},
|
||||
];
|
||||
|
||||
@ -53,7 +53,7 @@ const discoveryComparison = [
|
||||
{
|
||||
icon: "×",
|
||||
title: "Traditioneller Weg",
|
||||
text: "CHF 180+ für eine Full Size ausgeben, ohne zu wissen, ob sie wirklich passt. Risiko: Fehlkauf, Überforderung oder ein Duft, der im Regal bleibt.",
|
||||
text: "CHF 180+ für einen 50-ml-Flakon ausgeben, ohne zu wissen, ob er wirklich passt. Risiko: Fehlkauf, Überforderung oder ein Duft, der im Regal bleibt.",
|
||||
},
|
||||
{
|
||||
icon: "✓",
|
||||
@ -88,7 +88,7 @@ function DiscoveryOrderPanel({ onBuy }) {
|
||||
>
|
||||
Kaufen
|
||||
</button>
|
||||
<p>Nur das erste Set erstellt einen einmaligen CHF 48 Full-Size-Rabatt.</p>
|
||||
<p>Nur das erste Set erstellt einen einmaligen CHF 48 Rabatt auf den 50-ml-Kauf.</p>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
@ -105,7 +105,7 @@ function DiscoveryHero({ onBuy }) {
|
||||
<p className="discovery-intro">
|
||||
6 Düfte × 2ml. Jeden Duft eine Woche tragen. Verstehen, was
|
||||
wirklich funktioniert. Ohne Risiko. Der sichere Einstieg in die
|
||||
Welt der Nischendüfte, bevor du dich für eine Full Size entscheidest.
|
||||
Welt der Nischendüfte, bevor du dich für einen 50-ml-Flakon entscheidest.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -215,7 +215,7 @@ function DiscoveryIncludedSection() {
|
||||
<div className="discovery-product-image">
|
||||
<img
|
||||
src={perfume.image}
|
||||
alt={`Atmos ${perfume.name} Sample im Discovery Set`}
|
||||
alt={`Atmos ${perfume.name} Probe im Discovery Set`}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
@ -276,7 +276,7 @@ function DiscoveryFinalCta({ onBuy }) {
|
||||
</span>
|
||||
<h2 data-reveal="lines">Der sichere Einstieg.</h2>
|
||||
<p data-reveal="fade">
|
||||
Kostenloser Versand · 2–3 Werktage · Einmalige Anrechnung bei Full-Size
|
||||
Kostenloser Versand · 2–3 Werktage · Einmalige Anrechnung beim 50-ml-Kauf
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -296,13 +296,13 @@ function DiscoveryFinalCta({ onBuy }) {
|
||||
function DiscoverySetPage() {
|
||||
const { addToCart } = useShop();
|
||||
const buyDiscoverySet = () =>
|
||||
addToCart("discovery-set", 1, "Discovery Set added.").catch(() => {});
|
||||
addToCart("discovery-set", 1, "Discovery Set wurde in den Warenkorb gelegt.").catch(() => {});
|
||||
|
||||
return (
|
||||
<div className="discovery-page">
|
||||
<PageMeta
|
||||
title="Discovery Set"
|
||||
description="6 × 2ml Samples aller atmos-Düfte. Eine Woche pro Duft tragen, verstehen, welcher passt. CHF 48, anrechenbar beim Full-Size-Kauf."
|
||||
description="6 × 2-ml-Proben aller atmos-Düfte. Eine Woche pro Duft tragen, verstehen, welcher passt. CHF 48, anrechenbar beim 50-ml-Kauf."
|
||||
path="/discovery-set"
|
||||
/>
|
||||
<SharedNavbar variant="hero" active="testen" />
|
||||
|
||||
@ -548,7 +548,7 @@ function LandingPage() {
|
||||
DISCOVERY SET
|
||||
</h2>
|
||||
<p data-reveal="fade">
|
||||
{"Alle 6 D\u00FCfte als 2ml Samples."}
|
||||
{"Alle 6 D\u00FCfte als 2-ml-Proben."}
|
||||
<br />
|
||||
Jeden Duft eine Woche tragen.
|
||||
<br />
|
||||
|
||||
@ -31,7 +31,7 @@ function SmallBatchPage() {
|
||||
})
|
||||
.then(async (response) => {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(data.error || "Small Batch request failed.");
|
||||
if (!response.ok) throw new Error(data.error || "Small-Batch-Anfrage fehlgeschlagen.");
|
||||
setState({
|
||||
loading: false,
|
||||
error: "",
|
||||
@ -46,7 +46,7 @@ function SmallBatchPage() {
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Shop API unreachable. Start it with npm run dev and try again.",
|
||||
: "Shop-API nicht erreichbar. Starte sie mit npm run dev und versuche es erneut.",
|
||||
}));
|
||||
});
|
||||
}, [token, user?.loyaltyStatus]);
|
||||
@ -84,11 +84,11 @@ function SmallBatchPage() {
|
||||
{!user ? (
|
||||
<section className="small-panel" data-reveal-group>
|
||||
<span className="small-eyebrow" data-reveal="fade">
|
||||
Login erforderlich
|
||||
Anmeldung erforderlich
|
||||
</span>
|
||||
<h2 data-reveal="fade">Melde dich an, um deinen Zugang zu prüfen.</h2>
|
||||
<p data-reveal="fade">
|
||||
Small Batch Access wird aus deinen abgeschlossenen Bestellungen
|
||||
Der Small-Batch-Zugang wird aus deinen abgeschlossenen Bestellungen
|
||||
berechnet.
|
||||
</p>
|
||||
<div className="atmos-btn-row" data-reveal="fade">
|
||||
@ -107,7 +107,7 @@ function SmallBatchPage() {
|
||||
<div className="small-panel-head">
|
||||
<div>
|
||||
<span className="small-eyebrow" data-reveal="fade">
|
||||
Access Status
|
||||
Zugangsstatus
|
||||
</span>
|
||||
<h2 data-reveal="fade">
|
||||
{loyalty.unlocked ? "Freigeschaltet" : "Noch gesperrt"}
|
||||
@ -117,13 +117,13 @@ function SmallBatchPage() {
|
||||
className={`small-status-pill ${loyalty.unlocked ? "is-unlocked" : ""}`}
|
||||
data-reveal="fade"
|
||||
>
|
||||
{loyalty.unlocked ? "Unlocked" : "Locked"}
|
||||
{loyalty.unlocked ? "Freigeschaltet" : "Gesperrt"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="small-requirements" data-reveal="fade">
|
||||
<Requirement label="Discovery Set" met={loyalty.hasDiscoverySet} />
|
||||
<Requirement label="Full Size" met={loyalty.hasFullSize} />
|
||||
<Requirement label="50 ml Flakon" met={loyalty.hasFullSize} />
|
||||
<Requirement label="Bestellungen" met={loyalty.purchases >= 3}>
|
||||
{loyalty.purchases}/3
|
||||
</Requirement>
|
||||
@ -134,7 +134,7 @@ function SmallBatchPage() {
|
||||
</section>
|
||||
|
||||
{state.error && <p className="small-message">{state.error}</p>}
|
||||
{state.loading && <p className="small-message">Loading access…</p>}
|
||||
{state.loading && <p className="small-message">Zugang wird geladen…</p>}
|
||||
|
||||
{loyalty.unlocked && (
|
||||
<section
|
||||
|
||||
@ -16,7 +16,7 @@ const TOPICS = [
|
||||
},
|
||||
{
|
||||
label: "Beratung",
|
||||
title: "Duft, Sample, Discovery",
|
||||
title: "Duft, Probe, Discovery",
|
||||
text: "Unterstützung bei der Auswahl oder beim Einordnen eines Duftes.",
|
||||
},
|
||||
];
|
||||
@ -28,7 +28,7 @@ const FAQ = [
|
||||
},
|
||||
{
|
||||
q: "Kann ich zuerst testen?",
|
||||
a: "Ja — über das Discovery Set oder ein einzelnes Sample. So erlebst du den Duft auf der Haut.",
|
||||
a: "Ja — über das Discovery Set oder eine einzelne Probe. So erlebst du den Duft auf der Haut.",
|
||||
},
|
||||
{
|
||||
q: "Welcher Duft passt zu mir?",
|
||||
|
||||
@ -22,12 +22,12 @@ const request = async (path, options = {}, token) => {
|
||||
try {
|
||||
response = await fetch(path, { ...options, headers });
|
||||
} catch {
|
||||
throw new Error("Shop API unreachable. Start it with npm run dev and try again.");
|
||||
throw new Error("Shop-API nicht erreichbar. Starte sie mit npm run dev und versuche es erneut.");
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || "Shop request failed.");
|
||||
throw new Error(data.error || "Shop-Anfrage fehlgeschlagen.");
|
||||
}
|
||||
return data;
|
||||
};
|
||||
@ -67,7 +67,7 @@ export function ShopProvider({ children }) {
|
||||
try {
|
||||
return await task();
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Something went wrong.";
|
||||
const message = err instanceof Error ? err.message : "Etwas ist schiefgelaufen.";
|
||||
setError(message);
|
||||
throw err;
|
||||
} finally {
|
||||
@ -103,9 +103,9 @@ export function ShopProvider({ children }) {
|
||||
applyState(data);
|
||||
setPanelType("profile");
|
||||
showToast({
|
||||
title: "Account created",
|
||||
message: "Your profile is ready.",
|
||||
actionLabel: "Profile",
|
||||
title: "Konto erstellt",
|
||||
message: "Dein Profil ist bereit.",
|
||||
actionLabel: "Zum Profil",
|
||||
actionPanel: "profile",
|
||||
});
|
||||
return data;
|
||||
@ -124,9 +124,9 @@ export function ShopProvider({ children }) {
|
||||
applyState(data);
|
||||
setPanelType("profile");
|
||||
showToast({
|
||||
title: "Logged in",
|
||||
message: "Welcome back to atmos.",
|
||||
actionLabel: "Profile",
|
||||
title: "Angemeldet",
|
||||
message: "Willkommen zurück bei atmos.",
|
||||
actionLabel: "Zum Profil",
|
||||
actionPanel: "profile",
|
||||
});
|
||||
return data;
|
||||
@ -153,8 +153,8 @@ export function ShopProvider({ children }) {
|
||||
discounts: [],
|
||||
});
|
||||
showToast({
|
||||
title: "Logged out",
|
||||
message: "Your session has ended.",
|
||||
title: "Abgemeldet",
|
||||
message: "Deine Sitzung wurde beendet.",
|
||||
});
|
||||
}),
|
||||
[run, showToast, token]
|
||||
@ -179,7 +179,7 @@ export function ShopProvider({ children }) {
|
||||
if (!token || !user) {
|
||||
setPanelType("profile");
|
||||
setPanelOpen(true);
|
||||
throw new Error("Please log in before adding products to the cart.");
|
||||
throw new Error("Bitte melde dich an, bevor du Produkte in den Warenkorb legst.");
|
||||
}
|
||||
const data = await request(
|
||||
"/api/cart/items",
|
||||
@ -188,9 +188,9 @@ export function ShopProvider({ children }) {
|
||||
);
|
||||
setCart(data.cart);
|
||||
showToast({
|
||||
title: "Added to cart",
|
||||
title: "Im Warenkorb",
|
||||
message: itemMessage || data.message,
|
||||
actionLabel: "To the cart",
|
||||
actionLabel: "Zum Warenkorb",
|
||||
actionPanel: "cart",
|
||||
});
|
||||
return data;
|
||||
@ -236,9 +236,9 @@ export function ShopProvider({ children }) {
|
||||
);
|
||||
applyState(data);
|
||||
showToast({
|
||||
title: "Checkout complete",
|
||||
message: "Your order was placed and your purchase history was updated.",
|
||||
actionLabel: "Profile",
|
||||
title: "Bestellung abgeschlossen",
|
||||
message: "Deine Bestellung wurde aufgegeben und deine Kaufhistorie aktualisiert.",
|
||||
actionLabel: "Zum Profil",
|
||||
actionPanel: "profile",
|
||||
});
|
||||
return data;
|
||||
@ -256,8 +256,8 @@ export function ShopProvider({ children }) {
|
||||
);
|
||||
setUser(data.user);
|
||||
showToast({
|
||||
title: "Profile saved",
|
||||
message: "Your profile details were updated.",
|
||||
title: "Profil gespeichert",
|
||||
message: "Deine Profildaten wurden aktualisiert.",
|
||||
});
|
||||
return data;
|
||||
}),
|
||||
@ -276,8 +276,8 @@ export function ShopProvider({ children }) {
|
||||
current ? { ...current, notifications: data.notifications } : current
|
||||
);
|
||||
showToast({
|
||||
title: "Preferences saved",
|
||||
message: "Your drop and restock settings were updated.",
|
||||
title: "Einstellungen gespeichert",
|
||||
message: "Deine Drop- und Verfügbarkeits-Einstellungen wurden aktualisiert.",
|
||||
});
|
||||
return data;
|
||||
}),
|
||||
@ -290,7 +290,7 @@ export function ShopProvider({ children }) {
|
||||
if (!token || !user) {
|
||||
setPanelType("profile");
|
||||
setPanelOpen(true);
|
||||
throw new Error("Please log in to subscribe to restock updates.");
|
||||
throw new Error("Bitte melde dich an, um Verfügbarkeits-Updates zu abonnieren.");
|
||||
}
|
||||
const data = await request(
|
||||
"/api/product-subscriptions",
|
||||
@ -301,9 +301,9 @@ export function ShopProvider({ children }) {
|
||||
current ? { ...current, productSubscriptions: data.subscriptions } : current
|
||||
);
|
||||
showToast({
|
||||
title: "Subscription saved",
|
||||
message: "You will receive updates for this product.",
|
||||
actionLabel: "Profile",
|
||||
title: "Abo gespeichert",
|
||||
message: "Du erhältst Updates zu diesem Produkt.",
|
||||
actionLabel: "Zum Profil",
|
||||
actionPanel: "profile",
|
||||
});
|
||||
return data;
|
||||
@ -323,8 +323,8 @@ export function ShopProvider({ children }) {
|
||||
current ? { ...current, productSubscriptions: data.subscriptions } : current
|
||||
);
|
||||
showToast({
|
||||
title: "Subscription removed",
|
||||
message: "That restock update was deleted.",
|
||||
title: "Abo entfernt",
|
||||
message: "Dieses Verfügbarkeits-Update wurde gelöscht.",
|
||||
});
|
||||
return data;
|
||||
}),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user