this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Create monorepo structure with backend and frontend wired with shared library for types and mock data

+339 -176
+15
apps/api/package.json
··· 1 + { 2 + "name": "@cai/api", 3 + "private": true, 4 + "type": "module", 5 + "exports": { 6 + ".": "./src/index.ts" 7 + }, 8 + "scripts": { 9 + "dev": "bun --watch src/index.ts" 10 + }, 11 + "dependencies": { 12 + "@cai/shared": "workspace:*", 13 + "hono": "^4.12.15" 14 + } 15 + }
+46
apps/api/src/index.ts
··· 1 + import { characters, chats } from "@cai/shared"; 2 + import { Hono } from "hono"; 3 + 4 + declare const Bun: { 5 + env: Record<string, string | undefined>; 6 + serve: (options: { 7 + fetch: (request: Request) => Response | Promise<Response>; 8 + port: number; 9 + }) => { 10 + port: number; 11 + url: URL; 12 + }; 13 + }; 14 + 15 + const api = new Hono() 16 + .get("/health", (c) => c.json({ ok: true })) 17 + .get("/characters", (c) => c.json(characters)) 18 + .get("/chats", (c) => { 19 + return c.json( 20 + chats.map((chat) => { 21 + const { messages, ...chatSummary } = chat; 22 + return chatSummary; 23 + }), 24 + ); 25 + }) 26 + .get("/chats/:id", (c) => { 27 + const chat = chats.find((item) => item.id === c.req.param("id")); 28 + 29 + if (!chat) { 30 + return c.json({ message: "Chat not found" }, 404); 31 + } 32 + 33 + return c.json(chat); 34 + }); 35 + 36 + export const app = new Hono().route("/api", api); 37 + 38 + export type AppType = typeof app; 39 + 40 + const port = Number(Bun.env.PORT ?? 3001); 41 + const server = Bun.serve({ 42 + fetch: (request) => app.fetch(request), 43 + port, 44 + }); 45 + 46 + console.log(`API listening on ${server.url}`);
+7
apps/api/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json", 3 + "include": ["src/**/*.ts"], 4 + "compilerOptions": { 5 + "lib": ["ES2022", "DOM"] 6 + } 7 + }
+25
apps/web/components.json
··· 1 + { 2 + "$schema": "https://ui.shadcn.com/schema.json", 3 + "style": "base-luma", 4 + "rsc": false, 5 + "tsx": true, 6 + "tailwind": { 7 + "config": "", 8 + "css": "src/styles.css", 9 + "baseColor": "zinc", 10 + "cssVariables": true, 11 + "prefix": "" 12 + }, 13 + "iconLibrary": "lucide", 14 + "rtl": false, 15 + "aliases": { 16 + "components": "#/components", 17 + "utils": "#/lib/utils", 18 + "ui": "#/components/ui", 19 + "lib": "#/lib", 20 + "hooks": "#/hooks" 21 + }, 22 + "menuColor": "default", 23 + "menuAccent": "subtle", 24 + "registries": {} 25 + }
+38
apps/web/package.json
··· 1 + { 2 + "name": "@cai/web", 3 + "private": true, 4 + "type": "module", 5 + "scripts": { 6 + "dev": "vite dev --port 3000", 7 + "build": "vite build", 8 + "preview": "vite preview" 9 + }, 10 + "dependencies": { 11 + "@base-ui/react": "^1.4.1", 12 + "@cai/api": "workspace:*", 13 + "@cai/shared": "workspace:*", 14 + "@fontsource-variable/dm-sans": "^5.2.8", 15 + "@tailwindcss/vite": "^4.1.18", 16 + "@tanstack/react-query": "^5.100.5", 17 + "@tanstack/react-router": "latest", 18 + "@tanstack/router-plugin": "^1.132.0", 19 + "class-variance-authority": "^0.7.1", 20 + "hono": "^4.12.15", 21 + "lucide-react": "^1.11.0", 22 + "react": "^19.2.0", 23 + "react-dom": "^19.2.0", 24 + "shadcn": "^4.5.0", 25 + "tailwindcss": "^4.1.18", 26 + "tw-animate-css": "^1.4.0" 27 + }, 28 + "devDependencies": { 29 + "@tailwindcss/typography": "^0.5.16", 30 + "@types/node": "^22.10.2", 31 + "@types/react": "^19.2.0", 32 + "@types/react-dom": "^19.2.0", 33 + "@vitejs/plugin-react": "^6.0.1", 34 + "clsx": "^2.1.1", 35 + "tailwind-merge": "^3.5.0", 36 + "vite": "^8.0.0" 37 + } 38 + }
+25
apps/web/public/manifest.json
··· 1 + { 2 + "short_name": "TanStack App", 3 + "name": "Create TanStack App Sample", 4 + "icons": [ 5 + { 6 + "src": "favicon.ico", 7 + "sizes": "64x64 32x32 24x24 16x16", 8 + "type": "image/x-icon" 9 + }, 10 + { 11 + "src": "logo192.png", 12 + "type": "image/png", 13 + "sizes": "192x192" 14 + }, 15 + { 16 + "src": "logo512.png", 17 + "type": "image/png", 18 + "sizes": "512x512" 19 + } 20 + ], 21 + "start_url": ".", 22 + "display": "standalone", 23 + "theme_color": "#000000", 24 + "background_color": "#ffffff" 25 + }
+17
apps/web/src/hooks/use-characters.ts
··· 1 + import { useQuery } from "@tanstack/react-query"; 2 + import { api } from "#/lib/api"; 3 + 4 + export function useCharacters() { 5 + return useQuery({ 6 + queryKey: ["characters"], 7 + queryFn: async () => { 8 + const response = await api.api.characters.$get(); 9 + 10 + if (!response.ok) { 11 + throw new Error("Failed to load characters"); 12 + } 13 + 14 + return response.json(); 15 + }, 16 + }); 17 + }
+34
apps/web/src/hooks/use-chat.ts
··· 1 + import { useQuery } from "@tanstack/react-query"; 2 + import { api } from "#/lib/api"; 3 + 4 + interface UseChatOptions { 5 + id: string; 6 + } 7 + 8 + export function useChat({ id }: UseChatOptions) { 9 + const chatQuery = useQuery({ 10 + queryKey: ["chat", id], 11 + queryFn: async () => { 12 + const response = await api.api.chats[":id"].$get({ 13 + param: { id }, 14 + }); 15 + 16 + if (response.status === 404) { 17 + return undefined; 18 + } 19 + 20 + if (!response.ok) { 21 + throw new Error("Failed to load chat"); 22 + } 23 + 24 + return response.json(); 25 + }, 26 + }); 27 + 28 + return { 29 + chat: chatQuery.data, 30 + messages: chatQuery.data?.messages ?? [], 31 + isLoading: chatQuery.isLoading, 32 + isError: chatQuery.isError, 33 + }; 34 + }
+17
apps/web/src/hooks/use-chats.ts
··· 1 + import { useQuery } from "@tanstack/react-query"; 2 + import { api } from "#/lib/api"; 3 + 4 + export function useChats() { 5 + return useQuery({ 6 + queryKey: ["chats"], 7 + queryFn: async () => { 8 + const response = await api.api.chats.$get(); 9 + 10 + if (!response.ok) { 11 + throw new Error("Failed to load chats"); 12 + } 13 + 14 + return response.json(); 15 + }, 16 + }); 17 + }
+4
apps/web/src/lib/api.ts
··· 1 + import type { AppType } from "@cai/api"; 2 + import { hc } from "hono/client"; 3 + 4 + export const api = hc<AppType>("/");
+1
apps/web/src/lib/types.ts
··· 1 + export type { Character, Chat, ChatMessage, ChatSummary } from "@cai/shared";
+12
apps/web/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json", 3 + "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"], 4 + "compilerOptions": { 5 + "jsx": "react-jsx", 6 + "paths": { 7 + "#/*": ["./src/*"] 8 + }, 9 + "lib": ["ES2022", "DOM", "DOM.Iterable"], 10 + "types": ["vite/client", "node"] 11 + } 12 + }
+6 -1
biome.json
··· 1 1 { 2 2 "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 3 "files": { 4 - "includes": ["src/**/*.{css,js,jsx,ts,tsx}", "!src/routeTree.gen.ts"] 4 + "includes": [ 5 + "apps/**/*.{html,css,js,jsx,ts,tsx,json}", 6 + "packages/**/*.{html,css,js,jsx,ts,tsx,json}", 7 + "!**/dist", 8 + "!apps/web/src/routeTree.gen.ts" 9 + ] 5 10 }, 6 11 "linter": { 7 12 "rules": {
+25 -1
bun.lock
··· 4 4 "workspaces": { 5 5 "": { 6 6 "name": "cai", 7 + "devDependencies": { 8 + "@biomejs/biome": "^2.4.13", 9 + }, 10 + }, 11 + "apps/api": { 12 + "name": "@cai/api", 13 + "dependencies": { 14 + "@cai/shared": "workspace:*", 15 + "hono": "^4.12.15", 16 + }, 17 + }, 18 + "apps/web": { 19 + "name": "@cai/web", 7 20 "dependencies": { 8 21 "@base-ui/react": "^1.4.1", 22 + "@cai/api": "workspace:*", 23 + "@cai/shared": "workspace:*", 9 24 "@fontsource-variable/dm-sans": "^5.2.8", 10 25 "@tailwindcss/vite": "^4.1.18", 11 26 "@tanstack/react-query": "^5.100.5", 12 27 "@tanstack/react-router": "latest", 13 28 "@tanstack/router-plugin": "^1.132.0", 14 29 "class-variance-authority": "^0.7.1", 30 + "hono": "^4.12.15", 15 31 "lucide-react": "^1.11.0", 16 32 "react": "^19.2.0", 17 33 "react-dom": "^19.2.0", ··· 20 36 "tw-animate-css": "^1.4.0", 21 37 }, 22 38 "devDependencies": { 23 - "@biomejs/biome": "^2.4.13", 24 39 "@tailwindcss/typography": "^0.5.16", 25 40 "@types/node": "^22.10.2", 26 41 "@types/react": "^19.2.0", ··· 30 45 "tailwind-merge": "^3.5.0", 31 46 "vite": "^8.0.0", 32 47 }, 48 + }, 49 + "packages/shared": { 50 + "name": "@cai/shared", 33 51 }, 34 52 }, 35 53 "packages": { ··· 112 130 "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A=="], 113 131 114 132 "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.13", "", { "os": "win32", "cpu": "x64" }, "sha512-tTcMkXyBrmHi9BfrD2VNHs/5rYIUKETqsBlYOvSAABwBkJhSDVb5e7wPukftsQbO3WzQkXe6kaztC6WtUOXSoQ=="], 133 + 134 + "@cai/api": ["@cai/api@workspace:apps/api"], 135 + 136 + "@cai/shared": ["@cai/shared@workspace:packages/shared"], 137 + 138 + "@cai/web": ["@cai/web@workspace:apps/web"], 115 139 116 140 "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.63.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.4", "which": "^4.0.0", "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-jjkmzIRu19uH78AjFInqfcALehbDCZZ7M09hurVawyqNxtOXEg2LR73L59y4QnzfYDEzjbhVzGAd2uDHu0D1aQ=="], 117 141
-25
components.json
··· 1 - { 2 - "$schema": "https://ui.shadcn.com/schema.json", 3 - "style": "base-luma", 4 - "rsc": false, 5 - "tsx": true, 6 - "tailwind": { 7 - "config": "", 8 - "css": "src/styles.css", 9 - "baseColor": "zinc", 10 - "cssVariables": true, 11 - "prefix": "" 12 - }, 13 - "iconLibrary": "lucide", 14 - "rtl": false, 15 - "aliases": { 16 - "components": "#/components", 17 - "utils": "#/lib/utils", 18 - "ui": "#/components/ui", 19 - "lib": "#/lib", 20 - "hooks": "#/hooks" 21 - }, 22 - "menuColor": "default", 23 - "menuAccent": "subtle", 24 - "registries": {} 25 - }
index.html apps/web/index.html
+11 -27
package.json
··· 2 2 "name": "cai", 3 3 "private": true, 4 4 "type": "module", 5 + "workspaces": [ 6 + "apps/*", 7 + "packages/*" 8 + ], 5 9 "scripts": { 6 - "dev": "vite dev --port 3000", 7 - "build": "vite build", 8 - "preview": "vite preview", 10 + "dev": "bun run dev:api & bun run --cwd apps/web dev", 11 + "dev:web": "bun run --cwd apps/web dev", 12 + "dev:api": "bun run --cwd apps/api dev", 13 + "build": "bun run --cwd apps/web build", 14 + "preview": "bun run --cwd apps/web preview", 9 15 "lint": "biome lint .", 10 16 "lint:fix": "biome lint --write .", 11 17 "format": "biome format --write .", 12 18 "check": "biome check ." 13 19 }, 14 - "dependencies": { 15 - "@base-ui/react": "^1.4.1", 16 - "@fontsource-variable/dm-sans": "^5.2.8", 17 - "@tailwindcss/vite": "^4.1.18", 18 - "@tanstack/react-query": "^5.100.5", 19 - "@tanstack/react-router": "latest", 20 - "@tanstack/router-plugin": "^1.132.0", 21 - "class-variance-authority": "^0.7.1", 22 - "lucide-react": "^1.11.0", 23 - "react": "^19.2.0", 24 - "react-dom": "^19.2.0", 25 - "shadcn": "^4.5.0", 26 - "tailwindcss": "^4.1.18", 27 - "tw-animate-css": "^1.4.0" 28 - }, 20 + "dependencies": {}, 29 21 "devDependencies": { 30 - "@biomejs/biome": "^2.4.13", 31 - "@tailwindcss/typography": "^0.5.16", 32 - "@types/node": "^22.10.2", 33 - "@types/react": "^19.2.0", 34 - "@types/react-dom": "^19.2.0", 35 - "@vitejs/plugin-react": "^6.0.1", 36 - "clsx": "^2.1.1", 37 - "tailwind-merge": "^3.5.0", 38 - "vite": "^8.0.0" 22 + "@biomejs/biome": "^2.4.13" 39 23 } 40 24 }
+8
packages/shared/package.json
··· 1 + { 2 + "name": "@cai/shared", 3 + "private": true, 4 + "type": "module", 5 + "exports": { 6 + ".": "./src/index.ts" 7 + } 8 + }
+7
packages/shared/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json", 3 + "include": ["src/**/*.ts"], 4 + "compilerOptions": { 5 + "lib": ["ES2022"] 6 + } 7 + }
public/favicon.ico apps/web/public/favicon.ico
public/kitsune.gif apps/web/public/kitsune.gif
public/logo192.png apps/web/public/logo192.png
public/logo512.png apps/web/public/logo512.png
-25
public/manifest.json
··· 1 - { 2 - "short_name": "TanStack App", 3 - "name": "Create TanStack App Sample", 4 - "icons": [ 5 - { 6 - "src": "favicon.ico", 7 - "sizes": "64x64 32x32 24x24 16x16", 8 - "type": "image/x-icon" 9 - }, 10 - { 11 - "src": "logo192.png", 12 - "type": "image/png", 13 - "sizes": "192x192" 14 - }, 15 - { 16 - "src": "logo512.png", 17 - "type": "image/png", 18 - "sizes": "512x512" 19 - } 20 - ], 21 - "start_url": ".", 22 - "display": "standalone", 23 - "theme_color": "#000000", 24 - "background_color": "#ffffff" 25 - }
public/robots.txt apps/web/public/robots.txt
src/components/attribution.tsx apps/web/src/components/attribution.tsx
src/components/character-select.tsx apps/web/src/components/character-select.tsx
src/components/chat-form.tsx apps/web/src/components/chat-form.tsx
src/components/chat-message.tsx apps/web/src/components/chat-message.tsx
src/components/chat-messages.tsx apps/web/src/components/chat-messages.tsx
+2 -2
src/components/chat-sidebar.tsx apps/web/src/components/chat-sidebar.tsx
··· 1 1 import { Link, useLocation } from "@tanstack/react-router"; 2 2 import { useChats } from "#/hooks/use-chats"; 3 - import type { Chat } from "#/lib/types"; 3 + import type { ChatSummary } from "#/lib/types"; 4 4 import { Row } from "./layout"; 5 5 import { Logo } from "./logo"; 6 6 import { Text } from "./text"; ··· 46 46 } 47 47 48 48 interface ChatSidebarMenuItemProps { 49 - chat: Omit<Chat, "messages">; 49 + chat: ChatSummary; 50 50 } 51 51 52 52 function ChatSidebarMenuItem({ chat }: ChatSidebarMenuItemProps) {
src/components/chat.tsx apps/web/src/components/chat.tsx
src/components/container.tsx apps/web/src/components/container.tsx
+1 -1
src/components/heading.tsx apps/web/src/components/heading.tsx
··· 1 1 import { cva, type VariantProps } from "class-variance-authority"; 2 - import { cn } from "@/lib/utils"; 2 + import { cn } from "#/lib/utils"; 3 3 4 4 const heading = cva("tracking-tight", { 5 5 variants: {
src/components/layout.tsx apps/web/src/components/layout.tsx
src/components/logo.tsx apps/web/src/components/logo.tsx
src/components/nav.tsx apps/web/src/components/nav.tsx
src/components/page.tsx apps/web/src/components/page.tsx
+1 -1
src/components/text.tsx apps/web/src/components/text.tsx
··· 1 1 import { cva, type VariantProps } from "class-variance-authority"; 2 - import { cn } from "@/lib/utils"; 2 + import { cn } from "#/lib/utils"; 3 3 4 4 const text = cva("leading-relaxed max-w-prose", { 5 5 variants: {
src/components/ui/avatar.tsx apps/web/src/components/ui/avatar.tsx
src/components/ui/button.tsx apps/web/src/components/ui/button.tsx
src/components/ui/dropdown-menu.tsx apps/web/src/components/ui/dropdown-menu.tsx
src/components/ui/input.tsx apps/web/src/components/ui/input.tsx
src/components/ui/select.tsx apps/web/src/components/ui/select.tsx
src/components/ui/separator.tsx apps/web/src/components/ui/separator.tsx
src/components/ui/sheet.tsx apps/web/src/components/ui/sheet.tsx
src/components/ui/sidebar.tsx apps/web/src/components/ui/sidebar.tsx
src/components/ui/skeleton.tsx apps/web/src/components/ui/skeleton.tsx
src/components/ui/textarea.tsx apps/web/src/components/ui/textarea.tsx
src/components/ui/tooltip.tsx apps/web/src/components/ui/tooltip.tsx
-11
src/hooks/use-characters.ts
··· 1 - import { useQuery } from "@tanstack/react-query"; 2 - import { characters } from "#/lib/mock"; 3 - 4 - export function useCharacters() { 5 - return useQuery({ 6 - queryKey: ["characters"], 7 - queryFn: () => { 8 - return characters; 9 - }, 10 - }); 11 - }
-22
src/hooks/use-chat.ts
··· 1 - import { useQuery } from "@tanstack/react-query"; 2 - import { chats } from "#/lib/mock"; 3 - 4 - interface UseChatOptions { 5 - id: string; 6 - } 7 - 8 - export function useChat({ id }: UseChatOptions) { 9 - const chatQuery = useQuery({ 10 - queryKey: ["chat", id], 11 - queryFn: () => { 12 - return chats.find((chat) => chat.id === id); 13 - }, 14 - }); 15 - 16 - return { 17 - chat: chatQuery.data, 18 - messages: chatQuery.data?.messages ?? [], 19 - isLoading: chatQuery.isLoading, 20 - isError: chatQuery.isError, 21 - }; 22 - }
-14
src/hooks/use-chats.ts
··· 1 - import { useQuery } from "@tanstack/react-query"; 2 - import { chats } from "#/lib/mock"; 3 - 4 - export function useChats() { 5 - return useQuery({ 6 - queryKey: ["chats"], 7 - queryFn: () => { 8 - return chats.map((chat) => { 9 - const { messages, ...chatWithoutMessages } = chat; 10 - return chatWithoutMessages; 11 - }); 12 - }, 13 - }); 14 - }
src/hooks/use-mobile.ts apps/web/src/hooks/use-mobile.ts
+22 -1
src/lib/mock.ts packages/shared/src/index.ts
··· 1 - import type { Character, Chat } from "./types"; 1 + export type CharacterIcon = "heart" | "cat"; 2 + 3 + export interface Character { 4 + id: string; 5 + name: string; 6 + icon: CharacterIcon; 7 + } 8 + 9 + export interface Chat { 10 + id: string; 11 + title: string; 12 + character: Character; 13 + messages: ChatMessage[]; 14 + } 15 + 16 + export type ChatSummary = Omit<Chat, "messages">; 17 + 18 + export interface ChatMessage { 19 + id: string; 20 + author: "assistant" | "user"; 21 + text: string; 22 + } 2 23 3 24 export const characters: Character[] = [ 4 25 {
src/lib/query.ts apps/web/src/lib/query.ts
-20
src/lib/types.ts
··· 1 - import type { IconName } from "lucide-react/dynamic"; 2 - 3 - export interface Character { 4 - id: string; 5 - name: string; 6 - icon: IconName; 7 - } 8 - 9 - export interface Chat { 10 - id: string; 11 - title: string; 12 - character: Character; 13 - messages: ChatMessage[]; 14 - } 15 - 16 - export interface ChatMessage { 17 - id: string; 18 - author: "assistant" | "user"; 19 - text: string; 20 - }
src/lib/utils.ts apps/web/src/lib/utils.ts
src/main.tsx apps/web/src/main.tsx
src/routeTree.gen.ts apps/web/src/routeTree.gen.ts
src/router.tsx apps/web/src/router.tsx
src/routes/__root.tsx apps/web/src/routes/__root.tsx
src/routes/chats.index.tsx apps/web/src/routes/chats.index.tsx
src/routes/chats.tsx apps/web/src/routes/chats.tsx
src/routes/chats/$chatId.tsx apps/web/src/routes/chats/$chatId.tsx
src/routes/index.tsx apps/web/src/routes/index.tsx
src/styles.css apps/web/src/styles.css
+15 -25
tsconfig.json
··· 1 1 { 2 - "include": ["**/*.ts", "**/*.tsx"], 3 - "compilerOptions": { 4 - "target": "ES2022", 5 - "jsx": "react-jsx", 6 - "module": "ESNext", 7 - "paths": { 8 - "#/*": ["./src/*"], 9 - "@/*": ["./src/*"] 10 - }, 11 - "lib": ["ES2022", "DOM", "DOM.Iterable"], 12 - "types": ["vite/client"], 13 - 14 - /* Bundler mode */ 15 - "moduleResolution": "bundler", 16 - "allowImportingTsExtensions": true, 17 - "verbatimModuleSyntax": true, 18 - "noEmit": true, 2 + "files": [], 3 + "compilerOptions": { 4 + "target": "ES2022", 5 + "module": "ESNext", 6 + "moduleResolution": "bundler", 7 + "allowImportingTsExtensions": true, 8 + "verbatimModuleSyntax": true, 9 + "noEmit": true, 19 10 20 - /* Linting */ 21 - "skipLibCheck": true, 22 - "strict": true, 23 - "noUnusedLocals": true, 24 - "noUnusedParameters": true, 25 - "noFallthroughCasesInSwitch": true, 26 - "noUncheckedSideEffectImports": true 27 - } 11 + "skipLibCheck": true, 12 + "strict": true, 13 + "noUnusedLocals": true, 14 + "noUnusedParameters": true, 15 + "noFallthroughCasesInSwitch": true, 16 + "noUncheckedSideEffectImports": true 17 + } 28 18 }
vite.config.ts apps/web/vite.config.ts