update routing

This commit is contained in:
yzned
2025-07-20 18:09:45 +03:00
committed by yzned
parent 42b27b7ddc
commit bba27be7e2
10 changed files with 229 additions and 145 deletions

51
src/api/api.ts Normal file
View 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 };

View File

@@ -2,77 +2,19 @@ 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 Clock from "@/icons/clock.svg?react";
import { cn } from "@/lib/utils";
import { useNavigate } from "@tanstack/react-router";
import { useNavigate, useParams } from "@tanstack/react-router";
import { useAccountStore } from "@/contexts/AccountContext";
import { Button } from "../ui/button";
import { useToast } from "@/lib/hooks/use-toast";
const Chat = observer(() => {
const { id } = useParams({ from: "/interaction-panel/$id" });
return (
<div className="flex-1 h-full flex items-center justify-center">
<WelcomePanel />
</div>
<div className="flex-1 h-full flex items-center justify-center">{id}</div>
);
});
const WelcomePanel = () => {
const navigate = useNavigate();
const { userId } = useAccountStore();
const { toast } = useToast();
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>
);
};
const ChatInput = () => {
const navigate = useNavigate();
const { userId } = useAccountStore();
@@ -156,4 +98,4 @@ const ChatInput = () => {
);
};
export { Chat, WelcomePanel, ChatInput };
export { Chat, ChatInput };

View File

@@ -1 +0,0 @@

View File

@@ -4,11 +4,12 @@ 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 ThreeDots from "@/icons/three-dots.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 { Link, useMatchRoute, useParams } from "@tanstack/react-router";
import Logo from "@/icons/logo.svg?react";
import LogoWithText from "@/icons/logo-with-text.svg?react";
@@ -41,32 +42,6 @@ const MENU_ITEMS: MenuItemType[] = [
},
];
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 MainMenu = observer(() => {
const { isMainMenuOpen } = useAccountStore();
@@ -83,7 +58,6 @@ const MainMenu = observer(() => {
const MOCK_CHATS = Array.from({ length: 20 }, (_, i) => ({
id: i,
title: `Chat ${i + 1}`,
subtitle: `Last message preview...`,
}));
const MainMenuContent = observer(() => {
@@ -109,8 +83,9 @@ const MainMenuContent = observer(() => {
return (
<div
data-open={isMainMenuOpen}
ref={containerRef}
className="relative w-full h-full overflow-auto flex flex-col"
className="relative w-full h-full overflow-x-hidden data-[open=true]:overflow-y-auto overflow-y-hidden flex flex-col"
>
<header
data-scrolled={hasScrolled}
@@ -160,19 +135,12 @@ const MainMenuContent = observer(() => {
Recent
</p>
<ul className="space-y-2 pb-10">
<ul className=" pb-10">
{MOCK_CHATS.map((chat) => (
<li
<ChatCell
chat={{ id: chat.id.toString(), title: chat.title }}
key={chat.id}
className="rounded-md px-3 py-2 hover:bg-fill-100 cursor-pointer transition-colors"
>
<p className="text-sm font-medium text-text-light-900 truncate">
{chat.title}
</p>
<p className="text-xs text-text-light-400 truncate">
{chat.subtitle}
</p>
</li>
/>
))}
</ul>
</div>
@@ -196,4 +164,63 @@ const MainMenuContent = observer(() => {
);
});
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 ChatCell = ({ chat }: { chat: { id: string; title: string } }) => {
const matchRoute = useMatchRoute();
const match = matchRoute({ to: "/interaction-panel/$id", fuzzy: true });
const activeChatId = match ? match.id : undefined;
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>
<button
className="opacity-0 group-hover:opacity-100 duration-300 cursor-pointer flex items-center pr-3 justify-end w-5 h-full"
type="button"
>
<ThreeDots />
</button>
</div>
);
};
export { MainMenu, MenuItem, MENU_ITEMS, MainMenuContent };

3
src/icons/three-dots.svg Normal file
View 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

View File

@@ -9,19 +9,14 @@
// 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',
getParentRoute: () => rootRouteImport,
} as any)
const AuthRoute = AuthRouteImport.update({
id: '/auth',
path: '/auth',
@@ -37,6 +32,11 @@ const MagicLinkMagicLinkRoute = MagicLinkMagicLinkRouteImport.update({
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',
@@ -56,29 +56,29 @@ const AuthCodeRoute = AuthCodeRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/auth': typeof AuthRouteWithChildren
'/interaction-panel': typeof InteractionPanelRoute
'/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
'/auth': typeof AuthRouteWithChildren
'/interaction-panel': typeof InteractionPanelRoute
'/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
'/auth': typeof AuthRouteWithChildren
'/interaction-panel': typeof InteractionPanelRoute
'/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 {
@@ -86,47 +86,40 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/auth'
| '/interaction-panel'
| '/auth/code'
| '/auth/mail'
| '/auth/username'
| '/interaction-panel/$id'
| '/magic-link/$magic-link'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/auth'
| '/interaction-panel'
| '/auth/code'
| '/auth/mail'
| '/auth/username'
| '/interaction-panel/$id'
| '/magic-link/$magic-link'
id:
| '__root__'
| '/'
| '/auth'
| '/interaction-panel'
| '/auth/code'
| '/auth/mail'
| '/auth/username'
| '/interaction-panel/$id'
| '/magic-link/$magic-link'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AuthRoute: typeof AuthRouteWithChildren
InteractionPanelRoute: typeof InteractionPanelRoute
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
parentRoute: typeof rootRouteImport
}
'/auth': {
id: '/auth'
path: '/auth'
@@ -148,6 +141,13 @@ declare module '@tanstack/react-router' {
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'
@@ -189,7 +189,7 @@ const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AuthRoute: AuthRouteWithChildren,
InteractionPanelRoute: InteractionPanelRoute,
InteractionPanelIdRoute: InteractionPanelIdRoute,
MagicLinkMagicLinkRoute: MagicLinkMagicLinkRoute,
}
export const routeTree = rootRouteImport

View File

@@ -1,14 +1,13 @@
// src/routes/__root.tsx
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
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 />,
});
function RootLayout() {
const RootLayout = observer(() => {
const { userId } = useAccountStore();
return (
@@ -24,4 +23,4 @@ function RootLayout() {
{/* <TanStackRouterDevtools /> */}
</div>
);
}
});

View File

@@ -1,18 +1,23 @@
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,
component: () => <RouteComponent />,
});
function 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"] },
@@ -67,4 +72,4 @@ function RouteComponent() {
</div>
</div>
);
}
});

View File

@@ -1,20 +1,22 @@
import { Chat } from "@/components/interaction-panel/chat";
import { createFileRoute, Link } 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="h-full pt-5">
<div className="h-full ">
{!userId && (
<header className="px-10 h-[52px] flex items-center justify-between absolute w-full">
<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">
@@ -23,7 +25,64 @@ function App() {
</Link>
</header>
)}
<Chat />
<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>
);
};

View File

@@ -1,13 +1,12 @@
// 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,
export const Route = createFileRoute("/interaction-panel/$id")({
component: RouteComponent,
});
function InteractionPanelRoute() {
function RouteComponent() {
return (
<div className="flex h-full">
<Chat />