Squashed commit of the following:

commit ea08482596e10f27536af2d32040b476a18085e0
Author: icld <ian@icld.io>
Date:   Tue Feb 25 07:06:03 2025 -0800

    refactor: update WalletContext import and enhance layout components
This commit is contained in:
icld 2025-02-28 09:07:15 -08:00
parent cdd2bd2d89
commit 134d4ad316
17 changed files with 1312 additions and 163 deletions

View File

@ -1,3 +1,5 @@
import ProjectSearchLayout from '@/layouts/ProjectSearch';
import ProjectsScreen from '@/pages/org-slug/ProjectsScreen';
import { ThemeProvider } from 'next-themes';
import { useEffect } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
@ -7,7 +9,10 @@ import AuthPage from './pages/AuthPage';
import BuyPrepaidService from './pages/BuyPrepaidService';
import OnboardingDemoPage from './pages/OnboardingDemoPage';
import OnboardingPage from './pages/OnboardingPage';
import { projectsRoutesWithoutSearch } from './pages/org-slug/projects/project-routes';
import {
projectsRoutesWithoutSearch,
projectsRoutesWithSearch,
} from './pages/org-slug/projects/project-routes';
import Settings from './pages/org-slug/Settings';
import { BASE_URL } from './utils/constants';
@ -16,19 +21,19 @@ const router = createBrowserRouter([
path: ':orgSlug',
element: <DashboardLayout />,
children: [
// {
// element: <ProjectSearchLayout />,
// children: [`
// {
// path: '',
// element: <Projects />,`
// },
// {
// path: 'projects',
// children: projectsRoutesWithSearch,
// },
// ],
// },
{
element: <ProjectSearchLayout />,
children: [
{
path: '',
element: <ProjectsScreen />,
},
{
path: 'projects',
children: projectsRoutesWithSearch,
},
],
},
{
path: 'settings',
element: <Settings />,
@ -50,6 +55,7 @@ const router = createBrowserRouter([
{
path: '/buy-prepaid-service',
element: <BuyPrepaidService />,
errorElement: <div>Something went wrong!</div>,
},
{
path: '/onboarding',
@ -69,13 +75,15 @@ const router = createBrowserRouter([
function App() {
// Hacky way of checking session
// TODO: Handle redirect backs
useEffect(() => {
fetch(`${BASE_URL}/auth/session`, {
credentials: 'include',
}).then((res) => {
const path = window.location.pathname;
const publicPaths = ['/login', '/onboarding', '/onboarding-demo'];
console.log(res);
if (res.status !== 200) {
localStorage.clear();
if (!publicPaths.includes(path)) {

View File

@ -1,5 +1,5 @@
import { useWallet } from '@/context/WalletContextProvider';
import type React from 'react';
/**
* WalletSessionIdProps interface defines the props for the WalletSessionId component.
*/
@ -24,18 +24,18 @@ export const WalletSessionId: React.FC<WalletSessionIdProps> = ({
walletId,
className = '',
}) => {
// const { wallet } = useWallet();
const wallet = { id: 'x123xxx' };
const displayId = walletId || wallet?.id || 'Not Connected';
const { wallet, isConnected } = useWallet();
// const wallet = { id: 'x123xxx' };
console.log(wallet, 'from WalletSessionId.tsx');
const displayId = wallet?.id || 'Wallet';
return (
<div
className={`flex items-center gap-2 rounded-md bg-secondary px-2.5 py-0.5 ${className}`}
>
<div
className={`h-2 w-2 rounded-full ${wallet?.id ? 'bg-green-400' : 'bg-gray-400'}`}
className={`h-2 w-2 rounded-full ${isConnected ? 'bg-green-400' : 'bg-gray-400'}`}
/>
<span className="text-xs font-semibold text-secondary-foreground">
<span className="text-secondary-foreground text-xs font-semibold">
{displayId}
</span>
</div>

View File

@ -27,7 +27,7 @@ export function ScreenWrapper({
<div
className={cn(
'flex flex-1 flex-col',
'container mx-auto p-4 md:p-6 lg:p-8',
'container mx-auto p-4 md:p-6 lg:p-8 ',
className,
)}
{...props}

View File

@ -29,7 +29,7 @@ export function TabWrapper({
{...props}
orientation={orientation}
className={cn(
'w-full',
'w-full lg:max-w-2xl mx-auto',
orientation === 'vertical' && 'flex gap-6',
className,
)}

View File

@ -1,29 +1,96 @@
import assert from 'assert';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useDebounce } from 'usehooks-ts';
import { ProjectRepoCard } from '@/components/projects/create/ProjectRepoCard';
import { Select, SelectOption } from '@/components/shared/Select';
import { SelectOption } from '@/components/shared/Select';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { IconButton } from '@/components/ui/extended/button-w-icons';
import { IconInput } from '@/components/ui/extended/input-w-icons';
import { useOctokit } from '@/context/OctokitContext';
import { Github, RotateCw, Search } from 'lucide-react';
import { relativeTimeISO } from '@/utils/time';
import { ChevronDown, Github, RotateCw, Search } from 'lucide-react';
import { GitOrgDetails, GitRepositoryDetails } from '../../../../types/types';
const DEFAULT_SEARCHED_REPO = '';
const REPOS_PER_PAGE = 5;
interface RepoCardProps {
repo: GitRepositoryDetails;
onClick: () => void;
}
/**
* A card component that displays GitHub repository information in a clickable button format.
*
* @param {Object} props - The component props
* @param {Repository} props.repo - The repository object containing repository details
* @param {() => void} props.onClick - Callback function triggered when the card is clicked
* @returns {JSX.Element} A button element styled as a repository card
*/
function RepoCard({ repo, onClick }: RepoCardProps) {
return (
<button
onClick={onClick}
className="hover:bg-accent hover:text-accent-foreground bg-card text-card-foreground flex items-center w-full gap-3 p-3 text-left transition-colors border rounded-lg shadow"
>
<Github className="text-muted-foreground w-4 h-4" />
<span className="flex-1 text-sm">{repo.full_name}</span>
<span className="text-muted-foreground text-xs">
{repo.updated_at && relativeTimeISO(repo.updated_at)}
</span>
</button>
);
}
/**
* RepositoryList Component
*
* This component renders a list of repositories fetched from GitHub based on the selected account
* and search criteria. It allows users to select an organization or their personal account and search
* for repositories within that account.
* It uses `useOctokit` to fetch repositories and `ProjectRepoCard` to display each repository.
*
* @returns {JSX.Element} - The RepositoryList component.
*/
/**
* A component that displays a list of GitHub repositories with search and filtering capabilities.
*
* @component
* @description
* This component provides a user interface for:
* - Selecting between personal and organization repositories
* - Searching repositories by name
* - Displaying repository cards with basic repository information
* - Navigating to repository configuration
*
* @remarks
* The component uses:
* - GitHub Octokit for API interactions
* - Debounced search functionality
* - Responsive layout for desktop and mobile views
*
* @dependencies
* - useNavigate - For navigation between routes
* - useParams - For accessing URL parameters
* - useOctokit - Custom hook for GitHub API access
* - useDebounce - Custom hook for search debouncing
*
* @returns JSX.Element - A section containing repository selection, search, and list components
*
* @example
* ```tsx
* <RepositoryList />
* ```
*/
export const RepositoryList = () => {
const navigate = useNavigate();
const { orgSlug } = useParams();
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
const [selectedAccount, setSelectedAccount] = useState<SelectOption>();
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
@ -134,16 +201,37 @@ export const RepositoryList = () => {
return (
<section className="space-y-3">
{/* Dropdown and search */}
<div className="lg:flex-row lg:gap-3 flex flex-col items-center gap-0">
<div className="lg:basis-1/3 w-full">
<Select
options={options}
placeholder="Select a repository"
value={selectedAccount}
leftIcon={selectedAccount ? <Github /> : undefined}
rightIcon={<RotateCw />}
onChange={(value) => setSelectedAccount(value as SelectOption)}
/>
<div className="lg:gap-3 flex flex-col items-center gap-0">
<div className="flex flex-col w-full">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="w-full justify-between bg-[#27272a] hover:bg-[#27272a]/90 text-white"
>
<div className="flex items-center gap-2">
<Github className="w-4 h-4" />
<span>{selectedAccount?.label || 'git-account'}</span>
</div>
<ChevronDown className="w-4 h-4 opacity-50" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-[200px] bg-[#27272a] border-[#27272a]"
>
{options.map((option) => (
<DropdownMenuItem
key={option.value}
className="w-full text-white focus:bg-[#3f3f46] focus:text-white"
onClick={() => setSelectedAccount(option)}
>
<Github className="w-4 h-4 mr-2" />
{option.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="basis-2/3 flex flex-grow w-full">
<IconInput
@ -158,15 +246,17 @@ export const RepositoryList = () => {
{/* Repository list */}
{Boolean(repositoryDetails.length) ? (
<div className="flex flex-col gap-2">
{repositoryDetails.map((repo, index) => (
<div key={index}>
<ProjectRepoCard repository={repo} />
{/* Horizontal line */}
{index !== repositoryDetails.length - 1 && (
<div className="border-b border-border-separator/[0.06] w-full" />
)}
</div>
<div className="space-y-2">
{repositoryDetails.map((repo) => (
<RepoCard
key={repo.id}
repo={repo}
onClick={() =>
navigate(
`configure?owner=${repo.owner?.login}&name=${repo.name}&defaultBranch=${repo.default_branch}&fullName=${repo.full_name}&orgSlug=${orgSlug}`,
)
}
/>
))}
</div>
) : (

View File

@ -1,6 +1,6 @@
import { Tag } from '@/components/shared/Tag';
import { useToast } from '@/components/shared/Toast';
import { Button } from '@/components/ui';
import { Card } from '@/components/ui/card';
import { cn } from '@/utils/classnames';
import { GitBranch } from 'lucide-react';
import React, { ComponentPropsWithoutRef, useCallback } from 'react';
@ -22,6 +22,8 @@ export interface TemplateCardProps extends ComponentPropsWithoutRef<'div'> {
export const TemplateCard: React.FC<TemplateCardProps> = ({
template,
isGitAuth,
className,
...props
}: TemplateCardProps) => {
const { toast, dismiss } = useToast();
const navigate = useNavigate();
@ -36,11 +38,13 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
onDismiss: dismiss,
});
}
if (isGitAuth) {
return navigate(
`/${orgSlug}/projects/create/template?templateId=${template.id}`,
);
}
return toast({
id: 'connect-git-account',
title: 'Connect Git account to start with a template',
@ -49,38 +53,53 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
});
}, [orgSlug, dismiss, isGitAuth, navigate, template, toast]);
// Extract short name for icon (e.g., "PWA" from "Progressive Web App")
const shortName = template.name
.split(' ')
.map((word) => word[0])
.join('')
.slice(0, 3);
return (
<div
<Card
className={cn(
'flex items-center gap-3 px-3 py-3 bg-base-bg-alternate hover:bg-base-bg-emphasized rounded-2xl group relative cursor-pointer',
{
'cursor-default': template?.isComingSoon,
},
'flex flex-col gap-2 p-4 bg-[#121212] hover:bg-[#1a1a1a] rounded-xl cursor-pointer transition-all',
template?.isComingSoon && 'cursor-default opacity-70',
className,
)}
onClick={handleClick}
{...props}
>
{/* Icon */}
<div className="rounded-xl bg-base-bg border-border-interactive/10 shadow-card-sm px-1 py-1 border">
<GitBranch />
<div className="flex items-start gap-3">
{/* Icon */}
<div className="flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-md bg-[#1E1E1E] text-white font-medium text-xs">
{shortName}
</div>
<div className="flex flex-col gap-1">
{/* Name */}
<p className="text-sm font-medium text-white">{template.name}</p>
{/* Repo name */}
{template.repoFullName && (
<div className="flex items-center gap-1 text-[#8A8A8E] text-xs">
<GitBranch size={12} />
<span>{template.repoFullName}</span>
</div>
)}
</div>
</div>
{/* Name */}
<p className="text-elements-high-em flex-1 text-sm tracking-tighter text-left">
{template.name}
</p>
{template?.isComingSoon ? (
<Tag size="xs" type="neutral" leftIcon={<GitBranch />}>
{template?.isComingSoon && (
<Tag
size="xs"
type="neutral"
leftIcon={<GitBranch size={12} />}
className="self-start mt-1"
>
Soon
</Tag>
) : (
<Button
variant="secondary"
size="sm"
// iconOnly
className="group-hover:flex right-3 absolute hidden"
>
<GitBranch />
</Button>
)}
</div>
</Card>
);
};

View File

@ -1,4 +1,5 @@
import { useToast } from '@/hooks/use-toast';
import { BASE_URL } from '@/utils/constants';
import React, {
createContext,
ReactNode,
@ -32,22 +33,43 @@ interface WalletContextType {
const WalletContext = createContext<WalletContextType | undefined>(undefined);
/**
* @component WalletProvider
* @component WalletContextProvider
* @description Provides the WalletContext to its children.
* @param {Object} props - Component props
* @param {ReactNode} props.children - The children to render.
*/
export const WalletProvider: React.FC<{ children: ReactNode }> = ({
export const WalletContextProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [wallet, setWallet] = useState<WalletContextType['wallet']>(null);
const [isConnected, setIsConnected] = useState(false);
const { toast } = useToast();
useEffect(() => {
fetch(`${BASE_URL}/auth/session`, {
credentials: 'include',
}).then((res) => {
const path = window.location.pathname;
console.log(res);
if (res.status !== 200) {
setIsConnected(false);
localStorage.clear();
if (path !== '/login') {
window.location.pathname = '/login';
}
} else {
setIsConnected(true);
if (path === '/login') {
window.location.pathname = '/';
}
}
});
}, []);
useEffect(() => {
const handleWalletMessage = (event: MessageEvent) => {
if (event.origin !== import.meta.env.VITE_WALLET_IFRAME_URL) return;
console.log(event);
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
const address = event.data.data[0].address;
setWallet({
@ -66,7 +88,7 @@ export const WalletProvider: React.FC<{ children: ReactNode }> = ({
window.addEventListener('message', handleWalletMessage);
return () => window.removeEventListener('message', handleWalletMessage);
}, [toast]);
}, []);
const connect = async () => {
const iframe = document.getElementById('walletIframe') as HTMLIFrameElement;
@ -111,12 +133,12 @@ export const WalletProvider: React.FC<{ children: ReactNode }> = ({
* @function useWallet
* @description A hook that provides access to the WalletContext.
* @returns {WalletContextType} The wallet context value.
* @throws {Error} If used outside of a WalletProvider.
* @throws {Error} If used outside of a WalletContextProvider.
*/
export const useWallet = () => {
const context = useContext(WalletContext);
if (context === undefined) {
throw new Error('useWallet must be used within a WalletProvider');
throw new Error('useWallet must be used within a WalletContextProvider');
}
return context;
};

View File

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

View File

@ -36,7 +36,6 @@ export const DashboardLayout = ({
<OctokitProviderWithRouter>
<NavigationWrapper>
<Outlet />
</NavigationWrapper>
</OctokitProviderWithRouter>
</section>

View File

@ -1,10 +1,5 @@
import { ScreenWrapper } from '@/components/layout';
import { ProjectSearchBar } from '@/components/projects/ProjectSearchBar';
import { Button } from '@/components/ui/button';
import { useGQLClient } from '@/context/GQLClientContext';
import { formatAddress } from '@/utils/format';
import { User } from 'gql-client';
import { Bell, Plus } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { Outlet, useNavigate } from 'react-router-dom';
@ -29,50 +24,53 @@ const ProjectSearchLayout = () => {
}, []);
return (
<ScreenWrapper padded={false}>
<div className="flex flex-col h-full">
{/* Header */}
<div className="bg-background dark:bg-overlay hover:z-30 sticky top-0 border-b">
<div className="flex items-center gap-4 px-6 py-4">
<div className="flex-1">
<ProjectSearchBar
onChange={(project) => {
navigate(
`/${project.organization.slug}/projects/${project.id}`,
);
}}
/>
</div>
<div className="flex items-center gap-3">
<Button
variant="secondary"
size="icon"
onClick={() => {
fetchOrgSlug().then((organizationSlug) => {
navigate(`/${organizationSlug}/projects/create`);
});
}}
>
<Plus className="w-4 h-4" />
</Button>
<Button variant="ghost" size="icon">
<Bell className="w-4 h-4" />
</Button>
{user?.name && (
<p className="text-sm tracking-[-0.006em] text-muted-foreground">
{formatAddress(user.name)}
</p>
)}
</div>
</div>
</div>
<>
<Outlet />
</>
// <ScreenWrapper padded={false}>
// <div className="flex flex-col h-full">
// {/* Header */}
// <div className="bg-background dark:bg-overlay hover:z-30 sticky top-0 border-b">
// <div className="flex items-center gap-4 px-6 py-4">
// <div className="flex-1">
// <ProjectSearchBar
// onChange={(project) => {
// navigate(
// `/${project.organization.slug}/projects/${project.id}`,
// );
// }}
// />
// </div>
// <div className="flex items-center gap-3">
// <Button
// variant="secondary"
// size="icon"
// onClick={() => {
// fetchOrgSlug().then((organizationSlug) => {
// navigate(`/${organizationSlug}/projects/create`);
// });
// }}
// >
// <Plus className="w-4 h-4" />
// </Button>
// <Button variant="ghost" size="icon">
// <Bell className="w-4 h-4" />
// </Button>
// {user?.name && (
// <p className="text-sm tracking-[-0.006em] text-muted-foreground">
// {formatAddress(user.name)}
// </p>
// )}
// </div>
// </div>
// </div>
{/* Content */}
<div className="flex-1 overflow-y-auto">
<Outlet />
</div>
</div>
</ScreenWrapper>
// {/* Content */}
// <div className="flex-1 overflow-y-auto">
// <Outlet />
// </div>
// </div>
// </ScreenWrapper>
);
};

View File

@ -1,6 +1,7 @@
import { ScreenWrapper } from '@/components/layout';
import CheckBalanceIframe from '@/components/projects/create/CheckBalanceIframe';
import { Button } from '@/components/ui';
import { LaconicMark } from '@/laconic-assets/laconic-mark';
import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useMediaQuery } from 'usehooks-ts';
@ -39,8 +40,9 @@ const BuyPrepaidService = () => {
);
return (
<ScreenWrapper className="flex flex-col items-center justify-center">
<Button {...buttonSize}>
<ScreenWrapper className="flex flex-col items-center justify-center h-full">
<LaconicMark />
<Button {...buttonSize} variant="secondary">
<a href={SHOPIFY_APP_URL} target="_blank" rel="noopener noreferrer">
Buy prepaid service
</a>

View File

@ -6,6 +6,7 @@ import { Header, ScreenWrapper } from '@/components/layout';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogPortal } from '@/components/ui/dialog';
import { ArrowLeft } from 'lucide-react';
import { cn } from 'tailwind-variants';
export interface CreateProjectLayoutProps
extends ComponentPropsWithoutRef<'section'> {}
@ -31,20 +32,20 @@ export const CreateProjectLayout = ({
/>
);
// if (isDesktopView) {
// return (
// <ScreenWrapper className={cn('h-full flex-col', className)} {...props}>
// {header}
// <div className="flex-1 overflow-y-auto">
// <Outlet />
// </div>
// </ScreenWrapper>
// );
// }
if (isDesktopView) {
return (
<ScreenWrapper className={cn('h-full flex-col', className)()} {...props}>
{header}
<div className="flex-1 overflow-y-auto">
<Outlet />
</div>
</ScreenWrapper>
);
}
return (
<ScreenWrapper>
<Header
{/* <Header
title="Create new project"
backButton={
<Button variant="ghost" size="icon" asChild className="mr-2">
@ -53,7 +54,7 @@ export const CreateProjectLayout = ({
</Link>
</Button>
}
/>
/> */}
<Dialog modal={false} open={true}>
<DialogPortal>
<div className="md:hidden fixed inset-0 p-1 overflow-y-auto">

View File

@ -1,4 +1,10 @@
import templates from '@/assets/templates';
import {
TabWrapper,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/layout/screen-wrapper/TabWrapper';
import ConnectAccount from '@/components/projects/create/ConnectAccount';
import { RepositoryList } from '@/components/projects/create/RepositoryList';
import { TemplateCard } from '@/components/projects/create/TemplateCard';
@ -9,28 +15,46 @@ const NewProject = () => {
const { octokit, updateAuth, isAuth } = useOctokit();
return isAuth ? (
<>
<div className="space-y-3">
<Heading as="h3" className="pl-1 text-lg font-medium">
Start with a template
</Heading>
<div className="sm:grid-cols-2 xl:grid-cols-3 grid grid-cols-1 gap-3">
{templates.map((template) => {
return (
<TemplateCard
isGitAuth={Boolean(octokit)}
template={template}
key={template.id}
/>
);
})}
// <ScreenWrapper>
// <Heading as="h2" className="mb-6 text-2xl font-semibold">
// Create a new project
// </Heading>
<TabWrapper>
<TabsList className="mb-6">
<TabsTrigger value="templates">Start with a template</TabsTrigger>
<TabsTrigger value="repository">Import a repository</TabsTrigger>
</TabsList>
<TabsContent value="templates" className="space-y-6">
<div className="space-y-3">
<Heading as="h3" className="pl-1 text-lg font-medium">
Start with a template
</Heading>
<div className="flex flex-col space-y-3">
{templates.map((template) => {
return (
<TemplateCard
isGitAuth={Boolean(octokit)}
template={template}
key={template.id}
/>
);
})}
</div>
</div>
</div>
<Heading as="h3" className="pl-1 mt-10 mb-3 text-lg font-medium">
Import a repository
</Heading>
<RepositoryList />
</>
</TabsContent>
<TabsContent value="repository" className="space-y-6">
<div className="space-y-3">
<Heading as="h3" className="pl-1 text-lg font-medium">
Import a repository
</Heading>
<RepositoryList />
</div>
</TabsContent>
</TabWrapper>
) : (
<ConnectAccount onAuth={updateAuth} />
);

View File

@ -0,0 +1,225 @@
# Current Routing Structure
This document provides an overview of the current routing implementation in the frontend package.
The routing is currently spread across multiple files, making it difficult to follow and maintain.
## Main Router Configuration
The main router is defined in `src/App.tsx` using `createBrowserRouter` from React Router v6:
```tsx
// src/App.tsx
const router = createBrowserRouter([
{
path: ':orgSlug',
element: <DashboardLayout />,
children: [
{
element: <ProjectSearchLayout />,
children: [
{
path: '',
element: <ProjectsScreen />,
},
{
path: 'projects',
children: projectsRoutesWithSearch,
},
],
},
{
path: 'settings',
element: <Settings />,
},
{
path: 'projects',
children: projectsRoutesWithoutSearch,
},
],
},
{
path: '/',
element: <Index />,
},
{
path: '/login',
element: <AuthPage />,
},
{
path: '/buy-prepaid-service',
element: <BuyPrepaidService />,
errorElement: <div>Something went wrong!</div>,
},
]);
```
The router is then provided to the application using `RouterProvider`:
```tsx
// src/App.tsx
return (
<ThemeProvider attribute='class' defaultTheme='system' enableSystem>
<RouterProvider router={router} fallbackElement={<div>Loading...</div>} />
</ThemeProvider>
);
```
## Route Definitions in Other Files
The main router imports route definitions from other files:
### Project Routes
Project routes are split into two arrays defined in
`src/pages/org-slug/projects/project-routes.tsx`:
```tsx
// src/pages/org-slug/projects/project-routes.tsx
export const projectsRoutesWithoutSearch = [
{
path: 'create',
element: <CreateProjectLayout />,
children: createProjectRoutes,
},
{
path: ':id/settings/domains/add',
element: <AddDomain />,
children: addDomainRoutes,
},
];
export const projectsRoutesWithSearch = [
{
path: ':id',
element: <Id />,
children: projectTabRoutes,
},
];
```
### Create Project Routes
Create project routes are defined in `src/pages/org-slug/projects/create/create-project-routes.tsx`:
```tsx
// src/pages/org-slug/projects/create/create-project-routes.tsx
export const createProjectRoutes = [
{
index: true,
element: <NewProject />,
},
{
path: 'template',
element: <CreateWithTemplate />,
children: templateRoutes,
},
{
path: 'success/:id',
element: <Id />,
},
{
path: 'configure',
element: <Configure />,
},
{
path: 'deploy',
element: <Deploy />,
},
];
```
### Project Tab Routes
Project tab routes are defined in `src/pages/org-slug/projects/id/routes.tsx`:
```tsx
// src/pages/org-slug/projects/id/routes.tsx
export const projectTabRoutes = [
{
index: true,
element: <OverviewTabPanel />,
},
{
path: 'deployments',
element: <DeploymentsTabPanel />,
},
{
path: 'integrations',
element: <Integrations />,
},
{
path: 'settings',
element: <SettingsTabPanel />,
children: settingsTabRoutes,
},
];
```
### Settings Tab Routes
Settings tab routes are defined in the same file:
```tsx
// src/pages/org-slug/projects/id/routes.tsx
export const settingsTabRoutes = [
{
index: true,
element: <GeneralTabPanel />,
},
{
path: 'domains',
element: <Domains />,
},
{
path: 'git',
element: <GitTabPanel />,
},
{
path: 'environment-variables',
element: <EnvironmentVariablesTabPanel />,
},
{
path: 'collaborators',
element: <CollaboratorsTabPanel />,
},
];
```
## Layout Components
The application uses several layout components that render the `Outlet` component from React Router
to display nested routes:
### DashboardLayout
```tsx
// src/layouts/DashboardLayout.tsx
export const DashboardLayout = ({ className, ...props }: DashboardLayoutProps) => {
return (
<ThemeProvider attribute='class' defaultTheme='system' enableSystem>
<section {...props} className={cn('h-full', className)}>
<WalletContextProvider>
<OctokitProviderWithRouter>
<NavigationWrapper>
<Outlet />
</NavigationWrapper>
</OctokitProviderWithRouter>
</WalletContextProvider>
</section>
</ThemeProvider>
);
};
```
## Current Issues
1. **Scattered Route Definitions**: Routes are defined across multiple files, making it difficult to
understand the overall routing structure.
2. **Inconsistent Error Handling**: Only some routes have error elements defined.
3. **No Centralized Loading State**: Loading states are handled inconsistently or not at all.
4. **No Lazy Loading**: Components are imported directly, without using code splitting.
5. **No Standardized 404 Handling**: There's no catch-all route for handling 404 errors.
6. **Limited Type Safety**: Route definitions lack comprehensive TypeScript typing.
7. **No Reusable Error Boundaries**: Error handling is ad-hoc rather than using reusable components.
These issues will be addressed in the new routing strategy.

View File

@ -0,0 +1,82 @@
# Routing Consolidation Strategy
This document serves as the main entry point for the routing consolidation strategy for the frontend
package. The goal is to consolidate the routing into a centralized configuration while maintaining
all existing functionality.
## Table of Contents
1. [Current Routing Structure](./CURRENT_ROUTING.md)
2. [New Routing Strategy](./ROUTING_STRATEGY.md)
3. [Implementation Plan](./ROUTING_IMPLEMENTATION.md)
## Overview
The current routing implementation in the frontend package is spread across multiple files, making
it difficult to follow and maintain. The new routing strategy aims to consolidate the routing into a
centralized configuration, while also adding support for error boundaries, loading states, lazy
loading, and 404 handling.
## Key Benefits
- **Single Source of Truth**: All routes defined in a centralized location
- **Improved Error Handling**: Consistent error boundaries for all routes
- **Better Loading States**: Standardized loading states using the LoadingOverlay component
- **Code Splitting**: Lazy loading of components for improved performance
- **Graceful 404 Handling**: Dedicated NotFound component for handling non-existent routes
- **Type Safety**: Comprehensive TypeScript typing for route definitions
## Architecture Diagram
```mermaid
graph TD
A[App.tsx] --> B[routes/index.tsx]
B --> C[Route Configuration]
C --> D[Public Routes]
C --> E[Protected Routes]
C --> F[Catch-all Route]
D --> G[Lazy-loaded Components]
E --> H[Lazy-loaded Components]
G --> I[Error Boundaries]
H --> I
I --> J[Loading States]
style A fill:#d4f1f9,stroke:#05a4c9
style B fill:#d4f1f9,stroke:#05a4c9
style C fill:#d4f1f9,stroke:#05a4c9
style F fill:#ffe6cc,stroke:#d79b00
style I fill:#f8cecc,stroke:#b85450
style J fill:#d5e8d4,stroke:#82b366
```
## Implementation Summary
The implementation plan is divided into the following steps:
1. Create directory structure
2. Create error boundary component
3. Create NotFound component
4. Create route types
5. Create centralized route configuration
6. Update App.tsx
7. Update nested route files
8. Test the implementation
9. Refactor to use the `lazy` property (optional)
For detailed information about each step, see the
[Implementation Plan](./ROUTING_IMPLEMENTATION.md).
## Getting Started
To get started with the implementation, follow these steps:
1. Read the [Current Routing Structure](./CURRENT_ROUTING.md) to understand the existing routing
implementation.
2. Review the [New Routing Strategy](./ROUTING_STRATEGY.md) to understand the proposed changes.
3. Follow the [Implementation Plan](./ROUTING_IMPLEMENTATION.md) to implement the changes.
## Conclusion
The routing consolidation strategy provides a clear path to improving the routing implementation in
the frontend package. By centralizing the route definitions and adding support for error boundaries,
loading states, lazy loading, and 404 handling, the routing system will be more maintainable,
performant, and user-friendly.

View File

@ -0,0 +1,441 @@
# Routing Implementation Plan
This document outlines the step-by-step implementation plan for consolidating the routing in the
frontend package. The goal is to implement the new routing strategy without changing any existing
functionality.
## Prerequisites
Before starting the implementation, verify the following dependencies:
```bash
# Check React Router version (should be v6.4+)
yarn list react-router-dom
```
If the React Router version is below v6.4, update it:
```bash
yarn add react-router-dom@latest
```
## Implementation Steps
### 1. Create Directory Structure
Create the necessary directories for the new routing structure:
```bash
mkdir -p packages/frontend/src/routes
mkdir -p packages/frontend/src/components/error
```
### 2. Create Error Boundary Component
Create a reusable error boundary component:
```tsx
// packages/frontend/src/components/error/ErrorBoundary.tsx
import { useRouteError, isRouteErrorResponse, useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
export function ErrorBoundary() {
const error = useRouteError();
const navigate = useNavigate();
// Handle different types of errors
let errorMessage = 'An unexpected error occurred';
let statusCode = 500;
if (isRouteErrorResponse(error)) {
statusCode = error.status;
errorMessage = error.statusText || errorMessage;
} else if (error instanceof Error) {
errorMessage = error.message;
}
return (
<div className='flex flex-col items-center justify-center min-h-screen p-4'>
<h1 className='text-4xl font-bold mb-4'>Something went wrong</h1>
<p className='text-xl mb-6'>{errorMessage}</p>
<div className='flex gap-4'>
<Button onClick={() => navigate(-1)}>Go Back</Button>
<Button onClick={() => navigate('/')}>Go Home</Button>
</div>
</div>
);
}
```
### 3. Create NotFound Component
Create a component for handling 404 errors:
```tsx
// packages/frontend/src/components/error/NotFound.tsx
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
export function NotFound() {
return (
<div className='flex flex-col items-center justify-center min-h-screen p-4'>
<h1 className='text-4xl font-bold mb-4'>404 - Page Not Found</h1>
<p className='text-xl mb-6'>The page you are looking for does not exist.</p>
<Button asChild>
<Link to='/'>Go Home</Link>
</Button>
</div>
);
}
```
### 4. Create Route Types
Define TypeScript types for route objects:
```tsx
// packages/frontend/src/types/route.ts
import { RouteObject } from 'react-router-dom';
export interface AppRouteObject extends RouteObject {
auth?: boolean; // Whether the route requires authentication
title?: string; // Page title
children?: AppRouteObject[]; // Nested routes
}
```
### 5. Create Route Configuration
Create the centralized route configuration:
```tsx
// packages/frontend/src/routes/index.tsx
import { createBrowserRouter, RouteObject } from 'react-router-dom';
import { lazy, Suspense } from 'react';
import { ErrorBoundary } from '@/components/error/ErrorBoundary';
import { NotFound } from '@/components/error/NotFound';
import { LoadingOverlay } from '@/components/loading/loading-overlay';
import { AppRouteObject } from '@/types/route';
// Lazy load components
const Index = lazy(() => import('@/pages/index'));
const AuthPage = lazy(() => import('@/pages/AuthPage'));
const BuyPrepaidService = lazy(() => import('@/pages/BuyPrepaidService'));
const DashboardLayout = lazy(() => import('@/layouts/DashboardLayout'));
const ProjectSearchLayout = lazy(() => import('@/layouts/ProjectSearch'));
const ProjectsScreen = lazy(() => import('@/pages/org-slug/ProjectsScreen'));
const Settings = lazy(() => import('@/pages/org-slug/Settings'));
// Import route definitions from existing files
import {
projectsRoutesWithoutSearch,
projectsRoutesWithSearch,
} from '@/pages/org-slug/projects/project-routes';
// Route definitions
const routes: AppRouteObject[] = [
{
path: ':orgSlug',
element: (
<Suspense fallback={<LoadingOverlay />}>
<DashboardLayout />
</Suspense>
),
errorElement: <ErrorBoundary />,
children: [
{
element: (
<Suspense fallback={<LoadingOverlay />}>
<ProjectSearchLayout />
</Suspense>
),
children: [
{
path: '',
element: (
<Suspense fallback={<LoadingOverlay />}>
<ProjectsScreen />
</Suspense>
),
},
{
path: 'projects',
children: projectsRoutesWithSearch,
},
],
},
{
path: 'settings',
element: (
<Suspense fallback={<LoadingOverlay />}>
<Settings />
</Suspense>
),
},
{
path: 'projects',
children: projectsRoutesWithoutSearch,
},
],
},
{
path: '/',
element: (
<Suspense fallback={<LoadingOverlay />}>
<Index />
</Suspense>
),
errorElement: <ErrorBoundary />,
},
{
path: '/login',
element: (
<Suspense fallback={<LoadingOverlay />}>
<AuthPage />
</Suspense>
),
errorElement: <ErrorBoundary />,
},
{
path: '/buy-prepaid-service',
element: (
<Suspense fallback={<LoadingOverlay />}>
<BuyPrepaidService />
</Suspense>
),
errorElement: <ErrorBoundary />,
},
{
path: '*',
element: <NotFound />,
},
];
// Create and export the router
export const router = createBrowserRouter(routes);
```
### 6. Update App.tsx
Update the App.tsx file to use the centralized router:
```tsx
// packages/frontend/src/App.tsx
import { ThemeProvider } from 'next-themes';
import { useEffect } from 'react';
import { RouterProvider } from 'react-router-dom';
import { router } from './routes';
import { BASE_URL } from './utils/constants';
/**
* Main application component.
* Sets up routing, authentication, and theme provider.
* @returns {JSX.Element} The rendered application.
*/
function App() {
// Hacky way of checking session
// TODO: Handle redirect backs
useEffect(() => {
fetch(`${BASE_URL}/auth/session`, {
credentials: 'include',
}).then(res => {
const path = window.location.pathname;
console.log(res);
if (res.status !== 200) {
localStorage.clear();
if (path !== '/login') {
window.location.pathname = '/login';
}
} else {
if (path === '/login') {
window.location.pathname = '/';
}
}
});
}, []);
return (
<ThemeProvider attribute='class' defaultTheme='system' enableSystem>
<RouterProvider router={router} fallbackElement={<LoadingOverlay solid />} />
</ThemeProvider>
);
}
export default App;
```
### 7. Update Nested Route Files
Update the nested route files to use the new error boundary and loading overlay components. For
example:
```tsx
// packages/frontend/src/pages/org-slug/projects/project-routes.tsx
import { lazy, Suspense } from 'react';
import { LoadingOverlay } from '@/components/loading/loading-overlay';
import { ErrorBoundary } from '@/components/error/ErrorBoundary';
// Lazy load components
const CreateProjectLayout = lazy(() => import('./create/CreateProjectLayout'));
const Id = lazy(() => import('./Id'));
const AddDomain = lazy(() => import('./id/settings/domains/add'));
// Import route definitions
import { createProjectRoutes } from './create/create-project-routes';
import { projectTabRoutes } from './id/routes';
import { addDomainRoutes } from './id/settings/domains/add/routes';
export const projectsRoutesWithoutSearch = [
{
path: 'create',
element: (
<Suspense fallback={<LoadingOverlay />}>
<CreateProjectLayout />
</Suspense>
),
errorElement: <ErrorBoundary />,
children: createProjectRoutes,
},
{
path: ':id/settings/domains/add',
element: (
<Suspense fallback={<LoadingOverlay />}>
<AddDomain />
</Suspense>
),
errorElement: <ErrorBoundary />,
children: addDomainRoutes,
},
];
export const projectsRoutesWithSearch = [
{
path: ':id',
element: (
<Suspense fallback={<LoadingOverlay />}>
<Id />
</Suspense>
),
errorElement: <ErrorBoundary />,
children: projectTabRoutes,
},
];
```
### 8. Test the Implementation
Test the implementation to ensure that all routes work as expected:
```bash
# Start the development server
yarn dev
```
Test the following scenarios:
- Navigate to all existing routes
- Test error handling by intentionally causing errors
- Test 404 handling by navigating to non-existent routes
- Test loading states by throttling the network in the browser developer tools
### 9. Refactor to Use the `lazy` Property (Optional)
Once the initial implementation is working, refactor the route definitions to use the `lazy`
property instead of manually wrapping components in Suspense:
```tsx
// packages/frontend/src/routes/index.tsx
import { createBrowserRouter } from 'react-router-dom';
import { ErrorBoundary } from '@/components/error/ErrorBoundary';
import { NotFound } from '@/components/error/NotFound';
import { AppRouteObject } from '@/types/route';
// Route definitions
const routes: AppRouteObject[] = [
{
path: ':orgSlug',
lazy: () => import('@/layouts/DashboardLayout'),
errorElement: <ErrorBoundary />,
children: [
{
lazy: () => import('@/layouts/ProjectSearch'),
children: [
{
path: '',
lazy: () => import('@/pages/org-slug/ProjectsScreen'),
},
{
path: 'projects',
children: projectsRoutesWithSearch,
},
],
},
{
path: 'settings',
lazy: () => import('@/pages/org-slug/Settings'),
},
{
path: 'projects',
children: projectsRoutesWithoutSearch,
},
],
},
{
path: '/',
lazy: () => import('@/pages/index'),
errorElement: <ErrorBoundary />,
},
{
path: '/login',
lazy: () => import('@/pages/AuthPage'),
errorElement: <ErrorBoundary />,
},
{
path: '/buy-prepaid-service',
lazy: () => import('@/pages/BuyPrepaidService'),
errorElement: <ErrorBoundary />,
},
{
path: '*',
element: <NotFound />,
},
];
// Create and export the router
export const router = createBrowserRouter(routes);
```
## Implementation Timeline
| Step | Description | Estimated Time |
| --------- | ---------------------------------------------- | -------------- |
| 1 | Create directory structure | 5 minutes |
| 2 | Create error boundary component | 15 minutes |
| 3 | Create NotFound component | 15 minutes |
| 4 | Create route types | 10 minutes |
| 5 | Create route configuration | 30 minutes |
| 6 | Update App.tsx | 10 minutes |
| 7 | Update nested route files | 30 minutes |
| 8 | Test the implementation | 30 minutes |
| 9 | Refactor to use the `lazy` property (optional) | 30 minutes |
| **Total** | | **2-3 hours** |
## Rollback Plan
In case of issues, the implementation can be rolled back by reverting to the original App.tsx file:
```bash
# Revert App.tsx
git checkout -- packages/frontend/src/App.tsx
```
## Future Improvements
After the initial implementation, consider the following improvements:
1. **Route Guards**: Implement route guards for protected routes.
2. **Route Metadata**: Add metadata to routes for SEO and analytics.
3. **Route Transitions**: Add transitions between routes for a smoother user experience.
4. **Route Code Generation**: Create a script to generate route files from a configuration file.
5. **Route Testing**: Add tests for route components and configurations.

View File

@ -0,0 +1,238 @@
# Routing Strategy
This document outlines the new routing strategy for the frontend package. The goal is to consolidate
the routing into a centralized configuration while maintaining all existing functionality.
## Core Principles
1. **Single Source of Truth**: All routes will be defined in a centralized location.
2. **Type Safety**: Comprehensive TypeScript typing for route definitions.
3. **Error Handling**: Consistent error boundaries for all routes.
4. **Loading States**: Standardized loading states using the LoadingOverlay component.
5. **Code Splitting**: Lazy loading of components for improved performance.
6. **404 Handling**: Graceful handling of not found routes.
## Architecture Overview
```mermaid
graph TD
A[App.tsx] --> B[routes/index.tsx]
B --> C[Route Configuration]
C --> D[Public Routes]
C --> E[Protected Routes]
C --> F[Catch-all Route]
D --> G[Lazy-loaded Components]
E --> H[Lazy-loaded Components]
G --> I[Error Boundaries]
H --> I
I --> J[Loading States]
style A fill:#d4f1f9,stroke:#05a4c9
style B fill:#d4f1f9,stroke:#05a4c9
style C fill:#d4f1f9,stroke:#05a4c9
style F fill:#ffe6cc,stroke:#d79b00
style I fill:#f8cecc,stroke:#b85450
style J fill:#d5e8d4,stroke:#82b366
```
## Centralized Route Configuration
All routes will be defined in a single file: `src/routes/index.tsx`. This file will export a router
instance created using `createBrowserRouter` from React Router v6.
```tsx
// src/routes/index.tsx
import { createBrowserRouter, RouteObject } from 'react-router-dom';
import { ErrorBoundary } from '@/components/error/ErrorBoundary';
import { NotFound } from '@/components/error/NotFound';
// Type definitions for route objects
export interface AppRouteObject extends RouteObject {
// Additional properties for our routes
auth?: boolean;
title?: string;
}
// Route definitions
const routes: AppRouteObject[] = [
// Public routes
{
path: '/',
element: <RootLayout />,
errorElement: <ErrorBoundary />,
children: [
// Home page
{
index: true,
lazy: () => import('@/pages/Index'),
},
// Login page
{
path: 'login',
lazy: () => import('@/pages/AuthPage'),
},
// Buy prepaid service
{
path: 'buy-prepaid-service',
lazy: () => import('@/pages/BuyPrepaidService'),
},
// Organization routes
{
path: ':orgSlug',
lazy: () => import('@/layouts/DashboardLayout'),
children: [
// Organization routes...
],
},
// 404 page
{
path: '*',
element: <NotFound />,
},
],
},
];
// Create and export the router
export const router = createBrowserRouter(routes);
```
## Error Boundaries
A reusable error boundary component will be created to handle errors consistently across the
application.
```tsx
// src/components/error/ErrorBoundary.tsx
import { useRouteError, isRouteErrorResponse, useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
export function ErrorBoundary() {
const error = useRouteError();
const navigate = useNavigate();
// Handle different types of errors
let errorMessage = 'An unexpected error occurred';
let statusCode = 500;
if (isRouteErrorResponse(error)) {
statusCode = error.status;
errorMessage = error.statusText || errorMessage;
} else if (error instanceof Error) {
errorMessage = error.message;
}
return (
<div className='flex flex-col items-center justify-center min-h-screen p-4'>
<h1 className='text-4xl font-bold mb-4'>Something went wrong</h1>
<p className='text-xl mb-6'>{errorMessage}</p>
<div className='flex gap-4'>
<Button onClick={() => navigate(-1)}>Go Back</Button>
<Button onClick={() => navigate('/')}>Go Home</Button>
</div>
</div>
);
}
```
## Loading States
The existing LoadingOverlay component will be used for loading states when lazy loading components.
```tsx
// Example of lazy loading with LoadingOverlay
const ProjectsPage = React.lazy(() => import('@/pages/org-slug/ProjectsScreen'));
// In route definition
{
path: 'projects',
element: (
<React.Suspense fallback={<LoadingOverlay />}>
<ProjectsPage />
</React.Suspense>
),
}
```
## Lazy Loading
Components will be lazy loaded using React.lazy and dynamic imports. This will be implemented using
the `lazy` property of route objects in React Router v6.4+.
```tsx
// Example of lazy loading with the lazy property
{
path: 'settings',
lazy: () => import('@/pages/org-slug/Settings'),
}
```
The `lazy` function should return an object with either an `element` property or a `Component`
property. For example:
```tsx
// src/pages/org-slug/Settings.tsx
export function Component() {
return <SettingsPage />;
}
```
Or:
```tsx
// src/pages/org-slug/Settings.tsx
export default function Settings() {
return <SettingsPage />;
}
```
## 404 Handling
A dedicated NotFound component will be created to handle 404 errors gracefully.
```tsx
// src/components/error/NotFound.tsx
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
export function NotFound() {
return (
<div className='flex flex-col items-center justify-center min-h-screen p-4'>
<h1 className='text-4xl font-bold mb-4'>404 - Page Not Found</h1>
<p className='text-xl mb-6'>The page you are looking for does not exist.</p>
<Button asChild>
<Link to='/'>Go Home</Link>
</Button>
</div>
);
}
```
## Type Safety
TypeScript types will be defined for route objects to ensure type safety and provide better
developer experience.
```tsx
// src/types/route.ts
import { RouteObject } from 'react-router-dom';
export interface AppRouteObject extends RouteObject {
auth?: boolean; // Whether the route requires authentication
title?: string; // Page title
children?: AppRouteObject[]; // Nested routes
}
```
## Benefits of the New Strategy
1. **Improved Maintainability**: All routes are defined in a single location, making it easier to
understand and maintain the routing structure.
2. **Better Error Handling**: Consistent error boundaries for all routes.
3. **Improved Performance**: Lazy loading of components reduces the initial bundle size and improves
load times.
4. **Better User Experience**: Standardized loading states and 404 handling.
5. **Type Safety**: Comprehensive TypeScript typing for route definitions.
6. **Easier Navigation**: Centralized route configuration makes it easier to navigate between routes
programmatically.
7. **Better Developer Experience**: Clear structure and documentation make it easier for developers
to work with the routing system.