Big Updated, added some tests, added comments for all functions, I forgot the rest
This commit is contained in:
parent
d77271e4b7
commit
90a1059cc8
59 changed files with 1816 additions and 203 deletions
|
|
@ -7,7 +7,8 @@
|
|||
"format": "bunx prettier --write .",
|
||||
"lint": "eslint src",
|
||||
"dev": "vite --port 3000",
|
||||
"build": "tsc -b && vite build",
|
||||
"test": "bun test --pass-with-no-tests",
|
||||
"build": "bun run test && tsc -b && vite build",
|
||||
"preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .."
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -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" />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ const ConversationContext = React.createContext<contextValue | undefined>(
|
|||
undefined,
|
||||
);
|
||||
|
||||
/**
|
||||
* Executes ConversationProvider.
|
||||
* @param props Parameter props.
|
||||
* @returns unknown.
|
||||
*/
|
||||
export default function ConversationProvider(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
|
|
@ -64,6 +69,11 @@ export default function ConversationProvider(props: {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes useConversation.
|
||||
* @param none This function has no parameters.
|
||||
* @returns contextValue.
|
||||
*/
|
||||
export function useConversation(): contextValue {
|
||||
const context = React.useContext(ConversationContext);
|
||||
if (!context) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ import Switch from "./switch";
|
|||
import ConversationModal from "../modal/conversation";
|
||||
import CommunityModal from "../modal/community";
|
||||
|
||||
/**
|
||||
* Executes List.
|
||||
* @param none This function has no parameters.
|
||||
* @returns unknown.
|
||||
*/
|
||||
export default function List() {
|
||||
const [category, setCategory] = React.useState<
|
||||
"conversations" | "communities"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ import * as React from "react";
|
|||
|
||||
type Category = "conversations" | "communities";
|
||||
|
||||
/**
|
||||
* Executes Switch.
|
||||
* @param props Parameter props.
|
||||
* @returns unknown.
|
||||
*/
|
||||
export default function Switch(props: {
|
||||
category: Category;
|
||||
setCategory: (category: Category) => void;
|
||||
|
|
@ -28,6 +33,11 @@ export default function Switch(props: {
|
|||
updateIndicator();
|
||||
}, [updateIndicator]);
|
||||
|
||||
/**
|
||||
* Executes toggleCategory.
|
||||
* @param none This function has no parameters.
|
||||
* @returns unknown.
|
||||
*/
|
||||
function toggleCategory() {
|
||||
props.setCategory(
|
||||
props.category === "conversations" ? "communities" : "conversations",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import type { Community } from "@tensamin/shared/features/conversation/schema";
|
||||
|
||||
/**
|
||||
* Executes CommunityModal.
|
||||
* @param props Parameter props.
|
||||
* @returns unknown.
|
||||
*/
|
||||
export default function CommunityModal(props: { community: Community }) {
|
||||
return <div>{props.community.community_title}</div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Basic from "@/components/modals/basic";
|
||||
import { Basic, Loading } from "@/components/modals/basic";
|
||||
import Wrapper from "@tensamin/user/wrapper";
|
||||
import {
|
||||
ContextMenu,
|
||||
|
|
@ -8,22 +8,52 @@ import {
|
|||
ContextMenuTrigger,
|
||||
} from "@tensamin/ui/cmp/context-menu";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { type User } from "@tensamin/user/context";
|
||||
|
||||
/**
|
||||
* Renders the conversation modal user preview card.
|
||||
* @param user Loaded user data.
|
||||
* @returns Conversation preview card JSX.
|
||||
*/
|
||||
function renderConversationUser(user: User) {
|
||||
return <Basic user={user} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the conversation modal user preview card skeleton while loading user data.
|
||||
* @returns Conversation preview card skeleton JSX.
|
||||
*/
|
||||
function renderConversationUserLoading() {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a conversation context-menu entry for a specific user.
|
||||
* @param props Component props with selected user id.
|
||||
* @returns Conversation modal trigger and menu JSX.
|
||||
*/
|
||||
export default function ConversationModal(props: { userId: number }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
/**
|
||||
* Navigates to the selected conversation in chat view.
|
||||
* @returns Void.
|
||||
*/
|
||||
const onConversationClick = () => {
|
||||
void navigate({ to: "/chat", search: { id: props.userId } });
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>
|
||||
<div
|
||||
className="select-none cursor-pointer"
|
||||
onClick={() => {
|
||||
void navigate({ to: "/chat", search: { id: props.userId } });
|
||||
}}
|
||||
onClick={onConversationClick}
|
||||
>
|
||||
<Wrapper
|
||||
loading={renderConversationUserLoading()}
|
||||
userId={props.userId}
|
||||
component={(user) => <Basic user={user} />}
|
||||
component={renderConversationUser}
|
||||
/>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,60 @@ import { log } from "@tensamin/shared/log";
|
|||
import Link from "@tensamin/ui/link";
|
||||
import { Label } from "@tensamin/ui/cmp/label";
|
||||
|
||||
type SaveFn = ReturnType<typeof useStorage>["save"];
|
||||
|
||||
/**
|
||||
* Persists accepted legal documents and marks the first onboarding step complete.
|
||||
* @param save Storage save function.
|
||||
* @param currentDocs Current legal documents fetched from the server.
|
||||
* @param setPPandToSDone State setter for legal acceptance completion.
|
||||
* @returns Promise that resolves when persistence is complete.
|
||||
*/
|
||||
async function persistAcceptedDocs(
|
||||
save: SaveFn,
|
||||
currentDocs: z.infer<typeof legalDocsSchema>,
|
||||
setPPandToSDone: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
): Promise<void> {
|
||||
await save("accepted_privacy_policy", true);
|
||||
await save("accepted_terms_of_service", true);
|
||||
await save("ppandtos_done", true);
|
||||
await save("legal_docs", currentDocs);
|
||||
setPPandToSDone(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists analytics preference toggles and marks analytics onboarding complete.
|
||||
* @param save Storage save function.
|
||||
* @param crashReports Whether crash reports are enabled.
|
||||
* @param usageData Whether usage data is enabled.
|
||||
* @param setCrashReports State setter for crash reports.
|
||||
* @param setUsageData State setter for usage data.
|
||||
* @param setDoneWithAnalytics State setter for analytics completion.
|
||||
* @returns Promise that resolves when persistence is complete.
|
||||
*/
|
||||
async function persistAnalyticsPreferences(
|
||||
save: SaveFn,
|
||||
crashReports: boolean,
|
||||
usageData: boolean,
|
||||
setCrashReports: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
setUsageData: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
setDoneWithAnalytics: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
): Promise<void> {
|
||||
await save("analytics_crash_reports", crashReports);
|
||||
setCrashReports(crashReports);
|
||||
|
||||
await save("analytics_usage_data", usageData);
|
||||
setUsageData(usageData);
|
||||
|
||||
await save("analytics_done", true);
|
||||
setDoneWithAnalytics(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gates the application behind legal and analytics consent checks.
|
||||
* @param props Component props containing children to render after consent.
|
||||
* @returns Legal onboarding or wrapped children JSX.
|
||||
*/
|
||||
export default function Screen(props: { children: React.ReactNode }) {
|
||||
const { load, save } = useStorage();
|
||||
|
||||
|
|
@ -31,6 +85,37 @@ export default function Screen(props: { children: React.ReactNode }) {
|
|||
const [crashReports, setCrashReports] = React.useState(false);
|
||||
const [usageData, setUsageData] = React.useState(false);
|
||||
|
||||
/**
|
||||
* Handles continue action for privacy policy and terms acceptance.
|
||||
* @returns Void.
|
||||
*/
|
||||
const handleContinueLegal = React.useCallback((): void => {
|
||||
const currentDocs = remoteDocs;
|
||||
if (!currentDocs) {
|
||||
return;
|
||||
}
|
||||
|
||||
void persistAcceptedDocs(save, currentDocs, setPPandToSDone);
|
||||
}, [remoteDocs, save]);
|
||||
|
||||
/**
|
||||
* Handles continue action for analytics preferences.
|
||||
* @returns Void.
|
||||
*/
|
||||
const handleContinueAnalytics = React.useCallback((): void => {
|
||||
const currentCrashReports = crashReports;
|
||||
const currentUsageData = usageData;
|
||||
|
||||
void persistAnalyticsPreferences(
|
||||
save,
|
||||
currentCrashReports,
|
||||
currentUsageData,
|
||||
setCrashReports,
|
||||
setUsageData,
|
||||
setDoneWithAnalytics,
|
||||
);
|
||||
}, [crashReports, save, usageData]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
|
|
@ -178,20 +263,7 @@ export default function Screen(props: { children: React.ReactNode }) {
|
|||
</div>
|
||||
<ContinueButton
|
||||
disabled={!acceptedPP || !acceptedTOS}
|
||||
onClick={() => {
|
||||
const currentDocs = remoteDocs;
|
||||
if (!currentDocs) {
|
||||
return;
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
await save("accepted_privacy_policy", true);
|
||||
await save("accepted_terms_of_service", true);
|
||||
await save("ppandtos_done", true);
|
||||
await save("legal_docs", currentDocs);
|
||||
setPPandToSDone(true);
|
||||
})();
|
||||
}}
|
||||
onClick={handleContinueLegal}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -211,24 +283,7 @@ export default function Screen(props: { children: React.ReactNode }) {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ContinueButton
|
||||
onClick={() => {
|
||||
const currentCrashReports = crashReports;
|
||||
const currentUsageData = usageData;
|
||||
|
||||
void save("analytics_crash_reports", currentCrashReports).then(
|
||||
() => {
|
||||
setCrashReports(currentCrashReports);
|
||||
},
|
||||
);
|
||||
void save("analytics_usage_data", currentUsageData).then(() => {
|
||||
setUsageData(currentUsageData);
|
||||
});
|
||||
void save("analytics_done", true).then(() => {
|
||||
setDoneWithAnalytics(true);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ContinueButton onClick={handleContinueAnalytics} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -236,6 +291,11 @@ export default function Screen(props: { children: React.ReactNode }) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a large continue button used by legal and analytics steps.
|
||||
* @param props Button props with click callback and disabled state.
|
||||
* @returns Continue button JSX.
|
||||
*/
|
||||
function ContinueButton(props: { onClick: () => void; disabled?: boolean }) {
|
||||
return (
|
||||
<div className="w-full flex justify-end">
|
||||
|
|
@ -251,6 +311,11 @@ function ContinueButton(props: { onClick: () => void; disabled?: boolean }) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a larger checkbox row for onboarding preferences.
|
||||
* @param props Checkbox label, current value, and change callback.
|
||||
* @returns Checkbox row JSX.
|
||||
*/
|
||||
export function BigCheckbox(props: {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ window.setLogLevelToMax = () => {
|
|||
location.reload();
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes RootShell.
|
||||
* @param none This function has no parameters.
|
||||
* @returns unknown.
|
||||
*/
|
||||
function RootShell() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
|
|
@ -45,6 +50,11 @@ function RootShell() {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes AppShell.
|
||||
* @param none This function has no parameters.
|
||||
* @returns unknown.
|
||||
*/
|
||||
function AppShell() {
|
||||
return (
|
||||
<AppLayout>
|
||||
|
|
@ -53,6 +63,11 @@ function AppShell() {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes Chat.
|
||||
* @param none This function has no parameters.
|
||||
* @returns unknown.
|
||||
*/
|
||||
function Chat() {
|
||||
return (
|
||||
<ChatContext>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
/**
|
||||
* Executes Page.
|
||||
* @param none This function has no parameters.
|
||||
* @returns unknown.
|
||||
*/
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center text-4xl font-bold">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
/**
|
||||
* Executes Page.
|
||||
* @param none This function has no parameters.
|
||||
* @returns unknown.
|
||||
*/
|
||||
export default function Page() {
|
||||
return <div>Home</div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ import Sidebar from "@/components/sidebar";
|
|||
import Conversation from "@/features/conversation/context";
|
||||
import Navbar from "@/components/navbar";
|
||||
|
||||
/**
|
||||
* Executes Layout.
|
||||
* @param props Parameter props.
|
||||
* @returns unknown.
|
||||
*/
|
||||
export default function Layout(props: { children: ReactNode }) {
|
||||
return (
|
||||
<Socket>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ import LegalWrapper from "@/features/legal/screen";
|
|||
|
||||
import { Toaster } from "@tensamin/ui/cmp/sonner";
|
||||
|
||||
/**
|
||||
* Executes Layout.
|
||||
* @param props Parameter props.
|
||||
* @returns unknown.
|
||||
*/
|
||||
export default function Layout(props: { children: ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ import Form from "@/components/screens/login/form";
|
|||
import { Button } from "@tensamin/ui/cmp/button";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
/**
|
||||
* Executes Page.
|
||||
* @param none This function has no parameters.
|
||||
* @returns unknown.
|
||||
*/
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col gap-10 items-center justify-center">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
/**
|
||||
* Executes Page.
|
||||
* @param none This function has no parameters.
|
||||
* @returns unknown.
|
||||
*/
|
||||
export default function Page() {
|
||||
return <div>Signup Page</div>;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue