Merge pull request #21 from kleo-dev/ui-ux-patch

UI ux patch
This commit is contained in:
Klesti Selimaj
2025-10-17 12:45:12 +02:00
committed by GitHub
17 changed files with 321 additions and 141 deletions
+8 -1
View File
@@ -19,7 +19,14 @@ export async function POST(req: NextRequest) {
{ status: StatusCodes.BAD_REQUEST } { status: StatusCodes.BAD_REQUEST }
); );
if (!username || !display_name || !avatar_url) if (
!username ||
!display_name ||
avatar_url === undefined ||
avatar_url === null ||
node_address === undefined ||
node_address === null
)
return NextResponse.json( return NextResponse.json(
{ {
message: message:
+19 -30
View File
@@ -5,10 +5,10 @@ import { supabase } from "../supa";
/** /**
* POST /api/user * POST /api/user
* Register new user * Register new user
* Body: { name, username, email, password } * Body: { username, email, password }
*/ */
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
const { name, username, email, password } = await req.json(); const { username, email, password } = await req.json();
if (!email || !password || !username) if (!email || !password || !username)
return NextResponse.json( return NextResponse.json(
@@ -28,29 +28,35 @@ export async function POST(req: NextRequest) {
{ status: StatusCodes.CONFLICT } { status: StatusCodes.CONFLICT }
); );
const { data: authData, error: authError } = const { data: authData, error: authError } = await supabase.auth.signUp({
await supabase.auth.admin.createUser({
email, email,
password, password,
email_confirm: true,
user_metadata: { name, username },
}); });
if (authError) if (authError)
return NextResponse.json( return NextResponse.json(
{ message: authError.message }, { message: authError.message },
{ status: StatusCodes.BAD_REQUEST } { status: StatusCodes.INTERNAL_SERVER_ERROR }
);
if (!authData.user)
return NextResponse.json(
{ message: "User is null" },
{ status: StatusCodes.INTERNAL_SERVER_ERROR }
); );
await supabase.from("profiles").insert({ await supabase.from("profiles").insert({
id: authData.user.id, id: authData.user.id,
name,
username, username,
display_name: username,
avatar_url: "",
node_address: "",
}); });
return NextResponse.json({ return NextResponse.json({
message: "ok", message: "User registered. Please check your email to verify your account.",
user_id: authData.user.id, user_id: authData.user?.id,
email_sent: true,
}); });
} }
@@ -61,30 +67,14 @@ export async function POST(req: NextRequest) {
*/ */
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
const params = new URL(req.url).searchParams; const params = new URL(req.url).searchParams;
const [username, password] = [params.get("username"), params.get("password")]; const [email, password] = [params.get("email"), params.get("password")];
if (!username || !password) if (!email || !password)
return NextResponse.json( return NextResponse.json(
{ message: "Params username and password are required" }, { message: "Params email and password are required" },
{ status: StatusCodes.BAD_REQUEST } { status: StatusCodes.BAD_REQUEST }
); );
let email = username;
if (!username.includes("@")) {
const { data } = await supabase
.from("profiles")
.select("email")
.eq("email", username)
.maybeSingle();
email = data?.email;
}
if (!email)
return NextResponse.json(
{ message: "Invalid username" },
{ status: StatusCodes.UNAUTHORIZED }
);
const { data, error } = await supabase.auth.signInWithPassword({ const { data, error } = await supabase.auth.signInWithPassword({
email, email,
password, password,
@@ -104,7 +94,6 @@ export async function GET(req: NextRequest) {
user_id: session?.user?.id, user_id: session?.user?.id,
}); });
// Optionally set cookie
res.cookies.set("token", session?.access_token ?? "", { httpOnly: true }); res.cookies.set("token", session?.access_token ?? "", { httpOnly: true });
return res; return res;
+40
View File
@@ -0,0 +1,40 @@
import { NextRequest, NextResponse } from "next/server";
import { supabase } from "../supa";
import { StatusCodes } from "http-status-codes";
export async function GET(req: NextRequest) {
const email = req.nextUrl.searchParams.get("email");
const password = req.nextUrl.searchParams.get("password");
if (!email || !password)
return NextResponse.json(
{ message: "Missing email or password" },
{ status: StatusCodes.BAD_REQUEST }
);
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error)
return NextResponse.json(
{ message: error.message },
{ status: StatusCodes.INTERNAL_SERVER_ERROR }
);
if (!data)
return NextResponse.json(
{ verified: false, message: "User not found" },
{ status: StatusCodes.NOT_FOUND }
);
const res = NextResponse.json({
verified: !!data.user.email_confirmed_at,
user_id: data.user.id,
access_token: data.session.access_token,
refresh_token: data.session.refresh_token,
});
return res;
}
+5 -1
View File
@@ -2,16 +2,20 @@
import AppSidebar from "@/components/Sidebar"; import AppSidebar from "@/components/Sidebar";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useIsMobile } from "@/hooks/is-mobile";
import useApp from "@/hooks/use-app"; import useApp from "@/hooks/use-app";
export default function Chat() { export default function Chat() {
const app = useApp(); const app = useApp();
const isMobile = useIsMobile();
return ( return (
<AppSidebar app={app}> <AppSidebar app={app}>
<Card className="bg-background w-full"> <Card className="bg-background w-full">
<CardHeader> <CardHeader>
<CardTitle>Direct Messages</CardTitle> <CardTitle onClick={() => isMobile && app.setSidebarOpen(true)}>
Direct Messages
</CardTitle>
</CardHeader> </CardHeader>
<CardContent></CardContent> <CardContent></CardContent>
</Card> </Card>
+74
View File
@@ -0,0 +1,74 @@
"use client";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import Cookies from "js-cookie";
import axios from "axios";
export default function Verify() {
const [email, setEmail] = useState<string>();
const router = useRouter();
const [verified, setVerified] = useState(false);
useEffect(() => {
const email = Cookies.get("verify_temp_email");
setEmail(email);
const password = Cookies.get("verify_temp_password");
const i = setInterval(async () => {
const { data } = await axios.get("/api/verify", {
params: { email, password },
});
const response = data as {
verified: boolean;
user_id: string;
access_token?: string;
refresh_token?: string;
};
if (response.verified) {
clearInterval(i);
if (response.access_token) {
setVerified(true);
toast.info("Email verified, redirecting...");
Cookies.set("token", response.access_token);
Cookies.remove("verify_temp_email");
Cookies.remove("verify_temp_password");
setTimeout(() => router.push("/chat"), 500);
} else {
console.log(response);
}
}
}, 8000);
}, []);
return (
<div className="h-screen flex items-center">
<Card className="w-md m-auto">
<CardHeader>
<CardTitle className="text-3xl mx-auto">Verify Email</CardTitle>
<CardDescription>
{verified ? (
"Successfully verified"
) : (
<>
We sent you a verification email at {email}. Please verify to
continue.
</>
)}
</CardDescription>
</CardHeader>
<CardContent className="gap-3 flex flex-col">
Current status: {verified ? "Verified" : "Unverified"}
</CardContent>
</Card>
</div>
);
}
+67 -12
View File
@@ -10,8 +10,8 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "./ui/dropdown-menu"; } from "./ui/dropdown-menu";
import { ChevronLeftIcon, SmilePlusIcon } from "lucide-react"; import { ChevronLeftIcon, SendIcon, SmilePlusIcon } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { format, isToday, isYesterday } from "date-fns"; import { format, isToday, isYesterday } from "date-fns";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
@@ -19,6 +19,8 @@ import { UserProfile } from "@/hooks/get-user";
import axios from "axios"; import axios from "axios";
import ProfilePicture from "./ProfilePicture"; import ProfilePicture from "./ProfilePicture";
import App from "@/types/app"; import App from "@/types/app";
import { useIsMobile } from "@/hooks/use-mobile";
import { Textarea } from "./ui/textarea";
function MessageContainer({ message, app }: { message: Message; app: App }) { function MessageContainer({ message, app }: { message: Message; app: App }) {
useEffect(() => { useEffect(() => {
@@ -41,14 +43,14 @@ function MessageContainer({ message, app }: { message: Message; app: App }) {
url={app.profiles[message.from]?.avatar_url} url={app.profiles[message.from]?.avatar_url}
name={String( name={String(
app.profiles[message.from] app.profiles[message.from]
? app.profiles[message.from].display_name ? app.profiles[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">
{app.profiles[message.from] {app.profiles[message.from]
? app.profiles[message.from].display_name ? app.profiles[message.from]?.display_name
: message.from} : message.from}
<p className="text-neutral-500 text-xs"> <p className="text-neutral-500 text-xs">
@@ -142,13 +144,15 @@ export default function MessageBox({
messages: Message[]; messages: Message[];
}) { }) {
const [text, setText] = useState(""); const [text, setText] = useState("");
const isMobile = useIsMobile();
const inputRef = useRef<any>(null);
return ( return (
<div className="h-svh w-full max-h-svh flex flex-col pb-5 pl-5 gap-5"> <div className="h-svh w-full max-h-svh flex flex-col pb-5 pl-5 gap-5">
{channelName && ( {channelName && (
<header className="h-12 py-4 flex items-center border-b text-sm font-semibold"> <header className="h-12 py-4 flex items-center border-b text-sm font-semibold">
<span <span
onClick={() => app.setSidebarOpen(true)} onClick={() => isMobile && app.setSidebarOpen(true)}
className="cursor-pointer flex" className="cursor-pointer flex"
> >
<ChevronLeftIcon className="w-5 h-5" /> <ChevronLeftIcon className="w-5 h-5" />
@@ -156,27 +160,70 @@ export default function MessageBox({
</span> </span>
</header> </header>
)} )}
<div className="flex flex-col-reverse overflow-y-scroll"> <div className="mt-auto flex flex-col-reverse overflow-y-scroll">
{messages.toReversed().map((msg) => ( {messages.toReversed().map((msg) => (
<MessageContainer key={msg.id} message={msg} app={app} /> <MessageContainer key={msg.id} message={msg} app={app} />
))} ))}
</div> </div>
<footer className="flex gap-3 pr-5 w-full mt-auto"> <footer className="flex gap-3 pr-5 w-full h-max">
<Input {text.includes("\n") ? (
className="" <Textarea
ref={inputRef}
className="resize-none"
placeholder="Type a message..." placeholder="Type a message..."
value={text} value={text}
onChange={(e) => {
if (!e.target.value.includes("\n") && text.includes("\n")) {
setTimeout(() => {
if (inputRef.current) {
inputRef.current.selectionStart = text.length + 1;
inputRef.current.selectionEnd = text.length + 1;
inputRef.current.focus();
}
}, 10);
}
setText(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (!e.shiftKey) {
e.preventDefault();
sendMessage(text);
setText("");
}
}
}}
/>
) : (
<Input
placeholder="Type a message..."
ref={inputRef}
value={text}
onChange={(e) => setText(e.target.value)} onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
sendMessage(text); if (e.shiftKey) {
setText((p) => p + "\n");
setTimeout(() => {
if (inputRef.current) {
inputRef.current.selectionStart = text.length + 1;
inputRef.current.selectionEnd = text.length + 1;
inputRef.current.focus();
}
}, 10);
} else {
sendMessage(text.trim());
setText(""); setText("");
} }
}
}} }}
/> />
)}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="secondary"> <Button variant="secondary" className="hidden md:block">
<SmilePlusIcon /> <SmilePlusIcon />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -184,10 +231,18 @@ export default function MessageBox({
<DropdownMenuContent className=""> <DropdownMenuContent className="">
<EmojiPicker <EmojiPicker
onEmojiClick={(e) => setText(text + e.emoji)} onEmojiClick={(e) => setText(text + e.emoji)}
theme={emoji.Theme.DARK} theme={
app.clientSettings.theme === "dark"
? emoji.Theme.DARK
: emoji.Theme.LIGHT
}
/> />
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<Button variant="secondary">
<SendIcon />
</Button>
</footer> </footer>
</div> </div>
); );
+1 -1
View File
@@ -5,7 +5,7 @@ export default function ProfilePicture({
url, url,
}: { }: {
name: string; name: string;
url: string; url: string | undefined;
}) { }) {
return ( return (
<Avatar className="h-8 w-8"> <Avatar className="h-8 w-8">
+20 -9
View File
@@ -36,6 +36,7 @@ import Link from "next/link";
import { get, Response } from "@/lib/request"; import { get, Response } from "@/lib/request";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import App from "@/types/app"; import App from "@/types/app";
import { ProfileSettings } from "@/types/settings";
export default function AppSidebar({ export default function AppSidebar({
children, children,
@@ -106,9 +107,7 @@ export default function AppSidebar({
]; ];
setServers(updatedServers); setServers(updatedServers);
Cookies.set("servers", updatedServers.map((x) => x.join("@")).join(","), { Cookies.set("servers", updatedServers.map((x) => x.join("@")).join(","));
expires: 7,
});
setNewServer({ name: "", ip: "" }); setNewServer({ name: "", ip: "" });
}; };
@@ -203,7 +202,7 @@ export default function AppSidebar({
</Dialog> </Dialog>
<footer className="mt-auto"> <footer className="mt-auto">
<SettingsDialog /> <SettingsDialog profile={app.profile as ProfileSettings} />
</footer> </footer>
</div> </div>
@@ -245,6 +244,7 @@ export default function AppSidebar({
?.avatar_url || "" ?.avatar_url || ""
} }
key={id} key={id}
app={app}
/> />
))} ))}
</> </>
@@ -258,6 +258,7 @@ export default function AppSidebar({
id={app.profile?.id || "me"} id={app.profile?.id || "me"}
avatar={app.profile?.avatar_url || ""} avatar={app.profile?.avatar_url || ""}
status="online" status="online"
app={app}
settings settings
/> />
</footer> </footer>
@@ -268,7 +269,7 @@ export default function AppSidebar({
className="w-full h-full overflow-x-hidden" className="w-full h-full overflow-x-hidden"
onClick={() => app.setSidebarOpen(false)} onClick={() => app.setSidebarOpen(false)}
> >
<div className="overflow-x-hidden min-w-max flex-shrink-0"> <div className="overflow-x-hidden min-w-screen flex-shrink-0">
{children} {children}
</div> </div>
</div> </div>
@@ -285,12 +286,14 @@ function DMItem({
avatar, avatar,
status, status,
settings, settings,
app,
}: { }: {
name: string; name: string;
id: string; id: string;
avatar: string; avatar: string;
status: string; status: string;
settings?: boolean; settings?: boolean;
app: App;
}) { }) {
return ( return (
<Link <Link
@@ -306,6 +309,7 @@ function DMItem({
<SettingsDialog <SettingsDialog
className="ml-auto w-7 h-7 p-0 flex items-center justify-center" className="ml-auto w-7 h-7 p-0 flex items-center justify-center"
tab="profile" tab="profile"
profile={app.profile as ProfileSettings}
/> />
)} )}
</Link> </Link>
@@ -334,10 +338,12 @@ function ChannelIcon({ kind }: { kind: "text" | "voice" }) {
function getUser( function getUser(
id: string, id: string,
userList: StringMap<UserProfile>, userList: StringMap<UserProfile | null>,
setUserList: React.Dispatch<React.SetStateAction<StringMap<UserProfile>>> setUserList: React.Dispatch<
React.SetStateAction<StringMap<UserProfile | null>>
>
) { ) {
if (userList[id]) return userList[id]; if (userList[id] !== undefined) return userList[id];
const onResponse = (res: Response<UserProfile>) => { const onResponse = (res: Response<UserProfile>) => {
setUserList((prev) => { setUserList((prev) => {
@@ -348,5 +354,10 @@ function getUser(
get(`/api/profile/?id=${id}`, onResponse) get(`/api/profile/?id=${id}`, onResponse)
?.then(onResponse) ?.then(onResponse)
.catch((e) => {}); .catch((e) => {
setUserList((prev) => {
prev[id] = null;
return prev;
});
});
} }
+29 -9
View File
@@ -4,11 +4,12 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import axios from "axios"; import axios from "axios";
import { useState } from "react"; import { useState } from "react";
import Cookies from "js-cookie";
import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { CardContent, CardHeader, CardTitle } from "../ui/card"; import { CardContent, CardHeader, CardTitle } from "../ui/card";
import { Label } from "../ui/label"; import { Label } from "../ui/label";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import Cookies from "js-cookie";
export default function CreateAccount() { export default function CreateAccount() {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
@@ -20,7 +21,6 @@ export default function CreateAccount() {
const [feedback, setFeedback] = useState< const [feedback, setFeedback] = useState<
undefined | { kind: "error" | "info"; message: string } undefined | { kind: "error" | "info"; message: string }
>(); >();
const router = useRouter(); const router = useRouter();
const validateInput = (): const validateInput = ():
@@ -36,7 +36,7 @@ export default function CreateAccount() {
return { return {
kind: "error", kind: "error",
message: message:
"Username must be 3–20 characters and only letters, numbers, or underscores.", "Username must be 3-20 characters and only letters, numbers, or underscores.",
}; };
} }
if (password.length < 6) { if (password.length < 6) {
@@ -67,11 +67,31 @@ export default function CreateAccount() {
try { try {
setLoading(true); setLoading(true);
const res = await axios.post("/api/user", { username, password, email }); const { data } = await axios.post("/api/user", {
Cookies.set("token", (res.data as any).token); username,
password,
setFeedback({ kind: "info", message: "Account created! Redirecting..." }); email,
setTimeout(() => router.push("/chat"), 1000); });
const response = data as {
message: string;
email_sent: boolean;
user_id: string;
};
if (response.email_sent) {
toast.info(
"We sent you a verification email. Please verify to continue."
);
setFeedback({
message:
"We sent you a verification email. Please verify to continue.",
kind: "info",
});
Cookies.set("verify_temp_email", email);
Cookies.set("verify_temp_password", password);
router.push(`/verify`);
} else {
setFeedback({ message: response.message, kind: "error" });
}
} catch (err: any) { } catch (err: any) {
setFeedback({ setFeedback({
kind: "error", kind: "error",
+4 -12
View File
@@ -6,18 +6,10 @@ import axios from "axios";
import { useState } from "react"; import { useState } from "react";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Label } from "@/components/ui/label"; import { CardContent, CardHeader, CardTitle } from "../ui/card";
import Link from "next/link";
import {
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../ui/card";
export default function Login() { export default function Login() {
const [username, setUsername] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [feedback, setFeedback] = useState< const [feedback, setFeedback] = useState<
undefined | { kind: "error" | "info"; message: string } undefined | { kind: "error" | "info"; message: string }
@@ -29,7 +21,7 @@ export default function Login() {
setFeedback(undefined); setFeedback(undefined);
try { try {
const res = await axios.get("/api/user", { const res = await axios.get("/api/user", {
params: { username, password }, params: { email, password },
}); });
Cookies.set("token", (res.data as any).token); Cookies.set("token", (res.data as any).token);
@@ -57,7 +49,7 @@ export default function Login() {
<CardContent className="gap-3 flex flex-col"> <CardContent className="gap-3 flex flex-col">
<span> <span>
Email Email
<Input onChange={(e) => setUsername(e.target.value)} type="email" /> <Input onChange={(e) => setEmail(e.target.value)} type="email" />
</span> </span>
<span> <span>
Password Password
+5 -5
View File
@@ -1,6 +1,5 @@
"use client"; "use client";
import { useClientSettings } from "@/hooks/use-settings";
import { setClientSettings } from "@/lib/clientSettings"; import { setClientSettings } from "@/lib/clientSettings";
import SettingSchema from "./SettingSchema"; import SettingSchema from "./SettingSchema";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -11,19 +10,20 @@ import {
CLIENT_SCHEMA, CLIENT_SCHEMA,
ProfileSettings, ProfileSettings,
} from "@/types/settings"; } from "@/types/settings";
import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useRouter } from "next/navigation";
import axios from "axios"; import axios from "axios";
import { useClientSettings } from "@/hooks/use-settings";
import { useState } from "react";
export default function Settings({ export default function Settings({
tab, tab,
profileSettings, profileSettings: ps,
setProfileSettings,
}: { }: {
tab?: string; tab?: string;
profileSettings: ProfileSettings; profileSettings: ProfileSettings;
setProfileSettings: React.Dispatch<React.SetStateAction<ProfileSettings>>;
}) { }) {
const [settings, setSettings] = useClientSettings(); const [settings, setSettings] = useClientSettings();
const [profileSettings, setProfileSettings] = useState<ProfileSettings>(ps);
const router = useRouter(); const router = useRouter();
return ( return (
+4 -10
View File
@@ -3,7 +3,6 @@ import { Button } from "../ui/button";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
@@ -11,18 +10,17 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import Settings from "./Settings"; import Settings from "./Settings";
import { ProfileSettings } from "@/types/settings"; import { ProfileSettings } from "@/types/settings";
import { useProfileSettings } from "@/hooks/use-settings"; import { useState } from "react";
export default function SettingsDialog({ export default function SettingsDialog({
className, className,
tab, tab,
profile,
}: { }: {
className?: string; className?: string;
tab?: string; tab?: string;
profile?: ProfileSettings; profile: ProfileSettings;
}) { }) {
const [profileSettings, setProfileSettings] = useProfileSettings();
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -41,11 +39,7 @@ export default function SettingsDialog({
</DialogHeader> </DialogHeader>
<div className="w-full"> <div className="w-full">
<Settings <Settings tab={tab} profileSettings={profile} />
tab={tab}
profileSettings={profileSettings}
setProfileSettings={setProfileSettings}
/>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
-6
View File
@@ -17,12 +17,6 @@ export default function useUser() {
useEffect(() => { useEffect(() => {
async function fetchUser() { async function fetchUser() {
const token = Cookies.get("token");
if (!token) {
router.push("/login");
return;
}
try { try {
setUser((await get("/api/profile")).data); setUser((await get("/api/profile")).data);
} catch (e: any) { } catch (e: any) {
+20
View File
@@ -0,0 +1,20 @@
import { useEffect, useState } from "react";
export function useIsMobile() {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(max-width: 767px)");
const handleChange = () => setIsMobile(mediaQuery.matches);
// Set initial value
handleChange();
// Listen for changes
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, []);
return isMobile;
}
+6 -2
View File
@@ -3,16 +3,20 @@ import { useRef, useState } from "react";
import { useMessages } from "./use-messages"; import { useMessages } from "./use-messages";
import { StringMap } from "@/types/typeUtils"; import { StringMap } from "@/types/typeUtils";
import useUser, { UserProfile } from "./get-user"; import useUser, { UserProfile } from "./get-user";
import { useClientSettings } from "./use-settings";
export default function useApp(): App { export default function useApp(): App {
const node = useRef<WebSocket | null>(null); const node = useRef<WebSocket | null>(null);
const [profiles, setProfiles] = useState<StringMap<UserProfile>>({}); const [profiles, setProfiles] = useState<StringMap<UserProfile | null>>({});
const { messages, addMessage } = useMessages(); const { messages, addMessage } = useMessages();
const [sidebarOpen, setSidebarOpen] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(false);
const profile = useUser(); const profile = useUser();
const [clientSettings, setClientSettings] = useClientSettings();
return { return {
node, node,
clientSettings,
setClientSettings,
profile, profile,
profiles, profiles,
setProfiles, setProfiles,
+1 -29
View File
@@ -1,8 +1,7 @@
"use client"; "use client";
import { defaultSettings, getClientSettings } from "@/lib/clientSettings"; import { defaultSettings, getClientSettings } from "@/lib/clientSettings";
import { ProfileSettings, ClientSettings } from "@/types/settings"; import { ClientSettings } from "@/types/settings";
import axios from "axios";
import { Dispatch, SetStateAction, useEffect, useState } from "react"; import { Dispatch, SetStateAction, useEffect, useState } from "react";
export function useClientSettings(): [ export function useClientSettings(): [
@@ -17,30 +16,3 @@ export function useClientSettings(): [
return [settings, setSettings]; return [settings, setSettings];
} }
export function useProfileSettings(): [
ProfileSettings,
Dispatch<SetStateAction<ProfileSettings>>
] {
const [settings, setSettings] = useState<ProfileSettings>({
node_address: "",
username: "",
display_name: "",
avatar_url: "",
});
useEffect(() => {
async function fetchProfileSettings() {
try {
const res = await axios.get("/api/profile");
setSettings(res.data as ProfileSettings);
} catch (error) {
console.error("Failed to fetch profile settings:", error);
}
}
fetchProfileSettings();
}, []);
return [settings, setSettings];
}
+6 -2
View File
@@ -2,6 +2,7 @@ import { Dispatch, RefObject, SetStateAction } from "react";
import { StringMap } from "./typeUtils"; import { StringMap } from "./typeUtils";
import { UserProfile } from "@/hooks/get-user"; import { UserProfile } from "@/hooks/get-user";
import { Message } from "./types"; import { Message } from "./types";
import { ClientSettings } from "./settings";
export type SetState<T> = Dispatch<SetStateAction<T>>; export type SetState<T> = Dispatch<SetStateAction<T>>;
@@ -9,8 +10,11 @@ export default interface App {
node: RefObject<WebSocket | null>; node: RefObject<WebSocket | null>;
profile: UserProfile | null; profile: UserProfile | null;
profiles: StringMap<UserProfile>; clientSettings: ClientSettings;
setProfiles: SetState<StringMap<UserProfile>>; setClientSettings: SetState<ClientSettings>;
profiles: StringMap<UserProfile | null>;
setProfiles: SetState<StringMap<UserProfile | null>>;
messages: Message[]; messages: Message[];
addMessage: (v: Message) => void; addMessage: (v: Message) => void;