mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-16 12:33:18 +00:00
add components
This commit is contained in:
@@ -18,6 +18,8 @@
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@storybook/theming": "^7.2.1",
|
||||
"clsx": "^2.0.0",
|
||||
@@ -51,6 +53,7 @@
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-myPlugin": "file:./eslint",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"eslint-plugin-storybook": "^0.6.13",
|
||||
@@ -58,7 +61,6 @@
|
||||
"storybook": "^7.2.1",
|
||||
"stylelint": "^15.10.2",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5",
|
||||
"eslint-plugin-myPlugin": "file:./eslint"
|
||||
"vite": "^4.4.5"
|
||||
}
|
||||
}
|
||||
|
||||
66
pnpm-lock.yaml
generated
66
pnpm-lock.yaml
generated
@@ -20,6 +20,12 @@ dependencies:
|
||||
'@radix-ui/react-radio-group':
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-slider':
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-tabs':
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@reduxjs/toolkit':
|
||||
specifier: ^1.9.5
|
||||
version: 1.9.5(react-redux@8.1.2)(react@18.2.0)
|
||||
@@ -2547,6 +2553,37 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-remove-scroll: 2.5.5(@types/react@18.2.15)(react@18.2.0)
|
||||
|
||||
/@radix-ui/react-slider@1.1.2(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-NKs15MJylfzVsCagVSWKhGGLNR1W9qWs+HtgbmjjVUB3B9+lb3PYoXxVju3kOrpf0VKyVCtZp+iTwVoqpa1Chw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.22.6
|
||||
'@radix-ui/number': 1.0.1
|
||||
'@radix-ui/primitive': 1.0.1
|
||||
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.15)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.0.1(@types/react@18.2.15)(react@18.2.0)
|
||||
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.15)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.15)(react@18.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.15)(react@18.2.0)
|
||||
'@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.15)(react@18.2.0)
|
||||
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.15)(react@18.2.0)
|
||||
'@types/react': 18.2.15
|
||||
'@types/react-dom': 18.2.7
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-slot@1.0.2(@types/react@18.2.15)(react@18.2.0):
|
||||
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
||||
peerDependencies:
|
||||
@@ -2561,6 +2598,34 @@ packages:
|
||||
'@types/react': 18.2.15
|
||||
react: 18.2.0
|
||||
|
||||
/@radix-ui/react-tabs@1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.22.6
|
||||
'@radix-ui/primitive': 1.0.1
|
||||
'@radix-ui/react-context': 1.0.1(@types/react@18.2.15)(react@18.2.0)
|
||||
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.15)(react@18.2.0)
|
||||
'@radix-ui/react-id': 1.0.1(@types/react@18.2.15)(react@18.2.0)
|
||||
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.15)(react@18.2.0)
|
||||
'@types/react': 18.2.15
|
||||
'@types/react-dom': 18.2.7
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.15)(react@18.2.0):
|
||||
resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
|
||||
peerDependencies:
|
||||
@@ -10217,5 +10282,4 @@ packages:
|
||||
file:eslint:
|
||||
resolution: {directory: eslint, type: directory}
|
||||
name: eslint
|
||||
version: 0.0.0
|
||||
dev: true
|
||||
|
||||
@@ -9,3 +9,4 @@ export { default as Logo } from './logo'
|
||||
export { default as PersonOutline } from './person-outline'
|
||||
export { default as ChevronUp } from './chevron-up'
|
||||
export { default as Close } from './close'
|
||||
export { default as Search } from './search'
|
||||
|
||||
26
src/assets/icons/search.tsx
Normal file
26
src/assets/icons/search.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { SVGProps, Ref, forwardRef, memo } from 'react'
|
||||
const SvgComponent = (props: SVGProps<SVGSVGElement>, ref: Ref<SVGSVGElement>) => (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_21547_180)">
|
||||
<path
|
||||
d="M17.2583 16.075L14.425 13.25C15.3392 12.0854 15.8352 10.6472 15.8333 9.16667C15.8333 7.84813 15.4423 6.5592 14.7098 5.46287C13.9773 4.36654 12.9361 3.51206 11.7179 3.00747C10.4997 2.50289 9.15927 2.37087 7.86607 2.6281C6.57286 2.88534 5.38497 3.52027 4.45262 4.45262C3.52027 5.38497 2.88534 6.57286 2.6281 7.86607C2.37087 9.15927 2.50289 10.4997 3.00747 11.7179C3.51206 12.9361 4.36654 13.9773 5.46287 14.7098C6.5592 15.4423 7.84813 15.8333 9.16667 15.8333C10.6472 15.8352 12.0854 15.3392 13.25 14.425L16.075 17.2583C16.1525 17.3364 16.2446 17.3984 16.3462 17.4407C16.4477 17.4831 16.5567 17.5048 16.6667 17.5048C16.7767 17.5048 16.8856 17.4831 16.9871 17.4407C17.0887 17.3984 17.1809 17.3364 17.2583 17.2583C17.3364 17.1809 17.3984 17.0887 17.4407 16.9871C17.4831 16.8856 17.5048 16.7767 17.5048 16.6667C17.5048 16.5567 17.4831 16.4477 17.4407 16.3462C17.3984 16.2446 17.3364 16.1525 17.2583 16.075ZM4.16667 9.16667C4.16667 8.17776 4.45991 7.21106 5.00932 6.38882C5.55873 5.56657 6.33962 4.92571 7.25325 4.54727C8.16688 4.16883 9.17222 4.06982 10.1421 4.26274C11.112 4.45567 12.0029 4.93187 12.7022 5.63114C13.4015 6.3304 13.8777 7.22131 14.0706 8.19122C14.2635 9.16112 14.1645 10.1665 13.7861 11.0801C13.4076 11.9937 12.7668 12.7746 11.9445 13.324C11.1223 13.8734 10.1556 14.1667 9.16667 14.1667C7.84059 14.1667 6.56882 13.6399 5.63114 12.7022C4.69345 11.7645 4.16667 10.4928 4.16667 9.16667Z"
|
||||
fill="#808080"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_21547_180">
|
||||
<rect width={20} height={20} fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default memo(forwardRef(SvgComponent))
|
||||
0
src/components/decks/decks-table.tsx
Normal file
0
src/components/decks/decks-table.tsx
Normal file
@@ -52,7 +52,6 @@
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
background-color: var(--color-dark-900);
|
||||
border-radius: 50%;
|
||||
|
||||
&.disabled {
|
||||
|
||||
@@ -3,5 +3,8 @@ export * from './card'
|
||||
export * from './typography'
|
||||
export * from './checkbox'
|
||||
export * from './text-field'
|
||||
export * from './table'
|
||||
export * from './controlled'
|
||||
export * from './radio-group'
|
||||
export * from './page'
|
||||
export * from './slider'
|
||||
|
||||
1
src/components/ui/page/index.ts
Normal file
1
src/components/ui/page/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './page'
|
||||
6
src/components/ui/page/page.module.scss
Normal file
6
src/components/ui/page/page.module.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
.root {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 36px;
|
||||
padding-inline: 24px;
|
||||
}
|
||||
15
src/components/ui/page/page.tsx
Normal file
15
src/components/ui/page/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ComponentPropsWithoutRef, forwardRef } from 'react'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
return <div {...props} ref={ref} className={classNames.root} />
|
||||
})
|
||||
1
src/components/ui/slider/index.ts
Normal file
1
src/components/ui/slider/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './slider'
|
||||
52
src/components/ui/slider/slider.module.scss
Normal file
52
src/components/ui/slider/slider.module.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.root {
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.track {
|
||||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
|
||||
opacity: 0.5;
|
||||
background-color: var(--color-accent-500);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.range {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
background-color: var(--color-accent-500);
|
||||
}
|
||||
|
||||
.thumb {
|
||||
touch-action: pan-x;
|
||||
cursor: pointer;
|
||||
|
||||
display: block;
|
||||
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
background-color: var(--color-light-100);
|
||||
border-radius: 9999px;
|
||||
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
26
src/components/ui/slider/slider.tsx
Normal file
26
src/components/ui/slider/slider.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react'
|
||||
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import s from './slider.module.scss'
|
||||
const Slider = forwardRef<
|
||||
ElementRef<typeof SliderPrimitive.Root>,
|
||||
ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className={s.container}>
|
||||
<span>{props?.value?.[0]}</span>
|
||||
<SliderPrimitive.Root ref={ref} className={clsx(s.root, className)} {...props}>
|
||||
<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>{props?.value?.[1]}</span>
|
||||
</div>
|
||||
))
|
||||
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
export { Slider }
|
||||
@@ -1,5 +1,6 @@
|
||||
.table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
color: var(--color-light-100);
|
||||
border: 1px solid var(--color-dark-500);
|
||||
}
|
||||
|
||||
@@ -75,3 +75,49 @@ export const TableEmpty: FC<ComponentProps<'div'> & { mt?: string; mb?: string }
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
export type Column = {
|
||||
key: string
|
||||
title: string
|
||||
sortable?: boolean
|
||||
}
|
||||
export type Sort = {
|
||||
key: string
|
||||
direction: 'asc' | 'desc'
|
||||
} | null
|
||||
|
||||
export const TableHeader: FC<
|
||||
Omit<
|
||||
ComponentPropsWithoutRef<'thead'> & {
|
||||
columns: Column[]
|
||||
sort?: Sort
|
||||
onSort?: (sort: Sort) => void
|
||||
},
|
||||
'children'
|
||||
>
|
||||
> = ({ columns, sort, onSort, ...restProps }) => {
|
||||
const handleSort = (key: string, sortable?: boolean) => () => {
|
||||
if (!onSort || !sortable) return
|
||||
|
||||
if (sort?.key !== key) return onSort({ key, direction: 'asc' })
|
||||
|
||||
if (sort.direction === 'desc') return onSort(null)
|
||||
|
||||
return onSort({
|
||||
key,
|
||||
direction: sort?.direction === 'asc' ? 'desc' : 'asc',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<TableHead {...restProps}>
|
||||
<TableRow>
|
||||
{columns.map(({ title, key, sortable = true }) => (
|
||||
<TableHeadCell key={key} onClick={handleSort(key, sortable)}>
|
||||
{title}
|
||||
{sort && sort.key === key && <span>{sort.direction === 'asc' ? '▲' : '▼'}</span>}
|
||||
</TableHeadCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
)
|
||||
}
|
||||
|
||||
1
src/components/ui/tabs/index.ts
Normal file
1
src/components/ui/tabs/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './tabs'
|
||||
34
src/components/ui/tabs/tabs.module.scss
Normal file
34
src/components/ui/tabs/tabs.module.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
.list {
|
||||
display: inline-flex;
|
||||
background-color: var(--color-dark-900);
|
||||
}
|
||||
|
||||
.trigger {
|
||||
cursor: pointer;
|
||||
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
padding: 6px 24px;
|
||||
|
||||
background-color: var(--color-dark-900);
|
||||
border: 1px solid var(--color-dark-300);
|
||||
|
||||
&:last-of-type {
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
border-radius: 2px 0 0 2px;
|
||||
}
|
||||
|
||||
&[data-state='active'] {
|
||||
background-color: var(--color-accent-500);
|
||||
border-color: var(--color-accent-500);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
color: inherit;
|
||||
}
|
||||
37
src/components/ui/tabs/tabs.tsx
Normal file
37
src/components/ui/tabs/tabs.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react'
|
||||
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import s from './tabs.module.scss'
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = forwardRef<
|
||||
ElementRef<typeof TabsPrimitive.List>,
|
||||
ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List ref={ref} className={clsx(s.list, className)} {...props} />
|
||||
))
|
||||
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = forwardRef<
|
||||
ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger ref={ref} className={clsx(s.trigger, className)} {...props} />
|
||||
))
|
||||
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = forwardRef<
|
||||
ElementRef<typeof TabsPrimitive.Content>,
|
||||
ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content ref={ref} className={clsx(s.content, className)} {...props} />
|
||||
))
|
||||
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
@@ -40,6 +40,10 @@
|
||||
color: var(--color-danger-300);
|
||||
border-color: var(--color-danger-300);
|
||||
}
|
||||
|
||||
&.hasLeadingIcon {
|
||||
padding-left: 41px;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
@@ -47,6 +51,27 @@
|
||||
color: var(--color-dark-100);
|
||||
}
|
||||
|
||||
.leadingIcon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
bottom: 50%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 12px;
|
||||
padding: 0;
|
||||
|
||||
background: transparent;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(--outline-focus);
|
||||
}
|
||||
}
|
||||
|
||||
.showPassword {
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { clsx } from 'clsx'
|
||||
|
||||
import s from './text-field.module.scss'
|
||||
|
||||
import { VisibilityOff, Eye } from '@/assets'
|
||||
import { VisibilityOff, Eye, Search } from '@/assets'
|
||||
import { Typography } from '@/components'
|
||||
|
||||
export type TextFieldProps = {
|
||||
@@ -13,6 +13,7 @@ export type TextFieldProps = {
|
||||
labelProps?: ComponentProps<'label'>
|
||||
errorMessage?: string
|
||||
label?: string
|
||||
search?: boolean
|
||||
} & ComponentPropsWithoutRef<'input'>
|
||||
|
||||
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
@@ -27,6 +28,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
label,
|
||||
onChange,
|
||||
onValueChange,
|
||||
search,
|
||||
...restProps
|
||||
},
|
||||
ref
|
||||
@@ -45,9 +47,10 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
const classNames = {
|
||||
root: clsx(s.root, containerProps?.className),
|
||||
fieldContainer: clsx(s.fieldContainer),
|
||||
field: clsx(s.field, !!errorMessage && s.error, className),
|
||||
field: clsx(s.field, !!errorMessage && s.error, search && s.hasLeadingIcon, className),
|
||||
label: clsx(s.label, labelProps?.className),
|
||||
error: clsx(s.error),
|
||||
leadingIcon: s.leadingIcon,
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -58,6 +61,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
</Typography>
|
||||
)}
|
||||
<div className={classNames.fieldContainer}>
|
||||
{search && <Search className={classNames.leadingIcon} />}
|
||||
<input
|
||||
className={classNames.field}
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { StrictMode } from 'react'
|
||||
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
import { App } from '@/App'
|
||||
import { App } from './App'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
|
||||
17
src/pages/decks-page/decks-page.module.scss
Normal file
17
src/pages/decks-page/decks-page.module.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
.root {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
column-gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
87
src/pages/decks-page/decks-page.tsx
Normal file
87
src/pages/decks-page/decks-page.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import s from './decks-page.module.scss'
|
||||
|
||||
import {
|
||||
Button,
|
||||
Page,
|
||||
Typography,
|
||||
Column,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TextField,
|
||||
Slider,
|
||||
} from '@/components'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { useGetDecksQuery } from '@/services/decks'
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
key: 'name',
|
||||
title: 'Name',
|
||||
},
|
||||
{
|
||||
key: 'cardsCount',
|
||||
title: 'Cards',
|
||||
},
|
||||
{
|
||||
key: 'updated',
|
||||
title: 'Last Updated',
|
||||
},
|
||||
{
|
||||
key: 'author',
|
||||
title: 'Created By',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
title: '',
|
||||
},
|
||||
]
|
||||
|
||||
export const DecksPage = () => {
|
||||
const { data: decks } = useGetDecksQuery()
|
||||
const [activeTab, setActiveTab] = useState('my')
|
||||
const [range, setRange] = useState([0, 100])
|
||||
const [rangeValue, setRangeValue] = useState([0, 1])
|
||||
|
||||
if (!decks) return <div>loading...</div>
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className={s.root}>
|
||||
<div className={s.header}>
|
||||
<Typography variant="large">Decks</Typography>
|
||||
<Button>Add new deck</Button>
|
||||
</div>
|
||||
<div className={s.filters}>
|
||||
<TextField placeholder="Search" search />
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value={'my'}>My decks</TabsTrigger>
|
||||
<TabsTrigger value={'all'}>All decks</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<Slider onValueCommit={setRange} value={rangeValue} onValueChange={setRangeValue} />
|
||||
<Button variant={'secondary'}>Clear filters</Button>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader columns={columns} />
|
||||
<TableBody>
|
||||
{decks?.items.map(deck => (
|
||||
<TableRow key={deck.id}>
|
||||
<TableCell>{deck.name}</TableCell>
|
||||
<TableCell>{deck.cardsCount}</TableCell>
|
||||
<TableCell>{deck.updated}</TableCell>
|
||||
<TableCell>{deck.author.name}</TableCell>
|
||||
<TableCell>...</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
1
src/pages/decks-page/index.ts
Normal file
1
src/pages/decks-page/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './decks-page'
|
||||
2
src/pages/index.ts
Normal file
2
src/pages/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './sign-in-page'
|
||||
export * from './decks-page'
|
||||
1
src/pages/sign-in-page/index.ts
Normal file
1
src/pages/sign-in-page/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sign-in-page'
|
||||
9
src/pages/sign-in-page/sign-in-page.tsx
Normal file
9
src/pages/sign-in-page/sign-in-page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { SignIn, Page } from '@/components'
|
||||
|
||||
export const SignInPage = () => {
|
||||
return (
|
||||
<Page>
|
||||
<SignIn onSubmit={() => {}} />
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -6,21 +6,27 @@ import {
|
||||
RouterProvider,
|
||||
} from 'react-router-dom'
|
||||
|
||||
import { useGetDecksQuery } from '@/services/base-api'
|
||||
import { SignInPage, DecksPage } from './pages'
|
||||
|
||||
const publicRoutes: RouteObject[] = [
|
||||
{
|
||||
element: <Outlet />,
|
||||
children: [
|
||||
{
|
||||
path: '/login',
|
||||
element: <div>login</div>,
|
||||
element: <SignInPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const privateRoutes: RouteObject[] = [
|
||||
{
|
||||
path: '/',
|
||||
element: <div>hello</div>,
|
||||
element: <DecksPage />,
|
||||
},
|
||||
]
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
element: <PrivateRoutes />,
|
||||
@@ -30,14 +36,11 @@ const router = createBrowserRouter([
|
||||
])
|
||||
|
||||
export const Router = () => {
|
||||
const result = useGetDecksQuery()
|
||||
|
||||
console.log(result)
|
||||
|
||||
return <RouterProvider router={router} />
|
||||
}
|
||||
|
||||
function PrivateRoutes() {
|
||||
const isAuthenticated = false
|
||||
const isAuthenticated = true
|
||||
|
||||
return isAuthenticated ? <Outlet /> : <Navigate to="/login" />
|
||||
}
|
||||
|
||||
@@ -9,13 +9,5 @@ export const baseApi = createApi({
|
||||
headers.append('x-auth-skip', 'true')
|
||||
},
|
||||
}),
|
||||
endpoints: builder => {
|
||||
return {
|
||||
getDecks: builder.query<any, void>({
|
||||
query: () => `v1/decks`,
|
||||
}),
|
||||
}
|
||||
},
|
||||
endpoints: () => ({}),
|
||||
})
|
||||
|
||||
export const { useGetDecksQuery } = baseApi
|
||||
|
||||
13
src/services/decks/decks.service.ts
Normal file
13
src/services/decks/decks.service.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { DecksResponse } from './decks.types'
|
||||
|
||||
import { baseApi } from '@/services'
|
||||
|
||||
const decksService = baseApi.injectEndpoints({
|
||||
endpoints: builder => ({
|
||||
getDecks: builder.query<DecksResponse, void>({
|
||||
query: () => `v1/decks`,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
export const { useGetDecksQuery } = decksService
|
||||
33
src/services/decks/decks.types.ts
Normal file
33
src/services/decks/decks.types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export type Pagination = {
|
||||
totalPages: number
|
||||
currentPage: number
|
||||
itemsPerPage: number
|
||||
totalItems: number
|
||||
}
|
||||
|
||||
export type Author = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type Deck = {
|
||||
id: string
|
||||
userId: string
|
||||
name: string
|
||||
isPrivate: boolean
|
||||
shots: number
|
||||
cover?: string | null
|
||||
rating: number
|
||||
isDeleted: boolean | null
|
||||
isBlocked?: boolean | null
|
||||
created: string
|
||||
updated: string
|
||||
cardsCount: number
|
||||
author: Author
|
||||
}
|
||||
|
||||
export type DecksResponse = {
|
||||
maxCardsCount: number
|
||||
pagination: Pagination
|
||||
items: Deck[]
|
||||
}
|
||||
2
src/services/decks/index.ts
Normal file
2
src/services/decks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './decks.service'
|
||||
export * from './decks.types'
|
||||
1
src/services/index.ts
Normal file
1
src/services/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './base-api'
|
||||
@@ -39,3 +39,23 @@ body {
|
||||
|
||||
background-color: var(--color-dark-900);
|
||||
}
|
||||
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus input:-webkit-autofill,
|
||||
textarea:-webkit-autofill,
|
||||
textarea:-webkit-autofill:hover textarea:-webkit-autofill:focus,
|
||||
select:-webkit-autofill,
|
||||
select:-webkit-autofill:hover,
|
||||
select:-webkit-autofill:focus {
|
||||
background: linear-gradient(
|
||||
rgb(255 255 255 / 0%) 0%,
|
||||
rgb(0 174 255 / 4%) 50%,
|
||||
rgb(255 255 255 / 0%) 51%,
|
||||
rgb(0 174 255 / 3%) 100%
|
||||
);
|
||||
box-shadow: 0 0 0 1000px transparent inset;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
|
||||
-webkit-text-fill-color: var(--color-light-100);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user