mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-18 05:09:23 +00:00
lint everything
This commit is contained in:
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
})
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
})
|
||||
|
||||
@@ -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}>…</span>
|
||||
return <span className={classNames.dots}>…</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />,
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user