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:
parent
134d4ad316
commit
e1c2a8ce2c
165
DOCUMENTATION.md
165
DOCUMENTATION.md
@ -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/)
|
39
README.md
39
README.md
@ -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
@ -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 />} />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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)
|
@ -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.
|
@ -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.
|
@ -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;
|
1
packages/frontend/src/components/wallet/index.ts
Normal file
1
packages/frontend/src/components/wallet/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as AutoSignInIFrameModal } from './AutoSignInIFrameModal';
|
89
packages/frontend/src/context/OnboardingContext.tsx
Normal file
89
packages/frontend/src/context/OnboardingContext.tsx
Normal 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;
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
export * from './GQLClientContext';
|
||||
export * from './OctokitContext';
|
||||
export * from './OctokitProviderWithRouter';
|
||||
export * from './OnboardingContext';
|
||||
export * from './WalletContextProvider';
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
24
packages/frontend/src/layouts/RootLayout.tsx
Normal file
24
packages/frontend/src/layouts/RootLayout.tsx
Normal 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;
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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 */}
|
||||
|
89
qwrk-docs/project-docs/PROJECT_DOCUMENTATION.md
Normal file
89
qwrk-docs/project-docs/PROJECT_DOCUMENTATION.md
Normal 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/)
|
39
qwrk-docs/project-docs/README.md
Normal file
39
qwrk-docs/project-docs/README.md
Normal 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
|
33
qwrk-docs/project-docs/authentication/README.md
Normal file
33
qwrk-docs/project-docs/authentication/README.md
Normal 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"
|
227
qwrk-docs/project-docs/authentication/user-project-management.md
Normal file
227
qwrk-docs/project-docs/authentication/user-project-management.md
Normal 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
|
||||
```
|
89
qwrk-docs/standards/README.md
Normal file
89
qwrk-docs/standards/README.md
Normal 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/)
|
@ -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.
|
Loading…
Reference in New Issue
Block a user