@doswiftly/cli 0.1.18 → 0.1.19
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/README.md +23 -323
- package/dist/commands/check.js +1 -1
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +39 -20
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +4 -4
- package/dist/commands/sdk.js +5 -5
- package/dist/commands/sdk.js.map +1 -1
- package/dist/commands/template.js +4 -4
- package/dist/commands/template.js.map +1 -1
- package/dist/commands/types.js +5 -5
- package/dist/commands/types.js.map +1 -1
- package/dist/commands/verify.js +2 -2
- package/dist/commands/verify.js.map +1 -1
- package/dist/lib/package-manager.d.ts +1 -1
- package/dist/lib/package-manager.js +1 -1
- package/package.json +1 -1
- package/templates/storefront-nextjs/README.md +16 -12
- package/templates/storefront-nextjs/app/account/orders/page.tsx +2 -2
- package/templates/storefront-nextjs/app/account/page.tsx +2 -2
- package/templates/storefront-nextjs/app/auth/login/page.tsx +1 -1
- package/templates/storefront-nextjs/app/auth/register/page.tsx +1 -1
- package/templates/storefront-nextjs/app/cart/page.tsx +1 -1
- package/templates/storefront-nextjs/app/categories/[slug]/page.tsx +2 -2
- package/templates/storefront-nextjs/app/categories/page.tsx +1 -1
- package/templates/storefront-nextjs/app/collections/[slug]/page.tsx +1 -1
- package/templates/storefront-nextjs/app/collections/page.tsx +1 -1
- package/templates/storefront-nextjs/app/page.tsx +1 -1
- package/templates/storefront-nextjs/app/products/[slug]/page.tsx +1 -1
- package/templates/storefront-nextjs/app/products/page.tsx +2 -2
- package/templates/storefront-nextjs/app/search/page.tsx +1 -1
- package/templates/storefront-nextjs/components/auth/auth-guard.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/add-to-cart-button.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/cart-icon.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/currency-selector.tsx +2 -2
- package/templates/storefront-nextjs/components/commerce/product-filters.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/product-price.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/search-input.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/sort-select.tsx +1 -1
- package/templates/storefront-nextjs/components/providers.tsx +1 -1
- package/templates/storefront-nextjs/lib/currency.tsx +3 -3
- package/templates/storefront-nextjs/lib/format.ts +1 -1
- package/templates/storefront-nextjs/lib/graphql-queries.ts +3 -3
- package/templates/storefront-nextjs/package.dev.json +1 -1
- package/templates/storefront-nextjs/package.json +1 -1
- package/templates/storefront-nextjs/package.json.template +1 -1
- package/templates/storefront-nextjs-shadcn/.github/workflows/deploy.yml +47 -0
- package/templates/storefront-nextjs-shadcn/.github/workflows/preview.yml +47 -0
- package/templates/storefront-nextjs-shadcn/CLAUDE.md +148 -35
- package/templates/storefront-nextjs-shadcn/README.md +29 -162
- package/templates/storefront-nextjs-shadcn/app/account/addresses/page.tsx +98 -91
- package/templates/storefront-nextjs-shadcn/app/account/error.tsx +43 -0
- package/templates/storefront-nextjs-shadcn/app/account/loading.tsx +19 -0
- package/templates/storefront-nextjs-shadcn/app/account/loyalty/page.tsx +53 -162
- package/templates/storefront-nextjs-shadcn/app/account/orders/[id]/loading.tsx +60 -0
- package/templates/storefront-nextjs-shadcn/app/account/orders/[id]/page.tsx +36 -47
- package/templates/storefront-nextjs-shadcn/app/account/orders/page.tsx +46 -29
- package/templates/storefront-nextjs-shadcn/app/account/page.tsx +8 -5
- package/templates/storefront-nextjs-shadcn/app/account/settings/page.tsx +108 -71
- package/templates/storefront-nextjs-shadcn/app/api/auth/clear-token/route.ts +2 -86
- package/templates/storefront-nextjs-shadcn/app/api/auth/set-token/route.ts +2 -124
- package/templates/storefront-nextjs-shadcn/app/auth/forgot-password/page.tsx +10 -5
- package/templates/storefront-nextjs-shadcn/app/blog/[slug]/loading.tsx +17 -0
- package/templates/storefront-nextjs-shadcn/app/blog/[slug]/page.tsx +43 -2
- package/templates/storefront-nextjs-shadcn/app/blog/loading.tsx +19 -0
- package/templates/storefront-nextjs-shadcn/app/brands/page.tsx +2 -1
- package/templates/storefront-nextjs-shadcn/app/cart/loading.tsx +26 -0
- package/templates/storefront-nextjs-shadcn/app/cart/page.tsx +6 -3
- package/templates/storefront-nextjs-shadcn/app/categories/[slug]/category-products-client.tsx +56 -0
- package/templates/storefront-nextjs-shadcn/app/categories/[slug]/loading.tsx +32 -0
- package/templates/storefront-nextjs-shadcn/app/categories/[slug]/page.tsx +76 -59
- package/templates/storefront-nextjs-shadcn/app/categories/page.tsx +8 -4
- package/templates/storefront-nextjs-shadcn/app/checkout/error.tsx +43 -0
- package/templates/storefront-nextjs-shadcn/app/checkout/loading.tsx +31 -0
- package/templates/storefront-nextjs-shadcn/app/checkout/page.tsx +116 -79
- package/templates/storefront-nextjs-shadcn/app/collections/[handle]/loading.tsx +19 -0
- package/templates/storefront-nextjs-shadcn/app/collections/[handle]/page.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/app/collections/loading.tsx +18 -0
- package/templates/storefront-nextjs-shadcn/app/collections/page.tsx +7 -4
- package/templates/storefront-nextjs-shadcn/app/global-error.tsx +117 -0
- package/templates/storefront-nextjs-shadcn/app/globals.css +8 -0
- package/templates/storefront-nextjs-shadcn/app/layout.tsx +46 -11
- package/templates/storefront-nextjs-shadcn/app/products/[slug]/error.tsx +43 -0
- package/templates/storefront-nextjs-shadcn/app/products/[slug]/loading.tsx +29 -0
- package/templates/storefront-nextjs-shadcn/app/products/[slug]/page.tsx +6 -6
- package/templates/storefront-nextjs-shadcn/app/products/[slug]/product-client.tsx +15 -61
- package/templates/storefront-nextjs-shadcn/app/products/loading.tsx +32 -0
- package/templates/storefront-nextjs-shadcn/app/products/products-client.tsx +405 -151
- package/templates/storefront-nextjs-shadcn/app/search/loading.tsx +18 -0
- package/templates/storefront-nextjs-shadcn/app/wishlist/page.tsx +8 -5
- package/templates/storefront-nextjs-shadcn/codegen.ts +48 -31
- package/templates/storefront-nextjs-shadcn/components/account/customer-info.fragment.graphql +36 -0
- package/templates/storefront-nextjs-shadcn/components/account/order-details.tsx +3 -1
- package/templates/storefront-nextjs-shadcn/components/account/order-history.tsx +26 -24
- package/templates/storefront-nextjs-shadcn/components/account/order-summary.fragment.graphql +36 -0
- package/templates/storefront-nextjs-shadcn/components/auth/account-menu.tsx +9 -9
- package/templates/storefront-nextjs-shadcn/components/auth/login-form.tsx +11 -37
- package/templates/storefront-nextjs-shadcn/components/auth/register-form.tsx +37 -23
- package/templates/storefront-nextjs-shadcn/components/cart/cart-drawer.tsx +4 -3
- package/templates/storefront-nextjs-shadcn/components/cart/cart-icon.tsx +8 -5
- package/templates/storefront-nextjs-shadcn/components/cart/cart-item.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/cart/cart-line.fragment.graphql +53 -0
- package/templates/storefront-nextjs-shadcn/components/cart/cart-summary.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/cart/shipping-estimator.tsx +22 -7
- package/templates/storefront-nextjs-shadcn/components/commerce/currency-selector.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/commerce/product-actions.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/commerce/search-input.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/common/price-display.tsx +35 -11
- package/templates/storefront-nextjs-shadcn/components/discount/discount-breakdown.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/discount/discount-code-input.tsx +3 -3
- package/templates/storefront-nextjs-shadcn/components/filters/range-slider-filter.tsx +5 -5
- package/templates/storefront-nextjs-shadcn/components/gift-card/gift-card-input.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/home/category-grid.tsx +2 -1
- package/templates/storefront-nextjs-shadcn/components/home/collection-card.fragment.graphql +21 -0
- package/templates/storefront-nextjs-shadcn/components/home/featured-collections.tsx +2 -12
- package/templates/storefront-nextjs-shadcn/components/home/index.ts +0 -1
- package/templates/storefront-nextjs-shadcn/components/hydrated.tsx +24 -0
- package/templates/storefront-nextjs-shadcn/components/layout/breadcrumbs.tsx +4 -4
- package/templates/storefront-nextjs-shadcn/components/layout/category-node.fragment.graphql +22 -0
- package/templates/storefront-nextjs-shadcn/components/layout/currency-selector.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/layout/header.tsx +33 -23
- package/templates/storefront-nextjs-shadcn/components/loyalty/points-balance.tsx +2 -11
- package/templates/storefront-nextjs-shadcn/components/loyalty/points-history.tsx +8 -25
- package/templates/storefront-nextjs-shadcn/components/loyalty/referral-section.tsx +10 -19
- package/templates/storefront-nextjs-shadcn/components/loyalty/rewards-catalog.tsx +17 -41
- package/templates/storefront-nextjs-shadcn/components/loyalty/tier-progress.tsx +2 -29
- package/templates/storefront-nextjs-shadcn/components/order/index.ts +6 -1
- package/templates/storefront-nextjs-shadcn/components/product/b2b-price-display.tsx +3 -1
- package/templates/storefront-nextjs-shadcn/components/product/filter-active-pills.tsx +69 -0
- package/templates/storefront-nextjs-shadcn/components/product/filter-mobile-sheet.tsx +84 -0
- package/templates/storefront-nextjs-shadcn/components/product/filter-price-range.tsx +138 -0
- package/templates/storefront-nextjs-shadcn/components/product/index.ts +9 -2
- package/templates/storefront-nextjs-shadcn/components/product/product-card.fragment.graphql +49 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-card.tsx +3 -31
- package/templates/storefront-nextjs-shadcn/components/product/product-detail.fragment.graphql +52 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-filters.tsx +176 -123
- package/templates/storefront-nextjs-shadcn/components/product/product-grid.tsx +3 -5
- package/templates/storefront-nextjs-shadcn/components/product/product-image.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/product/product-price.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/product/product-reviews.tsx +5 -4
- package/templates/storefront-nextjs-shadcn/components/product/product-sort.tsx +19 -7
- package/templates/storefront-nextjs-shadcn/components/product/product-variant-selector.tsx +8 -23
- package/templates/storefront-nextjs-shadcn/components/product/product-variant.fragment.graphql +51 -0
- package/templates/storefront-nextjs-shadcn/components/product/review-card.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/product/review-form.tsx +1 -7
- package/templates/storefront-nextjs-shadcn/components/product/savings-display.tsx +17 -2
- package/templates/storefront-nextjs-shadcn/components/product/similar-products.tsx +3 -2
- package/templates/storefront-nextjs-shadcn/components/providers/index.ts +1 -1
- package/templates/storefront-nextjs-shadcn/components/providers/stores-provider.tsx +30 -0
- package/templates/storefront-nextjs-shadcn/components/providers/theme-provider.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/returns/index.ts +2 -2
- package/templates/storefront-nextjs-shadcn/components/returns/return-request-form.tsx +3 -2
- package/templates/storefront-nextjs-shadcn/components/search/search-results.tsx +3 -2
- package/templates/storefront-nextjs-shadcn/components/ui/form.tsx +174 -0
- package/templates/storefront-nextjs-shadcn/components/ui/index.ts +30 -2
- package/templates/storefront-nextjs-shadcn/components/ui/progress.tsx +40 -0
- package/templates/storefront-nextjs-shadcn/components/ui/sheet.tsx +107 -0
- package/templates/storefront-nextjs-shadcn/components/ui/slider.tsx +33 -0
- package/templates/storefront-nextjs-shadcn/components/ui/textarea.tsx +24 -0
- package/templates/storefront-nextjs-shadcn/components/wishlist/wishlist-icon.tsx +3 -1
- package/templates/storefront-nextjs-shadcn/generated/graphql.ts +12779 -0
- package/templates/storefront-nextjs-shadcn/graphql/custom.example.graphql +159 -0
- package/templates/storefront-nextjs-shadcn/hooks/index.ts +2 -0
- package/templates/storefront-nextjs-shadcn/hooks/use-auth-sync.ts +42 -0
- package/templates/storefront-nextjs-shadcn/hooks/use-auth.ts +17 -295
- package/templates/storefront-nextjs-shadcn/hooks/use-cart-actions.ts +51 -19
- package/templates/storefront-nextjs-shadcn/hooks/use-cart-sync.ts +13 -9
- package/templates/storefront-nextjs-shadcn/lib/auth/routes.ts +4 -17
- package/templates/storefront-nextjs-shadcn/lib/graphql/client.ts +22 -99
- package/templates/storefront-nextjs-shadcn/lib/graphql/config.ts +32 -0
- package/templates/storefront-nextjs-shadcn/lib/graphql/fragments.ts +34 -0
- package/templates/storefront-nextjs-shadcn/lib/graphql/hooks.ts +687 -632
- package/templates/storefront-nextjs-shadcn/lib/graphql/query-keys.ts +86 -0
- package/templates/storefront-nextjs-shadcn/lib/graphql/server.ts +131 -182
- package/templates/storefront-nextjs-shadcn/lib/graphql/types.ts +62 -0
- package/templates/storefront-nextjs-shadcn/lib/theme/theme-config.ts +0 -17
- package/templates/storefront-nextjs-shadcn/next-env.d.ts +6 -0
- package/templates/storefront-nextjs-shadcn/package.dev.json +1 -3
- package/templates/storefront-nextjs-shadcn/package.json +12 -13
- package/templates/storefront-nextjs-shadcn/package.json.template +6 -7
- package/templates/storefront-nextjs-shadcn/proxy.ts +3 -4
- package/templates/storefront-nextjs-shadcn/stores/cart-store.ts +41 -39
- package/templates/storefront-nextjs-shadcn/stores/checkout-store.ts +64 -75
- package/templates/storefront-nextjs-shadcn/stores/wishlist-store.ts +178 -177
- package/templates/storefront-nextjs-shadcn/tsconfig.json +23 -5
- package/templates/storefront-nextjs-shadcn/CART_INTEGRATION.md +0 -282
- package/templates/storefront-nextjs-shadcn/GRAPHQL_DOCUMENT_NAMES.md +0 -190
- package/templates/storefront-nextjs-shadcn/GRAPHQL_ERROR_HANDLING.md +0 -263
- package/templates/storefront-nextjs-shadcn/GRAPHQL_FIXES_SUMMARY.md +0 -135
- package/templates/storefront-nextjs-shadcn/GRAPHQL_INTEGRATION_COMPLETE.md +0 -142
- package/templates/storefront-nextjs-shadcn/INTEGRATION_CHECKLIST.md +0 -448
- package/templates/storefront-nextjs-shadcn/PRODUCT_DETAIL_PAGE_IMPLEMENTATION.md +0 -307
- package/templates/storefront-nextjs-shadcn/THEME_CUSTOMIZATION.md +0 -245
- package/templates/storefront-nextjs-shadcn/components/providers/currency-provider.tsx +0 -103
- package/templates/storefront-nextjs-shadcn/graphql/collections.example.ts +0 -168
- package/templates/storefront-nextjs-shadcn/graphql/products.example.ts +0 -160
- package/templates/storefront-nextjs-shadcn/lib/auth/cookies.ts +0 -220
- package/templates/storefront-nextjs-shadcn/lib/config.ts +0 -46
- package/templates/storefront-nextjs-shadcn/lib/currency/IMPLEMENTATION_SUMMARY.md +0 -254
- package/templates/storefront-nextjs-shadcn/lib/currency/README.md +0 -464
- package/templates/storefront-nextjs-shadcn/lib/currency/cookie-manager.test.ts +0 -328
- package/templates/storefront-nextjs-shadcn/lib/currency/cookie-manager.ts +0 -295
- package/templates/storefront-nextjs-shadcn/lib/currency/index.ts +0 -27
- package/templates/storefront-nextjs-shadcn/lib/format.ts +0 -226
- package/templates/storefront-nextjs-shadcn/lib/hooks.ts +0 -30
- package/templates/storefront-nextjs-shadcn/stores/auth-store.ts +0 -66
- package/templates/storefront-nextjs-shadcn/stores/currency-store.ts +0 -103
|
@@ -1,125 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
* API Route: Set Authentication Token
|
|
3
|
-
*
|
|
4
|
-
* Sets the customer access token in an httpOnly cookie.
|
|
5
|
-
* This provides security by preventing client-side JavaScript from accessing the token.
|
|
6
|
-
*
|
|
7
|
-
* Security Features:
|
|
8
|
-
* 1. httpOnly cookie - Cannot be accessed via JavaScript (XSS protection)
|
|
9
|
-
* 2. Secure flag - Only sent over HTTPS in production
|
|
10
|
-
* 3. SameSite=Lax - CSRF protection
|
|
11
|
-
* 4. Origin validation - Only accepts requests from same origin
|
|
12
|
-
* 5. Content-Type validation - Only accepts JSON
|
|
13
|
-
*
|
|
14
|
-
* @see lib/auth/cookies.ts - Cookie configuration
|
|
15
|
-
* @see hooks/use-auth.ts - Client-side usage
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```tsx
|
|
19
|
-
* // Client-side usage (via setAuthToken helper)
|
|
20
|
-
* await setAuthToken(customerAccessToken.accessToken);
|
|
21
|
-
* ```
|
|
22
|
-
*/
|
|
1
|
+
import { createSetTokenHandler } from '@doswiftly/storefront-sdk';
|
|
23
2
|
|
|
24
|
-
|
|
25
|
-
import { AUTH_COOKIE_CONFIG } from "@/lib/auth/cookies";
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Request body schema
|
|
29
|
-
*/
|
|
30
|
-
interface SetTokenRequest {
|
|
31
|
-
token: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* POST /api/auth/set-token
|
|
36
|
-
*
|
|
37
|
-
* Sets the authentication token in an httpOnly cookie.
|
|
38
|
-
*
|
|
39
|
-
* @param request - Next.js request object
|
|
40
|
-
* @returns Response with Set-Cookie header
|
|
41
|
-
*/
|
|
42
|
-
export async function POST(request: NextRequest) {
|
|
43
|
-
try {
|
|
44
|
-
// 1. CSRF Protection: Validate origin
|
|
45
|
-
const origin = request.headers.get("origin");
|
|
46
|
-
const host = request.headers.get("host");
|
|
47
|
-
|
|
48
|
-
// Only allow requests from same origin
|
|
49
|
-
if (origin && !origin.includes(host || "")) {
|
|
50
|
-
return NextResponse.json(
|
|
51
|
-
{ error: "Invalid origin" },
|
|
52
|
-
{ status: 403 }
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 2. Validate Content-Type
|
|
57
|
-
const contentType = request.headers.get("content-type");
|
|
58
|
-
if (!contentType?.includes("application/json")) {
|
|
59
|
-
return NextResponse.json(
|
|
60
|
-
{ error: "Content-Type must be application/json" },
|
|
61
|
-
{ status: 400 }
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// 3. Parse and validate request body
|
|
66
|
-
let body: SetTokenRequest;
|
|
67
|
-
try {
|
|
68
|
-
body = await request.json();
|
|
69
|
-
} catch {
|
|
70
|
-
return NextResponse.json(
|
|
71
|
-
{ error: "Invalid JSON body" },
|
|
72
|
-
{ status: 400 }
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const { token } = body;
|
|
77
|
-
|
|
78
|
-
if (!token || typeof token !== "string" || token.trim() === "") {
|
|
79
|
-
return NextResponse.json(
|
|
80
|
-
{ error: "Token is required and must be a non-empty string" },
|
|
81
|
-
{ status: 400 }
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// 4. Create response with Set-Cookie header
|
|
86
|
-
const response = NextResponse.json(
|
|
87
|
-
{ success: true, message: "Token set successfully" },
|
|
88
|
-
{ status: 200 }
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
// 5. Set httpOnly cookie
|
|
92
|
-
response.cookies.set({
|
|
93
|
-
name: AUTH_COOKIE_CONFIG.name,
|
|
94
|
-
value: token,
|
|
95
|
-
maxAge: AUTH_COOKIE_CONFIG.maxAge,
|
|
96
|
-
path: AUTH_COOKIE_CONFIG.path,
|
|
97
|
-
sameSite: AUTH_COOKIE_CONFIG.sameSite,
|
|
98
|
-
secure: AUTH_COOKIE_CONFIG.secure,
|
|
99
|
-
httpOnly: AUTH_COOKIE_CONFIG.httpOnly,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
return response;
|
|
103
|
-
} catch (error) {
|
|
104
|
-
console.error("Error setting auth token:", error);
|
|
105
|
-
return NextResponse.json(
|
|
106
|
-
{ error: "Internal server error" },
|
|
107
|
-
{ status: 500 }
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* OPTIONS /api/auth/set-token
|
|
114
|
-
*
|
|
115
|
-
* Handle preflight requests for CORS.
|
|
116
|
-
*/
|
|
117
|
-
export async function OPTIONS() {
|
|
118
|
-
return new NextResponse(null, {
|
|
119
|
-
status: 204,
|
|
120
|
-
headers: {
|
|
121
|
-
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
122
|
-
"Access-Control-Allow-Headers": "Content-Type",
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
}
|
|
3
|
+
export const POST = createSetTokenHandler();
|
|
@@ -3,22 +3,27 @@
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { useMutation } from "@tanstack/react-query";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { useExecute } from "@/lib/graphql/client";
|
|
7
|
+
import {
|
|
8
|
+
CustomerPasswordRecoverDocument,
|
|
9
|
+
type CustomerPasswordRecoverMutation,
|
|
10
|
+
} from "@/generated/graphql";
|
|
8
11
|
import { Button } from "@/components/ui/button";
|
|
9
12
|
import { Input } from "@/components/ui/input";
|
|
10
13
|
import { ArrowLeft } from "lucide-react";
|
|
11
14
|
|
|
12
15
|
export default function ForgotPasswordPage() {
|
|
16
|
+
const execute = useExecute();
|
|
13
17
|
const [email, setEmail] = useState("");
|
|
14
18
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
15
19
|
const [error, setError] = useState("");
|
|
16
20
|
|
|
17
|
-
const client = getGraphQLClient();
|
|
18
|
-
|
|
19
21
|
const recoverMutation = useMutation({
|
|
20
22
|
mutationFn: async (email: string) => {
|
|
21
|
-
return
|
|
23
|
+
return execute<CustomerPasswordRecoverMutation>(
|
|
24
|
+
CustomerPasswordRecoverDocument.toString(),
|
|
25
|
+
{ email },
|
|
26
|
+
);
|
|
22
27
|
},
|
|
23
28
|
onSuccess: () => {
|
|
24
29
|
setIsSubmitted(true);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
2
|
+
|
|
3
|
+
export default function BlogPostLoading() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="container mx-auto max-w-3xl px-4 py-8">
|
|
6
|
+
<Skeleton className="mb-4 h-5 w-64" />
|
|
7
|
+
<Skeleton className="mb-2 h-10 w-full" />
|
|
8
|
+
<Skeleton className="mb-8 h-5 w-48" />
|
|
9
|
+
<Skeleton className="mb-8 aspect-video w-full rounded-lg" />
|
|
10
|
+
<div className="space-y-4">
|
|
11
|
+
{[...Array(8)].map((_, i) => (
|
|
12
|
+
<Skeleton key={i} className="h-4 w-full" />
|
|
13
|
+
))}
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -16,9 +16,50 @@ import { Card, CardContent } from '@/components/ui/card';
|
|
|
16
16
|
import { Separator } from '@/components/ui/separator';
|
|
17
17
|
import { BlogCard } from '@/components/blog/blog-card';
|
|
18
18
|
import { BlogSidebar } from '@/components/blog/blog-sidebar';
|
|
19
|
+
import { sanitizeHtml } from '@doswiftly/storefront-sdk';
|
|
20
|
+
|
|
21
|
+
interface BlogImage {
|
|
22
|
+
url: string;
|
|
23
|
+
altText?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface BlogPost {
|
|
27
|
+
id: string;
|
|
28
|
+
title: string;
|
|
29
|
+
slug: string;
|
|
30
|
+
excerpt: string;
|
|
31
|
+
content: string;
|
|
32
|
+
contentType: string;
|
|
33
|
+
featuredImage: BlogImage | null;
|
|
34
|
+
author: {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
bio: string;
|
|
38
|
+
avatar: BlogImage | null;
|
|
39
|
+
};
|
|
40
|
+
category: {
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
slug: string;
|
|
44
|
+
} | null;
|
|
45
|
+
tags: { id: string; name: string; slug: string; postCount: number }[];
|
|
46
|
+
status: string;
|
|
47
|
+
publishedAt: string;
|
|
48
|
+
readingTime: number;
|
|
49
|
+
viewCount: number;
|
|
50
|
+
commentCount: number;
|
|
51
|
+
allowComments: boolean;
|
|
52
|
+
isFeatured: boolean;
|
|
53
|
+
seo: {
|
|
54
|
+
title: string;
|
|
55
|
+
description: string;
|
|
56
|
+
} | null;
|
|
57
|
+
createdAt: string;
|
|
58
|
+
updatedAt: string;
|
|
59
|
+
}
|
|
19
60
|
|
|
20
61
|
// Mock data - replace with actual GraphQL fetch
|
|
21
|
-
const mockPost = {
|
|
62
|
+
const mockPost: BlogPost = {
|
|
22
63
|
id: '1',
|
|
23
64
|
title: 'Jak wybrać idealny produkt dla siebie',
|
|
24
65
|
slug: 'jak-wybrac-idealny-produkt',
|
|
@@ -234,7 +275,7 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
|
|
|
234
275
|
{/* Content */}
|
|
235
276
|
<div
|
|
236
277
|
className="prose prose-lg max-w-none dark:prose-invert mb-8"
|
|
237
|
-
dangerouslySetInnerHTML={{ __html: post.content }}
|
|
278
|
+
dangerouslySetInnerHTML={{ __html: sanitizeHtml(post.content) }}
|
|
238
279
|
/>
|
|
239
280
|
|
|
240
281
|
{/* Tags */}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
2
|
+
|
|
3
|
+
export default function BlogLoading() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="container mx-auto px-4 py-8">
|
|
6
|
+
<Skeleton className="mb-8 h-10 w-32" />
|
|
7
|
+
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
8
|
+
{[...Array(6)].map((_, i) => (
|
|
9
|
+
<div key={i} className="space-y-3">
|
|
10
|
+
<Skeleton className="aspect-video w-full rounded-lg" />
|
|
11
|
+
<Skeleton className="h-4 w-24" />
|
|
12
|
+
<Skeleton className="h-6 w-full" />
|
|
13
|
+
<Skeleton className="h-4 w-5/6" />
|
|
14
|
+
</div>
|
|
15
|
+
))}
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
|
|
4
|
-
import { BrandGrid
|
|
4
|
+
import { BrandGrid } from "@/components/brand/brand-grid";
|
|
5
|
+
import type { BrandCardProps } from "@/components/brand/brand-card";
|
|
5
6
|
|
|
6
7
|
export default function BrandsPage() {
|
|
7
8
|
// TODO: Fetch brands from backend using GraphQL
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
2
|
+
|
|
3
|
+
export default function CartLoading() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="container mx-auto px-4 py-8">
|
|
6
|
+
<Skeleton className="mb-8 h-10 w-48" />
|
|
7
|
+
<div className="grid gap-8 lg:grid-cols-3">
|
|
8
|
+
<div className="space-y-4 lg:col-span-2">
|
|
9
|
+
{[...Array(3)].map((_, i) => (
|
|
10
|
+
<div key={i} className="flex gap-4 rounded-lg border p-4">
|
|
11
|
+
<Skeleton className="h-24 w-24 shrink-0 rounded-md" />
|
|
12
|
+
<div className="flex-1 space-y-2">
|
|
13
|
+
<Skeleton className="h-5 w-3/4" />
|
|
14
|
+
<Skeleton className="h-4 w-1/3" />
|
|
15
|
+
<Skeleton className="h-8 w-24" />
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
))}
|
|
19
|
+
</div>
|
|
20
|
+
<div>
|
|
21
|
+
<Skeleton className="h-64 w-full rounded-lg" />
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
|
-
import {
|
|
4
|
+
import { useCartStoreApi } from "@/stores/cart-store";
|
|
5
5
|
import { useCartSync } from "@/hooks/use-cart-sync";
|
|
6
6
|
import { useCartActions } from "@/hooks/use-cart-actions";
|
|
7
7
|
import { useCartDiscountCodesUpdate } from "@/lib/graphql/hooks";
|
|
@@ -26,6 +26,9 @@ export default function CartPage() {
|
|
|
26
26
|
isLoading,
|
|
27
27
|
} = useCartSync();
|
|
28
28
|
|
|
29
|
+
// Cart store API for getState() in callbacks
|
|
30
|
+
const cartStoreApi = useCartStoreApi();
|
|
31
|
+
|
|
29
32
|
// Actions that mutate via GraphQL
|
|
30
33
|
const { updateQuantity, removeFromCart } = useCartActions();
|
|
31
34
|
|
|
@@ -49,7 +52,7 @@ export default function CartPage() {
|
|
|
49
52
|
|
|
50
53
|
const handleApplyPromo = async (code: string) => {
|
|
51
54
|
try {
|
|
52
|
-
const cartId =
|
|
55
|
+
const cartId = cartStoreApi.getState().cartId;
|
|
53
56
|
if (!cartId) {
|
|
54
57
|
return { success: false, message: "Your cart is empty" };
|
|
55
58
|
}
|
|
@@ -86,7 +89,7 @@ export default function CartPage() {
|
|
|
86
89
|
|
|
87
90
|
const handleRemovePromo = async () => {
|
|
88
91
|
try {
|
|
89
|
-
const cartId =
|
|
92
|
+
const cartId = cartStoreApi.getState().cartId;
|
|
90
93
|
if (!cartId) return;
|
|
91
94
|
|
|
92
95
|
await discountMutation.mutateAsync({
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ProductGrid } from "@/components/product/product-grid";
|
|
4
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
5
|
+
import { useProducts } from "@/lib/graphql/hooks";
|
|
6
|
+
|
|
7
|
+
interface CategoryProductsClientProps {
|
|
8
|
+
categoryId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Client component for category product listing.
|
|
13
|
+
*
|
|
14
|
+
* Receives categoryId from server and uses filters.categoryId
|
|
15
|
+
* for correct backend filtering.
|
|
16
|
+
*/
|
|
17
|
+
export function CategoryProductsClient({
|
|
18
|
+
categoryId,
|
|
19
|
+
}: CategoryProductsClientProps) {
|
|
20
|
+
const { data, isLoading, error } = useProducts({
|
|
21
|
+
first: 20,
|
|
22
|
+
filters: { categoryId },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const products = data?.products ?? [];
|
|
26
|
+
|
|
27
|
+
if (isLoading) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
|
30
|
+
{Array.from({ length: 8 }).map((_, i) => (
|
|
31
|
+
<Skeleton key={i} className="aspect-square w-full rounded-lg" />
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (error) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="rounded-lg border border-border bg-muted/50 p-12 text-center">
|
|
40
|
+
<p className="text-muted-foreground">
|
|
41
|
+
Failed to load products. Please try again.
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<ProductGrid
|
|
49
|
+
products={products}
|
|
50
|
+
columns={4}
|
|
51
|
+
priorityCount={8}
|
|
52
|
+
showBadges
|
|
53
|
+
emptyMessage="No products in this category"
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
2
|
+
|
|
3
|
+
export default function CategoryLoading() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="container mx-auto px-4 py-8">
|
|
6
|
+
{/* Breadcrumbs */}
|
|
7
|
+
<Skeleton className="mb-6 h-5 w-64" />
|
|
8
|
+
|
|
9
|
+
{/* Category title */}
|
|
10
|
+
<Skeleton className="mb-2 h-9 w-48" />
|
|
11
|
+
<Skeleton className="mb-6 h-5 w-96" />
|
|
12
|
+
|
|
13
|
+
{/* Subcategory chips */}
|
|
14
|
+
<div className="mb-8 flex gap-2">
|
|
15
|
+
{[...Array(4)].map((_, i) => (
|
|
16
|
+
<Skeleton key={i} className="h-8 w-24 rounded-full" />
|
|
17
|
+
))}
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
{/* Product grid 2x4 */}
|
|
21
|
+
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
|
|
22
|
+
{[...Array(8)].map((_, i) => (
|
|
23
|
+
<div key={i} className="space-y-3">
|
|
24
|
+
<Skeleton className="aspect-square w-full rounded-lg" />
|
|
25
|
+
<Skeleton className="h-4 w-3/4" />
|
|
26
|
+
<Skeleton className="h-4 w-1/2" />
|
|
27
|
+
</div>
|
|
28
|
+
))}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -1,78 +1,95 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { useParams } from "next/navigation";
|
|
4
|
-
import { ProductGrid } from "@/components/product/product-grid";
|
|
1
|
+
import { Metadata } from "next";
|
|
2
|
+
import { notFound } from "next/navigation";
|
|
5
3
|
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { fetchCategory } from "@/lib/graphql/server";
|
|
5
|
+
import { CategoryProductsClient } from "./category-products-client";
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Generate metadata for category pages (SEO)
|
|
9
|
+
*/
|
|
10
|
+
export async function generateMetadata({
|
|
11
|
+
params,
|
|
12
|
+
}: {
|
|
13
|
+
params: Promise<{ slug: string }>;
|
|
14
|
+
}): Promise<Metadata> {
|
|
15
|
+
try {
|
|
16
|
+
const { slug } = await params;
|
|
17
|
+
const data = await fetchCategory(slug);
|
|
18
|
+
const category = data?.category;
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
query: `category:${slug}`,
|
|
17
|
-
});
|
|
20
|
+
if (!category) {
|
|
21
|
+
return { title: "Category Not Found" };
|
|
22
|
+
}
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
return {
|
|
25
|
+
title: category.name,
|
|
26
|
+
description:
|
|
27
|
+
category.description || `Browse ${category.name} products`,
|
|
28
|
+
};
|
|
29
|
+
} catch {
|
|
30
|
+
return { title: "Category" };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
20
33
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Category detail page — Server Component
|
|
36
|
+
*
|
|
37
|
+
* Fetches category metadata on the server for SEO,
|
|
38
|
+
* passes category ID to client component for product filtering.
|
|
39
|
+
*/
|
|
40
|
+
export default async function CategoryPage({
|
|
41
|
+
params,
|
|
42
|
+
}: {
|
|
43
|
+
params: Promise<{ slug: string }>;
|
|
44
|
+
}) {
|
|
45
|
+
const { slug } = await params;
|
|
46
|
+
|
|
47
|
+
let category;
|
|
48
|
+
try {
|
|
49
|
+
const data = await fetchCategory(slug);
|
|
50
|
+
category = data?.category;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('[CategoryPage] Failed to fetch category:', error instanceof Error ? error.message : error);
|
|
36
53
|
}
|
|
37
54
|
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
<div className="container mx-auto px-4 py-8">
|
|
41
|
-
<Breadcrumbs className="mb-6" />
|
|
42
|
-
<div className="rounded-lg border border-border bg-muted/50 p-12 text-center">
|
|
43
|
-
<h1 className="text-2xl font-bold text-foreground mb-2">
|
|
44
|
-
Category Not Found
|
|
45
|
-
</h1>
|
|
46
|
-
<p className="text-muted-foreground">
|
|
47
|
-
The category you're looking for doesn't exist.
|
|
48
|
-
</p>
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
);
|
|
55
|
+
if (!category) {
|
|
56
|
+
notFound();
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
return (
|
|
55
60
|
<div className="container mx-auto px-4 py-8">
|
|
56
61
|
<Breadcrumbs className="mb-6" />
|
|
57
62
|
|
|
58
|
-
{/* Category Header */}
|
|
63
|
+
{/* Category Header — SSR, SEO-friendly */}
|
|
59
64
|
<div className="mb-8">
|
|
60
|
-
<h1 className="text-3xl font-bold text-foreground
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
<h1 className="text-3xl font-bold text-foreground">{category.name}</h1>
|
|
66
|
+
{category.description && (
|
|
67
|
+
<p className="mt-2 text-muted-foreground">{category.description}</p>
|
|
68
|
+
)}
|
|
69
|
+
{category.children.length > 0 && (
|
|
70
|
+
<div className="mt-4 flex flex-wrap gap-2">
|
|
71
|
+
{category.children.map((child) => (
|
|
72
|
+
<a
|
|
73
|
+
key={child.id}
|
|
74
|
+
href={`/categories/${child.slug}`}
|
|
75
|
+
className="rounded-full border border-border bg-muted px-3 py-1 text-sm text-foreground hover:bg-accent transition-colors"
|
|
76
|
+
>
|
|
77
|
+
{child.name}
|
|
78
|
+
{child.productCount > 0 && (
|
|
79
|
+
<span className="ml-1 text-muted-foreground">
|
|
80
|
+
({child.productCount})
|
|
81
|
+
</span>
|
|
82
|
+
)}
|
|
83
|
+
</a>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
66
87
|
</div>
|
|
67
88
|
|
|
68
|
-
{/*
|
|
69
|
-
<
|
|
70
|
-
products={products}
|
|
71
|
-
columns={4}
|
|
72
|
-
priorityCount={8}
|
|
73
|
-
showBadges
|
|
74
|
-
emptyMessage="No products in this category"
|
|
75
|
-
/>
|
|
89
|
+
{/* Client component for product listing with filters */}
|
|
90
|
+
<CategoryProductsClient categoryId={category.id} />
|
|
76
91
|
</div>
|
|
77
92
|
);
|
|
78
93
|
}
|
|
94
|
+
|
|
95
|
+
export const revalidate = 60;
|
|
@@ -2,6 +2,7 @@ import { Metadata } from "next";
|
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import { Card } from "@/components/ui/card";
|
|
4
4
|
import { fetchCategories } from "@/lib/graphql/server";
|
|
5
|
+
import type { CategoryNodeFields } from "@/lib/graphql/fragments";
|
|
5
6
|
|
|
6
7
|
export const metadata: Metadata = {
|
|
7
8
|
title: "Categories",
|
|
@@ -12,11 +13,14 @@ export const metadata: Metadata = {
|
|
|
12
13
|
export const revalidate = 60;
|
|
13
14
|
|
|
14
15
|
export default async function CategoriesPage() {
|
|
15
|
-
|
|
16
|
-
// Response structure: { categories: { roots: [...], totalCount } }
|
|
17
|
-
const data = await fetchCategories();
|
|
16
|
+
let categoryList: CategoryNodeFields[] = [];
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
try {
|
|
19
|
+
const data = await fetchCategories();
|
|
20
|
+
categoryList = data?.categories?.roots || [];
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('[CategoriesPage] Failed to fetch categories:', error instanceof Error ? error.message : error);
|
|
23
|
+
}
|
|
20
24
|
|
|
21
25
|
return (
|
|
22
26
|
<div className="container mx-auto px-4 py-8">
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { AlertCircle } from "lucide-react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
8
|
+
|
|
9
|
+
export default function CheckoutError({
|
|
10
|
+
error,
|
|
11
|
+
reset,
|
|
12
|
+
}: {
|
|
13
|
+
error: Error & { digest?: string };
|
|
14
|
+
reset: () => void;
|
|
15
|
+
}) {
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
console.error("[Checkout Error]", error);
|
|
18
|
+
}, [error]);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="container mx-auto flex min-h-[400px] items-center justify-center px-4 py-16">
|
|
22
|
+
<Card className="max-w-md">
|
|
23
|
+
<CardHeader>
|
|
24
|
+
<CardTitle className="flex items-center gap-2 text-destructive">
|
|
25
|
+
<AlertCircle className="h-5 w-5" />
|
|
26
|
+
Checkout error
|
|
27
|
+
</CardTitle>
|
|
28
|
+
</CardHeader>
|
|
29
|
+
<CardContent className="space-y-4">
|
|
30
|
+
<p className="text-sm text-muted-foreground">
|
|
31
|
+
Something went wrong during checkout. Your cart items are safe.
|
|
32
|
+
</p>
|
|
33
|
+
<div className="flex gap-3">
|
|
34
|
+
<Button onClick={reset}>Try again</Button>
|
|
35
|
+
<Button variant="outline" asChild>
|
|
36
|
+
<Link href="/cart">Back to cart</Link>
|
|
37
|
+
</Button>
|
|
38
|
+
</div>
|
|
39
|
+
</CardContent>
|
|
40
|
+
</Card>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|