From bba27be7e243b93b9994c5fdeeeafc1992947a55 Mon Sep 17 00:00:00 2001 From: yzned Date: Sun, 20 Jul 2025 18:09:45 +0300 Subject: [PATCH] update routing --- src/api/api.ts | 51 ++++++++ src/components/interaction-panel/chat.tsx | 66 +---------- src/components/main-menu/collapsed-menu.tsx | 1 - src/components/main-menu/main-menu.tsx | 109 +++++++++++------- src/icons/three-dots.svg | 3 + src/routeTree.gen.ts | 42 +++---- src/routes/__root.tsx | 9 +- src/routes/auth/username.tsx | 11 +- src/routes/index.tsx | 75 ++++++++++-- .../$id.tsx} | 7 +- 10 files changed, 229 insertions(+), 145 deletions(-) create mode 100644 src/api/api.ts delete mode 100644 src/components/main-menu/collapsed-menu.tsx create mode 100644 src/icons/three-dots.svg rename src/routes/{interaction-panel.tsx => interaction-panel/$id.tsx} (64%) diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 0000000..e43af5a --- /dev/null +++ b/src/api/api.ts @@ -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[][], + ) { + 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 }; diff --git a/src/components/interaction-panel/chat.tsx b/src/components/interaction-panel/chat.tsx index 28fe54c..642eaea 100644 --- a/src/components/interaction-panel/chat.tsx +++ b/src/components/interaction-panel/chat.tsx @@ -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 ( -
- -
+
{id}
); }); -const WelcomePanel = () => { - const navigate = useNavigate(); - const { userId } = useAccountStore(); - const { toast } = useToast(); - - return ( -
-
- - Hi! -
I will help you create a crypto project on Cytonic -
- - -

Estimated build time: 3 minutes

-
-
- -
-

- Choose one of the most common prompts -
below or use your own to begin -

-
- {[ - { 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 }) => ( - - ))} -
- - -
-
- ); -}; - const ChatInput = () => { const navigate = useNavigate(); const { userId } = useAccountStore(); @@ -156,4 +98,4 @@ const ChatInput = () => { ); }; -export { Chat, WelcomePanel, ChatInput }; +export { Chat, ChatInput }; diff --git a/src/components/main-menu/collapsed-menu.tsx b/src/components/main-menu/collapsed-menu.tsx deleted file mode 100644 index 8b13789..0000000 --- a/src/components/main-menu/collapsed-menu.tsx +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/components/main-menu/main-menu.tsx b/src/components/main-menu/main-menu.tsx index f3682e2..b945932 100644 --- a/src/components/main-menu/main-menu.tsx +++ b/src/components/main-menu/main-menu.tsx @@ -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,39 +42,13 @@ const MENU_ITEMS: MenuItemType[] = [ }, ]; -const MenuItem = ({ item }: { item: MenuItemType }) => { - const { isMainMenuOpen } = useAccountStore(); - - return ( - - -
- {item.name} -
- - ); -}; - const MainMenu = observer(() => { const { isMainMenuOpen } = useAccountStore(); return (
@@ -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 (
{ Recent

-
    +
      {MOCK_CHATS.map((chat) => ( -
    • -

      - {chat.title} -

      -

      - {chat.subtitle} -

      -
    • + /> ))}
@@ -196,4 +164,63 @@ const MainMenuContent = observer(() => { ); }); +const MenuItem = ({ item }: { item: MenuItemType }) => { + const { isMainMenuOpen } = useAccountStore(); + + return ( + + +
+ {item.name} +
+ + ); +}; + +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 ( +
+ +
+ {chat.title} +
+ + + +
+ ); +}; + export { MainMenu, MenuItem, MENU_ITEMS, MainMenuContent }; diff --git a/src/icons/three-dots.svg b/src/icons/three-dots.svg new file mode 100644 index 0000000..2cce1ff --- /dev/null +++ b/src/icons/three-dots.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 2d8cda3..cfce1c5 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -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 diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 094d416..5f277fb 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -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: () => , }); - -function RootLayout() { +const RootLayout = observer(() => { const { userId } = useAccountStore(); return ( @@ -24,4 +23,4 @@ function RootLayout() { {/* */} ); -} +}); diff --git a/src/routes/auth/username.tsx b/src/routes/auth/username.tsx index 6177d87..6633bbd 100644 --- a/src/routes/auth/username.tsx +++ b/src/routes/auth/username.tsx @@ -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: () => , }); -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() { ); -} +}); diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 1db4189..15f29ea 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -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: () => , }); -function App() { +const App = observer(() => { const { userId } = useAccountStore(); return ( -
+
{!userId && ( -
+
)} - + +
+ +
); -} +}); + +const WelcomePanel = () => { + const navigate = useNavigate(); + const { userId } = useAccountStore(); + + return ( +
+
+ + Hi! +
I will help you create a crypto project on Cytonic +
+ + +

Estimated build time: 3 minutes

+
+
+ +
+

+ Choose one of the most common prompts +
below or use your own to begin +

+
+ {[ + { 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 }) => ( + + ))} +
+ + +
+
+ ); +}; diff --git a/src/routes/interaction-panel.tsx b/src/routes/interaction-panel/$id.tsx similarity index 64% rename from src/routes/interaction-panel.tsx rename to src/routes/interaction-panel/$id.tsx index 6fa1021..9f4f0d1 100644 --- a/src/routes/interaction-panel.tsx +++ b/src/routes/interaction-panel/$id.tsx @@ -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 (