Working profile system
This commit is contained in:
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
@@ -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})$/
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user