bestraw 1.0.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.
Files changed (66) hide show
  1. package/index.mjs +436 -0
  2. package/package.json +17 -0
  3. package/templates/.env.example +51 -0
  4. package/templates/Caddyfile +21 -0
  5. package/templates/docker-compose.yml +80 -0
  6. package/templates/web/Dockerfile +19 -0
  7. package/templates/web/next-env.d.ts +6 -0
  8. package/templates/web/next.config.ts +10 -0
  9. package/templates/web/node_modules/.bin/next +17 -0
  10. package/templates/web/node_modules/.bin/tsc +17 -0
  11. package/templates/web/node_modules/.bin/tsserver +17 -0
  12. package/templates/web/package.json +28 -0
  13. package/templates/web/postcss.config.mjs +8 -0
  14. package/templates/web/public/images/.gitkeep +0 -0
  15. package/templates/web/src/app/[locale]/auth/page.tsx +222 -0
  16. package/templates/web/src/app/[locale]/blog/[slug]/page.tsx +104 -0
  17. package/templates/web/src/app/[locale]/blog/page.tsx +90 -0
  18. package/templates/web/src/app/[locale]/error.tsx +41 -0
  19. package/templates/web/src/app/[locale]/info/page.tsx +186 -0
  20. package/templates/web/src/app/[locale]/layout.tsx +86 -0
  21. package/templates/web/src/app/[locale]/loyalty/page.tsx +135 -0
  22. package/templates/web/src/app/[locale]/menu/page.tsx +69 -0
  23. package/templates/web/src/app/[locale]/order/cart/page.tsx +199 -0
  24. package/templates/web/src/app/[locale]/order/checkout/page.tsx +489 -0
  25. package/templates/web/src/app/[locale]/order/confirmation/[id]/page.tsx +159 -0
  26. package/templates/web/src/app/[locale]/order/page.tsx +207 -0
  27. package/templates/web/src/app/[locale]/page.tsx +119 -0
  28. package/templates/web/src/app/globals.css +11 -0
  29. package/templates/web/src/app/robots.ts +14 -0
  30. package/templates/web/src/app/sitemap.ts +56 -0
  31. package/templates/web/src/bestraw.config.ts +9 -0
  32. package/templates/web/src/components/auth/OtpForm.tsx +98 -0
  33. package/templates/web/src/components/blog/ArticleCard.tsx +67 -0
  34. package/templates/web/src/components/blog/ArticleContent.tsx +14 -0
  35. package/templates/web/src/components/cart/CartDrawer.tsx +152 -0
  36. package/templates/web/src/components/cart/CartItem.tsx +111 -0
  37. package/templates/web/src/components/checkout/StripePaymentForm.tsx +54 -0
  38. package/templates/web/src/components/layout/Footer.tsx +40 -0
  39. package/templates/web/src/components/layout/Header.tsx +240 -0
  40. package/templates/web/src/components/layout/LocaleSwitcher.tsx +34 -0
  41. package/templates/web/src/components/loyalty/PointsBalance.tsx +96 -0
  42. package/templates/web/src/components/loyalty/RewardCard.tsx +73 -0
  43. package/templates/web/src/components/loyalty/TransactionHistory.tsx +108 -0
  44. package/templates/web/src/components/menu/CategorySection.tsx +42 -0
  45. package/templates/web/src/components/menu/MealCard.tsx +55 -0
  46. package/templates/web/src/components/menu/MealDetailModal.tsx +355 -0
  47. package/templates/web/src/components/menu/MenuContent.tsx +216 -0
  48. package/templates/web/src/components/order/MealOrderCard.tsx +220 -0
  49. package/templates/web/src/components/order/OrderStatusTracker.tsx +138 -0
  50. package/templates/web/src/components/order/PaymentStatus.tsx +62 -0
  51. package/templates/web/src/components/ui/Button.tsx +40 -0
  52. package/templates/web/src/components/ui/ErrorAlert.tsx +15 -0
  53. package/templates/web/src/i18n/config.ts +3 -0
  54. package/templates/web/src/i18n/request.ts +13 -0
  55. package/templates/web/src/i18n/routing.ts +10 -0
  56. package/templates/web/src/lib/client.ts +5 -0
  57. package/templates/web/src/lib/errors.ts +31 -0
  58. package/templates/web/src/lib/features.ts +10 -0
  59. package/templates/web/src/lib/hooks/useCustomerClient.ts +28 -0
  60. package/templates/web/src/lib/hooks/useMenu.ts +46 -0
  61. package/templates/web/src/messages/en.json +283 -0
  62. package/templates/web/src/messages/fr.json +283 -0
  63. package/templates/web/src/middleware.ts +8 -0
  64. package/templates/web/src/providers/CartProvider.tsx +162 -0
  65. package/templates/web/src/providers/StripeProvider.tsx +21 -0
  66. package/templates/web/tsconfig.json +27 -0
@@ -0,0 +1,162 @@
1
+ 'use client';
2
+
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useState,
9
+ type ReactNode,
10
+ } from 'react';
11
+
12
+ const STORAGE_KEY = 'bestraw-cart';
13
+
14
+ export interface CartItem {
15
+ mealDocumentId: string;
16
+ mealName: string;
17
+ unitPrice: number;
18
+ quantity: number;
19
+ selectedSauces: string[];
20
+ selectedSides: string[];
21
+ specialInstructions: string;
22
+ picture?: string;
23
+ }
24
+
25
+ interface CartContextValue {
26
+ items: CartItem[];
27
+ addItem: (item: CartItem) => void;
28
+ removeItem: (index: number) => void;
29
+ updateQuantity: (index: number, quantity: number) => void;
30
+ clearCart: () => void;
31
+ itemCount: number;
32
+ total: number;
33
+ isOpen: boolean;
34
+ openCart: () => void;
35
+ closeCart: () => void;
36
+ }
37
+
38
+ const CartContext = createContext<CartContextValue | null>(null);
39
+
40
+ function isSameItem(a: CartItem, b: CartItem): boolean {
41
+ return (
42
+ a.mealDocumentId === b.mealDocumentId &&
43
+ JSON.stringify(a.selectedSauces.slice().sort()) ===
44
+ JSON.stringify(b.selectedSauces.slice().sort()) &&
45
+ JSON.stringify(a.selectedSides.slice().sort()) ===
46
+ JSON.stringify(b.selectedSides.slice().sort())
47
+ );
48
+ }
49
+
50
+ function loadCart(): CartItem[] {
51
+ if (typeof window === 'undefined') return [];
52
+ try {
53
+ const raw = localStorage.getItem(STORAGE_KEY);
54
+ if (!raw) return [];
55
+ const parsed = JSON.parse(raw);
56
+ return Array.isArray(parsed) ? parsed : [];
57
+ } catch {
58
+ return [];
59
+ }
60
+ }
61
+
62
+ function saveCart(items: CartItem[]) {
63
+ try {
64
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
65
+ } catch {
66
+ // Storage full or unavailable
67
+ }
68
+ }
69
+
70
+ export function CartProvider({ children }: { children: ReactNode }) {
71
+ const [items, setItems] = useState<CartItem[]>([]);
72
+ const [isOpen, setIsOpen] = useState(false);
73
+ const [mounted, setMounted] = useState(false);
74
+
75
+ // Load from localStorage on mount
76
+ useEffect(() => {
77
+ setItems(loadCart());
78
+ setMounted(true);
79
+ }, []);
80
+
81
+ // Save to localStorage on every change (after initial mount)
82
+ useEffect(() => {
83
+ if (mounted) {
84
+ saveCart(items);
85
+ }
86
+ }, [items, mounted]);
87
+
88
+ const addItem = useCallback((item: CartItem) => {
89
+ setItems((prev) => {
90
+ const existingIndex = prev.findIndex((existing) =>
91
+ isSameItem(existing, item),
92
+ );
93
+ if (existingIndex !== -1) {
94
+ const updated = [...prev];
95
+ updated[existingIndex] = {
96
+ ...updated[existingIndex],
97
+ quantity: updated[existingIndex].quantity + item.quantity,
98
+ };
99
+ return updated;
100
+ }
101
+ return [...prev, item];
102
+ });
103
+ }, []);
104
+
105
+ const removeItem = useCallback((index: number) => {
106
+ setItems((prev) => prev.filter((_, i) => i !== index));
107
+ }, []);
108
+
109
+ const updateQuantity = useCallback((index: number, quantity: number) => {
110
+ if (quantity <= 0) {
111
+ setItems((prev) => prev.filter((_, i) => i !== index));
112
+ return;
113
+ }
114
+ setItems((prev) => {
115
+ const updated = [...prev];
116
+ if (updated[index]) {
117
+ updated[index] = { ...updated[index], quantity };
118
+ }
119
+ return updated;
120
+ });
121
+ }, []);
122
+
123
+ const clearCart = useCallback(() => {
124
+ setItems([]);
125
+ }, []);
126
+
127
+ const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
128
+ const total = items.reduce(
129
+ (sum, item) => sum + item.unitPrice * item.quantity,
130
+ 0,
131
+ );
132
+
133
+ const openCart = useCallback(() => setIsOpen(true), []);
134
+ const closeCart = useCallback(() => setIsOpen(false), []);
135
+
136
+ return (
137
+ <CartContext.Provider
138
+ value={{
139
+ items,
140
+ addItem,
141
+ removeItem,
142
+ updateQuantity,
143
+ clearCart,
144
+ itemCount,
145
+ total,
146
+ isOpen,
147
+ openCart,
148
+ closeCart,
149
+ }}
150
+ >
151
+ {children}
152
+ </CartContext.Provider>
153
+ );
154
+ }
155
+
156
+ export function useCart(): CartContextValue {
157
+ const context = useContext(CartContext);
158
+ if (!context) {
159
+ throw new Error('useCart must be used within a CartProvider');
160
+ }
161
+ return context;
162
+ }
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+
3
+ import { loadStripe } from '@stripe/stripe-js';
4
+ import { Elements } from '@stripe/react-stripe-js';
5
+
6
+ const stripePromise = loadStripe(
7
+ process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
8
+ );
9
+
10
+ interface StripeProviderProps {
11
+ clientSecret: string;
12
+ children: React.ReactNode;
13
+ }
14
+
15
+ export function StripeProvider({ clientSecret, children }: StripeProviderProps) {
16
+ return (
17
+ <Elements stripe={stripePromise} options={{ clientSecret }}>
18
+ {children}
19
+ </Elements>
20
+ );
21
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
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
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26
+ "exclude": ["node_modules"]
27
+ }