zopassport 0.1.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 (110) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +407 -0
  3. package/app/.env.example +15 -0
  4. package/app/README.md +28 -0
  5. package/app/package.json +24 -0
  6. package/app/reanimated-mock.js +102 -0
  7. package/app/reanimated-mock.jsx +97 -0
  8. package/app/src/App.tsx +331 -0
  9. package/app/src/components/FounderBadge.tsx +26 -0
  10. package/app/src/components/OTPInput.tsx +149 -0
  11. package/app/src/components/PhoneInput.tsx +109 -0
  12. package/app/src/components/ZoAuth.tsx +320 -0
  13. package/app/src/components/ZoAvatar.tsx +87 -0
  14. package/app/src/components/ZoLanding.tsx +231 -0
  15. package/app/src/components/ZoOnboarding.tsx +524 -0
  16. package/app/src/components/ZoPassportCard.tsx +183 -0
  17. package/app/src/components/ZoProgressRing.tsx +57 -0
  18. package/app/src/components/index.ts +16 -0
  19. package/app/src/components/wallet/MovingShine.tsx +43 -0
  20. package/app/src/components/wallet/TransactionItem.tsx +84 -0
  21. package/app/src/components/wallet/TransactionList.tsx +65 -0
  22. package/app/src/components/wallet/WalletCard.tsx +152 -0
  23. package/app/src/components/wallet/WalletScreen.tsx +190 -0
  24. package/app/src/components/wallet/ZoToken.tsx +69 -0
  25. package/app/src/components/wallet/index.ts +8 -0
  26. package/app/src/components/wallet/styles/index.ts +4 -0
  27. package/app/src/components/wallet/styles/walletStyles.ts +210 -0
  28. package/app/src/sdk/ZoPassportSDK.ts +277 -0
  29. package/app/src/sdk/lib/api/auth.ts +223 -0
  30. package/app/src/sdk/lib/api/avatar.ts +155 -0
  31. package/app/src/sdk/lib/api/client.ts +135 -0
  32. package/app/src/sdk/lib/api/index.ts +8 -0
  33. package/app/src/sdk/lib/api/profile.ts +80 -0
  34. package/app/src/sdk/lib/api/wallet.ts +59 -0
  35. package/app/src/sdk/lib/types/auth.ts +78 -0
  36. package/app/src/sdk/lib/types/avatar.ts +22 -0
  37. package/app/src/sdk/lib/types/index.ts +8 -0
  38. package/app/src/sdk/lib/types/profile.ts +18 -0
  39. package/app/src/sdk/lib/types/wallet.ts +103 -0
  40. package/app/src/sdk/lib/types.ts +205 -0
  41. package/app/src/sdk/lib/utils/index.ts +6 -0
  42. package/app/src/sdk/lib/utils/phone.ts +71 -0
  43. package/app/src/sdk/lib/utils/storage.ts +116 -0
  44. package/app/src/sdk/lib/utils/wallet.ts +73 -0
  45. package/app/src/sdk/types.ts +205 -0
  46. package/app/src/styles.css +154 -0
  47. package/app/svg-mock.js +125 -0
  48. package/app/svg-mock.jsx +120 -0
  49. package/app/vite.config.ts +70 -0
  50. package/assets/ASSETS_MANIFEST.md +124 -0
  51. package/assets/bae.png +0 -0
  52. package/assets/bro.png +0 -0
  53. package/assets/cultural-stickers/Business.png +0 -0
  54. package/assets/cultural-stickers/Default (2).jpg +0 -0
  55. package/assets/cultural-stickers/Design.png +0 -0
  56. package/assets/cultural-stickers/FollowYourHeart.png +0 -0
  57. package/assets/cultural-stickers/Food.png +0 -0
  58. package/assets/cultural-stickers/Game.png +0 -0
  59. package/assets/cultural-stickers/Health&Fitness.png +0 -0
  60. package/assets/cultural-stickers/Home&Lifestyle.png +0 -0
  61. package/assets/cultural-stickers/Law.png +0 -0
  62. package/assets/cultural-stickers/Literature&Stories.png +0 -0
  63. package/assets/cultural-stickers/Music&Entertainment.png +0 -0
  64. package/assets/cultural-stickers/Nature&Wildlife.png +0 -0
  65. package/assets/cultural-stickers/Photography.png +0 -0
  66. package/assets/cultural-stickers/Science&Technology.png +0 -0
  67. package/assets/cultural-stickers/Spiritual.png +0 -0
  68. package/assets/cultural-stickers/Sport.png +0 -0
  69. package/assets/cultural-stickers/Stories&Journal.png +0 -0
  70. package/assets/cultural-stickers/Television&Cinema.png +0 -0
  71. package/assets/cultural-stickers/Travel&Adventure.png +0 -0
  72. package/assets/cultural-stickers/z.jpg (1).jpg +0 -0
  73. package/assets/figma-assets/landing-zo-logo.png +0 -0
  74. package/assets/images/rank1.jpeg +0 -0
  75. package/assets/index.ts +76 -0
  76. package/assets/lotties/loader.json +1216 -0
  77. package/assets/lotties/spinner.json +1 -0
  78. package/assets/videos/loading-screen-background.mp4 +0 -0
  79. package/assets/videos/opening-disks.mp4 +0 -0
  80. package/assets/wallet/constants.ts +38 -0
  81. package/assets/zo-coin.gif +0 -0
  82. package/assets/zo-fallback.png +0 -0
  83. package/dist/assets/index.d.mts +136 -0
  84. package/dist/assets/index.d.ts +136 -0
  85. package/dist/assets/index.js +133 -0
  86. package/dist/assets/index.js.map +1 -0
  87. package/dist/assets/index.mjs +100 -0
  88. package/dist/assets/index.mjs.map +1 -0
  89. package/dist/index.d.mts +789 -0
  90. package/dist/index.d.ts +789 -0
  91. package/dist/index.js +1118 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/index.mjs +1060 -0
  94. package/dist/index.mjs.map +1 -0
  95. package/dist/react-native.d.mts +537 -0
  96. package/dist/react-native.d.ts +537 -0
  97. package/dist/react-native.js +1617 -0
  98. package/dist/react-native.js.map +1 -0
  99. package/dist/react-native.mjs +1588 -0
  100. package/dist/react-native.mjs.map +1 -0
  101. package/dist/react.d.mts +824 -0
  102. package/dist/react.d.ts +824 -0
  103. package/dist/react.js +3856 -0
  104. package/dist/react.js.map +1 -0
  105. package/dist/react.mjs +3801 -0
  106. package/dist/react.mjs.map +1 -0
  107. package/package.json +112 -0
  108. package/scripts/init.js +196 -0
  109. package/scripts/postinstall.js +174 -0
  110. package/scripts/verify-build.js +121 -0
@@ -0,0 +1,524 @@
1
+ // src/components/ZoOnboarding.tsx
2
+ // Onboarding flow: nickname, body type, location, avatar generation
3
+
4
+ import React, { useState, useEffect, useRef } from 'react';
5
+
6
+ export interface ZoOnboardingProps {
7
+ /** Callback when onboarding is complete */
8
+ onComplete: (userData: OnboardingData) => void;
9
+ /** Update profile function (from SDK) */
10
+ updateProfile: (updates: { first_name?: string; body_type?: 'bro' | 'bae'; place_name?: string }) => Promise<{ success: boolean; error?: string }>;
11
+ /** Get profile function (from SDK) to poll for avatar */
12
+ getProfile: () => Promise<any>;
13
+ /** Video background URL */
14
+ videoUrl?: string;
15
+ /** Zo logo URL */
16
+ logoUrl?: string;
17
+ /** Bro avatar preview URL */
18
+ broAvatarUrl?: string;
19
+ /** Bae avatar preview URL */
20
+ baeAvatarUrl?: string;
21
+ /** Additional CSS class */
22
+ className?: string;
23
+ }
24
+
25
+ export interface OnboardingData {
26
+ nickname: string;
27
+ bodyType: 'bro' | 'bae';
28
+ city: string;
29
+ avatarUrl: string | null;
30
+ }
31
+
32
+ type Step = 'input' | 'generating' | 'success';
33
+
34
+ export const ZoOnboarding: React.FC<ZoOnboardingProps> = ({
35
+ onComplete,
36
+ updateProfile,
37
+ getProfile,
38
+ videoUrl = '/videos/loading-screen-background.mp4',
39
+ logoUrl = '/figma-assets/landing-zo-logo.png',
40
+ broAvatarUrl = '/bro.png',
41
+ baeAvatarUrl = '/bae.png',
42
+ className = '',
43
+ }) => {
44
+ // State
45
+ const [step, setStep] = useState<Step>('input');
46
+ const [nickname, setNickname] = useState('');
47
+ const [bodyType, setBodyType] = useState<'bro' | 'bae'>('bro');
48
+ const [city, setCity] = useState('');
49
+ const [locationEnabled, setLocationEnabled] = useState(false);
50
+ const [isLoadingLocation, setIsLoadingLocation] = useState(false);
51
+ const [isSaving, setIsSaving] = useState(false);
52
+ const [error, setError] = useState('');
53
+ const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
54
+
55
+ // Refs for polling
56
+ const pollingRef = useRef<ReturnType<typeof setTimeout> | null>(null);
57
+ const attemptsRef = useRef(0);
58
+
59
+ // Validation
60
+ const isNicknameValid = nickname.length >= 4 && nickname.length <= 16 && /^[a-z0-9]*$/.test(nickname);
61
+ const canSubmit = isNicknameValid && locationEnabled && bodyType && !isSaving;
62
+
63
+ // Cleanup polling on unmount
64
+ useEffect(() => {
65
+ return () => {
66
+ if (pollingRef.current) clearTimeout(pollingRef.current);
67
+ };
68
+ }, []);
69
+
70
+ // Handle nickname change
71
+ const handleNicknameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
72
+ setNickname(e.target.value.toLowerCase());
73
+ setError('');
74
+ };
75
+
76
+ // Handle location enable
77
+ const handleLocationEnable = () => {
78
+ if ('geolocation' in navigator) {
79
+ setIsLoadingLocation(true);
80
+ navigator.geolocation.getCurrentPosition(
81
+ async (position) => {
82
+ try {
83
+ const response = await fetch(
84
+ `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${position.coords.latitude}&longitude=${position.coords.longitude}&localityLanguage=en`
85
+ );
86
+ const data = await response.json();
87
+ const detectedCity = data.city || data.locality || data.principalSubdivision || 'Unknown City';
88
+
89
+ setCity(detectedCity);
90
+ setLocationEnabled(true);
91
+ setIsLoadingLocation(false);
92
+ setError('');
93
+ } catch (err) {
94
+ console.error('Failed to get city:', err);
95
+ setError('Failed to detect location. Please try again.');
96
+ setLocationEnabled(false);
97
+ setIsLoadingLocation(false);
98
+ }
99
+ },
100
+ (err) => {
101
+ console.error('Location error:', err);
102
+ setError('Location access denied. Please enable permissions.');
103
+ setLocationEnabled(false);
104
+ setIsLoadingLocation(false);
105
+ }
106
+ );
107
+ } else {
108
+ setError('Geolocation is not supported by your browser.');
109
+ }
110
+ };
111
+
112
+ // Handle submit
113
+ const handleSubmit = async () => {
114
+ if (!canSubmit) return;
115
+
116
+ setIsSaving(true);
117
+ setError('');
118
+
119
+ try {
120
+ // Update profile via SDK
121
+ const result = await updateProfile({
122
+ first_name: nickname,
123
+ body_type: bodyType,
124
+ place_name: city,
125
+ });
126
+
127
+ if (!result.success) {
128
+ throw new Error(result.error || 'Profile update failed');
129
+ }
130
+
131
+ // Transition to generating step
132
+ setStep('generating');
133
+
134
+ // Start polling for avatar
135
+ pollForAvatar();
136
+ } catch (err: any) {
137
+ console.error('Error saving user:', err);
138
+ setError('Failed to save. Please try again.');
139
+ setIsSaving(false);
140
+ }
141
+ };
142
+
143
+ // Poll for avatar
144
+ const pollForAvatar = async () => {
145
+ attemptsRef.current += 1;
146
+ const maxAttempts = 30;
147
+
148
+ if (attemptsRef.current > maxAttempts) {
149
+ // Timeout - use default avatar
150
+ setAvatarUrl(bodyType === 'bro' ? broAvatarUrl : baeAvatarUrl);
151
+ setStep('success');
152
+ return;
153
+ }
154
+
155
+ try {
156
+ const profile = await getProfile();
157
+
158
+ if (profile?.avatar?.image) {
159
+ setAvatarUrl(profile.avatar.image);
160
+ setTimeout(() => setStep('success'), 1000);
161
+ return;
162
+ }
163
+
164
+ // Poll again in 1s
165
+ pollingRef.current = setTimeout(pollForAvatar, 1000);
166
+ } catch (err) {
167
+ console.error('Polling error:', err);
168
+ pollingRef.current = setTimeout(pollForAvatar, 1000);
169
+ }
170
+ };
171
+
172
+ // Handle complete
173
+ const handleComplete = () => {
174
+ onComplete({
175
+ nickname,
176
+ bodyType,
177
+ city,
178
+ avatarUrl,
179
+ });
180
+ };
181
+
182
+ const containerStyle: React.CSSProperties = {
183
+ position: 'fixed',
184
+ inset: 0,
185
+ display: 'flex',
186
+ flexDirection: 'column',
187
+ backgroundColor: 'black',
188
+ width: '100vw',
189
+ height: '100vh',
190
+ overflow: 'hidden',
191
+ fontFamily: 'Rubik, system-ui, sans-serif',
192
+ };
193
+
194
+ return (
195
+ <div className={`zo-onboarding ${className}`} style={containerStyle}>
196
+ {/* Video Background */}
197
+ <video
198
+ autoPlay
199
+ loop
200
+ muted
201
+ playsInline
202
+ style={{
203
+ position: 'absolute',
204
+ inset: 0,
205
+ width: '100%',
206
+ height: '100%',
207
+ objectFit: 'cover',
208
+ opacity: 0.4,
209
+ zIndex: 0,
210
+ pointerEvents: 'none',
211
+ }}
212
+ >
213
+ <source src={videoUrl} type="video/mp4" />
214
+ </video>
215
+
216
+ {/* Gradient Overlay */}
217
+ <div
218
+ style={{
219
+ position: 'absolute',
220
+ inset: 0,
221
+ background: 'linear-gradient(to bottom, transparent, transparent, black)',
222
+ zIndex: 0,
223
+ pointerEvents: 'none',
224
+ }}
225
+ />
226
+
227
+ {/* Zo Logo */}
228
+ <div style={{ position: 'absolute', left: '24px', top: '40px', width: '40px', height: '40px', zIndex: 50 }}>
229
+ <img src={logoUrl} alt="Zo" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
230
+ </div>
231
+
232
+ {/* Main Container */}
233
+ <div
234
+ style={{
235
+ position: 'relative',
236
+ zIndex: 10,
237
+ width: '100%',
238
+ height: '100%',
239
+ display: 'flex',
240
+ flexDirection: 'column',
241
+ alignItems: 'center',
242
+ overflowY: 'auto',
243
+ paddingTop: '120px',
244
+ paddingBottom: '40px',
245
+ }}
246
+ >
247
+ {/* INPUT STEP */}
248
+ {step === 'input' && (
249
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '100%', maxWidth: '360px', padding: '0 24px' }}>
250
+ {/* Title */}
251
+ <h1
252
+ style={{
253
+ fontFamily: 'Syne, system-ui, sans-serif',
254
+ fontWeight: 800,
255
+ color: 'white',
256
+ textAlign: 'center',
257
+ textTransform: 'uppercase',
258
+ letterSpacing: '0.32px',
259
+ lineHeight: 1.2,
260
+ marginBottom: '24px',
261
+ fontSize: 'clamp(24px, 6vw, 48px)',
262
+ }}
263
+ >
264
+ WHO ARE YOU?
265
+ </h1>
266
+
267
+ {/* Subtitle */}
268
+ <p
269
+ style={{
270
+ color: 'rgba(255, 255, 255, 0.6)',
271
+ textAlign: 'center',
272
+ lineHeight: 1.5,
273
+ marginBottom: '40px',
274
+ fontSize: 'clamp(14px, 2vw, 16px)',
275
+ }}
276
+ >
277
+ A difficult question, I know. We'll get to it.
278
+ <br />
279
+ But let's start with choosing a nick.
280
+ </p>
281
+
282
+ {/* Nickname Input */}
283
+ <input
284
+ type="text"
285
+ value={nickname}
286
+ onChange={handleNicknameChange}
287
+ placeholder="samurai"
288
+ maxLength={16}
289
+ style={{
290
+ width: '100%',
291
+ height: '56px',
292
+ padding: '0 20px',
293
+ backgroundColor: 'black',
294
+ border: '1px solid #49494A',
295
+ borderRadius: '12px',
296
+ color: 'white',
297
+ fontSize: '16px',
298
+ marginBottom: '40px',
299
+ }}
300
+ autoFocus
301
+ />
302
+
303
+ {/* Body Type Selection */}
304
+ <div style={{ width: '100%', marginBottom: '32px' }}>
305
+ <p style={{ color: 'rgba(255, 255, 255, 0.8)', fontSize: '14px', textAlign: 'center', marginBottom: '16px' }}>
306
+ Choose your avatar style
307
+ </p>
308
+
309
+ <div style={{ display: 'flex', gap: '16px', justifyContent: 'center' }}>
310
+ {/* Bae Option */}
311
+ <button
312
+ onClick={() => setBodyType('bae')}
313
+ style={{
314
+ display: 'flex',
315
+ flexDirection: 'column',
316
+ alignItems: 'center',
317
+ gap: '8px',
318
+ padding: '16px',
319
+ borderRadius: '16px',
320
+ border: bodyType === 'bae' ? '2px solid #CFFF50' : '2px solid rgba(255, 255, 255, 0.3)',
321
+ backgroundColor: bodyType === 'bae' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(255, 255, 255, 0.2)',
322
+ cursor: 'pointer',
323
+ minWidth: '120px',
324
+ transform: bodyType === 'bae' ? 'scale(1.05)' : 'scale(1)',
325
+ transition: 'all 0.3s',
326
+ }}
327
+ >
328
+ <div style={{ width: '64px', height: '64px', borderRadius: '50%', overflow: 'hidden', backgroundColor: 'rgba(255, 255, 255, 0.1)' }}>
329
+ <img src={baeAvatarUrl} alt="Bae" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
330
+ </div>
331
+ <span style={{ color: 'white', fontSize: '14px', fontWeight: 500 }}>Bae</span>
332
+ </button>
333
+
334
+ {/* Bro Option */}
335
+ <button
336
+ onClick={() => setBodyType('bro')}
337
+ style={{
338
+ display: 'flex',
339
+ flexDirection: 'column',
340
+ alignItems: 'center',
341
+ gap: '8px',
342
+ padding: '16px',
343
+ borderRadius: '16px',
344
+ border: bodyType === 'bro' ? '2px solid #CFFF50' : '2px solid rgba(255, 255, 255, 0.3)',
345
+ backgroundColor: bodyType === 'bro' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(255, 255, 255, 0.2)',
346
+ cursor: 'pointer',
347
+ minWidth: '120px',
348
+ transform: bodyType === 'bro' ? 'scale(1.05)' : 'scale(1)',
349
+ transition: 'all 0.3s',
350
+ }}
351
+ >
352
+ <div style={{ width: '64px', height: '64px', borderRadius: '50%', overflow: 'hidden', backgroundColor: 'rgba(255, 255, 255, 0.1)' }}>
353
+ <img src={broAvatarUrl} alt="Bro" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
354
+ </div>
355
+ <span style={{ color: 'white', fontSize: '14px', fontWeight: 500 }}>Bro</span>
356
+ </button>
357
+ </div>
358
+ </div>
359
+
360
+ {/* Location Button */}
361
+ <div style={{ width: '100%', marginBottom: '24px' }}>
362
+ {!locationEnabled ? (
363
+ <button
364
+ onClick={handleLocationEnable}
365
+ disabled={isLoadingLocation}
366
+ style={{
367
+ width: '100%',
368
+ height: '48px',
369
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
370
+ border: '1px solid rgba(255, 255, 255, 0.2)',
371
+ borderRadius: '12px',
372
+ color: 'rgba(255, 255, 255, 0.8)',
373
+ fontSize: '14px',
374
+ cursor: isLoadingLocation ? 'wait' : 'pointer',
375
+ display: 'flex',
376
+ alignItems: 'center',
377
+ justifyContent: 'center',
378
+ gap: '8px',
379
+ }}
380
+ >
381
+ 📍 {isLoadingLocation ? 'Detecting...' : 'Enable Location'}
382
+ </button>
383
+ ) : (
384
+ <div
385
+ style={{
386
+ width: '100%',
387
+ height: '48px',
388
+ backgroundColor: 'rgba(207, 255, 80, 0.1)',
389
+ border: '1px solid rgba(207, 255, 80, 0.5)',
390
+ borderRadius: '12px',
391
+ color: '#CFFF50',
392
+ fontSize: '14px',
393
+ display: 'flex',
394
+ alignItems: 'center',
395
+ justifyContent: 'center',
396
+ gap: '8px',
397
+ }}
398
+ >
399
+ 📍 <span style={{ color: 'white' }}>{city}</span>
400
+ </div>
401
+ )}
402
+ </div>
403
+
404
+ {/* Submit Button */}
405
+ <button
406
+ onClick={handleSubmit}
407
+ disabled={!canSubmit}
408
+ style={{
409
+ width: '100%',
410
+ height: '56px',
411
+ backgroundColor: 'white',
412
+ border: 'none',
413
+ borderRadius: '12px',
414
+ color: 'black',
415
+ fontSize: '16px',
416
+ fontWeight: 500,
417
+ cursor: canSubmit ? 'pointer' : 'not-allowed',
418
+ opacity: canSubmit ? 1 : 0.5,
419
+ }}
420
+ >
421
+ {isSaving ? 'Processing...' : 'Get Citizenship'}
422
+ </button>
423
+
424
+ {/* Error */}
425
+ {error && (
426
+ <p style={{ color: '#ef4444', fontSize: '14px', marginTop: '16px', textAlign: 'center' }}>
427
+ {error}
428
+ </p>
429
+ )}
430
+ </div>
431
+ )}
432
+
433
+ {/* GENERATING STEP */}
434
+ {step === 'generating' && (
435
+ <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
436
+ <div
437
+ style={{
438
+ width: '200px',
439
+ height: '200px',
440
+ borderRadius: '50%',
441
+ border: '2px solid rgba(255, 255, 255, 0.2)',
442
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
443
+ display: 'flex',
444
+ alignItems: 'center',
445
+ justifyContent: 'center',
446
+ overflow: 'hidden',
447
+ }}
448
+ >
449
+ <img
450
+ src={bodyType === 'bro' ? broAvatarUrl : baeAvatarUrl}
451
+ alt="Generating..."
452
+ style={{
453
+ width: '100%',
454
+ height: '100%',
455
+ objectFit: 'cover',
456
+ opacity: 0.8,
457
+ animation: 'pulse 2s ease-in-out infinite',
458
+ }}
459
+ />
460
+ </div>
461
+ </div>
462
+ )}
463
+
464
+ {/* SUCCESS STEP */}
465
+ {step === 'success' && (
466
+ <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
467
+ {/* Avatar */}
468
+ <div
469
+ style={{
470
+ width: '200px',
471
+ height: '200px',
472
+ borderRadius: '50%',
473
+ border: '4px solid white',
474
+ boxShadow: '0 0 40px rgba(255, 255, 255, 0.6)',
475
+ overflow: 'hidden',
476
+ }}
477
+ >
478
+ <img
479
+ src={avatarUrl || (bodyType === 'bro' ? broAvatarUrl : baeAvatarUrl)}
480
+ alt="Avatar"
481
+ style={{ width: '100%', height: '100%', objectFit: 'cover' }}
482
+ />
483
+ </div>
484
+
485
+ {/* Success Button */}
486
+ <button
487
+ onClick={handleComplete}
488
+ style={{
489
+ position: 'fixed',
490
+ bottom: '40px',
491
+ left: '50%',
492
+ transform: 'translateX(-50%)',
493
+ width: '90%',
494
+ maxWidth: '360px',
495
+ height: '56px',
496
+ backgroundColor: 'white',
497
+ border: 'none',
498
+ borderRadius: '12px',
499
+ color: 'black',
500
+ fontSize: '16px',
501
+ fontWeight: 700,
502
+ textTransform: 'uppercase',
503
+ letterSpacing: '0.05em',
504
+ cursor: 'pointer',
505
+ boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)',
506
+ }}
507
+ >
508
+ Zo Zo Zo! Let's Go
509
+ </button>
510
+ </div>
511
+ )}
512
+ </div>
513
+
514
+ {/* CSS Animation */}
515
+ <style>{`
516
+ @keyframes pulse {
517
+ 0%, 100% { transform: scale(1); opacity: 0.8; }
518
+ 50% { transform: scale(1.05); opacity: 1; }
519
+ }
520
+ `}</style>
521
+ </div>
522
+ );
523
+ };
524
+
@@ -0,0 +1,183 @@
1
+ // src/components/ZoPassportCard.tsx
2
+ // Leather passport card component
3
+
4
+ import React from 'react';
5
+ import { ZoProgressRing } from './ZoProgressRing';
6
+ import { FounderBadge } from './FounderBadge';
7
+ import { ZoAvatar } from './ZoAvatar';
8
+
9
+ // CDN URLs for passport backgrounds
10
+ const FOUNDER_BG = 'https://proxy.cdn.zo.xyz/gallery/media/images/a1659b07-94f0-4490-9b3c-3366715d9717_20250515053726.png';
11
+ const CITIZEN_BG = 'https://proxy.cdn.zo.xyz/gallery/media/images/bda9da5a-eefe-411d-8d90-667c80024463_20250515053805.png';
12
+
13
+ export interface ZoPassportCardProps {
14
+ /** User profile data */
15
+ profile: {
16
+ avatar?: string;
17
+ name?: string;
18
+ isFounder?: boolean;
19
+ };
20
+ /** Profile completion */
21
+ completion: {
22
+ done: number;
23
+ total: number;
24
+ };
25
+ /** Additional CSS class */
26
+ className?: string;
27
+ /** Optional: Override founder background URL */
28
+ founderBgUrl?: string;
29
+ /** Optional: Override citizen background URL */
30
+ citizenBgUrl?: string;
31
+ /** Optional: Default avatar fallback URL */
32
+ defaultAvatarUrl?: string;
33
+ }
34
+
35
+ export const ZoPassportCard: React.FC<ZoPassportCardProps> = ({
36
+ profile,
37
+ completion,
38
+ className = '',
39
+ founderBgUrl = FOUNDER_BG,
40
+ citizenBgUrl = CITIZEN_BG,
41
+ defaultAvatarUrl = '/images/rank1.jpeg',
42
+ }) => {
43
+ const isFounder = profile?.isFounder || false;
44
+ const name = profile?.name || 'New Citizen';
45
+ const avatar = profile?.avatar || defaultAvatarUrl;
46
+ const done = completion?.done || 0;
47
+ const total = completion?.total || 1;
48
+ const progress = Math.min(100, Math.max(0, (done / total) * 100));
49
+
50
+ const bgImage = isFounder ? founderBgUrl : citizenBgUrl;
51
+ const textColor = isFounder ? 'white' : '#111111';
52
+ const shadowStyle = isFounder
53
+ ? '0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 8px 10px -6px rgba(0, 0, 0, 0.1)'
54
+ : '0 20px 25px -5px rgba(241, 86, 63, 0.5), 0 8px 10px -6px rgba(241, 86, 63, 0.1)';
55
+
56
+ return (
57
+ <div
58
+ className={`zo-passport-card ${className}`}
59
+ style={{
60
+ position: 'relative',
61
+ width: '234px',
62
+ height: '300px',
63
+ borderRadius: '0 20px 20px 0',
64
+ overflow: 'hidden',
65
+ fontFamily: 'Rubik, system-ui, sans-serif',
66
+ boxShadow: shadowStyle,
67
+ }}
68
+ >
69
+ {/* Background Image */}
70
+ <div style={{ position: 'absolute', inset: 0 }}>
71
+ <img
72
+ src={bgImage}
73
+ alt="Passport Background"
74
+ style={{
75
+ width: '100%',
76
+ height: '100%',
77
+ objectFit: 'cover',
78
+ }}
79
+ />
80
+ </div>
81
+
82
+ {/* Circular Progress - Centered */}
83
+ <div
84
+ style={{
85
+ position: 'absolute',
86
+ inset: 0,
87
+ display: 'flex',
88
+ alignItems: 'center',
89
+ justifyContent: 'center',
90
+ top: '-10px',
91
+ }}
92
+ >
93
+ <ZoProgressRing
94
+ progress={progress}
95
+ size={140}
96
+ strokeWidth={4}
97
+ primaryColor={isFounder ? '#FFFFFF' : '#111111'}
98
+ secondaryColor={isFounder ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
99
+ />
100
+ </div>
101
+
102
+ {/* Avatar - Centered */}
103
+ <div
104
+ style={{
105
+ position: 'absolute',
106
+ inset: 0,
107
+ display: 'flex',
108
+ alignItems: 'center',
109
+ justifyContent: 'center',
110
+ top: '-10px',
111
+ }}
112
+ >
113
+ <div
114
+ style={{
115
+ width: '120px',
116
+ height: '120px',
117
+ borderRadius: '50%',
118
+ overflow: 'hidden',
119
+ }}
120
+ >
121
+ <ZoAvatar src={avatar} name={name} size={120} />
122
+ </div>
123
+
124
+ {/* Founder Badge */}
125
+ {isFounder && (
126
+ <div
127
+ style={{
128
+ position: 'absolute',
129
+ bottom: '84px',
130
+ right: '60px',
131
+ }}
132
+ >
133
+ <FounderBadge size={32} />
134
+ </div>
135
+ )}
136
+ </div>
137
+
138
+ {/* Text Container - Bottom */}
139
+ <div
140
+ style={{
141
+ position: 'absolute',
142
+ left: 0,
143
+ right: 0,
144
+ bottom: '16px',
145
+ textAlign: 'center',
146
+ padding: '0 16px',
147
+ display: 'flex',
148
+ flexDirection: 'column',
149
+ gap: '4px',
150
+ }}
151
+ >
152
+ <p
153
+ style={{
154
+ margin: 0,
155
+ fontWeight: 700,
156
+ fontSize: '18px',
157
+ lineHeight: '24px',
158
+ color: textColor,
159
+ overflow: 'hidden',
160
+ textOverflow: 'ellipsis',
161
+ whiteSpace: 'nowrap',
162
+ }}
163
+ >
164
+ {name}
165
+ </p>
166
+ <p
167
+ style={{
168
+ margin: 0,
169
+ fontSize: '10px',
170
+ lineHeight: '14px',
171
+ letterSpacing: '0.05em',
172
+ textTransform: 'uppercase',
173
+ color: textColor,
174
+ opacity: 0.7,
175
+ }}
176
+ >
177
+ {isFounder ? 'Founder of Zo World' : 'Citizen of Zo World'}
178
+ </p>
179
+ </div>
180
+ </div>
181
+ );
182
+ };
183
+