diff --git a/src/components/layout/layout.tsx b/src/components/layout/layout.tsx index c0e3434..6d4448c 100644 --- a/src/components/layout/layout.tsx +++ b/src/components/layout/layout.tsx @@ -1,27 +1,36 @@ 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 s from './layout.module.scss' +type AuthContext = { + isAuthenticated: boolean +} + +export function useAuthContext() { + return useOutletContext() +} + export const Layout = () => { const { data, isError, isLoading } = useMeQuery() + const isAuthenticated = !isError && !isLoading if (isLoading) { - return
loading...
+ return } return ( {}} userName={data?.name ?? ''} > - + ) } diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index 594e76e..cf20b41 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -1,3 +1,4 @@ +export * from './spinner' export * from './avatar' export * from './dropdown' export * from '../layout/header' diff --git a/src/components/ui/spinner/index.ts b/src/components/ui/spinner/index.ts new file mode 100644 index 0000000..21591ec --- /dev/null +++ b/src/components/ui/spinner/index.ts @@ -0,0 +1 @@ +export * from './spinner' diff --git a/src/components/ui/spinner/spinner.module.scss b/src/components/ui/spinner/spinner.module.scss new file mode 100644 index 0000000..480017c --- /dev/null +++ b/src/components/ui/spinner/spinner.module.scss @@ -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); + } +} diff --git a/src/components/ui/spinner/spinner.stories.tsx b/src/components/ui/spinner/spinner.stories.tsx new file mode 100644 index 0000000..038a9e3 --- /dev/null +++ b/src/components/ui/spinner/spinner.stories.tsx @@ -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 + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: {}, +} diff --git a/src/components/ui/spinner/spinner.tsx b/src/components/ui/spinner/spinner.tsx new file mode 100644 index 0000000..ff1a335 --- /dev/null +++ b/src/components/ui/spinner/spinner.tsx @@ -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, SpinnerProps>( + ({ className, fullScreen, size = '48px', style, ...rest }, ref) => { + const styles = { + height: size, + width: size, + ...style, + } satisfies CSSProperties + + if (fullScreen) { + return ( +
+ +
+ ) + } + + return + } +) diff --git a/src/pages/decks-page/decks-page.tsx b/src/pages/decks-page/decks-page.tsx index 0b11186..0a3f0f4 100644 --- a/src/pages/decks-page/decks-page.tsx +++ b/src/pages/decks-page/decks-page.tsx @@ -1,6 +1,6 @@ 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 { DeleteDeckDialog } from '@/components/decks/delete-deck-dialog' import { Pagination } from '@/components/ui/pagination' @@ -81,7 +81,7 @@ export const DecksPage = () => { const openCreateModal = () => setShowCreateModal(true) if (!decks || !me) { - return
loading...
+ return } return ( diff --git a/src/router.tsx b/src/router.tsx index 83a4e2f..18b6627 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -6,11 +6,10 @@ import { createBrowserRouter, } from 'react-router-dom' -import { Layout } from '@/components/layout' +import { Layout, useAuthContext } from '@/components/layout' import { DeckPage } from '@/pages/deck-page/deck-page' import { DecksPage, SignInPage } from './pages' -import { useMeQuery } from './services/auth/auth.service' const publicRoutes: RouteObject[] = [ { @@ -49,22 +48,11 @@ const router = createBrowserRouter([ ]) export const Router = () => { - const { isLoading } = useMeQuery() - - if (isLoading) { - return
loading...
- } - return } function PrivateRoutes() { - const { isError, isLoading } = useMeQuery() - - if (isLoading) { - return
loading...
- } - const isAuthenticated = !isError + const { isAuthenticated } = useAuthContext() return isAuthenticated ? : }