@4alldigital/foundation-ui--core 3.4.1 → 3.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4alldigital/foundation-ui--core",
3
- "version": "3.4.1",
3
+ "version": "3.5.0",
4
4
  "description": "Foundation UI Core Component Library (source distribution)",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -32,5 +32,5 @@
32
32
  },
33
33
  "author": "Joe Mewes",
34
34
  "license": "MIT",
35
- "gitHead": "c0bf0a089038c5d82870b126bec475a1b16f0f37"
35
+ "gitHead": "01f84484637258ca584addb4e9577b08d6b38739"
36
36
  }
@@ -0,0 +1,211 @@
1
+ import * as React from 'react';
2
+ import { useState, useEffect } from 'react';
3
+ import { cn } from '../../utils';
4
+ import { AddressFormProps, ShippingAddress, AddressErrors } from './AddressForm.types';
5
+ import TextInput from '../TextInput';
6
+ import FormSelect from '../FormSelect';
7
+
8
+ // Common countries
9
+ const COUNTRIES = [
10
+ { value: 'GB', label: 'United Kingdom' },
11
+ { value: 'US', label: 'United States' },
12
+ { value: 'CA', label: 'Canada' },
13
+ { value: 'AU', label: 'Australia' },
14
+ { value: 'DE', label: 'Germany' },
15
+ { value: 'FR', label: 'France' },
16
+ { value: 'ES', label: 'Spain' },
17
+ { value: 'IT', label: 'Italy' },
18
+ { value: 'NL', label: 'Netherlands' },
19
+ { value: 'IE', label: 'Ireland' },
20
+ ];
21
+
22
+ export const AddressForm = React.forwardRef<HTMLDivElement, AddressFormProps>(
23
+ ({ value = {}, onChange, onValidationChange, disabled = false, className, testID }, ref) => {
24
+ const [address, setAddress] = useState<Partial<ShippingAddress>>(value);
25
+ const [errors, setErrors] = useState<AddressErrors>({});
26
+ const [touched, setTouched] = useState<Record<string, boolean>>({});
27
+
28
+ // Validate field
29
+ const validateField = (name: keyof ShippingAddress, val: string): string | undefined => {
30
+ if (!val && name !== 'line2' && name !== 'phone') {
31
+ return 'This field is required';
32
+ }
33
+
34
+ if (name === 'postal_code' && val) {
35
+ // Basic postal code validation
36
+ if (val.length < 3) {
37
+ return 'Invalid postal code';
38
+ }
39
+ }
40
+
41
+ if (name === 'phone' && val) {
42
+ // Basic phone validation
43
+ if (val.length < 10) {
44
+ return 'Invalid phone number';
45
+ }
46
+ }
47
+
48
+ return undefined;
49
+ };
50
+
51
+ // Validate all fields
52
+ const validateForm = (): boolean => {
53
+ const newErrors: AddressErrors = {};
54
+ let isValid = true;
55
+
56
+ (['name', 'line1', 'city', 'state', 'postal_code', 'country'] as const).forEach((field) => {
57
+ const error = validateField(field, address[field] || '');
58
+ if (error) {
59
+ newErrors[field] = error;
60
+ isValid = false;
61
+ }
62
+ });
63
+
64
+ setErrors(newErrors);
65
+ return isValid;
66
+ };
67
+
68
+ // Handle field change
69
+ const handleChange = (name: keyof ShippingAddress, newValue: string) => {
70
+ const updated = { ...address, [name]: newValue };
71
+ setAddress(updated);
72
+
73
+ // Validate on change if already touched
74
+ if (touched[name]) {
75
+ const error = validateField(name, newValue);
76
+ setErrors((prev) => ({ ...prev, [name]: error }));
77
+ }
78
+
79
+ // Check if form is complete and valid
80
+ if (
81
+ updated.name &&
82
+ updated.line1 &&
83
+ updated.city &&
84
+ updated.state &&
85
+ updated.postal_code &&
86
+ updated.country
87
+ ) {
88
+ onChange(updated as ShippingAddress);
89
+ }
90
+ };
91
+
92
+ // Handle field blur
93
+ const handleBlur = (name: keyof ShippingAddress) => {
94
+ setTouched((prev) => ({ ...prev, [name]: true }));
95
+ const error = validateField(name, address[name] || '');
96
+ setErrors((prev) => ({ ...prev, [name]: error }));
97
+ };
98
+
99
+ // Notify validation state
100
+ useEffect(() => {
101
+ const isValid = validateForm();
102
+ onValidationChange?.(isValid);
103
+ }, [address, onValidationChange]);
104
+
105
+ return (
106
+ <div ref={ref} data-testid={testID || 'AddressForm'} className={cn('space-y-4', className)}>
107
+ <h3 className="text-lg font-semibold">Shipping Address</h3>
108
+
109
+ {/* Full Name */}
110
+ <TextInput
111
+ id="shipping-name"
112
+ label="Full Name"
113
+ value={address.name || ''}
114
+ onChange={(e) => handleChange('name', e.target.value)}
115
+ onBlur={() => handleBlur('name')}
116
+ disabled={disabled}
117
+ error={touched.name && errors.name}
118
+ required
119
+ />
120
+
121
+ {/* Address Line 1 */}
122
+ <TextInput
123
+ id="shipping-line1"
124
+ label="Address Line 1"
125
+ value={address.line1 || ''}
126
+ onChange={(e) => handleChange('line1', e.target.value)}
127
+ onBlur={() => handleBlur('line1')}
128
+ disabled={disabled}
129
+ error={touched.line1 && errors.line1}
130
+ required
131
+ />
132
+
133
+ {/* Address Line 2 */}
134
+ <TextInput
135
+ id="shipping-line2"
136
+ label="Address Line 2 (Optional)"
137
+ value={address.line2 || ''}
138
+ onChange={(e) => handleChange('line2', e.target.value)}
139
+ disabled={disabled}
140
+ />
141
+
142
+ {/* City and State */}
143
+ <div className="grid gap-4 sm:grid-cols-2">
144
+ <TextInput
145
+ id="shipping-city"
146
+ label="City"
147
+ value={address.city || ''}
148
+ onChange={(e) => handleChange('city', e.target.value)}
149
+ onBlur={() => handleBlur('city')}
150
+ disabled={disabled}
151
+ error={touched.city && errors.city}
152
+ required
153
+ />
154
+
155
+ <TextInput
156
+ id="shipping-state"
157
+ label="State / County"
158
+ value={address.state || ''}
159
+ onChange={(e) => handleChange('state', e.target.value)}
160
+ onBlur={() => handleBlur('state')}
161
+ disabled={disabled}
162
+ error={touched.state && errors.state}
163
+ required
164
+ />
165
+ </div>
166
+
167
+ {/* Postal Code and Country */}
168
+ <div className="grid gap-4 sm:grid-cols-2">
169
+ <TextInput
170
+ id="shipping-postal-code"
171
+ label="Postal Code"
172
+ value={address.postal_code || ''}
173
+ onChange={(e) => handleChange('postal_code', e.target.value)}
174
+ onBlur={() => handleBlur('postal_code')}
175
+ disabled={disabled}
176
+ error={touched.postal_code && errors.postal_code}
177
+ required
178
+ />
179
+
180
+ <FormSelect
181
+ id="shipping-country"
182
+ label="Country"
183
+ value={address.country || ''}
184
+ onChange={(value) => handleChange('country', value)}
185
+ onBlur={() => handleBlur('country')}
186
+ options={COUNTRIES}
187
+ disabled={disabled}
188
+ error={touched.country && errors.country}
189
+ required
190
+ />
191
+ </div>
192
+
193
+ {/* Phone */}
194
+ <TextInput
195
+ id="shipping-phone"
196
+ label="Phone Number (Optional)"
197
+ type="tel"
198
+ value={address.phone || ''}
199
+ onChange={(e) => handleChange('phone', e.target.value)}
200
+ onBlur={() => handleBlur('phone')}
201
+ disabled={disabled}
202
+ error={touched.phone && errors.phone}
203
+ />
204
+ </div>
205
+ );
206
+ },
207
+ );
208
+
209
+ AddressForm.displayName = 'AddressForm';
210
+
211
+ export type { AddressFormProps, ShippingAddress };
@@ -0,0 +1,29 @@
1
+ export interface ShippingAddress {
2
+ name: string;
3
+ line1: string;
4
+ line2?: string;
5
+ city: string;
6
+ state: string;
7
+ postal_code: string;
8
+ country: string;
9
+ phone?: string;
10
+ }
11
+
12
+ export interface AddressFormProps {
13
+ value?: Partial<ShippingAddress>;
14
+ onChange: (address: ShippingAddress) => void;
15
+ onValidationChange?: (isValid: boolean) => void;
16
+ disabled?: boolean;
17
+ className?: string;
18
+ testID?: string;
19
+ }
20
+
21
+ export interface AddressErrors {
22
+ name?: string;
23
+ line1?: string;
24
+ city?: string;
25
+ state?: string;
26
+ postal_code?: string;
27
+ country?: string;
28
+ phone?: string;
29
+ }
@@ -0,0 +1,2 @@
1
+ export { AddressForm } from './AddressForm';
2
+ export type { AddressFormProps, ShippingAddress } from './AddressForm.types';
@@ -14,6 +14,7 @@ import {
14
14
  } from '../ShadcnCarousel';
15
15
  import Image from '../Image';
16
16
  import Tabs from '../Tabs';
17
+ import { AddressForm, type ShippingAddress } from '../AddressForm';
17
18
  import { Minus, Plus, ShoppingCart } from 'lucide-react';
18
19
 
19
20
  export const ProductDetail = React.forwardRef<HTMLDivElement, ProductDetailProps>(
@@ -21,6 +22,8 @@ export const ProductDetail = React.forwardRef<HTMLDivElement, ProductDetailProps
21
22
  const [selectedSize, setSelectedSize] = useState<string | undefined>();
22
23
  const [selectedColor, setSelectedColor] = useState<string | undefined>();
23
24
  const [quantity, setQuantity] = useState(1);
25
+ const [shippingAddress, setShippingAddress] = useState<ShippingAddress | undefined>();
26
+ const [isShippingValid, setIsShippingValid] = useState(false);
24
27
 
25
28
  const formatPrice = (price: number, currency: string) => {
26
29
  return new Intl.NumberFormat('en-GB', {
@@ -70,6 +73,7 @@ export const ProductDetail = React.forwardRef<HTMLDivElement, ProductDetailProps
70
73
  selectedSize,
71
74
  selectedColor,
72
75
  quantity,
76
+ shippingAddress,
73
77
  };
74
78
  onPurchase(state);
75
79
  };
@@ -78,7 +82,8 @@ export const ProductDetail = React.forwardRef<HTMLDivElement, ProductDetailProps
78
82
  const canPurchase =
79
83
  !isLoading &&
80
84
  (sizeOptions.length === 0 || selectedSize) &&
81
- (colorOptions.length === 0 || selectedColor);
85
+ (colorOptions.length === 0 || selectedColor) &&
86
+ (!isAuthenticated || (isAuthenticated && isShippingValid));
82
87
 
83
88
  // Tab content
84
89
  const tabItems = [
@@ -205,6 +210,18 @@ export const ProductDetail = React.forwardRef<HTMLDivElement, ProductDetailProps
205
210
  </div>
206
211
  )}
207
212
 
213
+ {/* Shipping Address Form (only for authenticated users) */}
214
+ {isAuthenticated && (
215
+ <div className="border-t pt-6">
216
+ <AddressForm
217
+ value={shippingAddress}
218
+ onChange={setShippingAddress}
219
+ onValidationChange={setIsShippingValid}
220
+ disabled={isLoading}
221
+ />
222
+ </div>
223
+ )}
224
+
208
225
  {/* Purchase Button or Auth Message */}
209
226
  {!isAuthenticated ? (
210
227
  <div className="rounded-lg border border-primary bg-primary/10 p-6 text-center">
@@ -230,7 +247,8 @@ export const ProductDetail = React.forwardRef<HTMLDivElement, ProductDetailProps
230
247
  {!canPurchase && !isLoading && (
231
248
  <p className="text-sm text-muted-foreground">
232
249
  {!selectedSize && sizeOptions.length > 0 && 'Please select a size. '}
233
- {!selectedColor && colorOptions.length > 0 && 'Please select a color.'}
250
+ {!selectedColor && colorOptions.length > 0 && 'Please select a color. '}
251
+ {!isShippingValid && 'Please complete shipping address.'}
234
252
  </p>
235
253
  )}
236
254
  </>
@@ -1,9 +1,11 @@
1
- import { Product } from '../ProductCard/ProductCard.types';
1
+ import { Product } from '../ProductCard';
2
+ import { ShippingAddress } from '../AddressForm';
2
3
 
3
4
  export interface ProductDetailState {
4
5
  selectedSize?: string;
5
6
  selectedColor?: string;
6
7
  quantity: number;
8
+ shippingAddress?: ShippingAddress;
7
9
  }
8
10
 
9
11
  export interface ProductDetailProps {
@@ -25,6 +25,7 @@ export { default as Avatar } from './Avatar';
25
25
  export { default as Banner } from './Banner';
26
26
 
27
27
  // MOLECULES
28
+ export { AddressForm } from './AddressForm';
28
29
  export { default as ButtonGroup } from './ButtonGroup';
29
30
  export { default as Card } from './Card';
30
31
  export { default as DisplayHeading } from './DisplayHeading';
@@ -155,6 +156,7 @@ export type { Props as HeaderProps } from './Header/Header.types';
155
156
  export type { ShadcnButtonProps } from './ShadcnButton';
156
157
 
157
158
  // PRODUCT COMPONENT TYPES
159
+ export type { AddressFormProps, ShippingAddress } from './AddressForm';
158
160
  export type { ProductCardProps, ProductCardVariantProps, Product } from './ProductCard';
159
161
  export type { VariantSelectorProps, VariantOption } from './VariantSelector';
160
162
  export type { ProductDetailProps, ProductDetailState } from './ProductDetail';
@@ -0,0 +1,188 @@
1
+ import React from 'react';
2
+ import Screen from '../../components/Screen';
3
+ import DisplayHeading from '../../components/DisplayHeading';
4
+ import { format } from 'date-fns';
5
+ import { OrderDetailScreenProps } from './OrderDetailScreen.types';
6
+
7
+ const OrderDetailScreen = ({
8
+ testID,
9
+ order,
10
+ isLoading = false,
11
+ error,
12
+ onBackClick,
13
+ }: OrderDetailScreenProps) => {
14
+ const formatPrice = (amount: number, currency: string) => {
15
+ return new Intl.NumberFormat('en-US', {
16
+ style: 'currency',
17
+ currency: currency.toUpperCase(),
18
+ }).format(amount / 100);
19
+ };
20
+
21
+ const formatStatus = (status: string) => {
22
+ return status
23
+ .split('_')
24
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
25
+ .join(' ');
26
+ };
27
+
28
+ const getStatusColor = (status: string) => {
29
+ switch (status) {
30
+ case 'succeeded':
31
+ return 'bg-success/10 text-success';
32
+ case 'processing':
33
+ return 'bg-warning/10 text-warning';
34
+ case 'requires_payment_method':
35
+ case 'requires_confirmation':
36
+ case 'requires_action':
37
+ return 'bg-warning/10 text-warning';
38
+ case 'canceled':
39
+ return 'bg-muted text-muted-foreground';
40
+ default:
41
+ return 'bg-muted text-body-text';
42
+ }
43
+ };
44
+
45
+ return (
46
+ <Screen data-testid={testID || 'OrderDetailScreen'}>
47
+ <div className="container mx-auto px-4 py-8">
48
+ {onBackClick && (
49
+ <button onClick={onBackClick} className="mb-8 text-primary hover:underline">
50
+ ← Back to Orders
51
+ </button>
52
+ )}
53
+
54
+ {isLoading && (
55
+ <div className="flex items-center justify-center py-16">
56
+ <div className="h-12 w-12 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
57
+ </div>
58
+ )}
59
+
60
+ {error && (
61
+ <div className="rounded-lg bg-destructive/10 p-8 text-center">
62
+ <h2 className="mb-2 text-2xl font-bold text-destructive">Unable to Load Order</h2>
63
+ <p className="text-muted-foreground">{error.message || 'Please try again later.'}</p>
64
+ </div>
65
+ )}
66
+
67
+ {!isLoading && !error && order && (
68
+ <div className="space-y-6">
69
+ {/* Order Header */}
70
+ <div className="rounded-lg border border-border bg-card p-6">
71
+ <div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
72
+ <div>
73
+ <h2 className="mb-2 text-2xl font-bold">
74
+ Order #{order.id.slice(-8).toUpperCase()}
75
+ </h2>
76
+ <p className="text-muted-foreground">
77
+ Placed on {format(new Date(order.created * 1000), 'MMMM d, yyyy')}
78
+ </p>
79
+ </div>
80
+ <div
81
+ className={`inline-block rounded-full px-4 py-2 text-sm font-medium ${getStatusColor(order.status)}`}
82
+ >
83
+ {formatStatus(order.status)}
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ {/* Product Details */}
89
+ <div className="rounded-lg border border-border bg-card p-6">
90
+ <h3 className="mb-4 text-xl font-semibold">Product Details</h3>
91
+ <div className="space-y-3">
92
+ <div className="flex justify-between">
93
+ <span className="text-muted-foreground">Product</span>
94
+ <span className="font-medium">{order.productName}</span>
95
+ </div>
96
+ <div className="flex justify-between">
97
+ <span className="text-muted-foreground">Quantity</span>
98
+ <span className="font-medium">{order.quantity}</span>
99
+ </div>
100
+ {order.selectedVariants && Object.keys(order.selectedVariants).length > 0 && (
101
+ <div className="flex justify-between">
102
+ <span className="text-muted-foreground">Options</span>
103
+ <span className="font-medium">
104
+ {Object.entries(order.selectedVariants)
105
+ .map(([key, value]) => `${key}: ${value}`)
106
+ .join(', ')}
107
+ </span>
108
+ </div>
109
+ )}
110
+ <div className="flex justify-between border-t pt-3">
111
+ <span className="font-medium">Total</span>
112
+ <span className="text-2xl font-bold">
113
+ {formatPrice(order.amount, order.currency)}
114
+ </span>
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ {/* Shipping Address */}
120
+ {order.shipping && (
121
+ <div className="rounded-lg border border-border bg-card p-6">
122
+ <h3 className="mb-4 text-xl font-semibold">Shipping Address</h3>
123
+ <div className="space-y-1">
124
+ <p className="font-medium">{order.shipping.name}</p>
125
+ <p>{order.shipping.address.line1}</p>
126
+ {order.shipping.address.line2 && <p>{order.shipping.address.line2}</p>}
127
+ <p>
128
+ {order.shipping.address.city}, {order.shipping.address.state}{' '}
129
+ {order.shipping.address.postal_code}
130
+ </p>
131
+ <p>{order.shipping.address.country}</p>
132
+ {order.shipping.phone && (
133
+ <p className="mt-2 text-muted-foreground">Phone: {order.shipping.phone}</p>
134
+ )}
135
+ </div>
136
+ </div>
137
+ )}
138
+
139
+ {/* Tracking Information */}
140
+ {(order.trackingNumber || order.trackingUrl) && (
141
+ <div className="rounded-lg border border-border bg-card p-6">
142
+ <h3 className="mb-4 text-xl font-semibold">Tracking Information</h3>
143
+ <div className="space-y-3">
144
+ {order.trackingNumber && (
145
+ <div className="flex justify-between">
146
+ <span className="text-muted-foreground">Tracking Number</span>
147
+ <span className="font-medium">{order.trackingNumber}</span>
148
+ </div>
149
+ )}
150
+ {order.trackingUrl && (
151
+ <a
152
+ href={order.trackingUrl}
153
+ target="_blank"
154
+ rel="noopener noreferrer"
155
+ className="block w-full rounded-md bg-primary px-6 py-2 text-center text-white hover:bg-primary/90"
156
+ >
157
+ Track Package
158
+ </a>
159
+ )}
160
+ </div>
161
+ </div>
162
+ )}
163
+
164
+ {/* Receipt */}
165
+ {order.receiptUrl && (
166
+ <div className="rounded-lg border border-border bg-card p-6">
167
+ <h3 className="mb-4 text-xl font-semibold">Receipt</h3>
168
+ <a
169
+ href={order.receiptUrl}
170
+ target="_blank"
171
+ rel="noopener noreferrer"
172
+ className="block w-full rounded-md border border-border bg-secondary px-6 py-2 text-center hover:bg-secondary/80"
173
+ >
174
+ View Receipt
175
+ </a>
176
+ </div>
177
+ )}
178
+ </div>
179
+ )}
180
+ </div>
181
+ </Screen>
182
+ );
183
+ };
184
+
185
+ OrderDetailScreen.displayName = 'OrderDetailScreen';
186
+
187
+ export default OrderDetailScreen;
188
+ export type { OrderDetailScreenProps };
@@ -0,0 +1,39 @@
1
+ export interface ShippingAddress {
2
+ line1: string;
3
+ line2?: string;
4
+ city: string;
5
+ state: string;
6
+ postal_code: string;
7
+ country: string;
8
+ }
9
+
10
+ export interface Shipping {
11
+ name: string;
12
+ phone?: string;
13
+ address: ShippingAddress;
14
+ }
15
+
16
+ export interface OrderDetail {
17
+ id: string;
18
+ status: string;
19
+ amount: number;
20
+ currency: string;
21
+ created: number;
22
+ productId: string;
23
+ productName: string;
24
+ quantity: string;
25
+ partnerId?: string;
26
+ selectedVariants?: Record<string, string>;
27
+ shipping?: Shipping;
28
+ trackingNumber?: string;
29
+ trackingUrl?: string;
30
+ receiptUrl?: string;
31
+ }
32
+
33
+ export interface OrderDetailScreenProps {
34
+ testID?: string;
35
+ order?: OrderDetail;
36
+ isLoading?: boolean;
37
+ error?: Error | null;
38
+ onBackClick?: () => void;
39
+ }
@@ -0,0 +1,2 @@
1
+ export { default } from './OrderDetailScreen';
2
+ export type { OrderDetailScreenProps, OrderDetail } from './OrderDetailScreen.types';
@@ -0,0 +1,132 @@
1
+ import React from 'react';
2
+ import Screen from '../../components/Screen';
3
+ import DisplayHeading from '../../components/DisplayHeading';
4
+ import { formatDistance } from 'date-fns';
5
+ import { OrdersHistoryScreenProps } from './OrdersHistoryScreen.types';
6
+
7
+ const OrdersHistoryScreen = ({
8
+ testID,
9
+ orders = [],
10
+ isLoading = false,
11
+ error,
12
+ onOrderClick,
13
+ }: OrdersHistoryScreenProps) => {
14
+ const formatPrice = (amount: number, currency: string) => {
15
+ return new Intl.NumberFormat('en-US', {
16
+ style: 'currency',
17
+ currency: currency.toUpperCase(),
18
+ }).format(amount / 100);
19
+ };
20
+
21
+ const formatStatus = (status: string) => {
22
+ return status
23
+ .split('_')
24
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
25
+ .join(' ');
26
+ };
27
+
28
+ const getStatusColor = (status: string) => {
29
+ switch (status) {
30
+ case 'succeeded':
31
+ return 'text-success';
32
+ case 'processing':
33
+ return 'text-warning';
34
+ case 'requires_payment_method':
35
+ case 'requires_confirmation':
36
+ case 'requires_action':
37
+ return 'text-warning';
38
+ case 'canceled':
39
+ return 'text-muted-foreground';
40
+ default:
41
+ return 'text-body-text';
42
+ }
43
+ };
44
+
45
+ return (
46
+ <Screen data-testid={testID || 'OrdersHistoryScreen'}>
47
+ <DisplayHeading text="Order History" />
48
+
49
+ <div className="container mx-auto px-4 py-8">
50
+ {isLoading && (
51
+ <div className="flex items-center justify-center py-16">
52
+ <div className="h-12 w-12 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
53
+ </div>
54
+ )}
55
+
56
+ {error && (
57
+ <div className="rounded-lg bg-destructive/10 p-8 text-center">
58
+ <h2 className="mb-2 text-2xl font-bold text-destructive">Unable to Load Orders</h2>
59
+ <p className="text-muted-foreground">{error.message || 'Please try again later.'}</p>
60
+ </div>
61
+ )}
62
+
63
+ {!isLoading && !error && orders.length === 0 && (
64
+ <div className="rounded-lg border border-border bg-card p-12 text-center">
65
+ <h3 className="mb-2 text-xl font-semibold">No Orders Yet</h3>
66
+ <p className="mb-4 text-muted-foreground">
67
+ You haven&apos;t made any partnership product purchases yet.
68
+ </p>
69
+ <a href="/partnership" className="inline-block rounded-md bg-primary px-6 py-2 text-white hover:bg-primary/90">
70
+ Browse Products
71
+ </a>
72
+ </div>
73
+ )}
74
+
75
+ {!isLoading && !error && orders.length > 0 && (
76
+ <div className="space-y-4">
77
+ {orders.map((order) => (
78
+ <div
79
+ key={order.id}
80
+ className="cursor-pointer rounded-lg border border-border bg-card p-6 transition-all hover:shadow-lg"
81
+ onClick={() => onOrderClick?.(order.id)}
82
+ >
83
+ <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
84
+ <div className="flex-1">
85
+ <div className="mb-2 flex items-center gap-3">
86
+ <h4 className="text-lg font-semibold">{order.productName}</h4>
87
+ <span className={`text-sm font-medium ${getStatusColor(order.status)}`}>
88
+ {formatStatus(order.status)}
89
+ </span>
90
+ </div>
91
+ <div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
92
+ <span>Order #{order.id.slice(-8).toUpperCase()}</span>
93
+ <span>
94
+ {formatDistance(new Date(order.created * 1000), new Date(), {
95
+ addSuffix: true,
96
+ })}
97
+ </span>
98
+ {order.quantity && <span>Qty: {order.quantity}</span>}
99
+ {order.selectedVariants && (
100
+ <span>
101
+ {Object.entries(order.selectedVariants)
102
+ .map(([key, value]) => `${key}: ${value}`)
103
+ .join(', ')}
104
+ </span>
105
+ )}
106
+ </div>
107
+ {order.trackingNumber && (
108
+ <div className="mt-2 text-sm">
109
+ Tracking: <span className="font-medium">{order.trackingNumber}</span>
110
+ </div>
111
+ )}
112
+ </div>
113
+ <div className="text-right">
114
+ <p className="text-2xl font-bold">{formatPrice(order.amount, order.currency)}</p>
115
+ <button className="mt-2 text-sm text-primary hover:underline">
116
+ View Details
117
+ </button>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ ))}
122
+ </div>
123
+ )}
124
+ </div>
125
+ </Screen>
126
+ );
127
+ };
128
+
129
+ OrdersHistoryScreen.displayName = 'OrdersHistoryScreen';
130
+
131
+ export default OrdersHistoryScreen;
132
+ export type { OrdersHistoryScreenProps };
@@ -0,0 +1,22 @@
1
+ export interface Order {
2
+ id: string;
3
+ status: string;
4
+ amount: number;
5
+ currency: string;
6
+ created: number;
7
+ productId: string;
8
+ productName: string;
9
+ quantity: string;
10
+ partnerId?: string;
11
+ selectedVariants?: Record<string, string>;
12
+ trackingNumber?: string;
13
+ trackingUrl?: string;
14
+ }
15
+
16
+ export interface OrdersHistoryScreenProps {
17
+ testID?: string;
18
+ orders: Order[];
19
+ isLoading?: boolean;
20
+ error?: Error | null;
21
+ onOrderClick?: (orderId: string) => void;
22
+ }
@@ -0,0 +1,2 @@
1
+ export { default } from './OrdersHistoryScreen';
2
+ export type { OrdersHistoryScreenProps, Order } from './OrdersHistoryScreen.types';
@@ -0,0 +1,66 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import PurchaseConfirmationScreen from './PurchaseConfirmationScreen';
3
+
4
+ const meta: Meta<typeof PurchaseConfirmationScreen> = {
5
+ title: 'Templates/PurchaseConfirmationScreen',
6
+ component: PurchaseConfirmationScreen,
7
+ parameters: {
8
+ layout: 'fullscreen',
9
+ },
10
+ };
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof PurchaseConfirmationScreen>;
14
+
15
+ export const Default: Story = {
16
+ args: {
17
+ onContinueShopping: () => console.log('Continue Shopping'),
18
+ onGoToDashboard: () => console.log('Go to Dashboard'),
19
+ },
20
+ };
21
+
22
+ export const WithOrderDetails: Story = {
23
+ args: {
24
+ orderDetails: {
25
+ orderId: 'ORD-2024-001234',
26
+ orderDate: 'December 21, 2024',
27
+ items: [
28
+ {
29
+ name: 'NRG Performance Leggings',
30
+ quantity: 1,
31
+ price: 4500,
32
+ },
33
+ {
34
+ name: 'Athletic Tank Top',
35
+ quantity: 2,
36
+ price: 2500,
37
+ },
38
+ ],
39
+ total: 9500,
40
+ currency: 'gbp',
41
+ estimatedDelivery: 'December 28-30, 2024',
42
+ },
43
+ onContinueShopping: () => console.log('Continue Shopping'),
44
+ onViewOrders: () => console.log('View Orders'),
45
+ onGoToDashboard: () => console.log('Go to Dashboard'),
46
+ },
47
+ };
48
+
49
+ export const CustomMessage: Story = {
50
+ args: {
51
+ title: 'Order Confirmed!',
52
+ message: 'Your order has been successfully placed and is being processed. You will receive shipping updates via email.',
53
+ onContinueShopping: () => console.log('Continue Shopping'),
54
+ },
55
+ };
56
+
57
+ export const MinimalActions: Story = {
58
+ args: {
59
+ orderDetails: {
60
+ orderId: 'ORD-2024-001234',
61
+ total: 4500,
62
+ currency: 'gbp',
63
+ },
64
+ onContinueShopping: () => console.log('Continue Shopping'),
65
+ },
66
+ };
@@ -0,0 +1,144 @@
1
+ import React from 'react';
2
+ import Screen from '../../components/Screen';
3
+ import { Props } from './PurchaseConfirmationScreen.types';
4
+ import { ShadcnButton } from '../../components/ShadcnButton';
5
+ import { CheckCircle } from 'lucide-react';
6
+
7
+ const PurchaseConfirmationScreen = ({
8
+ testID,
9
+ title = 'Purchase Successful!',
10
+ message = 'Thank you for your purchase. A confirmation email has been sent to your email address with your order details.',
11
+ orderDetails,
12
+ onContinueShopping,
13
+ onViewOrders,
14
+ onGoToDashboard,
15
+ }: Props) => {
16
+ const formatPrice = (price: number, currency: string = 'gbp') => {
17
+ return new Intl.NumberFormat('en-GB', {
18
+ style: 'currency',
19
+ currency: currency.toUpperCase(),
20
+ }).format(price / 100);
21
+ };
22
+
23
+ return (
24
+ <Screen data-testid={testID || 'PurchaseConfirmationScreen'}>
25
+ <div className="container mx-auto px-4 py-16">
26
+ <div className="mx-auto max-w-2xl">
27
+ <div className="rounded-lg border border-border bg-card p-8 text-center shadow-lg">
28
+ {/* Success Icon */}
29
+ <div className="mb-6 flex justify-center">
30
+ <CheckCircle className="h-20 w-20 text-green-500" />
31
+ </div>
32
+
33
+ {/* Title */}
34
+ <h1 className="mb-4 text-3xl font-bold">{title}</h1>
35
+
36
+ {/* Message */}
37
+ <p className="mb-8 text-lg text-muted-foreground">{message}</p>
38
+
39
+ {/* Order Details */}
40
+ {orderDetails && (
41
+ <div className="mb-8 rounded-md bg-muted p-6 text-left">
42
+ <h2 className="mb-4 font-semibold">Order Details</h2>
43
+
44
+ {orderDetails.orderId && (
45
+ <div className="mb-2 flex justify-between text-sm">
46
+ <span className="text-muted-foreground">Order ID:</span>
47
+ <span className="font-medium">{orderDetails.orderId}</span>
48
+ </div>
49
+ )}
50
+
51
+ {orderDetails.orderDate && (
52
+ <div className="mb-2 flex justify-between text-sm">
53
+ <span className="text-muted-foreground">Order Date:</span>
54
+ <span className="font-medium">{orderDetails.orderDate}</span>
55
+ </div>
56
+ )}
57
+
58
+ {orderDetails.items && orderDetails.items.length > 0 && (
59
+ <div className="my-4 border-t border-border pt-4">
60
+ <h3 className="mb-2 text-sm font-semibold">Items:</h3>
61
+ {orderDetails.items.map((item, index) => (
62
+ <div key={index} className="mb-2 flex justify-between text-sm">
63
+ <span>
64
+ {item.name} x {item.quantity}
65
+ </span>
66
+ <span className="font-medium">
67
+ {formatPrice(item.price * item.quantity, orderDetails.currency)}
68
+ </span>
69
+ </div>
70
+ ))}
71
+ </div>
72
+ )}
73
+
74
+ {orderDetails.total !== undefined && (
75
+ <div className="mt-4 flex justify-between border-t border-border pt-4 text-base font-bold">
76
+ <span>Total:</span>
77
+ <span>{formatPrice(orderDetails.total, orderDetails.currency)}</span>
78
+ </div>
79
+ )}
80
+
81
+ {orderDetails.estimatedDelivery && (
82
+ <div className="mt-4 text-sm text-muted-foreground">
83
+ Estimated Delivery: {orderDetails.estimatedDelivery}
84
+ </div>
85
+ )}
86
+ </div>
87
+ )}
88
+
89
+ {/* Information Box */}
90
+ <div className="mb-8 rounded-md bg-muted p-6 text-left">
91
+ <h2 className="mb-3 font-semibold">What happens next?</h2>
92
+ <ul className="space-y-2 text-sm text-muted-foreground">
93
+ <li>✓ You'll receive an order confirmation email shortly</li>
94
+ <li>✓ Your order will be processed and shipped within 3-5 business days</li>
95
+ <li>✓ You'll receive a tracking number once your order ships</li>
96
+ <li>✓ If you have any questions, contact support</li>
97
+ </ul>
98
+ </div>
99
+
100
+ {/* Action Buttons */}
101
+ <div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
102
+ {onContinueShopping && (
103
+ <ShadcnButton
104
+ size="lg"
105
+ onClick={onContinueShopping}
106
+ className="w-full sm:w-auto"
107
+ >
108
+ Continue Shopping
109
+ </ShadcnButton>
110
+ )}
111
+
112
+ {onViewOrders && (
113
+ <ShadcnButton
114
+ size="lg"
115
+ variant="outline"
116
+ onClick={onViewOrders}
117
+ className="w-full sm:w-auto"
118
+ >
119
+ View Orders
120
+ </ShadcnButton>
121
+ )}
122
+
123
+ {onGoToDashboard && (
124
+ <ShadcnButton
125
+ size="lg"
126
+ variant="outline"
127
+ onClick={onGoToDashboard}
128
+ className="w-full sm:w-auto"
129
+ >
130
+ Go to Dashboard
131
+ </ShadcnButton>
132
+ )}
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </Screen>
138
+ );
139
+ };
140
+
141
+ PurchaseConfirmationScreen.displayName = 'PurchaseConfirmationScreen';
142
+
143
+ export default PurchaseConfirmationScreen;
144
+ export type { Props };
@@ -0,0 +1,30 @@
1
+ export interface OrderDetails {
2
+ orderId?: string;
3
+ orderDate?: string;
4
+ items?: Array<{
5
+ name: string;
6
+ quantity: number;
7
+ price: number;
8
+ }>;
9
+ total?: number;
10
+ currency?: string;
11
+ shippingAddress?: {
12
+ line1: string;
13
+ line2?: string;
14
+ city: string;
15
+ state: string;
16
+ postal_code: string;
17
+ country: string;
18
+ };
19
+ estimatedDelivery?: string;
20
+ }
21
+
22
+ export interface Props {
23
+ testID?: string;
24
+ title?: string;
25
+ message?: string;
26
+ orderDetails?: OrderDetails;
27
+ onContinueShopping?: () => void;
28
+ onViewOrders?: () => void;
29
+ onGoToDashboard?: () => void;
30
+ }
@@ -0,0 +1 @@
1
+ export { default } from './PurchaseConfirmationScreen';
@@ -11,11 +11,14 @@ export { default as HomeScreen } from './HomeScreen';
11
11
  export { default as LogoutScreen } from './LogoutScreen';
12
12
  export { default as MenuScreen } from './MenuScreen';
13
13
  export { default as NotFoundScreen } from './NotFoundScreen';
14
+ export { default as OrderDetailScreen } from './OrderDetailScreen';
15
+ export { default as OrdersHistoryScreen } from './OrdersHistoryScreen';
14
16
  export { default as PasswordResetScreen } from './PasswordResetScreen';
15
17
  export { default as PasswordResetAuthScreen } from './PasswordResetAuthScreen';
16
18
  export { default as ProductDetailScreen } from './ProductDetailScreen';
17
19
  export { default as ProductListScreen } from './ProductListScreen';
18
20
  export { default as ProfileScreen } from './ProfileScreen';
21
+ export { default as PurchaseConfirmationScreen } from './PurchaseConfirmationScreen';
19
22
  export { default as ScheduleScreen } from './ScheduleScreen';
20
23
  export { default as SubscriptionScreen } from './SubscriptionScreen';
21
24
  export { default as WorkoutScreen } from './WorkoutScreen';
@@ -34,11 +37,14 @@ export type { Props as HomeScreenProps } from './HomeScreen/HomeScreen.types';
34
37
  export type { Props as LogoutScreenProps } from './LogoutScreen/LogoutScreen.types';
35
38
  export type { Props as MenuScreenProps } from './MenuScreen/MenuScreen.types';
36
39
  export type { Props as NotFoundScreenProps } from './NotFoundScreen/NotFoundScreen.types';
40
+ export type { OrderDetailScreenProps, OrderDetail } from './OrderDetailScreen';
41
+ export type { OrdersHistoryScreenProps, Order } from './OrdersHistoryScreen';
37
42
  export type { Props as PasswordResetScreenProps } from './PasswordResetScreen/PasswordResetScreen.types';
38
43
  export type { Props as PasswordResetAuthScreenProps } from './PasswordResetAuthScreen/PasswordResetAuthScreen.types';
39
44
  export type { Props as ProductDetailScreenProps } from './ProductDetailScreen/ProductDetailScreen.types';
40
45
  export type { Props as ProductListScreenProps } from './ProductListScreen/ProductListScreen.types';
41
46
  export type { Props as ProfileScreenProps } from './ProfileScreen/ProfileScreen.types';
47
+ export type { Props as PurchaseConfirmationScreenProps } from './PurchaseConfirmationScreen/PurchaseConfirmationScreen.types';
42
48
  export type { Props as ScheduleScreenProps } from './ScheduleScreen/ScheduleScreen.types';
43
49
  export type { Props as SubscriptionScreenProps } from './SubscriptionScreen/SubscriptionScreen.types';
44
50
  export type { Props as WorkoutScreenProps } from './WorkoutScreen/WorkoutScreen.types';