Added react-router

This commit is contained in:
Gregory Schier
2023-02-25 18:04:14 -08:00
parent 93105a3e89
commit 83bb18df03
12 changed files with 180 additions and 67 deletions

63
package-lock.json generated
View File

@@ -31,7 +31,8 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0", "react-helmet-async": "^1.3.0",
"react-query": "^3.39.3" "react-query": "^3.39.3",
"react-router-dom": "^6.8.1"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^1.2.2", "@tauri-apps/cli": "^1.2.2",
@@ -1602,6 +1603,14 @@
"@babel/runtime": "^7.13.10" "@babel/runtime": "^7.13.10"
} }
}, },
"node_modules/@remix-run/router": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.2.tgz",
"integrity": "sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA==",
"engines": {
"node": ">=14"
}
},
"node_modules/@swc/core": { "node_modules/@swc/core": {
"version": "1.3.35", "version": "1.3.35",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.35.tgz", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.35.tgz",
@@ -5425,6 +5434,36 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
}, },
"node_modules/react-router": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz",
"integrity": "sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==",
"dependencies": {
"@remix-run/router": "1.3.2"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/react-router-dom": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.8.1.tgz",
"integrity": "sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==",
"dependencies": {
"@remix-run/router": "1.3.2",
"react-router": "6.8.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/react-style-singleton": { "node_modules/react-style-singleton": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@@ -7571,6 +7610,11 @@
"@babel/runtime": "^7.13.10" "@babel/runtime": "^7.13.10"
} }
}, },
"@remix-run/router": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.2.tgz",
"integrity": "sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA=="
},
"@swc/core": { "@swc/core": {
"version": "1.3.35", "version": "1.3.35",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.35.tgz", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.35.tgz",
@@ -10169,6 +10213,23 @@
} }
} }
}, },
"react-router": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz",
"integrity": "sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==",
"requires": {
"@remix-run/router": "1.3.2"
}
},
"react-router-dom": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.8.1.tgz",
"integrity": "sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==",
"requires": {
"@remix-run/router": "1.3.2",
"react-router": "6.8.1"
}
},
"react-style-singleton": { "react-style-singleton": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",

View File

@@ -35,7 +35,8 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0", "react-helmet-async": "^1.3.0",
"react-query": "^3.39.3" "react-query": "^3.39.3",
"react-router-dom": "^6.8.1"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^1.2.2", "@tauri-apps/cli": "^1.2.2",

View File

@@ -80,7 +80,7 @@ pub async fn create_workspace(
description: &str, description: &str,
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
) -> Result<Workspace, sqlx::Error> { ) -> Result<Workspace, sqlx::Error> {
let id = generate_id("wrk"); let id = generate_id("wk");
sqlx::query!( sqlx::query!(
r#" r#"
INSERT INTO workspaces (id, name, description) INSERT INTO workspaces (id, name, description)
@@ -106,7 +106,7 @@ pub async fn upsert_request(
url: &str, url: &str,
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
) -> Result<Request, sqlx::Error> { ) -> Result<Request, sqlx::Error> {
let id = generate_id("wrk"); let id = generate_id("rq");
sqlx::query!( sqlx::query!(
r#" r#"
INSERT INTO requests (id, workspace_id, name, url, method, body, updated_at, headers) INSERT INTO requests (id, workspace_id, name, url, method, body, updated_at, headers)

View File

@@ -9,7 +9,8 @@ import { Sidebar } from './components/Sidebar';
import { UrlBar } from './components/UrlBar'; import { UrlBar } from './components/UrlBar';
import { Grid } from './components/Grid'; import { Grid } from './components/Grid';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useRequests, useWorkspace, useWorkspaces } from './hooks/useWorkspaces'; import { useRequests } from './hooks/useWorkspaces';
import { useParams } from 'react-router-dom';
interface Response { interface Response {
url: string; url: string;
@@ -21,17 +22,26 @@ interface Response {
headers: Record<string, string>; headers: Record<string, string>;
} }
type Params = {
workspaceId: string;
requestId?: string;
};
function App() { function App() {
const { data } = useWorkspace(); const p = useParams<Params>();
console.log('DATA', data); const workspaceId = p.workspaceId ?? '';
const requestId = p.requestId;
const { data: requests } = useRequests(workspaceId);
const request = requests?.find((r) => r.id === requestId);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [response, setResponse] = useState<Response | null>(null); const [response, setResponse] = useState<Response | null>(null);
const [url, setUrl] = useState<string>('https://go-server.schier.dev/debug'); const [url, setUrl] = useState<string>(request?.url ?? '');
const [body, setBody] = useState<string>(`{\n "foo": "bar"\n}`); const [body, setBody] = useState<string>(request?.body ?? '');
const [method, setMethod] = useState<{ label: string; value: string }>({ const [method, setMethod] = useState<{ label: string; value: string }>({
label: 'GET', label: request?.method ?? 'GET',
value: 'GET', value: request?.method ?? 'GET',
}); });
useEffect(() => { useEffect(() => {
@@ -44,9 +54,6 @@ function App() {
return () => document.documentElement.removeEventListener('keypress', listener); return () => document.documentElement.removeEventListener('keypress', listener);
}, []); }, []);
if (!data) return null;
const { requests, workspace } = data;
async function sendRequest() { async function sendRequest() {
setLoading(true); setLoading(true);
setError(null); setError(null);
@@ -73,7 +80,7 @@ function App() {
return ( return (
<> <>
<div className="grid grid-cols-[auto_1fr] h-full text-gray-900"> <div className="grid grid-cols-[auto_1fr] h-full text-gray-900">
<Sidebar requests={requests ?? []} workspaceId={workspace.id} /> <Sidebar requests={requests ?? []} workspaceId={workspaceId} requestId={requestId} />
<Grid cols={2}> <Grid cols={2}>
<VStack className="w-full"> <VStack className="w-full">
<HStack as={WindowDragRegion} items="center" className="pl-3 pr-1.5"> <HStack as={WindowDragRegion} items="center" className="pl-3 pr-1.5">

View File

@@ -1,29 +1,29 @@
import { ButtonHTMLAttributes, forwardRef } from 'react'; import { ButtonHTMLAttributes, ComponentPropsWithoutRef, ElementType } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { Icon } from './Icon'; import { Icon } from './Icon';
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & { export interface ButtonProps<T extends ElementType>
extends ButtonHTMLAttributes<HTMLButtonElement> {
color?: 'primary' | 'secondary'; color?: 'primary' | 'secondary';
size?: 'xs' | 'sm' | 'md'; size?: 'xs' | 'sm' | 'md';
justify?: 'start' | 'center'; justify?: 'start' | 'center';
forDropdown?: boolean; forDropdown?: boolean;
}; as?: T;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button( export function Button<T extends ElementType>({
{ className,
className, as,
justify = 'center', justify = 'center',
children, children,
size = 'md', size = 'md',
forDropdown, forDropdown,
color, color,
...props ...props
}: ButtonProps, }: ButtonProps<T> & Omit<ComponentPropsWithoutRef<T>, keyof ButtonProps<T>>) {
ref, const Component = as || 'button';
) {
return ( return (
<button <Component
ref={ref}
className={classnames( className={classnames(
className, className,
'rounded-md flex items-center', 'rounded-md flex items-center',
@@ -41,6 +41,6 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
> >
{children} {children}
{forDropdown && <Icon icon="triangle-down" className="ml-1 -mr-1" />} {forDropdown && <Icon icon="triangle-down" className="ml-1 -mr-1" />}
</button> </Component>
); );
}); }

View File

@@ -1,16 +1,9 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { DropdownMenuRadioGroup } from '@radix-ui/react-dropdown-menu'; import { DropdownMenuRadioGroup } from '@radix-ui/react-dropdown-menu';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { import { CheckIcon } from '@radix-ui/react-icons';
CheckIcon, import { forwardRef, HTMLAttributes, ReactNode } from 'react';
ChevronRightIcon,
DotFilledIcon,
HamburgerMenuIcon,
} from '@radix-ui/react-icons';
import { forwardRef, HTMLAttributes, ReactNode, useState } from 'react';
import { Button } from './Button';
import classnames from 'classnames'; import classnames from 'classnames';
import { HotKey } from './HotKey';
interface DropdownMenuRadioProps { interface DropdownMenuRadioProps {
children: ReactNode; children: ReactNode;
@@ -232,13 +225,19 @@ function DropdownMenuSeparator({ className, ...props }: DropdownMenu.DropdownMen
); );
} }
function DropdownMenuTrigger({ className, ...props }: DropdownMenu.DropdownMenuTriggerProps) { function DropdownMenuTrigger({
children,
className,
...props
}: DropdownMenu.DropdownMenuTriggerProps) {
return ( return (
<DropdownMenu.Trigger <DropdownMenu.Trigger
asChild asChild
className={classnames(className, 'focus:outline-none')} className={classnames(className, 'focus:outline-none')}
{...props} {...props}
/> >
<>{children}</>
</DropdownMenu.Trigger>
); );
} }

View File

@@ -1,16 +1,12 @@
import { forwardRef } from 'react';
import { Icon, IconProps } from './Icon'; import { Icon, IconProps } from './Icon';
import { Button, ButtonProps } from './Button'; import { Button, ButtonProps } from './Button';
type Props = Omit<IconProps, 'size'> & ButtonProps; type Props = Omit<IconProps, 'size'> & ButtonProps<typeof Button>;
export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton( export function IconButton({ icon, spin, ...props }: Props) {
{ icon, spin, ...props }: Props,
ref,
) {
return ( return (
<Button ref={ref} className="group" {...props}> <Button className="group" {...props}>
<Icon icon={icon} spin={spin} className="text-gray-700 group-hover:text-gray-900" /> <Icon icon={icon} spin={spin} className="text-gray-700 group-hover:text-gray-900" />
</Button> </Button>
); );
}); }

View File

@@ -3,17 +3,19 @@ import classnames from 'classnames';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import { Button } from './Button'; import { Button } from './Button';
import useTheme from '../hooks/useTheme'; import useTheme from '../hooks/useTheme';
import { HStack } from './Stacks'; import { HStack, VStack } from './Stacks';
import { WindowDragRegion } from './WindowDragRegion'; import { WindowDragRegion } from './WindowDragRegion';
import { Request } from '../hooks/useWorkspaces'; import { Request } from '../hooks/useWorkspaces';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { Link } from 'react-router-dom';
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> { interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
workspaceId: string; workspaceId: string;
requests: Request[]; requests: Request[];
requestId?: string;
} }
export function Sidebar({ className, workspaceId, requests, ...props }: Props) { export function Sidebar({ className, requestId, workspaceId, requests, ...props }: Props) {
const { toggleTheme } = useTheme(); const { toggleTheme } = useTheme();
return ( return (
<div <div
@@ -35,11 +37,13 @@ export function Sidebar({ className, workspaceId, requests, ...props }: Props) {
}} }}
/> />
</HStack> </HStack>
<ul className="mx-2 py-2"> <VStack as="ul" className="py-2" space={1}>
{requests.map((r) => ( {requests.map((r) => (
<li key={r.id}> <li key={r.id} className="mx-2">
<Button <Button
className={classnames('w-full', false && 'bg-gray-50')} as={Link}
to={`/workspaces/${workspaceId}/requests/${r.id}`}
className={classnames('w-full', requestId === r.id && 'bg-gray-50')}
size="sm" size="sm"
justify="start" justify="start"
> >
@@ -47,7 +51,7 @@ export function Sidebar({ className, workspaceId, requests, ...props }: Props) {
</Button> </Button>
</li> </li>
))} ))}
</ul> </VStack>
</div> </div>
); );
} }

View File

@@ -27,10 +27,9 @@ export function useWorkspaces(): UseQueryResult<Workspace[]> {
}); });
} }
export function useRequests(): UseQueryResult<Request[]> { export function useRequests(workspaceId: string): UseQueryResult<Request[]> {
return useQuery('requests', async () => { return useQuery('requests', async () => {
const workspaces = (await invoke('workspaces')) as Workspace[]; const requests = (await invoke('requests', { workspaceId })) as Request[];
const requests = (await invoke('requests', { workspaceId: workspaces[0].id })) as Request[];
return requests.map(convertDates); return requests.map(convertDates);
}); });
} }

View File

@@ -1,29 +1,51 @@
import React from 'react'; import React from 'react';
import init, { greet } from 'hello';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import App from './App'; import App from './App';
import { HelmetProvider } from 'react-helmet-async'; import { HelmetProvider } from 'react-helmet-async';
import { MotionConfig } from 'framer-motion'; import { MotionConfig } from 'framer-motion';
import init, { greet } from 'hello';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { setTheme } from './lib/theme'; import { setTheme } from './lib/theme';
import './main.css';
import { QueryClient, QueryClientProvider } from 'react-query'; import { QueryClient, QueryClientProvider } from 'react-query';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { Layout } from './pages/Layout';
import { Workspaces } from './pages/Workspaces';
import './main.css';
setTheme(); setTheme();
await init(); await init();
greet(); greet();
await invoke('load_db'); await invoke('load_db');
const queryClient = new QueryClient(); const queryClient = new QueryClient();
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
path: '/',
element: <Workspaces />,
},
{
path: '/workspaces/:workspaceId',
element: <App />,
},
{
path: '/workspaces/:workspaceId/requests/:requestId',
element: <App />,
},
],
},
]);
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode> <React.StrictMode>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<MotionConfig transition={{ duration: 0.15 }}> <MotionConfig transition={{ duration: 0.15 }}>
<HelmetProvider> <HelmetProvider>
<App /> <RouterProvider router={router} />
</HelmetProvider> </HelmetProvider>
</MotionConfig> </MotionConfig>
</QueryClientProvider> </QueryClientProvider>

9
src-web/pages/Layout.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { Outlet } from 'react-router-dom';
export function Layout() {
return (
<div className="w-full h-full">
<Outlet />
</div>
);
}

View File

@@ -0,0 +1,15 @@
import { Link, useParams } from 'react-router-dom';
import { useWorkspaces } from '../hooks/useWorkspaces';
export function Workspaces() {
const workspaces = useWorkspaces();
return (
<ul className="p-12">
{workspaces.data?.map((w) => (
<Link key={w.id} to={`/workspaces/${w.id}`}>
{w.name}
</Link>
))}
</ul>
);
}