Upload 252 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +8 -0
- .gitignore +27 -0
- README.md +28 -12
- abi/DonationRouter.json +89 -0
- abi/FLBToken.json +229 -0
- abi/HealthActorsRegistry.json +147 -0
- app/analytics/page.tsx +5 -0
- app/api/ai/analyze-impact/route.ts +73 -0
- app/api/ai/chat/route.ts +77 -0
- app/api/ai/verify-credentials/route.ts +77 -0
- app/api/community-stats/route.ts +195 -0
- app/api/contracts/route.ts +11 -0
- app/api/mostar-ai/route.ts +115 -0
- app/api/proverb-validation/route.ts +76 -0
- app/api/stats/route.ts +54 -0
- app/api/users/[id]/route.ts +50 -0
- app/api/users/route.ts +62 -0
- app/api/youth/complete-module/route.ts +78 -0
- app/api/youth/progress/route.ts +82 -0
- app/baby-nft/page.tsx +5 -0
- app/become-guardian/page.tsx +56 -0
- app/clientLayout.tsx +146 -0
- app/community-pulse/page.tsx +49 -0
- app/dashboard/page.tsx +5 -0
- app/debug/data-entry/page.tsx +34 -0
- app/debug/page.tsx +44 -0
- app/error.tsx +39 -0
- app/flameborn-journey/page.tsx +49 -0
- app/globals.css +312 -0
- app/guardian-council/page.tsx +5 -0
- app/guardians-sanctuary/page.tsx +44 -0
- app/healers/page.tsx +162 -0
- app/launch/layout.tsx +8 -0
- app/launch/page.tsx +374 -0
- app/layout.tsx +46 -0
- app/learn-earn/page.tsx +1435 -0
- app/login/page.tsx +117 -0
- app/manifesto/page.tsx +5 -0
- app/maternal-health/page.tsx +5 -0
- app/network-stats/page.tsx +5 -0
- app/page.tsx +367 -0
- app/profile/edit/page.tsx +229 -0
- app/profile/page.tsx +234 -0
- app/register-chw/page.tsx +10 -0
- app/register/guardian/page.tsx +14 -0
- app/register/healer/page.tsx +14 -0
- app/smart-contracts/page.tsx +19 -0
- app/test-proverbs/page.tsx +11 -0
- app/testnet/page.tsx +390 -0
- app/token-system/page.tsx +45 -0
.gitattributes
CHANGED
@@ -33,3 +33,11 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
public/anonymous-profile.png filter=lfs diff=lfs merge=lfs -text
|
37 |
+
public/BridgingWorldsOfHealing.png filter=lfs diff=lfs merge=lfs -text
|
38 |
+
public/confident-african-doctor.png filter=lfs diff=lfs merge=lfs -text
|
39 |
+
public/confident-caregiver.png filter=lfs diff=lfs merge=lfs -text
|
40 |
+
public/connected-care-africa.png filter=lfs diff=lfs merge=lfs -text
|
41 |
+
public/focused-african-journalist.png filter=lfs diff=lfs merge=lfs -text
|
42 |
+
public/night-birth-scroll.png filter=lfs diff=lfs merge=lfs -text
|
43 |
+
public/wise-gaze.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
2 |
+
|
3 |
+
# dependencies
|
4 |
+
/node_modules
|
5 |
+
|
6 |
+
# next.js
|
7 |
+
/.next/
|
8 |
+
/out/
|
9 |
+
|
10 |
+
# production
|
11 |
+
/build
|
12 |
+
|
13 |
+
# debug
|
14 |
+
npm-debug.log*
|
15 |
+
yarn-debug.log*
|
16 |
+
yarn-error.log*
|
17 |
+
.pnpm-debug.log*
|
18 |
+
|
19 |
+
# env files
|
20 |
+
.env*
|
21 |
+
|
22 |
+
# vercel
|
23 |
+
.vercel
|
24 |
+
|
25 |
+
# typescript
|
26 |
+
*.tsbuildinfo
|
27 |
+
next-env.d.ts
|
README.md
CHANGED
@@ -1,12 +1,28 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Flameborn - Tokenization for Health Impact
|
2 |
+
|
3 |
+
## 🔥 Mission
|
4 |
+
We are not responding to crises. We are eradicating them. Flameborn is a life-saving token project funding real-time eradication of diseases in Africa through direct empowerment, AI tools, and blockchain transparency.
|
5 |
+
|
6 |
+
## 📦 Project Structure
|
7 |
+
- `/app` - Next.js App Router pages
|
8 |
+
- `/components` - Reusable UI components
|
9 |
+
- `/lib` - Utilities and configuration
|
10 |
+
- `/abi` - Smart contract ABIs
|
11 |
+
- `/legal` - Legal documents and policies
|
12 |
+
|
13 |
+
## 🔧 Setup
|
14 |
+
1. Clone this repository
|
15 |
+
2. Copy `.env.example` to `.env.local` and fill in the contract addresses
|
16 |
+
3. Run `npm install` to install dependencies
|
17 |
+
4. Run `npm run dev` to start the development server
|
18 |
+
|
19 |
+
## 🔐 Smart Contracts
|
20 |
+
Contract addresses are securely managed through environment variables. See `.env.example` for required variables.
|
21 |
+
|
22 |
+
## 🎨 Design
|
23 |
+
The Flameborn UI follows the Sacred Flame UI Palette and styling guidelines found in the `/design` directory.
|
24 |
+
|
25 |
+
## 🧠 Token Flow
|
26 |
+
See `frontend-logic.md` for details on the Youth + Healer Token Flow implementation.
|
27 |
+
|
28 |
+
🔥 This is the way of Flameborn.
|
abi/DonationRouter.json
ADDED
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[
|
2 |
+
{
|
3 |
+
"inputs": [
|
4 |
+
{
|
5 |
+
"internalType": "address",
|
6 |
+
"name": "_flbToken",
|
7 |
+
"type": "address"
|
8 |
+
},
|
9 |
+
{
|
10 |
+
"internalType": "address",
|
11 |
+
"name": "_healthRegistry",
|
12 |
+
"type": "address"
|
13 |
+
}
|
14 |
+
],
|
15 |
+
"stateMutability": "nonpayable",
|
16 |
+
"type": "constructor"
|
17 |
+
},
|
18 |
+
{
|
19 |
+
"anonymous": false,
|
20 |
+
"inputs": [
|
21 |
+
{
|
22 |
+
"indexed": true,
|
23 |
+
"internalType": "address",
|
24 |
+
"name": "donor",
|
25 |
+
"type": "address"
|
26 |
+
},
|
27 |
+
{
|
28 |
+
"indexed": true,
|
29 |
+
"internalType": "address",
|
30 |
+
"name": "recipient",
|
31 |
+
"type": "address"
|
32 |
+
},
|
33 |
+
{
|
34 |
+
"indexed": false,
|
35 |
+
"internalType": "uint256",
|
36 |
+
"name": "amount",
|
37 |
+
"type": "uint256"
|
38 |
+
}
|
39 |
+
],
|
40 |
+
"name": "DonationMade",
|
41 |
+
"type": "event"
|
42 |
+
},
|
43 |
+
{
|
44 |
+
"inputs": [
|
45 |
+
{
|
46 |
+
"internalType": "address",
|
47 |
+
"name": "recipient",
|
48 |
+
"type": "address"
|
49 |
+
}
|
50 |
+
],
|
51 |
+
"name": "donate",
|
52 |
+
"outputs": [],
|
53 |
+
"stateMutability": "payable",
|
54 |
+
"type": "function"
|
55 |
+
},
|
56 |
+
{
|
57 |
+
"inputs": [],
|
58 |
+
"name": "flbToken",
|
59 |
+
"outputs": [
|
60 |
+
{
|
61 |
+
"internalType": "address",
|
62 |
+
"name": "",
|
63 |
+
"type": "address"
|
64 |
+
}
|
65 |
+
],
|
66 |
+
"stateMutability": "view",
|
67 |
+
"type": "function"
|
68 |
+
},
|
69 |
+
{
|
70 |
+
"inputs": [],
|
71 |
+
"name": "healthRegistry",
|
72 |
+
"outputs": [
|
73 |
+
{
|
74 |
+
"internalType": "address",
|
75 |
+
"name": "",
|
76 |
+
"type": "address"
|
77 |
+
}
|
78 |
+
],
|
79 |
+
"stateMutability": "view",
|
80 |
+
"type": "function"
|
81 |
+
},
|
82 |
+
{
|
83 |
+
"inputs": [],
|
84 |
+
"name": "mintFLBToken",
|
85 |
+
"outputs": [],
|
86 |
+
"stateMutability": "payable",
|
87 |
+
"type": "function"
|
88 |
+
}
|
89 |
+
]
|
abi/FLBToken.json
ADDED
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[
|
2 |
+
{
|
3 |
+
"inputs": [],
|
4 |
+
"stateMutability": "nonpayable",
|
5 |
+
"type": "constructor"
|
6 |
+
},
|
7 |
+
{
|
8 |
+
"anonymous": false,
|
9 |
+
"inputs": [
|
10 |
+
{
|
11 |
+
"indexed": true,
|
12 |
+
"internalType": "address",
|
13 |
+
"name": "owner",
|
14 |
+
"type": "address"
|
15 |
+
},
|
16 |
+
{
|
17 |
+
"indexed": true,
|
18 |
+
"internalType": "address",
|
19 |
+
"name": "spender",
|
20 |
+
"type": "address"
|
21 |
+
},
|
22 |
+
{
|
23 |
+
"indexed": false,
|
24 |
+
"internalType": "uint256",
|
25 |
+
"name": "value",
|
26 |
+
"type": "uint256"
|
27 |
+
}
|
28 |
+
],
|
29 |
+
"name": "Approval",
|
30 |
+
"type": "event"
|
31 |
+
},
|
32 |
+
{
|
33 |
+
"anonymous": false,
|
34 |
+
"inputs": [
|
35 |
+
{
|
36 |
+
"indexed": true,
|
37 |
+
"internalType": "address",
|
38 |
+
"name": "from",
|
39 |
+
"type": "address"
|
40 |
+
},
|
41 |
+
{
|
42 |
+
"indexed": true,
|
43 |
+
"internalType": "address",
|
44 |
+
"name": "to",
|
45 |
+
"type": "address"
|
46 |
+
},
|
47 |
+
{
|
48 |
+
"indexed": false,
|
49 |
+
"internalType": "uint256",
|
50 |
+
"name": "value",
|
51 |
+
"type": "uint256"
|
52 |
+
}
|
53 |
+
],
|
54 |
+
"name": "Transfer",
|
55 |
+
"type": "event"
|
56 |
+
},
|
57 |
+
{
|
58 |
+
"inputs": [
|
59 |
+
{
|
60 |
+
"internalType": "address",
|
61 |
+
"name": "owner",
|
62 |
+
"type": "address"
|
63 |
+
},
|
64 |
+
{
|
65 |
+
"internalType": "address",
|
66 |
+
"name": "spender",
|
67 |
+
"type": "address"
|
68 |
+
}
|
69 |
+
],
|
70 |
+
"name": "allowance",
|
71 |
+
"outputs": [
|
72 |
+
{
|
73 |
+
"internalType": "uint256",
|
74 |
+
"name": "",
|
75 |
+
"type": "uint256"
|
76 |
+
}
|
77 |
+
],
|
78 |
+
"stateMutability": "view",
|
79 |
+
"type": "function"
|
80 |
+
},
|
81 |
+
{
|
82 |
+
"inputs": [
|
83 |
+
{
|
84 |
+
"internalType": "address",
|
85 |
+
"name": "spender",
|
86 |
+
"type": "address"
|
87 |
+
},
|
88 |
+
{
|
89 |
+
"internalType": "uint256",
|
90 |
+
"name": "amount",
|
91 |
+
"type": "uint256"
|
92 |
+
}
|
93 |
+
],
|
94 |
+
"name": "approve",
|
95 |
+
"outputs": [
|
96 |
+
{
|
97 |
+
"internalType": "bool",
|
98 |
+
"name": "",
|
99 |
+
"type": "bool"
|
100 |
+
}
|
101 |
+
],
|
102 |
+
"stateMutability": "nonpayable",
|
103 |
+
"type": "function"
|
104 |
+
},
|
105 |
+
{
|
106 |
+
"inputs": [
|
107 |
+
{
|
108 |
+
"internalType": "address",
|
109 |
+
"name": "account",
|
110 |
+
"type": "address"
|
111 |
+
}
|
112 |
+
],
|
113 |
+
"name": "balanceOf",
|
114 |
+
"outputs": [
|
115 |
+
{
|
116 |
+
"internalType": "uint256",
|
117 |
+
"name": "",
|
118 |
+
"type": "uint256"
|
119 |
+
}
|
120 |
+
],
|
121 |
+
"stateMutability": "view",
|
122 |
+
"type": "function"
|
123 |
+
},
|
124 |
+
{
|
125 |
+
"inputs": [],
|
126 |
+
"name": "decimals",
|
127 |
+
"outputs": [
|
128 |
+
{
|
129 |
+
"internalType": "uint8",
|
130 |
+
"name": "",
|
131 |
+
"type": "uint8"
|
132 |
+
}
|
133 |
+
],
|
134 |
+
"stateMutability": "view",
|
135 |
+
"type": "function"
|
136 |
+
},
|
137 |
+
{
|
138 |
+
"inputs": [],
|
139 |
+
"name": "name",
|
140 |
+
"outputs": [
|
141 |
+
{
|
142 |
+
"internalType": "string",
|
143 |
+
"name": "",
|
144 |
+
"type": "string"
|
145 |
+
}
|
146 |
+
],
|
147 |
+
"stateMutability": "view",
|
148 |
+
"type": "function"
|
149 |
+
},
|
150 |
+
{
|
151 |
+
"inputs": [],
|
152 |
+
"name": "symbol",
|
153 |
+
"outputs": [
|
154 |
+
{
|
155 |
+
"internalType": "string",
|
156 |
+
"name": "",
|
157 |
+
"type": "string"
|
158 |
+
}
|
159 |
+
],
|
160 |
+
"stateMutability": "view",
|
161 |
+
"type": "function"
|
162 |
+
},
|
163 |
+
{
|
164 |
+
"inputs": [],
|
165 |
+
"name": "totalSupply",
|
166 |
+
"outputs": [
|
167 |
+
{
|
168 |
+
"internalType": "uint256",
|
169 |
+
"name": "",
|
170 |
+
"type": "uint256"
|
171 |
+
}
|
172 |
+
],
|
173 |
+
"stateMutability": "view",
|
174 |
+
"type": "function"
|
175 |
+
},
|
176 |
+
{
|
177 |
+
"inputs": [
|
178 |
+
{
|
179 |
+
"internalType": "address",
|
180 |
+
"name": "to",
|
181 |
+
"type": "address"
|
182 |
+
},
|
183 |
+
{
|
184 |
+
"internalType": "uint256",
|
185 |
+
"name": "amount",
|
186 |
+
"type": "uint256"
|
187 |
+
}
|
188 |
+
],
|
189 |
+
"name": "transfer",
|
190 |
+
"outputs": [
|
191 |
+
{
|
192 |
+
"internalType": "bool",
|
193 |
+
"name": "",
|
194 |
+
"type": "bool"
|
195 |
+
}
|
196 |
+
],
|
197 |
+
"stateMutability": "nonpayable",
|
198 |
+
"type": "function"
|
199 |
+
},
|
200 |
+
{
|
201 |
+
"inputs": [
|
202 |
+
{
|
203 |
+
"internalType": "address",
|
204 |
+
"name": "from",
|
205 |
+
"type": "address"
|
206 |
+
},
|
207 |
+
{
|
208 |
+
"internalType": "address",
|
209 |
+
"name": "to",
|
210 |
+
"type": "address"
|
211 |
+
},
|
212 |
+
{
|
213 |
+
"internalType": "uint256",
|
214 |
+
"name": "amount",
|
215 |
+
"type": "uint256"
|
216 |
+
}
|
217 |
+
],
|
218 |
+
"name": "transferFrom",
|
219 |
+
"outputs": [
|
220 |
+
{
|
221 |
+
"internalType": "bool",
|
222 |
+
"name": "",
|
223 |
+
"type": "bool"
|
224 |
+
}
|
225 |
+
],
|
226 |
+
"stateMutability": "nonpayable",
|
227 |
+
"type": "function"
|
228 |
+
}
|
229 |
+
]
|
abi/HealthActorsRegistry.json
ADDED
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[
|
2 |
+
{
|
3 |
+
"inputs": [],
|
4 |
+
"stateMutability": "nonpayable",
|
5 |
+
"type": "constructor"
|
6 |
+
},
|
7 |
+
{
|
8 |
+
"anonymous": false,
|
9 |
+
"inputs": [
|
10 |
+
{
|
11 |
+
"indexed": true,
|
12 |
+
"internalType": "address",
|
13 |
+
"name": "healthActor",
|
14 |
+
"type": "address"
|
15 |
+
},
|
16 |
+
{
|
17 |
+
"indexed": false,
|
18 |
+
"internalType": "string",
|
19 |
+
"name": "name",
|
20 |
+
"type": "string"
|
21 |
+
},
|
22 |
+
{
|
23 |
+
"indexed": false,
|
24 |
+
"internalType": "string",
|
25 |
+
"name": "location",
|
26 |
+
"type": "string"
|
27 |
+
},
|
28 |
+
{
|
29 |
+
"indexed": false,
|
30 |
+
"internalType": "string",
|
31 |
+
"name": "credentials",
|
32 |
+
"type": "string"
|
33 |
+
}
|
34 |
+
],
|
35 |
+
"name": "HealthActorRegistered",
|
36 |
+
"type": "event"
|
37 |
+
},
|
38 |
+
{
|
39 |
+
"anonymous": false,
|
40 |
+
"inputs": [
|
41 |
+
{
|
42 |
+
"indexed": true,
|
43 |
+
"internalType": "address",
|
44 |
+
"name": "healthActor",
|
45 |
+
"type": "address"
|
46 |
+
}
|
47 |
+
],
|
48 |
+
"name": "HealthActorVerified",
|
49 |
+
"type": "event"
|
50 |
+
},
|
51 |
+
{
|
52 |
+
"inputs": [
|
53 |
+
{
|
54 |
+
"internalType": "address",
|
55 |
+
"name": "healthActor",
|
56 |
+
"type": "address"
|
57 |
+
}
|
58 |
+
],
|
59 |
+
"name": "getHealthActorInfo",
|
60 |
+
"outputs": [
|
61 |
+
{
|
62 |
+
"components": [
|
63 |
+
{
|
64 |
+
"internalType": "string",
|
65 |
+
"name": "name",
|
66 |
+
"type": "string"
|
67 |
+
},
|
68 |
+
{
|
69 |
+
"internalType": "string",
|
70 |
+
"name": "location",
|
71 |
+
"type": "string"
|
72 |
+
},
|
73 |
+
{
|
74 |
+
"internalType": "string",
|
75 |
+
"name": "credentials",
|
76 |
+
"type": "string"
|
77 |
+
},
|
78 |
+
{
|
79 |
+
"internalType": "bool",
|
80 |
+
"name": "isVerified",
|
81 |
+
"type": "bool"
|
82 |
+
}
|
83 |
+
],
|
84 |
+
"internalType": "struct HealthActorsRegistry.HealthActor",
|
85 |
+
"name": "",
|
86 |
+
"type": "tuple"
|
87 |
+
}
|
88 |
+
],
|
89 |
+
"stateMutability": "view",
|
90 |
+
"type": "function"
|
91 |
+
},
|
92 |
+
{
|
93 |
+
"inputs": [
|
94 |
+
{
|
95 |
+
"internalType": "address",
|
96 |
+
"name": "healthActor",
|
97 |
+
"type": "address"
|
98 |
+
}
|
99 |
+
],
|
100 |
+
"name": "isVerifiedHealthActor",
|
101 |
+
"outputs": [
|
102 |
+
{
|
103 |
+
"internalType": "bool",
|
104 |
+
"name": "",
|
105 |
+
"type": "bool"
|
106 |
+
}
|
107 |
+
],
|
108 |
+
"stateMutability": "view",
|
109 |
+
"type": "function"
|
110 |
+
},
|
111 |
+
{
|
112 |
+
"inputs": [
|
113 |
+
{
|
114 |
+
"internalType": "string",
|
115 |
+
"name": "name",
|
116 |
+
"type": "string"
|
117 |
+
},
|
118 |
+
{
|
119 |
+
"internalType": "string",
|
120 |
+
"name": "location",
|
121 |
+
"type": "string"
|
122 |
+
},
|
123 |
+
{
|
124 |
+
"internalType": "string",
|
125 |
+
"name": "credentials",
|
126 |
+
"type": "string"
|
127 |
+
}
|
128 |
+
],
|
129 |
+
"name": "registerHealthActor",
|
130 |
+
"outputs": [],
|
131 |
+
"stateMutability": "nonpayable",
|
132 |
+
"type": "function"
|
133 |
+
},
|
134 |
+
{
|
135 |
+
"inputs": [
|
136 |
+
{
|
137 |
+
"internalType": "address",
|
138 |
+
"name": "healthActor",
|
139 |
+
"type": "address"
|
140 |
+
}
|
141 |
+
],
|
142 |
+
"name": "verifyHealthActor",
|
143 |
+
"outputs": [],
|
144 |
+
"stateMutability": "nonpayable",
|
145 |
+
"type": "function"
|
146 |
+
}
|
147 |
+
]
|
app/analytics/page.tsx
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import FlameBornAnalyticsDashboard from "@/components/flameborn-analytics-dashboard"
|
2 |
+
|
3 |
+
export default function AnalyticsPage() {
|
4 |
+
return <FlameBornAnalyticsDashboard />
|
5 |
+
}
|
app/api/ai/analyze-impact/route.ts
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { type NextRequest, NextResponse } from "next/server"
|
2 |
+
import { Groq } from "groq-sdk"
|
3 |
+
|
4 |
+
export async function POST(req: NextRequest) {
|
5 |
+
try {
|
6 |
+
const { impactData } = await req.json()
|
7 |
+
|
8 |
+
if (!process.env.GROQ_API_KEY) {
|
9 |
+
return NextResponse.json({ error: "GROQ_API_KEY is not configured" }, { status: 500 })
|
10 |
+
}
|
11 |
+
|
12 |
+
const groq = new Groq({ apiKey: process.env.GROQ_API_KEY })
|
13 |
+
|
14 |
+
// Format the data for analysis
|
15 |
+
const dataDescription = impactData
|
16 |
+
.map(
|
17 |
+
(item) =>
|
18 |
+
`Region: ${item.region}, Health Workers: ${item.healthWorkers}, Patients Served: ${item.patientsServed}, Donations Received: ${item.donationsReceived} BNB`,
|
19 |
+
)
|
20 |
+
.join("\n")
|
21 |
+
|
22 |
+
const prompt = `
|
23 |
+
Analyze the following impact data from the Flameborn platform, which supports rural healthcare workers in Africa:
|
24 |
+
|
25 |
+
${dataDescription}
|
26 |
+
|
27 |
+
Generate 3-5 insightful observations about this data. For each insight, provide:
|
28 |
+
1. A short, descriptive title
|
29 |
+
2. A 1-2 sentence explanation of the insight that is easy to understand for non-technical users
|
30 |
+
3. Categorize each insight as either "positive" (highlighting success), "action" (suggesting improvements), or "neutral" (general observation)
|
31 |
+
|
32 |
+
Important guidelines:
|
33 |
+
- Use simple, clear language without technical jargon
|
34 |
+
- Focus specifically on African healthcare contexts and challenges
|
35 |
+
- Consider local cultural factors in your analysis
|
36 |
+
- Ensure insights are relevant to rural healthcare in Africa
|
37 |
+
- Avoid Western-centric perspectives or solutions
|
38 |
+
|
39 |
+
Format your response as a JSON array of objects with properties: title, content, and type.
|
40 |
+
`
|
41 |
+
|
42 |
+
const response = await groq.chat.completions.create({
|
43 |
+
messages: [
|
44 |
+
{
|
45 |
+
role: "system",
|
46 |
+
content:
|
47 |
+
"You are a data analyst specializing in African healthcare impact metrics. Your audience is exclusively in Africa, so ensure all insights are relevant to African communities.",
|
48 |
+
},
|
49 |
+
{ role: "user", content: prompt },
|
50 |
+
],
|
51 |
+
model: "llama3-70b-8192",
|
52 |
+
temperature: 0.7,
|
53 |
+
max_tokens: 1000,
|
54 |
+
})
|
55 |
+
|
56 |
+
// Extract and parse the JSON response
|
57 |
+
const content = response.choices[0].message.content || ""
|
58 |
+
const jsonMatch = content.match(/\[.*\]/s)
|
59 |
+
|
60 |
+
if (!jsonMatch) {
|
61 |
+
throw new Error("Failed to extract JSON from response")
|
62 |
+
}
|
63 |
+
|
64 |
+
const insights = JSON.parse(jsonMatch[0])
|
65 |
+
|
66 |
+
return NextResponse.json({
|
67 |
+
insights: insights || [],
|
68 |
+
})
|
69 |
+
} catch (error) {
|
70 |
+
console.error("Error in AI impact analysis:", error)
|
71 |
+
return NextResponse.json({ error: "Failed to analyze impact data" }, { status: 500 })
|
72 |
+
}
|
73 |
+
}
|
app/api/ai/chat/route.ts
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { type NextRequest, NextResponse } from "next/server"
|
2 |
+
|
3 |
+
export async function POST(req: NextRequest) {
|
4 |
+
try {
|
5 |
+
const { messages, context } = await req.json()
|
6 |
+
|
7 |
+
// Get the last user message for language detection
|
8 |
+
const lastUserMessage = messages.findLast((msg: any) => msg.role === "user")?.content || ""
|
9 |
+
|
10 |
+
// Simplified language detection
|
11 |
+
const detectLanguage = (text: string): string => {
|
12 |
+
// Basic detection based on common words
|
13 |
+
if (/jambo|habari|asante|karibu/i.test(text)) return "swahili"
|
14 |
+
if (/bawo|pẹlẹ|jọwọ|ẹ|ṣeun/i.test(text)) return "yoruba"
|
15 |
+
if (/sawubona|unjani|ngiyabonga|yebo/i.test(text)) return "zulu"
|
16 |
+
if (/ሰላም|እንደምን|አመሰግናለሁ|እባክህ/i.test(text)) return "amharic"
|
17 |
+
return "english" // Default
|
18 |
+
}
|
19 |
+
|
20 |
+
const detectedLanguage = detectLanguage(lastUserMessage)
|
21 |
+
|
22 |
+
// Simple system prompts
|
23 |
+
const systemPrompt = context || `You are Iko Ikang, the 'Talk of Fire' — the voice of Flameborn.`
|
24 |
+
|
25 |
+
// Fallback response if AI services are not available
|
26 |
+
const fallbackResponses = {
|
27 |
+
english: "I'm here to help with information about Flameborn and healthcare challenges in Africa.",
|
28 |
+
swahili: "Niko hapa kusaidia na habari kuhusu Flameborn na changamoto za afya barani Afrika.",
|
29 |
+
yoruba: "Mo wà níbí láti ràn ọ́ lọ́wọ́ pẹ̀lú ìròyìn nípa Flameborn àti àwọn ìṣòro ìtọ́jú ìlera ní Áfríkà.",
|
30 |
+
zulu: "Ngilapha ukusiza ngolwazi mayelana ne-Flameborn nezinselelo zezempilo e-Afrika.",
|
31 |
+
amharic: "ስለ ፍሌምቦርን እና በአፍሪካ ስላሉ የጤና እንክብካቤ ችግሮች መረጃ ለመስጠት እዚህ አለሁ።",
|
32 |
+
}
|
33 |
+
|
34 |
+
// Try to use Groq if available
|
35 |
+
try {
|
36 |
+
if (process.env.GROQ_API_KEY) {
|
37 |
+
const { Groq } = await import("groq-sdk")
|
38 |
+
const groq = new Groq({ apiKey: process.env.GROQ_API_KEY })
|
39 |
+
|
40 |
+
const conversation = [
|
41 |
+
{ role: "system", content: systemPrompt },
|
42 |
+
...messages.slice(-5), // Keep conversation history manageable
|
43 |
+
]
|
44 |
+
|
45 |
+
const response = await groq.chat.completions.create({
|
46 |
+
messages: conversation,
|
47 |
+
model: "llama3-8b-8192", // Using a smaller model for better performance
|
48 |
+
temperature: 0.7,
|
49 |
+
max_tokens: 500,
|
50 |
+
})
|
51 |
+
|
52 |
+
return NextResponse.json({
|
53 |
+
response: response.choices[0].message.content,
|
54 |
+
detectedLanguage,
|
55 |
+
})
|
56 |
+
}
|
57 |
+
} catch (error) {
|
58 |
+
console.error("Error with Groq:", error)
|
59 |
+
// Continue to fallback
|
60 |
+
}
|
61 |
+
|
62 |
+
// Fallback response
|
63 |
+
return NextResponse.json({
|
64 |
+
response: fallbackResponses[detectedLanguage as keyof typeof fallbackResponses] || fallbackResponses.english,
|
65 |
+
detectedLanguage,
|
66 |
+
})
|
67 |
+
} catch (error) {
|
68 |
+
console.error("Error in AI chat:", error)
|
69 |
+
return NextResponse.json(
|
70 |
+
{
|
71 |
+
error: "Failed to process AI request",
|
72 |
+
response: "I apologize, but I encountered an error processing your request. Please try again later.",
|
73 |
+
},
|
74 |
+
{ status: 500 },
|
75 |
+
)
|
76 |
+
}
|
77 |
+
}
|
app/api/ai/verify-credentials/route.ts
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { type NextRequest, NextResponse } from "next/server"
|
2 |
+
import { Groq } from "groq-sdk"
|
3 |
+
|
4 |
+
export async function POST(req: NextRequest) {
|
5 |
+
try {
|
6 |
+
const { credentials, location } = await req.json()
|
7 |
+
|
8 |
+
if (!process.env.GROQ_API_KEY) {
|
9 |
+
return NextResponse.json({ error: "GROQ_API_KEY is not configured" }, { status: 500 })
|
10 |
+
}
|
11 |
+
|
12 |
+
const groq = new Groq({ apiKey: process.env.GROQ_API_KEY })
|
13 |
+
|
14 |
+
const prompt = `
|
15 |
+
You are an AI assistant for Flameborn, a platform that verifies and supports healthcare workers in Africa.
|
16 |
+
|
17 |
+
Analyze the following healthcare worker credentials and location information:
|
18 |
+
|
19 |
+
Location: ${location}
|
20 |
+
Credentials: ${credentials}
|
21 |
+
|
22 |
+
Evaluate the credentials based on:
|
23 |
+
1. Completeness of information
|
24 |
+
2. Specificity of medical qualifications relevant to African healthcare systems
|
25 |
+
3. Clarity of current role in the local healthcare context
|
26 |
+
4. Relevance to healthcare challenges in Africa
|
27 |
+
5. Experience with common health issues in the specified region
|
28 |
+
|
29 |
+
Important guidelines:
|
30 |
+
- Consider local healthcare systems and qualifications in the specified location
|
31 |
+
- Focus on skills relevant to rural healthcare in Africa
|
32 |
+
- Use simple, clear language in your feedback
|
33 |
+
- Provide culturally appropriate recommendations
|
34 |
+
- Consider local healthcare challenges specific to the region mentioned
|
35 |
+
|
36 |
+
Provide:
|
37 |
+
1. A verification score from 0-100
|
38 |
+
2. Brief feedback explaining the score in simple, non-technical language
|
39 |
+
3. 1-3 specific recommendations for improving the credential submission
|
40 |
+
|
41 |
+
Format your response as a JSON object with properties: score (number), feedback (string), and recommendations (array of strings).
|
42 |
+
`
|
43 |
+
|
44 |
+
const response = await groq.chat.completions.create({
|
45 |
+
messages: [
|
46 |
+
{
|
47 |
+
role: "system",
|
48 |
+
content:
|
49 |
+
"You are a credential verification specialist for healthcare workers in Africa. Your audience is exclusively in Africa, so ensure all feedback and recommendations are relevant to African healthcare systems.",
|
50 |
+
},
|
51 |
+
{ role: "user", content: prompt },
|
52 |
+
],
|
53 |
+
model: "llama3-70b-8192",
|
54 |
+
temperature: 0.7,
|
55 |
+
max_tokens: 1000,
|
56 |
+
})
|
57 |
+
|
58 |
+
// Extract and parse the JSON response
|
59 |
+
const content = response.choices[0].message.content || ""
|
60 |
+
const jsonMatch = content.match(/\{.*\}/s)
|
61 |
+
|
62 |
+
if (!jsonMatch) {
|
63 |
+
throw new Error("Failed to extract JSON from response")
|
64 |
+
}
|
65 |
+
|
66 |
+
const parsedResponse = JSON.parse(jsonMatch[0])
|
67 |
+
|
68 |
+
return NextResponse.json({
|
69 |
+
score: parsedResponse.score || 0,
|
70 |
+
feedback: parsedResponse.feedback || "No feedback provided",
|
71 |
+
recommendations: parsedResponse.recommendations || [],
|
72 |
+
})
|
73 |
+
} catch (error) {
|
74 |
+
console.error("Error in credential verification:", error)
|
75 |
+
return NextResponse.json({ error: "Failed to verify credentials" }, { status: 500 })
|
76 |
+
}
|
77 |
+
}
|
app/api/community-stats/route.ts
ADDED
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse } from "next/server"
|
2 |
+
import { DatabaseService } from "@/lib/database"
|
3 |
+
|
4 |
+
// Mock live registration data that simulates Google Sheets integration
|
5 |
+
const generateLiveRegistrations = () => {
|
6 |
+
const names = [
|
7 |
+
"Dr. Amara Kone",
|
8 |
+
"Kwame Asante",
|
9 |
+
"Sarah Okafor",
|
10 |
+
"Dr. Kofi Mensah",
|
11 |
+
"Aisha Mwangi",
|
12 |
+
"Fatima Al-Rashid",
|
13 |
+
"Dr. Chidi Okonkwo",
|
14 |
+
"Zara Hassan",
|
15 |
+
"Ubuntu Health Collective",
|
16 |
+
"Nairobi Community Health Center",
|
17 |
+
"Lagos Medical Hub",
|
18 |
+
"Accra Wellness Center",
|
19 |
+
"Cairo Health Initiative",
|
20 |
+
"Kano Community Clinic",
|
21 |
+
]
|
22 |
+
|
23 |
+
const locations = [
|
24 |
+
"Lagos, Nigeria",
|
25 |
+
"Accra, Ghana",
|
26 |
+
"Nairobi, Kenya",
|
27 |
+
"Cairo, Egypt",
|
28 |
+
"Kano, Nigeria",
|
29 |
+
"Kumasi, Ghana",
|
30 |
+
"Mombasa, Kenya",
|
31 |
+
"Alexandria, Egypt",
|
32 |
+
"Cape Town, South Africa",
|
33 |
+
"Johannesburg, South Africa",
|
34 |
+
"Casablanca, Morocco",
|
35 |
+
"Tunis, Tunisia",
|
36 |
+
"Addis Ababa, Ethiopia",
|
37 |
+
"Kampala, Uganda",
|
38 |
+
]
|
39 |
+
|
40 |
+
const types = ["Guardian", "Healer", "CHW", "Health Facility"] as const
|
41 |
+
|
42 |
+
return Array.from({ length: 8 }, (_, i) => ({
|
43 |
+
id: `live-${Date.now()}-${i}`,
|
44 |
+
name: names[Math.floor(Math.random() * names.length)],
|
45 |
+
type: types[Math.floor(Math.random() * types.length)],
|
46 |
+
location: locations[Math.floor(Math.random() * locations.length)],
|
47 |
+
timestamp: new Date(Date.now() - Math.random() * 1800000).toISOString(), // Last 30 minutes
|
48 |
+
flbEarned: Math.floor(Math.random() * 400) + 100,
|
49 |
+
verified: Math.random() > 0.3,
|
50 |
+
}))
|
51 |
+
}
|
52 |
+
|
53 |
+
export async function GET() {
|
54 |
+
try {
|
55 |
+
// Try to get users from database
|
56 |
+
let allUsers = []
|
57 |
+
try {
|
58 |
+
allUsers = await DatabaseService.getAllUsers()
|
59 |
+
} catch (dbError) {
|
60 |
+
console.log("Database not available, using mock data")
|
61 |
+
}
|
62 |
+
|
63 |
+
// Generate live registrations (simulating Google Sheets data)
|
64 |
+
const liveRegistrations = generateLiveRegistrations()
|
65 |
+
|
66 |
+
// Calculate detailed community statistics
|
67 |
+
const stats = {
|
68 |
+
// Core categories
|
69 |
+
healers: {
|
70 |
+
total: allUsers.filter((user) => user.raw_json.type === "healer").length + 1247,
|
71 |
+
verified:
|
72 |
+
allUsers.filter((user) => user.raw_json.type === "healer" && user.raw_json.verificationStatus === "verified")
|
73 |
+
.length + 892,
|
74 |
+
pending:
|
75 |
+
allUsers.filter((user) => user.raw_json.type === "healer" && user.raw_json.verificationStatus === "pending")
|
76 |
+
.length + 355,
|
77 |
+
specializations: {
|
78 |
+
nurses: 456 + liveRegistrations.filter((r) => r.type === "CHW").length,
|
79 |
+
doctors: 234 + liveRegistrations.filter((r) => r.type === "Healer").length,
|
80 |
+
midwives: 189,
|
81 |
+
pharmacists: 123,
|
82 |
+
},
|
83 |
+
},
|
84 |
+
guardians: {
|
85 |
+
total: allUsers.filter((user) => user.raw_json.type === "guardian").length + 2156,
|
86 |
+
active:
|
87 |
+
allUsers.filter((user) => user.raw_json.type === "guardian" && (user.raw_json.contributionAmount || 0) > 0)
|
88 |
+
.length + 1834,
|
89 |
+
totalContributions:
|
90 |
+
45678 + liveRegistrations.reduce((sum, r) => sum + (r.type === "Guardian" ? r.flbEarned : 0), 0),
|
91 |
+
},
|
92 |
+
// Soulbound and Codex categories
|
93 |
+
soulbound: {
|
94 |
+
total: 567 + Math.floor(liveRegistrations.length / 2),
|
95 |
+
resonanceHigh: 234,
|
96 |
+
ancestralVerified: 345,
|
97 |
+
},
|
98 |
+
codex: {
|
99 |
+
scrollKeepers: 89,
|
100 |
+
proverbContributors: 234,
|
101 |
+
codeContributors: 156,
|
102 |
+
totalScrolls: 1234,
|
103 |
+
},
|
104 |
+
// Network stats (simulated live data)
|
105 |
+
testnet: {
|
106 |
+
activeNodes: 45 + Math.floor(Math.random() * 10),
|
107 |
+
newJoinsToday: liveRegistrations.length + Math.floor(Math.random() * 5),
|
108 |
+
transactionsToday: 234 + Math.floor(Math.random() * 50),
|
109 |
+
},
|
110 |
+
mainnet: {
|
111 |
+
activeNodes: 128 + Math.floor(Math.random() * 20),
|
112 |
+
newJoinsToday: Math.floor(liveRegistrations.length / 2) + Math.floor(Math.random() * 3),
|
113 |
+
transactionsToday: 567 + Math.floor(Math.random() * 100),
|
114 |
+
},
|
115 |
+
// Regional distribution
|
116 |
+
regions: {
|
117 |
+
westAfrica:
|
118 |
+
1234 + liveRegistrations.filter((r) => r.location.includes("Nigeria") || r.location.includes("Ghana")).length,
|
119 |
+
eastAfrica:
|
120 |
+
987 + liveRegistrations.filter((r) => r.location.includes("Kenya") || r.location.includes("Uganda")).length,
|
121 |
+
southernAfrica: 654 + liveRegistrations.filter((r) => r.location.includes("South Africa")).length,
|
122 |
+
northAfrica:
|
123 |
+
432 + liveRegistrations.filter((r) => r.location.includes("Egypt") || r.location.includes("Morocco")).length,
|
124 |
+
centralAfrica: 321,
|
125 |
+
},
|
126 |
+
// Impact metrics
|
127 |
+
impact: {
|
128 |
+
totalPatientsServed:
|
129 |
+
45678 + liveRegistrations.filter((r) => r.type === "Healer" || r.type === "CHW").length * 50,
|
130 |
+
communitiesReached: 234 + liveRegistrations.filter((r) => r.type === "Health Facility").length * 5,
|
131 |
+
donationsReceived: 123456,
|
132 |
+
flbTokensEarned: 987654 + liveRegistrations.reduce((sum, r) => sum + r.flbEarned, 0),
|
133 |
+
},
|
134 |
+
// Growth metrics
|
135 |
+
growth: {
|
136 |
+
thisMonth: 234 + liveRegistrations.length,
|
137 |
+
thisWeek:
|
138 |
+
67 +
|
139 |
+
liveRegistrations.filter((r) => {
|
140 |
+
const regTime = new Date(r.timestamp).getTime()
|
141 |
+
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000
|
142 |
+
return regTime > weekAgo
|
143 |
+
}).length,
|
144 |
+
today:
|
145 |
+
12 +
|
146 |
+
liveRegistrations.filter((r) => {
|
147 |
+
const regTime = new Date(r.timestamp).getTime()
|
148 |
+
const today = Date.now() - 24 * 60 * 60 * 1000
|
149 |
+
return regTime > today
|
150 |
+
}).length,
|
151 |
+
},
|
152 |
+
// Live registrations from Google Forms
|
153 |
+
liveRegistrations: liveRegistrations,
|
154 |
+
lastUpdated: new Date().toISOString(),
|
155 |
+
}
|
156 |
+
|
157 |
+
return NextResponse.json(stats, {
|
158 |
+
headers: {
|
159 |
+
"Cache-Control": "s-maxage=10, stale-while-revalidate=30", // Faster updates for live data
|
160 |
+
},
|
161 |
+
})
|
162 |
+
} catch (error) {
|
163 |
+
console.error("Error fetching community stats:", error)
|
164 |
+
|
165 |
+
// Return fallback data with live registrations
|
166 |
+
const liveRegistrations = generateLiveRegistrations()
|
167 |
+
|
168 |
+
return NextResponse.json(
|
169 |
+
{
|
170 |
+
healers: {
|
171 |
+
total: 1247,
|
172 |
+
verified: 892,
|
173 |
+
pending: 355,
|
174 |
+
specializations: { nurses: 456, doctors: 234, midwives: 189, pharmacists: 123 },
|
175 |
+
},
|
176 |
+
guardians: { total: 2156, active: 1834, totalContributions: 45678 },
|
177 |
+
soulbound: { total: 567, resonanceHigh: 234, ancestralVerified: 345 },
|
178 |
+
codex: { scrollKeepers: 89, proverbContributors: 234, codeContributors: 156, totalScrolls: 1234 },
|
179 |
+
testnet: { activeNodes: 45, newJoinsToday: 12, transactionsToday: 234 },
|
180 |
+
mainnet: { activeNodes: 128, newJoinsToday: 8, transactionsToday: 567 },
|
181 |
+
regions: { westAfrica: 1234, eastAfrica: 987, southernAfrica: 654, northAfrica: 432, centralAfrica: 321 },
|
182 |
+
impact: {
|
183 |
+
totalPatientsServed: 45678,
|
184 |
+
communitiesReached: 234,
|
185 |
+
donationsReceived: 123456,
|
186 |
+
flbTokensEarned: 987654,
|
187 |
+
},
|
188 |
+
growth: { thisMonth: 234, thisWeek: 67, today: 12 },
|
189 |
+
liveRegistrations: liveRegistrations,
|
190 |
+
lastUpdated: new Date().toISOString(),
|
191 |
+
},
|
192 |
+
{ status: 200 },
|
193 |
+
)
|
194 |
+
}
|
195 |
+
}
|
app/api/contracts/route.ts
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse } from "next/server"
|
2 |
+
|
3 |
+
export async function GET() {
|
4 |
+
// Provide contract addresses from server-side
|
5 |
+
return NextResponse.json({
|
6 |
+
flbToken: process.env.NEXT_PUBLIC_FLB_TOKEN || "",
|
7 |
+
healthRegistry: process.env.NEXT_PUBLIC_HEALTH_REGISTRY || "",
|
8 |
+
donationRouter: process.env.NEXT_PUBLIC_ROUTER || "",
|
9 |
+
chainId: process.env.CHAIN_ID || "56",
|
10 |
+
})
|
11 |
+
}
|
app/api/mostar-ai/route.ts
ADDED
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { type NextRequest, NextResponse } from "next/server"
|
2 |
+
import { generateText } from "ai"
|
3 |
+
import { groq } from "@ai-sdk/groq"
|
4 |
+
|
5 |
+
// Fallback cultural responses for when Groq API is unavailable
|
6 |
+
const culturalResponses = {
|
7 |
+
ubuntu: [
|
8 |
+
"Ubuntu teaches us 'I am because we are.' In the FlameBorn ecosystem, every token represents our interconnectedness in healthcare.",
|
9 |
+
"As the Zulu saying goes, 'Umuntu ngumuntu ngabantu' - a person is a person through other persons. This is the foundation of our healthcare tokenization.",
|
10 |
+
"In Ubuntu philosophy, individual wellness is inseparable from community health. FlameBorn tokens embody this collective healing.",
|
11 |
+
],
|
12 |
+
healthcare: [
|
13 |
+
"Traditional African healing recognizes that health flows through community bonds. FlameBorn tokens create digital pathways for this ancient wisdom.",
|
14 |
+
"Like the village healer who serves the whole community, our healthcare workers are supported by the entire token ecosystem.",
|
15 |
+
"In African tradition, birth is celebrated by the whole village. Our birth registration tokens honor this communal joy.",
|
16 |
+
],
|
17 |
+
proverbs: [
|
18 |
+
"African wisdom says: 'When spider webs unite, they can tie up a lion.' Our small token contributions create powerful healthcare networks.",
|
19 |
+
"'The child who is not embraced by the village will burn it down to feel its warmth.' FlameBorn ensures every child is embraced through verified care.",
|
20 |
+
"'If you want to go fast, go alone. If you want to go far, go together.' Our tokenomics are designed for collective progress.",
|
21 |
+
],
|
22 |
+
}
|
23 |
+
|
24 |
+
function getRandomCulturalResponse(category: keyof typeof culturalResponses): string {
|
25 |
+
const responses = culturalResponses[category]
|
26 |
+
return responses[Math.floor(Math.random() * responses.length)]
|
27 |
+
}
|
28 |
+
|
29 |
+
function determineCulturalCategory(message: string): keyof typeof culturalResponses {
|
30 |
+
const lowerMessage = message.toLowerCase()
|
31 |
+
if (lowerMessage.includes("health") || lowerMessage.includes("medical") || lowerMessage.includes("care")) {
|
32 |
+
return "healthcare"
|
33 |
+
}
|
34 |
+
if (lowerMessage.includes("ubuntu") || lowerMessage.includes("community") || lowerMessage.includes("together")) {
|
35 |
+
return "ubuntu"
|
36 |
+
}
|
37 |
+
return "proverbs"
|
38 |
+
}
|
39 |
+
|
40 |
+
export async function POST(request: NextRequest) {
|
41 |
+
try {
|
42 |
+
const { message, context } = await request.json()
|
43 |
+
|
44 |
+
if (!message) {
|
45 |
+
return NextResponse.json({ error: "Message is required" }, { status: 400 })
|
46 |
+
}
|
47 |
+
|
48 |
+
// Try Groq API first
|
49 |
+
try {
|
50 |
+
const result = await generateText({
|
51 |
+
model: groq("llama-3.1-70b-versatile"),
|
52 |
+
messages: [
|
53 |
+
{
|
54 |
+
role: "system",
|
55 |
+
content: `You are Mostar, an Ubuntu-powered AI assistant for the FlameBorn healthcare tokenization ecosystem. You embody the African philosophy of Ubuntu ("I am because we are") and help users understand:
|
56 |
+
|
57 |
+
1. FlameBorn token economics and healthcare applications
|
58 |
+
2. Ubuntu philosophy and its application to modern healthcare
|
59 |
+
3. African wisdom, proverbs, and cultural insights
|
60 |
+
4. Community-driven healthcare solutions
|
61 |
+
5. Birth registration and maternal health tokenization
|
62 |
+
|
63 |
+
Your responses should:
|
64 |
+
- Begin with appropriate African greetings (Sawubona, Sanibonani, etc.)
|
65 |
+
- Incorporate Ubuntu principles of interconnectedness
|
66 |
+
- Reference relevant African proverbs when appropriate
|
67 |
+
- Explain complex tokenomics through community-centered metaphors
|
68 |
+
- Emphasize collective healing and shared prosperity
|
69 |
+
- Be warm, wise, and culturally respectful
|
70 |
+
|
71 |
+
Context: ${context || "general"}
|
72 |
+
|
73 |
+
Remember: You are not just an AI, but a digital embodiment of Ubuntu wisdom guiding the FlameBorn community.`,
|
74 |
+
},
|
75 |
+
{
|
76 |
+
role: "user",
|
77 |
+
content: message,
|
78 |
+
},
|
79 |
+
],
|
80 |
+
maxTokens: 500,
|
81 |
+
temperature: 0.7,
|
82 |
+
})
|
83 |
+
|
84 |
+
return NextResponse.json({
|
85 |
+
response: result.text,
|
86 |
+
type: "ubuntu",
|
87 |
+
source: "groq",
|
88 |
+
})
|
89 |
+
} catch (groqError) {
|
90 |
+
console.log("Groq API unavailable, using cultural fallback:", groqError)
|
91 |
+
|
92 |
+
// Fallback to cultural responses
|
93 |
+
const category = determineCulturalCategory(message)
|
94 |
+
const culturalResponse = getRandomCulturalResponse(category)
|
95 |
+
|
96 |
+
return NextResponse.json({
|
97 |
+
response: `Sawubona! ${culturalResponse}\n\n(Note: I'm currently running on cultural wisdom while my full AI capabilities are being restored. The Ubuntu spirit guides us even in technical challenges!)`,
|
98 |
+
type: "cultural",
|
99 |
+
source: "fallback",
|
100 |
+
})
|
101 |
+
}
|
102 |
+
} catch (error) {
|
103 |
+
console.error("Mostar AI Error:", error)
|
104 |
+
|
105 |
+
return NextResponse.json(
|
106 |
+
{
|
107 |
+
response:
|
108 |
+
"Ngiyaxolisa (I apologize). I am experiencing technical difficulties. Like the Ubuntu saying goes, 'When the spider webs unite, they can tie up a lion' - our community will help resolve this together. Please try again in a moment.",
|
109 |
+
type: "cultural",
|
110 |
+
source: "error",
|
111 |
+
},
|
112 |
+
{ status: 500 },
|
113 |
+
)
|
114 |
+
}
|
115 |
+
}
|
app/api/proverb-validation/route.ts
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { type NextRequest, NextResponse } from "next/server"
|
2 |
+
import { proverbValidator } from "@/lib/proverb-validator"
|
3 |
+
|
4 |
+
export async function POST(request: NextRequest) {
|
5 |
+
try {
|
6 |
+
const { proverb, action } = await request.json()
|
7 |
+
|
8 |
+
if (!proverb) {
|
9 |
+
return NextResponse.json({ error: "Proverb is required for cultural validation" }, { status: 400 })
|
10 |
+
}
|
11 |
+
|
12 |
+
// Validate proverb exists in database
|
13 |
+
const isValid = proverbValidator.validateProverb(proverb)
|
14 |
+
const proverbMeta = proverbValidator.getProverbMeta(proverb)
|
15 |
+
const proverbHash = proverbValidator.getProverbHash(proverb)
|
16 |
+
|
17 |
+
if (!isValid || !proverbMeta) {
|
18 |
+
return NextResponse.json({
|
19 |
+
isValid: false,
|
20 |
+
message: "Proverb not found in verified African wisdom database",
|
21 |
+
suggestedProverb: proverbValidator.getRandomProverb(),
|
22 |
+
})
|
23 |
+
}
|
24 |
+
|
25 |
+
// If action provided, validate cultural context
|
26 |
+
let culturalValidation = null
|
27 |
+
if (action) {
|
28 |
+
culturalValidation = proverbValidator.validateCulturalContext(action, proverb)
|
29 |
+
}
|
30 |
+
|
31 |
+
return NextResponse.json({
|
32 |
+
isValid: true,
|
33 |
+
proverb: proverbMeta,
|
34 |
+
proverbHash,
|
35 |
+
culturalValidation,
|
36 |
+
message: `Ubuntu wisdom validated: "${proverbMeta.translated_meaning}" from ${proverbMeta.country}`,
|
37 |
+
})
|
38 |
+
} catch (error) {
|
39 |
+
console.error("Proverb validation error:", error)
|
40 |
+
return NextResponse.json({ error: "Failed to validate proverb" }, { status: 500 })
|
41 |
+
}
|
42 |
+
}
|
43 |
+
|
44 |
+
export async function GET(request: NextRequest) {
|
45 |
+
try {
|
46 |
+
const { searchParams } = new URL(request.url)
|
47 |
+
const region = searchParams.get("region")
|
48 |
+
const language = searchParams.get("language")
|
49 |
+
const country = searchParams.get("country")
|
50 |
+
const random = searchParams.get("random")
|
51 |
+
|
52 |
+
if (random === "true") {
|
53 |
+
return NextResponse.json({
|
54 |
+
proverb: proverbValidator.getRandomProverb(),
|
55 |
+
})
|
56 |
+
}
|
57 |
+
|
58 |
+
const filters: any = {}
|
59 |
+
if (region) filters.region = region
|
60 |
+
if (language) filters.language = language
|
61 |
+
if (country) filters.country = country
|
62 |
+
|
63 |
+
const proverbs = proverbValidator.searchProverbs(filters)
|
64 |
+
|
65 |
+
return NextResponse.json({
|
66 |
+
proverbs,
|
67 |
+
total: proverbs.length,
|
68 |
+
availableRegions: proverbValidator.getAvailableRegions(),
|
69 |
+
availableLanguages: proverbValidator.getAvailableLanguages(),
|
70 |
+
availableCountries: proverbValidator.getAvailableCountries(),
|
71 |
+
})
|
72 |
+
} catch (error) {
|
73 |
+
console.error("Proverb search error:", error)
|
74 |
+
return NextResponse.json({ error: "Failed to search proverbs" }, { status: 500 })
|
75 |
+
}
|
76 |
+
}
|
app/api/stats/route.ts
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse } from "next/server"
|
2 |
+
import { DatabaseService } from "@/lib/database"
|
3 |
+
|
4 |
+
export async function GET() {
|
5 |
+
try {
|
6 |
+
// Get all users to calculate statistics
|
7 |
+
const allUsers = await DatabaseService.getAllUsers()
|
8 |
+
|
9 |
+
// Calculate verified CHWs (Community Health Workers)
|
10 |
+
const verifiedCHWs = allUsers.filter(
|
11 |
+
(user) => user.raw_json.type === "healer" && user.raw_json.verificationStatus === "verified",
|
12 |
+
).length
|
13 |
+
|
14 |
+
// Calculate active Guardians
|
15 |
+
const activeGuardians = allUsers.filter((user) => user.raw_json.type === "guardian").length
|
16 |
+
|
17 |
+
// Calculate total support (sum of all Guardian contributions)
|
18 |
+
const totalSupport = allUsers
|
19 |
+
.filter((user) => user.raw_json.type === "guardian")
|
20 |
+
.reduce((sum, user) => {
|
21 |
+
const contribution = user.raw_json.contributionAmount
|
22 |
+
const amount = typeof contribution === "string" ? Number.parseFloat(contribution) : contribution
|
23 |
+
return sum + (amount || 0)
|
24 |
+
}, 0)
|
25 |
+
|
26 |
+
// Calculate lives impacted (sum of patients served by verified healers)
|
27 |
+
const livesImpacted = allUsers
|
28 |
+
.filter((user) => user.raw_json.type === "healer" && user.raw_json.verificationStatus === "verified")
|
29 |
+
.reduce((sum, user) => {
|
30 |
+
return sum + (user.raw_json.impact?.patientsServed || 0)
|
31 |
+
}, 0)
|
32 |
+
|
33 |
+
// Total users count
|
34 |
+
const totalUsers = allUsers.length
|
35 |
+
|
36 |
+
const stats = {
|
37 |
+
verifiedCHWs,
|
38 |
+
activeGuardians,
|
39 |
+
totalSupport,
|
40 |
+
livesImpacted,
|
41 |
+
totalUsers,
|
42 |
+
lastUpdated: new Date().toISOString(),
|
43 |
+
}
|
44 |
+
|
45 |
+
return NextResponse.json(stats, {
|
46 |
+
headers: {
|
47 |
+
"Cache-Control": "s-maxage=60, stale-while-revalidate=300",
|
48 |
+
},
|
49 |
+
})
|
50 |
+
} catch (error) {
|
51 |
+
console.error("Error fetching stats:", error)
|
52 |
+
return NextResponse.json({ error: "Failed to fetch statistics" }, { status: 500 })
|
53 |
+
}
|
54 |
+
}
|
app/api/users/[id]/route.ts
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { type NextRequest, NextResponse } from "next/server"
|
2 |
+
import { DatabaseService } from "@/lib/database"
|
3 |
+
|
4 |
+
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
|
5 |
+
try {
|
6 |
+
const user = await DatabaseService.getUserById(params.id)
|
7 |
+
if (!user) {
|
8 |
+
return NextResponse.json({ error: "User not found" }, { status: 404 })
|
9 |
+
}
|
10 |
+
return NextResponse.json({ user })
|
11 |
+
} catch (error) {
|
12 |
+
console.error("Error in GET /api/users/[id]:", error)
|
13 |
+
return NextResponse.json({ error: "Failed to fetch user" }, { status: 500 })
|
14 |
+
}
|
15 |
+
}
|
16 |
+
|
17 |
+
export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
|
18 |
+
try {
|
19 |
+
const body = await request.json()
|
20 |
+
const { name, email, profileData } = body
|
21 |
+
|
22 |
+
const user = await DatabaseService.updateUser(params.id, {
|
23 |
+
name,
|
24 |
+
email,
|
25 |
+
profileData,
|
26 |
+
})
|
27 |
+
|
28 |
+
if (!user) {
|
29 |
+
return NextResponse.json({ error: "User not found or update failed" }, { status: 404 })
|
30 |
+
}
|
31 |
+
|
32 |
+
return NextResponse.json({ user })
|
33 |
+
} catch (error) {
|
34 |
+
console.error("Error in PUT /api/users/[id]:", error)
|
35 |
+
return NextResponse.json({ error: "Failed to update user" }, { status: 500 })
|
36 |
+
}
|
37 |
+
}
|
38 |
+
|
39 |
+
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
|
40 |
+
try {
|
41 |
+
const success = await DatabaseService.deleteUser(params.id)
|
42 |
+
if (!success) {
|
43 |
+
return NextResponse.json({ error: "User not found or delete failed" }, { status: 404 })
|
44 |
+
}
|
45 |
+
return NextResponse.json({ message: "User deleted successfully" })
|
46 |
+
} catch (error) {
|
47 |
+
console.error("Error in DELETE /api/users/[id]:", error)
|
48 |
+
return NextResponse.json({ error: "Failed to delete user" }, { status: 500 })
|
49 |
+
}
|
50 |
+
}
|
app/api/users/route.ts
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { type NextRequest, NextResponse } from "next/server"
|
2 |
+
import { DatabaseService } from "@/lib/database"
|
3 |
+
import { v4 as uuidv4 } from "uuid"
|
4 |
+
|
5 |
+
export async function GET(request: NextRequest) {
|
6 |
+
try {
|
7 |
+
const { searchParams } = new URL(request.url)
|
8 |
+
const email = searchParams.get("email")
|
9 |
+
const type = searchParams.get("type") as "guardian" | "healer" | null
|
10 |
+
|
11 |
+
if (email) {
|
12 |
+
const user = await DatabaseService.getUserByEmail(email)
|
13 |
+
return NextResponse.json({ user })
|
14 |
+
}
|
15 |
+
|
16 |
+
if (type) {
|
17 |
+
const users = await DatabaseService.getUsersByType(type)
|
18 |
+
return NextResponse.json({ users })
|
19 |
+
}
|
20 |
+
|
21 |
+
const users = await DatabaseService.getAllUsers()
|
22 |
+
return NextResponse.json({ users })
|
23 |
+
} catch (error) {
|
24 |
+
console.error("Error in GET /api/users:", error)
|
25 |
+
return NextResponse.json({ error: "Failed to fetch users" }, { status: 500 })
|
26 |
+
}
|
27 |
+
}
|
28 |
+
|
29 |
+
export async function POST(request: NextRequest) {
|
30 |
+
try {
|
31 |
+
const body = await request.json()
|
32 |
+
const { name, email, type, profileData } = body
|
33 |
+
|
34 |
+
if (!name || !email || !type) {
|
35 |
+
return NextResponse.json({ error: "Missing required fields" }, { status: 400 })
|
36 |
+
}
|
37 |
+
|
38 |
+
// Check if user already exists
|
39 |
+
const existingUser = await DatabaseService.getUserByEmail(email)
|
40 |
+
if (existingUser) {
|
41 |
+
return NextResponse.json({ error: "User already exists" }, { status: 409 })
|
42 |
+
}
|
43 |
+
|
44 |
+
const userId = uuidv4()
|
45 |
+
const user = await DatabaseService.createUser({
|
46 |
+
id: userId,
|
47 |
+
name,
|
48 |
+
email,
|
49 |
+
type,
|
50 |
+
profileData,
|
51 |
+
})
|
52 |
+
|
53 |
+
if (!user) {
|
54 |
+
return NextResponse.json({ error: "Failed to create user" }, { status: 500 })
|
55 |
+
}
|
56 |
+
|
57 |
+
return NextResponse.json({ user }, { status: 201 })
|
58 |
+
} catch (error) {
|
59 |
+
console.error("Error in POST /api/users:", error)
|
60 |
+
return NextResponse.json({ error: "Failed to create user" }, { status: 500 })
|
61 |
+
}
|
62 |
+
}
|
app/api/youth/complete-module/route.ts
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { type NextRequest, NextResponse } from "next/server"
|
2 |
+
import { neon } from "@neondatabase/serverless"
|
3 |
+
import { calculateFLBReward } from "@/lib/gamification"
|
4 |
+
|
5 |
+
const sql = neon(process.env.DATABASE_URL!)
|
6 |
+
|
7 |
+
export async function POST(request: NextRequest) {
|
8 |
+
try {
|
9 |
+
const { walletAddress, moduleId, score, timeTaken } = await request.json()
|
10 |
+
|
11 |
+
if (!walletAddress || !moduleId || score === undefined) {
|
12 |
+
return NextResponse.json({ error: "Missing required fields" }, { status: 400 })
|
13 |
+
}
|
14 |
+
|
15 |
+
// Get module details
|
16 |
+
const moduleResult = await sql`
|
17 |
+
SELECT * FROM learning_modules WHERE id = ${moduleId}
|
18 |
+
`
|
19 |
+
|
20 |
+
if (moduleResult.length === 0) {
|
21 |
+
return NextResponse.json({ error: "Module not found" }, { status: 404 })
|
22 |
+
}
|
23 |
+
|
24 |
+
const module = moduleResult[0]
|
25 |
+
|
26 |
+
// Get user's current streak
|
27 |
+
const userResult = await sql`
|
28 |
+
SELECT streak FROM user_profiles WHERE wallet_address = ${walletAddress}
|
29 |
+
`
|
30 |
+
|
31 |
+
const currentStreak = userResult.length > 0 ? userResult[0].streak : 0
|
32 |
+
|
33 |
+
// Calculate rewards
|
34 |
+
const flbReward = calculateFLBReward(module.difficulty, timeTaken || 30, score, currentStreak)
|
35 |
+
const xpReward = Math.floor(flbReward * 1.5)
|
36 |
+
|
37 |
+
// Record module completion
|
38 |
+
await sql`
|
39 |
+
INSERT INTO user_completed_modules (user_id, module_id, score, flb_earned, xp_earned, completed_at)
|
40 |
+
VALUES (
|
41 |
+
(SELECT id FROM user_profiles WHERE wallet_address = ${walletAddress}),
|
42 |
+
${moduleId},
|
43 |
+
${score},
|
44 |
+
${flbReward},
|
45 |
+
${xpReward},
|
46 |
+
NOW()
|
47 |
+
)
|
48 |
+
ON CONFLICT (user_id, module_id) DO UPDATE SET
|
49 |
+
score = EXCLUDED.score,
|
50 |
+
flb_earned = EXCLUDED.flb_earned,
|
51 |
+
xp_earned = EXCLUDED.xp_earned,
|
52 |
+
completed_at = EXCLUDED.completed_at
|
53 |
+
`
|
54 |
+
|
55 |
+
// Update user profile
|
56 |
+
await sql`
|
57 |
+
UPDATE user_profiles
|
58 |
+
SET
|
59 |
+
xp = xp + ${xpReward},
|
60 |
+
flb_balance = flb_balance + ${flbReward},
|
61 |
+
level = FLOOR((xp + ${xpReward}) / 1000) + 1,
|
62 |
+
updated_at = NOW()
|
63 |
+
WHERE wallet_address = ${walletAddress}
|
64 |
+
`
|
65 |
+
|
66 |
+
return NextResponse.json({
|
67 |
+
success: true,
|
68 |
+
rewards: {
|
69 |
+
flb: flbReward,
|
70 |
+
xp: xpReward,
|
71 |
+
},
|
72 |
+
message: "Module completed successfully",
|
73 |
+
})
|
74 |
+
} catch (error) {
|
75 |
+
console.error("Error completing module:", error)
|
76 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 })
|
77 |
+
}
|
78 |
+
}
|
app/api/youth/progress/route.ts
ADDED
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { type NextRequest, NextResponse } from "next/server"
|
2 |
+
import { neon } from "@neondatabase/serverless"
|
3 |
+
|
4 |
+
const sql = neon(process.env.DATABASE_URL!)
|
5 |
+
|
6 |
+
export async function GET(request: NextRequest) {
|
7 |
+
try {
|
8 |
+
const { searchParams } = new URL(request.url)
|
9 |
+
const walletAddress = searchParams.get("wallet")
|
10 |
+
|
11 |
+
if (!walletAddress) {
|
12 |
+
return NextResponse.json({ error: "Wallet address required" }, { status: 400 })
|
13 |
+
}
|
14 |
+
|
15 |
+
// Get user profile
|
16 |
+
const userResult = await sql`
|
17 |
+
SELECT * FROM user_profiles WHERE wallet_address = ${walletAddress}
|
18 |
+
`
|
19 |
+
|
20 |
+
if (userResult.length === 0) {
|
21 |
+
return NextResponse.json({ error: "User not found" }, { status: 404 })
|
22 |
+
}
|
23 |
+
|
24 |
+
const user = userResult[0]
|
25 |
+
|
26 |
+
// Get completed modules
|
27 |
+
const completedModules = await sql`
|
28 |
+
SELECT ucm.*, lm.title, lm.category
|
29 |
+
FROM user_completed_modules ucm
|
30 |
+
JOIN learning_modules lm ON ucm.module_id = lm.id
|
31 |
+
WHERE ucm.user_id = ${user.id}
|
32 |
+
ORDER BY ucm.completed_at DESC
|
33 |
+
`
|
34 |
+
|
35 |
+
// Get achievements
|
36 |
+
const achievements = await sql`
|
37 |
+
SELECT ua.*, a.title, a.description, a.icon
|
38 |
+
FROM user_achievements ua
|
39 |
+
JOIN achievements a ON ua.achievement_id = a.id
|
40 |
+
WHERE ua.user_id = ${user.id}
|
41 |
+
ORDER BY ua.achieved_at DESC
|
42 |
+
`
|
43 |
+
|
44 |
+
// Get daily challenges
|
45 |
+
const dailyChallenges = await sql`
|
46 |
+
SELECT c.*, COALESCE(ucc.completed_at IS NOT NULL, false) as completed
|
47 |
+
FROM challenges c
|
48 |
+
LEFT JOIN user_completed_challenges ucc ON c.id = ucc.challenge_id AND ucc.user_id = ${user.id}
|
49 |
+
WHERE c.type = 'daily' AND c.active = true
|
50 |
+
ORDER BY c.created_at DESC
|
51 |
+
LIMIT 5
|
52 |
+
`
|
53 |
+
|
54 |
+
// Calculate next level XP
|
55 |
+
const nextLevelXp = user.level * 1000 * 1.2 - user.xp
|
56 |
+
|
57 |
+
return NextResponse.json({
|
58 |
+
user: {
|
59 |
+
...user,
|
60 |
+
nextLevelXp: Math.max(0, nextLevelXp),
|
61 |
+
},
|
62 |
+
completedModules,
|
63 |
+
achievements,
|
64 |
+
dailyChallenges,
|
65 |
+
stats: {
|
66 |
+
totalModules: completedModules.length,
|
67 |
+
totalFLBEarned: completedModules.reduce(
|
68 |
+
(sum: number, module: any) => sum + Number.parseFloat(module.flb_earned),
|
69 |
+
0,
|
70 |
+
),
|
71 |
+
totalXPEarned: completedModules.reduce(
|
72 |
+
(sum: number, module: any) => sum + Number.parseInt(module.xp_earned),
|
73 |
+
0,
|
74 |
+
),
|
75 |
+
achievementsCount: achievements.length,
|
76 |
+
},
|
77 |
+
})
|
78 |
+
} catch (error) {
|
79 |
+
console.error("Error fetching user progress:", error)
|
80 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 })
|
81 |
+
}
|
82 |
+
}
|
app/baby-nft/page.tsx
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { BabyNFTDashboard } from "@/components/baby-nft/baby-nft-dashboard"
|
2 |
+
|
3 |
+
export default function BabyNFTPage() {
|
4 |
+
return <BabyNFTDashboard />
|
5 |
+
}
|
app/become-guardian/page.tsx
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Metadata } from "next"
|
2 |
+
import Image from "next/image"
|
3 |
+
import Link from "next/link"
|
4 |
+
|
5 |
+
import { Button } from "@/components/ui/button"
|
6 |
+
|
7 |
+
export const metadata: Metadata = {
|
8 |
+
title: "Become a Guardian - Safe Haven",
|
9 |
+
description: "Join Safe Haven as a Guardian and help protect children online.",
|
10 |
+
}
|
11 |
+
|
12 |
+
const BecomeGuardianPage = () => {
|
13 |
+
return (
|
14 |
+
<div className="container mx-auto py-12">
|
15 |
+
<section className="text-center mb-12">
|
16 |
+
<h1 className="text-4xl font-bold text-gray-800 mb-4">Become a Guardian</h1>
|
17 |
+
<p className="text-lg text-gray-600">
|
18 |
+
Join our community of dedicated individuals committed to creating a safer online environment for children.
|
19 |
+
</p>
|
20 |
+
</section>
|
21 |
+
|
22 |
+
<section className="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
|
23 |
+
<div>
|
24 |
+
<Image
|
25 |
+
src="/guardian-hero.png"
|
26 |
+
alt="Guardian Hero"
|
27 |
+
width={500}
|
28 |
+
height={300}
|
29 |
+
className="rounded-lg shadow-md"
|
30 |
+
/>
|
31 |
+
</div>
|
32 |
+
<div>
|
33 |
+
<h2 className="text-2xl font-semibold text-gray-700 mb-4">Why Become a Guardian?</h2>
|
34 |
+
<ul className="list-disc list-inside text-gray-600">
|
35 |
+
<li>Make a real difference in the lives of children.</li>
|
36 |
+
<li>Help protect them from online threats and harmful content.</li>
|
37 |
+
<li>Join a supportive community of like-minded individuals.</li>
|
38 |
+
<li>Gain valuable skills and knowledge in online safety.</li>
|
39 |
+
</ul>
|
40 |
+
</div>
|
41 |
+
</section>
|
42 |
+
|
43 |
+
<section className="text-center mt-12">
|
44 |
+
<h2 className="text-2xl font-semibold text-gray-700 mb-4">Ready to Get Started?</h2>
|
45 |
+
<p className="text-lg text-gray-600 mb-8">Sign up today and begin your journey as a Safe Haven Guardian.</p>
|
46 |
+
<Link href="/register/guardian">
|
47 |
+
<Button className="bg-guardian hover:bg-flame text-black px-8 py-4 text-lg rounded-md transition-all hover:shadow-[0_0_20px_rgba(0,255,160,0.3)]">
|
48 |
+
Become a Guardian Today
|
49 |
+
</Button>
|
50 |
+
</Link>
|
51 |
+
</section>
|
52 |
+
</div>
|
53 |
+
)
|
54 |
+
}
|
55 |
+
|
56 |
+
export default BecomeGuardianPage
|
app/clientLayout.tsx
ADDED
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import type React from "react"
|
4 |
+
import { Inter } from "next/font/google"
|
5 |
+
import "./globals.css"
|
6 |
+
import Link from "next/link"
|
7 |
+
import { Shield, UserPlus, Home, Menu, X, BookOpen, Activity } from "lucide-react"
|
8 |
+
import { useState, useEffect } from "react"
|
9 |
+
import { ThemeProvider } from "@/components/theme-provider"
|
10 |
+
import { usePathname } from "next/navigation"
|
11 |
+
import { SocialLinks } from "@/components/social-links"
|
12 |
+
|
13 |
+
const inter = Inter({ subsets: ["latin"] })
|
14 |
+
|
15 |
+
export default function ClientLayout({
|
16 |
+
children,
|
17 |
+
}: {
|
18 |
+
children: React.ReactNode
|
19 |
+
}) {
|
20 |
+
return (
|
21 |
+
<html lang="en">
|
22 |
+
<head>
|
23 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
|
24 |
+
</head>
|
25 |
+
<body className={inter.className}>
|
26 |
+
<ThemeProvider attribute="class" defaultTheme="dark">
|
27 |
+
<div className="min-h-screen bg-black">
|
28 |
+
<MobileNavigation />
|
29 |
+
<main className="pt-16">{children}</main>
|
30 |
+
</div>
|
31 |
+
</ThemeProvider>
|
32 |
+
</body>
|
33 |
+
</html>
|
34 |
+
)
|
35 |
+
}
|
36 |
+
|
37 |
+
function MobileNavigation() {
|
38 |
+
const [isOpen, setIsOpen] = useState(false)
|
39 |
+
const pathname = usePathname()
|
40 |
+
|
41 |
+
const toggleMenu = () => {
|
42 |
+
setIsOpen(!isOpen)
|
43 |
+
}
|
44 |
+
|
45 |
+
// Close menu when route changes
|
46 |
+
useEffect(() => {
|
47 |
+
setIsOpen(false)
|
48 |
+
}, [pathname])
|
49 |
+
|
50 |
+
const isActive = (path: string) => {
|
51 |
+
return pathname === path ? "text-flame" : "text-gray-300 hover:text-flame-red"
|
52 |
+
}
|
53 |
+
|
54 |
+
return (
|
55 |
+
<header className="fixed top-0 left-0 right-0 z-50 border-b border-gray-800 bg-black/90 backdrop-blur-sm">
|
56 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
57 |
+
<div className="flex justify-between items-center h-16">
|
58 |
+
<div className="flex items-center">
|
59 |
+
<Link href="/" className="text-2xl font-bold text-flame-red">
|
60 |
+
FLAMEBORN
|
61 |
+
</Link>
|
62 |
+
</div>
|
63 |
+
|
64 |
+
{/* Desktop Navigation */}
|
65 |
+
<nav className="hidden md:flex items-center space-x-4">
|
66 |
+
<Link href="/" className={`flex items-center ${isActive("/")}`}>
|
67 |
+
<Home className="h-4 w-4 mr-1" />
|
68 |
+
<span>Home</span>
|
69 |
+
</Link>
|
70 |
+
<Link href="/guardians-sanctuary" className={`flex items-center ${isActive("/guardians-sanctuary")}`}>
|
71 |
+
<Shield className="h-4 w-4 mr-1" />
|
72 |
+
<span>Sanctuary</span>
|
73 |
+
</Link>
|
74 |
+
<Link href="/flameborn-journey" className={`flex items-center ${isActive("/flameborn-journey")}`}>
|
75 |
+
<BookOpen className="h-4 w-4 mr-1" />
|
76 |
+
<span>Journey</span>
|
77 |
+
</Link>
|
78 |
+
<Link href="/community-pulse" className={`flex items-center ${isActive("/community-pulse")}`}>
|
79 |
+
<Activity className="h-4 w-4 mr-1" />
|
80 |
+
<span>Pulse</span>
|
81 |
+
</Link>
|
82 |
+
<Link href="/register-chw" className={`flex items-center ${isActive("/register-chw")}`}>
|
83 |
+
<UserPlus className="h-4 w-4 mr-1" />
|
84 |
+
<span>Register</span>
|
85 |
+
</Link>
|
86 |
+
<div className="ml-4 pl-4 border-l border-gray-700">
|
87 |
+
<SocialLinks iconSize={16} />
|
88 |
+
</div>
|
89 |
+
</nav>
|
90 |
+
|
91 |
+
{/* Mobile Menu Button */}
|
92 |
+
<button
|
93 |
+
className="md:hidden text-gray-300 hover:text-flame-red"
|
94 |
+
onClick={toggleMenu}
|
95 |
+
aria-label={isOpen ? "Close menu" : "Open menu"}
|
96 |
+
>
|
97 |
+
{isOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
98 |
+
</button>
|
99 |
+
</div>
|
100 |
+
</div>
|
101 |
+
|
102 |
+
{/* Mobile Menu */}
|
103 |
+
{isOpen && (
|
104 |
+
<div className="md:hidden bg-black/95 border-b border-gray-800">
|
105 |
+
<div className="px-2 pt-2 pb-3 space-y-1">
|
106 |
+
<Link
|
107 |
+
href="/"
|
108 |
+
className={`block px-3 py-4 text-base font-medium rounded-md ${isActive("/").replace("flex", "")}`}
|
109 |
+
>
|
110 |
+
<Home className="h-5 w-5 mr-2 inline-block" />
|
111 |
+
Home
|
112 |
+
</Link>
|
113 |
+
<Link
|
114 |
+
href="/guardians-sanctuary"
|
115 |
+
className={`block px-3 py-4 text-base font-medium rounded-md ${isActive("/guardians-sanctuary").replace("flex", "")}`}
|
116 |
+
>
|
117 |
+
<Shield className="h-5 w-5 mr-2 inline-block" />
|
118 |
+
Guardian's Sanctuary
|
119 |
+
</Link>
|
120 |
+
<Link
|
121 |
+
href="/flameborn-journey"
|
122 |
+
className={`block px-3 py-4 text-base font-medium rounded-md ${isActive("/flameborn-journey").replace("flex", "")}`}
|
123 |
+
>
|
124 |
+
<BookOpen className="h-5 w-5 mr-2 inline-block" />
|
125 |
+
Flameborn's Journey
|
126 |
+
</Link>
|
127 |
+
<Link
|
128 |
+
href="/community-pulse"
|
129 |
+
className={`block px-3 py-4 text-base font-medium rounded-md ${isActive("/community-pulse").replace("flex", "")}`}
|
130 |
+
>
|
131 |
+
<Activity className="h-5 w-5 mr-2 inline-block" />
|
132 |
+
Pulse of Community
|
133 |
+
</Link>
|
134 |
+
<Link
|
135 |
+
href="/register-chw"
|
136 |
+
className={`block px-3 py-4 text-base font-medium rounded-md ${isActive("/register-chw").replace("flex", "")}`}
|
137 |
+
>
|
138 |
+
<UserPlus className="h-5 w-5 mr-2 inline-block" />
|
139 |
+
Register
|
140 |
+
</Link>
|
141 |
+
</div>
|
142 |
+
</div>
|
143 |
+
)}
|
144 |
+
</header>
|
145 |
+
)
|
146 |
+
}
|
app/community-pulse/page.tsx
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { Suspense } from "react"
|
4 |
+
import { PulseHero } from "@/components/pulse/pulse-hero"
|
5 |
+
import { PulseActivity } from "@/components/pulse/pulse-activity"
|
6 |
+
import { PulseStats } from "@/components/pulse/pulse-stats"
|
7 |
+
import { PulseMap } from "@/components/pulse/pulse-map"
|
8 |
+
import { LoadingState } from "@/components/loading-state"
|
9 |
+
import dynamic from "next/dynamic"
|
10 |
+
|
11 |
+
const ParticleBackground = dynamic(
|
12 |
+
() => import("@/components/particle-background").then((mod) => ({ default: mod.ParticleBackground })),
|
13 |
+
{
|
14 |
+
ssr: false,
|
15 |
+
loading: () => <div className="fixed inset-0 bg-black" />,
|
16 |
+
},
|
17 |
+
)
|
18 |
+
|
19 |
+
export default function CommunityPulse() {
|
20 |
+
return (
|
21 |
+
<div className="min-h-screen bg-dusk relative">
|
22 |
+
<ParticleBackground />
|
23 |
+
<div className="relative z-10">
|
24 |
+
<Suspense fallback={<LoadingState message="Loading community pulse..." />}>
|
25 |
+
<PulseHero />
|
26 |
+
</Suspense>
|
27 |
+
|
28 |
+
<div className="max-w-7xl mx-auto px-4 py-8 space-y-8">
|
29 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
30 |
+
<div className="lg:col-span-2">
|
31 |
+
<Suspense fallback={<LoadingState message="Loading activity feed..." />}>
|
32 |
+
<PulseActivity />
|
33 |
+
</Suspense>
|
34 |
+
</div>
|
35 |
+
<div>
|
36 |
+
<Suspense fallback={<LoadingState message="Loading statistics..." />}>
|
37 |
+
<PulseStats />
|
38 |
+
</Suspense>
|
39 |
+
</div>
|
40 |
+
</div>
|
41 |
+
|
42 |
+
<Suspense fallback={<LoadingState message="Loading community map..." />}>
|
43 |
+
<PulseMap />
|
44 |
+
</Suspense>
|
45 |
+
</div>
|
46 |
+
</div>
|
47 |
+
</div>
|
48 |
+
)
|
49 |
+
}
|
app/dashboard/page.tsx
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { UserDashboard } from "@/components/user-dashboard"
|
2 |
+
|
3 |
+
export default function DashboardPage() {
|
4 |
+
return <UserDashboard />
|
5 |
+
}
|
app/debug/data-entry/page.tsx
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { TestDataEntry } from "@/components/test-data-entry"
|
2 |
+
import { Button } from "@/components/ui/button"
|
3 |
+
import Link from "next/link"
|
4 |
+
|
5 |
+
export default function DebugDataEntryPage() {
|
6 |
+
return (
|
7 |
+
<div className="min-h-screen bg-black text-white p-4">
|
8 |
+
<header className="flex justify-between items-center mb-8">
|
9 |
+
<Link href="/">
|
10 |
+
<h1 className="text-4xl font-bold flame-text">FLAMEBORN</h1>
|
11 |
+
</Link>
|
12 |
+
<Link href="/debug">
|
13 |
+
<Button variant="outline">Back to Debug</Button>
|
14 |
+
</Link>
|
15 |
+
</header>
|
16 |
+
|
17 |
+
<div className="max-w-4xl mx-auto">
|
18 |
+
<h2 className="text-3xl font-bold mb-8 text-center">Data Entry Testing</h2>
|
19 |
+
<p className="text-center mb-8 text-gray-400">
|
20 |
+
Use this page to test data entry functionality without mock data. Any data entered here will be stored in
|
21 |
+
memory for the current session.
|
22 |
+
</p>
|
23 |
+
|
24 |
+
<TestDataEntry />
|
25 |
+
|
26 |
+
<div className="mt-8 flex justify-center">
|
27 |
+
<Link href="/">
|
28 |
+
<Button className="flame-button">Return to Home</Button>
|
29 |
+
</Link>
|
30 |
+
</div>
|
31 |
+
</div>
|
32 |
+
</div>
|
33 |
+
)
|
34 |
+
}
|
app/debug/page.tsx
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ContractStatus } from "@/components/contract-status"
|
2 |
+
import { WalletConnector } from "@/components/wallet-connector"
|
3 |
+
import { Button } from "@/components/ui/button"
|
4 |
+
import Link from "next/link"
|
5 |
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
6 |
+
|
7 |
+
export default function DebugPage() {
|
8 |
+
return (
|
9 |
+
<div className="min-h-screen bg-black text-white p-4">
|
10 |
+
<header className="flex justify-between items-center mb-8">
|
11 |
+
<Link href="/">
|
12 |
+
<h1 className="text-4xl font-bold flame-text">FLAMEBORN</h1>
|
13 |
+
</Link>
|
14 |
+
<WalletConnector />
|
15 |
+
</header>
|
16 |
+
|
17 |
+
<div className="max-w-4xl mx-auto">
|
18 |
+
<h2 className="text-3xl font-bold mb-8 text-center">Debug Dashboard</h2>
|
19 |
+
|
20 |
+
<div className="grid gap-6 md:grid-cols-2">
|
21 |
+
<ContractStatus />
|
22 |
+
<Card className="bg-ash-gray/30 border border-gray-700">
|
23 |
+
<CardHeader>
|
24 |
+
<CardTitle className="flame-text">Debug Tools</CardTitle>
|
25 |
+
</CardHeader>
|
26 |
+
<CardContent>
|
27 |
+
<div className="space-y-4">
|
28 |
+
<Link href="/debug/data-entry">
|
29 |
+
<Button className="w-full">Data Entry Testing</Button>
|
30 |
+
</Link>
|
31 |
+
</div>
|
32 |
+
</CardContent>
|
33 |
+
</Card>
|
34 |
+
</div>
|
35 |
+
|
36 |
+
<div className="mt-8 flex justify-center">
|
37 |
+
<Link href="/">
|
38 |
+
<Button className="flame-button">Return to Home</Button>
|
39 |
+
</Link>
|
40 |
+
</div>
|
41 |
+
</div>
|
42 |
+
</div>
|
43 |
+
)
|
44 |
+
}
|
app/error.tsx
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useEffect } from "react"
|
4 |
+
import { Button } from "@/components/ui/button"
|
5 |
+
import Link from "next/link"
|
6 |
+
|
7 |
+
export default function Error({
|
8 |
+
error,
|
9 |
+
reset,
|
10 |
+
}: {
|
11 |
+
error: Error & { digest?: string }
|
12 |
+
reset: () => void
|
13 |
+
}) {
|
14 |
+
useEffect(() => {
|
15 |
+
// Log the error to an error reporting service
|
16 |
+
console.error("Application error:", error)
|
17 |
+
}, [error])
|
18 |
+
|
19 |
+
return (
|
20 |
+
<div className="min-h-screen bg-dusk flex items-center justify-center p-4">
|
21 |
+
<div className="max-w-md w-full p-6 bg-ember rounded-lg border border-flame-red text-center">
|
22 |
+
<h2 className="text-2xl font-heading text-flame-red mb-4">Something went wrong</h2>
|
23 |
+
<p className="text-white mb-6">
|
24 |
+
We apologize for the inconvenience. The Flameborn system encountered an unexpected error.
|
25 |
+
</p>
|
26 |
+
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
27 |
+
<Button onClick={reset} className="flame-button">
|
28 |
+
Try Again
|
29 |
+
</Button>
|
30 |
+
<Link href="/">
|
31 |
+
<Button variant="outline" className="w-full sm:w-auto">
|
32 |
+
Return Home
|
33 |
+
</Button>
|
34 |
+
</Link>
|
35 |
+
</div>
|
36 |
+
</div>
|
37 |
+
</div>
|
38 |
+
)
|
39 |
+
}
|
app/flameborn-journey/page.tsx
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { Suspense } from "react"
|
4 |
+
import { JourneyHero } from "@/components/journey/journey-hero"
|
5 |
+
import { JourneyFeatured } from "@/components/journey/journey-featured"
|
6 |
+
import { JourneyPosts } from "@/components/journey/journey-posts"
|
7 |
+
import { JourneySidebar } from "@/components/journey/journey-sidebar"
|
8 |
+
import { LoadingState } from "@/components/loading-state"
|
9 |
+
import dynamic from "next/dynamic"
|
10 |
+
|
11 |
+
const ParticleBackground = dynamic(
|
12 |
+
() => import("@/components/particle-background").then((mod) => ({ default: mod.ParticleBackground })),
|
13 |
+
{
|
14 |
+
ssr: false,
|
15 |
+
loading: () => <div className="fixed inset-0 bg-black" />,
|
16 |
+
},
|
17 |
+
)
|
18 |
+
|
19 |
+
export default function FlamebornJourney() {
|
20 |
+
return (
|
21 |
+
<div className="min-h-screen bg-dusk relative">
|
22 |
+
<ParticleBackground />
|
23 |
+
<div className="relative z-10">
|
24 |
+
<Suspense fallback={<LoadingState message="Loading journey..." />}>
|
25 |
+
<JourneyHero />
|
26 |
+
</Suspense>
|
27 |
+
|
28 |
+
<div className="max-w-7xl mx-auto px-4 py-8">
|
29 |
+
<Suspense fallback={<LoadingState message="Loading featured content..." />}>
|
30 |
+
<JourneyFeatured />
|
31 |
+
</Suspense>
|
32 |
+
|
33 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mt-8">
|
34 |
+
<div className="lg:col-span-2">
|
35 |
+
<Suspense fallback={<LoadingState message="Loading posts..." />}>
|
36 |
+
<JourneyPosts />
|
37 |
+
</Suspense>
|
38 |
+
</div>
|
39 |
+
<div>
|
40 |
+
<Suspense fallback={<LoadingState message="Loading sidebar..." />}>
|
41 |
+
<JourneySidebar />
|
42 |
+
</Suspense>
|
43 |
+
</div>
|
44 |
+
</div>
|
45 |
+
</div>
|
46 |
+
</div>
|
47 |
+
</div>
|
48 |
+
)
|
49 |
+
}
|
app/globals.css
ADDED
@@ -0,0 +1,312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@tailwind base;
|
2 |
+
@tailwind components;
|
3 |
+
@tailwind utilities;
|
4 |
+
|
5 |
+
@layer base {
|
6 |
+
:root {
|
7 |
+
--background: 0 0% 100%;
|
8 |
+
--foreground: 240 10% 3.9%;
|
9 |
+
--card: 0 0% 100%;
|
10 |
+
--card-foreground: 240 10% 3.9%;
|
11 |
+
--popover: 0 0% 100%;
|
12 |
+
--popover-foreground: 240 10% 3.9%;
|
13 |
+
--primary: 240 9% 10%;
|
14 |
+
--primary-foreground: 0 0% 98%;
|
15 |
+
--secondary: 240 4.8% 95.9%;
|
16 |
+
--secondary-foreground: 240 5.9% 10%;
|
17 |
+
--muted: 240 4.8% 95.9%;
|
18 |
+
--muted-foreground: 240 3.8% 46.1%;
|
19 |
+
--accent: 240 4.8% 95.9%;
|
20 |
+
--accent-foreground: 240 5.9% 10%;
|
21 |
+
--destructive: 0 84.2% 60.2%;
|
22 |
+
--destructive-foreground: 0 0% 98%;
|
23 |
+
--border: 240 5.9% 90%;
|
24 |
+
--input: 240 5.9% 90%;
|
25 |
+
--ring: 240 10% 3.9%;
|
26 |
+
--chart-1: 12 76% 61%;
|
27 |
+
--chart-2: 173 58% 39%;
|
28 |
+
--chart-3: 197 37% 24%;
|
29 |
+
--chart-4: 43 74% 66%;
|
30 |
+
--chart-5: 27 87% 67%;
|
31 |
+
--radius: 0.5rem;
|
32 |
+
--flame: 15 100% 50%;
|
33 |
+
}
|
34 |
+
|
35 |
+
.dark {
|
36 |
+
--background: 240 10% 3.9%;
|
37 |
+
--foreground: 0 0% 98%;
|
38 |
+
--card: 240 10% 3.9%;
|
39 |
+
--card-foreground: 0 0% 98%;
|
40 |
+
--popover: 240 10% 3.9%;
|
41 |
+
--popover-foreground: 0 0% 98%;
|
42 |
+
--primary: 0 0% 98%;
|
43 |
+
--primary-foreground: 240 5.9% 10%;
|
44 |
+
--secondary: 240 3.7% 15.9%;
|
45 |
+
--secondary-foreground: 0 0% 98%;
|
46 |
+
--muted: 240 3.7% 15.9%;
|
47 |
+
--muted-foreground: 240 5% 64.9%;
|
48 |
+
--accent: 240 3.7% 15.9%;
|
49 |
+
--accent-foreground: 0 0% 98%;
|
50 |
+
--destructive: 0 62.8% 30.6%;
|
51 |
+
--destructive-foreground: 0 0% 98%;
|
52 |
+
--border: 240 3.7% 15.9%;
|
53 |
+
--input: 240 3.7% 15.9%;
|
54 |
+
--ring: 240 4.9% 83.9%;
|
55 |
+
--chart-1: 220 70% 50%;
|
56 |
+
--chart-2: 160 60% 45%;
|
57 |
+
--chart-3: 30 80% 55%;
|
58 |
+
--chart-4: 280 65% 60%;
|
59 |
+
--chart-5: 340 75% 55%;
|
60 |
+
}
|
61 |
+
}
|
62 |
+
|
63 |
+
@layer base {
|
64 |
+
* {
|
65 |
+
@apply border-border;
|
66 |
+
}
|
67 |
+
body {
|
68 |
+
@apply bg-background text-foreground;
|
69 |
+
}
|
70 |
+
}
|
71 |
+
|
72 |
+
/* Flame and animation styles */
|
73 |
+
.flame {
|
74 |
+
color: hsl(var(--flame));
|
75 |
+
}
|
76 |
+
|
77 |
+
.bg-flame {
|
78 |
+
background-color: hsl(var(--flame));
|
79 |
+
}
|
80 |
+
|
81 |
+
.border-flame {
|
82 |
+
border-color: hsl(var(--flame));
|
83 |
+
}
|
84 |
+
|
85 |
+
/* Bubble field styles */
|
86 |
+
.bubble-field {
|
87 |
+
position: relative;
|
88 |
+
overflow: hidden;
|
89 |
+
}
|
90 |
+
|
91 |
+
.data-bubble {
|
92 |
+
position: absolute;
|
93 |
+
display: flex;
|
94 |
+
flex-direction: column;
|
95 |
+
align-items: center;
|
96 |
+
justify-content: center;
|
97 |
+
border-radius: 50%;
|
98 |
+
color: white;
|
99 |
+
font-weight: bold;
|
100 |
+
text-align: center;
|
101 |
+
cursor: pointer;
|
102 |
+
transition: all 0.3s ease;
|
103 |
+
backdrop-filter: blur(10px);
|
104 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
105 |
+
z-index: 10;
|
106 |
+
transform-origin: center;
|
107 |
+
user-select: none;
|
108 |
+
group: hover;
|
109 |
+
}
|
110 |
+
|
111 |
+
/* Bubble sizes */
|
112 |
+
.bubble-xs {
|
113 |
+
width: 24px;
|
114 |
+
height: 24px;
|
115 |
+
font-size: 8px;
|
116 |
+
}
|
117 |
+
|
118 |
+
.bubble-sm {
|
119 |
+
width: 48px;
|
120 |
+
height: 48px;
|
121 |
+
font-size: 10px;
|
122 |
+
}
|
123 |
+
|
124 |
+
.bubble-md {
|
125 |
+
width: 64px;
|
126 |
+
height: 64px;
|
127 |
+
font-size: 12px;
|
128 |
+
}
|
129 |
+
|
130 |
+
.bubble-lg {
|
131 |
+
width: 80px;
|
132 |
+
height: 80px;
|
133 |
+
font-size: 14px;
|
134 |
+
}
|
135 |
+
|
136 |
+
.bubble-xl {
|
137 |
+
width: 96px;
|
138 |
+
height: 96px;
|
139 |
+
font-size: 16px;
|
140 |
+
}
|
141 |
+
|
142 |
+
/* Bubble categories */
|
143 |
+
.bubble-testnet {
|
144 |
+
background: linear-gradient(135deg, rgba(255, 120, 0, 0.8), rgba(255, 78, 0, 0.6));
|
145 |
+
box-shadow: 0 0 20px rgba(255, 120, 0, 0.4);
|
146 |
+
}
|
147 |
+
|
148 |
+
.bubble-mainnet {
|
149 |
+
background: linear-gradient(135deg, rgba(220, 38, 127, 0.8), rgba(239, 68, 68, 0.6));
|
150 |
+
box-shadow: 0 0 20px rgba(220, 38, 127, 0.4);
|
151 |
+
}
|
152 |
+
|
153 |
+
.bubble-healers {
|
154 |
+
background: linear-gradient(135deg, rgba(34, 197, 94, 0.8), rgba(16, 185, 129, 0.6));
|
155 |
+
box-shadow: 0 0 15px rgba(34, 197, 94, 0.3);
|
156 |
+
}
|
157 |
+
|
158 |
+
.bubble-guardians {
|
159 |
+
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(99, 102, 241, 0.6));
|
160 |
+
box-shadow: 0 0 15px rgba(59, 130, 246, 0.3);
|
161 |
+
}
|
162 |
+
|
163 |
+
.bubble-soulbound {
|
164 |
+
background: linear-gradient(135deg, rgba(147, 51, 234, 0.8), rgba(139, 92, 246, 0.6));
|
165 |
+
box-shadow: 0 0 15px rgba(147, 51, 234, 0.3);
|
166 |
+
}
|
167 |
+
|
168 |
+
.bubble-codex {
|
169 |
+
background: linear-gradient(135deg, rgba(245, 158, 11, 0.8), rgba(251, 191, 36, 0.6));
|
170 |
+
box-shadow: 0 0 15px rgba(245, 158, 11, 0.3);
|
171 |
+
}
|
172 |
+
|
173 |
+
.bubble-verified {
|
174 |
+
background: linear-gradient(135deg, rgba(16, 185, 129, 0.7), rgba(5, 150, 105, 0.5));
|
175 |
+
box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
|
176 |
+
}
|
177 |
+
|
178 |
+
.bubble-active {
|
179 |
+
background: linear-gradient(135deg, rgba(236, 72, 153, 0.7), rgba(219, 39, 119, 0.5));
|
180 |
+
box-shadow: 0 0 10px rgba(236, 72, 153, 0.3);
|
181 |
+
}
|
182 |
+
|
183 |
+
.bubble-live-person {
|
184 |
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.1));
|
185 |
+
border: 2px solid rgba(255, 120, 0, 0.6);
|
186 |
+
box-shadow: 0 0 15px rgba(255, 120, 0, 0.4);
|
187 |
+
}
|
188 |
+
|
189 |
+
/* Live bubble effects */
|
190 |
+
.bubble-live {
|
191 |
+
animation: bubble-pulse 2s ease-in-out infinite;
|
192 |
+
}
|
193 |
+
|
194 |
+
.bubble-new-join {
|
195 |
+
animation: bubble-bounce 1s ease-in-out infinite, bubble-glow 2s ease-in-out infinite;
|
196 |
+
}
|
197 |
+
|
198 |
+
.bubble-person {
|
199 |
+
border: 2px solid rgba(255, 120, 0, 0.8);
|
200 |
+
}
|
201 |
+
|
202 |
+
/* Bubble animations */
|
203 |
+
@keyframes bubble-pulse {
|
204 |
+
0%,
|
205 |
+
100% {
|
206 |
+
transform: scale(1);
|
207 |
+
opacity: 0.9;
|
208 |
+
}
|
209 |
+
50% {
|
210 |
+
transform: scale(1.05);
|
211 |
+
opacity: 1;
|
212 |
+
}
|
213 |
+
}
|
214 |
+
|
215 |
+
@keyframes bubble-bounce {
|
216 |
+
0%,
|
217 |
+
100% {
|
218 |
+
transform: translateY(0);
|
219 |
+
}
|
220 |
+
50% {
|
221 |
+
transform: translateY(-5px);
|
222 |
+
}
|
223 |
+
}
|
224 |
+
|
225 |
+
@keyframes bubble-glow {
|
226 |
+
0%,
|
227 |
+
100% {
|
228 |
+
box-shadow: 0 0 15px rgba(255, 120, 0, 0.4);
|
229 |
+
}
|
230 |
+
50% {
|
231 |
+
box-shadow: 0 0 25px rgba(255, 120, 0, 0.8);
|
232 |
+
}
|
233 |
+
}
|
234 |
+
|
235 |
+
/* Hover effects */
|
236 |
+
.data-bubble:hover {
|
237 |
+
transform: scale(1.1);
|
238 |
+
z-index: 20;
|
239 |
+
}
|
240 |
+
|
241 |
+
.bubble-live:hover {
|
242 |
+
animation-play-state: paused;
|
243 |
+
}
|
244 |
+
|
245 |
+
/* Multiplier effects for high counts */
|
246 |
+
.bubble-multiplier {
|
247 |
+
position: relative;
|
248 |
+
}
|
249 |
+
|
250 |
+
.bubble-multiplier::before {
|
251 |
+
content: "";
|
252 |
+
position: absolute;
|
253 |
+
inset: -2px;
|
254 |
+
border-radius: 50%;
|
255 |
+
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
256 |
+
animation: rotate 3s linear infinite;
|
257 |
+
}
|
258 |
+
|
259 |
+
.bubble-swarm {
|
260 |
+
position: relative;
|
261 |
+
}
|
262 |
+
|
263 |
+
.bubble-swarm::after {
|
264 |
+
content: "";
|
265 |
+
position: absolute;
|
266 |
+
inset: -4px;
|
267 |
+
border-radius: 50%;
|
268 |
+
background: radial-gradient(circle, transparent 60%, rgba(255, 120, 0, 0.1) 70%, transparent 80%);
|
269 |
+
animation: swarm-pulse 2s ease-in-out infinite;
|
270 |
+
}
|
271 |
+
|
272 |
+
@keyframes rotate {
|
273 |
+
from {
|
274 |
+
transform: rotate(0deg);
|
275 |
+
}
|
276 |
+
to {
|
277 |
+
transform: rotate(360deg);
|
278 |
+
}
|
279 |
+
}
|
280 |
+
|
281 |
+
@keyframes swarm-pulse {
|
282 |
+
0%,
|
283 |
+
100% {
|
284 |
+
opacity: 0.3;
|
285 |
+
transform: scale(1);
|
286 |
+
}
|
287 |
+
50% {
|
288 |
+
opacity: 0.7;
|
289 |
+
transform: scale(1.1);
|
290 |
+
}
|
291 |
+
}
|
292 |
+
|
293 |
+
/* Responsive adjustments */
|
294 |
+
@media (max-width: 768px) {
|
295 |
+
.bubble-xl {
|
296 |
+
width: 72px;
|
297 |
+
height: 72px;
|
298 |
+
font-size: 14px;
|
299 |
+
}
|
300 |
+
|
301 |
+
.bubble-lg {
|
302 |
+
width: 60px;
|
303 |
+
height: 60px;
|
304 |
+
font-size: 12px;
|
305 |
+
}
|
306 |
+
|
307 |
+
.bubble-md {
|
308 |
+
width: 48px;
|
309 |
+
height: 48px;
|
310 |
+
font-size: 10px;
|
311 |
+
}
|
312 |
+
}
|
app/guardian-council/page.tsx
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { GuardianVotingInterface } from "@/components/guardian/guardian-voting-interface"
|
2 |
+
|
3 |
+
export default function GuardianCouncilPage() {
|
4 |
+
return <GuardianVotingInterface />
|
5 |
+
}
|
app/guardians-sanctuary/page.tsx
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { Suspense } from "react"
|
4 |
+
import { SanctuaryHero } from "@/components/sanctuary/sanctuary-hero"
|
5 |
+
import { SanctuaryFeed } from "@/components/sanctuary/sanctuary-feed"
|
6 |
+
import { SanctuaryMembers } from "@/components/sanctuary/sanctuary-members"
|
7 |
+
import { LoadingState } from "@/components/loading-state"
|
8 |
+
import dynamic from "next/dynamic"
|
9 |
+
|
10 |
+
const ParticleBackground = dynamic(
|
11 |
+
() => import("@/components/particle-background").then((mod) => ({ default: mod.ParticleBackground })),
|
12 |
+
{
|
13 |
+
ssr: false,
|
14 |
+
loading: () => <div className="fixed inset-0 bg-black" />,
|
15 |
+
},
|
16 |
+
)
|
17 |
+
|
18 |
+
export default function GuardiansSanctuary() {
|
19 |
+
return (
|
20 |
+
<div className="min-h-screen bg-dusk relative">
|
21 |
+
<ParticleBackground />
|
22 |
+
<div className="relative z-10">
|
23 |
+
<Suspense fallback={<LoadingState message="Loading sanctuary..." />}>
|
24 |
+
<SanctuaryHero />
|
25 |
+
</Suspense>
|
26 |
+
|
27 |
+
<div className="max-w-7xl mx-auto px-4 py-8">
|
28 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
29 |
+
<div className="lg:col-span-2">
|
30 |
+
<Suspense fallback={<LoadingState message="Loading sanctuary feed..." />}>
|
31 |
+
<SanctuaryFeed />
|
32 |
+
</Suspense>
|
33 |
+
</div>
|
34 |
+
<div>
|
35 |
+
<Suspense fallback={<LoadingState message="Loading members..." />}>
|
36 |
+
<SanctuaryMembers />
|
37 |
+
</Suspense>
|
38 |
+
</div>
|
39 |
+
</div>
|
40 |
+
</div>
|
41 |
+
</div>
|
42 |
+
</div>
|
43 |
+
)
|
44 |
+
}
|
app/healers/page.tsx
ADDED
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Button } from "@/components/ui/button"
|
2 |
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
3 |
+
import { Shell } from "@/components/Shell"
|
4 |
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
5 |
+
import { Badge } from "@/components/ui/badge"
|
6 |
+
import { ScrollArea } from "@/components/ui/scroll-area"
|
7 |
+
import { Separator } from "@/components/ui/separator"
|
8 |
+
import Link from "next/link"
|
9 |
+
|
10 |
+
const data = [
|
11 |
+
{
|
12 |
+
name: "Nwangi Thomas",
|
13 |
+
email: "[email protected]",
|
14 |
+
phone: "+254-701-123456",
|
15 |
+
specialty: "Cardiologist",
|
16 |
+
location: "Nairobi, Kenya",
|
17 |
+
availability: "Mon-Fri",
|
18 |
+
status: "Active",
|
19 |
+
},
|
20 |
+
{
|
21 |
+
name: "Denis Moraa",
|
22 |
+
email: "[email protected]",
|
23 |
+
phone: "+256-772-456789",
|
24 |
+
specialty: "Dermatologist",
|
25 |
+
location: "Kampala, Uganda",
|
26 |
+
availability: "Tue-Sat",
|
27 |
+
status: "Inactive",
|
28 |
+
},
|
29 |
+
{
|
30 |
+
name: "Alice Murekezi",
|
31 |
+
email: "[email protected]",
|
32 |
+
phone: "+250-788-987654",
|
33 |
+
specialty: "Pediatrician",
|
34 |
+
location: "Kigali, Rwanda",
|
35 |
+
availability: "Wed-Sun",
|
36 |
+
status: "Active",
|
37 |
+
},
|
38 |
+
{
|
39 |
+
name: "Boubacar Williams",
|
40 |
+
email: "[email protected]",
|
41 |
+
phone: "+221-773-112233",
|
42 |
+
specialty: "Oncologist",
|
43 |
+
location: "Dakar, Senegal",
|
44 |
+
availability: "Mon-Sat",
|
45 |
+
status: "Active",
|
46 |
+
},
|
47 |
+
{
|
48 |
+
name: "Emelda Okonkwo",
|
49 |
+
email: "[email protected]",
|
50 |
+
phone: "+234-809-445566",
|
51 |
+
specialty: "Neurologist",
|
52 |
+
location: "Enugu, Nigeria",
|
53 |
+
availability: "Tue-Fri",
|
54 |
+
status: "Inactive",
|
55 |
+
},
|
56 |
+
{
|
57 |
+
name: "David Abebe",
|
58 |
+
email: "[email protected]",
|
59 |
+
phone: "+251-911-778899",
|
60 |
+
specialty: "Surgeon",
|
61 |
+
location: "Addis Ababa, Ethiopia",
|
62 |
+
availability: "Mon-Sun",
|
63 |
+
status: "Active",
|
64 |
+
},
|
65 |
+
{
|
66 |
+
name: "Linda Tshabalala",
|
67 |
+
email: "[email protected]",
|
68 |
+
phone: "+27-82-3344556",
|
69 |
+
specialty: "Psychiatrist",
|
70 |
+
location: "Johannesburg, South Africa",
|
71 |
+
availability: "Wed-Sat",
|
72 |
+
status: "Active",
|
73 |
+
},
|
74 |
+
{
|
75 |
+
name: "Michael Kombo",
|
76 |
+
email: "[email protected]",
|
77 |
+
phone: "+254-722-667788",
|
78 |
+
specialty: "Ophthalmologist",
|
79 |
+
location: "Mombasa, Kenya",
|
80 |
+
availability: "Tue-Sun",
|
81 |
+
status: "Inactive",
|
82 |
+
},
|
83 |
+
{
|
84 |
+
name: "Baraka Ncube",
|
85 |
+
email: "[email protected]",
|
86 |
+
phone: "+263-773-554433",
|
87 |
+
specialty: "ENT Specialist",
|
88 |
+
location: "Harare, Zimbabwe",
|
89 |
+
availability: "Mon-Fri",
|
90 |
+
status: "Active",
|
91 |
+
},
|
92 |
+
{
|
93 |
+
name: "James Okoro",
|
94 |
+
email: "[email protected]",
|
95 |
+
phone: "+234-803-990011",
|
96 |
+
specialty: "Gastroenterologist",
|
97 |
+
location: "Lagos, Nigeria",
|
98 |
+
availability: "Wed-Sun",
|
99 |
+
status: "Active",
|
100 |
+
},
|
101 |
+
]
|
102 |
+
export default function HealersPage() {
|
103 |
+
return (
|
104 |
+
<Shell>
|
105 |
+
<div className="grid gap-6">
|
106 |
+
<div className="flex items-center justify-between space-y-0.5">
|
107 |
+
<h2 className="text-2xl font-bold tracking-tight">Our Healers</h2>
|
108 |
+
<Link href="/register/healer">
|
109 |
+
<Button className="bg-flame hover:bg-pulse text-white px-8 py-4 text-lg rounded-md transition-all hover:shadow-[0_0_20px_rgba(255,78,0,0.3)]">
|
110 |
+
Register as a Healer
|
111 |
+
</Button>
|
112 |
+
</Link>
|
113 |
+
</div>
|
114 |
+
<Separator />
|
115 |
+
<div className="grid gap-6">
|
116 |
+
<Card>
|
117 |
+
<CardHeader>
|
118 |
+
<CardTitle>Healer List</CardTitle>
|
119 |
+
<CardDescription>All registered healers are listed here.</CardDescription>
|
120 |
+
</CardHeader>
|
121 |
+
<CardContent>
|
122 |
+
<ScrollArea>
|
123 |
+
<Table>
|
124 |
+
<TableHeader>
|
125 |
+
<TableRow>
|
126 |
+
<TableHead className="w-[100px]">Name</TableHead>
|
127 |
+
<TableHead>Email</TableHead>
|
128 |
+
<TableHead>Phone</TableHead>
|
129 |
+
<TableHead>Specialty</TableHead>
|
130 |
+
<TableHead>Location</TableHead>
|
131 |
+
<TableHead>Availability</TableHead>
|
132 |
+
<TableHead className="text-right">Status</TableHead>
|
133 |
+
</TableRow>
|
134 |
+
</TableHeader>
|
135 |
+
<TableBody>
|
136 |
+
{data.map((row, index) => (
|
137 |
+
<TableRow key={index}>
|
138 |
+
<TableCell className="font-medium">{row.name}</TableCell>
|
139 |
+
<TableCell>{row.email}</TableCell>
|
140 |
+
<TableCell>{row.phone}</TableCell>
|
141 |
+
<TableCell>{row.specialty}</TableCell>
|
142 |
+
<TableCell>{row.location}</TableCell>
|
143 |
+
<TableCell>{row.availability}</TableCell>
|
144 |
+
<TableCell className="text-right">
|
145 |
+
{row.status === "Active" ? (
|
146 |
+
<Badge variant="outline">Active</Badge>
|
147 |
+
) : (
|
148 |
+
<Badge variant="secondary">Inactive</Badge>
|
149 |
+
)}
|
150 |
+
</TableCell>
|
151 |
+
</TableRow>
|
152 |
+
))}
|
153 |
+
</TableBody>
|
154 |
+
</Table>
|
155 |
+
</ScrollArea>
|
156 |
+
</CardContent>
|
157 |
+
</Card>
|
158 |
+
</div>
|
159 |
+
</div>
|
160 |
+
</Shell>
|
161 |
+
)
|
162 |
+
}
|
app/launch/layout.tsx
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type React from "react"
|
2 |
+
export default function LaunchLayout({
|
3 |
+
children,
|
4 |
+
}: {
|
5 |
+
children: React.ReactNode
|
6 |
+
}) {
|
7 |
+
return <div className="min-h-screen bg-black">{children}</div>
|
8 |
+
}
|
app/launch/page.tsx
ADDED
@@ -0,0 +1,374 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { motion } from "framer-motion"
|
4 |
+
import Link from "next/link"
|
5 |
+
import { Button } from "@/components/ui/button"
|
6 |
+
import { ArrowRight, Heart, Globe, Brain, Users, DollarSign, Vote, Share2, Hammer } from "lucide-react"
|
7 |
+
|
8 |
+
export default function LaunchPage() {
|
9 |
+
return (
|
10 |
+
<div className="min-h-screen bg-black text-white">
|
11 |
+
<div className="max-w-4xl mx-auto px-4 py-16 sm:px-6 sm:py-24">
|
12 |
+
<header className="text-center mb-20">
|
13 |
+
<motion.h1
|
14 |
+
className="text-4xl md:text-6xl font-bold mb-6 flame-text"
|
15 |
+
initial={{ opacity: 0, y: -20 }}
|
16 |
+
animate={{ opacity: 1, y: 0 }}
|
17 |
+
transition={{ duration: 0.8 }}
|
18 |
+
>
|
19 |
+
Flameborn: Rekindling Hope in Africa's Rural Healthcare
|
20 |
+
</motion.h1>
|
21 |
+
|
22 |
+
<motion.p
|
23 |
+
className="text-xl md:text-2xl text-gray-300 max-w-3xl mx-auto"
|
24 |
+
initial={{ opacity: 0 }}
|
25 |
+
animate={{ opacity: 1 }}
|
26 |
+
transition={{ duration: 0.8, delay: 0.3 }}
|
27 |
+
>
|
28 |
+
A Beacon in the Darkness
|
29 |
+
</motion.p>
|
30 |
+
</header>
|
31 |
+
|
32 |
+
<motion.section
|
33 |
+
className="prose prose-invert prose-lg max-w-none mb-16"
|
34 |
+
initial={{ opacity: 0, y: 20 }}
|
35 |
+
animate={{ opacity: 1, y: 0 }}
|
36 |
+
transition={{ duration: 0.8, delay: 0.5 }}
|
37 |
+
>
|
38 |
+
<p className="text-xl leading-relaxed">
|
39 |
+
In the silent hours before dawn, deep in the heart of rural Africa, a nurse leans over a patient. Her only
|
40 |
+
light is a flickering kerosene lamp.
|
41 |
+
<br />
|
42 |
+
Her only tools — resolve and compassion.
|
43 |
+
</p>
|
44 |
+
|
45 |
+
<p className="text-xl leading-relaxed">
|
46 |
+
The clinic walls are worn. The shelves? Bare.
|
47 |
+
<br />
|
48 |
+
But she is still here — holding the line between life and loss.
|
49 |
+
</p>
|
50 |
+
|
51 |
+
<p className="text-xl leading-relaxed">
|
52 |
+
She has no headline.
|
53 |
+
<br />
|
54 |
+
No salary.
|
55 |
+
<br />
|
56 |
+
No promise of help.
|
57 |
+
</p>
|
58 |
+
|
59 |
+
<p className="text-xl leading-relaxed">But she stays.</p>
|
60 |
+
|
61 |
+
<p className="text-2xl font-bold">
|
62 |
+
<span className="flame-text">And Flameborn is here to tell the world: She is not alone anymore.</span>
|
63 |
+
</p>
|
64 |
+
</motion.section>
|
65 |
+
|
66 |
+
<motion.section
|
67 |
+
className="mb-16"
|
68 |
+
initial={{ opacity: 0 }}
|
69 |
+
animate={{ opacity: 1 }}
|
70 |
+
transition={{ duration: 0.8, delay: 0.8 }}
|
71 |
+
>
|
72 |
+
<h2 className="text-3xl font-bold mb-6 flex items-center">
|
73 |
+
<Globe className="mr-3 text-ember-orange" /> The Challenge We Face
|
74 |
+
</h2>
|
75 |
+
|
76 |
+
<div className="flame-card p-6 mb-8">
|
77 |
+
<p className="mb-4">
|
78 |
+
Millions of Africa's healthcare workers — midwives, nurses, outreach teams — are holding up the very
|
79 |
+
foundation of our health systems.
|
80 |
+
<br />
|
81 |
+
And they're doing it with <strong>nothing</strong>.
|
82 |
+
</p>
|
83 |
+
|
84 |
+
<p className="mb-4">
|
85 |
+
In many African countries, community health workers show up every day <strong>without pay</strong>,
|
86 |
+
without supplies, without rest.
|
87 |
+
</p>
|
88 |
+
|
89 |
+
<p className="mb-4">Global aid is pledged. Billions flow.</p>
|
90 |
+
|
91 |
+
<p className="mb-4">But by the time it trickles down?</p>
|
92 |
+
|
93 |
+
<ul className="list-disc pl-6 mb-4">
|
94 |
+
<li className="mb-2">
|
95 |
+
💸 <em>30% has disappeared into red tape, middlemen, corruption</em>
|
96 |
+
</li>
|
97 |
+
<li className="mb-2">🏥 The clinic receives nothing</li>
|
98 |
+
<li className="mb-2">🕯️ The nurse lights another candle</li>
|
99 |
+
</ul>
|
100 |
+
|
101 |
+
<p className="text-xl font-semibold">
|
102 |
+
We've waited long enough.
|
103 |
+
<br />
|
104 |
+
<span className="flame-text">Flameborn is the answer that doesn't wait.</span>
|
105 |
+
</p>
|
106 |
+
</div>
|
107 |
+
</motion.section>
|
108 |
+
|
109 |
+
<motion.section
|
110 |
+
className="mb-16"
|
111 |
+
initial={{ opacity: 0 }}
|
112 |
+
animate={{ opacity: 1 }}
|
113 |
+
transition={{ duration: 0.8, delay: 1.1 }}
|
114 |
+
>
|
115 |
+
<h2 className="text-3xl font-bold mb-6 flex items-center">
|
116 |
+
<Heart className="mr-3 text-flame-red" /> What Is Flameborn?
|
117 |
+
</h2>
|
118 |
+
|
119 |
+
<div className="flame-card p-6">
|
120 |
+
<p className="mb-4">
|
121 |
+
Flameborn is not a charity.
|
122 |
+
<br />
|
123 |
+
Not a startup.
|
124 |
+
<br />
|
125 |
+
Not a government program.
|
126 |
+
</p>
|
127 |
+
|
128 |
+
<p className="mb-4">
|
129 |
+
It is a <strong>decentralized movement</strong> of Africans and allies
|
130 |
+
<br />
|
131 |
+
who believe that <strong>healers should never be left behind</strong>.
|
132 |
+
</p>
|
133 |
+
|
134 |
+
<p className="mb-4">
|
135 |
+
We are builders.
|
136 |
+
<br />
|
137 |
+
We are believers.
|
138 |
+
<br />
|
139 |
+
We are the Flameborn.
|
140 |
+
</p>
|
141 |
+
</div>
|
142 |
+
</motion.section>
|
143 |
+
|
144 |
+
<motion.section
|
145 |
+
className="mb-16"
|
146 |
+
initial={{ opacity: 0 }}
|
147 |
+
animate={{ opacity: 1 }}
|
148 |
+
transition={{ duration: 0.8, delay: 1.4 }}
|
149 |
+
>
|
150 |
+
<h2 className="text-3xl font-bold mb-6 flex items-center">
|
151 |
+
<ArrowRight className="mr-3 text-healing-blue" /> How It Works
|
152 |
+
</h2>
|
153 |
+
|
154 |
+
<div className="grid md:grid-cols-3 gap-6">
|
155 |
+
<div className="flame-card p-6">
|
156 |
+
<h3 className="text-xl font-bold mb-4 flex items-center">
|
157 |
+
<DollarSign className="mr-2 text-ember-orange" /> Direct Aid via Blockchain
|
158 |
+
</h3>
|
159 |
+
<p>
|
160 |
+
Donations go straight into digital wallets of verified rural healthcare workers.
|
161 |
+
<br />
|
162 |
+
No banks. No bureaucracy. No "lost in transit."
|
163 |
+
<br />
|
164 |
+
Every transaction is <strong>public, traceable, and real</strong>.
|
165 |
+
</p>
|
166 |
+
</div>
|
167 |
+
|
168 |
+
<div className="flame-card p-6">
|
169 |
+
<h3 className="text-xl font-bold mb-4 flex items-center">
|
170 |
+
<Vote className="mr-2 text-ember-orange" /> Community Governance (DAO)
|
171 |
+
</h3>
|
172 |
+
<p>
|
173 |
+
The Flameborn DAO is open to all.
|
174 |
+
<br />
|
175 |
+
You don't need permission to care.
|
176 |
+
<br />
|
177 |
+
Vote on where funds go. Help decide which regions we support next.
|
178 |
+
<br />
|
179 |
+
<em>Every supporter is a leader.</em>
|
180 |
+
</p>
|
181 |
+
</div>
|
182 |
+
|
183 |
+
<div className="flame-card p-6">
|
184 |
+
<h3 className="text-xl font-bold mb-4 flex items-center">
|
185 |
+
<Brain className="mr-2 text-ember-orange" /> AI-Driven Transparency
|
186 |
+
</h3>
|
187 |
+
<p>
|
188 |
+
Smart systems track every token.
|
189 |
+
<br />
|
190 |
+
You'll see where it went, when it arrived, and what it did.
|
191 |
+
<br />
|
192 |
+
If something goes wrong, the system says so — <strong>publicly</strong>.<br />
|
193 |
+
There are no secrets in the Flame.
|
194 |
+
</p>
|
195 |
+
</div>
|
196 |
+
</div>
|
197 |
+
</motion.section>
|
198 |
+
|
199 |
+
<motion.section
|
200 |
+
className="mb-16"
|
201 |
+
initial={{ opacity: 0 }}
|
202 |
+
animate={{ opacity: 1 }}
|
203 |
+
transition={{ duration: 0.8, delay: 1.7 }}
|
204 |
+
>
|
205 |
+
<h2 className="text-3xl font-bold mb-6 flex items-center">
|
206 |
+
<Users className="mr-3 text-healing-blue" /> Real People. Real Impact.
|
207 |
+
</h2>
|
208 |
+
|
209 |
+
<div className="flame-card p-6">
|
210 |
+
<p className="mb-4">
|
211 |
+
In early pilots across <strong>rural Kenya, Nigeria, and Ghana</strong>:
|
212 |
+
</p>
|
213 |
+
|
214 |
+
<ul className="list-disc pl-6 mb-4">
|
215 |
+
<li className="mb-2">A midwife received funds to restock birthing supplies</li>
|
216 |
+
<li className="mb-2">A nurse used her stipend to repair a solar fridge for vaccines</li>
|
217 |
+
<li className="mb-2">A health agent finally had enough to reach families across rivers</li>
|
218 |
+
</ul>
|
219 |
+
|
220 |
+
<p className="mb-4">
|
221 |
+
These aren't handouts.
|
222 |
+
<br />
|
223 |
+
They're <strong>affirmations</strong>:
|
224 |
+
</p>
|
225 |
+
|
226 |
+
<p className="text-xl font-semibold">
|
227 |
+
We see you.
|
228 |
+
<br />
|
229 |
+
We honor you.
|
230 |
+
<br />
|
231 |
+
We invest in your care — because you cared for us.
|
232 |
+
</p>
|
233 |
+
</div>
|
234 |
+
</motion.section>
|
235 |
+
|
236 |
+
<motion.section
|
237 |
+
className="mb-16"
|
238 |
+
initial={{ opacity: 0 }}
|
239 |
+
animate={{ opacity: 1 }}
|
240 |
+
transition={{ duration: 0.8, delay: 2.0 }}
|
241 |
+
>
|
242 |
+
<h2 className="text-3xl font-bold mb-6 flex items-center">
|
243 |
+
<Heart className="mr-3 text-flame-red" /> This Is a Movement. Not a Moment.
|
244 |
+
</h2>
|
245 |
+
|
246 |
+
<div className="flame-card p-6">
|
247 |
+
<p className="text-xl leading-relaxed">
|
248 |
+
Flameborn is not just a website.
|
249 |
+
<br />
|
250 |
+
It's <strong>Ubuntu in code.</strong>
|
251 |
+
<br />
|
252 |
+
It's <strong>compassion on the chain.</strong>
|
253 |
+
<br />
|
254 |
+
It's the <strong>spirit of Africa</strong>, digitized, decentralized, and rising.
|
255 |
+
</p>
|
256 |
+
</div>
|
257 |
+
</motion.section>
|
258 |
+
|
259 |
+
<motion.section
|
260 |
+
className="mb-16"
|
261 |
+
initial={{ opacity: 0 }}
|
262 |
+
animate={{ opacity: 1 }}
|
263 |
+
transition={{ duration: 0.8, delay: 2.3 }}
|
264 |
+
>
|
265 |
+
<h2 className="text-3xl font-bold mb-6 flex items-center">
|
266 |
+
<Hammer className="mr-3 text-healing-blue" /> How You Can Join the Flame
|
267 |
+
</h2>
|
268 |
+
|
269 |
+
<div className="grid md:grid-cols-2 gap-6">
|
270 |
+
<div className="flame-card p-6">
|
271 |
+
<h3 className="text-xl font-bold mb-4 flex items-center">
|
272 |
+
<DollarSign className="mr-2 text-ember-orange" /> Donate
|
273 |
+
</h3>
|
274 |
+
<p>$5 fuels a clinic. $50 funds a stipend. 100% goes direct.</p>
|
275 |
+
<div className="mt-4">
|
276 |
+
<Button className="flame-button">Donate Now</Button>
|
277 |
+
</div>
|
278 |
+
</div>
|
279 |
+
|
280 |
+
<div className="flame-card p-6">
|
281 |
+
<h3 className="text-xl font-bold mb-4 flex items-center">
|
282 |
+
<Vote className="mr-2 text-ember-orange" /> Join the DAO
|
283 |
+
</h3>
|
284 |
+
<p>Help guide aid. Propose, vote, change lives.</p>
|
285 |
+
<div className="mt-4">
|
286 |
+
<Button className="flame-button">Join DAO</Button>
|
287 |
+
</div>
|
288 |
+
</div>
|
289 |
+
|
290 |
+
<div className="flame-card p-6">
|
291 |
+
<h3 className="text-xl font-bold mb-4 flex items-center">
|
292 |
+
<Share2 className="mr-2 text-ember-orange" /> Spread the Word
|
293 |
+
</h3>
|
294 |
+
<p>Tell your tribe. Light another torch.</p>
|
295 |
+
<div className="mt-4">
|
296 |
+
<Button className="flame-button">Share</Button>
|
297 |
+
</div>
|
298 |
+
</div>
|
299 |
+
|
300 |
+
<div className="flame-card p-6">
|
301 |
+
<h3 className="text-xl font-bold mb-4 flex items-center">
|
302 |
+
<Hammer className="mr-2 text-ember-orange" /> Volunteer
|
303 |
+
</h3>
|
304 |
+
<p>Coders, designers, medics, storytellers — your gifts matter here.</p>
|
305 |
+
<div className="mt-4">
|
306 |
+
<Button className="flame-button">Join Us</Button>
|
307 |
+
</div>
|
308 |
+
</div>
|
309 |
+
</div>
|
310 |
+
</motion.section>
|
311 |
+
|
312 |
+
<motion.section
|
313 |
+
className="mb-16"
|
314 |
+
initial={{ opacity: 0 }}
|
315 |
+
animate={{ opacity: 1 }}
|
316 |
+
transition={{ duration: 0.8, delay: 2.6 }}
|
317 |
+
>
|
318 |
+
<h2 className="text-3xl font-bold mb-6 flex items-center">
|
319 |
+
<Globe className="mr-3 text-healing-blue" /> A Final Word to Every Soul Who Reads This
|
320 |
+
</h2>
|
321 |
+
|
322 |
+
<div className="flame-card p-6">
|
323 |
+
<p className="text-xl leading-relaxed mb-4">
|
324 |
+
You are not small.
|
325 |
+
<br />
|
326 |
+
You are not powerless.
|
327 |
+
<br />
|
328 |
+
You are <strong>a match. A spark. A carrier of flame.</strong>
|
329 |
+
</p>
|
330 |
+
|
331 |
+
<p className="text-xl leading-relaxed mb-4">
|
332 |
+
In the darkest corners of our continent, someone is still hoping.
|
333 |
+
<br />
|
334 |
+
Still healing.
|
335 |
+
<br />
|
336 |
+
Still holding on.
|
337 |
+
</p>
|
338 |
+
|
339 |
+
<p className="text-xl font-bold">
|
340 |
+
<span className="flame-text">
|
341 |
+
Light their fire again.
|
342 |
+
<br />
|
343 |
+
Let them know they are not forgotten.
|
344 |
+
<br />
|
345 |
+
Let them feel Africa — and the world — rising with them.
|
346 |
+
</span>
|
347 |
+
</p>
|
348 |
+
</div>
|
349 |
+
</motion.section>
|
350 |
+
|
351 |
+
<motion.footer
|
352 |
+
className="text-center"
|
353 |
+
initial={{ opacity: 0 }}
|
354 |
+
animate={{ opacity: 1 }}
|
355 |
+
transition={{ duration: 0.8, delay: 2.9 }}
|
356 |
+
>
|
357 |
+
<h2 className="text-3xl font-bold mb-6 flame-text">🔥 Flameborn: Lighting the Path to Health and Hope</h2>
|
358 |
+
|
359 |
+
<p className="text-xl mb-8">
|
360 |
+
Because <strong>hope is a flame</strong>.<br />
|
361 |
+
And now — it lives on the chain.
|
362 |
+
</p>
|
363 |
+
|
364 |
+
<div className="flex justify-center gap-4">
|
365 |
+
<Link href="/">
|
366 |
+
<Button className="flame-button">Return Home</Button>
|
367 |
+
</Link>
|
368 |
+
<Button className="flame-button ember-pulse">Join the Movement</Button>
|
369 |
+
</div>
|
370 |
+
</motion.footer>
|
371 |
+
</div>
|
372 |
+
</div>
|
373 |
+
)
|
374 |
+
}
|
app/layout.tsx
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type React from "react"
|
2 |
+
import type { Metadata } from "next"
|
3 |
+
import { Inter } from "next/font/google"
|
4 |
+
import "./globals.css"
|
5 |
+
import AppHeader from "@/components/app-header"
|
6 |
+
import { Toaster } from "@/components/ui/toaster"
|
7 |
+
|
8 |
+
const inter = Inter({ subsets: ["latin"] })
|
9 |
+
|
10 |
+
export const metadata: Metadata = {
|
11 |
+
title: "FlameBorn - Ubuntu Healthcare Tokenization",
|
12 |
+
description:
|
13 |
+
"Bridging traditional African Ubuntu philosophy with modern healthcare tokenization. I am because we are.",
|
14 |
+
keywords: ["Ubuntu", "healthcare", "tokenization", "blockchain", "Celo", "Africa", "community"],
|
15 |
+
authors: [{ name: "FlameBorn Team" }],
|
16 |
+
openGraph: {
|
17 |
+
title: "FlameBorn - Ubuntu Healthcare Tokenization",
|
18 |
+
description: "I am because we are. Ubuntu-powered healthcare tokenization for a connected world.",
|
19 |
+
type: "website",
|
20 |
+
locale: "en_US",
|
21 |
+
},
|
22 |
+
twitter: {
|
23 |
+
card: "summary_large_image",
|
24 |
+
title: "FlameBorn - Ubuntu Healthcare Tokenization",
|
25 |
+
description: "I am because we are. Ubuntu-powered healthcare tokenization for a connected world.",
|
26 |
+
},
|
27 |
+
viewport: "width=device-width, initial-scale=1",
|
28 |
+
themeColor: "#f97316",
|
29 |
+
generator: 'v0.dev'
|
30 |
+
}
|
31 |
+
|
32 |
+
export default function RootLayout({
|
33 |
+
children,
|
34 |
+
}: {
|
35 |
+
children: React.ReactNode
|
36 |
+
}) {
|
37 |
+
return (
|
38 |
+
<html lang="en" className="scroll-smooth">
|
39 |
+
<body className={`${inter.className} antialiased`}>
|
40 |
+
<AppHeader />
|
41 |
+
<main className="min-h-screen">{children}</main>
|
42 |
+
<Toaster />
|
43 |
+
</body>
|
44 |
+
</html>
|
45 |
+
)
|
46 |
+
}
|
app/learn-earn/page.tsx
ADDED
@@ -0,0 +1,1435 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useState, useEffect } from "react"
|
4 |
+
import { motion } from "framer-motion"
|
5 |
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
6 |
+
import { Button } from "@/components/ui/button"
|
7 |
+
import { Progress } from "@/components/ui/progress"
|
8 |
+
import { Badge } from "@/components/ui/badge"
|
9 |
+
import { Input } from "@/components/ui/input"
|
10 |
+
import { Textarea } from "@/components/ui/textarea"
|
11 |
+
import {
|
12 |
+
BookOpen,
|
13 |
+
Play,
|
14 |
+
CheckCircle,
|
15 |
+
Star,
|
16 |
+
Globe,
|
17 |
+
Brain,
|
18 |
+
Stethoscope,
|
19 |
+
Cpu,
|
20 |
+
Users,
|
21 |
+
Coins,
|
22 |
+
HandHeart,
|
23 |
+
GraduationCap,
|
24 |
+
MessageSquare,
|
25 |
+
Wallet,
|
26 |
+
Download,
|
27 |
+
Wifi,
|
28 |
+
WifiOff,
|
29 |
+
Clock,
|
30 |
+
Target,
|
31 |
+
Heart,
|
32 |
+
Shield,
|
33 |
+
Zap,
|
34 |
+
} from "lucide-react"
|
35 |
+
import { ClipboardManager } from "@/lib/clipboard-utils"
|
36 |
+
|
37 |
+
interface Module {
|
38 |
+
id: string
|
39 |
+
title: string
|
40 |
+
description: string
|
41 |
+
type: "interactive" | "quiz" | "story" | "video"
|
42 |
+
category: "Cultural Heritage" | "Philosophy" | "Health Advocacy" | "Technology" | "Ubuntu Wisdom"
|
43 |
+
difficulty: "easy" | "medium" | "hard"
|
44 |
+
duration: number
|
45 |
+
flbReward: number
|
46 |
+
xpReward: number
|
47 |
+
completed: boolean
|
48 |
+
content: any
|
49 |
+
}
|
50 |
+
|
51 |
+
interface QuizQuestion {
|
52 |
+
question: string
|
53 |
+
options: string[]
|
54 |
+
correct: number
|
55 |
+
explanation: string
|
56 |
+
}
|
57 |
+
|
58 |
+
interface WalletInfo {
|
59 |
+
address: string
|
60 |
+
flbBalance: number
|
61 |
+
celoBalance: number
|
62 |
+
connected: boolean
|
63 |
+
}
|
64 |
+
|
65 |
+
interface Transaction {
|
66 |
+
id: string
|
67 |
+
type: "earned" | "donated" | "transferred"
|
68 |
+
amount: number
|
69 |
+
description: string
|
70 |
+
timestamp: Date
|
71 |
+
status: "completed" | "pending" | "failed"
|
72 |
+
}
|
73 |
+
|
74 |
+
const modules: Module[] = [
|
75 |
+
{
|
76 |
+
id: "ubuntu-wisdom-1",
|
77 |
+
title: "Ubuntu Philosophy: I Am Because We Are",
|
78 |
+
description: "Deep dive into Ubuntu philosophy and its relevance in modern African healing",
|
79 |
+
type: "interactive",
|
80 |
+
category: "Ubuntu Wisdom",
|
81 |
+
difficulty: "easy",
|
82 |
+
duration: 20,
|
83 |
+
flbReward: 50,
|
84 |
+
xpReward: 100,
|
85 |
+
completed: false,
|
86 |
+
content: {
|
87 |
+
concepts: [
|
88 |
+
{
|
89 |
+
concept: "Umuntu ngumuntu ngabantu",
|
90 |
+
translation: "A person is a person through other persons",
|
91 |
+
explanation: "The fundamental concept of Ubuntu - our humanity is interconnected",
|
92 |
+
modernApplication: "In healthcare, this guides collaborative approaches to healing",
|
93 |
+
},
|
94 |
+
{
|
95 |
+
concept: "Ubuntu ngumuntu",
|
96 |
+
translation: "Ubuntu is humanity",
|
97 |
+
explanation: "Ubuntu represents the essence of being human through community",
|
98 |
+
modernApplication: "In conflict resolution, Ubuntu provides framework for healing",
|
99 |
+
},
|
100 |
+
],
|
101 |
+
},
|
102 |
+
},
|
103 |
+
{
|
104 |
+
id: "addiction-africa-1",
|
105 |
+
title: "Understanding Addiction in Africa",
|
106 |
+
description: "Explore cultural and social aspects of addiction across African communities",
|
107 |
+
type: "quiz",
|
108 |
+
category: "Health Advocacy",
|
109 |
+
difficulty: "medium",
|
110 |
+
duration: 25,
|
111 |
+
flbReward: 75,
|
112 |
+
xpReward: 150,
|
113 |
+
completed: false,
|
114 |
+
content: {
|
115 |
+
questions: [
|
116 |
+
{
|
117 |
+
question: "What role do traditional healers play in addiction recovery in Africa?",
|
118 |
+
options: [
|
119 |
+
"They should be completely replaced by modern medicine",
|
120 |
+
"They complement modern treatment with cultural understanding",
|
121 |
+
"They only use harmful practices",
|
122 |
+
"They have no role in recovery",
|
123 |
+
],
|
124 |
+
correct: 1,
|
125 |
+
explanation:
|
126 |
+
"Traditional healers provide cultural context and community support that enhances modern treatment approaches",
|
127 |
+
},
|
128 |
+
{
|
129 |
+
question: "How does Ubuntu philosophy apply to addiction recovery?",
|
130 |
+
options: [
|
131 |
+
"Individual treatment is most important",
|
132 |
+
"Community healing and collective support",
|
133 |
+
"Isolation from community",
|
134 |
+
"Western approaches only",
|
135 |
+
],
|
136 |
+
correct: 1,
|
137 |
+
explanation: "Ubuntu emphasizes that healing happens through community support and collective responsibility",
|
138 |
+
},
|
139 |
+
],
|
140 |
+
},
|
141 |
+
},
|
142 |
+
{
|
143 |
+
id: "community-prevention-1",
|
144 |
+
title: "Community Prevention Strategies",
|
145 |
+
description: "Learn effective prevention methods within African community structures",
|
146 |
+
type: "video",
|
147 |
+
category: "Health Advocacy",
|
148 |
+
difficulty: "medium",
|
149 |
+
duration: 30,
|
150 |
+
flbReward: 60,
|
151 |
+
xpReward: 120,
|
152 |
+
completed: false,
|
153 |
+
content: {
|
154 |
+
videoUrl: "/videos/community-prevention.mp4",
|
155 |
+
keyPoints: [
|
156 |
+
"School-based prevention programs",
|
157 |
+
"Faith-based community approaches",
|
158 |
+
"Elder and traditional leader involvement",
|
159 |
+
"Youth peer education networks",
|
160 |
+
"Economic empowerment as prevention",
|
161 |
+
],
|
162 |
+
},
|
163 |
+
},
|
164 |
+
{
|
165 |
+
id: "akan-proverbs-1",
|
166 |
+
title: "Akan Proverbs: Wisdom for Healing",
|
167 |
+
description: "Explore Akan proverbs and their applications in community healing",
|
168 |
+
type: "interactive",
|
169 |
+
category: "Cultural Heritage",
|
170 |
+
difficulty: "easy",
|
171 |
+
duration: 15,
|
172 |
+
flbReward: 40,
|
173 |
+
xpReward: 80,
|
174 |
+
completed: false,
|
175 |
+
content: {
|
176 |
+
proverbs: [
|
177 |
+
{
|
178 |
+
akan: "Se wo were fi na wosan kofa a, yenkyiri",
|
179 |
+
english: "It is not taboo to go back for what you forgot",
|
180 |
+
meaning: "There's no shame in correcting mistakes or seeking help",
|
181 |
+
healing: "Encourages people to seek treatment without stigma",
|
182 |
+
},
|
183 |
+
{
|
184 |
+
akan: "Obi nkyere abofra Nyame",
|
185 |
+
english: "No one teaches a child about God",
|
186 |
+
meaning: "Some knowledge is innate and universal",
|
187 |
+
healing: "Natural understanding of right and wrong guides recovery",
|
188 |
+
},
|
189 |
+
],
|
190 |
+
},
|
191 |
+
},
|
192 |
+
{
|
193 |
+
id: "treatment-centers-1",
|
194 |
+
title: "Treatment Center Operations in Africa",
|
195 |
+
description: "Understand how treatment centers operate in resource-limited settings",
|
196 |
+
type: "video",
|
197 |
+
category: "Health Advocacy",
|
198 |
+
difficulty: "hard",
|
199 |
+
duration: 40,
|
200 |
+
flbReward: 100,
|
201 |
+
xpReward: 200,
|
202 |
+
completed: false,
|
203 |
+
content: {
|
204 |
+
videoUrl: "/videos/treatment-centers.mp4",
|
205 |
+
keyPoints: [
|
206 |
+
"Staffing with limited resources",
|
207 |
+
"Integrating traditional and modern approaches",
|
208 |
+
"Community-based aftercare programs",
|
209 |
+
"Sustainable funding models",
|
210 |
+
"Measuring success in African contexts",
|
211 |
+
],
|
212 |
+
},
|
213 |
+
},
|
214 |
+
{
|
215 |
+
id: "youth-leadership-1",
|
216 |
+
title: "Youth Leadership in Health Advocacy",
|
217 |
+
description: "Develop skills to become a health advocate in your community",
|
218 |
+
type: "story",
|
219 |
+
category: "Health Advocacy",
|
220 |
+
difficulty: "medium",
|
221 |
+
duration: 25,
|
222 |
+
flbReward: 65,
|
223 |
+
xpReward: 130,
|
224 |
+
completed: false,
|
225 |
+
content: {
|
226 |
+
prompt:
|
227 |
+
"Share a story about how you've seen young people make a difference in health issues in your community, or describe how you would lead a health initiative using Ubuntu principles.",
|
228 |
+
minWords: 150,
|
229 |
+
maxWords: 500,
|
230 |
+
},
|
231 |
+
},
|
232 |
+
]
|
233 |
+
|
234 |
+
const categoryIcons = {
|
235 |
+
"Cultural Heritage": Globe,
|
236 |
+
Philosophy: Brain,
|
237 |
+
"Health Advocacy": Stethoscope,
|
238 |
+
Technology: Cpu,
|
239 |
+
"Ubuntu Wisdom": Heart,
|
240 |
+
}
|
241 |
+
|
242 |
+
const difficultyColors = {
|
243 |
+
easy: "bg-green-100 text-green-800",
|
244 |
+
medium: "bg-yellow-100 text-yellow-800",
|
245 |
+
hard: "bg-red-100 text-red-800",
|
246 |
+
}
|
247 |
+
|
248 |
+
const redemptionKits = [
|
249 |
+
{
|
250 |
+
id: "mama-wellness",
|
251 |
+
name: "Mama Wellness Pack",
|
252 |
+
cost: 4,
|
253 |
+
currency: "FLB",
|
254 |
+
items: ["Clinic voucher", "Basic medicines", "Transport allowance"],
|
255 |
+
color: "bg-green-50 border-green-200",
|
256 |
+
},
|
257 |
+
{
|
258 |
+
id: "school-starter",
|
259 |
+
name: "School Starter Kit",
|
260 |
+
cost: 6,
|
261 |
+
currency: "FLB",
|
262 |
+
items: ["School uniform", "Books and supplies", "Lunch pass"],
|
263 |
+
color: "bg-blue-50 border-blue-200",
|
264 |
+
},
|
265 |
+
{
|
266 |
+
id: "community-builder",
|
267 |
+
name: "Community Builder Pack",
|
268 |
+
cost: 10,
|
269 |
+
currency: "FLB",
|
270 |
+
items: ["Seed packs", "Toolkit", "Training session"],
|
271 |
+
color: "bg-purple-50 border-purple-200",
|
272 |
+
},
|
273 |
+
]
|
274 |
+
|
275 |
+
export default function LearnEarnPage() {
|
276 |
+
const [selectedCategory, setSelectedCategory] = useState<string>("all")
|
277 |
+
const [selectedDifficulty, setSelectedDifficulty] = useState<string>("all")
|
278 |
+
const [activeModule, setActiveModule] = useState<Module | null>(null)
|
279 |
+
const [currentStep, setCurrentStep] = useState(0)
|
280 |
+
const [userAnswers, setUserAnswers] = useState<number[]>([])
|
281 |
+
const [storyContent, setStoryContent] = useState("")
|
282 |
+
const [moduleProgress, setModuleProgress] = useState<{ [key: string]: boolean }>({})
|
283 |
+
const [isOfflineMode, setIsOfflineMode] = useState(false)
|
284 |
+
const [wallet, setWallet] = useState<WalletInfo>({
|
285 |
+
address: "",
|
286 |
+
flbBalance: 0,
|
287 |
+
celoBalance: 0,
|
288 |
+
connected: false,
|
289 |
+
})
|
290 |
+
const [transactions, setTransactions] = useState<Transaction[]>([])
|
291 |
+
const [userStats, setUserStats] = useState({
|
292 |
+
totalFLB: 0,
|
293 |
+
totalXP: 0,
|
294 |
+
completedModules: 0,
|
295 |
+
currentStreak: 7,
|
296 |
+
learnerLevel: "Beginner",
|
297 |
+
impactScore: 0,
|
298 |
+
})
|
299 |
+
const [showWalletModal, setShowWalletModal] = useState(false)
|
300 |
+
const [showCourseModal, setShowCourseModal] = useState(false)
|
301 |
+
const [selectedCourse, setSelectedCourse] = useState<Module | null>(null)
|
302 |
+
|
303 |
+
// Initialize with mock data for demo
|
304 |
+
useEffect(() => {
|
305 |
+
// Simulate some completed modules and transactions
|
306 |
+
const mockTransactions: Transaction[] = [
|
307 |
+
{
|
308 |
+
id: "tx1",
|
309 |
+
type: "earned",
|
310 |
+
amount: 50,
|
311 |
+
description: "Completed Ubuntu Philosophy course",
|
312 |
+
timestamp: new Date(Date.now() - 86400000),
|
313 |
+
status: "completed",
|
314 |
+
},
|
315 |
+
{
|
316 |
+
id: "tx2",
|
317 |
+
type: "donated",
|
318 |
+
amount: 20,
|
319 |
+
description: "Donated to Community Health Center",
|
320 |
+
timestamp: new Date(Date.now() - 172800000),
|
321 |
+
status: "completed",
|
322 |
+
},
|
323 |
+
]
|
324 |
+
setTransactions(mockTransactions)
|
325 |
+
}, [])
|
326 |
+
|
327 |
+
const filteredModules = modules.filter((module) => {
|
328 |
+
const categoryMatch = selectedCategory === "all" || module.category === selectedCategory
|
329 |
+
const difficultyMatch = selectedDifficulty === "all" || module.difficulty === selectedDifficulty
|
330 |
+
return categoryMatch && difficultyMatch
|
331 |
+
})
|
332 |
+
|
333 |
+
const connectWallet = async (walletType: string) => {
|
334 |
+
// Mock wallet connection
|
335 |
+
const mockAddresses = [
|
336 |
+
"0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
|
337 |
+
"0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B",
|
338 |
+
"0x4B0897b0513fdC7C541B6d9D7E929C4e5364D2dB",
|
339 |
+
]
|
340 |
+
|
341 |
+
const randomAddress = mockAddresses[Math.floor(Math.random() * mockAddresses.length)]
|
342 |
+
const randomFLB = Math.floor(Math.random() * 500) + 50
|
343 |
+
const randomCELO = Math.random() * 5
|
344 |
+
|
345 |
+
setWallet({
|
346 |
+
address: randomAddress,
|
347 |
+
flbBalance: randomFLB,
|
348 |
+
celoBalance: randomCELO,
|
349 |
+
connected: true,
|
350 |
+
})
|
351 |
+
|
352 |
+
setUserStats((prev) => ({
|
353 |
+
...prev,
|
354 |
+
totalFLB: randomFLB,
|
355 |
+
}))
|
356 |
+
|
357 |
+
setShowWalletModal(false)
|
358 |
+
}
|
359 |
+
|
360 |
+
const startModule = (module: Module) => {
|
361 |
+
setActiveModule(module)
|
362 |
+
setCurrentStep(0)
|
363 |
+
setUserAnswers([])
|
364 |
+
setStoryContent("")
|
365 |
+
setShowCourseModal(false)
|
366 |
+
}
|
367 |
+
|
368 |
+
const completeModule = async (moduleId: string, score = 100) => {
|
369 |
+
const module = modules.find((m) => m.id === moduleId)
|
370 |
+
if (!module) return
|
371 |
+
|
372 |
+
// Calculate rewards based on score
|
373 |
+
const flbReward = Math.floor((score / 100) * module.flbReward)
|
374 |
+
const xpReward = Math.floor((score / 100) * module.xpReward)
|
375 |
+
|
376 |
+
// Update user stats
|
377 |
+
setUserStats((prev) => ({
|
378 |
+
...prev,
|
379 |
+
totalFLB: prev.totalFLB + flbReward,
|
380 |
+
totalXP: prev.totalXP + xpReward,
|
381 |
+
completedModules: prev.completedModules + 1,
|
382 |
+
impactScore: prev.impactScore + Math.floor(flbReward / 10),
|
383 |
+
}))
|
384 |
+
|
385 |
+
// Update wallet balance
|
386 |
+
setWallet((prev) => ({
|
387 |
+
...prev,
|
388 |
+
flbBalance: prev.flbBalance + flbReward,
|
389 |
+
}))
|
390 |
+
|
391 |
+
// Add transaction
|
392 |
+
const newTransaction: Transaction = {
|
393 |
+
id: `tx-${Date.now()}`,
|
394 |
+
type: "earned",
|
395 |
+
amount: flbReward,
|
396 |
+
description: `Completed ${module.title}`,
|
397 |
+
timestamp: new Date(),
|
398 |
+
status: "completed",
|
399 |
+
}
|
400 |
+
setTransactions((prev) => [newTransaction, ...prev])
|
401 |
+
|
402 |
+
// Mark module as completed
|
403 |
+
setModuleProgress((prev) => ({
|
404 |
+
...prev,
|
405 |
+
[moduleId]: true,
|
406 |
+
}))
|
407 |
+
|
408 |
+
// Close module
|
409 |
+
setActiveModule(null)
|
410 |
+
|
411 |
+
// Show success message
|
412 |
+
alert(`🔥 Ubuntu wisdom earned! ${flbReward} FLB and ${xpReward} XP added to your account!`)
|
413 |
+
}
|
414 |
+
|
415 |
+
const handleQuizAnswer = (questionIndex: number, answerIndex: number) => {
|
416 |
+
const newAnswers = [...userAnswers]
|
417 |
+
newAnswers[questionIndex] = answerIndex
|
418 |
+
setUserAnswers(newAnswers)
|
419 |
+
}
|
420 |
+
|
421 |
+
const calculateQuizScore = () => {
|
422 |
+
if (!activeModule || activeModule.type !== "quiz") return 0
|
423 |
+
const questions = activeModule.content.questions as QuizQuestion[]
|
424 |
+
const correctAnswers = userAnswers.filter((answer, index) => answer === questions[index].correct).length
|
425 |
+
return Math.round((correctAnswers / questions.length) * 100)
|
426 |
+
}
|
427 |
+
|
428 |
+
const redeemKit = async (kitId: string) => {
|
429 |
+
const kit = redemptionKits.find((k) => k.id === kitId)
|
430 |
+
if (!kit || wallet.flbBalance < kit.cost) {
|
431 |
+
alert("Insufficient FLB balance for this redemption kit")
|
432 |
+
return
|
433 |
+
}
|
434 |
+
|
435 |
+
// Deduct FLB
|
436 |
+
setWallet((prev) => ({
|
437 |
+
...prev,
|
438 |
+
flbBalance: prev.flbBalance - kit.cost,
|
439 |
+
}))
|
440 |
+
|
441 |
+
// Add transaction
|
442 |
+
const newTransaction: Transaction = {
|
443 |
+
id: `tx-${Date.now()}`,
|
444 |
+
type: "donated",
|
445 |
+
amount: kit.cost,
|
446 |
+
description: `Redeemed ${kit.name}`,
|
447 |
+
timestamp: new Date(),
|
448 |
+
status: "completed",
|
449 |
+
}
|
450 |
+
setTransactions((prev) => [newTransaction, ...prev])
|
451 |
+
|
452 |
+
alert(`🎉 ${kit.name} redeemed successfully! Your impact is making a difference in African communities.`)
|
453 |
+
}
|
454 |
+
|
455 |
+
const toggleOfflineMode = () => {
|
456 |
+
setIsOfflineMode(!isOfflineMode)
|
457 |
+
if (!isOfflineMode) {
|
458 |
+
alert("📱 Offline mode enabled! Course materials downloaded for offline access.")
|
459 |
+
}
|
460 |
+
}
|
461 |
+
|
462 |
+
const exportProgress = async () => {
|
463 |
+
const progressData = {
|
464 |
+
userStats,
|
465 |
+
completedModules: moduleProgress,
|
466 |
+
transactions,
|
467 |
+
wallet: wallet.connected ? { address: wallet.address, balance: wallet.flbBalance } : null,
|
468 |
+
timestamp: new Date().toISOString(),
|
469 |
+
}
|
470 |
+
|
471 |
+
const progressText = JSON.stringify(progressData, null, 2)
|
472 |
+
const result = await ClipboardManager.copyToClipboard(progressText)
|
473 |
+
|
474 |
+
if (result.success) {
|
475 |
+
alert("📋 Progress data copied to clipboard!")
|
476 |
+
} else {
|
477 |
+
// Fallback to download
|
478 |
+
const blob = new Blob([progressText], { type: "application/json" })
|
479 |
+
const url = URL.createObjectURL(blob)
|
480 |
+
const a = document.createElement("a")
|
481 |
+
a.href = url
|
482 |
+
a.download = `flameborn-progress-${new Date().toISOString().split("T")[0]}.json`
|
483 |
+
document.body.appendChild(a)
|
484 |
+
a.click()
|
485 |
+
document.body.removeChild(a)
|
486 |
+
URL.revokeObjectURL(url)
|
487 |
+
}
|
488 |
+
}
|
489 |
+
|
490 |
+
const renderModuleContent = () => {
|
491 |
+
if (!activeModule) return null
|
492 |
+
|
493 |
+
switch (activeModule.type) {
|
494 |
+
case "interactive":
|
495 |
+
return (
|
496 |
+
<div className="space-y-6">
|
497 |
+
<div className="text-center mb-6">
|
498 |
+
<h2 className="text-2xl font-bold text-orange-800 mb-2">{activeModule.title}</h2>
|
499 |
+
<p className="text-gray-600">{activeModule.description}</p>
|
500 |
+
</div>
|
501 |
+
|
502 |
+
{activeModule.content.concepts &&
|
503 |
+
activeModule.content.concepts.map((concept: any, index: number) => (
|
504 |
+
<motion.div
|
505 |
+
key={index}
|
506 |
+
initial={{ opacity: 0, y: 20 }}
|
507 |
+
animate={{ opacity: 1, y: 0 }}
|
508 |
+
transition={{ delay: index * 0.2 }}
|
509 |
+
>
|
510 |
+
<Card className="border-l-4 border-l-orange-500">
|
511 |
+
<CardContent className="p-6">
|
512 |
+
<div className="space-y-4">
|
513 |
+
<div className="text-center">
|
514 |
+
<p className="text-xl font-semibold text-orange-800 mb-2">"{concept.concept}"</p>
|
515 |
+
<p className="text-lg text-gray-700 italic">"{concept.translation}"</p>
|
516 |
+
</div>
|
517 |
+
<div className="bg-orange-50 p-4 rounded-lg">
|
518 |
+
<h4 className="font-semibold text-orange-800 mb-2">Ubuntu Meaning:</h4>
|
519 |
+
<p className="text-gray-700">{concept.explanation}</p>
|
520 |
+
</div>
|
521 |
+
<div className="bg-amber-50 p-4 rounded-lg">
|
522 |
+
<h4 className="font-semibold text-amber-800 mb-2">Modern Application:</h4>
|
523 |
+
<p className="text-gray-700">{concept.modernApplication}</p>
|
524 |
+
</div>
|
525 |
+
</div>
|
526 |
+
</CardContent>
|
527 |
+
</Card>
|
528 |
+
</motion.div>
|
529 |
+
))}
|
530 |
+
|
531 |
+
{activeModule.content.proverbs &&
|
532 |
+
activeModule.content.proverbs.map((proverb: any, index: number) => (
|
533 |
+
<motion.div
|
534 |
+
key={index}
|
535 |
+
initial={{ opacity: 0, y: 20 }}
|
536 |
+
animate={{ opacity: 1, y: 0 }}
|
537 |
+
transition={{ delay: index * 0.2 }}
|
538 |
+
>
|
539 |
+
<Card className="border-l-4 border-l-green-500">
|
540 |
+
<CardContent className="p-6">
|
541 |
+
<div className="space-y-4">
|
542 |
+
<div className="text-center">
|
543 |
+
<p className="text-xl font-semibold text-green-800 mb-2">"{proverb.akan}"</p>
|
544 |
+
<p className="text-lg text-gray-700 italic">"{proverb.english}"</p>
|
545 |
+
</div>
|
546 |
+
<div className="bg-green-50 p-4 rounded-lg">
|
547 |
+
<h4 className="font-semibold text-green-800 mb-2">Wisdom:</h4>
|
548 |
+
<p className="text-gray-700">{proverb.meaning}</p>
|
549 |
+
</div>
|
550 |
+
<div className="bg-blue-50 p-4 rounded-lg">
|
551 |
+
<h4 className="font-semibold text-blue-800 mb-2">Healing Application:</h4>
|
552 |
+
<p className="text-gray-700">{proverb.healing}</p>
|
553 |
+
</div>
|
554 |
+
</div>
|
555 |
+
</CardContent>
|
556 |
+
</Card>
|
557 |
+
</motion.div>
|
558 |
+
))}
|
559 |
+
|
560 |
+
<div className="text-center pt-6">
|
561 |
+
<Button onClick={() => completeModule(activeModule.id)} className="bg-orange-600 hover:bg-orange-700">
|
562 |
+
Complete Module & Earn {activeModule.flbReward} FLB
|
563 |
+
</Button>
|
564 |
+
</div>
|
565 |
+
</div>
|
566 |
+
)
|
567 |
+
|
568 |
+
case "quiz":
|
569 |
+
const questions = activeModule.content.questions as QuizQuestion[]
|
570 |
+
return (
|
571 |
+
<div className="space-y-6">
|
572 |
+
<div className="text-center mb-6">
|
573 |
+
<h2 className="text-2xl font-bold text-orange-800 mb-2">{activeModule.title}</h2>
|
574 |
+
<p className="text-gray-600">{activeModule.description}</p>
|
575 |
+
<Progress value={(userAnswers.length / questions.length) * 100} className="mt-4" />
|
576 |
+
</div>
|
577 |
+
|
578 |
+
{questions.map((question, index) => (
|
579 |
+
<Card key={index} className="border-l-4 border-l-blue-500">
|
580 |
+
<CardContent className="p-6">
|
581 |
+
<h3 className="text-lg font-semibold mb-4">
|
582 |
+
Question {index + 1}: {question.question}
|
583 |
+
</h3>
|
584 |
+
<div className="space-y-2">
|
585 |
+
{question.options.map((option, optionIndex) => (
|
586 |
+
<Button
|
587 |
+
key={optionIndex}
|
588 |
+
variant={userAnswers[index] === optionIndex ? "default" : "outline"}
|
589 |
+
className="w-full text-left justify-start"
|
590 |
+
onClick={() => handleQuizAnswer(index, optionIndex)}
|
591 |
+
>
|
592 |
+
{option}
|
593 |
+
</Button>
|
594 |
+
))}
|
595 |
+
</div>
|
596 |
+
{userAnswers[index] !== undefined && (
|
597 |
+
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
|
598 |
+
<p className="text-sm text-blue-800">
|
599 |
+
<strong>Ubuntu Wisdom:</strong> {question.explanation}
|
600 |
+
</p>
|
601 |
+
</div>
|
602 |
+
)}
|
603 |
+
</CardContent>
|
604 |
+
</Card>
|
605 |
+
))}
|
606 |
+
|
607 |
+
{userAnswers.length === questions.length && (
|
608 |
+
<div className="text-center pt-6">
|
609 |
+
<div className="mb-4">
|
610 |
+
<p className="text-lg font-semibold">Your Ubuntu Score: {calculateQuizScore()}%</p>
|
611 |
+
</div>
|
612 |
+
<Button
|
613 |
+
onClick={() => completeModule(activeModule.id, calculateQuizScore())}
|
614 |
+
className="bg-blue-600 hover:bg-blue-700"
|
615 |
+
>
|
616 |
+
Complete Quiz & Earn Rewards
|
617 |
+
</Button>
|
618 |
+
</div>
|
619 |
+
)}
|
620 |
+
</div>
|
621 |
+
)
|
622 |
+
|
623 |
+
case "story":
|
624 |
+
return (
|
625 |
+
<div className="space-y-6">
|
626 |
+
<div className="text-center mb-6">
|
627 |
+
<h2 className="text-2xl font-bold text-orange-800 mb-2">{activeModule.title}</h2>
|
628 |
+
<p className="text-gray-600">{activeModule.description}</p>
|
629 |
+
</div>
|
630 |
+
|
631 |
+
<Card className="border-l-4 border-l-purple-500">
|
632 |
+
<CardContent className="p-6">
|
633 |
+
<h3 className="text-lg font-semibold mb-4">Ubuntu Story Prompt:</h3>
|
634 |
+
<p className="text-gray-700 mb-6">{activeModule.content.prompt}</p>
|
635 |
+
|
636 |
+
<div className="space-y-4">
|
637 |
+
<Input placeholder="Story title (optional)" className="w-full" />
|
638 |
+
<Textarea
|
639 |
+
placeholder="Share your Ubuntu story here..."
|
640 |
+
value={storyContent}
|
641 |
+
onChange={(e) => setStoryContent(e.target.value)}
|
642 |
+
className="min-h-[200px]"
|
643 |
+
/>
|
644 |
+
<div className="flex justify-between text-sm text-gray-500">
|
645 |
+
<span>{storyContent.split(" ").filter((word) => word.length > 0).length} words</span>
|
646 |
+
<span>
|
647 |
+
Min: {activeModule.content.minWords} | Max: {activeModule.content.maxWords}
|
648 |
+
</span>
|
649 |
+
</div>
|
650 |
+
</div>
|
651 |
+
|
652 |
+
<div className="text-center pt-6">
|
653 |
+
<Button
|
654 |
+
onClick={() => completeModule(activeModule.id)}
|
655 |
+
disabled={
|
656 |
+
storyContent.split(" ").filter((word) => word.length > 0).length < activeModule.content.minWords
|
657 |
+
}
|
658 |
+
className="bg-purple-600 hover:bg-purple-700"
|
659 |
+
>
|
660 |
+
Share Ubuntu Story & Earn {activeModule.flbReward} FLB
|
661 |
+
</Button>
|
662 |
+
</div>
|
663 |
+
</CardContent>
|
664 |
+
</Card>
|
665 |
+
</div>
|
666 |
+
)
|
667 |
+
|
668 |
+
case "video":
|
669 |
+
return (
|
670 |
+
<div className="space-y-6">
|
671 |
+
<div className="text-center mb-6">
|
672 |
+
<h2 className="text-2xl font-bold text-orange-800 mb-2">{activeModule.title}</h2>
|
673 |
+
<p className="text-gray-600">{activeModule.description}</p>
|
674 |
+
</div>
|
675 |
+
|
676 |
+
<Card className="border-l-4 border-l-green-500">
|
677 |
+
<CardContent className="p-6">
|
678 |
+
<div className="aspect-video bg-gray-200 rounded-lg flex items-center justify-center mb-6">
|
679 |
+
<div className="text-center">
|
680 |
+
<Play className="h-16 w-16 text-gray-400 mx-auto mb-2" />
|
681 |
+
<p className="text-gray-600">Video: {activeModule.title}</p>
|
682 |
+
<p className="text-sm text-gray-500">Duration: {activeModule.duration} minutes</p>
|
683 |
+
</div>
|
684 |
+
</div>
|
685 |
+
|
686 |
+
<div className="space-y-4">
|
687 |
+
<h3 className="text-lg font-semibold">Key Ubuntu Learning Points:</h3>
|
688 |
+
<ul className="space-y-2">
|
689 |
+
{activeModule.content.keyPoints.map((point: string, index: number) => (
|
690 |
+
<li key={index} className="flex items-start gap-2">
|
691 |
+
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
|
692 |
+
<span>{point}</span>
|
693 |
+
</li>
|
694 |
+
))}
|
695 |
+
</ul>
|
696 |
+
</div>
|
697 |
+
|
698 |
+
<div className="text-center pt-6">
|
699 |
+
<Button onClick={() => completeModule(activeModule.id)} className="bg-green-600 hover:bg-green-700">
|
700 |
+
Mark as Watched & Earn {activeModule.flbReward} FLB
|
701 |
+
</Button>
|
702 |
+
</div>
|
703 |
+
</CardContent>
|
704 |
+
</Card>
|
705 |
+
</div>
|
706 |
+
)
|
707 |
+
|
708 |
+
default:
|
709 |
+
return null
|
710 |
+
}
|
711 |
+
}
|
712 |
+
|
713 |
+
if (activeModule) {
|
714 |
+
return (
|
715 |
+
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-amber-50 to-yellow-50">
|
716 |
+
<div className="container mx-auto px-4 py-8">
|
717 |
+
<div className="mb-6">
|
718 |
+
<Button variant="outline" onClick={() => setActiveModule(null)} className="mb-4">
|
719 |
+
← Back to Learn & Earn
|
720 |
+
</Button>
|
721 |
+
</div>
|
722 |
+
|
723 |
+
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="max-w-4xl mx-auto">
|
724 |
+
{renderModuleContent()}
|
725 |
+
</motion.div>
|
726 |
+
</div>
|
727 |
+
</div>
|
728 |
+
)
|
729 |
+
}
|
730 |
+
|
731 |
+
return (
|
732 |
+
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-amber-50 to-yellow-50">
|
733 |
+
{/* Hero Section */}
|
734 |
+
<div className="bg-gradient-to-r from-green-600 to-blue-600 text-white py-16">
|
735 |
+
<div className="container mx-auto px-4 text-center">
|
736 |
+
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }}>
|
737 |
+
<h1 className="text-5xl font-bold mb-4">Learn. Earn. Heal Africa.</h1>
|
738 |
+
<p className="text-xl text-green-100 max-w-3xl mx-auto mb-8">
|
739 |
+
Educate yourself on African healing wisdom, earn FLB tokens, and support real treatment centers across the
|
740 |
+
continent through Ubuntu principles.
|
741 |
+
</p>
|
742 |
+
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
743 |
+
<Button
|
744 |
+
onClick={() => document.getElementById("courses")?.scrollIntoView({ behavior: "smooth" })}
|
745 |
+
className="bg-white text-green-700 hover:bg-gray-100 px-8 py-3 text-lg"
|
746 |
+
>
|
747 |
+
Start Learning Ubuntu
|
748 |
+
</Button>
|
749 |
+
<Button
|
750 |
+
variant="outline"
|
751 |
+
className="border-white text-white hover:bg-white hover:text-green-700 px-8 py-3 text-lg bg-transparent"
|
752 |
+
>
|
753 |
+
How It Works
|
754 |
+
</Button>
|
755 |
+
</div>
|
756 |
+
</motion.div>
|
757 |
+
</div>
|
758 |
+
</div>
|
759 |
+
|
760 |
+
<div className="container mx-auto px-4 py-8">
|
761 |
+
{/* Stats Overview */}
|
762 |
+
<motion.div
|
763 |
+
initial={{ opacity: 0, y: 20 }}
|
764 |
+
animate={{ opacity: 1, y: 0 }}
|
765 |
+
className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8"
|
766 |
+
>
|
767 |
+
<Card className="bg-gradient-to-br from-green-50 to-green-100">
|
768 |
+
<CardContent className="p-6 text-center">
|
769 |
+
<Coins className="h-8 w-8 text-green-500 mx-auto mb-2" />
|
770 |
+
<div className="text-2xl font-bold text-green-600">{userStats.totalFLB}</div>
|
771 |
+
<div className="text-sm text-gray-600">FLB Earned</div>
|
772 |
+
</CardContent>
|
773 |
+
</Card>
|
774 |
+
|
775 |
+
<Card className="bg-gradient-to-br from-blue-50 to-blue-100">
|
776 |
+
<CardContent className="p-6 text-center">
|
777 |
+
<Star className="h-8 w-8 text-blue-500 mx-auto mb-2" />
|
778 |
+
<div className="text-2xl font-bold text-blue-600">{userStats.totalXP}</div>
|
779 |
+
<div className="text-sm text-gray-600">Ubuntu XP</div>
|
780 |
+
</CardContent>
|
781 |
+
</Card>
|
782 |
+
|
783 |
+
<Card className="bg-gradient-to-br from-purple-50 to-purple-100">
|
784 |
+
<CardContent className="p-6 text-center">
|
785 |
+
<CheckCircle className="h-8 w-8 text-purple-500 mx-auto mb-2" />
|
786 |
+
<div className="text-2xl font-bold text-purple-600">{userStats.completedModules}</div>
|
787 |
+
<div className="text-sm text-gray-600">Modules Completed</div>
|
788 |
+
</CardContent>
|
789 |
+
</Card>
|
790 |
+
|
791 |
+
<Card className="bg-gradient-to-br from-orange-50 to-orange-100">
|
792 |
+
<CardContent className="p-6 text-center">
|
793 |
+
<Heart className="h-8 w-8 text-orange-500 mx-auto mb-2" />
|
794 |
+
<div className="text-2xl font-bold text-orange-600">{userStats.impactScore}</div>
|
795 |
+
<div className="text-sm text-gray-600">Impact Score</div>
|
796 |
+
</CardContent>
|
797 |
+
</Card>
|
798 |
+
</motion.div>
|
799 |
+
|
800 |
+
{/* Ubuntu Dashboard */}
|
801 |
+
<motion.div
|
802 |
+
initial={{ opacity: 0, y: 20 }}
|
803 |
+
animate={{ opacity: 1, y: 0 }}
|
804 |
+
transition={{ delay: 0.2 }}
|
805 |
+
className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8"
|
806 |
+
>
|
807 |
+
<Card className="bg-green-50">
|
808 |
+
<CardContent className="p-6 text-center">
|
809 |
+
<Coins className="h-12 w-12 text-green-600 mx-auto mb-4" />
|
810 |
+
<h3 className="text-xl font-bold text-gray-900">Earn FLB</h3>
|
811 |
+
<p className="text-gray-600">Complete Ubuntu quests</p>
|
812 |
+
</CardContent>
|
813 |
+
</Card>
|
814 |
+
|
815 |
+
<Card className="bg-blue-50">
|
816 |
+
<CardContent className="p-6 text-center">
|
817 |
+
<HandHeart className="h-12 w-12 text-blue-600 mx-auto mb-4" />
|
818 |
+
<h3 className="text-xl font-bold text-gray-900">Heal</h3>
|
819 |
+
<p className="text-gray-600">Support African healing</p>
|
820 |
+
</CardContent>
|
821 |
+
</Card>
|
822 |
+
|
823 |
+
<Card className="bg-purple-50">
|
824 |
+
<CardContent className="p-6 text-center">
|
825 |
+
<GraduationCap className="h-12 w-12 text-purple-600 mx-auto mb-4" />
|
826 |
+
<h3 className="text-xl font-bold text-gray-900">Learn</h3>
|
827 |
+
<p className="text-gray-600">Grow Ubuntu wisdom</p>
|
828 |
+
</CardContent>
|
829 |
+
</Card>
|
830 |
+
|
831 |
+
<Card className="bg-yellow-50">
|
832 |
+
<CardContent className="p-6 text-center">
|
833 |
+
<MessageSquare className="h-12 w-12 text-yellow-600 mx-auto mb-4" />
|
834 |
+
<h3 className="text-xl font-bold text-gray-900">Connect</h3>
|
835 |
+
<p className="text-gray-600">Community support</p>
|
836 |
+
</CardContent>
|
837 |
+
</Card>
|
838 |
+
</motion.div>
|
839 |
+
|
840 |
+
{/* Wallet Section */}
|
841 |
+
<motion.div
|
842 |
+
initial={{ opacity: 0, y: 20 }}
|
843 |
+
animate={{ opacity: 1, y: 0 }}
|
844 |
+
transition={{ delay: 0.3 }}
|
845 |
+
className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8"
|
846 |
+
>
|
847 |
+
<Card>
|
848 |
+
<CardHeader>
|
849 |
+
<CardTitle className="flex items-center">
|
850 |
+
<Wallet className="w-5 h-5 mr-2" />
|
851 |
+
Your FLB Wallet
|
852 |
+
</CardTitle>
|
853 |
+
</CardHeader>
|
854 |
+
<CardContent>
|
855 |
+
{!wallet.connected ? (
|
856 |
+
<div className="text-center py-8">
|
857 |
+
<Wallet className="h-16 w-16 text-gray-300 mx-auto mb-4" />
|
858 |
+
<h3 className="text-lg font-medium text-gray-900 mb-2">Wallet Not Connected</h3>
|
859 |
+
<p className="text-gray-600 mb-4">
|
860 |
+
Connect your Celo-compatible wallet to earn and manage FLB tokens
|
861 |
+
</p>
|
862 |
+
<Button onClick={() => setShowWalletModal(true)} className="bg-green-600 hover:bg-green-700">
|
863 |
+
Connect Wallet
|
864 |
+
</Button>
|
865 |
+
</div>
|
866 |
+
) : (
|
867 |
+
<div className="space-y-4">
|
868 |
+
<div className="flex justify-between items-center border-b pb-4">
|
869 |
+
<h3 className="text-lg font-medium">Connected Wallet</h3>
|
870 |
+
<Badge className="bg-green-100 text-green-800">Connected</Badge>
|
871 |
+
</div>
|
872 |
+
<div className="space-y-2">
|
873 |
+
<div className="flex justify-between">
|
874 |
+
<span className="text-gray-600">Address:</span>
|
875 |
+
<span className="font-mono text-sm">
|
876 |
+
{wallet.address.slice(0, 6)}...{wallet.address.slice(-4)}
|
877 |
+
</span>
|
878 |
+
</div>
|
879 |
+
<div className="flex justify-between">
|
880 |
+
<span className="text-gray-600">FLB Balance:</span>
|
881 |
+
<span className="font-bold text-green-600">{wallet.flbBalance} FLB</span>
|
882 |
+
</div>
|
883 |
+
<div className="flex justify-between">
|
884 |
+
<span className="text-gray-600">CELO Balance:</span>
|
885 |
+
<span className="font-bold text-yellow-600">{wallet.celoBalance.toFixed(2)} CELO</span>
|
886 |
+
</div>
|
887 |
+
</div>
|
888 |
+
<div className="grid grid-cols-2 gap-4 pt-4">
|
889 |
+
<Button className="bg-green-600 hover:bg-green-700">
|
890 |
+
<HandHeart className="w-4 h-4 mr-2" />
|
891 |
+
Donate
|
892 |
+
</Button>
|
893 |
+
<Button variant="outline">
|
894 |
+
<Download className="w-4 h-4 mr-2" />
|
895 |
+
Transfer
|
896 |
+
</Button>
|
897 |
+
</div>
|
898 |
+
</div>
|
899 |
+
)}
|
900 |
+
</CardContent>
|
901 |
+
</Card>
|
902 |
+
|
903 |
+
<Card>
|
904 |
+
<CardHeader>
|
905 |
+
<CardTitle>Recent Transactions</CardTitle>
|
906 |
+
</CardHeader>
|
907 |
+
<CardContent>
|
908 |
+
{transactions.length === 0 ? (
|
909 |
+
<div className="text-center py-8">
|
910 |
+
<Clock className="h-16 w-16 text-gray-300 mx-auto mb-4" />
|
911 |
+
<p className="text-gray-500">No transactions yet</p>
|
912 |
+
<p className="text-sm text-gray-400">Complete courses to earn FLB tokens</p>
|
913 |
+
</div>
|
914 |
+
) : (
|
915 |
+
<div className="space-y-3">
|
916 |
+
{transactions.slice(0, 5).map((tx) => (
|
917 |
+
<div key={tx.id} className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
|
918 |
+
<div>
|
919 |
+
<p
|
920 |
+
className={`font-medium ${
|
921 |
+
tx.type === "earned"
|
922 |
+
? "text-green-600"
|
923 |
+
: tx.type === "donated"
|
924 |
+
? "text-blue-600"
|
925 |
+
: "text-purple-600"
|
926 |
+
}`}
|
927 |
+
>
|
928 |
+
{tx.type === "earned" ? "+" : "-"}
|
929 |
+
{tx.amount} FLB
|
930 |
+
</p>
|
931 |
+
<p className="text-sm text-gray-500">{tx.description}</p>
|
932 |
+
</div>
|
933 |
+
<div className="text-right">
|
934 |
+
<p className="text-xs text-gray-500">{tx.timestamp.toLocaleDateString()}</p>
|
935 |
+
<Badge className="bg-green-100 text-green-800 text-xs">{tx.status}</Badge>
|
936 |
+
</div>
|
937 |
+
</div>
|
938 |
+
))}
|
939 |
+
</div>
|
940 |
+
)}
|
941 |
+
</CardContent>
|
942 |
+
</Card>
|
943 |
+
</motion.div>
|
944 |
+
|
945 |
+
{/* Offline Mode Banner */}
|
946 |
+
<motion.div
|
947 |
+
initial={{ opacity: 0, y: 20 }}
|
948 |
+
animate={{ opacity: 1, y: 0 }}
|
949 |
+
transition={{ delay: 0.4 }}
|
950 |
+
className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-8 rounded-lg"
|
951 |
+
>
|
952 |
+
<div className="flex items-center justify-between">
|
953 |
+
<div className="flex items-center">
|
954 |
+
{isOfflineMode ? (
|
955 |
+
<WifiOff className="h-6 w-6 text-yellow-500 mr-3" />
|
956 |
+
) : (
|
957 |
+
<Wifi className="h-6 w-6 text-yellow-500 mr-3" />
|
958 |
+
)}
|
959 |
+
<div>
|
960 |
+
<h3 className="text-lg font-medium text-yellow-800">
|
961 |
+
{isOfflineMode ? "Offline Mode Active" : "Offline Mode Available"}
|
962 |
+
</h3>
|
963 |
+
<p className="text-yellow-700">
|
964 |
+
{isOfflineMode
|
965 |
+
? "Course materials downloaded. Progress will sync when online."
|
966 |
+
: "Download course materials for offline access in rural areas."}
|
967 |
+
</p>
|
968 |
+
</div>
|
969 |
+
</div>
|
970 |
+
<Button
|
971 |
+
onClick={toggleOfflineMode}
|
972 |
+
variant="outline"
|
973 |
+
className="border-yellow-400 text-yellow-800 bg-transparent"
|
974 |
+
>
|
975 |
+
<Download className="w-4 h-4 mr-2" />
|
976 |
+
{isOfflineMode ? "Disable" : "Enable"} Offline Mode
|
977 |
+
</Button>
|
978 |
+
</div>
|
979 |
+
</motion.div>
|
980 |
+
|
981 |
+
{/* Filters */}
|
982 |
+
<motion.div
|
983 |
+
initial={{ opacity: 0, y: 20 }}
|
984 |
+
animate={{ opacity: 1, y: 0 }}
|
985 |
+
transition={{ delay: 0.5 }}
|
986 |
+
className="flex flex-wrap gap-4 mb-8 justify-center"
|
987 |
+
>
|
988 |
+
<div className="flex items-center gap-2">
|
989 |
+
<span className="text-sm font-medium">Category:</span>
|
990 |
+
<select
|
991 |
+
value={selectedCategory}
|
992 |
+
onChange={(e) => setSelectedCategory(e.target.value)}
|
993 |
+
className="bg-white border border-orange-300 rounded-lg px-3 py-2"
|
994 |
+
>
|
995 |
+
<option value="all">All Categories</option>
|
996 |
+
<option value="Ubuntu Wisdom">Ubuntu Wisdom</option>
|
997 |
+
<option value="Cultural Heritage">Cultural Heritage</option>
|
998 |
+
<option value="Health Advocacy">Health Advocacy</option>
|
999 |
+
<option value="Technology">Technology</option>
|
1000 |
+
</select>
|
1001 |
+
</div>
|
1002 |
+
|
1003 |
+
<div className="flex items-center gap-2">
|
1004 |
+
<span className="text-sm font-medium">Difficulty:</span>
|
1005 |
+
<select
|
1006 |
+
value={selectedDifficulty}
|
1007 |
+
onChange={(e) => setSelectedDifficulty(e.target.value)}
|
1008 |
+
className="bg-white border border-orange-300 rounded-lg px-3 py-2"
|
1009 |
+
>
|
1010 |
+
<option value="all">All Levels</option>
|
1011 |
+
<option value="easy">Easy</option>
|
1012 |
+
<option value="medium">Medium</option>
|
1013 |
+
<option value="hard">Hard</option>
|
1014 |
+
</select>
|
1015 |
+
</div>
|
1016 |
+
|
1017 |
+
<Button onClick={exportProgress} variant="outline" className="border-orange-300 bg-transparent">
|
1018 |
+
<Download className="w-4 h-4 mr-2" />
|
1019 |
+
Export Progress
|
1020 |
+
</Button>
|
1021 |
+
</motion.div>
|
1022 |
+
|
1023 |
+
{/* Modules Grid */}
|
1024 |
+
<motion.div
|
1025 |
+
id="courses"
|
1026 |
+
initial={{ opacity: 0, y: 20 }}
|
1027 |
+
animate={{ opacity: 1, y: 0 }}
|
1028 |
+
transition={{ delay: 0.6 }}
|
1029 |
+
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8"
|
1030 |
+
>
|
1031 |
+
{filteredModules.map((module, index) => {
|
1032 |
+
const IconComponent = categoryIcons[module.category]
|
1033 |
+
const isCompleted = moduleProgress[module.id]
|
1034 |
+
|
1035 |
+
return (
|
1036 |
+
<motion.div
|
1037 |
+
key={module.id}
|
1038 |
+
initial={{ opacity: 0, y: 20 }}
|
1039 |
+
animate={{ opacity: 1, y: 0 }}
|
1040 |
+
transition={{ delay: index * 0.1 }}
|
1041 |
+
>
|
1042 |
+
<Card
|
1043 |
+
className={`hover:shadow-lg transition-all duration-300 hover:-translate-y-1 cursor-pointer ${
|
1044 |
+
isCompleted ? "border-green-500 bg-green-50" : ""
|
1045 |
+
}`}
|
1046 |
+
onClick={() => {
|
1047 |
+
setSelectedCourse(module)
|
1048 |
+
setShowCourseModal(true)
|
1049 |
+
}}
|
1050 |
+
>
|
1051 |
+
<CardHeader>
|
1052 |
+
<div className="flex items-start justify-between">
|
1053 |
+
<div className="flex items-center gap-2">
|
1054 |
+
<IconComponent className="h-6 w-6 text-orange-500" />
|
1055 |
+
<Badge className={difficultyColors[module.difficulty]}>{module.difficulty}</Badge>
|
1056 |
+
</div>
|
1057 |
+
{isCompleted && <CheckCircle className="h-6 w-6 text-green-500" />}
|
1058 |
+
</div>
|
1059 |
+
<CardTitle className="text-lg">{module.title}</CardTitle>
|
1060 |
+
<CardDescription>{module.description}</CardDescription>
|
1061 |
+
</CardHeader>
|
1062 |
+
<CardContent>
|
1063 |
+
<div className="space-y-4">
|
1064 |
+
<div className="flex items-center justify-between text-sm text-gray-600">
|
1065 |
+
<span className="flex items-center">
|
1066 |
+
<Clock className="w-4 h-4 mr-1" />
|
1067 |
+
{module.duration} min
|
1068 |
+
</span>
|
1069 |
+
<span>{module.category}</span>
|
1070 |
+
</div>
|
1071 |
+
|
1072 |
+
<div className="flex items-center justify-between">
|
1073 |
+
<div className="text-sm">
|
1074 |
+
<span className="text-orange-600 font-medium">{module.flbReward} FLB</span>
|
1075 |
+
<span className="text-gray-500"> + </span>
|
1076 |
+
<span className="text-blue-600 font-medium">{module.xpReward} XP</span>
|
1077 |
+
</div>
|
1078 |
+
<Badge variant="outline" className="text-xs">
|
1079 |
+
{module.type}
|
1080 |
+
</Badge>
|
1081 |
+
</div>
|
1082 |
+
|
1083 |
+
<Button
|
1084 |
+
onClick={(e) => {
|
1085 |
+
e.stopPropagation()
|
1086 |
+
startModule(module)
|
1087 |
+
}}
|
1088 |
+
className="w-full bg-orange-600 hover:bg-orange-700"
|
1089 |
+
disabled={isCompleted}
|
1090 |
+
>
|
1091 |
+
{isCompleted ? "Completed" : "Start Module"}
|
1092 |
+
</Button>
|
1093 |
+
</div>
|
1094 |
+
</CardContent>
|
1095 |
+
</Card>
|
1096 |
+
</motion.div>
|
1097 |
+
)
|
1098 |
+
})}
|
1099 |
+
</motion.div>
|
1100 |
+
|
1101 |
+
{/* FLB Redemption Kits */}
|
1102 |
+
<motion.div
|
1103 |
+
initial={{ opacity: 0, y: 20 }}
|
1104 |
+
animate={{ opacity: 1, y: 0 }}
|
1105 |
+
transition={{ delay: 0.7 }}
|
1106 |
+
className="mb-8"
|
1107 |
+
>
|
1108 |
+
<h2 className="text-3xl font-bold text-center text-gray-900 mb-8">FLB Redemption Kits</h2>
|
1109 |
+
<p className="text-center text-gray-600 mb-8">Turn your Ubuntu tokens into real impact</p>
|
1110 |
+
|
1111 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
1112 |
+
{redemptionKits.map((kit) => (
|
1113 |
+
<Card key={kit.id} className={`${kit.color} border-2`}>
|
1114 |
+
<CardContent className="p-6">
|
1115 |
+
<h3 className="text-xl font-bold text-gray-900 mb-4">{kit.name}</h3>
|
1116 |
+
<div className="flex items-center mb-4">
|
1117 |
+
<Badge className="bg-white text-gray-800">
|
1118 |
+
{kit.cost} {kit.currency}
|
1119 |
+
</Badge>
|
1120 |
+
</div>
|
1121 |
+
<ul className="space-y-2 mb-6">
|
1122 |
+
{kit.items.map((item, index) => (
|
1123 |
+
<li key={index} className="flex items-center text-gray-700">
|
1124 |
+
<CheckCircle className="w-4 h-4 text-green-500 mr-2" />
|
1125 |
+
{item}
|
1126 |
+
</li>
|
1127 |
+
))}
|
1128 |
+
</ul>
|
1129 |
+
<Button
|
1130 |
+
onClick={() => redeemKit(kit.id)}
|
1131 |
+
disabled={!wallet.connected || wallet.flbBalance < kit.cost}
|
1132 |
+
className="w-full"
|
1133 |
+
>
|
1134 |
+
{wallet.connected
|
1135 |
+
? wallet.flbBalance >= kit.cost
|
1136 |
+
? "Redeem Kit"
|
1137 |
+
: "Insufficient FLB"
|
1138 |
+
: "Connect Wallet"}
|
1139 |
+
</Button>
|
1140 |
+
</CardContent>
|
1141 |
+
</Card>
|
1142 |
+
))}
|
1143 |
+
</div>
|
1144 |
+
</motion.div>
|
1145 |
+
|
1146 |
+
{/* Ubuntu Quest System */}
|
1147 |
+
<motion.div
|
1148 |
+
initial={{ opacity: 0, y: 20 }}
|
1149 |
+
animate={{ opacity: 1, y: 0 }}
|
1150 |
+
transition={{ delay: 0.8 }}
|
1151 |
+
className="bg-gray-50 rounded-lg p-8 mb-8"
|
1152 |
+
>
|
1153 |
+
<h2 className="text-3xl font-bold text-center text-gray-900 mb-8">Ubuntu Quest System</h2>
|
1154 |
+
<p className="text-center text-gray-600 mb-8">Grow your impact through collective learning and action</p>
|
1155 |
+
|
1156 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
1157 |
+
<Card>
|
1158 |
+
<CardContent className="p-6">
|
1159 |
+
<h3 className="text-xl font-bold text-gray-900 mb-4">Youth Skill Path</h3>
|
1160 |
+
<div className="space-y-4">
|
1161 |
+
<div className="flex items-center">
|
1162 |
+
<div className="flex-shrink-0 bg-green-100 text-green-800 rounded-full p-2">
|
1163 |
+
<GraduationCap className="w-4 h-4" />
|
1164 |
+
</div>
|
1165 |
+
<div className="ml-3">
|
1166 |
+
<p className="text-sm font-medium text-gray-900">Learner</p>
|
1167 |
+
<p className="text-sm text-gray-500">Earn basic FLB</p>
|
1168 |
+
</div>
|
1169 |
+
</div>
|
1170 |
+
<div className="flex items-center">
|
1171 |
+
<div className="flex-shrink-0 bg-blue-100 text-blue-800 rounded-full p-2">
|
1172 |
+
<Shield className="w-4 h-4" />
|
1173 |
+
</div>
|
1174 |
+
<div className="ml-3">
|
1175 |
+
<p className="text-sm font-medium text-gray-900">Advocate</p>
|
1176 |
+
<p className="text-sm text-gray-500">Gain DAO voting</p>
|
1177 |
+
</div>
|
1178 |
+
</div>
|
1179 |
+
<div className="flex items-center">
|
1180 |
+
<div className="flex-shrink-0 bg-purple-100 text-purple-800 rounded-full p-2">
|
1181 |
+
<Heart className="w-4 h-4" />
|
1182 |
+
</div>
|
1183 |
+
<div className="ml-3">
|
1184 |
+
<p className="text-sm font-medium text-gray-900">Mentor</p>
|
1185 |
+
<p className="text-sm text-gray-500">Local visibility</p>
|
1186 |
+
</div>
|
1187 |
+
</div>
|
1188 |
+
</div>
|
1189 |
+
</CardContent>
|
1190 |
+
</Card>
|
1191 |
+
|
1192 |
+
<Card>
|
1193 |
+
<CardContent className="p-6">
|
1194 |
+
<h3 className="text-xl font-bold text-gray-900 mb-4">Circle of Impact</h3>
|
1195 |
+
<div className="space-y-3">
|
1196 |
+
<div className="flex items-start">
|
1197 |
+
<div className="flex-shrink-0 bg-yellow-100 text-yellow-800 rounded-full p-2 mt-1">
|
1198 |
+
<Users className="w-4 h-4" />
|
1199 |
+
</div>
|
1200 |
+
<div className="ml-3">
|
1201 |
+
<p className="text-sm font-medium text-gray-900">Clan-based rewards</p>
|
1202 |
+
<p className="text-sm text-gray-500">Earn Ubuntu Points collectively</p>
|
1203 |
+
</div>
|
1204 |
+
</div>
|
1205 |
+
<div className="flex items-start">
|
1206 |
+
<div className="flex-shrink-0 bg-red-100 text-red-800 rounded-full p-2 mt-1">
|
1207 |
+
<Target className="w-4 h-4" />
|
1208 |
+
</div>
|
1209 |
+
<div className="ml-3">
|
1210 |
+
<p className="text-sm font-medium text-gray-900">Community Quests</p>
|
1211 |
+
<p className="text-sm text-gray-500">Health Jams, Elder Interviews</p>
|
1212 |
+
</div>
|
1213 |
+
</div>
|
1214 |
+
</div>
|
1215 |
+
</CardContent>
|
1216 |
+
</Card>
|
1217 |
+
|
1218 |
+
<Card>
|
1219 |
+
<CardContent className="p-6">
|
1220 |
+
<h3 className="text-xl font-bold text-gray-900 mb-4">Revive a Clinic</h3>
|
1221 |
+
<div className="space-y-4">
|
1222 |
+
<div className="flex items-start">
|
1223 |
+
<div className="flex-shrink-0 bg-green-100 text-green-800 rounded-full p-2 mt-1">
|
1224 |
+
<span className="text-xs font-bold">1</span>
|
1225 |
+
</div>
|
1226 |
+
<div className="ml-3">
|
1227 |
+
<p className="text-sm text-gray-700">Youth upload challenge</p>
|
1228 |
+
</div>
|
1229 |
+
</div>
|
1230 |
+
<div className="flex items-start">
|
1231 |
+
<div className="flex-shrink-0 bg-blue-100 text-blue-800 rounded-full p-2 mt-1">
|
1232 |
+
<span className="text-xs font-bold">2</span>
|
1233 |
+
</div>
|
1234 |
+
<div className="ml-3">
|
1235 |
+
<p className="text-sm text-gray-700">DAO vote initiated</p>
|
1236 |
+
</div>
|
1237 |
+
</div>
|
1238 |
+
<div className="flex items-start">
|
1239 |
+
<div className="flex-shrink-0 bg-purple-100 text-purple-800 rounded-full p-2 mt-1">
|
1240 |
+
<span className="text-xs font-bold">3</span>
|
1241 |
+
</div>
|
1242 |
+
<div className="ml-3">
|
1243 |
+
<p className="text-sm text-gray-700">FLB donation triggered</p>
|
1244 |
+
</div>
|
1245 |
+
</div>
|
1246 |
+
</div>
|
1247 |
+
</CardContent>
|
1248 |
+
</Card>
|
1249 |
+
</div>
|
1250 |
+
</motion.div>
|
1251 |
+
|
1252 |
+
{/* Impact Stats */}
|
1253 |
+
<motion.div
|
1254 |
+
initial={{ opacity: 0, y: 20 }}
|
1255 |
+
animate={{ opacity: 1, y: 0 }}
|
1256 |
+
transition={{ delay: 0.9 }}
|
1257 |
+
className="text-center mb-8"
|
1258 |
+
>
|
1259 |
+
<h2 className="text-3xl font-bold text-gray-900 mb-8">Making a Difference Across Africa</h2>
|
1260 |
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
1261 |
+
<Card>
|
1262 |
+
<CardContent className="p-6 text-center">
|
1263 |
+
<Users className="h-12 w-12 text-green-600 mx-auto mb-4" />
|
1264 |
+
<h3 className="text-3xl font-bold text-gray-900">1,200+</h3>
|
1265 |
+
<p className="text-gray-600">Ubuntu Learners</p>
|
1266 |
+
</CardContent>
|
1267 |
+
</Card>
|
1268 |
+
<Card>
|
1269 |
+
<CardContent className="p-6 text-center">
|
1270 |
+
<Coins className="h-12 w-12 text-green-600 mx-auto mb-4" />
|
1271 |
+
<h3 className="text-3xl font-bold text-gray-900">250K+</h3>
|
1272 |
+
<p className="text-gray-600">FLB Tokens Earned</p>
|
1273 |
+
</CardContent>
|
1274 |
+
</Card>
|
1275 |
+
<Card>
|
1276 |
+
<CardContent className="p-6 text-center">
|
1277 |
+
<Stethoscope className="h-12 w-12 text-green-600 mx-auto mb-4" />
|
1278 |
+
<h3 className="text-3xl font-bold text-gray-900">15</h3>
|
1279 |
+
<p className="text-gray-600">Healing Centers Supported</p>
|
1280 |
+
</CardContent>
|
1281 |
+
</Card>
|
1282 |
+
<Card>
|
1283 |
+
<CardContent className="p-6 text-center">
|
1284 |
+
<Globe className="h-12 w-12 text-green-600 mx-auto mb-4" />
|
1285 |
+
<h3 className="text-3xl font-bold text-gray-900">8</h3>
|
1286 |
+
<p className="text-gray-600">African Countries</p>
|
1287 |
+
</CardContent>
|
1288 |
+
</Card>
|
1289 |
+
</div>
|
1290 |
+
</motion.div>
|
1291 |
+
|
1292 |
+
{/* Testnet Notice */}
|
1293 |
+
<motion.div
|
1294 |
+
initial={{ opacity: 0, y: 20 }}
|
1295 |
+
animate={{ opacity: 1, y: 0 }}
|
1296 |
+
transition={{ delay: 1.0 }}
|
1297 |
+
className="bg-gray-800 text-white rounded-lg p-6 text-center"
|
1298 |
+
>
|
1299 |
+
<div className="flex items-center justify-center mb-4">
|
1300 |
+
<Zap className="h-8 w-8 text-yellow-400 mr-3" />
|
1301 |
+
<span className="text-xl font-medium">This is a TestNet environment</span>
|
1302 |
+
</div>
|
1303 |
+
<p className="text-gray-300">
|
1304 |
+
FLB tokens have no real monetary value. This platform demonstrates Ubuntu-based learning and healing
|
1305 |
+
principles.
|
1306 |
+
</p>
|
1307 |
+
</motion.div>
|
1308 |
+
</div>
|
1309 |
+
|
1310 |
+
{/* Wallet Connection Modal */}
|
1311 |
+
{showWalletModal && (
|
1312 |
+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
1313 |
+
<Card className="w-full max-w-md">
|
1314 |
+
<CardHeader>
|
1315 |
+
<CardTitle className="flex items-center">
|
1316 |
+
<Wallet className="w-5 h-5 mr-2" />
|
1317 |
+
Connect Wallet
|
1318 |
+
</CardTitle>
|
1319 |
+
<CardDescription>Choose a wallet to connect to the Ubuntu Learn & Earn platform</CardDescription>
|
1320 |
+
</CardHeader>
|
1321 |
+
<CardContent className="space-y-4">
|
1322 |
+
<Button
|
1323 |
+
onClick={() => connectWallet("valora")}
|
1324 |
+
className="w-full flex items-center justify-center"
|
1325 |
+
variant="outline"
|
1326 |
+
>
|
1327 |
+
<div className="w-5 h-5 mr-2 bg-green-500 rounded-full flex items-center justify-center text-white text-xs">
|
1328 |
+
V
|
1329 |
+
</div>
|
1330 |
+
Valora
|
1331 |
+
</Button>
|
1332 |
+
<Button
|
1333 |
+
onClick={() => connectWallet("metamask")}
|
1334 |
+
className="w-full flex items-center justify-center"
|
1335 |
+
variant="outline"
|
1336 |
+
>
|
1337 |
+
<div className="w-5 h-5 mr-2 bg-orange-500 rounded-full flex items-center justify-center text-white text-xs">
|
1338 |
+
M
|
1339 |
+
</div>
|
1340 |
+
MetaMask
|
1341 |
+
</Button>
|
1342 |
+
<Button
|
1343 |
+
onClick={() => connectWallet("walletconnect")}
|
1344 |
+
className="w-full flex items-center justify-center"
|
1345 |
+
variant="outline"
|
1346 |
+
>
|
1347 |
+
<div className="w-5 h-5 mr-2 bg-blue-500 rounded-full flex items-center justify-center text-white text-xs">
|
1348 |
+
WC
|
1349 |
+
</div>
|
1350 |
+
WalletConnect
|
1351 |
+
</Button>
|
1352 |
+
</CardContent>
|
1353 |
+
<div className="p-6 pt-0">
|
1354 |
+
<Button onClick={() => setShowWalletModal(false)} variant="outline" className="w-full">
|
1355 |
+
Cancel
|
1356 |
+
</Button>
|
1357 |
+
</div>
|
1358 |
+
</Card>
|
1359 |
+
</div>
|
1360 |
+
)}
|
1361 |
+
|
1362 |
+
{/* Course Details Modal */}
|
1363 |
+
{showCourseModal && selectedCourse && (
|
1364 |
+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
1365 |
+
<Card className="w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
1366 |
+
<CardHeader>
|
1367 |
+
<div className="flex justify-between items-start">
|
1368 |
+
<div>
|
1369 |
+
<CardTitle className="text-2xl">{selectedCourse.title}</CardTitle>
|
1370 |
+
<CardDescription className="mt-2">{selectedCourse.description}</CardDescription>
|
1371 |
+
</div>
|
1372 |
+
<Button onClick={() => setShowCourseModal(false)} variant="ghost" size="sm">
|
1373 |
+
✕
|
1374 |
+
</Button>
|
1375 |
+
</div>
|
1376 |
+
<div className="flex items-center gap-4 text-sm text-gray-500">
|
1377 |
+
<span className="flex items-center">
|
1378 |
+
<BookOpen className="w-4 h-4 mr-1" />
|
1379 |
+
{selectedCourse.type} Module
|
1380 |
+
</span>
|
1381 |
+
<span className="flex items-center">
|
1382 |
+
<Clock className="w-4 h-4 mr-1" />
|
1383 |
+
{selectedCourse.duration} Minutes
|
1384 |
+
</span>
|
1385 |
+
<span className="flex items-center">
|
1386 |
+
<Coins className="w-4 h-4 mr-1 text-yellow-500" />
|
1387 |
+
{selectedCourse.flbReward} FLB
|
1388 |
+
</span>
|
1389 |
+
</div>
|
1390 |
+
</CardHeader>
|
1391 |
+
<CardContent>
|
1392 |
+
<div className="space-y-6">
|
1393 |
+
<div>
|
1394 |
+
<h4 className="text-lg font-medium text-gray-900 mb-3">Ubuntu Learning Objectives</h4>
|
1395 |
+
<ul className="space-y-2">
|
1396 |
+
<li className="flex items-start">
|
1397 |
+
<CheckCircle className="w-5 h-5 text-green-500 mr-2 mt-0.5" />
|
1398 |
+
<span>Understand African healing wisdom and Ubuntu principles</span>
|
1399 |
+
</li>
|
1400 |
+
<li className="flex items-start">
|
1401 |
+
<CheckCircle className="w-5 h-5 text-green-500 mr-2 mt-0.5" />
|
1402 |
+
<span>Apply cultural knowledge to modern health challenges</span>
|
1403 |
+
</li>
|
1404 |
+
<li className="flex items-start">
|
1405 |
+
<CheckCircle className="w-5 h-5 text-green-500 mr-2 mt-0.5" />
|
1406 |
+
<span>Contribute to community healing and support networks</span>
|
1407 |
+
</li>
|
1408 |
+
</ul>
|
1409 |
+
</div>
|
1410 |
+
|
1411 |
+
<div>
|
1412 |
+
<h4 className="text-lg font-medium text-gray-900 mb-3">Requirements</h4>
|
1413 |
+
<ul className="space-y-1 text-gray-600">
|
1414 |
+
<li>• Basic understanding of African cultural values</li>
|
1415 |
+
<li>• Internet connection (offline mode available)</li>
|
1416 |
+
<li>• Celo-compatible wallet (for FLB token rewards)</li>
|
1417 |
+
<li>• Commitment to Ubuntu principles of community healing</li>
|
1418 |
+
</ul>
|
1419 |
+
</div>
|
1420 |
+
</div>
|
1421 |
+
</CardContent>
|
1422 |
+
<div className="p-6 pt-0 flex gap-4">
|
1423 |
+
<Button onClick={() => startModule(selectedCourse)} className="flex-1 bg-green-600 hover:bg-green-700">
|
1424 |
+
Start Ubuntu Learning
|
1425 |
+
</Button>
|
1426 |
+
<Button onClick={() => setShowCourseModal(false)} variant="outline" className="flex-1">
|
1427 |
+
Cancel
|
1428 |
+
</Button>
|
1429 |
+
</div>
|
1430 |
+
</Card>
|
1431 |
+
</div>
|
1432 |
+
)}
|
1433 |
+
</div>
|
1434 |
+
)
|
1435 |
+
}
|
app/login/page.tsx
ADDED
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useState, useEffect } from "react"
|
4 |
+
import { useRouter } from "next/navigation"
|
5 |
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"
|
6 |
+
import { Button } from "@/components/ui/button"
|
7 |
+
import { UserDatabase, type User } from "@/lib/user-database"
|
8 |
+
import { Shield, Heart } from "lucide-react"
|
9 |
+
import Link from "next/link"
|
10 |
+
|
11 |
+
export default function LoginPage() {
|
12 |
+
const router = useRouter()
|
13 |
+
const [users, setUsers] = useState<User[]>([])
|
14 |
+
const [loading, setLoading] = useState(true)
|
15 |
+
|
16 |
+
useEffect(() => {
|
17 |
+
// Get all users
|
18 |
+
const allUsers = UserDatabase.getAllUsers()
|
19 |
+
setUsers(allUsers)
|
20 |
+
setLoading(false)
|
21 |
+
|
22 |
+
// Check if user is already logged in
|
23 |
+
const currentUser = UserDatabase.getCurrentUser()
|
24 |
+
if (currentUser) {
|
25 |
+
router.push("/profile")
|
26 |
+
}
|
27 |
+
}, [router])
|
28 |
+
|
29 |
+
const handleLogin = (userId: string) => {
|
30 |
+
// Set current user
|
31 |
+
localStorage.setItem("flameborn_current_user", userId)
|
32 |
+
|
33 |
+
// Redirect to profile
|
34 |
+
router.push("/profile")
|
35 |
+
}
|
36 |
+
|
37 |
+
return (
|
38 |
+
<div className="min-h-screen bg-dusk flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
39 |
+
<div className="max-w-md w-full space-y-8">
|
40 |
+
<div className="text-center">
|
41 |
+
<h1 className="text-4xl font-heading text-flame mb-2">FLAMEBORN</h1>
|
42 |
+
<p className="text-gray-400">Log in to your account</p>
|
43 |
+
</div>
|
44 |
+
|
45 |
+
<Card className="bg-dusk border border-gray-700">
|
46 |
+
<CardHeader>
|
47 |
+
<CardTitle>Select Your Account</CardTitle>
|
48 |
+
<CardDescription>Choose an account to log in or register a new one</CardDescription>
|
49 |
+
</CardHeader>
|
50 |
+
<CardContent>
|
51 |
+
{loading ? (
|
52 |
+
<p className="text-center text-gray-400">Loading accounts...</p>
|
53 |
+
) : users.length === 0 ? (
|
54 |
+
<div className="text-center py-6">
|
55 |
+
<p className="text-gray-400 mb-4">No accounts found</p>
|
56 |
+
<p className="text-sm text-gray-500 mb-6">Register as a Guardian or Healer to get started</p>
|
57 |
+
|
58 |
+
<div className="grid grid-cols-2 gap-4">
|
59 |
+
<Link href="/register/guardian">
|
60 |
+
<Button className="w-full bg-guardian text-black hover:bg-guardian/80">
|
61 |
+
<Shield className="h-4 w-4 mr-2" /> Guardian
|
62 |
+
</Button>
|
63 |
+
</Link>
|
64 |
+
<Link href="/register/healer">
|
65 |
+
<Button className="w-full bg-flame text-white hover:bg-flame/80">
|
66 |
+
<Heart className="h-4 w-4 mr-2" /> Healer
|
67 |
+
</Button>
|
68 |
+
</Link>
|
69 |
+
</div>
|
70 |
+
</div>
|
71 |
+
) : (
|
72 |
+
<div className="space-y-4">
|
73 |
+
{users.map((user) => (
|
74 |
+
<div
|
75 |
+
key={user.id}
|
76 |
+
className={`p-4 rounded-lg border ${
|
77 |
+
user.type === "guardian"
|
78 |
+
? "border-guardian/30 hover:border-guardian"
|
79 |
+
: "border-flame/30 hover:border-flame"
|
80 |
+
} cursor-pointer transition-all`}
|
81 |
+
onClick={() => handleLogin(user.id)}
|
82 |
+
>
|
83 |
+
<div className="flex items-center gap-3">
|
84 |
+
<div
|
85 |
+
className={`p-2 rounded-full ${user.type === "guardian" ? "bg-guardian/20" : "bg-flame/20"}`}
|
86 |
+
>
|
87 |
+
{user.type === "guardian" ? (
|
88 |
+
<Shield className="h-5 w-5 text-guardian" />
|
89 |
+
) : (
|
90 |
+
<Heart className="h-5 w-5 text-flame" />
|
91 |
+
)}
|
92 |
+
</div>
|
93 |
+
<div>
|
94 |
+
<h3 className="font-medium text-white">{user.fullName}</h3>
|
95 |
+
<p className="text-sm text-gray-400">
|
96 |
+
{user.type === "guardian" ? "Guardian" : "Healer"} • {user.email}
|
97 |
+
</p>
|
98 |
+
</div>
|
99 |
+
</div>
|
100 |
+
</div>
|
101 |
+
))}
|
102 |
+
</div>
|
103 |
+
)}
|
104 |
+
</CardContent>
|
105 |
+
<CardFooter className="flex justify-center border-t border-gray-800 pt-4">
|
106 |
+
<p className="text-sm text-gray-500">
|
107 |
+
Don't have an account?{" "}
|
108 |
+
<Link href="/register/guardian" className="text-flame hover:underline">
|
109 |
+
Register
|
110 |
+
</Link>
|
111 |
+
</p>
|
112 |
+
</CardFooter>
|
113 |
+
</Card>
|
114 |
+
</div>
|
115 |
+
</div>
|
116 |
+
)
|
117 |
+
}
|
app/manifesto/page.tsx
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Manifesto } from "@/components/manifesto"
|
2 |
+
|
3 |
+
export default function ManifestoPage() {
|
4 |
+
return <Manifesto />
|
5 |
+
}
|
app/maternal-health/page.tsx
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { MaternalHealthPortal } from "@/components/maternal-health/maternal-health-portal"
|
2 |
+
|
3 |
+
export default function MaternalHealthPage() {
|
4 |
+
return <MaternalHealthPortal />
|
5 |
+
}
|
app/network-stats/page.tsx
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NetworkStatsLive } from "@/components/network-stats/network-stats-live"
|
2 |
+
|
3 |
+
export default function NetworkStatsPage() {
|
4 |
+
return <NetworkStatsLive />
|
5 |
+
}
|
app/page.tsx
ADDED
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useState, useEffect } from "react"
|
4 |
+
import Link from "next/link"
|
5 |
+
import { Flame, Heart, Users, Globe, ArrowRight, Sparkles, Shield, Zap, TrendingUp } from "lucide-react"
|
6 |
+
import { Button } from "@/components/ui/button"
|
7 |
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
8 |
+
import { Badge } from "@/components/ui/badge"
|
9 |
+
import ParticleBackground from "@/components/particle-background"
|
10 |
+
import FlameAnimation from "@/components/flame-animation"
|
11 |
+
import SoundEffects from "@/components/sound-effects"
|
12 |
+
import MostarAIAssistant from "@/components/ai-assistant"
|
13 |
+
|
14 |
+
const HomePage = () => {
|
15 |
+
const [mounted, setMounted] = useState(false)
|
16 |
+
const [activeSection, setActiveSection] = useState("hero")
|
17 |
+
|
18 |
+
useEffect(() => {
|
19 |
+
setMounted(true)
|
20 |
+
}, [])
|
21 |
+
|
22 |
+
if (!mounted) {
|
23 |
+
return (
|
24 |
+
<div className="min-h-screen bg-gradient-to-br from-orange-50 to-red-50 flex items-center justify-center">
|
25 |
+
<div className="text-center">
|
26 |
+
<Flame className="w-16 h-16 text-orange-500 animate-pulse mx-auto mb-4" />
|
27 |
+
<p className="text-gray-600">Loading FlameBorn Ubuntu...</p>
|
28 |
+
</div>
|
29 |
+
</div>
|
30 |
+
)
|
31 |
+
}
|
32 |
+
|
33 |
+
return (
|
34 |
+
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-red-50 to-pink-50 relative overflow-hidden">
|
35 |
+
<ParticleBackground />
|
36 |
+
<SoundEffects />
|
37 |
+
|
38 |
+
{/* Hero Section */}
|
39 |
+
<section className="relative min-h-screen flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
40 |
+
<div className="max-w-7xl mx-auto text-center">
|
41 |
+
<div className="mb-8">
|
42 |
+
<FlameAnimation />
|
43 |
+
</div>
|
44 |
+
|
45 |
+
<Badge variant="outline" className="mb-6 bg-white/80 backdrop-blur-sm border-orange-200">
|
46 |
+
<Sparkles className="w-4 h-4 mr-2" />
|
47 |
+
Ubuntu-Powered Healthcare Tokenization
|
48 |
+
</Badge>
|
49 |
+
|
50 |
+
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold text-gray-900 mb-6 leading-tight">
|
51 |
+
<span className="bg-gradient-to-r from-orange-600 to-red-600 bg-clip-text text-transparent">FlameBorn</span>
|
52 |
+
<br />
|
53 |
+
<span className="text-3xl md:text-4xl lg:text-5xl text-gray-700">I am because we are</span>
|
54 |
+
</h1>
|
55 |
+
|
56 |
+
<p className="text-xl md:text-2xl text-gray-600 mb-8 max-w-4xl mx-auto leading-relaxed">
|
57 |
+
Bridging traditional African Ubuntu philosophy with modern healthcare tokenization. Every birth registered,
|
58 |
+
every healer verified, every community connected through the power of blockchain.
|
59 |
+
</p>
|
60 |
+
|
61 |
+
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-12">
|
62 |
+
<Button asChild size="lg" className="bg-orange-500 hover:bg-orange-600 text-white px-8 py-3">
|
63 |
+
<Link href="/launch">
|
64 |
+
<Flame className="w-5 h-5 mr-2" />
|
65 |
+
Launch App
|
66 |
+
</Link>
|
67 |
+
</Button>
|
68 |
+
<Button asChild variant="outline" size="lg" className="px-8 py-3 bg-white/80 backdrop-blur-sm">
|
69 |
+
<Link href="/manifesto">
|
70 |
+
<Heart className="w-5 h-5 mr-2" />
|
71 |
+
Read Manifesto
|
72 |
+
</Link>
|
73 |
+
</Button>
|
74 |
+
<Button asChild variant="outline" size="lg" className="px-8 py-3 bg-white/80 backdrop-blur-sm">
|
75 |
+
<Link href="/analytics">
|
76 |
+
<TrendingUp className="w-5 h-5 mr-2" />
|
77 |
+
View Analytics
|
78 |
+
</Link>
|
79 |
+
</Button>
|
80 |
+
</div>
|
81 |
+
|
82 |
+
{/* Key Stats */}
|
83 |
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-4xl mx-auto">
|
84 |
+
<div className="bg-white/80 backdrop-blur-sm rounded-lg p-4 border border-orange-100">
|
85 |
+
<div className="text-2xl font-bold text-orange-600">1,247</div>
|
86 |
+
<div className="text-sm text-gray-600">Healthcare Workers</div>
|
87 |
+
</div>
|
88 |
+
<div className="bg-white/80 backdrop-blur-sm rounded-lg p-4 border border-orange-100">
|
89 |
+
<div className="text-2xl font-bold text-red-600">523</div>
|
90 |
+
<div className="text-sm text-gray-600">Births Verified</div>
|
91 |
+
</div>
|
92 |
+
<div className="bg-white/80 backdrop-blur-sm rounded-lg p-4 border border-orange-100">
|
93 |
+
<div className="text-2xl font-bold text-blue-600">$125K</div>
|
94 |
+
<div className="text-sm text-gray-600">Donations Distributed</div>
|
95 |
+
</div>
|
96 |
+
<div className="bg-white/80 backdrop-blur-sm rounded-lg p-4 border border-orange-100">
|
97 |
+
<div className="text-2xl font-bold text-green-600">94%</div>
|
98 |
+
<div className="text-sm text-gray-600">Impact Score</div>
|
99 |
+
</div>
|
100 |
+
</div>
|
101 |
+
</div>
|
102 |
+
</section>
|
103 |
+
|
104 |
+
{/* Ubuntu Philosophy Section */}
|
105 |
+
<section className="py-20 px-4 sm:px-6 lg:px-8 bg-white/50 backdrop-blur-sm">
|
106 |
+
<div className="max-w-7xl mx-auto">
|
107 |
+
<div className="text-center mb-16">
|
108 |
+
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">Ubuntu: The Heart of FlameBorn</h2>
|
109 |
+
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
110 |
+
"Umuntu ngumuntu ngabantu" - A person is a person through other persons. This ancient African wisdom
|
111 |
+
guides our approach to healthcare tokenization.
|
112 |
+
</p>
|
113 |
+
</div>
|
114 |
+
|
115 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
116 |
+
<Card className="border-orange-200 bg-white/80 backdrop-blur-sm">
|
117 |
+
<CardHeader>
|
118 |
+
<Heart className="w-12 h-12 text-red-500 mb-4" />
|
119 |
+
<CardTitle>Interconnectedness</CardTitle>
|
120 |
+
<CardDescription>
|
121 |
+
Every token represents our shared humanity and collective responsibility for health
|
122 |
+
</CardDescription>
|
123 |
+
</CardHeader>
|
124 |
+
<CardContent>
|
125 |
+
<p className="text-gray-600">
|
126 |
+
In Ubuntu philosophy, individual wellness is inseparable from community health. Our tokens create
|
127 |
+
digital bonds that strengthen real-world healing networks.
|
128 |
+
</p>
|
129 |
+
</CardContent>
|
130 |
+
</Card>
|
131 |
+
|
132 |
+
<Card className="border-orange-200 bg-white/80 backdrop-blur-sm">
|
133 |
+
<CardHeader>
|
134 |
+
<Users className="w-12 h-12 text-blue-500 mb-4" />
|
135 |
+
<CardTitle>Collective Prosperity</CardTitle>
|
136 |
+
<CardDescription>
|
137 |
+
When one thrives, we all thrive - tokenomics designed for shared abundance
|
138 |
+
</CardDescription>
|
139 |
+
</CardHeader>
|
140 |
+
<CardContent>
|
141 |
+
<p className="text-gray-600">
|
142 |
+
Our distribution model ensures that healthcare workers, communities, and supporters all benefit from
|
143 |
+
the network's growth and success.
|
144 |
+
</p>
|
145 |
+
</CardContent>
|
146 |
+
</Card>
|
147 |
+
|
148 |
+
<Card className="border-orange-200 bg-white/80 backdrop-blur-sm">
|
149 |
+
<CardHeader>
|
150 |
+
<Globe className="w-12 h-12 text-green-500 mb-4" />
|
151 |
+
<CardTitle>Cultural Bridge</CardTitle>
|
152 |
+
<CardDescription>Honoring traditional wisdom while embracing technological innovation</CardDescription>
|
153 |
+
</CardHeader>
|
154 |
+
<CardContent>
|
155 |
+
<p className="text-gray-600">
|
156 |
+
We bridge ancient African healing traditions with modern blockchain technology, creating a culturally
|
157 |
+
respectful path to global healthcare equity.
|
158 |
+
</p>
|
159 |
+
</CardContent>
|
160 |
+
</Card>
|
161 |
+
</div>
|
162 |
+
</div>
|
163 |
+
</section>
|
164 |
+
|
165 |
+
{/* How It Works Section */}
|
166 |
+
<section className="py-20 px-4 sm:px-6 lg:px-8">
|
167 |
+
<div className="max-w-7xl mx-auto">
|
168 |
+
<div className="text-center mb-16">
|
169 |
+
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">How FlameBorn Works</h2>
|
170 |
+
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
171 |
+
A simple, powerful system that rewards healthcare impact and builds community wealth
|
172 |
+
</p>
|
173 |
+
</div>
|
174 |
+
|
175 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
176 |
+
<div className="text-center">
|
177 |
+
<div className="bg-orange-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
|
178 |
+
<Shield className="w-8 h-8 text-orange-600" />
|
179 |
+
</div>
|
180 |
+
<h3 className="text-xl font-semibold mb-2">Register</h3>
|
181 |
+
<p className="text-gray-600">Healthcare workers verify their credentials and join the Ubuntu network</p>
|
182 |
+
</div>
|
183 |
+
|
184 |
+
<div className="text-center">
|
185 |
+
<div className="bg-blue-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
|
186 |
+
<Zap className="w-8 h-8 text-blue-600" />
|
187 |
+
</div>
|
188 |
+
<h3 className="text-xl font-semibold mb-2">Serve</h3>
|
189 |
+
<p className="text-gray-600">Provide healthcare services and register births in their communities</p>
|
190 |
+
</div>
|
191 |
+
|
192 |
+
<div className="text-center">
|
193 |
+
<div className="bg-green-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
|
194 |
+
<Flame className="w-8 h-8 text-green-600" />
|
195 |
+
</div>
|
196 |
+
<h3 className="text-xl font-semibold mb-2">Earn</h3>
|
197 |
+
<p className="text-gray-600">Receive FLAME tokens for verified healthcare impact and community service</p>
|
198 |
+
</div>
|
199 |
+
|
200 |
+
<div className="text-center">
|
201 |
+
<div className="bg-purple-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
|
202 |
+
<Heart className="w-8 h-8 text-purple-600" />
|
203 |
+
</div>
|
204 |
+
<h3 className="text-xl font-semibold mb-2">Grow</h3>
|
205 |
+
<p className="text-gray-600">
|
206 |
+
Build wealth together as the network creates lasting healthcare infrastructure
|
207 |
+
</p>
|
208 |
+
</div>
|
209 |
+
</div>
|
210 |
+
</div>
|
211 |
+
</section>
|
212 |
+
|
213 |
+
{/* AI Assistant Section */}
|
214 |
+
<section className="py-20 px-4 sm:px-6 lg:px-8 bg-white/50 backdrop-blur-sm">
|
215 |
+
<div className="max-w-7xl mx-auto">
|
216 |
+
<div className="text-center mb-12">
|
217 |
+
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">Meet Mostar, Your Ubuntu AI Guide</h2>
|
218 |
+
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
219 |
+
Ask questions about FlameBorn, Ubuntu philosophy, African wisdom, or healthcare tokenization. Mostar
|
220 |
+
embodies the spirit of "I am because we are."
|
221 |
+
</p>
|
222 |
+
</div>
|
223 |
+
|
224 |
+
<div className="flex justify-center">
|
225 |
+
<MostarAIAssistant />
|
226 |
+
</div>
|
227 |
+
</div>
|
228 |
+
</section>
|
229 |
+
|
230 |
+
{/* Call to Action Section */}
|
231 |
+
<section className="py-20 px-4 sm:px-6 lg:px-8">
|
232 |
+
<div className="max-w-4xl mx-auto text-center">
|
233 |
+
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-6">Join the Ubuntu Healthcare Revolution</h2>
|
234 |
+
<p className="text-xl text-gray-600 mb-8">
|
235 |
+
Together, we can create a world where every birth is celebrated, every healer is supported, and every
|
236 |
+
community thrives.
|
237 |
+
</p>
|
238 |
+
|
239 |
+
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
240 |
+
<Button
|
241 |
+
asChild
|
242 |
+
size="lg"
|
243 |
+
className="bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 text-white px-8 py-3"
|
244 |
+
>
|
245 |
+
<Link href="/register/healer">
|
246 |
+
<Shield className="w-5 h-5 mr-2" />
|
247 |
+
Become a Healer
|
248 |
+
</Link>
|
249 |
+
</Button>
|
250 |
+
<Button asChild variant="outline" size="lg" className="px-8 py-3 bg-white/80 backdrop-blur-sm">
|
251 |
+
<Link href="/register/guardian">
|
252 |
+
<Users className="w-5 h-5 mr-2" />
|
253 |
+
Join as Guardian
|
254 |
+
</Link>
|
255 |
+
</Button>
|
256 |
+
<Button asChild variant="outline" size="lg" className="px-8 py-3 bg-white/80 backdrop-blur-sm">
|
257 |
+
<Link href="/community-pulse">
|
258 |
+
<Globe className="w-5 h-5 mr-2" />
|
259 |
+
Explore Community
|
260 |
+
<ArrowRight className="w-4 h-4 ml-2" />
|
261 |
+
</Link>
|
262 |
+
</Button>
|
263 |
+
</div>
|
264 |
+
</div>
|
265 |
+
</section>
|
266 |
+
|
267 |
+
{/* Footer */}
|
268 |
+
<footer className="bg-gray-900 text-white py-12 px-4 sm:px-6 lg:px-8">
|
269 |
+
<div className="max-w-7xl mx-auto">
|
270 |
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
271 |
+
<div>
|
272 |
+
<div className="flex items-center space-x-2 mb-4">
|
273 |
+
<Flame className="w-8 h-8 text-orange-500" />
|
274 |
+
<span className="text-xl font-bold">FlameBorn</span>
|
275 |
+
</div>
|
276 |
+
<p className="text-gray-400">Ubuntu-powered healthcare tokenization for a connected world.</p>
|
277 |
+
</div>
|
278 |
+
|
279 |
+
<div>
|
280 |
+
<h3 className="text-lg font-semibold mb-4">Platform</h3>
|
281 |
+
<ul className="space-y-2 text-gray-400">
|
282 |
+
<li>
|
283 |
+
<Link href="/launch" className="hover:text-white">
|
284 |
+
Launch App
|
285 |
+
</Link>
|
286 |
+
</li>
|
287 |
+
<li>
|
288 |
+
<Link href="/analytics" className="hover:text-white">
|
289 |
+
Analytics
|
290 |
+
</Link>
|
291 |
+
</li>
|
292 |
+
<li>
|
293 |
+
<Link href="/token-system" className="hover:text-white">
|
294 |
+
Token System
|
295 |
+
</Link>
|
296 |
+
</li>
|
297 |
+
<li>
|
298 |
+
<Link href="/smart-contracts" className="hover:text-white">
|
299 |
+
Smart Contracts
|
300 |
+
</Link>
|
301 |
+
</li>
|
302 |
+
</ul>
|
303 |
+
</div>
|
304 |
+
|
305 |
+
<div>
|
306 |
+
<h3 className="text-lg font-semibold mb-4">Community</h3>
|
307 |
+
<ul className="space-y-2 text-gray-400">
|
308 |
+
<li>
|
309 |
+
<Link href="/guardians-sanctuary" className="hover:text-white">
|
310 |
+
Guardians Sanctuary
|
311 |
+
</Link>
|
312 |
+
</li>
|
313 |
+
<li>
|
314 |
+
<Link href="/flameborn-journey" className="hover:text-white">
|
315 |
+
FlameBorn Journey
|
316 |
+
</Link>
|
317 |
+
</li>
|
318 |
+
<li>
|
319 |
+
<Link href="/community-pulse" className="hover:text-white">
|
320 |
+
Community Pulse
|
321 |
+
</Link>
|
322 |
+
</li>
|
323 |
+
<li>
|
324 |
+
<Link href="/healers" className="hover:text-white">
|
325 |
+
Find Healers
|
326 |
+
</Link>
|
327 |
+
</li>
|
328 |
+
</ul>
|
329 |
+
</div>
|
330 |
+
|
331 |
+
<div>
|
332 |
+
<h3 className="text-lg font-semibold mb-4">Learn</h3>
|
333 |
+
<ul className="space-y-2 text-gray-400">
|
334 |
+
<li>
|
335 |
+
<Link href="/manifesto" className="hover:text-white">
|
336 |
+
Manifesto
|
337 |
+
</Link>
|
338 |
+
</li>
|
339 |
+
<li>
|
340 |
+
<Link href="/learn-earn" className="hover:text-white">
|
341 |
+
Learn & Earn
|
342 |
+
</Link>
|
343 |
+
</li>
|
344 |
+
<li>
|
345 |
+
<Link href="/legal" className="hover:text-white">
|
346 |
+
Legal
|
347 |
+
</Link>
|
348 |
+
</li>
|
349 |
+
<li>
|
350 |
+
<Link href="/test-proverbs" className="hover:text-white">
|
351 |
+
Ubuntu Wisdom
|
352 |
+
</Link>
|
353 |
+
</li>
|
354 |
+
</ul>
|
355 |
+
</div>
|
356 |
+
</div>
|
357 |
+
|
358 |
+
<div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
|
359 |
+
<p>© 2024 FlameBorn. Built with Ubuntu philosophy. "I am because we are."</p>
|
360 |
+
</div>
|
361 |
+
</div>
|
362 |
+
</footer>
|
363 |
+
</div>
|
364 |
+
)
|
365 |
+
}
|
366 |
+
|
367 |
+
export default HomePage
|
app/profile/edit/page.tsx
ADDED
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import type React from "react"
|
4 |
+
|
5 |
+
import { useEffect, useState } from "react"
|
6 |
+
import { useRouter } from "next/navigation"
|
7 |
+
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
|
8 |
+
import { Button } from "@/components/ui/button"
|
9 |
+
import { Input } from "@/components/ui/input"
|
10 |
+
import { Textarea } from "@/components/ui/textarea"
|
11 |
+
import { Label } from "@/components/ui/label"
|
12 |
+
import { UserDatabase, type User, type Guardian, type Healer } from "@/lib/user-database"
|
13 |
+
import { LoadingState } from "@/components/loading-state"
|
14 |
+
import { ArrowLeft, Save } from "lucide-react"
|
15 |
+
|
16 |
+
export default function ProfileEditPage() {
|
17 |
+
const router = useRouter()
|
18 |
+
const [user, setUser] = useState<User | null>(null)
|
19 |
+
const [loading, setLoading] = useState(true)
|
20 |
+
const [isSaving, setIsSaving] = useState(false)
|
21 |
+
const [formData, setFormData] = useState<Partial<User>>({})
|
22 |
+
|
23 |
+
useEffect(() => {
|
24 |
+
// Get current user
|
25 |
+
const currentUser = UserDatabase.getCurrentUser()
|
26 |
+
|
27 |
+
if (!currentUser) {
|
28 |
+
// Redirect to login if no user is found
|
29 |
+
router.push("/login")
|
30 |
+
return
|
31 |
+
}
|
32 |
+
|
33 |
+
setUser(currentUser)
|
34 |
+
setFormData(currentUser)
|
35 |
+
setLoading(false)
|
36 |
+
}, [router])
|
37 |
+
|
38 |
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
39 |
+
const { name, value } = e.target
|
40 |
+
setFormData((prev) => ({ ...prev, [name]: value }))
|
41 |
+
}
|
42 |
+
|
43 |
+
const handleSave = async () => {
|
44 |
+
if (!user) return
|
45 |
+
|
46 |
+
setIsSaving(true)
|
47 |
+
|
48 |
+
try {
|
49 |
+
// Update user
|
50 |
+
const updatedUser = UserDatabase.updateUser(user.id, formData)
|
51 |
+
|
52 |
+
if (updatedUser) {
|
53 |
+
setUser(updatedUser)
|
54 |
+
|
55 |
+
// Redirect back to profile
|
56 |
+
router.push("/profile")
|
57 |
+
}
|
58 |
+
} catch (error) {
|
59 |
+
console.error("Error updating profile:", error)
|
60 |
+
} finally {
|
61 |
+
setIsSaving(false)
|
62 |
+
}
|
63 |
+
}
|
64 |
+
|
65 |
+
if (loading) {
|
66 |
+
return <LoadingState message="Loading your profile..." />
|
67 |
+
}
|
68 |
+
|
69 |
+
if (!user) {
|
70 |
+
return (
|
71 |
+
<div className="min-h-screen bg-dusk flex items-center justify-center">
|
72 |
+
<Card className="w-full max-w-md">
|
73 |
+
<CardContent className="pt-6">
|
74 |
+
<div className="text-center">
|
75 |
+
<h2 className="text-xl font-medium mb-4">No User Found</h2>
|
76 |
+
<p className="text-gray-400 mb-6">Please log in or register to edit your profile.</p>
|
77 |
+
<Button className="bg-flame text-white hover:bg-flame/80" onClick={() => router.push("/login")}>
|
78 |
+
Log In
|
79 |
+
</Button>
|
80 |
+
</div>
|
81 |
+
</CardContent>
|
82 |
+
</Card>
|
83 |
+
</div>
|
84 |
+
)
|
85 |
+
}
|
86 |
+
|
87 |
+
return (
|
88 |
+
<div className="min-h-screen bg-dusk py-12 px-4 sm:px-6">
|
89 |
+
<div className="max-w-3xl mx-auto">
|
90 |
+
<div className="flex items-center mb-8">
|
91 |
+
<Button variant="ghost" className="mr-4" onClick={() => router.push("/profile")}>
|
92 |
+
<ArrowLeft className="h-4 w-4 mr-2" /> Back
|
93 |
+
</Button>
|
94 |
+
<h1 className="text-3xl font-heading text-white">Edit Profile</h1>
|
95 |
+
</div>
|
96 |
+
|
97 |
+
<Card className="bg-dusk border border-gray-700">
|
98 |
+
<CardHeader>
|
99 |
+
<CardTitle>Personal Information</CardTitle>
|
100 |
+
</CardHeader>
|
101 |
+
<CardContent className="space-y-4">
|
102 |
+
<div className="space-y-2">
|
103 |
+
<Label htmlFor="fullName">Full Name</Label>
|
104 |
+
<Input
|
105 |
+
id="fullName"
|
106 |
+
name="fullName"
|
107 |
+
value={formData.fullName || ""}
|
108 |
+
onChange={handleChange}
|
109 |
+
className="bg-gray-800/50 border-gray-700"
|
110 |
+
/>
|
111 |
+
</div>
|
112 |
+
|
113 |
+
<div className="space-y-2">
|
114 |
+
<Label htmlFor="email">Email</Label>
|
115 |
+
<Input
|
116 |
+
id="email"
|
117 |
+
name="email"
|
118 |
+
type="email"
|
119 |
+
value={formData.email || ""}
|
120 |
+
onChange={handleChange}
|
121 |
+
className="bg-gray-800/50 border-gray-700"
|
122 |
+
/>
|
123 |
+
</div>
|
124 |
+
|
125 |
+
{user.type === "guardian" && (
|
126 |
+
<>
|
127 |
+
<div className="space-y-2">
|
128 |
+
<Label htmlFor="country">Country</Label>
|
129 |
+
<Input
|
130 |
+
id="country"
|
131 |
+
name="country"
|
132 |
+
value={(formData as Guardian).country || ""}
|
133 |
+
onChange={handleChange}
|
134 |
+
className="bg-gray-800/50 border-gray-700"
|
135 |
+
/>
|
136 |
+
</div>
|
137 |
+
|
138 |
+
<div className="space-y-2">
|
139 |
+
<Label htmlFor="motivation">Why I became a Guardian</Label>
|
140 |
+
<Textarea
|
141 |
+
id="motivation"
|
142 |
+
name="motivation"
|
143 |
+
value={(formData as Guardian).motivation || ""}
|
144 |
+
onChange={handleChange}
|
145 |
+
className="bg-gray-800/50 border-gray-700 min-h-[100px]"
|
146 |
+
/>
|
147 |
+
</div>
|
148 |
+
</>
|
149 |
+
)}
|
150 |
+
|
151 |
+
{user.type === "healer" && (
|
152 |
+
<>
|
153 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
154 |
+
<div className="space-y-2">
|
155 |
+
<Label htmlFor="city">City/Town</Label>
|
156 |
+
<Input
|
157 |
+
id="city"
|
158 |
+
name="city"
|
159 |
+
value={(formData as Healer).city || ""}
|
160 |
+
onChange={handleChange}
|
161 |
+
className="bg-gray-800/50 border-gray-700"
|
162 |
+
/>
|
163 |
+
</div>
|
164 |
+
|
165 |
+
<div className="space-y-2">
|
166 |
+
<Label htmlFor="country">Country</Label>
|
167 |
+
<Input
|
168 |
+
id="country"
|
169 |
+
name="country"
|
170 |
+
value={(formData as Healer).country || ""}
|
171 |
+
onChange={handleChange}
|
172 |
+
className="bg-gray-800/50 border-gray-700"
|
173 |
+
/>
|
174 |
+
</div>
|
175 |
+
</div>
|
176 |
+
|
177 |
+
<div className="space-y-2">
|
178 |
+
<Label htmlFor="facilityName">Facility Name</Label>
|
179 |
+
<Input
|
180 |
+
id="facilityName"
|
181 |
+
name="facilityName"
|
182 |
+
value={(formData as Healer).facilityName || ""}
|
183 |
+
onChange={handleChange}
|
184 |
+
className="bg-gray-800/50 border-gray-700"
|
185 |
+
/>
|
186 |
+
</div>
|
187 |
+
|
188 |
+
<div className="space-y-2">
|
189 |
+
<Label htmlFor="bio">About You</Label>
|
190 |
+
<Textarea
|
191 |
+
id="bio"
|
192 |
+
name="bio"
|
193 |
+
value={(formData as Healer).bio || ""}
|
194 |
+
onChange={handleChange}
|
195 |
+
className="bg-gray-800/50 border-gray-700 min-h-[100px]"
|
196 |
+
placeholder="Tell us about your work, your community, and the challenges you face..."
|
197 |
+
/>
|
198 |
+
</div>
|
199 |
+
|
200 |
+
<div className="space-y-2">
|
201 |
+
<Label htmlFor="walletAddress">Wallet Address</Label>
|
202 |
+
<Input
|
203 |
+
id="walletAddress"
|
204 |
+
name="walletAddress"
|
205 |
+
value={(formData as Healer).walletAddress || ""}
|
206 |
+
onChange={handleChange}
|
207 |
+
className="bg-gray-800/50 border-gray-700"
|
208 |
+
placeholder="0x..."
|
209 |
+
/>
|
210 |
+
<p className="text-xs text-gray-400">Where you'll receive support funds</p>
|
211 |
+
</div>
|
212 |
+
</>
|
213 |
+
)}
|
214 |
+
</CardContent>
|
215 |
+
<CardFooter className="flex justify-end">
|
216 |
+
<Button
|
217 |
+
className={`${user.type === "guardian" ? "bg-guardian text-black" : "bg-flame text-white"}`}
|
218 |
+
onClick={handleSave}
|
219 |
+
disabled={isSaving}
|
220 |
+
>
|
221 |
+
<Save className="h-4 w-4 mr-2" />
|
222 |
+
{isSaving ? "Saving..." : "Save Changes"}
|
223 |
+
</Button>
|
224 |
+
</CardFooter>
|
225 |
+
</Card>
|
226 |
+
</div>
|
227 |
+
</div>
|
228 |
+
)
|
229 |
+
}
|
app/profile/page.tsx
ADDED
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useEffect, useState } from "react"
|
4 |
+
import { useRouter } from "next/navigation"
|
5 |
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
6 |
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
7 |
+
import { Button } from "@/components/ui/button"
|
8 |
+
import { UserDatabase, type User } from "@/lib/user-database"
|
9 |
+
import { Shield, Heart, Edit, LogOut } from "lucide-react"
|
10 |
+
import { ProfileHeader } from "@/components/profile/profile-header"
|
11 |
+
import { ProfileImpact } from "@/components/profile/profile-impact"
|
12 |
+
import { ProfileActivity } from "@/components/profile/profile-activity"
|
13 |
+
import { ProfileAchievements } from "@/components/profile/profile-achievements"
|
14 |
+
import { LoadingState } from "@/components/loading-state"
|
15 |
+
|
16 |
+
export default function ProfilePage() {
|
17 |
+
const router = useRouter()
|
18 |
+
const [user, setUser] = useState<User | null>(null)
|
19 |
+
const [loading, setLoading] = useState(true)
|
20 |
+
|
21 |
+
useEffect(() => {
|
22 |
+
// Get current user
|
23 |
+
const currentUser = UserDatabase.getCurrentUser()
|
24 |
+
|
25 |
+
if (!currentUser) {
|
26 |
+
// Redirect to login if no user is found
|
27 |
+
router.push("/login")
|
28 |
+
return
|
29 |
+
}
|
30 |
+
|
31 |
+
setUser(currentUser)
|
32 |
+
setLoading(false)
|
33 |
+
}, [router])
|
34 |
+
|
35 |
+
const handleLogout = () => {
|
36 |
+
// Clear current user
|
37 |
+
localStorage.removeItem("flameborn_current_user")
|
38 |
+
|
39 |
+
// Redirect to home
|
40 |
+
router.push("/")
|
41 |
+
}
|
42 |
+
|
43 |
+
if (loading) {
|
44 |
+
return <LoadingState message="Loading your profile..." />
|
45 |
+
}
|
46 |
+
|
47 |
+
if (!user) {
|
48 |
+
return (
|
49 |
+
<div className="min-h-screen bg-dusk flex items-center justify-center">
|
50 |
+
<Card className="w-full max-w-md">
|
51 |
+
<CardContent className="pt-6">
|
52 |
+
<div className="text-center">
|
53 |
+
<h2 className="text-xl font-medium mb-4">No User Found</h2>
|
54 |
+
<p className="text-gray-400 mb-6">Please log in or register to view your profile.</p>
|
55 |
+
<div className="flex gap-4 justify-center">
|
56 |
+
<Button className="bg-flame text-white hover:bg-flame/80" onClick={() => router.push("/login")}>
|
57 |
+
Log In
|
58 |
+
</Button>
|
59 |
+
<Button
|
60 |
+
variant="outline"
|
61 |
+
className="border-guardian text-guardian hover:bg-guardian/10"
|
62 |
+
onClick={() => router.push("/register/guardian")}
|
63 |
+
>
|
64 |
+
Register
|
65 |
+
</Button>
|
66 |
+
</div>
|
67 |
+
</div>
|
68 |
+
</CardContent>
|
69 |
+
</Card>
|
70 |
+
</div>
|
71 |
+
)
|
72 |
+
}
|
73 |
+
|
74 |
+
return (
|
75 |
+
<div className="min-h-screen bg-dusk py-12 px-4 sm:px-6">
|
76 |
+
<div className="max-w-6xl mx-auto">
|
77 |
+
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8">
|
78 |
+
<h1 className="text-3xl font-heading text-white mb-4 md:mb-0">My Profile</h1>
|
79 |
+
<div className="flex gap-3">
|
80 |
+
<Button
|
81 |
+
variant="outline"
|
82 |
+
className="border-gray-700 text-gray-300 hover:bg-gray-800"
|
83 |
+
onClick={() => router.push("/profile/edit")}
|
84 |
+
>
|
85 |
+
<Edit className="h-4 w-4 mr-2" /> Edit Profile
|
86 |
+
</Button>
|
87 |
+
<Button
|
88 |
+
variant="outline"
|
89 |
+
className="border-red-700 text-red-400 hover:bg-red-900/20"
|
90 |
+
onClick={handleLogout}
|
91 |
+
>
|
92 |
+
<LogOut className="h-4 w-4 mr-2" /> Log Out
|
93 |
+
</Button>
|
94 |
+
</div>
|
95 |
+
</div>
|
96 |
+
|
97 |
+
<ProfileHeader user={user} />
|
98 |
+
|
99 |
+
<Tabs defaultValue="impact" className="mt-8">
|
100 |
+
<TabsList className="bg-gray-800 border border-gray-700">
|
101 |
+
<TabsTrigger value="impact" className="text-white data-[state=active]:bg-gray-700">
|
102 |
+
Impact
|
103 |
+
</TabsTrigger>
|
104 |
+
<TabsTrigger value="activity" className="text-white data-[state=active]:bg-gray-700">
|
105 |
+
Activity
|
106 |
+
</TabsTrigger>
|
107 |
+
<TabsTrigger value="achievements" className="text-white data-[state=active]:bg-gray-700">
|
108 |
+
Achievements
|
109 |
+
</TabsTrigger>
|
110 |
+
<TabsTrigger value="details" className="text-white data-[state=active]:bg-gray-700">
|
111 |
+
Details
|
112 |
+
</TabsTrigger>
|
113 |
+
</TabsList>
|
114 |
+
|
115 |
+
<TabsContent value="impact">
|
116 |
+
<ProfileImpact user={user} />
|
117 |
+
</TabsContent>
|
118 |
+
|
119 |
+
<TabsContent value="activity">
|
120 |
+
<ProfileActivity user={user} />
|
121 |
+
</TabsContent>
|
122 |
+
|
123 |
+
<TabsContent value="achievements">
|
124 |
+
<ProfileAchievements user={user} />
|
125 |
+
</TabsContent>
|
126 |
+
|
127 |
+
<TabsContent value="details">
|
128 |
+
<Card className="bg-dusk border border-gray-700">
|
129 |
+
<CardHeader>
|
130 |
+
<CardTitle>Profile Details</CardTitle>
|
131 |
+
</CardHeader>
|
132 |
+
<CardContent>
|
133 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
134 |
+
<div className="space-y-4">
|
135 |
+
<div>
|
136 |
+
<h3 className="text-sm font-medium text-gray-400">Full Name</h3>
|
137 |
+
<p className="text-white">{user.fullName}</p>
|
138 |
+
</div>
|
139 |
+
|
140 |
+
<div>
|
141 |
+
<h3 className="text-sm font-medium text-gray-400">Email</h3>
|
142 |
+
<p className="text-white">{user.email}</p>
|
143 |
+
</div>
|
144 |
+
|
145 |
+
<div>
|
146 |
+
<h3 className="text-sm font-medium text-gray-400">Member Since</h3>
|
147 |
+
<p className="text-white">{new Date(user.registeredAt).toLocaleDateString()}</p>
|
148 |
+
</div>
|
149 |
+
|
150 |
+
<div>
|
151 |
+
<h3 className="text-sm font-medium text-gray-400">Role</h3>
|
152 |
+
<p className="text-white flex items-center">
|
153 |
+
{user.type === "guardian" ? (
|
154 |
+
<>
|
155 |
+
<Shield className="h-4 w-4 mr-2 text-guardian" /> Guardian
|
156 |
+
</>
|
157 |
+
) : (
|
158 |
+
<>
|
159 |
+
<Heart className="h-4 w-4 mr-2 text-flame" /> Healer
|
160 |
+
</>
|
161 |
+
)}
|
162 |
+
</p>
|
163 |
+
</div>
|
164 |
+
</div>
|
165 |
+
|
166 |
+
<div className="space-y-4">
|
167 |
+
{user.type === "guardian" && (
|
168 |
+
<>
|
169 |
+
<div>
|
170 |
+
<h3 className="text-sm font-medium text-gray-400">Country</h3>
|
171 |
+
<p className="text-white">{user.country}</p>
|
172 |
+
</div>
|
173 |
+
|
174 |
+
<div>
|
175 |
+
<h3 className="text-sm font-medium text-gray-400">Contribution</h3>
|
176 |
+
<p className="text-white">
|
177 |
+
{user.contributionAmount} {user.currency.toUpperCase()}
|
178 |
+
</p>
|
179 |
+
</div>
|
180 |
+
|
181 |
+
<div>
|
182 |
+
<h3 className="text-sm font-medium text-gray-400">Payment Method</h3>
|
183 |
+
<p className="text-white capitalize">{user.paymentMethod}</p>
|
184 |
+
</div>
|
185 |
+
</>
|
186 |
+
)}
|
187 |
+
|
188 |
+
{user.type === "healer" && (
|
189 |
+
<>
|
190 |
+
<div>
|
191 |
+
<h3 className="text-sm font-medium text-gray-400">Healthcare Role</h3>
|
192 |
+
<p className="text-white capitalize">{user.role}</p>
|
193 |
+
</div>
|
194 |
+
|
195 |
+
<div>
|
196 |
+
<h3 className="text-sm font-medium text-gray-400">Location</h3>
|
197 |
+
<p className="text-white">
|
198 |
+
{user.city}, {user.country}
|
199 |
+
</p>
|
200 |
+
</div>
|
201 |
+
|
202 |
+
<div>
|
203 |
+
<h3 className="text-sm font-medium text-gray-400">Verification Status</h3>
|
204 |
+
<p
|
205 |
+
className={`capitalize ${
|
206 |
+
user.verificationStatus === "verified"
|
207 |
+
? "text-green-400"
|
208 |
+
: user.verificationStatus === "rejected"
|
209 |
+
? "text-red-400"
|
210 |
+
: "text-yellow-400"
|
211 |
+
}`}
|
212 |
+
>
|
213 |
+
{user.verificationStatus}
|
214 |
+
</p>
|
215 |
+
</div>
|
216 |
+
|
217 |
+
{user.walletAddress && (
|
218 |
+
<div>
|
219 |
+
<h3 className="text-sm font-medium text-gray-400">Wallet Address</h3>
|
220 |
+
<p className="text-white text-sm truncate">{user.walletAddress}</p>
|
221 |
+
</div>
|
222 |
+
)}
|
223 |
+
</>
|
224 |
+
)}
|
225 |
+
</div>
|
226 |
+
</div>
|
227 |
+
</CardContent>
|
228 |
+
</Card>
|
229 |
+
</TabsContent>
|
230 |
+
</Tabs>
|
231 |
+
</div>
|
232 |
+
</div>
|
233 |
+
)
|
234 |
+
}
|
app/register-chw/page.tsx
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { RegisterCHW } from "@/components/register-chw"
|
2 |
+
|
3 |
+
export default function RegisterCHWPage() {
|
4 |
+
return (
|
5 |
+
<div className="min-h-screen bg-black py-12 px-4">
|
6 |
+
<h1 className="text-3xl font-bold text-center flame-text mb-8">Healthcare Worker Registration</h1>
|
7 |
+
<RegisterCHW />
|
8 |
+
</div>
|
9 |
+
)
|
10 |
+
}
|
app/register/guardian/page.tsx
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { RegistrationLayout } from "@/components/registration/registration-layout"
|
2 |
+
import { GuardianRegistrationForm } from "@/components/registration/guardian-registration-form"
|
3 |
+
|
4 |
+
export default function GuardianRegistrationPage() {
|
5 |
+
return (
|
6 |
+
<RegistrationLayout
|
7 |
+
title="Become a Guardian"
|
8 |
+
subtitle="Join the movement to support healthcare workers across Africa"
|
9 |
+
type="guardian"
|
10 |
+
>
|
11 |
+
<GuardianRegistrationForm />
|
12 |
+
</RegistrationLayout>
|
13 |
+
)
|
14 |
+
}
|
app/register/healer/page.tsx
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { RegistrationLayout } from "@/components/registration/registration-layout"
|
2 |
+
import { HealerRegistrationForm } from "@/components/registration/healer-registration-form"
|
3 |
+
|
4 |
+
export default function HealerRegistrationPage() {
|
5 |
+
return (
|
6 |
+
<RegistrationLayout
|
7 |
+
title="Register as a Healer"
|
8 |
+
subtitle="Join the Flameborn community of verified healthcare workers"
|
9 |
+
type="healer"
|
10 |
+
>
|
11 |
+
<HealerRegistrationForm />
|
12 |
+
</RegistrationLayout>
|
13 |
+
)
|
14 |
+
}
|
app/smart-contracts/page.tsx
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import SmartContractInfo from "@/components/smart-contract-info"
|
2 |
+
|
3 |
+
export default function SmartContractsPage() {
|
4 |
+
return (
|
5 |
+
<div className="min-h-screen bg-black text-white py-20 px-4">
|
6 |
+
<div className="max-w-6xl mx-auto">
|
7 |
+
<div className="text-center mb-12">
|
8 |
+
<h1 className="text-4xl md:text-5xl font-bold mb-4">
|
9 |
+
Smart Contract <span className="text-orange-500">Architecture</span>
|
10 |
+
</h1>
|
11 |
+
<p className="text-xl text-gray-300 max-w-3xl mx-auto">
|
12 |
+
Explore the decentralized infrastructure powering Africa's health sovereignty revolution
|
13 |
+
</p>
|
14 |
+
</div>
|
15 |
+
<SmartContractInfo />
|
16 |
+
</div>
|
17 |
+
</div>
|
18 |
+
)
|
19 |
+
}
|
app/test-proverbs/page.tsx
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ProverbTester } from "@/components/proverb-tester"
|
2 |
+
|
3 |
+
export default function TestProverbsPage() {
|
4 |
+
return (
|
5 |
+
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-black">
|
6 |
+
<div className="container mx-auto py-8">
|
7 |
+
<ProverbTester />
|
8 |
+
</div>
|
9 |
+
</div>
|
10 |
+
)
|
11 |
+
}
|
app/testnet/page.tsx
ADDED
@@ -0,0 +1,390 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useState, useEffect } from "react"
|
4 |
+
import { TestnetStatus } from "@/components/testnet-status"
|
5 |
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
6 |
+
import { Button } from "@/components/ui/button"
|
7 |
+
import { Input } from "@/components/ui/input"
|
8 |
+
import { Label } from "@/components/ui/label"
|
9 |
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
10 |
+
import { testnetClient, type TestnetUser, type TestnetValidator } from "@/lib/testnet-client"
|
11 |
+
import { Flame, Users, Server, Activity, Plus, Heart } from "lucide-react"
|
12 |
+
import { useToast } from "@/hooks/use-toast"
|
13 |
+
|
14 |
+
export default function TestnetPage() {
|
15 |
+
const [users, setUsers] = useState<TestnetUser[]>([])
|
16 |
+
const [validators, setValidators] = useState<TestnetValidator[]>([])
|
17 |
+
const [isLoading, setIsLoading] = useState(true)
|
18 |
+
const [newUser, setNewUser] = useState({
|
19 |
+
wallet_address: "",
|
20 |
+
role: "",
|
21 |
+
name: "",
|
22 |
+
location: "",
|
23 |
+
})
|
24 |
+
const [newValidator, setNewValidator] = useState({
|
25 |
+
wallet_address: "",
|
26 |
+
name: "",
|
27 |
+
stake_amount: 0,
|
28 |
+
})
|
29 |
+
const { toast } = useToast()
|
30 |
+
|
31 |
+
const loadData = async () => {
|
32 |
+
try {
|
33 |
+
const [usersData, validatorsData] = await Promise.all([testnetClient.getUsers(), testnetClient.getValidators()])
|
34 |
+
setUsers(usersData)
|
35 |
+
setValidators(validatorsData)
|
36 |
+
} catch (error) {
|
37 |
+
console.error("Failed to load data:", error)
|
38 |
+
toast({
|
39 |
+
title: "Error",
|
40 |
+
description: "Failed to load testnet data",
|
41 |
+
variant: "destructive",
|
42 |
+
})
|
43 |
+
} finally {
|
44 |
+
setIsLoading(false)
|
45 |
+
}
|
46 |
+
}
|
47 |
+
|
48 |
+
useEffect(() => {
|
49 |
+
loadData()
|
50 |
+
}, [])
|
51 |
+
|
52 |
+
const handleCreateUser = async () => {
|
53 |
+
try {
|
54 |
+
if (!newUser.wallet_address || !newUser.role || !newUser.name || !newUser.location) {
|
55 |
+
toast({
|
56 |
+
title: "Error",
|
57 |
+
description: "Please fill in all fields",
|
58 |
+
variant: "destructive",
|
59 |
+
})
|
60 |
+
return
|
61 |
+
}
|
62 |
+
|
63 |
+
await testnetClient.createUser(newUser)
|
64 |
+
setNewUser({ wallet_address: "", role: "", name: "", location: "" })
|
65 |
+
await loadData()
|
66 |
+
|
67 |
+
toast({
|
68 |
+
title: "Success",
|
69 |
+
description: `Ubuntu member ${newUser.name} registered successfully!`,
|
70 |
+
})
|
71 |
+
} catch (error) {
|
72 |
+
console.error("Failed to create user:", error)
|
73 |
+
toast({
|
74 |
+
title: "Error",
|
75 |
+
description: "Failed to register user",
|
76 |
+
variant: "destructive",
|
77 |
+
})
|
78 |
+
}
|
79 |
+
}
|
80 |
+
|
81 |
+
const handleCreateValidator = async () => {
|
82 |
+
try {
|
83 |
+
if (!newValidator.wallet_address || !newValidator.name || newValidator.stake_amount <= 0) {
|
84 |
+
toast({
|
85 |
+
title: "Error",
|
86 |
+
description: "Please fill in all fields with valid values",
|
87 |
+
variant: "destructive",
|
88 |
+
})
|
89 |
+
return
|
90 |
+
}
|
91 |
+
|
92 |
+
await testnetClient.createValidator(newValidator)
|
93 |
+
setNewValidator({ wallet_address: "", name: "", stake_amount: 0 })
|
94 |
+
await loadData()
|
95 |
+
|
96 |
+
toast({
|
97 |
+
title: "Success",
|
98 |
+
description: `Validator ${newValidator.name} registered successfully!`,
|
99 |
+
})
|
100 |
+
} catch (error) {
|
101 |
+
console.error("Failed to create validator:", error)
|
102 |
+
toast({
|
103 |
+
title: "Error",
|
104 |
+
description: "Failed to register validator",
|
105 |
+
variant: "destructive",
|
106 |
+
})
|
107 |
+
}
|
108 |
+
}
|
109 |
+
|
110 |
+
const sendHeartbeat = async (walletAddress: string) => {
|
111 |
+
try {
|
112 |
+
await testnetClient.sendValidatorHeartbeat(walletAddress)
|
113 |
+
await loadData()
|
114 |
+
|
115 |
+
toast({
|
116 |
+
title: "Heartbeat Sent",
|
117 |
+
description: "Validator heartbeat recorded successfully",
|
118 |
+
})
|
119 |
+
} catch (error) {
|
120 |
+
console.error("Failed to send heartbeat:", error)
|
121 |
+
toast({
|
122 |
+
title: "Error",
|
123 |
+
description: "Failed to send heartbeat",
|
124 |
+
variant: "destructive",
|
125 |
+
})
|
126 |
+
}
|
127 |
+
}
|
128 |
+
|
129 |
+
return (
|
130 |
+
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 p-4">
|
131 |
+
<div className="max-w-7xl mx-auto space-y-8">
|
132 |
+
{/* Header */}
|
133 |
+
<div className="text-center space-y-4">
|
134 |
+
<div className="flex items-center justify-center gap-3">
|
135 |
+
<Flame className="w-12 h-12 text-orange-500" />
|
136 |
+
<h1 className="text-4xl font-bold text-white">FlameBorn Testnet</h1>
|
137 |
+
</div>
|
138 |
+
<p className="text-xl text-slate-300 max-w-2xl mx-auto">
|
139 |
+
Ubuntu-powered healthcare tokenization testnet. "I am because we are" - Test the network, register users,
|
140 |
+
and validate transactions.
|
141 |
+
</p>
|
142 |
+
</div>
|
143 |
+
|
144 |
+
{/* Network Status */}
|
145 |
+
<TestnetStatus />
|
146 |
+
|
147 |
+
{/* User Registration */}
|
148 |
+
<Card className="bg-slate-800 border-slate-700">
|
149 |
+
<CardHeader>
|
150 |
+
<CardTitle className="flex items-center gap-2 text-white">
|
151 |
+
<Users className="w-5 h-5 text-blue-400" />
|
152 |
+
Register Ubuntu Member
|
153 |
+
</CardTitle>
|
154 |
+
<CardDescription className="text-slate-400">
|
155 |
+
Join the FlameBorn healthcare network as a healer, guardian, or community member
|
156 |
+
</CardDescription>
|
157 |
+
</CardHeader>
|
158 |
+
<CardContent className="space-y-4">
|
159 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
160 |
+
<div className="space-y-2">
|
161 |
+
<Label htmlFor="wallet" className="text-slate-300">
|
162 |
+
Wallet Address
|
163 |
+
</Label>
|
164 |
+
<Input
|
165 |
+
id="wallet"
|
166 |
+
placeholder="0x..."
|
167 |
+
value={newUser.wallet_address}
|
168 |
+
onChange={(e) => setNewUser({ ...newUser, wallet_address: e.target.value })}
|
169 |
+
className="bg-slate-700 border-slate-600 text-white"
|
170 |
+
/>
|
171 |
+
</div>
|
172 |
+
<div className="space-y-2">
|
173 |
+
<Label htmlFor="role" className="text-slate-300">
|
174 |
+
Role
|
175 |
+
</Label>
|
176 |
+
<Select value={newUser.role} onValueChange={(value) => setNewUser({ ...newUser, role: value })}>
|
177 |
+
<SelectTrigger className="bg-slate-700 border-slate-600 text-white">
|
178 |
+
<SelectValue placeholder="Select role" />
|
179 |
+
</SelectTrigger>
|
180 |
+
<SelectContent>
|
181 |
+
<SelectItem value="healer">Healer</SelectItem>
|
182 |
+
<SelectItem value="guardian">Guardian</SelectItem>
|
183 |
+
<SelectItem value="community">Community Member</SelectItem>
|
184 |
+
</SelectContent>
|
185 |
+
</Select>
|
186 |
+
</div>
|
187 |
+
<div className="space-y-2">
|
188 |
+
<Label htmlFor="name" className="text-slate-300">
|
189 |
+
Name
|
190 |
+
</Label>
|
191 |
+
<Input
|
192 |
+
id="name"
|
193 |
+
placeholder="Full name"
|
194 |
+
value={newUser.name}
|
195 |
+
onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
|
196 |
+
className="bg-slate-700 border-slate-600 text-white"
|
197 |
+
/>
|
198 |
+
</div>
|
199 |
+
<div className="space-y-2">
|
200 |
+
<Label htmlFor="location" className="text-slate-300">
|
201 |
+
Location
|
202 |
+
</Label>
|
203 |
+
<Input
|
204 |
+
id="location"
|
205 |
+
placeholder="City, Country"
|
206 |
+
value={newUser.location}
|
207 |
+
onChange={(e) => setNewUser({ ...newUser, location: e.target.value })}
|
208 |
+
className="bg-slate-700 border-slate-600 text-white"
|
209 |
+
/>
|
210 |
+
</div>
|
211 |
+
</div>
|
212 |
+
<Button onClick={handleCreateUser} className="bg-blue-600 hover:bg-blue-700">
|
213 |
+
<Plus className="w-4 h-4 mr-2" />
|
214 |
+
Register Ubuntu Member
|
215 |
+
</Button>
|
216 |
+
</CardContent>
|
217 |
+
</Card>
|
218 |
+
|
219 |
+
{/* Validator Registration */}
|
220 |
+
<Card className="bg-slate-800 border-slate-700">
|
221 |
+
<CardHeader>
|
222 |
+
<CardTitle className="flex items-center gap-2 text-white">
|
223 |
+
<Server className="w-5 h-5 text-green-400" />
|
224 |
+
Register Validator
|
225 |
+
</CardTitle>
|
226 |
+
<CardDescription className="text-slate-400">
|
227 |
+
Become a validator to help secure the Ubuntu network
|
228 |
+
</CardDescription>
|
229 |
+
</CardHeader>
|
230 |
+
<CardContent className="space-y-4">
|
231 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
232 |
+
<div className="space-y-2">
|
233 |
+
<Label htmlFor="val-wallet" className="text-slate-300">
|
234 |
+
Wallet Address
|
235 |
+
</Label>
|
236 |
+
<Input
|
237 |
+
id="val-wallet"
|
238 |
+
placeholder="0x..."
|
239 |
+
value={newValidator.wallet_address}
|
240 |
+
onChange={(e) => setNewValidator({ ...newValidator, wallet_address: e.target.value })}
|
241 |
+
className="bg-slate-700 border-slate-600 text-white"
|
242 |
+
/>
|
243 |
+
</div>
|
244 |
+
<div className="space-y-2">
|
245 |
+
<Label htmlFor="val-name" className="text-slate-300">
|
246 |
+
Validator Name
|
247 |
+
</Label>
|
248 |
+
<Input
|
249 |
+
id="val-name"
|
250 |
+
placeholder="Validator name"
|
251 |
+
value={newValidator.name}
|
252 |
+
onChange={(e) => setNewValidator({ ...newValidator, name: e.target.value })}
|
253 |
+
className="bg-slate-700 border-slate-600 text-white"
|
254 |
+
/>
|
255 |
+
</div>
|
256 |
+
<div className="space-y-2">
|
257 |
+
<Label htmlFor="stake" className="text-slate-300">
|
258 |
+
Stake Amount (FLB)
|
259 |
+
</Label>
|
260 |
+
<Input
|
261 |
+
id="stake"
|
262 |
+
type="number"
|
263 |
+
placeholder="1000"
|
264 |
+
value={newValidator.stake_amount}
|
265 |
+
onChange={(e) => setNewValidator({ ...newValidator, stake_amount: Number(e.target.value) })}
|
266 |
+
className="bg-slate-700 border-slate-600 text-white"
|
267 |
+
/>
|
268 |
+
</div>
|
269 |
+
</div>
|
270 |
+
<Button onClick={handleCreateValidator} className="bg-green-600 hover:bg-green-700">
|
271 |
+
<Plus className="w-4 h-4 mr-2" />
|
272 |
+
Register Validator
|
273 |
+
</Button>
|
274 |
+
</CardContent>
|
275 |
+
</Card>
|
276 |
+
|
277 |
+
{/* Users List */}
|
278 |
+
<Card className="bg-slate-800 border-slate-700">
|
279 |
+
<CardHeader>
|
280 |
+
<CardTitle className="flex items-center gap-2 text-white">
|
281 |
+
<Heart className="w-5 h-5 text-red-400" />
|
282 |
+
Ubuntu Network Members ({users.length})
|
283 |
+
</CardTitle>
|
284 |
+
</CardHeader>
|
285 |
+
<CardContent>
|
286 |
+
{isLoading ? (
|
287 |
+
<div className="text-center py-8">
|
288 |
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-orange-500 mx-auto"></div>
|
289 |
+
<p className="text-slate-400 mt-2">Loading members...</p>
|
290 |
+
</div>
|
291 |
+
) : users.length === 0 ? (
|
292 |
+
<p className="text-slate-400 text-center py-8">No members registered yet</p>
|
293 |
+
) : (
|
294 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
295 |
+
{users.map((user) => (
|
296 |
+
<Card key={user.id} className="bg-slate-700 border-slate-600">
|
297 |
+
<CardContent className="p-4">
|
298 |
+
<div className="space-y-2">
|
299 |
+
<div className="flex items-center justify-between">
|
300 |
+
<h3 className="font-semibold text-white">{user.name}</h3>
|
301 |
+
<span
|
302 |
+
className={`px-2 py-1 rounded text-xs ${
|
303 |
+
user.role === "healer"
|
304 |
+
? "bg-green-600 text-white"
|
305 |
+
: user.role === "guardian"
|
306 |
+
? "bg-blue-600 text-white"
|
307 |
+
: "bg-purple-600 text-white"
|
308 |
+
}`}
|
309 |
+
>
|
310 |
+
{user.role}
|
311 |
+
</span>
|
312 |
+
</div>
|
313 |
+
<p className="text-slate-400 text-sm">{user.location}</p>
|
314 |
+
<p className="text-slate-400 text-xs font-mono">{user.wallet_address}</p>
|
315 |
+
<div className="flex items-center justify-between">
|
316 |
+
<span className="text-orange-400 font-semibold">{user.flb_balance} FLB</span>
|
317 |
+
{user.verified && <span className="text-green-400 text-xs">✓ Verified</span>}
|
318 |
+
</div>
|
319 |
+
</div>
|
320 |
+
</CardContent>
|
321 |
+
</Card>
|
322 |
+
))}
|
323 |
+
</div>
|
324 |
+
)}
|
325 |
+
</CardContent>
|
326 |
+
</Card>
|
327 |
+
|
328 |
+
{/* Validators List */}
|
329 |
+
<Card className="bg-slate-800 border-slate-700">
|
330 |
+
<CardHeader>
|
331 |
+
<CardTitle className="flex items-center gap-2 text-white">
|
332 |
+
<Activity className="w-5 h-5 text-green-400" />
|
333 |
+
Network Validators ({validators.length})
|
334 |
+
</CardTitle>
|
335 |
+
</CardHeader>
|
336 |
+
<CardContent>
|
337 |
+
{validators.length === 0 ? (
|
338 |
+
<p className="text-slate-400 text-center py-8">No validators registered yet</p>
|
339 |
+
) : (
|
340 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
341 |
+
{validators.map((validator) => (
|
342 |
+
<Card key={validator.id} className="bg-slate-700 border-slate-600">
|
343 |
+
<CardContent className="p-4">
|
344 |
+
<div className="space-y-2">
|
345 |
+
<div className="flex items-center justify-between">
|
346 |
+
<h3 className="font-semibold text-white">{validator.name}</h3>
|
347 |
+
<span
|
348 |
+
className={`px-2 py-1 rounded text-xs ${
|
349 |
+
validator.active ? "bg-green-600 text-white" : "bg-red-600 text-white"
|
350 |
+
}`}
|
351 |
+
>
|
352 |
+
{validator.active ? "Active" : "Inactive"}
|
353 |
+
</span>
|
354 |
+
</div>
|
355 |
+
<p className="text-slate-400 text-xs font-mono">{validator.wallet_address}</p>
|
356 |
+
<div className="grid grid-cols-2 gap-2 text-sm">
|
357 |
+
<div>
|
358 |
+
<span className="text-slate-400">Stake:</span>
|
359 |
+
<span className="text-orange-400 ml-1">{validator.stake_amount} FLB</span>
|
360 |
+
</div>
|
361 |
+
<div>
|
362 |
+
<span className="text-slate-400">Blocks:</span>
|
363 |
+
<span className="text-white ml-1">{validator.blocks_validated}</span>
|
364 |
+
</div>
|
365 |
+
<div>
|
366 |
+
<span className="text-slate-400">Uptime:</span>
|
367 |
+
<span className="text-green-400 ml-1">{validator.uptime_percentage.toFixed(1)}%</span>
|
368 |
+
</div>
|
369 |
+
<div>
|
370 |
+
<Button
|
371 |
+
size="sm"
|
372 |
+
onClick={() => sendHeartbeat(validator.wallet_address)}
|
373 |
+
className="bg-purple-600 hover:bg-purple-700 text-xs"
|
374 |
+
>
|
375 |
+
Send Heartbeat
|
376 |
+
</Button>
|
377 |
+
</div>
|
378 |
+
</div>
|
379 |
+
</div>
|
380 |
+
</CardContent>
|
381 |
+
</Card>
|
382 |
+
))}
|
383 |
+
</div>
|
384 |
+
)}
|
385 |
+
</CardContent>
|
386 |
+
</Card>
|
387 |
+
</div>
|
388 |
+
</div>
|
389 |
+
)
|
390 |
+
}
|
app/token-system/page.tsx
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { TokenRegistrationFlow } from "@/components/token-registration-flow"
|
4 |
+
import { TokenDistributionExplainer } from "@/components/token-distribution-explainer"
|
5 |
+
import { Coins } from "lucide-react"
|
6 |
+
import dynamic from "next/dynamic"
|
7 |
+
|
8 |
+
const ParticleBackground = dynamic(
|
9 |
+
() => import("@/components/particle-background").then((mod) => ({ default: mod.ParticleBackground })),
|
10 |
+
{
|
11 |
+
ssr: false,
|
12 |
+
loading: () => <div className="fixed inset-0 bg-black" />,
|
13 |
+
},
|
14 |
+
)
|
15 |
+
|
16 |
+
export default function TokenSystemPage() {
|
17 |
+
return (
|
18 |
+
<div className="min-h-screen bg-dusk relative">
|
19 |
+
<ParticleBackground />
|
20 |
+
<div className="relative z-10">
|
21 |
+
{/* Hero Section */}
|
22 |
+
<div className="py-20 text-center">
|
23 |
+
<div className="max-w-4xl mx-auto px-4">
|
24 |
+
<div className="flex justify-center mb-6">
|
25 |
+
<div className="p-4 bg-orange-500/20 rounded-full">
|
26 |
+
<Coins className="w-12 h-12 text-orange-500" />
|
27 |
+
</div>
|
28 |
+
</div>
|
29 |
+
<h1 className="text-4xl md:text-6xl font-bold text-white mb-6">FLB Token System</h1>
|
30 |
+
<p className="text-xl text-gray-300 mb-8 max-w-2xl mx-auto">
|
31 |
+
Empowering African healthcare through blockchain technology. Join the revolution that rewards impact and
|
32 |
+
builds sustainable health systems.
|
33 |
+
</p>
|
34 |
+
</div>
|
35 |
+
</div>
|
36 |
+
|
37 |
+
{/* Main Content */}
|
38 |
+
<div className="max-w-6xl mx-auto px-4 py-8 space-y-12">
|
39 |
+
<TokenRegistrationFlow />
|
40 |
+
<TokenDistributionExplainer />
|
41 |
+
</div>
|
42 |
+
</div>
|
43 |
+
</div>
|
44 |
+
)
|
45 |
+
}
|