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.
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { ShoppingBag, User, ShoppingCart, X } from 'lucide-react';
6
+ import { useCart } from '@/providers/cart-provider';
7
+ import { useCustomerAuth } from '@/providers/customer-auth-provider';
8
+
9
+ export const Navbar: React.FC = () => {
10
+ const { totalItems, items, subtotal, removeItem, updateQuantity } = useCart();
11
+ const { user, logout } = useCustomerAuth();
12
+ const [isCartOpen, setIsCartOpen] = useState(false);
13
+
14
+ return (
15
+ <>
16
+ <header className="sticky top-0 z-40 bg-white dark:bg-zinc-900 border-b border-zinc-200 dark:border-zinc-800">
17
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
18
+ <Link href="/" className="flex items-center gap-2 text-xl font-bold text-zinc-900 dark:text-white">
19
+ <ShoppingBag className="w-6 h-6 text-indigo-600" />
20
+ <span>ScrymeStore</span>
21
+ </Link>
22
+
23
+ <nav className="flex items-center gap-6">
24
+ <Link href="/" className="text-sm font-medium hover:text-indigo-600 transition">
25
+ Products
26
+ </Link>
27
+ <Link href="/account" className="text-sm font-medium hover:text-indigo-600 transition flex items-center gap-1">
28
+ <User className="w-4 h-4" />
29
+ <span>{user ? user.firstName || 'Account' : 'Login'}</span>
30
+ </Link>
31
+ <button
32
+ onClick={() => setIsCartOpen(true)}
33
+ className="relative p-2 text-zinc-700 dark:text-zinc-200 hover:text-indigo-600 transition"
34
+ aria-label="Shopping Cart"
35
+ >
36
+ <ShoppingCart className="w-6 h-6" />
37
+ {totalItems > 0 && (
38
+ <span className="absolute top-0 right-0 bg-indigo-600 text-white text-xs font-bold rounded-full h-5 w-5 flex items-center justify-center">
39
+ {totalItems}
40
+ </span>
41
+ )}
42
+ </button>
43
+ </nav>
44
+ </div>
45
+ </header>
46
+
47
+ {/* Slide-over Cart */}
48
+ {isCartOpen && (
49
+ <div className="fixed inset-0 z-50 overflow-hidden bg-black/50 backdrop-blur-sm flex justify-end">
50
+ <div className="w-full max-w-md bg-white dark:bg-zinc-900 h-full shadow-2xl flex flex-col p-6">
51
+ <div className="flex items-center justify-between border-b pb-4 dark:border-zinc-800">
52
+ <h2 className="text-lg font-semibold text-zinc-900 dark:text-white">Your Cart ({totalItems})</h2>
53
+ <button onClick={() => setIsCartOpen(false)} className="text-zinc-500 hover:text-zinc-800 dark:hover:text-white">
54
+ <X className="w-6 h-6" />
55
+ </button>
56
+ </div>
57
+
58
+ <div className="flex-1 overflow-y-auto py-4 space-y-4">
59
+ {items.length === 0 ? (
60
+ <p className="text-center text-zinc-500 py-10">Your cart is empty.</p>
61
+ ) : (
62
+ items.map((item) => (
63
+ <div key={item.id} className="flex items-center justify-between border-b pb-3 dark:border-zinc-800">
64
+ <div>
65
+ <h4 className="font-medium text-zinc-900 dark:text-white">{item.name}</h4>
66
+ <p className="text-sm text-zinc-500">${item.price.toFixed(2)}</p>
67
+ <div className="flex items-center gap-2 mt-2">
68
+ <button
69
+ onClick={() => updateQuantity(item.id, item.quantity - 1)}
70
+ className="px-2 py-1 bg-zinc-100 dark:bg-zinc-800 rounded text-sm"
71
+ >
72
+ -
73
+ </button>
74
+ <span className="text-sm font-semibold">{item.quantity}</span>
75
+ <button
76
+ onClick={() => updateQuantity(item.id, item.quantity + 1)}
77
+ className="px-2 py-1 bg-zinc-100 dark:bg-zinc-800 rounded text-sm"
78
+ >
79
+ +
80
+ </button>
81
+ </div>
82
+ </div>
83
+ <button
84
+ onClick={() => removeItem(item.id)}
85
+ className="text-red-500 hover:text-red-700 text-sm"
86
+ >
87
+ Remove
88
+ </button>
89
+ </div>
90
+ ))
91
+ )}
92
+ </div>
93
+
94
+ {items.length > 0 && (
95
+ <div className="border-t pt-4 dark:border-zinc-800 space-y-4">
96
+ <div className="flex justify-between font-semibold text-lg">
97
+ <span>Subtotal:</span>
98
+ <span>${subtotal.toFixed(2)}</span>
99
+ </div>
100
+ <Link
101
+ href="/checkout"
102
+ onClick={() => setIsCartOpen(false)}
103
+ className="block w-full text-center bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-3 rounded-lg transition"
104
+ >
105
+ Proceed to Checkout
106
+ </Link>
107
+ </div>
108
+ )}
109
+ </div>
110
+ </div>
111
+ )}
112
+ </>
113
+ );
114
+ };
@@ -0,0 +1,18 @@
1
+ import { ScrymeClientSDK } from '@scryme/sdk/client';
2
+ import { ScrymeServerSDK } from '@scryme/sdk/server';
3
+
4
+ export const ORG_SLUG = process.env.NEXT_PUBLIC_SCRYME_ORG_SLUG || 'demo-store';
5
+ export const API_URL = process.env.NEXT_PUBLIC_SCRYME_API_URL || 'https://api.scryme.com';
6
+
7
+ export const scrymeClient = new ScrymeClientSDK({
8
+ baseUrl: API_URL,
9
+ orgSlug: ORG_SLUG,
10
+ });
11
+
12
+ export const getScrymeServerClient = (token?: string) => {
13
+ return new ScrymeServerSDK({
14
+ baseUrl: API_URL,
15
+ orgSlug: ORG_SLUG,
16
+ token,
17
+ });
18
+ };
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "scryme-ecommerce-starter",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@scryme/sdk": "^9.69.0",
13
+ "lucide-react": "^0.475.0",
14
+ "next": "^15.1.0",
15
+ "react": "^19.0.0",
16
+ "react-dom": "^19.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^20.0.0",
20
+ "@types/react": "^19.0.0",
21
+ "@types/react-dom": "^19.0.0",
22
+ "autoprefixer": "^10.4.20",
23
+ "postcss": "^8.4.49",
24
+ "tailwindcss": "^3.4.17",
25
+ "typescript": "^5.0.0"
26
+ }
27
+ }
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -0,0 +1,107 @@
1
+ 'use client';
2
+
3
+ import React, { createContext, useContext, useEffect, useState } from 'react';
4
+
5
+ export interface CartItem {
6
+ id: string;
7
+ name: string;
8
+ price: number;
9
+ quantity: number;
10
+ imageUrl?: string;
11
+ }
12
+
13
+ interface CartContextType {
14
+ items: CartItem[];
15
+ addItem: (item: Omit<CartItem, 'quantity'> & { quantity?: number }) => void;
16
+ removeItem: (id: string) => void;
17
+ updateQuantity: (id: string, quantity: number) => void;
18
+ clearCart: () => void;
19
+ totalItems: number;
20
+ subtotal: number;
21
+ }
22
+
23
+ const CartContext = createContext<CartContextType | undefined>(undefined);
24
+
25
+ const CART_STORAGE_KEY = 'scryme_cart_items';
26
+
27
+ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
28
+ const [items, setItems] = useState<CartItem[]>([]);
29
+ const [isInitialized, setIsInitialized] = useState(false);
30
+
31
+ useEffect(() => {
32
+ try {
33
+ const stored = localStorage.getItem(CART_STORAGE_KEY);
34
+ if (stored) {
35
+ setItems(JSON.parse(stored));
36
+ }
37
+ } catch (e) {
38
+ console.error('Failed to parse stored cart:', e);
39
+ } finally {
40
+ setIsInitialized(true);
41
+ }
42
+ }, []);
43
+
44
+ useEffect(() => {
45
+ if (isInitialized) {
46
+ localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(items));
47
+ }
48
+ }, [items, isInitialized]);
49
+
50
+ const addItem = (item: Omit<CartItem, 'quantity'> & { quantity?: number }) => {
51
+ const qty = item.quantity || 1;
52
+ setItems((prev) => {
53
+ const existing = prev.find((i) => i.id === item.id);
54
+ if (existing) {
55
+ return prev.map((i) =>
56
+ i.id === item.id ? { ...i, quantity: i.quantity + qty } : i
57
+ );
58
+ }
59
+ return [...prev, { ...item, quantity: qty }];
60
+ });
61
+ };
62
+
63
+ const removeItem = (id: string) => {
64
+ setItems((prev) => prev.filter((i) => i.id !== id));
65
+ };
66
+
67
+ const updateQuantity = (id: string, quantity: number) => {
68
+ if (quantity <= 0) {
69
+ removeItem(id);
70
+ return;
71
+ }
72
+ setItems((prev) =>
73
+ prev.map((i) => (i.id === id ? { ...i, quantity } : i))
74
+ );
75
+ };
76
+
77
+ const clearCart = () => {
78
+ setItems([]);
79
+ };
80
+
81
+ const totalItems = items.reduce((sum, i) => sum + i.quantity, 0);
82
+ const subtotal = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
83
+
84
+ return (
85
+ <CartContext.Provider
86
+ value={{
87
+ items,
88
+ addItem,
89
+ removeItem,
90
+ updateQuantity,
91
+ clearCart,
92
+ totalItems,
93
+ subtotal,
94
+ }}
95
+ >
96
+ {children}
97
+ </CartContext.Provider>
98
+ );
99
+ };
100
+
101
+ export const useCart = () => {
102
+ const context = useContext(CartContext);
103
+ if (!context) {
104
+ throw new Error('useCart must be used within a CartProvider');
105
+ }
106
+ return context;
107
+ };
@@ -0,0 +1,76 @@
1
+ 'use client';
2
+
3
+ import React, { createContext, useContext, useEffect, useState } from 'react';
4
+ import { scrymeClient } from '@/lib/scryme';
5
+
6
+ interface CustomerUser {
7
+ id?: string;
8
+ email?: string;
9
+ firstName?: string;
10
+ lastName?: string;
11
+ phone?: string;
12
+ }
13
+
14
+ interface CustomerAuthContextType {
15
+ user: CustomerUser | null;
16
+ isLoading: boolean;
17
+ login: (email: string) => Promise<void>;
18
+ logout: () => void;
19
+ }
20
+
21
+ const CustomerAuthContext = createContext<CustomerAuthContextType | undefined>(undefined);
22
+
23
+ export const CustomerAuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
24
+ const [user, setUser] = useState<CustomerUser | null>(null);
25
+ const [isLoading, setIsLoading] = useState(true);
26
+
27
+ useEffect(() => {
28
+ // Check if customer session exists
29
+ scrymeClient.customer.auth
30
+ .getSession()
31
+ .then((res) => {
32
+ if (res && res.data && res.data.user) {
33
+ setUser(res.data.user);
34
+ }
35
+ })
36
+ .catch(() => {
37
+ setUser(null);
38
+ })
39
+ .finally(() => {
40
+ setIsLoading(false);
41
+ });
42
+ }, []);
43
+
44
+ const login = async (email: string) => {
45
+ setIsLoading(true);
46
+ try {
47
+ // Example OTP/Login sequence via SDK
48
+ await scrymeClient.customer.auth.login({ email });
49
+ const session = await scrymeClient.customer.auth.getSession();
50
+ if (session?.data?.user) {
51
+ setUser(session.data.user);
52
+ }
53
+ } finally {
54
+ setIsLoading(false);
55
+ }
56
+ };
57
+
58
+ const logout = () => {
59
+ scrymeClient.customer.auth.logout();
60
+ setUser(null);
61
+ };
62
+
63
+ return (
64
+ <CustomerAuthContext.Provider value={{ user, isLoading, login, logout }}>
65
+ {children}
66
+ </CustomerAuthContext.Provider>
67
+ );
68
+ };
69
+
70
+ export const useCustomerAuth = () => {
71
+ const context = useContext(CustomerAuthContext);
72
+ if (!context) {
73
+ throw new Error('useCustomerAuth must be used within CustomerAuthProvider');
74
+ }
75
+ return context;
76
+ };
@@ -0,0 +1,20 @@
1
+ import type { Config } from 'tailwindcss';
2
+
3
+ const config: Config = {
4
+ content: [
5
+ './pages/**/*.{js,ts,jsx,tsx,mdx}',
6
+ './components/**/*.{js,ts,jsx,tsx,mdx}',
7
+ './app/**/*.{js,ts,jsx,tsx,mdx}',
8
+ './providers/**/*.{js,ts,jsx,tsx,mdx}',
9
+ ],
10
+ theme: {
11
+ extend: {
12
+ colors: {
13
+ background: 'var(--background)',
14
+ foreground: 'var(--foreground)',
15
+ },
16
+ },
17
+ },
18
+ plugins: [],
19
+ };
20
+ export default config;
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26
+ "exclude": ["node_modules"]
27
+ }