Consolidate wrapper components
This commit is contained in:
parent
8353c1ecbe
commit
9d7ab3abac
25
.github/workflows/lint.yaml
vendored
25
.github/workflows/lint.yaml
vendored
@ -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
|
39
.github/workflows/test-app-deployment.yaml
vendored
39
.github/workflows/test-app-deployment.yaml
vendored
@ -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 }}
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -3,5 +3,7 @@
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
"tv\\('([^)]*)\\')",
|
||||
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
|
||||
]
|
||||
],
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
|
13
package.json
13
package.json
@ -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"
|
||||
}
|
||||
}
|
25
packages/frontend/config/tabPageConfig.tsx
Normal file
25
packages/frontend/config/tabPageConfig.tsx
Normal 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>,
|
||||
},
|
||||
],
|
||||
};
|
@ -78,9 +78,7 @@ function App() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<RouterProvider router={router} />
|
||||
);
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
39
packages/frontend/src/components/PageWithSubNav.tsx
Normal file
39
packages/frontend/src/components/PageWithSubNav.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
12
packages/frontend/src/components/ScreenWrapper.tsx
Normal file
12
packages/frontend/src/components/ScreenWrapper.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
28
packages/frontend/src/components/navigation/LaconicIcon.tsx
Normal file
28
packages/frontend/src/components/navigation/LaconicIcon.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
@ -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> */}
|
||||
</>
|
||||
);
|
||||
}
|
105
packages/frontend/src/components/navigation/TopNavigation.tsx
Normal file
105
packages/frontend/src/components/navigation/TopNavigation.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
22
packages/frontend/src/components/screen-header/Header.tsx
Normal file
22
packages/frontend/src/components/screen-header/Header.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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';
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
17
packages/frontend/src/components/tab-navigation/types.ts
Normal file
17
packages/frontend/src/components/tab-navigation/types.ts
Normal 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>
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
99
packages/frontend/src/context/WalletContext.tsx
Normal file
99
packages/frontend/src/context/WalletContext.tsx
Normal 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;
|
||||
};
|
33
packages/frontend/src/hooks/useGitHubAuth.ts
Normal file
33
packages/frontend/src/hooks/useGitHubAuth.ts
Normal 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 }
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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 */}
|
||||
|
@ -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'} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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 = [
|
||||
{
|
||||
|
101
packages/frontend/src/stores/github.ts
Normal file
101
packages/frontend/src/stores/github.ts
Normal 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,
|
||||
}),
|
||||
}
|
||||
)
|
||||
)
|
@ -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);
|
109
qwrk/docs/frontend/layouts/LAYOUT_STRATEGY.md
Normal file
109
qwrk/docs/frontend/layouts/LAYOUT_STRATEGY.md
Normal 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
|
37
qwrk/docs/frontend/routes/ROUTES.md
Normal file
37
qwrk/docs/frontend/routes/ROUTES.md
Normal 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 |
|
70
qwrk/poa/02-shared-components.md
Normal file
70
qwrk/poa/02-shared-components.md
Normal 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
|
7
qwrk/poa/03-component-usage.md
Normal file
7
qwrk/poa/03-component-usage.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Component Usage Analysis
|
||||
last-updated: 2023-11-28
|
||||
---
|
||||
|
||||
// ...existing code...
|
||||
```
|
109
scripts/kill-ports.js
Executable file
109
scripts/kill-ports.js
Executable 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
303
scripts/mono-start.js
Normal 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
53
scripts/open-terminals.js
Executable 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
49
start-app.js
Normal 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);
|
||||
}
|
||||
})();
|
Loading…
Reference in New Issue
Block a user