feat: add spinner component, add layout context

This commit is contained in:
2023-12-31 11:34:24 +01:00
parent 1e09839696
commit dd9cc3e3aa
8 changed files with 126 additions and 21 deletions

View File

@@ -1,27 +1,36 @@
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { Outlet } from 'react-router-dom' import { Outlet, useOutletContext } from 'react-router-dom'
import { Header, HeaderProps } from '@/components' import { Header, HeaderProps, Spinner } from '@/components'
import { useMeQuery } from '@/services/auth/auth.service' import { useMeQuery } from '@/services/auth/auth.service'
import s from './layout.module.scss' import s from './layout.module.scss'
type AuthContext = {
isAuthenticated: boolean
}
export function useAuthContext() {
return useOutletContext<AuthContext>()
}
export const Layout = () => { export const Layout = () => {
const { data, isError, isLoading } = useMeQuery() const { data, isError, isLoading } = useMeQuery()
const isAuthenticated = !isError && !isLoading
if (isLoading) { if (isLoading) {
return <div>loading...</div> return <Spinner fullScreen />
} }
return ( return (
<LayoutPrimitive <LayoutPrimitive
avatar={data?.avatar ?? null} avatar={data?.avatar ?? null}
email={data?.email ?? ''} email={data?.email ?? ''}
isLoggedIn={!isError && !!data} isLoggedIn={isAuthenticated}
onLogout={() => {}} onLogout={() => {}}
userName={data?.name ?? ''} userName={data?.name ?? ''}
> >
<Outlet /> <Outlet context={{ isAuthenticated } satisfies AuthContext} />
</LayoutPrimitive> </LayoutPrimitive>
) )
} }

View File

@@ -1,3 +1,4 @@
export * from './spinner'
export * from './avatar' export * from './avatar'
export * from './dropdown' export * from './dropdown'
export * from '../layout/header' export * from '../layout/header'

View File

@@ -0,0 +1 @@
export * from './spinner'

View File

@@ -0,0 +1,57 @@
.fullScreenContainer {
position: fixed;
z-index: 9999;
top: var(--header-height);
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: calc(100vh - var(--header-height));
background-color: rgb(0 0 0 / 50%);
}
.loader {
position: relative;
display: inline-block;
box-sizing: border-box;
width: 48px;
height: 48px;
border: 3px solid #fff;
border-radius: 50%;
animation: rotation 1s linear infinite;
}
.loader::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-sizing: border-box;
width: 40px;
height: 40px;
border: 3px solid transparent;
border-bottom-color: var(--color-accent-500);
border-radius: 50%;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Spinner } from './'
const meta = {
component: Spinner,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
title: 'Components/Spinner',
} satisfies Meta<typeof Spinner>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View File

@@ -0,0 +1,30 @@
import { CSSProperties, ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react'
import { clsx } from 'clsx'
import s from './spinner.module.scss'
export type SpinnerProps = {
fullScreen?: boolean
size?: CSSProperties['width']
} & ComponentPropsWithoutRef<'span'>
export const Spinner = forwardRef<ElementRef<'span'>, SpinnerProps>(
({ className, fullScreen, size = '48px', style, ...rest }, ref) => {
const styles = {
height: size,
width: size,
...style,
} satisfies CSSProperties
if (fullScreen) {
return (
<div className={s.fullScreenContainer}>
<span className={clsx(s.loader, className)} ref={ref} style={styles} {...rest} />
</div>
)
}
return <span className={clsx(s.loader, className)} ref={ref} style={styles} {...rest} />
}
)

View File

@@ -1,6 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import { Button, DecksTable, Page, Slider, TextField, Typography } from '@/components' import { Button, DecksTable, Page, Slider, Spinner, TextField, Typography } from '@/components'
import { DeckDialog } from '@/components/decks/deck-dialog' import { DeckDialog } from '@/components/decks/deck-dialog'
import { DeleteDeckDialog } from '@/components/decks/delete-deck-dialog' import { DeleteDeckDialog } from '@/components/decks/delete-deck-dialog'
import { Pagination } from '@/components/ui/pagination' import { Pagination } from '@/components/ui/pagination'
@@ -81,7 +81,7 @@ export const DecksPage = () => {
const openCreateModal = () => setShowCreateModal(true) const openCreateModal = () => setShowCreateModal(true)
if (!decks || !me) { if (!decks || !me) {
return <div>loading...</div> return <Spinner fullScreen />
} }
return ( return (

View File

@@ -6,11 +6,10 @@ import {
createBrowserRouter, createBrowserRouter,
} from 'react-router-dom' } from 'react-router-dom'
import { Layout } from '@/components/layout' import { Layout, useAuthContext } from '@/components/layout'
import { DeckPage } from '@/pages/deck-page/deck-page' import { DeckPage } from '@/pages/deck-page/deck-page'
import { DecksPage, SignInPage } from './pages' import { DecksPage, SignInPage } from './pages'
import { useMeQuery } from './services/auth/auth.service'
const publicRoutes: RouteObject[] = [ const publicRoutes: RouteObject[] = [
{ {
@@ -49,22 +48,11 @@ const router = createBrowserRouter([
]) ])
export const Router = () => { export const Router = () => {
const { isLoading } = useMeQuery()
if (isLoading) {
return <div>loading...</div>
}
return <RouterProvider router={router} /> return <RouterProvider router={router} />
} }
function PrivateRoutes() { function PrivateRoutes() {
const { isError, isLoading } = useMeQuery() const { isAuthenticated } = useAuthContext()
if (isLoading) {
return <div>loading...</div>
}
const isAuthenticated = !isError
return isAuthenticated ? <Outlet /> : <Navigate to={'/login'} /> return isAuthenticated ? <Outlet /> : <Navigate to={'/login'} />
} }