Seth0330 commited on
Commit
1cfd5fd
·
verified ·
1 Parent(s): d0567db

Update frontend/src/pages/StudentDashboard.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/pages/StudentDashboard.jsx +207 -147
frontend/src/pages/StudentDashboard.jsx CHANGED
@@ -1,7 +1,7 @@
1
  // frontend/src/pages/StudentDashboard.jsx
2
- import React, { useState } from "react";
3
- import { useQuery } from "@tanstack/react-query";
4
  import { useNavigate } from "react-router-dom";
 
5
 
6
  import api from "../api/client";
7
  import AppHeader from "../components/AppHeader";
@@ -9,49 +9,58 @@ import UserMenu from "../components/UserMenu";
9
  import StudentAIAssistant from "../components/student/StudentAIAssistant";
10
 
11
  export default function StudentDashboard() {
12
- const [showChat, setShowChat] = useState(false);
13
  const navigate = useNavigate();
14
 
15
- // Read student from sessionStorage
16
- const storedStudent = JSON.parse(sessionStorage.getItem("student") || "{}");
17
- const studentName = storedStudent.name || "Student";
18
- const studentEmail = storedStudent.email || "student@example.com";
19
 
20
- function handleStudentLogout() {
 
 
 
 
 
 
21
  sessionStorage.removeItem("student");
22
  navigate("/login");
23
  }
24
 
25
- const email = storedStudent.email;
26
-
27
- // Fetch plans
28
- const plansQuery = useQuery({
29
- queryKey: ["student-plans"],
30
- queryFn: async () => {
31
- const res = await api.get("/student/plans");
32
- return Array.isArray(res.data) ? res.data : [];
33
- },
34
- initialData: [],
35
- });
36
-
37
- // Fetch memberships
38
- const membershipsQuery = useQuery({
39
- queryKey: ["student-memberships", email],
40
- queryFn: async () => {
41
- if (!email) return [];
42
- const res = await api.get(`/student/memberships?email=${email}`);
43
- return Array.isArray(res.data) ? res.data : [];
44
- },
45
- enabled: !!email,
46
- initialData: [],
47
- });
48
-
49
- const plans = plansQuery.data || [];
50
- const memberships = membershipsQuery.data || [];
 
 
 
 
 
 
51
 
52
  return (
53
  <div className="min-h-screen bg-slate-50">
54
- {/* ---------- HEADER ---------- */}
55
  <AppHeader
56
  title="Karate Student Portal"
57
  subtitle={`Welcome back, ${studentName}. Train hard, stay consistent.`}
@@ -59,154 +68,205 @@ export default function StudentDashboard() {
59
  <div className="flex items-center gap-3">
60
  <button
61
  type="button"
62
- onClick={() => setShowChat(true)}
63
  className="inline-flex items-center gap-2 rounded-lg bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium px-4 py-2 shadow-sm"
64
  >
 
65
  Ask the Sensei
66
  </button>
67
  <UserMenu
68
  name={studentName}
69
  email={studentEmail}
70
- onLogout={handleStudentLogout}
71
  />
72
  </div>
73
  }
74
  />
75
 
76
- {/* ---------- CONTENT ---------- */}
77
- <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
78
  {/* Available plans */}
79
- <div className="mb-8 bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
80
- <h2 className="text-lg font-semibold text-slate-900">
81
- Available membership plans
82
- </h2>
83
- <p className="text-sm text-slate-500 mb-4">
84
- Choose a plan and pay securely via Stripe.
85
- </p>
86
-
87
- {plans.length === 0 ? (
88
- <p className="text-sm text-slate-500">
89
- No public plans are available right now. Please check back later
90
- or ask at the front desk.
91
  </p>
92
- ) : (
93
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-4">
94
- {plans.map((p) => (
95
- <div
96
- key={p.id}
97
- className="p-5 rounded-xl border border-slate-200 bg-slate-50 shadow-sm"
98
- >
99
- <h3 className="text-lg font-semibold text-slate-800">
100
- {p.name}
101
- </h3>
102
- {p.description && (
103
- <p className="text-sm text-slate-500 mb-3">
104
- {p.description}
105
- </p>
106
- )}
107
- <p className="text-base font-semibold mb-4">
108
- ${p.price} / {p.billing_period}
109
- </p>
110
- {p.stripe_link && (
111
- <a
112
- href={p.stripe_link}
113
- target="_blank"
114
- rel="noopener noreferrer"
115
- className="inline-flex items-center justify-center w-full bg-blue-600 text-white text-sm font-medium py-2.5 rounded-lg hover:bg-blue-700"
116
- >
117
- Pay with Stripe
118
- </a>
119
- )}
120
- </div>
121
- ))}
122
- </div>
123
- )}
124
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  {/* Rank / Next class / Attendance */}
127
- <div className="grid grid-cols-1 sm:grid-cols-3 gap-6 mb-10">
128
- <div className="bg-white border border-slate-200 rounded-2xl p-6 shadow-sm">
129
- <h4 className="text-xs font-semibold text-slate-500 uppercase">
130
  Current rank
131
- </h4>
132
- <p className="text-xl font-bold text-slate-800 mt-2">White Belt</p>
133
- <p className="text-sm text-slate-500 mt-1">
 
 
134
  Progress updates will appear here.
135
  </p>
136
  </div>
137
 
138
- <div className="bg-white border border-slate-200 rounded-2xl p-6 shadow-sm">
139
- <h4 className="text-xs font-semibold text-slate-500 uppercase">
140
  Next class
141
- </h4>
142
- <p className="text-xl font-bold text-slate-800 mt-2">
143
  Tuesday, 6:00 PM
144
  </p>
145
- <p className="text-sm text-slate-500 mt-1">
146
  Location: Main Dojo, Kamloops.
147
  </p>
148
  </div>
149
 
150
- <div className="bg-white border border-slate-200 rounded-2xl p-6 shadow-sm">
151
- <h4 className="text-xs font-semibold text-slate-500 uppercase">
152
  Attendance
153
- </h4>
154
- <p className="text-xl font-bold text-slate-800 mt-2">
155
  0 / 8 classes
156
  </p>
157
- <p className="text-sm text-slate-500 mt-1">
158
- This months attendance summary.
159
  </p>
160
  </div>
161
- </div>
162
 
163
- {/* Upcoming renewals & payments */}
164
- <div className="bg-white border border-slate-200 rounded-2xl p-6 shadow-sm mb-10">
165
- <h4 className="text-sm font-semibold text-slate-900">
166
  Upcoming renewals &amp; payments
167
- </h4>
168
- <p className="text-sm text-slate-500 mt-2">
169
- Once you complete a Stripe payment for a membership, you&apos;ll see
170
- your renewal date, plan, and payment details here.
171
- </p>
172
- </div>
173
-
174
- {/* Membership records */}
175
- <div className="bg-white border border-slate-200 rounded-2xl p-6 shadow-sm">
176
- <h4 className="text-sm font-semibold text-slate-900">
177
- Your memberships
178
- </h4>
179
-
180
- {memberships.length === 0 ? (
181
- <p className="text-sm text-slate-500 mt-2">
182
- You don&apos;t have an active membership yet. Pick a plan above to
183
- get started.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  </p>
185
- ) : (
186
- <div className="mt-4 space-y-4">
187
- {memberships.map((m) => (
188
- <div
189
- key={m.id}
190
- className="border-b border-slate-100 pb-4 last:border-none"
191
- >
192
- <p className="text-base font-semibold text-slate-800">
193
- {m.plan_name}
194
- </p>
195
- <p className="text-sm text-slate-500 mt-1">
196
- Status: {m.status} — Renews on {m.renewal_date}
197
- </p>
198
- </div>
199
- ))}
200
- </div>
201
- )}
202
- </div>
203
- </div>
204
-
205
- {/* Ask the Sensei side panel */}
206
- <StudentAIAssistant
207
- open={showChat}
208
- onClose={() => setShowChat(false)}
209
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  </div>
211
  );
212
  }
 
1
  // frontend/src/pages/StudentDashboard.jsx
2
+ import React, { useEffect, useState } from "react";
 
3
  import { useNavigate } from "react-router-dom";
4
+ import { Sparkles } from "lucide-react";
5
 
6
  import api from "../api/client";
7
  import AppHeader from "../components/AppHeader";
 
9
  import StudentAIAssistant from "../components/student/StudentAIAssistant";
10
 
11
  export default function StudentDashboard() {
 
12
  const navigate = useNavigate();
13
 
14
+ const stored = JSON.parse(sessionStorage.getItem("student") || "{}");
15
+ const studentName = stored.name || "student1";
16
+ const studentEmail = stored.email || "student1@example.com";
 
17
 
18
+ const [plans, setPlans] = useState([]);
19
+ const [memberships, setMemberships] = useState([]);
20
+ const [upcomingRenewal, setUpcomingRenewal] = useState(null);
21
+ const [loading, setLoading] = useState(true);
22
+ const [aiOpen, setAiOpen] = useState(false);
23
+
24
+ function handleLogout() {
25
  sessionStorage.removeItem("student");
26
  navigate("/login");
27
  }
28
 
29
+ useEffect(() => {
30
+ if (!studentEmail) return;
31
+
32
+ async function loadData() {
33
+ setLoading(true);
34
+ try {
35
+ // Available plans
36
+ const plansRes = await api.get("/student/plans");
37
+ setPlans(Array.isArray(plansRes.data) ? plansRes.data : []);
38
+
39
+ // Memberships for this student
40
+ const membershipsRes = await api.get("/student/memberships", {
41
+ params: { email: studentEmail },
42
+ });
43
+ setMemberships(Array.isArray(membershipsRes.data) ? membershipsRes.data : []);
44
+
45
+ // Upcoming renewal (may 404 if none)
46
+ try {
47
+ const upcomingRes = await api.get("/student/upcoming-renewal", {
48
+ params: { email: studentEmail },
49
+ });
50
+ setUpcomingRenewal(upcomingRes.data);
51
+ } catch (err) {
52
+ setUpcomingRenewal(null);
53
+ }
54
+ } finally {
55
+ setLoading(false);
56
+ }
57
+ }
58
+
59
+ loadData();
60
+ }, [studentEmail]);
61
 
62
  return (
63
  <div className="min-h-screen bg-slate-50">
 
64
  <AppHeader
65
  title="Karate Student Portal"
66
  subtitle={`Welcome back, ${studentName}. Train hard, stay consistent.`}
 
68
  <div className="flex items-center gap-3">
69
  <button
70
  type="button"
71
+ onClick={() => setAiOpen(true)}
72
  className="inline-flex items-center gap-2 rounded-lg bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium px-4 py-2 shadow-sm"
73
  >
74
+ <Sparkles className="w-4 h-4" />
75
  Ask the Sensei
76
  </button>
77
  <UserMenu
78
  name={studentName}
79
  email={studentEmail}
80
+ onLogout={handleLogout}
81
  />
82
  </div>
83
  }
84
  />
85
 
86
+ <main className="max-w-6xl mx-auto px-4 py-6 sm:py-8 space-y-6">
 
87
  {/* Available plans */}
88
+ <section className="bg-white rounded-2xl shadow-sm border border-slate-200">
89
+ <div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
90
+ <h2 className="text-sm font-semibold text-slate-900">
91
+ Available membership plans
92
+ </h2>
93
+ <p className="text-xs text-slate-500">
94
+ Choose a plan and pay securely via Stripe.
 
 
 
 
 
95
  </p>
96
+ </div>
97
+ <div className="px-5 py-4">
98
+ {loading ? (
99
+ <p className="text-sm text-slate-500">Loading plans…</p>
100
+ ) : plans.length === 0 ? (
101
+ <p className="text-sm text-slate-500">
102
+ No public plans are available right now. Please check back later
103
+ or ask at the front desk.
104
+ </p>
105
+ ) : (
106
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
107
+ {plans.map((plan) => (
108
+ <div
109
+ key={plan.id}
110
+ className="bg-slate-50 border border-slate-200 rounded-xl px-4 py-4 flex flex-col justify-between"
111
+ >
112
+ <div className="space-y-1">
113
+ <h3 className="text-sm font-semibold text-slate-900">
114
+ {plan.name}
115
+ </h3>
116
+ {plan.description && (
117
+ <p className="text-xs text-slate-500">
118
+ {plan.description}
119
+ </p>
120
+ )}
121
+ <p className="text-sm font-medium text-slate-900 mt-2">
122
+ ${plan.price.toFixed(2)}{" "}
123
+ <span className="text-xs text-slate-500">
124
+ / {plan.billing_period}
125
+ </span>
126
+ </p>
127
+ </div>
128
+ {plan.stripe_link && (
129
+ <a
130
+ href={plan.stripe_link}
131
+ target="_blank"
132
+ rel="noreferrer"
133
+ className="mt-4 inline-flex justify-center items-center rounded-md bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium px-4 py-2"
134
+ >
135
+ Pay with Stripe
136
+ </a>
137
+ )}
138
+ </div>
139
+ ))}
140
+ </div>
141
+ )}
142
+ </div>
143
+ </section>
144
 
145
  {/* Rank / Next class / Attendance */}
146
+ <section className="grid grid-cols-1 sm:grid-cols-3 gap-4">
147
+ <div className="bg-white rounded-2xl shadow-sm border border-slate-200 px-5 py-4">
148
+ <p className="text-xs font-semibold text-slate-500 uppercase">
149
  Current rank
150
+ </p>
151
+ <p className="mt-2 text-base font-semibold text-slate-900">
152
+ White Belt
153
+ </p>
154
+ <p className="mt-1 text-xs text-slate-500">
155
  Progress updates will appear here.
156
  </p>
157
  </div>
158
 
159
+ <div className="bg-white rounded-2xl shadow-sm border border-slate-200 px-5 py-4">
160
+ <p className="text-xs font-semibold text-slate-500 uppercase">
161
  Next class
162
+ </p>
163
+ <p className="mt-2 text-base font-semibold text-slate-900">
164
  Tuesday, 6:00 PM
165
  </p>
166
+ <p className="mt-1 text-xs text-slate-500">
167
  Location: Main Dojo, Kamloops.
168
  </p>
169
  </div>
170
 
171
+ <div className="bg-white rounded-2xl shadow-sm border border-slate-200 px-5 py-4">
172
+ <p className="text-xs font-semibold text-slate-500 uppercase">
173
  Attendance
174
+ </p>
175
+ <p className="mt-2 text-base font-semibold text-slate-900">
176
  0 / 8 classes
177
  </p>
178
+ <p className="mt-1 text-xs text-slate-500">
179
+ This month&apos;s attendance summary.
180
  </p>
181
  </div>
182
+ </section>
183
 
184
+ {/* Upcoming renewals */}
185
+ <section className="bg-white rounded-2xl shadow-sm border border-slate-200 px-5 py-4">
186
+ <h2 className="text-sm font-semibold text-slate-900">
187
  Upcoming renewals &amp; payments
188
+ </h2>
189
+ <div className="mt-2 text-sm text-slate-600">
190
+ {upcomingRenewal ? (
191
+ <p>
192
+ Your next renewal is{" "}
193
+ <span className="font-semibold">
194
+ {upcomingRenewal.plan_name}
195
+ </span>{" "}
196
+ on{" "}
197
+ <span className="font-semibold">
198
+ {upcomingRenewal.renewal_date}
199
+ </span>
200
+ . Your membership status is{" "}
201
+ <span className="font-semibold">
202
+ {upcomingRenewal.status}
203
+ </span>
204
+ .
205
+ </p>
206
+ ) : (
207
+ <p>
208
+ Once you complete a Stripe payment for a membership, you&apos;ll
209
+ see your renewal date, plan, and payment details here.
210
+ </p>
211
+ )}
212
+ </div>
213
+ </section>
214
+
215
+ {/* Your memberships */}
216
+ <section className="bg-white rounded-2xl shadow-sm border border-slate-200">
217
+ <div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
218
+ <h2 className="text-sm font-semibold text-slate-900">
219
+ Your memberships
220
+ </h2>
221
+ <p className="text-xs text-slate-500">
222
+ {memberships.length} record{memberships.length === 1 ? "" : "s"}
223
  </p>
224
+ </div>
225
+ <div className="px-5 py-4 text-sm text-slate-700">
226
+ {loading ? (
227
+ <p className="text-slate-500">Loading memberships…</p>
228
+ ) : memberships.length === 0 ? (
229
+ <p className="text-slate-500">
230
+ You don&apos;t have an active membership yet. Pick a plan above
231
+ to get started.
232
+ </p>
233
+ ) : (
234
+ <div className="overflow-x-auto">
235
+ <table className="min-w-full text-sm">
236
+ <thead>
237
+ <tr className="text-left text-xs text-slate-500 uppercase border-b border-slate-100">
238
+ <th className="py-2 pr-4">Plan</th>
239
+ <th className="py-2 pr-4">Status</th>
240
+ <th className="py-2 pr-4">Started</th>
241
+ <th className="py-2 pr-4">Renewal</th>
242
+ <th className="py-2 pr-4">Price</th>
243
+ </tr>
244
+ </thead>
245
+ <tbody>
246
+ {memberships.map((m) => (
247
+ <tr key={m.id} className="border-b border-slate-50">
248
+ <td className="py-2 pr-4">{m.plan_name}</td>
249
+ <td className="py-2 pr-4">
250
+ <span className="inline-flex items-center rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-medium text-emerald-700">
251
+ {m.status}
252
+ </span>
253
+ </td>
254
+ <td className="py-2 pr-4">{m.start_date}</td>
255
+ <td className="py-2 pr-4">{m.renewal_date}</td>
256
+ <td className="py-2 pr-4">
257
+ ${m.price.toFixed(2)} / {m.billing_period}
258
+ </td>
259
+ </tr>
260
+ ))}
261
+ </tbody>
262
+ </table>
263
+ </div>
264
+ )}
265
+ </div>
266
+ </section>
267
+ </main>
268
+
269
+ <StudentAIAssistant open={aiOpen} onClose={() => setAiOpen(false)} />
270
  </div>
271
  );
272
  }