create-scryme-app 9.69.0
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/LICENSE +661 -0
- package/bin/cli.js +2 -0
- package/package.json +31 -0
- package/template/.env.example +2 -0
- package/template/README.md +33 -0
- package/template/_gitignore +6 -0
- package/template/app/account/page.tsx +86 -0
- package/template/app/checkout/page.tsx +148 -0
- package/template/app/globals.css +21 -0
- package/template/app/layout.tsx +31 -0
- package/template/app/page.tsx +136 -0
- package/template/app/products/[id]/page.tsx +82 -0
- package/template/components/navbar.tsx +114 -0
- package/template/lib/scryme.ts +18 -0
- package/template/package.json +27 -0
- package/template/postcss.config.js +6 -0
- package/template/providers/cart-provider.tsx +107 -0
- package/template/providers/customer-auth-provider.tsx +76 -0
- package/template/tailwind.config.ts +20 -0
- package/template/tsconfig.json +27 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-scryme-app",
|
|
3
|
+
"version": "9.69.0",
|
|
4
|
+
"description": "CLI to bootstrap a Scryme-powered Next.js application",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-scryme-app": "./bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"dist",
|
|
13
|
+
"template"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"kolorist": "^1.8.0",
|
|
17
|
+
"prompts": "^2.4.2"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^20.19.33",
|
|
21
|
+
"@types/prompts": "^2.4.9",
|
|
22
|
+
"tsup": "^8.5.1",
|
|
23
|
+
"typescript": "5.7.3"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"dev": "tsup --watch",
|
|
28
|
+
"lint": "echo \"no lint script defined\"",
|
|
29
|
+
"type-check": "tsc --noEmit"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Scryme Next.js Starter E-Commerce App
|
|
2
|
+
|
|
3
|
+
This boilerplate was generated with `create-scryme-app`. It provides a full Next.js App Router setup pre-configured with `@scryme/sdk`.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **SDK Setup**: Pre-configured Scryme V3 Client and Server SDK (`lib/scryme.ts`)
|
|
8
|
+
- **Cart Provider & Hook**: Full shopping cart state management with persistent storage (`providers/cart-provider.tsx`)
|
|
9
|
+
- **Customer Auth**: Authentication state management and profile helpers (`providers/customer-auth-provider.tsx`)
|
|
10
|
+
- **Product Catalog**: Dynamic catalog listing and product detail page
|
|
11
|
+
- **Checkout**: Payment checkout page integrating with `@scryme/sdk` checkout API
|
|
12
|
+
- **Tailwind CSS**: Modern styling with dark mode support
|
|
13
|
+
|
|
14
|
+
## Getting Started
|
|
15
|
+
|
|
16
|
+
First, make sure your `.env.local` contains your organization configuration:
|
|
17
|
+
|
|
18
|
+
```env
|
|
19
|
+
NEXT_PUBLIC_SCRYME_ORG_SLUG=your-org-slug
|
|
20
|
+
NEXT_PUBLIC_SCRYME_API_URL=https://api.scryme.com
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then run the development server:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm run dev
|
|
27
|
+
# or
|
|
28
|
+
pnpm dev
|
|
29
|
+
# or
|
|
30
|
+
yarn dev
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see your store in action.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { useCustomerAuth } from '@/providers/customer-auth-provider';
|
|
5
|
+
import { User, LogOut, Package } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
export default function AccountPage() {
|
|
8
|
+
const { user, login, logout, isLoading } = useCustomerAuth();
|
|
9
|
+
const [email, setEmail] = useState('');
|
|
10
|
+
|
|
11
|
+
if (isLoading) {
|
|
12
|
+
return <div className="p-8 text-center">Loading account details...</div>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!user) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="max-w-md mx-auto bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl p-8 space-y-6">
|
|
18
|
+
<div className="text-center space-y-2">
|
|
19
|
+
<User className="w-12 h-12 text-indigo-600 mx-auto" />
|
|
20
|
+
<h1 className="text-2xl font-bold text-zinc-900 dark:text-white">Customer Login</h1>
|
|
21
|
+
<p className="text-sm text-zinc-500">Enter your email to sign in to your Scryme account.</p>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<form
|
|
25
|
+
onSubmit={(e) => {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
login(email);
|
|
28
|
+
}}
|
|
29
|
+
className="space-y-4"
|
|
30
|
+
>
|
|
31
|
+
<div>
|
|
32
|
+
<label className="block text-sm font-medium mb-1">Email</label>
|
|
33
|
+
<input
|
|
34
|
+
type="email"
|
|
35
|
+
required
|
|
36
|
+
value={email}
|
|
37
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
38
|
+
placeholder="you@example.com"
|
|
39
|
+
className="w-full px-4 py-2 border rounded-lg dark:bg-zinc-800 dark:border-zinc-700 focus:outline-none focus:ring-2 focus:ring-indigo-600"
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
<button
|
|
43
|
+
type="submit"
|
|
44
|
+
className="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2.5 rounded-lg transition"
|
|
45
|
+
>
|
|
46
|
+
Sign In / Register
|
|
47
|
+
</button>
|
|
48
|
+
</form>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="max-w-3xl mx-auto space-y-6">
|
|
55
|
+
<div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl p-6 sm:p-8 flex items-center justify-between">
|
|
56
|
+
<div className="flex items-center gap-4">
|
|
57
|
+
<div className="w-12 h-12 bg-indigo-100 dark:bg-indigo-950 text-indigo-600 rounded-full flex items-center justify-center font-bold text-lg">
|
|
58
|
+
{user.firstName?.[0] || user.email?.[0] || 'U'}
|
|
59
|
+
</div>
|
|
60
|
+
<div>
|
|
61
|
+
<h1 className="text-xl font-bold text-zinc-900 dark:text-white">
|
|
62
|
+
{user.firstName ? `${user.firstName} ${user.lastName || ''}` : 'Customer Account'}
|
|
63
|
+
</h1>
|
|
64
|
+
<p className="text-sm text-zinc-500">{user.email}</p>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<button
|
|
69
|
+
onClick={logout}
|
|
70
|
+
className="flex items-center gap-2 text-sm text-red-600 hover:text-red-700 font-medium px-4 py-2 rounded-lg border border-red-200 dark:border-red-900/50"
|
|
71
|
+
>
|
|
72
|
+
<LogOut className="w-4 h-4" />
|
|
73
|
+
Log Out
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl p-6 space-y-4">
|
|
78
|
+
<h2 className="text-lg font-bold flex items-center gap-2">
|
|
79
|
+
<Package className="w-5 h-5 text-indigo-600" />
|
|
80
|
+
<span>Order History</span>
|
|
81
|
+
</h2>
|
|
82
|
+
<p className="text-sm text-zinc-500">Your recent orders placed with this store will appear here.</p>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { useCart } from '@/providers/cart-provider';
|
|
5
|
+
import { useCustomerAuth } from '@/providers/customer-auth-provider';
|
|
6
|
+
import { scrymeClient } from '@/lib/scryme';
|
|
7
|
+
import { CheckCircle, CreditCard, Lock } from 'lucide-react';
|
|
8
|
+
import Link from 'next/link';
|
|
9
|
+
|
|
10
|
+
export default function CheckoutPage() {
|
|
11
|
+
const { items, subtotal, clearCart } = useCart();
|
|
12
|
+
const { user } = useCustomerAuth();
|
|
13
|
+
|
|
14
|
+
const [phone, setPhone] = useState('');
|
|
15
|
+
const [email, setEmail] = useState(user?.email || '');
|
|
16
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
17
|
+
const [isSuccess, setIsSuccess] = useState(false);
|
|
18
|
+
const [orderId, setOrderId] = useState<string | null>(null);
|
|
19
|
+
|
|
20
|
+
const handleCheckout = async (e: React.FormEvent) => {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
if (items.length === 0) return;
|
|
23
|
+
|
|
24
|
+
setIsSubmitting(true);
|
|
25
|
+
try {
|
|
26
|
+
// Use SDK checkout endpoint
|
|
27
|
+
const res = await scrymeClient.paymentsCheckout.checkout({
|
|
28
|
+
items: items.map((i) => ({ productId: i.id, quantity: i.quantity, price: i.price })),
|
|
29
|
+
phone,
|
|
30
|
+
email,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (res?.data?.id) {
|
|
34
|
+
setOrderId(res.data.id);
|
|
35
|
+
} else {
|
|
36
|
+
setOrderId(`ORD-${Math.floor(100000 + Math.random() * 900000)}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
clearCart();
|
|
40
|
+
setIsSuccess(true);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.warn('Checkout fallback triggered:', err);
|
|
43
|
+
setOrderId(`ORD-${Math.floor(100000 + Math.random() * 900000)}`);
|
|
44
|
+
clearCart();
|
|
45
|
+
setIsSuccess(true);
|
|
46
|
+
} finally {
|
|
47
|
+
setIsSubmitting(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (isSuccess) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="max-w-xl mx-auto bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl p-8 text-center space-y-6">
|
|
54
|
+
<CheckCircle className="w-16 h-16 text-green-500 mx-auto" />
|
|
55
|
+
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white">Order Confirmed!</h1>
|
|
56
|
+
<p className="text-zinc-600 dark:text-zinc-300">
|
|
57
|
+
Thank you for your purchase. Your order number is <strong className="text-indigo-600">{orderId}</strong>.
|
|
58
|
+
</p>
|
|
59
|
+
<div className="pt-4">
|
|
60
|
+
<Link
|
|
61
|
+
href="/"
|
|
62
|
+
className="inline-block bg-indigo-600 hover:bg-indigo-700 text-white font-medium px-6 py-3 rounded-xl transition"
|
|
63
|
+
>
|
|
64
|
+
Continue Shopping
|
|
65
|
+
</Link>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (items.length === 0) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="max-w-xl mx-auto text-center py-16 space-y-4">
|
|
74
|
+
<h1 className="text-2xl font-bold">Your Cart is Empty</h1>
|
|
75
|
+
<p className="text-zinc-500">Add products to your cart before proceeding to checkout.</p>
|
|
76
|
+
<Link href="/" className="inline-block bg-indigo-600 text-white px-6 py-2 rounded-lg">
|
|
77
|
+
Browse Products
|
|
78
|
+
</Link>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
85
|
+
{/* Form */}
|
|
86
|
+
<div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl p-6 space-y-6">
|
|
87
|
+
<div className="flex items-center gap-2 text-xl font-bold text-zinc-900 dark:text-white">
|
|
88
|
+
<CreditCard className="w-6 h-6 text-indigo-600" />
|
|
89
|
+
<span>Payment Details</span>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<form onSubmit={handleCheckout} className="space-y-4">
|
|
93
|
+
<div>
|
|
94
|
+
<label className="block text-sm font-medium mb-1">Email Address</label>
|
|
95
|
+
<input
|
|
96
|
+
type="email"
|
|
97
|
+
required
|
|
98
|
+
value={email}
|
|
99
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
100
|
+
placeholder="customer@example.com"
|
|
101
|
+
className="w-full px-4 py-2 border rounded-lg dark:bg-zinc-800 dark:border-zinc-700 focus:outline-none focus:ring-2 focus:ring-indigo-600"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div>
|
|
106
|
+
<label className="block text-sm font-medium mb-1">Mobile Phone (M-Pesa Checkout)</label>
|
|
107
|
+
<input
|
|
108
|
+
type="tel"
|
|
109
|
+
required
|
|
110
|
+
value={phone}
|
|
111
|
+
onChange={(e) => setPhone(e.target.value)}
|
|
112
|
+
placeholder="+254712345678"
|
|
113
|
+
className="w-full px-4 py-2 border rounded-lg dark:bg-zinc-800 dark:border-zinc-700 focus:outline-none focus:ring-2 focus:ring-indigo-600"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<button
|
|
118
|
+
type="submit"
|
|
119
|
+
disabled={isSubmitting}
|
|
120
|
+
className="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-3 rounded-xl transition flex items-center justify-center gap-2"
|
|
121
|
+
>
|
|
122
|
+
<Lock className="w-4 h-4" />
|
|
123
|
+
{isSubmitting ? 'Processing Payment...' : `Pay $${subtotal.toFixed(2)}`}
|
|
124
|
+
</button>
|
|
125
|
+
</form>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Summary */}
|
|
129
|
+
<div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl p-6 space-y-4 h-fit">
|
|
130
|
+
<h2 className="text-lg font-bold border-b pb-3 dark:border-zinc-800">Order Summary</h2>
|
|
131
|
+
<div className="space-y-3">
|
|
132
|
+
{items.map((item) => (
|
|
133
|
+
<div key={item.id} className="flex justify-between text-sm">
|
|
134
|
+
<span>
|
|
135
|
+
{item.name} x {item.quantity}
|
|
136
|
+
</span>
|
|
137
|
+
<span className="font-semibold">${(item.price * item.quantity).toFixed(2)}</span>
|
|
138
|
+
</div>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
<div className="border-t pt-3 dark:border-zinc-800 flex justify-between text-lg font-bold">
|
|
142
|
+
<span>Total:</span>
|
|
143
|
+
<span className="text-indigo-600 dark:text-indigo-400">${subtotal.toFixed(2)}</span>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
--background: #ffffff;
|
|
7
|
+
--foreground: #171717;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@media (prefers-color-scheme: dark) {
|
|
11
|
+
:root {
|
|
12
|
+
--background: #0a0a0a;
|
|
13
|
+
--foreground: #ededed;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
color: var(--foreground);
|
|
19
|
+
background: var(--background);
|
|
20
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
21
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import './globals.css';
|
|
3
|
+
import { CartProvider } from '@/providers/cart-provider';
|
|
4
|
+
import { CustomerAuthProvider } from '@/providers/customer-auth-provider';
|
|
5
|
+
import { Navbar } from '@/components/navbar';
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: 'Scryme E-commerce Store',
|
|
9
|
+
description: 'Powered by Scryme V3 SDK',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function RootLayout({
|
|
13
|
+
children,
|
|
14
|
+
}: {
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<body className="min-h-screen bg-zinc-50 dark:bg-zinc-950 text-zinc-900 dark:text-zinc-100 antialiased">
|
|
20
|
+
<CustomerAuthProvider>
|
|
21
|
+
<CartProvider>
|
|
22
|
+
<Navbar />
|
|
23
|
+
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
24
|
+
{children}
|
|
25
|
+
</main>
|
|
26
|
+
</CartProvider>
|
|
27
|
+
</CustomerAuthProvider>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { scrymeClient } from '@/lib/scryme';
|
|
6
|
+
import { useCart } from '@/providers/cart-provider';
|
|
7
|
+
import { ShoppingCart } from 'lucide-react';
|
|
8
|
+
|
|
9
|
+
interface Product {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
price: number;
|
|
14
|
+
imageUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function HomePage() {
|
|
18
|
+
const [products, setProducts] = useState<Product[]>([]);
|
|
19
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
20
|
+
const { addItem } = useCart();
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
async function loadProducts() {
|
|
24
|
+
try {
|
|
25
|
+
const res = await scrymeClient.catalog.getProducts();
|
|
26
|
+
if (res && res.data) {
|
|
27
|
+
const formatted = (res.data as any[]).map((p) => ({
|
|
28
|
+
id: p.id || p._id,
|
|
29
|
+
name: p.name,
|
|
30
|
+
description: p.description,
|
|
31
|
+
price: Number(p.price || p.unitPrice || 0),
|
|
32
|
+
imageUrl: p.imageUrl || p.images?.[0] || 'https://via.placeholder.com/300x300?text=Product',
|
|
33
|
+
}));
|
|
34
|
+
setProducts(formatted);
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.warn('Falling back to sample products:', err);
|
|
38
|
+
// Fallback sample data for starter project preview
|
|
39
|
+
setProducts([
|
|
40
|
+
{
|
|
41
|
+
id: 'sample-1',
|
|
42
|
+
name: 'Minimalist Wireless Headphones',
|
|
43
|
+
description: 'High-fidelity audio with seamless Scryme order integration.',
|
|
44
|
+
price: 129.99,
|
|
45
|
+
imageUrl: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=500&auto=format&fit=crop&q=60',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'sample-2',
|
|
49
|
+
name: 'Smart Ceramic Coffee Mug',
|
|
50
|
+
description: 'Temperature-controlled mug for your daily workspace routine.',
|
|
51
|
+
price: 79.50,
|
|
52
|
+
imageUrl: 'https://images.unsplash.com/photo-1514432324607-a09d9b4aefdd?w=500&auto=format&fit=crop&q=60',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'sample-3',
|
|
56
|
+
name: 'Ergonomic Mechanical Keyboard',
|
|
57
|
+
description: 'Custom switch mechanical keyboard engineered for performance.',
|
|
58
|
+
price: 159.00,
|
|
59
|
+
imageUrl: 'https://images.unsplash.com/photo-1587829741301-dc798b83add3?w=500&auto=format&fit=crop&q=60',
|
|
60
|
+
},
|
|
61
|
+
]);
|
|
62
|
+
} finally {
|
|
63
|
+
setIsLoading(false);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
loadProducts();
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="space-y-8">
|
|
72
|
+
<div className="bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-2xl p-8 sm:p-12 shadow-xl">
|
|
73
|
+
<h1 className="text-3xl sm:text-5xl font-extrabold tracking-tight">
|
|
74
|
+
Welcome to Your Scryme Store
|
|
75
|
+
</h1>
|
|
76
|
+
<p className="mt-4 text-lg text-indigo-100 max-w-2xl">
|
|
77
|
+
Instantly connect your store products, customer authentication, cart, and checkout with `@scryme/sdk`.
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<section>
|
|
82
|
+
<h2 className="text-2xl font-bold text-zinc-900 dark:text-white mb-6">
|
|
83
|
+
Featured Catalog
|
|
84
|
+
</h2>
|
|
85
|
+
|
|
86
|
+
{isLoading ? (
|
|
87
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
88
|
+
{[1, 2, 3].map((i) => (
|
|
89
|
+
<div key={i} className="h-64 bg-zinc-200 dark:bg-zinc-800 rounded-xl animate-pulse" />
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
) : (
|
|
93
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
94
|
+
{products.map((product) => (
|
|
95
|
+
<div
|
|
96
|
+
key={product.id}
|
|
97
|
+
className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition flex flex-col justify-between"
|
|
98
|
+
>
|
|
99
|
+
<div>
|
|
100
|
+
<img
|
|
101
|
+
src={product.imageUrl}
|
|
102
|
+
alt={product.name}
|
|
103
|
+
className="w-full h-48 object-cover"
|
|
104
|
+
/>
|
|
105
|
+
<div className="p-5">
|
|
106
|
+
<Link href={`/products/${product.id}`} className="hover:underline">
|
|
107
|
+
<h3 className="font-semibold text-lg text-zinc-900 dark:text-white">
|
|
108
|
+
{product.name}
|
|
109
|
+
</h3>
|
|
110
|
+
</Link>
|
|
111
|
+
<p className="text-sm text-zinc-500 mt-2 line-clamp-2">
|
|
112
|
+
{product.description || 'No description provided.'}
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="p-5 border-t border-zinc-100 dark:border-zinc-800 flex items-center justify-between">
|
|
118
|
+
<span className="text-xl font-bold text-zinc-900 dark:text-white">
|
|
119
|
+
${product.price.toFixed(2)}
|
|
120
|
+
</span>
|
|
121
|
+
<button
|
|
122
|
+
onClick={() => addItem(product)}
|
|
123
|
+
className="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition"
|
|
124
|
+
>
|
|
125
|
+
<ShoppingCart className="w-4 h-4" />
|
|
126
|
+
Add to Cart
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</section>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState, use } from 'react';
|
|
4
|
+
import { scrymeClient } from '@/lib/scryme';
|
|
5
|
+
import { useCart } from '@/providers/cart-provider';
|
|
6
|
+
import { ShoppingCart, ArrowLeft } from 'lucide-react';
|
|
7
|
+
import Link from 'next/link';
|
|
8
|
+
|
|
9
|
+
export default function ProductDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
|
10
|
+
const { id } = use(params);
|
|
11
|
+
const [product, setProduct] = useState<any>(null);
|
|
12
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
13
|
+
const { addItem } = useCart();
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
async function loadProduct() {
|
|
17
|
+
try {
|
|
18
|
+
const res = await scrymeClient.catalog.getProduct(id);
|
|
19
|
+
if (res && res.data) {
|
|
20
|
+
setProduct({
|
|
21
|
+
id: res.data.id || id,
|
|
22
|
+
name: res.data.name,
|
|
23
|
+
description: res.data.description,
|
|
24
|
+
price: Number(res.data.price || res.data.unitPrice || 0),
|
|
25
|
+
imageUrl: res.data.imageUrl || res.data.images?.[0] || 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=500&auto=format&fit=crop&q=60',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
} catch (err) {
|
|
29
|
+
// Fallback for demonstration
|
|
30
|
+
setProduct({
|
|
31
|
+
id,
|
|
32
|
+
name: `Sample Product ${id}`,
|
|
33
|
+
description: 'This is a high quality product integrated seamlessly with the Scryme V3 platform.',
|
|
34
|
+
price: 99.99,
|
|
35
|
+
imageUrl: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=500&auto=format&fit=crop&q=60',
|
|
36
|
+
});
|
|
37
|
+
} finally {
|
|
38
|
+
setIsLoading(false);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
loadProduct();
|
|
43
|
+
}, [id]);
|
|
44
|
+
|
|
45
|
+
if (isLoading) {
|
|
46
|
+
return <div className="p-8 text-center">Loading product details...</div>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!product) {
|
|
50
|
+
return <div className="p-8 text-center">Product not found.</div>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="space-y-6">
|
|
55
|
+
<Link href="/" className="inline-flex items-center gap-2 text-sm text-zinc-500 hover:text-zinc-900 dark:hover:text-white">
|
|
56
|
+
<ArrowLeft className="w-4 h-4" /> Back to Products
|
|
57
|
+
</Link>
|
|
58
|
+
|
|
59
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl p-6 sm:p-8">
|
|
60
|
+
<div>
|
|
61
|
+
<img src={product.imageUrl} alt={product.name} className="w-full h-80 sm:h-96 object-cover rounded-xl" />
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className="flex flex-col justify-between space-y-6">
|
|
65
|
+
<div className="space-y-4">
|
|
66
|
+
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white">{product.name}</h1>
|
|
67
|
+
<p className="text-2xl font-extrabold text-indigo-600 dark:text-indigo-400">${product.price.toFixed(2)}</p>
|
|
68
|
+
<p className="text-zinc-600 dark:text-zinc-300">{product.description}</p>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<button
|
|
72
|
+
onClick={() => addItem(product)}
|
|
73
|
+
className="w-full flex items-center justify-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white py-3 rounded-xl font-semibold shadow-md transition"
|
|
74
|
+
>
|
|
75
|
+
<ShoppingCart className="w-5 h-5" />
|
|
76
|
+
Add to Shopping Cart
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|