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:
parent
cdd2bd2d89
commit
134d4ad316
@ -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)) {
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
)}
|
||||
|
@ -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>
|
||||
) : (
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
export * from './GQLClientContext';
|
||||
export * from './OctokitContext';
|
||||
export * from './OctokitProviderWithRouter';
|
||||
export * from './WalletContext';
|
||||
export * from './WalletContextProvider';
|
||||
|
@ -36,7 +36,6 @@ export const DashboardLayout = ({
|
||||
<OctokitProviderWithRouter>
|
||||
<NavigationWrapper>
|
||||
<Outlet />
|
||||
|
||||
</NavigationWrapper>
|
||||
</OctokitProviderWithRouter>
|
||||
</section>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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} />
|
||||
);
|
||||
|
225
qwrk/docs/frontend/CURRENT_ROUTING.md
Normal file
225
qwrk/docs/frontend/CURRENT_ROUTING.md
Normal 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.
|
82
qwrk/docs/frontend/ROUTING.md
Normal file
82
qwrk/docs/frontend/ROUTING.md
Normal 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.
|
441
qwrk/docs/frontend/ROUTING_IMPLEMENTATION.md
Normal file
441
qwrk/docs/frontend/ROUTING_IMPLEMENTATION.md
Normal 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.
|
238
qwrk/docs/frontend/ROUTING_STRATEGY.md
Normal file
238
qwrk/docs/frontend/ROUTING_STRATEGY.md
Normal 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.
|
Loading…
Reference in New Issue
Block a user