Consolidate wrapper components

This commit is contained in:
icld 2025-02-20 09:43:07 -08:00
parent 8353c1ecbe
commit 9d7ab3abac
43 changed files with 3933 additions and 2967 deletions

View File

@ -1,25 +0,0 @@
name: Lint
on:
pull_request:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: yarn
- name: Build libs
run: yarn workspace gql-client run build
- name: Linter check
run: yarn lint

View File

@ -1,39 +0,0 @@
name: Test webapp deployment
on:
schedule:
- cron: '0 3 * * *'
workflow_dispatch:
jobs:
test_app_deployment:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: yarn
- name: Test webapp deployment
run: ./packages/deployer/test/test-webapp-deployment-undeployment.sh
- name: Notify Vulcanize Slack on CI failure
if: ${{ always() && github.ref_name == 'main' }}
uses: ravsamhq/notify-slack-action@v2
with:
status: ${{ job.status }}
notify_when: 'failure'
env:
SLACK_WEBHOOK_URL: ${{ secrets.VULCANIZE_SLACK_CI_ALERTS_WEBHOOK }}
- name: Notify DeepStack Slack on CI failure
if: ${{ always() && github.ref_name == 'main' }}
uses: ravsamhq/notify-slack-action@v2
with:
status: ${{ job.status }}
notify_when: 'failure'
env:
SLACK_WEBHOOK_URL: ${{ secrets.DEEPSTACK_SLACK_CI_ALERTS_WEBHOOK }}

View File

@ -3,5 +3,7 @@
"tailwindCSS.experimental.classRegex": [
"tv\\('([^)]*)\\')",
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
]
],
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}

View File

@ -8,12 +8,19 @@
"depcheck": "^1.4.2",
"husky": "^8.0.3",
"lerna": "^8.0.0",
"patch-package": "^8.0.0"
"patch-package": "^8.0.0",
"chalk": "^4.1.2",
"concurrently": "^8.2.0"
},
"scripts": {
"prepare": "husky install",
"build": "lerna run build --stream",
"lint": "lerna run lint --stream"
"lint": "lerna run lint --stream",
"start": "yarn kill:ports && yarn dev",
"dev": "yarn && yarn build --ignore frontend && concurrently --names \"BACKEND,FRONTEND\" --prefix-colors \"blue.bold,green.bold\" --prefix \"[{name}]\" \"yarn start:backend\" \"yarn start:frontend\"",
"start:backend": "yarn workspace backend start",
"start:frontend": "yarn workspace frontend dev",
"kill:ports": "node scripts/kill-ports.js"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
}

View File

@ -0,0 +1,25 @@
export const tabPageConfig: TabPageNavigationConfig = {
defaultTab: 'tab1',
tabs: [
{
id: 'tab1',
label: 'Overview',
content: <div>This is the overview content</div>,
},
{
id: 'tab2',
label: 'Details',
content: <div>This is the details content</div>,
},
{
id: 'tab3',
label: 'Settings',
content: <div>This is the settings content</div>,
},
{
id: 'tab4',
label: 'History',
content: <div>This is the history content</div>,
},
],
};

View File

@ -78,9 +78,7 @@ function App() {
});
}, []);
return (
<RouterProvider router={router} />
);
return <RouterProvider router={router} />;
}
export default App;

View File

@ -0,0 +1,39 @@
import { NavigationWrapper } from "@/components/navigation/NavigationWrapper"
import { ScreenWrapper } from "@/components/screen-wrapper/ScreenWrapper"
import { Header } from "@/components/screen-header/Header"
import { TabPageNavigation } from "@/components/tab-navigation/TabPageNavigation"
import { ActionButton } from "@/components/screen-header/ActionButton"
import { Circle, PlusCircle } from "lucide-react"
import { tabPageConfig } from "@/components/tab-navigation/tabPageConfig"
export default function PageWithSubNav() {
return (
<NavigationWrapper>
<ScreenWrapper>
<div className="p-4 sm:p-8">
<Header
title="PageTitle"
subtitle="Optional subtitle for the page"
actions={[
<ActionButton
key="secondary"
label="Second Action"
icon={Circle}
variant="outline"
onClick={() => console.log("Secondary action clicked")}
/>,
<ActionButton
key="primary"
label="Action"
icon={PlusCircle}
onClick={() => console.log("Primary action clicked")}
/>,
]}
/>
<TabPageNavigation config={tabPageConfig} />
</div>
</ScreenWrapper>
</NavigationWrapper>
)
}

View File

@ -0,0 +1,12 @@
import type React from 'react';
interface ScreenWrapperProps {
children: React.ReactNode;
}
export function ScreenWrapper({ children }: ScreenWrapperProps) {
return (
<div className="min-h-[calc(100vh-4rem)] bg-background flex flex-col gap-4 bg-background xl:px-8 pt-20 pb-8 px-4">
{children}
</div>
);
}

View File

@ -0,0 +1,27 @@
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
export function ColorModeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@ -0,0 +1,46 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { useGitHubAuth } from "@/hooks/useGitHubAuth"
import { Github } from "lucide-react"
export function GitHubSessionButton() {
const { isAuthenticated, user, login, logout } = useGitHubAuth()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className={`relative ${isAuthenticated ? "text-green-500" : "text-muted-foreground"}`}
>
<Github className="h-[1.2rem] w-[1.2rem]" />
{isAuthenticated && <span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-green-500" />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent align="end" className="w-56" sideOffset={4} >
{isAuthenticated && user ? (
<>
<div className="flex items-center gap-2 p-2">
<Avatar className="h-8 w-8">
<AvatarImage src={user.avatar_url} alt={user.login} />
<AvatarFallback>{user.login.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<span className="text-sm font-medium">{user.name || user.login}</span>
<span className="text-xs text-muted-foreground">{user.login}</span>
</div>
</div>
<DropdownMenuItem onClick={logout}>Log out</DropdownMenuItem>
</>
) : (
<DropdownMenuItem onClick={login}>Log in with GitHub</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenu>
)
}

View File

@ -0,0 +1,28 @@
import type React from "react"
interface LaconicIconProps {
className?: string
width?: number
height?: number
}
export const LaconicIcon: React.FC<LaconicIconProps> = ({ className = "", width = 40, height = 40 }) => {
return (
<svg
width={width}
height={height}
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.7179 20.6493C14.6275 17.7397 16.4284 13.7219 16.4276 9.28566C16.4288 8.68351 16.3955 8.08808 16.3294 7.5L7.5 7.5008L7.50026 24.4654C7.49946 26.5218 8.28351 28.5788 9.85175 30.1469C11.4201 31.7151 13.4785 32.5001 15.5353 32.4991L32.5 32.5L32.4994 23.6694C31.9126 23.6048 31.3171 23.5713 30.7136 23.5711C26.2786 23.5718 22.2605 25.3725 19.351 28.2819C17.2337 30.346 13.8392 30.3464 11.7483 28.2554C9.65859 26.1656 9.65764 22.7701 11.7179 20.6493ZM30.6686 9.33579C28.2303 6.89759 24.2689 6.89665 21.8298 9.33579C19.3906 11.7748 19.3916 15.7361 21.8298 18.1743C24.2694 20.6138 28.2295 20.6134 30.6686 18.1743C33.1078 15.7353 33.1083 11.7752 30.6686 9.33579Z"
className="fill-current"
/>
</svg>
)
}

View File

@ -0,0 +1,20 @@
import { type ReactNode } from 'react';
import { Outlet } from 'react-router-dom';
import { TopNavigation } from './TopNavigation';
interface NavigationWrapperProps {
children?: ReactNode;
}
export function NavigationWrapper({ children }: NavigationWrapperProps) {
return (
<>
<div className="fixed top-0 left-0 right-0 z-10">
<TopNavigation />
</div>
{/* <div className="flex-1 mt-16 px-4 py-6"> */}
{children || <Outlet />}
{/* </div> */}
</>
);
}

View File

@ -0,0 +1,105 @@
"use client"
import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { Menu, Shapes, Wallet } from "lucide-react"
import { Link } from "react-router-dom"
import { ColorModeToggle } from "./ColorModeToggle"
import { GitHubSessionButton } from "./GitHubSessionButton"
import { LaconicIcon } from "./LaconicIcon"
import { WalletSessionId } from "./WalletSessionId"
export function TopNavigation() {
// This is a placeholder. In a real app, you'd manage this state with your auth system
return (
<PopoverPrimitive.Root>
<div className="bg-background">
{/* Top Navigation - Desktop */}
<nav className="hidden h-16 border-b md:flex items-center justify-between px-6">
<div className="flex items-center gap-6">
{/* Logo / Home Link */}
<Button variant="ghost" asChild className="p-0 hover:bg-transparent">
<Link to="/" className="flex h-12 w-12 items-center justify-center">
<LaconicIcon className="text-foreground" />
</Link>
</Button>
{/* Navigation Items */}
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="gap-2">
<Link to="/projects">
<Shapes className="h-5 w-5" />
<span>Projects</span>
</Link>
</Button>
<Button variant="ghost" asChild className="gap-2 text-muted-foreground">
<Link to="/wallet">
<Wallet className="h-5 w-5" />
<span>Wallet</span>
</Link>
</Button>
</div>
</div>
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="text-muted-foreground">
<Link to="/support">Support</Link>
</Button>
<Button variant="ghost" asChild className="text-muted-foreground">
<Link to="/docs">Documentation</Link>
</Button>
<GitHubSessionButton />
<ColorModeToggle />
<WalletSessionId walletId="0xAb...1234" />
</div>
</nav>
{/* Top Navigation - Mobile */}
<nav className="flex h-16 items-center justify-between border-b px-4 md:hidden">
<Sheet>
<SheetTrigger >
<Button asChild variant="outline" size="icon">
<div>
<Menu className="h-4 w-4" />
</div>
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-[300px] sm:w-[400px] flex flex-col">
<nav className="flex flex-col space-y-4 flex-grow">
<Button variant="ghost" asChild className="justify-start gap-2">
<Link to="/projects">
<Shapes className="h-5 w-5" />
<span>Projects</span>
</Link>
</Button>
<Button variant="ghost" asChild className="justify-start gap-2">
<Link to="/wallet">
<Wallet className="h-5 w-5" />
<span>Wallet</span>
</Link>
</Button>
<Button variant="ghost" asChild className="justify-start">
<Link to="/support">Support</Link>
</Button>
<Button variant="ghost" asChild className="justify-start">
<Link to="/docs">Documentation</Link>
</Button>
</nav>
<div className="mt-auto flex items-center justify-between">
<GitHubSessionButton />
<ColorModeToggle />
<WalletSessionId walletId="0xAb...1234" />
</div>
</SheetContent>
</Sheet>
<div className="flex items-center gap-2">
<ColorModeToggle />
<WalletSessionId walletId="0xAb...1234" />
</div>
</nav>
</div>
</PopoverPrimitive.Root>
)
}

View File

@ -0,0 +1,28 @@
import type React from 'react';
interface WalletSessionIdProps {
walletId?: string;
className?: string;
}
export const WalletSessionId: React.FC<WalletSessionIdProps> = ({
walletId,
className = '',
}) => {
// const { wallet } = useWallet();
const wallet = {id: 'x123xxx'}
const displayId = walletId || wallet?.id || 'Not Connected';
return (
<div
className={`flex items-center gap-2 rounded-md bg-secondary px-2.5 py-0.5 ${className}`}
>
<div
className={`h-2 w-2 rounded-full ${wallet?.id ? 'bg-green-400' : 'bg-gray-400'}`}
/>
<span className="text-xs font-semibold text-secondary-foreground">
{displayId}
</span>
</div>
);
};

View File

@ -0,0 +1,30 @@
import { Button } from "@/components/ui/button"
import type { LucideIcon } from "lucide-react"
import { useToast } from "../shared"
interface ActionButtonProps {
label: string
icon: LucideIcon
variant?: "default" | "outline"
onClick?: () => void
}
export function ActionButton({ label, icon: Icon, variant = "default", onClick }: ActionButtonProps) {
const { toast } = useToast()
const handleClick = () => {
onClick?.()
toast({
title: "Action Triggered",
description: "TODO: Connect action",
})
}
return (
<Button variant={variant} onClick={handleClick} className="gap-2">
<Icon className="h-4 w-4" />
<span className="hidden sm:inline">{label}</span>
</Button>
)
}

View File

@ -0,0 +1,22 @@
import React from "react"
interface HeaderProps {
title: string
subtitle?: string
actions?: React.ReactNode[]
}
export function Header({ title, subtitle, actions }: HeaderProps) {
return (
<div className="mb-4 flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-xl font-bold sm:text-3xl">{title}</h1>
{subtitle && <p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>}
</div>
<div className="mt-4 flex items-center gap-2 sm:mt-0">
{actions && actions.map((action, index) => <React.Fragment key={index}>{action}</React.Fragment>)}
</div>
</div>
)
}

View File

@ -0,0 +1,9 @@
import type React from "react"
interface ScreenWrapperProps {
children: React.ReactNode
}
export function ScreenWrapper({ children }: ScreenWrapperProps) {
return <div className="min-h-[calc(100vh-4rem)] bg-background pt-16">{children}</div>
}

View File

@ -1,128 +0,0 @@
/* React Calendar */
.react-calendar {
@apply border-none font-sans;
}
/* Weekdays -- START */
.react-calendar__month-view__weekdays {
@apply p-0 flex items-center justify-center;
}
.react-calendar__month-view__weekdays__weekday {
@apply h-8 w-12 flex items-center justify-center p-0 font-medium text-xs text-elements-disabled mb-2;
}
abbr[title] {
text-decoration: none;
}
/* Weekdays -- END */
/* Days -- START */
.react-calendar__month-view__days {
@apply p-0 gap-0;
}
.react-calendar__month-view__days__day--neighboringMonth {
@apply !text-elements-disabled;
}
.react-calendar__month-view__days__day--neighboringMonth:hover {
@apply !text-elements-disabled !bg-transparent;
}
.react-calendar__month-view__days__day--neighboringMonth:focus-visible {
@apply !text-elements-disabled !bg-transparent;
}
/* For weekend days */
.react-calendar__month-view__days__day--weekend {
/* color: ${colors.grey[950]} !important; */
}
.react-calendar__tile {
@apply h-12 w-12 text-elements-high-em dark:text-foreground;
}
.react-calendar__tile:hover {
@apply bg-base-bg-emphasized dark:bg-overlay3 rounded-lg;
}
.react-calendar__tile:focus-visible {
@apply bg-base-bg-emphasized rounded-lg ring-2 ring-primary ring-offset-2 z-10;
}
.react-calendar__tile--now {
@apply bg-base-bg-emphasized dark:bg-overlay3 text-elements-high-em rounded-lg;
}
.react-calendar__tile--now:hover {
@apply bg-base-bg-emphasized text-elements-high-em rounded-lg;
}
.react-calendar__tile--now:focus-visible {
@apply bg-base-bg-emphasized text-elements-high-em rounded-lg ring-2 ring-primary ring-offset-2;
}
.react-calendar__tile--active {
@apply bg-controls-primary text-elements-on-primary rounded-lg;
}
.react-calendar__tile--active:hover {
@apply bg-controls-primary-hovered;
}
.react-calendar__tile--active:focus-visible {
@apply bg-controls-primary-hovered ring-2 ring-primary ring-offset-2;
}
/* Range -- START */
.react-calendar__tile--range {
@apply bg-controls-secondary dark:bg-overlay3 text-elements-on-secondary rounded-none;
}
.react-calendar__tile--range:hover {
@apply bg-controls-secondary-hovered text-elements-on-secondary rounded-none;
}
.react-calendar__tile--range:focus-visible {
@apply bg-controls-secondary-hovered text-elements-on-secondary rounded-lg;
}
.react-calendar__tile--rangeStart {
@apply bg-controls-primary dark:bg-primary text-elements-on-primary rounded-lg;
}
.react-calendar__tile--rangeStart:hover {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg;
}
.react-calendar__tile--rangeStart:focus-visible {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg ring-2 ring-primary ring-offset-2;
}
.react-calendar__tile--rangeEnd {
@apply bg-controls-primary dark:bg-primary text-elements-on-primary rounded-lg;
}
.react-calendar__tile--rangeEnd:hover {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg;
}
.react-calendar__tile--rangeEnd:focus-visible {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg ring-2 ring-primary ring-offset-2;
}
/* Range -- END */
/* Days -- END */
/* Months -- START */
.react-calendar__tile--hasActive {
@apply bg-controls-primary text-elements-on-primary rounded-lg;
}
.react-calendar__tile--hasActive:hover {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg;
}
.react-calendar__tile--hasActive:focus-visible {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg ring-2 ring-primary ring-offset-2;
}

View File

@ -1,3 +1,9 @@
import { Button } from 'components/shared/Button';
import {
ChevronGrabberHorizontal,
ChevronLeft,
ChevronRight,
} from 'components/shared/CustomIcon';
import {
ComponentPropsWithRef,
MouseEvent,
@ -10,14 +16,7 @@ import {
CalendarProps as ReactCalendarProps,
} from 'react-calendar';
import { CalendarTheme, calendarTheme } from './Calendar.theme';
import { Button } from 'components/shared/Button';
import {
ChevronGrabberHorizontal,
ChevronLeft,
ChevronRight,
} from 'components/shared/CustomIcon';
import './Calendar.css';
import { format } from 'date-fns';
import { cn } from 'utils/classnames';

View File

@ -0,0 +1,27 @@
import { Card } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import type { TabPageNavigationConfig } from "./types"
interface TabPageNavigationProps {
config: TabPageNavigationConfig
}
export function TabPageNavigation({ config }: TabPageNavigationProps) {
return (
<Tabs defaultValue={config.defaultTab} className="mb-4">
<TabsList className="w-full justify-start md:w-auto max-w-3xl">
{config.tabs.map((tab) => (
<TabsTrigger key={tab.id} value={tab.id}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{config.tabs.map((tab) => (
<TabsContent key={tab.id} value={tab.id}>
<Card className="mx-auto max-w-3xl p-6">{tab.content}</Card>
</TabsContent>
))}
</Tabs>
)
}

View File

@ -0,0 +1,28 @@
import type { TabPageNavigationConfig } from "./types"
export const tabPageConfig: TabPageNavigationConfig = {
defaultTab: "tab1",
tabs: [
{
id: "tab1",
label: "Overview",
content: <div>This is the overview content</div>,
},
{
id: "tab2",
label: "Details",
content: <div>This is the details content</div>,
},
{
id: "tab3",
label: "Settings",
content: <div>This is the settings content</div>,
},
{
id: "tab4",
label: "History",
content: <div>This is the history content</div>,
},
],
}

View File

@ -0,0 +1,17 @@
import { z } from "zod"
import type { ReactNode } from "react"
export const TabSchema = z.object({
id: z.string(),
label: z.string(),
content: z.custom<ReactNode>((v) => v !== null && v !== undefined),
})
export const TabPageNavigationSchema = z.object({
defaultTab: z.string(),
tabs: z.array(TabSchema),
})
export type Tab = z.infer<typeof TabSchema>
export type TabPageNavigationConfig = z.infer<typeof TabPageNavigationSchema>

View File

@ -1,6 +1,6 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils"
@ -181,19 +181,9 @@ const DropdownMenuShortcut = ({
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
DropdownMenuSubTrigger, DropdownMenuTrigger
}

View File

@ -0,0 +1,99 @@
import { useToast } from '@/hooks/use-toast';
import React, {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from 'react';
interface WalletContextType {
wallet: {
id: string;
address?: string;
} | null;
isConnected: boolean;
connect: () => Promise<void>;
disconnect: () => void;
}
const WalletContext = createContext<WalletContextType | undefined>(undefined);
export const WalletProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [wallet, setWallet] = useState<WalletContextType['wallet']>(null);
const [isConnected, setIsConnected] = useState(false);
const { toast } = useToast();
useEffect(() => {
const handleWalletMessage = (event: MessageEvent) => {
if (event.origin !== import.meta.env.VITE_WALLET_IFRAME_URL) return;
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
const address = event.data.data[0].address;
setWallet({
id: address,
address: address,
});
setIsConnected(true);
toast({
title: 'Wallet Connected',
// variant: 'success',
duration: 3000,
// id: '',
});
}
};
window.addEventListener('message', handleWalletMessage);
return () => window.removeEventListener('message', handleWalletMessage);
}, [toast]);
const connect = async () => {
const iframe = document.getElementById('walletIframe') as HTMLIFrameElement;
if (iframe?.contentWindow) {
iframe.contentWindow.postMessage(
{
type: 'REQUEST_WALLET_ACCOUNTS',
chainId: import.meta.env.VITE_LACONICD_CHAIN_ID,
},
import.meta.env.VITE_WALLET_IFRAME_URL,
);
} else {
toast({
title: 'Wallet Connection Failed',
description: 'Wallet iframe not found or not loaded',
// variant: 'error',
duration: 3000,
});
}
};
const disconnect = () => {
setWallet(null);
setIsConnected(false);
toast({
title: 'Wallet Disconnected',
// variant: 'info',
duration: 3000,
});
};
return (
<WalletContext.Provider
value={{ wallet, isConnected, connect, disconnect }}
>
{children}
</WalletContext.Provider>
);
};
export const useWallet = () => {
const context = useContext(WalletContext);
if (context === undefined) {
throw new Error('useWallet must be used within a WalletProvider');
}
return context;
};

View File

@ -0,0 +1,33 @@
"use client"
import { useOctokit } from "@/context/OctokitContext"
import { GitHubUserSchema } from "@/stores/github"
import { useEffect, useState } from "react"
import type { z } from "zod"
type GitHubUser = z.infer<typeof GitHubUserSchema>
export function useGitHubAuth() {
const { octokit, isAuth: isAuthenticated, updateAuth } = useOctokit()
const [user, setUser] = useState<GitHubUser | null>(null)
useEffect(() => {
const fetchUser = async () => {
if (!octokit || !isAuthenticated) return
try {
const { data } = await octokit.rest.users.getAuthenticated()
const validatedUser = GitHubUserSchema.parse(data)
setUser(validatedUser)
} catch (error) {
console.error('GitHub authentication failed:', error)
setUser(null)
}
}
fetchUser()
}, [octokit, isAuthenticated])
return { isAuthenticated, user, login: updateAuth, logout: updateAuth }
}

View File

@ -1,79 +1,76 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 240 10% 3.9%;
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -1,20 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import assert from 'assert';
import { GQLClient } from 'gql-client';
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ThemeProvider } from '@snowballtools/material-tailwind-react-fork';
// import { ThemeProvider } from '@snowballtools/material-tailwind-react-fork';
import '@fontsource/inter';
import '@fontsource-variable/jetbrains-mono';
// import '@fontsource-variable/jetbrains-mono';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { GQLClientProvider } from './context/GQLClientContext';
import { SERVER_GQL_PATH } from './constants';
import { Toaster } from 'components/shared/Toast';
import { LogErrorBoundary } from 'utils/log-error';
import { BASE_URL } from 'utils/constants';
import App from './App';
import { Toaster } from './components/ui/toaster';
import { SERVER_GQL_PATH } from './constants';
import { GQLClientProvider } from './context/GQLClientContext';
import './index.css';
import reportWebVitals from './reportWebVitals';
console.log(`v-0.0.9`);
@ -28,16 +27,16 @@ const gqlEndpoint = `${BASE_URL}/${SERVER_GQL_PATH}`;
const gqlClient = new GQLClient({ gqlEndpoint });
root.render(
<LogErrorBoundary>
<React.StrictMode>
<ThemeProvider>
<GQLClientProvider client={gqlClient}>
<App />
<Toaster />
</GQLClientProvider>
</ThemeProvider>
</React.StrictMode>
</LogErrorBoundary>,
// <LogErrorBoundary>
<React.StrictMode>
{/* <ThemeProvider> */}
<GQLClientProvider client={gqlClient}>
<App />
<Toaster />
</GQLClientProvider>
{/* </ThemeProvider> */}
</React.StrictMode>,
// </LogErrorBoundary>,
);
// If you want to start measuring performance in your app, pass a function

View File

@ -1,13 +1,13 @@
import { User } from 'gql-client';
import { useCallback, useEffect, useState } from 'react';
import { Outlet, useNavigate } from 'react-router-dom';
import { User } from 'gql-client';
import HorizontalLine from 'components/HorizontalLine';
import { useGQLClient } from 'context/GQLClientContext';
import { NotificationBellIcon, PlusIcon } from 'components/shared/CustomIcon';
import { Button } from 'components/shared/Button';
import { formatAddress } from 'utils/format';
import { ProjectSearchBar } from 'components/projects/ProjectSearchBar';
import { Button } from 'components/shared/Button';
import { NotificationBellIcon, PlusIcon } from 'components/shared/CustomIcon';
import { useGQLClient } from 'context/GQLClientContext';
import { formatAddress } from 'utils/format';
const ProjectSearch = () => {
const navigate = useNavigate();
@ -29,6 +29,8 @@ const ProjectSearch = () => {
return organizations[0].slug;
}, []);
console.log(user);
return (
<section className="h-full flex flex-col">
{/* Header */}

View File

@ -1,7 +1,8 @@
import { Organization } from 'gql-client';
import { Loader2 } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { Navigate } from 'react-router-dom';
import { useGQLClient } from '../context/GQLClientContext';
import { Organization } from 'gql-client';
const Index = () => {
const client = useGQLClient();
@ -16,13 +17,13 @@ const Index = () => {
useEffect(() => {
fetchUserOrganizations();
}, []);
console.log(organization);
return (
<>
{Boolean(organization) ? (
<Navigate to={organization!.slug} />
) : (
<>Loading</>
<Loader2 className={'animate-spin w-12 h-12'} />
)}
</>
);

View File

@ -1,10 +1,10 @@
import { useCallback, useEffect, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { Header } from '@/components/screen-header/Header';
import { ScreenWrapper } from '@/components/ScreenWrapper';
import CheckBalanceIframe from 'components/projects/create/CheckBalanceIframe';
import { ProjectCard } from 'components/projects/ProjectCard';
import { Badge, Button, Heading } from 'components/shared';
import { PlusIcon } from 'components/shared/CustomIcon';
import { useGQLClient } from 'context/GQLClientContext';
import { Project } from 'gql-client';
@ -33,30 +33,13 @@ const Projects = () => {
}
}, [isBalanceSufficient]);
return (
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
{/* Header */}
console.log(orgSlug, projects);
return (
<ScreenWrapper>
<Header title="Projects" actions={[]} />
{/* <TabPageNavigation config={tabPageConfig} /> */}
<div className="flex items-center">
<div className="grow">
<div className="flex gap-4 items-center">
<Heading as="h2" className="text-[24px] dark:text-foreground">
Projects
</Heading>
<Badge className="bg-base-bg-alternate text-elements-mid-em h-7 w-7">
{projects.length}
</Badge>
</div>
</div>
<Link to="projects/create">
<Button>
<PlusIcon className="mr-2" />
CREATE PROJECT
</Button>
</Link>
</div>
{/* List of projects */}
<div className="grid grid-flow-row grid-cols-[repeat(auto-fill,_minmax(280px,_1fr))] gap-4">
{projects.length > 0 &&
@ -65,8 +48,12 @@ const Projects = () => {
})}
</div>
<CheckBalanceIframe onBalanceChange={setIsBalanceSufficient} isPollingEnabled={false} amount='1' />
</section>
<CheckBalanceIframe
onBalanceChange={setIsBalanceSufficient}
isPollingEnabled={false}
amount="1"
/>
</ScreenWrapper>
);
};

View File

@ -1,20 +1,10 @@
import { Logo } from 'components/Logo';
import { Button } from 'components/shared/Button';
import {
CrossIcon,
MenuIcon,
NotificationBellIcon,
SearchIcon,
} from 'components/shared/CustomIcon';
import { Sidebar } from 'components/shared/Sidebar';
import { useToast } from '@/hooks/use-toast';
import { cn } from '@/lib/utils';
import { NavigationWrapper } from 'components/navigation/NavigationWrapper';
import { OctokitProvider } from 'context/OctokitContext';
import { ComponentPropsWithoutRef, useEffect, useState } from 'react';
import { Outlet, useParams } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';
import { cn } from 'utils/classnames';
import { useMediaQuery } from 'usehooks-ts';
import { ProjectSearchBarDialog } from 'components/projects/ProjectSearchBar';
import { ThemeProvider } from 'next-themes';
import { ComponentPropsWithoutRef, useEffect } from 'react';
import { Outlet } from 'react-router-dom';
export interface DashboardLayoutProps
extends ComponentPropsWithoutRef<'section'> {}
@ -22,109 +12,24 @@ export const DashboardLayout = ({
className,
...props
}: DashboardLayoutProps) => {
const { orgSlug } = useParams();
const isDesktop = useMediaQuery('(min-width: 960px)');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const { toast } = useToast();
useEffect(() => {
if (isDesktop) {
setIsSidebarOpen(false);
}
}, [isDesktop]);
console.log('DashboardLayout');
toast({
title: 'DashboardLayout',
variant: 'default',
});
}, []);
return (
<>
<section
{...props}
className={cn(
'flex flex-col lg:flex-row h-screen bg-background',
className,
)}
>
{/* Header on mobile */}
<div className="flex lg:hidden items-center px-4 py-2.5 justify-between">
<Logo orgSlug={orgSlug} />
<div className="flex items-center gap-0.5">
<AnimatePresence>
{isSidebarOpen ? (
<motion.div
key="crossIcon"
initial={{ opacity: 0 }}
animate={{
opacity: 1,
transition: { duration: 0.2, delay: 0.3 },
}}
exit={{ opacity: 0, transition: { duration: 0 } }}
>
<Button
iconOnly
variant="ghost"
onClick={() => setIsSidebarOpen(false)}
>
<CrossIcon size={18} />
</Button>
</motion.div>
) : (
<motion.div
key="menuIcons"
initial={{ opacity: 0 }}
animate={{
opacity: 1,
transition: { duration: 0.2, delay: 0.2 },
}}
exit={{ opacity: 0, transition: { duration: 0 } }}
>
<>
<Button iconOnly variant="ghost">
<NotificationBellIcon size={18} />
</Button>
<Button
iconOnly
variant="ghost"
onClick={() => setIsSearchOpen(true)}
>
<SearchIcon size={18} />
</Button>
<Button
iconOnly
variant="ghost"
onClick={() => setIsSidebarOpen(true)}
>
<MenuIcon size={18} />
</Button>
</>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
<div className="flex h-full w-full overflow-hidden">
<Sidebar mobileOpen={isSidebarOpen} />
<motion.div
className={cn(
'w-full h-full pr-1 pl-1 py-1 lg:pl-0 lg:pr-3 lg:py-3 overflow-y-hidden min-w-[320px]',
{ 'flex-shrink-0': isSidebarOpen || !isDesktop },
)}
animate={{
x: isSidebarOpen || isDesktop ? 0 : -320,
}}
transition={{ ease: 'easeInOut', duration: 0.3 }}
>
<div className="rounded-t-3xl lg:rounded-3xl dark:bg-background bg-base-bg h-full shadow-card dark:shadow-background overflow-y-auto relative">
<OctokitProvider>
<Outlet />
</OctokitProvider>
</div>
</motion.div>
</div>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<section {...props} className={cn('h-full', className)}>
<OctokitProvider>
<NavigationWrapper>
<Outlet />
</NavigationWrapper>
</OctokitProvider>
</section>
<ProjectSearchBarDialog
open={isSearchOpen}
onClickItem={() => setIsSearchOpen(false)}
onClose={() => setIsSearchOpen(false)}
/>
</>
</ThemeProvider>
);
};

View File

@ -1,9 +1,9 @@
import Id from './Id';
import AddDomain from './id/settings/domains/add';
import { createProjectRoutes } from './create/routes';
import { projectTabRoutes } from './id/routes';
import { addDomainRoutes } from './id/settings/domains/add/routes';
import { CreateProjectLayout } from './create/layout';
import { createProjectRoutes } from './create/routes';
import Id from './Id';
import { projectTabRoutes } from './id/routes';
import AddDomain from './id/settings/domains/add';
import { addDomainRoutes } from './id/settings/domains/add/routes';
export const projectsRoutesWithoutSearch = [
{

View File

@ -0,0 +1,101 @@
import { Octokit } from 'octokit'
import { z } from 'zod'
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
// Define Zod schema for GitHub user data
export const GitHubUserSchema = z.object({
login: z.string(),
avatar_url: z.string().url(),
name: z.string().nullable(),
email: z.string().email().nullable(),
})
// Define error schema
export const GitHubErrorSchema = z.object({
message: z.string(),
status: z.number().optional(),
})
// Infer TypeScript types from Zod schemas
export type GitHubUser = z.infer<typeof GitHubUserSchema>
export type GitHubError = z.infer<typeof GitHubErrorSchema>
interface GitHubState {
isAuth: boolean
user: GitHubUser | null
octokit: Octokit | null
isLoading: boolean
error: GitHubError | null
// Actions
setUser: (user: GitHubUser | null) => void
setIsAuth: (isAuth: boolean) => void
setOctokit: (octokit: Octokit | null) => void
setError: (error: GitHubError | null) => void
setIsLoading: (isLoading: boolean) => void
// Async actions
fetchUser: () => Promise<void>
reset: () => void
}
export const useGitHubStore = create<GitHubState>()(
persist(
(set, get) => ({
isAuth: false,
user: null,
octokit: null,
isLoading: false,
error: null,
setUser: (user) => set({ user }),
setIsAuth: (isAuth) => set({ isAuth }),
setOctokit: (octokit) => set({ octokit }),
setError: (error) => set({ error }),
setIsLoading: (isLoading) => set({ isLoading }),
fetchUser: async () => {
const { octokit, isAuth } = get()
if (!octokit || !isAuth) {
set({ user: null })
return
}
set({ isLoading: true, error: null })
try {
const { data } = await octokit.rest.users.getAuthenticated()
// Validate the data with Zod schema
const validatedUser = GitHubUserSchema.parse({
login: data.login,
avatar_url: data.avatar_url,
name: data.name,
email: data.email,
})
set({ user: validatedUser, error: null })
} catch (error) {
const gitHubError = GitHubErrorSchema.parse({
message: error instanceof Error ? error.message : 'Failed to fetch GitHub user data',
status: error instanceof Error && 'status' in error ? error.status as number : undefined
})
set({ user: null, error: gitHubError })
console.error('Failed to fetch GitHub user data:', error)
} finally {
set({ isLoading: false })
}
},
reset: () => set({
isAuth: false,
user: null,
octokit: null,
error: null,
isLoading: false
}),
}),
{
name: 'github-storage',
// Only persist these fields
partialize: (state) => ({
isAuth: state.isAuth,
user: state.user,
}),
}
)
)

View File

@ -1,302 +1,88 @@
import withMT from '@snowballtools/material-tailwind-react-fork/utils/withMT';
import colors from 'tailwindcss/colors';
import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
export default withMT({
const config = {
darkMode: ["class"],
content: [
'./src/**/*.{js,jsx,ts,tsx}',
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
'../../node_modules/@snowballtools/material-tailwind-react-fork/components/**/*.{js,ts,jsx,tsx}',
'../../node_modules/@snowballtools/material-tailwind-react-fork/theme/components/**/*.{js,ts,jsx,tsx}',
"./components/**/*.{ts,tsx}",
],
'../../node_modules/@snowballtools/material-tailwind-react-fork/theme/components/**/*.{js,ts,jsx,tsx}'
],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px'
}
},
colors: {
background: '#0F0F0F',
overlay: '#18181A',
overlay2: '#29292E',
overlay3: '#48474F',
primary: '#0000F4',
'primary-hovered': '#0000F4AA',
foreground: '#FBFBFB',
'foreground-secondary': '#8E8E8E',
error: '#B20710',
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
}
},
extend: {
screens: {
xxs: '400px',
xs: '480px'
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
letterSpacing: {
tight: '-0.084px'
},
fontFamily: {
sans: [
'var(--font-sans)',
...fontFamily.sans
],
display: [
'Inter Display',
'sans-serif'
],
mono: [
'DM Mono',
'monospace'
]
},
fontSize: {
'2xs': '0.625rem',
'3xs': '0.5rem'
},
keyframes: {
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: '0'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
spin: 'spin 3s linear infinite'
},
colors: {
sky: 'colors.sky',
slate: 'colors.slate',
emerald: {
'50': '#ecfdf5',
'100': '#d1fae5',
'200': '#a9f1d0',
'300': '#6ee7b7',
'400': '#34d399',
'500': '#10b981',
'600': '#059669',
'700': '#047857',
'800': '#065f46',
'900': '#064e3b'
},
gray: {
'0': '#ffffff',
'50': '#f8fafc',
'100': '#f1f5f9',
'200': '#e2e9f0',
'300': '#cbd6e1',
'400': '#94a7b8',
'500': '#60788f',
'600': '#475969',
'700': '#334555',
'800': '#1b2d3e',
'900': '#0b1d2e'
},
orange: {
'50': '#fff7ed',
'100': '#ffedd5',
'200': '#fed7aa',
'300': '#fdba74',
'400': '#fb923c',
'500': '#f97316',
'600': '#ea580c',
'700': '#c2410c',
'800': '#9a3412',
'900': '#7c2d12'
},
rose: {
'50': '#fff1f2',
'100': '#ffe4e6',
'200': '#fecdd3',
'300': '#fda4af',
'400': '#fb7185',
'500': '#f43f5e',
'600': '#e11d48',
'700': '#be123c',
'800': '#9f1239',
'900': '#881337'
},
snowball: {
'50': '#ecf6fe',
'100': '#e1f1fe',
'200': '#ddeefd',
'300': '#cfe6fc',
'400': '#74bafb',
'500': '#47a4fa',
'600': '#0f86f5',
'700': '#0977dc',
'800': '#075185',
'900': '#0a3a5c'
},
base: {
canvas: '#ECF6FE',
bg: '#ffffff',
'bg-alternate': '#f8fafc',
'bg-emphasized': '#f1f5f9',
'bg-emphasized-danger': '#fff1f2',
'bg-emphasized-info': '#ecf6fe',
'bg-emphasized-success': '#ecfdf5',
'bg-emphasized-warning': '#fff7ed'
},
border: 'hsl(var(--border))',
controls: {
danger: '#e11d48',
'danger-hovered': '#be123c',
disabled: '#e2e9f0',
'disabled-active': '#74bafb',
elevated: '#ffffff',
inset: '#e2e9f0',
'inset-hovered': '#cbd6e1',
primary: '#0f86f5',
'primary-hovered': '#0977dc',
secondary: '#ddeefd',
'secondary-hovered': '#cfe6fc',
tertiary: '#ffffff',
'tertiary-hovered': '#f8fafc'
},
elements: {
danger: '#e11d48',
disabled: '#94a7b8',
'high-em': '#0b1d2e',
info: '#0f86f5',
link: '#0f86f5',
'link-hovered': '#0977dc',
'low-em': '#60788f',
'mid-em': '#475969',
'on-danger': '#ffffff',
'on-disabled': '#60788f',
'on-disabled-active': '#0a3a5c',
'on-emphasized-danger': '#9f1239',
'on-emphasized-info': '#0a3a5c',
'on-emphasized-success': '#065f46',
'on-emphasized-warning': '#9a3412',
'on-high-contrast': '#ffffff',
'on-primary': '#ffffff',
'on-secondary': '#0977dc',
'on-secondary-tinted': '#075185',
'on-tertiary': '#1b2d3e',
success: '#059669',
warning: '#ea580c'
},
surface: {
card: '#ffffff',
'card-hovered': '#f8fafc',
floating: '#ffffff',
'floating-hovered': '#f1f5f9',
'high-contrast': '#0b1d2e'
},
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
},
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
}
},
boxShadow: {
button: '0px -2px 0px 0px rgba(0, 0, 0, 0.04) inset, 0px 0px 4px 0px rgba(255, 255, 255, 0.25) inset',
dropdown: '0px 3px 20px rgba(8, 47, 86, 0.1), 0px 0px 4px rgba(8, 47, 86, 0.14)',
field: '0px 1px 2px rgba(0, 0, 0, 0.04)',
inset: 'inset 0px 1px 0px rgba(8, 47, 86, 0.06)',
card: '0px 0px 0px 1px #E8F0F7, 0px 2px 4px rgba(8, 47, 86, 0.04)',
'card-sm': '0px 1px 2px -1px rgba(4, 25, 47, 0.08)'
},
spacing: {
'2.5': '0.625rem',
'3.25': '0.8125rem',
'3.5': '0.875rem',
'4.5': '1.125rem'
},
zIndex: {
tooltip: '52',
toast: '9999'
}
}
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
fontFamily: {
sans: ['Inter', ...fontFamily.sans],
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
});
}
module.exports = withMT(config);

View File

@ -0,0 +1,109 @@
# Layout Strategy with Vite & React Router
## Core Principles
1. **NavigationWrapper**
- Top-level container
- Includes global navigation
- Handles routing structure
2. **ScreenWrapper**
- Standard content area
- Consistent spacing
- Responsive behavior
3. **Header Components**
- Page-specific headers
- Action buttons
- Consistent styling
4. **TabWrapper**
- Tabbed navigation
- State management
- Responsive design
## Implementation Guide
### 1. App Structure
```typescript
// src/App.tsx
const App = () => (
<NavigationWrapper>
<Routes>
<Route path="/" element={<HomeLayout />}>
<Route index element={<HomePage />} />
<Route path="about" element={<AboutPage />} />
</Route>
<Route path="/projects/*" element={<ProjectLayout />} />
</Routes>
</NavigationWrapper>
);
```
### 2. Layout Components
```typescript
// src/layouts/HomeLayout.tsx
export const HomeLayout = () => (
<ScreenWrapper>
<Header />
<Outlet />
</ScreenWrapper>
);
// src/layouts/ProjectLayout.tsx
export const ProjectLayout = () => (
<ScreenWrapper>
<ProjectHeader />
<TabWrapper>
<Outlet />
</TabWrapper>
</ScreenWrapper>
);
```
### 3. Component Usage
```typescript
// src/pages/HomePage.tsx
const HomePage = () => (
<ScreenWrapper>
<Header
title="Welcome"
actions={[
<Button>Primary Action</Button>,
<Button variant="outline">Secondary Action</Button>
]}
/>
<MainContent />
</ScreenWrapper>
);
```
## Best Practices
1. **Consistent Wrappers**
- Use NavigationWrapper at top level
- Apply ScreenWrapper for content areas
- Utilize Header components for page titles
2. **Vite Optimization**
- Leverage code splitting
- Use dynamic imports for large components
- Optimize build process
3. **Routing Structure**
- Maintain clear route hierarchy
- Use nested routes for related pages
- Keep route definitions centralized
4. **Responsive Design**
- Ensure wrappers handle mobile layouts
- Test across viewport sizes
- Use consistent breakpoints
## Migration Checklist
- [ ] Update App.tsx structure
- [ ] Create layout components
- [ ] Refactor existing pages
- [ ] Update Storybook stories
- [ ] Write integration tests

View File

@ -0,0 +1,37 @@
# Frontend Route Component Mapping
## Core Route Hierarchy
```mermaid
graph TD
A[/:orgSlug/] --> B[projects/]
B --> C[create/]
C --> D[template/]
D --> E[configure]
D --> F[deploy]
C --> G[success/:id]
B --> H[:id/]
H --> I[overview]
H --> J[deployments]
H --> K[settings/]
K --> L[general]
K --> M[git]
K --> N[domains]
K --> O[environment-variables]
```
## Component Mapping Table
| Route Path | Component File | Entry Point |
|------------|----------------|-------------|
| `:orgSlug/projects/create` | `pages/org-slug/projects/create/index.tsx` | NewProject |
| `:orgSlug/projects/create/template` | `pages/org-slug/projects/create/template/index.tsx` | CreateRepo |
| `:orgSlug/projects/create/template/configure` | `pages/org-slug/projects/create/template/Configure.tsx` | Configure |
| `:orgSlug/projects/create/template/deploy` | `pages/org-slug/projects/create/template/Deploy.tsx` | Deploy |
| `:orgSlug/projects/create/success/:id` | `pages/org-slug/projects/create/success/Id.tsx` | Id |
| `:orgSlug/projects/:id/overview` | `pages/org-slug/projects/id/Overview.tsx` | OverviewTabPanel |
| `:orgSlug/projects/:id/deployments` | `pages/org-slug/projects/id/Deployments.tsx` | DeploymentsTabPanel |
| `:orgSlug/projects/:id/settings/general` | `pages/org-slug/projects/id/settings/General.tsx` | GeneralTabPanel |
| `:orgSlug/projects/:id/settings/git` | `pages/org-slug/projects/id/settings/Git.tsx` | GitTabPanel |
| `:orgSlug/projects/:id/settings/domains` | `pages/org-slug/projects/id/settings/Domains.tsx` | Domains |
| `:orgSlug/projects/:id/settings/environment-variables` | `pages/org-slug/projects/id/settings/EnvironmentVariables.tsx` | EnvironmentVariablesTabPanel |

View File

@ -0,0 +1,70 @@
---
title: Shared Components Registry
last-updated: 2023-11-28
---
# Shared Components Registry
## UI Components
### Core Components
- `Alert` - System notifications and warnings
- `Avatar` - User profile pictures with fallback
- `Badge` - Status and count indicators
- `Box` - Basic layout container
- `Button` - Action triggers
- `Calendar` - Date selection
- `Checkbox` - Boolean selection
- `DatePicker` - Date selection with calendar
- `Dialog` - Modal dialogs
- `Form` - Form wrapper with validation
- `FormControl` - Form field container
- `FormHelperText` - Form field helper text
- `Heading` - Text headings
- `IconWithFrame` - Icons with background frame
- `Input` - Text input fields
- `InputOTP` - One-time password input
- `Modal` - Overlay windows
- `OverflownText` - Text with ellipsis
- `Radio` - Single option selection
- `Select` - Dropdown selection
- `Separator` - Visual dividers
- `Sheet` - Slide-out panels
- `Switch` - Toggle switches
- `Table` - Data tables
- `Tabs` - Content organization
- `Tag` - Labels and categories
- `Toast` - Temporary notifications
- `ToggleGroup` - Button group toggles
- `Tooltip` - Hover information
- `UserSelect` - User selection dropdown
### Layout Components
- `NavigationWrapper` - Navigation container
- `ScreenWrapper` - Page container
- `Header` - Page headers
- `TabPageNavigation` - Tab-based navigation
### Form Components
- `FormField` - Form field wrapper
- `FormItem` - Form item container
- `FormLabel` - Form field labels
- `FormMessage` - Form field messages
- `FormDescription` - Form field descriptions
### Feedback Components
- `LoadingIcon` - Loading spinners
- `InlineNotification` - Inline alerts
- `Progress` - Progress indicators
- `Spinner` - Loading animation
### Compound Components
- `ActionButton` - Buttons with icons
- `WavyBorder` - Decorative borders
- `CustomIcon` - Icon system
- `ConfirmDialog` - Confirmation modals

View File

@ -0,0 +1,7 @@
---
title: Component Usage Analysis
last-updated: 2023-11-28
---
// ...existing code...
```

109
scripts/kill-ports.js Executable file
View File

@ -0,0 +1,109 @@
#!/usr/bin/env node
const { exec } = require('child_process');
const { promisify } = require('util');
const chalk = require('chalk');
const execAsync = promisify(exec);
const ports = [3000, 3001, 8000];
async function killProcessOnPort(port) {
try {
console.log(chalk.blue(`🔍 Checking if port ${port} is in use...`));
if (process.platform === 'darwin' || process.platform === 'linux') {
// Try with lsof first
try {
const { stdout } = await execAsync(`lsof -i :${port} -t`);
if (stdout.trim()) {
console.log(chalk.yellow(`⚠️ Port ${port} is in use. Attempting to terminate processes...`));
const pids = stdout.trim().split('\n').filter(Boolean);
for (const pid of pids) {
try {
await execAsync(`kill -9 ${pid}`);
console.log(chalk.green(`✅ Terminated process ${pid} using port ${port}`));
} catch (error) {
console.error(chalk.red(`Failed to kill process ${pid}: ${error.message}`));
}
}
} else {
console.log(chalk.green(`✅ Port ${port} is available`));
}
} catch (lsofError) {
// Fallback to netstat if lsof fails
try {
const { stdout } = await execAsync(`netstat -anp 2>/dev/null | grep ${port} | grep LISTEN`);
if (stdout.trim()) {
console.log(chalk.yellow(`⚠️ Port ${port} is in use. Using netstat fallback...`));
// Extract PIDs with regex - this is more complex with netstat
const pidRegex = /\s(\d+)\/\w+\s*$/;
const matches = stdout.match(pidRegex);
if (matches && matches[1]) {
const pid = matches[1];
try {
await execAsync(`kill -9 ${pid}`);
console.log(chalk.green(`✅ Terminated process ${pid} using port ${port}`));
} catch (error) {
console.error(chalk.red(`Failed to kill process ${pid}: ${error.message}`));
}
} else {
console.log(chalk.yellow(`⚠️ Could not extract PID from netstat output`));
}
} else {
console.log(chalk.green(`✅ Port ${port} is available`));
}
} catch (netstatError) {
console.log(chalk.yellow(`⚠️ Could not check port ${port}: Both lsof and netstat failed`));
}
}
} else if (process.platform === 'win32') {
// Windows approach
const { stdout } = await execAsync(`netstat -ano | findstr :${port}`);
if (stdout.trim()) {
console.log(chalk.yellow(`⚠️ Port ${port} is in use. Attempting to terminate processes...`));
const pids = stdout.split('\n')
.filter(line => line.includes(`:${port}`))
.map(line => line.trim().split(/\s+/).pop() || '')
.filter(Boolean);
for (const pid of pids) {
try {
await execAsync(`taskkill /F /PID ${pid}`);
console.log(chalk.green(`✅ Terminated process ${pid} using port ${port}`));
} catch (error) {
console.error(chalk.red(`Failed to kill process ${pid}: ${error.message}`));
}
}
} else {
console.log(chalk.green(`✅ Port ${port} is available`));
}
}
} catch (error) {
// General error handling
console.log(chalk.yellow(`⚠️ Could not check port ${port}: ${error.message}`));
console.log(chalk.blue('Continuing anyway...'));
}
}
async function main() {
console.log(chalk.cyan.bold('🧹 Port Cleanup Utility'));
console.log(chalk.cyan('---------------------'));
for (const port of ports) {
await killProcessOnPort(port);
}
console.log(chalk.green('✅ Port cleanup complete'));
}
main().catch(error => {
console.error(chalk.red(`❌ Unhandled error: ${error.message}`));
console.log(chalk.yellow('Continuing with startup anyway...'));
process.exit(0); // Exit successfully even if port killing fails
});

303
scripts/mono-start.js Normal file
View File

@ -0,0 +1,303 @@
#!/usr/bin/env node
const { spawn, exec } = require('child_process');
const chalk = require('chalk');
const path = require('path');
const fs = require('fs');
const { promisify } = require('util');
const os = require('os');
const execAsync = promisify(exec);
// Configuration
const config = {
rootDir: process.cwd(),
backendDir: path.join(process.cwd(), 'packages/backend'),
frontendDir: path.join(process.cwd(), 'packages/frontend'),
ports: {
backend: 8000,
frontend: 3000,
alternativeFrontend: 3001
}
};
// Helper to check if a port is in use and kill the process if needed
async function ensurePortAvailable(port) {
try {
console.log(chalk.blue(`🔍 Checking if port ${port} is in use...`));
// Different commands for different operating systems
let command;
if (process.platform === 'win32') {
command = `netstat -ano | findstr :${port}`;
} else {
command = `lsof -i :${port} -t`;
}
const { stdout } = await execAsync(command);
if (stdout.trim()) {
console.log(chalk.yellow(`⚠️ Port ${port} is in use. Attempting to terminate processes...`));
let pids;
if (process.platform === 'win32') {
// Extract PIDs from Windows netstat output
pids = stdout.split('\n')
.filter(line => line.includes(`:${port}`))
.map(line => line.trim().split(/\s+/).pop() || '')
.filter(Boolean);
} else {
// Unix lsof already returns PIDs directly
pids = stdout.trim().split('\n').filter(Boolean);
}
// Kill each process
for (const pid of pids) {
try {
const killCommand = process.platform === 'win32'
? `taskkill /F /PID ${pid}`
: `kill -9 ${pid}`;
await execAsync(killCommand);
console.log(chalk.green(`✅ Terminated process ${pid} using port ${port}`));
} catch (error) {
console.error(chalk.red(`Failed to kill process ${pid}: ${error}`));
}
}
} else {
console.log(chalk.green(`✅ Port ${port} is available`));
}
} catch (error) {
// If the command fails (e.g., lsof not found), assume port is available
console.log(chalk.yellow(`⚠️ Could not check port ${port}: ${error}`));
}
}
// Helper to run commands with proper output handling
function runCommand(command, args, cwd, label) {
return new Promise((resolve, reject) => {
console.log(chalk.blue(`🚀 Starting: ${label}...`));
const childProcess = spawn(command, args, {
cwd,
stdio: 'pipe',
shell: true
});
let stdout = '';
let stderr = '';
childProcess.stdout && childProcess.stdout.on('data', (data) => {
const output = data.toString();
stdout += output;
process.stdout.write(chalk.gray(`[${label}] `) + output);
});
childProcess.stderr && childProcess.stderr.on('data', (data) => {
const output = data.toString();
stderr += output;
process.stderr.write(chalk.yellow(`[${label}] `) + output);
});
childProcess.on('error', (error) => {
console.error(chalk.red(`❌ Error in ${label}: ${error.message}`));
reject(error);
});
childProcess.on('close', (code) => {
if (code === 0) {
console.log(chalk.green(`✅ Completed: ${label}`));
resolve();
} else {
console.error(chalk.red(`❌ Failed: ${label} (exit code: ${code})`));
reject(new Error(`Command failed with exit code ${code}: ${stderr}`));
}
});
});
}
// Check if iTerm2 is available (macOS only)
async function isITerm2Available() {
if (process.platform !== 'darwin') return false;
try {
const { stdout } = await execAsync('osascript -e "exists application \\"iTerm2\\""');
return stdout.trim() === 'true';
} catch (error) {
return false;
}
}
// Open split panes in iTerm2
async function openITerm2SplitPanes(backendCommand, frontendCommand) {
const script = `
tell application "iTerm2"
create window with default profile
tell current window
tell current session
set backendSession to (split horizontally with default profile)
set frontendSession to (split vertically with default profile)
select
write text "cd ${config.backendDir} && clear && echo '🚀 BACKEND SERVER' && ${backendCommand}"
set name to "Backend Server"
tell backendSession
select
write text "cd ${config.frontendDir} && clear && echo '🚀 FRONTEND DEV SERVER' && ${frontendCommand}"
set name to "Frontend Dev Server"
end tell
tell frontendSession
select
write text "cd ${config.rootDir} && clear && echo '📊 MONOREPO ROOT'"
set name to "Monorepo Root"
end tell
end tell
end tell
end tell
`;
try {
await execAsync(`osascript -e '${script}'`);
return true;
} catch (error) {
console.error(chalk.yellow(`⚠️ Failed to open iTerm2 with split panes: ${error.message}`));
return false;
}
}
// Open a new terminal based on platform
async function openTerminals(backendCommand, frontendCommand) {
// First check if we can use iTerm2 on macOS
if (process.platform === 'darwin') {
const iTerm2Available = await isITerm2Available();
if (iTerm2Available) {
console.log(chalk.blue('🖥️ Opening iTerm2 with split panes...'));
const success = await openITerm2SplitPanes(backendCommand, frontendCommand);
if (success) return true;
}
}
// Fallback to separate terminal windows
const escapedBackendCommand = backendCommand.replace(/"/g, '\\"');
const escapedFrontendCommand = frontendCommand.replace(/"/g, '\\"');
const escapedBackendDir = config.backendDir.replace(/"/g, '\\"');
const escapedFrontendDir = config.frontendDir.replace(/"/g, '\\"');
// Platform-specific terminal opening commands
if (process.platform === 'darwin') {
// macOS - Terminal.app
const backendScript = `
tell application "Terminal"
do script "cd \\"${escapedBackendDir}\\" && clear && echo '🚀 BACKEND SERVER' && ${escapedBackendCommand}"
set custom title of front window to "Backend Server"
end tell
`;
const frontendScript = `
tell application "Terminal"
do script "cd \\"${escapedFrontendDir}\\" && clear && echo '🚀 FRONTEND DEV SERVER' && ${escapedFrontendCommand}"
set custom title of front window to "Frontend Dev Server"
end tell
`;
await execAsync(`osascript -e '${backendScript}'`);
await execAsync(`osascript -e '${frontendScript}'`);
} else if (process.platform === 'win32') {
// Windows - try Windows Terminal first, fall back to cmd
try {
// Windows Terminal (supports multiple tabs)
await execAsync(`wt -w 0 -d "${escapedBackendDir}" cmd /k "title Backend Server && ${escapedBackendCommand}" ; split-pane -d "${escapedFrontendDir}" cmd /k "title Frontend Dev Server && ${escapedFrontendCommand}"`);
} catch (error) {
// Fallback to regular cmd windows
spawn('cmd.exe', ['/c', 'start', 'cmd.exe', '/K',
`cd /d "${escapedBackendDir}" && title Backend Server && ${escapedBackendCommand}`]);
spawn('cmd.exe', ['/c', 'start', 'cmd.exe', '/K',
`cd /d "${escapedFrontendDir}" && title Frontend Dev Server && ${escapedFrontendCommand}`]);
}
} else {
// Linux terminals with split support
try {
// Try Tilix (supports split screen)
await execAsync(`tilix --window-style=disable-csd-hide-toolbar --maximize --session-file=<(echo '[{"command":"cd ${escapedBackendDir} && ${escapedBackendCommand}","title":"Backend Server"},{"command":"cd ${escapedFrontendDir} && ${escapedFrontendCommand}","title":"Frontend Dev Server"}]')`);
} catch (error) {
try {
// Try Terminator (supports split screen)
await execAsync(`terminator --maximize -e "bash -c 'cd ${escapedBackendDir} && ${escapedBackendCommand}'" -e "bash -c 'cd ${escapedFrontendDir} && ${escapedFrontendCommand}'"`);
} catch (error) {
// Fallback to separate gnome-terminal windows
try {
spawn('gnome-terminal', ['--', 'bash', '-c',
`cd "${escapedBackendDir}" && echo -e "\\033]0;Backend Server\\007" && ${escapedBackendCommand}; exec bash`]);
spawn('gnome-terminal', ['--', 'bash', '-c',
`cd "${escapedFrontendDir}" && echo -e "\\033]0;Frontend Dev Server\\007" && ${escapedFrontendCommand}; exec bash`]);
} catch (error) {
// Last resort: try xterm
spawn('xterm', ['-T', 'Backend Server', '-e',
`cd "${escapedBackendDir}" && ${escapedBackendCommand}; exec bash`]);
spawn('xterm', ['-T', 'Frontend Dev Server', '-e',
`cd "${escapedFrontendDir}" && ${escapedFrontendCommand}; exec bash`]);
}
}
}
}
return true;
}
// Check if directory exists
function checkDirectoryExists(dir, name) {
if (!fs.existsSync(dir)) {
console.error(chalk.red(`❌ Error: ${name} directory not found at ${dir}`));
throw new Error(`Directory not found: ${dir}`);
}
}
// Main execution function
async function runMonorepoWorkflow() {
console.log(chalk.cyan.bold('📦 Monorepo Workflow Script'));
console.log(chalk.cyan('---------------------------'));
try {
// Validate directories
checkDirectoryExists(config.rootDir, 'Root');
checkDirectoryExists(config.backendDir, 'Backend');
checkDirectoryExists(config.frontendDir, 'Frontend');
// Ensure all required ports are available
await ensurePortAvailable(config.ports.backend);
await ensurePortAvailable(config.ports.frontend);
await ensurePortAvailable(config.ports.alternativeFrontend);
// Step 1: Install dependencies at root
await runCommand('yarn', [], config.rootDir, 'Root dependency installation');
// Step 2: Build packages (ignoring frontend)
await runCommand('yarn', ['build', '--ignore', 'frontend'], config.rootDir, 'Building packages');
// Step 3: Start services in split terminal
console.log(chalk.blue('🚀 Opening terminal with services...'));
await openTerminals('yarn start', 'yarn dev');
console.log(chalk.green.bold('✅ Development environment started!'));
console.log(chalk.cyan('Backend running at:') + chalk.yellow(` http://localhost:${config.ports.backend}`));
console.log(chalk.cyan('Frontend running at:') + chalk.yellow(` http://localhost:${config.ports.frontend}`));
console.log(chalk.gray('Check the opened terminal windows for detailed logs.'));
// Exit successfully
process.exit(0);
} catch (error) {
console.error(chalk.red.bold('❌ Workflow failed:'));
console.error(chalk.red(error.message));
process.exit(1);
}
}
// Execute the workflow
runMonorepoWorkflow().catch(error => {
console.error(chalk.red.bold('❌ Unhandled error:'));
console.error(error);
process.exit(1);
});

53
scripts/open-terminals.js Executable file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const chalk = require('chalk');
const path = require('path');
const fs = require('fs');
const rootDir = process.cwd();
const backendDir = path.join(rootDir, 'packages/backend');
const frontendDir = path.join(rootDir, 'packages/frontend');
// Create shell scripts for each service
const backendScript = path.join(rootDir, 'scripts', 'start-backend.sh');
const frontendScript = path.join(rootDir, 'scripts', 'start-frontend.sh');
// Write shell scripts
fs.writeFileSync(backendScript, `#!/bin/bash
cd "${backendDir}"
echo "🚀 Starting Backend Server..."
yarn start
`, { mode: 0o755 });
fs.writeFileSync(frontendScript, `#!/bin/bash
cd "${frontendDir}"
echo "🚀 Starting Frontend Dev Server..."
yarn dev
`, { mode: 0o755 });
console.log(chalk.cyan.bold('📦 Starting development environment'));
try {
// Clean up ports first
console.log(chalk.blue('🧹 Cleaning up ports...'));
execSync('yarn kill:ports', { stdio: 'inherit' });
// Build packages
console.log(chalk.blue('🔨 Building packages...'));
execSync('yarn build --ignore frontend', { stdio: 'inherit' });
console.log(chalk.green('✅ Build complete'));
console.log(chalk.blue('🚀 Starting services in separate terminals...'));
// Open new terminals with the shell scripts
execSync(`open -a Terminal ${backendScript}`);
execSync(`open -a Terminal ${frontendScript}`);
console.log(chalk.green('✅ Services started in separate terminal windows'));
console.log(chalk.cyan('Backend running at:') + chalk.yellow(' http://localhost:8000'));
console.log(chalk.cyan('Frontend running at:') + chalk.yellow(' http://localhost:3000'));
} catch (error) {
console.error(chalk.red(`❌ Error: ${error.message}`));
process.exit(1);
}

49
start-app.js Normal file
View File

@ -0,0 +1,49 @@
const { exec } = require('child_process');
const path = require('path');
// Function to execute shell commands
const runCommand = (command, cwd) => {
return new Promise((resolve, reject) => {
console.log(`Running command: ${command} in ${cwd}`);
const process = exec(command, { cwd }, (error, stdout, stderr) => {
if (error) {
console.error(`Command failed: ${command}`);
console.error(stderr);
reject(error);
return;
}
console.log(stdout);
resolve(stdout);
});
process.stdout.pipe(process.stdout);
process.stderr.pipe(process.stderr);
});
};
(async () => {
try {
const rootDir = __dirname;
const backendDir = path.join(rootDir, 'packages', 'backend');
const frontendDir = path.join(rootDir, 'packages', 'frontend');
console.log('Starting application setup...');
console.log('Running yarn install...');
await runCommand('yarn', rootDir);
console.log('Running yarn build...');
await runCommand('yarn build', rootDir);
console.log('Starting backend...');
await runCommand('yarn start', backendDir);
console.log('Starting frontend in new terminal...');
exec(`osascript -e 'tell application "Terminal" to do script "cd ${frontendDir} && yarn dev"'`);
console.log('Application started successfully!');
} catch (error) {
console.error('Error during setup:', error);
process.exit(1);
}
})();

4498
yarn.lock

File diff suppressed because it is too large Load Diff