Big Updated, added some tests, added comments for all functions, I forgot the rest

This commit is contained in:
Alois 2026-03-15 14:52:09 +01:00
commit 90a1059cc8
59 changed files with 1816 additions and 203 deletions

View file

@ -2,10 +2,16 @@ import type { User } from "@tensamin/user/context";
import { Avatar, AvatarImage, AvatarFallback } from "@tensamin/ui/cmp/avatar";
import { reduceDisplay } from "./utils";
import { Card, CardHeader } from "@tensamin/ui/cmp/card";
import { Skeleton } from "@tensamin/ui/cmp/skeleton";
export default function Basic(props: { user: User }) {
/**
* Executes Basic.
* @param props Parameter props.
* @returns unknown.
*/
export function Basic(props: { user: User }) {
return (
<Card className="animate-in fade-in duration-300 rounded-2xl">
<Card className="animate-in fade-in duration-300 rounded-2xl py-0">
<CardHeader className="flex flex-row gap-2.5 items-center justify-start p-2">
<Avatar>
<AvatarImage src={props.user.avatar} />
@ -18,3 +24,11 @@ export default function Basic(props: { user: User }) {
</Card>
);
}
/**
* Executes Loading.
* @returns unknown.
*/
export function Loading() {
return <Skeleton className="h-12 rounded-xl" />;
}

View file

@ -1,3 +1,8 @@
/**
* Executes reduceDisplay.
* @param display Parameter display.
* @returns unknown.
*/
export function reduceDisplay(display: string) {
const words = display.split(" ");
if (words.length === 1) {

View file

@ -4,11 +4,32 @@ import { useNavigate, useRouterState } from "@tanstack/react-router";
import * as React from "react";
import { useUser, type User } from "@tensamin/user/context";
/**
* Navigates to the home route when the navbar home button is clicked.
* @param navigate Router navigate function from TanStack Router.
* @returns Void.
*/
function handleHomeButtonClick(navigate: ReturnType<typeof useNavigate>): void {
void navigate({ to: "/" });
}
/**
* Renders the top navigation bar and currently selected conversation user.
* @returns Navbar JSX element.
*/
export default function Navbar() {
const navigate = useNavigate();
const { get } = useUser();
const search = useRouterState({ select: (state) => state.location.search });
/**
* Delegates navbar home button click to navigation helper.
* @returns Void.
*/
const onHomeButtonClick = React.useCallback(() => {
handleHomeButtonClick(navigate);
}, [navigate]);
const [user, setUser] = React.useState<User | null>(null);
const currentId = React.useMemo(
@ -30,13 +51,11 @@ export default function Navbar() {
return (
<div className="w-full h-13.5 flex items-center justify-center">
<Button
onClick={() => {
void navigate({ to: "/" });
}}
className="w-9 h-9 aspect-square p-0 rounded-lg"
onClick={onHomeButtonClick}
className="w-9 h-9 aspect-square rounded-lg"
variant="outline"
>
<House size={18} />
<House className="size-4.5" />
</Button>
<p className="font-medium pl-3 text-md">{user?.display}</p>
<div className="w-full" />

View file

@ -32,11 +32,118 @@ const formSchema = z.object({
private_key: z.string().min(1).max(92),
});
/**
* Parses a .tu file payload into credentials.
* @param rawFileContent UTF-8 file content from an uploaded .tu file.
* @returns Parsed user id and private key credentials.
*/
function parseTuFileContent(rawFileContent: string): {
userId: number;
privateKey: string;
} {
if (rawFileContent.length !== 92 || !rawFileContent.includes("::")) {
throw new Error("Invalid file");
}
const [userIdString, privateKey] = rawFileContent.split("::");
const userId = Number(userIdString);
if (!userId || !privateKey) {
throw new Error("Invalid file");
}
return { userId, privateKey };
}
/**
* Renders the login form for file upload and manual credential login.
* @returns Login form JSX.
*/
export default function Form() {
const uploadRef = React.useRef<HTMLInputElement | null>(null);
const { save } = useStorage();
const navigate = useNavigate();
/**
* Opens the hidden file input when the upload tile is clicked.
* @returns Void.
*/
const handleUploadTileClick = React.useCallback((): void => {
uploadRef.current?.click();
}, []);
/**
* Handles uploaded .tu files and stores resolved credentials.
* @param event Change event from the hidden file input.
* @returns Promise that resolves when processing has finished.
*/
const handleFileInputChange = React.useCallback(
async (event: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
try {
const file = event.currentTarget.files?.[0];
if (!file) {
throw new Error("No file selected");
}
const raw = await file.text();
const parsed = parseTuFileContent(raw);
await save("user_id", parsed.userId);
await save("private_key", parsed.privateKey);
void navigate({ to: "/" });
} catch (error) {
log(0, "Login", "red", error);
toast("error", "Failed to load file");
}
},
[navigate, save],
);
/**
* Handles username and private key login submission.
* @param event Form submit event.
* @returns Promise that resolves after login processing.
*/
const handleCredentialsSubmit = React.useCallback(
async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const rawData = Object.fromEntries(formData);
const inputParse = formSchema.safeParse(rawData);
if (!inputParse.success) {
toast("error", "Please enter valid data");
return;
}
try {
const response = await fetch(
`https://omega.tensamin.net/api/get/id/${inputParse.data.username}`,
);
const data = await response.json();
const parse = fetchedUser.safeParse(data);
if (!parse.success) {
log(0, "Login", "red", "Invalid response from server");
toast("error", "Invalid response from server");
return;
}
const user = parse.data;
await save("user_id", user.data.user_id);
await save("private_key", inputParse.data.private_key);
void navigate({ to: "/" });
} catch (error) {
log(0, "Login", "red", error);
toast("error", "Failed to fetch user data");
}
},
[navigate, save],
);
return (
<div className="flex gap-5">
<Card className="w-75 h-80">
@ -45,35 +152,13 @@ export default function Form() {
</CardHeader>
<CardContent className="h-full flex items-center justify-center">
<div
onClick={() => uploadRef.current?.click()}
onClick={handleUploadTileClick}
className="cursor-pointer w-60 aspect-square mb-17 bg-input/13 hover:bg-input/30 transition-all duration-300 ease-in-out border-dotted border-input/75 border-3 flex items-center justify-center rounded-lg"
>
<Upload className="text-input/75" size={34} />
</div>
<input
onChange={async (e) => {
try {
const file = e.currentTarget.files?.[0];
if (file) {
const raw = await file.text();
if (raw.length !== 92) throw new Error("Invalid file");
if (!raw.includes("::")) throw new Error("Invalid file");
const [userIdString, privateKey] = raw.split("::");
const userId = Number(userIdString);
if (!userId || !privateKey) throw new Error("Invalid file");
save("user_id", userId);
save("private_key", privateKey);
void navigate({ to: "/" });
} else {
throw new Error("No file selected");
}
} catch (err) {
log(0, "Login", "red", err);
toast("error", "Failed to load file");
}
}}
onChange={handleFileInputChange}
type="file"
ref={uploadRef}
className="hidden"
@ -87,44 +172,7 @@ export default function Form() {
<CardContent>
<form
className="flex flex-col gap-5 h-full"
onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const rawData = Object.fromEntries(formData);
const inputParse = formSchema.safeParse(rawData);
if (!inputParse.success) {
toast("error", "Please enter valid data");
return;
}
const response = await fetch(
"https://omega.tensamin.net/api/get/id/" +
inputParse.data.username,
);
response
.json()
.then((data) => {
const parse = fetchedUser.safeParse(data);
if (parse.success) {
const user = parse.data;
save("user_id", user.data.user_id);
save("private_key", inputParse.data.private_key);
void navigate({ to: "/" });
} else {
log(0, "Login", "red", "Invalid response from server");
toast("error", "Invalid response from server");
}
})
.catch((err) => {
log(0, "Login", "red", err);
toast("error", "Failed to fetch user data");
});
}}
onSubmit={handleCredentialsSubmit}
>
<div className="flex flex-col gap-2">
<Label htmlFor="username">Username</Label>

View file

@ -1,11 +1,33 @@
import { useStorage } from "@tensamin/storage/context";
import Wrapper from "@tensamin/user/wrapper";
import { type User } from "@tensamin/user/context";
import * as React from "react";
import Basic from "./modals/basic";
import { Basic, Loading } from "./modals/basic";
import List from "@/features/conversation/list/body";
/**
* Renders sidebar user summary content for the current user.
* @param user Loaded user data.
* @returns Sidebar user card JSX.
*/
function renderSidebarUser(user: User): React.ReactNode {
return <Basic user={user} />;
}
/**
* Renders sidebar user summary content skeleton while loading user data.
* @returns Sidebar user card skeleton JSX.
*/
function renderSidebarUserLoading(): React.ReactNode {
return <Loading />;
}
/**
* Renders the conversation sidebar with account summary and conversation list.
* @returns Sidebar JSX.
*/
export default function Sidebar() {
const [userId, setUserId] = React.useState(0);
const [userId, setUserId] = React.useState<undefined | number>(undefined);
const { load } = useStorage();
React.useEffect(() => {
@ -14,9 +36,11 @@ export default function Sidebar() {
return (
<div className="w-75 h-full flex flex-col gap-3 p-2">
{userId !== 0 && (
<Wrapper userId={userId} component={(user) => <Basic user={user} />} />
)}
<Wrapper
loading={renderSidebarUserLoading()}
userId={userId}
component={renderSidebarUser}
/>
<div className="h-full">
<List />
</div>