refactor: consolidate project documentation and routing strategy

This commit introduces a comprehensive documentation strategy for the project, focusing on:

- Centralizing routing configuration
- Adding detailed documentation for frontend architecture
- Creating standards for component documentation
- Implementing a feature building process template
- Removing legacy documentation files

Key changes include:
- Added routing strategy and implementation documents
- Created project-wide documentation standards
- Introduced new documentation structure in qwrk-docs
- Removed redundant README and documentation files
- Enhanced routing and layout documentation
This commit is contained in:
icld 2025-03-02 06:14:24 -08:00
parent 134d4ad316
commit e1c2a8ce2c
52 changed files with 1151 additions and 2633 deletions

View File

@ -1,165 +0,0 @@
# 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,39 +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.
## Development Guidelines
### Project Standards
We maintain a set of project-wide standards and conventions in the [standards](./standards)
directory. These standards help ensure consistency across the codebase and make it easier for
developers to collaborate.
Current standards:
- [Component Documentation Standards](./standards/COMPONENT_DOCUMENTATION.md) - Guidelines for
documenting components, hooks, and utilities
- [Feature Building Process](./standards/FEATURE_BUILDING.md) - Standardized approach to building
new features from design to implementation

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,10 @@
import ProjectSearchLayout from '@/layouts/ProjectSearch';
import ProjectsScreen from '@/pages/org-slug/ProjectsScreen';
import { ThemeProvider } from 'next-themes';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
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 BuyPrepaidService from './pages/BuyPrepaidService';
@ -16,60 +17,126 @@ import {
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: <ProjectsScreen />,
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 />,
errorElement: <div>Something went wrong!</div>,
},
{
path: '/onboarding',
element: <OnboardingPage />,
},
{
path: '/onboarding-demo',
element: <OnboardingDemoPage />,
},
]);
/**
* Main application component.
* Sets up routing, authentication, and theme provider.
* Sets up routing and error handling.
* @returns {JSX.Element} The rendered application.
*/
function App() {
@ -81,7 +148,7 @@ function App() {
credentials: 'include',
}).then((res) => {
const path = window.location.pathname;
const publicPaths = ['/login', '/onboarding', '/onboarding-demo'];
const publicPaths = ['/login', '/onboarding', '/onboarding-demo', '/'];
console.log(res);
if (res.status !== 200) {
@ -91,16 +158,14 @@ function App() {
}
} else {
if (path === '/login') {
window.location.pathname = '/';
window.location.pathname = '/deploy-tools';
}
}
});
}, []);
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<RouterProvider router={router} fallbackElement={<div>Loading...</div>} />
</ThemeProvider>
<RouterProvider router={router} fallbackElement={<LoadingOverlay />} />
);
}

View File

@ -11,6 +11,7 @@ import {
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';
@ -42,7 +43,8 @@ export function TopNavigation() {
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);
@ -59,10 +61,10 @@ export function TopNavigation() {
}, [client]);
useEffect(() => {
if (isGitHubConnected) {
if (isGitHubConnected && isReady) {
fetchDefaultOrganization();
}
}, [isGitHubConnected, fetchDefaultOrganization]);
}, [isGitHubConnected, fetchDefaultOrganization, isReady]);
const handleOnboardingClose = () => {
setShowOnboarding(false);
@ -84,10 +86,10 @@ export function TopNavigation() {
// 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);
};
@ -220,11 +222,19 @@ export function TopNavigation() {
<Button variant="ghost" asChild className="justify-start">
<Link to="/docs">Documentation</Link>
</Button>
<Button variant="ghost" className="justify-start" onClick={handleRunOnboarding}>
<Button
variant="ghost"
className="justify-start"
onClick={handleRunOnboarding}
>
Run Onboarding
</Button>
{isGitHubConnected && (
<Button variant="ghost" className="justify-start" onClick={handleCreateNew}>
<Button
variant="ghost"
className="justify-start"
onClick={handleCreateNew}
>
Create New
</Button>
)}

View File

@ -1,10 +1,9 @@
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
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";
import { OnboardingFormData, Step } from './types';
// Local storage keys
const ONBOARDING_COMPLETED_KEY = 'onboarding_completed';
@ -20,16 +19,16 @@ interface OnboardingDialogProps {
/**
* 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,
const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
trigger,
defaultOpen = false,
onClose
onClose,
}) => {
const onboardingStore = useOnboarding();
const { setCurrentStep, currentStep, formData } = onboardingStore;
@ -41,7 +40,8 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
// Check for force connect flag when component mounts
useEffect(() => {
const shouldForceConnect = localStorage.getItem(ONBOARDING_FORCE_CONNECT_KEY) === 'true';
const shouldForceConnect =
localStorage.getItem(ONBOARDING_FORCE_CONNECT_KEY) === 'true';
if (shouldForceConnect) {
setForceConnectStep(true);
// Clear the flag so it's only used once
@ -53,10 +53,10 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
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') {
@ -71,12 +71,25 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
}
};
// 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);
@ -91,18 +104,18 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
const initializeOnboarding = () => {
// Reset previous state
resetOnboardingState();
// If GitHub is connected AND we're not forcing the connect step,
// 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();
};
@ -114,7 +127,7 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
initializeOnboarding();
setShowContinueAlert(false);
};
// Continue from saved state and don't force the connect step
const continueOnboarding = () => {
// Reset the force flag since we're continuing
@ -128,7 +141,7 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
const state = {
currentStep,
formData,
forceConnectStep // Save this flag as part of the state
forceConnectStep, // Save this flag as part of the state
};
localStorage.setItem(ONBOARDING_STATE_KEY, JSON.stringify(state));
} catch (error) {
@ -142,14 +155,14 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
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') {
@ -179,45 +192,29 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
// Handle dialog close attempt
const handleOpenChange = (open: boolean) => {
if (!open && isOpen) {
// If closing and not on the last step, show warning
if (currentStep !== 'deploy') {
setShowExitWarning(true);
return; // Prevent closing until user confirms
// 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();
}
// If on the last step or user confirmed, close normally
completeClose();
} else {
setIsOpen(open);
}
};
// Complete the closing process
const completeClose = () => {
// Mark as completed when dialog is closed
localStorage.setItem(ONBOARDING_COMPLETED_KEY, 'true');
// Clear progress flag
localStorage.removeItem(ONBOARDING_PROGRESS_KEY);
localStorage.removeItem(ONBOARDING_STATE_KEY);
// Reset onboarding state for next time
resetOnboardingState();
// Close the dialog
setIsOpen(false);
if (onClose) {
onClose();
}
};
// Cancel closing
// Define the missing functions to handle dialog closing
const cancelClose = () => {
setShowExitWarning(false);
};
const completeClose = () => {
closeOnboarding();
setShowExitWarning(false);
};
return (
<>
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
@ -230,36 +227,42 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
</Dialog>
{/* Exit Warning Dialog */}
<AlertDialog open={showExitWarning} onOpenChange={setShowExitWarning}>
{/* <AlertDialog open={showExitWarning} onOpenChange={setShowExitWarning}>
<AlertDialogContent>
<AlertDialogHeader>
<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>
</AlertDialogHeader>
<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>
<AlertDialogAction onClick={completeClose}>
Exit Anyway
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</AlertDialog> */}
{/* Continue Progress Dialog */}
<AlertDialog open={showContinueAlert} onOpenChange={setShowContinueAlert}>
{/* <AlertDialog open={showContinueAlert} onOpenChange={setShowContinueAlert}>
<AlertDialogContent>
<AlertDialogHeader>
<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>
</AlertDialogHeader>
<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>
<AlertDialogCancel onClick={startFresh}>
Start Fresh
</AlertDialogCancel>
<AlertDialogAction onClick={continueOnboarding}>
Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</AlertDialog> */}
</>
);
};
@ -272,4 +275,4 @@ export const hasCompletedOnboarding = (): boolean => {
return localStorage.getItem(ONBOARDING_COMPLETED_KEY) === 'true';
};
export default OnboardingDialog;
export default OnboardingDialog;

View File

@ -1,4 +1,4 @@
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { GitHubLogoIcon } from '@radix-ui/react-icons';
interface Repository {
@ -18,15 +18,16 @@ interface RepoCardProps {
function RepoCard({ repo, onClick }: RepoCardProps) {
return (
<Card
// as="button"
<Button
onClick={onClick}
className="w-full flex items-center gap-3 p-3 rounded-lg hover:bg-accent hover:text-accent-foreground transition-colors text-left"
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>
</Card>
</Button>
);
}

View File

@ -1,88 +0,0 @@
# Onboarding Flow Component Architecture
## Core Layout Components
1. **AppLayout**
- Uses shadcn/ui `Sheet` for responsive sidebar
- Main content area with proper padding
2. **Sidebar**
- Uses shadcn/ui `Sidebar` component
- Contains logo and navigation steps
3. **NavigationStep**
- Modified shadcn/ui `NavigationMenuItem`
- Displays icon, step label, and active state
4. **ProgressIndicator**
- Custom component using shadcn/ui styling conventions
- Horizontal dots showing current flow progress
## Page Components
1. **ConnectView**
- Main component for initial GitHub connection screen
- Contains options to import or start from template
2. **RepositoryListView**
- Displays recent repositories with timestamps
- Uses shadcn/ui `ScrollArea` for scrollable list
3. **ConfigureView**
- Settings configuration screen with form elements
- Multiple sections for different configuration options
4. **DeployView**
- Final deployment screen with status information
- Confirmation elements and action buttons
## UI Components
1. **TemplateCard**
- Uses shadcn/ui `Card`, `CardHeader`, `CardContent`, `CardFooter`
- Displays template options with icons and descriptions
2. **ActionButton**
- Extends shadcn/ui `Button` with variants for primary/secondary actions
- Consistent styling across all screens
3. **RepositoryItem**
- Custom list item with GitHub repo information
- Uses Lucide icons and formatting for timestamps
4. **FormComponents**
- Uses shadcn/ui `Form`, `FormField`, `FormItem`, `FormLabel`, `FormControl`
- Includes `Input`, `Select`, `Checkbox`, etc.
5. **EnvironmentVariablesSection**
- Form section for environment variables
- Uses shadcn/ui `Input` and add button functionality
6. **DeploymentStatusIndicator**
- Shows deployment status with appropriate icon
- Uses shadcn/ui styling conventions
7. **NavigationControls**
- Contains Previous/Next buttons
- Uses shadcn/ui `Button` with appropriate variants
## Icons
- All icons from Lucide React library:
- `Git`, `Github`, `Settings`, `Box`, `Terminal`
- `Plus`, `Check`, `ChevronRight`, `ChevronLeft`
- `ArrowRight`, `ExternalLink`
## Form Elements
1. **ProjectForm**
- Uses shadcn/ui `Form` component
- Fields for project name, deployment type, etc.
2. **DropdownSelects**
- Uses shadcn/ui `Select`, `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem`
- For container URLs, deployment numbers, etc.
3. **CheckboxGroup**
- Group of shadcn/ui `Checkbox` components
- For environment type selection (Production, Preview, Development)

View File

@ -1,133 +0,0 @@
# Form Integration Plan for Onboarding Flow
## Overview
This document outlines how to integrate existing form components into the onboarding flow without modifying their functionality. The navigation state will be managed by Zustand as defined previously, while the form sections will maintain their current implementation.
## Key Form Sections to Integrate
### Connect Step Forms
1. **GitHub Connection Form**
- Location: Appears in the first screen
- Purpose: Authenticates with GitHub
- Integration point: `ConnectView.tsx`
- Implementation note: Import existing form as-is
2. **Repository Selection List**
- Location: Appears after GitHub connection
- Purpose: Displays repositories with timestamps for selection
- Integration point: `ConnectView.tsx` (conditional render after connection)
- Implementation note: Preserve existing event handlers
3. **Template Selection Grid**
- Location: Alternative to repository selection
- Purpose: Displays template options
- Integration point: `ConnectView.tsx` (conditional render)
- Implementation note: Maintain existing selection logic
### Configure Step Forms
1. **Deployment URL Configuration**
- Location: Top of configure screen
- Purpose: Sets deployment URL
- Integration point: `ConfigureView.tsx`
- Implementation note: Import without modifying validation
2. **Environment Variables Form**
- Location: Middle of configure screen
- Purpose: Configure environment settings
- Integration point: `ConfigureView.tsx`
- Implementation note: Preserve existing add/remove functionality
3. **Deployment Options Form**
- Location: Bottom of configure screen
- Purpose: Sets deployment constraints (max price, instances)
- Integration point: `ConfigureView.tsx`
- Implementation note: Keep all validation rules intact
4. **Environment Type Selection**
- Location: Configure screen
- Purpose: Select environments (Production/Preview/Development)
- Integration point: `ConfigureView.tsx`
- Implementation note: Maintain checkbox group behavior
### Deploy Step Forms
1. **Deployment Status Display**
- Location: Deploy screen
- Purpose: Shows deployment progress
- Integration point: `DeployView.tsx`
- Implementation note: Keep status monitoring logic
2. **Payment Details Form**
- Location: Final deploy screen
- Purpose: Collect payment information
- Integration point: `DeployView.tsx` (conditional render)
- Implementation note: Preserve all payment processing logic
## Integration Approach
1. **Wrapper Component Strategy**
```tsx
// Example approach for integrating existing forms
const ConfigureView: React.FC = () => {
const { markStepCompleted, goToNextStep, goToPreviousStep } = useNavigationStore()
// Completion handler that works with the existing form
const handleStepComplete = (formData: any) => {
// Form handled its own submission already
// Just update navigation state
markStepCompleted(OnboardingStep.CONFIGURE)
goToNextStep()
}
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">Configure</h1>
{/* Import existing form component without modifying it */}
<ExistingDeploymentUrlForm />
<ExistingEnvironmentVariablesForm />
<ExistingDeploymentOptionsForm />
{/* Navigation controls added around existing forms */}
<div className="flex justify-between">
<Button variant="outline" onClick={goToPreviousStep}>
Previous
</Button>
<Button onClick={handleStepComplete}>
Next
</Button>
</div>
</div>
)
}
```
2. **Event Coordination**
- Listen for existing form submission events
- Update navigation state after form events complete
- Don't modify form validation or submission logic
3. **Form Data Access**
- Access form data through existing mechanisms (refs, context, etc.)
- Don't introduce new state management for form data
## Implementation Guidelines
1. **DO NOT:**
- Change form validation logic
- Modify form submission handlers
- Alter form state management
- Change form UI components
2. **DO:**
- Wrap existing forms in the appropriate step components
- Add navigation controls outside the forms
- Listen for form completion events
- Update navigation state based on form events
## Data Flow
1. User interacts with existing form components
2. Forms validate and process data using their existing logic
3. After form processing completes, navigation state is updated
4. Navigation controls the flow between steps
This approach ensures that the existing form functionality remains unchanged while integrating with the new navigation flow.

View File

@ -1,124 +0,0 @@
# Onboarding Flow State Management
This document describes the state management approach for the onboarding flow using Zustand.
## Complete State Management
The full state management approach includes all aspects of the onboarding flow:
```typescript
// Define the steps of our onboarding flow
export enum OnboardingStep {
CONNECT = 'connect',
CONFIGURE = 'configure',
DEPLOY = 'deploy'
}
// Define the repository type
interface Repository {
id: string
name: string
lastUpdated: Date
}
// Define the template type
interface Template {
id: string
name: string
description: string
type: 'web' | 'api' | 'other'
}
// Define the deployment configuration type
interface DeploymentConfig {
deploymentUrl?: string
maxDeploys?: number
environmentTypes: {
production: boolean
preview: boolean
development: boolean
}
selectedContainerUrl?: string
}
// Define our store state
interface OnboardingState {
// Current step in the flow
currentStep: OnboardingStep
// Step completion status
completedSteps: {
[key in OnboardingStep]: boolean
}
// GitHub connection status
isConnected: boolean
// Selected method (import or template)
selectedMethod?: 'import' | 'template'
// Selected repository (if import method is chosen)
selectedRepository?: Repository
// Recent repositories (shown in the import list)
recentRepositories: Repository[]
// Selected template (if template method is chosen)
selectedTemplate?: Template
// Deployment configuration
deploymentConfig: DeploymentConfig
// Deployment status
deploymentStatus?: 'configuring' | 'ready' | 'deployed' | 'failed'
// Actions
setCurrentStep: (step: OnboardingStep) => void
markStepCompleted: (step: OnboardingStep) => void
connect: () => void
disconnect: () => void
selectMethod: (method: 'import' | 'template') => void
selectRepository: (repository: Repository) => void
selectTemplate: (template: Template) => void
updateDeploymentConfig: (config: Partial<DeploymentConfig>) => void
setDeploymentStatus: (status: 'configuring' | 'ready' | 'deployed' | 'failed') => void
resetOnboarding: () => void
goToNextStep: () => void
goToPreviousStep: () => void
}
```
## Navigation-Only State Management
For integration with existing form components, a navigation-only state management approach is recommended:
```typescript
// Define the steps of our onboarding flow
export enum OnboardingStep {
CONNECT = 'connect',
CONFIGURE = 'configure',
DEPLOY = 'deploy'
}
// Define our navigation state
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
}
```
The navigation-only approach allows for easier integration with existing form components and state management.

View File

@ -0,0 +1,167 @@
import axios from 'axios';
import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { generateNonce, SiweMessage } from 'siwe';
import { Box, Modal } from '@mui/material';
import { BASE_URL } from '../../utils/constants';
const axiosInstance = axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
withCredentials: true,
});
/**
* AutoSignInIFrameModal component that handles wallet authentication through an iframe.
* This component is responsible for:
* 1. Getting the wallet address
* 2. Creating a Sign-In With Ethereum message
* 3. Requesting signature from the wallet
* 4. Validating the signature with the backend
*
* TODO: Consider renaming this to WalletAuthModal in the future for better semantic clarity
*
* @returns {JSX.Element} A modal with an iframe for wallet authentication
*/
const AutoSignInIFrameModal = () => {
const navigate = useNavigate();
const [accountAddress, setAccountAddress] = useState();
// Handle sign-in response from the wallet iframe
useEffect(() => {
const handleSignInResponse = async (event: MessageEvent) => {
if (event.origin !== import.meta.env.VITE_WALLET_IFRAME_URL) return;
if (event.data.type === 'SIGN_IN_RESPONSE') {
try {
const { success } = (
await axiosInstance.post('/auth/validate', {
message: event.data.data.message,
signature: event.data.data.signature,
})
).data;
if (success === true) {
navigate('/');
}
} catch (error) {
console.error('Error signing in:', error);
}
}
};
window.addEventListener('message', handleSignInResponse);
return () => {
window.removeEventListener('message', handleSignInResponse);
};
}, [navigate]);
// Initiate auto sign-in when account address is available
useEffect(() => {
const initiateAutoSignIn = async () => {
if (!accountAddress) return;
const iframe = document.getElementById(
'walletAuthFrame',
) as HTMLIFrameElement;
if (!iframe.contentWindow) {
console.error('Iframe not found or not loaded');
return;
}
const message = new SiweMessage({
version: '1',
domain: window.location.host,
uri: window.location.origin,
chainId: 1,
address: accountAddress,
nonce: generateNonce(),
statement: 'Sign in With Ethereum.',
}).prepareMessage();
iframe.contentWindow.postMessage(
{
type: 'AUTO_SIGN_IN',
chainId: '1',
message,
},
import.meta.env.VITE_WALLET_IFRAME_URL,
);
};
initiateAutoSignIn();
}, [accountAddress]);
// Listen for wallet accounts data
useEffect(() => {
const handleAccountsDataResponse = async (event: MessageEvent) => {
if (event.origin !== import.meta.env.VITE_WALLET_IFRAME_URL) return;
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
setAccountAddress(event.data.data[0].address);
}
};
window.addEventListener('message', handleAccountsDataResponse);
return () => {
window.removeEventListener('message', handleAccountsDataResponse);
};
}, []);
// Request wallet address when iframe is loaded
const getAddressFromWallet = useCallback(() => {
const iframe = document.getElementById(
'walletAuthFrame',
) as HTMLIFrameElement;
if (!iframe.contentWindow) {
console.error('Iframe not found or not loaded');
return;
}
iframe.contentWindow.postMessage(
{
type: 'REQUEST_CREATE_OR_GET_ACCOUNTS',
chainId: '1',
},
import.meta.env.VITE_WALLET_IFRAME_URL,
);
}, []);
return (
<Modal open={true} disableEscapeKeyDown keepMounted>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '90%',
maxWidth: '1200px',
height: '600px',
maxHeight: '80vh',
overflow: 'auto',
outline: 'none',
}}
>
<iframe
onLoad={getAddressFromWallet}
id="walletAuthFrame"
src={`${import.meta.env.VITE_WALLET_IFRAME_URL}/auto-sign-in`}
width="100%"
height="100%"
sandbox="allow-scripts allow-same-origin"
></iframe>
</Box>
</Modal>
);
};
export default AutoSignInIFrameModal;

View File

@ -0,0 +1 @@
export { default as AutoSignInIFrameModal } from './AutoSignInIFrameModal';

View File

@ -0,0 +1,89 @@
import {
OnboardingDialog,
hasCompletedOnboarding,
} from '@/components/onboarding-flow';
import React, {
ReactNode,
createContext,
useContext,
useEffect,
useState,
} from 'react';
import { useOctokit } from './OctokitContext';
// Local storage keys - must match the ones in OnboardingDialog.tsx
const ONBOARDING_COMPLETED_KEY = 'onboarding_completed';
interface OnboardingContextType {
showOnboarding: boolean;
setShowOnboarding: (show: boolean) => void;
handleOnboardingClosed: () => void;
isOnboardingCompleted: () => boolean;
}
const OnboardingContext = createContext<OnboardingContextType | undefined>(
undefined,
);
interface OnboardingProviderProps {
children: ReactNode;
}
export const OnboardingProvider: React.FC<OnboardingProviderProps> = ({
children,
}) => {
const [showOnboarding, setShowOnboarding] = useState(false);
const { octokit } = useOctokit();
// Check if GitHub is connected
const isGitHubConnected = Boolean(octokit);
// Handle onboarding completion
const handleOnboardingClosed = () => {
setShowOnboarding(false);
// Mark onboarding as completed to prevent it from showing again
localStorage.setItem(ONBOARDING_COMPLETED_KEY, 'true');
};
// Check if onboarding is completed
const isOnboardingCompleted = (): boolean => {
return hasCompletedOnboarding();
};
// Determine if onboarding should be shown based on completion status and GitHub connection
useEffect(() => {
const onboardingCompleted = isOnboardingCompleted();
// Only show onboarding if not completed AND GitHub is not connected
if (!onboardingCompleted && !isGitHubConnected) {
setShowOnboarding(true);
} else {
setShowOnboarding(false);
}
}, [isGitHubConnected]);
return (
<OnboardingContext.Provider
value={{
showOnboarding,
setShowOnboarding,
handleOnboardingClosed,
isOnboardingCompleted,
}}
>
{children}
{showOnboarding && (
<OnboardingDialog defaultOpen={true} onClose={handleOnboardingClosed} />
)}
</OnboardingContext.Provider>
);
};
export const useOnboarding = (): OnboardingContextType => {
const context = useContext(OnboardingContext);
if (context === undefined) {
throw new Error('useOnboarding must be used within an OnboardingProvider');
}
return context;
};

View File

@ -1,18 +1,33 @@
import { AutoSignInIFrameModal } from '@/components/wallet';
import { useToast } from '@/hooks/use-toast';
import { BASE_URL } from '@/utils/constants';
import axios from 'axios';
import React, {
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { generateNonce, SiweMessage } from 'siwe';
const axiosInstance = axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
withCredentials: true,
});
/**
* @interface WalletContextType
* @description Defines the structure of the WalletContext value.
* @property {object | null} wallet - The wallet object containing id and address.
* @property {boolean} isConnected - Indicates if the wallet is connected.
* @property {boolean} isReady - Indicates if the app is ready to make API calls.
* @property {function} connect - Function to connect the wallet.
* @property {function} disconnect - Function to disconnect the wallet.
*/
@ -22,6 +37,7 @@ interface WalletContextType {
address?: string;
} | null;
isConnected: boolean;
isReady: boolean;
connect: () => Promise<void>;
disconnect: () => void;
}
@ -43,29 +59,49 @@ export const WalletContextProvider: React.FC<{ children: ReactNode }> = ({
}) => {
const [wallet, setWallet] = useState<WalletContextType['wallet']>(null);
const [isConnected, setIsConnected] = useState(false);
const [isReady, setIsReady] = useState(false);
const { toast } = useToast();
const [accountAddress, setAccountAddress] = useState<string>();
const navigate = useNavigate();
const location = useLocation();
// Update isReady state when connection changes
useEffect(() => {
if (isConnected) {
// Add a small delay to ensure session is fully established
const timer = setTimeout(() => {
setIsReady(true);
console.log('Wallet is now ready for API calls');
}, 500);
return () => clearTimeout(timer);
} else {
setIsReady(false);
}
}, [isConnected]);
// Check session status on mount
useEffect(() => {
fetch(`${BASE_URL}/auth/session`, {
credentials: 'include',
}).then((res) => {
const path = window.location.pathname;
const path = location.pathname;
console.log(res);
if (res.status !== 200) {
setIsConnected(false);
localStorage.clear();
if (path !== '/login') {
window.location.pathname = '/login';
navigate('/login');
}
} else {
setIsConnected(true);
if (path === '/login') {
window.location.pathname = '/';
navigate('/');
}
}
});
}, []);
}, [navigate, location]);
// Handle wallet messages for account data
useEffect(() => {
const handleWalletMessage = (event: MessageEvent) => {
if (event.origin !== import.meta.env.VITE_WALLET_IFRAME_URL) return;
@ -76,6 +112,7 @@ export const WalletContextProvider: React.FC<{ children: ReactNode }> = ({
id: address,
address: address,
});
setAccountAddress(address);
setIsConnected(true);
toast({
title: 'Wallet Connected',
@ -90,6 +127,96 @@ export const WalletContextProvider: React.FC<{ children: ReactNode }> = ({
return () => window.removeEventListener('message', handleWalletMessage);
}, []);
// Handle sign-in response from the wallet iframe
useEffect(() => {
const handleSignInResponse = async (event: MessageEvent) => {
if (event.origin !== import.meta.env.VITE_WALLET_IFRAME_URL) return;
if (event.data.type === 'SIGN_IN_RESPONSE') {
try {
const { success } = (
await axiosInstance.post('/auth/validate', {
message: event.data.data.message,
signature: event.data.data.signature,
})
).data;
if (success === true) {
setIsConnected(true);
if (location.pathname === '/login') {
navigate('/');
}
}
} catch (error) {
console.error('Error signing in:', error);
}
}
};
window.addEventListener('message', handleSignInResponse);
return () => {
window.removeEventListener('message', handleSignInResponse);
};
}, [navigate, location]);
// Initiate auto sign-in when account address is available
useEffect(() => {
const initiateAutoSignIn = async () => {
if (!accountAddress) return;
const iframe = document.getElementById(
'walletAuthFrame',
) as HTMLIFrameElement;
if (!iframe?.contentWindow) {
console.error('Iframe not found or not loaded');
return;
}
const message = new SiweMessage({
version: '1',
domain: window.location.host,
uri: window.location.origin,
chainId: 1,
address: accountAddress,
nonce: generateNonce(),
statement: 'Sign in With Ethereum.',
}).prepareMessage();
iframe.contentWindow.postMessage(
{
type: 'AUTO_SIGN_IN',
chainId: '1',
message,
},
import.meta.env.VITE_WALLET_IFRAME_URL,
);
};
initiateAutoSignIn();
}, [accountAddress]);
// Request wallet address when iframe is loaded
const getAddressFromWallet = useCallback(() => {
const iframe = document.getElementById(
'walletAuthFrame',
) as HTMLIFrameElement;
if (!iframe?.contentWindow) {
console.error('Iframe not found or not loaded');
return;
}
iframe.contentWindow.postMessage(
{
type: 'REQUEST_CREATE_OR_GET_ACCOUNTS',
chainId: '1',
},
import.meta.env.VITE_WALLET_IFRAME_URL,
);
}, []);
const connect = async () => {
const iframe = document.getElementById('walletIframe') as HTMLIFrameElement;
if (iframe?.contentWindow) {
@ -122,9 +249,10 @@ export const WalletContextProvider: React.FC<{ children: ReactNode }> = ({
return (
<WalletContext.Provider
value={{ wallet, isConnected, connect, disconnect }}
value={{ wallet, isConnected, isReady, connect, disconnect }}
>
{children}
{!isConnected && <AutoSignInIFrameModal />}
</WalletContext.Provider>
);
};

View File

@ -1,4 +1,5 @@
export * from './GQLClientContext';
export * from './OctokitContext';
export * from './OctokitProviderWithRouter';
export * from './OnboardingContext';
export * from './WalletContextProvider';

View File

@ -1,4 +1,5 @@
import { useGQLClient } from '@/context/GQLClientContext';
import { useWallet } from '@/context/WalletContextProvider';
import { User } from 'gql-client';
import { useCallback, useEffect, useState } from 'react';
import { Outlet, useNavigate } from 'react-router-dom';
@ -7,21 +8,24 @@ const ProjectSearchLayout = () => {
const navigate = useNavigate();
const client = useGQLClient();
const [user, setUser] = useState<User>();
const { isReady } = useWallet();
const fetchUser = useCallback(async () => {
const { user } = await client.getUser();
setUser(user);
}, []);
}, [client]);
useEffect(() => {
fetchUser();
}, []);
if (isReady) {
fetchUser();
}
}, [fetchUser, isReady]);
const fetchOrgSlug = useCallback(async () => {
const { organizations } = await client.getOrganizations();
// TODO: Get the selected organization. This is temp
return organizations[0].slug;
}, []);
}, [client]);
return (
<>

View File

@ -0,0 +1,24 @@
import { WalletContextProvider } from '@/context';
import { OnboardingProvider } from '@/context/OnboardingContext';
import { ThemeProvider } from 'next-themes';
import { Outlet } from 'react-router-dom';
/**
* Root layout component that wraps the application with necessary providers
* that need access to the router context.
*
* @returns {JSX.Element} The root layout with outlet for child routes
*/
const RootLayout = () => {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<OnboardingProvider>
<WalletContextProvider>
<Outlet />
</WalletContextProvider>
</OnboardingProvider>
</ThemeProvider>
);
};
export default RootLayout;

View File

@ -1,9 +1,6 @@
import OnboardingDialog from '@/components/onboarding/OnboardingDialog';
import AutoSignInIFrameModal from '@/components/shared/auth/AutoSignInIFrameModal';
import { Button } from '@/components/ui/button';
/**
* AuthPage component that renders the authentication page with an auto sign-in iframe modal.
* AuthPage component that renders the authentication page.
* The AutoSignInIFrameModal is now included in the WalletContextProvider.
*
* @returns {JSX.Element} A JSX element that renders the authentication page.
*/
@ -20,17 +17,7 @@ const AuthPage = () => {
</div>
</div>
<div className="flex-center relative z-10 flex-1 pb-12 flex-col">
<AutoSignInIFrameModal />
<div className="mt-8 text-center">
<p className="text-muted-foreground mb-2">Want to see our new onboarding flow?</p>
<OnboardingDialog
trigger={
<Button variant="outline">
Try Onboarding Demo
</Button>
}
/>
</div>
<p className="text-center text-lg">Authenticating with wallet...</p>
</div>
</div>
);

View File

@ -1,17 +1,23 @@
import OnboardingDialog from '@/components/onboarding/OnboardingDialog';
import OnboardingDialog from '@/components/onboarding-do-not-use/OnboardingDialog';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import React, { useState } from 'react';
/**
* OnboardingDemoPage component
*
*
* This page serves as a landing page with a prominent button to open the onboarding dialog.
* It provides information about the onboarding feature and launches it in a modal dialog.
*/
const OnboardingDemoPage: React.FC = () => {
const [dialogOpen, setDialogOpen] = useState(false);
return (
<div className="flex items-center justify-center min-h-screen bg-background p-4">
<Card className="w-full max-w-2xl">
@ -34,12 +40,13 @@ const OnboardingDemoPage: React.FC = () => {
<li>Deploying your application</li>
</ul>
<p className="text-muted-foreground mb-8">
All data in this demo is simulated and no actual deployments will be made.
All data in this demo is simulated and no actual deployments will
be made.
</p>
</div>
<div className="flex justify-center">
<OnboardingDialog
<OnboardingDialog
trigger={
<Button size="lg" className="px-8">
Open Onboarding Dialog
@ -54,4 +61,4 @@ const OnboardingDemoPage: React.FC = () => {
);
};
export default OnboardingDemoPage;
export default OnboardingDemoPage;

View File

@ -1,9 +1,9 @@
import React from 'react';
import OnboardingLayout from '../components/onboarding/OnboardingLayout';
import OnboardingLayout from '../components/onboarding-do-not-use/OnboardingLayout';
/**
* OnboardingPage component
*
*
* This page serves as a demo wrapper for the onboarding feature.
* It simply renders the OnboardingLayout component which handles
* the entire onboarding flow.
@ -12,4 +12,4 @@ const OnboardingPage: React.FC = () => {
return <OnboardingLayout />;
};
export default OnboardingPage;
export default OnboardingPage;

View File

@ -1,14 +1,13 @@
import { LoadingOverlay } from '@/components/loading/loading-overlay';
import { useOnboarding } from '@/context/OnboardingContext';
import { Organization } from 'gql-client';
import { Loader2 } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { Navigate, useNavigate } from 'react-router-dom';
import { OnboardingDialog, hasCompletedOnboarding } from '../components/onboarding-flow';
import { Navigate } from 'react-router-dom';
import { useGQLClient } from '../context/GQLClientContext';
import { useOctokit } from '../context/OctokitContext';
/**
* Index component that handles post-authentication flow.
* Shows onboarding dialog if needed, then redirects to first organization.
* Uses OnboardingContext for onboarding state, then redirects to first organization.
*
* @returns {JSX.Element} The rendered component.
*/
@ -16,17 +15,11 @@ const Index = () => {
const client = useGQLClient();
const [organization, setOrganization] = useState<Organization>();
const [loading, setLoading] = useState(true);
const [showOnboarding, setShowOnboarding] = useState(false);
const navigate = useNavigate();
const { octokit } = useOctokit();
// Check if GitHub is connected
const isGitHubConnected = Boolean(octokit);
const { showOnboarding } = useOnboarding();
/**
* Fetches the user's organizations from the GQLClient.
* Sets the first organization in the list to the organization state.
* If no organizations are found, shows onboarding.
*
* @async
* @function fetchUserOrganizations
@ -36,51 +29,37 @@ const Index = () => {
try {
setLoading(true);
const { organizations } = await client.getOrganizations();
if (organizations && organizations.length > 0) {
// By default information of first organization displayed
setOrganization(organizations[0]);
// Check if onboarding is needed
const onboardingCompleted = hasCompletedOnboarding();
// We need onboarding if it hasn't been completed or GitHub is not connected
if (!onboardingCompleted || !isGitHubConnected) {
setShowOnboarding(true);
}
} else {
// No organizations found, show onboarding
setShowOnboarding(true);
}
} catch (error) {
console.error('Error fetching organizations:', error);
// Show onboarding on error
setShowOnboarding(true);
} finally {
setLoading(false);
}
}, [client, isGitHubConnected]);
}, [client]);
useEffect(() => {
fetchUserOrganizations();
}, [fetchUserOrganizations]);
// Handle onboarding completion
const handleOnboardingClosed = () => {
setShowOnboarding(false);
// Fetch organizations again after onboarding in case new ones were created
fetchUserOrganizations();
};
if (loading) {
return <Loader2 className="animate-spin w-12 h-12" />;
return <LoadingOverlay />;
}
// If onboarding is showing, render nothing here as the OnboardingProvider
// will render the onboarding dialog
if (showOnboarding) {
return <OnboardingDialog defaultOpen={true} onClose={handleOnboardingClosed} />;
return null;
}
return organization ? <Navigate to={organization.slug} /> : <Loader2 className="animate-spin w-12 h-12" />;
return organization ? (
<Navigate to={organization.slug} />
) : (
<LoadingOverlay />
);
};
export default Index;

View File

@ -16,33 +16,38 @@ import CheckBalanceIframe from '@/components/projects/create/CheckBalanceIframe'
import { ProjectCard } from '@/components/projects/ProjectCard';
import { Button } from '@/components/ui/button';
import { useGQLClient } from '@/context/GQLClientContext';
import { useWallet } from '@/context/WalletContextProvider';
import { Project } from 'gql-client';
import { Plus } from 'lucide-react';
const ProjectsScreen = () => {
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>();
const [projects, setProjects] = useState<Project[]>([]);
const { isReady } = useWallet();
const navigate = useNavigate();
const client = useGQLClient();
const { orgSlug } = useParams();
const fetchProjects = useCallback(async () => {
const { projectsInOrganization } = await client.getProjectsInOrganization(
orgSlug!,
);
if (!orgSlug) return;
const { projectsInOrganization } =
await client.getProjectsInOrganization(orgSlug);
setProjects(projectsInOrganization);
}, [orgSlug]);
}, [orgSlug, client]);
useEffect(() => {
fetchProjects();
}, [orgSlug]);
if (isReady && orgSlug) {
fetchProjects();
}
}, [fetchProjects, orgSlug, isReady]);
useEffect(() => {
if (isBalanceSufficient === false) {
navigate('/buy-prepaid-service');
}
}, [isBalanceSufficient]);
}, [isBalanceSufficient, navigate]);
return (
<ScreenWrapper>

View File

@ -1,11 +1,9 @@
import Lottie from 'lottie-react';
import { Link, useParams, useSearchParams } from 'react-router-dom';
import { Heading } from '@/components/shared/Heading';
import { Badge, Button } from '@/components/ui';
import { ArrowLeft, HelpCircle } from 'lucide-react';
import logoAnimation from '@/components/../../public/lottie/logo.json';
import { useGQLClient } from '@/context/GQLClientContext';
import { Project } from 'gql-client';
import { useEffect, useState } from 'react';
@ -38,7 +36,7 @@ const Id = () => {
<div className="flex flex-col gap-8 lg:gap-11 max-w-[522px] mx-auto py-6 lg:py-12">
{/* Icon */}
<div className="flex justify-center">
<Lottie animationData={logoAnimation} loop={false} size={40} />
{/* <Lottie animationData={logoAnimation} loop={false} size={40} /> */}
</div>
{/* Heading */}

View File

@ -0,0 +1,89 @@
# Snowball Tools Project Documentation
## Overview
This document consolidates project standards, documentation guidelines, and best practices for the
Snowball Tools project.
## Project Purpose and Standards
### Core Principles
1. **Consistency** - Establish consistent patterns across the codebase
2. **Onboarding** - Help new developers understand project conventions
3. **Maintainability** - Make code easier to maintain and extend
4. **Quality** - Encourage best practices that lead to higher quality code
## Documentation Standards
### TSDoc and TypeDoc
We use [TSDoc](https://tsdoc.org/) for documenting TypeScript code and
[TypeDoc](https://typedoc.org/) for generating API documentation.
#### Basic Comment Structure
TSDoc comments start with `/**` and end with `*/`:
```typescript
/**
* This is a TSDoc comment.
*/
```
#### 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 |
| `@public`, `@protected`, `@private` | Visibility modifiers |
### Documentation 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
## Generating Documentation
Generate documentation:
```bash
yarn docs
```
Watch and regenerate documentation:
```bash
yarn docs:watch
```
## Contributing to Standards
To suggest changes or additions to project standards:
1. Discuss proposed changes with the team
2. Update the relevant documentation
3. Provide examples demonstrating the benefits of the proposed changes
## Enforcement
While these standards are not automatically enforced, developers are encouraged to follow them, and
code reviewers should check for adherence to these guidelines.
## Resources
- [TSDoc Official Documentation](https://tsdoc.org/)
- [TypeDoc Official Documentation](https://typedoc.org/)
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)

View File

@ -0,0 +1,39 @@
# Snowball Tools Documentation
Welcome to the Snowball Tools documentation repository. This directory contains comprehensive
documentation about the platform's architecture, components, and functionality.
## Documentation Structure
- [Authentication](./authentication/README.md) - User authentication, project access, and
permissions
- [Frontend](./frontend/README.md) - Frontend architecture and components
## Purpose
This documentation is designed to help:
- New developers understand the system architecture
- AI assistants and agents navigate the codebase
- Document key technical decisions and patterns
- Provide a reference for complex subsystems
## Mermaid Diagrams
Many documents include [Mermaid](https://mermaid-js.github.io/) diagrams for visual representation
of:
- Sequence flows
- Entity relationships
- Component architecture
- State machines
## Contribution Guidelines
When adding to this documentation:
1. Place documentation in the appropriate subdirectory
2. Include clear, descriptive titles and section headings
3. Reference specific code files when relevant
4. Include visual diagrams for complex processes
5. Update the relevant README.md indices

View File

@ -0,0 +1,33 @@
# Authentication Documentation
This directory contains documentation about the authentication and user management systems.
## Available Documents
- [User Project Authentication and Management](./user-project-management.md) - Comprehensive
overview of how users authenticate and how projects are managed and counted
## Topics Covered
- Wallet Authentication
- GitHub Integration
- Project-User Relationships
- Project Counting and Access
- Database Structures
## Diagrams
The documentation includes:
- Sequence diagrams showing authentication flows
- Entity relationship diagrams for data structures
- Process flows for user interactions
## Contributing
When adding to this documentation:
1. Follow the established Markdown format
2. Include relevant diagrams using Mermaid
3. Reference code files with relative paths
4. Focus on explaining "why" not just "how"

View File

@ -0,0 +1,227 @@
# User Project Authentication and Management
## Overview
This document explains how users authenticate with the system and how their projects are managed and
counted. The process begins with wallet authentication and extends to GitHub integration for project
access and creation.
## Authentication Flow
Users in the system are identified primarily by their wallet address, but to create or access
projects, they must authenticate with GitHub. This dual authentication approach ensures secure
access to both blockchain and version control resources.
### Wallet Authentication
Wallet authentication is handled through an iframe-based mechanism that:
1. Requests the user's wallet address
2. Generates a Sign-In With Ethereum (SIWE) message
3. Asks the user to sign this message to prove wallet ownership
4. Verifies the signature on the backend
5. Creates or retrieves the user account based on the Ethereum address
### GitHub Authentication
After wallet authentication, GitHub authentication is required to:
1. Access repositories for project creation
2. Deploy projects with proper permissions
3. Link project changes to the appropriate GitHub account
When a user connects their GitHub account:
- A GitHub OAuth flow is initiated
- The received GitHub token is stored in the user record
- The user can now access their GitHub repositories through the platform
## Project-User Relationships
Projects in the system have two types of relationships with users:
1. **Direct Ownership**: The user who created the project is designated as the owner
2. **Project Membership**: Users can be added as members with specific permissions (View, Edit)
This dual relationship structure allows for collaborative project management while maintaining clear
ownership.
## Database Structure
The key entities involved in user project management:
- **User**: Stores wallet address, GitHub token, and other user information
- **Project**: Contains project details including repository, framework, and deployment settings
- **ProjectMember**: Junction table that links users to projects with specific permissions
- **Organization**: Groups projects and provides team-level management
## Counting User Projects
To count a user's projects, the system:
1. Retrieves the user's ID from their wallet authentication
2. Identifies the organization(s) the user belongs to
3. Queries projects where either:
- The user is the project owner, OR
- The user is linked via the ProjectMember table
4. Returns the combined list of projects the user has access to
The query used looks similar to:
```sql
SELECT projects.*
FROM projects
LEFT JOIN project_members ON projects.id = project_members.project_id
LEFT JOIN organizations ON projects.organization_id = organizations.id
WHERE (projects.owner_id = :userId OR project_members.user_id = :userId)
AND organizations.slug = :organizationSlug
```
## Sequence Diagram
```mermaid
sequenceDiagram
participant User
participant Frontend
participant WalletIframe
participant Backend
participant GitHub
participant Database
%% Wallet Authentication
User->>Frontend: Visits platform
Frontend->>WalletIframe: Loads wallet authentication iframe
WalletIframe->>User: Requests wallet connection
User->>WalletIframe: Connects wallet
WalletIframe->>Frontend: Message event with wallet address
Frontend->>User: Requests signature of SIWE message
User->>Frontend: Signs message
Frontend->>Backend: Validate signature
Backend->>Database: Create/Retrieve user account
Database->>Backend: User account details
Backend->>Frontend: Authentication successful
%% GitHub Authentication
Frontend->>User: Prompts for GitHub authentication
User->>GitHub: Authorizes application
GitHub->>Frontend: Returns OAuth code
Frontend->>Backend: Exchange code for token
Backend->>GitHub: Validate token
GitHub->>Backend: Token validation response
Backend->>Database: Store GitHub token with user
Database->>Backend: Update confirmation
Backend->>Frontend: GitHub connection successful
%% Project Access
User->>Frontend: Request projects list
Frontend->>Backend: getProjectsInOrganization(orgSlug)
Backend->>Database: Query for owned & member projects
Database->>Backend: Projects data
Backend->>Frontend: Projects list
Frontend->>User: Display projects count and list
```
## Entity Relationship Diagram
```mermaid
erDiagram
User ||--o{ Project : owns
User ||--o{ ProjectMember : has
Project ||--o{ ProjectMember : includes
Organization ||--o{ Project : contains
User ||--o{ UserOrganization : belongs_to
Organization ||--o{ UserOrganization : has_members
User {
string id PK
string ethAddress
string name
string email
string gitHubToken
boolean isVerified
datetime createdAt
datetime updatedAt
string subOrgId
string turnkeyWalletId
}
Project {
string id PK
string ownerId FK
string name
string repository
string prodBranch
string description
string framework
string organizationId FK
datetime createdAt
datetime updatedAt
}
ProjectMember {
string id PK
string userId FK
string projectId FK
string[] permissions
boolean isPending
datetime createdAt
datetime updatedAt
}
Organization {
string id PK
string name
string slug
datetime createdAt
datetime updatedAt
}
UserOrganization {
string id PK
string userId FK
string organizationId FK
string role
datetime createdAt
datetime updatedAt
}
</mermaid>
## Implementation Details
### Frontend Components
Key components involved in project management:
- **WalletContextProvider**: Manages wallet connection state and authentication
- **OctokitProvider**: Handles GitHub authentication and API access
- **ProjectSearchLayout**: Retrieves and displays user projects
### Backend Services
- **AuthService**: Handles SIWE validation and session management
- **Service.getProjectsInOrganization**: Retrieves projects for a user in an organization
- **Database.getProjectsInOrganization**: Performs the actual database query
## API Endpoints
### GraphQL Queries
- `getProjectsInOrganization(organizationSlug: String!)`: Returns all projects a user has access to in an organization
- `projectMembers(projectId: String!)`: Returns all members of a specific project
- `getUser()`: Returns the authenticated user's information
## Future Considerations
1. **Performance Optimization**: For users with many projects, pagination or optimized queries may be needed
2. **Permission Caching**: Consider caching project access for frequently accessed projects
3. **Multi-Wallet Support**: Plan for supporting multiple wallet connections per user
4. **Cross-Organization Projects**: Consider how to handle projects that span multiple organizations
## References
- **WalletContextProvider.tsx**: Main wallet authentication logic
- **OctokitContext.tsx**: GitHub authentication management
- **service.ts**: Service layer with project retrieval methods
- **database.ts**: Database queries for project access
- **User.ts** and **ProjectMember.ts**: Entity definitions showing relationships
```

View File

@ -0,0 +1,89 @@
# Snowball Tools Project Documentation
## Overview
This document consolidates project standards, documentation guidelines, and best practices for the
Snowball Tools project.
## Project Purpose and Standards
### Core Principles
1. **Consistency** - Establish consistent patterns across the codebase
2. **Onboarding** - Help new developers understand project conventions
3. **Maintainability** - Make code easier to maintain and extend
4. **Quality** - Encourage best practices that lead to higher quality code
## Documentation Standards
### TSDoc and TypeDoc
We use [TSDoc](https://tsdoc.org/) for documenting TypeScript code and
[TypeDoc](https://typedoc.org/) for generating API documentation.
#### Basic Comment Structure
TSDoc comments start with `/**` and end with `*/`:
```typescript
/**
* This is a TSDoc comment.
*/
```
#### 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 |
| `@public`, `@protected`, `@private` | Visibility modifiers |
### Documentation 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
## Generating Documentation
Generate documentation:
```bash
yarn docs
```
Watch and regenerate documentation:
```bash
yarn docs:watch
```
## Contributing to Standards
To suggest changes or additions to project standards:
1. Discuss proposed changes with the team
2. Update the relevant documentation
3. Provide examples demonstrating the benefits of the proposed changes
## Enforcement
While these standards are not automatically enforced, developers are encouraged to follow them, and
code reviewers should check for adherence to these guidelines.
## Resources
- [TSDoc Official Documentation](https://tsdoc.org/)
- [TypeDoc Official Documentation](https://typedoc.org/)
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)

View File

@ -1,41 +0,0 @@
# Project Standards and Conventions
This directory contains documentation for project-wide standards, conventions, and best practices.
These guidelines help ensure consistency across the codebase and make it easier for developers to
collaborate.
## Contents
- [Component Documentation Standards](./COMPONENT_DOCUMENTATION.md) - Guidelines and templates for
documenting components, hooks, and utilities
- [Feature Building Process](./FEATURE_BUILDING.md) - Standardized approach to building new features
from design to implementation
- [Feature Building Template](./FEATURE_BUILDING_TEMPLATE.md) - Template document to use when
starting a new feature
## Purpose
The standards defined in this directory serve several important purposes:
1. **Consistency** - Establishes consistent patterns across the codebase
2. **Onboarding** - Helps new developers understand project conventions
3. **Maintainability** - Makes code easier to maintain and extend
4. **Quality** - Encourages best practices that lead to higher quality code
## Adoption
These standards should be applied to all new code and, when feasible, retroactively applied to
existing code during refactoring efforts.
## Contributing
To suggest changes or additions to these standards, please:
1. Discuss proposed changes with the team
2. Update the relevant documentation
3. Provide examples demonstrating the benefits of the proposed changes
## Enforcement
While these standards are not automatically enforced, developers are encouraged to follow them and
code reviewers should check for adherence to these guidelines.