Skip to content

Commit dc61b09

Browse files
shavonnegJajared
andauthored
Ms3 shavonne/profile management (#60)
* Add profile page with main components * Update Skills Component * Update attempted questions table * Update columns of question table * Update side contents to include topic preferences * Add EditProfile modal * Update edit profile modal to include profile image icon and add minor layout edits * Add overall padding to profile page --------- Co-authored-by: shavonneg <> Co-authored-by: Jajabonks <[email protected]>
1 parent e261989 commit dc61b09

21 files changed

+1176
-0
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"use client";
2+
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogDescription,
7+
DialogHeader,
8+
DialogTitle,
9+
} from "@/components/ui/dialog";
10+
11+
import React, { useCallback } from "react";
12+
import { zodResolver } from "@hookform/resolvers/zod";
13+
import { useForm } from "react-hook-form";
14+
import { z } from "zod";
15+
16+
import { Form, FormLabel } from "@/components/ui/form";
17+
import { Button } from "@/components/ui/button";
18+
import { TextInput } from "@/components/form/TextInput";
19+
import { RadioGroupInput } from "@/components/form/RadioGroupInput";
20+
import { useToast } from "@/hooks/use-toast";
21+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
22+
import { CodeXml } from "lucide-react";
23+
24+
interface EditProfileModalProps {
25+
isOpen: boolean;
26+
setIsOpen: (open: boolean) => void;
27+
userProfile: {
28+
displayName: string;
29+
username: string;
30+
email: string;
31+
proficiency: string;
32+
};
33+
}
34+
35+
const FormSchema = z.object({
36+
displayName: z.string().min(1, "Display Name is required"),
37+
username: z.string().min(1, "Username is required"),
38+
email: z.string().email("Invalid email format"),
39+
proficiency: z.enum(["Beginner", "Intermediate", "Expert"]),
40+
});
41+
42+
export function EditProfile({
43+
isOpen,
44+
setIsOpen,
45+
userProfile,
46+
}: EditProfileModalProps) {
47+
const { toast } = useToast();
48+
49+
const form = useForm<z.infer<typeof FormSchema>>({
50+
resolver: zodResolver(FormSchema),
51+
defaultValues: {
52+
displayName: userProfile.displayName,
53+
username: userProfile.username,
54+
email: userProfile.email,
55+
},
56+
});
57+
58+
const onSubmit = useCallback(
59+
async (data: z.infer<typeof FormSchema>) => {
60+
console.log("Form data:", data);
61+
toast({
62+
title: "Profile updated!",
63+
description: "Your profile has been updated successfully.",
64+
});
65+
setIsOpen(false);
66+
},
67+
[toast, setIsOpen]
68+
);
69+
70+
return (
71+
<Dialog open={isOpen} onOpenChange={setIsOpen}>
72+
<DialogContent className="max-w-md">
73+
<DialogHeader>
74+
<DialogTitle className="text-primary">Edit Profile</DialogTitle>
75+
<Form {...form}>
76+
<form
77+
onSubmit={form.handleSubmit(onSubmit)}
78+
className="flex flex-col space-y-4"
79+
>
80+
{/* Profile Image Upload */}
81+
<FormLabel className="pt-8">Profile Image</FormLabel>
82+
<div className="flex flex-row justify-center items-center p-2">
83+
<input
84+
type="file"
85+
className="hidden"
86+
id="profile-upload"
87+
accept="image/*"
88+
/>
89+
90+
<Avatar>
91+
<AvatarImage/>
92+
<AvatarFallback className="text-base font-normal text-foreground">
93+
<CodeXml/>
94+
</AvatarFallback>
95+
</Avatar>
96+
97+
<div className="pl-6">
98+
<label
99+
htmlFor="profile-upload"
100+
className="bg-background-200 text-sm rounded-lg font-bold p-2 cursor-pointer"
101+
>
102+
Upload Image
103+
</label>
104+
<DialogDescription className="pt-2">.png, .jpeg files up to 2MB. Recommended size is 256x256px.</DialogDescription>
105+
</div>
106+
</div>
107+
108+
{/* Display Name */}
109+
<TextInput label="Display Name" name="displayName" placeholder="Display Name" />
110+
111+
{/* Username */}
112+
<TextInput label="Username" name="username" placeholder="Username" />
113+
{form.formState.errors.username && (
114+
<p className="text-destructive text-sm">
115+
{form.formState.errors.username.message || "Username already taken"}
116+
</p>
117+
)}
118+
119+
{/* Email */}
120+
<TextInput label="Email" name="email" placeholder="Email" />
121+
122+
{/* Proficiency Radio Buttons */}
123+
<RadioGroupInput
124+
label="Proficiency"
125+
name="proficiency"
126+
options={[
127+
{ value: "Beginner", optionLabel: "Beginner" },
128+
{ value: "Intermediate", optionLabel: "Intermediate" },
129+
{ value: "Expert", optionLabel: "Expert" },
130+
]}
131+
/>
132+
133+
<Button className="p-5" type="submit">
134+
{form.formState.isSubmitting
135+
? "Updating Profile"
136+
: "Save"}
137+
</Button>
138+
</form>
139+
</Form>
140+
</DialogHeader>
141+
</DialogContent>
142+
</Dialog>
143+
);
144+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Card } from "@/components/ui/card";
2+
import CollaborationHeatmap from "./HeatMap";
3+
4+
export default function GitGraph() {
5+
const monthsData = [
6+
[1, 0, 0, 0, 1, 1, 0, 1, 0, 0], // January
7+
[0, 1, 1, 0, 0, 1, 0, 1, 0, 0], // February
8+
[1, 0, 0, 1, 0, 1, 1, 1, 0, 0], // March
9+
[1, 0, 0, 0, 1, 1, 0, 1, 0, 0], // April
10+
[0, 1, 1, 0, 0, 1, 0, 1, 0, 0], // May
11+
[1, 0, 0, 1, 0, 1, 1, 1, 0, 0], // June
12+
[1, 0, 0, 0, 1, 1, 0, 1, 0, 0], // July
13+
[0, 1, 1, 0, 0, 1, 0, 1, 0, 0], // August
14+
[1, 0, 0, 1, 0, 1, 1, 1, 0, 0], // Sept
15+
[1, 0, 0, 0, 1, 1, 0, 1, 0, 0], // Oct
16+
[0, 1, 1, 0, 0, 1, 0, 1, 0, 0], // Nov
17+
[1, 0, 0, 1, 0, 1, 1, 1, 0, 0], // Dec
18+
];
19+
20+
return (
21+
<Card className="flex flex-col rounded-md p-6 gap-4">
22+
<StatsHeader />
23+
<div className="flex flex-row">
24+
<StatsDetails/>
25+
<CollaborationHeatmap
26+
monthsData={monthsData}
27+
year={2024}
28+
/>
29+
</div>
30+
</Card>
31+
)
32+
}
33+
34+
function StatsHeader() {
35+
return (
36+
<div className="flex flex-row gap-2">
37+
<big className="font-bold">15</big>
38+
<big className="text-card-foreground-100"> collaborations done in the past one year</big>
39+
</div>
40+
)
41+
}
42+
function StatsDetails() {
43+
return (
44+
<div className="flex flex-col gap-2 pt-2 pl-2 pr-4">
45+
<div className="flex flex-col p-1">
46+
<small className="font-bold"> Total Active: </small>
47+
<small className="text-card-foreground-100"> 15 Days </small>
48+
</div>
49+
<div className="flex flex-col p-1">
50+
<small className="font-bold"> Max Streaks: </small>
51+
<small className="text-card-foreground-100"> 3 Days </small>
52+
</div>
53+
</div>
54+
)
55+
}
56+
57+
function MonthlyCommits({ month } : { month: string }) {
58+
59+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
interface CollaborationHeatmapProps {
2+
monthsData: Array<Array<number>>;
3+
year: number;
4+
}
5+
6+
export default function CollaborationHeatmap({
7+
monthsData,
8+
year,
9+
}: CollaborationHeatmapProps) {
10+
return (
11+
<section className="flex flex-row gap-8 rounded-lg max-w-4xl">
12+
<div className="grid grid-cols-12 gap-2 mt-4">
13+
{monthsData.map((month, monthIndex) => (
14+
<div key={monthIndex} className="flex flex-col items-center">
15+
<div className="grid grid-cols-3 gap-1">
16+
{month.map((active, dayIndex) => (
17+
<div
18+
key={dayIndex}
19+
className={`w-4 h-4 ${active ? "bg-primary" : "bg-background-100"}`}
20+
/>
21+
))}
22+
</div>
23+
<p className="text-sm text-card-foreground-100 pt-4">{getMonthName(monthIndex)}</p>
24+
</div>
25+
))}
26+
</div>
27+
28+
<div className="flex flex-col items-center">
29+
{[2024, 2023, 2022, 2021].map((yr) => (
30+
<button
31+
key={yr}
32+
className={`w-20 py-1 font-bold my-1 rounded-md ${year === yr ? "bg-primary" : "text-card-foreground-100"}`}
33+
>
34+
{yr}
35+
</button>
36+
))}
37+
</div>
38+
39+
</section>
40+
);
41+
}
42+
43+
// Convert month index to name
44+
function getMonthName(index: number): string {
45+
const months = [
46+
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
47+
"Jul", "Aug", "Sept", "Oct", "Nov", "Dec"
48+
];
49+
return months[index];
50+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import GitGraph from "./GitGraph";
2+
import { QuestionsStatsCard } from "@/app/dashboard/_components/QuestionsStatsCard";
3+
import { SkillsCard } from "./SkillsCard";
4+
import QuestionHistory from "./QuestionHistory/QuestionHistory";
5+
6+
export default function MainContent() {
7+
return (
8+
<div className="flex flex-col gap-4">
9+
<GitGraph />
10+
<h2 className="font-bold pb-2 pt-2">Collaborations</h2>
11+
<div className="flex flex-row gap-4">
12+
<QuestionHistory/>
13+
<div className="flex flex-col gap-4">
14+
<QuestionsStatsCard />
15+
<SkillsCard />
16+
</div>
17+
</div>
18+
19+
20+
</div>
21+
)
22+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Question } from "@/types/Question";
2+
import { ColumnDef } from "@tanstack/react-table";
3+
import UserAvatar from "@/components/UserAvatar";
4+
5+
const mockCollaborators = [
6+
{ id: 1, name: "Jm San Diego" },
7+
{ id: 2, name: "Charlie Brown"},
8+
];
9+
10+
const StatusColumn: ColumnDef<Question> = {
11+
accessorKey: "_id",
12+
header: () => <div className="px-4">Collaborators</div>,
13+
cell: () => {
14+
return (
15+
<div className="flex items-center space-x-0">
16+
{mockCollaborators.map((collaborator) => (
17+
<UserAvatar
18+
key={collaborator.id}
19+
src={"https://non-existent.com"}
20+
name={collaborator.name}
21+
isHoverEnabled={true}
22+
className="w-8 h-8"
23+
/>
24+
))}
25+
</div>
26+
);
27+
},
28+
};
29+
30+
export default StatusColumn;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Question, QuestionSchema } from "@/types/Question";
2+
import { ColumnDef } from "@tanstack/react-table";
3+
import { DataTableColumnHeader } from "../data-table-column-header";
4+
import { cn } from "@/lib/utils";
5+
6+
const DifficultyColumn: ColumnDef<Question> = {
7+
accessorKey: "difficulty",
8+
header: ({ column }) => {
9+
return (
10+
<DataTableColumnHeader
11+
column={column}
12+
title="Difficulty"
13+
className="w-1/4"
14+
/>
15+
);
16+
},
17+
cell: ({ row }) => {
18+
const question = QuestionSchema.parse(row.original);
19+
20+
return (
21+
<span
22+
className={cn(
23+
question.difficulty == "Easy" && "text-difficulty-easy",
24+
question.difficulty == "Medium" && "text-difficulty-medium",
25+
question.difficulty == "Hard" && "text-difficulty-hard"
26+
)}
27+
>
28+
{question.difficulty}
29+
</span>
30+
);
31+
},
32+
sortingFn: (rowA, rowB) => {
33+
const difficultyMap = {
34+
Easy: 0,
35+
Medium: 1,
36+
Hard: 2,
37+
};
38+
return (
39+
difficultyMap[rowA.original.difficulty] -
40+
difficultyMap[rowB.original.difficulty]
41+
);
42+
},
43+
filterFn: (row, key, value) => {
44+
return value.includes(row.getValue(key));
45+
},
46+
};
47+
48+
export default DifficultyColumn;

0 commit comments

Comments
 (0)