@rovela-ai/sdk 0.19.4 → 0.19.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/components/AdminPageHeader.d.ts.map +1 -1
- package/dist/admin/components/AdminPageHeader.js +9 -2
- package/dist/admin/components/AdminPageHeader.js.map +1 -1
- package/dist/admin/components/AdminUserMenu.d.ts.map +1 -1
- package/dist/admin/components/AdminUserMenu.js +2 -1
- package/dist/admin/components/AdminUserMenu.js.map +1 -1
- package/dist/admin/components/CustomerDetails.d.ts.map +1 -1
- package/dist/admin/components/CustomerDetails.js +2 -1
- package/dist/admin/components/CustomerDetails.js.map +1 -1
- package/dist/admin/components/EmailsManager.d.ts.map +1 -1
- package/dist/admin/components/EmailsManager.js +2 -1
- package/dist/admin/components/EmailsManager.js.map +1 -1
- package/dist/admin/components/LowStockAlert.d.ts.map +1 -1
- package/dist/admin/components/LowStockAlert.js +3 -2
- package/dist/admin/components/LowStockAlert.js.map +1 -1
- package/dist/admin/components/OrderDetails.d.ts.map +1 -1
- package/dist/admin/components/OrderDetails.js +2 -1
- package/dist/admin/components/OrderDetails.js.map +1 -1
- package/dist/admin/components/ProductTable.d.ts.map +1 -1
- package/dist/admin/components/ProductTable.js +4 -3
- package/dist/admin/components/ProductTable.js.map +1 -1
- package/dist/admin/components/RecentOrders.d.ts.map +1 -1
- package/dist/admin/components/RecentOrders.js +2 -1
- package/dist/admin/components/RecentOrders.js.map +1 -1
- package/dist/admin/components/SEOPreview.d.ts +2 -8
- package/dist/admin/components/SEOPreview.d.ts.map +1 -1
- package/dist/admin/components/SEOPreview.js +32 -5
- package/dist/admin/components/SEOPreview.js.map +1 -1
- package/dist/admin/components/SecondaryMetricsRow.d.ts.map +1 -1
- package/dist/admin/components/SecondaryMetricsRow.js +3 -2
- package/dist/admin/components/SecondaryMetricsRow.js.map +1 -1
- package/dist/admin/components/StatsCards.d.ts.map +1 -1
- package/dist/admin/components/StatsCards.js +2 -1
- package/dist/admin/components/StatsCards.js.map +1 -1
- package/docs/changelog/README.md +53 -0
- package/docs/changelog/epoch-01-chrome-consent.md +63 -0
- package/docs/changelog/epoch-02-platform-commerce.md +108 -0
- package/docs/changelog/epoch-03-i18n-files.md +72 -0
- package/docs/changelog/epoch-04-emails.md +65 -0
- package/docs/changelog/epoch-05-admin-experience.md +98 -0
- package/docs/changelog/epoch-06-mobile-integrity.md +92 -0
- package/docs/design/admin-ledger-visual-universe.html +1961 -0
- package/docs/design/admin-sidebar-and-band.html +542 -0
- package/package.json +3 -1
- package/templates/store-template/.claude/skills/add-section/SKILL.md +438 -0
- package/templates/store-template/.claude/skills/build-feature/SKILL.md +201 -0
- package/templates/store-template/.claude/skills/customize-theme/SKILL.md +167 -0
- package/templates/store-template/.claude/skills/debug-errors/SKILL.md +68 -0
- package/templates/store-template/.claude/skills/frontend-design/SKILL.md +176 -0
- package/templates/store-template/.claude/skills/generate-images/SKILL.md +115 -0
- package/templates/store-template/.claude/skills/manage-products/SKILL.md +187 -0
- package/templates/store-template/.claude/skills/override-sdk-component/SKILL.md +154 -0
- package/templates/store-template/.env.example +50 -0
- package/templates/store-template/.mcp.json +8 -0
- package/templates/store-template/.rovela/blueprint.example.json +190 -0
- package/templates/store-template/CLAUDE.md +14 -0
- package/templates/store-template/app/LayoutContent.tsx +106 -0
- package/templates/store-template/app/[...path]/page.tsx +79 -0
- package/templates/store-template/app/account/addresses/page.tsx +45 -0
- package/templates/store-template/app/account/layout.tsx +17 -0
- package/templates/store-template/app/account/orders/[id]/invoice/page.tsx +277 -0
- package/templates/store-template/app/account/orders/[id]/page.tsx +725 -0
- package/templates/store-template/app/account/orders/page.tsx +153 -0
- package/templates/store-template/app/account/page.tsx +184 -0
- package/templates/store-template/app/account/profile/page.tsx +434 -0
- package/templates/store-template/app/admin/(dashboard)/account/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/events/page.tsx +14 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/layout.tsx +48 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/visitors/page.tsx +14 -0
- package/templates/store-template/app/admin/(dashboard)/categories/[id]/page.tsx +38 -0
- package/templates/store-template/app/admin/(dashboard)/categories/new/page.tsx +35 -0
- package/templates/store-template/app/admin/(dashboard)/categories/page.tsx +223 -0
- package/templates/store-template/app/admin/(dashboard)/customers/[id]/page.tsx +26 -0
- package/templates/store-template/app/admin/(dashboard)/customers/page.tsx +33 -0
- package/templates/store-template/app/admin/(dashboard)/discounts/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/emails/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/files/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/layout.tsx +44 -0
- package/templates/store-template/app/admin/(dashboard)/loading.tsx +88 -0
- package/templates/store-template/app/admin/(dashboard)/locations/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/orders/[id]/page.tsx +66 -0
- package/templates/store-template/app/admin/(dashboard)/orders/page.tsx +26 -0
- package/templates/store-template/app/admin/(dashboard)/page.tsx +146 -0
- package/templates/store-template/app/admin/(dashboard)/payments/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/products/[id]/page.tsx +38 -0
- package/templates/store-template/app/admin/(dashboard)/products/new/page.tsx +35 -0
- package/templates/store-template/app/admin/(dashboard)/products/page.tsx +31 -0
- package/templates/store-template/app/admin/(dashboard)/settings/page.tsx +46 -0
- package/templates/store-template/app/admin/(dashboard)/shipping/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/tax/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/users/[id]/page.tsx +114 -0
- package/templates/store-template/app/admin/(dashboard)/users/page.tsx +16 -0
- package/templates/store-template/app/admin/accept-invite/page.tsx +36 -0
- package/templates/store-template/app/admin/forgot-password/page.tsx +13 -0
- package/templates/store-template/app/admin/layout.tsx +47 -0
- package/templates/store-template/app/admin/login/page.tsx +528 -0
- package/templates/store-template/app/admin/reset-password/page.tsx +36 -0
- package/templates/store-template/app/api/account/addresses/[id]/route.ts +10 -0
- package/templates/store-template/app/api/account/addresses/route.ts +10 -0
- package/templates/store-template/app/api/account/change-password/route.ts +107 -0
- package/templates/store-template/app/api/account/orders/[id]/refund/route.ts +11 -0
- package/templates/store-template/app/api/account/orders/[id]/return/route.ts +14 -0
- package/templates/store-template/app/api/account/orders/[id]/route.ts +42 -0
- package/templates/store-template/app/api/account/orders/route.ts +29 -0
- package/templates/store-template/app/api/account/profile/route.ts +165 -0
- package/templates/store-template/app/api/admin/accept-invite/route.ts +11 -0
- package/templates/store-template/app/api/admin/analytics/events/route.ts +9 -0
- package/templates/store-template/app/api/admin/analytics/route.ts +10 -0
- package/templates/store-template/app/api/admin/analytics/visitors/route.ts +9 -0
- package/templates/store-template/app/api/admin/branding/favicon/route.ts +9 -0
- package/templates/store-template/app/api/admin/branding/logo/route.ts +9 -0
- package/templates/store-template/app/api/admin/categories/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/categories/route.ts +9 -0
- package/templates/store-template/app/api/admin/check/route.ts +10 -0
- package/templates/store-template/app/api/admin/customer-groups/[id]/route.ts +10 -0
- package/templates/store-template/app/api/admin/customer-groups/route.ts +10 -0
- package/templates/store-template/app/api/admin/customers/[id]/addresses/[addressId]/route.ts +15 -0
- package/templates/store-template/app/api/admin/customers/[id]/addresses/route.ts +15 -0
- package/templates/store-template/app/api/admin/customers/[id]/route.ts +11 -0
- package/templates/store-template/app/api/admin/customers/bulk/route.ts +8 -0
- package/templates/store-template/app/api/admin/customers/route.ts +9 -0
- package/templates/store-template/app/api/admin/discounts/[id]/route.ts +11 -0
- package/templates/store-template/app/api/admin/discounts/route.ts +10 -0
- package/templates/store-template/app/api/admin/emails/preview/route.ts +4 -0
- package/templates/store-template/app/api/admin/emails/route.ts +5 -0
- package/templates/store-template/app/api/admin/emails/test/route.ts +4 -0
- package/templates/store-template/app/api/admin/files/route.ts +8 -0
- package/templates/store-template/app/api/admin/forgot-password/route.ts +7 -0
- package/templates/store-template/app/api/admin/linked-customer/route.ts +11 -0
- package/templates/store-template/app/api/admin/locations/[id]/route.ts +10 -0
- package/templates/store-template/app/api/admin/locations/route.ts +10 -0
- package/templates/store-template/app/api/admin/me/password/route.ts +13 -0
- package/templates/store-template/app/api/admin/me/route.ts +11 -0
- package/templates/store-template/app/api/admin/orders/[id]/refund/route.ts +7 -0
- package/templates/store-template/app/api/admin/orders/[id]/return/approve/route.ts +7 -0
- package/templates/store-template/app/api/admin/orders/[id]/return/reject/route.ts +7 -0
- package/templates/store-template/app/api/admin/orders/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/orders/[id]/ship-with-shippo/route.ts +6 -0
- package/templates/store-template/app/api/admin/orders/[id]/shippo-rates/route.ts +6 -0
- package/templates/store-template/app/api/admin/orders/bulk/route.ts +8 -0
- package/templates/store-template/app/api/admin/orders/route.ts +9 -0
- package/templates/store-template/app/api/admin/payment-methods/[id]/route.ts +10 -0
- package/templates/store-template/app/api/admin/payment-methods/route.ts +10 -0
- package/templates/store-template/app/api/admin/products/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/products/bulk/route.ts +9 -0
- package/templates/store-template/app/api/admin/products/route.ts +9 -0
- package/templates/store-template/app/api/admin/products/stats/route.ts +10 -0
- package/templates/store-template/app/api/admin/reset-password/route.ts +10 -0
- package/templates/store-template/app/api/admin/settings/route.ts +11 -0
- package/templates/store-template/app/api/admin/setup/route.ts +10 -0
- package/templates/store-template/app/api/admin/setup-guide/manual-mark/route.ts +13 -0
- package/templates/store-template/app/api/admin/setup-guide/route.ts +10 -0
- package/templates/store-template/app/api/admin/shipping/carriers/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/carriers/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/rates/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/rates/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/shippo/api-key/route.ts +1 -0
- package/templates/store-template/app/api/admin/shipping/shippo/validate/route.ts +1 -0
- package/templates/store-template/app/api/admin/shipping/zones/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/zones/route.ts +9 -0
- package/templates/store-template/app/api/admin/stats/route.ts +9 -0
- package/templates/store-template/app/api/admin/stripe-status/route.ts +1 -0
- package/templates/store-template/app/api/admin/tax-zones/[id]/route.ts +7 -0
- package/templates/store-template/app/api/admin/tax-zones/route.ts +7 -0
- package/templates/store-template/app/api/admin/users/[id]/route.ts +14 -0
- package/templates/store-template/app/api/admin/users/route.ts +12 -0
- package/templates/store-template/app/api/admin-auth/[...nextauth]/route.ts +17 -0
- package/templates/store-template/app/api/analytics/track/route.ts +12 -0
- package/templates/store-template/app/api/auth/[...nextauth]/route.ts +19 -0
- package/templates/store-template/app/api/auth/forgot-password/route.ts +9 -0
- package/templates/store-template/app/api/auth/register/route.ts +9 -0
- package/templates/store-template/app/api/auth/resend-verification/route.ts +9 -0
- package/templates/store-template/app/api/auth/reset-password/route.ts +10 -0
- package/templates/store-template/app/api/auth/verify-email/route.ts +10 -0
- package/templates/store-template/app/api/categories/route.ts +10 -0
- package/templates/store-template/app/api/checkout/countries/route.ts +7 -0
- package/templates/store-template/app/api/checkout/discount/route.ts +9 -0
- package/templates/store-template/app/api/checkout/manual/route.ts +10 -0
- package/templates/store-template/app/api/checkout/payment-methods/route.ts +8 -0
- package/templates/store-template/app/api/checkout/pickup-locations/route.ts +8 -0
- package/templates/store-template/app/api/checkout/route.ts +10 -0
- package/templates/store-template/app/api/checkout/shipping/route.ts +21 -0
- package/templates/store-template/app/api/checkout/status/route.ts +10 -0
- package/templates/store-template/app/api/contact/route.ts +164 -0
- package/templates/store-template/app/api/media/presign/route.ts +7 -0
- package/templates/store-template/app/api/media/route.ts +7 -0
- package/templates/store-template/app/api/products/[slug]/route.ts +10 -0
- package/templates/store-template/app/api/products/route.ts +10 -0
- package/templates/store-template/app/api/store/settings/route.ts +12 -0
- package/templates/store-template/app/api/webhooks/stripe/route.ts +63 -0
- package/templates/store-template/app/auth/forgot-password/page.tsx +38 -0
- package/templates/store-template/app/auth/layout.tsx +17 -0
- package/templates/store-template/app/auth/reset-password/page.tsx +81 -0
- package/templates/store-template/app/auth/signin/page.tsx +62 -0
- package/templates/store-template/app/auth/signup/page.tsx +38 -0
- package/templates/store-template/app/auth/verify-email/page.tsx +185 -0
- package/templates/store-template/app/cart/page.tsx +137 -0
- package/templates/store-template/app/checkout/page.tsx +88 -0
- package/templates/store-template/app/checkout/success/page.tsx +50 -0
- package/templates/store-template/app/contact/layout.tsx +24 -0
- package/templates/store-template/app/contact/page.tsx +321 -0
- package/templates/store-template/app/faq/page.tsx +153 -0
- package/templates/store-template/app/globals.css +99 -0
- package/templates/store-template/app/layout.tsx +269 -0
- package/templates/store-template/app/not-found.tsx +35 -0
- package/templates/store-template/app/page.tsx +23 -0
- package/templates/store-template/app/privacy/page.tsx +281 -0
- package/templates/store-template/app/products/[slug]/ProductDetailClient.tsx +179 -0
- package/templates/store-template/app/products/[slug]/page.tsx +149 -0
- package/templates/store-template/app/products/layout.tsx +26 -0
- package/templates/store-template/app/products/page.tsx +102 -0
- package/templates/store-template/app/robots.ts +25 -0
- package/templates/store-template/app/sitemap.ts +76 -0
- package/templates/store-template/app/terms/page.tsx +313 -0
- package/templates/store-template/components/PathSyncBridge.tsx +61 -0
- package/templates/store-template/components/Providers.tsx +37 -0
- package/templates/store-template/components/layout/Footer.tsx +150 -0
- package/templates/store-template/components/layout/Header.tsx +162 -0
- package/templates/store-template/components/sections/.gitkeep +7 -0
- package/templates/store-template/components/sections/CTA.tsx +51 -0
- package/templates/store-template/components/sections/Features.tsx +91 -0
- package/templates/store-template/components/sections/Hero.tsx +85 -0
- package/templates/store-template/eslint.config.mjs +18 -0
- package/templates/store-template/lib/store-config.ts +77 -0
- package/templates/store-template/lib/store-policy.ts +68 -0
- package/templates/store-template/lib/utils.ts +21 -0
- package/templates/store-template/next-env.d.ts +6 -0
- package/templates/store-template/next.config.js +105 -0
- package/templates/store-template/package.json +32 -0
- package/templates/store-template/postcss.config.js +6 -0
- package/templates/store-template/proxy.ts +150 -0
- package/templates/store-template/public/rovela-logo.png +0 -0
- package/templates/store-template/tailwind.config.ts +74 -0
- package/templates/store-template/tsconfig.json +41 -0
- package/templates/store-template/types/next-auth.d.ts +32 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checkout Page
|
|
5
|
+
*
|
|
6
|
+
* Checkout behavior depends on store settings:
|
|
7
|
+
* - Shipping enabled: Full pre-checkout flow (Address → Shipping → Review → Stripe)
|
|
8
|
+
* - Shipping disabled: Simple checkout with direct Stripe redirect (no shipping step)
|
|
9
|
+
*
|
|
10
|
+
* When shipping is enabled, uses CheckoutFlow component which:
|
|
11
|
+
* 1. Collects shipping address via ShippingForm
|
|
12
|
+
* 2. Fetches shipping options via POST /api/checkout/shipping
|
|
13
|
+
* 3. Shows shipping options grouped by carrier via ShippingOptions
|
|
14
|
+
* 4. Displays order summary with totals
|
|
15
|
+
* 5. Redirects to Stripe Checkout for payment (card only, address pre-filled)
|
|
16
|
+
*
|
|
17
|
+
* Both manual (zone-based) and Shippo (carrier API) modes are supported.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useEffect } from 'react'
|
|
21
|
+
import { useRouter } from 'next/navigation'
|
|
22
|
+
import { useAuth } from '@rovela-ai/sdk/auth'
|
|
23
|
+
import { useCart } from '@rovela-ai/sdk/cart'
|
|
24
|
+
import { CheckoutFlow, CheckoutButton } from '@rovela-ai/sdk/checkout'
|
|
25
|
+
import { useStoreSettings, useT, useLocalePath } from '@rovela-ai/sdk/core'
|
|
26
|
+
|
|
27
|
+
export default function CheckoutPage() {
|
|
28
|
+
const t = useT()
|
|
29
|
+
const localePath = useLocalePath()
|
|
30
|
+
const router = useRouter()
|
|
31
|
+
// `useAuth()` filters out admin sessions (returns null if the signed-in
|
|
32
|
+
// user is an admin, not a customer). Using it instead of raw `useSession`
|
|
33
|
+
// prevents an admin's email from being prefilled as the "customer email"
|
|
34
|
+
// at checkout — which would otherwise leak into the Stripe session.
|
|
35
|
+
const { customer } = useAuth()
|
|
36
|
+
const { items } = useCart()
|
|
37
|
+
const { settings, isLoading: isSettingsLoading } = useStoreSettings()
|
|
38
|
+
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)
|
|
39
|
+
|
|
40
|
+
// Check if shipping is enabled
|
|
41
|
+
const shippingEnabled = settings?.shippingEnabled ?? true
|
|
42
|
+
|
|
43
|
+
// Redirect to cart if empty
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (itemCount === 0) {
|
|
46
|
+
router.push(localePath('/cart'))
|
|
47
|
+
}
|
|
48
|
+
}, [itemCount, router, localePath])
|
|
49
|
+
|
|
50
|
+
// Don't render checkout if cart is empty
|
|
51
|
+
if (itemCount === 0) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// When shipping is disabled, show simple checkout (direct to Stripe)
|
|
56
|
+
// This handles the edge case where user navigates directly to /checkout
|
|
57
|
+
if (!isSettingsLoading && !shippingEnabled) {
|
|
58
|
+
return (
|
|
59
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-md">
|
|
60
|
+
<h1 className="text-2xl font-bold font-heading mb-4 text-center">{t('checkoutPage.title')}</h1>
|
|
61
|
+
<p className="text-muted-foreground mb-8 text-center">
|
|
62
|
+
{t('checkoutPage.reviewAndPay')}
|
|
63
|
+
</p>
|
|
64
|
+
<CheckoutButton size="lg" className="w-full">
|
|
65
|
+
{t('checkout.proceedToPayment')}
|
|
66
|
+
</CheckoutButton>
|
|
67
|
+
<button
|
|
68
|
+
onClick={() => router.push(localePath('/cart'))}
|
|
69
|
+
className="w-full mt-4 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
70
|
+
>
|
|
71
|
+
← {t('checkoutPage.returnToCart')}
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 max-w-6xl">
|
|
79
|
+
<h1 className="text-2xl font-bold font-heading mb-8">{t('checkoutPage.title')}</h1>
|
|
80
|
+
<CheckoutFlow
|
|
81
|
+
customerEmail={customer?.email ?? undefined}
|
|
82
|
+
customerId={customer?.id ?? undefined}
|
|
83
|
+
showOrderSummary={true}
|
|
84
|
+
onCancel={() => router.push(localePath('/cart'))}
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checkout Success Page
|
|
5
|
+
*
|
|
6
|
+
* Displayed after a successful checkout — Stripe redirect OR a manual-payment
|
|
7
|
+
* order (`?orderId=` set by the manual checkout path). Clears the cart and
|
|
8
|
+
* shows the order confirmation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Suspense, useEffect, useRef } from 'react'
|
|
12
|
+
import { useSearchParams } from 'next/navigation'
|
|
13
|
+
import { useCart } from '@rovela-ai/sdk/cart'
|
|
14
|
+
import { CheckoutSuccess } from '@rovela-ai/sdk/checkout'
|
|
15
|
+
import { useLocalePath } from '@rovela-ai/sdk/core'
|
|
16
|
+
|
|
17
|
+
function CheckoutSuccessContent() {
|
|
18
|
+
const localePath = useLocalePath()
|
|
19
|
+
const { clearCart } = useCart()
|
|
20
|
+
const hasClearedCart = useRef(false)
|
|
21
|
+
const searchParams = useSearchParams()
|
|
22
|
+
const orderId = searchParams.get('orderId') || undefined
|
|
23
|
+
|
|
24
|
+
// Clear cart on mount (only once)
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!hasClearedCart.current) {
|
|
27
|
+
clearCart()
|
|
28
|
+
hasClearedCart.current = true
|
|
29
|
+
}
|
|
30
|
+
}, [clearCart])
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
34
|
+
<CheckoutSuccess
|
|
35
|
+
orderId={orderId}
|
|
36
|
+
continueShoppingHref={localePath('/products')}
|
|
37
|
+
viewOrderHref={localePath(orderId ? `/account/orders/${orderId}` : '/account/orders')}
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default function CheckoutSuccessPage() {
|
|
44
|
+
// useSearchParams requires a Suspense boundary for static prerender.
|
|
45
|
+
return (
|
|
46
|
+
<Suspense fallback={null}>
|
|
47
|
+
<CheckoutSuccessContent />
|
|
48
|
+
</Suspense>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { localizedAlternates, getRequestLocale } from '@rovela-ai/sdk/core/server'
|
|
2
|
+
import { getT } from '@rovela-ai/sdk/core'
|
|
3
|
+
import type { Metadata } from 'next'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Server layout for /contact. The page is a client component and can't export
|
|
7
|
+
* metadata, so this thin wrapper supplies the self-referencing canonical
|
|
8
|
+
* (resolved against the root layout's metadataBase).
|
|
9
|
+
*/
|
|
10
|
+
export async function generateMetadata() {
|
|
11
|
+
const { locale } = await getRequestLocale()
|
|
12
|
+
return {
|
|
13
|
+
title: getT(locale)('contactPage.title'),
|
|
14
|
+
alternates: await localizedAlternates('/contact'),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function ContactLayout({
|
|
19
|
+
children,
|
|
20
|
+
}: {
|
|
21
|
+
children: React.ReactNode
|
|
22
|
+
}) {
|
|
23
|
+
return children
|
|
24
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Contact Page
|
|
5
|
+
*
|
|
6
|
+
* Contact form that sends messages to the store owner.
|
|
7
|
+
* Uses SDK email system via /api/contact endpoint.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState } from 'react'
|
|
11
|
+
import { Mail, Send, CheckCircle, AlertCircle } from 'lucide-react'
|
|
12
|
+
import { useStoreSettings, useT } from '@rovela-ai/sdk/core'
|
|
13
|
+
import { cn, isValidEmail } from '@/lib/utils'
|
|
14
|
+
|
|
15
|
+
interface FormData {
|
|
16
|
+
name: string
|
|
17
|
+
email: string
|
|
18
|
+
subject: string
|
|
19
|
+
message: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface FormErrors {
|
|
23
|
+
name?: string
|
|
24
|
+
email?: string
|
|
25
|
+
subject?: string
|
|
26
|
+
message?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type FormStatus = 'idle' | 'submitting' | 'success' | 'error'
|
|
30
|
+
|
|
31
|
+
export default function ContactPage() {
|
|
32
|
+
const t = useT()
|
|
33
|
+
const { storeName } = useStoreSettings()
|
|
34
|
+
const [formData, setFormData] = useState<FormData>({
|
|
35
|
+
name: '',
|
|
36
|
+
email: '',
|
|
37
|
+
subject: '',
|
|
38
|
+
message: '',
|
|
39
|
+
})
|
|
40
|
+
const [errors, setErrors] = useState<FormErrors>({})
|
|
41
|
+
const [status, setStatus] = useState<FormStatus>('idle')
|
|
42
|
+
const [errorMessage, setErrorMessage] = useState('')
|
|
43
|
+
|
|
44
|
+
const validateForm = (): boolean => {
|
|
45
|
+
const newErrors: FormErrors = {}
|
|
46
|
+
|
|
47
|
+
if (!formData.name.trim() || formData.name.trim().length < 2) {
|
|
48
|
+
newErrors.name = t('contactPage.nameMin')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!formData.email.trim()) {
|
|
52
|
+
newErrors.email = t('form.emailRequired')
|
|
53
|
+
} else if (!isValidEmail(formData.email)) {
|
|
54
|
+
newErrors.email = t('form.emailInvalid')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!formData.subject.trim() || formData.subject.trim().length < 5) {
|
|
58
|
+
newErrors.subject = t('contactPage.subjectMin')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!formData.message.trim() || formData.message.trim().length < 10) {
|
|
62
|
+
newErrors.message = t('contactPage.messageMin')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setErrors(newErrors)
|
|
66
|
+
return Object.keys(newErrors).length === 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
70
|
+
e.preventDefault()
|
|
71
|
+
|
|
72
|
+
if (!validateForm()) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setStatus('submitting')
|
|
77
|
+
setErrorMessage('')
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch('/api/contact', {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify(formData),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const data = await response.json()
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(data.error || t('contactPage.sendFailed'))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setStatus('success')
|
|
95
|
+
setFormData({ name: '', email: '', subject: '', message: '' })
|
|
96
|
+
} catch (err) {
|
|
97
|
+
setStatus('error')
|
|
98
|
+
setErrorMessage(err instanceof Error ? err.message : t('contactPage.somethingWrong'))
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const handleChange = (
|
|
103
|
+
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
104
|
+
) => {
|
|
105
|
+
const { name, value } = e.target
|
|
106
|
+
setFormData((prev) => ({ ...prev, [name]: value }))
|
|
107
|
+
// Clear error when user starts typing
|
|
108
|
+
if (errors[name as keyof FormErrors]) {
|
|
109
|
+
setErrors((prev) => ({ ...prev, [name]: undefined }))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
115
|
+
<div className="max-w-2xl mx-auto">
|
|
116
|
+
{/* Header */}
|
|
117
|
+
<div className="text-center mb-12">
|
|
118
|
+
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-6">
|
|
119
|
+
<Mail className="h-8 w-8 text-primary" />
|
|
120
|
+
</div>
|
|
121
|
+
<h1 className="text-3xl font-bold font-heading">{t('contactPage.title')}</h1>
|
|
122
|
+
<p className="mt-4 text-muted-foreground">
|
|
123
|
+
{t('contactPage.intro')}
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Success State */}
|
|
128
|
+
{status === 'success' && (
|
|
129
|
+
<div className="rounded-lg border border-green-200 bg-green-50 dark:bg-green-950/20 dark:border-green-900 p-6 mb-8">
|
|
130
|
+
<div className="flex items-start gap-4">
|
|
131
|
+
<CheckCircle className="h-6 w-6 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
|
|
132
|
+
<div>
|
|
133
|
+
<h3 className="font-semibold text-green-800 dark:text-green-200">
|
|
134
|
+
{t('contactPage.sentTitle')}
|
|
135
|
+
</h3>
|
|
136
|
+
<p className="mt-1 text-sm text-green-700 dark:text-green-300">
|
|
137
|
+
{t('contactPage.sentBody', { storeName })}
|
|
138
|
+
</p>
|
|
139
|
+
<button
|
|
140
|
+
onClick={() => setStatus('idle')}
|
|
141
|
+
className="mt-4 text-sm font-medium text-green-700 dark:text-green-300 hover:text-green-800 dark:hover:text-green-200 underline"
|
|
142
|
+
>
|
|
143
|
+
{t('contactPage.sendAnother')}
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{/* Error State */}
|
|
151
|
+
{status === 'error' && (
|
|
152
|
+
<div className="rounded-lg border border-red-200 bg-red-50 dark:bg-red-950/20 dark:border-red-900 p-6 mb-8">
|
|
153
|
+
<div className="flex items-start gap-4">
|
|
154
|
+
<AlertCircle className="h-6 w-6 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
|
|
155
|
+
<div>
|
|
156
|
+
<h3 className="font-semibold text-red-800 dark:text-red-200">
|
|
157
|
+
{t('contactPage.failedTitle')}
|
|
158
|
+
</h3>
|
|
159
|
+
<p className="mt-1 text-sm text-red-700 dark:text-red-300">
|
|
160
|
+
{errorMessage || t('contactPage.failedBody')}
|
|
161
|
+
</p>
|
|
162
|
+
<button
|
|
163
|
+
onClick={() => setStatus('idle')}
|
|
164
|
+
className="mt-4 text-sm font-medium text-red-700 dark:text-red-300 hover:text-red-800 dark:hover:text-red-200 underline"
|
|
165
|
+
>
|
|
166
|
+
{t('contactPage.tryAgain')}
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
{/* Contact Form */}
|
|
174
|
+
{status !== 'success' && (
|
|
175
|
+
<form onSubmit={handleSubmit} className="rounded-lg border bg-card p-6 sm:p-8">
|
|
176
|
+
<div className="space-y-6">
|
|
177
|
+
{/* Name Field */}
|
|
178
|
+
<div>
|
|
179
|
+
<label
|
|
180
|
+
htmlFor="name"
|
|
181
|
+
className="block text-sm font-medium mb-2"
|
|
182
|
+
>
|
|
183
|
+
{t('auth.name')}
|
|
184
|
+
</label>
|
|
185
|
+
<input
|
|
186
|
+
type="text"
|
|
187
|
+
id="name"
|
|
188
|
+
name="name"
|
|
189
|
+
value={formData.name}
|
|
190
|
+
onChange={handleChange}
|
|
191
|
+
className={cn(
|
|
192
|
+
'flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors',
|
|
193
|
+
errors.name ? 'border-red-500' : 'border-input'
|
|
194
|
+
)}
|
|
195
|
+
placeholder={t('account.namePlaceholder')}
|
|
196
|
+
disabled={status === 'submitting'}
|
|
197
|
+
/>
|
|
198
|
+
{errors.name && (
|
|
199
|
+
<p className="mt-1 text-sm text-red-500">{errors.name}</p>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{/* Email Field */}
|
|
204
|
+
<div>
|
|
205
|
+
<label
|
|
206
|
+
htmlFor="email"
|
|
207
|
+
className="block text-sm font-medium mb-2"
|
|
208
|
+
>
|
|
209
|
+
{t('form.email')}
|
|
210
|
+
</label>
|
|
211
|
+
<input
|
|
212
|
+
type="email"
|
|
213
|
+
id="email"
|
|
214
|
+
name="email"
|
|
215
|
+
value={formData.email}
|
|
216
|
+
onChange={handleChange}
|
|
217
|
+
className={cn(
|
|
218
|
+
'flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors',
|
|
219
|
+
errors.email ? 'border-red-500' : 'border-input'
|
|
220
|
+
)}
|
|
221
|
+
placeholder={t('checkout.emailPlaceholder')}
|
|
222
|
+
disabled={status === 'submitting'}
|
|
223
|
+
/>
|
|
224
|
+
{errors.email && (
|
|
225
|
+
<p className="mt-1 text-sm text-red-500">{errors.email}</p>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Subject Field */}
|
|
230
|
+
<div>
|
|
231
|
+
<label
|
|
232
|
+
htmlFor="subject"
|
|
233
|
+
className="block text-sm font-medium mb-2"
|
|
234
|
+
>
|
|
235
|
+
{t('contactPage.subject')}
|
|
236
|
+
</label>
|
|
237
|
+
<input
|
|
238
|
+
type="text"
|
|
239
|
+
id="subject"
|
|
240
|
+
name="subject"
|
|
241
|
+
value={formData.subject}
|
|
242
|
+
onChange={handleChange}
|
|
243
|
+
className={cn(
|
|
244
|
+
'flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors',
|
|
245
|
+
errors.subject ? 'border-red-500' : 'border-input'
|
|
246
|
+
)}
|
|
247
|
+
placeholder={t('contactPage.subjectPlaceholder')}
|
|
248
|
+
disabled={status === 'submitting'}
|
|
249
|
+
/>
|
|
250
|
+
{errors.subject && (
|
|
251
|
+
<p className="mt-1 text-sm text-red-500">{errors.subject}</p>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{/* Message Field */}
|
|
256
|
+
<div>
|
|
257
|
+
<label
|
|
258
|
+
htmlFor="message"
|
|
259
|
+
className="block text-sm font-medium mb-2"
|
|
260
|
+
>
|
|
261
|
+
{t('contactPage.message')}
|
|
262
|
+
</label>
|
|
263
|
+
<textarea
|
|
264
|
+
id="message"
|
|
265
|
+
name="message"
|
|
266
|
+
rows={5}
|
|
267
|
+
value={formData.message}
|
|
268
|
+
onChange={handleChange}
|
|
269
|
+
className={cn(
|
|
270
|
+
'flex w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors resize-none',
|
|
271
|
+
errors.message ? 'border-red-500' : 'border-input'
|
|
272
|
+
)}
|
|
273
|
+
placeholder={t('contactPage.messagePlaceholder')}
|
|
274
|
+
disabled={status === 'submitting'}
|
|
275
|
+
/>
|
|
276
|
+
{errors.message && (
|
|
277
|
+
<p className="mt-1 text-sm text-red-500">{errors.message}</p>
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
{/* Submit Button */}
|
|
282
|
+
<button
|
|
283
|
+
type="submit"
|
|
284
|
+
disabled={status === 'submitting'}
|
|
285
|
+
className={cn(
|
|
286
|
+
'w-full flex items-center justify-center gap-2 rounded-md bg-primary px-6 py-3 text-sm font-semibold text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors',
|
|
287
|
+
status === 'submitting' && 'opacity-70 cursor-not-allowed'
|
|
288
|
+
)}
|
|
289
|
+
>
|
|
290
|
+
{status === 'submitting' ? (
|
|
291
|
+
<>
|
|
292
|
+
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent" />
|
|
293
|
+
{t('contactPage.sending')}
|
|
294
|
+
</>
|
|
295
|
+
) : (
|
|
296
|
+
<>
|
|
297
|
+
<Send className="h-4 w-4" />
|
|
298
|
+
{t('contactPage.sendMessage')}
|
|
299
|
+
</>
|
|
300
|
+
)}
|
|
301
|
+
</button>
|
|
302
|
+
</div>
|
|
303
|
+
</form>
|
|
304
|
+
)}
|
|
305
|
+
|
|
306
|
+
{/* Additional Contact Info */}
|
|
307
|
+
<div className="mt-12 text-center">
|
|
308
|
+
<p className="text-sm text-muted-foreground">
|
|
309
|
+
{t('contactPage.reachDirectly')}{' '}
|
|
310
|
+
<a
|
|
311
|
+
href={`mailto:${process.env.NEXT_PUBLIC_SUPPORT_EMAIL || 'support@store.com'}`}
|
|
312
|
+
className="text-primary hover:underline"
|
|
313
|
+
>
|
|
314
|
+
{t('contactPage.supportEmail')}
|
|
315
|
+
</a>
|
|
316
|
+
</p>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
)
|
|
321
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { localizedAlternates, getRequestLocale } from '@rovela-ai/sdk/core/server'
|
|
2
|
+
import type { Metadata } from 'next'
|
|
3
|
+
import { LocaleLink as Link, getT } from '@rovela-ai/sdk/core'
|
|
4
|
+
import { findSettings } from '@rovela-ai/sdk/core/server'
|
|
5
|
+
import { buildFaqJsonLd, serializeJsonLd } from '@rovela-ai/sdk/core'
|
|
6
|
+
import { storeConfig } from '@/lib/store-config'
|
|
7
|
+
import {
|
|
8
|
+
returnWindowDays,
|
|
9
|
+
shipsToCountries,
|
|
10
|
+
freeShippingThreshold,
|
|
11
|
+
supportEmail,
|
|
12
|
+
} from '@/lib/store-policy'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Store FAQ — server-rendered, with `FAQPage` JSON-LD whose Q&A mirrors the
|
|
16
|
+
* visible content 1:1 (both read from store_settings + lib/store-policy). FAQ
|
|
17
|
+
* markup is the single highest-leverage GEO signal (≈3× AI-citation rate), and
|
|
18
|
+
* AI engines extract answers verbatim, so each answer is self-contained.
|
|
19
|
+
*
|
|
20
|
+
* Server Component (no 'use client') so the schema + content are in the initial
|
|
21
|
+
* HTML — AI crawlers don't execute JavaScript.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export async function generateMetadata() {
|
|
25
|
+
const { locale } = await getRequestLocale()
|
|
26
|
+
const t = getT(locale)
|
|
27
|
+
return {
|
|
28
|
+
title: t('faqPage.title'),
|
|
29
|
+
description: t('faqPage.metaDescription'),
|
|
30
|
+
alternates: await localizedAlternates('/faq'),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const COUNTRY_NAMES: Record<string, string> = {
|
|
35
|
+
US: 'the United States',
|
|
36
|
+
CA: 'Canada',
|
|
37
|
+
GB: 'the United Kingdom',
|
|
38
|
+
AU: 'Australia',
|
|
39
|
+
NZ: 'New Zealand',
|
|
40
|
+
IE: 'Ireland',
|
|
41
|
+
DE: 'Germany',
|
|
42
|
+
FR: 'France',
|
|
43
|
+
ES: 'Spain',
|
|
44
|
+
IT: 'Italy',
|
|
45
|
+
NL: 'the Netherlands',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatCountries(codes: string[]): string {
|
|
49
|
+
const names = codes.map((c) => COUNTRY_NAMES[c] || c)
|
|
50
|
+
if (names.length === 1) return names[0]
|
|
51
|
+
if (names.length === 2) return `${names[0]} and ${names[1]}`
|
|
52
|
+
return `${names.slice(0, -1).join(', ')}, and ${names[names.length - 1]}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default async function FaqPage() {
|
|
56
|
+
const { locale } = await getRequestLocale()
|
|
57
|
+
const t = getT(locale)
|
|
58
|
+
const settings = await findSettings().catch(() => null)
|
|
59
|
+
|
|
60
|
+
const countries = shipsToCountries(settings)
|
|
61
|
+
const freeThreshold = freeShippingThreshold(settings)
|
|
62
|
+
const email = supportEmail(settings)
|
|
63
|
+
const returnDays = returnWindowDays()
|
|
64
|
+
const currency = (settings?.storeCurrency || 'USD').toUpperCase()
|
|
65
|
+
const guestCheckout = settings?.guestCheckout !== false
|
|
66
|
+
const processing = String(storeConfig.shipping?.processingTime ?? '1-2-days').replace(/-/g, ' ')
|
|
67
|
+
|
|
68
|
+
const faqs: Array<{ question: string; answer: string }> = [
|
|
69
|
+
{
|
|
70
|
+
question: 'What payment methods do you accept?',
|
|
71
|
+
answer:
|
|
72
|
+
'We accept major credit and debit cards through our secure checkout. Your payment details are encrypted and handled by our payment provider — we never store full card numbers.',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
question: 'Which countries do you ship to?',
|
|
76
|
+
answer: `We currently ship to ${formatCountries(countries)}.`,
|
|
77
|
+
},
|
|
78
|
+
freeThreshold > 0
|
|
79
|
+
? {
|
|
80
|
+
question: 'How much does shipping cost?',
|
|
81
|
+
answer: `Shipping is calculated at checkout based on your address. Orders over ${freeThreshold} ${currency} qualify for free shipping.`,
|
|
82
|
+
}
|
|
83
|
+
: {
|
|
84
|
+
question: 'How much does shipping cost?',
|
|
85
|
+
answer:
|
|
86
|
+
'Shipping is calculated at checkout based on your delivery address and the items in your order.',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
question: 'How long will my order take to arrive?',
|
|
90
|
+
answer: `Orders are typically processed within ${processing} business days and then shipped. Delivery time depends on your location and the carrier.`,
|
|
91
|
+
},
|
|
92
|
+
returnDays > 0
|
|
93
|
+
? {
|
|
94
|
+
question: 'What is your return policy?',
|
|
95
|
+
answer: `You can return unused items in their original condition within ${returnDays} days of delivery. Return shipping is the customer's responsibility unless the item arrived damaged or incorrect.`,
|
|
96
|
+
}
|
|
97
|
+
: {
|
|
98
|
+
question: 'What is your return policy?',
|
|
99
|
+
answer:
|
|
100
|
+
'All sales are final. If your item arrives damaged or incorrect, contact us right away and we will make it right.',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
question: 'Do I need an account to place an order?',
|
|
104
|
+
answer: guestCheckout
|
|
105
|
+
? 'No account is required — you can check out as a guest. Creating an account lets you track orders and check out faster next time.'
|
|
106
|
+
: 'Please create an account at checkout so you can track your order and manage returns.',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
question: 'How can I contact you?',
|
|
110
|
+
answer: email
|
|
111
|
+
? `Email us at ${email} and we'll get back to you as soon as we can. You can also use our contact page.`
|
|
112
|
+
: `Reach us through our contact page and we'll get back to you as soon as we can.`,
|
|
113
|
+
},
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
const faqJsonLd = buildFaqJsonLd(faqs)
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
120
|
+
<script
|
|
121
|
+
type="application/ld+json"
|
|
122
|
+
dangerouslySetInnerHTML={{ __html: serializeJsonLd(faqJsonLd) }}
|
|
123
|
+
/>
|
|
124
|
+
<div className="max-w-3xl mx-auto">
|
|
125
|
+
<div className="mb-12">
|
|
126
|
+
<h1 className="text-3xl font-bold font-heading">{t('faqPage.title')}</h1>
|
|
127
|
+
<p className="mt-4 text-muted-foreground">
|
|
128
|
+
{t('faqPage.subtitle')}
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className="space-y-10">
|
|
133
|
+
{faqs.map((f) => (
|
|
134
|
+
<section key={f.question}>
|
|
135
|
+
<h2 className="text-xl font-semibold font-heading mb-3">{f.question}</h2>
|
|
136
|
+
<p className="text-muted-foreground leading-relaxed">{f.answer}</p>
|
|
137
|
+
</section>
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="mt-12 pt-8 border-t">
|
|
142
|
+
<p className="text-muted-foreground">
|
|
143
|
+
{t('faqPage.stillHaveQuestion')}{' '}
|
|
144
|
+
<Link href="/contact" className="text-primary hover:underline">
|
|
145
|
+
{t('faqPage.contactUsLink')}
|
|
146
|
+
</Link>
|
|
147
|
+
.
|
|
148
|
+
</p>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
}
|