Skip to content

Commit 647b108

Browse files
merge will -> phillip. implements a profile view
1 parent f66d42b commit 647b108

28 files changed

+7476
-3
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,8 @@
99
"typescript.tsdk": "${workspaceFolder}/client/node_modules/typescript/lib",
1010
"shellcheck.ignorePatterns": {
1111
"**/.env*": true
12+
},
13+
"[typescriptreact]": {
14+
"editor.defaultFormatter": "vscode.typescript-language-features"
1215
}
1316
}

client/package-lock.json

Lines changed: 93 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"prepare": "cd .. && husky client/.husky"
1616
},
1717
"dependencies": {
18+
"@radix-ui/react-avatar": "^1.1.11",
1819
"@radix-ui/react-navigation-menu": "^1.2.14",
1920
"@radix-ui/react-separator": "^1.1.8",
2021
"@radix-ui/react-slot": "^1.2.4",

client/public/test.png

39.5 KB
Loading
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as AvatarPrimitive from "@radix-ui/react-avatar";
2+
import * as React from "react";
3+
4+
import { cn } from "@/lib/utils";
5+
6+
const Avatar = React.forwardRef<
7+
React.ElementRef<typeof AvatarPrimitive.Root>,
8+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
9+
>(({ className, ...props }, ref) => (
10+
<AvatarPrimitive.Root
11+
ref={ref}
12+
className={cn(
13+
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
14+
className,
15+
)}
16+
{...props}
17+
/>
18+
));
19+
Avatar.displayName = AvatarPrimitive.Root.displayName;
20+
21+
const AvatarImage = React.forwardRef<
22+
React.ElementRef<typeof AvatarPrimitive.Image>,
23+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
24+
>(({ className, ...props }, ref) => (
25+
<AvatarPrimitive.Image
26+
ref={ref}
27+
className={cn("aspect-square h-full w-full", className)}
28+
{...props}
29+
/>
30+
));
31+
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
32+
33+
const AvatarFallback = React.forwardRef<
34+
React.ElementRef<typeof AvatarPrimitive.Fallback>,
35+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
36+
>(({ className, ...props }, ref) => (
37+
<AvatarPrimitive.Fallback
38+
ref={ref}
39+
className={cn(
40+
"flex h-full w-full items-center justify-center rounded-full bg-muted",
41+
className,
42+
)}
43+
{...props}
44+
/>
45+
));
46+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
47+
48+
export { Avatar, AvatarFallback, AvatarImage };

client/src/hooks/userprofile.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
2+
3+
import api from "@/lib/api";
4+
5+
type UserProfile = {
6+
id: number;
7+
username: string;
8+
email: string;
9+
first_name: string;
10+
last_name: string;
11+
is_staff: boolean;
12+
is_superuser: boolean;
13+
profile: {
14+
id: number;
15+
profile_info: string;
16+
role: string;
17+
user: number;
18+
};
19+
};
20+
21+
export const useUserProfile = (
22+
args?: Omit<UseQueryOptions, "queryKey" | "queryFn">,
23+
) => {
24+
return useQuery({
25+
...args,
26+
queryKey: ["userProfile"],
27+
queryFn: async () => {
28+
const user_id = 1; /* for now here just put 1 to make sure we can still fetch before the login system is created" */
29+
const res = await api.get("/user/");
30+
const user_profile = res.data;
31+
const CurrentUser = user_profile.find(
32+
(u: UserProfile) => u.id === user_id,
33+
);
34+
35+
return CurrentUser;
36+
},
37+
});
38+
};

client/src/pages/edit.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { useEffect, useState } from "react";
2+
3+
import { Button } from "@/components/ui/button";
4+
import { useUserProfile } from "@/hooks/userprofile";
5+
import api from "@/lib/api";
6+
7+
import Layout from "./layout";
8+
9+
export default function EditProfile() {
10+
const { data, isLoading } = useUserProfile();
11+
const [username, setUsername] = useState("");
12+
const [email, setEmail] = useState("");
13+
const [bio, setBio] = useState("");
14+
15+
useEffect(() => {
16+
if (data) {
17+
setUsername(data.username);
18+
setEmail(data.email);
19+
setBio(data.profile.profile_info);
20+
}
21+
}, [data]);
22+
23+
const handleSave = async () => {
24+
try {
25+
await api.patch(`/user/profile/${data.profile.id}/`, {
26+
profile_info: bio,
27+
}); // 只改profile info用patch,暂时****因为ProfileSerializer的fields = "__all__"要求全部字段,但目前user, role没有传
28+
alert("Profile updated!");
29+
} catch (error) {
30+
console.error("Failed to update profile:", error);
31+
}
32+
};
33+
34+
return (
35+
<Layout>
36+
<div className="mx-auto mt-20 max-w-xl space-y-4">
37+
<h1 className="text-2xl font-bold">Edit Profile</h1>
38+
39+
{isLoading ? (
40+
<p>Loading...</p>
41+
) : (
42+
<>
43+
<div>
44+
<label>Username</label>
45+
<input
46+
type="text"
47+
value={username}
48+
onChange={(e) => setUsername(e.target.value)}
49+
className="w-full rounded border p-2"
50+
/>
51+
</div>
52+
53+
<div>
54+
<label>Email</label>
55+
<input
56+
type="email"
57+
value={email}
58+
onChange={(e) => setEmail(e.target.value)}
59+
className="w-full rounded border p-2"
60+
/>
61+
</div>
62+
63+
<div>
64+
<label>Bio</label>
65+
<textarea
66+
value={bio}
67+
onChange={(e) => setBio(e.target.value)}
68+
className="w-full rounded border p-2"
69+
rows={4}
70+
></textarea>
71+
</div>
72+
73+
<Button onClick={handleSave}>Save</Button>
74+
</>
75+
)}
76+
</div>
77+
</Layout>
78+
);
79+
}

client/src/pages/profile.tsx

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,76 @@
1+
import { useRouter } from "next/router";
2+
import { useEffect } from "react";
3+
4+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
5+
import { Badge } from "@/components/ui/badge";
6+
import { Button } from "@/components/ui/button";
7+
import { Card, CardContent, CardFooter } from "@/components/ui/card";
8+
import { Separator } from "@/components/ui/separator";
9+
import { useUserProfile } from "@/hooks/userprofile";
10+
111
import Layout from "./layout";
212

313
export default function Home() {
14+
const router = useRouter();
15+
const { data, isLoading } = useUserProfile();
16+
useEffect(() => {
17+
console.log("user profile:", data);
18+
}, [data]);
19+
420
return (
521
<Layout>
22+
<Card className="mx-auto mt-10 w-full max-w-2xl pb-10 pt-1">
23+
<CardContent>
24+
<div className="flex flex-col items-center space-y-3 text-center">
25+
<Avatar className="border-black-500 mt-20 h-32 w-32 rounded-full border-2">
26+
<AvatarImage src="/test.png" alt="Profile Picture" />
27+
<AvatarFallback>Profile Picture</AvatarFallback>
28+
</Avatar>
29+
<div className="UserName">
30+
<h1 className="mt-2 text-xl font-semibold">
31+
{isLoading ? "Loading" : data?.username}
32+
</h1>
33+
</div>
34+
<div className="UserEmail">
35+
<p className="text-sm text-gray-500">
36+
{isLoading ? "Loading" : data?.email}
37+
</p>
38+
</div>
39+
<div className="UserTags">
40+
<Badge className="Tag1 mt-2" variant="secondary">
41+
Outdoor Enthusiast
42+
</Badge>
43+
<Badge className="Tag2 ml-2 mt-2" variant="secondary">
44+
Anime Geek
45+
</Badge>
46+
<Badge className="Tag3 ml-2 mt-2" variant="secondary">
47+
Traveler
48+
</Badge>
49+
<Badge className="Tag4 ml-2 mt-2" variant="secondary">
50+
Gamer
51+
</Badge>
52+
</div>
53+
<div className="Separator w-full px-40 py-3">
54+
<Separator />
55+
</div>
56+
<div className="self-intro px-20">{data?.profile.profile_info}</div>
57+
</div>
58+
<CardFooter className="flex justify-center">
59+
<Button className="mt-20" onClick={() => router.push("/edit")}>
60+
Edit Profile
61+
</Button>
62+
</CardFooter>
63+
</CardContent>
64+
</Card>
65+
66+
{/*
667
<div className="p-6">
768
<h1 className="text-2xl font-bold">Profile</h1>
869
<p>Profile</p>
9-
</div>
70+
<p>{isLoading ? "Loading" : `Username: ${data?.username}`}</p>
71+
<p>{isLoading ? "Loading" : data?.email}</p>
72+
73+
</div> */}
1074
</Layout>
1175
);
1276
}

0 commit comments

Comments
 (0)