mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-16 20:59:27 +00:00
Merge master into storybook-deploy
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type { Preview } from '@storybook/react'
|
||||
import '@fontsource/roboto/400.css'
|
||||
import '../src/styles/index.scss'
|
||||
import { withRouter } from 'storybook-addon-react-router-v6'
|
||||
import { reactRouterParameters, withRouter } from 'storybook-addon-react-router-v6'
|
||||
import { themes } from '@storybook/theming'
|
||||
import { ToastContainer } from 'react-toastify'
|
||||
import 'react-toastify/dist/ReactToastify.min.css'
|
||||
@@ -9,6 +9,12 @@ import 'react-toastify/dist/ReactToastify.min.css'
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
reactRouter: reactRouterParameters({
|
||||
routing: {
|
||||
handle: 'Nav',
|
||||
path: '*',
|
||||
},
|
||||
}),
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@hookform/resolvers": "^3.3.3",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
|
||||
98
pnpm-lock.yaml
generated
98
pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ dependencies:
|
||||
'@radix-ui/react-dialog':
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-dropdown-menu':
|
||||
specifier: ^2.0.6
|
||||
version: 2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-label':
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||
@@ -2540,6 +2543,33 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.6
|
||||
'@radix-ui/primitive': 1.0.1
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@radix-ui/react-id': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@radix-ui/react-menu': 2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@types/react': 18.2.46
|
||||
'@types/react-dom': 18.2.18
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.46)(react@18.2.0):
|
||||
resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
|
||||
peerDependencies:
|
||||
@@ -2633,6 +2663,44 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-menu@2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.6
|
||||
'@radix-ui/primitive': 1.0.1
|
||||
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-id': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.46)(react@18.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@types/react': 18.2.46
|
||||
'@types/react-dom': 18.2.18
|
||||
aria-hidden: 1.2.3
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-remove-scroll: 2.5.5(@types/react@18.2.46)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==}
|
||||
peerDependencies:
|
||||
@@ -2662,6 +2730,36 @@ packages:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
/@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.6
|
||||
'@floating-ui/react-dom': 2.0.4(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||
'@radix-ui/rect': 1.0.1
|
||||
'@types/react': 18.2.46
|
||||
'@types/react-dom': 18.2.18
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-portal@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==}
|
||||
peerDependencies:
|
||||
|
||||
@@ -2,19 +2,31 @@ import { Ref, SVGProps, forwardRef, memo } from 'react'
|
||||
const SvgComponent = (props: SVGProps<SVGSVGElement>, ref: Ref<SVGSVGElement>) => (
|
||||
<svg
|
||||
fill={'none'}
|
||||
height={24}
|
||||
height={16}
|
||||
ref={ref}
|
||||
width={24}
|
||||
width={16}
|
||||
xmlns={'http://www.w3.org/2000/svg'}
|
||||
{...props}
|
||||
>
|
||||
<g clipPath={'url(#a)'} fill={'#000'}>
|
||||
<g clipPath={'url(#clip0_5928_3635)'}>
|
||||
<path
|
||||
d={
|
||||
'M7 6a1 1 0 0 0 0-2H5a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h2a1 1 0 0 0 0-2H6V6h1Zm13.82 5.42-2.82-4a1 1 0 1 0-1.63 1.16L18.09 11H10a1 1 0 0 0 0 2h8l-1.8 2.4a1 1 0 0 0 1.6 1.2l3-4a1 1 0 0 0 .02-1.18Z'
|
||||
'M4.66669 4.00081C4.8435 4.00081 5.01307 3.93058 5.13809 3.80555C5.26312 3.68053 5.33335 3.51096 5.33335 3.33415C5.33335 3.15734 5.26312 2.98777 5.13809 2.86274C5.01307 2.73772 4.8435 2.66748 4.66669 2.66748H3.33335C3.15654 2.66748 2.98697 2.73772 2.86195 2.86274C2.73693 2.98777 2.66669 3.15734 2.66669 3.33415V12.6675C2.66669 12.8443 2.73693 13.0139 2.86195 13.1389C2.98697 13.2639 3.15654 13.3341 3.33335 13.3341H4.66669C4.8435 13.3341 5.01307 13.2639 5.13809 13.1389C5.26312 13.0139 5.33335 12.8443 5.33335 12.6675C5.33335 12.4907 5.26312 12.3211 5.13809 12.1961C5.01307 12.0711 4.8435 12.0008 4.66669 12.0008H4.00002V4.00081H4.66669Z'
|
||||
}
|
||||
fill={'white'}
|
||||
/>
|
||||
<path
|
||||
d={
|
||||
'M13.88 7.61411L12 4.94745C11.898 4.80372 11.7433 4.70621 11.5697 4.67623C11.396 4.64625 11.2176 4.68623 11.0733 4.78745C11.0012 4.83797 10.9399 4.90228 10.8927 4.97665C10.8456 5.05103 10.8137 5.134 10.7988 5.22077C10.784 5.30755 10.7864 5.39641 10.8061 5.48223C10.8257 5.56805 10.8622 5.64913 10.9133 5.72078L12.06 7.33411H6.66667C6.48986 7.33411 6.32029 7.40435 6.19526 7.52938C6.07024 7.6544 6 7.82397 6 8.00078C6 8.17759 6.07024 8.34716 6.19526 8.47219C6.32029 8.59721 6.48986 8.66745 6.66667 8.66745H12L10.8 10.2674C10.7475 10.3375 10.7093 10.4172 10.6875 10.502C10.6658 10.5868 10.661 10.6751 10.6734 10.7617C10.6857 10.8484 10.7151 10.9318 10.7597 11.0071C10.8043 11.0824 10.8633 11.1483 10.9333 11.2008C11.0487 11.2873 11.1891 11.3341 11.3333 11.3341C11.4368 11.3341 11.5389 11.31 11.6315 11.2637C11.724 11.2174 11.8046 11.1502 11.8667 11.0674L13.8667 8.40078C13.952 8.28803 13.9994 8.15104 14.0018 8.00962C14.0041 7.86821 13.9615 7.7297 13.88 7.61411Z'
|
||||
}
|
||||
fill={'white'}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id={'clip0_5928_3635'}>
|
||||
<rect fill={'white'} height={'16'} transform={'translate(0 0.000976562)'} width={'16'} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
const ForwardRef = forwardRef(SvgComponent)
|
||||
|
||||
18
src/components/layout/header/header.module.scss
Normal file
18
src/components/layout/header/header.module.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
.root {
|
||||
width: 100%;
|
||||
height: var(--header-height);
|
||||
padding: 12px var(--horizontal-padding);
|
||||
|
||||
background: var(--color-dark-700);
|
||||
border-bottom: 1px solid var(--color-dark-500);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
34
src/components/layout/header/header.stories.tsx
Normal file
34
src/components/layout/header/header.stories.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Header } from './'
|
||||
|
||||
const meta = {
|
||||
argTypes: {
|
||||
onLogout: { action: 'logout' },
|
||||
},
|
||||
component: Header,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
title: 'Components/Header',
|
||||
} satisfies Meta<typeof Header>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const LoggedIn: Story = {
|
||||
// @ts-expect-error onLogout is required but it is provided through argTypes
|
||||
args: {
|
||||
avatar: 'https://avatars.githubusercontent.com/u/1196870?v=4',
|
||||
email: 'johndoe@gmail.com',
|
||||
isLoggedIn: true,
|
||||
userName: 'John Doe',
|
||||
},
|
||||
}
|
||||
|
||||
export const LoggedOut: Story = {
|
||||
args: {
|
||||
isLoggedIn: false,
|
||||
},
|
||||
}
|
||||
37
src/components/layout/header/header.tsx
Normal file
37
src/components/layout/header/header.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { memo } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { Logo } from '@/assets'
|
||||
import { UserDropdown, UserDropdownProps } from '@/components/layout/header/user-dropdown'
|
||||
|
||||
import s from './header.module.scss'
|
||||
|
||||
import { Button } from '../../ui'
|
||||
|
||||
export type HeaderProps =
|
||||
| (Partial<UserDropdownProps> & {
|
||||
isLoggedIn: false
|
||||
})
|
||||
| (UserDropdownProps & {
|
||||
isLoggedIn: true
|
||||
})
|
||||
|
||||
export const Header = memo(({ avatar, email, isLoggedIn, onLogout, userName }: HeaderProps) => {
|
||||
return (
|
||||
<header className={s.root}>
|
||||
<div className={s.content}>
|
||||
<Link to={'/'}>
|
||||
<Logo />
|
||||
</Link>
|
||||
{isLoggedIn && (
|
||||
<UserDropdown avatar={avatar} email={email} onLogout={onLogout} userName={userName} />
|
||||
)}
|
||||
{!isLoggedIn && (
|
||||
<Button as={Link} to={'/sign-in'}>
|
||||
Sign In
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
})
|
||||
1
src/components/layout/header/user-dropdown/index.ts
Normal file
1
src/components/layout/header/user-dropdown/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './user-dropdown'
|
||||
@@ -0,0 +1,3 @@
|
||||
.email {
|
||||
color: var(--color-dark-100);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { UserDropdown } from './'
|
||||
|
||||
const meta = {
|
||||
argTypes: {
|
||||
onLogout: { action: 'logout' },
|
||||
},
|
||||
component: UserDropdown,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
title: 'Components/UserDropdown',
|
||||
} satisfies Meta<typeof UserDropdown>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
// @ts-expect-error onLogout is required but it is provided through argTypes
|
||||
args: {
|
||||
avatar: 'https://avatars.githubusercontent.com/u/1196870?v=4',
|
||||
email: 'johndoe@gmail.com',
|
||||
userName: 'John Doe',
|
||||
},
|
||||
}
|
||||
59
src/components/layout/header/user-dropdown/user-dropdown.tsx
Normal file
59
src/components/layout/header/user-dropdown/user-dropdown.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { ComponentPropsWithoutRef } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { Logout, PersonOutline } from '@/assets'
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Typography,
|
||||
} from '@/components'
|
||||
|
||||
import s from './user-dropdown.module.scss'
|
||||
|
||||
export type UserDropdownProps = {
|
||||
avatar: string
|
||||
email: string
|
||||
onLogout: ComponentPropsWithoutRef<typeof DropdownMenuItem>['onSelect']
|
||||
userName: string
|
||||
}
|
||||
|
||||
export const UserDropdown = ({ avatar, email, onLogout, userName }: UserDropdownProps) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button rounded variant={'icon'}>
|
||||
<Avatar src={avatar} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>
|
||||
<Avatar src={avatar} />
|
||||
<div>
|
||||
<Typography variant={'subtitle2'}>{userName}</Typography>
|
||||
<Typography className={s.email} variant={'caption'}>
|
||||
{email}
|
||||
</Typography>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to={'/profile'}>
|
||||
<PersonOutline />
|
||||
My profile
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onSelect={onLogout}>
|
||||
<Logout />
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
4
src/components/ui/avatar/avatar.module.scss
Normal file
4
src/components/ui/avatar/avatar.module.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.avatar {
|
||||
object-fit: cover;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
18
src/components/ui/avatar/avatar.stories.tsx
Normal file
18
src/components/ui/avatar/avatar.stories.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Avatar } from './'
|
||||
|
||||
const meta = {
|
||||
component: Avatar,
|
||||
tags: ['autodocs'],
|
||||
title: 'Components/Avatar',
|
||||
} satisfies Meta<typeof Avatar>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
src: 'https://avatars.githubusercontent.com/u/1196870?v=4',
|
||||
},
|
||||
}
|
||||
23
src/components/ui/avatar/avatar.tsx
Normal file
23
src/components/ui/avatar/avatar.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { CSSProperties, ComponentPropsWithoutRef } from 'react'
|
||||
|
||||
import clsx from 'clsx'
|
||||
|
||||
import s from './avatar.module.scss'
|
||||
|
||||
export type AvatarProps = ComponentPropsWithoutRef<'img'> & {
|
||||
size?: CSSProperties['width']
|
||||
}
|
||||
|
||||
export const Avatar = ({ className, size = '36px', style, ...rest }: AvatarProps) => {
|
||||
return (
|
||||
<img
|
||||
className={clsx(className, s.avatar)}
|
||||
style={{
|
||||
...style,
|
||||
height: size,
|
||||
width: size,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
1
src/components/ui/avatar/index.ts
Normal file
1
src/components/ui/avatar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './avatar'
|
||||
@@ -1,4 +1,4 @@
|
||||
@mixin button {
|
||||
.button {
|
||||
all: unset;
|
||||
|
||||
cursor: pointer;
|
||||
@@ -28,7 +28,8 @@
|
||||
var(--transition-duration-basic) color;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-info-700);
|
||||
outline: var(--outline-focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@@ -43,8 +44,6 @@
|
||||
}
|
||||
|
||||
.primary {
|
||||
@include button;
|
||||
|
||||
color: var(--color-light-100);
|
||||
background-color: var(--color-accent-500);
|
||||
box-shadow: 0 4px 18px rgb(140 97 255 / 35%);
|
||||
@@ -59,8 +58,6 @@
|
||||
}
|
||||
|
||||
.secondary {
|
||||
@include button;
|
||||
|
||||
color: var(--color-light-100);
|
||||
background-color: var(--color-dark-300);
|
||||
box-shadow: 0 2px 10px 0 #6d6d6d40;
|
||||
@@ -75,8 +72,6 @@
|
||||
}
|
||||
|
||||
.tertiary {
|
||||
@include button;
|
||||
|
||||
color: var(--color-accent-500);
|
||||
background-color: var(--color-dark-900);
|
||||
border: 1px solid var(--color-accent-700);
|
||||
@@ -91,8 +86,6 @@
|
||||
}
|
||||
|
||||
.link {
|
||||
@include button;
|
||||
|
||||
padding: 0.375rem 0;
|
||||
|
||||
font-weight: var(--font-weight-bold);
|
||||
@@ -110,4 +103,13 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(--outline-focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&.rounded {
|
||||
border-radius: 9999px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react'
|
||||
import {
|
||||
ComponentPropsWithoutRef,
|
||||
ElementRef,
|
||||
ElementType,
|
||||
ForwardedRef,
|
||||
ReactNode,
|
||||
forwardRef,
|
||||
} from 'react'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import s from './button.module.scss'
|
||||
|
||||
@@ -7,13 +16,38 @@ export type ButtonProps<T extends ElementType = 'button'> = {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
fullWidth?: boolean
|
||||
rounded?: boolean
|
||||
variant?: 'icon' | 'link' | 'primary' | 'secondary' | 'tertiary'
|
||||
} & ComponentPropsWithoutRef<T>
|
||||
|
||||
export const Button = <T extends ElementType = 'button'>(props: ButtonProps<T>) => {
|
||||
const { as: Component = 'button', className, fullWidth, variant = 'primary', ...rest } = props
|
||||
const ButtonPolymorph = <T extends ElementType = 'button'>(props: ButtonProps<T>, ref: any) => {
|
||||
const {
|
||||
as: Component = 'button',
|
||||
className,
|
||||
fullWidth,
|
||||
rounded,
|
||||
variant = 'primary',
|
||||
...rest
|
||||
} = props
|
||||
|
||||
return (
|
||||
<Component className={`${s[variant]} ${fullWidth ? s.fullWidth : ''} ${className}`} {...rest} />
|
||||
<Component
|
||||
className={clsx(
|
||||
s.button,
|
||||
s[variant],
|
||||
fullWidth && s.fullWidth,
|
||||
className,
|
||||
rounded && s.rounded
|
||||
)}
|
||||
{...rest}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const Button = forwardRef(ButtonPolymorph) as <T extends ElementType = 'button'>(
|
||||
props: ButtonProps<T> &
|
||||
Omit<ComponentPropsWithoutRef<T>, keyof ButtonProps<T>> & {
|
||||
ref?: ForwardedRef<ElementRef<T>>
|
||||
}
|
||||
) => ReturnType<typeof ButtonPolymorph>
|
||||
|
||||
117
src/components/ui/dropdown/dropdown.module.scss
Normal file
117
src/components/ui/dropdown/dropdown.module.scss
Normal file
@@ -0,0 +1,117 @@
|
||||
.item {
|
||||
all: unset;
|
||||
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
|
||||
padding: 6px 8px;
|
||||
|
||||
line-height: 16px;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
transition-property:
|
||||
color,
|
||||
background-color,
|
||||
border-color,
|
||||
fill,
|
||||
stroke,
|
||||
-webkit-text-decoration-color;
|
||||
|
||||
&:focus-visible {
|
||||
background-color: var(--color-dark-100);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 101;
|
||||
|
||||
padding: 0 4px;
|
||||
|
||||
background-color: var(--color-dark-700);
|
||||
border: 1px solid var(--color-dark-500);
|
||||
border-radius: 4px;
|
||||
|
||||
&[data-state='open'] {
|
||||
animation: fade-in 300ms linear;
|
||||
}
|
||||
|
||||
&[data-state='closed'] {
|
||||
animation: fade-out 300ms linear;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
top: -3.75px;
|
||||
right: calc(50% - 3px);
|
||||
transform: rotate(45deg);
|
||||
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
|
||||
background-color: var(--color-dark-700);
|
||||
border: 1px solid var(--color-dark-500);
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
|
||||
stroke-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.itemsBox {
|
||||
> :first-child {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 1px;
|
||||
margin: 6px 0.5rem;
|
||||
background: var(--color-dark-300);
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
80
src/components/ui/dropdown/dropdown.stories.tsx
Normal file
80
src/components/ui/dropdown/dropdown.stories.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Edit2Outline, Logout, PersonOutline, PlayCircleOutline, TrashOutline } from '@/assets'
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components'
|
||||
|
||||
const meta = {
|
||||
component: DropdownMenu,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
title: 'Components/Dropdown',
|
||||
} satisfies Meta<typeof DropdownMenu>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Learn: Story = {
|
||||
args: {},
|
||||
render: () => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Open</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<PlayCircleOutline />
|
||||
Learn
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Edit2Outline />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<TrashOutline />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
),
|
||||
}
|
||||
|
||||
export const HeaderDropdown: Story = {
|
||||
args: {},
|
||||
render: () => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button rounded variant={'icon'}>
|
||||
<Avatar src={'https://avatars.githubusercontent.com/u/1196870?v=4'} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<PlayCircleOutline />
|
||||
Learn
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<PersonOutline />
|
||||
My profile
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Logout />
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
),
|
||||
}
|
||||
78
src/components/ui/dropdown/dropdown.tsx
Normal file
78
src/components/ui/dropdown/dropdown.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react'
|
||||
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import s from './dropdown.module.scss'
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuContent = forwardRef<
|
||||
ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ children, className, sideOffset = 8, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
className={clsx(s.content, className)}
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
{...props}
|
||||
>
|
||||
<div>
|
||||
<DropdownMenuPrimitive.Arrow asChild className={s.arrowBox}>
|
||||
<div className={s.arrow} />
|
||||
</DropdownMenuPrimitive.Arrow>
|
||||
<div className={s.itemsBox}>{children}</div>
|
||||
</div>
|
||||
</DropdownMenuPrimitive.Content>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = forwardRef<
|
||||
ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item className={clsx(s.item, className)} ref={ref} {...props} />
|
||||
))
|
||||
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuSeparator = forwardRef<
|
||||
ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator className={clsx(s.separator, className)} ref={ref} {...props} />
|
||||
))
|
||||
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuLabel = forwardRef<
|
||||
ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label className={clsx(s.label, className)} ref={ref} {...props} />
|
||||
))
|
||||
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
}
|
||||
1
src/components/ui/dropdown/index.ts
Normal file
1
src/components/ui/dropdown/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dropdown'
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Header } from './'
|
||||
|
||||
const meta = {
|
||||
component: Header,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
title: 'Components/Header',
|
||||
} satisfies Meta<typeof Header>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import s from './header.module.scss'
|
||||
|
||||
export type HeaderProps = {
|
||||
avatar?: string
|
||||
name?: string
|
||||
onLogout?: () => void
|
||||
}
|
||||
|
||||
export const Header = ({}: HeaderProps) => {
|
||||
return <div>Header</div>
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
export * from './header'
|
||||
export * from './avatar'
|
||||
export * from './dropdown'
|
||||
export * from '../layout/header'
|
||||
export * from './button'
|
||||
export * from './card'
|
||||
export * from './typography'
|
||||
|
||||
@@ -5,20 +5,13 @@ import {
|
||||
DecksResponse,
|
||||
GetDecksArgs,
|
||||
UpdateDeckArgs,
|
||||
} from './decks.types'
|
||||
import { baseApi } from '@/services'
|
||||
baseApi,
|
||||
} from '@/services'
|
||||
|
||||
const decksService = baseApi.injectEndpoints({
|
||||
endpoints: builder => ({
|
||||
createDeck: builder.mutation<DeckResponse, CreateDeckArgs>({
|
||||
invalidatesTags: ['Decks'],
|
||||
onQueryStarted: async (_, { dispatch, getCacheEntry, getState, queryFulfilled }) => {
|
||||
const data = getCacheEntry()
|
||||
const state = getState()
|
||||
|
||||
decksService.util.re
|
||||
await queryFulfilled
|
||||
},
|
||||
query: body => ({
|
||||
body,
|
||||
method: 'POST',
|
||||
|
||||
@@ -25,8 +25,15 @@ option {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: inherit;
|
||||
display: flex;
|
||||
color: currentcolor;
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(--outline-focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
29
src/styles/_mixins.scss
Normal file
29
src/styles/_mixins.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
@mixin sm-up {
|
||||
@media screen and (width > 600px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin md-up {
|
||||
@media screen and (width > 768px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin lg-up {
|
||||
@media screen and (width > 1024px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin xl-up {
|
||||
@media screen and (width > 1280px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin xxl-up {
|
||||
@media screen and (width > 1536px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
@use 'mixins' as *;
|
||||
|
||||
:root {
|
||||
/* outlines */
|
||||
--color-outline-focus: var(--color-info-900);
|
||||
@@ -11,4 +13,12 @@
|
||||
|
||||
/* header */
|
||||
--header-height: 60px;
|
||||
|
||||
/* max-width */
|
||||
--horizontal-padding: 16px;
|
||||
--max-width: calc(1005px + var(--horizontal-padding) * 2);
|
||||
|
||||
@include md-up {
|
||||
--horizontal-padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@forward 'mixins';
|
||||
@forward 'colors';
|
||||
@forward 'typography';
|
||||
@forward 'boilerplate';
|
||||
|
||||
Reference in New Issue
Block a user