@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,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public Store Settings API
|
|
3
|
+
*
|
|
4
|
+
* Returns public (non-sensitive) store settings.
|
|
5
|
+
* Used by StoreSettingsProvider to get dynamic store configuration.
|
|
6
|
+
*
|
|
7
|
+
* This endpoint is PUBLIC - no authentication required.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getPublicSettings } from '@rovela-ai/sdk/core/api'
|
|
11
|
+
|
|
12
|
+
export { getPublicSettings as GET }
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe Webhook Route
|
|
3
|
+
*
|
|
4
|
+
* POST /api/webhooks/stripe - Handle Stripe webhook events
|
|
5
|
+
*
|
|
6
|
+
* The SDK's handleStripeWebhook() already:
|
|
7
|
+
* - Verifies webhook signature
|
|
8
|
+
* - Creates orders on checkout.session.completed
|
|
9
|
+
* - Sends order confirmation emails
|
|
10
|
+
* - Handles refunds
|
|
11
|
+
*
|
|
12
|
+
* §46 wrapper-and-extend: when the SDK returns analytics context from a
|
|
13
|
+
* successful checkout completion, fire a server-side `purchase` event tied
|
|
14
|
+
* to the same visitor session that browsed the storefront. The partial
|
|
15
|
+
* unique index on (order_id) WHERE event='purchase' absorbs Stripe's
|
|
16
|
+
* at-least-once delivery — duplicate webhooks ON CONFLICT DO NOTHING.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { handleStripeWebhook } from '@rovela-ai/sdk/checkout/server'
|
|
20
|
+
import { recordEvent } from '@rovela-ai/sdk/analytics/server'
|
|
21
|
+
|
|
22
|
+
export async function POST(request: Request) {
|
|
23
|
+
const payload = await request.text()
|
|
24
|
+
const signature = request.headers.get('stripe-signature')
|
|
25
|
+
|
|
26
|
+
if (!signature) {
|
|
27
|
+
return Response.json(
|
|
28
|
+
{ error: 'Missing stripe-signature header' },
|
|
29
|
+
{ status: 400 }
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const result = await handleStripeWebhook(payload, signature)
|
|
34
|
+
|
|
35
|
+
// Fire server-side `purchase` analytics event when the order was created
|
|
36
|
+
// AND the storefront passed visitor identity through Stripe metadata
|
|
37
|
+
// (CheckoutFlow does this automatically via useCheckout). Fire-and-forget;
|
|
38
|
+
// recordEvent swallows errors so analytics CAN'T break webhook delivery.
|
|
39
|
+
if (
|
|
40
|
+
result?.event === 'checkout.session.completed' &&
|
|
41
|
+
result.orderId &&
|
|
42
|
+
result.analytics?.visitorId &&
|
|
43
|
+
result.analytics?.sessionId &&
|
|
44
|
+
typeof result.analytics.valueCents === 'number' &&
|
|
45
|
+
result.analytics.currency
|
|
46
|
+
) {
|
|
47
|
+
void recordEvent({
|
|
48
|
+
event: {
|
|
49
|
+
event: 'purchase',
|
|
50
|
+
visitor_id: result.analytics.visitorId,
|
|
51
|
+
session_id: result.analytics.sessionId,
|
|
52
|
+
order_id: result.orderId,
|
|
53
|
+
value_cents: result.analytics.valueCents,
|
|
54
|
+
currency: result.analytics.currency,
|
|
55
|
+
path: '/checkout/success',
|
|
56
|
+
},
|
|
57
|
+
device: null,
|
|
58
|
+
country: null,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Response.json(result)
|
|
63
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Forgot Password Page
|
|
5
|
+
*
|
|
6
|
+
* Request password reset email.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { LocaleLink as Link } from '@rovela-ai/sdk/core'
|
|
10
|
+
import { ForgotPasswordForm } from '@rovela-ai/sdk/auth'
|
|
11
|
+
import { useT } from '@rovela-ai/sdk/core'
|
|
12
|
+
|
|
13
|
+
export default function ForgotPasswordPage() {
|
|
14
|
+
const t = useT()
|
|
15
|
+
return (
|
|
16
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
17
|
+
<div className="max-w-md mx-auto">
|
|
18
|
+
<div className="text-center mb-8">
|
|
19
|
+
<h1 className="text-2xl font-bold font-heading">{t('authPage.resetTitle')}</h1>
|
|
20
|
+
<p className="mt-2 text-muted-foreground">
|
|
21
|
+
{t('authPage.resetSubtitle')}
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div className="rounded-lg border bg-card p-6">
|
|
26
|
+
<ForgotPasswordForm />
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<p className="mt-6 text-center text-sm text-muted-foreground">
|
|
30
|
+
{t('authPage.rememberPassword')}{' '}
|
|
31
|
+
<Link href="/auth/signin" className="text-primary hover:underline">
|
|
32
|
+
{t('authPage.signIn')}
|
|
33
|
+
</Link>
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth area gate — when the merchant sets Customer accounts to Off
|
|
3
|
+
* (admin Settings → Checkout Settings), every auth page (sign in, sign up,
|
|
4
|
+
* forgot/reset password, verify email) redirects home BEFORE any HTML
|
|
5
|
+
* ships. Fail-open: a settings-read error never blocks a normal store.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { redirect } from 'next/navigation'
|
|
9
|
+
import { findSettings } from '@rovela-ai/sdk/core/server'
|
|
10
|
+
|
|
11
|
+
export const dynamic = 'force-dynamic'
|
|
12
|
+
|
|
13
|
+
export default async function AuthLayout({ children }: { children: React.ReactNode }) {
|
|
14
|
+
const settings = await findSettings().catch(() => null)
|
|
15
|
+
if (settings?.customerAccounts === false) redirect('/')
|
|
16
|
+
return <>{children}</>
|
|
17
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reset Password Page
|
|
5
|
+
*
|
|
6
|
+
* Set new password using reset token from email.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Suspense } from 'react'
|
|
10
|
+
import { useSearchParams, useRouter } from 'next/navigation'
|
|
11
|
+
import { LocaleLink as Link, useLocalePath } from '@rovela-ai/sdk/core'
|
|
12
|
+
import { ResetPasswordForm } from '@rovela-ai/sdk/auth'
|
|
13
|
+
import { useT } from '@rovela-ai/sdk/core'
|
|
14
|
+
|
|
15
|
+
function ResetPasswordContent() {
|
|
16
|
+
const t = useT()
|
|
17
|
+
const localePath = useLocalePath()
|
|
18
|
+
const router = useRouter()
|
|
19
|
+
const searchParams = useSearchParams()
|
|
20
|
+
const token = searchParams.get('token')
|
|
21
|
+
|
|
22
|
+
if (!token) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
25
|
+
<div className="max-w-md mx-auto text-center">
|
|
26
|
+
<h1 className="text-2xl font-bold font-heading text-destructive">
|
|
27
|
+
{t('authPage.invalidResetLink')}
|
|
28
|
+
</h1>
|
|
29
|
+
<p className="mt-4 text-muted-foreground">
|
|
30
|
+
{t('authPage.invalidResetLinkBody')}
|
|
31
|
+
</p>
|
|
32
|
+
<Link
|
|
33
|
+
href="/auth/forgot-password"
|
|
34
|
+
className="mt-8 inline-block rounded-md bg-primary px-8 py-3 text-sm font-semibold text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
|
|
35
|
+
>
|
|
36
|
+
{t('authPage.requestNewLink')}
|
|
37
|
+
</Link>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
45
|
+
<div className="max-w-md mx-auto">
|
|
46
|
+
<div className="text-center mb-8">
|
|
47
|
+
<h1 className="text-2xl font-bold font-heading">{t('authPage.setNewPassword')}</h1>
|
|
48
|
+
<p className="mt-2 text-muted-foreground">
|
|
49
|
+
Enter your new password below
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className="rounded-lg border bg-card p-6">
|
|
54
|
+
<ResetPasswordForm
|
|
55
|
+
token={token}
|
|
56
|
+
onSuccess={() => router.push(localePath('/auth/signin?reset=success'))}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function LoadingFallback() {
|
|
65
|
+
return (
|
|
66
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
67
|
+
<div className="max-w-md mx-auto text-center">
|
|
68
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto" />
|
|
69
|
+
<p className="mt-4 text-muted-foreground">Loading...</p>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default function ResetPasswordPage() {
|
|
76
|
+
return (
|
|
77
|
+
<Suspense fallback={<LoadingFallback />}>
|
|
78
|
+
<ResetPasswordContent />
|
|
79
|
+
</Suspense>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sign In Page
|
|
5
|
+
*
|
|
6
|
+
* Customer sign in using SDK auth components.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Suspense } from 'react'
|
|
10
|
+
import { useRouter, useSearchParams } from 'next/navigation'
|
|
11
|
+
import { SignInForm } from '@rovela-ai/sdk/auth'
|
|
12
|
+
import { useStoreSettings } from '@rovela-ai/sdk/core'
|
|
13
|
+
import { useT } from '@rovela-ai/sdk/core'
|
|
14
|
+
|
|
15
|
+
function SignInContent() {
|
|
16
|
+
const t = useT()
|
|
17
|
+
const router = useRouter()
|
|
18
|
+
const searchParams = useSearchParams()
|
|
19
|
+
const callbackUrl = searchParams.get('callbackUrl') || '/account'
|
|
20
|
+
const { storeName } = useStoreSettings()
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
24
|
+
<div className="max-w-md mx-auto">
|
|
25
|
+
<div className="text-center mb-8">
|
|
26
|
+
<h1 className="text-2xl font-bold font-heading">{t('authPage.welcomeBack')}</h1>
|
|
27
|
+
<p className="mt-2 text-muted-foreground">
|
|
28
|
+
{t('authPage.signInSubtitle', { storeName })}
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div className="rounded-lg border bg-card p-6">
|
|
33
|
+
<SignInForm
|
|
34
|
+
onSuccess={() => router.push(callbackUrl)}
|
|
35
|
+
forgotPasswordHref="/auth/forgot-password"
|
|
36
|
+
signUpHref="/auth/signup"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function LoadingFallback() {
|
|
45
|
+
const t = useT()
|
|
46
|
+
return (
|
|
47
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
48
|
+
<div className="max-w-md mx-auto text-center">
|
|
49
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto" />
|
|
50
|
+
<p className="mt-4 text-muted-foreground">{t('authPage.loading')}</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default function SignInPage() {
|
|
57
|
+
return (
|
|
58
|
+
<Suspense fallback={<LoadingFallback />}>
|
|
59
|
+
<SignInContent />
|
|
60
|
+
</Suspense>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sign Up Page
|
|
5
|
+
*
|
|
6
|
+
* Customer registration using SDK auth components.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useRouter } from 'next/navigation'
|
|
10
|
+
import { SignUpForm } from '@rovela-ai/sdk/auth'
|
|
11
|
+
import { useStoreSettings, useLocalePath } from '@rovela-ai/sdk/core'
|
|
12
|
+
import { useT } from '@rovela-ai/sdk/core'
|
|
13
|
+
|
|
14
|
+
export default function SignUpPage() {
|
|
15
|
+
const t = useT()
|
|
16
|
+
const localePath = useLocalePath()
|
|
17
|
+
const router = useRouter()
|
|
18
|
+
const { storeName } = useStoreSettings()
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
22
|
+
<div className="max-w-md mx-auto">
|
|
23
|
+
<div className="text-center mb-8">
|
|
24
|
+
<h1 className="text-2xl font-bold font-heading">{t('authPage.createAccount')}</h1>
|
|
25
|
+
<p className="mt-2 text-muted-foreground">
|
|
26
|
+
{t('authPage.signUpSubtitle', { storeName })}
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div className="rounded-lg border bg-card p-6">
|
|
31
|
+
<SignUpForm
|
|
32
|
+
onSuccess={() => router.push(localePath('/auth/verify-email'))}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verify Email Page
|
|
5
|
+
*
|
|
6
|
+
* Shows verification notice or handles token verification.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Suspense, useEffect, useState, useCallback } from 'react'
|
|
10
|
+
import { useSearchParams, useRouter } from 'next/navigation'
|
|
11
|
+
import { LocaleLink as Link, useLocalePath } from '@rovela-ai/sdk/core'
|
|
12
|
+
import { CheckCircle, XCircle, Mail, RefreshCw } from 'lucide-react'
|
|
13
|
+
import { useT } from '@rovela-ai/sdk/core'
|
|
14
|
+
|
|
15
|
+
function VerifyEmailContent() {
|
|
16
|
+
const t = useT()
|
|
17
|
+
const localePath = useLocalePath()
|
|
18
|
+
const router = useRouter()
|
|
19
|
+
const searchParams = useSearchParams()
|
|
20
|
+
const token = searchParams.get('token')
|
|
21
|
+
const email = searchParams.get('email') || ''
|
|
22
|
+
|
|
23
|
+
const [status, setStatus] = useState<'pending' | 'verifying' | 'success' | 'error'>('pending')
|
|
24
|
+
const [error, setError] = useState<string | null>(null)
|
|
25
|
+
const [isResending, setIsResending] = useState(false)
|
|
26
|
+
const [resent, setResent] = useState(false)
|
|
27
|
+
|
|
28
|
+
// If token present, verify it
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (token) {
|
|
31
|
+
setStatus('verifying')
|
|
32
|
+
fetch(`/api/auth/verify-email?token=${token}`)
|
|
33
|
+
.then(res => res.json())
|
|
34
|
+
.then(data => {
|
|
35
|
+
if (data.success) {
|
|
36
|
+
setStatus('success')
|
|
37
|
+
// Redirect to signin after 3 seconds
|
|
38
|
+
setTimeout(() => router.push(localePath('/auth/signin')), 3000)
|
|
39
|
+
} else {
|
|
40
|
+
setStatus('error')
|
|
41
|
+
setError(data.error || t('authPage.verificationFailed'))
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
.catch(() => {
|
|
45
|
+
setStatus('error')
|
|
46
|
+
setError(t('authPage.verificationError'))
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}, [token, router])
|
|
50
|
+
|
|
51
|
+
const handleResend = useCallback(async () => {
|
|
52
|
+
if (!email || isResending) return
|
|
53
|
+
setIsResending(true)
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch('/api/auth/resend-verification', {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify({ email }),
|
|
59
|
+
})
|
|
60
|
+
if (res.ok) {
|
|
61
|
+
setResent(true)
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
setIsResending(false)
|
|
65
|
+
}
|
|
66
|
+
}, [email, isResending])
|
|
67
|
+
|
|
68
|
+
// Token verification flow
|
|
69
|
+
if (token) {
|
|
70
|
+
if (status === 'verifying') {
|
|
71
|
+
return (
|
|
72
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
73
|
+
<div className="max-w-md mx-auto text-center">
|
|
74
|
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto" />
|
|
75
|
+
<h1 className="mt-6 text-2xl font-bold font-heading">
|
|
76
|
+
{t('authPage.verifyingEmail')}
|
|
77
|
+
</h1>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (status === 'success') {
|
|
84
|
+
return (
|
|
85
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
86
|
+
<div className="max-w-md mx-auto text-center">
|
|
87
|
+
<CheckCircle className="h-16 w-16 mx-auto text-green-500" />
|
|
88
|
+
<h1 className="mt-6 text-2xl font-bold font-heading">
|
|
89
|
+
{t('authPage.emailVerified')}
|
|
90
|
+
</h1>
|
|
91
|
+
<p className="mt-4 text-muted-foreground">
|
|
92
|
+
Your email has been verified. Redirecting to sign in...
|
|
93
|
+
</p>
|
|
94
|
+
<Link
|
|
95
|
+
href="/auth/signin"
|
|
96
|
+
className="mt-8 inline-block rounded-md bg-primary px-8 py-3 text-sm font-semibold text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
|
|
97
|
+
>
|
|
98
|
+
Sign In Now
|
|
99
|
+
</Link>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (status === 'error') {
|
|
106
|
+
return (
|
|
107
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
108
|
+
<div className="max-w-md mx-auto text-center">
|
|
109
|
+
<XCircle className="h-16 w-16 mx-auto text-destructive" />
|
|
110
|
+
<h1 className="mt-6 text-2xl font-bold font-heading text-destructive">
|
|
111
|
+
Verification Failed
|
|
112
|
+
</h1>
|
|
113
|
+
<p className="mt-4 text-muted-foreground">
|
|
114
|
+
{error}
|
|
115
|
+
</p>
|
|
116
|
+
<Link
|
|
117
|
+
href="/auth/signin"
|
|
118
|
+
className="mt-8 inline-block rounded-md bg-primary px-8 py-3 text-sm font-semibold text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
|
|
119
|
+
>
|
|
120
|
+
Try Again
|
|
121
|
+
</Link>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// No token - show verification notice
|
|
129
|
+
return (
|
|
130
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
131
|
+
<div className="max-w-md mx-auto text-center">
|
|
132
|
+
<Mail className="h-16 w-16 mx-auto text-primary" />
|
|
133
|
+
<h1 className="mt-6 text-2xl font-bold font-heading">
|
|
134
|
+
Check your email
|
|
135
|
+
</h1>
|
|
136
|
+
<p className="mt-4 text-muted-foreground">
|
|
137
|
+
We've sent you a verification link. Please check your inbox and click the link to verify your email address.
|
|
138
|
+
</p>
|
|
139
|
+
|
|
140
|
+
{email && !resent && (
|
|
141
|
+
<button
|
|
142
|
+
onClick={handleResend}
|
|
143
|
+
disabled={isResending}
|
|
144
|
+
className="mt-8 inline-flex items-center gap-2 text-sm text-primary hover:underline disabled:opacity-50"
|
|
145
|
+
>
|
|
146
|
+
<RefreshCw className={`h-4 w-4 ${isResending ? 'animate-spin' : ''}`} />
|
|
147
|
+
{isResending ? 'Sending...' : 'Resend verification email'}
|
|
148
|
+
</button>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{resent && (
|
|
152
|
+
<p className="mt-8 text-sm text-green-600">
|
|
153
|
+
Verification email sent! Check your inbox.
|
|
154
|
+
</p>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
<p className="mt-6 text-sm text-muted-foreground">
|
|
158
|
+
Already verified?{' '}
|
|
159
|
+
<Link href="/auth/signin" className="text-primary hover:underline">
|
|
160
|
+
Sign in
|
|
161
|
+
</Link>
|
|
162
|
+
</p>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function LoadingFallback() {
|
|
169
|
+
return (
|
|
170
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
171
|
+
<div className="max-w-md mx-auto text-center">
|
|
172
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto" />
|
|
173
|
+
<p className="mt-4 text-muted-foreground">Loading...</p>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export default function VerifyEmailPage() {
|
|
180
|
+
return (
|
|
181
|
+
<Suspense fallback={<LoadingFallback />}>
|
|
182
|
+
<VerifyEmailContent />
|
|
183
|
+
</Suspense>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cart Page
|
|
5
|
+
*
|
|
6
|
+
* Uses SDK components for cart display:
|
|
7
|
+
* - CartItem: Individual cart line item
|
|
8
|
+
* - CartSummary: Subtotal and totals
|
|
9
|
+
* - useCart: Cart state management
|
|
10
|
+
*
|
|
11
|
+
* Checkout behavior depends on store settings:
|
|
12
|
+
* - Shipping enabled: Link to /checkout for pre-checkout flow (Address → Shipping → Review)
|
|
13
|
+
* - Shipping disabled: Direct CheckoutButton to Stripe (no pre-checkout needed)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { LocaleLink as Link } from '@rovela-ai/sdk/core'
|
|
17
|
+
import { ShoppingBag } from 'lucide-react'
|
|
18
|
+
import { useCart, CartItem, CartSummary, getCartItemsWithFallback } from '@rovela-ai/sdk/cart'
|
|
19
|
+
import { CheckoutButton } from '@rovela-ai/sdk/checkout'
|
|
20
|
+
import { useStoreSettings, useT, useStoreFormatting } from '@rovela-ai/sdk/core'
|
|
21
|
+
|
|
22
|
+
export default function CartPage() {
|
|
23
|
+
// Use fallback to handle module duplication issues where this route chunk
|
|
24
|
+
// may have a different store instance than the one CartProvider hydrated
|
|
25
|
+
const items = getCartItemsWithFallback()
|
|
26
|
+
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)
|
|
27
|
+
const t = useT()
|
|
28
|
+
const { clearCart } = useCart()
|
|
29
|
+
const { settings } = useStoreSettings()
|
|
30
|
+
// Locale-aware price shapes (active i18n locale + store currency).
|
|
31
|
+
const { formatPrice } = useStoreFormatting()
|
|
32
|
+
|
|
33
|
+
// Check if shipping is enabled (determines checkout flow)
|
|
34
|
+
const shippingEnabled = settings?.shippingEnabled ?? true
|
|
35
|
+
|
|
36
|
+
// Calculate subtotal for free shipping message
|
|
37
|
+
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
38
|
+
|
|
39
|
+
// Empty cart
|
|
40
|
+
if (itemCount === 0) {
|
|
41
|
+
return (
|
|
42
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
43
|
+
<div className="text-center max-w-md mx-auto">
|
|
44
|
+
<ShoppingBag className="h-16 w-16 mx-auto text-muted-foreground" />
|
|
45
|
+
<h1 className="mt-6 text-2xl font-bold font-heading">{t('cart.empty')}</h1>
|
|
46
|
+
<p className="mt-4 text-muted-foreground">
|
|
47
|
+
{t('cartPage.emptyHint')}
|
|
48
|
+
</p>
|
|
49
|
+
<Link
|
|
50
|
+
href="/products"
|
|
51
|
+
className="mt-8 inline-block rounded-md bg-primary px-8 py-3 text-sm font-semibold text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
|
|
52
|
+
>
|
|
53
|
+
{t('checkout.continueShopping')}
|
|
54
|
+
</Link>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
62
|
+
<div className="flex items-center justify-between mb-8">
|
|
63
|
+
<h1 className="text-3xl font-bold font-heading">{t('cart.title')}</h1>
|
|
64
|
+
<button
|
|
65
|
+
onClick={clearCart}
|
|
66
|
+
className="text-sm text-muted-foreground hover:text-destructive transition-colors"
|
|
67
|
+
>
|
|
68
|
+
{t('cartPage.clearCart')}
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
|
|
73
|
+
{/* Cart Items */}
|
|
74
|
+
<div className="lg:col-span-2 space-y-4">
|
|
75
|
+
{items.map((item) => (
|
|
76
|
+
<CartItem
|
|
77
|
+
key={item.variantId ? `${item.id}-${item.variantId}` : item.id}
|
|
78
|
+
item={item}
|
|
79
|
+
/>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Cart Summary */}
|
|
84
|
+
<div className="lg:col-span-1">
|
|
85
|
+
<div className="sticky top-24 rounded-lg border bg-card p-6">
|
|
86
|
+
<h2 className="text-lg font-semibold mb-4">{t('checkout.orderSummary')}</h2>
|
|
87
|
+
|
|
88
|
+
<CartSummary />
|
|
89
|
+
|
|
90
|
+
<div className="mt-6">
|
|
91
|
+
{shippingEnabled ? (
|
|
92
|
+
<Link
|
|
93
|
+
href="/checkout"
|
|
94
|
+
className="w-full inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-11 px-8 text-base font-medium transition-colors"
|
|
95
|
+
>
|
|
96
|
+
{t('cartPage.proceedToCheckout')}
|
|
97
|
+
</Link>
|
|
98
|
+
) : (
|
|
99
|
+
<CheckoutButton size="lg" className="w-full">
|
|
100
|
+
{t('cartPage.proceedToCheckout')}
|
|
101
|
+
</CheckoutButton>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<p className="mt-4 text-center text-sm text-muted-foreground">
|
|
106
|
+
{shippingEnabled ? t('cart.shippingTaxesAtCheckout') : t('cartPage.taxesAtCheckout')}
|
|
107
|
+
</p>
|
|
108
|
+
|
|
109
|
+
{/* Free Shipping Message */}
|
|
110
|
+
{settings?.shippingEnabled && settings?.freeShippingThreshold > 0 && (
|
|
111
|
+
<div className="mt-4 text-center">
|
|
112
|
+
{subtotal < settings.freeShippingThreshold ? (
|
|
113
|
+
<p className="text-sm text-primary font-medium">
|
|
114
|
+
{t('cartPage.addMoreForFreeShipping', { amount: formatPrice(settings.freeShippingThreshold - subtotal) })}
|
|
115
|
+
</p>
|
|
116
|
+
) : (
|
|
117
|
+
<p className="text-sm text-green-600 font-medium">
|
|
118
|
+
✓ {t('cartPage.qualifyFreeShipping')}
|
|
119
|
+
</p>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
<div className="mt-6 pt-6 border-t">
|
|
125
|
+
<Link
|
|
126
|
+
href="/products"
|
|
127
|
+
className="text-sm text-primary hover:underline"
|
|
128
|
+
>
|
|
129
|
+
← {t('checkout.continueShopping')}
|
|
130
|
+
</Link>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
)
|
|
137
|
+
}
|