Compare commits
10 Commits
de28d80769
...
842815f583
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
842815f583 | ||
|
|
46e9adca05 | ||
|
|
fd9f991383 | ||
|
|
3953cccd0f | ||
|
|
7f1b2b9a05 | ||
|
|
0ce3c0307f | ||
|
|
bba27be7e2 | ||
|
|
42b27b7ddc | ||
|
|
39b3a837df | ||
|
|
ee83aefabc |
1
.gitignore
vendored
@@ -7,3 +7,4 @@ count.txt
|
||||
.env
|
||||
.nitro
|
||||
.tanstack
|
||||
pnpm-lock.yaml
|
||||
@@ -10,6 +10,9 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@codesandbox/sandpack-react": "^2.20.0",
|
||||
"@codesandbox/sandpack-themes": "^2.0.21",
|
||||
"@radix-ui/react-toast": "^1.2.14",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
@@ -40,5 +43,6 @@
|
||||
"vite": "^6.1.0",
|
||||
"vitest": "^3.0.5",
|
||||
"web-vitals": "^4.2.4"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531"
|
||||
}
|
||||
4949
pnpm-lock.yaml
generated
4
public/icons/Code.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6.48002 8.82659L2.67189 12L6.48002 15.1735C6.55787 15.2358 6.62252 15.313 6.67017 15.4006C6.71781 15.4882 6.74751 15.5845 6.75752 15.6837C6.76753 15.7829 6.75765 15.8831 6.72846 15.9785C6.69927 16.0739 6.65135 16.1624 6.58751 16.2391C6.52368 16.3157 6.4452 16.3788 6.35667 16.4247C6.26815 16.4706 6.17135 16.4984 6.07195 16.5065C5.97255 16.5146 5.87254 16.5027 5.77777 16.4717C5.683 16.4406 5.59537 16.391 5.52002 16.3256L1.02002 12.5757C0.935581 12.5053 0.867647 12.4172 0.821028 12.3176C0.774409 12.2181 0.750244 12.1095 0.750244 11.9996C0.750244 11.8896 0.774409 11.781 0.821028 11.6815C0.867647 11.5819 0.935581 11.4938 1.02002 11.4235L5.52002 7.67346C5.67293 7.54616 5.87015 7.48481 6.0683 7.50292C6.26644 7.52103 6.44928 7.61711 6.57658 7.77002C6.70389 7.92294 6.76523 8.12016 6.74712 8.3183C6.72901 8.51645 6.63293 8.69928 6.48002 8.82659ZM22.98 11.4235L18.48 7.67346C18.4043 7.61043 18.3169 7.56292 18.2228 7.53366C18.1288 7.5044 18.0299 7.49396 17.9317 7.50292C17.8336 7.51189 17.7382 7.54009 17.651 7.58592C17.5638 7.63175 17.4865 7.69431 17.4235 7.77002C17.2962 7.92294 17.2348 8.12016 17.2529 8.3183C17.271 8.51645 17.3671 8.69928 17.52 8.82659L21.3281 12L17.52 15.1735C17.4422 15.2358 17.3775 15.313 17.3299 15.4006C17.2822 15.4882 17.2525 15.5845 17.2425 15.6837C17.2325 15.7829 17.2424 15.8831 17.2716 15.9785C17.3008 16.0739 17.3487 16.1624 17.4125 16.2391C17.4764 16.3157 17.5548 16.3788 17.6434 16.4247C17.7319 16.4706 17.8287 16.4984 17.9281 16.5065C18.0275 16.5146 18.1275 16.5027 18.2223 16.4717C18.317 16.4406 18.4047 16.391 18.48 16.3256L22.98 12.5757C23.0645 12.5053 23.1324 12.4172 23.179 12.3176C23.2256 12.2181 23.2498 12.1095 23.2498 11.9996C23.2498 11.8896 23.2256 11.781 23.179 11.6815C23.1324 11.5819 23.0645 11.4938 22.98 11.4235ZM15.256 3.04502C15.1634 3.01141 15.0651 2.99636 14.9667 3.00074C14.8683 3.00512 14.7717 3.02883 14.6825 3.07053C14.5932 3.11223 14.5131 3.1711 14.4466 3.24377C14.3801 3.31645 14.3286 3.4015 14.295 3.49409L8.29502 19.9941C8.26126 20.0867 8.2461 20.1851 8.25041 20.2836C8.25471 20.3821 8.2784 20.4788 8.32011 20.5681C8.36181 20.6575 8.42073 20.7377 8.49348 20.8043C8.56623 20.8708 8.65139 20.9224 8.74408 20.956C8.82625 20.9852 8.91282 21.0001 9.00002 21C9.15404 21 9.30433 20.9526 9.43046 20.8642C9.5566 20.7758 9.65246 20.6507 9.70502 20.506L15.705 4.00596C15.7386 3.91338 15.7537 3.81508 15.7493 3.71669C15.7449 3.61829 15.7212 3.52172 15.6795 3.43249C15.6378 3.34325 15.5789 3.26311 15.5063 3.19663C15.4336 3.13015 15.3485 3.07863 15.256 3.04502Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
4
public/icons/Preview.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M19.5 3.75H4.5C3.90326 3.75 3.33097 3.98705 2.90901 4.40901C2.48705 4.83097 2.25 5.40326 2.25 6V16.5C2.25 17.0967 2.48705 17.669 2.90901 18.091C3.33097 18.5129 3.90326 18.75 4.5 18.75H19.5C20.0967 18.75 20.669 18.5129 21.091 18.091C21.5129 17.669 21.75 17.0967 21.75 16.5V6C21.75 5.40326 21.5129 4.83097 21.091 4.40901C20.669 3.98705 20.0967 3.75 19.5 3.75ZM20.25 16.5C20.25 16.6989 20.171 16.8897 20.0303 17.0303C19.8897 17.171 19.6989 17.25 19.5 17.25H4.5C4.30109 17.25 4.11032 17.171 3.96967 17.0303C3.82902 16.8897 3.75 16.6989 3.75 16.5V6C3.75 5.80109 3.82902 5.61032 3.96967 5.46967C4.11032 5.32902 4.30109 5.25 4.5 5.25H19.5C19.6989 5.25 19.8897 5.32902 20.0303 5.46967C20.171 5.61032 20.25 5.80109 20.25 6V16.5ZM15.75 21C15.75 21.1989 15.671 21.3897 15.5303 21.5303C15.3897 21.671 15.1989 21.75 15 21.75H9C8.80109 21.75 8.61032 21.671 8.46967 21.5303C8.32902 21.3897 8.25 21.1989 8.25 21C8.25 20.8011 8.32902 20.6103 8.46967 20.4697C8.61032 20.329 8.80109 20.25 9 20.25H15C15.1989 20.25 15.3897 20.329 15.5303 20.4697C15.671 20.6103 15.75 20.8011 15.75 21Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
51
src/api/api.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
const _apiRoot = "https://ai-api.cytonic.com";
|
||||
export class ApiError extends Error {
|
||||
constructor(description: string, error: string, trace_id: string) {
|
||||
super(error);
|
||||
this.name = "ApiError";
|
||||
this.message = description;
|
||||
this.stack = trace_id;
|
||||
this.cause = error;
|
||||
}
|
||||
}
|
||||
|
||||
async function request(
|
||||
entrypoint: string,
|
||||
method: "POST" | "GET" = "GET",
|
||||
body?: object,
|
||||
) {
|
||||
const apiRoot = localStorage.getItem("API_ROOT") || _apiRoot;
|
||||
const address =
|
||||
apiRoot + (entrypoint[0] === "/" ? entrypoint : `/${entrypoint}`);
|
||||
|
||||
const res = await fetch(address, {
|
||||
method,
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
...(body && { "Content-Type": "application/json" }),
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
throw new ApiError(error.description, error.error, error.trace_id);
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
const api = {
|
||||
get(
|
||||
entrypoint: string,
|
||||
queryObj?: string | URLSearchParams | Record<string, string> | string[][],
|
||||
) {
|
||||
let queryStr = "";
|
||||
if (queryObj) queryStr = `?${new URLSearchParams(queryObj).toString()}`;
|
||||
return request(entrypoint + queryStr);
|
||||
},
|
||||
post(entrypoint: string, body?: object) {
|
||||
return request(entrypoint, "POST", body);
|
||||
},
|
||||
};
|
||||
|
||||
export { api };
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Button } from "../ui/button";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useAccountStore } from "@/contexts/AccountContext";
|
||||
|
||||
export const MainMenu = observer(() => {
|
||||
const { isMainMenuOpen, setIsMainMenuOpen } = useAccountStore();
|
||||
return (
|
||||
<div
|
||||
data-open={isMainMenuOpen}
|
||||
className="w-[30px] data-[open=true]:w-[124px] transition-all border-r p-2"
|
||||
>
|
||||
<Button onClick={() => setIsMainMenuOpen(!isMainMenuOpen)}>
|
||||
{isMainMenuOpen ? "Close" : "Open"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,24 +1,101 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRef, useState } from "react";
|
||||
import Arrow from "@/icons/arrow.svg?react";
|
||||
import Paperclip from "@/icons/paperclip.svg?react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { useAccountStore } from "@/contexts/AccountContext";
|
||||
import { Button } from "../ui/button";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
export const Chat = observer(() => {
|
||||
const { isSandboxOpen, setIsSandboxOpen, isMainMenuOpen, setIsMainMenuOpen } =
|
||||
useAccountStore();
|
||||
|
||||
const Chat = observer(() => {
|
||||
const { id } = useParams({ from: "/interaction-panel/$id" });
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsSandboxOpen(!isSandboxOpen);
|
||||
if (isMainMenuOpen && !isSandboxOpen) {
|
||||
setIsMainMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
className="ml-10"
|
||||
>
|
||||
OPEN SANDBOX
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex h-full flex-1 items-center justify-center">{id}</div>
|
||||
);
|
||||
});
|
||||
|
||||
const ChatInput = () => {
|
||||
const navigate = useNavigate();
|
||||
const { userId } = useAccountStore();
|
||||
|
||||
const [message, setMessage] = useState("");
|
||||
const [showShadow, setShowShadow] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const el = e.target;
|
||||
setMessage(el.value);
|
||||
|
||||
el.style.height = "auto";
|
||||
el.style.height = `${Math.min(el.scrollHeight, 200)}px`;
|
||||
|
||||
setShowShadow(el.scrollTop + el.clientHeight < el.scrollHeight);
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
const el = textareaRef.current;
|
||||
if (!el) return;
|
||||
setShowShadow(el.scrollTop + el.clientHeight < el.scrollHeight);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex flex-col justify-between rounded-[16px] border border-[#E8E8E8] pt-4 pr-2 pb-2 pl-4 transition-all duration-200"
|
||||
style={{
|
||||
boxShadow: "0px 1px 8px 0px #00000012",
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
onInput={handleInput}
|
||||
onScroll={handleScroll}
|
||||
className="max-h-[150px] min-h-[40px] w-full resize-none overflow-y-auto pr-4 text-[14px] placeholder:text-[#818181] focus:outline-none"
|
||||
placeholder="Ask whatever you want..."
|
||||
value={message}
|
||||
rows={1}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute bottom-0 left-0 h-5 w-full transition-opacity duration-300",
|
||||
showShadow ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to bottom, rgba(255,255,255,0), rgba(255,255,255,0.95), #ffffff)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<div className="flex gap-6">
|
||||
<span className="flex cursor-pointer items-center gap-2">
|
||||
<Paperclip />
|
||||
<p className="text-[#C0C0C0] text-[14px]">Add attachment</p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
className="h-10 w-10 rotate-180"
|
||||
disabled={message.trim() === ""}
|
||||
onClick={() => {
|
||||
if (userId === undefined) {
|
||||
navigate({
|
||||
to: "/auth/mail",
|
||||
viewTransition: { types: ["warp"] },
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Arrow color="white" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Chat, ChatInput };
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { useAccountStore } from "@/contexts/AccountContext";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
export const Sandbox = observer(() => {
|
||||
const { isSandboxOpen } = useAccountStore();
|
||||
|
||||
return (
|
||||
<div
|
||||
data-open={isSandboxOpen}
|
||||
data-open={true}
|
||||
className="w-0 data-[open=true]:w-[40svw] border-l-1 transition-all border-r p-0 bg-red-600"
|
||||
>
|
||||
SANDBOOOOX
|
||||
|
||||
71
src/components/main-menu/chat-cell.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Dropdown } from "../ui/dropdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Link, useMatchRoute } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import ThreeDots from "@/icons/three-dots.svg?react";
|
||||
import Edit from "@/icons/edit.svg?react";
|
||||
import Share from "@/icons/share.svg?react";
|
||||
import Trash from "@/icons/trash.svg?react";
|
||||
|
||||
export const ChatCell = ({ chat }: { chat: { id: string; title: string } }) => {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
|
||||
const matchRoute = useMatchRoute();
|
||||
const match = matchRoute({ to: "/interaction-panel/$id", fuzzy: true });
|
||||
|
||||
const activeChatId = match ? match.id : undefined;
|
||||
|
||||
const DROPDOWN_OPTIONS = [
|
||||
{
|
||||
name: "Share",
|
||||
icon: <Share />,
|
||||
onClick: () => console.log("1"),
|
||||
},
|
||||
{
|
||||
name: "Rename",
|
||||
icon: <Edit />,
|
||||
onClick: () => console.log("2"),
|
||||
},
|
||||
{
|
||||
name: "Delete",
|
||||
icon: <Trash />,
|
||||
onClick: () => console.log("3"),
|
||||
className: "text-feedback-negative-900",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
data-active={activeChatId === chat.id}
|
||||
className="data-[active=true]:bg-fill-150 block rounded-md h-10 hover:bg-fill-100 cursor-pointer transition-colors group flex items-center"
|
||||
>
|
||||
<Link
|
||||
to="/interaction-panel/$id"
|
||||
params={{ id: chat.id }}
|
||||
viewTransition={{ types: ["warp"] }}
|
||||
key={chat.id}
|
||||
className="flex-1 h-full items-center flex pl-3"
|
||||
>
|
||||
<div className="text-sm font-medium text-text-light-900 truncate">
|
||||
{chat.title}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Dropdown
|
||||
options={DROPDOWN_OPTIONS}
|
||||
isOpen={isDropdownOpen}
|
||||
onToggle={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||
classNameTrigger="cursor-pointer flex items-center pr-3 justify-end w-5 h-full"
|
||||
>
|
||||
<ThreeDots
|
||||
className={cn(
|
||||
"duration-300",
|
||||
isDropdownOpen
|
||||
? "opacity-100"
|
||||
: "opacity-0 group-hover:opacity-100",
|
||||
)}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
227
src/components/main-menu/main-menu.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useAccountStore } from "@/contexts/AccountContext";
|
||||
import Plus from "@/icons/plus.svg?react";
|
||||
import Books from "@/icons/books.svg?react";
|
||||
import Discord from "@/icons/discord.svg?react";
|
||||
import Profile from "@/icons/profile.svg?react";
|
||||
import Settings from "@/icons/settings.svg?react";
|
||||
import SignOut from "@/icons/sign-out.svg?react";
|
||||
|
||||
import { LINKS } from "@/lib/constants";
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { Button, type ButtonVariantType } from "../ui/button";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
import Logo from "@/icons/logo.svg?react";
|
||||
import LogoWithText from "@/icons/logo-with-text.svg?react";
|
||||
import Sidebar from "@/icons/sidebar.svg?react";
|
||||
import { ChatCell } from "./chat-cell";
|
||||
import { Dropdown } from "../ui/dropdown";
|
||||
|
||||
interface MenuItemType extends ButtonVariantType {
|
||||
name: string;
|
||||
icon: ReactNode;
|
||||
path: string;
|
||||
}
|
||||
|
||||
const MainMenu = observer(() => {
|
||||
const { isMainMenuOpen } = useAccountStore();
|
||||
|
||||
return (
|
||||
<div
|
||||
data-open={isMainMenuOpen}
|
||||
className="w-[64px] data-[open=true]:w-[227px] transition-all border-r border-r border-fill-150"
|
||||
>
|
||||
<MainMenuContent />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const MOCK_CHATS = Array.from({ length: 20 }, (_, i) => ({
|
||||
id: i,
|
||||
title: `Chat ${i + 1}`,
|
||||
}));
|
||||
|
||||
const MainMenuContent = observer(() => {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
|
||||
const { isMainMenuOpen, setIsMainMenuOpen } = useAccountStore();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [hasScrolled, setHasScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
setHasScrolled(container.scrollTop > 0);
|
||||
};
|
||||
|
||||
container.addEventListener("scroll", handleScroll);
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
container.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const DROPDOWN_OPTIONS = [
|
||||
{
|
||||
name: "user-mail",
|
||||
icon: <Profile />,
|
||||
onClick: () => console.log("1"),
|
||||
className: "text-text-light-500",
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
icon: <Settings />,
|
||||
onClick: () => console.log("2"),
|
||||
},
|
||||
{
|
||||
name: "Sigh out",
|
||||
icon: <SignOut />,
|
||||
onClick: () => console.log("3"),
|
||||
},
|
||||
];
|
||||
|
||||
const showBorder = hasScrolled && isMainMenuOpen;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-open={isMainMenuOpen}
|
||||
ref={containerRef}
|
||||
className="relative w-full h-full overflow-x-hidden data-[open=true]:overflow-y-auto overflow-y-hidden flex flex-col"
|
||||
>
|
||||
<header
|
||||
data-scrolled={showBorder}
|
||||
className="data-[scrolled=true]:border-b data-[scrolled=true]:border-fill-150 sticky top-0 z-10 bg-white pt-[30px] space-y-12 flex flex-col w-full px-4 pb-4 transition-shadow"
|
||||
>
|
||||
<div className="relative w-full h-[32px]">
|
||||
{isMainMenuOpen ? (
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<LogoWithText />
|
||||
<Button
|
||||
className="w-8 h-8 hover:cursor-w-resize"
|
||||
variant="ghost"
|
||||
onClick={() => setIsMainMenuOpen(false)}
|
||||
>
|
||||
<Sidebar />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative group px-1">
|
||||
<Logo className="transition-opacity duration-150 group-hover:opacity-0" />
|
||||
<Button
|
||||
className="w-8 h-8 absolute top-0 left-0 opacity-0 group-hover:opacity-100 transition-opacity duration-150 hover:cursor-e-resize"
|
||||
variant="ghost"
|
||||
onClick={() => setIsMainMenuOpen(true)}
|
||||
>
|
||||
<Sidebar />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-open={isMainMenuOpen}
|
||||
className="space-y-4 w-full data-[open=true]:flex data-[open=true]:flex-col data-[open=true]:items-center"
|
||||
>
|
||||
{MENU_ITEMS.map((item) => (
|
||||
<MenuItem item={item} key={item.name} />
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
data-open={isMainMenuOpen}
|
||||
className="data-[open=false]:opacity-0 transition-opacity px-4 flex-1 mt-5"
|
||||
>
|
||||
<p className="text-text-light-200 font-[500] text-[14px] mb-2">
|
||||
Recent
|
||||
</p>
|
||||
|
||||
<ul className=" pb-10">
|
||||
{MOCK_CHATS.map((chat) => (
|
||||
<ChatCell
|
||||
chat={{ id: chat.id.toString(), title: chat.title }}
|
||||
key={chat.id}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<footer className="sticky bottom-0 z-10 bg-white w-full p-4 border-t border-fill-150 w-full">
|
||||
<Dropdown
|
||||
options={DROPDOWN_OPTIONS}
|
||||
isOpen={isDropdownOpen}
|
||||
onToggle={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||
classNameContent="min-w-[200px]"
|
||||
classNameTrigger="w-[195px]"
|
||||
>
|
||||
<Button
|
||||
data-open={isMainMenuOpen}
|
||||
className="w-8 h-8 p-2 transition-all data-[open=true]:w-full data-[open=true]:h-10 items-center justify-start data-[open=true]:py-2 data-[open=true]:px-4"
|
||||
variant="secondary"
|
||||
>
|
||||
<Profile />
|
||||
<div
|
||||
data-open={isMainMenuOpen}
|
||||
className="data-[open=true]:opacity-100 opacity-0 transition-opacity duration-200 font-[500] text-[14px]"
|
||||
>
|
||||
username
|
||||
</div>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const MenuItem = ({ item }: { item: MenuItemType }) => {
|
||||
const { isMainMenuOpen } = useAccountStore();
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={item.path}
|
||||
className="group flex items-center gap-3 w-full whitespace-nowrap"
|
||||
viewTransition={{ types: ["warp"] }}
|
||||
>
|
||||
<Button
|
||||
data-variant={item.variant}
|
||||
variant={item.variant}
|
||||
className="w-8 h-8 data-[variant=secondary]:group-hover:bg-fill-150 data-[variant=primary]:group-hover:bg-fill-700 transition-colors"
|
||||
>
|
||||
{item.icon}
|
||||
</Button>
|
||||
<div
|
||||
data-open={isMainMenuOpen}
|
||||
className="data-[open=true]:opacity-100 opacity-0 transition-opacity duration-200 font-[500] text-[14px]"
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const MENU_ITEMS: MenuItemType[] = [
|
||||
{
|
||||
name: "New Chat",
|
||||
icon: <Plus className="scale-80" />,
|
||||
path: "/",
|
||||
variant: "primary",
|
||||
},
|
||||
{
|
||||
name: "All chats",
|
||||
icon: <Books className="scale-90" />,
|
||||
path: "/all-chats",
|
||||
variant: "secondary",
|
||||
},
|
||||
{
|
||||
name: "Support",
|
||||
icon: <Discord width={12} height={12} className="text-[#5865F2]" />,
|
||||
path: LINKS.discord,
|
||||
variant: "secondary",
|
||||
},
|
||||
];
|
||||
|
||||
export { MainMenu, MenuItem, MENU_ITEMS, MainMenuContent };
|
||||
@@ -3,13 +3,15 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ButtonVariantType = VariantProps<typeof buttonVariants>;
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 font-medium whitespace-nowrap rounded-[8px] px-4 text-[14px] leading-[130%] tracking-[0.01em] transition-colors hover:cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:*:fill-text-secondary [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
"h-[52px] inline-flex items-center justify-center gap-2 font-medium whitespace-nowrap rounded-[8px] px-4 text-[14px] leading-[130%] tracking-[0.01em] transition-colors hover:cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:*:fill-text-secondary [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary:
|
||||
"bg-fill-800 text-white hover:bg-fill-700 disabled:bg-fill-400",
|
||||
"bg-fill-800 text-white hover:bg-fill-700 disabled:bg-fill-200",
|
||||
secondary:
|
||||
"bg-fill-100 text-text-light-900 hover:bg-fill-150 disabled:bg-fill-100 disabled:text-text-light-200",
|
||||
tertiary:
|
||||
@@ -45,4 +47,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
export { Button, buttonVariants, type ButtonVariantType };
|
||||
|
||||
85
src/components/ui/dropdown.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { type FC, type ReactNode } from "react";
|
||||
import clsx from "clsx";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface DropdownOption {
|
||||
name: string;
|
||||
icon?: ReactNode;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface DropdownProps {
|
||||
children: ReactNode;
|
||||
options: DropdownOption[];
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
classNameContent?: string;
|
||||
classNameTrigger?: string;
|
||||
}
|
||||
|
||||
const Dropdown: FC<DropdownProps> = ({
|
||||
children,
|
||||
options = [],
|
||||
isOpen,
|
||||
onToggle,
|
||||
classNameContent = "",
|
||||
classNameTrigger = "",
|
||||
}) => {
|
||||
return (
|
||||
<DropdownMenu.Root open={isOpen} onOpenChange={onToggle}>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(
|
||||
"inline-flex items-center focus:outline-none",
|
||||
classNameTrigger,
|
||||
)}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
className={clsx(
|
||||
"z-50 overflow-hidden p-2 bg-white border border-fill-150 rounded-[16px] min-w-[130px]",
|
||||
classNameContent,
|
||||
)}
|
||||
side="bottom"
|
||||
align="start"
|
||||
style={{ boxShadow: "0px 4px 10px 0px #00000014" }}
|
||||
sideOffset={8}
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<DropdownMenu.Item
|
||||
key={`${option.name}-${index}`}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
option.onClick();
|
||||
}}
|
||||
className={cn(
|
||||
"focus:outline-none flex items-center gap-2 h-8 px-2 text-sm text-text-light-900 hover:bg-fill-100 cursor-pointer rounded-[8px] transition-colors",
|
||||
option.className,
|
||||
)}
|
||||
>
|
||||
{option.icon && (
|
||||
<span className="w-4 h-4 flex items-center justify-center text-inherit">
|
||||
{option.icon}
|
||||
</span>
|
||||
)}
|
||||
<span>{option.name}</span>
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export { Dropdown };
|
||||
62
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { HTMLInputTypeAttribute } from "react";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type InputProps = Omit<React.ComponentProps<"input">, "type"> & {
|
||||
type?: HTMLInputTypeAttribute | "token";
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
label?: string;
|
||||
|
||||
rightContent?: React.ReactNode;
|
||||
};
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ children, className, type, label, required, ...props }, ref) => {
|
||||
return (
|
||||
<div className="group flex w-full flex-col gap-3">
|
||||
{label && (
|
||||
<span
|
||||
data-required={required}
|
||||
className="text-text-light-500 font-[500] text-[12px] after:ml-0.5 after:text-negative-primary data-[required=true]:after:content-['*']"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"group flex rounded-[8px] items-center w-full border transition-colors border-fill-200 hover:border-fill-400",
|
||||
"focus-within:border-fill-400",
|
||||
"disabled:border-b-fill-secondary",
|
||||
"data-[invalid=true]:border-b-negative-primary",
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type={type}
|
||||
ref={ref}
|
||||
data-disabled={props.disabled}
|
||||
className={cn(
|
||||
"text-[14px] px-4 py-3 flex w-full border-none bg-transparent transition-colors placeholder:text-text-secondary",
|
||||
"focus:placeholder:opacity-0 disabled:cursor-not-allowed disabled:text-text-quartinary",
|
||||
"data-[disabled=true]:placeholder:text-text-quinary",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{props.rightContent && (
|
||||
<div className="pr-2 pb-2 flex items-center gap-1 text-text-primary text-sm">
|
||||
{props.rightContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
@@ -17,7 +17,7 @@ export const TextLink: FC<TextLinkProps> = ({
|
||||
<a
|
||||
data-disabled={disabled}
|
||||
className={cn(
|
||||
"text-feedback-info-900 data-[disabled=true]:opacity-50 font-[600] data-[disabled=false]:cursor-pointer data-[disabled=false]:hover:border-b-[2px] w-fit transition-all duration-75",
|
||||
"text-feedback-info-900 data-[disabled=true]:opacity-50 font-[500] data-[disabled=false]:cursor-pointer data-[disabled=false]:hover:border-b-[2px] w-fit transition-all duration-75",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -13,6 +13,11 @@ import { cn } from "@/lib/utils";
|
||||
import clsx from "clsx";
|
||||
import { useToast } from "@/lib/hooks/use-toast";
|
||||
|
||||
import Discord from "@/icons/discord.svg?react";
|
||||
import Copy from "@/icons/copy.svg?react";
|
||||
import { TextLink } from "./text-link";
|
||||
import { LINKS } from "@/lib/constants";
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider;
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
@@ -35,10 +40,10 @@ const toastVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
success: "bg-feedback-positive-100",
|
||||
warning: "bg-feedback-attention-100",
|
||||
error: "bg-feedback-negative-100",
|
||||
info: "bg-feedback-info-100",
|
||||
success: "bg-fill-100",
|
||||
warning: "bg-fill-100",
|
||||
error: "bg-fill-100",
|
||||
info: "bg-fill-100",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -51,7 +56,7 @@ const ToastIcon = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant }) => {
|
||||
>(({ className, variant = "success" }) => {
|
||||
if (variant === "success") {
|
||||
return (
|
||||
<SuccessIcon className={clsx(className, "text-feedback-positive-900")} />
|
||||
@@ -213,7 +218,7 @@ type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||
|
||||
export function Toaster() {
|
||||
function Toaster() {
|
||||
const { toasts, isPaused, setIsPaused } = useToast();
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -258,28 +263,12 @@ export function Toaster() {
|
||||
>
|
||||
<ToastIcon className="size-12 mr-4" variant={props.variant} />
|
||||
<div className="grid gap-2 w-full">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
<div className="flex items-center w-full justify-between">
|
||||
{title && <ToastTitle>{title}</ToastTitle>} <ToastClose />
|
||||
</div>
|
||||
|
||||
{description && (
|
||||
<ToastDescription>
|
||||
{/* {props.variant === "errorWithCopy" && (
|
||||
<p className="text-[16px] mb-2">
|
||||
To solve problems, contact us <br />
|
||||
via{" "}
|
||||
<a
|
||||
href={"https://discord.gg/cytonic"}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-brand relative"
|
||||
>
|
||||
Discord
|
||||
<div className="h-[1px] w-[56px] bg-brand absolute left-0" />
|
||||
</a>{" "}
|
||||
and send the error text
|
||||
</p>
|
||||
)} */}
|
||||
{description}
|
||||
</ToastDescription>
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
|
||||
{copy && (
|
||||
@@ -287,7 +276,6 @@ export function Toaster() {
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
|
||||
{withTimeline && (
|
||||
<ToastTimeline
|
||||
@@ -304,6 +292,32 @@ export function Toaster() {
|
||||
);
|
||||
}
|
||||
|
||||
const TOASTER_DESCRIPTION_PATTERNS = {
|
||||
errorPattern: (
|
||||
<div>
|
||||
<span>
|
||||
<p>Please try again later</p>
|
||||
<p className="mt-2">
|
||||
If the problem persists, copy the error message below and contact our
|
||||
support team
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<div className="space-x-4 flex">
|
||||
<TextLink className="flex items-center gap-1">
|
||||
<Copy className="text-feedback-info-900" width={15} height={12} />[
|
||||
Error code ]
|
||||
</TextLink>
|
||||
|
||||
<TextLink className="flex items-center gap-1" href={LINKS.discord}>
|
||||
<Discord className="text-feedback-info-900" width={15} height={12} />[
|
||||
Support ]
|
||||
</TextLink>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
@@ -317,4 +331,6 @@ export {
|
||||
ToastIcon,
|
||||
ToastCopy,
|
||||
ToastTimeline,
|
||||
Toaster,
|
||||
TOASTER_DESCRIPTION_PATTERNS,
|
||||
};
|
||||
|
||||
3
src/icons/arrow.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.219375 8.53104L6.96937 15.281C7.11011 15.4218 7.30098 15.5008 7.5 15.5008C7.69902 15.5008 7.88989 15.4218 8.03063 15.281C8.17136 15.1403 8.25042 14.9494 8.25042 14.7504C8.25042 14.5514 8.17136 14.3605 8.03063 14.2198L2.56031 8.75042H17.25C17.4489 8.75042 17.6397 8.6714 17.7803 8.53075C17.921 8.3901 18 8.19933 18 8.00042C18 7.8015 17.921 7.61074 17.7803 7.47009C17.6397 7.32943 17.4489 7.25042 17.25 7.25042H2.56031L8.03063 1.78104C8.17136 1.64031 8.25042 1.44944 8.25042 1.25042C8.25042 1.05139 8.17136 0.860523 8.03063 0.719792C7.88989 0.579062 7.69902 0.5 7.5 0.5C7.30098 0.5 7.11011 0.579062 6.96937 0.719792L0.219375 7.46979C0.149642 7.53945 0.0943228 7.62216 0.0565796 7.71321C0.0188364 7.80426 -0.000589371 7.90186 -0.000589371 8.00042C-0.000589371 8.09898 0.0188364 8.19657 0.0565796 8.28762C0.0943228 8.37867 0.149642 8.46139 0.219375 8.53104Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 992 B |
3
src/icons/books.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.4781 11.1594L10.4038 1.29686C10.3769 1.16789 10.3249 1.04549 10.2506 0.936688C10.1763 0.827889 10.0813 0.734839 9.97093 0.662882C9.86059 0.590925 9.73711 0.541479 9.6076 0.517386C9.47809 0.493293 9.34509 0.495026 9.21625 0.522487L6.29062 1.15124C6.03184 1.2079 5.8059 1.36445 5.66194 1.58683C5.51798 1.80921 5.46764 2.07944 5.52187 2.33874L7.59625 12.2012C7.64248 12.4263 7.76477 12.6285 7.94255 12.774C8.12034 12.9194 8.34279 12.9992 8.5725 13C8.64351 12.9999 8.71431 12.9924 8.78375 12.9775L11.7094 12.3487C11.9685 12.2919 12.1946 12.1351 12.3386 11.9123C12.4826 11.6896 12.5327 11.4189 12.4781 11.1594ZM6.5 2.13436C6.5 2.13061 6.5 2.12874 6.5 2.12874L9.425 1.50374L9.63313 2.49561L6.70813 3.12499L6.5 2.13436ZM6.91375 4.10124L9.84 3.47311L10.0487 4.46686L7.125 5.09561L6.91375 4.10124ZM7.32875 6.07436L10.255 5.44561L11.0863 9.39811L8.16 10.0269L7.32875 6.07436ZM11.5 11.3712L8.575 11.9962L8.36687 11.0044L11.2919 10.375L11.5 11.3656C11.5 11.3694 11.5 11.3712 11.5 11.3712ZM4.5 0.999987H1.5C1.23478 0.999987 0.98043 1.10534 0.792893 1.29288C0.605357 1.48042 0.5 1.73477 0.5 1.99999V12C0.5 12.2652 0.605357 12.5196 0.792893 12.7071C0.98043 12.8946 1.23478 13 1.5 13H4.5C4.76522 13 5.01957 12.8946 5.20711 12.7071C5.39464 12.5196 5.5 12.2652 5.5 12V1.99999C5.5 1.73477 5.39464 1.48042 5.20711 1.29288C5.01957 1.10534 4.76522 0.999987 4.5 0.999987ZM1.5 1.99999H4.5V2.99999H1.5V1.99999ZM1.5 3.99999H4.5V9.99999H1.5V3.99999ZM4.5 12H1.5V11H4.5V12Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
3
src/icons/clock.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 0.25C8.07164 0.25 6.18657 0.821828 4.58319 1.89317C2.97982 2.96452 1.73013 4.48726 0.992179 6.26884C0.254224 8.05042 0.061142 10.0108 0.437348 11.9021C0.813554 13.7934 1.74215 15.5307 3.10571 16.8943C4.46928 18.2579 6.20656 19.1865 8.09787 19.5627C9.98919 19.9389 11.9496 19.7458 13.7312 19.0078C15.5127 18.2699 17.0355 17.0202 18.1068 15.4168C19.1782 13.8134 19.75 11.9284 19.75 10C19.7473 7.41498 18.7192 4.93661 16.8913 3.10872C15.0634 1.28084 12.585 0.25273 10 0.25ZM10 18.25C8.36831 18.25 6.77326 17.7661 5.41655 16.8596C4.05984 15.9531 3.00242 14.6646 2.378 13.1571C1.75358 11.6496 1.5902 9.99085 1.90853 8.3905C2.22685 6.79016 3.01259 5.32015 4.16637 4.16637C5.32016 3.01259 6.79017 2.22685 8.39051 1.90852C9.99085 1.59019 11.6497 1.75357 13.1571 2.37799C14.6646 3.00242 15.9531 4.05984 16.8596 5.41655C17.7662 6.77325 18.25 8.3683 18.25 10C18.2475 12.1873 17.3775 14.2843 15.8309 15.8309C14.2843 17.3775 12.1873 18.2475 10 18.25ZM16 10C16 10.1989 15.921 10.3897 15.7803 10.5303C15.6397 10.671 15.4489 10.75 15.25 10.75H10C9.80109 10.75 9.61033 10.671 9.46967 10.5303C9.32902 10.3897 9.25 10.1989 9.25 10V4.75C9.25 4.55109 9.32902 4.36032 9.46967 4.21967C9.61033 4.07902 9.80109 4 10 4C10.1989 4 10.3897 4.07902 10.5303 4.21967C10.671 4.36032 10.75 4.55109 10.75 4.75V9.25H15.25C15.4489 9.25 15.6397 9.32902 15.7803 9.46967C15.921 9.61032 16 9.80109 16 10Z" fill="#C0C0C0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
3
src/icons/discord.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="18" viewBox="0 0 24 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.9831 1.50753C18.4748 0.794022 16.8791 0.287044 15.2381 0C15.0337 0.37359 14.7949 0.87607 14.6303 1.27582C12.8611 1.00684 11.1081 1.00684 9.37143 1.27582C9.20684 0.876161 8.96264 0.37359 8.75636 0C7.11383 0.287168 5.51674 0.795438 4.00767 1.51126C1.00474 6.09907 0.190662 10.5728 0.597654 14.9832C2.5894 16.4869 4.5196 17.4004 6.41728 17.9982C6.88893 17.3425 7.30584 16.6483 7.66368 15.9228C6.98233 15.6605 6.32548 15.3373 5.70096 14.9571C5.86529 14.8339 6.02577 14.7055 6.18216 14.5722C9.96656 16.3618 14.0785 16.3618 17.8178 14.5722C17.9749 14.7047 18.1353 14.833 18.2989 14.9571C17.6734 15.3384 17.0153 15.6622 16.3326 15.9247C16.6925 16.6531 17.1087 17.348 17.579 18C19.4785 17.4023 21.4105 16.4888 23.4022 14.9832C23.8798 9.87048 22.5865 5.43781 19.9831 1.50753ZM8.1793 12.2709C7.04322 12.2709 6.11154 11.1986 6.11154 9.89292C6.11154 8.58722 7.02337 7.51313 8.1793 7.51313C9.33523 7.51313 10.2669 8.58531 10.247 9.89292C10.2488 11.1986 9.33531 12.2709 8.1793 12.2709ZM15.8206 12.2709C14.6845 12.2709 13.7529 11.1986 13.7529 9.89292C13.7529 8.58722 14.6647 7.51313 15.8206 7.51313C16.9766 7.51313 17.9082 8.58531 17.8883 9.89292C17.8883 11.1986 16.9766 12.2709 15.8206 12.2709Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
src/icons/edit.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.2069 3.58561L9.41438 0.792486C9.32152 0.699603 9.21127 0.625923 9.08993 0.575654C8.96859 0.525385 8.83853 0.499512 8.70719 0.499512C8.57585 0.499512 8.4458 0.525385 8.32446 0.575654C8.20312 0.625923 8.09287 0.699603 8 0.792486L0.29313 8.49998C0.199867 8.59251 0.125926 8.70265 0.0756045 8.824C0.025283 8.94535 -0.000414649 9.07549 5.05934e-06 9.20686V12C5.05934e-06 12.2652 0.105362 12.5196 0.292898 12.7071C0.480435 12.8946 0.734788 13 1 13H3.79313C3.9245 13.0004 4.05464 12.9747 4.17599 12.9244C4.29735 12.8741 4.40748 12.8001 4.5 12.7069L12.2069 4.99999C12.2998 4.90712 12.3734 4.79687 12.4237 4.67553C12.474 4.55419 12.4999 4.42414 12.4999 4.2928C12.4999 4.16146 12.474 4.0314 12.4237 3.91006C12.3734 3.78872 12.2998 3.67847 12.2069 3.58561ZM3.79313 12H1V9.20686L6.5 3.70686L9.29313 6.49999L3.79313 12ZM10 5.79249L7.20688 2.99999L8.70688 1.49999L11.5 4.29249L10 5.79249Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1014 B |
10
src/icons/logo-with-text.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="123" height="25" viewBox="0 0 123 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.21611 1.09424C7.96286 0.347493 9.17358 0.347492 9.92033 1.09424L10.9714 2.14531C11.511 2.68494 12.386 2.68494 12.9256 2.1453L13.9767 1.09424C14.7234 0.347492 15.9341 0.347492 16.6809 1.09424L23.3374 7.75078C24.0842 8.49753 24.0842 9.70825 23.3374 10.455L22.2863 11.5061C21.7467 12.0457 21.7467 12.9206 22.2863 13.4603L23.3374 14.5113C24.0842 15.2581 24.0842 16.4688 23.3374 17.2155L16.6809 23.8721C15.9341 24.6188 14.7234 24.6188 13.9767 23.8721L12.9256 22.821C12.386 22.2814 11.511 22.2814 10.9714 22.821L9.92033 23.8721C9.17358 24.6188 7.96286 24.6188 7.21611 23.8721L0.559574 17.2155C-0.187175 16.4688 -0.187175 15.2581 0.559574 14.5113L1.61064 13.4603C2.15027 12.9206 2.15027 12.0457 1.61064 11.5061L0.559574 10.455C-0.187176 9.70825 -0.187176 8.49753 0.559574 7.75078L7.21611 1.09424ZM10.9714 20.5548C11.511 21.0944 12.386 21.0944 12.9256 20.5548L20.0201 13.4603C20.5598 12.9206 20.5598 12.0457 20.0201 11.5061L12.9256 4.41153C12.386 3.87189 11.511 3.87189 10.9714 4.41153L3.87686 11.5061C3.33722 12.0457 3.33722 12.9206 3.87686 13.4603L10.9714 20.5548Z" fill="#202020"/>
|
||||
<path d="M104.238 18.5163V5.66672H106.911V18.5163H104.238ZM105.587 3.92044C105.068 3.92044 104.627 3.75537 104.264 3.42522C103.918 3.0777 103.745 2.63462 103.745 2.09596C103.745 1.55731 103.918 1.12291 104.264 0.792765C104.627 0.445245 105.068 0.271484 105.587 0.271484C106.124 0.271484 106.565 0.445245 106.911 0.792765C107.257 1.12291 107.43 1.55731 107.43 2.09596C107.43 2.63462 107.257 3.0777 106.911 3.42522C106.565 3.75537 106.124 3.92044 105.587 3.92044Z" fill="#202020"/>
|
||||
<path d="M89.0493 18.5161V5.66652H91.6706V7.59525H92.0858C92.3281 7.07397 92.7606 6.58745 93.3835 6.13567C94.0064 5.6839 94.932 5.45801 96.1605 5.45801C97.1294 5.45801 97.9858 5.67521 98.7298 6.10961C99.4911 6.54401 100.088 7.16085 100.521 7.96015C100.953 8.74207 101.169 9.68906 101.169 10.8011V18.5161H98.4963V11.0096C98.4963 9.89757 98.2194 9.0809 97.6658 8.55962C97.1121 8.02096 96.3508 7.75164 95.3819 7.75164C94.2745 7.75164 93.3835 8.11653 92.7087 8.84632C92.0512 9.57611 91.7225 10.636 91.7225 12.0261V18.5161H89.0493Z" fill="#202020"/>
|
||||
<path d="M79.5956 18.8811C78.3152 18.8811 77.1733 18.6204 76.1698 18.0992C75.1836 17.5605 74.405 16.796 73.834 15.8055C73.263 14.8151 72.9775 13.6422 72.9775 12.2869V11.8959C72.9775 10.5406 73.263 9.37642 73.834 8.40337C74.405 7.41294 75.1836 6.6484 76.1698 6.10974C77.1733 5.57109 78.3152 5.30176 79.5956 5.30176C80.8759 5.30176 82.0179 5.57109 83.0214 6.10974C84.0249 6.6484 84.8122 7.41294 85.3831 8.40337C85.9541 9.37642 86.2396 10.5406 86.2396 11.8959V12.2869C86.2396 13.6422 85.9541 14.8151 85.3831 15.8055C84.8122 16.796 84.0249 17.5605 83.0214 18.0992C82.0179 18.6204 80.8759 18.8811 79.5956 18.8811ZM79.5956 16.4832C80.7721 16.4832 81.7237 16.1096 82.4504 15.3624C83.1944 14.5979 83.5664 13.5467 83.5664 12.2087V11.9741C83.5664 10.6362 83.2031 9.59362 82.4764 8.84646C81.7497 8.08191 80.7894 7.69964 79.5956 7.69964C78.4363 7.69964 77.4847 8.08191 76.7407 8.84646C76.0141 9.59362 75.6507 10.6362 75.6507 11.9741V12.2087C75.6507 13.5467 76.0141 14.5979 76.7407 15.3624C77.4847 16.1096 78.4363 16.4832 79.5956 16.4832Z" fill="#202020"/>
|
||||
<path d="M67.7495 18.5159C66.9709 18.5159 66.348 18.2813 65.8809 17.8122C65.431 17.343 65.2061 16.7175 65.2061 15.9356V7.93396H62.4551V5.66639H65.2061V1.41797H67.8793V5.66639H71.6944V7.93396H67.8793V15.4664C67.8793 15.9877 68.1215 16.2484 68.606 16.2484H71.2791V18.5159H67.7495Z" fill="#202020"/>
|
||||
<path d="M49.5246 23.7283V21.3826H56.6617C57.1462 21.3826 57.3884 21.1219 57.3884 20.6007V16.6389H56.9732C56.8175 16.9865 56.5752 17.3253 56.2465 17.6554C55.935 17.9682 55.5111 18.2288 54.9748 18.4374C54.4384 18.6459 53.7636 18.7501 52.9504 18.7501C51.9815 18.7501 51.1164 18.5329 50.3551 18.0985C49.5938 17.6641 48.9969 17.0473 48.5644 16.248C48.1318 15.4487 47.9155 14.5017 47.9155 13.407V5.66602H50.5887V13.1985C50.5887 14.3106 50.8655 15.1359 51.4192 15.6746C51.9729 16.1959 52.7428 16.4565 53.729 16.4565C54.8191 16.4565 55.6928 16.0916 56.3503 15.3618C57.0251 14.632 57.3625 13.5721 57.3625 12.182V5.66602H60.0356V21.148C60.0356 21.9299 59.8021 22.5555 59.3349 23.0246C58.8851 23.4938 58.2622 23.7283 57.4663 23.7283H49.5246Z" fill="#202020"/>
|
||||
<path d="M34.6252 17.0251C33.4018 15.7446 32.79 14.1227 32.79 12.1594C32.79 10.196 33.4018 8.57415 34.6252 7.29371C35.8486 6.01327 37.3737 5.37305 39.2004 5.37305C41.2451 5.37305 42.9126 6.16692 44.2031 7.75467C44.7561 8.48879 45.1416 9.27413 45.3594 10.1107H42.5942C42.4769 9.68387 42.259 9.29974 41.9406 8.95829C41.287 8.19002 40.3736 7.80589 39.2004 7.80589C38.1614 7.80589 37.2899 8.20709 36.586 9.0095C35.8989 9.81191 35.5553 10.8619 35.5553 12.1594C35.5553 13.4569 35.8989 14.5069 36.586 15.3093C37.2899 16.1117 38.1614 16.5129 39.2004 16.5129C40.4071 16.5129 41.354 16.1288 42.0411 15.3605C42.3428 15.019 42.569 14.6349 42.7199 14.2081H45.3594C45.1416 15.0788 44.7561 15.8727 44.2031 16.5897C42.9294 18.1604 41.2618 18.9457 39.2004 18.9457C37.3737 18.9457 35.8486 18.3055 34.6252 17.0251Z" fill="#202020"/>
|
||||
<path d="M111.446 17.0251C110.223 15.7446 109.611 14.1227 109.611 12.1594C109.611 10.196 110.223 8.57415 111.446 7.29371C112.67 6.01327 114.195 5.37305 116.022 5.37305C118.066 5.37305 119.734 6.16692 121.024 7.75467C121.577 8.48879 121.963 9.27413 122.181 10.1107H119.415C119.298 9.68387 119.08 9.29974 118.762 8.95829C118.108 8.19002 117.195 7.80589 116.022 7.80589C114.983 7.80589 114.111 8.20709 113.407 9.0095C112.72 9.81191 112.377 10.8619 112.377 12.1594C112.377 13.4569 112.72 14.5069 113.407 15.3093C114.111 16.1117 114.983 16.5129 116.022 16.5129C117.228 16.5129 118.175 16.1288 118.862 15.3605C119.164 15.019 119.39 14.6349 119.541 14.2081H122.181C121.963 15.0788 121.577 15.8727 121.024 16.5897C119.751 18.1604 118.083 18.9457 116.022 18.9457C114.195 18.9457 112.67 18.3055 111.446 17.0251Z" fill="#202020"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
3
src/icons/logo.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.21611 1.09424C7.96286 0.347493 9.17358 0.347492 9.92033 1.09424L10.9714 2.14531C11.511 2.68494 12.386 2.68494 12.9256 2.1453L13.9767 1.09424C14.7234 0.347492 15.9341 0.347492 16.6809 1.09424L23.3374 7.75078C24.0842 8.49753 24.0842 9.70825 23.3374 10.455L22.2863 11.5061C21.7467 12.0457 21.7467 12.9206 22.2863 13.4603L23.3374 14.5113C24.0842 15.2581 24.0842 16.4688 23.3374 17.2155L16.6809 23.8721C15.9341 24.6188 14.7234 24.6188 13.9767 23.8721L12.9256 22.821C12.386 22.2814 11.511 22.2814 10.9714 22.821L9.92033 23.8721C9.17358 24.6188 7.96286 24.6188 7.21611 23.8721L0.559574 17.2155C-0.187175 16.4688 -0.187175 15.2581 0.559574 14.5113L1.61064 13.4603C2.15027 12.9206 2.15027 12.0457 1.61064 11.5061L0.559574 10.455C-0.187176 9.70825 -0.187176 8.49753 0.559574 7.75078L7.21611 1.09424ZM10.9714 20.5548C11.511 21.0944 12.386 21.0944 12.9256 20.5548L20.0201 13.4603C20.5598 12.9206 20.5598 12.0457 20.0201 11.5061L12.9256 4.41153C12.386 3.87189 11.511 3.87189 10.9714 4.41153L3.87686 11.5061C3.33722 12.0457 3.33722 12.9206 3.87686 13.4603L10.9714 20.5548Z" fill="#202020"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
src/icons/paperclip.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6558 9.46967C16.7255 9.53932 16.7808 9.62204 16.8186 9.71309C16.8563 9.80413 16.8757 9.90173 16.8757 10.0003C16.8757 10.0989 16.8563 10.1964 16.8186 10.2875C16.7808 10.3785 16.7255 10.4613 16.6558 10.5309L8.96359 18.2184C7.97885 19.203 6.6433 19.7561 5.25076 19.756C3.85821 19.756 2.52274 19.2027 1.53812 18.2179C0.553503 17.2332 0.000400399 15.8977 0.000488292 14.5051C0.000576185 13.1126 0.553847 11.7771 1.53859 10.7925L10.8442 1.34998C11.5472 0.646202 12.5011 0.250528 13.4958 0.250001C14.4906 0.249473 15.4448 0.644135 16.1486 1.34717C16.8524 2.0502 17.248 3.00401 17.2486 3.99877C17.2491 4.99353 16.8544 5.94776 16.1514 6.65154L6.8439 16.094C6.42121 16.5167 5.84792 16.7542 5.25015 16.7542C4.65238 16.7542 4.07909 16.5167 3.6564 16.094C3.23371 15.6714 2.99625 15.0981 2.99625 14.5003C2.99625 13.9025 3.23371 13.3292 3.6564 12.9065L11.4658 4.97342C11.5342 4.90044 11.6165 4.84188 11.7079 4.8012C11.7993 4.76051 11.8979 4.73851 11.9979 4.7365C12.0979 4.73448 12.1973 4.7525 12.2902 4.78947C12.3832 4.82645 12.4678 4.88165 12.539 4.95181C12.6103 5.02198 12.6669 5.10569 12.7053 5.19804C12.7438 5.29038 12.7634 5.38948 12.7629 5.4895C12.7625 5.58953 12.7421 5.68846 12.7029 5.78048C12.6636 5.87249 12.6064 5.95573 12.5345 6.02529L4.72421 13.9669C4.65428 14.0362 4.5987 14.1187 4.56065 14.2096C4.52259 14.3004 4.5028 14.3979 4.50241 14.4964C4.50202 14.5949 4.52103 14.6925 4.55836 14.7836C4.5957 14.8748 4.65062 14.9577 4.71999 15.0276C4.78937 15.0976 4.87184 15.1531 4.96269 15.1912C5.05355 15.2293 5.15101 15.249 5.24951 15.2494C5.34801 15.2498 5.44563 15.2308 5.53678 15.1935C5.62794 15.1562 5.71085 15.1012 5.78078 15.0319L15.0873 5.59404C15.51 5.17222 15.7478 4.59977 15.7485 4.00261C15.7491 3.40545 15.5124 2.83251 15.0906 2.40982C14.6688 1.98713 14.0963 1.74932 13.4992 1.74871C12.902 1.74809 12.3291 1.98472 11.9064 2.40654L2.60265 11.8453C2.25411 12.1933 1.97753 12.6065 1.78869 13.0614C1.59985 13.5162 1.50246 14.0039 1.50207 14.4964C1.50167 14.9889 1.59829 15.4767 1.78641 15.9318C1.97452 16.387 2.25045 16.8007 2.59843 17.1492C2.94641 17.4977 3.35964 17.7743 3.81451 17.9632C4.26938 18.152 4.757 18.2494 5.24951 18.2498C5.74202 18.2502 6.22979 18.1536 6.68496 17.9654C7.14014 17.7773 7.5538 17.5014 7.90234 17.1534L15.5955 9.46592C15.7366 9.32587 15.9276 9.24759 16.1264 9.24829C16.3252 9.249 16.5156 9.32862 16.6558 9.46967Z" fill="#C0C0C0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
3
src/icons/plus.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 6C12 6.13261 11.9473 6.25979 11.8536 6.35355C11.7598 6.44732 11.6326 6.5 11.5 6.5H6.5V11.5C6.5 11.6326 6.44732 11.7598 6.35355 11.8536C6.25979 11.9473 6.13261 12 6 12C5.86739 12 5.74021 11.9473 5.64645 11.8536C5.55268 11.7598 5.5 11.6326 5.5 11.5V6.5H0.5C0.367392 6.5 0.240215 6.44732 0.146447 6.35355C0.0526785 6.25979 0 6.13261 0 6C0 5.86739 0.0526785 5.74021 0.146447 5.64645C0.240215 5.55268 0.367392 5.5 0.5 5.5H5.5V0.5C5.5 0.367392 5.55268 0.240215 5.64645 0.146447C5.74021 0.0526785 5.86739 0 6 0C6.13261 0 6.25979 0.0526785 6.35355 0.146447C6.44732 0.240215 6.5 0.367392 6.5 0.5V5.5H11.5C11.6326 5.5 11.7598 5.55268 11.8536 5.64645C11.9473 5.74021 12 5.86739 12 6Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 812 B |
3
src/icons/profile.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.125 9.25C14.125 10.0658 13.8831 10.8634 13.4298 11.5417C12.9766 12.2201 12.3323 12.7488 11.5786 13.061C10.8248 13.3732 9.99543 13.4549 9.19526 13.2957C8.39508 13.1366 7.66008 12.7437 7.08319 12.1668C6.5063 11.5899 6.11343 10.8549 5.95427 10.0547C5.7951 9.25458 5.87679 8.42518 6.189 7.67143C6.50121 6.91769 7.02992 6.27345 7.70828 5.82019C8.38663 5.36693 9.18416 5.125 10 5.125C11.0936 5.12624 12.1421 5.56124 12.9154 6.33455C13.6888 7.10787 14.1238 8.15636 14.125 9.25ZM19.75 10C19.75 11.9284 19.1782 13.8134 18.1068 15.4168C17.0355 17.0202 15.5127 18.2699 13.7312 19.0078C11.9496 19.7458 9.98919 19.9389 8.09787 19.5627C6.20656 19.1865 4.46928 18.2579 3.10571 16.8943C1.74215 15.5307 0.813554 13.7934 0.437348 11.9021C0.061142 10.0108 0.254225 8.05042 0.992179 6.26884C1.73013 4.48726 2.97982 2.96451 4.58319 1.89317C6.18657 0.821828 8.07164 0.25 10 0.25C12.585 0.25273 15.0634 1.28084 16.8913 3.10872C18.7192 4.93661 19.7473 7.41498 19.75 10ZM18.25 10C18.2488 8.88956 18.0237 7.79077 17.5881 6.76934C17.1525 5.7479 16.5154 4.82481 15.7148 4.05525C14.9143 3.2857 13.9668 2.68549 12.929 2.29053C11.8911 1.89556 10.7843 1.71395 9.67469 1.75656C5.25907 1.92719 1.73782 5.605 1.75 10.0234C1.75424 12.0349 2.49609 13.9749 3.835 15.4759C4.38028 14.6851 5.07292 14.0068 5.875 13.4781C5.94339 13.433 6.02469 13.4114 6.10646 13.4169C6.18824 13.4223 6.26599 13.4543 6.32782 13.5081C7.34705 14.3897 8.6496 14.8749 9.99719 14.8749C11.3448 14.8749 12.6473 14.3897 13.6666 13.5081C13.7284 13.4543 13.8061 13.4223 13.8879 13.4169C13.9697 13.4114 14.051 13.433 14.1194 13.4781C14.9225 14.0065 15.6161 14.6848 16.1622 15.4759C17.5077 13.9694 18.251 12.0199 18.25 10Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
3
src/icons/settings.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.5 6.5H12.4769C12.4207 5.87498 12.257 5.26436 11.9931 4.695L12.8794 4.18313C12.9942 4.11682 13.0781 4.0076 13.1124 3.87949C13.1468 3.75138 13.1288 3.61487 13.0625 3.5C12.9962 3.38513 12.887 3.3013 12.7589 3.26696C12.6308 3.23262 12.4942 3.25057 12.3794 3.31687L11.4919 3.82937C11.1293 3.31744 10.6826 2.87069 10.1706 2.50813L10.6831 1.62063C10.7494 1.50575 10.7674 1.36925 10.733 1.24114C10.6987 1.11303 10.6149 1.0038 10.5 0.9375C10.3851 0.871196 10.2486 0.85324 10.1205 0.887583C9.9924 0.921925 9.88318 1.00575 9.81688 1.12063L9.305 2.00688C8.73564 1.74299 8.12502 1.57934 7.5 1.52312V0.5C7.5 0.367392 7.44732 0.240215 7.35355 0.146447C7.25979 0.0526784 7.13261 0 7 0C6.86739 0 6.74021 0.0526784 6.64645 0.146447C6.55268 0.240215 6.5 0.367392 6.5 0.5V1.52312C5.87498 1.57934 5.26436 1.74299 4.695 2.00688L4.18313 1.12063C4.15029 1.06375 4.10658 1.01389 4.05448 0.973905C4.00239 0.93392 3.94292 0.904587 3.87949 0.887583C3.81605 0.870578 3.74989 0.866234 3.68478 0.874799C3.61967 0.883364 3.55688 0.90467 3.5 0.9375C3.38513 1.0038 3.3013 1.11303 3.26696 1.24114C3.23262 1.36925 3.25057 1.50575 3.31687 1.62063L3.82937 2.50813C3.31744 2.87069 2.87069 3.31744 2.50813 3.82937L1.62063 3.31687C1.50575 3.25057 1.36925 3.23262 1.24114 3.26696C1.11303 3.3013 1.0038 3.38513 0.9375 3.5C0.871196 3.61487 0.85324 3.75138 0.887583 3.87949C0.921925 4.0076 1.00575 4.11682 1.12063 4.18313L2.00688 4.695C1.74299 5.26436 1.57934 5.87498 1.52312 6.5H0.5C0.367392 6.5 0.240215 6.55268 0.146447 6.64645C0.0526784 6.74021 0 6.86739 0 7C0 7.13261 0.0526784 7.25979 0.146447 7.35355C0.240215 7.44732 0.367392 7.5 0.5 7.5H1.52312C1.57934 8.12502 1.74299 8.73564 2.00688 9.305L1.12063 9.81688C1.02525 9.87188 0.950695 9.95684 0.908536 10.0585C0.866377 10.1603 0.85897 10.273 0.887464 10.3794C0.915957 10.4857 0.978758 10.5797 1.06612 10.6467C1.15348 10.7137 1.26052 10.75 1.37063 10.75C1.45841 10.7503 1.5447 10.7272 1.62063 10.6831L2.50813 10.1706C2.87069 10.6826 3.31744 11.1293 3.82937 11.4919L3.31687 12.3794C3.25057 12.4942 3.23262 12.6308 3.26696 12.7589C3.3013 12.887 3.38513 12.9962 3.5 13.0625C3.61487 13.1288 3.75138 13.1468 3.87949 13.1124C4.0076 13.0781 4.11682 12.9942 4.18313 12.8794L4.695 11.9931C5.26436 12.257 5.87498 12.4207 6.5 12.4769V13.5C6.5 13.6326 6.55268 13.7598 6.64645 13.8536C6.74021 13.9473 6.86739 14 7 14C7.13261 14 7.25979 13.9473 7.35355 13.8536C7.44732 13.7598 7.5 13.6326 7.5 13.5V12.4769C8.12502 12.4207 8.73564 12.257 9.305 11.9931L9.81688 12.8794C9.88318 12.9942 9.9924 13.0781 10.1205 13.1124C10.2486 13.1468 10.3851 13.1288 10.5 13.0625C10.6149 12.9962 10.6987 12.887 10.733 12.7589C10.7674 12.6308 10.7494 12.4942 10.6831 12.3794L10.1706 11.4919C10.6826 11.1293 11.1293 10.6826 11.4919 10.1706L12.3794 10.6831C12.4363 10.716 12.499 10.7373 12.5642 10.7459C12.6293 10.7544 12.6954 10.7501 12.7589 10.7331C12.8223 10.7161 12.8818 10.6867 12.9339 10.6467C12.986 10.6068 13.0297 10.5569 13.0625 10.5C13.1289 10.3852 13.1469 10.2488 13.1127 10.1206C13.0785 9.99253 12.9948 9.88327 12.88 9.81688L11.9937 9.305C12.2574 8.7356 12.4209 8.12498 12.4769 7.5H13.5C13.6326 7.5 13.7598 7.44732 13.8536 7.35355C13.9473 7.25979 14 7.13261 14 7C14 6.86739 13.9473 6.74021 13.8536 6.64645C13.7598 6.55268 13.6326 6.5 13.5 6.5ZM7 2.5C8.10643 2.50132 9.17369 2.90976 9.99831 3.64746C10.8229 4.38517 11.3472 5.40054 11.4712 6.5H7.28875L5.1975 2.87812C5.76561 2.62843 6.37944 2.49966 7 2.5ZM2.5 7C2.50038 6.2951 2.66636 5.60015 2.98455 4.97115C3.30274 4.34215 3.76424 3.7967 4.33187 3.37875L6.4225 7L4.33187 10.6213C3.76424 10.2033 3.30274 9.65785 2.98455 9.02885C2.66636 8.39985 2.50038 7.7049 2.5 7ZM7 11.5C6.37967 11.5013 5.76584 11.3736 5.1975 11.125L7.28875 7.5H11.4712C11.3472 8.59946 10.8229 9.61483 9.99831 10.3525C9.17369 11.0902 8.10643 11.4987 7 11.5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
3
src/icons/share.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 4.5C12 4.63261 11.9473 4.75979 11.8536 4.85355C11.7598 4.94732 11.6326 5 11.5 5C11.3674 5 11.2402 4.94732 11.1464 4.85355C11.0527 4.75979 11 4.63261 11 4.5V1.7075L6.85437 5.85375C6.76055 5.94757 6.63331 6.00028 6.50062 6.00028C6.36794 6.00028 6.2407 5.94757 6.14688 5.85375C6.05305 5.75993 6.00035 5.63268 6.00035 5.5C6.00035 5.36732 6.05305 5.24007 6.14688 5.14625L10.2925 1H7.5C7.36739 1 7.24021 0.947321 7.14645 0.853553C7.05268 0.759785 7 0.632608 7 0.5C7 0.367392 7.05268 0.240215 7.14645 0.146447C7.24021 0.0526785 7.36739 0 7.5 0H11.5C11.6326 0 11.7598 0.0526785 11.8536 0.146447C11.9473 0.240215 12 0.367392 12 0.5V4.5ZM9.5 6C9.36739 6 9.24021 6.05268 9.14645 6.14645C9.05268 6.24021 9 6.36739 9 6.5V11H1V3H5.5C5.63261 3 5.75979 2.94732 5.85355 2.85355C5.94732 2.75979 6 2.63261 6 2.5C6 2.36739 5.94732 2.24021 5.85355 2.14645C5.75979 2.05268 5.63261 2 5.5 2H1C0.734784 2 0.48043 2.10536 0.292893 2.29289C0.105357 2.48043 0 2.73478 0 3V11C0 11.2652 0.105357 11.5196 0.292893 11.7071C0.48043 11.8946 0.734784 12 1 12H9C9.26522 12 9.51957 11.8946 9.70711 11.7071C9.89464 11.5196 10 11.2652 10 11V6.5C10 6.36739 9.94732 6.24021 9.85355 6.14645C9.75979 6.05268 9.63261 6 9.5 6Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
src/icons/sidebar.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5 0.5H1.5C1.23478 0.5 0.98043 0.605357 0.792893 0.792893C0.605357 0.98043 0.5 1.23478 0.5 1.5V10.5C0.5 10.7652 0.605357 11.0196 0.792893 11.2071C0.98043 11.3946 1.23478 11.5 1.5 11.5H12.5C12.7652 11.5 13.0196 11.3946 13.2071 11.2071C13.3946 11.0196 13.5 10.7652 13.5 10.5V1.5C13.5 1.23478 13.3946 0.98043 13.2071 0.792893C13.0196 0.605357 12.7652 0.5 12.5 0.5ZM1.5 1.5H4V10.5H1.5V1.5ZM12.5 10.5H5V1.5H12.5V10.5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 551 B |
3
src/icons/sign-out.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.5 11.5C5.5 11.6326 5.44732 11.7598 5.35355 11.8536C5.25979 11.9473 5.13261 12 5 12H1C0.867392 12 0.740215 11.9473 0.646447 11.8536C0.552679 11.7598 0.5 11.6326 0.5 11.5V0.5C0.5 0.367392 0.552679 0.240215 0.646447 0.146447C0.740215 0.0526785 0.867392 0 1 0H5C5.13261 0 5.25979 0.0526785 5.35355 0.146447C5.44732 0.240215 5.5 0.367392 5.5 0.5C5.5 0.632608 5.44732 0.759785 5.35355 0.853553C5.25979 0.947321 5.13261 1 5 1H1.5V11H5C5.13261 11 5.25979 11.0527 5.35355 11.1464C5.44732 11.2402 5.5 11.3674 5.5 11.5ZM12.3538 5.64625L9.85375 3.14625C9.75993 3.05243 9.63268 2.99972 9.5 2.99972C9.36732 2.99972 9.24007 3.05243 9.14625 3.14625C9.05243 3.24007 8.99972 3.36732 8.99972 3.5C8.99972 3.63268 9.05243 3.75993 9.14625 3.85375L10.7931 5.5H5C4.86739 5.5 4.74021 5.55268 4.64645 5.64645C4.55268 5.74021 4.5 5.86739 4.5 6C4.5 6.13261 4.55268 6.25979 4.64645 6.35355C4.74021 6.44732 4.86739 6.5 5 6.5H10.7931L9.14625 8.14625C9.05243 8.24007 8.99972 8.36732 8.99972 8.5C8.99972 8.63268 9.05243 8.75993 9.14625 8.85375C9.24007 8.94757 9.36732 9.00028 9.5 9.00028C9.63268 9.00028 9.75993 8.94757 9.85375 8.85375L12.3538 6.35375C12.4002 6.30731 12.4371 6.25217 12.4623 6.19147C12.4874 6.13077 12.5004 6.06571 12.5004 6C12.5004 5.93429 12.4874 5.86923 12.4623 5.80853C12.4371 5.74783 12.4002 5.69269 12.3538 5.64625Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
3
src/icons/three-dots.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="2" height="10" viewBox="0 0 2 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.75 5C1.75 5.14834 1.70601 5.29334 1.6236 5.41668C1.54119 5.54002 1.42406 5.63614 1.28701 5.69291C1.14997 5.74968 0.999168 5.76453 0.853682 5.73559C0.708197 5.70665 0.57456 5.63522 0.46967 5.53033C0.364781 5.42544 0.29335 5.2918 0.264411 5.14632C0.235472 5.00083 0.250325 4.85003 0.307091 4.71299C0.363856 4.57594 0.459986 4.45881 0.583323 4.3764C0.70666 4.29399 0.851664 4.25 1 4.25C1.19891 4.25 1.38968 4.32902 1.53033 4.46967C1.67098 4.61032 1.75 4.80109 1.75 5ZM1 1.5C1.14834 1.5 1.29334 1.45601 1.41668 1.3736C1.54001 1.29119 1.63614 1.17406 1.69291 1.03701C1.74968 0.899968 1.76453 0.749169 1.73559 0.603683C1.70665 0.458197 1.63522 0.32456 1.53033 0.21967C1.42544 0.114781 1.2918 0.0433503 1.14632 0.0144114C1.00083 -0.0145275 0.850032 0.000324965 0.712988 0.0570907C0.575943 0.113856 0.458809 0.209986 0.376398 0.333323C0.293987 0.45666 0.25 0.601664 0.25 0.750001C0.25 0.948913 0.329018 1.13968 0.46967 1.28033C0.610322 1.42098 0.801088 1.5 1 1.5ZM1 8.5C0.851664 8.5 0.70666 8.54399 0.583323 8.6264C0.459986 8.70881 0.363856 8.82594 0.307091 8.96299C0.250325 9.10003 0.235472 9.25083 0.264411 9.39632C0.29335 9.5418 0.364781 9.67544 0.46967 9.78033C0.57456 9.88522 0.708197 9.95665 0.853682 9.98559C0.999168 10.0145 1.14997 9.99968 1.28701 9.94291C1.42406 9.88614 1.54119 9.79002 1.6236 9.66668C1.70601 9.54334 1.75 9.39834 1.75 9.25C1.75 9.05109 1.67098 8.86032 1.53033 8.71967C1.38968 8.57902 1.19891 8.5 1 8.5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
3
src/icons/trash.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.5 2H0.5C0.367392 2 0.240215 2.05268 0.146447 2.14645C0.0526785 2.24021 0 2.36739 0 2.5C0 2.63261 0.0526785 2.75979 0.146447 2.85355C0.240215 2.94732 0.367392 3 0.5 3H1V12C1 12.2652 1.10536 12.5196 1.29289 12.7071C1.48043 12.8946 1.73478 13 2 13H10C10.2652 13 10.5196 12.8946 10.7071 12.7071C10.8946 12.5196 11 12.2652 11 12V3H11.5C11.6326 3 11.7598 2.94732 11.8536 2.85355C11.9473 2.75979 12 2.63261 12 2.5C12 2.36739 11.9473 2.24021 11.8536 2.14645C11.7598 2.05268 11.6326 2 11.5 2ZM10 12H2V3H10V12ZM3 0.5C3 0.367392 3.05268 0.240215 3.14645 0.146447C3.24021 0.0526784 3.36739 0 3.5 0H8.5C8.63261 0 8.75979 0.0526784 8.85355 0.146447C8.94732 0.240215 9 0.367392 9 0.5C9 0.632608 8.94732 0.759785 8.85355 0.853553C8.75979 0.947322 8.63261 1 8.5 1H3.5C3.36739 1 3.24021 0.947322 3.14645 0.853553C3.05268 0.759785 3 0.632608 3 0.5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 969 B |
@@ -1,3 +1,8 @@
|
||||
export const LS_TOKENS = {
|
||||
isMainMenuOpen: "isMainMenuOpen",
|
||||
userId: "userId",
|
||||
};
|
||||
|
||||
export const LINKS = {
|
||||
discord: "https://discord.gg/cytonic",
|
||||
};
|
||||
|
||||
@@ -9,12 +9,17 @@
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as InteractionPanelRouteImport } from './routes/interaction-panel'
|
||||
import { Route as AuthRouteImport } from './routes/auth'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as MagicLinkMagicLinkRouteImport } from './routes/magic-link/$magic-link'
|
||||
import { Route as InteractionPanelIdRouteImport } from './routes/interaction-panel/$id'
|
||||
import { Route as AuthUsernameRouteImport } from './routes/auth/username'
|
||||
import { Route as AuthMailRouteImport } from './routes/auth/mail'
|
||||
import { Route as AuthCodeRouteImport } from './routes/auth/code'
|
||||
|
||||
const InteractionPanelRoute = InteractionPanelRouteImport.update({
|
||||
id: '/interaction-panel',
|
||||
path: '/interaction-panel',
|
||||
const AuthRoute = AuthRouteImport.update({
|
||||
id: '/auth',
|
||||
path: '/auth',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
@@ -22,40 +27,104 @@ const IndexRoute = IndexRouteImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const MagicLinkMagicLinkRoute = MagicLinkMagicLinkRouteImport.update({
|
||||
id: '/magic-link/$magic-link',
|
||||
path: '/magic-link/$magic-link',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const InteractionPanelIdRoute = InteractionPanelIdRouteImport.update({
|
||||
id: '/interaction-panel/$id',
|
||||
path: '/interaction-panel/$id',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AuthUsernameRoute = AuthUsernameRouteImport.update({
|
||||
id: '/username',
|
||||
path: '/username',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AuthMailRoute = AuthMailRouteImport.update({
|
||||
id: '/mail',
|
||||
path: '/mail',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AuthCodeRoute = AuthCodeRouteImport.update({
|
||||
id: '/code',
|
||||
path: '/code',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/interaction-panel': typeof InteractionPanelRoute
|
||||
'/auth': typeof AuthRouteWithChildren
|
||||
'/auth/code': typeof AuthCodeRoute
|
||||
'/auth/mail': typeof AuthMailRoute
|
||||
'/auth/username': typeof AuthUsernameRoute
|
||||
'/interaction-panel/$id': typeof InteractionPanelIdRoute
|
||||
'/magic-link/$magic-link': typeof MagicLinkMagicLinkRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/interaction-panel': typeof InteractionPanelRoute
|
||||
'/auth': typeof AuthRouteWithChildren
|
||||
'/auth/code': typeof AuthCodeRoute
|
||||
'/auth/mail': typeof AuthMailRoute
|
||||
'/auth/username': typeof AuthUsernameRoute
|
||||
'/interaction-panel/$id': typeof InteractionPanelIdRoute
|
||||
'/magic-link/$magic-link': typeof MagicLinkMagicLinkRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/interaction-panel': typeof InteractionPanelRoute
|
||||
'/auth': typeof AuthRouteWithChildren
|
||||
'/auth/code': typeof AuthCodeRoute
|
||||
'/auth/mail': typeof AuthMailRoute
|
||||
'/auth/username': typeof AuthUsernameRoute
|
||||
'/interaction-panel/$id': typeof InteractionPanelIdRoute
|
||||
'/magic-link/$magic-link': typeof MagicLinkMagicLinkRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/interaction-panel'
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/auth'
|
||||
| '/auth/code'
|
||||
| '/auth/mail'
|
||||
| '/auth/username'
|
||||
| '/interaction-panel/$id'
|
||||
| '/magic-link/$magic-link'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/interaction-panel'
|
||||
id: '__root__' | '/' | '/interaction-panel'
|
||||
to:
|
||||
| '/'
|
||||
| '/auth'
|
||||
| '/auth/code'
|
||||
| '/auth/mail'
|
||||
| '/auth/username'
|
||||
| '/interaction-panel/$id'
|
||||
| '/magic-link/$magic-link'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/auth'
|
||||
| '/auth/code'
|
||||
| '/auth/mail'
|
||||
| '/auth/username'
|
||||
| '/interaction-panel/$id'
|
||||
| '/magic-link/$magic-link'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
InteractionPanelRoute: typeof InteractionPanelRoute
|
||||
AuthRoute: typeof AuthRouteWithChildren
|
||||
InteractionPanelIdRoute: typeof InteractionPanelIdRoute
|
||||
MagicLinkMagicLinkRoute: typeof MagicLinkMagicLinkRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/interaction-panel': {
|
||||
id: '/interaction-panel'
|
||||
path: '/interaction-panel'
|
||||
fullPath: '/interaction-panel'
|
||||
preLoaderRoute: typeof InteractionPanelRouteImport
|
||||
'/auth': {
|
||||
id: '/auth'
|
||||
path: '/auth'
|
||||
fullPath: '/auth'
|
||||
preLoaderRoute: typeof AuthRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
@@ -65,12 +134,63 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/magic-link/$magic-link': {
|
||||
id: '/magic-link/$magic-link'
|
||||
path: '/magic-link/$magic-link'
|
||||
fullPath: '/magic-link/$magic-link'
|
||||
preLoaderRoute: typeof MagicLinkMagicLinkRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/interaction-panel/$id': {
|
||||
id: '/interaction-panel/$id'
|
||||
path: '/interaction-panel/$id'
|
||||
fullPath: '/interaction-panel/$id'
|
||||
preLoaderRoute: typeof InteractionPanelIdRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/auth/username': {
|
||||
id: '/auth/username'
|
||||
path: '/username'
|
||||
fullPath: '/auth/username'
|
||||
preLoaderRoute: typeof AuthUsernameRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/auth/mail': {
|
||||
id: '/auth/mail'
|
||||
path: '/mail'
|
||||
fullPath: '/auth/mail'
|
||||
preLoaderRoute: typeof AuthMailRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/auth/code': {
|
||||
id: '/auth/code'
|
||||
path: '/code'
|
||||
fullPath: '/auth/code'
|
||||
preLoaderRoute: typeof AuthCodeRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthRouteChildren {
|
||||
AuthCodeRoute: typeof AuthCodeRoute
|
||||
AuthMailRoute: typeof AuthMailRoute
|
||||
AuthUsernameRoute: typeof AuthUsernameRoute
|
||||
}
|
||||
|
||||
const AuthRouteChildren: AuthRouteChildren = {
|
||||
AuthCodeRoute: AuthCodeRoute,
|
||||
AuthMailRoute: AuthMailRoute,
|
||||
AuthUsernameRoute: AuthUsernameRoute,
|
||||
}
|
||||
|
||||
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
InteractionPanelRoute: InteractionPanelRoute,
|
||||
AuthRoute: AuthRouteWithChildren,
|
||||
InteractionPanelIdRoute: InteractionPanelIdRoute,
|
||||
MagicLinkMagicLinkRoute: MagicLinkMagicLinkRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
// src/routes/__root.tsx
|
||||
import { Outlet, createRootRoute } from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||
import { MainMenu } from "@/components/common/main-menu";
|
||||
import { useAccountStore } from "@/contexts/AccountContext";
|
||||
import { MainMenu } from "@/components/main-menu/main-menu";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: RootLayout,
|
||||
component: () => <RootLayout />,
|
||||
});
|
||||
const RootLayout = observer(() => {
|
||||
const { userId } = useAccountStore();
|
||||
|
||||
function RootLayout() {
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<MainMenu />
|
||||
{userId && <MainMenu />}
|
||||
|
||||
<div className="flex flex-col flex-1">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
@@ -18,7 +20,7 @@ function RootLayout() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TanStackRouterDevtools />
|
||||
{/* <TanStackRouterDevtools /> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
22
src/routes/auth.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createFileRoute, Link, Outlet } from "@tanstack/react-router";
|
||||
import LogoWithText from "@/icons/logo-with-text.svg?react";
|
||||
|
||||
export const Route = createFileRoute("/auth")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<div className="h-full pt-5">
|
||||
<Link
|
||||
to="/"
|
||||
viewTransition={{ types: ["warp"] }}
|
||||
className="px-10 h-[52px] flex items-center absolute"
|
||||
>
|
||||
<LogoWithText />
|
||||
</Link>
|
||||
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
181
src/routes/auth/code.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { TextLink } from "@/components/ui/text-link";
|
||||
|
||||
export const Route = createFileRoute("/auth/code")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
const [code, setCode] = useState<string[]>(Array(6).fill(""));
|
||||
|
||||
const [resendTimeout, setResendTimeout] = useState(() => {
|
||||
const savedTimeout = localStorage.getItem("resendTimeout");
|
||||
const savedTimestamp = localStorage.getItem("resendTimeoutTimestamp");
|
||||
|
||||
if (savedTimeout && savedTimestamp) {
|
||||
const secondsPassed = Math.floor(
|
||||
(Date.now() - Number(savedTimestamp)) / 1000,
|
||||
);
|
||||
const remainingTime = Math.max(0, Number(savedTimeout) - secondsPassed);
|
||||
return remainingTime > 0 ? Math.floor(remainingTime) : 0;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
document.getElementById("code-input-0")?.focus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (resendTimeout <= 0) {
|
||||
localStorage.removeItem("resendTimeout");
|
||||
localStorage.removeItem("resendTimeoutTimestamp");
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem("resendTimeout", resendTimeout.toString());
|
||||
localStorage.setItem("resendTimeoutTimestamp", Date.now().toString());
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setResendTimeout(resendTimeout - 1);
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [resendTimeout]);
|
||||
|
||||
const handleChange = (index: number, value: string) => {
|
||||
if (!/^\d?$/.test(value)) return;
|
||||
|
||||
const newCode = [...code];
|
||||
newCode[index] = value;
|
||||
setCode(newCode);
|
||||
|
||||
if (value && index < 5) {
|
||||
const nextInput = document.getElementById(`code-input-${index + 1}`);
|
||||
nextInput?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (
|
||||
index: number,
|
||||
e: React.KeyboardEvent<HTMLInputElement>,
|
||||
) => {
|
||||
if (e.key === "Backspace") {
|
||||
const newCode = [...code];
|
||||
|
||||
if (code[index] === "") {
|
||||
if (index > 0) {
|
||||
newCode[index - 1] = "";
|
||||
setCode(newCode);
|
||||
document.getElementById(`code-input-${index - 1}`)?.focus();
|
||||
}
|
||||
} else {
|
||||
newCode[index] = "";
|
||||
setCode(newCode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
||||
const pasted = e.clipboardData
|
||||
.getData("Text")
|
||||
.replace(/\D/g, "")
|
||||
.slice(0, 6);
|
||||
if (!pasted) return;
|
||||
|
||||
const newCode = [...code];
|
||||
for (let i = 0; i < pasted.length; i++) {
|
||||
newCode[i] = pasted[i];
|
||||
}
|
||||
setCode(newCode);
|
||||
|
||||
const nextIndex = pasted.length < 6 ? pasted.length : 5;
|
||||
document.getElementById(`code-input-${nextIndex}`)?.focus();
|
||||
};
|
||||
|
||||
const handleResendClick = () => {
|
||||
setResendTimeout(60);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const fullCode = code.join("");
|
||||
console.log("Confirm code:", fullCode);
|
||||
navigate({
|
||||
to: "/auth/username",
|
||||
viewTransition: { types: ["warp"] },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full gap-10 tracking-[-2%] flex-col">
|
||||
<div className="text-brand-gray text-center">
|
||||
<p className="font-medium text-[48px] leading-[120%]">
|
||||
Welcome to Cytonic!
|
||||
</p>
|
||||
<p className="font-medium text-[32px] leading-[120%]">Sign in</p>
|
||||
<p className="text-text-light-200 whitespace-nowrap mt-4">
|
||||
We’ve sent a code to{" "}
|
||||
<span className="text-text-light-900">email@example.com </span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (code.every((c) => c !== "")) {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
className="space-y-10"
|
||||
>
|
||||
<div className="flex space-x-2">
|
||||
{code.map((digit, i) => (
|
||||
<Input
|
||||
key={i}
|
||||
id={`code-input-${i}`}
|
||||
className="w-10 h-10 p-0 text-center text-lg"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="\d*"
|
||||
maxLength={1}
|
||||
value={digit}
|
||||
onPaste={handlePaste}
|
||||
onChange={(e) => handleChange(i, e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(i, e)}
|
||||
autoComplete="one-time-code"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
type="submit"
|
||||
className="text-[18px] w-full"
|
||||
disabled={code.some((c) => c === "")}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
|
||||
<div className="text-[14px] w-[286px] whitespace-nowrap">
|
||||
<span className="tracking-[-2%] text-text-light-200">
|
||||
Didn't receive a code?
|
||||
</span>
|
||||
{resendTimeout > 0 ? (
|
||||
<span className="font-[500] ml-4">
|
||||
Next resend in {resendTimeout}s
|
||||
</span>
|
||||
) : (
|
||||
<TextLink className="font-[500] ml-4" onClick={handleResendClick}>
|
||||
[ Click to resend ]
|
||||
</TextLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
src/routes/auth/mail.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Route = createFileRoute("/auth/mail")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const [email, setEmail] = useState("");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (email.trim() === "") return;
|
||||
navigate({
|
||||
to: "/auth/code",
|
||||
viewTransition: { types: ["warp"] },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full gap-10 tracking-[-2%] flex-col">
|
||||
<div className="text-brand-gray text-center">
|
||||
<p className="font-medium text-[48px] leading-[120%] ">
|
||||
Welcome to Cytonic!
|
||||
</p>
|
||||
<p className="font-medium text-[32px] leading-[120%]">Sign in</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 w-[300px]">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
label="Email"
|
||||
placeholder="Email@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<Button
|
||||
className="text-[18px] w-full"
|
||||
disabled={email.trim() === ""}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Send code
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
75
src/routes/auth/username.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useAccountStore } from "@/contexts/AccountContext";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Route = createFileRoute("/auth/username")({
|
||||
component: () => <RouteComponent />,
|
||||
});
|
||||
|
||||
const RouteComponent = observer(() => {
|
||||
const [username, setUsername] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const { setUserId } = useAccountStore();
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (username.trim() === "") return;
|
||||
|
||||
setUserId("ID");
|
||||
navigate({
|
||||
to: "/",
|
||||
viewTransition: { types: ["warp"] },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full gap-10 tracking-[-2%] flex-col">
|
||||
<div className="text-brand-gray text-center">
|
||||
<p className="font-medium text-[48px] leading-[120%] ">
|
||||
Welcome to Cytonic!
|
||||
</p>
|
||||
<p className="font-medium text-[32px] leading-[120%]">Sign in</p>
|
||||
<p className="text-text-light-200 whitespace-nowrap mt-4">
|
||||
Create a username
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 w-[300px]">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
label="Username"
|
||||
placeholder="Pink Axolotl"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
className="text-[18px] w-full"
|
||||
disabled={username.trim() === ""}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className="text-[18px] w-full"
|
||||
onClick={() => {
|
||||
navigate({ to: "/", viewTransition: { types: ["warp"] } });
|
||||
}}
|
||||
>
|
||||
Skip for now
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,14 +1,88 @@
|
||||
import { TextLink } from "@/components/ui/text-link";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
|
||||
import LogoWithText from "@/icons/logo-with-text.svg?react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAccountStore } from "@/contexts/AccountContext";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Clock from "@/icons/clock.svg?react";
|
||||
import { ChatInput } from "@/components/interaction-panel/chat";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: App,
|
||||
component: () => <App />,
|
||||
});
|
||||
|
||||
function App() {
|
||||
const App = observer(() => {
|
||||
const { userId } = useAccountStore();
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<TextLink>Lable</TextLink>
|
||||
<div className="h-full ">
|
||||
{!userId && (
|
||||
<header className=" pt-5 px-10 h-[52px] flex items-center justify-between absolute w-full">
|
||||
<LogoWithText />
|
||||
<Link to="/auth/mail" viewTransition={{ types: ["warp"] }}>
|
||||
<Button className=" text-[18px] w-[106px] cursor-pointer">
|
||||
Sign in
|
||||
</Button>
|
||||
</Link>
|
||||
</header>
|
||||
)}
|
||||
|
||||
<div className="flex-1 h-full flex items-center justify-center">
|
||||
<WelcomePanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const WelcomePanel = () => {
|
||||
const navigate = useNavigate();
|
||||
const { userId } = useAccountStore();
|
||||
|
||||
return (
|
||||
<div className="max-w-[741px] flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-2 font-medium">
|
||||
<span className="leading-[120%] text-black text-[24px]">
|
||||
Hi!
|
||||
<br /> I will help you create a crypto project on Cytonic
|
||||
</span>
|
||||
<span className="text-[#C0C0C0] text-[14px] flex gap-2 items-center">
|
||||
<Clock />
|
||||
<p>Estimated build time: 3 minutes</p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-[#C0C0C0] text-[14px] font-medium leading-[120%]">
|
||||
Choose one of the most common prompts
|
||||
<br /> below or use your own to begin
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
{[
|
||||
{ emoji: "🐶", text: "Build a memecoin launchpad" },
|
||||
{ emoji: "🏦", text: "Build a new stablecoin protocol" },
|
||||
{ emoji: "🌐", text: "Create a RWA marketplace on Cytonic" },
|
||||
{ emoji: "📊", text: "Ship a token dashboard" },
|
||||
].map(({ emoji, text }) => (
|
||||
<button
|
||||
type="button"
|
||||
key={emoji}
|
||||
onClick={() => {
|
||||
if (userId === undefined) {
|
||||
navigate({
|
||||
to: "/auth/mail",
|
||||
viewTransition: { types: ["warp"] },
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="text-left cursor-pointer hover:bg-fill-100 transition-all w-[173px] h-[123px] p-4 rounded-[16px] border border-[#E8E8E8] flex flex-col justify-between"
|
||||
>
|
||||
<p className="text-[14px] font-medium leading-[120%]">{text}</p>
|
||||
<div className="text-[24px] leading-[100%]">{emoji}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ChatInput />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// src/routes/chat.tsx
|
||||
import { Chat } from "@/components/interaction-panel/chat";
|
||||
import { Sandbox } from "@/components/interaction-panel/sandbox";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/interaction-panel")({
|
||||
component: InteractionPanelRoute,
|
||||
});
|
||||
|
||||
function InteractionPanelRoute() {
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
<Chat />
|
||||
<Sandbox />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
98
src/routes/interaction-panel/$id.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Chat } from "@/components/interaction-panel/chat";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import {
|
||||
SandpackCodeEditor,
|
||||
SandpackFileExplorer,
|
||||
SandpackLayout,
|
||||
SandpackProvider,
|
||||
} from "@codesandbox/sandpack-react";
|
||||
import { Sandpack } from "@codesandbox/sandpack-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
export const Route = createFileRoute("/interaction-panel/$id")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
<Chat />
|
||||
<Sandbox />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Sandbox = observer(() => {
|
||||
const [previewState, setPreviewState] = useState<"preview" | "code">(
|
||||
"preview",
|
||||
);
|
||||
const togglePreviewState = () => {
|
||||
setPreviewState((prev) => (prev === "preview" ? "code" : "preview"));
|
||||
};
|
||||
|
||||
const files = {
|
||||
"/test.js": `const test = "content"`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-[40svw] p-0 px-2 pt-4 pb-2 font-work transition-all">
|
||||
<div className="flex h-full flex-col gap-4">
|
||||
<div className="flex min-h-[42px] flex-row items-center justify-between">
|
||||
<div className="flex h-full flex-row gap-0.5 rounded-lg bg-[#F5F5F5] p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
data-active={previewState === "preview"}
|
||||
onClick={togglePreviewState}
|
||||
className="group inline-flex cursor-pointer flex-row items-center justify-between gap-1 rounded-lg px-4 transition-all data-[active=true]:bg-[#D6D6D6]"
|
||||
>
|
||||
<div className="mask-[url('/icons/Preview.svg')] size-6 bg-[#939393] bg-center bg-cover transition-colors group-data-[active=true]:bg-black" />
|
||||
Preview
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-active={previewState === "code"}
|
||||
onClick={togglePreviewState}
|
||||
className="group inline-flex cursor-pointer flex-row items-center justify-between gap-1 rounded-lg px-4 transition-all data-[active=true]:bg-[#D6D6D6]"
|
||||
>
|
||||
<div className="mask-[url('/icons/Code.svg')] size-6 bg-[#939393] bg-center bg-cover transition-colors group-data-[active=true]:bg-black" />
|
||||
Code
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-active={previewState === "code"}
|
||||
className="hidden w-full data-[active=true]:flex"
|
||||
>
|
||||
<SandpackProvider files={{}} theme="light" template="react">
|
||||
<SandpackLayout>
|
||||
<SandpackFileExplorer
|
||||
style={{ height: "100svh", width: "30%" }}
|
||||
/>
|
||||
<SandpackCodeEditor
|
||||
closableTabs
|
||||
showTabs
|
||||
style={{ height: "100svh", width: "70%" }}
|
||||
/>
|
||||
</SandpackLayout>
|
||||
</SandpackProvider>
|
||||
</div>
|
||||
<div
|
||||
data-active={previewState === "preview"}
|
||||
className="hidden h-full w-full data-[active=true]:block"
|
||||
>
|
||||
<Sandpack
|
||||
theme="light"
|
||||
template="react"
|
||||
options={{
|
||||
editorWidthPercentage: 0,
|
||||
editorHeight: "88.75svh",
|
||||
showNavigator: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
194
src/routes/magic-link/$magic-link.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
|
||||
import { TextLink } from "@/components/ui/text-link";
|
||||
import LogoWithText from "@/icons/logo-with-text.svg?react";
|
||||
|
||||
export const Route = createFileRoute("/magic-link/$magic-link")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
const { "magic-link": magicLink } = Route.useParams();
|
||||
|
||||
const [code, setCode] = useState<string[]>(Array(6).fill(""));
|
||||
const [isAutoSubmitted, _] = useState(false);
|
||||
|
||||
const [resendTimeout, setResendTimeout] = useState(() => {
|
||||
const savedTimeout = localStorage.getItem("resendTimeout");
|
||||
const savedTimestamp = localStorage.getItem("resendTimeoutTimestamp");
|
||||
|
||||
if (savedTimeout && savedTimestamp) {
|
||||
const secondsPassed = Math.floor(
|
||||
(Date.now() - Number(savedTimestamp)) / 1000,
|
||||
);
|
||||
const remainingTime = Math.max(0, Number(savedTimeout) - secondsPassed);
|
||||
return remainingTime > 0 ? Math.floor(remainingTime) : 0;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (magicLink && /^\d{6}$/.test(magicLink)) {
|
||||
const codeArray = magicLink.split("").slice(0, 6);
|
||||
setCode(codeArray);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
handleConfirm(codeArray.join(""));
|
||||
}, 800);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [magicLink]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resendTimeout <= 0) {
|
||||
localStorage.removeItem("resendTimeout");
|
||||
localStorage.removeItem("resendTimeoutTimestamp");
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem("resendTimeout", resendTimeout.toString());
|
||||
localStorage.setItem("resendTimeoutTimestamp", Date.now().toString());
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setResendTimeout(resendTimeout - 1);
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [resendTimeout]);
|
||||
|
||||
const handleChange = (index: number, value: string) => {
|
||||
if (!/^\d?$/.test(value)) return;
|
||||
|
||||
const newCode = [...code];
|
||||
newCode[index] = value;
|
||||
setCode(newCode);
|
||||
|
||||
if (value && index < 5) {
|
||||
const nextInput = document.getElementById(`code-input-${index + 1}`);
|
||||
nextInput?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (
|
||||
index: number,
|
||||
e: React.KeyboardEvent<HTMLInputElement>,
|
||||
) => {
|
||||
if (e.key === "Backspace") {
|
||||
const newCode = [...code];
|
||||
|
||||
if (code[index] === "") {
|
||||
if (index > 0) {
|
||||
newCode[index - 1] = "";
|
||||
setCode(newCode);
|
||||
document.getElementById(`code-input-${index - 1}`)?.focus();
|
||||
}
|
||||
} else {
|
||||
newCode[index] = "";
|
||||
setCode(newCode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
||||
const pasted = e.clipboardData
|
||||
.getData("Text")
|
||||
.replace(/\D/g, "")
|
||||
.slice(0, 6);
|
||||
if (!pasted) return;
|
||||
|
||||
const newCode = [...code];
|
||||
for (let i = 0; i < pasted.length; i++) {
|
||||
newCode[i] = pasted[i];
|
||||
}
|
||||
setCode(newCode);
|
||||
|
||||
const nextIndex = pasted.length < 6 ? pasted.length : 5;
|
||||
document.getElementById(`code-input-${nextIndex}`)?.focus();
|
||||
};
|
||||
|
||||
const handleResendClick = () => {
|
||||
setResendTimeout(60);
|
||||
};
|
||||
|
||||
const handleConfirm = (fullCode: string) => {
|
||||
console.log(fullCode);
|
||||
navigate({
|
||||
to: "/auth/username",
|
||||
viewTransition: { types: ["warp"] },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full pt-5">
|
||||
<Link
|
||||
to="/"
|
||||
viewTransition={{ types: ["warp"] }}
|
||||
className="px-10 h-[52px] flex items-center absolute"
|
||||
>
|
||||
<LogoWithText />
|
||||
</Link>
|
||||
<div className="flex items-center justify-center h-full gap-10 tracking-[-2%] flex-col">
|
||||
<div className="text-brand-gray text-center">
|
||||
<p className="font-medium text-[48px] leading-[120%]">
|
||||
Welcome to Cytonic!
|
||||
</p>
|
||||
<p className="font-medium text-[32px] leading-[120%]">Sign in</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-10">
|
||||
<div className="flex space-x-2">
|
||||
{code.map((digit, i) => (
|
||||
<Input
|
||||
key={i}
|
||||
id={"code-input-${i}"}
|
||||
className="w-10 h-10 p-0 text-center text-lg"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="\d*"
|
||||
maxLength={1}
|
||||
value={digit}
|
||||
onPaste={handlePaste}
|
||||
onChange={(e) => handleChange(i, e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(i, e)}
|
||||
autoComplete="one-time-code"
|
||||
readOnly={isAutoSubmitted}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
className="text-[18px] w-full"
|
||||
disabled={code.some((c) => c === "") || isAutoSubmitted}
|
||||
onClick={() => handleConfirm(code.join(""))}
|
||||
>
|
||||
{isAutoSubmitted ? "Processing..." : "Confirm"}
|
||||
</Button>
|
||||
|
||||
<div className="text-[14px] w-[286px] whitespace-nowrap">
|
||||
<span className="tracking-[-2%] text-text-light-200">
|
||||
Didn't receive a code?
|
||||
</span>
|
||||
{resendTimeout > 0 ? (
|
||||
<span className="font-[500] ml-4">
|
||||
Next resend in {resendTimeout}s
|
||||
</span>
|
||||
) : (
|
||||
<TextLink
|
||||
className="font-[500] ml-4"
|
||||
onClick={handleResendClick}
|
||||
>
|
||||
[ Click to resend ]
|
||||
</TextLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { LS_TOKENS } from "@/lib/constants";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
export class AccountStore {
|
||||
userId?: string;
|
||||
|
||||
isMainMenuOpen: boolean;
|
||||
isSandboxOpen: boolean;
|
||||
|
||||
@@ -12,6 +14,9 @@ export class AccountStore {
|
||||
this.isMainMenuOpen = savedState ? savedState === "true" : false;
|
||||
|
||||
this.isSandboxOpen = false;
|
||||
|
||||
const savedUserId = localStorage.getItem(LS_TOKENS.userId);
|
||||
this.userId = savedUserId ? savedUserId : undefined;
|
||||
}
|
||||
|
||||
setIsMainMenuOpen(value: boolean) {
|
||||
@@ -22,4 +27,9 @@ export class AccountStore {
|
||||
setIsSandboxOpen(value: boolean) {
|
||||
this.isSandboxOpen = value;
|
||||
}
|
||||
|
||||
setUserId(value: string) {
|
||||
this.userId = value;
|
||||
localStorage.setItem(LS_TOKENS.userId, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
@theme {
|
||||
--font-work: "work-sans", "sans-serif";
|
||||
}
|
||||
|
||||
textarea:focus,
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-bg-dark:#212121;
|
||||
--color-bg-light:#FFFFFF;
|
||||
--color-brand-gray:#201E1E;
|
||||
|
||||
--color-text-light-900:#0A0A0A;
|
||||
--color-text-light-500:#939393;
|
||||
@@ -20,6 +27,7 @@
|
||||
--color-fill-700:#363636;
|
||||
--color-fill-400:#939393;
|
||||
--color-fill-300:#C0C0C0;
|
||||
--color-fill-200:#D6D6D6;
|
||||
--color-fill-150:#E8E8E8;
|
||||
--color-fill-100:#F5F5F5;
|
||||
|
||||
@@ -151,3 +159,16 @@
|
||||
.toast-progress-animation {
|
||||
animation: toast-progress 5s linear forwards;
|
||||
}
|
||||
|
||||
.custom-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.custom-layout {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.custom-file-explorer .custom-file-explorer-list {
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||