Add user menu sidebar (#195)
This commit is contained in:
26
public/kittycad-logomark.svg
Normal file
26
public/kittycad-logomark.svg
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<svg width="788" height="183" viewBox="0 0 788 183" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16.6075 166.835V161.454H5.84388V156.072H0.462097V43.0543H5.84388V37.6725H11.2257V32.2907H16.6075V26.9089H21.9892V16.1454H27.371V10.7636H32.7528V5.38179H38.1346V0H43.5164V5.38179H48.8982V10.7636H54.28V16.1454H59.6617V21.5271H75.8071V16.1454H81.1889V10.7636H86.5707V5.38179H91.9525V0H97.3342V5.38179H102.716V10.7636H108.098V16.1454H113.48V26.9089H118.861V32.2907H124.243V37.6725H129.625V43.0543H135.007V156.072H129.625V161.454H118.861V166.835H102.716V172.217H108.098V182.981H75.8071V172.217H81.1889V166.835H54.28V172.217H59.6617V182.981H27.371V172.217H32.7528V166.835H16.6075Z" fill="#101412"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.9892 26.9095L21.9892 26.9102V32.2919H16.6075V37.6737H11.2257V43.0555H5.84388V48.4364V53.8191V123.781V129.163V129.164V134.545V156.073H16.6075V161.455H38.1346V166.836V172.217H32.7528V177.599H54.28V172.217H48.8982V166.836V161.455H86.5707V166.836V172.217H81.1889V177.599H102.716V172.217H97.3342V166.836V161.455H118.861V156.073H129.625V134.545V129.164V129.163V123.781V53.8191V48.4364V43.0555H124.243V37.6737H118.861V32.2919H113.48V26.9102V26.9095H108.098V16.1459H102.716V10.7641H97.3342V5.38232H91.9525V10.7641H86.5707V16.1459H81.1889V21.5277H75.8071V26.9102H59.6617V21.5277H54.28V16.1459H48.8982V10.7641H43.5164V5.38232H38.1346V10.7641H32.7528V16.1459H27.371V26.9095H21.9892ZM11.2257 129.164H124.243V129.163H11.2257V129.164ZM11.2257 139.927V145.309H38.1346V139.927H11.2257ZM21.9893 150.691V156.072H38.1346V150.691H21.9893ZM97.3343 145.309V139.927H102.716V145.309H97.3343ZM108.098 139.927V145.309H113.48V139.927H108.098ZM118.861 145.309V139.927H124.243V145.309H118.861ZM97.3343 150.691V156.072H113.48V150.691H97.3343ZM48.8982 145.309H86.5707V156.073H48.8982V145.309Z" fill="#D0FF00"/>
|
||||||
|
<path d="M5.84388 129.163V123.781H129.625V129.163H5.84388Z" fill="#B1E515"/>
|
||||||
|
<path d="M21.9892 64.5812V59.1995H113.48V64.5812H118.861V107.636H113.48V113.017H21.9892V107.636H16.6075V64.5812H21.9892Z" fill="#1F2320"/>
|
||||||
|
<path d="M86.5707 86.1092V64.582H97.3343V86.1092H86.5707Z" fill="#D0FF00"/>
|
||||||
|
<path d="M59.6617 86.1092H75.8071V91.491H70.4253V102.255H86.5707V96.8727H91.9525V102.255H86.5707V107.636H48.8982V102.255H43.5164V96.8727H48.8982V102.255H65.0435V91.491H59.6617V86.1092Z" fill="#D0FF00"/>
|
||||||
|
<path d="M48.8982 80.7274V75.3456H32.7528V80.7274H27.371V75.3456H32.7528V69.9638H48.8982V75.3456H54.28V80.7274H48.8982Z" fill="#D0FF00"/>
|
||||||
|
<path d="M118.861 43.0534V37.6716H124.243V43.0534H118.861Z" fill="#92C51B"/>
|
||||||
|
<path d="M16.6075 43.0534V37.6716H11.2257V43.0534H16.6075Z" fill="#92C51B"/>
|
||||||
|
<path d="M113.48 37.6728V32.291H118.861V37.6728H113.48Z" fill="#92C51B"/>
|
||||||
|
<path d="M21.9892 37.6728V32.291H16.6075V37.6728H21.9892Z" fill="#92C51B"/>
|
||||||
|
<rect x="65.0435" y="26.9087" width="5.38179" height="10.7636" fill="#B1E515"/>
|
||||||
|
<path d="M86.5707 37.6723V32.2905H91.9525V26.9087H97.3342V32.2905H102.716V37.6723H86.5707Z" fill="#101412"/>
|
||||||
|
<path d="M38.1346 32.2905V26.9087H43.5164V32.2905H48.8982V37.6723H32.7528V32.2905H38.1346Z" fill="#101412"/>
|
||||||
|
<path d="M21.9892 129.163V123.781H38.1346V129.163H21.9892Z" fill="#92C51B"/>
|
||||||
|
<rect x="59.6617" y="26.9087" width="16.1454" height="5.38179" fill="#92C51B"/>
|
||||||
|
<path d="M191.977 42.5414V82.1808L230.437 40.331L246.794 52.7091L215.701 86.3068L257.109 142.745H229.848L200.966 102.074L191.977 111.8V142.745H170.315V42.5414H191.977Z" fill="#101412"/>
|
||||||
|
<path d="M286.165 49.3199C286.165 52.66 284.937 55.5089 282.481 57.8666C280.124 60.1261 277.275 61.2559 273.935 61.2559C270.594 61.2559 267.795 60.1261 265.535 57.8666C263.276 55.5089 262.146 52.66 262.146 49.3199C262.146 45.9797 263.276 43.1308 265.535 40.7731C267.795 38.4153 270.594 37.2365 273.935 37.2365C277.275 37.2365 280.124 38.4153 282.481 40.7731C284.937 43.1308 286.165 45.9797 286.165 49.3199ZM284.839 71.2763V142.745H263.177V71.2763H284.839Z" fill="#101412"/>
|
||||||
|
<path d="M332.949 145.25C324.108 145.25 316.789 142.991 310.993 138.472C305.295 133.953 302.446 127.322 302.446 118.578V89.1066H292.278V72.3078H302.446V54.6248L323.96 51.0882V72.3078H338.402L343.117 89.1066H323.96V115.336C323.96 118.775 324.845 121.624 326.613 123.883C328.381 126.044 330.935 127.125 334.276 127.125C335.553 127.125 336.928 126.929 338.402 126.536C339.875 126.143 341.349 125.602 342.822 124.915L349.159 140.24C347.489 141.615 345.033 142.794 341.791 143.777C338.549 144.759 335.602 145.25 332.949 145.25Z" fill="#101412"/>
|
||||||
|
<path d="M389.435 145.25C380.593 145.25 373.274 142.991 367.478 138.472C361.781 133.953 358.932 127.322 358.932 118.578V89.1066H348.764V72.3078H358.932V54.6248L380.446 51.0882V72.3078H394.887L399.602 89.1066H380.446V115.336C380.446 118.775 381.33 121.624 383.098 123.883C384.867 126.044 387.421 127.125 390.761 127.125C392.038 127.125 393.413 126.929 394.887 126.536C396.361 126.143 397.834 125.602 399.308 124.915L405.644 140.24C403.974 141.615 401.518 142.794 398.276 143.777C395.034 144.759 392.087 145.25 389.435 145.25Z" fill="#101412"/>
|
||||||
|
<path d="M428.679 145.103L431.184 139.061L405.249 73.9287L427.058 71.2763C429.711 78.3495 432.363 85.4227 435.016 92.4959C437.668 99.5691 440.272 106.642 442.826 113.715L457.856 71.2763H480.255L448.425 151.734C446.068 157.53 441.991 162.491 436.195 166.617C430.398 170.841 424.209 173.739 417.627 175.311L410.112 157.776C413.649 156.4 417.333 154.681 421.164 152.618C424.995 150.555 427.5 148.05 428.679 145.103Z" fill="#101412"/>
|
||||||
|
<path d="M508.208 142.745L492.371 126.907V50.9472L508.208 35.1094H555.953L571.867 50.9472V75.7034H550.109V61.557L545.42 56.8672H518.818L514.128 61.557V116.297L518.818 120.987H545.42L550.109 116.297V102.151H571.867V126.907L555.953 142.745H508.208Z" fill="#101412"/>
|
||||||
|
<path d="M588.792 142.745V50.9472L604.63 35.1094H654.296L670.134 50.9472V142.745H648.453V111.069H610.473V142.745H588.792ZM610.473 89.3885H648.453V61.557L643.763 56.8672H615.163L610.473 61.557V89.3885Z" fill="#101412"/>
|
||||||
|
<path d="M688.517 142.745V35.1094H752.561L768.399 50.9472V126.907L752.561 142.745H688.517ZM710.275 120.987H742.028L746.718 116.297V61.557L742.028 56.8672H710.275V120.987Z" fill="#101412"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB |
@ -27,7 +27,7 @@
|
|||||||
},
|
},
|
||||||
"http": {
|
"http": {
|
||||||
"request": true,
|
"request": true,
|
||||||
"scope": ["https://dev.kittycad.io/*", "https://kittycad.io/*"]
|
"scope": ["https://dev.kittycad.io/*", "https://kittycad.io/*", "https://api.dev.kittycad.io/*"]
|
||||||
},
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"open": true
|
"open": true
|
||||||
|
@ -22,6 +22,7 @@ import { EngineCommandManager } from './lang/std/engineConnection'
|
|||||||
import { isOverlap } from './lib/utils'
|
import { isOverlap } from './lib/utils'
|
||||||
import { SetToken } from './components/TokenInput'
|
import { SetToken } from './components/TokenInput'
|
||||||
import { AppHeader } from './components/AppHeader'
|
import { AppHeader } from './components/AppHeader'
|
||||||
|
import { isTauri } from './lib/isTauri'
|
||||||
import { KCLError } from './lang/errors'
|
import { KCLError } from './lang/errors'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
@ -85,6 +86,7 @@ export function App() {
|
|||||||
debugPanel: s.debugPanel,
|
debugPanel: s.debugPanel,
|
||||||
addKCLError: s.addKCLError,
|
addKCLError: s.addKCLError,
|
||||||
}))
|
}))
|
||||||
|
const showTauriTokenInput = isTauri() && !token
|
||||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||||
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
||||||
setCode(value)
|
setCode(value)
|
||||||
@ -271,8 +273,7 @@ export function App() {
|
|||||||
<AppHeader />
|
<AppHeader />
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<Allotment snap={true}>
|
<Allotment snap={true}>
|
||||||
<Allotment vertical defaultSizes={[5, 400, 1, 1, 200]} minSize={20}>
|
<Allotment vertical defaultSizes={[400, 1, 1, 200]} minSize={20}>
|
||||||
<SetToken />
|
|
||||||
<div className="h-full flex flex-col items-start">
|
<div className="h-full flex flex-col items-start">
|
||||||
<PanelHeader title="Editor" />
|
<PanelHeader title="Editor" />
|
||||||
<button
|
<button
|
||||||
|
84
src/Auth.tsx
84
src/Auth.tsx
@ -1,74 +1,32 @@
|
|||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import fetcher from './lib/fetcher'
|
import fetcher from './lib/fetcher'
|
||||||
import withBaseUrl from './lib/withBaseURL'
|
import withBaseUrl from './lib/withBaseURL'
|
||||||
import { App } from './App'
|
import { User, useStore } from './useStore'
|
||||||
import { SetToken } from './components/TokenInput'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useStore } from './useStore'
|
import { useEffect } from 'react'
|
||||||
import { createBrowserRouter, redirect, RouterProvider } from 'react-router-dom'
|
import { isTauri } from './lib/isTauri'
|
||||||
import { ErrorPage } from './components/ErrorPage'
|
|
||||||
import { Settings } from './routes/Settings'
|
|
||||||
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
// Wrapper around protected routes, used in src/Router.tsx
|
||||||
{
|
export const Auth = ({ children }: React.PropsWithChildren) => {
|
||||||
path: '/',
|
const { data: user, isLoading } = useSWR<
|
||||||
element: <App />,
|
User | Partial<{ error_code: string }>
|
||||||
errorElement: <ErrorPage />,
|
>(withBaseUrl('/user'), fetcher)
|
||||||
loader: () => {
|
const { token, setUser } = useStore((s) => ({
|
||||||
const store = localStorage.getItem('store')
|
|
||||||
if (store === null) {
|
|
||||||
return redirect('/onboarding')
|
|
||||||
} else {
|
|
||||||
const status = JSON.parse(store).state.onboardingStatus
|
|
||||||
if (status !== 'done' && status !== 'dismissed') {
|
|
||||||
return redirect('/onboarding/' + status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/settings',
|
|
||||||
element: <Settings />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/onboarding',
|
|
||||||
element: <Onboarding />,
|
|
||||||
children: onboardingRoutes,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
export const Auth = () => {
|
|
||||||
const { data: user } = useSWR(withBaseUrl('/user'), fetcher) as any
|
|
||||||
const { token } = useStore((s) => ({
|
|
||||||
token: s.token,
|
token: s.token,
|
||||||
|
setUser: s.setUser,
|
||||||
}))
|
}))
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const isLocalHost =
|
useEffect(() => {
|
||||||
typeof window !== 'undefined' && window.location.hostname === 'localhost'
|
if (user && 'id' in user) setUser(user)
|
||||||
|
}, [user, setUser])
|
||||||
|
|
||||||
if ((window as any).__TAURI__ && !token) {
|
if (
|
||||||
return <SetToken />
|
(isTauri() && !token) ||
|
||||||
|
(!isTauri() && !isLoading && !(user && 'id' in user))
|
||||||
|
) {
|
||||||
|
navigate('/signin')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user && !isLocalHost) {
|
return isLoading ? <>Loading...</> : <>{children}</>
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className=" bg-gray-800 p-1 px-4 rounded-r-lg pointer-events-auto flex items-center">
|
|
||||||
<a
|
|
||||||
className="font-bold mr-2 text-purple-400"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target={'_self'}
|
|
||||||
href={`https://dev.kittycad.io/signin?callbackUrl=${encodeURIComponent(
|
|
||||||
typeof window !== 'undefined' && window.location.href
|
|
||||||
)}`}
|
|
||||||
>
|
|
||||||
Sign in
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <RouterProvider router={router} />
|
|
||||||
}
|
}
|
||||||
|
60
src/Router.tsx
Normal file
60
src/Router.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { App } from './App'
|
||||||
|
import { createBrowserRouter, redirect, RouterProvider } from 'react-router-dom'
|
||||||
|
import { ErrorPage } from './components/ErrorPage'
|
||||||
|
import { Settings } from './routes/Settings'
|
||||||
|
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
|
||||||
|
import SignIn from './routes/SignIn'
|
||||||
|
import { Auth } from './Auth'
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: (
|
||||||
|
<Auth>
|
||||||
|
<App />
|
||||||
|
</Auth>
|
||||||
|
),
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
|
loader: () => {
|
||||||
|
const store = localStorage.getItem('store')
|
||||||
|
if (store === null) {
|
||||||
|
return redirect('/onboarding')
|
||||||
|
} else {
|
||||||
|
const status = JSON.parse(store).state.onboardingStatus
|
||||||
|
if (status !== 'done' && status !== 'dismissed') {
|
||||||
|
return redirect('/onboarding/' + status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
element: (
|
||||||
|
<Auth>
|
||||||
|
<Settings />
|
||||||
|
</Auth>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/onboarding',
|
||||||
|
element: (
|
||||||
|
<Auth>
|
||||||
|
<Onboarding />
|
||||||
|
</Auth>
|
||||||
|
),
|
||||||
|
children: onboardingRoutes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/signin',
|
||||||
|
element: <SignIn />,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All routes in the app, used in src/index.tsx
|
||||||
|
* @returns RouterProvider
|
||||||
|
*/
|
||||||
|
export const Router = () => {
|
||||||
|
return <RouterProvider router={router} />
|
||||||
|
}
|
@ -1,12 +1,16 @@
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
interface ActionButtonProps extends React.PropsWithChildren {
|
interface ActionButtonProps extends React.PropsWithChildren {
|
||||||
icon?: ActionIconProps
|
icon?: ActionIconProps
|
||||||
className?: string
|
className?: string
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
to?: string
|
to?: string
|
||||||
as?: 'button' | 'link'
|
Element?:
|
||||||
|
| 'button'
|
||||||
|
| 'link'
|
||||||
|
| React.ComponentType<React.HTMLAttributes<HTMLButtonElement>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionButton = ({
|
export const ActionButton = ({
|
||||||
@ -14,22 +18,33 @@ export const ActionButton = ({
|
|||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
to = '/',
|
to = '/',
|
||||||
as = 'button',
|
Element = 'button',
|
||||||
children,
|
children,
|
||||||
}: ActionButtonProps) => {
|
}: ActionButtonProps) => {
|
||||||
const classNames = `group mono flex items-center gap-2 text-chalkboard-110 rounded-sm border border-chalkboard-40 hover:border-liquid-40 p-[3px] ${
|
const classNames = `group mono flex items-center gap-2 text-chalkboard-110 rounded-sm border border-chalkboard-40 hover:border-liquid-40 p-[3px] ${
|
||||||
icon ? 'pr-2' : 'px-2'
|
icon ? 'pr-2' : 'px-2'
|
||||||
} ${className}`
|
} ${className}`
|
||||||
|
|
||||||
return as === 'button' ? (
|
if (Element === 'button') {
|
||||||
|
return (
|
||||||
<button onClick={onClick} className={classNames}>
|
<button onClick={onClick} className={classNames}>
|
||||||
{icon && <ActionIcon {...icon} />}
|
{icon && <ActionIcon {...icon} />}
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
)
|
||||||
|
} else if (Element === 'link') {
|
||||||
|
return (
|
||||||
<Link to={to} className={classNames}>
|
<Link to={to} className={classNames}>
|
||||||
{icon && <ActionIcon {...icon} />}
|
{icon && <ActionIcon {...icon} />}
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Element onClick={onClick} className={classNames}>
|
||||||
|
{icon && <ActionIcon {...icon} />}
|
||||||
|
{children}
|
||||||
|
</Element>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,35 @@
|
|||||||
import { faGear } from '@fortawesome/free-solid-svg-icons'
|
import { Link } from 'react-router-dom'
|
||||||
import { Toolbar } from '../Toolbar'
|
import { Toolbar } from '../Toolbar'
|
||||||
import { ActionButton } from './ActionButton'
|
import { useStore } from '../useStore'
|
||||||
|
import UserSidebarMenu from './UserSidebarMenu'
|
||||||
|
|
||||||
interface AppHeaderProps extends React.PropsWithChildren {
|
interface AppHeaderProps extends React.PropsWithChildren {
|
||||||
showToolbar?: boolean
|
showToolbar?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppHeader = ({ showToolbar = true, children }: AppHeaderProps) => {
|
export const AppHeader = ({ showToolbar = true, children }: AppHeaderProps) => {
|
||||||
|
const { user } = useStore((s) => ({
|
||||||
|
user: s.user,
|
||||||
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="py-1 px-5 bg-chalkboard-10 border-b border-chalkboard-30 flex justify-between items-center">
|
<header className="py-1 px-5 bg-chalkboard-10 border-b border-chalkboard-30 flex justify-between items-center">
|
||||||
<a href="/project-settings">
|
<Link to="/">
|
||||||
<img
|
<img
|
||||||
src="/kitt-arcade-winking.svg"
|
src="/kitt-arcade-winking.svg"
|
||||||
alt="KittyCAD App"
|
alt="KittyCAD App"
|
||||||
className="h-9 w-auto"
|
className="h-9 w-auto"
|
||||||
/>
|
/>
|
||||||
<span className="sr-only">KittyCAD App</span>
|
<span className="sr-only">KittyCAD App</span>
|
||||||
</a>
|
</Link>
|
||||||
{/* Toolbar if the context deems it */}
|
{/* Toolbar if the context deems it */}
|
||||||
{showToolbar && (
|
{showToolbar && (
|
||||||
<div className="max-w-4xl">
|
<div className="max-w-4xl">
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* If there are children, show them, otherwise... */}
|
{/* If there are children, show them, otherwise show User menu */}
|
||||||
{children || (
|
{children || <UserSidebarMenu user={user} />}
|
||||||
// TODO: If signed out, show the token paste field
|
|
||||||
|
|
||||||
// If signed in, show the account avatar
|
|
||||||
<ActionButton as="link" icon={{ icon: faGear }} to="/settings">
|
|
||||||
Settings
|
|
||||||
</ActionButton>
|
|
||||||
)}
|
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
import { invoke } from '@tauri-apps/api/tauri'
|
|
||||||
import { useStore } from '../useStore'
|
|
||||||
|
|
||||||
export const LoginButton = () => {
|
|
||||||
const { setToken } = useStore((s) => ({
|
|
||||||
setToken: s.setToken,
|
|
||||||
}))
|
|
||||||
const handleClick = async () => {
|
|
||||||
// We want to invoke our command to login via device auth.
|
|
||||||
try {
|
|
||||||
const token: string = await invoke('login')
|
|
||||||
setToken(token)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('login button', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return <button onClick={() => handleClick()}>Login</button>
|
|
||||||
}
|
|
93
src/components/UserSidebarMenu.tsx
Normal file
93
src/components/UserSidebarMenu.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { Popover } from '@headlessui/react'
|
||||||
|
import { User, useStore } from '../useStore'
|
||||||
|
import { ActionButton } from './ActionButton'
|
||||||
|
import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { A } from '@tauri-apps/api/path-c062430b'
|
||||||
|
|
||||||
|
const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { setToken } = useStore((s) => ({
|
||||||
|
setToken: s.setToken,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover className="relative">
|
||||||
|
{user?.image ? (
|
||||||
|
<Popover.Button>
|
||||||
|
<div className="rounded-full border border-chalkboard-70 hover:border-liquid-50 overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={user?.image || ''}
|
||||||
|
alt={user?.name || ''}
|
||||||
|
className="h-8 w-8"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Popover.Button>
|
||||||
|
) : (
|
||||||
|
<ActionButton
|
||||||
|
Element={Popover.Button}
|
||||||
|
icon={{ icon: faBars }}
|
||||||
|
className="border-transparent"
|
||||||
|
>
|
||||||
|
Menu
|
||||||
|
</ActionButton>
|
||||||
|
)}
|
||||||
|
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
|
||||||
|
|
||||||
|
<Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 border border-liquid-100 shadow-md rounded-l-lg">
|
||||||
|
{user && (
|
||||||
|
<div className="flex items-center gap-4 px-4 py-3 bg-liquid-100">
|
||||||
|
<div className="rounded-full shadow-inner overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={user?.image || ''}
|
||||||
|
alt={user?.name || ''}
|
||||||
|
className="h-8 w-8"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="m-0 text-liquid-10 text-mono">
|
||||||
|
{user.name ||
|
||||||
|
user.first_name + ' ' + user.last_name ||
|
||||||
|
user.email}
|
||||||
|
</p>
|
||||||
|
{(user.name || user.first_name) && (
|
||||||
|
<p className="m-0 text-liquid-40 text-xs">{user.email}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="p-4 flex flex-col gap-2">
|
||||||
|
<ActionButton
|
||||||
|
Element="link"
|
||||||
|
icon={{ icon: faGear }}
|
||||||
|
to="/settings"
|
||||||
|
className="border-transparent"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
onClick={() => {
|
||||||
|
setToken('')
|
||||||
|
navigate('/signin')
|
||||||
|
}}
|
||||||
|
icon={{
|
||||||
|
icon: faSignOutAlt,
|
||||||
|
bgClassName: 'bg-destroy-80',
|
||||||
|
iconClassName:
|
||||||
|
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
||||||
|
}}
|
||||||
|
className="border-transparent hover:border-destroy-40"
|
||||||
|
>
|
||||||
|
Sign out
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserSidebarMenu
|
@ -1,13 +1,13 @@
|
|||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import { Auth } from './Auth'
|
|
||||||
import reportWebVitals from './reportWebVitals'
|
import reportWebVitals from './reportWebVitals'
|
||||||
import { Toaster } from 'react-hot-toast'
|
import { Toaster } from 'react-hot-toast'
|
||||||
|
import { Router } from './Router'
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||||
root.render(
|
root.render(
|
||||||
<>
|
<>
|
||||||
<Auth />
|
<Router />
|
||||||
<Toaster position="bottom-center" />
|
<Toaster position="bottom-center" />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
|
import { useStore } from '../useStore'
|
||||||
|
|
||||||
export default async function fetcher<JSON = any>(
|
export default async function fetcher<JSON = any>(
|
||||||
input: RequestInfo,
|
input: RequestInfo,
|
||||||
init: RequestInit = {}
|
init: RequestInit = {}
|
||||||
): Promise<JSON> {
|
): Promise<JSON> {
|
||||||
const credentials = 'include'
|
const { token } = useStore.getState()
|
||||||
const res = await fetch(input, { ...init, credentials })
|
const headers = { ...init.headers } as Record<string, string>
|
||||||
|
if (token) {
|
||||||
|
headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = 'include' as RequestCredentials
|
||||||
|
const res = await fetch(input, { ...init, credentials, headers })
|
||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
3
src/lib/isTauri.ts
Normal file
3
src/lib/isTauri.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function isTauri(): boolean {
|
||||||
|
return '__TAURI__' in window
|
||||||
|
}
|
@ -77,7 +77,7 @@ export const Settings = () => {
|
|||||||
<>
|
<>
|
||||||
<AppHeader showToolbar={false}>
|
<AppHeader showToolbar={false}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
as="link"
|
Element="link"
|
||||||
to="/"
|
to="/"
|
||||||
icon={{
|
icon={{
|
||||||
icon: faXmark,
|
icon: faXmark,
|
||||||
@ -109,7 +109,7 @@ export const Settings = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
as="button"
|
Element="button"
|
||||||
className="bg-chalkboard-100 hover:bg-chalkboard-90 text-chalkboard-10 border-chalkboard-100 hover:border-chalkboard-70"
|
className="bg-chalkboard-100 hover:bg-chalkboard-90 text-chalkboard-10 border-chalkboard-100 hover:border-chalkboard-70"
|
||||||
onClick={handleDirectorySelection}
|
onClick={handleDirectorySelection}
|
||||||
icon={{
|
icon={{
|
||||||
|
86
src/routes/SignIn.tsx
Normal file
86
src/routes/SignIn.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { ActionButton } from '../components/ActionButton'
|
||||||
|
import { isTauri } from '../lib/isTauri'
|
||||||
|
import { useStore } from '../useStore'
|
||||||
|
import { invoke } from '@tauri-apps/api/tauri'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
const SignIn = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { setToken } = useStore((s) => ({
|
||||||
|
setToken: s.setToken,
|
||||||
|
}))
|
||||||
|
const signInTauri = async () => {
|
||||||
|
// We want to invoke our command to login via device auth.
|
||||||
|
try {
|
||||||
|
const token: string = await invoke('login')
|
||||||
|
setToken(token)
|
||||||
|
navigate('/')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('login button', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="h-full min-h-screen bg-chalkboard-20 m-0 p-0 pt-24">
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src="/kittycad-logomark.svg"
|
||||||
|
alt="KittyCAD"
|
||||||
|
className="w-48 inline-block"
|
||||||
|
/>
|
||||||
|
<span className="text-3xl leading-none w-auto inline-block align-middle ml-2">
|
||||||
|
Modeling App
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="font-bold text-2xl mt-12 mb-6 text-chalkboard-110">
|
||||||
|
Sign in to get started with the KittyCAD Modeling App
|
||||||
|
</h1>
|
||||||
|
<p className="py-4">
|
||||||
|
KCMA is an open-source CAD application for creating accurate 3D models
|
||||||
|
for use in manufacturing. It is built on top of the KittyCAD API.
|
||||||
|
KittyCAD is the first software infrastructure company built
|
||||||
|
specifically for the needs of the manufacturing industry. With KCMA we
|
||||||
|
are showing how the KittyCAD API can be used to build entirely new
|
||||||
|
kinds of software for manufacturing.
|
||||||
|
</p>
|
||||||
|
<p className="py-4">
|
||||||
|
KCMA is currently in development. If you would like to be notified
|
||||||
|
when KCMA is ready for production, please sign up for our mailing list
|
||||||
|
at{' '}
|
||||||
|
<a
|
||||||
|
href="https://kittycad.io"
|
||||||
|
className="font-bold text-liquid-80 hover:text-liquid-70"
|
||||||
|
>
|
||||||
|
kittycad.io
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
{isTauri() ? (
|
||||||
|
<ActionButton
|
||||||
|
onClick={signInTauri}
|
||||||
|
icon={{ icon: faSignInAlt }}
|
||||||
|
className="w-fit mt-4"
|
||||||
|
>
|
||||||
|
Sign in
|
||||||
|
</ActionButton>
|
||||||
|
) : (
|
||||||
|
<ActionButton
|
||||||
|
Element="link"
|
||||||
|
to={`https://dev.kittycad.io/signin?callbackUrl=${encodeURIComponent(
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
window.location.href.replace('signin', '')
|
||||||
|
)}`}
|
||||||
|
icon={{ icon: faSignInAlt }}
|
||||||
|
className="w-fit mt-4"
|
||||||
|
>
|
||||||
|
Sign in
|
||||||
|
</ActionButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignIn
|
@ -108,6 +108,20 @@ interface DefaultDir {
|
|||||||
dir: string
|
dir: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: import real OpenAPI User type from schema
|
||||||
|
export interface User {
|
||||||
|
company?: string
|
||||||
|
created_at: string
|
||||||
|
email: string
|
||||||
|
first_name?: string
|
||||||
|
id: string
|
||||||
|
image?: string
|
||||||
|
last_name?: string
|
||||||
|
name?: string
|
||||||
|
phone?: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
editorView: EditorView | null
|
editorView: EditorView | null
|
||||||
setEditorView: (editorView: EditorView) => void
|
setEditorView: (editorView: EditorView) => void
|
||||||
@ -182,6 +196,8 @@ export interface StoreState {
|
|||||||
setHomeMenuItems: (items: { name: string; path: string }[]) => void
|
setHomeMenuItems: (items: { name: string; path: string }[]) => void
|
||||||
token: string
|
token: string
|
||||||
setToken: (token: string) => void
|
setToken: (token: string) => void
|
||||||
|
user?: User
|
||||||
|
setUser: (user: User | undefined) => void
|
||||||
debugPanel: boolean
|
debugPanel: boolean
|
||||||
setDebugPanel: (debugPanel: boolean) => void
|
setDebugPanel: (debugPanel: boolean) => void
|
||||||
}
|
}
|
||||||
@ -355,6 +371,8 @@ export const useStore = create<StoreState>()(
|
|||||||
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
|
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
|
||||||
token: '',
|
token: '',
|
||||||
setToken: (token) => set({ token }),
|
setToken: (token) => set({ token }),
|
||||||
|
user: undefined,
|
||||||
|
setUser: (user) => set({ user }),
|
||||||
debugPanel: false,
|
debugPanel: false,
|
||||||
setDebugPanel: (debugPanel) => set({ debugPanel }),
|
setDebugPanel: (debugPanel) => set({ debugPanel }),
|
||||||
}),
|
}),
|
||||||
|
Reference in New Issue
Block a user