@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,106 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Layout Content Component
|
|
5
|
+
*
|
|
6
|
+
* Handles route-aware layout rendering to separate admin and customer experiences.
|
|
7
|
+
*
|
|
8
|
+
* - Customer routes (/products, /cart, etc.): Full store experience with Header, Footer, Cart
|
|
9
|
+
* - Admin routes (/admin/*): Minimal wrapper, admin layout handles its own chrome
|
|
10
|
+
*
|
|
11
|
+
* This separation ensures the store owner feels a clear disconnection between
|
|
12
|
+
* the admin panel and the customer-facing store.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { usePathname } from 'next/navigation'
|
|
16
|
+
import { CartProvider, CartDrawer } from '@rovela-ai/sdk/cart'
|
|
17
|
+
import {
|
|
18
|
+
CookieConsentProvider,
|
|
19
|
+
type CookieConsentConfig,
|
|
20
|
+
CookieBanner,
|
|
21
|
+
} from '@rovela-ai/sdk/core/cookie-consent'
|
|
22
|
+
import { AnalyticsProvider } from '@rovela-ai/sdk/analytics'
|
|
23
|
+
import { RovelaLocaleProvider, type StoreLocale } from '@rovela-ai/sdk/core'
|
|
24
|
+
import { Header } from '@/components/layout/Header'
|
|
25
|
+
import { Footer } from '@/components/layout/Footer'
|
|
26
|
+
import { PathSyncBridge } from '@/components/PathSyncBridge'
|
|
27
|
+
|
|
28
|
+
interface LayoutContentProps {
|
|
29
|
+
children: React.ReactNode
|
|
30
|
+
/** R021: master toggle from store_settings.cookie_banner_enabled.
|
|
31
|
+
* When false, the banner never renders and the consent provider
|
|
32
|
+
* short-circuits (grants all categories). */
|
|
33
|
+
cookieBannerEnabled?: boolean
|
|
34
|
+
/** Merchant banner configuration (store_settings.cookie_consent, sanitized). */
|
|
35
|
+
cookieConsentConfig?: CookieConsentConfig
|
|
36
|
+
/** Region-rule verdict computed server-side from the visitor's IP country. */
|
|
37
|
+
consentRegionExempt?: boolean
|
|
38
|
+
/** Request-resolved storefront locale (root layout). */
|
|
39
|
+
locale?: StoreLocale
|
|
40
|
+
/** Server-resolved store locale config (hydration-safe navigation). */
|
|
41
|
+
defaultLocale?: StoreLocale
|
|
42
|
+
enabledLocales?: StoreLocale[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function LayoutContent({
|
|
46
|
+
children,
|
|
47
|
+
cookieBannerEnabled = true,
|
|
48
|
+
cookieConsentConfig,
|
|
49
|
+
consentRegionExempt = false,
|
|
50
|
+
locale = 'en',
|
|
51
|
+
defaultLocale,
|
|
52
|
+
enabledLocales,
|
|
53
|
+
}: LayoutContentProps) {
|
|
54
|
+
const pathname = usePathname()
|
|
55
|
+
|
|
56
|
+
// Check if current route is an admin route
|
|
57
|
+
const isAdminRoute = pathname?.startsWith('/admin')
|
|
58
|
+
|
|
59
|
+
// Admin routes: No store header/footer, no cart infrastructure.
|
|
60
|
+
// The admin layout (app/admin/(dashboard)/layout.tsx) provides its own chrome.
|
|
61
|
+
// Cookie banner is customer-facing only — admins don't see it either way.
|
|
62
|
+
//
|
|
63
|
+
// PathSyncBridge is mounted on BOTH branches so the Rovela editor's path
|
|
64
|
+
// bar reflects whichever URL the iframe is showing — admin or customer.
|
|
65
|
+
if (isAdminRoute) {
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<PathSyncBridge />
|
|
69
|
+
{children}
|
|
70
|
+
</>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Customer routes: Full store experience.
|
|
75
|
+
//
|
|
76
|
+
// R021: CookieConsentProvider wraps the storefront so any script (added
|
|
77
|
+
// by Claude Code in response to a merchant request) can gate itself via
|
|
78
|
+
// `useCookieConsent()`. CookieBanner is the EDPB-compliant UI; it only
|
|
79
|
+
// renders when the master toggle is on AND the user hasn't decided yet
|
|
80
|
+
// (or their 12-month consent expired). Do not remove — this is the
|
|
81
|
+
// GDPR compliance surface for EU/UK-seated stores.
|
|
82
|
+
// AnalyticsProvider (§46) is mounted INSIDE CookieConsentProvider so it can
|
|
83
|
+
// read consent state via useCookieConsent(). It auto-fires page_view +
|
|
84
|
+
// session_start on the customer branch only — admin sessions short-circuit
|
|
85
|
+
// automatically. Never mount on the admin branch.
|
|
86
|
+
return (
|
|
87
|
+
<RovelaLocaleProvider locale={locale} defaultLocale={defaultLocale} enabledLocales={enabledLocales}>
|
|
88
|
+
<CookieConsentProvider
|
|
89
|
+
enabled={cookieBannerEnabled}
|
|
90
|
+
config={cookieConsentConfig}
|
|
91
|
+
regionExempt={consentRegionExempt}
|
|
92
|
+
>
|
|
93
|
+
<AnalyticsProvider>
|
|
94
|
+
<CartProvider>
|
|
95
|
+
<PathSyncBridge />
|
|
96
|
+
<Header />
|
|
97
|
+
<main className="flex-1">{children}</main>
|
|
98
|
+
<Footer />
|
|
99
|
+
<CartDrawer />
|
|
100
|
+
<CookieBanner />
|
|
101
|
+
</CartProvider>
|
|
102
|
+
</AnalyticsProvider>
|
|
103
|
+
</CookieConsentProvider>
|
|
104
|
+
</RovelaLocaleProvider>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catch-all URL redirect — SEO migration bridge.
|
|
3
|
+
*
|
|
4
|
+
* Fires ONLY for paths no real route matched: old URLs from a previous platform
|
|
5
|
+
* (PrestaShop, etc.) plus genuine 404s. It reads the PrestaShop id out of the
|
|
6
|
+
* incoming old URL and permanently redirects to the matching new page, using the
|
|
7
|
+
* `store_url_map` the catalog import populates by design (id → new path). An
|
|
8
|
+
* optional `store_redirects` table (a merchant CSV export) gives exact-path
|
|
9
|
+
* overrides for URLs that carry no id. Anything unmapped renders the normal 404.
|
|
10
|
+
*
|
|
11
|
+
* Read-only and fully guarded: any DB / missing-table error degrades to a clean
|
|
12
|
+
* 404, so this can never break the storefront. `permanentRedirect` / `notFound`
|
|
13
|
+
* are called OUTSIDE the try/catch — they signal via a thrown control-flow error
|
|
14
|
+
* that must not be swallowed.
|
|
15
|
+
*/
|
|
16
|
+
import { permanentRedirect, notFound } from 'next/navigation'
|
|
17
|
+
import { getSql } from '@rovela-ai/sdk/core/server'
|
|
18
|
+
|
|
19
|
+
export const dynamic = 'force-dynamic'
|
|
20
|
+
|
|
21
|
+
async function resolveRedirect(pathname: string): Promise<string | null> {
|
|
22
|
+
try {
|
|
23
|
+
const sql = getSql()
|
|
24
|
+
|
|
25
|
+
// 1. Exact-path overlay (optional merchant CSV export).
|
|
26
|
+
const exact = (await sql`
|
|
27
|
+
SELECT to_path FROM store_redirects WHERE from_path = ${pathname} LIMIT 1
|
|
28
|
+
`) as { to_path: string }[]
|
|
29
|
+
if (exact[0]?.to_path) return exact[0].to_path
|
|
30
|
+
|
|
31
|
+
// 2. By-design id engine — read the id out of the old URL (config-independent).
|
|
32
|
+
const segments = pathname.split('/').filter(Boolean)
|
|
33
|
+
if (segments.length === 0) return null
|
|
34
|
+
const idMatch = segments[segments.length - 1].match(/^(\d+)-/)
|
|
35
|
+
if (!idMatch) return null
|
|
36
|
+
const psId = idMatch[1]
|
|
37
|
+
|
|
38
|
+
// PrestaShop shape: products end in .html and/or sit under a category path;
|
|
39
|
+
// categories are a single `/{id}-{slug}` segment.
|
|
40
|
+
const kind =
|
|
41
|
+
pathname.endsWith('.html') || segments.length >= 2 ? 'product' : 'category'
|
|
42
|
+
|
|
43
|
+
// PrestaShop root/home categories (ids 1, 2) → homepage.
|
|
44
|
+
if (kind === 'category' && (psId === '1' || psId === '2')) return '/'
|
|
45
|
+
|
|
46
|
+
const hit = (await sql`
|
|
47
|
+
SELECT to_path FROM store_url_map WHERE kind = ${kind} AND ps_id = ${psId} LIMIT 1
|
|
48
|
+
`) as { to_path: string }[]
|
|
49
|
+
if (hit[0]?.to_path) return hit[0].to_path
|
|
50
|
+
|
|
51
|
+
// 3. Fallback for an unmapped (inactive/deleted) product: if the old URL's
|
|
52
|
+
// leading category segment resolves to a live category, send them there
|
|
53
|
+
// instead of 404. Never blanket-redirect to homepage (that reads as a
|
|
54
|
+
// soft 404 to search engines).
|
|
55
|
+
if (kind === 'product' && segments.length >= 2) {
|
|
56
|
+
const cat = (await sql`
|
|
57
|
+
SELECT id FROM categories WHERE slug = ${segments[0]} LIMIT 1
|
|
58
|
+
`) as { id: string }[]
|
|
59
|
+
if (cat[0]?.id) return `/products?category=${cat[0].id}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null
|
|
63
|
+
} catch {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default async function CatchAllRedirect({
|
|
69
|
+
params,
|
|
70
|
+
}: {
|
|
71
|
+
params: Promise<{ path?: string[] }>
|
|
72
|
+
}) {
|
|
73
|
+
const { path = [] } = await params
|
|
74
|
+
const pathname = '/' + path.map((s) => decodeURIComponent(s)).join('/')
|
|
75
|
+
|
|
76
|
+
const to = await resolveRedirect(pathname)
|
|
77
|
+
if (to) permanentRedirect(to)
|
|
78
|
+
notFound()
|
|
79
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Customer Addresses Page
|
|
5
|
+
*
|
|
6
|
+
* Self-service address book (add / edit / remove / set default). Admin identity
|
|
7
|
+
* cannot reach this page: `useAuth()` role-filters the session and `AuthGuard`
|
|
8
|
+
* redirects anything that isn't a signed-in customer to `/auth/signin`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { LocaleLink as Link } from '@rovela-ai/sdk/core'
|
|
12
|
+
import { ChevronLeft } from 'lucide-react'
|
|
13
|
+
import { AuthGuard } from '@rovela-ai/sdk/auth'
|
|
14
|
+
import { AddressBook } from '@rovela-ai/sdk/auth'
|
|
15
|
+
import { useT } from '@rovela-ai/sdk/core'
|
|
16
|
+
|
|
17
|
+
function AddressesContent() {
|
|
18
|
+
const t = useT()
|
|
19
|
+
return (
|
|
20
|
+
<div className="container mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-8">
|
|
21
|
+
<Link
|
|
22
|
+
href="/account"
|
|
23
|
+
className="mb-6 inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
|
|
24
|
+
>
|
|
25
|
+
<ChevronLeft className="h-4 w-4" />
|
|
26
|
+
{t('account.backToAccountLower')}
|
|
27
|
+
</Link>
|
|
28
|
+
|
|
29
|
+
<h1 className="mb-2 text-3xl font-bold font-heading">{t('account.myAddresses')}</h1>
|
|
30
|
+
<p className="mb-8 text-sm text-muted-foreground">
|
|
31
|
+
{t('account.addressesHint')}
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<AddressBook />
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default function AddressesPage() {
|
|
40
|
+
return (
|
|
41
|
+
<AuthGuard redirectTo="/auth/signin">
|
|
42
|
+
<AddressesContent />
|
|
43
|
+
</AuthGuard>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account area gate — when the merchant sets Customer accounts to Off
|
|
3
|
+
* (admin Settings → Checkout Settings), the whole /account area redirects
|
|
4
|
+
* home BEFORE any HTML ships. Customer data is never deleted — flipping
|
|
5
|
+
* the setting back restores access. Fail-open on a settings-read error.
|
|
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 AccountLayout({ children }: { children: React.ReactNode }) {
|
|
14
|
+
const settings = await findSettings().catch(() => null)
|
|
15
|
+
if (settings?.customerAccounts === false) redirect('/')
|
|
16
|
+
return <>{children}</>
|
|
17
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Order Invoice Page
|
|
5
|
+
*
|
|
6
|
+
* A clean, print-ready invoice for a single order, built from the immutable
|
|
7
|
+
* order snapshot + store settings. "Download" uses the browser's print-to-PDF.
|
|
8
|
+
* Admin identity cannot reach this page (AuthGuard + role-filtered useAuth).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState, useEffect, use } from 'react'
|
|
12
|
+
import { LocaleLink as Link } from '@rovela-ai/sdk/core'
|
|
13
|
+
import { ChevronLeft, Printer, Loader2 } from 'lucide-react'
|
|
14
|
+
import { AuthGuard } from '@rovela-ai/sdk/auth'
|
|
15
|
+
import { useStoreSettings, useT, useStoreFormatting } from '@rovela-ai/sdk/core'
|
|
16
|
+
|
|
17
|
+
interface InvoiceItem {
|
|
18
|
+
id: string
|
|
19
|
+
name: string
|
|
20
|
+
quantity: number
|
|
21
|
+
price: number
|
|
22
|
+
attributes?: Record<string, string>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface InvoiceAddress {
|
|
26
|
+
firstName?: string
|
|
27
|
+
lastName?: string
|
|
28
|
+
name?: string
|
|
29
|
+
company?: string
|
|
30
|
+
line1: string
|
|
31
|
+
line2?: string
|
|
32
|
+
city: string
|
|
33
|
+
state?: string
|
|
34
|
+
postalCode: string
|
|
35
|
+
country: string
|
|
36
|
+
phone?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface InvoiceOrder {
|
|
40
|
+
id: string
|
|
41
|
+
status: string
|
|
42
|
+
total: number
|
|
43
|
+
subtotal: number
|
|
44
|
+
tax: number
|
|
45
|
+
shipping: number
|
|
46
|
+
discountAmount?: number | string
|
|
47
|
+
discountCode?: string | null
|
|
48
|
+
refundAmount?: number
|
|
49
|
+
createdAt: string
|
|
50
|
+
items: InvoiceItem[]
|
|
51
|
+
shippingAddress: InvoiceAddress
|
|
52
|
+
billingAddress?: InvoiceAddress
|
|
53
|
+
customerEmail?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function addrName(a: InvoiceAddress): string {
|
|
57
|
+
if (a.firstName || a.lastName) return `${a.firstName ?? ''} ${a.lastName ?? ''}`.trim()
|
|
58
|
+
return a.name ?? ''
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function AddressBlock({ address }: { address: InvoiceAddress }) {
|
|
62
|
+
return (
|
|
63
|
+
<div className="text-sm text-muted-foreground leading-relaxed">
|
|
64
|
+
<p className="font-medium text-foreground">{addrName(address)}</p>
|
|
65
|
+
{address.company && <p>{address.company}</p>}
|
|
66
|
+
<p>{address.line1}</p>
|
|
67
|
+
{address.line2 && <p>{address.line2}</p>}
|
|
68
|
+
<p>
|
|
69
|
+
{address.city}
|
|
70
|
+
{address.state ? `, ${address.state}` : ''} {address.postalCode}
|
|
71
|
+
</p>
|
|
72
|
+
<p>{address.country}</p>
|
|
73
|
+
{address.phone && <p>{address.phone}</p>}
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function InvoiceContent({ orderId }: { orderId: string }) {
|
|
79
|
+
const t = useT()
|
|
80
|
+
const { settings } = useStoreSettings()
|
|
81
|
+
// Locale-aware price and date shapes (active i18n locale + store currency).
|
|
82
|
+
const { formatPrice, formatDate } = useStoreFormatting()
|
|
83
|
+
const [order, setOrder] = useState<InvoiceOrder | null>(null)
|
|
84
|
+
// Per-order snapshot (orders.currency): the order renders in the currency
|
|
85
|
+
// it was PLACED in, even after the store's currency changes.
|
|
86
|
+
const orderCurrency = (order as { currency?: string | null } | null)?.currency
|
|
87
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
88
|
+
const [error, setError] = useState<string | null>(null)
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
let active = true
|
|
92
|
+
async function load() {
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(`/api/account/orders/${orderId}`)
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
if (active) setError(res.status === 404 ? t('account.orderNotFound') : t('account.orderLoadFailed'))
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
const data = await res.json()
|
|
100
|
+
if (active) setOrder(data.order ?? data)
|
|
101
|
+
} catch {
|
|
102
|
+
if (active) setError(t('account.orderLoadFailed'))
|
|
103
|
+
} finally {
|
|
104
|
+
if (active) setIsLoading(false)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
load()
|
|
108
|
+
return () => {
|
|
109
|
+
active = false
|
|
110
|
+
}
|
|
111
|
+
}, [orderId])
|
|
112
|
+
|
|
113
|
+
if (isLoading) {
|
|
114
|
+
return (
|
|
115
|
+
<div className="flex items-center justify-center py-24 text-muted-foreground">
|
|
116
|
+
<Loader2 className="h-6 w-6 animate-spin" />
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (error || !order) {
|
|
122
|
+
return (
|
|
123
|
+
<div className="container mx-auto max-w-2xl px-4 py-16 text-center">
|
|
124
|
+
<p className="text-muted-foreground">{error || t('account.orderNotFound')}</p>
|
|
125
|
+
<Link href="/account/orders" className="mt-4 inline-block text-sm text-primary hover:underline">
|
|
126
|
+
{t('invoice.backToOrders')}
|
|
127
|
+
</Link>
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const billing = order.billingAddress ?? order.shippingAddress
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className="container mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-8">
|
|
136
|
+
{/* Screen-only controls */}
|
|
137
|
+
<div className="mb-6 flex items-center justify-between print:hidden">
|
|
138
|
+
<Link
|
|
139
|
+
href={`/account/orders/${order.id}`}
|
|
140
|
+
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
|
|
141
|
+
>
|
|
142
|
+
<ChevronLeft className="h-4 w-4" />
|
|
143
|
+
{t('invoice.backToOrder')}
|
|
144
|
+
</Link>
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onClick={() => window.print()}
|
|
148
|
+
className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
|
149
|
+
>
|
|
150
|
+
<Printer className="h-4 w-4" />
|
|
151
|
+
{t('invoice.printSavePdf')}
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Invoice document */}
|
|
156
|
+
<div className="rounded-lg border bg-card p-8 print:border-0 print:p-0">
|
|
157
|
+
{/* Header */}
|
|
158
|
+
<div className="flex flex-col gap-6 border-b pb-6 sm:flex-row sm:items-start sm:justify-between">
|
|
159
|
+
<div>
|
|
160
|
+
{settings?.logoUrl ? (
|
|
161
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
162
|
+
<img src={settings.logoUrl} alt={settings.storeName} className="mb-2 h-10 w-auto object-contain" />
|
|
163
|
+
) : (
|
|
164
|
+
<p className="text-xl font-bold font-heading">{settings?.storeName}</p>
|
|
165
|
+
)}
|
|
166
|
+
{settings?.storeEmail && (
|
|
167
|
+
<p className="text-sm text-muted-foreground">{settings.storeEmail}</p>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
<div className="text-left sm:text-right">
|
|
171
|
+
<h1 className="text-2xl font-bold font-heading">{t('invoice.title')}</h1>
|
|
172
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
173
|
+
#{order.id.slice(0, 8).toUpperCase()}
|
|
174
|
+
</p>
|
|
175
|
+
<p className="text-sm text-muted-foreground">
|
|
176
|
+
{formatDate(new Date(order.createdAt))}
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{/* Parties */}
|
|
182
|
+
<div className="grid grid-cols-1 gap-8 py-6 sm:grid-cols-2">
|
|
183
|
+
<div>
|
|
184
|
+
<p className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
185
|
+
{t('invoice.billTo')}
|
|
186
|
+
</p>
|
|
187
|
+
<AddressBlock address={billing} />
|
|
188
|
+
</div>
|
|
189
|
+
<div>
|
|
190
|
+
<p className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
191
|
+
{t('invoice.shipTo')}
|
|
192
|
+
</p>
|
|
193
|
+
<AddressBlock address={order.shippingAddress} />
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{/* Line items */}
|
|
198
|
+
<table className="w-full text-sm">
|
|
199
|
+
<thead>
|
|
200
|
+
<tr className="border-b text-left text-xs uppercase tracking-wide text-muted-foreground">
|
|
201
|
+
<th className="py-2 font-semibold">{t('invoice.item')}</th>
|
|
202
|
+
<th className="py-2 text-center font-semibold">{t('invoice.qty')}</th>
|
|
203
|
+
<th className="py-2 text-right font-semibold">{t('invoice.price')}</th>
|
|
204
|
+
<th className="py-2 text-right font-semibold">{t('common.total')}</th>
|
|
205
|
+
</tr>
|
|
206
|
+
</thead>
|
|
207
|
+
<tbody>
|
|
208
|
+
{order.items.map((item, i) => (
|
|
209
|
+
<tr key={item.id || i} className="border-b border-border/60">
|
|
210
|
+
<td className="py-3">
|
|
211
|
+
<p className="font-medium text-foreground">{item.name}</p>
|
|
212
|
+
{item.attributes && Object.keys(item.attributes).length > 0 && (
|
|
213
|
+
<p className="text-xs text-muted-foreground">
|
|
214
|
+
{Object.entries(item.attributes)
|
|
215
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
216
|
+
.join(', ')}
|
|
217
|
+
</p>
|
|
218
|
+
)}
|
|
219
|
+
</td>
|
|
220
|
+
<td className="py-3 text-center text-muted-foreground">{item.quantity}</td>
|
|
221
|
+
<td className="py-3 text-right text-muted-foreground">{formatPrice(item.price, orderCurrency)}</td>
|
|
222
|
+
<td className="py-3 text-right font-medium">
|
|
223
|
+
{formatPrice(item.price * item.quantity, orderCurrency)}
|
|
224
|
+
</td>
|
|
225
|
+
</tr>
|
|
226
|
+
))}
|
|
227
|
+
</tbody>
|
|
228
|
+
</table>
|
|
229
|
+
|
|
230
|
+
{/* Totals */}
|
|
231
|
+
<div className="ml-auto mt-6 w-full max-w-xs space-y-2 text-sm">
|
|
232
|
+
<div className="flex justify-between text-muted-foreground">
|
|
233
|
+
<span>{t('common.subtotal')}</span>
|
|
234
|
+
<span>{formatPrice(order.subtotal, orderCurrency)}</span>
|
|
235
|
+
</div>
|
|
236
|
+
{Number(order.discountAmount ?? 0) > 0 && (
|
|
237
|
+
<div className="flex justify-between text-emerald-600">
|
|
238
|
+
<span>{t('common.discount')}{order.discountCode ? ` (${order.discountCode})` : ''}</span>
|
|
239
|
+
<span>-{formatPrice(order.discountAmount!, orderCurrency)}</span>
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
<div className="flex justify-between text-muted-foreground">
|
|
243
|
+
<span>{t('common.shipping')}</span>
|
|
244
|
+
<span>{order.shipping > 0 ? formatPrice(order.shipping, orderCurrency) : t('common.free')}</span>
|
|
245
|
+
</div>
|
|
246
|
+
<div className="flex justify-between text-muted-foreground">
|
|
247
|
+
<span>{t('common.tax')}</span>
|
|
248
|
+
<span>{formatPrice(order.tax, orderCurrency)}</span>
|
|
249
|
+
</div>
|
|
250
|
+
<div className="flex justify-between border-t pt-2 text-base font-semibold text-foreground">
|
|
251
|
+
<span>{t('common.total')}</span>
|
|
252
|
+
<span>{formatPrice(order.total, orderCurrency)}</span>
|
|
253
|
+
</div>
|
|
254
|
+
{order.refundAmount != null && order.refundAmount > 0 && (
|
|
255
|
+
<div className="flex justify-between text-amber-600">
|
|
256
|
+
<span>{t('account.refunded')}</span>
|
|
257
|
+
<span>-{formatPrice(order.refundAmount)}</span>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<p className="mt-8 border-t pt-6 text-center text-xs text-muted-foreground">
|
|
263
|
+
{t('invoice.thankYou')}
|
|
264
|
+
</p>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export default function InvoicePage({ params }: { params: Promise<{ id: string }> }) {
|
|
271
|
+
const { id } = use(params)
|
|
272
|
+
return (
|
|
273
|
+
<AuthGuard redirectTo="/auth/signin">
|
|
274
|
+
<InvoiceContent orderId={id} />
|
|
275
|
+
</AuthGuard>
|
|
276
|
+
)
|
|
277
|
+
}
|