mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-18 05:09:23 +00:00
homework 2 done
This commit is contained in:
28
src/components/auth/check-email/check-email.module.scss
Normal file
28
src/components/auth/check-email/check-email.module.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
.card {
|
||||
max-width: 413px;
|
||||
padding: 35px 33px 48px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 27px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.iconContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-bottom: 19px;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
margin-bottom: 65px;
|
||||
color: var(--color-light-900);
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.signInLink {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
18
src/components/auth/check-email/check-email.stories.tsx
Normal file
18
src/components/auth/check-email/check-email.stories.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { CheckEmail } from './'
|
||||
|
||||
const meta = {
|
||||
title: 'Auth/Check email',
|
||||
component: CheckEmail,
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof CheckEmail>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
email: 'your_email@domain.com',
|
||||
},
|
||||
}
|
||||
31
src/components/auth/check-email/check-email.tsx
Normal file
31
src/components/auth/check-email/check-email.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { Email } from '../../../assets/icons'
|
||||
import { Button, Card, Typography } from '../../ui'
|
||||
|
||||
import s from './check-email.module.scss'
|
||||
|
||||
type Props = {
|
||||
email: string
|
||||
}
|
||||
|
||||
export const CheckEmail = ({ email }: Props) => {
|
||||
const message = `We've sent an e-mail with instructions to ${email}`
|
||||
|
||||
return (
|
||||
<Card className={s.card}>
|
||||
<Typography variant="large" className={s.title}>
|
||||
Check your email
|
||||
</Typography>
|
||||
<div className={s.iconContainer}>
|
||||
<Email />
|
||||
</div>
|
||||
<Typography variant="body2" className={s.instructions}>
|
||||
{message}
|
||||
</Typography>
|
||||
<Button fullWidth as={Link} to={'/sing-in'}>
|
||||
Back to Sign in
|
||||
</Button>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
1
src/components/auth/check-email/index.ts
Normal file
1
src/components/auth/check-email/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './check-email'
|
||||
@@ -1 +1,5 @@
|
||||
export * from './login-form'
|
||||
export * from './check-email'
|
||||
export * from './new-password'
|
||||
export * from './recover-password'
|
||||
export * from './sign-up'
|
||||
export * from './sign-in'
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './login-form'
|
||||
@@ -1,9 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(3),
|
||||
rememberMe: z.boolean().default(false),
|
||||
})
|
||||
|
||||
export type LoginFormData = z.infer<typeof loginSchema>
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { LoginForm } from './login-form'
|
||||
import { LoginFormData } from './login-form.schema'
|
||||
|
||||
const meta = {
|
||||
title: 'Auth/LoginForm',
|
||||
component: LoginForm,
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof LoginForm>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
onSubmit: (data: LoginFormData) => console.log(data),
|
||||
},
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { DevTool } from '@hookform/devtools'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import { LoginFormData, loginSchema } from './login-form.schema'
|
||||
|
||||
import { Button, TextField, ControlledCheckbox } from '@/components'
|
||||
|
||||
type Props = {
|
||||
onSubmit: (data: LoginFormData) => void
|
||||
}
|
||||
|
||||
export const LoginForm = ({ onSubmit }: Props) => {
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<LoginFormData>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
})
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<DevTool control={control} />
|
||||
<TextField {...register('email')} label={'email'} errorMessage={errors.email?.message} />
|
||||
<TextField
|
||||
{...register('password')}
|
||||
label={'password'}
|
||||
errorMessage={errors.password?.message}
|
||||
/>
|
||||
<ControlledCheckbox label={'remember me'} control={control} name={'rememberMe'} />
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
1
src/components/auth/new-password/index.ts
Normal file
1
src/components/auth/new-password/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './new-password'
|
||||
38
src/components/auth/new-password/new-password.module.scss
Normal file
38
src/components/auth/new-password/new-password.module.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
.card {
|
||||
max-width: 413px;
|
||||
padding: 33px 36px 60px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
width: 100%;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 51px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin-bottom: 19px;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
margin-bottom: 41px;
|
||||
color: var(--color-light-900);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.caption {
|
||||
margin-bottom: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.signInLink {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
18
src/components/auth/new-password/new-password.stories.tsx
Normal file
18
src/components/auth/new-password/new-password.stories.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { NewPassword } from './'
|
||||
|
||||
const meta = {
|
||||
title: 'Auth/New password',
|
||||
component: NewPassword,
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof NewPassword>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
onSubmit: data => console.info(data),
|
||||
},
|
||||
}
|
||||
55
src/components/auth/new-password/new-password.tsx
Normal file
55
src/components/auth/new-password/new-password.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { DevTool } from '@hookform/devtools'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Button, Card, ControlledTextField, Typography } from '../../ui'
|
||||
|
||||
import s from './new-password.module.scss'
|
||||
|
||||
const schema = z.object({
|
||||
password: z.string().nonempty('Enter password'),
|
||||
})
|
||||
|
||||
type FormType = z.infer<typeof schema>
|
||||
|
||||
type Props = {
|
||||
onSubmit: (data: FormType) => void
|
||||
}
|
||||
|
||||
export const NewPassword = (props: Props) => {
|
||||
const { control, handleSubmit } = useForm<FormType>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
password: '',
|
||||
},
|
||||
})
|
||||
|
||||
const handleFormSubmitted = handleSubmit(props.onSubmit)
|
||||
|
||||
return (
|
||||
<>
|
||||
<DevTool control={control} />
|
||||
<Card className={s.card}>
|
||||
<Typography variant="large" className={s.title}>
|
||||
Create new password
|
||||
</Typography>
|
||||
<form onSubmit={handleFormSubmitted}>
|
||||
<ControlledTextField
|
||||
placeholder={'Password'}
|
||||
name={'password'}
|
||||
control={control}
|
||||
type={'password'}
|
||||
containerProps={{ className: s.input }}
|
||||
/>
|
||||
<Typography variant="caption" className={s.instructions}>
|
||||
Create new password and we will send you further instructions to email
|
||||
</Typography>
|
||||
<Button fullWidth type={'submit'}>
|
||||
Create new password
|
||||
</Button>
|
||||
</form>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
1
src/components/auth/recover-password/index.ts
Normal file
1
src/components/auth/recover-password/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './recover-password'
|
||||
@@ -0,0 +1,39 @@
|
||||
.card {
|
||||
max-width: 413px;
|
||||
padding: 33px 36px 29px;
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 100%;
|
||||
margin-bottom: 19px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-bottom: 31px;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
margin-bottom: 65px;
|
||||
color: var(--color-light-900);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.caption {
|
||||
margin-bottom: 11px;
|
||||
color: var(--color-light-900);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loginLink {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
font-size: var(--font-size-m);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-accent-500);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { RecoverPassword } from './'
|
||||
|
||||
const meta = {
|
||||
title: 'Auth/Recover password',
|
||||
component: RecoverPassword,
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof RecoverPassword>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
onSubmit: data => console.info(data),
|
||||
},
|
||||
}
|
||||
59
src/components/auth/recover-password/recover-password.tsx
Normal file
59
src/components/auth/recover-password/recover-password.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { DevTool } from '@hookform/devtools'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Button, Card, ControlledTextField, Typography } from '../../ui'
|
||||
|
||||
import s from './recover-password.module.scss'
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email('Invalid email address').nonempty('Enter email'),
|
||||
})
|
||||
|
||||
type FormType = z.infer<typeof schema>
|
||||
|
||||
type Props = {
|
||||
onSubmit: (data: FormType) => void
|
||||
}
|
||||
|
||||
export const RecoverPassword = (props: Props) => {
|
||||
const { control, handleSubmit } = useForm<FormType>({
|
||||
mode: 'onSubmit',
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
},
|
||||
})
|
||||
|
||||
const handleFormSubmitted = handleSubmit(props.onSubmit)
|
||||
|
||||
return (
|
||||
<>
|
||||
<DevTool control={control} />
|
||||
<Card className={s.card}>
|
||||
<Typography variant="large" className={s.title}>
|
||||
Forgot your password?
|
||||
</Typography>
|
||||
<form onSubmit={handleFormSubmitted}>
|
||||
<div className={s.form}>
|
||||
<ControlledTextField placeholder={'Email'} name={'email'} control={control} />
|
||||
</div>
|
||||
<Typography variant="body2" className={s.instructions}>
|
||||
Enter your email address and we will send you further instructions
|
||||
</Typography>
|
||||
<Button className={s.button} fullWidth type={'submit'}>
|
||||
Send Instructions
|
||||
</Button>
|
||||
</form>
|
||||
<Typography variant="body2" className={s.caption}>
|
||||
Did you remember your password?
|
||||
</Typography>
|
||||
<Typography variant="link1" as={Link} to="/sign-in" className={s.loginLink}>
|
||||
Try logging in
|
||||
</Typography>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
1
src/components/auth/sign-in/index.ts
Normal file
1
src/components/auth/sign-in/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sign-in'
|
||||
55
src/components/auth/sign-in/sign-in.module.scss
Normal file
55
src/components/auth/sign-in/sign-in.module.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 413px;
|
||||
padding: 33px 36px 29px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 27px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.recoverPasswordLink,
|
||||
.recoverPasswordLink:visited {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
width: 100%;
|
||||
margin-bottom: 66px;
|
||||
|
||||
color: inherit;
|
||||
text-align: right;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.caption {
|
||||
margin-bottom: 11px;
|
||||
color: var(--color-light-900);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.signUpLink {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
font-size: var(--font-size-m);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-accent-500);
|
||||
}
|
||||
18
src/components/auth/sign-in/sign-in.stories.tsx
Normal file
18
src/components/auth/sign-in/sign-in.stories.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { SignIn } from './'
|
||||
|
||||
const meta = {
|
||||
title: 'Auth/Sign in',
|
||||
component: SignIn,
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof SignIn>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
onSubmit: data => console.info(data),
|
||||
},
|
||||
}
|
||||
87
src/components/auth/sign-in/sign-in.tsx
Normal file
87
src/components/auth/sign-in/sign-in.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { DevTool } from '@hookform/devtools'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Button, Card, ControlledCheckbox, ControlledTextField, Typography } from '../../ui'
|
||||
|
||||
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(),
|
||||
})
|
||||
|
||||
type FormType = z.infer<typeof schema>
|
||||
|
||||
type Props = {
|
||||
onSubmit: (data: FormType) => void
|
||||
}
|
||||
|
||||
export const SignIn = (props: Props) => {
|
||||
const { control, handleSubmit } = useForm<FormType>({
|
||||
mode: 'onSubmit',
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
rememberMe: false,
|
||||
},
|
||||
})
|
||||
|
||||
const handleFormSubmitted = handleSubmit(props.onSubmit)
|
||||
|
||||
return (
|
||||
<>
|
||||
<DevTool control={control} />
|
||||
<Card className={s.card}>
|
||||
<Typography variant="large" className={s.title}>
|
||||
Sign In
|
||||
</Typography>
|
||||
<form onSubmit={handleFormSubmitted}>
|
||||
<div className={s.form}>
|
||||
<ControlledTextField
|
||||
placeholder={'Email'}
|
||||
label={'Email'}
|
||||
name={'email'}
|
||||
control={control}
|
||||
/>
|
||||
<ControlledTextField
|
||||
placeholder={'Password'}
|
||||
label={'Password'}
|
||||
type={'password'}
|
||||
name={'password'}
|
||||
control={control}
|
||||
/>
|
||||
</div>
|
||||
<ControlledCheckbox
|
||||
className={s.checkbox}
|
||||
label={'Remember me'}
|
||||
control={control}
|
||||
name={'rememberMe'}
|
||||
position={'left'}
|
||||
/>
|
||||
<Typography
|
||||
variant="body2"
|
||||
as={Link}
|
||||
to="/recover-password"
|
||||
className={s.recoverPasswordLink}
|
||||
>
|
||||
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 variant="link1" as={Link} to="/sign-up" className={s.signUpLink}>
|
||||
Sign Up
|
||||
</Typography>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
1
src/components/auth/sign-up/index.ts
Normal file
1
src/components/auth/sign-up/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sign-up'
|
||||
38
src/components/auth/sign-up/sign-up.module.scss
Normal file
38
src/components/auth/sign-up/sign-up.module.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 413px;
|
||||
padding: 33px 36px 29px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
width: 100%;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 27px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.caption {
|
||||
margin-bottom: 11px;
|
||||
color: var(--color-light-900);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.signInLink {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
font-size: var(--font-size-m);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-accent-500);
|
||||
}
|
||||
18
src/components/auth/sign-up/sign-up.stories.tsx
Normal file
18
src/components/auth/sign-up/sign-up.stories.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { SignUp } from './'
|
||||
|
||||
const meta = {
|
||||
title: 'Auth/Sign up',
|
||||
component: SignUp,
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof SignUp>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
onSubmit: data => console.info(data),
|
||||
},
|
||||
}
|
||||
95
src/components/auth/sign-up/sign-up.tsx
Normal file
95
src/components/auth/sign-up/sign-up.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { DevTool } from '@hookform/devtools'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { omit } from 'remeda'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Button, Card, ControlledTextField, Typography } from '../../ui'
|
||||
|
||||
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({
|
||||
message: 'Passwords do not match',
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['passwordConfirmation'],
|
||||
})
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
type FormType = z.infer<typeof schema>
|
||||
|
||||
type Props = {
|
||||
onSubmit: (data: Omit<FormType, 'passwordConfirmation'>) => void
|
||||
}
|
||||
|
||||
export const SignUp = (props: Props) => {
|
||||
const { control, handleSubmit } = useForm<FormType>({
|
||||
mode: 'onSubmit',
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirmation: '',
|
||||
},
|
||||
})
|
||||
|
||||
const handleFormSubmitted = handleSubmit(data =>
|
||||
props.onSubmit(omit(data, ['passwordConfirmation']))
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<DevTool control={control} />
|
||||
<Card className={s.card}>
|
||||
<Typography variant="large" className={s.title}>
|
||||
Sign Up
|
||||
</Typography>
|
||||
<form onSubmit={handleFormSubmitted}>
|
||||
<div className={s.form}>
|
||||
<ControlledTextField
|
||||
label={'Email'}
|
||||
placeholder={'Email'}
|
||||
name={'email'}
|
||||
control={control}
|
||||
/>
|
||||
<ControlledTextField
|
||||
placeholder={'Password'}
|
||||
label={'Password'}
|
||||
type={'password'}
|
||||
name={'password'}
|
||||
control={control}
|
||||
/>
|
||||
<ControlledTextField
|
||||
placeholder={'Confirm password'}
|
||||
label={'Confirm password'}
|
||||
type={'password'}
|
||||
name={'passwordConfirmation'}
|
||||
control={control}
|
||||
/>
|
||||
</div>
|
||||
<Button className={s.button} fullWidth type={'submit'}>
|
||||
Sign Up
|
||||
</Button>
|
||||
</form>
|
||||
{/* eslint-disable-next-line react/no-unescaped-entities */}
|
||||
<Typography variant="body2" className={s.caption}>
|
||||
Already have an account?
|
||||
</Typography>
|
||||
<Typography variant="link1" as={Link} to="/sign-in" className={s.signInLink}>
|
||||
Sign In
|
||||
</Typography>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './ui'
|
||||
export * from './auth'
|
||||
export * from './profile'
|
||||
|
||||
1
src/components/profile/index.ts
Normal file
1
src/components/profile/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './personal-information'
|
||||
1
src/components/profile/personal-information/index.ts
Normal file
1
src/components/profile/personal-information/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './personal-information'
|
||||
@@ -0,0 +1,95 @@
|
||||
.card {
|
||||
max-width: 413px;
|
||||
padding: 33px 33px 41px;
|
||||
}
|
||||
|
||||
.photoContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-bottom: 6px;
|
||||
|
||||
> div {
|
||||
position: relative;
|
||||
|
||||
> img {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
> .editAvatarButton {
|
||||
all: unset;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(--outline-focus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 27px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nameWithEditButton {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.editNameButton {
|
||||
all: unset;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(--outline-focus);
|
||||
}
|
||||
}
|
||||
|
||||
.email {
|
||||
margin-bottom: 13px;
|
||||
color: var(--color-dark-100);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.caption {
|
||||
margin-bottom: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.signInLink {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { PersonalInformation } from './'
|
||||
|
||||
const meta = {
|
||||
title: 'Profile/Personal information',
|
||||
component: PersonalInformation,
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof PersonalInformation>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
email: 'your_email@domain.com',
|
||||
avatar: 'https://picsum.photos/200',
|
||||
name: 'John Doe',
|
||||
onAvatarChange: () => {
|
||||
console.info('avatar changed')
|
||||
},
|
||||
onNameChange: () => {
|
||||
console.info('name changed')
|
||||
},
|
||||
onLogout: () => {
|
||||
console.info('logout')
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Camera, Edit, Logout } from '../../../assets/icons'
|
||||
import { Button, Card, Typography } from '../../ui'
|
||||
|
||||
import s from './personal-information.module.scss'
|
||||
|
||||
type Props = {
|
||||
email: string
|
||||
avatar: string
|
||||
name: string
|
||||
onLogout: () => void
|
||||
onAvatarChange: (newAvatar: string) => void
|
||||
onNameChange: (newName: string) => void
|
||||
}
|
||||
export const PersonalInformation = ({
|
||||
avatar,
|
||||
email,
|
||||
name,
|
||||
onAvatarChange,
|
||||
onNameChange,
|
||||
onLogout,
|
||||
}: Props) => {
|
||||
const handleAvatarChanged = () => {
|
||||
onAvatarChange('new Avatar')
|
||||
}
|
||||
const handleNameChanged = () => {
|
||||
onNameChange('New name')
|
||||
}
|
||||
const handleLogout = () => {
|
||||
onLogout()
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={s.card}>
|
||||
<Typography variant="large" className={s.title}>
|
||||
Personal Information
|
||||
</Typography>
|
||||
<div className={s.photoContainer}>
|
||||
<div>
|
||||
<img src={avatar} alt={'avatar'} />
|
||||
<button className={s.editAvatarButton} onClick={handleAvatarChanged}>
|
||||
<Camera />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.nameWithEditButton}>
|
||||
<Typography variant="h1" className={s.name}>
|
||||
{name}
|
||||
</Typography>
|
||||
<button className={s.editNameButton} onClick={handleNameChanged}>
|
||||
<Edit />
|
||||
</button>
|
||||
</div>
|
||||
<Typography variant="body2" className={s.email}>
|
||||
{/* eslint-disable-next-line react/no-unescaped-entities */}
|
||||
{email}
|
||||
</Typography>
|
||||
<div className={s.buttonContainer}>
|
||||
<Button variant={'secondary'} onClick={handleLogout}>
|
||||
<Logout />
|
||||
Sign Out
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Control, FieldPath, FieldValues, useController } from 'react-hook-form'
|
||||
|
||||
import { RadioGroup, RadioGroupProps } from '@/components/ui'
|
||||
|
||||
export type ControlledRadioGroupProps<TFieldValues extends FieldValues> = {
|
||||
name: FieldPath<TFieldValues>
|
||||
control: Control<TFieldValues>
|
||||
} & Omit<RadioGroupProps, 'onChange' | 'value' | 'id'>
|
||||
|
||||
export const ControlledRadioGroup = <TFieldValues extends FieldValues>(
|
||||
props: ControlledRadioGroupProps<TFieldValues>
|
||||
) => {
|
||||
const {
|
||||
field: { onChange, ...field },
|
||||
fieldState: { error },
|
||||
} = useController({
|
||||
name: props.name,
|
||||
control: props.control,
|
||||
})
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
{...props}
|
||||
{...field}
|
||||
onValueChange={onChange}
|
||||
errorMessage={error?.message}
|
||||
id={props.name}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './controlled-radio-group'
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Control, FieldPath, FieldValues, useController } from 'react-hook-form'
|
||||
|
||||
import { TextField, TextFieldProps } from '@/components'
|
||||
|
||||
export type ControlledTextFieldProps<TFieldValues extends FieldValues> = {
|
||||
name: FieldPath<TFieldValues>
|
||||
control: Control<TFieldValues>
|
||||
} & Omit<TextFieldProps, 'onChange' | 'value' | 'id'>
|
||||
|
||||
export const ControlledTextField = <TFieldValues extends FieldValues>(
|
||||
props: ControlledTextFieldProps<TFieldValues>
|
||||
) => {
|
||||
const {
|
||||
field,
|
||||
fieldState: { error },
|
||||
} = useController({
|
||||
name: props.name,
|
||||
control: props.control,
|
||||
})
|
||||
|
||||
return <TextField {...props} {...field} errorMessage={error?.message} id={props.name} />
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './controlled-text-field'
|
||||
@@ -1 +1,3 @@
|
||||
export * from './controlled-checkbox'
|
||||
export * from './controlled-text-field'
|
||||
export * from './controlled-radio-group'
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from './typography'
|
||||
export * from './checkbox'
|
||||
export * from './text-field'
|
||||
export * from './controlled'
|
||||
export * from './radio-group'
|
||||
|
||||
9
src/components/ui/label/label.module.scss
Normal file
9
src/components/ui/label/label.module.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.label {
|
||||
display: inline-block;
|
||||
|
||||
margin-bottom: 1px;
|
||||
|
||||
font-size: var(--font-size-s);
|
||||
line-height: var(--line-height-m);
|
||||
color: var(var(--color-light-100));
|
||||
}
|
||||
23
src/components/ui/label/label.tsx
Normal file
23
src/components/ui/label/label.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentPropsWithoutRef, FC, ReactNode } from 'react'
|
||||
|
||||
import * as LabelRadixUI from '@radix-ui/react-label'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import s from './label.module.scss'
|
||||
|
||||
export type LabelProps = {
|
||||
label?: ReactNode
|
||||
} & ComponentPropsWithoutRef<'label'>
|
||||
|
||||
export const Label: FC<LabelProps> = ({ label, children, className, ...rest }) => {
|
||||
const classNames = {
|
||||
label: clsx(s.label, className),
|
||||
}
|
||||
|
||||
return (
|
||||
<LabelRadixUI.Root {...rest}>
|
||||
{label && <div className={classNames.label}>{label}</div>}
|
||||
{children}
|
||||
</LabelRadixUI.Root>
|
||||
)
|
||||
}
|
||||
1
src/components/ui/radio-group/index.ts
Normal file
1
src/components/ui/radio-group/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './radio-group'
|
||||
83
src/components/ui/radio-group/radio-group.module.scss
Normal file
83
src/components/ui/radio-group/radio-group.module.scss
Normal file
@@ -0,0 +1,83 @@
|
||||
.root {
|
||||
cursor: pointer;
|
||||
|
||||
&[data-disabled] {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.option {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
|
||||
> label {
|
||||
cursor: pointer;
|
||||
|
||||
[data-disabled] & {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
transition: var(--transtition-duration-basic) background-color;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
||||
display: block;
|
||||
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
border: 2px solid var(--color-accent-500);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
transition: 100ms background-color;
|
||||
|
||||
[data-state='checked'] & {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--color-accent-500);
|
||||
}
|
||||
}
|
||||
|
||||
.option:focus-visible & {
|
||||
background-color: var(--color-dark-700);
|
||||
}
|
||||
|
||||
.option:hover & {
|
||||
background-color: var(--color-dark-500);
|
||||
}
|
||||
|
||||
.option:active & {
|
||||
background-color: var(--color-accent-900);
|
||||
}
|
||||
|
||||
.root[data-disabled] & {
|
||||
background-color: var(--color-dark-900);
|
||||
}
|
||||
}
|
||||
31
src/components/ui/radio-group/radio-group.stories.tsx
Normal file
31
src/components/ui/radio-group/radio-group.stories.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { RadioGroup } from './'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Radio Group',
|
||||
component: RadioGroup,
|
||||
tags: ['autodocs'],
|
||||
} 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' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
options: [
|
||||
{ label: 'Option One', value: 'option-one' },
|
||||
{ label: 'Option Two', value: 'option-two' },
|
||||
],
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
63
src/components/ui/radio-group/radio-group.tsx
Normal file
63
src/components/ui/radio-group/radio-group.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import s from './radio-group.module.scss'
|
||||
|
||||
import { Typography } from '@/components'
|
||||
|
||||
const RadioGroupRoot = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, 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>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item ref={ref} className={clsx(s.option, className)} {...props}>
|
||||
<div className={s.icon}></div>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
})
|
||||
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
||||
|
||||
type Option = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
export type RadioGroupProps = Omit<
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>,
|
||||
'children'
|
||||
> & {
|
||||
options: Option[]
|
||||
errorMessage?: string
|
||||
}
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
RadioGroupProps
|
||||
>((props, ref) => {
|
||||
const { options, errorMessage, ...restProps } = props
|
||||
|
||||
return (
|
||||
<RadioGroupRoot {...restProps} ref={ref}>
|
||||
{options.map(option => (
|
||||
<div className={s.label} key={option.value}>
|
||||
<RadioGroupItem value={option.value} id={option.value} />
|
||||
<Typography variant={'body2'} as={'label'} htmlFor={option.value}>
|
||||
{option.label}
|
||||
</Typography>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroupRoot>
|
||||
)
|
||||
})
|
||||
|
||||
export { RadioGroupRoot, RadioGroupItem, RadioGroup }
|
||||
1
src/components/ui/table/index.tsx
Normal file
1
src/components/ui/table/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './table'
|
||||
50
src/components/ui/table/table.module.scss
Normal file
50
src/components/ui/table/table.module.scss
Normal file
@@ -0,0 +1,50 @@
|
||||
.table {
|
||||
border-collapse: collapse;
|
||||
color: var(--color-light-100);
|
||||
border: 1px solid var(--color-dark-500);
|
||||
}
|
||||
|
||||
.headCell {
|
||||
padding: 6px 24px;
|
||||
|
||||
font-size: var(--font-size-s);
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--line-height-m);
|
||||
|
||||
background-color: var(--color-dark-500);
|
||||
|
||||
> span {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.sortable > span {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.tableCell {
|
||||
padding: 6px 24px;
|
||||
|
||||
font-size: var(--font-size-s);
|
||||
line-height: var(--line-height-m);
|
||||
|
||||
background-color: var(--color-bg-table);
|
||||
border-bottom: 1px solid var(--color-dark-500);
|
||||
}
|
||||
|
||||
.chevronDown {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
128
src/components/ui/table/table.stories.tsx
Normal file
128
src/components/ui/table/table.stories.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Meta } from '@storybook/react'
|
||||
|
||||
import { Table, TableBody, TableCell, TableEmpty, TableHead, TableHeadCell, TableRow } from './'
|
||||
|
||||
import { Typography } from '@/components'
|
||||
|
||||
export default {
|
||||
title: 'Components/Table',
|
||||
component: 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'}
|
||||
variant={'link1'}
|
||||
href="https://it-incubator.io/"
|
||||
target="_blank"
|
||||
>
|
||||
Какая-то ссылка кудато на какой-то источник с информациейо ссылка кудато на какой-то
|
||||
источник
|
||||
</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 = [
|
||||
{
|
||||
id: '01',
|
||||
title: 'Web Basic',
|
||||
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor',
|
||||
link: 'Какая-то ссылка кудато на какой-то источник с информациейо ссылка кудато на какой-то',
|
||||
category: 'Основной',
|
||||
type: 'Читать',
|
||||
},
|
||||
{
|
||||
id: '02',
|
||||
title: 'Web Basic',
|
||||
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor',
|
||||
link: 'Какая-то ссылка куда-то',
|
||||
category: 'Основной',
|
||||
type: 'Читать',
|
||||
},
|
||||
{
|
||||
id: '03',
|
||||
title: 'Web Basic',
|
||||
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor',
|
||||
link: 'Какая-то ссылка кудато на какой-то источник с информациейо ссылка кудато на какой-то. Какая-то ссылка кудато на какой-то источник с информациейо ссылка куда-то на какой-то',
|
||||
category: 'Основной',
|
||||
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>
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const Empty = {
|
||||
render: () => <TableEmpty />,
|
||||
}
|
||||
77
src/components/ui/table/table.tsx
Normal file
77
src/components/ui/table/table.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { ComponentProps, ComponentPropsWithoutRef, ElementRef, FC, forwardRef } from 'react'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import s from './table.module.scss'
|
||||
|
||||
import { Typography } from '@/components'
|
||||
|
||||
export const Table = forwardRef<HTMLTableElement, ComponentPropsWithoutRef<'table'>>(
|
||||
({ className, ...rest }, ref) => {
|
||||
const classNames = {
|
||||
table: clsx(className, s.table),
|
||||
}
|
||||
|
||||
return <table className={classNames.table} {...rest} ref={ref} />
|
||||
}
|
||||
)
|
||||
export const TableHead = forwardRef<ElementRef<'thead'>, ComponentPropsWithoutRef<'thead'>>(
|
||||
({ ...rest }, ref) => {
|
||||
return <thead {...rest} ref={ref} />
|
||||
}
|
||||
)
|
||||
|
||||
export const TableBody = forwardRef<ElementRef<'tbody'>, ComponentPropsWithoutRef<'tbody'>>(
|
||||
({ ...rest }, ref) => {
|
||||
return <tbody {...rest} ref={ref} />
|
||||
}
|
||||
)
|
||||
|
||||
export const TableRow = forwardRef<ElementRef<'tr'>, ComponentPropsWithoutRef<'tr'>>(
|
||||
({ ...rest }, ref) => {
|
||||
return <tr {...rest} ref={ref} />
|
||||
}
|
||||
)
|
||||
|
||||
export const TableHeadCell = forwardRef<ElementRef<'th'>, ComponentPropsWithoutRef<'th'>>(
|
||||
({ className, children, ...rest }, ref) => {
|
||||
const classNames = {
|
||||
headCell: clsx(className, s.headCell),
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
return <td className={classNames.cell} {...rest} ref={ref} />
|
||||
}
|
||||
)
|
||||
|
||||
export const TableEmpty: FC<ComponentProps<'div'> & { mt?: string; mb?: string }> = ({
|
||||
className,
|
||||
mt = '89px',
|
||||
mb,
|
||||
}) => {
|
||||
const classNames = {
|
||||
empty: clsx(className, s.empty),
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography
|
||||
variant={'h2'}
|
||||
className={classNames.empty}
|
||||
style={{ marginTop: mt, marginBottom: mb }}
|
||||
>
|
||||
Пока тут еще нет данных! :(
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user