WIP: feat(primary): develop-feat #56

Draft
icld wants to merge 19 commits from QWRK-org/laconic-deploy:develop-feat into main
566 changed files with 29645 additions and 21522 deletions

40
.github/workflows/docs.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Generate and Deploy Docs
on:
workflow_dispatch: # Allow manual triggering
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for proper branch detection
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Update TypeDoc config for current branch
run: |
CURRENT_BRANCH=${GITHUB_REF#refs/heads/}
echo "Current branch: $CURRENT_BRANCH"
# Update gitRevision in typedoc.json to use the current branch
sed -i "s|\"gitRevision\": \"qrigin/[^\"]*\"|\"gitRevision\": \"qrigin/$CURRENT_BRANCH\"|" typedoc.mjs
- name: Generate documentation
run: yarn typedoc
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: docs
clean: true
token: ${{ secrets.ACTIONS_ONLY }} # Use the ACTION_ONLY token

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 }}

6
.gitignore vendored
View File

@ -3,6 +3,7 @@ yarn-error.log
.yarnrc.yml
.yarn/
.yarnrc
.cursor
packages/backend/environments/local.toml
packages/backend/dev/
@ -10,3 +11,8 @@ packages/frontend/dist/
# ignore all .DS_Store files
**/.DS_Store
.vscode
# TypeDoc generated documentation
docs/
.cursor/

View File

@ -1,7 +0,0 @@
{
// IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [
"tv\\('([^)]*)\\')",
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
]
}

165
DOCUMENTATION.md Normal file
View File

@ -0,0 +1,165 @@
# Documentation Guide for Snowball Tools
This guide explains how to write and generate documentation for the Snowball Tools project.
## TSDoc and TypeDoc
We use [TSDoc](https://tsdoc.org/) for documenting our TypeScript code and [TypeDoc](https://typedoc.org/) for generating API documentation from those comments.
## Writing Documentation
### Basic Comment Structure
TSDoc comments start with `/**` and end with `*/`. Each line within the comment block typically starts with a `*`.
```typescript
/**
* This is a TSDoc comment.
*/
```
### Documenting Functions
```typescript
/**
* Calculates the sum of two numbers.
*
* @param a - The first number
* @param b - The second number
* @returns The sum of a and b
*
* @example
* ```ts
* const result = add(1, 2);
* console.log(result); // 3
* ```
*/
function add(a: number, b: number): number {
return a + b;
}
```
### Documenting Classes
```typescript
/**
* Represents a user in the system.
*
* @remarks
* This class is used throughout the application to represent user data.
*
* @example
* ```ts
* const user = new User('John', 'Doe');
* console.log(user.fullName); // "John Doe"
* ```
*/
class User {
/**
* Creates a new User instance.
*
* @param firstName - The user's first name
* @param lastName - The user's last name
*/
constructor(
/** The user's first name */
public firstName: string,
/** The user's last name */
public lastName: string
) {}
/**
* Gets the user's full name.
*/
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
```
### Documenting Interfaces
```typescript
/**
* Configuration options for the application.
*
* @public
*/
interface AppConfig {
/**
* The port number the server should listen on.
* @default 3000
*/
port: number;
/**
* The host the server should bind to.
* @default "localhost"
*/
host: string;
/**
* Whether to enable debug mode.
* @default false
*/
debug?: boolean;
}
```
## Common TSDoc Tags
| Tag | Description |
|-----|-------------|
| `@param` | Documents a function parameter |
| `@returns` | Documents the return value |
| `@throws` | Documents exceptions that might be thrown |
| `@example` | Provides an example of usage |
| `@remarks` | Adds additional information |
| `@deprecated` | Marks an item as deprecated |
| `@see` | Refers to related documentation |
| `@default` | Documents the default value |
| `@public`, `@protected`, `@private` | Visibility modifiers |
| `@internal` | Marks an item as internal (not part of the public API) |
| `@beta` | Marks an item as in beta stage |
| `@alpha` | Marks an item as in alpha stage |
| `@experimental` | Marks an item as experimental |
## Generating Documentation
To generate documentation for the project, run:
```bash
yarn docs
```
This will create a `docs` directory with the generated documentation.
To watch for changes and regenerate documentation automatically:
```bash
yarn docs:watch
```
## Best Practices
1. **Document Public APIs**: Always document public APIs thoroughly.
2. **Include Examples**: Provide examples for complex functions or classes.
3. **Be Concise**: Keep documentation clear and to the point.
4. **Use Proper Grammar**: Use proper grammar and punctuation.
5. **Update Documentation**: Keep documentation in sync with code changes.
6. **Document Parameters**: Document all parameters, including their types and purpose.
7. **Document Return Values**: Document what a function returns.
8. **Document Exceptions**: Document any exceptions that might be thrown.
## Example Files
For reference, check out these example files that demonstrate proper TSDoc usage:
- `packages/backend/src/utils/tsdoc-example.ts`
- `packages/frontend/src/utils/tsdoc-example.ts`
## Resources
- [TSDoc Official Documentation](https://tsdoc.org/)
- [TypeDoc Official Documentation](https://typedoc.org/)
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)

View File

@ -1,23 +0,0 @@
# snowballtools-base
This is a [yarn workspace](https://yarnpkg.com/features/workspaces) monorepo for the dashboard.
## Getting Started
### Install dependencies
In the root of the project, run:
```zsh
yarn
```
### Build backend
```zsh
yarn build --ignore frontend
```
### Environment variables, running the development server, and deployment
Follow the instructions in the README.md files of the [backend](packages/backend/README.md) and [frontend](packages/frontend/README.md) packages.

114
docs-readme.md Normal file
View File

@ -0,0 +1,114 @@
# Snowball Tools API Documentation
This documentation is automatically generated using [TypeDoc](https://typedoc.org/) from TSDoc comments in the codebase.
## Packages
The monorepo contains the following packages:
- **frontend**: The frontend web application
- **backend**: The backend API server
- **gql-client**: GraphQL client library
- **deployer**: Deployment utilities
## How to Use This Documentation
The documentation is organized by packages and modules. You can navigate through the sidebar to explore the different parts of the codebase.
## Contributing to Documentation
To contribute to the documentation:
1. Add TSDoc comments to your code using the JSDoc syntax
2. Run `yarn docs` to generate the documentation
3. Preview the documentation in the `docs` directory
## TSDoc Comment Examples
### Function Documentation
```typescript
/**
* Calculates the sum of two numbers.
*
* @param a - The first number
* @param b - The second number
* @returns The sum of a and b
*
* @example
* ```ts
* const result = add(1, 2);
* console.log(result); // 3
* ```
*/
function add(a: number, b: number): number {
return a + b;
}
```
### Class Documentation
```typescript
/**
* Represents a user in the system.
*
* @remarks
* This class is used throughout the application to represent user data.
*
* @example
* ```ts
* const user = new User('John', 'Doe');
* console.log(user.fullName); // "John Doe"
* ```
*/
class User {
/**
* Creates a new User instance.
*
* @param firstName - The user's first name
* @param lastName - The user's last name
*/
constructor(
/** The user's first name */
public firstName: string,
/** The user's last name */
public lastName: string
) {}
/**
* Gets the user's full name.
*/
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
```
### Interface Documentation
```typescript
/**
* Configuration options for the application.
*
* @public
*/
interface AppConfig {
/**
* The port number the server should listen on.
* @default 3000
*/
port: number;
/**
* The host the server should bind to.
* @default "localhost"
*/
host: string;
/**
* Whether to enable debug mode.
* @default false
*/
debug?: boolean;
}
```

View File

@ -5,14 +5,30 @@
"packages/*"
],
"devDependencies": {
"@microsoft/tsdoc": "^0.15.1",
"chalk": "^4.1.2",
"concurrently": "^8.2.0",
"depcheck": "^1.4.2",
"husky": "^8.0.3",
"lerna": "^8.0.0",
"patch-package": "^8.0.0"
"lerna": "^8.2.0",
"patch-package": "^8.0.0",
"rimraf": "^6.0.1",
"tsdoc": "^0.0.4",
"typedoc": "^0.27.9",
"typedoc-plugin-markdown": "^4.4.2",
"typedoc-unhoax-theme": "^0.4.6"
},
"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",
"docs": "yarn typedoc",
"docs:watch": "yarn typedoc --watch"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@ -54,7 +54,7 @@
"@types/cookie-session": "^2.0.49",
"@types/express-session": "^1.17.10",
"@types/fs-extra": "^11.0.4",
"better-sqlite3": "^9.2.2",
"better-sqlite3": "^9.4.1",
"copyfiles": "^2.4.1",
"prettier": "^3.1.1",
"workspace": "^0.0.1-preview.1"

View File

@ -1,22 +1,22 @@
import debug from 'debug';
import express from 'express';
import cors from 'cors';
import { ApolloServer } from 'apollo-server-express';
import { createServer } from 'http';
import {
ApolloServerPluginDrainHttpServer,
ApolloServerPluginLandingPageLocalDefault,
AuthenticationError,
} from 'apollo-server-core';
import { ApolloServer } from 'apollo-server-express';
import cors from 'cors';
import debug from 'debug';
import express from 'express';
import session from 'express-session';
import { createServer } from 'http';
import { TypeSource } from '@graphql-tools/utils';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { TypeSource } from '@graphql-tools/utils';
import { ServerConfig } from './config';
import { DEFAULT_GQL_PATH } from './constants';
import githubRouter from './routes/github';
import authRouter from './routes/auth';
import githubRouter from './routes/github';
import stagingRouter from './routes/staging';
import { Service } from './service';
@ -101,7 +101,7 @@ export const createAndStartServer = async (
}
app.use(
session(sessionOptions)
session(sessionOptions) as unknown as express.RequestHandler
);
server.applyMiddleware({
@ -116,9 +116,9 @@ export const createAndStartServer = async (
app.use(express.json());
app.set('service', service);
app.use('/auth', authRouter);
app.use('/api/github', githubRouter);
app.use('/staging', stagingRouter);
app.use('/auth', authRouter as express.RequestHandler);
app.use('/api/github', githubRouter as express.RequestHandler);
app.use('/staging', stagingRouter as express.RequestHandler);
app.use((err: any, req: any, res: any, next: any) => {
console.error(err);

View File

@ -0,0 +1,237 @@
/**
* This file demonstrates proper TSDoc usage for the Snowball Tools project.
*
* @packageDocumentation
* @module Utils
*/
/**
* Configuration options for the application.
*
* @public
*/
export interface AppConfig {
/**
* The port number the server should listen on.
* @default 3000
*/
port: number;
/**
* The host the server should bind to.
* @default "localhost"
*/
host: string;
/**
* Whether to enable debug mode.
* @default false
*/
debug?: boolean;
}
/**
* Represents a user in the system.
*
* @remarks
* This class is used throughout the application to represent user data.
*
* @example
* ```ts
* const user = new User('John', 'Doe');
* console.log(user.fullName); // "John Doe"
* ```
*/
export class User {
/**
* Creates a new User instance.
*
* @param firstName - The user's first name
* @param lastName - The user's last name
*/
constructor(
/** The user's first name */
public firstName: string,
/** The user's last name */
public lastName: string
) {}
/**
* Gets the user's full name.
*
* @returns The user's full name as a string
*/
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
/**
* Updates the user's name.
*
* @param firstName - The new first name
* @param lastName - The new last name
* @returns The updated user instance
*
* @throws {Error} If either name is empty
*/
updateName(firstName: string, lastName: string): User {
if (!firstName || !lastName) {
throw new Error('First name and last name cannot be empty');
}
this.firstName = firstName;
this.lastName = lastName;
return this;
}
}
/**
* Calculates the sum of two numbers.
*
* @param a - The first number
* @param b - The second number
* @returns The sum of a and b
*
* @example
* ```ts
* const result = add(1, 2);
* console.log(result); // 3
* ```
*/
export function add(a: number, b: number): number {
return a + b;
}
/**
* Formats a date according to the specified format.
*
* @param date - The date to format
* @param format - The format string (default: 'YYYY-MM-DD')
* @returns The formatted date string
*
* @remarks
* This function uses a simple format string where:
* - YYYY: 4-digit year
* - MM: 2-digit month
* - DD: 2-digit day
*
* @example
* ```ts
* const date = new Date('2023-01-15');
* const formatted = formatDate(date);
* console.log(formatted); // "2023-01-15"
* ```
*/
export function formatDate(date: Date, format: string = 'YYYY-MM-DD'): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return format
.replace('YYYY', year.toString())
.replace('MM', month)
.replace('DD', day);
}
/**
* Type of log level for the application.
*/
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
/**
* A simple logger utility.
*
* @remarks
* This logger provides methods for different log levels and can be configured
* to filter logs based on minimum level.
*/
export class Logger {
/**
* Creates a new Logger instance.
*
* @param name - The name of the logger
* @param minLevel - The minimum log level to display (default: 'info')
*/
constructor(
private name: string,
private minLevel: LogLevel = 'info'
) {}
/**
* Logs a debug message.
*
* @param message - The message to log
* @param data - Optional data to include
*/
debug(message: string, data?: unknown): void {
this.log('debug', message, data);
}
/**
* Logs an info message.
*
* @param message - The message to log
* @param data - Optional data to include
*/
info(message: string, data?: unknown): void {
this.log('info', message, data);
}
/**
* Logs a warning message.
*
* @param message - The message to log
* @param data - Optional data to include
*/
warn(message: string, data?: unknown): void {
this.log('warn', message, data);
}
/**
* Logs an error message.
*
* @param message - The message to log
* @param error - Optional error to include
*/
error(message: string, error?: Error): void {
this.log('error', message, error);
}
/**
* Internal method to handle logging.
*
* @param level - The log level
* @param message - The message to log
* @param data - Optional data to include
*
* @internal
*/
private log(level: LogLevel, message: string, data?: unknown): void {
const levels: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3
};
if (levels[level] >= levels[this.minLevel]) {
const timestamp = new Date().toISOString();
const formattedMessage = `[${timestamp}] [${level.toUpperCase()}] [${this.name}] ${message}`;
switch (level) {
case 'debug':
console.debug(formattedMessage, data || '');
break;
case 'info':
console.info(formattedMessage, data || '');
break;
case 'warn':
console.warn(formattedMessage, data || '');
break;
case 'error':
console.error(formattedMessage, data || '');
break;
}
}
}
}

View File

@ -16,10 +16,14 @@
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.local.example
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*storybook.log
*storybook.log
.cursor/

View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@ -20,16 +20,36 @@
"@emotion/styled": "^11.13.0",
"@fontsource-variable/jetbrains-mono": "^5.0.19",
"@fontsource/inter": "^5.0.16",
"@hookform/resolvers": "^4.1.0",
"@mui/material": "^6.1.3",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-aspect-ratio": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-context-menu": "^2.2.6",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-menubar": "^1.1.6",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@snowballtools/material-tailwind-react-fork": "^2.1.10",
"@snowballtools/smartwallet-alchemy-light": "^0.2.0",
"@snowballtools/types": "^0.2.0",
@ -44,31 +64,45 @@
"@web3modal/siwe": "4.0.5",
"assert": "^2.1.0",
"axios": "^1.6.7",
"clsx": "^2.1.0",
"date-fns": "^3.3.1",
"ethers": "^5.6.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
"date-fns": "^4.1.0",
"downshift": "^8.3.2",
"embla-carousel-react": "^8.5.2",
"ethers": "^5.6.2",
"framer-motion": "^11.0.8",
"gql-client": "^1.0.0",
"input-otp": "^1.4.2",
"lottie-react": "^2.4.0",
"lucide-react": "^0.475.0",
"luxon": "^3.4.4",
"next-themes": "^0.4.4",
"octokit": "^3.1.2",
"react": "^18.2.0",
"react-calendar": "^4.8.0",
"react-code-blocks": "^0.1.6",
"react-day-picker": "^8.9.1",
"react-day-picker": "8.10.1",
"react-dom": "^18.2.0",
"react-dropdown": "^1.11.0",
"react-hook-form": "^7.49.0",
"react-hook-form": "^7.54.2",
"react-oauth-popup": "^1.0.5",
"react-resizable-panels": "^2.1.7",
"react-router-dom": "^6.20.1",
"react-timer-hook": "^3.0.7",
"recharts": "^2.15.1",
"siwe": "2.1.4",
"sonner": "^2.0.0",
"tailwind-merge": "^3.0.1",
"tailwind-variants": "^0.2.0",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^2.15.1",
"uuid": "^9.0.1",
"vaul": "^1.1.2",
"viem": "^2.7.11",
"web-vitals": "^2.1.4"
"web-vitals": "^2.1.4",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@chromatic-com/storybook": "^1.3.3",
@ -83,7 +117,7 @@
"@types/jest": "^27.5.2",
"@types/lodash": "^4.17.0",
"@types/luxon": "^3.3.7",
"@types/node": "^16.18.68",
"@types/node": "^22.13.5",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/uuid": "^9.0.8",

File diff suppressed because it is too large Load Diff

View File

@ -1,85 +1,171 @@
import { useEffect } from 'react';
import ProjectSearchLayout from '@/layouts/ProjectSearch';
import ProjectsScreen from '@/pages/org-slug/ProjectsScreen';
import { useEffect, useState } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Projects from './pages/org-slug';
import Settings from './pages/org-slug/Settings';
import {
projectsRoutesWithSearch,
projectsRoutesWithoutSearch,
} from './pages/org-slug/projects/routes';
import ProjectSearchLayout from './layouts/ProjectSearch';
import { LoadingOverlay } from './components/loading/loading-overlay';
import { DashboardLayout } from './layouts/DashboardLayout';
import RootLayout from './layouts/RootLayout';
import Index from './pages';
import AuthPage from './pages/AuthPage';
import { DashboardLayout } from './pages/org-slug/layout';
import { BASE_URL } from 'utils/constants';
import BuyPrepaidService from './pages/BuyPrepaidService';
import OnboardingDemoPage from './pages/OnboardingDemoPage';
import OnboardingPage from './pages/OnboardingPage';
import {
projectsRoutesWithoutSearch,
projectsRoutesWithSearch,
} from './pages/org-slug/projects/project-routes';
import Settings from './pages/org-slug/Settings';
import { BASE_URL } from './utils/constants';
/**
* IframeLoader component that ensures wallet iframe is loaded
* before rendering children components that depend on it.
*
* TEMPORARY SOLUTION: This is a quick fix for iframe loading issues.
*
* TODO: Future Refactoring Plan (Medium effort, 4-8 hours):
* - Move iframe management directly into WalletContextProvider
* - Handle multiple wallet-related iframes in a single location
*/
const IframeLoader = ({ children }: { children: React.ReactNode }) => {
const [iframeLoaded, setIframeLoaded] = useState(false);
useEffect(() => {
const createIframe = () => {
// Check if iframe already exists
let iframe = document.getElementById(
'wallet-iframe',
) as HTMLIFrameElement;
if (!iframe) {
iframe = document.createElement('iframe');
iframe.id = 'wallet-iframe';
iframe.style.display = 'none';
iframe.src = `${window.location.origin}/wallet-iframe.html`;
iframe.onload = () => {
setIframeLoaded(true);
};
document.body.appendChild(iframe);
} else {
// If iframe already exists, consider it loaded
setIframeLoaded(true);
}
};
createIframe();
// Cleanup function
return () => {
const iframe = document.getElementById('wallet-iframe');
if (iframe && iframe.parentNode) {
iframe.parentNode.removeChild(iframe);
}
};
}, []);
if (!iframeLoaded) {
return <LoadingOverlay />;
}
return <>{children}</>;
};
// Wrap RootLayout with IframeLoader
const LoaderWrappedRootLayout = () => (
<IframeLoader>
<RootLayout />
</IframeLoader>
);
const router = createBrowserRouter([
{
path: ':orgSlug',
element: <DashboardLayout />,
element: <LoaderWrappedRootLayout />,
children: [
{
element: <ProjectSearchLayout />,
path: ':orgSlug',
element: <DashboardLayout />,
children: [
{
path: '',
element: <Projects />,
element: <ProjectSearchLayout />,
children: [
{
path: '',
element: <ProjectsScreen />,
},
{
path: 'projects',
children: projectsRoutesWithSearch,
},
],
},
{
path: 'settings',
element: <Settings />,
},
{
path: 'projects',
children: projectsRoutesWithSearch,
children: projectsRoutesWithoutSearch,
},
],
},
{
path: 'settings',
element: <Settings />,
path: '/',
element: <Index />,
},
{
path: 'projects',
children: projectsRoutesWithoutSearch,
path: '/login',
element: <AuthPage />,
},
{
path: '/buy-prepaid-service',
element: <BuyPrepaidService />,
errorElement: <div>Something went wrong!</div>,
},
{
path: '/onboarding',
element: <OnboardingPage />,
},
{
path: '/onboarding-demo',
element: <OnboardingDemoPage />,
},
],
},
{
path: '/',
element: <Index />,
},
{
path: '/login',
element: <AuthPage />,
},
{
path: '/buy-prepaid-service',
element: <BuyPrepaidService />,
},
]);
/**
* Main application component.
* Sets up routing and error handling.
* @returns {JSX.Element} The rendered application.
*/
function App() {
// Hacky way of checking session
// TODO: Handle redirect backs
useEffect(() => {
fetch(`${BASE_URL}/auth/session`, {
credentials: 'include',
}).then((res) => {
const path = window.location.pathname;
const publicPaths = ['/login', '/onboarding', '/onboarding-demo', '/'];
console.log(res);
if (res.status !== 200) {
localStorage.clear();
if (path !== '/login') {
if (!publicPaths.includes(path)) {
window.location.pathname = '/login';
}
} else {
if (path === '/login') {
window.location.pathname = '/';
window.location.pathname = '/deploy-tools';
}
}
});
}, []);
return (
<RouterProvider router={router} />
<RouterProvider router={router} fallbackElement={<LoadingOverlay />} />
);
}

View File

@ -1,8 +1,8 @@
import {
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO,
VITE_GITHUB_PWA_TEMPLATE_REPO,
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO,
} from 'utils/constants';
VITE_GITHUB_PWA_TEMPLATE_REPO,
} from '@/utils/constants';
export default [
{

View File

@ -1,219 +0,0 @@
import React from 'react';
type Props = React.PropsWithChildren<{
className?: string;
snowZIndex?: number;
}>;
export const CloudyFlow = ({ className, children, snowZIndex }: Props) => {
return (
<div className={`bg-sky-100 relative ${className || ''}`}>
{children}
<div
className="absolute inset-0 overflow-hidden"
style={{ zIndex: snowZIndex || 0 }}
>
<div className="w-[3.72px] h-[3.72px] left-[587px] top-[147px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.72px] h-[4.72px] left-[742px] top-[336px] absolute bg-white rounded-full" />
<div className="w-[3.49px] h-[3.49px] left-[36px] top-[68px] absolute bg-white rounded-full" />
<div className="w-[3.25px] h-[3.25px] left-[55px] top-[114px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.60px] h-[5.60px] left-[1334px] top-[63px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[3.53px] h-[3.53px] left-[988px] top-[108px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.65px] h-[2.65px] left-[1380px] top-[16px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[3.60px] h-[3.60px] left-[1284px] top-[95px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-0.5 h-0.5 left-[1191px] top-[376px] absolute bg-white rounded-full" />
<div className="w-[2.83px] h-[2.83px] left-[1182px] top-[257px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[2.41px] h-[2.41px] left-[627px] top-[26px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.71px] h-[5.71px] left-[30px] top-[33px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.09px] h-[4.09px] left-[425px] top-[386px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[3.38px] h-[3.38px] left-[394px] top-[29px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.70px] h-[4.70px] left-[817px] top-[113px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-1.5 h-1.5 left-[1194px] top-[332px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.89px] h-[4.89px] left-[811px] top-[76px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.25px] h-[4.25px] left-[458px] top-[366px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[4.82px] h-[4.82px] left-[936px] top-[46px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[3.74px] h-[3.74px] left-[64px] top-[132px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-1 h-1 left-[763px] top-[10px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[3.67px] h-[3.67px] left-[861px] top-[106px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[3.62px] h-[3.62px] left-[710px] top-[278px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[3.45px] h-[3.45px] left-[1069px] top-[329px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.92px] h-[2.92px] left-[1286px] top-[299px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.84px] h-[4.84px] left-[219px] top-[269px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[2.39px] h-[2.39px] left-[817px] top-[121px] absolute bg-white rounded-full" />
<div className="w-[5.83px] h-[5.83px] left-[168px] top-[320px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.94px] h-[5.94px] left-[419px] top-[244px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.67px] h-[4.67px] left-[604px] top-[309px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.87px] h-[5.87px] left-[1098px] top-[379px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[5.85px] h-[5.85px] left-[644px] top-[352px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[4.19px] h-[4.19px] left-[1361px] top-[349px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[2.84px] h-[2.84px] left-[1299px] top-[194px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[4.51px] h-[4.51px] left-[468px] top-[319px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.73px] h-[2.73px] left-[1084px] top-[86px] absolute bg-white rounded-full" />
<div className="w-[3.43px] h-[3.43px] left-[1271px] top-[28px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[2.25px] h-[2.25px] left-[106px] top-[197px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[2.82px] h-[2.82px] left-[122px] top-[173px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.89px] h-[2.89px] left-[343px] top-[345px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.82px] h-[2.82px] left-[433px] top-[40px] absolute bg-white rounded-full" />
<div className="w-[4.11px] h-[4.11px] left-[904px] top-[350px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.42px] h-[4.42px] left-[1066px] top-[349px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[4.67px] h-[4.67px] left-[904px] top-[317px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[5.54px] h-[5.54px] left-[501px] top-[336px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[4.11px] h-[4.11px] left-[1149px] top-[206px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[3.55px] h-[3.55px] left-[235px] top-[362px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[2.60px] h-[2.60px] left-[1246px] top-[1px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.94px] h-[2.94px] left-[788px] top-[6px] absolute bg-white rounded-full" />
<div className="w-[4.19px] h-[4.19px] left-[527px] top-[365px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[4.13px] h-[4.13px] left-[201px] top-[53px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.94px] h-[2.94px] left-[765px] top-[13px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[4.11px] h-[4.11px] left-[1254px] top-[30px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[3.85px] h-[3.85px] left-[107px] top-[316px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.72px] h-[5.72px] left-[1305px] top-[8px] absolute bg-white rounded-full" />
<div className="w-[5.46px] h-[5.46px] left-[102px] top-[316px] absolute bg-white rounded-full" />
<div className="w-[3.77px] h-[3.77px] left-[1322px] top-[334px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.84px] h-[4.84px] left-[1370px] top-[317px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.55px] h-[5.55px] left-[945px] top-[258px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[2.24px] h-[2.24px] left-[266px] top-[362px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.89px] h-[2.89px] left-[987px] top-[156px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[3.46px] h-[3.46px] left-[10px] top-[168px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.67px] h-[5.67px] left-[441px] top-[291px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[4.07px] h-[4.07px] left-[962px] top-[364px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[5.57px] h-[5.57px] left-[599px] top-[293px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[4.41px] h-[4.41px] left-[358px] top-[163px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[2.31px] h-[2.31px] left-[670px] top-[182px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.60px] h-[2.60px] left-[621px] top-[257px] absolute bg-white rounded-full" />
<div className="w-[2.16px] h-[2.16px] left-[48px] top-[322px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.91px] h-[5.91px] left-[491px] top-[5px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.50px] h-[5.50px] left-[1139px] top-[274px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[3.74px] h-[3.74px] left-[24px] top-[177px] absolute bg-white rounded-full" />
<div className="w-[5.57px] h-[5.57px] left-[1166px] top-[316px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[5px] h-[5px] left-[445px] top-[326px] absolute bg-white rounded-full" />
<div className="w-[3.01px] h-[3.01px] left-[438px] top-[252px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[4.14px] h-[4.14px] left-[554px] top-[131px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.30px] h-[5.30px] left-[1010px] top-[116px] absolute bg-white rounded-full" />
<div className="w-[5.53px] h-[5.53px] left-[437px] top-[367px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.87px] h-[5.87px] left-[948px] top-[27px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[2.87px] h-[2.87px] left-[826px] top-[20px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[3.89px] h-[3.89px] left-[1222px] top-[112px] absolute bg-white rounded-full" />
<div className="w-[3.77px] h-[3.77px] left-[796px] top-[395px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.09px] h-[2.09px] left-[272px] top-[103px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.12px] h-[4.12px] left-[76px] top-[2px] absolute bg-white rounded-full" />
<div className="w-[3.51px] h-[3.51px] left-[226px] top-[276px] absolute bg-white rounded-full" />
<div className="w-[3.03px] h-[3.03px] left-[723px] top-[197px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.14px] h-[2.14px] left-[1259px] top-[17px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[3.28px] h-[3.28px] left-[1244px] top-[293px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[4.45px] h-[4.45px] left-[118px] top-[128px] absolute bg-white rounded-full" />
<div className="w-[4.15px] h-[4.15px] left-[490px] top-[204px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[4.93px] h-[4.93px] left-[552px] top-[38px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[5.56px] h-[5.56px] left-[115px] top-[303px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.35px] h-[2.35px] left-[509px] top-[278px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.24px] h-[5.24px] left-[804px] top-[389px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[2.44px] h-[2.44px] left-[1013px] top-[50px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[3.69px] h-[3.69px] left-[1183px] top-[95px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.83px] h-[2.83px] left-[278px] top-[181px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[3.22px] h-[3.22px] left-[1316px] top-[282px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[3.55px] h-[3.55px] left-[736px] top-[119px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[2.29px] h-[2.29px] left-[483px] top-[319px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.14px] h-[2.14px] left-[1135px] top-[19px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[3.64px] h-[3.64px] left-[39px] top-[126px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.30px] h-[5.30px] left-[237px] top-[369px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.57px] h-[5.57px] left-[1156px] top-[126px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.78px] h-[2.78px] left-[1295px] top-[74px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-0.5 h-0.5 left-[76px] top-[227px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[3.61px] h-[3.61px] left-[108px] top-[89px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.37px] h-[5.37px] left-[191px] top-[167px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.18px] h-[4.18px] left-[164px] top-[117px] absolute bg-white rounded-full" />
<div className="w-[5.15px] h-[5.15px] left-[533px] top-[261px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-1.5 h-1.5 left-[327px] top-[157px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[5.74px] h-[5.74px] left-[1242px] top-[122px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.22px] h-[4.22px] left-[129px] top-[265px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[2.30px] h-[2.30px] left-[1305px] top-[86px] absolute bg-white rounded-full" />
<div className="w-[2.70px] h-[2.70px] left-[1235px] top-[120px] absolute bg-white rounded-full" />
<div className="w-[2.15px] h-[2.15px] left-[596px] top-[103px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[2.17px] h-[2.17px] left-[483px] top-[233px] absolute bg-white rounded-full" />
<div className="w-[5.09px] h-[5.09px] left-[706px] top-[188px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[4.15px] h-[4.15px] left-[141px] top-[2px] absolute bg-white rounded-full" />
<div className="w-[4.20px] h-[4.20px] left-[48px] top-[124px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[3.51px] h-[3.51px] left-[1095px] top-[201px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[3.21px] h-[3.21px] left-[730px] top-[185px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[2.61px] h-[2.61px] left-[722px] top-[319px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[2.28px] h-[2.28px] left-[444px] top-[26px] absolute bg-white rounded-full" />
<div className="w-[4.49px] h-[4.49px] left-[355px] top-[212px] absolute bg-white rounded-full" />
<div className="w-[3.69px] h-[3.69px] left-[1280px] top-[312px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.23px] h-[4.23px] left-[1114px] top-[113px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[3.48px] h-[3.48px] left-[729px] top-[117px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.11px] h-[4.11px] left-[647px] top-[276px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.16px] h-[4.16px] left-[365px] top-[116px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.35px] h-[5.35px] left-[94px] top-[194px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.84px] h-[5.84px] left-[2px] top-[84px] absolute bg-white rounded-full" />
<div className="w-[4.43px] h-[4.43px] left-[1382px] top-[23px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.38px] h-[5.38px] left-[857px] top-[284px] absolute bg-white rounded-full" />
<div className="w-[2.77px] h-[2.77px] left-[1228px] top-[385px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.65px] h-[4.65px] left-[165px] top-[184px] absolute bg-white rounded-full" />
<div className="w-[5.53px] h-[5.53px] left-[568px] top-[354px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[3.59px] h-[3.59px] left-[1303px] top-[371px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[5.84px] h-[5.84px] left-[235px] top-[188px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[3.84px] h-[3.84px] left-[902px] top-[211px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[3.45px] h-[3.45px] left-[367px] top-[161px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[4.08px] h-[4.08px] left-[855px] top-[394px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[3.25px] h-[3.25px] left-[383px] top-[47px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.39px] h-[4.39px] left-[1313px] top-[165px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.60px] h-[5.60px] left-[697px] top-[327px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.09px] h-[2.09px] left-[646px] top-[370px] absolute bg-white rounded-full" />
<div className="w-[3.13px] h-[3.13px] left-[728px] top-[122px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.53px] h-[5.53px] left-[203px] top-[293px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.83px] h-[5.83px] left-[424px] top-[121px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.82px] h-[4.82px] left-[1358px] top-[176px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[3.18px] h-[3.18px] left-[1212px] top-[24px] absolute bg-white rounded-full" />
<div className="w-[5.23px] h-[5.23px] left-[260px] top-[217px] absolute bg-white rounded-full" />
<div className="w-[5.29px] h-[5.29px] left-[1204px] top-[367px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[3.47px] h-[3.47px] left-[1163px] top-[159px] absolute bg-white rounded-full" />
<div className="w-[5.77px] h-[5.77px] left-[1257px] top-[115px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.31px] h-[5.31px] left-[222px] top-[356px] absolute bg-white rounded-full" />
<div className="w-[5.43px] h-[5.43px] left-[1141px] top-[349px] absolute bg-white rounded-full" />
<div className="w-[5.62px] h-[5.62px] left-[683px] top-[81px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[3.91px] h-[3.91px] left-[269px] top-[3px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[3.51px] h-[3.51px] left-[305px] top-[310px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[5.41px] h-[5.41px] left-[530px] top-[94px] absolute bg-white rounded-full" />
<div className="w-[4.64px] h-[4.64px] left-[730px] top-[301px] absolute bg-white rounded-full" />
<div className="w-[3.59px] h-[3.59px] left-[716px] top-[14px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.77px] h-[4.77px] left-[544px] top-[13px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[2.29px] h-[2.29px] left-[357px] top-[281px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[2.42px] h-[2.42px] left-[1346px] top-[112px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[3.42px] h-[3.42px] left-[671px] top-[150px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.40px] h-[4.40px] left-[1324px] top-[268px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.21px] h-[5.21px] left-[1028px] top-[376px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[4.27px] h-[4.27px] left-[499px] top-[50px] absolute bg-white rounded-full" />
<div className="w-[4.35px] h-[4.35px] left-[543px] top-[359px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.25px] h-[5.25px] left-[1245px] top-[296px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[5.52px] h-[5.52px] left-[360px] top-[98px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[4.46px] h-[4.46px] left-[741px] top-[358px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[3.90px] h-[3.90px] left-[1262px] top-[184px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[5.75px] h-[5.75px] left-[552px] top-[335px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[4.95px] h-[4.95px] left-[120px] top-[178px] absolute bg-white rounded-full" />
<div className="w-[3.28px] h-[3.28px] left-[1337px] top-[293px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[2.43px] h-[2.43px] left-[233px] top-[310px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-1 h-1 left-[218px] top-[322px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[3.68px] h-[3.68px] left-[984px] top-[8px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[2.44px] h-[2.44px] left-[832px] top-[55px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[3.93px] h-[3.93px] left-[1105px] top-[209px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[4.08px] h-[4.08px] left-[957px] top-[23px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[2.33px] h-[2.33px] left-[1066px] top-[390px] absolute bg-white bg-opacity-80 rounded-full" />
<div className="w-[3.25px] h-[3.25px] left-[737px] top-[118px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.18px] h-[5.18px] left-[202px] top-[19px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.05px] h-[5.05px] left-[466px] top-[17px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[3.85px] h-[3.85px] left-[144px] top-[153px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.35px] h-[5.35px] left-[233px] top-[330px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-1 h-1 left-[730px] top-[179px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[4.46px] h-[4.46px] left-[1156px] top-[342px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.22px] h-[5.22px] left-[1275px] top-[204px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.50px] h-[5.50px] left-[38px] top-[343px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.14px] h-[5.14px] left-[867px] top-[113px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[2.19px] h-[2.19px] left-[1277px] top-[314px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[3.74px] h-[3.74px] left-[1136px] top-[197px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.37px] h-[5.37px] left-[34px] top-[226px] absolute bg-white bg-opacity-60 rounded-full" />
<div className="w-[5.93px] h-[5.93px] left-[727px] top-[272px] absolute bg-white bg-opacity-50 rounded-full" />
<div className="w-[5.29px] h-[5.29px] left-[277px] top-[43px] absolute bg-white bg-opacity-80 rounded-full" />
</div>
</div>
);
};

View File

@ -1,172 +0,0 @@
import { useCallback, useMemo, useState } from 'react';
import { format } from 'date-fns';
import {
DayPicker,
SelectSingleEventHandler,
DateRange,
} from 'react-day-picker';
import {
Button,
Input,
Popover,
PopoverContent,
PopoverHandler,
} from '@snowballtools/material-tailwind-react-fork';
import HorizontalLine from './HorizontalLine';
// https://www.material-tailwind.com/docs/react/plugins/date-picker#date-picker
const DAY_PICKER_CLASS_NAMES = {
caption: 'flex justify-center py-2 mb-4 relative items-center',
caption_label: 'text-sm font-medium text-gray-900',
nav: 'flex items-center',
nav_button:
'h-6 w-6 bg-transparent hover:bg-blue-gray-50 p-1 rounded-md transition-colors duration-300',
nav_button_previous: 'absolute left-1.5',
nav_button_next: 'absolute right-1.5',
table: 'w-full border-collapse',
head_row: 'flex font-medium text-gray-900',
head_cell: 'm-0.5 w-9 font-normal text-sm',
row: 'flex w-full mt-2',
cell: 'text-gray-600 rounded-md h-9 w-9 text-center text-sm p-0 m-0.5 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-gray-900/20 [&:has([aria-selected].day-outside)]:text-white [&:has([aria-selected])]:bg-gray-900/50 first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
day: 'h-9 w-9 p-0 font-normal',
day_range_end: 'day-range-end',
day_selected:
'rounded-md bg-gray-900 text-white hover:bg-gray-900 hover:text-white focus:bg-gray-900 focus:text-white',
day_today: 'rounded-md bg-gray-200 text-gray-900',
day_outside:
'day-outside text-gray-500 opacity-50 aria-selected:bg-gray-500 aria-selected:text-gray-900 aria-selected:bg-opacity-10',
day_disabled: 'text-gray-500 opacity-50',
day_hidden: 'invisible',
};
type SingleDateHandler = (value: Date) => void;
type RangeDateHandler = (value: DateRange) => void;
interface SingleDatePickerProps {
mode: 'single';
selected?: Date;
onSelect: SingleDateHandler;
}
interface RangeDatePickerProps {
mode: 'range';
selected?: DateRange;
onSelect: RangeDateHandler;
}
const DatePicker = ({
mode = 'single',
selected,
onSelect,
}: SingleDatePickerProps | RangeDatePickerProps) => {
const [isOpen, setIsOpen] = useState(false);
const [rangeSelected, setRangeSelected] = useState<DateRange>();
const inputValue = useMemo(() => {
if (mode === 'single') {
return selected ? format(selected as Date, 'PPP') : 'Select Date';
}
if (mode === 'range') {
const selectedRange = selected as DateRange | undefined;
return selectedRange && selectedRange.from && selectedRange.to
? format(selectedRange.from, 'PP') +
'-' +
format(selectedRange.to, 'PP')
: 'All time';
}
}, [selected, mode]);
const handleSingleSelect = useCallback<SelectSingleEventHandler>((value) => {
if (value) {
(onSelect as SingleDateHandler)(value);
setIsOpen(false);
}
}, []);
const handleRangeSelect = useCallback(() => {
if (rangeSelected?.to) {
(onSelect as RangeDateHandler)(rangeSelected);
setIsOpen(false);
}
}, [rangeSelected]);
const components = {
IconLeft: ({ ...props }) => (
<i {...props} className="h-4 w-4 stroke-2">
{'<'}
</i>
),
IconRight: ({ ...props }) => (
<i {...props} className="h-4 w-4 stroke-2">
{'>'}
</i>
),
};
const commonDayPickerProps = {
components,
className: 'border-0',
classNames: DAY_PICKER_CLASS_NAMES,
showOutsideDays: true,
};
return (
<Popover
placement="bottom"
open={isOpen}
handler={(value) => setIsOpen(value)}
>
<PopoverHandler>
<Input onChange={() => null} value={inputValue} />
</PopoverHandler>
{/* TODO: Figure out what placeholder is for */}
{/* @ts-ignore */}
<PopoverContent>
{mode === 'single' && (
<DayPicker
mode="single"
onSelect={handleSingleSelect}
selected={selected as Date}
{...commonDayPickerProps}
/>
)}
{mode === 'range' && (
<>
<DayPicker
mode="range"
onSelect={setRangeSelected}
selected={rangeSelected as DateRange}
{...commonDayPickerProps}
/>
<HorizontalLine />
<div className="flex justify-end">
{/* TODO: Figure out what placeholder is for */}
<Button
size="sm"
className="rounded-full mr-2"
variant="outlined"
onClick={() => setIsOpen(false)}
>
Cancel
</Button>
{/* TODO: Figure out what placeholder is for */}
<Button
size="sm"
className="rounded-full"
color="gray"
onClick={() => handleRangeSelect()}
>
Select
</Button>
</div>
</>
)}
</PopoverContent>
</Popover>
);
};
export default DatePicker;

View File

@ -9,6 +9,14 @@ export interface Option {
label: string;
}
/**
* Props for the Dropdown component.
* @interface DropdownProps
* @property {Option[]} options - The list of options to display in the dropdown.
* @property {(arg: ReactDropdownOption) => void} onChange - Callback fired when an option is selected.
* @property {string} [placeholder] - Placeholder text for the dropdown.
* @property {Option} [value] - The currently selected option.
*/
interface DropdownProps {
options: Option[];
onChange: (arg: ReactDropdownOption) => void;
@ -16,6 +24,22 @@ interface DropdownProps {
value?: Option;
}
/**
* A dropdown component that wraps the ReactDropdown library.
*
* @component
* @param {DropdownProps} props - The props for the Dropdown component.
* @returns {React.ReactElement} A dropdown element.
*
* @example
* ```tsx
* <Dropdown
* options={[{ value: '1', label: 'One' }, { value: '2', label: 'Two' }]}
* onChange={(option) => console.log(option)}
* placeholder="Select an option"
* />
* ```
*/
const Dropdown = ({ placeholder, options, onChange, value }: DropdownProps) => {
return (
<ReactDropdown

View File

@ -1,12 +1,29 @@
import { cn } from '@/utils/classnames';
import { Duration } from 'luxon';
import { ComponentPropsWithoutRef } from 'react';
import { cn } from 'utils/classnames';
/**
* Props for the FormatMillisecond component.
* @interface FormatMilliSecondProps
* @property {number} time - The time in milliseconds to format.
*/
export interface FormatMilliSecondProps
extends ComponentPropsWithoutRef<'div'> {
time: number;
}
/**
* A component that formats a given time in milliseconds into a human-readable format.
*
* @component
* @param {FormatMilliSecondProps} props - The props for the FormatMillisecond component.
* @returns {React.ReactElement} A formatted time element.
*
* @example
* ```tsx
* <FormatMillisecond time={3600000} />
* ```
*/
const FormatMillisecond = ({ time, ...props }: FormatMilliSecondProps) => {
const formatTime = Duration.fromMillis(time)
.shiftTo('days', 'hours', 'minutes', 'seconds')

View File

@ -1,3 +1,14 @@
/**
* A simple horizontal line component.
*
* @component
* @returns {React.ReactElement} A horizontal line element.
*
* @example
* ```tsx
* <HorizontalLine />
* ```
*/
const HorizontalLine = () => {
return <hr className="h-px bg-gray-100 border-0" />;
};

View File

@ -1,9 +1,26 @@
import { Link } from 'react-router-dom';
/**
* Props for the Logo component.
* @interface LogoProps
* @property {string} [orgSlug] - The organization slug used for the link.
*/
interface LogoProps {
orgSlug?: string;
}
/**
* A component that renders the Snowball logo with a link to the organization's page.
*
* @component
* @param {LogoProps} props - The props for the Logo component.
* @returns {React.ReactElement} A logo element.
*
* @example
* ```tsx
* <Logo orgSlug="my-organization" />
* ```
*/
export const Logo = ({ orgSlug }: LogoProps) => {
return (
<Link to={`/${orgSlug}`}>

View File

@ -1,21 +1,33 @@
import React, { forwardRef, RefAttributes } from 'react';
import { SearchIcon } from './shared/CustomIcon';
import { Input, InputProps } from './shared/Input';
import { IconInput, type IconInputProps } from '@/components/ui/extended/input-w-icons';
import { Search } from 'lucide-react';
/**
* A search bar component with an icon input.
*
* @component
* @param {InputProps & RefAttributes<HTMLInputElement>} props - The props for the SearchBar component.
* @returns {React.ReactElement} A search bar element.
*
* @example
* ```tsx
* <SearchBar value="search term" onChange={(e) => console.log(e.target.value)} />
* ```
*/
const SearchBar: React.ForwardRefRenderFunction<
HTMLInputElement,
InputProps & RefAttributes<HTMLInputElement>
IconInputProps & RefAttributes<HTMLInputElement>
> = ({ value, onChange, placeholder = 'Search', ...props }, ref) => {
return (
<div className="relative flex w-full">
<Input
leftIcon={<SearchIcon className="text-foreground-secondary" />}
<IconInput
leftIcon={<Search className="text-foreground-secondary" />}
onChange={onChange}
value={value}
type="search"
placeholder={placeholder}
appearance="borderless"
// appearance="borderless"
className="w-full lg:w-[459px]"
{...props}
ref={ref}

View File

@ -4,17 +4,42 @@ const COLOR_COMPLETED = '#059669';
const COLOR_ACTIVE = '#CFE6FC';
const COLOR_NOT_STARTED = '#F1F5F9';
/**
* Represents a step in the stepper.
* @interface StepperValue
* @property {number} step - The step number.
* @property {string} route - The route associated with the step.
* @property {string} label - The label for the step.
*/
interface StepperValue {
step: number;
route: string;
label: string;
}
/**
* Props for the Stepper component.
* @interface StepperProps
* @property {number} activeStep - The currently active step.
* @property {StepperValue[]} stepperValues - The values for each step.
*/
interface StepperProps {
activeStep: number;
stepperValues: StepperValue[];
}
/**
* A stepper component that displays a series of steps with different states.
*
* @component
* @param {StepperProps} props - The props for the Stepper component.
* @returns {React.ReactElement} A stepper element.
*
* @example
* ```tsx
* <Stepper activeStep={1} stepperValues={[{ step: 1, route: '/step1', label: 'Step 1' }]} />
* ```
*/
const Stepper = ({ activeStep, stepperValues }: StepperProps) => {
return (
<StepperNav

View File

@ -11,11 +11,29 @@ const setStopWatchOffset = (time: string) => {
return currentTime;
};
/**
* Props for the Stopwatch component.
* @interface StopwatchProps
* @property {Date} offsetTimestamp - The initial timestamp for the stopwatch.
* @property {boolean} isPaused - Whether the stopwatch is paused.
*/
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
offsetTimestamp: Date;
isPaused: boolean;
}
/**
* A stopwatch component that tracks elapsed time.
*
* @component
* @param {StopwatchProps} props - The props for the Stopwatch component.
* @returns {React.ReactElement} A stopwatch element.
*
* @example
* ```tsx
* <Stopwatch offsetTimestamp={new Date()} isPaused={false} />
* ```
*/
const Stopwatch = ({ offsetTimestamp, isPaused, ...props }: StopwatchProps) => {
const { totalSeconds, pause, start } = useStopwatch({
autoStart: true,
@ -29,4 +47,4 @@ const Stopwatch = ({ offsetTimestamp, isPaused, ...props }: StopwatchProps) => {
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
};
export { Stopwatch, setStopWatchOffset };
export { setStopWatchOffset, Stopwatch };

View File

@ -3,6 +3,14 @@ import * as CSS from 'csstype';
//
// Nav
//
/**
* Describes a step in the stepper navigation.
* @interface IStepDescription
* @property {() => JSX.Element} stepContent - The content of the step.
* @property {string} [stepStateColor] - The color representing the step's state.
* @property {number} [stepStatusCircleSize] - The size of the status circle.
* @property {() => void} [onClickHandler] - Handler for click events on the step.
*/
export interface IStepDescription {
stepContent: () => JSX.Element;
stepStateColor?: string;
@ -10,10 +18,27 @@ export interface IStepDescription {
onClickHandler?: () => void | undefined;
}
/**
* Props for the StepperNav component.
* @interface IStepperNavProps
* @property {IStepDescription[]} steps - The steps to display in the navigation.
*/
export interface IStepperNavProps {
steps: IStepDescription[];
}
/**
* A navigation component for displaying steps in a vertical layout.
*
* @component
* @param {IStepperNavProps} props - The props for the StepperNav component.
* @returns {React.ReactElement} A stepper navigation element.
*
* @example
* ```tsx
* <StepperNav steps={[{ stepContent: () => <div>Step 1</div> }]} />
* ```
*/
export const StepperNav = (props: IStepperNavProps): JSX.Element => {
return (
<nav>

View File

@ -0,0 +1,76 @@
import { ReactNode } from 'react';
import {
Header,
NavigationWrapper,
ScreenWrapper,
TabWrapper,
TabsContent,
TabsList,
TabsTrigger,
} from '../layout';
/**
* Props for the ExamplePage component.
* @interface ExamplePageProps
* @property {ReactNode} [children] - The content to display within the example page.
*/
interface ExamplePageProps {
children?: ReactNode;
}
/**
* An example page component demonstrating the standard layout components.
*
* @component
* @param {ExamplePageProps} props - The props for the ExamplePage component.
* @returns {React.ReactElement} An example page element.
*
* @example
* ```tsx
* <ExamplePage>
* <div>Custom content here</div>
* </ExamplePage>
* ```
*/
export function ExamplePage({ children }: ExamplePageProps) {
return (
<NavigationWrapper>
<ScreenWrapper>
<Header
title="Example Page"
subtitle="Demonstrating the standard layout components"
actions={[
<button key="action" className="btn-primary">
Action
</button>,
]}
/>
<TabWrapper defaultValue="tab1">
<TabsList>
<TabsTrigger value="tab1">Overview</TabsTrigger>
<TabsTrigger value="tab2">Details</TabsTrigger>
<TabsTrigger value="tab3">Settings</TabsTrigger>
</TabsList>
<TabsContent value="tab1">
<div className="p-4">
<h2>Overview Content</h2>
{children}
</div>
</TabsContent>
<TabsContent value="tab2">
<div className="p-4">
<h2>Details Content</h2>
</div>
</TabsContent>
<TabsContent value="tab3">
<div className="p-4">
<h2>Settings Content</h2>
</div>
</TabsContent>
</TabWrapper>
</ScreenWrapper>
</NavigationWrapper>
);
}

View File

@ -0,0 +1 @@
export * from './ui';

View File

@ -0,0 +1,9 @@
export { NavigationWrapper } from './navigation';
export { Header } from './screen-header/Header';
export { ScreenWrapper } from './screen-wrapper/ScreenWrapper';
export { TabWrapper } from './screen-wrapper/TabWrapper';
// Re-export tab components for convenience
export {
TabsContent, TabsList,
TabsTrigger
} from '@/components/ui/tabs';

View File

@ -0,0 +1,33 @@
import { cn } from '@/lib/utils';
import { TopNavigation } from './TopNavigation';
/**
* NavigationWrapperProps interface extends React.HTMLProps<HTMLDivElement> to include all standard HTML div attributes.
*/
interface NavigationWrapperProps extends React.HTMLProps<HTMLDivElement> {}
/**
* Wraps the application's navigation and content within a consistent layout.
* It provides a basic structure with a top navigation bar and a content area.
*
* @param {NavigationWrapperProps} props - The props passed to the NavigationWrapper component.
* @param {React.ReactNode} props.children - The content to be rendered within the wrapper.
* @param {string} [props.className] - Optional CSS class names to apply to the wrapper.
* @param {React.HTMLAttributes<HTMLDivElement>} props.props - Other standard HTML attributes.
*
* @returns {React.ReactElement} A div element containing the top navigation and the children.
*/
export function NavigationWrapper({
children,
className,
...props
}: NavigationWrapperProps) {
// const { isAuthenticated } = useGitHubAuth();
return (
<div className={cn('min-h-screen bg-background', className)} {...props}>
<TopNavigation />
{children}
</div>
);
}

View File

@ -0,0 +1,283 @@
'use client';
import { OnboardingDialog } from '@/components/onboarding-flow';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { useGQLClient } from '@/context/GQLClientContext';
import { useOctokit } from '@/context/OctokitContext';
import { useWallet } from '@/context/WalletContextProvider';
import { LaconicMark } from '@/laconic-assets/laconic-mark';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { Organization } from 'gql-client';
import { Menu, Plus, Shapes, Wallet } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { ProjectSearchBar } from '../search/ProjectSearchBar';
import { ColorModeToggle } from './components/ColorModeToggle';
import { GitHubSessionButton } from './components/GitHubSessionButton';
import { WalletSessionId } from './components/WalletSessionId';
/**
* TopNavigation Component
*
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=299-1294&m=dev
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=299-1294&t=LEaCSA9ND6svuP3P-4
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=298-242&t=LEaCSA9ND6svuP3P-4
* https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=299-1294&m=dev
* Renders the top navigation bar, adapting its layout for desktop and mobile views.
* It includes the project search bar, navigation links, user authentication via GitHub,
* color mode toggle, and wallet session information. On mobile, it utilizes a sheet
* for a responsive and accessible navigation experience.
*
* @returns {React.ReactElement} A PopoverPrimitive.Root element containing the top navigation bar.
*/
export function TopNavigation() {
const navigate = useNavigate();
const [showOnboarding, setShowOnboarding] = useState(false);
const { octokit } = useOctokit();
const client = useGQLClient();
const [defaultOrg, setDefaultOrg] = useState<Organization | null>(null);
const { isReady } = useWallet();
// Check if GitHub is connected
const isGitHubConnected = Boolean(octokit);
// Fetch the default organization (first one in the list)
const fetchDefaultOrganization = useCallback(async () => {
try {
const { organizations } = await client.getOrganizations();
if (organizations && organizations.length > 0) {
setDefaultOrg(organizations[0]);
}
} catch (error) {
console.error('Error fetching organizations:', error);
}
}, [client]);
useEffect(() => {
if (isGitHubConnected && isReady) {
fetchDefaultOrganization();
}
}, [isGitHubConnected, fetchDefaultOrganization, isReady]);
const handleOnboardingClose = () => {
setShowOnboarding(false);
// Refresh organization data after onboarding
fetchDefaultOrganization();
};
// Navigate to create page with organization slug
const handleCreateNew = () => {
if (defaultOrg) {
navigate(`/${defaultOrg.slug}/projects/create`);
} else {
// If no organization is available, show onboarding
setShowOnboarding(true);
}
};
const handleRunOnboarding = () => {
// Clear existing onboarding progress data
localStorage.removeItem('onboarding_progress');
localStorage.removeItem('onboarding_state');
// Force starting from connect step
localStorage.setItem('onboarding_force_connect', 'true');
setShowOnboarding(true);
};
return (
<PopoverPrimitive.Root>
<div className="bg-background">
{/* Top Navigation - Desktop */}
<nav className="md:flex items-center justify-between hidden h-16 px-6 border-b">
<div className="flex items-center gap-6">
{/* Logo / Home Link */}
{/* <Button
variant="ghost"
asChild
className="hover:bg-transparent p-0"
> */}
<Link to="/" className="flex items-center justify-center w-10 h-10">
<LaconicMark />
</Link>
{/* </Button> */}
{/* Search Bar */}
{/* <div className="w-96">
<ProjectSearchBar
onChange={(project) => {
navigate(
`/${project.organization.slug}/projects/${project.id}`,
);
}}
/>
</div> */}
{/* Navigation Items */}
<div className="flex items-center gap-4">
<Link to="/deploy-tools/projects">
<Button variant="ghost" asChild className="gap-2">
<Shapes className="w-5 h-5" />
<span>Projects</span>
</Button>
</Link>
<Button
variant="ghost"
asChild
className="text-muted-foreground gap-2"
>
<Link to="/wallet">
<Button asChild variant="ghost">
<Wallet className="w-5 h-5" />
<span>Wallet</span>
</Button>
</Link>
</Button>
</div>
</div>
<div className="flex items-center gap-4">
{/* Add New Button with Dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full">
<Plus className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{isGitHubConnected && (
<>
<DropdownMenuItem onClick={handleCreateNew}>
Create New
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem onClick={handleRunOnboarding}>
Run Onboarding
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* <NavigationActions /> */}
<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="md:hidden flex items-center justify-between h-16 px-4 border-b">
<Sheet>
<SheetTrigger>
<Button asChild variant="outline" size="icon">
<div>
<Menu className="w-4 h-4" />
</div>
</Button>
</SheetTrigger>
<SheetContent
side="left"
className="w-[300px] sm:w-[400px] flex flex-col"
>
<nav className="flex flex-col flex-grow space-y-4">
<div className="px-4 py-2">
<ProjectSearchBar
onChange={(project) => {
navigate(
`/${project.organization.slug}/projects/${project.id}`,
);
}}
/>
</div>
<Button variant="ghost" asChild className="justify-start gap-2">
<Link to="/projects">
<Shapes className="w-5 h-5" />
<span>Projects</span>
</Link>
</Button>
<Button variant="ghost" asChild className="justify-start gap-2">
<Link to="/wallet">
<Wallet className="w-5 h-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>
<Button
variant="ghost"
className="justify-start"
onClick={handleRunOnboarding}
>
Run Onboarding
</Button>
{isGitHubConnected && (
<Button
variant="ghost"
className="justify-start"
onClick={handleCreateNew}
>
Create New
</Button>
)}
</nav>
<div className="flex items-center justify-between mt-auto">
<GitHubSessionButton />
<ColorModeToggle />
<WalletSessionId walletId="0xAb...1234" />
</div>
</SheetContent>
</Sheet>
<div className="flex items-center gap-2">
{/* Add New Button (Mobile) */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full">
<Plus className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{isGitHubConnected && (
<>
<DropdownMenuItem onClick={handleCreateNew}>
Create New
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem onClick={handleRunOnboarding}>
Run Onboarding
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<ColorModeToggle />
<WalletSessionId walletId="0xAb...1234" />
</div>
</nav>
</div>
{/* Onboarding Dialog */}
{showOnboarding && (
<OnboardingDialog defaultOpen={true} onClose={handleOnboardingClose} />
)}
</PopoverPrimitive.Root>
);
}

View File

@ -0,0 +1,19 @@
import { Button } from '@/components/ui/button';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
export function ColorModeToggle() {
const { theme, setTheme } = useTheme();
function toggleTheme() {
setTheme(theme === 'dark' ? 'light' : 'dark');
}
return (
<Button variant="outline" size="icon" onClick={toggleTheme}>
<Sun className="h-[1.2rem] w-[1.2rem] 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>
);
}

View File

@ -0,0 +1,67 @@
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';
/**
* Renders a button that allows users to log in or log out with their GitHub account.
* It displays the user's avatar and username when logged in, and provides a logout option.
*
* @returns {React.ReactElement} A DropdownMenu element containing the GitHub session button.
*/
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,51 @@
import type React from 'react';
/**
* LaconicIconProps interface defines the props for the LaconicIcon component.
*/
interface LaconicIconProps {
/**
* Optional CSS class names to apply to the component.
*/
className?: string;
/**
* The width of the icon.
* @default 40
*/
width?: number;
/**
* The height of the icon.
* @default 40
*/
height?: number;
}
/**
* A component that renders the Laconic icon.
*
* @param {LaconicIconProps} props - The props for the component.
* @returns {React.ReactElement} An SVG element representing the Laconic icon.
*/
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,44 @@
import { Button } from '@/components/ui/button';
import { useGQLClient } from '@/context/GQLClientContext';
import { Bell, Plus } from 'lucide-react';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
/**
* Renders the navigation actions, including buttons for creating a new project and displaying notifications.
*
* @returns {React.ReactElement} A div element containing the navigation actions.
*/
export function NavigationActions() {
const navigate = useNavigate();
const client = useGQLClient();
/**
* Fetches the organization slug.
* @returns {Promise<string>} The organization slug.
*/
const fetchOrgSlug = useCallback(async () => {
const { organizations } = await client.getOrganizations();
// TODO: Get the selected organization. This is temp
return organizations[0].slug;
}, [client]);
return (
<div className="flex items-center gap-3">
<Button
variant="secondary"
size="icon"
onClick={() => {
fetchOrgSlug().then((organizationSlug) => {
navigate(`/${organizationSlug}/projects/create`);
});
}}
>
<Plus className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Bell className="h-4 w-4" />
</Button>
</div>
);
}

View File

@ -0,0 +1,43 @@
import { useWallet } from '@/context/WalletContextProvider';
import type React from 'react';
/**
* WalletSessionIdProps interface defines the props for the WalletSessionId component.
*/
interface WalletSessionIdProps {
/**
* The wallet ID to display.
*/
walletId?: string;
/**
* Optional CSS class names to apply to the component.
*/
className?: string;
}
/**
* A component that displays the wallet session ID.
*
* @param {WalletSessionIdProps} props - The props for the component.
* @returns {React.ReactElement} A div element containing the wallet session ID.
*/
export const WalletSessionId: React.FC<WalletSessionIdProps> = ({
walletId,
className = '',
}) => {
const { wallet, isConnected } = useWallet();
// const wallet = { id: 'x123xxx' };
console.log(wallet, 'from WalletSessionId.tsx');
const displayId = wallet?.id || 'Wallet';
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 ${isConnected ? 'bg-green-400' : 'bg-gray-400'}`}
/>
<span className="text-secondary-foreground text-xs font-semibold">
{displayId}
</span>
</div>
);
};

View File

@ -0,0 +1,9 @@
export { ProjectSearchBar } from '../search/ProjectSearchBar';
export { ColorModeToggle } from './components/ColorModeToggle';
export { GitHubSessionButton } from './components/GitHubSessionButton';
export { LaconicIcon } from './components/LaconicIcon';
export { NavigationActions } from './components/NavigationActions';
export { WalletSessionId } from './components/WalletSessionId';
export { NavigationWrapper } from './NavigationWrapper';
export { TopNavigation } from './TopNavigation';

View File

@ -0,0 +1,56 @@
import { Button } from '@/components/ui/button';
import { useToast } from '@/hooks/use-toast';
import type { LucideIcon } from 'lucide-react';
/**
* ActionButtonProps interface defines the props for the ActionButton component.
*/
interface ActionButtonProps {
/**
* The label of the button.
*/
label: string;
/**
* The icon to display in the button.
*/
icon: LucideIcon;
/**
* The variant of the button.
* @default 'default'
*/
variant?: 'default' | 'outline';
/**
* Callback function to be called when the button is clicked.
*/
onClick?: () => void;
}
/**
* A button component that displays an icon and a label, and triggers an action when clicked.
*
* @param {ActionButtonProps} props - The props for the component.
* @returns {React.ReactElement} A Button element.
*/
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,89 @@
import { cn } from '@/lib/utils';
import { ReactNode } from 'react';
/**
* HeaderProps interface defines the props for the Header component.
*/
interface HeaderProps {
/**
* The title of the header.
*/
title: string;
/**
* The subtitle of the header.
*/
subtitle?: string;
/**
* An array of actions to display in the header.
*/
actions?: ReactNode[];
/**
* A back button to display in the header.
*/
backButton?: ReactNode;
/**
* Optional CSS class names to apply to the header.
*/
className?: string;
/**
* The layout of the header.
* @default 'default'
*/
layout?: 'default' | 'compact';
}
/**
* A component that renders a header with a title, subtitle, actions, and back button.
*
* @param {HeaderProps} props - The props for the component.
* @returns {React.ReactElement} A div element containing the header content.
*/
export function Header({
title,
subtitle,
actions,
backButton,
className,
layout = 'default',
}: HeaderProps) {
return (
<div
className={cn(
'flex flex-col sm:flex-row sm:items-center sm:justify-between',
layout === 'compact' ? 'mb-2' : 'mb-4',
className,
)}
>
<div className="flex items-center gap-2">
{backButton}
<div>
<h1
className={cn(
'font-bold',
layout === 'compact'
? 'text-lg sm:text-xl'
: 'text-xl sm:text-3xl',
)}
>
{title}
</h1>
{subtitle && (
<p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>
)}
</div>
</div>
{actions && actions.length > 0 && (
<div
className={cn(
'flex items-center gap-2',
layout === 'default' ? 'mt-4 sm:mt-0' : 'mt-2 sm:mt-0',
)}
>
{actions.map((action, index) => (
<div key={index}>{action}</div>
))}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,38 @@
import { cn } from '@/lib/utils';
/**
* ScreenWrapperProps interface extends React.HTMLProps<HTMLDivElement> to include all standard HTML div attributes.
*/
interface ScreenWrapperProps extends React.HTMLProps<HTMLDivElement> {
/**
* Whether the screen should be padded.
* @default true
*/
padded?: boolean;
}
/**
* A wrapper component for screens, providing consistent padding and layout.
*
* @param {ScreenWrapperProps} props - The props for the component.
* @returns {React.ReactElement} A div element containing the screen content.
*/
export function ScreenWrapper({
children,
className,
// padded = true,
...props
}: ScreenWrapperProps) {
return (
<div
className={cn(
'flex flex-1 flex-col',
'container mx-auto p-4 md:p-6 lg:p-8 ',
className,
)}
{...props}
>
{children}
</div>
);
}

View File

@ -0,0 +1,43 @@
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { cn } from '@/lib/utils';
/**
* TabWrapperProps interface extends React.ComponentProps<typeof Tabs> to include all standard Tabs attributes.
*/
interface TabWrapperProps extends React.ComponentProps<typeof Tabs> {
/**
* The orientation of the tabs.
* @default 'horizontal'
*/
orientation?: 'horizontal' | 'vertical';
}
/**
* A wrapper component for the Tabs component from `ui/tabs`.
*
* @param {TabWrapperProps} props - The props for the component.
* @returns {React.ReactElement} A Tabs element containing the tabs and content.
*/
export function TabWrapper({
children,
className,
orientation = 'horizontal',
...props
}: TabWrapperProps) {
return (
<Tabs
{...props}
orientation={orientation}
className={cn(
'w-full lg:max-w-2xl mx-auto',
orientation === 'vertical' && 'flex gap-6',
className,
)}
>
{children}
</Tabs>
);
}
// Re-export tab components for convenience
export { TabsContent, TabsList, TabsTrigger };

View File

@ -0,0 +1,106 @@
import { useCombobox } from 'downshift';
import { Project } from 'gql-client';
import { useCallback, useEffect, useState } from 'react';
import { useDebounceValue } from 'usehooks-ts';
import SearchBar from '@/components/SearchBar';
import { useGQLClient } from '@/context/GQLClientContext';
import { cn } from '@/utils/classnames';
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
/**
* ProjectSearchBarProps interface defines the props for the ProjectSearchBar component.
*/
interface ProjectSearchBarProps {
/**
* Callback function to be called when a project is selected.
* @param data - The selected project data.
*/
onChange?: (data: Project) => void;
}
/**
* A search bar component that allows the user to search for projects.
*
* @param {ProjectSearchBarProps} props - The props for the component.
* @returns {React.ReactElement} A div element containing the search bar and project list.
*/
export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
const [items, setItems] = useState<Project[]>([]);
const [selectedItem, setSelectedItem] = useState<Project | null>(null);
const client = useGQLClient();
const {
isOpen,
getMenuProps,
getInputProps,
getItemProps,
highlightedIndex,
inputValue,
} = useCombobox({
items,
itemToString(item) {
return item ? item.name : '';
},
selectedItem,
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
if (newSelectedItem) {
setSelectedItem(newSelectedItem);
if (onChange) {
onChange(newSelectedItem);
}
}
},
});
const [debouncedInputValue, _] = useDebounceValue<string>(inputValue, 300);
const fetchProjects = useCallback(
async (inputValue: string) => {
const { searchProjects } = await client.searchProjects(inputValue);
setItems(searchProjects);
},
[client],
);
useEffect(() => {
if (debouncedInputValue) {
fetchProjects(debouncedInputValue);
}
}, [fetchProjects, debouncedInputValue]);
return (
<div className="relative w-full lg:w-fit dark:bg-overlay">
<SearchBar {...getInputProps()} />
<div
{...getMenuProps({}, { suppressRefError: true })}
className={cn(
'flex flex-col shadow-dropdown rounded-xl dark:bg-overlay2 bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50',
{ hidden: !inputValue || !isOpen },
)}
>
{items.length ? (
<>
<div className="px-2 py-2">
<p className="text-elements-mid-em text-xs font-medium">
Suggestions
</p>
</div>
{items.map((item, index) => (
<ProjectSearchBarItem
{...getItemProps({ item, index })}
key={item.id}
item={item}
active={highlightedIndex === index || selectedItem === item}
/>
))}
</>
) : (
<ProjectSearchBarEmpty />
)}
</div>
</div>
);
};

View File

@ -0,0 +1,146 @@
import { Button } from '@/components/ui/button';
import { IconInput } from '@/components/ui/extended/input-w-icons';
import { useGQLClient } from '@/context/GQLClientContext';
import * as Dialog from '@radix-ui/react-dialog';
import { useCombobox } from 'downshift';
import { Project } from 'gql-client';
import { Search, X } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDebounceValue } from 'usehooks-ts';
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
/**
* ProjectSearchBarDialogProps interface defines the props for the ProjectSearchBarDialog component.
*/
interface ProjectSearchBarDialogProps extends Dialog.DialogProps {
/**
* Whether the dialog is open.
*/
open?: boolean;
/**
* Callback function to be called when the dialog is closed.
*/
onClose?: () => void;
/**
* Callback function to be called when a project item is clicked.
* @param data - The selected project data.
*/
onClickItem?: (data: Project) => void;
}
/**
* A dialog component that allows the user to search for projects.
*
* @param {ProjectSearchBarDialogProps} props - The props for the component.
* @returns {React.ReactElement} A Dialog.Root element containing the search bar and project list.
*/
export const ProjectSearchBarDialog = ({
onClose,
onClickItem,
...props
}: ProjectSearchBarDialogProps) => {
const [items, setItems] = useState<Project[]>([]);
const [selectedItem, setSelectedItem] = useState<Project | null>(null);
const client = useGQLClient();
const navigate = useNavigate();
const {
getInputProps,
getItemProps,
getMenuProps,
inputValue,
setInputValue,
} = useCombobox({
items,
itemToString(item) {
return item ? item.name : '';
},
selectedItem,
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
if (newSelectedItem) {
setSelectedItem(newSelectedItem);
onClickItem?.(newSelectedItem);
navigate(
`/${newSelectedItem.organization.slug}/projects/${newSelectedItem.id}`,
);
}
},
});
const [debouncedInputValue, _] = useDebounceValue<string>(inputValue, 300);
const fetchProjects = useCallback(
async (inputValue: string) => {
const { searchProjects } = await client.searchProjects(inputValue);
setItems(searchProjects);
},
[client],
);
useEffect(() => {
if (debouncedInputValue) {
fetchProjects(debouncedInputValue);
}
}, [fetchProjects, debouncedInputValue]);
const handleClose = () => {
setInputValue('');
setItems([]);
onClose?.();
};
return (
<Dialog.Root {...props}>
<Dialog.Portal>
<Dialog.Overlay className="bg-base-bg md:hidden fixed inset-0 overflow-y-auto" />
<Dialog.Content>
<div className="fixed inset-0 top-0 flex flex-col h-full">
<div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
<IconInput
{...getInputProps({}, { suppressRefError: true })}
leftIcon={<Search />}
placeholder="Search"
className="border-none shadow-none"
autoFocus
/>
<Button size="icon" variant="ghost" onClick={handleClose}>
<X size={16} />
</Button>
</div>
{/* Content */}
<div
className="flex flex-col gap-1 px-2 py-2"
{...getMenuProps(
{},
{
suppressRefError: true,
},
)}
>
{items.length > 0 ? (
<>
<div className="px-2 py-2">
<p className="text-elements-mid-em text-xs font-medium">
Suggestions
</p>
</div>
{items.map((item, index) => (
<ProjectSearchBarItem
key={item.id}
item={item}
{...getItemProps({ item, index })}
/>
))}
</>
) : (
inputValue && <ProjectSearchBarEmpty />
)}
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
};

View File

@ -0,0 +1,33 @@
import { cn } from '@/utils/classnames';
import { Search } from 'lucide-react';
import { ComponentPropsWithoutRef } from 'react';
/**
* ProjectSearchBarEmptyProps interface defines the props for the ProjectSearchBarEmpty component.
*/
interface ProjectSearchBarEmptyProps extends ComponentPropsWithoutRef<'div'> {}
/**
* A component that renders a message when no projects match the search query.
*
* @param {ProjectSearchBarEmptyProps} props - The props for the component.
* @returns {React.ReactElement} A div element displaying a "no projects found" message.
*/
export const ProjectSearchBarEmpty = ({
className,
...props
}: ProjectSearchBarEmptyProps) => {
return (
<div
{...props}
className={cn('flex items-center px-2 py-2 gap-3', className)}
>
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-orange-50 text-elements-warning dark:bg-red-50 text-error">
<Search size={16} />
</div>
<p className="text-elements-low-em text-sm dark:text-foreground-secondary tracking-[-0.006em]">
No projects matching this name
</p>
</div>
);
};

View File

@ -0,0 +1,75 @@
import { Avatar } from '@/components/shared/Avatar';
import { OmitCommon } from '@/types/common';
import { cn } from '@/utils/classnames';
import { Overwrite, UseComboboxGetItemPropsReturnValue } from 'downshift';
import { Project } from 'gql-client';
import { ComponentPropsWithoutRef, forwardRef } from 'react';
import { getInitials } from '../../../../utils/geInitials';
/**
* Represents a type that merges ComponentPropsWithoutRef<'li'> with certain exclusions.
* @type {MergedComponentPropsWithoutRef}
*/
type MergedComponentPropsWithoutRef = OmitCommon<
ComponentPropsWithoutRef<'button'>,
Omit<
Overwrite<UseComboboxGetItemPropsReturnValue, Project[]>,
'index' | 'item'
>
>;
/**
* ProjectSearchBarItemProps interface defines the props for the ProjectSearchBarItem component.
*/
interface ProjectSearchBarItemProps extends MergedComponentPropsWithoutRef {
/**
* The project item to display.
*/
item: Project;
/**
* Whether the item is active.
*/
active?: boolean;
}
/**
* A component that renders a project item in the search bar.
*
* @param {ProjectSearchBarItemProps} props - The props for the component.
* @returns {React.ReactElement} A button element representing a project item.
*/
const ProjectSearchBarItem = forwardRef<
HTMLButtonElement,
ProjectSearchBarItemProps
>(({ item, active, ...props }, ref) => {
return (
<button
{...props}
ref={ref}
key={item.id}
className={cn(
'px-2 py-2 flex items-center gap-3 rounded-lg text-left hover:bg-base-bg-emphasized',
{
'bg-base-bg-emphasized': active,
},
)}
>
<Avatar
size={32}
imageSrc={item.icon}
initials={getInitials(item.name)}
/>
<div className="flex flex-col flex-1">
<p className="text-sm tracking-[-0.006em] text-elements-high-em">
{item.name}
</p>
<p className="text-elements-low-em text-xs">{item.organization.name}</p>
</div>
</button>
);
});
ProjectSearchBarItem.displayName = 'ProjectSearchBarItem';
export { ProjectSearchBarItem };

View File

@ -0,0 +1,2 @@
export * from './ProjectSearchBar';
export * from './ProjectSearchBarDialog';

View File

@ -0,0 +1,83 @@
'use client';
import { LaconicMark } from '@/laconic-assets/laconic-mark';
import { cn } from '@/lib/utils';
import { Loader2 } from 'lucide-react';
export interface LoadingOverlayProps {
/**
* Controls the visibility of the overlay.
* When false, the component returns null.
* @default true
*/
isLoading?: boolean;
/**
* Optional className for styling the overlay container.
* This will be merged with the default styles.
*/
className?: string;
/**
* Whether to show the Laconic logo in the overlay.
* @default true
*/
showLogo?: boolean;
/**
* Whether to show the loading spinner below the logo.
* @default true
*/
showSpinner?: boolean;
/**
* The z-index value for the overlay.
* Adjust this if you need the overlay to appear above or below other elements.
* @default 50
*/
zIndex?: number;
/**
* Whether to use solid black background instead of semi-transparent.
* Useful for initial page load and full-screen loading states.
* @default false
*/
solid?: boolean;
}
export function LoadingOverlay({
isLoading = true,
className,
showLogo = true,
showSpinner = true,
zIndex = 50,
solid = false,
}: LoadingOverlayProps) {
if (!isLoading) return null;
return (
<div
className={cn(
'fixed inset-0 flex flex-col items-center justify-center',
solid ? 'bg-black' : 'bg-black/90 backdrop-blur-sm',
className,
)}
style={{ zIndex }}
role="alert"
aria-busy="true"
aria-label="Loading"
>
{showLogo && (
<div className="animate-fade-in mb-4">
<LaconicMark className="text-white" />
</div>
)}
{showSpinner && (
<Loader2
className="animate-spin w-6 h-6 text-white"
aria-hidden="true"
/>
)}
</div>
);
}

View File

@ -0,0 +1,110 @@
/**
* NavigationSidebar.tsx
*
* This component displays a sidebar with navigation steps for the onboarding flow.
* It highlights the current step and allows navigation to completed steps.
*
* Implementation:
* 1. Display the logo at the top
* 2. Show a list of navigation items for each step
* 3. Highlight the current step
* 4. Make completed steps clickable for navigation
* 5. Style according to the shadcn/ui design system
*/
import { cn } from '@/lib/utils';
import {
Code,
GitBranch,
Github,
Rocket,
Server,
Settings
} from 'lucide-react';
import React from 'react';
import useNavigationStore, { OnboardingStep } from './store/navigationStore';
// Map steps to icons and labels
const stepConfig = {
[OnboardingStep.CONNECT]: {
icon: Github,
label: 'Connect'
},
[OnboardingStep.REPOSITORY]: {
icon: GitBranch,
label: 'Repository'
},
[OnboardingStep.TEMPLATE]: {
icon: Code,
label: 'Template'
},
[OnboardingStep.CONFIGURE]: {
icon: Settings,
label: 'Configure'
},
[OnboardingStep.DEPLOYMENT_OPTIONS]: {
icon: Server,
label: 'Options'
},
[OnboardingStep.DEPLOY]: {
icon: Rocket,
label: 'Deploy'
},
};
const NavigationSidebar: React.FC = () => {
const { currentStep, completedSteps, setCurrentStep, canGoToStep } = useNavigationStore();
// Order of steps to display
const steps = [
OnboardingStep.CONNECT,
OnboardingStep.REPOSITORY,
OnboardingStep.TEMPLATE,
OnboardingStep.CONFIGURE,
OnboardingStep.DEPLOYMENT_OPTIONS,
OnboardingStep.DEPLOY,
];
return (
<div className="w-64 h-full bg-background border-r p-4 flex flex-col">
{/* Logo */}snowballtools-base
<div className="mb-8 p-2">
<h1 className="text-xl font-bold">Snowball</h1>
</div>
{/* Navigation Items */}
<nav className="space-y-2">
{steps.map((step) => {
const { icon: Icon, label } = stepConfig[step];
const isActive = currentStep === step;
const isCompleted = completedSteps[step];
const isClickable = canGoToStep(step);
return (
<button
key={step}
className={cn(
"w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors",
isActive && "bg-primary text-primary-foreground",
!isActive && isCompleted && "text-foreground hover:bg-muted/80",
!isActive && !isCompleted && "text-muted-foreground",
!isClickable && "opacity-50 cursor-not-allowed",
isClickable && !isActive && "hover:bg-muted cursor-pointer"
)}
onClick={() => isClickable && setCurrentStep(step)}
disabled={!isClickable}
>
<Icon size={18} />
<span>{label}</span>
{isCompleted && !isActive && (
<span className="ml-auto w-2 h-2 bg-primary rounded-full" />
)}
</button>
);
})}
</nav>
</div>
);
};
export default NavigationSidebar;

View File

@ -0,0 +1,36 @@
import {
Dialog,
DialogContent, DialogTrigger
} from '@/components/ui/dialog';
import React from 'react';
import Onboarding from '../onboarding-flow/Onboarding';
interface OnboardingDialogProps {
trigger?: React.ReactNode;
defaultOpen?: boolean;
}
/**
* OnboardingDialog component
*
* A dialog modal that contains the onboarding flow.
* Can be triggered by a custom element or automatically opened.
*/
const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
trigger,
defaultOpen = false
}) => {
return (
<Dialog defaultOpen={defaultOpen}>
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
<DialogContent className="max-w-[95vw] max-h-[95vh] w-[1200px] h-[800px] overflow-hidden p-0">
<div className="h-full overflow-hidden">
{/* <OnboardingLayout isDialog={true} /> */}
<Onboarding/>
</div>
</DialogContent>
</Dialog>
);
};
export default OnboardingDialog;

View File

@ -0,0 +1,88 @@
/**
* OnboardingLayout.tsx
*
* This component serves as the main wrapper for the entire onboarding flow.
* It handles the layout structure with a sidebar and main content area.
* Modified to work well in both full page and dialog contexts.
*
* Implementation:
* 1. Use the existing layout components where possible
* 2. Render the NavigationSidebar on the left
* 3. Render the current step component in the main content area
* 4. The current step should be determined by the navigation store
* 5. Add proper spacing and responsive behavior
*
* Dependencies:
* - NavigationSidebar
* - ConnectView
* - RepositoryListView
* - TemplateSelectionView
* - ConfigureView
* - DeploymentOptionsView
* - DeployView
* - useNavigationStore
*/
import React from 'react';
import NavigationSidebar from './NavigationSidebar';
import ProgressIndicator from './ProgressIndicator';
import useNavigationStore, { OnboardingStep } from './store/navigationStore';
// Import view components
import ConfigureView from './views/ConfigureView';
import ConnectView from './views/ConnectView';
import DeploymentOptionsView from './views/DeploymentOptionsView';
import DeployView from './views/DeployView';
import RepositoryListView from './views/RepositoryListView';
import TemplateSelectionView from './views/TemplateSelectionView';
interface OnboardingLayoutProps {
isDialog?: boolean;
}
const OnboardingLayout: React.FC<OnboardingLayoutProps> = ({
isDialog = false
}) => {
const { currentStep } = useNavigationStore();
// Render the appropriate view component based on the current step
const renderCurrentView = () => {
switch (currentStep) {
case OnboardingStep.CONNECT:
return <ConnectView />;
case OnboardingStep.REPOSITORY:
return <RepositoryListView />;
case OnboardingStep.TEMPLATE:
return <TemplateSelectionView />;
case OnboardingStep.CONFIGURE:
return <ConfigureView />;
case OnboardingStep.DEPLOYMENT_OPTIONS:
return <DeploymentOptionsView />;
case OnboardingStep.DEPLOY:
return <DeployView />;
default:
return <ConnectView />;
}
};
return (
<div className={`flex ${isDialog ? 'h-full' : 'h-screen'} w-full bg-background`}>
{/* Navigation Sidebar */}
<NavigationSidebar />
{/* Main Content Area */}
<div className="flex-1 flex flex-col overflow-hidden">
<main className="flex-1 overflow-y-auto p-6">
{renderCurrentView()}
</main>
{/* Progress Indicator Footer */}
<footer className="border-t p-4">
<ProgressIndicator />
</footer>
</div>
</div>
);
};
export default OnboardingLayout;

View File

@ -0,0 +1,60 @@
/**
* ProgressIndicator.tsx
*
* This component displays a horizontal indicator showing the current progress
* through the onboarding flow steps.
*
* Implementation:
* 1. Use dots to represent each step
* 2. Highlight the current step
* 3. Show completed steps differently
* 4. Style according to the shadcn/ui design system
*/
import { cn } from '@/lib/utils';
import React from 'react';
import useNavigationStore, { OnboardingStep } from './store/navigationStore';
const ProgressIndicator: React.FC = () => {
const { currentStep, completedSteps } = useNavigationStore();
// Define the steps to show in the progress indicator
const steps = [
OnboardingStep.CONNECT,
OnboardingStep.REPOSITORY,
OnboardingStep.TEMPLATE,
OnboardingStep.CONFIGURE,
OnboardingStep.DEPLOYMENT_OPTIONS,
OnboardingStep.DEPLOY,
];
return (
<div className="flex items-center justify-center space-x-2 py-4">
{steps.map((step, index) => {
const isActive = currentStep === step;
const isCompleted = completedSteps[step];
return (
<div
key={step}
className="flex flex-col items-center"
>
<div
className={cn(
"w-3 h-3 rounded-full transition-all duration-200",
isActive && "w-4 h-4 bg-primary",
isCompleted && !isActive && "bg-primary opacity-70",
!isActive && !isCompleted && "bg-muted"
)}
/>
{index < steps.length - 1 && (
<div className="w-8 h-0.5 bg-muted mt-1.5" />
)}
</div>
);
})}
</div>
);
};
export default ProgressIndicator;

View File

@ -0,0 +1,31 @@
# Onboarding Flow
This directory contains the implementation of the onboarding flow for the application.
## Documentation
Please refer to the following documentation for details about the onboarding flow:
- [Component Breakdown](./docs/Component-Breakdown.md) - Overview of all components used in the onboarding flow
- [State Management](./docs/State-Management.md) - Details about Zustand store implementations for state management
- [Form Integration Plan](./docs/Form-Integration-Plan.md) - Guidelines for integrating existing form components
## Implementation Approach
The onboarding flow uses a navigation-only state management approach with Zustand to coordinate between steps, while preserving the functionality of existing form components.
## Directory Structure
- `/docs` - Documentation files
- `/store` - Zustand store implementation
- `/views` - Main view components for each step of the flow
## Getting Started
To implement the onboarding flow, follow these steps:
1. Review the documentation in the `/docs` directory
2. Implement the navigation store as described in the State Management document
3. Create the core layout components
4. Implement each view component according to the Form Integration Plan
5. Test the flow with existing form components

View File

@ -0,0 +1,24 @@
/**
* index.ts
*
* This file exports all components from the onboarding module for easy importing.
*/
// Export the main layout
export { default as OnboardingLayout } from './OnboardingLayout';
// Export the navigation store
export { default as useNavigationStore } from './store/navigationStore';
export { OnboardingStep } from './store/navigationStore';
// Export view components
export { default as ConnectView } from './views/ConnectView';
export { default as RepositoryListView } from './views/RepositoryListView';
export { default as TemplateSelectionView } from './views/TemplateSelectionView';
export { default as ConfigureView } from './views/ConfigureView';
export { default as DeploymentOptionsView } from './views/DeploymentOptionsView';
export { default as DeployView } from './views/DeployView';
// Export other components
export { default as NavigationSidebar } from './NavigationSidebar';
export { default as ProgressIndicator } from './ProgressIndicator';

View File

@ -0,0 +1,175 @@
/**
* navigationStore.ts
*
* This file implements the Zustand store for navigation state management.
* It focuses on navigation-only state to coordinate between steps.
*
* Implementation:
* 1. Define the OnboardingStep enum for all steps
* 2. Create a NavigationState interface with:
* - currentStep tracking
* - completedSteps status object
* - navigation actions (setCurrentStep, markStepCompleted, etc.)
* 3. Implement the store with Zustand
* 4. Add persistence with localStorage
* 5. Implement navigation logic (can only go to completed steps or next incomplete)
* 6. Add actions for resetting navigation
*
* See the State-Management.md documentation for detailed guidance.
*/
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
// Define the steps of the onboarding flow
export enum OnboardingStep {
CONNECT = 'connect',
REPOSITORY = 'repository',
TEMPLATE = 'template',
CONFIGURE = 'configure',
DEPLOYMENT_OPTIONS = 'deployment_options',
DEPLOY = 'deploy'
}
// Define the navigation state interface
interface NavigationState {
// Current step in the flow
currentStep: OnboardingStep;
// Step completion status
completedSteps: {
[key in OnboardingStep]: boolean;
};
// Navigation actions
setCurrentStep: (step: OnboardingStep) => void;
markStepCompleted: (step: OnboardingStep) => void;
markStepIncomplete: (step: OnboardingStep) => void;
goToNextStep: () => void;
goToPreviousStep: () => void;
resetNavigation: () => void;
canGoToStep: (step: OnboardingStep) => boolean;
}
// Order of steps for navigation
const stepOrder: OnboardingStep[] = [
OnboardingStep.CONNECT,
OnboardingStep.REPOSITORY,
OnboardingStep.TEMPLATE,
OnboardingStep.CONFIGURE,
OnboardingStep.DEPLOYMENT_OPTIONS,
OnboardingStep.DEPLOY,
];
// Create and export the store
const useNavigationStore = create<NavigationState>()(
persist(
(set, get) => ({
// Initial state
currentStep: OnboardingStep.CONNECT,
completedSteps: {
[OnboardingStep.CONNECT]: false,
[OnboardingStep.REPOSITORY]: false,
[OnboardingStep.TEMPLATE]: false,
[OnboardingStep.CONFIGURE]: false,
[OnboardingStep.DEPLOYMENT_OPTIONS]: false,
[OnboardingStep.DEPLOY]: false,
},
// Set current step if allowed
setCurrentStep: (step: OnboardingStep) => {
const { canGoToStep } = get();
if (canGoToStep(step)) {
set({ currentStep: step });
}
},
// Mark a step as completed
markStepCompleted: (step: OnboardingStep) => {
set((state) => ({
completedSteps: {
...state.completedSteps,
[step]: true,
},
}));
},
// Mark a step as incomplete
markStepIncomplete: (step: OnboardingStep) => {
set((state) => ({
completedSteps: {
...state.completedSteps,
[step]: false,
},
}));
},
// Go to the next step in the flow
goToNextStep: () => {
const { currentStep } = get();
const currentIndex = stepOrder.indexOf(currentStep);
if (currentIndex < stepOrder.length - 1) {
const nextStep = stepOrder[currentIndex + 1];
set({ currentStep: nextStep });
}
},
// Go to the previous step in the flow
goToPreviousStep: () => {
const { currentStep } = get();
const currentIndex = stepOrder.indexOf(currentStep);
if (currentIndex > 0) {
const prevStep = stepOrder[currentIndex - 1];
set({ currentStep: prevStep });
}
},
// Reset navigation state
resetNavigation: () => {
set({
currentStep: OnboardingStep.CONNECT,
completedSteps: {
[OnboardingStep.CONNECT]: false,
[OnboardingStep.REPOSITORY]: false,
[OnboardingStep.TEMPLATE]: false,
[OnboardingStep.CONFIGURE]: false,
[OnboardingStep.DEPLOYMENT_OPTIONS]: false,
[OnboardingStep.DEPLOY]: false,
},
});
},
// Check if navigation to a step is allowed
canGoToStep: (step: OnboardingStep) => {
const { completedSteps } = get();
const stepIndex = stepOrder.indexOf(step);
const currentStepIndex = stepOrder.indexOf(get().currentStep);
// Can always go to current step or previous completed steps
if (step === get().currentStep || (stepIndex < currentStepIndex && completedSteps[step])) {
return true;
}
// Can go to the next step if all previous steps are completed
if (stepIndex === currentStepIndex + 1) {
// Check if all previous steps are completed
for (let i = 0; i < stepIndex; i++) {
if (!completedSteps[stepOrder[i]]) {
return false;
}
}
return true;
}
return false;
},
}),
{
name: 'onboarding-navigation-storage',
}
)
);
export default useNavigationStore;

View File

@ -0,0 +1,163 @@
/**
* ConfigureView.tsx
*
* This component displays configuration options for deployment.
* It includes settings for deployment URL, environment variables, and other options.
*
* Implementation:
* 1. Display form sections for various configuration options
* 2. Include input fields for deployment URL
* 3. Add environment variables section
* 4. Include deployment options (number of instances, etc.)
* 5. Add navigation controls
*/
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { ArrowLeft, ArrowRight, Plus, Settings } from 'lucide-react';
import React, { useState } from 'react';
import useNavigationStore, { OnboardingStep } from '../store/navigationStore';
const ConfigureView: React.FC = () => {
const [deploymentUrl, setDeploymentUrl] = useState('');
const [maxDeployments, setMaxDeployments] = useState('');
const [environmentTypes, setEnvironmentTypes] = useState({
production: true,
preview: true,
development: false,
});
const { markStepCompleted, goToNextStep, goToPreviousStep } = useNavigationStore();
const handleNext = () => {
// In a real app, would validate inputs
markStepCompleted(OnboardingStep.CONFIGURE);
goToNextStep();
};
const toggleEnvironmentType = (type: 'production' | 'preview' | 'development') => {
setEnvironmentTypes({
...environmentTypes,
[type]: !environmentTypes[type],
});
};
return (
<div className="flex flex-col max-w-4xl mx-auto space-y-8">
{/* Header */}
<div className="text-center mb-6">
<h1 className="text-3xl font-bold mb-2">Configure</h1>
<p className="text-lg text-muted-foreground">
Set the deployment URL for a single deployment or by creating a separate section for multiple deployments
</p>
</div>
<div className="flex flex-col items-center">
<div className="w-16 h-16 flex items-center justify-center rounded-full bg-primary/10 mb-4">
<Settings size={32} className="text-primary" />
</div>
<h2 className="text-2xl font-semibold mb-4">Configure</h2>
</div>
{/* Configuration Form */}
<Card>
<CardHeader>
<CardTitle>Deployment Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Deployment URL */}
<div className="space-y-2">
<Label htmlFor="deployment-url">Deployment URL</Label>
<Input
id="deployment-url"
placeholder="my-app.example.com"
value={deploymentUrl}
onChange={(e) => setDeploymentUrl(e.target.value)}
/>
</div>
{/* Number of Deployments */}
<div className="space-y-2">
<Label htmlFor="max-deployments">Number of Deployments</Label>
<Select value={maxDeployments} onValueChange={setMaxDeployments}>
<SelectTrigger id="max-deployments">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1</SelectItem>
<SelectItem value="2">2</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="4">4</SelectItem>
<SelectItem value="5">5</SelectItem>
</SelectContent>
</Select>
</div>
{/* Environment Variables */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label>Environment Variables</Label>
<Button variant="ghost" size="sm">
<Plus className="h-4 w-4 mr-1" />
Add Variable
</Button>
</div>
{/* Environment variables would be implemented here */}
</div>
{/* Environment Types */}
<div className="space-y-2">
<Label>Environment Types</Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="production"
checked={environmentTypes.production}
onCheckedChange={() => toggleEnvironmentType('production')}
/>
<Label htmlFor="production" className="cursor-pointer">Production</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="preview"
checked={environmentTypes.preview}
onCheckedChange={() => toggleEnvironmentType('preview')}
/>
<Label htmlFor="preview" className="cursor-pointer">Preview</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="development"
checked={environmentTypes.development}
onCheckedChange={() => toggleEnvironmentType('development')}
/>
<Label htmlFor="development" className="cursor-pointer">Development</Label>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Navigation Buttons */}
<div className="flex justify-between">
<Button variant="outline" onClick={goToPreviousStep}>
<ArrowLeft className="mr-2 h-4 w-4" />
Previous
</Button>
<Button onClick={handleNext}>
Next
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
);
};
export default ConfigureView;

View File

@ -0,0 +1,143 @@
/**
* ConnectView.tsx
*
* This component displays the initial connection screen for GitHub authentication.
* It shows a button to connect with GitHub and options to select import or template after connection.
*
* Implementation:
* 1. Use existing Card components from shared/ui
* 2. Add a GitHub connect button with icon
* 3. Show two card options after connection: Import Repository or Start with Template
* 4. Add selection behavior for the two options
* 5. Show a "Next" button that's enabled when an option is selected
* 6. Use navigation store to mark step as completed and go to next step
* 7. Match the dark theme UI shown in the screenshots
*
* Dependencies:
* - Card, CardHeader, CardContent, CardFooter from shared components
* - Button from shared components
* - Github icon from Lucide React
* - useNavigationStore
*/
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { ArrowRight, Code, GitBranch, Github } from 'lucide-react';
import React, { useState } from 'react';
import useNavigationStore, { OnboardingStep } from '../store/navigationStore';
const ConnectView: React.FC = () => {
const [isConnected, setIsConnected] = useState(false);
const [selectedOption, setSelectedOption] = useState<'import' | 'template' | null>(null);
const { markStepCompleted, goToNextStep, setCurrentStep } = useNavigationStore();
// Function to simulate GitHub connection
const handleConnect = () => {
// In a real app, this would trigger GitHub OAuth
setIsConnected(true);
// Mark the connect step as completed
markStepCompleted(OnboardingStep.CONNECT);
};
// Function to handle option selection
const handleOptionSelect = (option: 'import' | 'template') => {
setSelectedOption(option);
};
// Function to handle navigation to next step
const handleNext = () => {
if (selectedOption === 'import') {
setCurrentStep(OnboardingStep.REPOSITORY);
} else if (selectedOption === 'template') {
setCurrentStep(OnboardingStep.TEMPLATE);
}
};
return (
<div className="flex flex-col items-center max-w-4xl mx-auto space-y-8">
{/* Header */}
<div className="text-center mb-6">
<h1 className="text-3xl font-bold mb-2">Connect</h1>
<p className="text-lg text-muted-foreground">
Connect and import a GitHub repo or start from a template
</p>
</div>
{!isConnected ? (
// Not connected state - Show GitHub connect
<div className="flex flex-col items-center space-y-6 w-full max-w-md">
<div className="flex justify-center p-6">
<div className="w-24 h-24 flex items-center justify-center rounded-full bg-primary/10">
<Github size={48} className="text-primary" />
</div>
</div>
<h2 className="text-2xl font-semibold text-center">Deploy your first app</h2>
<p className="text-center text-muted-foreground">
Once connected, you can import a repository from your account or start with one of our templates.
</p>
<Button onClick={handleConnect} size="lg" className="mt-4">
<Github className="mr-2 h-5 w-5" />
Connect to GitHub
</Button>
</div>
) : (
// Connected state - Show import/template options
<div className="w-full">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
{/* Import Repository Option */}
<Card
className={`cursor-pointer border-2 transition-colors ${
selectedOption === 'import' ? 'border-primary' : 'border-border'
}`}
onClick={() => handleOptionSelect('import')}
>
<CardContent className="p-6 flex flex-col items-center">
<div className="w-16 h-16 flex items-center justify-center rounded-full bg-primary/10 mb-4">
<GitBranch size={32} className="text-primary" />
</div>
<h3 className="text-xl font-semibold mb-2">Import a repository</h3>
<p className="text-center text-muted-foreground">
Select from your existing GitHub repositories
</p>
</CardContent>
</Card>
{/* Start with Template Option */}
<Card
className={`cursor-pointer border-2 transition-colors ${
selectedOption === 'template' ? 'border-primary' : 'border-border'
}`}
onClick={() => handleOptionSelect('template')}
>
<CardContent className="p-6 flex flex-col items-center">
<div className="w-16 h-16 flex items-center justify-center rounded-full bg-primary/10 mb-4">
<Code size={32} className="text-primary" />
</div>
<h3 className="text-xl font-semibold mb-2">Start with a template</h3>
<p className="text-center text-muted-foreground">
Choose from our pre-configured templates
</p>
</CardContent>
</Card>
</div>
{/* Navigation Buttons */}
<div className="flex justify-end">
<Button
onClick={handleNext}
disabled={!selectedOption}
className="px-6"
>
Next
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
)}
</div>
);
};
export default ConnectView;

View File

@ -0,0 +1,126 @@
/**
* DeployView.tsx
*
* This component displays the final deployment screen with status information
* and payment options.
*
* Implementation:
* 1. Display deployment status with confirmations
* 2. Show project details
* 3. Include payment or deployment button
* 4. Add navigation controls
* 5. Match the dark theme UI shown in the screenshots
*/
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { ArrowLeft, ArrowRight, Check, Rocket } from 'lucide-react';
import React, { useState } from 'react';
import useNavigationStore, { OnboardingStep } from '../store/navigationStore';
const DeployView: React.FC = () => {
const [isDeployed, setIsDeployed] = useState(false);
const { markStepCompleted, goToPreviousStep } = useNavigationStore();
const handleDeploy = () => {
// In a real app, this would actually deploy the project
setIsDeployed(true);
markStepCompleted(OnboardingStep.DEPLOY);
};
// Mock project data
const project = {
name: 'Progressive Web App (PWA)',
url: 'git.account/repo-name',
status: 'configured',
};
return (
<div className="flex flex-col max-w-4xl mx-auto space-y-8">
{/* Header */}
<div className="text-center mb-6">
<h1 className="text-3xl font-bold mb-2">Deploy</h1>
<p className="text-lg text-muted-foreground">
Your deployment is configured and ready to go!
</p>
</div>
<div className="flex flex-col items-center">
<div className="w-16 h-16 flex items-center justify-center rounded-full bg-primary/10 mb-4">
<Rocket size={32} className="text-primary" />
</div>
<h2 className="text-2xl font-semibold mb-4">Deploy</h2>
</div>
{/* Deployment Status Card */}
<Card className="border border-border">
<CardContent className="p-6">
<div className="text-center mb-6">
<h3 className="text-xl font-medium mb-2">
{isDeployed ? 'Deployment Complete!' : 'Your deployment is configured and ready to go!'}
</h3>
<p className="text-muted-foreground">
{isDeployed
? 'Your app has been successfully deployed and is now live.'
: 'Review your settings and click "Pay and Deploy" to launch your application.'}
</p>
</div>
{/* Project Details */}
<div className="space-y-4 mb-6">
<div className="flex justify-between items-center p-3 bg-muted/50 rounded-md">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-green-500"></div>
<span className="font-medium">Progressive Web App (PWA)</span>
</div>
<span className="text-sm text-muted-foreground">$7 per month</span>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground p-2">
<Check size={16} className="text-green-500" />
<span>git.account/repo-name</span>
</div>
</div>
{/* Deploy Button */}
{!isDeployed ? (
<Button
className="w-full"
size="lg"
onClick={handleDeploy}
>
Pay and Deploy
</Button>
) : (
<div className="flex flex-col items-center gap-4">
<div className="flex items-center gap-2 text-green-500">
<Check size={20} />
<span className="font-medium">Successfully Deployed</span>
</div>
<Button variant="outline" className="w-full">
View Dashboard
</Button>
</div>
)}
</CardContent>
</Card>
{/* Navigation Buttons */}
<div className="flex justify-between">
<Button variant="outline" onClick={goToPreviousStep}>
<ArrowLeft className="mr-2 h-4 w-4" />
Previous
</Button>
{isDeployed && (
<Button variant="default">
Finish
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
)}
</div>
</div>
);
};
export default DeployView;

View File

@ -0,0 +1,151 @@
/**
* DeploymentOptionsView.tsx
*
* This component displays advanced deployment options like container selection,
* account selection, and other deployment-specific settings.
*
* Implementation:
* 1. Display forms for deployment options
* 2. Include dropdown for container URL selection
* 3. Include account selection
* 4. Add navigation controls
*/
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { ArrowLeft, ArrowRight, Server } from 'lucide-react';
import React, { useState } from 'react';
import useNavigationStore, { OnboardingStep } from '../store/navigationStore';
const DeploymentOptionsView: React.FC = () => {
const [selectedContainer, setSelectedContainer] = useState('');
const [selectedAccount, setSelectedAccount] = useState('');
const [environmentTypes, setEnvironmentTypes] = useState({
production: true,
preview: true,
development: false,
});
const { markStepCompleted, goToNextStep, goToPreviousStep } = useNavigationStore();
const handleNext = () => {
// In a real app, would validate inputs
markStepCompleted(OnboardingStep.DEPLOYMENT_OPTIONS);
goToNextStep();
};
const toggleEnvironmentType = (type: 'production' | 'preview' | 'development') => {
setEnvironmentTypes({
...environmentTypes,
[type]: !environmentTypes[type],
});
};
return (
<div className="flex flex-col max-w-4xl mx-auto space-y-8">
{/* Header */}
<div className="text-center mb-6">
<h1 className="text-3xl font-bold mb-2">Configure</h1>
<p className="text-lg text-muted-foreground">
Set the deployment URL for a single deployment or by creating a separate section for multiple deployments
</p>
</div>
<div className="flex flex-col items-center">
<div className="w-16 h-16 flex items-center justify-center rounded-full bg-primary/10 mb-4">
<Server size={32} className="text-primary" />
</div>
<h2 className="text-2xl font-semibold mb-4">Deployment Options</h2>
</div>
{/* Deployment Options Form */}
<Card>
<CardHeader>
<CardTitle>Container Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Container URL */}
<div className="space-y-2">
<Label htmlFor="container-url">Select Container URL</Label>
<Select value={selectedContainer} onValueChange={setSelectedContainer}>
<SelectTrigger id="container-url">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="container1">container-123.example.com</SelectItem>
<SelectItem value="container2">container-456.example.com</SelectItem>
<SelectItem value="container3">container-789.example.com</SelectItem>
</SelectContent>
</Select>
</div>
{/* Environment Types */}
<div className="space-y-2">
<Label>Environment Variables</Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="production-option"
checked={environmentTypes.production}
onCheckedChange={() => toggleEnvironmentType('production')}
/>
<Label htmlFor="production-option" className="cursor-pointer">Production</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="preview-option"
checked={environmentTypes.preview}
onCheckedChange={() => toggleEnvironmentType('preview')}
/>
<Label htmlFor="preview-option" className="cursor-pointer">Preview</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="development-option"
checked={environmentTypes.development}
onCheckedChange={() => toggleEnvironmentType('development')}
/>
<Label htmlFor="development-option" className="cursor-pointer">Development</Label>
</div>
</div>
</div>
{/* Account Selection */}
<div className="space-y-2">
<Label htmlFor="select-account">Select Account</Label>
<Select value={selectedAccount} onValueChange={setSelectedAccount}>
<SelectTrigger id="select-account">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="account1">Personal Account</SelectItem>
<SelectItem value="account2">Team Account</SelectItem>
<SelectItem value="account3">Organization Account</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Navigation Buttons */}
<div className="flex justify-between">
<Button variant="outline" onClick={goToPreviousStep}>
<ArrowLeft className="mr-2 h-4 w-4" />
Previous
</Button>
<Button onClick={handleNext}>
Next
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
);
};
export default DeploymentOptionsView;

View File

@ -0,0 +1,106 @@
/**
* RepositoryListView.tsx
*
* This component displays a list of GitHub repositories for the user to select from.
* It shows repository names with timestamps and allows selection.
*
* Implementation:
* 1. Use a scrollable list component
* 2. Show repository names with timestamps
* 3. Allow selection with radio buttons
* 4. Add navigation controls (back/next)
* 5. Add proper spacing and responsive behavior
*/
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { ScrollArea } from '@/components/ui/scroll-area';
import { ArrowLeft, ArrowRight, GitBranch } from 'lucide-react';
import React, { useState } from 'react';
import useNavigationStore, { OnboardingStep } from '../store/navigationStore';
// Mock data for repositories
const mockRepositories = [
{ id: '1', name: 'username/repo-name', updatedAt: '4 minutes ago' },
{ id: '2', name: 'username/another-repo', updatedAt: '6 minutes ago' },
{ id: '3', name: 'username/test-project', updatedAt: '2 hours ago' },
{ id: '4', name: 'username/awesome-app', updatedAt: '1 day ago' },
{ id: '5', name: 'username/frontend-demo', updatedAt: '3 days ago' },
];
const RepositoryListView: React.FC = () => {
const [selectedRepo, setSelectedRepo] = useState<string | null>(null);
const { markStepCompleted, goToNextStep, goToPreviousStep } = useNavigationStore();
const handleRepoSelect = (repoId: string) => {
setSelectedRepo(repoId);
};
const handleNext = () => {
if (selectedRepo) {
markStepCompleted(OnboardingStep.REPOSITORY);
goToNextStep();
}
};
return (
<div className="flex flex-col max-w-4xl mx-auto space-y-8">
{/* Header */}
<div className="text-center mb-6">
<h1 className="text-3xl font-bold mb-2">Connect</h1>
<p className="text-lg text-muted-foreground">
Connect and import a GitHub repo or start from a template
</p>
</div>
<div className="flex flex-col items-center">
<div className="w-16 h-16 flex items-center justify-center rounded-full bg-primary/10 mb-4">
<GitBranch size={32} className="text-primary" />
</div>
<h2 className="text-2xl font-semibold mb-4">Import a repository</h2>
</div>
{/* Repository List */}
<div className="border rounded-lg w-full">
<ScrollArea className="h-64">
<RadioGroup value={selectedRepo || ''} onValueChange={handleRepoSelect} className="p-1">
{mockRepositories.map((repo) => (
<div
key={repo.id}
className="flex items-center space-x-3 border-b last:border-0 p-4 hover:bg-muted/50 transition-colors"
>
<RadioGroupItem value={repo.id} id={`repo-${repo.id}`} className="data-[state=checked]:border-primary data-[state=checked]:bg-primary" />
<Label htmlFor={`repo-${repo.id}`} className="flex flex-1 justify-between cursor-pointer">
<div className="flex items-center space-x-2">
<GitBranch size={18} className="text-muted-foreground" />
<span>{repo.name}</span>
</div>
<span className="text-muted-foreground text-sm">{repo.updatedAt}</span>
</Label>
</div>
))}
</RadioGroup>
</ScrollArea>
</div>
{/* Navigation Buttons */}
<div className="flex justify-between">
<Button variant="outline" onClick={goToPreviousStep}>
<ArrowLeft className="mr-2 h-4 w-4" />
Previous
</Button>
<Button
onClick={handleNext}
disabled={!selectedRepo}
>
Next
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
);
};
export default RepositoryListView;

View File

@ -0,0 +1,146 @@
/**
* TemplateSelectionView.tsx
*
* This component displays a grid of templates for the user to select from.
* It shows template cards with icons, names, and descriptions.
*
* Implementation:
* 1. Display a grid of template cards
* 2. Show template details including icon, name, and description
* 3. Allow selection of a template
* 4. Add navigation controls
* 5. Add proper spacing and responsive behavior
*/
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { ArrowLeft, ArrowRight, Code } from 'lucide-react';
import React, { useState } from 'react';
import useNavigationStore, { OnboardingStep } from '../store/navigationStore';
// Mock data for templates
const mockTemplates = [
{
id: '1',
name: 'Progressive Web App (PWA)',
description: 'A responsive web app with offline capabilities',
icon: Code,
category: 'web'
},
{
id: '2',
name: 'Design Upload Pack',
description: 'Simple file upload and processing pipeline',
icon: Code,
category: 'utility'
},
{
id: '3',
name: 'React + Redux + TailwindCSS',
description: 'Modern frontend stack with state management',
icon: Code,
category: 'web'
},
{
id: '4',
name: 'Node.js API Starter',
description: 'Backend API with Express and MongoDB',
icon: Code,
category: 'api'
},
{
id: '5',
name: 'E-commerce Platform',
description: 'Full-stack shop with payment processing',
icon: Code,
category: 'fullstack'
},
{
id: '6',
name: 'Static Blog Starter',
description: 'JAMstack blog with markdown support',
icon: Code,
category: 'content'
},
];
const TemplateSelectionView: React.FC = () => {
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null);
const { markStepCompleted, goToNextStep, goToPreviousStep } = useNavigationStore();
const handleTemplateSelect = (templateId: string) => {
setSelectedTemplate(templateId);
};
const handleNext = () => {
if (selectedTemplate) {
markStepCompleted(OnboardingStep.TEMPLATE);
goToNextStep();
}
};
return (
<div className="flex flex-col max-w-4xl mx-auto space-y-8">
{/* Header */}
<div className="text-center mb-6">
<h1 className="text-3xl font-bold mb-2">Connect</h1>
<p className="text-lg text-muted-foreground">
Connect and import a GitHub repo or start from a template
</p>
</div>
<div className="flex flex-col items-center">
<div className="w-16 h-16 flex items-center justify-center rounded-full bg-primary/10 mb-4">
<Code size={32} className="text-primary" />
</div>
<h2 className="text-2xl font-semibold mb-4">Start with a template</h2>
</div>
{/* Template Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{mockTemplates.map((template) => {
const Icon = template.icon;
const isSelected = selectedTemplate === template.id;
return (
<Card
key={template.id}
className={`cursor-pointer border-2 transition-colors ${
isSelected ? 'border-primary' : 'border-border'
}`}
onClick={() => handleTemplateSelect(template.id)}
>
<CardContent className="p-4 flex items-start space-x-4">
<div className="w-10 h-10 flex items-center justify-center rounded-full bg-primary/10 mt-1">
<Icon size={20} className="text-primary" />
</div>
<div>
<h3 className="font-medium">{template.name}</h3>
<p className="text-sm text-muted-foreground">{template.description}</p>
</div>
</CardContent>
</Card>
);
})}
</div>
{/* Navigation Buttons */}
<div className="flex justify-between">
<Button variant="outline" onClick={goToPreviousStep}>
<ArrowLeft className="mr-2 h-4 w-4" />
Previous
</Button>
<Button
onClick={handleNext}
disabled={!selectedTemplate}
>
Next
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
);
};
export default TemplateSelectionView;

View File

@ -0,0 +1,117 @@
# Onboarding Flow Optimization Recommendations
Based on the analysis of the current implementation and comparison with the Figma designs, here are recommendations for optimizing the structure and documentation of the onboarding flow components.
## Implemented Improvements ✅
1. **Consistent File Naming**
- ✅ Renamed `ConnectDeployFirstApp.tsx` to `connect-deploy-first-app.tsx` to maintain kebab-case convention
- ✅ All component files now follow the same naming pattern
2. **Step Organization**
- ✅ Each step directory now has an `index.ts` file that exports all components
- ✅ The connect, configure, and deploy steps follow the same pattern
3. **Common Components**
- ✅ Moved shared UI elements to a `common/` subdirectory
- ✅ These include `background-svg.tsx`, `laconic-icon-lettering.tsx`, etc.
4. **Types Extraction**
- ✅ Created a separate `types.ts` file for shared interfaces and types
- ✅ Moved the `Step` type and related interfaces from `store.ts` to this file
5. **Directory Structure**
- ✅ Reorganized into a more logical structure with `common/`, `sidebar/`, and step-specific directories
- ✅ Created appropriate index.ts files to simplify imports
6. **Documentation Standards**
- ✅ Created project-wide documentation standards in `/standards/COMPONENT_DOCUMENTATION.md`
## Remaining Recommendations 🚧
### Documentation Improvements
1. **Component Documentation**
- [ ] Apply JSDoc pattern to all components following the project standards in `/standards/COMPONENT_DOCUMENTATION.md`
- [ ] Include description, props interface, examples, and component hierarchy
2. **Figma References**
- [ ] Add specific Figma node IDs to each component file
- [ ] Example: `/** @see https://www.figma.com/file/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=571-3500 */`
3. **Props Documentation**
- [ ] Document all props with JSDoc comments in each component
- [ ] Include type information, description, and default values
### Implementation Recommendations
1. **State Management**
- [ ] Add form validation to the store
- [ ] Consider adding persistence for the onboarding flow state
2. **Component Splitting**
- [ ] Break down large components into smaller, focused ones
- [ ] For example, separate form elements in the configure step
3. **Progressive Enhancement**
- [ ] Implement loading states for asynchronous operations
- [ ] Add error handling for API requests
- [ ] Add animations for transitions between steps
4. **Accessibility**
- [ ] Ensure all interactive elements have proper ARIA attributes
- [ ] Add keyboard navigation
- [ ] Test with screen readers
## Current Directory Structure
```
onboarding-flow/
├── README.md
├── OPTIMIZATION.md
├── index.ts
├── types.ts
├── store.ts
├── Onboarding.tsx
├── common/
│ ├── index.ts
│ ├── background-svg.tsx
│ ├── laconic-icon-lettering.tsx
│ ├── onboarding-container.tsx
│ ├── step-header.tsx
│ └── step-navigation.tsx
├── sidebar/
│ ├── index.ts
│ └── sidebar-nav.tsx
├── connect-step/
│ ├── index.ts
│ ├── connect-step.tsx
│ ├── connect-button.tsx
│ ├── connect-initial.tsx
│ ├── connect-deploy-first-app.tsx
│ ├── repository-list.tsx
│ └── template-list.tsx
├── configure-step/
│ ├── index.ts
│ └── configure-step.tsx
└── deploy-step/
├── index.ts
└── deploy-step.tsx
```
## Next Steps
1. Complete the documentation of all components using the project-wide standards
2. Implement the missing features identified in the README.md
3. Consider adding more specialized components for the configure and deploy steps
4. Add unit tests and Storybook stories for all components

View File

@ -0,0 +1,82 @@
/**
* @component Onboarding
* @description Main component that orchestrates the onboarding flow
*
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=571-3500&m=dev
*/
import {
OnboardingContainer, StepNavigation
} from '@/components/onboarding-flow/common';
import { ConfigureStep } from '@/components/onboarding-flow/configure-step';
import { ConnectStep } from '@/components/onboarding-flow/connect-step';
import { DeployStep } from '@/components/onboarding-flow/deploy-step';
import { SidebarNav } from '@/components/onboarding-flow/sidebar';
import { useOnboarding } from '@/components/onboarding-flow/store';
import { ScrollArea } from '@/components/ui/scroll-area';
import { FileCog, GitPullRequest, SquareArrowOutDownRight } from 'lucide-react';
/** Icons for each step in the onboarding flow */
const stepIcons = {
connect: <GitPullRequest className="h-6 w-6 stroke-2" />,
configure: <FileCog className="h-6 w-6 stroke-2" />,
deploy: <SquareArrowOutDownRight className="h-6 w-6 stroke-2" />,
};
/** Titles for each step in the onboarding flow */
const stepTitles = {
connect: 'Connect',
configure: 'Configure',
deploy: 'Deploy',
};
/** Descriptions for each step in the onboarding flow */
const stepDescriptions = {
connect: 'Connect and import a GitHub repository to start deploying.',
configure: 'Set up your deployment configuration and environment variables.',
deploy: 'Review your settings and deploy your project.',
};
/**
* Main onboarding page component
* Orchestrates the entire onboarding flow and manages step transitions
*
* Component Hierarchy:
* - OnboardingContainer
* - SidebarNav (step progress)
* - Main content
* - StepHeader (current step info)
* - Step content (ConnectStep | ConfigureStep | DeployStep)
* - StepNavigation (previous/next controls)
*
* @returns {JSX.Element} Complete onboarding interface
*/
export default function Onboarding() {
const { currentStep, nextStep, previousStep } = useOnboarding();
return (
<OnboardingContainer>
<SidebarNav currentStep={currentStep} />
<div className="flex-1 bg-primary-foreground rounded-lg p-8 shadow-[0_1px_2px_0_rgba(0,0,0,0.06),0_1px_3px_0_rgba(0,0,0,0.1)] flex flex-col overflow-hidden">
{/* <StepHeader
icon={stepIcons[currentStep]}
title={stepTitles[currentStep]}
description={stepDescriptions[currentStep]}
/> */}
<div className="py-4 px-1">
<ScrollArea className="flex-1 mt-6 mb-6">
{currentStep === 'connect' && <ConnectStep />}
{currentStep === 'configure' && <ConfigureStep />}
{currentStep === 'deploy' && <DeployStep />}
</ScrollArea>
</div>
<StepNavigation
currentStep={currentStep}
onPrevious={previousStep}
onNext={nextStep}
nextLabel={currentStep === 'deploy' ? 'Deploy' : 'Next'}
/>
</div>
</OnboardingContainer>
);
}

View File

@ -0,0 +1,278 @@
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
import { useOctokit } from '@/context/OctokitContext';
import React, { useEffect, useState } from 'react';
import Onboarding from './Onboarding';
import { useOnboarding } from './store';
import { OnboardingFormData, Step } from './types';
// Local storage keys
const ONBOARDING_COMPLETED_KEY = 'onboarding_completed';
const ONBOARDING_STATE_KEY = 'onboarding_state';
const ONBOARDING_PROGRESS_KEY = 'onboarding_progress';
const ONBOARDING_FORCE_CONNECT_KEY = 'onboarding_force_connect';
interface OnboardingDialogProps {
trigger?: React.ReactNode;
defaultOpen?: boolean;
onClose?: () => void;
}
/**
* OnboardingDialog component
*
* A dialog modal that contains the onboarding flow.
* Can be triggered by a custom element or automatically opened.
* Sets the initial step based on GitHub connection status.
* Provides warnings when exiting mid-step and options to continue progress.
*/
const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
trigger,
defaultOpen = false,
onClose,
}) => {
const onboardingStore = useOnboarding();
const { setCurrentStep, currentStep, formData } = onboardingStore;
const { octokit } = useOctokit();
const [showExitWarning, setShowExitWarning] = useState(false);
const [showContinueAlert, setShowContinueAlert] = useState(false);
const [isOpen, setIsOpen] = useState(defaultOpen);
const [forceConnectStep, setForceConnectStep] = useState(false);
// Check for force connect flag when component mounts
useEffect(() => {
const shouldForceConnect =
localStorage.getItem(ONBOARDING_FORCE_CONNECT_KEY) === 'true';
if (shouldForceConnect) {
setForceConnectStep(true);
// Clear the flag so it's only used once
localStorage.removeItem(ONBOARDING_FORCE_CONNECT_KEY);
}
}, []);
// Local implementation of reset function that handles all necessary state
const resetOnboardingState = () => {
// Reset step to connect
setCurrentStep('connect');
// Flag to force starting from the connect step
setForceConnectStep(true);
// Also reset form data to ensure substeps are cleared
const store = onboardingStore as any;
if (typeof store.updateFormData === 'function') {
store.updateFormData({
projectName: '',
repoName: '',
repoDescription: '',
framework: '',
access: 'public',
organizationSlug: '',
});
}
};
// Close and reset onboarding dialog
const closeOnboarding = () => {
// Remove the "in progress" flag from localStorage
localStorage.removeItem(ONBOARDING_PROGRESS_KEY);
// Also remove saved state to prevent issues on next open
localStorage.removeItem(ONBOARDING_STATE_KEY);
// Reset component state
resetOnboardingState();
setShowContinueAlert(false);
// Explicitly set isOpen to false to ensure dialog closes
setIsOpen(false);
};
// Check if there's existing progress
useEffect(() => {
if (isOpen) {
const savedProgress = localStorage.getItem(ONBOARDING_PROGRESS_KEY);
const savedState = localStorage.getItem(ONBOARDING_STATE_KEY);
if (savedProgress === 'true' && savedState && !forceConnectStep) {
// Show continue or start fresh dialog
setShowContinueAlert(true);
} else {
// Set initial step based on GitHub connection status
initializeOnboarding();
}
}
}, [isOpen, forceConnectStep]);
// Set the initial step based on GitHub connection status
const initializeOnboarding = () => {
// Reset previous state
resetOnboardingState();
// If GitHub is connected AND we're not forcing the connect step,
// start at the configure step. Otherwise, start at the connect step
if (octokit && !forceConnectStep) {
setCurrentStep('configure');
} else {
setCurrentStep('connect');
}
// Mark that we have onboarding in progress
localStorage.setItem(ONBOARDING_PROGRESS_KEY, 'true');
// Save the initial state
saveCurrentState();
};
// Start fresh by initializing onboarding and forcing the connect step
const startFresh = () => {
// Set flag to force starting from the connect step
setForceConnectStep(true);
initializeOnboarding();
setShowContinueAlert(false);
};
// Continue from saved state and don't force the connect step
const continueOnboarding = () => {
// Reset the force flag since we're continuing
setForceConnectStep(false);
loadSavedState();
};
// Save current onboarding state
const saveCurrentState = () => {
try {
const state = {
currentStep,
formData,
forceConnectStep, // Save this flag as part of the state
};
localStorage.setItem(ONBOARDING_STATE_KEY, JSON.stringify(state));
} catch (error) {
console.error('Error saving onboarding state:', error);
}
};
// Load saved onboarding state
const loadSavedState = () => {
try {
const savedState = localStorage.getItem(ONBOARDING_STATE_KEY);
if (savedState) {
const state = JSON.parse(savedState);
// Restore the force flag if it exists
if (state.forceConnectStep !== undefined) {
setForceConnectStep(state.forceConnectStep);
}
setCurrentStep(state.currentStep as Step);
// Also restore form data to preserve org/repo selection
const store = onboardingStore as any;
if (state.formData && typeof store.updateFormData === 'function') {
store.updateFormData(state.formData as Partial<OnboardingFormData>);
}
}
} catch (error) {
console.error('Error loading onboarding state:', error);
initializeOnboarding();
}
setShowContinueAlert(false);
};
// Save state on step changes
useEffect(() => {
if (isOpen) {
saveCurrentState();
}
}, [currentStep, formData, forceConnectStep]);
// Mark onboarding as completed when user reaches the deploy step
useEffect(() => {
if (currentStep === 'deploy') {
localStorage.setItem(ONBOARDING_COMPLETED_KEY, 'true');
}
}, [currentStep]);
// Handle dialog close attempt
const handleOpenChange = (open: boolean) => {
// First update the isOpen state to ensure UI responds immediately
setIsOpen(open);
if (!open) {
// When dialog is closing, properly clean up
closeOnboarding();
if (onClose) {
onClose();
}
}
};
// Define the missing functions to handle dialog closing
const cancelClose = () => {
setShowExitWarning(false);
};
const completeClose = () => {
closeOnboarding();
setShowExitWarning(false);
};
return (
<>
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
<DialogContent className="max-w-[95vw] max-h-[95vh] w-[1200px] h-[800px] overflow-hidden p-0">
<div className="h-full overflow-hidden">
<Onboarding />
</div>
</DialogContent>
</Dialog>
{/* Exit Warning Dialog */}
{/* <AlertDialog open={showExitWarning} onOpenChange={setShowExitWarning}>
<AlertDialogContent>
<AlertDialogTitle>Exit Onboarding?</AlertDialogTitle>
<AlertDialogDescription>
You haven't completed the onboarding process. If you exit now, your
progress will be lost, including any organization or repository
selections.
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel onClick={cancelClose}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={completeClose}>
Exit Anyway
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog> */}
{/* Continue Progress Dialog */}
{/* <AlertDialog open={showContinueAlert} onOpenChange={setShowContinueAlert}>
<AlertDialogContent>
<AlertDialogTitle>Continue Onboarding?</AlertDialogTitle>
<AlertDialogDescription>
You're in the middle of setting up your project, including
organization and repository selection. Would you like to continue
where you left off or start fresh?
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel onClick={startFresh}>
Start Fresh
</AlertDialogCancel>
<AlertDialogAction onClick={continueOnboarding}>
Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog> */}
</>
);
};
/**
* Helper function to check if the user has completed onboarding
* @returns {boolean} Whether onboarding has been completed
*/
export const hasCompletedOnboarding = (): boolean => {
return localStorage.getItem(ONBOARDING_COMPLETED_KEY) === 'true';
};
export default OnboardingDialog;

View File

@ -0,0 +1,109 @@
# Laconic Onboarding Flow
This directory contains the components that make up the onboarding flow for the Laconic platform. The onboarding process guides users through connecting their GitHub repository, configuring their deployment settings, and deploying their application.
## 📝 Figma Design Reference
The design for this component is available at:
[Laconic Design - Onboarding Flow](https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=571-3500&m=dev)
## 🏗️ Component Structure
The onboarding flow is structured as a multi-step wizard with three main steps:
1. **Connect** - Allows users to connect to GitHub and select a repository
2. **Configure** - Lets users configure deployment settings and environment variables
3. **Deploy** - Finalizes the setup and deploys the application
### Directory Structure
```
onboarding-flow/
├── README.md # This file
├── OPTIMIZATION.md # Optimization recommendations
├── DOCUMENTATION_TEMPLATE.md # Documentation templates
├── index.ts # Main exports
├── types.ts # Shared types
├── store.ts # Zustand store for state management
├── Onboarding.tsx # Main component that orchestrates the flow
├── common/ # Shared components
│ ├── index.ts # Exports for common components
│ ├── background-svg.tsx # Background visual element
│ ├── laconic-icon-lettering.tsx # Laconic branding component
│ ├── onboarding-container.tsx # Container layout for the onboarding UI
│ ├── step-header.tsx # Header for each step
│ └── step-navigation.tsx # Navigation controls (prev/next buttons)
├── sidebar/ # Sidebar components
│ ├── index.ts # Exports for sidebar components
│ └── sidebar-nav.tsx # Sidebar showing all steps and progress
├── connect-step/ # Components for the Connect step
│ ├── index.ts # Exports for Connect step
│ ├── connect-step.tsx # Main component for the Connect step
│ ├── connect-button.tsx # GitHub connection button
│ ├── connect-initial.tsx # Initial Connect screen
│ ├── connect-deploy-first-app.tsx # First app deployment guidance
│ ├── repository-list.tsx # List of GitHub repositories
│ └── template-list.tsx # List of available templates
├── configure-step/ # Components for the Configure step
│ ├── index.ts # Exports for Configure step
│ └── configure-step.tsx # Main component for the Configure step
└── deploy-step/ # Components for the Deploy step
├── index.ts # Exports for Deploy step
└── deploy-step.tsx # Main component for the Deploy step
```
## 🔄 State Management
The onboarding flow uses Zustand for state management. The store is defined in `store.ts` and exposed through the `useOnboarding` hook.
## ✅ Completed Items
- ✅ Basic component structure following the Figma design
- ✅ Step navigation with progress indicators
- ✅ Sidebar navigation showing all steps
- ✅ Connect step UI with repository and template selection
- ✅ Configure step with deployment type selection
- ✅ Deploy step with deployment status
- ✅ Organized directory structure
- ✅ Consistent file naming conventions
- ✅ Centralized type definitions
- ✅ Main documentation and README
## 🚧 To-Do Items
- [ ] Complete JSDoc documentation for all components
- [ ] Implement actual GitHub API integration for repository fetching
- [ ] Add form validation for configuration inputs
- [ ] Implement actual deployment functionality
- [ ] Add error handling for API requests
- [ ] Add loading states for asynchronous operations
- [ ] Implement responsive design for mobile devices
- [ ] Add unit tests for all components
- [ ] Add Storybook stories for component documentation
- [ ] Implement animations for step transitions
## 📋 Usage Guidelines
To use the onboarding flow in your application:
```tsx
import { Onboarding } from '@/components/onboarding-flow';
export default function OnboardingPage() {
return <Onboarding />;
}
```
The onboarding flow is self-contained and manages its own state through the Zustand store.
## 🧩 Component Hierarchy
```
Onboarding
├── OnboardingContainer
│ ├── SidebarNav
│ └── Main Content
│ ├── StepHeader
│ ├── Step Content (ConnectStep | ConfigureStep | DeployStep)
│ └── StepNavigation
```

View File

@ -0,0 +1,13 @@
export function BackgroundSVG() {
return (
<svg width="268" height="270" viewBox="0 0 268 270" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M22.272 155.661C56.6042 121.33 77.8555 73.9178 77.8459 21.5708C77.8586 14.4654 77.4662 7.43923 76.6865 0.5L-27.5 0.509485L-27.4968 200.691C-27.5063 224.957 -18.2546 249.229 0.250641 267.733C18.7573 286.238 43.0469 295.501 67.3161 295.49L267.5 295.5L267.493 191.299C260.569 190.536 253.542 190.141 246.421 190.139C194.088 190.147 146.674 211.395 112.341 245.726C87.3587 270.083 47.3035 270.087 22.6295 245.413C-2.02868 220.754 -2.03989 180.687 22.272 155.661ZM245.89 22.1623C217.117 -6.60845 170.373 -6.61953 141.591 22.1623C112.809 50.9426 112.82 97.687 141.591 126.456C170.379 155.242 217.108 155.238 245.89 126.456C274.671 97.6759 274.677 50.9474 245.89 22.1623Z"
fill="#27272A"
/>
</svg>
)
}

View File

@ -0,0 +1,13 @@
/**
* Common components for the onboarding flow
*
* This module exports shared components used across the onboarding flow.
*
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=571-3500&m=dev
*/
export { BackgroundSVG } from './background-svg';
export { LaconicIconLettering } from './laconic-icon-lettering';
export { OnboardingContainer } from './onboarding-container';
export { StepHeader } from './step-header';
export { StepNavigation } from './step-navigation';

View File

@ -0,0 +1,42 @@
export function LaconicIconLettering() {
return (
<svg width="115" height="20" viewBox="0 0 115 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_498_4195)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.37388 10.5194C5.70149 8.19185 7.14225 4.97748 7.1416 1.42853C7.14246 0.94681 7.11586 0.470456 7.063 0L-0.000488281 0.000643078L-0.000273922 13.5723C-0.000917354 15.2174 0.62632 16.863 1.88091 18.1175C3.1356 19.3721 4.78235 20.0001 6.42772 19.9993L19.9995 20L19.999 12.9355C19.5296 12.8838 19.0532 12.857 18.5704 12.8569C15.0224 12.8574 11.8079 14.298 9.48026 16.6255C7.78654 18.2768 5.07093 18.2771 3.39812 16.6043C1.72638 14.9325 1.72562 12.2161 3.37388 10.5194ZM18.5344 1.46863C16.5837 -0.481929 13.4146 -0.48268 11.4633 1.46863C9.512 3.41984 9.51276 6.58895 11.4633 8.53941C13.415 10.491 16.5831 10.4907 18.5344 8.53941C20.4857 6.5882 20.4861 3.42016 18.5344 1.46863Z"
fill="currentColor"
/>
<path d="M31.4741 18.5838H39.2552V16.3302H34.075V1.41351H31.4741V18.5838Z" fill="currentColor" />
<path
d="M49.8108 1.41351H45.4976L40.9893 18.5838H43.6769L44.8039 14.2913H50.3744L51.5014 18.5838H54.3191L49.8108 1.41351ZM45.3458 12.145L47.6 3.2593H47.6866L49.8541 12.145H45.3458Z"
fill="currentColor"
/>
<path
d="M62.9292 8.06885H65.9636C65.9636 3.17534 64.3814 1.07196 60.6968 1.07196C56.817 1.07196 55.1479 3.73341 55.1479 9.97909C55.1479 16.2462 56.817 18.9291 60.6968 18.9291C64.3814 18.9291 65.9636 16.8901 65.9853 12.1468H62.9509C62.9293 15.8599 62.4741 16.7828 60.6968 16.7828C58.6594 16.7828 58.1607 15.4307 58.1824 9.97909C58.1824 4.54896 58.681 3.19678 60.6968 3.21823C62.4741 3.21823 62.9292 4.18413 62.9292 8.06885Z"
fill="currentColor"
/>
<path
d="M73.7781 1.07209C77.7229 1.09364 79.4135 3.77643 79.4135 10.0007C79.4135 16.2249 77.7229 18.9078 73.7781 18.9292C69.8117 18.9507 68.1211 16.2678 68.1211 10.0007C68.1211 3.73354 69.8117 1.05064 73.7781 1.07209ZM71.1555 10.0007C71.1555 15.4308 71.6757 16.783 73.7781 16.783C75.8589 16.783 76.3791 15.4308 76.3791 10.0007C76.3791 4.54909 75.8589 3.19691 73.7781 3.21847C71.6757 3.23992 71.1555 4.59209 71.1555 10.0007Z"
fill="currentColor"
/>
<path
d="M85.0819 18.5624L82.481 18.5838V1.41351H87.0544L91.3243 15.4073H91.3676V1.41351H93.968V18.5838H89.677L85.1254 3.51689H85.0819V18.5624Z"
fill="currentColor"
/>
<path d="M100.468 1.41351H97.8677V18.5838H100.468V1.41351Z" fill="currentColor" />
<path
d="M111.139 8.06885H114.174C114.174 3.17534 112.591 1.07196 108.906 1.07196C105.028 1.07196 103.358 3.73341 103.358 9.97909C103.358 16.2462 105.028 18.9291 108.906 18.9291C112.591 18.9291 114.174 16.8901 114.195 12.1468H111.161C111.139 15.8599 110.684 16.7828 108.906 16.7828C106.869 16.7828 106.371 15.4307 106.393 9.97909C106.393 4.54896 106.891 3.19678 108.906 3.21823C110.684 3.21823 111.139 4.18413 111.139 8.06885Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_498_4195">
<rect width="115" height="20" fill="white" />
</clipPath>
</defs>
</svg>
)
}

View File

@ -0,0 +1,34 @@
import type React from "react"
/**
* Props for the OnboardingContainer component
* @interface OnboardingContainerProps
* @property {React.ReactNode} children - The content to be rendered inside the container
*/
interface OnboardingContainerProps {
children: React.ReactNode
}
/**
* A container component that provides the layout structure for the onboarding flow
*
* @component
* @example
* ```tsx
* <OnboardingContainer>
* <SidebarNav currentStep={currentStep} />
* <MainContent />
* </OnboardingContainer>
* ```
*
* @param {OnboardingContainerProps} props - Component props
* @returns {JSX.Element} A container with consistent sizing and layout
*/
export function OnboardingContainer({ children }: OnboardingContainerProps) {
return (
<div className="min-h-screen w-full bg-background flex items-center justify-center p-8 relative overflow-hidden">
<div className="flex gap-6 w-full max-w-[1200px] min-h-[700px] h-full relative z-10 overflow-hidden">{children}</div>
</div>
)
}

View File

@ -0,0 +1,46 @@
import type React from "react"
/**
* Props for the StepHeader component
* @interface StepHeaderProps
* @property {React.ReactNode} icon - The icon to display next to the header
* @property {string} title - The title of the current step
* @property {string} description - A brief description of the current step
*/
interface StepHeaderProps {
icon: React.ReactNode
title: string
description: string
}
/**
* Displays the header for the current onboarding step
* Used at the top of each step's content area
*
* @component
* @example
* ```tsx
* <StepHeader
* icon={<GitPullRequest />}
* title="Connect"
* description="Connect and import a GitHub repository"
* />
* ```
*
* @param {StepHeaderProps} props - Component props
* @returns {JSX.Element} A header with icon, title, and description
*/
export function StepHeader({ icon, title, description }: StepHeaderProps) {
return (
<div className="flex items-center gap-3.5 mb-8">
<div className="flex items-center justify-center w-11 h-11 bg-foreground rounded-lg p-2.5">
<div className="text-accent">{icon}</div>
</div>
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-semibold text-foreground">{title}</h1>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
</div>
)
}

View File

@ -0,0 +1,80 @@
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Step } from '../types';
/**
* Props for the StepNavigation component
* @interface StepNavigationProps
* @property {Step} currentStep - The current active step in the onboarding flow
* @property {() => void} [onPrevious] - Callback function for the previous button
* @property {() => void} [onNext] - Callback function for the next button
* @property {string} [nextLabel] - Custom label for the next button
*/
interface StepNavigationProps {
currentStep: Step;
onPrevious?: () => void;
onNext?: () => void;
nextLabel?: string;
}
/** Order of steps in the onboarding flow */
const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy'];
/**
* Navigation component for moving between onboarding steps
* Displays progress indicators and previous/next buttons
*
* @component
* @example
* ```tsx
* <StepNavigation
* currentStep="connect"
* onPrevious={handlePrevious}
* onNext={handleNext}
* nextLabel="Continue"
* />
* ```
*
* @param {StepNavigationProps} props - Component props
* @returns {JSX.Element} Navigation controls with progress indicators
*/
export function StepNavigation({
currentStep,
onPrevious,
onNext,
nextLabel = 'Next',
}: StepNavigationProps) {
const currentStepIndex = STEP_ORDER.indexOf(currentStep);
const totalSteps = STEP_ORDER.length;
return (
<div className="flex items-center justify-between mt-8">
<Button
variant="ghost"
onClick={onPrevious}
disabled={currentStepIndex === 0}
className={cn(
'text-foreground hover:text-foreground/80',
currentStepIndex === 0 &&
'text-muted-foreground hover:text-muted-foreground',
)}
>
Previous
</Button>
<div className="flex gap-2">
{STEP_ORDER.map((step, index) => (
<div
key={step}
className={cn(
'h-1 w-8 rounded-full transition-colors',
index === currentStepIndex ? 'bg-zinc-800' : 'bg-zinc-300',
)}
/>
))}
</div>
<Button onClick={onNext} disabled={currentStepIndex === totalSteps - 1}>
{currentStepIndex === totalSteps - 1 ? 'Deploy' : nextLabel}
</Button>
</div>
);
}

View File

@ -0,0 +1,69 @@
import { useOnboarding } from "@/components/onboarding-flow/store"
import Configure from "@/components/projects/create/Configure"
import { FileCog } from "lucide-react"
import { useState } from "react"
/**
* Second step in the onboarding flow
* Handles deployment configuration and environment setup
*
* Features:
* - Deployment type selection (auction/LRN)
* - Environment variable configuration
* - Account selection
*
* @component
*/
export function ConfigureStep() {
const { formData, setFormData } = useOnboarding()
const [activeTab, setActiveTab] = useState<"create-auction" | "deployer-lrn">("create-auction")
const [environments, setEnvironments] = useState({
production: false,
preview: false,
development: false,
})
// const handleEnvironmentChange = (env: keyof typeof environments) => {
// setEnvironments((prev) => ({
// ...prev,
// [env]: !prev[env],
// }))
// setFormData({
// environmentVars: {
// ...formData.environmentVars,
// [env]: !environments[env],
// },
// })
// }
return (
<div className="w-full">
<div className="max-w-2xl mx-auto space-y-8">
<div className="flex flex-col items-center justify-center w-full max-w-[445px] mx-auto">
<div className="w-full flex flex-col items-center gap-6">
{/* Header section with icon and description */}
<div className="flex flex-col items-center gap-1">
<FileCog className="w-16 h-16 text-foreground" />
<div className="flex flex-col items-center gap-1">
<h2 className="text-2xl font-bold text-foreground">Configure</h2>
<p className="text-base text-muted-foreground text-center">
Set the deployer LRN for a single deployment or by creating a deployer auction for multiple deployments
</p>
</div>
</div>
{/* Content sections will be placed here:
1. Deployment type tabs (auction/LRN)
2. Configuration forms
3. Environment variables
4. Account selection
...content here/ */}
<Configure/>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,9 @@
/**
* Configure step components for the onboarding flow
*
* This module exports components related to the Configure step of the onboarding flow.
*
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=571-3500&m=dev
*/
export { ConfigureStep } from './configure-step';

View File

@ -0,0 +1,52 @@
import { Button, ButtonProps } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Github } from 'lucide-react';
import * as React from 'react';
interface ConnectButtonProps extends Omit<ButtonProps, 'leftIcon'> {
/**
* Optional override for the icon
*/
icon?: React.ReactNode;
/**
* Optional label for the button
*/
label?: string;
}
/**
* A specialized button for the Connect step based on Figma design
* Used for connecting to services like GitHub
*
* Figma Design Reference:
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=498-3285&m=dev
*/
export function ConnectButton({
className,
icon = <Github />,
label = 'Connect to GitHub',
children = label,
variant = 'outline',
size = 'lg',
...props
}: ConnectButtonProps) {
return (
<Button
className={cn(
'font-medium rounded-md transition-all duration-200',
'bg-white text-foreground',
'focus:ring-2 focus:ring-primary/25 focus:ring-offset-2',
'border border-zinc-200',
'py-2 px-4 h-10',
'flex items-center justify-center gap-2',
className,
)}
variant={variant}
size={size}
leftIcon={icon}
{...props}
>
{children}
</Button>
);
}

View File

@ -0,0 +1,39 @@
/**
* This component is used to connect the first account to the user's GitHub account.
* Should only show if not connected and have no projects
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=498-3262&m=dev
*/
import { Shapes } from 'lucide-react';
import { ConnectButton } from './connect-button';
export default function ConnectDeployFirstApp() {
return (
<div className="flex flex-col items-center justify-center gap-6 w-full max-w-[573px]">
<div className="flex flex-col items-center justify-center gap-6 px-16 w-full">
<div className="flex flex-col items-center gap-6 w-full max-w-[445px]">
<div className="w-16 h-16">
<Shapes className="w-full h-full text-foreground" />
</div>
<div className="flex flex-col items-center gap-1">
<h2 className="text-2xl font-bold text-foreground text-center leading-8">
Deploy your first app
</h2>
<p className="text-base text-muted-foreground text-center leading-6">
Once connected, you can import a repository from your account or
start with one of our templates.
</p>
</div>
<ConnectButton />
</div>
</div>
<div className="flex items-center gap-2.5 w-[276px]">
<div className="flex-1 h-1.5 rounded-full bg-primary" />
<div className="flex-1 h-1.5 rounded-full bg-muted-foreground/30" />
<div className="flex-1 h-1.5 rounded-full bg-muted-foreground/30" />
</div>
</div>
);
}

View File

@ -0,0 +1,33 @@
import { Button } from "@/components/ui/button"
import { GitHubLogoIcon } from "@radix-ui/react-icons"
interface ConnectInitialProps {
onConnect: () => void
}
export function ConnectInitial({ onConnect }: ConnectInitialProps) {
return (
<div className="flex flex-col items-center justify-center max-w-md mx-auto text-center">
<div className="mb-6">
// TODO: use lucide icon gitpullrequest in place of svg
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="24" cy="12" r="4" fill="currentColor" />
<circle cx="12" cy="36" r="4" fill="currentColor" />
<circle cx="36" cy="36" r="4" fill="currentColor" />
<line x1="24" y1="16" x2="14" y2="33" stroke="currentColor" strokeWidth="2" />
<line x1="24" y1="16" x2="34" y2="33" stroke="currentColor" strokeWidth="2" />
<line x1="14" y1="36" x2="34" y2="36" stroke="currentColor" strokeWidth="2" />
</svg>
</div>
<h2 className="text-xl font-semibold mb-3 text-foreground">Deploy your first app</h2>
<p className="text-muted-foreground mb-6">
Once connected, you can import a repository from your account or start with one of our templates.
</p>
<Button onClick={onConnect} className="gap-2">
<GitHubLogoIcon className="h-4 w-4" />
Connect to GitHub
</Button>
</div>
)
}

View File

@ -0,0 +1,61 @@
'use client';
import { useOnboarding } from '@/components/onboarding-flow/store';
import ConnectAccount from '@/components/projects/create/ConnectAccount';
import { useState } from 'react';
import { ConnectButton } from './connect-button';
type ConnectState = 'initial' | 'repository-select' | 'template-select';
/**
* First step in the onboarding flow
* Handles GitHub connection and repository selection
*
* States:
* - initial: Shows GitHub connect button
* - repository-select: Shows list of repositories
* - template-select: Shows available templates
*
* @component
*/
export function ConnectStep() {
const [connectState, setConnectState] = useState<ConnectState>('initial');
const [projectName, setProjectName] = useState('');
const { setFormData, nextStep } = useOnboarding();
const handleConnect = () => {
setConnectState('repository-select');
};
const handleRepositorySelect = (repo: { name: string }) => {
setFormData({ repoName: repo.name });
nextStep();
};
const handleTemplateSelect = (template: { id: string; name: string }) => {
setFormData({
repoName: projectName,
framework: template.id,
});
nextStep();
};
return (
<div className="max-w-2xl w-full">
{/* <ConnectAccountTabPanel />\ */}
{connectState === 'initial' ? (
<div className="flex flex-col items-center justify-center gap-6 p-8">
<h2 className="text-2xl font-semibold text-center">
Connect to GitHub
</h2>
<p className="text-center text-muted-foreground">
Connect your GitHub account to get started
</p>
<ConnectButton onClick={handleConnect} />
</div>
) : (
<ConnectAccount onAuth={() => {}} />
)}
</div>
);
}

View File

@ -0,0 +1,14 @@
/**
* Connect step components for the onboarding flow
*
* This module exports components related to the Connect step of the onboarding flow.
*
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=571-3500&m=dev
*/
export * from './connect-button';
export * from './connect-deploy-first-app';
export * from './connect-initial';
export * from './connect-step';
export * from './repository-list';
export * from './template-list';

View File

@ -0,0 +1,45 @@
import { Button } from '@/components/ui/button';
import { GitHubLogoIcon } from '@radix-ui/react-icons';
interface Repository {
name: string;
updatedAt: string;
}
interface RepositoryListProps {
repositories: Repository[];
onSelect: (repo: Repository) => void;
}
interface RepoCardProps {
repo: Repository;
onClick: () => void;
}
function RepoCard({ repo, onClick }: RepoCardProps) {
return (
<Button
onClick={onClick}
variant="ghost"
className="w-full flex items-center gap-3 p-3 hover:bg-accent hover:text-accent-foreground text-left"
aria-label={`Select repository ${repo.name}`}
>
<GitHubLogoIcon className="h-4 w-4 text-muted-foreground" />
<span className="flex-1 text-sm">{repo.name}</span>
<span className="text-xs text-muted-foreground">{repo.updatedAt}</span>
</Button>
);
}
export function RepositoryList({
repositories,
onSelect,
}: RepositoryListProps) {
return (
<div className="space-y-2">
{repositories.map((repo) => (
<RepoCard key={repo.name} repo={repo} onClick={() => onSelect(repo)} />
))}
</div>
);
}

View File

@ -0,0 +1,51 @@
import type React from "react"
import { Input } from "@/components/ui/input"
interface Template {
id: string
name: string
description: string
icon: React.ReactNode
}
interface TemplateListProps {
templates: Template[]
onSelect: (template: Template) => void
projectName: string
onProjectNameChange: (name: string) => void
}
export function TemplateList({ templates, onSelect, projectName, onProjectNameChange }: TemplateListProps) {
return (
<div className="space-y-6">
<div className="space-y-2">
<label htmlFor="project-name" className="text-sm font-medium text-foreground">
Project Name
</label>
<Input
id="project-name"
value={projectName}
onChange={(e) => onProjectNameChange(e.target.value)}
placeholder="new-repository-name"
className="bg-background"
/>
</div>
<div className="space-y-2">
{templates.map((template) => (
<button
key={template.id}
onClick={() => onSelect(template)}
className="w-full flex items-center gap-3 p-3 rounded-lg hover:bg-accent hover:text-accent-foreground transition-colors text-left"
>
<div className="h-8 w-8 rounded bg-muted flex items-center justify-center">{template.icon}</div>
<div>
<div className="text-sm font-medium">{template.name}</div>
<div className="text-xs text-muted-foreground">{template.description}</div>
</div>
</button>
))}
</div>
</div>
)
}

View File

@ -0,0 +1,53 @@
"use client"
import { useOnboarding } from "@/components/onboarding-flow/store"
import Deploy from "@/components/projects/create/Deploy"
/**
* Final step in the onboarding flow
* Displays deployment summary and triggers deployment
*
* Features:
* - Configuration summary
* - Repository display
* - Deploy action
*
* @component
*/
export function DeployStep() {
const { formData } = useOnboarding()
const handleDeploy = () => {
// TODO: Implement actual deployment logic
console.log("Deploying with configuration:", formData)
}
return (
<div className="w-full">
<div className="max-w-2xl mx-auto space-y-8">
<div className="flex flex-col items-center justify-center w-full max-w-[445px] mx-auto">
<div className="w-full flex flex-col items-center gap-6">
{/* Header section */}
<div className="flex flex-col items-center gap-1">
<div className="flex flex-col items-center gap-1">
<h2 className="text-2xl font-bold text-foreground">Deploy</h2>
<p className="text-base text-muted-foreground text-center">
Your deployment is configured and ready to go!
</p>
</div>
</div>
Content sections will be placed here:
1. Repository info card
2. Configuration summary
3. Deploy button
{/* ...content here */}
<Deploy/>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,9 @@
/**
* Deploy step components for the onboarding flow
*
* This module exports components related to the Deploy step of the onboarding flow.
*
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=571-3500&m=dev
*/
export { DeployStep } from './deploy-step';

View File

@ -0,0 +1,30 @@
/**
* Onboarding Flow
*
* This module exports all components related to the onboarding flow.
* The onboarding process guides users through connecting their GitHub repository,
* configuring their deployment settings, and deploying their application.
*
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=571-3500&m=dev
*/
// Main component
export { default as Onboarding } from './Onboarding';
export { default as OnboardingDialog, hasCompletedOnboarding } from './OnboardingDialog';
// Step components
export { ConfigureStep } from './configure-step';
export { ConnectStep } from './connect-step';
export { DeployStep } from './deploy-step';
// Common components
export * from './common';
// Sidebar components
export * from './sidebar';
// Store and hooks
export { useOnboarding } from './store';
// Types
export * from './types';

View File

@ -0,0 +1,9 @@
/**
* Sidebar components for the onboarding flow
*
* This module exports sidebar navigation components used in the onboarding flow.
*
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=571-3500&m=dev
*/
export { SidebarNav } from './sidebar-nav';

View File

@ -0,0 +1,138 @@
/**
* @component SidebarNav
* @description Sidebar navigation component showing all steps and their status
*
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=571-3500&m=dev
*/
import {
BackgroundSVG,
LaconicIconLettering,
} from '@/components/onboarding-flow/common';
import { type Step } from '@/components/onboarding-flow/types';
import { Separator } from '@/components/ui/separator';
import { cn } from '@/lib/utils';
import { FileCog, GitPullRequest, SquareArrowOutDownRight } from 'lucide-react';
import type React from 'react';
/**
* Props for the StepItem subcomponent
* @interface StepItemProps
* @property {React.ReactNode} icon - Icon representing the step
* @property {string} title - Step title
* @property {string} description - Step description
* @property {boolean} isActive - Whether this step is currently active
*/
interface StepItemProps {
icon: React.ReactNode;
title: string;
description: string;
isActive: boolean;
}
/**
* Individual step item in the sidebar navigation
* @component
* @param {StepItemProps} props - Component props
*/
const StepItem = ({ icon, title, description, isActive }: StepItemProps) => (
<div className="flex items-center gap-3.5">
<div
className={cn(
'flex items-center justify-center w-11 h-11 rounded-lg p-2.5 transition-colors',
isActive ? 'bg-foreground' : 'bg-primary-foreground border',
)}
>
<div
className={cn(
'w-6 h-6 stroke-2 transition-colors',
isActive ? 'text-accent' : 'text-muted-foreground',
)}
>
{icon}
</div>
</div>
<div className="flex flex-col gap-1">
<h3
className={cn(
'text-lg font-semibold leading-7 transition-colors',
isActive ? 'text-foreground' : 'text-muted-foreground',
)}
>
{title}
</h3>
<p
className={cn(
'text-sm leading-5 transition-colors',
isActive ? 'text-foreground' : 'text-muted-foreground',
)}
>
{description}
</p>
</div>
</div>
);
/**
* Props for the SidebarNav component
* @interface SidebarNavProps
* @property {Step} currentStep - Currently active step in the onboarding flow
*/
interface SidebarNavProps {
currentStep: Step;
}
/**
* Sidebar navigation component showing all steps and their status
*
* @component
* @example
* ```tsx
* <SidebarNav currentStep="connect" />
* ```
*
* Interacts with:
* - useOnboarding store for step state
* - StepItem subcomponent for individual step display
*
* @param {SidebarNavProps} props - Component props
* @returns {JSX.Element} Sidebar with step navigation
*/
export function SidebarNav({ currentStep }: SidebarNavProps) {
return (
<div className="w-[379px] p-6 bg-primary-foreground rounded-lg shadow-[0_1px_2px_0_rgba(0,0,0,0.06),0_1px_3px_0_rgba(0,0,0,0.1)] relative overflow-hidden">
<div className="h-5 mb-6 text-zinc-800 dark:text-white">
<LaconicIconLettering />
</div>
<Separator className="mb-6" />
<div className="space-y-6">
<StepItem
icon={<GitPullRequest />}
title="Connect"
description="Connect and import a GitHub repo"
isActive={currentStep === 'connect'}
/>
<StepItem
icon={<FileCog />}
title="Configure"
description="Define the deployment type"
isActive={currentStep === 'configure'}
/>
<StepItem
icon={<SquareArrowOutDownRight />}
title="Deploy"
description="Review and confirm deployment"
isActive={currentStep === 'deploy'}
/>
</div>
<div className="absolute bottom-0 left-0">
<BackgroundSVG />
</div>
</div>
);
}

View File

@ -0,0 +1,68 @@
/**
* Zustand store for managing onboarding state
*
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=571-3500&m=dev
*/
import { create } from 'zustand';
import { type OnboardingFormData, type Step } from './types';
/**
* State management for the onboarding flow
* @interface OnboardingState
* @property {Step} currentStep - Current active step
* @property {OnboardingFormData} formData - Collected form data
* @property {(step: Step) => void} setCurrentStep - Updates the current step
* @property {(data: Partial<OnboardingFormData>) => void} setFormData - Updates form data
* @property {() => void} nextStep - Moves to the next step
* @property {() => void} previousStep - Moves to the previous step
*/
interface OnboardingState {
currentStep: Step;
formData: OnboardingFormData;
setCurrentStep: (step: Step) => void;
setFormData: (data: Partial<OnboardingFormData>) => void;
nextStep: () => void;
previousStep: () => void;
}
/** Order of steps in the onboarding flow */
const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy'];
/**
* Zustand store for managing onboarding state
* Used across all onboarding components to maintain flow state
*
* @example
* ```tsx
* const { currentStep, formData, nextStep } = useOnboarding()
* ```
*/
export const useOnboarding = create<OnboardingState>((set) => ({
currentStep: 'connect',
formData: {
projectName: '',
repoName: '',
repoDescription: '',
framework: '',
access: 'public',
organizationSlug: '',
},
setCurrentStep: (step) => set({ currentStep: step }),
setFormData: (data) =>
set((state) => ({
formData: { ...state.formData, ...data },
})),
nextStep: () =>
set((state) => {
const currentIndex = STEP_ORDER.indexOf(state.currentStep);
const nextStep = STEP_ORDER[currentIndex + 1];
return nextStep ? { currentStep: nextStep } : state;
}),
previousStep: () =>
set((state) => {
const currentIndex = STEP_ORDER.indexOf(state.currentStep);
const previousStep = STEP_ORDER[currentIndex - 1];
return previousStep ? { currentStep: previousStep } : state;
}),
}));

View File

@ -0,0 +1,69 @@
import { create } from 'zustand';
import { OnboardingFormData, Step } from './types';
// Define the state for the onboarding flow
export interface OnboardingState {
currentStep: Step;
setCurrentStep: (step: Step) => void;
nextStep: () => void;
previousStep: () => void;
formData: OnboardingFormData;
updateFormData: (data: Partial<OnboardingFormData>) => void;
resetOnboarding: () => void;
}
// Create the store with the initial state
export const useOnboarding = create<OnboardingState>((set) => {
// The steps in order
const STEPS: Step[] = ['connect', 'configure', 'deploy'];
// Initial form data
const initialFormData: OnboardingFormData = {
projectName: '',
repoName: '',
repoDescription: '',
framework: '',
access: 'public',
organizationSlug: '',
};
return {
// Current step state (start with the connect step)
currentStep: 'connect',
// Function to set the current step
setCurrentStep: (step) => set({ currentStep: step }),
// Function to move to the next step
nextStep: () => set((state) => {
const currentIndex = STEPS.indexOf(state.currentStep);
if (currentIndex < STEPS.length - 1) {
return { currentStep: STEPS[currentIndex + 1] };
}
return state;
}),
// Function to move to the previous step
previousStep: () => set((state) => {
const currentIndex = STEPS.indexOf(state.currentStep);
if (currentIndex > 0) {
return { currentStep: STEPS[currentIndex - 1] };
}
return state;
}),
// Form data state
formData: initialFormData,
// Function to update form data
updateFormData: (data) => set((state) => ({
formData: { ...state.formData, ...data }
})),
// Function to reset the onboarding state
resetOnboarding: () => set({
currentStep: 'connect',
formData: initialFormData
}),
};
});

View File

@ -0,0 +1,89 @@
/**
* Shared type definitions for the onboarding flow
*
* @see https://www.figma.com/design/cfMOy1RJasIu3QyzAMBFxB/Laconic?node-id=571-3500&m=dev
*/
/**
* Available steps in the onboarding flow
*/
export type Step = 'connect' | 'configure' | 'deploy';
/**
* Form data collected during the onboarding process
* @interface OnboardingFormData
* @property {string} projectName - Project name
* @property {string} repoName - Repository name
* @property {string} repoDescription - Repository description
* @property {string} framework - Framework used for the project
* @property {string} access - Access level of the repository
* @property {string} organizationSlug - Organization slug
*/
export interface OnboardingFormData {
projectName: string;
repoName: string;
repoDescription: string;
framework: string;
access: 'public' | 'private';
organizationSlug: string;
}
/**
* GitHub repository information
* @interface Repository
* @property {string} id - Unique identifier for the repository
* @property {string} name - Repository name
* @property {string} fullName - Full repository name including owner
* @property {string} [description] - Repository description
* @property {boolean} [isPrivate] - Whether the repository is private
* @property {string} [url] - Repository URL
*/
export interface Repository {
id: string;
name: string;
fullName: string;
description?: string;
isPrivate?: boolean;
url?: string;
}
/**
* Template information for new projects
* @interface Template
* @property {string} id - Unique identifier for the template
* @property {string} name - Template name
* @property {string} [description] - Template description
* @property {string} [thumbnail] - Template thumbnail URL
*/
export interface Template {
id: string;
name: string;
description?: string;
thumbnail?: string;
}
/**
* Deployment type information
* @interface DeploymentType
* @property {string} id - Unique identifier for the deployment type
* @property {string} name - Deployment type name
* @property {string} [description] - Deployment type description
*/
export interface DeploymentType {
id: string;
name: string;
description?: string;
}
/**
* Environment variable definition
* @interface EnvironmentVariable
* @property {string} key - Environment variable key
* @property {string} value - Environment variable value
* @property {boolean} [isSecret] - Whether the variable is a secret
*/
export interface EnvironmentVariable {
key: string;
value: string;
isSecret?: boolean;
}

View File

@ -0,0 +1,41 @@
import { create } from "zustand"
export type Step = "connect" | "configure" | "deploy"
interface OnboardingState {
currentStep: Step
formData: {
githubRepo?: string
deploymentType?: string
environmentVars?: Record<string, string>
}
setCurrentStep: (step: Step) => void
setFormData: (data: Partial<OnboardingState["formData"]>) => void
nextStep: () => void
previousStep: () => void
}
const STEP_ORDER: Step[] = ["connect", "configure", "deploy"]
export const useOnboarding = create<OnboardingState>((set) => ({
currentStep: "connect",
formData: {},
setCurrentStep: (step) => set({ currentStep: step }),
setFormData: (data) =>
set((state) => ({
formData: { ...state.formData, ...data },
})),
nextStep: () =>
set((state) => {
const currentIndex = STEP_ORDER.indexOf(state.currentStep)
const nextStep = STEP_ORDER[currentIndex + 1]
return nextStep ? { currentStep: nextStep } : state
}),
previousStep: () =>
set((state) => {
const currentIndex = STEP_ORDER.indexOf(state.currentStep)
const previousStep = STEP_ORDER[currentIndex - 1]
return previousStep ? { currentStep: previousStep } : state
}),
}))

View File

@ -1,6 +1,6 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
} from '@/components/shared/ConfirmDialog';
interface CancelDeploymentDialogProps extends ConfirmDialogProps {}
@ -18,7 +18,7 @@ export const CancelDeploymentDialog = ({
open={open}
confirmButtonTitle="Yes, cancel deployment"
handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }}
confirmButtonProps={{ variant: 'destructive' }}
>
<p className="text-sm text-elements-high-em tracking-[-0.006em]">
This will halt the deployment and you&apos;ll have to start the process

View File

@ -1,19 +1,18 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
} from '@/components/shared/ConfirmDialog';
import { Deployment, Domain } from 'gql-client';
import { Link } from 'react-router-dom';
import DeploymentDialogBodyCard from 'components/projects/project/deployments/DeploymentDialogBodyCard';
import { Button } from 'components/shared/Button';
import DeploymentDialogBodyCard from '@/components/projects/project/deployments/DeploymentDialogBodyCard';
import { TagProps } from '@/components/shared/Tag';
import { Button } from '@/components/ui';
import {
ChevronDoubleDownIcon,
LinkChainIcon,
} from 'components/shared/CustomIcon';
import { TagProps } from 'components/shared/Tag';
import {
ArrowRightCircleFilledIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
ArrowRightCircle,
ChevronDown,
Link as LinkIcon,
Loader2,
} from 'lucide-react';
interface ChangeStateToProductionDialogProps extends ConfirmDialogProps {
deployment: Deployment;
@ -51,13 +50,13 @@ export const ChangeStateToProductionDialog = ({
confirmButtonProps={{
disabled: isConfirmButtonLoading,
rightIcon: isConfirmButtonLoading ? (
<LoadingIcon className="animate-spin" />
<Loader2 className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon />
<ArrowRightCircle />
),
}}
>
<div className="flex flex-col gap-7">
<div className="gap-7 flex flex-col">
<div className="flex flex-col gap-3">
<p className="text-sm text-elements-high-em tracking-[-0.006em]">
Upon confirmation, this deployment will be changed to production.
@ -68,9 +67,9 @@ export const ChangeStateToProductionDialog = ({
/>
{newDeployment && (
<>
<div className="flex items-center justify-between w-full text-elements-info">
<div className="text-elements-info flex items-center justify-between w-full">
{Array.from({ length: 7 }).map((_, index) => (
<ChevronDoubleDownIcon key={index} />
<ChevronDown key={index} />
))}
</div>
<DeploymentDialogBodyCard
@ -87,15 +86,16 @@ export const ChangeStateToProductionDialog = ({
{domains.length > 0 &&
domains.map((value) => {
return (
<Button
as="a"
href={value.name}
leftIcon={<LinkChainIcon size={18} />}
variant="link"
key={value.id}
>
{value.name}
</Button>
<Link to={value.name}>
<Button
leftIcon={<LinkIcon size={18} />}
// variant="link"
key={value.id}
asChild
>
{value.name}
</Button>
</Link>
);
})}
</div>

View File

@ -1,10 +1,7 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
import {
ArrowRightCircleFilledIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
} from '@/components/shared/ConfirmDialog';
import { AlertTriangle } from 'lucide-react';
interface DeleteDeploymentDialogProps extends ConfirmDialogProps {
isConfirmButtonLoading?: boolean;
@ -30,16 +27,16 @@ export const DeleteDeploymentDialog = ({
}
handleConfirm={handleConfirm}
confirmButtonProps={{
variant: 'danger',
variant: 'destructive',
disabled: isConfirmButtonLoading,
rightIcon: isConfirmButtonLoading ? (
<LoadingIcon className="animate-spin" />
<AlertTriangle className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon />
<AlertTriangle />
),
}}
>
<p className="text-sm text-elements-high-em">
<p className="text-elements-high-em text-sm">
Once deleted, the deployment will not be accessible.
</p>
</ConfirmDialog>

View File

@ -1,6 +1,6 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
} from '@/components/shared/ConfirmDialog';
interface DeleteDomainDialogProps extends ConfirmDialogProps {
projectName: string;
@ -23,9 +23,9 @@ export const DeleteDomainDialog = ({
open={open}
confirmButtonTitle="Yes, delete domain"
handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }}
confirmButtonProps={{ variant: 'destructive' }}
>
<p className="text-sm text-elements-high-em">
<p className="text-elements-high-em text-sm">
Once deleted, the project{' '}
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
{projectName}

View File

@ -1,6 +1,6 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
} from '@/components/shared/ConfirmDialog';
interface DeleteVariableDialogProps extends ConfirmDialogProps {
variableKey: string;
@ -21,9 +21,9 @@ export const DeleteVariableDialog = ({
open={open}
confirmButtonTitle="Yes, confirm delete"
handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }}
confirmButtonProps={{ variant: 'destructive' }}
>
<p className="text-sm text-elements-mid-em">
<p className="text-elements-mid-em text-sm">
Are you sure you want to delete the variable{' '}
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
{variableKey}

View File

@ -1,6 +1,6 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
} from '@/components/shared/ConfirmDialog';
interface DeleteWebhookDialogProps extends ConfirmDialogProps {
webhookUrl: string;
@ -21,9 +21,9 @@ export const DeleteWebhookDialog = ({
open={open}
confirmButtonTitle="Yes, confirm delete"
handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }}
confirmButtonProps={{ variant: 'destructive' }}
>
<p className="text-sm text-elements-mid-em">
<p className="text-elements-mid-em text-sm">
Are you sure you want to delete{' '}
<span className="text-sm font-mono text-elements-high-em px-0.5">
{webhookUrl}

View File

@ -1,6 +1,6 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
} from '@/components/shared/ConfirmDialog';
interface DisconnectRepositoryDialogProps extends ConfirmDialogProps {}
@ -18,9 +18,9 @@ export const DisconnectRepositoryDialog = ({
open={open}
confirmButtonTitle="Yes, confirm disconnect"
handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }}
confirmButtonProps={{ variant: 'destructive' }}
>
<p className="text-sm text-elements-high-em">
<p className="text-elements-high-em text-sm">
Any data tied to your Git project may become misconfigured. Are you sure
you want to continue?
</p>

View File

@ -1,8 +1,8 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
} from '@/components/shared/ConfirmDialog';
import { formatAddress } from 'utils/format';
import { formatAddress } from '@/utils/format';
interface RemoveMemberDialogProps extends ConfirmDialogProps {
memberName: string;
@ -27,9 +27,9 @@ export const RemoveMemberDialog = ({
open={open}
confirmButtonTitle="Yes, remove member"
handleConfirm={handleConfirm}
confirmButtonProps={{ variant: 'danger' }}
confirmButtonProps={{ variant: 'destructive' }}
>
<p className="text-sm text-elements-high-em">
<p className="text-elements-high-em text-sm">
Once removed, {formatAddress(memberName)} ({formatAddress(ethAddress)}@
{emailDomain}) will not be able to access this project.
</p>

View File

@ -1,6 +1,6 @@
import ConfirmDialog, {
ConfirmDialogProps,
} from 'components/shared/ConfirmDialog';
} from '@/components/shared/ConfirmDialog';
interface TransferProjectDialogProps extends ConfirmDialogProps {
projectName: string;

View File

@ -1,3 +1,12 @@
/**
* @deprecated This theme file is deprecated and will be removed in a future version.
* The ProjectCard has been refactored to use component-based styling with shadcn/ui components.
* Please use the following components instead:
* - ProjectStatusDot for status indicators
* - ProjectDeploymentInfo for deployment information
* - ProjectCardActions for card actions
*/
import { VariantProps, tv } from 'tailwind-variants';
export const projectCardTheme = tv({
@ -73,20 +82,21 @@ export const projectCardTheme = tv({
'group-hover:bg-surface-card-hovered',
'dark:group-hover:bg-overlay2',
],
dot: ['h-2', 'w-2', 'rounded-full'],
},
variants: {
status: {
success: {
deploymentStatus: ['bg-emerald-500'],
dot: ['bg-emerald-500'],
},
'in-progress': {
deploymentStatus: ['bg-orange-400'],
dot: ['bg-orange-400'],
},
failure: {
deploymentStatus: ['bg-error'],
dot: ['bg-destructive'],
},
pending: {
deploymentStatus: ['bg-gray-500'],
dot: ['bg-muted'],
},
},
},

Some files were not shown because too many files have changed in this diff Show More