Skip to content

Commit 1c46fbc

Browse files
authored
Merge pull request #46 from Eric-Zhang-Developer/feat/profile-logic-and-design
Feat: Profile logic and design update
2 parents cf91afd + ec6e00b commit 1c46fbc

File tree

4 files changed

+221
-41
lines changed

4 files changed

+221
-41
lines changed

src/app/profile/page.tsx

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { Shield, Star, Trophy, Swords, Pencil } from "lucide-react";
1+
import { Shield, Star, Trophy, Swords } from "lucide-react";
22
import { createClient as createServerClient } from "../../lib/supabase/server";
33
import { redirect } from "next/navigation";
44
import { Cinzel } from "next/font/google";
5+
import EditClass from "../../components/edit-class";
6+
import BackToDashBoardLink from "../../components/back-to-dashboard-link";
57

68
//font for words
79
const cinzel = Cinzel({
@@ -19,6 +21,24 @@ export default async function ProfilePage() {
1921
redirect("/login");
2022
}
2123

24+
// Fetch user profile from database
25+
const { data: profile, error: profileError } = await supabase
26+
.from('profiles')
27+
.select('class')
28+
.eq('id', user.id)
29+
.maybeSingle(); // Use maybeSingle() instead of single() to handle case where profile doesn't exist
30+
31+
// Map enum values to display names
32+
const classDisplayMap: Record<string, string> = {
33+
'warrior': 'Python Warrior',
34+
'mage': 'Java Mage',
35+
'rogue': 'C++ Rogue',
36+
};
37+
38+
// Get class from database, default to null (will show as empty or default)
39+
const userClassEnum = profile?.class as string | null;
40+
const userClass = userClassEnum ? classDisplayMap[userClassEnum] || userClassEnum : null;
41+
2242
const username = user.email?.split('@')[0] || 'User';
2343

2444
const joinedDate = user.created_at
@@ -48,6 +68,9 @@ export default async function ProfilePage() {
4868

4969
{/* Content */}
5070
<div className={`relative ${cinzel.className}`} style={{ zIndex: 1 }}>
71+
{/* Back Button */}
72+
<BackToDashBoardLink />
73+
5174
{/* Header */}
5275
<div className="text-center mb-8">
5376
<h1 className="text-4xl font-bold mb-2" style={{ color: "#be9661" }}>
@@ -197,19 +220,6 @@ export default async function ProfilePage() {
197220
border: "0.5px solid rgba(190, 150, 97, 0.3)",
198221
}}
199222
>
200-
{/* Edit Profile Button */}
201-
<button
202-
className="absolute top-6 right-6 px-4 py-2 rounded-lg flex items-center gap-2 border"
203-
style={{
204-
borderColor: "rgba(190, 150, 97, 0.3)",
205-
color: "#E0E0E0",
206-
backgroundColor: "transparent",
207-
}}
208-
>
209-
<Pencil size={16} style={{ color: "#be9661" }} />
210-
<span>Edit Profile</span>
211-
</button>
212-
213223
{/* Username */}
214224
<h2 className="text-3xl font-bold mb-1" style={{ color: "#be9661" }}>
215225
{username}
@@ -236,7 +246,7 @@ export default async function ProfilePage() {
236246
Level
237247
</span>
238248
<span className="text-2xl font-bold" style={{ color: "#be9661" }}>
239-
42
249+
0
240250
</span>
241251
</div>
242252

@@ -255,7 +265,7 @@ export default async function ProfilePage() {
255265
XP
256266
</span>
257267
<span className="text-2xl font-bold" style={{ color: "#be9661" }}>
258-
12,847
268+
0
259269
</span>
260270
</div>
261271

@@ -274,32 +284,14 @@ export default async function ProfilePage() {
274284
Quests
275285
</span>
276286
<span className="text-2xl font-bold" style={{ color: "#be9661" }}>
277-
127
287+
0/8
278288
</span>
279289
</div>
280290
</div>
281291

282292
{/* Detailed Info */}
283293
{/* Class */}
284-
<div className="flex items-start gap-3 mb-4 relative">
285-
<div
286-
className="absolute left-0 top-0 bottom-0 rounded"
287-
style={{
288-
backgroundColor: "#be9661",
289-
width: "4px",
290-
marginTop: "-2px",
291-
marginBottom: "-2px",
292-
}}
293-
/>
294-
<div className="pl-3">
295-
<span className="text-sm block mb-1" style={{ color: "#A0A0A0" }}>
296-
Class
297-
</span>
298-
<span className="text-base block" style={{ color: "#E0E0E0" }}>
299-
Python Warrior
300-
</span>
301-
</div>
302-
</div>
294+
<EditClass currentClass={userClass || 'Not set'} currentClassEnum={userClassEnum} userId={user.id} />
303295

304296
{/* Guild */}
305297
<div className="flex items-start gap-3 mb-4 relative">
@@ -369,5 +361,4 @@ export default async function ProfilePage() {
369361
</div>
370362
</main>
371363
);
372-
}
373-
364+
}

src/components/back-to-dashboard-link.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@ export default function BackToDashBoardLink() {
55
return (
66
<Link
77
href="/dashboard"
8-
className="mb-4 inline-block text-indigo-600 hover:text-indigo-800 transition hover:scale-105 group"
8+
className="mb-6 inline-flex items-center gap-2 px-4 py-2 rounded-lg transition-opacity hover:opacity-80"
9+
style={{
10+
backgroundColor: "#232331",
11+
color: "#be9661",
12+
border: "0.5px solid rgba(190, 150, 97, 0.3)",
13+
}}
914
>
10-
<ArrowLeft className="inline-block w-5 h-5 mr-1" />
11-
<span className="text-sm font-medium">Back to Dashboard</span>
15+
<ArrowLeft size={18} />
16+
<span>Back to Dashboard</span>
1217
</Link>
1318
);
1419
}

src/components/edit-class.tsx

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
import { createClient } from "../lib/supabase/client";
5+
import { Pencil, Check, X } from "lucide-react";
6+
7+
// Map enum values to display names
8+
const CLASS_OPTIONS = [
9+
{ value: 'warrior', label: 'Python Warrior', comingSoon: false },
10+
{ value: 'mage', label: 'Java Mage', comingSoon: true },
11+
{ value: 'rogue', label: 'C++ Rogue', comingSoon: true },
12+
] as const;
13+
14+
interface EditClassProps {
15+
currentClass: string;
16+
currentClassEnum: string | null;
17+
userId: string;
18+
}
19+
20+
export default function EditClass({ currentClass, currentClassEnum, userId }: EditClassProps) {
21+
const [isEditing, setIsEditing] = useState(false);
22+
// Default to 'warrior' (Python Warrior) which is available
23+
const [selectedClassEnum, setSelectedClassEnum] = useState<string>(currentClassEnum || 'warrior');
24+
const [isSaving, setIsSaving] = useState(false);
25+
const supabase = createClient();
26+
27+
useEffect(() => {
28+
// If current class is coming soon or null, default to warrior (Python Warrior)
29+
const currentOption = CLASS_OPTIONS.find(opt => opt.value === currentClassEnum);
30+
if (!currentClassEnum || currentOption?.comingSoon) {
31+
setSelectedClassEnum('warrior');
32+
} else {
33+
setSelectedClassEnum(currentClassEnum);
34+
}
35+
}, [currentClassEnum]);
36+
37+
const handleSave = async () => {
38+
setIsSaving(true);
39+
try {
40+
// Use upsert to insert if profile doesn't exist, or update if it does
41+
const { data, error } = await supabase
42+
.from('profiles')
43+
.upsert({
44+
id: userId,
45+
class: selectedClassEnum
46+
}, {
47+
onConflict: 'id'
48+
})
49+
.select();
50+
51+
if (error) {
52+
console.error("Supabase error:", error);
53+
throw error;
54+
}
55+
56+
console.log("Class saved successfully:", data);
57+
setIsEditing(false);
58+
// Refresh the page to show updated class
59+
window.location.reload();
60+
} catch (error) {
61+
console.error("Error updating class:", error);
62+
alert(`Failed to update class: ${error instanceof Error ? error.message : 'Unknown error'}`);
63+
} finally {
64+
setIsSaving(false);
65+
}
66+
};
67+
68+
const handleCancel = () => {
69+
// Reset to current class or default to warrior if coming soon
70+
const currentOption = CLASS_OPTIONS.find(opt => opt.value === currentClassEnum);
71+
if (!currentClassEnum || currentOption?.comingSoon) {
72+
setSelectedClassEnum('warrior');
73+
} else {
74+
setSelectedClassEnum(currentClassEnum);
75+
}
76+
setIsEditing(false);
77+
};
78+
79+
if (isEditing) {
80+
return (
81+
<div className="flex items-start gap-3 mb-4 relative">
82+
<div
83+
className="absolute left-0 top-0 bottom-0 rounded"
84+
style={{
85+
backgroundColor: "#be9661",
86+
width: "4px",
87+
marginTop: "-2px",
88+
marginBottom: "-2px",
89+
}}
90+
/>
91+
<div className="pl-3 flex-1">
92+
<span className="text-sm block mb-2" style={{ color: "#A0A0A0" }}>
93+
Class
94+
</span>
95+
<select
96+
value={selectedClassEnum}
97+
onChange={(e) => setSelectedClassEnum(e.target.value)}
98+
className="w-full px-3 py-2 rounded-lg mb-2"
99+
style={{
100+
backgroundColor: "#232331",
101+
color: "#E0E0E0",
102+
border: "0.5px solid rgba(190, 150, 97, 0.3)",
103+
}}
104+
>
105+
{CLASS_OPTIONS.map((option) => (
106+
<option key={option.value} value={option.value}>
107+
{option.label}{option.comingSoon ? ' - Coming Soon' : ''}
108+
</option>
109+
))}
110+
</select>
111+
<div className="flex gap-2">
112+
<button
113+
onClick={handleSave}
114+
disabled={isSaving}
115+
className="px-3 py-1 rounded-lg flex items-center gap-1"
116+
style={{
117+
backgroundColor: "#be9661",
118+
color: "#191922",
119+
border: "none",
120+
}}
121+
>
122+
<Check size={14} />
123+
<span>{isSaving ? "Saving..." : "Save"}</span>
124+
</button>
125+
<button
126+
onClick={handleCancel}
127+
disabled={isSaving}
128+
className="px-3 py-1 rounded-lg flex items-center gap-1"
129+
style={{
130+
backgroundColor: "#232331",
131+
color: "#E0E0E0",
132+
border: "0.5px solid rgba(190, 150, 97, 0.3)",
133+
}}
134+
>
135+
<X size={14} />
136+
<span>Cancel</span>
137+
</button>
138+
</div>
139+
</div>
140+
</div>
141+
);
142+
}
143+
144+
return (
145+
<div className="flex items-start gap-3 mb-4 relative">
146+
<div
147+
className="absolute left-0 top-0 bottom-0 rounded"
148+
style={{
149+
backgroundColor: "#be9661",
150+
width: "4px",
151+
marginTop: "-2px",
152+
marginBottom: "-2px",
153+
}}
154+
/>
155+
<div className="pl-3 flex-1">
156+
<div className="flex items-center justify-between">
157+
<div>
158+
<span className="text-sm block mb-1" style={{ color: "#A0A0A0" }}>
159+
Class
160+
</span>
161+
<span className="text-base block" style={{ color: "#E0E0E0" }}>
162+
{currentClass || 'Not set'}
163+
</span>
164+
</div>
165+
<button
166+
onClick={() => setIsEditing(true)}
167+
className="p-1 rounded transition-opacity hover:opacity-80"
168+
style={{ color: "#be9661" }}
169+
title="Edit class"
170+
>
171+
<Pencil size={16} />
172+
</button>
173+
</div>
174+
</div>
175+
</div>
176+
);
177+
}

src/lib/types/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@ export type QuizData = {
99
id: string;
1010
questions: Question[];
1111
};
12+
13+
export type Profile = {
14+
id: string;
15+
class: string;
16+
created_at: string;
17+
updated_at: string;
18+
};

0 commit comments

Comments
 (0)