lint everything

This commit is contained in:
2023-11-18 16:34:09 +01:00
parent 68e5977fb2
commit 1af65eb479
78 changed files with 2282 additions and 2258 deletions

View File

@@ -3,16 +3,16 @@ import type { Meta, StoryObj } from '@storybook/react'
import { CheckEmail } from './'
const meta = {
component: CheckEmail,
tags: ['autodocs'],
title: 'Auth/Check email',
component: CheckEmail,
tags: ['autodocs'],
title: 'Auth/Check email',
} satisfies Meta<typeof CheckEmail>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
email: 'your_email@domain.com',
},
args: {
email: 'your_email@domain.com',
},
}

View File

@@ -6,26 +6,26 @@ import { Button, Card, Typography } from '../../ui'
import s from './check-email.module.scss'
type Props = {
email: string
email: string
}
export const CheckEmail = ({ email }: Props) => {
const message = `We've sent an e-mail with instructions to ${email}`
const message = `We've sent an e-mail with instructions to ${email}`
return (
<Card className={s.card}>
<Typography className={s.title} variant={'large'}>
Check your email
</Typography>
<div className={s.iconContainer}>
<Email />
</div>
<Typography className={s.instructions} variant={'body2'}>
{message}
</Typography>
<Button as={Link} fullWidth to={'/sing-in'}>
Back to Sign in
</Button>
</Card>
)
return (
<Card className={s.card}>
<Typography className={s.title} variant={'large'}>
Check your email
</Typography>
<div className={s.iconContainer}>
<Email />
</div>
<Typography className={s.instructions} variant={'body2'}>
{message}
</Typography>
<Button as={Link} fullWidth to={'/sing-in'}>
Back to Sign in
</Button>
</Card>
)
}

View File

@@ -3,16 +3,16 @@ import type { Meta, StoryObj } from '@storybook/react'
import { NewPassword } from './'
const meta = {
component: NewPassword,
tags: ['autodocs'],
title: 'Auth/New password',
component: NewPassword,
tags: ['autodocs'],
title: 'Auth/New password',
} satisfies Meta<typeof NewPassword>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
onSubmit: data => console.info(data),
},
args: {
onSubmit: data => console.info(data),
},
}

View File

@@ -8,48 +8,48 @@ import { z } from 'zod'
import s from './new-password.module.scss'
const schema = z.object({
password: z.string().nonempty('Enter password'),
password: z.string().nonempty('Enter password'),
})
type FormType = z.infer<typeof schema>
type Props = {
onSubmit: (data: FormType) => void
onSubmit: (data: FormType) => void
}
export const NewPassword = (props: Props) => {
const { control, handleSubmit } = useForm<FormType>({
defaultValues: {
password: '',
},
resolver: zodResolver(schema),
})
const { control, handleSubmit } = useForm<FormType>({
defaultValues: {
password: '',
},
resolver: zodResolver(schema),
})
const handleFormSubmitted = handleSubmit(props.onSubmit)
const handleFormSubmitted = handleSubmit(props.onSubmit)
return (
<>
<DevTool control={control} />
<Card className={s.card}>
<Typography className={s.title} variant={'large'}>
Create new password
</Typography>
<form onSubmit={handleFormSubmitted}>
<ControlledTextField
containerProps={{ className: s.input }}
control={control}
name={'password'}
placeholder={'Password'}
type={'password'}
/>
<Typography className={s.instructions} variant={'caption'}>
Create new password and we will send you further instructions to email
</Typography>
<Button fullWidth type={'submit'}>
Create new password
</Button>
</form>
</Card>
</>
)
return (
<>
<DevTool control={control} />
<Card className={s.card}>
<Typography className={s.title} variant={'large'}>
Create new password
</Typography>
<form onSubmit={handleFormSubmitted}>
<ControlledTextField
containerProps={{ className: s.input }}
control={control}
name={'password'}
placeholder={'Password'}
type={'password'}
/>
<Typography className={s.instructions} variant={'caption'}>
Create new password and we will send you further instructions to email
</Typography>
<Button fullWidth type={'submit'}>
Create new password
</Button>
</form>
</Card>
</>
)
}

View File

@@ -3,16 +3,16 @@ import type { Meta, StoryObj } from '@storybook/react'
import { RecoverPassword } from './'
const meta = {
component: RecoverPassword,
tags: ['autodocs'],
title: 'Auth/Recover password',
component: RecoverPassword,
tags: ['autodocs'],
title: 'Auth/Recover password',
} satisfies Meta<typeof RecoverPassword>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
onSubmit: data => console.info(data),
},
args: {
onSubmit: data => console.info(data),
},
}

View File

@@ -9,51 +9,55 @@ import { z } from 'zod'
import s from './recover-password.module.scss'
const schema = z.object({
email: z.string().email('Invalid email address').nonempty('Enter email'),
email: z.string().email('Invalid email address').nonempty('Enter email'),
})
type FormType = z.infer<typeof schema>
type Props = {
onSubmit: (data: FormType) => void
onSubmit: (data: FormType) => void
}
export const RecoverPassword = (props: Props) => {
const { control, handleSubmit } = useForm<FormType>({
defaultValues: {
email: '',
},
mode: 'onSubmit',
resolver: zodResolver(schema),
})
const { control, handleSubmit } = useForm<FormType>({
defaultValues: {
email: '',
},
mode: 'onSubmit',
resolver: zodResolver(schema),
})
const handleFormSubmitted = handleSubmit(props.onSubmit)
const handleFormSubmitted = handleSubmit(props.onSubmit)
return (
<>
<DevTool control={control} />
<Card className={s.card}>
<Typography className={s.title} variant={'large'}>
Forgot your password?
</Typography>
<form onSubmit={handleFormSubmitted}>
<div className={s.form}>
<ControlledTextField control={control} name={'email'} placeholder={'Email'} />
</div>
<Typography className={s.instructions} variant={'body2'}>
Enter your email address and we will send you further instructions
</Typography>
<Button className={s.button} fullWidth type={'submit'}>
Send Instructions
</Button>
</form>
<Typography className={s.caption} variant={'body2'}>
Did you remember your password?
</Typography>
<Typography as={Link} className={s.loginLink} to={'/sign-in'} variant={'link1'}>
Try logging in
</Typography>
</Card>
</>
)
return (
<>
<DevTool control={control} />
<Card className={s.card}>
<Typography className={s.title} variant={'large'}>
Forgot your password?
</Typography>
<form onSubmit={handleFormSubmitted}>
<div className={s.form}>
<ControlledTextField
control={control}
name={'email'}
placeholder={'Email'}
/>
</div>
<Typography className={s.instructions} variant={'body2'}>
Enter your email address and we will send you further instructions
</Typography>
<Button className={s.button} fullWidth type={'submit'}>
Send Instructions
</Button>
</form>
<Typography className={s.caption} variant={'body2'}>
Did you remember your password?
</Typography>
<Typography as={Link} className={s.loginLink} to={'/sign-in'} variant={'link1'}>
Try logging in
</Typography>
</Card>
</>
)
}

View File

@@ -3,16 +3,16 @@ import type { Meta, StoryObj } from '@storybook/react'
import { SignIn } from './'
const meta = {
component: SignIn,
tags: ['autodocs'],
title: 'Auth/Sign in',
component: SignIn,
tags: ['autodocs'],
title: 'Auth/Sign in',
} satisfies Meta<typeof SignIn>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
onSubmit: data => console.info(data),
},
args: {
onSubmit: data => console.info(data),
},
}

View File

@@ -9,79 +9,79 @@ import { z } from 'zod'
import s from './sign-in.module.scss'
const schema = z.object({
email: z.string().email('Invalid email address').nonempty('Enter email'),
password: z.string().nonempty('Enter password'),
rememberMe: z.boolean().optional(),
email: z.string().email('Invalid email address').nonempty('Enter email'),
password: z.string().nonempty('Enter password'),
rememberMe: z.boolean().optional(),
})
type FormType = z.infer<typeof schema>
type Props = {
onSubmit: (data: FormType) => void
onSubmit: (data: FormType) => void
}
export const SignIn = (props: Props) => {
const { control, handleSubmit } = useForm<FormType>({
defaultValues: {
email: '',
password: '',
rememberMe: false,
},
mode: 'onSubmit',
resolver: zodResolver(schema),
})
const { control, handleSubmit } = useForm<FormType>({
defaultValues: {
email: '',
password: '',
rememberMe: false,
},
mode: 'onSubmit',
resolver: zodResolver(schema),
})
const handleFormSubmitted = handleSubmit(props.onSubmit)
const handleFormSubmitted = handleSubmit(props.onSubmit)
return (
<>
<DevTool control={control} />
<Card className={s.card}>
<Typography className={s.title} variant={'large'}>
Sign In
</Typography>
<form onSubmit={handleFormSubmitted}>
<div className={s.form}>
<ControlledTextField
control={control}
label={'Email'}
name={'email'}
placeholder={'Email'}
/>
<ControlledTextField
control={control}
label={'Password'}
name={'password'}
placeholder={'Password'}
type={'password'}
/>
</div>
<ControlledCheckbox
className={s.checkbox}
control={control}
label={'Remember me'}
name={'rememberMe'}
position={'left'}
/>
<Typography
as={Link}
className={s.recoverPasswordLink}
to={'/recover-password'}
variant={'body2'}
>
Forgot Password?
</Typography>
<Button className={s.button} fullWidth type={'submit'}>
Sign In
</Button>
</form>
<Typography className={s.caption} variant={'body2'}>
{`Don't have an account?`}
</Typography>
<Typography as={Link} className={s.signUpLink} to={'/sign-up'} variant={'link1'}>
Sign Up
</Typography>
</Card>
</>
)
return (
<>
<DevTool control={control} />
<Card className={s.card}>
<Typography className={s.title} variant={'large'}>
Sign In
</Typography>
<form onSubmit={handleFormSubmitted}>
<div className={s.form}>
<ControlledTextField
control={control}
label={'Email'}
name={'email'}
placeholder={'Email'}
/>
<ControlledTextField
control={control}
label={'Password'}
name={'password'}
placeholder={'Password'}
type={'password'}
/>
</div>
<ControlledCheckbox
className={s.checkbox}
control={control}
label={'Remember me'}
name={'rememberMe'}
position={'left'}
/>
<Typography
as={Link}
className={s.recoverPasswordLink}
to={'/recover-password'}
variant={'body2'}
>
Forgot Password?
</Typography>
<Button className={s.button} fullWidth type={'submit'}>
Sign In
</Button>
</form>
<Typography className={s.caption} variant={'body2'}>
{`Don't have an account?`}
</Typography>
<Typography as={Link} className={s.signUpLink} to={'/sign-up'} variant={'link1'}>
Sign Up
</Typography>
</Card>
</>
)
}

View File

@@ -3,16 +3,16 @@ import type { Meta, StoryObj } from '@storybook/react'
import { SignUp } from './'
const meta = {
component: SignUp,
tags: ['autodocs'],
title: 'Auth/Sign up',
component: SignUp,
tags: ['autodocs'],
title: 'Auth/Sign up',
} satisfies Meta<typeof SignUp>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
onSubmit: data => console.info(data),
},
args: {
onSubmit: data => console.info(data),
},
}

View File

@@ -10,86 +10,86 @@ import { z } from 'zod'
import s from './sign-up.module.scss'
const schema = z
.object({
email: z.string().email('Invalid email address').nonempty('Enter email'),
password: z.string().nonempty('Enter password'),
passwordConfirmation: z.string().nonempty('Confirm your password'),
})
.superRefine((data, ctx) => {
if (data.password !== data.passwordConfirmation) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Passwords do not match',
path: ['passwordConfirmation'],
})
}
.object({
email: z.string().email('Invalid email address').nonempty('Enter email'),
password: z.string().nonempty('Enter password'),
passwordConfirmation: z.string().nonempty('Confirm your password'),
})
.superRefine((data, ctx) => {
if (data.password !== data.passwordConfirmation) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Passwords do not match',
path: ['passwordConfirmation'],
})
}
return data
})
return data
})
type FormType = z.infer<typeof schema>
type Props = {
onSubmit: (data: Omit<FormType, 'passwordConfirmation'>) => void
onSubmit: (data: Omit<FormType, 'passwordConfirmation'>) => void
}
export const SignUp = (props: Props) => {
const { control, handleSubmit } = useForm<FormType>({
defaultValues: {
email: '',
password: '',
passwordConfirmation: '',
},
mode: 'onSubmit',
resolver: zodResolver(schema),
})
const { control, handleSubmit } = useForm<FormType>({
defaultValues: {
email: '',
password: '',
passwordConfirmation: '',
},
mode: 'onSubmit',
resolver: zodResolver(schema),
})
const handleFormSubmitted = handleSubmit(data =>
props.onSubmit(omit(data, ['passwordConfirmation']))
)
const handleFormSubmitted = handleSubmit(data =>
props.onSubmit(omit(data, ['passwordConfirmation']))
)
return (
<>
<DevTool control={control} />
<Card className={s.card}>
<Typography className={s.title} variant={'large'}>
Sign Up
</Typography>
<form onSubmit={handleFormSubmitted}>
<div className={s.form}>
<ControlledTextField
control={control}
label={'Email'}
name={'email'}
placeholder={'Email'}
/>
<ControlledTextField
control={control}
label={'Password'}
name={'password'}
placeholder={'Password'}
type={'password'}
/>
<ControlledTextField
control={control}
label={'Confirm password'}
name={'passwordConfirmation'}
placeholder={'Confirm password'}
type={'password'}
/>
</div>
<Button className={s.button} fullWidth type={'submit'}>
Sign Up
</Button>
</form>
{/* eslint-disable-next-line react/no-unescaped-entities */}
<Typography className={s.caption} variant={'body2'}>
Already have an account?
</Typography>
<Typography as={Link} className={s.signInLink} to={'/sign-in'} variant={'link1'}>
Sign In
</Typography>
</Card>
</>
)
return (
<>
<DevTool control={control} />
<Card className={s.card}>
<Typography className={s.title} variant={'large'}>
Sign Up
</Typography>
<form onSubmit={handleFormSubmitted}>
<div className={s.form}>
<ControlledTextField
control={control}
label={'Email'}
name={'email'}
placeholder={'Email'}
/>
<ControlledTextField
control={control}
label={'Password'}
name={'password'}
placeholder={'Password'}
type={'password'}
/>
<ControlledTextField
control={control}
label={'Confirm password'}
name={'passwordConfirmation'}
placeholder={'Confirm password'}
type={'password'}
/>
</div>
<Button className={s.button} fullWidth type={'submit'}>
Sign Up
</Button>
</form>
{/* eslint-disable-next-line react/no-unescaped-entities */}
<Typography className={s.caption} variant={'body2'}>
Already have an account?
</Typography>
<Typography as={Link} className={s.signInLink} to={'/sign-in'} variant={'link1'}>
Sign In
</Typography>
</Card>
</>
)
}

View File

@@ -3,45 +3,45 @@ import { Card } from '@/services/decks'
import { formatDate } from '@/utils'
const columns: Column[] = [
{
key: 'question',
sortable: true,
title: 'Question',
},
{
key: 'answer',
sortable: true,
title: 'Answer',
},
{
key: 'updated',
sortable: true,
title: 'Last Updated',
},
{
key: 'grade',
sortable: true,
title: 'Grade',
},
{
key: 'question',
sortable: true,
title: 'Question',
},
{
key: 'answer',
sortable: true,
title: 'Answer',
},
{
key: 'updated',
sortable: true,
title: 'Last Updated',
},
{
key: 'grade',
sortable: true,
title: 'Grade',
},
]
type Props = {
cards: Card[] | undefined
cards: Card[] | undefined
}
export const CardsTable = ({ cards }: Props) => {
return (
<Table>
<TableHeader columns={columns} />
<TableBody>
{cards?.map(card => (
<TableRow key={card.id}>
<TableCell>{card.question}</TableCell>
<TableCell>{card.answer}</TableCell>
<TableCell>{formatDate(card.updated)}</TableCell>
<TableCell>{card.grade}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
return (
<Table>
<TableHeader columns={columns} />
<TableBody>
{cards?.map(card => (
<TableRow key={card.id}>
<TableCell>{card.question}</TableCell>
<TableCell>{card.answer}</TableCell>
<TableCell>{formatDate(card.updated)}</TableCell>
<TableCell>{card.grade}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}

View File

@@ -5,68 +5,68 @@ import { Button } from '@/components'
import { Meta, StoryObj } from '@storybook/react'
const meta = {
component: DeckDialog,
tags: ['autodocs'],
title: 'Decks/Deck Dialog',
component: DeckDialog,
tags: ['autodocs'],
title: 'Decks/Deck Dialog',
} satisfies Meta<typeof DeckDialog>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
onOpenChange: () => {},
open: true,
},
render: args => {
const [open, setOpen] = useState(false)
const closeModal = () => setOpen(false)
args: {
onOpenChange: () => {},
open: true,
},
render: args => {
const [open, setOpen] = useState(false)
const closeModal = () => setOpen(false)
return (
<>
<Button onClick={() => setOpen(true)}>Open Modal</Button>
<DeckDialog
{...args}
onCancel={closeModal}
onConfirm={data => {
console.log(data)
closeModal()
}}
onOpenChange={setOpen}
open={open}
/>
</>
)
},
return (
<>
<Button onClick={() => setOpen(true)}>Open Modal</Button>
<DeckDialog
{...args}
onCancel={closeModal}
onConfirm={data => {
console.log(data)
closeModal()
}}
onOpenChange={setOpen}
open={open}
/>
</>
)
},
}
export const WithDefaultValues: Story = {
args: {
onOpenChange: () => {},
open: true,
},
render: args => {
const [open, setOpen] = useState(false)
const closeModal = () => setOpen(false)
args: {
onOpenChange: () => {},
open: true,
},
render: args => {
const [open, setOpen] = useState(false)
const closeModal = () => setOpen(false)
return (
<>
<Button onClick={() => setOpen(true)}>Open Modal</Button>
<DeckDialog
{...args}
defaultValues={{
isPrivate: true,
name: 'some name',
}}
onCancel={closeModal}
onConfirm={data => {
console.log(data)
closeModal()
}}
onOpenChange={setOpen}
open={open}
/>
</>
)
},
return (
<>
<Button onClick={() => setOpen(true)}>Open Modal</Button>
<DeckDialog
{...args}
defaultValues={{
isPrivate: true,
name: 'some name',
}}
onCancel={closeModal}
onConfirm={data => {
console.log(data)
closeModal()
}}
onOpenChange={setOpen}
open={open}
/>
</>
)
},
}

View File

@@ -7,47 +7,52 @@ import { z } from 'zod'
import s from './deck-dialog.module.scss'
const newDeckSchema = z.object({
isPrivate: z.boolean(),
name: z.string().min(3).max(50),
isPrivate: z.boolean(),
name: z.string().min(3).max(50),
})
type FormValues = z.infer<typeof newDeckSchema>
type Props = Pick<DialogProps, 'onCancel' | 'onOpenChange' | 'open'> & {
defaultValues?: FormValues
onConfirm: (data: FormValues) => void
defaultValues?: FormValues
onConfirm: (data: FormValues) => void
}
export const DeckDialog = ({
defaultValues = { isPrivate: false, name: '' },
onCancel,
onConfirm,
...dialogProps
defaultValues = { isPrivate: false, name: '' },
onCancel,
onConfirm,
...dialogProps
}: Props) => {
const { control, handleSubmit, reset } = useForm<FormValues>({
defaultValues,
resolver: zodResolver(newDeckSchema),
})
const onSubmit = handleSubmit(data => {
onConfirm(data)
dialogProps.onOpenChange?.(false)
reset()
})
const handleCancel = () => {
reset()
onCancel?.()
}
const { control, handleSubmit, reset } = useForm<FormValues>({
defaultValues,
resolver: zodResolver(newDeckSchema),
})
const onSubmit = handleSubmit(data => {
onConfirm(data)
dialogProps.onOpenChange?.(false)
reset()
})
const handleCancel = () => {
reset()
onCancel?.()
}
return (
<Dialog {...dialogProps} onCancel={handleCancel} onConfirm={onSubmit} title={'Create new deck'}>
<form className={s.content} onSubmit={onSubmit}>
<ControlledTextField control={control} label={'Deck name'} name={'name'} />
<ControlledCheckbox
control={control}
label={'Private'}
name={'isPrivate'}
position={'left'}
/>
</form>
</Dialog>
)
return (
<Dialog
{...dialogProps}
onCancel={handleCancel}
onConfirm={onSubmit}
title={'Create new deck'}
>
<form className={s.content} onSubmit={onSubmit}>
<ControlledTextField control={control} label={'Deck name'} name={'name'} />
<ControlledCheckbox
control={control}
label={'Private'}
name={'isPrivate'}
position={'left'}
/>
</form>
</Dialog>
)
}

View File

@@ -2,86 +2,89 @@ import { Link } from 'react-router-dom'
import { Edit2Outline, PlayCircleOutline, TrashOutline } from '@/assets'
import {
Button,
Column,
Table,
TableBody,
TableCell,
TableHeader,
TableRow,
Typography,
Button,
Column,
Table,
TableBody,
TableCell,
TableHeader,
TableRow,
Typography,
} from '@/components'
import { Deck } from '@/services/decks'
import { formatDate } from '@/utils'
import s from './decks-table.module.scss'
const columns: Column[] = [
{
key: 'name',
title: 'Name',
},
{
key: 'cardsCount',
title: 'Cards',
},
{
key: 'updated',
title: 'Last Updated',
},
{
key: 'author',
title: 'Created By',
},
{
key: 'actions',
title: '',
},
{
key: 'name',
title: 'Name',
},
{
key: 'cardsCount',
title: 'Cards',
},
{
key: 'updated',
title: 'Last Updated',
},
{
key: 'author',
title: 'Created By',
},
{
key: 'actions',
title: '',
},
]
type Props = {
currentUserId: string
decks: Deck[] | undefined
onDeleteClick: (id: string) => void
onEditClick: (id: string) => void
currentUserId: string
decks: Deck[] | undefined
onDeleteClick: (id: string) => void
onEditClick: (id: string) => void
}
export const DecksTable = ({ currentUserId, decks, onDeleteClick, onEditClick }: Props) => {
const handleEditClick = (id: string) => () => onEditClick(id)
const handleDeleteClick = (id: string) => () => onDeleteClick(id)
const handleEditClick = (id: string) => () => onEditClick(id)
const handleDeleteClick = (id: string) => () => onDeleteClick(id)
return (
<Table>
<TableHeader columns={columns} />
<TableBody>
{decks?.map(deck => (
<TableRow key={deck.id}>
<TableCell>
<Typography as={Link} to={`/decks/${deck.id}`} variant={'body2'}>
{deck.name}
</Typography>
</TableCell>
<TableCell>{deck.cardsCount}</TableCell>
<TableCell>{formatDate(deck.updated)}</TableCell>
<TableCell>{deck.author.name}</TableCell>
<TableCell>
<div className={s.iconsContainer}>
<Button as={Link} to={`/decks/${deck.id}/learn`} variant={'icon'}>
<PlayCircleOutline />
</Button>
{deck.author.id === currentUserId && (
<>
<Button onClick={handleEditClick(deck.id)} variant={'icon'}>
<Edit2Outline />
</Button>
<Button onClick={handleDeleteClick(deck.id)} variant={'icon'}>
<TrashOutline />
</Button>
</>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
return (
<Table>
<TableHeader columns={columns} />
<TableBody>
{decks?.map(deck => (
<TableRow key={deck.id}>
<TableCell>
<Typography as={Link} to={`/decks/${deck.id}`} variant={'body2'}>
{deck.name}
</Typography>
</TableCell>
<TableCell>{deck.cardsCount}</TableCell>
<TableCell>{formatDate(deck.updated)}</TableCell>
<TableCell>{deck.author.name}</TableCell>
<TableCell>
<div className={s.iconsContainer}>
<Button as={Link} to={`/decks/${deck.id}/learn`} variant={'icon'}>
<PlayCircleOutline />
</Button>
{deck.author.id === currentUserId && (
<>
<Button onClick={handleEditClick(deck.id)} variant={'icon'}>
<Edit2Outline />
</Button>
<Button
onClick={handleDeleteClick(deck.id)}
variant={'icon'}
>
<TrashOutline />
</Button>
</>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}

View File

@@ -5,35 +5,35 @@ import { Button } from '@/components'
import { Meta, StoryObj } from '@storybook/react'
const meta = {
component: DeleteDeckDialog,
tags: ['autodocs'],
title: 'Decks/Delete Deck Dialog',
component: DeleteDeckDialog,
tags: ['autodocs'],
title: 'Decks/Delete Deck Dialog',
} satisfies Meta<typeof DeleteDeckDialog>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
deckName: 'Deck Name',
onOpenChange: () => {},
open: true,
},
render: args => {
const [open, setOpen] = useState(false)
const closeModal = () => setOpen(false)
args: {
deckName: 'Deck Name',
onOpenChange: () => {},
open: true,
},
render: args => {
const [open, setOpen] = useState(false)
const closeModal = () => setOpen(false)
return (
<>
<Button onClick={() => setOpen(true)}>Open Modal</Button>
<DeleteDeckDialog
{...args}
onCancel={closeModal}
onConfirm={closeModal}
onOpenChange={setOpen}
open={open}
/>
</>
)
},
return (
<>
<Button onClick={() => setOpen(true)}>Open Modal</Button>
<DeleteDeckDialog
{...args}
onCancel={closeModal}
onConfirm={closeModal}
onOpenChange={setOpen}
open={open}
/>
</>
)
},
}

View File

@@ -3,17 +3,17 @@ import { Dialog, DialogProps } from '@/components'
import s from './delete-deck-dialog.module.scss'
export default {}
type Props = Pick<DialogProps, 'onCancel' | 'onConfirm' | 'onOpenChange' | 'open'> & {
deckName: string
deckName: string
}
export const DeleteDeckDialog = ({ deckName, ...dialogProps }: Props) => {
return (
<Dialog {...dialogProps} title={'Delete deck'}>
<div className={s.content}>
<p>
Do you really want to remove <strong>{deckName}</strong>?
</p>
<p>All cards will be deleted.</p>
</div>
</Dialog>
)
return (
<Dialog {...dialogProps} title={'Delete deck'}>
<div className={s.content}>
<p>
Do you really want to remove <strong>{deckName}</strong>?
</p>
<p>All cards will be deleted.</p>
</div>
</Dialog>
)
}

View File

@@ -3,27 +3,27 @@ import type { Meta, StoryObj } from '@storybook/react'
import { PersonalInformation } from './'
const meta = {
component: PersonalInformation,
tags: ['autodocs'],
title: 'Profile/Personal information',
component: PersonalInformation,
tags: ['autodocs'],
title: 'Profile/Personal information',
} satisfies Meta<typeof PersonalInformation>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
avatar: 'https://picsum.photos/200',
email: 'your_email@domain.com',
name: 'John Doe',
onAvatarChange: () => {
console.info('avatar changed')
args: {
avatar: 'https://picsum.photos/200',
email: 'your_email@domain.com',
name: 'John Doe',
onAvatarChange: () => {
console.info('avatar changed')
},
onLogout: () => {
console.info('logout')
},
onNameChange: () => {
console.info('name changed')
},
},
onLogout: () => {
console.info('logout')
},
onNameChange: () => {
console.info('name changed')
},
},
}

View File

@@ -4,62 +4,62 @@ import { Button, Card, Typography } from '../../ui'
import s from './personal-information.module.scss'
type Props = {
avatar: string
email: string
name: string
onAvatarChange: (newAvatar: string) => void
onLogout: () => void
onNameChange: (newName: string) => void
avatar: string
email: string
name: string
onAvatarChange: (newAvatar: string) => void
onLogout: () => void
onNameChange: (newName: string) => void
}
export const PersonalInformation = ({
avatar,
email,
name,
onAvatarChange,
onLogout,
onNameChange,
avatar,
email,
name,
onAvatarChange,
onLogout,
onNameChange,
}: Props) => {
const handleAvatarChanged = () => {
onAvatarChange('new Avatar')
}
const handleNameChanged = () => {
onNameChange('New name')
}
const handleLogout = () => {
onLogout()
}
const handleAvatarChanged = () => {
onAvatarChange('new Avatar')
}
const handleNameChanged = () => {
onNameChange('New name')
}
const handleLogout = () => {
onLogout()
}
return (
<Card className={s.card}>
<Typography className={s.title} variant={'large'}>
Personal Information
</Typography>
<div className={s.photoContainer}>
<div>
<img alt={'avatar'} src={avatar} />
<button className={s.editAvatarButton} onClick={handleAvatarChanged}>
<Camera />
</button>
</div>
</div>
<div className={s.nameWithEditButton}>
<Typography className={s.name} variant={'h1'}>
{name}
</Typography>
<button className={s.editNameButton} onClick={handleNameChanged}>
<Edit />
</button>
</div>
<Typography className={s.email} variant={'body2'}>
{/* eslint-disable-next-line react/no-unescaped-entities */}
{email}
</Typography>
<div className={s.buttonContainer}>
<Button onClick={handleLogout} variant={'secondary'}>
<Logout />
Sign Out
</Button>
</div>
</Card>
)
return (
<Card className={s.card}>
<Typography className={s.title} variant={'large'}>
Personal Information
</Typography>
<div className={s.photoContainer}>
<div>
<img alt={'avatar'} src={avatar} />
<button className={s.editAvatarButton} onClick={handleAvatarChanged}>
<Camera />
</button>
</div>
</div>
<div className={s.nameWithEditButton}>
<Typography className={s.name} variant={'h1'}>
{name}
</Typography>
<button className={s.editNameButton} onClick={handleNameChanged}>
<Edit />
</button>
</div>
<Typography className={s.email} variant={'body2'}>
{/* eslint-disable-next-line react/no-unescaped-entities */}
{email}
</Typography>
<div className={s.buttonContainer}>
<Button onClick={handleLogout} variant={'secondary'}>
<Logout />
Sign Out
</Button>
</div>
</Card>
)
}

View File

@@ -1,65 +1,67 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './'
import {Camera} from "@/assets";
const meta = {
argTypes: {
variant: {
control: { type: 'radio' },
options: ['primary', 'secondary', 'tertiary', 'link'],
argTypes: {
variant: {
control: { type: 'radio' },
options: ['primary', 'secondary', 'tertiary', 'link'],
},
},
},
component: Button,
tags: ['autodocs'],
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
title: 'Components/Button',
} satisfies Meta<typeof Button>
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {
children: 'Primary Button',
disabled: false,
variant: 'primary',
},
args: {
children: <>Turn Camera On <Camera/></>,
disabled: false,
variant: 'primary',
},
}
export const Secondary: Story = {
args: {
children: 'Secondary Button',
disabled: false,
variant: 'secondary',
},
args: {
children: 'Secondary Button',
disabled: false,
variant: 'secondary',
},
}
export const Tertiary: Story = {
args: {
children: 'Tertiary Button',
disabled: false,
variant: 'tertiary',
},
args: {
children: 'Tertiary Button',
disabled: false,
variant: 'tertiary',
},
}
export const Link: Story = {
args: {
children: 'Tertiary Button',
disabled: false,
variant: 'link',
},
args: {
children: 'Tertiary Button',
disabled: false,
variant: 'link',
},
}
export const FullWidth: Story = {
args: {
children: 'Full Width Button',
disabled: false,
fullWidth: true,
variant: 'primary',
},
args: {
children: 'Full Width Button',
disabled: false,
fullWidth: true,
variant: 'primary',
},
}
export const AsLink: Story = {
args: {
as: 'a',
children: 'Link that looks like a button',
variant: 'primary',
},
args: {
as: 'button',
children: 'Link that looks like a button',
variant: 'primary',
href: 'https://google.com',
},
}

View File

@@ -3,17 +3,20 @@ import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react'
import s from './button.module.scss'
export type ButtonProps<T extends ElementType = 'button'> = {
as?: T
children: ReactNode
className?: string
fullWidth?: boolean
variant?: 'icon' | 'link' | 'primary' | 'secondary' | 'tertiary'
as?: T
children: ReactNode
className?: string
fullWidth?: 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 { as: Component = 'button', className, fullWidth, variant = 'primary', ...rest } = props
return (
<Component className={`${s[variant]} ${fullWidth ? s.fullWidth : ''} ${className}`} {...rest} />
)
return (
<Component
className={`${s[variant]} ${fullWidth ? s.fullWidth : ''} ${className}`}
{...rest}
/>
)
}

View File

@@ -4,21 +4,21 @@ import { Card } from './'
import { Typography } from '@/components'
const meta = {
component: Card,
tags: ['autodocs'],
title: 'Components/Card',
component: Card,
tags: ['autodocs'],
title: 'Components/Card',
} satisfies Meta<typeof Card>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
children: <Typography variant={'large'}>Card</Typography>,
style: {
height: '300px',
padding: '24px',
width: '300px',
args: {
children: <Typography variant={'large'}>Card</Typography>,
style: {
height: '300px',
padding: '24px',
width: '300px',
},
},
},
}

View File

@@ -7,9 +7,9 @@ import s from './card.module.scss'
export type CardProps = {} & ComponentPropsWithoutRef<'div'>
export const Card = forwardRef<HTMLDivElement, CardProps>(({ className, ...restProps }, ref) => {
const classNames = {
root: clsx(s.root, className),
}
const classNames = {
root: clsx(s.root, className),
}
return <div className={classNames.root} ref={ref} {...restProps}></div>
return <div className={classNames.root} ref={ref} {...restProps}></div>
})

View File

@@ -3,32 +3,32 @@ import { useState } from 'react'
import { Checkbox } from './checkbox'
import { Meta, StoryObj } from '@storybook/react'
const meta = {
component: Checkbox,
tags: ['autodocs'],
title: 'Components/Checkbox',
component: Checkbox,
tags: ['autodocs'],
title: 'Components/Checkbox',
} satisfies Meta<typeof Checkbox>
export default meta
type Story = StoryObj<typeof meta>
export const Uncontrolled: Story = {
args: {
disabled: false,
label: 'Click here',
},
args: {
disabled: false,
label: 'Click here',
},
}
export const Controlled: Story = {
render: args => {
const [checked, setChecked] = useState(false)
render: args => {
const [checked, setChecked] = useState(false)
return (
<Checkbox
{...args}
checked={checked}
label={'Click here'}
onChange={() => setChecked(!checked)}
/>
)
},
return (
<Checkbox
{...args}
checked={checked}
label={'Click here'}
onChange={() => setChecked(!checked)}
/>
)
},
}

View File

@@ -9,57 +9,60 @@ import { clsx } from 'clsx'
import s from './checkbox.module.scss'
export type CheckboxProps = {
checked?: boolean
className?: string
disabled?: boolean
id?: string
label?: string
onChange?: (checked: boolean) => void
position?: 'left'
required?: boolean
checked?: boolean
className?: string
disabled?: boolean
id?: string
label?: string
onChange?: (checked: boolean) => void
position?: 'left'
required?: boolean
}
export const Checkbox: FC<CheckboxProps> = ({
checked,
className,
disabled,
id,
label,
onChange,
position,
required,
checked,
className,
disabled,
id,
label,
onChange,
position,
required,
}) => {
const classNames = {
buttonWrapper: clsx(s.buttonWrapper, disabled && s.disabled, position === 'left' && s.left),
container: clsx(s.container, className),
indicator: s.indicator,
label: clsx(s.label, disabled && s.disabled),
root: s.root,
}
const classNames = {
buttonWrapper: clsx(s.buttonWrapper, disabled && s.disabled, position === 'left' && s.left),
container: clsx(s.container, className),
indicator: s.indicator,
label: clsx(s.label, disabled && s.disabled),
root: s.root,
}
return (
<div className={classNames.container}>
<LabelRadix.Root asChild>
<Typography as={'label'} className={classNames.label} variant={'body2'}>
<div className={classNames.buttonWrapper}>
<CheckboxRadix.Root
checked={checked}
className={classNames.root}
disabled={disabled}
id={id}
onCheckedChange={onChange}
required={required}
>
{checked && (
<CheckboxRadix.Indicator className={classNames.indicator} forceMount>
<Check />
</CheckboxRadix.Indicator>
)}
</CheckboxRadix.Root>
</div>
{label}
</Typography>
</LabelRadix.Root>
</div>
)
return (
<div className={classNames.container}>
<LabelRadix.Root asChild>
<Typography as={'label'} className={classNames.label} variant={'body2'}>
<div className={classNames.buttonWrapper}>
<CheckboxRadix.Root
checked={checked}
className={classNames.root}
disabled={disabled}
id={id}
onCheckedChange={onChange}
required={required}
>
{checked && (
<CheckboxRadix.Indicator
className={classNames.indicator}
forceMount
>
<Check />
</CheckboxRadix.Indicator>
)}
</CheckboxRadix.Root>
</div>
{label}
</Typography>
</LabelRadix.Root>
</div>
)
}

View File

@@ -3,34 +3,34 @@ import { FieldValues, UseControllerProps, useController } from 'react-hook-form'
import { Checkbox, CheckboxProps } from '../../'
export type ControlledCheckboxProps<TFieldValues extends FieldValues> =
UseControllerProps<TFieldValues> & Omit<CheckboxProps, 'id' | 'onChange' | 'value'>
UseControllerProps<TFieldValues> & Omit<CheckboxProps, 'id' | 'onChange' | 'value'>
export const ControlledCheckbox = <TFieldValues extends FieldValues>({
control,
defaultValue,
name,
rules,
shouldUnregister,
...checkboxProps
}: ControlledCheckboxProps<TFieldValues>) => {
const {
field: { onChange, value },
} = useController({
control,
defaultValue,
name,
rules,
shouldUnregister,
})
...checkboxProps
}: ControlledCheckboxProps<TFieldValues>) => {
const {
field: { onChange, value },
} = useController({
control,
defaultValue,
name,
rules,
shouldUnregister,
})
return (
<Checkbox
{...{
checked: value,
id: name,
onChange,
...checkboxProps,
}}
/>
)
return (
<Checkbox
{...{
checked: value,
id: name,
onChange,
...checkboxProps,
}}
/>
)
}

View File

@@ -3,28 +3,28 @@ import { Control, FieldPath, FieldValues, useController } from 'react-hook-form'
import { RadioGroup, RadioGroupProps } from '@/components/ui'
export type ControlledRadioGroupProps<TFieldValues extends FieldValues> = {
control: Control<TFieldValues>
name: FieldPath<TFieldValues>
control: Control<TFieldValues>
name: FieldPath<TFieldValues>
} & Omit<RadioGroupProps, 'id' | 'onChange' | 'value'>
export const ControlledRadioGroup = <TFieldValues extends FieldValues>(
props: ControlledRadioGroupProps<TFieldValues>
props: ControlledRadioGroupProps<TFieldValues>
) => {
const {
field: { onChange, ...field },
fieldState: { error },
} = useController({
control: props.control,
name: props.name,
})
const {
field: { onChange, ...field },
fieldState: { error },
} = useController({
control: props.control,
name: props.name,
})
return (
<RadioGroup
{...props}
{...field}
errorMessage={error?.message}
id={props.name}
onValueChange={onChange}
/>
)
return (
<RadioGroup
{...props}
{...field}
errorMessage={error?.message}
id={props.name}
onValueChange={onChange}
/>
)
}

View File

@@ -3,20 +3,20 @@ import { Control, FieldPath, FieldValues, useController } from 'react-hook-form'
import { TextField, TextFieldProps } from '@/components'
export type ControlledTextFieldProps<TFieldValues extends FieldValues> = {
control: Control<TFieldValues>
name: FieldPath<TFieldValues>
control: Control<TFieldValues>
name: FieldPath<TFieldValues>
} & Omit<TextFieldProps, 'id' | 'onChange' | 'value'>
export const ControlledTextField = <TFieldValues extends FieldValues>(
props: ControlledTextFieldProps<TFieldValues>
props: ControlledTextFieldProps<TFieldValues>
) => {
const {
field,
fieldState: { error },
} = useController({
control: props.control,
name: props.name,
})
const {
field,
fieldState: { error },
} = useController({
control: props.control,
name: props.name,
})
return <TextField {...props} {...field} errorMessage={error?.message} id={props.name} />
return <TextField {...props} {...field} errorMessage={error?.message} id={props.name} />
}

View File

@@ -4,31 +4,31 @@ import { Dialog } from './'
import { Meta, StoryObj } from '@storybook/react'
const meta = {
component: Dialog,
tags: ['autodocs'],
title: 'Components/Dialog',
component: Dialog,
tags: ['autodocs'],
title: 'Components/Dialog',
} satisfies Meta<typeof Dialog>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
children: 'Modal',
onOpenChange: () => {},
open: true,
title: 'Modal',
},
render: args => {
const [open, setOpen] = useState(false)
args: {
children: 'Modal',
onOpenChange: () => {},
open: true,
title: 'Modal',
},
render: args => {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>Open Modal</button>
<Dialog {...args} onOpenChange={setOpen} open={open}>
Dialog content here
</Dialog>
</>
)
},
return (
<>
<button onClick={() => setOpen(true)}>Open Modal</button>
<Dialog {...args} onOpenChange={setOpen} open={open}>
Dialog content here
</Dialog>
</>
)
},
}

View File

@@ -3,28 +3,28 @@ import { Button, Modal, ModalProps } from '@/components'
import s from './dialog.module.scss'
export type DialogProps = ModalProps & {
cancelText?: string
confirmText?: string
onCancel?: () => void
onConfirm?: () => void
cancelText?: string
confirmText?: string
onCancel?: () => void
onConfirm?: () => void
}
export const Dialog = ({
cancelText = 'Cancel',
children,
confirmText = 'OK',
onCancel,
onConfirm,
...modalProps
cancelText = 'Cancel',
children,
confirmText = 'OK',
onCancel,
onConfirm,
...modalProps
}: DialogProps) => {
return (
<Modal {...modalProps}>
{children}
<div className={s.buttons}>
<Button onClick={onCancel} variant={'secondary'}>
{cancelText}
</Button>
<Button onClick={onConfirm}>{confirmText}</Button>
</div>
</Modal>
)
return (
<Modal {...modalProps}>
{children}
<div className={s.buttons}>
<Button onClick={onCancel} variant={'secondary'}>
{cancelText}
</Button>
<Button onClick={onConfirm}>{confirmText}</Button>
</div>
</Modal>
)
}

View File

@@ -6,18 +6,18 @@ import { clsx } from 'clsx'
import s from './label.module.scss'
export type LabelProps = {
label?: ReactNode
label?: ReactNode
} & ComponentPropsWithoutRef<'label'>
export const Label: FC<LabelProps> = ({ children, className, label, ...rest }) => {
const classNames = {
label: clsx(s.label, className),
}
const classNames = {
label: clsx(s.label, className),
}
return (
<LabelRadixUI.Root {...rest}>
{label && <div className={classNames.label}>{label}</div>}
{children}
</LabelRadixUI.Root>
)
return (
<LabelRadixUI.Root {...rest}>
{label && <div className={classNames.label}>{label}</div>}
{children}
</LabelRadixUI.Root>
)
}

View File

@@ -4,31 +4,31 @@ import { Modal } from '@/components'
import { Meta, StoryObj } from '@storybook/react'
const meta = {
component: Modal,
tags: ['autodocs'],
title: 'Components/Modal',
component: Modal,
tags: ['autodocs'],
title: 'Components/Modal',
} satisfies Meta<typeof Modal>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
children: 'Modal',
onOpenChange: () => {},
open: true,
title: 'Modal',
},
render: args => {
const [open, setOpen] = useState(false)
args: {
children: 'Modal',
onOpenChange: () => {},
open: true,
title: 'Modal',
},
render: args => {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>Open Modal</button>
<Modal {...args} onOpenChange={setOpen} open={open}>
Modal content here
</Modal>
</>
)
},
return (
<>
<button onClick={() => setOpen(true)}>Open Modal</button>
<Modal {...args} onOpenChange={setOpen} open={open}>
Modal content here
</Modal>
</>
)
},
}

View File

@@ -7,30 +7,30 @@ import * as DialogPrimitive from '@radix-ui/react-dialog'
import s from './modal.module.scss'
export type ModalProps = {
children: ReactNode
onOpenChange: (open: boolean) => void
open: boolean
title?: string
children: ReactNode
onOpenChange: (open: boolean) => void
open: boolean
title?: string
} & Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Dialog>, 'onOpenChange' | 'open'>
export const Modal = ({ children, title, ...props }: ModalProps) => {
return (
<DialogPrimitive.Root {...props}>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className={s.overlay} />
<DialogPrimitive.Content className={s.content}>
<div className={s.header}>
<DialogPrimitive.Title asChild>
<Typography as={'h2'} variant={'h2'}>
{title}
</Typography>
</DialogPrimitive.Title>
<DialogPrimitive.Close className={s.closeButton}>
<Close />
</DialogPrimitive.Close>
</div>
{children}
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
)
return (
<DialogPrimitive.Root {...props}>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className={s.overlay} />
<DialogPrimitive.Content className={s.content}>
<div className={s.header}>
<DialogPrimitive.Title asChild>
<Typography as={'h2'} variant={'h2'}>
{title}
</Typography>
</DialogPrimitive.Title>
<DialogPrimitive.Close className={s.closeButton}>
<Close />
</DialogPrimitive.Close>
</div>
{children}
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
)
}

View File

@@ -7,9 +7,9 @@ import s from './page.module.scss'
export type PageProps = ComponentPropsWithoutRef<'div'>
export const Page = forwardRef<HTMLDivElement, PageProps>(({ className, ...props }, ref) => {
const classNames = {
root: clsx(s.root, className),
}
const classNames = {
root: clsx(s.root, className),
}
return <div {...props} className={classNames.root} ref={ref} />
return <div {...props} className={classNames.root} ref={ref} />
})

View File

@@ -7,187 +7,194 @@ import { clsx } from 'clsx'
import s from './pagination.module.scss'
type PaginationConditionals =
| {
onPerPageChange: (itemPerPage: number) => void
perPage: number
perPageOptions: number[]
}
| {
onPerPageChange?: never
perPage?: null
perPageOptions?: never
}
| {
onPerPageChange: (itemPerPage: number) => void
perPage: number
perPageOptions: number[]
}
| {
onPerPageChange?: never
perPage?: null
perPageOptions?: never
}
export type PaginationProps = {
count: number
onChange: (page: number) => void
onPerPageChange?: (itemPerPage: number) => void
page: number
perPage?: number
perPageOptions?: number[]
siblings?: number
count: number
onChange: (page: number) => void
onPerPageChange?: (itemPerPage: number) => void
page: number
perPage?: number
perPageOptions?: number[]
siblings?: number
} & PaginationConditionals
const classNames = {
container: s.container,
dots: s.dots,
icon: s.icon,
item: s.item,
pageButton(selected?: boolean) {
return clsx(this.item, selected && s.selected)
},
root: s.root,
select: s.select,
selectBox: s.selectBox,
container: s.container,
dots: s.dots,
icon: s.icon,
item: s.item,
pageButton(selected?: boolean) {
return clsx(this.item, selected && s.selected)
},
root: s.root,
select: s.select,
selectBox: s.selectBox,
}
export const Pagination: FC<PaginationProps> = ({
count,
onChange,
onPerPageChange,
page,
perPage = null,
perPageOptions,
siblings,
}) => {
const {
handleMainPageClicked,
handleNextPageClicked,
handlePreviousPageClicked,
isFirstPage,
isLastPage,
paginationRange,
} = usePagination({
count,
onChange,
onPerPageChange,
page,
perPage = null,
perPageOptions,
siblings,
})
}) => {
const {
handleMainPageClicked,
handleNextPageClicked,
handlePreviousPageClicked,
isFirstPage,
isLastPage,
paginationRange,
} = usePagination({
count,
onChange,
page,
siblings,
})
const showPerPageSelect = !!perPage && !!perPageOptions && !!onPerPageChange
const showPerPageSelect = !!perPage && !!perPageOptions && !!onPerPageChange
return (
<div className={classNames.root}>
<div className={classNames.container}>
<PrevButton disabled={isFirstPage} onClick={handlePreviousPageClicked} />
return (
<div className={classNames.root}>
<div className={classNames.container}>
<PrevButton disabled={isFirstPage} onClick={handlePreviousPageClicked} />
<MainPaginationButtons
currentPage={page}
onClick={handleMainPageClicked}
paginationRange={paginationRange}
/>
<MainPaginationButtons
currentPage={page}
onClick={handleMainPageClicked}
paginationRange={paginationRange}
/>
<NextButton disabled={isLastPage} onClick={handleNextPageClicked} />
</div>
<NextButton disabled={isLastPage} onClick={handleNextPageClicked} />
</div>
{showPerPageSelect && (
<PerPageSelect
{...{
onPerPageChange,
perPage,
perPageOptions,
}}
/>
)}
</div>
)
{showPerPageSelect && (
<PerPageSelect
{...{
onPerPageChange,
perPage,
perPageOptions,
}}
/>
)}
</div>
)
}
type NavigationButtonProps = {
disabled?: boolean
onClick: () => void
disabled?: boolean
onClick: () => void
}
type PageButtonProps = NavigationButtonProps & {
page: number
selected: boolean
page: number
selected: boolean
}
const Dots: FC = () => {
return <span className={classNames.dots}>&#8230;</span>
return <span className={classNames.dots}>&#8230;</span>
}
const PageButton: FC<PageButtonProps> = ({ disabled, onClick, page, selected }) => {
return (
<button
className={classNames.pageButton(selected)}
disabled={selected || disabled}
onClick={onClick}
>
{page}
</button>
)
return (
<button
className={classNames.pageButton(selected)}
disabled={selected || disabled}
onClick={onClick}
>
{page}
</button>
)
}
const PrevButton: FC<NavigationButtonProps> = ({ disabled, onClick }) => {
return (
<button className={classNames.item} disabled={disabled} onClick={onClick}>
<KeyboardArrowLeft className={classNames.icon} />
</button>
)
return (
<button className={classNames.item} disabled={disabled} onClick={onClick}>
<KeyboardArrowLeft className={classNames.icon} />
</button>
)
}
const NextButton: FC<NavigationButtonProps> = ({ disabled, onClick }) => {
return (
<button className={classNames.item} disabled={disabled} onClick={onClick}>
<KeyboardArrowRight className={classNames.icon} />
</button>
)
return (
<button className={classNames.item} disabled={disabled} onClick={onClick}>
<KeyboardArrowRight className={classNames.icon} />
</button>
)
}
type MainPaginationButtonsProps = {
currentPage: number
onClick: (pageNumber: number) => () => void
paginationRange: (number | string)[]
currentPage: number
onClick: (pageNumber: number) => () => void
paginationRange: (number | string)[]
}
const MainPaginationButtons: FC<MainPaginationButtonsProps> = ({
currentPage,
onClick,
paginationRange,
currentPage,
onClick,
paginationRange,
}) => {
return (
<>
{paginationRange.map((page: number | string, index) => {
const isSelected = page === currentPage
return (
<>
{paginationRange.map((page: number | string, index) => {
const isSelected = page === currentPage
if (typeof page !== 'number') {
return <Dots key={index} />
}
if (typeof page !== 'number') {
return <Dots key={index} />
}
return <PageButton key={index} onClick={onClick(page)} page={page} selected={isSelected} />
})}
</>
)
return (
<PageButton
key={index}
onClick={onClick(page)}
page={page}
selected={isSelected}
/>
)
})}
</>
)
}
export type PerPageSelectProps = {
onPerPageChange: (itemPerPage: number) => void
perPage: number
perPageOptions: number[]
onPerPageChange: (itemPerPage: number) => void
perPage: number
perPageOptions: number[]
}
export const PerPageSelect: FC<PerPageSelectProps> = (
{
// perPage,
// perPageOptions,
// onPerPageChange,
}
{
// perPage,
// perPageOptions,
// onPerPageChange,
}
) => {
// const selectOptions = perPageOptions.map(value => ({
// label: value,
// value,
// }))
// const selectOptions = perPageOptions.map(value => ({
// label: value,
// value,
// }))
return (
<div className={classNames.selectBox}>
Показать
{/*<Select*/}
{/* className={classNames.select}*/}
{/* value={perPage}*/}
{/* options={selectOptions}*/}
{/* onChange={onPerPageChange}*/}
{/* variant="pagination"*/}
{/*/>*/}
на странице
</div>
)
return (
<div className={classNames.selectBox}>
Показать
{/*<Select*/}
{/* className={classNames.select}*/}
{/* value={perPage}*/}
{/* options={selectOptions}*/}
{/* onChange={onPerPageChange}*/}
{/* variant="pagination"*/}
{/*/>*/}
на странице
</div>
)
}

View File

@@ -3,110 +3,110 @@ import { useCallback, useMemo } from 'react'
// original code: https://www.freecodecamp.org/news/build-a-custom-pagination-component-in-react/
const range = (start: number, end: number) => {
let length = end - start + 1
const length = end - start + 1
/*
/*
Create an array of certain length and set the elements within it from
start value to end value.
*/
return Array.from({ length }, (_, idx) => idx + start)
return Array.from({ length }, (_, idx) => idx + start)
}
const DOTS = '...'
type UsePaginationParamType = {
count: number
siblings?: number
page: number
onChange: (pageNumber: number) => void
count: number
onChange: (pageNumber: number) => void
page: number
siblings?: number
}
type PaginationRange = (number | '...')[]
type PaginationRange = ('...' | number)[]
export const usePagination = ({ count, siblings = 1, page, onChange }: UsePaginationParamType) => {
const paginationRange = useMemo(() => {
// Pages count is determined as siblingCount + firstPage + lastPage + page + 2*DOTS
const totalPageNumbers = siblings + 5
export const usePagination = ({ count, onChange, page, siblings = 1 }: UsePaginationParamType) => {
const paginationRange = useMemo(() => {
// Pages count is determined as siblingCount + firstPage + lastPage + page + 2*DOTS
const totalPageNumbers = siblings + 5
/*
/*
Case 1:
If the number of pages is less than the page numbers we want to show in our
paginationComponent, we return the range [1..totalPageCount]
*/
if (totalPageNumbers >= count) {
return range(1, count)
}
if (totalPageNumbers >= count) {
return range(1, count)
}
/*
/*
Calculate left and right sibling index and make sure they are within range 1 and totalPageCount
*/
const leftSiblingIndex = Math.max(page - siblings, 1)
const rightSiblingIndex = Math.min(page + siblings, count)
const leftSiblingIndex = Math.max(page - siblings, 1)
const rightSiblingIndex = Math.min(page + siblings, count)
/*
/*
We do not show dots when there is only one page number to be inserted
between the extremes of siblings and the page limits i.e 1 and totalPageCount.
Hence, we are using leftSiblingIndex > 2 and rightSiblingIndex < totalPageCount - 2
*/
const shouldShowLeftDots = leftSiblingIndex > 2
const shouldShowRightDots = rightSiblingIndex < count - 2
const shouldShowLeftDots = leftSiblingIndex > 2
const shouldShowRightDots = rightSiblingIndex < count - 2
const firstPageIndex = 1
const lastPageIndex = count
const firstPageIndex = 1
const lastPageIndex = count
/*
/*
Case 2: No left dots to show, but rights dots to be shown
*/
if (!shouldShowLeftDots && shouldShowRightDots) {
let leftItemCount = 3 + 2 * siblings
let leftRange = range(1, leftItemCount)
if (!shouldShowLeftDots && shouldShowRightDots) {
const leftItemCount = 3 + 2 * siblings
const leftRange = range(1, leftItemCount)
return [...leftRange, DOTS, count]
}
return [...leftRange, DOTS, count]
}
/*
/*
Case 3: No right dots to show, but left dots to be shown
*/
if (shouldShowLeftDots && !shouldShowRightDots) {
let rightItemCount = 3 + 2 * siblings
let rightRange = range(count - rightItemCount + 1, count)
if (shouldShowLeftDots && !shouldShowRightDots) {
const rightItemCount = 3 + 2 * siblings
const rightRange = range(count - rightItemCount + 1, count)
return [firstPageIndex, DOTS, ...rightRange]
}
return [firstPageIndex, DOTS, ...rightRange]
}
/*
/*
Case 4: Both left and right dots to be shown
*/
if (shouldShowLeftDots && shouldShowRightDots) {
let middleRange = range(leftSiblingIndex, rightSiblingIndex)
if (shouldShowLeftDots && shouldShowRightDots) {
const middleRange = range(leftSiblingIndex, rightSiblingIndex)
return [firstPageIndex, DOTS, ...middleRange, DOTS, lastPageIndex]
return [firstPageIndex, DOTS, ...middleRange, DOTS, lastPageIndex]
}
}, [siblings, page, count]) as PaginationRange
const lastPage = paginationRange.at(-1)
const isFirstPage = page === 1
const isLastPage = page === lastPage
const handleNextPageClicked = useCallback(() => {
onChange(page + 1)
}, [page, onChange])
const handlePreviousPageClicked = useCallback(() => {
onChange(page - 1)
}, [page, onChange])
function handleMainPageClicked(pageNumber: number) {
return () => onChange(pageNumber)
}
}, [siblings, page, count]) as PaginationRange
const lastPage = paginationRange.at(-1)
const isFirstPage = page === 1
const isLastPage = page === lastPage
const handleNextPageClicked = useCallback(() => {
onChange(page + 1)
}, [page, onChange])
const handlePreviousPageClicked = useCallback(() => {
onChange(page - 1)
}, [page, onChange])
function handleMainPageClicked(pageNumber: number) {
return () => onChange(pageNumber)
}
return {
paginationRange,
isFirstPage,
isLastPage,
handleMainPageClicked,
handleNextPageClicked,
handlePreviousPageClicked,
}
return {
handleMainPageClicked,
handleNextPageClicked,
handlePreviousPageClicked,
isFirstPage,
isLastPage,
paginationRange,
}
}

View File

@@ -3,29 +3,29 @@ import type { Meta, StoryObj } from '@storybook/react'
import { RadioGroup } from './'
const meta = {
component: RadioGroup,
tags: ['autodocs'],
title: 'Components/Radio Group',
component: RadioGroup,
tags: ['autodocs'],
title: 'Components/Radio Group',
} satisfies Meta<typeof RadioGroup>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
options: [
{ label: 'Option One', value: 'option-one' },
{ label: 'Option Two', value: 'option-two' },
],
},
args: {
options: [
{ label: 'Option One', value: 'option-one' },
{ label: 'Option Two', value: 'option-two' },
],
},
}
export const Disabled: Story = {
args: {
disabled: true,
options: [
{ label: 'Option One', value: 'option-one' },
{ label: 'Option Two', value: 'option-two' },
],
},
args: {
disabled: true,
options: [
{ label: 'Option One', value: 'option-one' },
{ label: 'Option Two', value: 'option-two' },
],
},
}

View File

@@ -7,56 +7,56 @@ import { clsx } from 'clsx'
import s from './radio-group.module.scss'
const RadioGroupRoot = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return <RadioGroupPrimitive.Root className={clsx(s.root, className)} {...props} ref={ref} />
return <RadioGroupPrimitive.Root className={clsx(s.root, className)} {...props} ref={ref} />
})
RadioGroupRoot.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ children, className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item className={clsx(s.option, className)} ref={ref} {...props}>
<div className={s.icon}></div>
</RadioGroupPrimitive.Item>
)
return (
<RadioGroupPrimitive.Item className={clsx(s.option, className)} ref={ref} {...props}>
<div className={s.icon}></div>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
type Option = {
label: string
value: string
label: string
value: string
}
export type RadioGroupProps = Omit<
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>,
'children'
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>,
'children'
> & {
errorMessage?: string
options: Option[]
errorMessage?: string
options: Option[]
}
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
RadioGroupProps
React.ElementRef<typeof RadioGroupPrimitive.Root>,
RadioGroupProps
>((props, ref) => {
const { errorMessage, options, ...restProps } = props
const { errorMessage, options, ...restProps } = props
return (
<RadioGroupRoot {...restProps} ref={ref}>
{options.map(option => (
<div className={s.label} key={option.value}>
<RadioGroupItem id={option.value} value={option.value} />
<Typography as={'label'} htmlFor={option.value} variant={'body2'}>
{option.label}
</Typography>
</div>
))}
</RadioGroupRoot>
)
return (
<RadioGroupRoot {...restProps} ref={ref}>
{options.map(option => (
<div className={s.label} key={option.value}>
<RadioGroupItem id={option.value} value={option.value} />
<Typography as={'label'} htmlFor={option.value} variant={'body2'}>
{option.label}
</Typography>
</div>
))}
</RadioGroupRoot>
)
})
export { RadioGroup, RadioGroupItem, RadioGroupRoot }

View File

@@ -5,37 +5,37 @@ import { clsx } from 'clsx'
import s from './slider.module.scss'
const Slider = forwardRef<
ElementRef<typeof SliderPrimitive.Root>,
Omit<ComponentPropsWithoutRef<typeof SliderPrimitive.Root>, 'value'> & {
value?: (number | undefined)[]
}
>(({ className, max, onValueChange, value, ...props }, ref) => {
useEffect(() => {
if (value?.[1] === undefined || value?.[1] === null) {
onValueChange?.([value?.[0] ?? 0, max ?? 0])
ElementRef<typeof SliderPrimitive.Root>,
Omit<ComponentPropsWithoutRef<typeof SliderPrimitive.Root>, 'value'> & {
value?: (number | undefined)[]
}
}, [max, value, onValueChange])
>(({ className, max, onValueChange, value, ...props }, ref) => {
useEffect(() => {
if (value?.[1] === undefined || value?.[1] === null) {
onValueChange?.([value?.[0] ?? 0, max ?? 0])
}
}, [max, value, onValueChange])
return (
<div className={s.container}>
<span>{value?.[0]}</span>
<SliderPrimitive.Root
className={clsx(s.root, className)}
max={max}
onValueChange={onValueChange}
ref={ref}
{...props}
value={[value?.[0] ?? 0, value?.[1] ?? max ?? 0]}
>
<SliderPrimitive.Track className={s.track}>
<SliderPrimitive.Range className={'absolute h-full bg-primary'} />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className={s.thumb} />
<SliderPrimitive.Thumb className={s.thumb} />
</SliderPrimitive.Root>
<span>{value?.[1]}</span>
</div>
)
return (
<div className={s.container}>
<span>{value?.[0]}</span>
<SliderPrimitive.Root
className={clsx(s.root, className)}
max={max}
onValueChange={onValueChange}
ref={ref}
{...props}
value={[value?.[0] ?? 0, value?.[1] ?? max ?? 0]}
>
<SliderPrimitive.Track className={s.track}>
<SliderPrimitive.Range className={'absolute h-full bg-primary'} />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className={s.thumb} />
<SliderPrimitive.Thumb className={s.thumb} />
</SliderPrimitive.Root>
<span>{value?.[1]}</span>
</div>
)
})
Slider.displayName = SliderPrimitive.Root.displayName

View File

@@ -3,124 +3,127 @@ import { Typography } from '@/components'
import { Meta } from '@storybook/react'
export default {
component: Table,
title: 'Components/Table',
component: Table,
title: 'Components/Table',
} as Meta<typeof Table>
export const Default = {
args: {
children: (
<>
<TableHead>
<TableRow>
<TableHeadCell>Название</TableHeadCell>
<TableHeadCell align={'center'}>Описание</TableHeadCell>
<TableHeadCell>Ссылка</TableHeadCell>
<TableHeadCell>Тип</TableHeadCell>
<TableHeadCell>Вид</TableHeadCell>
<TableHeadCell />
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>Web Basic</TableCell>
<TableCell>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut sed do eiusmod tempoei usmodr sit amet, consectetur adipiscing elit, sed
do...
</TableCell>
<TableCell>
<Typography
as={'a'}
href={'https://it-incubator.io/'}
target={'_blank'}
variant={'link1'}
>
Какая-то ссылка кудато на какой-то источник с информациейо ссылка кудато на какой-то
источник
</Typography>
</TableCell>
<TableCell>Основной</TableCell>
<TableCell>Читать</TableCell>
<TableCell>🦎</TableCell>
</TableRow>
<TableRow>
<TableCell>Web Basic</TableCell>
<TableCell>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut sed do eiusmod tempoei usmodr sit amet, consectetur adipiscing elit, sed
do...
</TableCell>
<TableCell>
Какая-то ссылка кудато на какой-то источник с информациейо ссылка кудато на какой-то
источник
</TableCell>
<TableCell>Основной</TableCell>
<TableCell>Читать</TableCell>
<TableCell></TableCell>
</TableRow>
</TableBody>
</>
),
},
args: {
children: (
<>
<TableHead>
<TableRow>
<TableHeadCell>Название</TableHeadCell>
<TableHeadCell align={'center'}>Описание</TableHeadCell>
<TableHeadCell>Ссылка</TableHeadCell>
<TableHeadCell>Тип</TableHeadCell>
<TableHeadCell>Вид</TableHeadCell>
<TableHeadCell />
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>Web Basic</TableCell>
<TableCell>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut sed do eiusmod tempoei usmodr sit amet, consectetur
adipiscing elit, sed do...
</TableCell>
<TableCell>
<Typography
as={'a'}
href={'https://it-incubator.io/'}
target={'_blank'}
variant={'link1'}
>
Какая-то ссылка кудато на какой-то источник с информациейо ссылка
кудато на какой-то источник
</Typography>
</TableCell>
<TableCell>Основной</TableCell>
<TableCell>Читать</TableCell>
<TableCell>🦎</TableCell>
</TableRow>
<TableRow>
<TableCell>Web Basic</TableCell>
<TableCell>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut sed do eiusmod tempoei usmodr sit amet, consectetur
adipiscing elit, sed do...
</TableCell>
<TableCell>
Какая-то ссылка кудато на какой-то источник с информациейо ссылка кудато
на какой-то источник
</TableCell>
<TableCell>Основной</TableCell>
<TableCell>Читать</TableCell>
<TableCell></TableCell>
</TableRow>
</TableBody>
</>
),
},
}
const data = [
{
category: 'Основной',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor',
id: '01',
link: 'Какая-то ссылка кудато на какой-то источник с информациейо ссылка кудато на какой-то',
title: 'Web Basic',
type: 'Читать',
},
{
category: 'Основной',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor',
id: '02',
link: 'Какая-то ссылка куда-то',
title: 'Web Basic',
type: 'Читать',
},
{
category: 'Основной',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor',
id: '03',
link: 'Какая-то ссылка кудато на какой-то источник с информациейо ссылка кудато на какой-то. Какая-то ссылка кудато на какой-то источник с информациейо ссылка куда-то на какой-то',
title: 'Web Basic',
type: 'Читать',
},
{
category: 'Основной',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor',
id: '01',
link: 'Какая-то ссылка кудато на какой-то источник с информациейо ссылка кудато на какой-то',
title: 'Web Basic',
type: 'Читать',
},
{
category: 'Основной',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor',
id: '02',
link: 'Какая-то ссылка куда-то',
title: 'Web Basic',
type: 'Читать',
},
{
category: 'Основной',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor',
id: '03',
link: 'Какая-то ссылка кудато на какой-то источник с информациейо ссылка кудато на какой-то. Какая-то ссылка кудато на какой-то источник с информациейо ссылка куда-то на какой-то',
title: 'Web Basic',
type: 'Читать',
},
]
export const WithMapMethod = {
args: {
children: (
<>
<TableHead>
<TableRow>
<TableHeadCell>Название</TableHeadCell>
<TableHeadCell align={'center'}>Описание</TableHeadCell>
<TableHeadCell>Ссылка</TableHeadCell>
<TableHeadCell>Тип</TableHeadCell>
<TableHeadCell>Вид</TableHeadCell>
</TableRow>
</TableHead>
<TableBody>
{data.map(item => (
<TableRow key={item.id}>
<TableCell>{item.title}</TableCell>
<TableCell>{item.description}</TableCell>
<TableCell>{item.link}</TableCell>
<TableCell>{item.category}</TableCell>
<TableCell>{item.type}</TableCell>
</TableRow>
))}
</TableBody>
</>
),
},
args: {
children: (
<>
<TableHead>
<TableRow>
<TableHeadCell>Название</TableHeadCell>
<TableHeadCell align={'center'}>Описание</TableHeadCell>
<TableHeadCell>Ссылка</TableHeadCell>
<TableHeadCell>Тип</TableHeadCell>
<TableHeadCell>Вид</TableHeadCell>
</TableRow>
</TableHead>
<TableBody>
{data.map(item => (
<TableRow key={item.id}>
<TableCell>{item.title}</TableCell>
<TableCell>{item.description}</TableCell>
<TableCell>{item.link}</TableCell>
<TableCell>{item.category}</TableCell>
<TableCell>{item.type}</TableCell>
</TableRow>
))}
</TableBody>
</>
),
},
}
export const Empty = {
render: () => <TableEmpty />,
render: () => <TableEmpty />,
}

View File

@@ -6,123 +6,125 @@ import { clsx } from 'clsx'
import s from './table.module.scss'
export const Table = forwardRef<HTMLTableElement, ComponentPropsWithoutRef<'table'>>(
({ className, ...rest }, ref) => {
const classNames = {
table: clsx(className, s.table),
}
({ className, ...rest }, ref) => {
const classNames = {
table: clsx(className, s.table),
}
return <table className={classNames.table} {...rest} ref={ref} />
}
return <table className={classNames.table} {...rest} ref={ref} />
}
)
export const TableHead = forwardRef<ElementRef<'thead'>, ComponentPropsWithoutRef<'thead'>>(
({ ...rest }, ref) => {
return <thead {...rest} ref={ref} />
}
({ ...rest }, ref) => {
return <thead {...rest} ref={ref} />
}
)
export const TableBody = forwardRef<ElementRef<'tbody'>, ComponentPropsWithoutRef<'tbody'>>(
({ ...rest }, ref) => {
return <tbody {...rest} ref={ref} />
}
({ ...rest }, ref) => {
return <tbody {...rest} ref={ref} />
}
)
export const TableRow = forwardRef<ElementRef<'tr'>, ComponentPropsWithoutRef<'tr'>>(
({ ...rest }, ref) => {
return <tr {...rest} ref={ref} />
}
({ ...rest }, ref) => {
return <tr {...rest} ref={ref} />
}
)
export const TableHeadCell = forwardRef<ElementRef<'th'>, ComponentPropsWithoutRef<'th'>>(
({ children, className, ...rest }, ref) => {
const classNames = {
headCell: clsx(className, s.headCell),
}
({ children, className, ...rest }, ref) => {
const classNames = {
headCell: clsx(className, s.headCell),
}
return (
<th className={classNames.headCell} {...rest} ref={ref}>
<span>{children}</span>
</th>
)
}
return (
<th className={classNames.headCell} {...rest} ref={ref}>
<span>{children}</span>
</th>
)
}
)
export const TableCell = forwardRef<ElementRef<'td'>, ComponentPropsWithoutRef<'td'>>(
({ className, ...rest }, ref) => {
const classNames = {
cell: clsx(className, s.tableCell),
}
({ className, ...rest }, ref) => {
const classNames = {
cell: clsx(className, s.tableCell),
}
return <td className={classNames.cell} {...rest} ref={ref} />
}
return <td className={classNames.cell} {...rest} ref={ref} />
}
)
export const TableEmpty: FC<ComponentProps<'div'> & { mb?: string; mt?: string }> = ({
className,
mb,
mt = '89px',
className,
mb,
mt = '89px',
}) => {
const classNames = {
empty: clsx(className, s.empty),
}
const classNames = {
empty: clsx(className, s.empty),
}
return (
<Typography
className={classNames.empty}
style={{ marginBottom: mb, marginTop: mt }}
variant={'h2'}
>
Пока тут еще нет данных! :(
</Typography>
)
return (
<Typography
className={classNames.empty}
style={{ marginBottom: mb, marginTop: mt }}
variant={'h2'}
>
Пока тут еще нет данных! :(
</Typography>
)
}
export type Column = {
key: string
sortable?: boolean
title: string
key: string
sortable?: boolean
title: string
}
export type Sort = {
direction: 'asc' | 'desc'
key: string
direction: 'asc' | 'desc'
key: string
} | null
export const TableHeader: FC<
Omit<
ComponentPropsWithoutRef<'thead'> & {
columns: Column[]
onSort?: (sort: Sort) => void
sort?: Sort
},
'children'
>
Omit<
ComponentPropsWithoutRef<'thead'> & {
columns: Column[]
onSort?: (sort: Sort) => void
sort?: Sort
},
'children'
>
> = ({ columns, onSort, sort, ...restProps }) => {
const handleSort = (key: string, sortable?: boolean) => () => {
if (!onSort || !sortable) {
return
const handleSort = (key: string, sortable?: boolean) => () => {
if (!onSort || !sortable) {
return
}
if (sort?.key !== key) {
return onSort({ direction: 'asc', key })
}
if (sort.direction === 'desc') {
return onSort(null)
}
return onSort({
direction: sort?.direction === 'asc' ? 'desc' : 'asc',
key,
})
}
if (sort?.key !== key) {
return onSort({ direction: 'asc', key })
}
if (sort.direction === 'desc') {
return onSort(null)
}
return onSort({
direction: sort?.direction === 'asc' ? 'desc' : 'asc',
key,
})
}
return (
<TableHead {...restProps}>
<TableRow>
{columns.map(({ key, sortable = true, title }) => (
<TableHeadCell key={key} onClick={handleSort(key, sortable)}>
{title}
{sort && sort.key === key && <span>{sort.direction === 'asc' ? '▲' : '▼'}</span>}
</TableHeadCell>
))}
</TableRow>
</TableHead>
)
return (
<TableHead {...restProps}>
<TableRow>
{columns.map(({ key, sortable = true, title }) => (
<TableHeadCell key={key} onClick={handleSort(key, sortable)}>
{title}
{sort && sort.key === key && (
<span>{sort.direction === 'asc' ? '▲' : '▼'}</span>
)}
</TableHeadCell>
))}
</TableRow>
</TableHead>
)
}

View File

@@ -8,28 +8,28 @@ import s from './tabs.module.scss'
const Tabs = TabsPrimitive.Root
const TabsList = forwardRef<
ElementRef<typeof TabsPrimitive.List>,
ComponentPropsWithoutRef<typeof TabsPrimitive.List>
ElementRef<typeof TabsPrimitive.List>,
ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List className={clsx(s.list, className)} ref={ref} {...props} />
<TabsPrimitive.List className={clsx(s.list, className)} ref={ref} {...props} />
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = forwardRef<
ElementRef<typeof TabsPrimitive.Trigger>,
ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
ElementRef<typeof TabsPrimitive.Trigger>,
ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger className={clsx(s.trigger, className)} ref={ref} {...props} />
<TabsPrimitive.Trigger className={clsx(s.trigger, className)} ref={ref} {...props} />
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = forwardRef<
ElementRef<typeof TabsPrimitive.Content>,
ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
ElementRef<typeof TabsPrimitive.Content>,
ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content className={clsx(s.content, className)} ref={ref} {...props} />
<TabsPrimitive.Content className={clsx(s.content, className)} ref={ref} {...props} />
))
TabsContent.displayName = TabsPrimitive.Content.displayName

View File

@@ -3,33 +3,33 @@ import type { Meta, StoryObj } from '@storybook/react'
import { TextField } from './'
const meta = {
title: 'Components/TextField',
component: TextField,
tags: ['autodocs'],
component: TextField,
tags: ['autodocs'],
title: 'Components/TextField',
} satisfies Meta<typeof TextField>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
label: 'Label',
placeholder: 'Placeholder',
},
args: {
label: 'Label',
placeholder: 'Placeholder',
},
}
export const Password: Story = {
args: {
label: 'Label',
placeholder: 'Password',
type: 'password',
},
args: {
label: 'Label',
placeholder: 'Password',
type: 'password',
},
}
export const Error: Story = {
args: {
label: 'Input with error',
value: 'Wrong value',
errorMessage: 'Error message',
},
args: {
errorMessage: 'Error message',
label: 'Input with error',
value: 'Wrong value',
},
}

View File

@@ -7,91 +7,91 @@ import { clsx } from 'clsx'
import s from './text-field.module.scss'
export type TextFieldProps = {
containerProps?: ComponentProps<'div'>
errorMessage?: string
label?: string
labelProps?: ComponentProps<'label'>
onValueChange?: (value: string) => void
search?: boolean
containerProps?: ComponentProps<'div'>
errorMessage?: string
label?: string
labelProps?: ComponentProps<'label'>
onValueChange?: (value: string) => void
search?: boolean
} & ComponentPropsWithoutRef<'input'>
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
(
{
className,
containerProps,
errorMessage,
label,
labelProps,
onChange,
onValueChange,
placeholder,
search,
type,
...restProps
},
ref
) => {
const [showPassword, setShowPassword] = useState(false)
(
{
className,
containerProps,
errorMessage,
label,
labelProps,
onChange,
onValueChange,
placeholder,
search,
type,
...restProps
},
ref
) => {
const [showPassword, setShowPassword] = useState(false)
const isShowPasswordButtonShown = type === 'password'
const isShowPasswordButtonShown = type === 'password'
const finalType = getFinalType(type, showPassword)
const finalType = getFinalType(type, showPassword)
function handleChange(e: ChangeEvent<HTMLInputElement>) {
onChange?.(e)
onValueChange?.(e.target.value)
function handleChange(e: ChangeEvent<HTMLInputElement>) {
onChange?.(e)
onValueChange?.(e.target.value)
}
const classNames = {
error: clsx(s.error),
field: clsx(s.field, !!errorMessage && s.error, search && s.hasLeadingIcon, className),
fieldContainer: clsx(s.fieldContainer),
label: clsx(s.label, labelProps?.className),
leadingIcon: s.leadingIcon,
root: clsx(s.root, containerProps?.className),
}
return (
<div className={classNames.root}>
{label && (
<Typography as={'label'} className={classNames.label} variant={'body2'}>
{label}
</Typography>
)}
<div className={classNames.fieldContainer}>
{search && <Search className={classNames.leadingIcon} />}
<input
className={classNames.field}
onChange={handleChange}
placeholder={placeholder}
ref={ref}
type={finalType}
{...restProps}
/>
{isShowPasswordButtonShown && (
<button
className={s.showPassword}
onClick={() => setShowPassword(prev => !prev)}
type={'button'}
>
{showPassword ? <VisibilityOff /> : <Eye />}
</button>
)}
</div>
<Typography className={classNames.error} variant={'error'}>
{errorMessage}
</Typography>
</div>
)
}
const classNames = {
error: clsx(s.error),
field: clsx(s.field, !!errorMessage && s.error, search && s.hasLeadingIcon, className),
fieldContainer: clsx(s.fieldContainer),
label: clsx(s.label, labelProps?.className),
leadingIcon: s.leadingIcon,
root: clsx(s.root, containerProps?.className),
}
return (
<div className={classNames.root}>
{label && (
<Typography as={'label'} className={classNames.label} variant={'body2'}>
{label}
</Typography>
)}
<div className={classNames.fieldContainer}>
{search && <Search className={classNames.leadingIcon} />}
<input
className={classNames.field}
onChange={handleChange}
placeholder={placeholder}
ref={ref}
type={finalType}
{...restProps}
/>
{isShowPasswordButtonShown && (
<button
className={s.showPassword}
onClick={() => setShowPassword(prev => !prev)}
type={'button'}
>
{showPassword ? <VisibilityOff /> : <Eye />}
</button>
)}
</div>
<Typography className={classNames.error} variant={'error'}>
{errorMessage}
</Typography>
</div>
)
}
)
function getFinalType(type: ComponentProps<'input'>['type'], showPassword: boolean) {
if (type === 'password' && showPassword) {
return 'text'
}
if (type === 'password' && showPassword) {
return 'text'
}
return type
return type
}

View File

@@ -3,121 +3,121 @@ import type { Meta, StoryObj } from '@storybook/react'
import { Typography } from './'
const meta = {
argTypes: {
variant: {
control: { type: 'radio' },
options: [
'large',
'h1',
'h2',
'h3',
'body1',
'body2',
'subtitle1',
'subtitle2',
'caption',
'overline',
'link1',
'link2',
'error',
],
argTypes: {
variant: {
control: { type: 'radio' },
options: [
'large',
'h1',
'h2',
'h3',
'body1',
'body2',
'subtitle1',
'subtitle2',
'caption',
'overline',
'link1',
'link2',
'error',
],
},
},
},
component: Typography,
tags: ['autodocs'],
title: 'Components/Typography',
component: Typography,
tags: ['autodocs'],
title: 'Components/Typography',
} satisfies Meta<typeof Typography>
export default meta
type Story = StoryObj<typeof meta>
export const Large: Story = {
args: {
children: 'Card content',
variant: 'large',
},
args: {
children: 'Card content',
variant: 'large',
},
}
export const H1: Story = {
args: {
children: 'Card content',
variant: 'h1',
},
args: {
children: 'Card content',
variant: 'h1',
},
}
export const H2: Story = {
args: {
children: 'Card content',
variant: 'h2',
},
args: {
children: 'Card content',
variant: 'h2',
},
}
export const H3: Story = {
args: {
children: 'Card content',
variant: 'h3',
},
args: {
children: 'Card content',
variant: 'h3',
},
}
export const Body1: Story = {
args: {
children: 'Card content',
variant: 'body1',
},
args: {
children: 'Card content',
variant: 'body1',
},
}
export const Body2: Story = {
args: {
children: 'Card content',
variant: 'body2',
},
args: {
children: 'Card content',
variant: 'body2',
},
}
export const Subtitle1: Story = {
args: {
children: 'Card content',
variant: 'subtitle1',
},
args: {
children: 'Card content',
variant: 'subtitle1',
},
}
export const Subtitle2: Story = {
args: {
children: 'Card content',
variant: 'subtitle2',
},
args: {
children: 'Card content',
variant: 'subtitle2',
},
}
export const Caption: Story = {
args: {
children: 'Card content',
variant: 'caption',
},
args: {
children: 'Card content',
variant: 'caption',
},
}
export const Overline: Story = {
args: {
children: 'Card content',
variant: 'overline',
},
args: {
children: 'Card content',
variant: 'overline',
},
}
export const Link1: Story = {
args: {
children: 'Card content',
variant: 'link1',
},
args: {
children: 'Card content',
variant: 'link1',
},
}
export const Link2: Story = {
args: {
children: 'Card content',
variant: 'link2',
},
args: {
children: 'Card content',
variant: 'link2',
},
}
export const Error: Story = {
args: {
children: 'Card content',
variant: 'error',
},
args: {
children: 'Card content',
variant: 'error',
},
}

View File

@@ -5,33 +5,33 @@ import { clsx } from 'clsx'
import s from './typography.module.scss'
export interface TextProps<T extends ElementType> {
as?: T
children?: ReactNode
className?: string
variant?:
| 'body1'
| 'body2'
| 'caption'
| 'error'
| 'h1'
| 'h2'
| 'h3'
| 'large'
| 'link1'
| 'link2'
| 'overline'
| 'subtitle1'
| 'subtitle2'
as?: T
children?: ReactNode
className?: string
variant?:
| 'body1'
| 'body2'
| 'caption'
| 'error'
| 'h1'
| 'h2'
| 'h3'
| 'large'
| 'link1'
| 'link2'
| 'overline'
| 'subtitle1'
| 'subtitle2'
}
export function Typography<T extends ElementType = 'p'>({
as,
className,
variant = 'body1',
...restProps
as,
className,
variant = 'body1',
...restProps
}: TextProps<T> & Omit<ComponentPropsWithoutRef<T>, keyof TextProps<T>>) {
const classNames = clsx(s.text, s[variant], className)
const Component = as || 'p'
const classNames = clsx(s.text, s[variant], className)
const Component = as || 'p'
return <Component className={classNames} {...restProps} />
return <Component className={classNames} {...restProps} />
}