Working profile system

This commit is contained in:
2025-10-12 14:56:56 +02:00
parent ab84de8b03
commit 5f9bb4c877
8 changed files with 115 additions and 27 deletions
+53 -1
View File
@@ -1,6 +1,58 @@
import { StatusCodes } from "http-status-codes"; import { StatusCodes } from "http-status-codes";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { supabase } from "../supa"; import { supabase } from "../supa";
import { ProfileSettings } from "@/types/settings";
/**
* POST /api/profile
* body { username, display_name, avatar_url }
*/
export async function POST(req: NextRequest) {
const url = new URL(req.url);
const token = req.cookies.get("token")?.value;
const { username, display_name, avatar_url } =
(await req.json()) as ProfileSettings;
if (!token)
return NextResponse.json(
{ message: "Token cookie not found" },
{ status: StatusCodes.BAD_REQUEST }
);
if (!username || !display_name || !avatar_url)
return NextResponse.json(
{ message: "Invalid body, requires username, display_name, avatar_url" },
{ status: StatusCodes.BAD_REQUEST }
);
const {
data: { user },
} = await supabase.auth.getUser(token);
if (!user)
return NextResponse.json(
{ message: "Invalid token" },
{ status: StatusCodes.NOT_FOUND }
);
const { error } = await supabase
.from("profiles")
.update({
username,
display_name,
avatar_url,
})
.eq("id", user.id);
if (error) {
return NextResponse.json(
{ message: error.message },
{ status: StatusCodes.INTERNAL_SERVER_ERROR }
);
}
return NextResponse.json({ message: "ok" });
}
/** /**
* GET /api/profile?id=<id> * GET /api/profile?id=<id>
@@ -33,7 +85,7 @@ export async function GET(req: NextRequest) {
const { data: profile } = await supabase const { data: profile } = await supabase
.from("profiles") .from("profiles")
.select("id, username, name, email") .select("id, username, display_name, avatar_url")
.eq("id", user_id) .eq("id", user_id)
.maybeSingle(); .maybeSingle();
+12 -9
View File
@@ -1,19 +1,20 @@
"use client"; "use client";
import { useAccountSettings, useClientSettings } from "@/hooks/use-settings"; import { useProfileSettings, useClientSettings } from "@/hooks/use-settings";
import { setClientSettings } from "@/lib/clientSettings"; import { setClientSettings } from "@/lib/clientSettings";
import Settings from "./settings"; import Settings from "./settings";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { CardContent } from "@/components/ui/card"; import { CardContent } from "@/components/ui/card";
import { ACCOUNT_SCHEMA, CLIENT_SCHEMA } from "@/types/settings"; import { PROFILE_SCHEMA, CLIENT_SCHEMA } from "@/types/settings";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ChevronLeftCircle } from "lucide-react"; import { ChevronLeftCircle } from "lucide-react";
import axios from "axios";
export default function SettingsPage() { export default function SettingsPage() {
const [settings, setSettings] = useClientSettings(); const [settings, setSettings] = useClientSettings();
const [accountSettings, setAccountSettings] = useAccountSettings(); const [profileSettings, setProfileSettings] = useProfileSettings();
const router = useRouter(); const router = useRouter();
return ( return (
@@ -28,7 +29,7 @@ export default function SettingsPage() {
<Tabs className="mx-auto mt-3" defaultValue="client"> <Tabs className="mx-auto mt-3" defaultValue="client">
<TabsList className="mx-auto"> <TabsList className="mx-auto">
<TabsTrigger value="client">Client Settings</TabsTrigger> <TabsTrigger value="client">Client Settings</TabsTrigger>
<TabsTrigger value="account">Account Settings</TabsTrigger> <TabsTrigger value="profile">Profile Settings</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="client"> <TabsContent value="client">
<Card className="w-xl mx-auto"> <Card className="w-xl mx-auto">
@@ -45,14 +46,16 @@ export default function SettingsPage() {
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>
<TabsContent value="account"> <TabsContent value="profile">
<Card className="w-xl mx-auto"> <Card className="w-xl mx-auto">
<CardContent className="flex flex-col gap-3"> <CardContent className="flex flex-col gap-3">
<Settings <Settings
settings={accountSettings} settings={profileSettings}
setSettings={setAccountSettings} setSettings={setProfileSettings}
schema={ACCOUNT_SCHEMA} schema={PROFILE_SCHEMA}
onSave={() => {}} onSave={async () => {
await axios.post("/api/profile", profileSettings);
}}
/> />
</CardContent> </CardContent>
</Card> </Card>
+3 -2
View File
@@ -44,16 +44,17 @@ function MessageContainer({
<div className="flex gap-2"> <div className="flex gap-2">
{/* <div className="w-8 h-8 rounded-full bg-gray-400" /> */} {/* <div className="w-8 h-8 rounded-full bg-gray-400" /> */}
<ProfilePicture <ProfilePicture
url={userList[message.from]?.avatar_url}
name={String( name={String(
userList[message.from] userList[message.from]
? userList[message.from].username ? userList[message.from].display_name
: message.from : message.from
)} )}
/> />
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1">
<span className="font-bold flex gap-1 items-center"> <span className="font-bold flex gap-1 items-center">
{userList[message.from] {userList[message.from]
? userList[message.from].username ? userList[message.from].display_name
: message.from} : message.from}
<p className="text-neutral-500 text-xs"> <p className="text-neutral-500 text-xs">
+8 -2
View File
@@ -1,9 +1,15 @@
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
export default function ProfilePicture({ name }: { name: string }) { export default function ProfilePicture({
name,
url,
}: {
name: string;
url: string;
}) {
return ( return (
<Avatar className="h-8 w-8"> <Avatar className="h-8 w-8">
<AvatarImage src="https://github.com/kleo-dev.png" alt={name} /> <AvatarImage src={url} alt={name} />
<AvatarFallback>{name[0]}</AvatarFallback> <AvatarFallback>{name[0]}</AvatarFallback>
</Avatar> </Avatar>
); );
+1 -1
View File
@@ -56,7 +56,7 @@ export default function Login() {
</CardHeader> </CardHeader>
<CardContent className="gap-3 flex flex-col"> <CardContent className="gap-3 flex flex-col">
<span> <span>
Username or email Email
<Input onChange={(e) => setUsername(e.target.value)} type="email" /> <Input onChange={(e) => setUsername(e.target.value)} type="email" />
</span> </span>
<span> <span>
+3 -2
View File
@@ -6,9 +6,10 @@ import { useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export interface User { export interface User {
id: number; id: string;
username: string; username: string;
email: string; display_name: string;
avatar_url: string;
} }
export default function useUser() { export default function useUser() {
+25 -6
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { defaultSettings, getClientSettings } from "@/lib/clientSettings"; import { defaultSettings, getClientSettings } from "@/lib/clientSettings";
import { AccountSettings, ClientSettings } from "@/types/settings"; import { ProfileSettings, ClientSettings } from "@/types/settings";
import { Dispatch, SetStateAction, useEffect, useState } from "react"; import { Dispatch, SetStateAction, useEffect, useState } from "react";
export function useClientSettings(): [ export function useClientSettings(): [
@@ -17,13 +17,32 @@ export function useClientSettings(): [
return [settings, setSettings]; return [settings, setSettings];
} }
export function useAccountSettings(): [ export function useProfileSettings(): [
AccountSettings, ProfileSettings,
Dispatch<SetStateAction<AccountSettings>> Dispatch<SetStateAction<ProfileSettings>>
] { ] {
const [settings, setSettings] = useState<AccountSettings>({ const [settings, setSettings] = useState<ProfileSettings>({
node_ip: "node0.voxa.org", node_address: "",
username: "",
display_name: "",
avatar_url: "",
}); });
useEffect(() => {
async function fetchProfileSettings() {
try {
const res = await fetch("/api/profile");
if (res.ok) {
const data = (await res.json()) as ProfileSettings;
setSettings(data);
}
} catch (error) {
console.error("Failed to fetch profile settings:", error);
}
}
fetchProfileSettings();
}, []);
return [settings, setSettings]; return [settings, setSettings];
} }
+10 -4
View File
@@ -4,16 +4,22 @@ export interface ClientSettings {
theme: "light" | "dark"; theme: "light" | "dark";
} }
export interface AccountSettings { export interface ProfileSettings {
node_ip: string; username: string;
display_name: string;
avatar_url: string;
node_address: string;
} }
export const CLIENT_SCHEMA: SettingsSchema = { export const CLIENT_SCHEMA: SettingsSchema = {
theme: ["light", "dark"], theme: ["light", "dark"],
}; };
export const ACCOUNT_SCHEMA: SettingsSchema = { export const PROFILE_SCHEMA: SettingsSchema = {
node_ip: new RegexSetting( username: "string",
display_name: "string",
avatar_url: "string",
node_address: new RegexSetting(
"node2.voxa.org", "node2.voxa.org",
/^(?:(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(?:\d{1,3}\.){3}\d{1,3})$/ /^(?:(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(?:\d{1,3}\.){3}\d{1,3})$/
), ),