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