@digilogiclabs/create-saas-app 1.5.4 → 1.6.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/CHANGELOG.md +109 -49
- package/bin/index.js +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/templates/mobile/base/template/App.tsx +113 -23
- package/dist/templates/mobile/base/template/package.json +5 -2
- package/dist/templates/web/base/template/package.json +1 -1
- package/dist/templates/web/base/template/src/app/checkout/page.tsx +99 -8
- package/dist/templates/web/base/template/src/app/dashboard/page.tsx +309 -0
- package/dist/templates/web/base/template/src/app/globals.css +97 -0
- package/dist/templates/web/base/template/src/app/login/page.tsx +36 -8
- package/dist/templates/web/base/template/src/app/page.tsx +123 -83
- package/dist/templates/web/base/template/src/app/signup/page.tsx +36 -8
- package/dist/templates/web/base/template/src/components/shared/header.tsx +49 -30
- package/dist/templates/web/ui-auth/template/package.json +2 -2
- package/dist/templates/web/ui-auth/template/src/app/page.tsx +203 -61
- package/dist/templates/web/ui-auth-payments/template/package.json +2 -2
- package/dist/templates/web/ui-auth-payments/template/src/app/checkout/page.tsx +253 -87
- package/dist/templates/web/ui-auth-payments/template/src/app/globals.css +129 -0
- package/dist/templates/web/ui-auth-payments/template/src/app/page.tsx +246 -74
- package/dist/templates/web/ui-auth-payments/template/src/components/shared/header.tsx +106 -40
- package/dist/templates/web/ui-auth-payments-audio/template/package.json +2 -2
- package/dist/templates/web/ui-auth-payments-audio/template/src/app/page.tsx +221 -82
- package/dist/templates/web/ui-auth-payments-audio/template/src/components/shared/header.tsx +132 -40
- package/dist/templates/web/ui-auth-payments-video/template/package.json +2 -2
- package/dist/templates/web/ui-auth-payments-video/template/src/app/globals.css +146 -0
- package/dist/templates/web/ui-auth-payments-video/template/src/app/page.tsx +259 -85
- package/dist/templates/web/ui-auth-payments-video/template/src/components/shared/header.tsx +133 -41
- package/package.json +106 -106
- package/src/templates/mobile/base/template/App.tsx +113 -23
- package/src/templates/mobile/base/template/package.json +5 -2
- package/src/templates/web/base/template/package.json +1 -1
- package/src/templates/web/base/template/src/app/checkout/page.tsx +99 -8
- package/src/templates/web/base/template/src/app/dashboard/page.tsx +309 -0
- package/src/templates/web/base/template/src/app/globals.css +97 -0
- package/src/templates/web/base/template/src/app/login/page.tsx +36 -8
- package/src/templates/web/base/template/src/app/page.tsx +123 -83
- package/src/templates/web/base/template/src/app/signup/page.tsx +36 -8
- package/src/templates/web/base/template/src/components/shared/header.tsx +49 -30
- package/src/templates/web/ui-auth/template/package.json +2 -2
- package/src/templates/web/ui-auth/template/src/app/page.tsx +203 -61
- package/src/templates/web/ui-auth-payments/template/package.json +2 -2
- package/src/templates/web/ui-auth-payments/template/src/app/checkout/page.tsx +253 -87
- package/src/templates/web/ui-auth-payments/template/src/app/globals.css +129 -0
- package/src/templates/web/ui-auth-payments/template/src/app/page.tsx +246 -74
- package/src/templates/web/ui-auth-payments/template/src/components/shared/header.tsx +106 -40
- package/src/templates/web/ui-auth-payments-audio/template/package.json +2 -2
- package/src/templates/web/ui-auth-payments-audio/template/src/app/page.tsx +221 -82
- package/src/templates/web/ui-auth-payments-audio/template/src/components/shared/header.tsx +132 -40
- package/src/templates/web/ui-auth-payments-video/template/package.json +2 -2
- package/src/templates/web/ui-auth-payments-video/template/src/app/globals.css +146 -0
- package/src/templates/web/ui-auth-payments-video/template/src/app/page.tsx +259 -85
- package/src/templates/web/ui-auth-payments-video/template/src/components/shared/header.tsx +133 -41
- package/dist/index.js +0 -1173
- package/dist/index.js.map +0 -1
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
4
|
import { usePayments, formatCurrency } from '@digilogiclabs/saas-factory-payments';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Button,
|
|
7
|
+
Card,
|
|
8
|
+
PageTransition,
|
|
9
|
+
MobileContainer,
|
|
10
|
+
ResponsiveGrid,
|
|
11
|
+
SwipeableCard,
|
|
12
|
+
NetworkAwareContent,
|
|
13
|
+
OfflineWrapper,
|
|
14
|
+
TouchInput,
|
|
15
|
+
MobileForm,
|
|
16
|
+
useNetworkInfo,
|
|
17
|
+
useOfflineState
|
|
18
|
+
} from '@digilogiclabs/saas-factory-ui';
|
|
19
|
+
import { CreditCard, Check, Star, Wifi, WifiOff, ArrowLeft, ArrowRight } from 'lucide-react';
|
|
6
20
|
|
|
7
21
|
export default function CheckoutPage() {
|
|
8
22
|
const { loading } = usePayments();
|
|
23
|
+
const networkInfo = useNetworkInfo();
|
|
24
|
+
const isOnline = useOfflineState();
|
|
25
|
+
const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
|
|
9
26
|
|
|
10
27
|
const handlePlanSelect = (planId: string) => {
|
|
11
28
|
console.log('Plan selected:', planId);
|
|
29
|
+
setSelectedPlan(planId);
|
|
12
30
|
// Handle plan selection - redirect to payment processing
|
|
13
31
|
};
|
|
14
32
|
|
|
@@ -47,96 +65,244 @@ export default function CheckoutPage() {
|
|
|
47
65
|
];
|
|
48
66
|
|
|
49
67
|
return (
|
|
50
|
-
<
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
<PageTransition type="slide" direction="up" duration={350}>
|
|
69
|
+
<OfflineWrapper
|
|
70
|
+
cacheStrategy="cache-first"
|
|
71
|
+
showOfflineIndicator={true}
|
|
72
|
+
>
|
|
73
|
+
<div className="min-h-screen bg-gray-100">
|
|
74
|
+
<MobileContainer className="py-8">
|
|
75
|
+
{/* Network Status */}
|
|
76
|
+
<div className="flex justify-center mb-4">
|
|
77
|
+
<div className="flex items-center gap-2 text-sm px-3 py-1 rounded-full bg-white shadow-sm">
|
|
78
|
+
{isOnline ? (
|
|
79
|
+
<>
|
|
80
|
+
<Wifi className="w-4 h-4 text-green-600" />
|
|
81
|
+
<span>Secure Connection</span>
|
|
82
|
+
{networkInfo?.effectiveType && (
|
|
83
|
+
<span className="bg-green-100 text-green-800 px-2 py-1 rounded text-xs">
|
|
84
|
+
{networkInfo.effectiveType.toUpperCase()}
|
|
85
|
+
</span>
|
|
86
|
+
)}
|
|
87
|
+
</>
|
|
88
|
+
) : (
|
|
89
|
+
<>
|
|
90
|
+
<WifiOff className="w-4 h-4 text-red-600" />
|
|
91
|
+
<span>Offline - Plans cached</span>
|
|
92
|
+
</>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div className="text-center mb-12">
|
|
98
|
+
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">Choose Your Plan</h1>
|
|
99
|
+
<p className="text-lg md:text-xl text-gray-600">Select the perfect plan for your needs</p>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Subscription Plans */}
|
|
103
|
+
<NetworkAwareContent
|
|
104
|
+
showOnSlow={
|
|
105
|
+
<ResponsiveGrid columns={{ base: 1, md: 2 }} gap="lg" className="mb-8">
|
|
106
|
+
{plans.slice(0, 2).map((plan) => (
|
|
107
|
+
<Card key={plan.id} className={`p-6 relative ${plan.popular ? 'ring-2 ring-blue-500' : ''}`}>
|
|
108
|
+
{plan.popular && (
|
|
109
|
+
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
|
|
110
|
+
<span className="bg-blue-500 text-white px-3 py-1 rounded-full text-sm font-medium flex items-center gap-1">
|
|
111
|
+
<Star className="w-3 h-3" />
|
|
112
|
+
Most Popular
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
<h3 className="text-xl font-bold mb-2">{plan.name}</h3>
|
|
117
|
+
<div className="text-3xl font-bold mb-4">
|
|
118
|
+
{formatCurrency(plan.price)}
|
|
119
|
+
<span className="text-sm font-normal">/{plan.interval}</span>
|
|
120
|
+
</div>
|
|
121
|
+
<ul className="mb-6 space-y-2">
|
|
122
|
+
{plan.features.slice(0, 3).map((feature, index) => (
|
|
123
|
+
<li key={index} className="flex items-center">
|
|
124
|
+
<Check className="text-green-500 mr-2 w-4 h-4" />
|
|
125
|
+
{feature}
|
|
126
|
+
</li>
|
|
127
|
+
))}
|
|
128
|
+
</ul>
|
|
129
|
+
<Button
|
|
130
|
+
onClick={() => handlePlanSelect(plan.id)}
|
|
131
|
+
disabled={loading}
|
|
132
|
+
className="w-full"
|
|
133
|
+
variant={plan.popular ? "default" : "outline"}
|
|
134
|
+
size="lg"
|
|
135
|
+
>
|
|
136
|
+
{loading ? 'Processing...' : 'Select Plan'}
|
|
137
|
+
</Button>
|
|
138
|
+
</Card>
|
|
139
|
+
))}
|
|
140
|
+
</ResponsiveGrid>
|
|
141
|
+
}
|
|
142
|
+
>
|
|
143
|
+
<ResponsiveGrid columns={{ base: 1, md: 3 }} gap="lg" className="mb-8">
|
|
59
144
|
{plans.map((plan) => (
|
|
60
|
-
<
|
|
61
|
-
{plan.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
145
|
+
<SwipeableCard
|
|
146
|
+
key={plan.id}
|
|
147
|
+
leftActions={[
|
|
148
|
+
{
|
|
149
|
+
id: 'info',
|
|
150
|
+
label: 'More Info',
|
|
151
|
+
onAction: () => console.log('Show plan details'),
|
|
152
|
+
color: 'blue'
|
|
153
|
+
}
|
|
154
|
+
]}
|
|
155
|
+
rightActions={[
|
|
156
|
+
{
|
|
157
|
+
id: 'select',
|
|
158
|
+
label: 'Quick Select',
|
|
159
|
+
onAction: () => handlePlanSelect(plan.id),
|
|
160
|
+
color: 'green',
|
|
161
|
+
icon: ArrowRight
|
|
162
|
+
}
|
|
163
|
+
]}
|
|
164
|
+
threshold={60}
|
|
165
|
+
hapticFeedback={true}
|
|
166
|
+
showActionLabels={true}
|
|
167
|
+
>
|
|
168
|
+
<Card className={`p-6 relative ${plan.popular ? 'ring-2 ring-blue-500' : ''} ${selectedPlan === plan.id ? 'bg-blue-50 border-blue-200' : ''}`}>
|
|
169
|
+
{plan.popular && (
|
|
170
|
+
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
|
|
171
|
+
<span className="bg-blue-500 text-white px-3 py-1 rounded-full text-sm font-medium flex items-center gap-1">
|
|
172
|
+
<Star className="w-3 h-3" />
|
|
173
|
+
Most Popular
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
<h3 className="text-xl font-bold mb-2">{plan.name}</h3>
|
|
178
|
+
<div className="text-3xl font-bold mb-4">
|
|
179
|
+
{formatCurrency(plan.price)}
|
|
180
|
+
<span className="text-sm font-normal">/{plan.interval}</span>
|
|
66
181
|
</div>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
className="
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
</Card>
|
|
182
|
+
<ul className="mb-6 space-y-2">
|
|
183
|
+
{plan.features.map((feature, index) => (
|
|
184
|
+
<li key={index} className="flex items-center">
|
|
185
|
+
<Check className="text-green-500 mr-2 w-4 h-4" />
|
|
186
|
+
{feature}
|
|
187
|
+
</li>
|
|
188
|
+
))}
|
|
189
|
+
</ul>
|
|
190
|
+
<Button
|
|
191
|
+
onClick={() => handlePlanSelect(plan.id)}
|
|
192
|
+
disabled={loading}
|
|
193
|
+
className="w-full"
|
|
194
|
+
variant={selectedPlan === plan.id ? "default" : plan.popular ? "default" : "outline"}
|
|
195
|
+
size="lg"
|
|
196
|
+
>
|
|
197
|
+
{loading ? 'Processing...' : selectedPlan === plan.id ? 'Selected' : 'Select Plan'}
|
|
198
|
+
</Button>
|
|
199
|
+
<p className="text-xs text-center text-gray-500 mt-2">
|
|
200
|
+
Swipe left for details • Swipe right to select
|
|
201
|
+
</p>
|
|
202
|
+
</Card>
|
|
203
|
+
</SwipeableCard>
|
|
90
204
|
))}
|
|
91
|
-
|
|
205
|
+
</ResponsiveGrid>
|
|
206
|
+
</NetworkAwareContent>
|
|
92
207
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<label className="block text-sm font-medium mb-2">CVC</label>
|
|
118
|
-
<input
|
|
119
|
-
type="text"
|
|
120
|
-
placeholder="123"
|
|
121
|
-
className="w-full p-3 border rounded-lg"
|
|
122
|
-
disabled
|
|
123
|
-
/>
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
<Button
|
|
127
|
-
onClick={handlePayment}
|
|
128
|
-
disabled={loading}
|
|
129
|
-
className="w-full"
|
|
208
|
+
{/* Mobile-Optimized Payment Form */}
|
|
209
|
+
<SwipeableCard
|
|
210
|
+
leftActions={[
|
|
211
|
+
{
|
|
212
|
+
id: 'back',
|
|
213
|
+
label: 'Back',
|
|
214
|
+
onAction: () => window.history.back(),
|
|
215
|
+
color: 'gray',
|
|
216
|
+
icon: ArrowLeft
|
|
217
|
+
}
|
|
218
|
+
]}
|
|
219
|
+
rightActions={[
|
|
220
|
+
{
|
|
221
|
+
id: 'pay',
|
|
222
|
+
label: 'Pay Now',
|
|
223
|
+
onAction: handlePayment,
|
|
224
|
+
color: 'green',
|
|
225
|
+
icon: CreditCard
|
|
226
|
+
}
|
|
227
|
+
]}
|
|
228
|
+
threshold={60}
|
|
229
|
+
hapticFeedback={true}
|
|
230
|
+
showActionLabels={true}
|
|
231
|
+
className="max-w-md mx-auto"
|
|
130
232
|
>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
233
|
+
<Card className="p-6">
|
|
234
|
+
<div className="flex items-center gap-3 mb-6">
|
|
235
|
+
<div className="p-2 bg-blue-100 rounded-full">
|
|
236
|
+
<CreditCard className="w-5 h-5 text-blue-600" />
|
|
237
|
+
</div>
|
|
238
|
+
<h2 className="text-2xl font-bold">Payment Details</h2>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<MobileForm className="space-y-4">
|
|
242
|
+
<div>
|
|
243
|
+
<label className="block text-sm font-medium mb-2">Card Number</label>
|
|
244
|
+
<TouchInput
|
|
245
|
+
type="text"
|
|
246
|
+
placeholder="1234 5678 9012 3456"
|
|
247
|
+
disabled
|
|
248
|
+
mobileOptimized={true}
|
|
249
|
+
hapticFeedback={true}
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
<ResponsiveGrid columns={{ base: 2 }} gap="md">
|
|
253
|
+
<div>
|
|
254
|
+
<label className="block text-sm font-medium mb-2">Expiry Date</label>
|
|
255
|
+
<TouchInput
|
|
256
|
+
type="text"
|
|
257
|
+
placeholder="MM/YY"
|
|
258
|
+
disabled
|
|
259
|
+
mobileOptimized={true}
|
|
260
|
+
hapticFeedback={true}
|
|
261
|
+
/>
|
|
262
|
+
</div>
|
|
263
|
+
<div>
|
|
264
|
+
<label className="block text-sm font-medium mb-2">CVC</label>
|
|
265
|
+
<TouchInput
|
|
266
|
+
type="text"
|
|
267
|
+
placeholder="123"
|
|
268
|
+
disabled
|
|
269
|
+
mobileOptimized={true}
|
|
270
|
+
hapticFeedback={true}
|
|
271
|
+
/>
|
|
272
|
+
</div>
|
|
273
|
+
</ResponsiveGrid>
|
|
274
|
+
|
|
275
|
+
{selectedPlan && (
|
|
276
|
+
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
|
|
277
|
+
<div className="flex items-center justify-between">
|
|
278
|
+
<span className="font-medium">Selected Plan:</span>
|
|
279
|
+
<span className="text-blue-600 font-bold">
|
|
280
|
+
{plans.find(p => p.id === selectedPlan)?.name}
|
|
281
|
+
</span>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
|
|
286
|
+
<Button
|
|
287
|
+
onClick={handlePayment}
|
|
288
|
+
disabled={loading || !isOnline}
|
|
289
|
+
className="w-full"
|
|
290
|
+
size="lg"
|
|
291
|
+
>
|
|
292
|
+
{loading ? 'Processing...' : !isOnline ? 'Offline - Cannot Process' : 'Complete Payment'}
|
|
293
|
+
</Button>
|
|
294
|
+
|
|
295
|
+
<div className="space-y-2 text-sm text-gray-600 text-center">
|
|
296
|
+
<p>This is a demo. Payment processing requires Stripe integration.</p>
|
|
297
|
+
<p className="text-xs">Swipe left to go back • Swipe right to pay now</p>
|
|
298
|
+
</div>
|
|
299
|
+
</MobileForm>
|
|
300
|
+
</Card>
|
|
301
|
+
</SwipeableCard>
|
|
302
|
+
</MobileContainer>
|
|
303
|
+
</div>
|
|
304
|
+
</OfflineWrapper>
|
|
305
|
+
</PageTransition>
|
|
140
306
|
);
|
|
141
307
|
}
|
|
142
308
|
|
|
@@ -40,3 +40,132 @@
|
|
|
40
40
|
@apply bg-background text-foreground;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
/* Mobile-first optimizations */
|
|
45
|
+
@media (max-width: 768px) {
|
|
46
|
+
.mobile-optimized {
|
|
47
|
+
touch-action: manipulation;
|
|
48
|
+
-webkit-tap-highlight-color: transparent;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Better touch targets */
|
|
52
|
+
button,
|
|
53
|
+
a,
|
|
54
|
+
input,
|
|
55
|
+
select,
|
|
56
|
+
textarea {
|
|
57
|
+
min-height: 44px;
|
|
58
|
+
min-width: 44px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Smooth scrolling for mobile */
|
|
62
|
+
html {
|
|
63
|
+
scroll-behavior: smooth;
|
|
64
|
+
-webkit-overflow-scrolling: touch;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Animated moving banner */
|
|
69
|
+
@keyframes marquee {
|
|
70
|
+
0% {
|
|
71
|
+
transform: translate3d(100%, 0, 0);
|
|
72
|
+
}
|
|
73
|
+
100% {
|
|
74
|
+
transform: translate3d(-100%, 0, 0);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.animate-marquee {
|
|
79
|
+
animation: marquee 15s linear infinite;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Network-aware styles */
|
|
83
|
+
@media (prefers-reduced-data: reduce) {
|
|
84
|
+
.high-bandwidth-content {
|
|
85
|
+
display: none;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Reduced motion preferences */
|
|
90
|
+
@media (prefers-reduced-motion: reduce) {
|
|
91
|
+
*,
|
|
92
|
+
*::before,
|
|
93
|
+
*::after {
|
|
94
|
+
animation-duration: 0.01ms !important;
|
|
95
|
+
animation-iteration-count: 1 !important;
|
|
96
|
+
transition-duration: 0.01ms !important;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.animate-marquee {
|
|
100
|
+
animation: none;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Hamburger menu animations */
|
|
105
|
+
.hamburger-line {
|
|
106
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
107
|
+
transform-origin: center;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.hamburger-open .hamburger-line:nth-child(1) {
|
|
111
|
+
transform: rotate(45deg) translate(6px, 6px);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.hamburger-open .hamburger-line:nth-child(2) {
|
|
115
|
+
opacity: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.hamburger-open .hamburger-line:nth-child(3) {
|
|
119
|
+
transform: rotate(-45deg) translate(6px, -6px);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Custom animations for mobile interactions */
|
|
123
|
+
@keyframes haptic-feedback {
|
|
124
|
+
0% { transform: scale(1); }
|
|
125
|
+
50% { transform: scale(0.95); }
|
|
126
|
+
100% { transform: scale(1); }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.haptic-feedback {
|
|
130
|
+
animation: haptic-feedback 0.1s ease-in-out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Offline indicator styles */
|
|
134
|
+
.offline-indicator {
|
|
135
|
+
position: fixed;
|
|
136
|
+
top: 0;
|
|
137
|
+
left: 0;
|
|
138
|
+
right: 0;
|
|
139
|
+
background: #f59e0b;
|
|
140
|
+
color: white;
|
|
141
|
+
text-align: center;
|
|
142
|
+
padding: 8px;
|
|
143
|
+
font-size: 14px;
|
|
144
|
+
z-index: 9999;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Loading states */
|
|
148
|
+
.loading-skeleton {
|
|
149
|
+
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|
150
|
+
background-size: 200% 100%;
|
|
151
|
+
animation: loading 1.5s infinite;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@keyframes loading {
|
|
155
|
+
0% {
|
|
156
|
+
background-position: 200% 0;
|
|
157
|
+
}
|
|
158
|
+
100% {
|
|
159
|
+
background-position: -200% 0;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Progressive image loading */
|
|
164
|
+
.progressive-image {
|
|
165
|
+
filter: blur(5px);
|
|
166
|
+
transition: filter 0.3s;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.progressive-image.loaded {
|
|
170
|
+
filter: blur(0);
|
|
171
|
+
}
|