Big restructure

This commit is contained in:
Alois 2026-03-14 12:43:26 +01:00
commit 4ee57ab459
69 changed files with 124 additions and 124 deletions

View file

@ -0,0 +1,20 @@
import type { User } from "@tensamin/user/context";
import Avatar from "@tensamin/ui/avatar";
import { reduceDisplay } from "./utils";
import { Card, CardHeader } from "@tensamin/ui/card";
export default function Basic(props: { user: User }) {
return (
<Card className="animate-in fade-in duration-300 rounded-2xl">
<CardHeader className="flex flex-row gap-2.5 items-center justify-start p-2">
<Avatar
img={props.user.avatar}
fallback={reduceDisplay(props.user.display)}
/>
<div className="flex flex-col gap-1 w-full items-start justify-center text-[15px]">
<p>{props.user.display}</p>
</div>
</CardHeader>
</Card>
);
}

View file

@ -0,0 +1,8 @@
export function reduceDisplay(display: string) {
const words = display.split(" ");
if (words.length === 1) {
return display.slice(0, 2).toUpperCase();
} else {
return words[0].charAt(0).toUpperCase() + words[1].charAt(0).toUpperCase();
}
}

View file

@ -0,0 +1,45 @@
import { Button } from "@tensamin/ui/button";
import { House } from "lucide-react";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import * as React from "react";
import { useUser, type User } from "@tensamin/user/context";
export default function Navbar() {
const navigate = useNavigate();
const { get } = useUser();
const search = useRouterState({ select: (state) => state.location.search });
const [user, setUser] = React.useState<User | null>(null);
const currentId = React.useMemo(
() => Number((search as Record<string, unknown>).id ?? Number.NaN),
[search],
);
React.useEffect(() => {
if (Number.isNaN(currentId)) {
setUser(null);
return;
}
get(currentId)
.then(setUser)
.catch(() => setUser(null));
}, [currentId, get]);
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"
variant="outline"
>
<House size={18} />
</Button>
<p className="font-medium pl-3 text-md">{user?.display}</p>
<div className="w-full" />
</div>
);
}

View file

@ -0,0 +1,138 @@
import { Button } from "@tensamin/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@tensamin/ui/card";
import { Input } from "@tensamin/ui/input";
import { Label } from "@tensamin/ui/label";
import { useStorage } from "@tensamin/storage/context";
import { log, toast } from "@tensamin/shared/log";
import { useNavigate } from "@tanstack/react-router";
import { Upload } from "lucide-react";
import * as React from "react";
import { z } from "zod";
const fetchedUser = z.object({
id: z.uuidv4(),
type: z.string(),
data: z.object({
iota_id: z.number(),
username: z.string(),
sub_level: z.number(),
public_key: z.base64(),
user_id: z.number(),
sub_end: z.number(),
}),
});
const formSchema = z.object({
username: z.string().min(1).max(15),
private_key: z.string().min(1).max(92),
});
export default function Form() {
const uploadRef = React.useRef<HTMLInputElement | null>(null);
const { save } = useStorage();
const navigate = useNavigate();
return (
<div className="flex gap-5">
<Card className="w-75 h-80">
<CardHeader>
<CardTitle>Use .tu file</CardTitle>
</CardHeader>
<CardContent className="h-full flex items-center justify-center">
<div
onClick={() => uploadRef.current?.click()}
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");
}
}}
type="file"
ref={uploadRef}
className="hidden"
/>
</CardContent>
</Card>
<Card className="w-75 h-auto">
<CardHeader>
<CardTitle>Use credentials</CardTitle>
</CardHeader>
<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");
});
}}
>
<div className="flex flex-col gap-2">
<Label htmlFor="username">Username</Label>
<Input type="text" id="username" name="username" />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="private_key">Private Key</Label>
<Input type="password" id="private_key" name="private_key" />
</div>
<Button type="submit">Login</Button>
</form>
</CardContent>
</Card>
</div>
);
}

View file

@ -0,0 +1,28 @@
import { useStorage } from "@tensamin/storage/context";
import Wrapper from "@tensamin/user/wrapper";
import * as React from "react";
import Basic from "./modals/basic";
import List from "@/features/conversation/list/body";
export default function Sidebar() {
const [userId, setUserId] = React.useState(0);
const { load } = useStorage();
React.useEffect(() => {
load("user_id").then(setUserId);
}, [load]);
return (
<div className="w-75 h-full flex flex-col gap-3 p-2">
{userId !== 0 && (
<Wrapper
userId={userId}
component={(user) => <Basic user={user} />}
/>
)}
<div className="h-full">
<List />
</div>
</div>
);
}