homework 2 done

This commit is contained in:
2023-08-03 19:52:53 +02:00
parent c230948b57
commit 8d75b18f61
53 changed files with 1631 additions and 510 deletions

View 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;
}

View 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',
},
}

View 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>
)
}

View File

@@ -0,0 +1 @@
export * from './check-email'

View File

@@ -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'

View File

@@ -1 +0,0 @@
export * from './login-form'

View File

@@ -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>

View File

@@ -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),
},
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1 @@
export * from './new-password'

View 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;
}

View 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),
},
}

View 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>
</>
)
}

View File

@@ -0,0 +1 @@
export * from './recover-password'

View File

@@ -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);
}

View File

@@ -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),
},
}

View 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>
</>
)
}

View File

@@ -0,0 +1 @@
export * from './sign-in'

View 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);
}

View 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),
},
}

View 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>
</>
)
}

View File

@@ -0,0 +1 @@
export * from './sign-up'

View 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);
}

View 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),
},
}

View 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>
</>
)
}

View File

@@ -1,2 +1,3 @@
export * from './ui'
export * from './auth'
export * from './profile'

View File

@@ -0,0 +1 @@
export * from './personal-information'

View File

@@ -0,0 +1 @@
export * from './personal-information'

View File

@@ -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;
}

View File

@@ -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')
},
},
}

View File

@@ -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>
)
}

View File

@@ -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}
/>
)
}

View File

@@ -0,0 +1 @@
export * from './controlled-radio-group'

View File

@@ -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} />
}

View File

@@ -0,0 +1 @@
export * from './controlled-text-field'

View File

@@ -1 +1,3 @@
export * from './controlled-checkbox'
export * from './controlled-text-field'
export * from './controlled-radio-group'

View File

@@ -4,3 +4,4 @@ export * from './typography'
export * from './checkbox'
export * from './text-field'
export * from './controlled'
export * from './radio-group'

View 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));
}

View 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>
)
}

View File

@@ -0,0 +1 @@
export * from './radio-group'

View 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);
}
}

View 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,
},
}

View 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 }

View File

@@ -0,0 +1 @@
export * from './table'

View 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%;
}

View 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 />,
}

View 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>
)
}