mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-16 20:59:27 +00:00
feat: add spinner component, add layout context
This commit is contained in:
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
1
src/components/ui/spinner/index.ts
Normal file
1
src/components/ui/spinner/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './spinner'
|
||||||
57
src/components/ui/spinner/spinner.module.scss
Normal file
57
src/components/ui/spinner/spinner.module.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/components/ui/spinner/spinner.stories.tsx
Normal file
19
src/components/ui/spinner/spinner.stories.tsx
Normal 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: {},
|
||||||
|
}
|
||||||
30
src/components/ui/spinner/spinner.tsx
Normal file
30
src/components/ui/spinner/spinner.tsx
Normal 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} />
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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'} />
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user