├── .storybook ├── manager-head.html ├── manager.ts ├── preview-head.html ├── theme.ts ├── main.ts └── preview.tsx ├── static ├── favicon │ └── fav.ico ├── images │ ├── empty.png │ ├── quera.png │ └── branding.png └── fonts │ └── iranyekan │ ├── fonts │ ├── eot │ │ ├── IRANYekanWebBlack.eot │ │ ├── IRANYekanWebBold.eot │ │ ├── IRANYekanWebLight.eot │ │ ├── IRANYekanWebThin.eot │ │ ├── IRANYekanWebMedium.eot │ │ ├── IRANYekanWebRegular.eot │ │ ├── IRANYekanWebExtraBlack.eot │ │ └── IRANYekanWebExtraBold.eot │ ├── ttf │ │ ├── IRANYekanWebBlack.ttf │ │ ├── IRANYekanWebBold.ttf │ │ ├── IRANYekanWebLight.ttf │ │ ├── IRANYekanWebThin.ttf │ │ ├── IRANYekanWebMedium.ttf │ │ ├── IRANYekanWebRegular.ttf │ │ ├── IRANYekanWebExtraBlack.ttf │ │ └── IRANYekanWebExtraBold.ttf │ ├── woff │ │ ├── IRANYekanWebBlack.woff │ │ ├── IRANYekanWebBold.woff │ │ ├── IRANYekanWebLight.woff │ │ ├── IRANYekanWebMedium.woff │ │ ├── IRANYekanWebThin.woff │ │ ├── IRANYekanWebRegular.woff │ │ ├── IRANYekanWebExtraBlack.woff │ │ └── IRANYekanWebExtraBold.woff │ └── woff2 │ │ ├── IRANYekanWebBold.woff2 │ │ ├── IRANYekanWebThin.woff2 │ │ ├── IRANYekanWebBlack.woff2 │ │ ├── IRANYekanWebLight.woff2 │ │ ├── IRANYekanWebMedium.woff2 │ │ ├── IRANYekanWebExtraBold.woff2 │ │ ├── IRANYekanWebRegular.woff2 │ │ └── IRANYekanWebExtraBlack.woff2 │ └── css │ └── fontiran.css ├── src ├── theme │ ├── foundations │ │ ├── sizes.ts │ │ ├── breakpoints.ts │ │ ├── z-index.ts │ │ ├── fonts.ts │ │ └── colors.ts │ ├── components │ │ ├── pagination.ts │ │ ├── form-label.ts │ │ ├── checkbox.ts │ │ ├── modal.ts │ │ ├── input.ts │ │ ├── heading.ts │ │ ├── switch.ts │ │ ├── textarea.ts │ │ ├── breadcrumb.ts │ │ ├── alert.ts │ │ ├── button.ts │ │ ├── card.ts │ │ └── steps.ts │ ├── styles.ts │ ├── semantictokens.ts │ └── index.ts ├── contexts.ts ├── components │ ├── PinJob.tsx │ ├── MdIcon.tsx │ ├── Sidebar.tsx │ ├── UserQCVProgress.stories.tsx │ ├── UserAvatar.stories.tsx │ ├── AnimateCounter.tsx │ ├── RichText.tsx │ ├── Empty.stories.tsx │ ├── Select.stories.tsx │ ├── Empty.tsx │ ├── ProblemIcon.tsx │ ├── QuoteItem.tsx │ ├── AnimateCounter.stories.tsx │ ├── GrayTag.tsx │ ├── Pagination.stories.tsx │ ├── UserAvatar.tsx │ ├── ShareLink.tsx │ ├── SignInModalProvider.stories.tsx │ ├── UserQCVProgress.tsx │ ├── SocialNetworkIcon.stories.tsx │ ├── TechnologyLabel.tsx │ ├── HiringCompanyLogo.stories.tsx │ ├── Scrollable.tsx │ ├── SocialNetworkIcon.tsx │ ├── FadedHorizontalScrollable.stories.tsx │ ├── HiringCompanyLogo.tsx │ ├── UploadResume.stories.tsx │ ├── PinButton.tsx │ ├── Faded.tsx │ ├── FaqAccordion.tsx │ ├── FadedHorizontalScrollable.tsx │ ├── Faded.stories.tsx │ ├── FaqAccordion.stories.tsx │ ├── SignInModalProvider.tsx │ ├── SearchBar.tsx │ ├── SearchBox.stories.tsx │ ├── Card.tsx │ ├── FileInputField.tsx │ ├── ReportButton.stories.tsx │ ├── Select.tsx │ ├── UploadResume.tsx │ ├── Pagination.tsx │ ├── SearchBox.tsx │ └── ReportButton.tsx ├── api │ └── rest │ │ └── axios-client.ts ├── utils │ ├── cookie.ts │ ├── querystring.ts │ ├── storybook.tsx │ └── string.ts ├── hooks │ ├── useDebouncedValue.ts │ ├── useRemoteChoices.ts │ └── useScrollOnDrag.ts └── index.ts ├── README.md ├── tsconfig.pkg.json ├── .gitignore ├── .prettierrc.js ├── tsconfig.json ├── .editorconfig ├── .github └── workflows │ ├── storybook.yml │ └── publish_package.yml ├── package.json └── .eslintrc.js /.storybook/manager-head.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/favicon/fav.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/favicon/fav.ico -------------------------------------------------------------------------------- /static/images/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/images/empty.png -------------------------------------------------------------------------------- /static/images/quera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/images/quera.png -------------------------------------------------------------------------------- /static/images/branding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/images/branding.png -------------------------------------------------------------------------------- /src/theme/foundations/sizes.ts: -------------------------------------------------------------------------------- 1 | export const sizes = { 2 | container: { 3 | xl: "1160px", 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/contexts.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export const ReachedPageBottom = React.createContext(false); 4 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from "@storybook/manager-api"; 2 | import Theme from "./theme"; 3 | 4 | addons.setConfig({ 5 | theme: Theme, 6 | }); 7 | -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/eot/IRANYekanWebBlack.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/eot/IRANYekanWebBlack.eot -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/eot/IRANYekanWebBold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/eot/IRANYekanWebBold.eot -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/eot/IRANYekanWebLight.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/eot/IRANYekanWebLight.eot -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/eot/IRANYekanWebThin.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/eot/IRANYekanWebThin.eot -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/ttf/IRANYekanWebBlack.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/ttf/IRANYekanWebBlack.ttf -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/ttf/IRANYekanWebBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/ttf/IRANYekanWebBold.ttf -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/ttf/IRANYekanWebLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/ttf/IRANYekanWebLight.ttf -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/ttf/IRANYekanWebThin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/ttf/IRANYekanWebThin.ttf -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/eot/IRANYekanWebMedium.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/eot/IRANYekanWebMedium.eot -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/eot/IRANYekanWebRegular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/eot/IRANYekanWebRegular.eot -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/ttf/IRANYekanWebMedium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/ttf/IRANYekanWebMedium.ttf -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/ttf/IRANYekanWebRegular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/ttf/IRANYekanWebRegular.ttf -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff/IRANYekanWebBlack.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff/IRANYekanWebBlack.woff -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff/IRANYekanWebBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff/IRANYekanWebBold.woff -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff/IRANYekanWebLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff/IRANYekanWebLight.woff -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff/IRANYekanWebMedium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff/IRANYekanWebMedium.woff -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff/IRANYekanWebThin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff/IRANYekanWebThin.woff -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff2/IRANYekanWebBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff2/IRANYekanWebBold.woff2 -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff2/IRANYekanWebThin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff2/IRANYekanWebThin.woff2 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quera Core UI 2 | 3 | Quera's component library based on Chakra-UI. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install @querateam/qui 9 | ``` 10 | -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/eot/IRANYekanWebExtraBlack.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/eot/IRANYekanWebExtraBlack.eot -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/eot/IRANYekanWebExtraBold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/eot/IRANYekanWebExtraBold.eot -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/ttf/IRANYekanWebExtraBlack.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/ttf/IRANYekanWebExtraBlack.ttf -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/ttf/IRANYekanWebExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/ttf/IRANYekanWebExtraBold.ttf -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff/IRANYekanWebRegular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff/IRANYekanWebRegular.woff -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff2/IRANYekanWebBlack.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff2/IRANYekanWebBlack.woff2 -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff2/IRANYekanWebLight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff2/IRANYekanWebLight.woff2 -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff2/IRANYekanWebMedium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff2/IRANYekanWebMedium.woff2 -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff/IRANYekanWebExtraBlack.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff/IRANYekanWebExtraBlack.woff -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff/IRANYekanWebExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff/IRANYekanWebExtraBold.woff -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff2/IRANYekanWebExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff2/IRANYekanWebExtraBold.woff2 -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff2/IRANYekanWebRegular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff2/IRANYekanWebRegular.woff2 -------------------------------------------------------------------------------- /src/theme/components/pagination.ts: -------------------------------------------------------------------------------- 1 | // This component is created by Quera Team. 2 | 3 | export const Pagination = { 4 | baseStyle: { 5 | justifyContent: "center", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /static/fonts/iranyekan/fonts/woff2/IRANYekanWebExtraBlack.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QueraTeam/quera-core-ui/HEAD/static/fonts/iranyekan/fonts/woff2/IRANYekanWebExtraBlack.woff2 -------------------------------------------------------------------------------- /tsconfig.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src" 5 | }, 6 | "exclude": ["node_modules", "out", "storybook-static", "src/**/*.stories.tsx", ".storybook/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /src/theme/components/form-label.ts: -------------------------------------------------------------------------------- 1 | // Default: https://github.com/chakra-ui/chakra-ui/blob/main/packages/theme/src/components/form-label.ts 2 | 3 | export const FormLabel = { 4 | baseStyle: { 5 | fontSize: "sm", 6 | marginBottom: 1, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/theme/foundations/breakpoints.ts: -------------------------------------------------------------------------------- 1 | import { createBreakpoints } from "@chakra-ui/theme-tools"; 2 | 3 | export const breakpoints = createBreakpoints({ 4 | sm: "576px", 5 | md: "768px", 6 | lg: "992px", 7 | xl: "1200px", 8 | "2xl": "1600px", 9 | }); 10 | -------------------------------------------------------------------------------- /.storybook/theme.ts: -------------------------------------------------------------------------------- 1 | import { create } from "@storybook/theming/create"; 2 | 3 | export default create({ 4 | base: "dark", 5 | brandTitle: "Quera", 6 | brandUrl: "https://quera.org", 7 | brandImage: "/images/branding.png", 8 | brandTarget: "_blank", 9 | }); 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE settings 2 | .idea 3 | .vscode 4 | 5 | # Dependency directories 6 | node_modules/ 7 | 8 | # builds 9 | storybook-static/ 10 | dist/ 11 | 12 | # TypeScript cache 13 | *.tsbuildinfo 14 | 15 | # Optional npm cache directory 16 | .npm 17 | 18 | # Output of 'npm pack' 19 | *.tgz 20 | -------------------------------------------------------------------------------- /src/components/PinJob.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PinButton, PinProps } from "./PinButton"; 3 | 4 | export const PinJob = (props: PinProps) => ; 5 | 6 | export const IconPinJob = (props: PinProps) => ; 7 | -------------------------------------------------------------------------------- /src/api/rest/axios-client.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { getBrowserCookie } from "../../utils/cookie"; 3 | 4 | export const axiosClient = axios.create({ 5 | // baseURL: "http://localhost:8000", 6 | headers: { 7 | "x-csrftoken": getBrowserCookie("csrf_token") || "", 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/theme/components/checkbox.ts: -------------------------------------------------------------------------------- 1 | // Default: https://github.com/chakra-ui/chakra-ui/blob/main/packages/theme/src/components/checkbox.ts 2 | 3 | export const Checkbox = { 4 | baseStyle: { 5 | control: { 6 | // 1px is too thin. 7 | // border: "1px solid", 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/theme/foundations/z-index.ts: -------------------------------------------------------------------------------- 1 | export const zIndices = { 2 | hide: -1, 3 | auto: "auto", 4 | base: 0, 5 | docked: 10, 6 | dropdown: 700, 7 | sticky: 800, 8 | banner: 900, 9 | overlay: 1300, 10 | modal: 1400, 11 | popover: 1500, 12 | skipLink: 1600, 13 | toast: 1700, 14 | tooltip: 1800, 15 | }; 16 | -------------------------------------------------------------------------------- /src/theme/foundations/fonts.ts: -------------------------------------------------------------------------------- 1 | const defaultFont = [ 2 | "IRANYekan", 3 | "Tahoma", 4 | "Helvetica", 5 | "sans-serif", 6 | "'Apple Color Emoji'", 7 | "'Segoe UI Emoji'", 8 | "'Segoe UI Symbol'", 9 | "'Noto Color Emoji'", 10 | ].join(", "); 11 | 12 | export const fonts = { body: defaultFont, heading: defaultFont }; 13 | -------------------------------------------------------------------------------- /src/theme/components/modal.ts: -------------------------------------------------------------------------------- 1 | // Default: https://github.com/chakra-ui/chakra-ui/blob/main/packages/theme/src/components/modal.ts 2 | 3 | export const Modal = { 4 | baseStyle: { 5 | dialog: { 6 | ".chakra-modal__footer": { 7 | borderTop: "1px solid", 8 | borderColor: "border.gray", 9 | }, 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/theme/components/input.ts: -------------------------------------------------------------------------------- 1 | // Default: https://github.com/chakra-ui/chakra-ui/blob/main/packages/theme/src/components/input.ts 2 | 3 | export const Input = { 4 | defaultProps: { 5 | focusBorderColor: "brand.500", 6 | }, 7 | variants: { 8 | outline: () => ({ 9 | field: { 10 | bg: "input.bg", 11 | }, 12 | }), 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/theme/components/heading.ts: -------------------------------------------------------------------------------- 1 | // Default: https://github.com/chakra-ui/chakra-ui/blob/main/packages/theme/src/components/heading.ts 2 | 3 | const baseStyle = { 4 | fontWeight: "medium", 5 | }; 6 | 7 | const sizes = { 8 | md: { 9 | fontSize: ["md", "lg", "lg", "xl"], 10 | }, 11 | }; 12 | 13 | export const Heading = { 14 | baseStyle, 15 | sizes, 16 | }; 17 | -------------------------------------------------------------------------------- /src/theme/components/switch.ts: -------------------------------------------------------------------------------- 1 | // Default: https://github.com/chakra-ui/chakra-ui/blob/main/packages/theme/src/components/switch.ts 2 | 3 | import { mode, StyleFunctionProps } from "@chakra-ui/theme-tools"; 4 | 5 | export const Switch = { 6 | baseStyle: (props: StyleFunctionProps) => ({ 7 | thumb: { 8 | bg: mode("white", "gray.700")(props), 9 | }, 10 | }), 11 | }; 12 | -------------------------------------------------------------------------------- /src/theme/components/textarea.ts: -------------------------------------------------------------------------------- 1 | // Default: https://github.com/chakra-ui/chakra-ui/blob/main/packages/theme/src/components/textarea.ts 2 | 3 | export const Textarea = { 4 | baseStyle: { 5 | display: "block", 6 | }, 7 | defaultProps: { 8 | focusBorderColor: "brand.500", 9 | }, 10 | variants: { 11 | outline: { 12 | bg: "input.bg", 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/cookie.ts: -------------------------------------------------------------------------------- 1 | export const getCookie = (cookie: string, name: string): string => 2 | cookie 3 | ?.split(";") 4 | .find((row) => row.trim().startsWith(name)) 5 | ?.split("=")[1] 6 | .trim(); 7 | 8 | export const getBrowserCookie = (name: string): string => { 9 | if (typeof document === "undefined" || !document.cookie) return undefined; 10 | return getCookie(document.cookie, name); 11 | }; 12 | -------------------------------------------------------------------------------- /src/theme/components/breadcrumb.ts: -------------------------------------------------------------------------------- 1 | // Default: https://github.com/chakra-ui/chakra-ui/blob/main/packages/theme/src/components/breadcrumb.ts 2 | 3 | export const Breadcrumb = { 4 | variants: { 5 | oneline: { 6 | container: { 7 | overflow: "hidden", 8 | ol: { overflow: "hidden", whiteSpace: "nowrap", textOverflow: "ellipsis" }, 9 | li: { display: "inline" }, 10 | }, 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/querystring.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQuery } from "querystring"; 2 | 3 | export const getSingleQueryParam = (queryParam: string | string[]): string => { 4 | if (!queryParam) return undefined; 5 | if (Array.isArray(queryParam) && queryParam.length >= 1) return queryParam[0]; 6 | return queryParam as string; 7 | }; 8 | 9 | export const getPageNumber = ( 10 | query: ParsedUrlQuery, 11 | paramName: string = "page" 12 | ): number => Number(getSingleQueryParam(query[paramName])) || 1; 13 | -------------------------------------------------------------------------------- /src/theme/components/alert.ts: -------------------------------------------------------------------------------- 1 | import { ComponentStyleConfig } from "@chakra-ui/react"; 2 | import { mode, StyleFunctionProps } from "@chakra-ui/theme-tools"; 3 | 4 | export const Alert: ComponentStyleConfig = { 5 | variants: { 6 | solid: (props: StyleFunctionProps) => { 7 | const bg = props.status === "info" ? "gray" : props.colorScheme; 8 | 9 | return { 10 | container: { 11 | bg: mode(`${bg}.500`, `${bg}.300`)(props), 12 | }, 13 | }; 14 | }, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // Should match ".editorconfig" 2 | module.exports = { 3 | useTabs: false, 4 | tabWidth: 4, 5 | endOfLine: "lf", 6 | printWidth: 120, 7 | trailingComma: "all", 8 | overrides: [ 9 | { 10 | files: ["*.js", "*.ts", "*.jsx", "*.tsx", "*.md", "*.json", "*.yml", "*.yaml", "*.jsbeautifyrc", "*.html"], 11 | options: { 12 | tabWidth: 2, 13 | }, 14 | }, 15 | { 16 | files: ["*.md"], 17 | options: { 18 | printWidth: 80, 19 | }, 20 | }, 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/MdIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Icon, IconProps, forwardRef } from "@chakra-ui/react"; 3 | 4 | export interface MdIconProps extends IconProps { 5 | icon: string; 6 | css?: any; 7 | } 8 | export const MdIcon = forwardRef(({ icon, ...props }, ref) => ( 9 | 19 | 20 | 21 | )); 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "Commonjs", 5 | "target": "es2015", 6 | "outDir": "./dist", 7 | "rootDir": ".", 8 | "declaration": true, 9 | "strict": false, 10 | "jsx": "react", 11 | "skipLibCheck": true, 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "esModuleInterop": true 17 | }, 18 | "include": ["src/**/*", ".storybook/*"], 19 | "exclude": ["node_modules", "out", "storybook-static"] 20 | } 21 | -------------------------------------------------------------------------------- /src/hooks/useDebouncedValue.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | /** 4 | * This hook allows you to debounce any fast changing value. 5 | * @param value 6 | * @param delay 7 | */ 8 | export const useDebouncedValue = (value: any, delay: number) => { 9 | // State and setters for debounced value 10 | const [debouncedValue, setDebouncedValue] = React.useState(value); 11 | React.useEffect(() => { 12 | const handler = setTimeout(() => { 13 | setDebouncedValue(value); 14 | }, delay); 15 | return () => { 16 | clearTimeout(handler); 17 | }; 18 | }, [value, delay]); 19 | return debouncedValue; 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Box, Flex } from "@chakra-ui/react"; 3 | 4 | export const SidebarRow = ({ children }: { children: React.ReactNode }) => ( 5 | 6 | {children} 7 | 8 | ); 9 | 10 | export const SidebarLabel = ({ children }: { children: React.ReactNode }) => ( 11 | 12 | {children} 13 | 14 | ); 15 | 16 | export const SidebarValue = ({ children }: { children: React.ReactNode }) => ( 17 | 18 | {children} 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /src/theme/styles.ts: -------------------------------------------------------------------------------- 1 | import { mode, StyleFunctionProps } from "@chakra-ui/theme-tools"; 2 | 3 | export const styles = { 4 | global: (props: StyleFunctionProps) => ({ 5 | body: { 6 | color: mode("gray.600", "whiteAlpha.800")(props), 7 | bg: mode("defaultBackground", "gray.800")(props), 8 | transitionProperty: "unset", 9 | transitionDuration: "unset", 10 | }, 11 | html: { 12 | fontSize: 14, 13 | scrollBehavior: "smooth", 14 | colorScheme: mode("light", "dark")(props), 15 | }, 16 | "p, ul, ol": { 17 | lineHeight: 2, 18 | }, 19 | "*, *::before, &::after": { 20 | borderColor: "border.gray", 21 | }, 22 | }), 23 | }; 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | # Should match ".prettierrc.js" 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | max_line_length = 120 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | charset = utf-8 14 | 15 | # Do not add "md" here! It breaks Markdown re-formatting in PyCharm. 16 | [*.{js,ts,jsx,tsx,json,yml,yaml,html,jsbeautifyrc}] 17 | indent_size = 2 18 | 19 | [*.md] 20 | # Shorter lines in documentation files improves readability 21 | max_line_length = 80 22 | # 2 spaces at the end of a line forces a line break in MarkDown 23 | trim_trailing_whitespace = false 24 | 25 | [Makefile] 26 | indent_style = tab 27 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | 3 | const config: StorybookConfig = { 4 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 5 | staticDirs: ["../static"], 6 | addons: [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | "@storybook/addon-onboarding", 10 | "@storybook/addon-interactions", 11 | ], 12 | framework: { 13 | name: "@storybook/react-vite", 14 | options: {}, 15 | }, 16 | docs: { 17 | autodocs: "tag", 18 | }, 19 | refs: { 20 | "@chakra-ui/react": { 21 | disable: true, // disabled until they get their stuff together. currently just causes errors and never loads 22 | }, 23 | }, 24 | }; 25 | export default config; 26 | -------------------------------------------------------------------------------- /src/components/UserQCVProgress.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { StoryObj, Meta } from "@storybook/react"; 3 | import { UserQCVProgress } from "./UserQCVProgress"; 4 | 5 | const meta: Meta = { 6 | component: UserQCVProgress, 7 | parameters: { 8 | controls: { 9 | exclude: /(name)|(avatar)/g, 10 | }, 11 | }, 12 | }; 13 | 14 | export const Primary: StoryObj = { 15 | argTypes: { 16 | progress: { 17 | control: { 18 | type: "number", 19 | }, 20 | }, 21 | isMobile: { 22 | control: { type: "boolean" }, 23 | }, 24 | }, 25 | args: { 26 | progress: 40, 27 | isMobile: false, 28 | avatar: "/images/quera.png", 29 | name: "Quera", 30 | }, 31 | }; 32 | 33 | export default meta; 34 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { Preview } from "@storybook/react"; 3 | import { ChakraProvider, Container, Flex } from "@chakra-ui/react"; 4 | import { theme } from "../src"; 5 | const preview: Preview = { 6 | parameters: { 7 | actions: { argTypesRegex: "^on[A-Z].*" }, 8 | controls: { 9 | matchers: { 10 | color: /(background|color)$/i, 11 | date: /Date$/, 12 | }, 13 | }, 14 | }, 15 | decorators: [ 16 | (Story) => ( 17 | 18 | 24 | 25 | 26 | 27 | ), 28 | ], 29 | }; 30 | 31 | export default preview; 32 | -------------------------------------------------------------------------------- /src/components/UserAvatar.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { HStack } from "@chakra-ui/react"; 3 | import { StoryObj, Meta } from "@storybook/react"; 4 | import { UserAvatar } from "./UserAvatar"; 5 | 6 | const meta: Meta = { 7 | component: UserAvatar, 8 | parameters: { 9 | controls: { 10 | exclude: /(htmlTranslate)|(as)|(_.*)|(name)|(src)/g, 11 | }, 12 | }, 13 | }; 14 | 15 | export const Primary: StoryObj = { 16 | args: { 17 | src: "/images/quera.png", 18 | name: "Quera", 19 | circleColor: "#ff0000", 20 | }, 21 | argTypes: { 22 | circleColor: { 23 | control: { 24 | type: "color", 25 | }, 26 | }, 27 | name: { 28 | control: { type: "text" }, 29 | }, 30 | }, 31 | }; 32 | 33 | export default meta; 34 | -------------------------------------------------------------------------------- /src/components/AnimateCounter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { persianDigits } from "../utils/string"; 3 | 4 | export const AnimateCounter = ({ value, time }: { value: string; time: number }) => { 5 | const [timer, setTimer] = React.useState("0"); 6 | React.useEffect(() => { 7 | let start = 0; 8 | const end = parseInt(value, 10); 9 | const incrementTime = (time / end) * 1000; 10 | const step = Math.floor(end / 400) || 1; 11 | const timerInterval = setInterval(() => { 12 | start = start + step > end ? end : start + step; 13 | setTimer(`${start}`); 14 | if (start === end) { 15 | clearInterval(timerInterval); 16 | } 17 | }, incrementTime); 18 | return () => clearInterval(timerInterval); 19 | }, [time, value]); 20 | return <>{persianDigits(timer)}; 21 | }; 22 | -------------------------------------------------------------------------------- /src/theme/semantictokens.ts: -------------------------------------------------------------------------------- 1 | // https://chakra-ui.com/docs/styled-system/features/semantic-tokens 2 | 3 | // chakra will generate a css variable for every semantic token. For example: 4 | // css variable generated for `colors: { border.gray }` is `--chakra-colors-border-gray`. 5 | 6 | export const semanticTokens = { 7 | colors: { 8 | "border.gray": { 9 | default: "gray.300", 10 | _dark: "whiteAlpha.300", 11 | }, 12 | "text.pale": { 13 | default: "gray.500", 14 | _dark: "gray.400", 15 | }, 16 | "text.pale.extra": { 17 | default: "gray.400", 18 | _dark: "gray.500", 19 | }, 20 | "text.brand": { 21 | default: "brand.600", 22 | _dark: "brand.300", 23 | }, 24 | "input.bg": { 25 | default: "white", 26 | _dark: "whiteAlpha.200", 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/utils/storybook.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { action } from "@storybook/addon-actions"; 3 | import { StoryFn } from "@storybook/react"; 4 | import { FormProvider, useForm } from "react-hook-form"; 5 | 6 | export const StorybookFormProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 7 | const methods = useForm(); 8 | return ( 9 | 10 |
{children}
11 |
12 | ); 13 | }; 14 | 15 | export const WithRHF = 16 | (showSubmitButton: boolean) => 17 | (Story: React.FC): ReturnType> => ( 18 | 19 | 20 | {showSubmitButton && } 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /src/components/RichText.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Box, BoxProps } from "@chakra-ui/react"; 3 | 4 | export const RichText = (props: BoxProps) => ( 5 | 28 | ); 29 | -------------------------------------------------------------------------------- /src/components/Empty.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { Meta, StoryObj } from "@storybook/react"; 3 | import { VStack } from "@chakra-ui/react"; 4 | import { Empty } from "./Empty"; 5 | 6 | const meta: Meta = { 7 | component: Empty, 8 | parameters: { 9 | controls: { 10 | exclude: /emptyImage|(as)|(_.*)/g, 11 | }, 12 | }, 13 | }; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Primary: Story = { 18 | render: ({ ...args }) => ( 19 | 20 | } {...args} /> 21 | 22 | ), 23 | argTypes: { 24 | size: { 25 | options: ["sm", "md", "lg"], 26 | }, 27 | }, 28 | args: { 29 | message: "گشتم نبود، نگرد نیست!", 30 | }, 31 | }; 32 | 33 | export default meta; 34 | -------------------------------------------------------------------------------- /src/theme/components/button.ts: -------------------------------------------------------------------------------- 1 | // Default: https://github.com/chakra-ui/chakra-ui/blob/main/packages/theme/src/components/button.ts 2 | 3 | import { StyleFunctionProps } from "@chakra-ui/react"; 4 | 5 | export const Button = { 6 | baseStyle: { 7 | fontWeight: "medium", 8 | }, 9 | variants: { 10 | outline: (props: StyleFunctionProps) => { 11 | const { colorScheme: c } = props; 12 | return { 13 | border: "1px solid", 14 | borderColor: c === "gray" ? "border.gray" : "currentColor", 15 | }; 16 | }, 17 | }, 18 | sizes: { 19 | lg: { 20 | fontSize: "md", 21 | px: 7, 22 | }, 23 | md: { 24 | fontSize: "md", 25 | px: 6, 26 | }, 27 | sm: { 28 | fontSize: "md", 29 | px: 4, 30 | }, 31 | xs: { 32 | fontSize: "md", 33 | px: 3, 34 | }, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/Select.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { StoryObj, Meta } from "@storybook/react"; 3 | import { Select } from "./Select"; 4 | 5 | const meta: Meta = { 6 | component: Select, 7 | parameters: { 8 | controls: { 9 | exclude: /options/g, 10 | }, 11 | }, 12 | }; 13 | type Story = StoryObj; 14 | 15 | interface Technology { 16 | label: string; 17 | value: string; 18 | } 19 | 20 | const technologies: Technology[] = [ 21 | { label: "javascript", value: "javascript" }, 22 | { label: "python", value: "python" }, 23 | { label: "java", value: "java" }, 24 | { label: "react", value: "react" }, 25 | { label: "html", value: "html" }, 26 | ]; 27 | 28 | export const Primary: Story = { 29 | args: { 30 | options: technologies, 31 | isMulti: false, 32 | isLoading: true, 33 | }, 34 | }; 35 | 36 | export default meta; 37 | -------------------------------------------------------------------------------- /src/components/Empty.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Box, Text, FlexProps } from "@chakra-ui/react"; 2 | import * as React from "react"; 3 | 4 | export interface EmptyResultProps extends FlexProps { 5 | size: "sm" | "md" | "lg"; 6 | message: string; 7 | emptyImage: React.ReactNode; 8 | } 9 | export const Empty = ({ size, message, children, emptyImage, ...flexProps }: EmptyResultProps) => { 10 | const imageSize = { 11 | sm: "120px", 12 | md: "200px", 13 | lg: "300px", 14 | }[size]; 15 | return ( 16 | 25 | {emptyImage} 26 | 27 | {message} 28 | {children} 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/theme/foundations/colors.ts: -------------------------------------------------------------------------------- 1 | const gray = { 2 | 50: "#FAFAFA", 3 | 100: "#F2F2F2", 4 | 200: "#D9D9D9", 5 | 300: "#D5D5D5", 6 | 400: "#AEAEAE", 7 | 500: "#808080", 8 | 600: "#666666", 9 | 700: "#373737", 10 | 800: "#202020", 11 | 900: "#191919", 12 | }; 13 | 14 | // const gray = { 15 | // 50: "#F7FAFC", 16 | // 100: "#EDF2F7", 17 | // 200: "#D9DEE4", 18 | // 300: "#CBD5E0", 19 | // 400: "#A0AEC0", 20 | // 500: "#718096", 21 | // 600: "#4A5568", 22 | // 700: "#2D3748", 23 | // 800: "#1A202C", 24 | // 900: "#171923", 25 | // }; 26 | 27 | const brand = { 28 | 50: "#e1f5f9", 29 | 100: "#b2e4f1", 30 | 200: "#82d2e9", 31 | 300: "#55c0e1", 32 | 400: "#33b3dd", 33 | 500: "#00bcef", 34 | 600: "#0099cc", 35 | 700: "#0086ba", 36 | 800: "#0076a6", 37 | 900: "#005685", 38 | }; 39 | 40 | export const colors = { 41 | black: gray["900"], 42 | // gray, 43 | brand, 44 | defaultBackground: "#F5F8FA", 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/ProblemIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tooltip, CircularProgress } from "@chakra-ui/react"; 3 | import { persianDigits } from "../utils/string"; 4 | 5 | export const ProblemIcon = ({ solvedPercent }: { solvedPercent: number }) => { 6 | let tooltipLabel; 7 | 8 | switch (solvedPercent) { 9 | case 100: 10 | tooltipLabel = "حل کامل"; 11 | break; 12 | case null: 13 | tooltipLabel = "بدون تلاش"; 14 | break; 15 | default: 16 | tooltipLabel = `حل ناتمام: ٪${persianDigits(solvedPercent)} نمره`; 17 | break; 18 | } 19 | return ( 20 | 21 | 22 | 28 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/QuoteItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { HStack, VStack, Text, Heading, Box } from "@chakra-ui/react"; 3 | import { UserAvatar } from "./UserAvatar"; 4 | 5 | export interface QuoteItemProps { 6 | image: string; 7 | narrator_name: string; 8 | narrator_role: string; 9 | quote: string; 10 | } 11 | 12 | export const QuoteItem = ({ quote }: { quote: QuoteItemProps }) => ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | {quote.narrator_name} 20 | 21 | {quote.narrator_role} 22 | 23 | 24 | {quote.quote} 25 | 26 | 27 | ); 28 | -------------------------------------------------------------------------------- /src/components/AnimateCounter.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { Card, CardBody, Heading, HStack } from "@chakra-ui/react"; 4 | import { within, userEvent } from "@storybook/testing-library"; 5 | 6 | import { expect } from "@storybook/jest"; 7 | import type { Meta, StoryObj } from "@storybook/react"; 8 | import { AnimateCounter } from "./AnimateCounter"; 9 | 10 | const meta: Meta = { 11 | component: AnimateCounter, 12 | decorators: [ 13 | (Story) => ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ), 24 | ], 25 | }; 26 | 27 | type Story = StoryObj; 28 | 29 | export const Primary: Story = { 30 | args: { 31 | value: "999", 32 | time: 5, 33 | }, 34 | }; 35 | 36 | export default meta; 37 | -------------------------------------------------------------------------------- /src/components/GrayTag.tsx: -------------------------------------------------------------------------------- 1 | import { Tag } from "@chakra-ui/react"; 2 | import * as React from "react"; 3 | 4 | type GrayTagProps = { 5 | children: React.ReactNode; 6 | title?: string; 7 | isBold?: boolean; 8 | isLink?: boolean; 9 | dir?: "ltr" | "rtl" | "auto"; 10 | onClick?: React.MouseEventHandler; 11 | }; 12 | 13 | export const GrayTag = ({ children, title, isBold, isLink, onClick, dir = "ltr" }: GrayTagProps) => ( 14 | 29 | {/* putting dir on outer tag caused margin problems */} 30 | {children} 31 | 32 | ); 33 | -------------------------------------------------------------------------------- /src/components/Pagination.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { ComponentMeta, ComponentStory } from "@storybook/react"; 4 | 5 | import { Card, CardBody } from "@chakra-ui/react"; 6 | import { persianDigits } from "../utils/string"; 7 | import { Pagination } from "./Pagination"; 8 | 9 | export default { 10 | title: "Components/Pagination", 11 | component: Pagination, 12 | parameters: { 13 | controls: { 14 | include: ["page", "total", "pageSize", "onPageChange", "siblingRange", "beginRange", "endRange", "renderNumber"], 15 | }, 16 | }, 17 | } as ComponentMeta; 18 | 19 | const Template: ComponentStory = (args) => ( 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | 27 | export const Base = Template.bind({}); 28 | Base.args = { 29 | total: 467, 30 | pageSize: 15, 31 | page: 12, 32 | renderNumber: (num: string | number) => persianDigits(num), 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/UserAvatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { forwardRef, BoxProps, Box, useColorModeValue, BorderProps } from "@chakra-ui/react"; 3 | 4 | export interface UserAvatarProps extends BoxProps { 5 | src: string; 6 | name: string; 7 | size?: number; 8 | circleColor?: BorderProps["borderColor"]; 9 | } 10 | 11 | export const UserAvatar = forwardRef(({ src, name, size, circleColor, ...rest }, ref) => { 12 | const sz = size || 72; 13 | const bgColor = useColorModeValue("white", "gray.700"); 14 | 15 | return ( 16 | span": { borderRadius: "full" }, 25 | }} 26 | ref={ref} 27 | {...rest} 28 | > 29 | {src && {`پروفایل} 30 | 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /src/components/ShareLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tooltip, Button, useClipboard, ButtonProps } from "@chakra-ui/react"; 3 | import { mdiShareVariantOutline } from "@mdi/js"; 4 | 5 | import { MdIcon } from "./MdIcon"; 6 | 7 | interface ShareLinkProps extends ButtonProps { 8 | helpText: string; 9 | successText: string; 10 | url: string; 11 | } 12 | export const ShareLink = ({ helpText, successText, url, ...buttonProps }: ShareLinkProps) => { 13 | const { hasCopied, onCopy } = useClipboard(url); 14 | const [isHover, setIsHover] = React.useState(false); 15 | 16 | return ( 17 | 18 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/SignInModalProvider.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { Meta, StoryObj } from "@storybook/react"; 4 | import { Button, Card, CardBody, HStack } from "@chakra-ui/react"; 5 | import { SignInModalProvider, useSignInModal } from "./SignInModalProvider"; 6 | 7 | const meta: Meta = { 8 | title: "Components/SignIn Modal", 9 | component: SignInModalProvider, 10 | decorators: [ 11 | (Story) => ( 12 | 13 | 14 | 15 | ), 16 | ], 17 | }; 18 | 19 | const SignInModalWrapper = () => { 20 | const signInModal = useSignInModal(); 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export const Primary: StoryObj = { 34 | render: () => , 35 | }; 36 | 37 | export default meta; 38 | -------------------------------------------------------------------------------- /src/components/UserQCVProgress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { HStack, Link, Progress, Spacer, VStack } from "@chakra-ui/react"; 3 | import { persianDigits } from "../utils/string"; 4 | 5 | export interface UserQCVProgressProps { 6 | name: string; 7 | avatar: string; 8 | progress: number; 9 | isMobile: boolean; 10 | } 11 | 12 | export const UserQCVProgress = ({ name, avatar, progress, isMobile }: UserQCVProgressProps) => ( 13 | 14 | {name} 15 | 16 | 17 | {!isMobile && {name}} 18 | {persianDigits(progress)}٪ پیشرفت 19 | 20 | 21 | مشاهده 22 | 23 | 24 | تکمیل 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | -------------------------------------------------------------------------------- /src/components/SocialNetworkIcon.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { StoryObj, Meta } from "@storybook/react"; 3 | import { HStack } from "@chakra-ui/react"; 4 | import { SocialNetworkIcon } from "./SocialNetworkIcon"; 5 | 6 | const meta: Meta = { 7 | component: SocialNetworkIcon, 8 | parameters: { 9 | controls: { 10 | exclude: /(url)|(_.*)|(css)/g, 11 | }, 12 | }, 13 | decorators: [ 14 | (Story) => ( 15 | 16 | 17 | 18 | ), 19 | ], 20 | }; 21 | 22 | export const Primary: StoryObj = { 23 | args: { 24 | boxSize: 24, 25 | url: "#", 26 | type: "github", 27 | }, 28 | argTypes: { 29 | type: { 30 | options: ["linkedin", "facebook", "github", "twitter", "instagram", "youtube", "google plus"], 31 | control: { type: "select" }, 32 | }, 33 | boxSize: { 34 | control: { 35 | type: "number", 36 | }, 37 | }, 38 | icon: { 39 | table: { 40 | disable: true, 41 | }, 42 | }, 43 | }, 44 | }; 45 | 46 | export default meta; 47 | -------------------------------------------------------------------------------- /.github/workflows/storybook.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | permissions: 3 | contents: write 4 | on: 5 | workflow_dispatch: 6 | push: 7 | paths: ["src/stories/**", ".storybook/**"] # Trigger the action only when files change in the folders defined here 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 🛎️ 13 | uses: actions/checkout@v2.3.1 14 | with: 15 | persist-credentials: false 16 | - name: Install and Build 🔧 17 | run: | # Install npm packages and build the Storybook files 18 | npm install 19 | npm run build-storybook 20 | touch ./sb_dist/.nojekyll 21 | - name: Deploy 🚀 22 | uses: JamesIves/github-pages-deploy-action@v4 23 | with: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | BRANCH: gh-pages # The branch the action should deploy to. 26 | FOLDER: sb_dist # The folder that the build-storybook script generates files. 27 | CLEAN: true # Automatically remove deleted files from the deploy branch 28 | TARGET_FOLDER: docs # The folder that we serve our Storybook files from 29 | SINGLE-COMMIT: true 30 | -------------------------------------------------------------------------------- /src/components/TechnologyLabel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { HStack, Wrap } from "@chakra-ui/react"; 3 | import { FadedHorizontalOneLine } from "./Faded"; 4 | import { GrayTag } from "./GrayTag"; 5 | 6 | type TechnologyLabelProps = { 7 | children: React.ReactNode; 8 | isMain?: boolean; 9 | isLink?: boolean; 10 | onClick?: React.MouseEventHandler; 11 | }; 12 | 13 | export const TechnologyLabel = ({ isMain, ...props }: TechnologyLabelProps) => ( 14 | 15 | ); 16 | 17 | type FadedTechnologyLabelsProps = { 18 | technologies: { code: string; name: string; is_main?: boolean }[]; 19 | oneLine?: boolean; 20 | }; 21 | 22 | export const FadedTechnologyLabels = ({ technologies, oneLine }: FadedTechnologyLabelsProps) => { 23 | if (!technologies || technologies.length === 0) return null; 24 | 25 | const renderedTechnologies = technologies.map((tech) => ( 26 | 27 | {tech.name} 28 | 29 | )); 30 | if (oneLine) return {renderedTechnologies}; 31 | return {renderedTechnologies}; 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/HiringCompanyLogo.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Meta, StoryObj } from "@storybook/react"; 3 | import { HStack } from "@chakra-ui/react"; 4 | import { HiringCompanyLogo, borderTypeValues } from "./HiringCompanyLogo"; 5 | 6 | const meta: Meta = { 7 | component: HiringCompanyLogo, 8 | parameters: { 9 | controls: { 10 | exclude: /(as)|(_.*)/g, 11 | }, 12 | }, 13 | decorators: [ 14 | (Story) => ( 15 | 16 | 17 | 18 | ), 19 | ], 20 | }; 21 | 22 | type Story = StoryObj; 23 | 24 | export const Primary: Story = { 25 | argTypes: { 26 | borderType: { 27 | type: "string", 28 | options: borderTypeValues, 29 | }, 30 | name: { 31 | control: "string", 32 | defaultValue: "کوئرا", 33 | }, 34 | size: { 35 | control: { 36 | type: "number", 37 | defaultValue: 190, 38 | }, 39 | }, 40 | hiring: { 41 | control: "boolean", 42 | }, 43 | }, 44 | args: { 45 | src: "/images/quera.png", 46 | }, 47 | }; 48 | 49 | export default meta; 50 | -------------------------------------------------------------------------------- /src/components/Scrollable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Box, BoxProps, Link, LinkProps, HeadingProps, chakra, forwardRef } from "@chakra-ui/react"; 3 | 4 | interface ScrollableCoreProps { 5 | id: string | undefined; 6 | offset?: BoxProps["top"]; 7 | } 8 | 9 | export const ScrollableCore = ({ id, offset = "-120px" }: ScrollableCoreProps) => ( 10 | 11 | ); 12 | 13 | export const AnchorLink = (props: LinkProps) => ( 14 | 15 | # 16 | 17 | ); 18 | 19 | interface ScrollableHeaderProps extends HeadingProps { 20 | id: string | undefined; 21 | offset?: BoxProps["top"]; 22 | } 23 | 24 | export const ScrollableHeader = forwardRef( 25 | ({ id, as = "h2", offset, children, ...rest }, ref) => ( 26 | 38 | {children} 39 | {id && ( 40 | <> 41 | 42 | 43 | 44 | )} 45 | 46 | ), 47 | ); 48 | -------------------------------------------------------------------------------- /src/components/SocialNetworkIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Link } from "@chakra-ui/react"; 3 | import { mdiFacebook, mdiGithub, mdiGooglePlus, mdiInstagram, mdiLinkedin, mdiTwitter, mdiYoutube } from "@mdi/js"; 4 | import { MdIcon, MdIconProps } from "./MdIcon"; 5 | 6 | const SocialNetworkEnum = { 7 | LinkedIn: "linkedin", 8 | Facebook: "facebook", 9 | GitHub: "github", 10 | Twitter: "twitter", 11 | Instagram: "instagram", 12 | // Telegram: "telegram", 13 | YouTube: "youtube", 14 | GooglePlus: "google plus", 15 | } as const; 16 | 17 | export type SocialNetworkTypes = (typeof SocialNetworkEnum)[keyof typeof SocialNetworkEnum]; 18 | 19 | const SOCIAL_NETWORK_TYPES_TO_ICON: Record = { 20 | [SocialNetworkEnum.LinkedIn]: mdiLinkedin, 21 | [SocialNetworkEnum.Facebook]: mdiFacebook, 22 | [SocialNetworkEnum.GitHub]: mdiGithub, 23 | [SocialNetworkEnum.Twitter]: mdiTwitter, 24 | [SocialNetworkEnum.Instagram]: mdiInstagram, 25 | [SocialNetworkEnum.YouTube]: mdiYoutube, 26 | [SocialNetworkEnum.GooglePlus]: mdiGooglePlus, 27 | }; 28 | 29 | export const SocialNetworkIcon = ({ 30 | url, 31 | type, 32 | ...props 33 | }: Partial & { url: string; type: SocialNetworkTypes }) => { 34 | const icon = SOCIAL_NETWORK_TYPES_TO_ICON[type]; 35 | return ( 36 | 37 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/theme/components/card.ts: -------------------------------------------------------------------------------- 1 | // This component is created by Quera Team. 2 | 3 | import { mode } from "@chakra-ui/theme-tools"; 4 | 5 | const parts = ["card", "header", "row", "body", "footer", "button"]; 6 | 7 | type Dict = Record; 8 | 9 | const baseStyle = (props: Dict) => ({ 10 | card: { 11 | boxShadow: "base", 12 | borderRadius: ["unset", null, "xl"], 13 | bg: mode("white", "gray.700")(props), 14 | }, 15 | row: {}, 16 | header: {}, 17 | body: {}, 18 | button: { 19 | bg: "transparent", 20 | width: "full", 21 | borderTopRadius: 0, 22 | }, 23 | }); 24 | 25 | const sizes = { 26 | sm: { 27 | row: { 28 | px: 3, 29 | py: 2, 30 | }, 31 | header: { 32 | fontSize: "md", 33 | fontWeight: "medium", 34 | }, 35 | body: { 36 | p: 3, 37 | }, 38 | button: { 39 | py: 4, 40 | }, 41 | }, 42 | md: { 43 | row: { 44 | px: 6, 45 | py: 3, 46 | }, 47 | header: { 48 | fontSize: "md", 49 | fontWeight: "medium", 50 | }, 51 | body: { 52 | p: 6, 53 | }, 54 | button: { 55 | py: 5, 56 | }, 57 | }, 58 | lg: { 59 | row: { 60 | px: 6, 61 | py: 4, 62 | }, 63 | header: { 64 | fontSize: "xl", 65 | fontWeight: "bold", 66 | }, 67 | body: { 68 | p: 6, 69 | }, 70 | button: { 71 | py: 6, 72 | }, 73 | }, 74 | }; 75 | 76 | export const Card = { 77 | parts, 78 | baseStyle, 79 | sizes, 80 | defaultProps: { 81 | size: "lg", 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /src/components/FadedHorizontalScrollable.stories.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-array-index-key */ 2 | import * as React from "react"; 3 | 4 | import { Card, CardBody, HStack, Text } from "@chakra-ui/react"; 5 | import type { Meta, StoryObj } from "@storybook/react"; 6 | import { FadedHorizontalScrollable } from "./FadedHorizontalScrollable"; 7 | 8 | const meta: Meta = { 9 | component: FadedHorizontalScrollable, 10 | parameters: { 11 | controls: { 12 | exclude: /^(?!.*\bnavigation\b).*/, 13 | }, 14 | }, 15 | }; 16 | 17 | type Story = StoryObj; 18 | 19 | export const Base: Story = { 20 | argTypes: { 21 | navigation: { 22 | type: "boolean", 23 | description: "sets the arrows on the left or right side of the fade", 24 | }, 25 | }, 26 | render: ({ navigation }) => ( 27 | 28 | 29 | {Array(10) 30 | .fill(1) 31 | .map((item, index) => ( 32 | 33 | 34 | 35 | لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها 36 | و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است. 37 | 38 | 39 | 40 | ))} 41 | 42 | 43 | ), 44 | }; 45 | 46 | export default meta; 47 | -------------------------------------------------------------------------------- /src/components/HiringCompanyLogo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Box, Tooltip, BoxProps, useColorModeValue } from "@chakra-ui/react"; 3 | 4 | export const borderTypeValues = ["default", "none", "fair", "matching"] as const; 5 | export type HiringCompanyLogoBorderType = (typeof borderTypeValues)[number]; 6 | export interface HiringCompanyLogoProps extends BoxProps { 7 | src: string; 8 | name: string; 9 | hiring?: boolean; 10 | size?: number | string; 11 | borderType?: HiringCompanyLogoBorderType; 12 | } 13 | 14 | export const HiringCompanyLogo = ({ 15 | hiring, 16 | src, 17 | name, 18 | size, 19 | borderType = "default", 20 | ...boxProps 21 | }: HiringCompanyLogoProps) => { 22 | const bgColor = useColorModeValue("white", "gray.700"); 23 | const logoSize = size || 72; 24 | 25 | // default case 26 | let borderColor = "brand.500"; 27 | let borderWidth = "2px solid"; 28 | 29 | if (borderType === "fair") { 30 | borderColor = "#00ed97"; 31 | } else if (borderType === "matching") { 32 | borderColor = "orange.400"; 33 | } else if (borderType === "none") { 34 | borderWidth = "none"; 35 | } 36 | 37 | return ( 38 | 39 | span": { display: "block !important" }, 47 | }} 48 | {...boxProps} 49 | > 50 | {`لوگوی 51 | 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/UploadResume.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { StoryObj, Meta } from "@storybook/react"; 3 | import { useForm, useFormContext } from "react-hook-form"; 4 | import { UploadResume } from "./UploadResume"; 5 | import { WithRHF } from "../utils/storybook"; 6 | 7 | const meta: Meta = { 8 | component: UploadResume, 9 | decorators: [WithRHF(false)], 10 | parameters: { 11 | controls: { 12 | exclude: /(_.*)|(as)|(field)|(control)|(previousFile)|(resumeTypes)|(userProfile)/g, 13 | }, 14 | }, 15 | }; 16 | 17 | export enum ResumeTypeEnum { 18 | UPLOAD = "CV", 19 | QCV = "QCV", 20 | } 21 | 22 | const CompleteUploadResume = (props) => { 23 | const { control } = useFormContext(); 24 | 25 | return ( 26 | 38 | ); 39 | }; 40 | 41 | export const Primary: StoryObj = { 42 | argTypes: { 43 | isMobile: { 44 | control: { type: "boolean" }, 45 | }, 46 | showFileSize: { 47 | control: { type: "boolean" }, 48 | }, 49 | minProgressPercent: { 50 | control: { type: "number" }, 51 | }, 52 | }, 53 | args: { 54 | isMobile: false, 55 | showFileSize: true, 56 | minProgressPercent: 50, 57 | }, 58 | render: (args) => , 59 | }; 60 | 61 | export default meta; 62 | -------------------------------------------------------------------------------- /src/hooks/useRemoteChoices.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { debounce } from "lodash"; 3 | import { AxiosResponse } from "axios"; 4 | 5 | type UseRemoteChoicesReturn = { 6 | choices: Choice[]; 7 | isLoading: boolean; 8 | handleSearchChange: (searchTerm: string) => void; 9 | }; 10 | 11 | export const useRemoteChoices = ( 12 | initialChoices: Choice[], 13 | fetchChoices: (searchTerm: string) => Promise>, 14 | normalizeRemoteChoices: (choice: RemoteChoice) => Choice, 15 | delay: number = 1000, 16 | ): UseRemoteChoicesReturn => { 17 | const [remoteChoices, setRemoteChoices] = React.useState([]); 18 | const [isLoading, setIsLoading] = React.useState(false); 19 | const [searchMode, setSearchMode] = React.useState(false); 20 | 21 | // eslint-disable-next-line react-hooks/exhaustive-deps 22 | const search = React.useCallback( 23 | debounce((search_term: string) => { 24 | fetchChoices(search_term).then((response) => { 25 | setRemoteChoices(response.data.map(normalizeRemoteChoices)); 26 | setIsLoading(false); 27 | setSearchMode(true); 28 | }); 29 | }, delay), 30 | [], 31 | ); 32 | 33 | const handleSearchChange = (searchTerm: string) => { 34 | if (searchTerm) { 35 | setIsLoading(true); 36 | search(searchTerm); 37 | } else { 38 | search.cancel(); 39 | setIsLoading(false); 40 | setSearchMode(false); 41 | setRemoteChoices([]); 42 | } 43 | }; 44 | 45 | return { 46 | choices: searchMode ? remoteChoices : initialChoices, 47 | handleSearchChange, 48 | isLoading, 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/PinButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Button, ButtonProps, HStack, Spinner, Text, Tooltip } from "@chakra-ui/react"; 3 | import { mdiBookmark, mdiBookmarkOutline } from "@mdi/js"; 4 | import { MdIcon } from "./MdIcon"; 5 | 6 | export interface PinProps extends ButtonProps { 7 | identity: number | string; 8 | pinned: boolean; 9 | controlled?: boolean; 10 | hintType?: "tooltip" | "text"; 11 | onPin?: React.Dispatch>; 12 | } 13 | 14 | interface PinButtonProps extends PinProps {} 15 | export const PinButton = ({ pinned, controlled = false, hintType = "tooltip", ...buttonProps }: PinButtonProps) => { 16 | const [isPinning] = React.useState(false); 17 | const [_isPinned, setIsPinned] = React.useState(pinned); 18 | const [isHover, setIsHover] = React.useState(false); 19 | 20 | React.useEffect(() => { 21 | setIsPinned(pinned); 22 | }, [pinned]); 23 | 24 | const isPinned = controlled ? pinned : _isPinned; 25 | 26 | const hint = isPinned ? "حذف نشان" : "نشان کردن"; 27 | const icon = isPinned ? : ; 28 | 29 | const button = ( 30 | 45 | ); 46 | 47 | if (hintType === "tooltip") 48 | return ( 49 | 50 | {button} 51 | 52 | ); 53 | return button; 54 | }; 55 | -------------------------------------------------------------------------------- /src/theme/components/steps.ts: -------------------------------------------------------------------------------- 1 | // Default: https://github.com/jeanverster/chakra-ui-steps/blob/main/src/theme/index.ts 2 | 3 | import { ComponentStyleConfig } from "@chakra-ui/react"; 4 | import { mode, StyleFunctionProps } from "@chakra-ui/theme-tools"; 5 | import { StepsStyleConfig } from "chakra-ui-steps"; 6 | 7 | export const Steps: ComponentStyleConfig = { 8 | ...StepsStyleConfig, 9 | baseStyle: (props: StyleFunctionProps) => { 10 | const { colorScheme: c } = props; 11 | const inactiveColor = mode("gray.200", "gray.600")(props); 12 | const activeColor = `${c}.500`; 13 | const baseStyle = StepsStyleConfig.baseStyle(props); 14 | return { 15 | ...baseStyle, 16 | steps: { 17 | ...baseStyle.steps, 18 | textAlign: "inherit", 19 | }, 20 | connector: { 21 | ...baseStyle.connector, 22 | borderColor: inactiveColor, 23 | transitionProperty: "border-color", 24 | transitionDuration: "normal", 25 | _highlighted: { 26 | borderColor: activeColor, 27 | }, 28 | }, 29 | icon: { 30 | ...baseStyle.icon, 31 | color: "var(--chakra-colors-brand-500)", 32 | }, 33 | stepIconContainer: { 34 | ...baseStyle.stepIconContainer, 35 | bg: inactiveColor, 36 | borderColor: inactiveColor, 37 | transitionProperty: "background, border-color", 38 | transitionDuration: "normal", 39 | _activeStep: { 40 | bg: activeColor, 41 | color: mode("white", "black")(props), 42 | borderColor: activeColor, 43 | _invalid: { 44 | bg: "red.500", 45 | borderColor: "red.500", 46 | }, 47 | }, 48 | _highlighted: { 49 | bg: inactiveColor, 50 | borderColor: activeColor, 51 | }, 52 | "&[data-clickable]:hover": { 53 | borderColor: activeColor, 54 | }, 55 | }, 56 | }; 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/Faded.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import * as React from "react"; 3 | 4 | export const FadedHorizontal = styled.div<{ percent?: number }>` 5 | mask-image: linear-gradient( 6 | 270deg, 7 | rgba(0, 0, 0, 1) 0%, 8 | rgba(0, 0, 0, 1) ${(props) => props.percent || 75}%, 9 | rgba(0, 0, 0, 0) 100% 10 | ); 11 | -webkit-mask-image: linear-gradient( 12 | 270deg, 13 | rgba(0, 0, 0, 1) 0%, 14 | rgba(0, 0, 0, 1) ${(props) => props.percent || 75}%, 15 | rgba(0, 0, 0, 0) 100% 16 | ); 17 | `; 18 | 19 | export const FadedHorizontalOneLine = styled(FadedHorizontal)` 20 | overflow-x: hidden; 21 | white-space: nowrap; 22 | `; 23 | 24 | export const FadedVertical = styled.div<{ percent?: number }>` 25 | mask-image: linear-gradient( 26 | to top, 27 | rgba(0, 0, 0, 0) 0%, 28 | rgba(0, 0, 0, 1) min(150px, ${(props) => 100 - (props.percent || 50)}%) 29 | ); 30 | -webkit-mask-image: linear-gradient( 31 | to top, 32 | rgba(0, 0, 0, 0) 0%, 33 | rgba(0, 0, 0, 1) min(150px, ${(props) => 100 - (props.percent || 50)}%) 34 | ); 35 | `; 36 | 37 | type StyledShortenerWrapperProps = { 38 | fade: boolean; 39 | fadedMaxHeight: number | string; 40 | }; 41 | const StyledShortenerWrapper = styled.div((props) => ({ 42 | overflowY: "hidden", 43 | maxHeight: props.fade ? props.fadedMaxHeight : "auto", 44 | })); 45 | 46 | type FadedBoxProps = { 47 | fade: boolean; 48 | children: React.ReactNode; 49 | fadedMaxHeight: number | string; 50 | fadedPercent?: number; 51 | }; 52 | // eslint-disable-next-line react/display-name 53 | export const FadedBox = React.forwardRef( 54 | ({ fade, fadedMaxHeight, fadedPercent = 30, children }, ref) => ( 55 | 56 | 57 | {children} 58 | 59 | 60 | ), 61 | ); 62 | -------------------------------------------------------------------------------- /src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { extendTheme, withDefaultColorScheme, ColorMode, ThemeDirection, Tooltip } from "@chakra-ui/react"; 2 | import { Steps } from "./components/steps"; 3 | 4 | // Global style overrides 5 | import { styles } from "./styles"; 6 | 7 | // Semantic tokens 8 | import { semanticTokens } from "./semantictokens"; 9 | 10 | // Foundational style overrides 11 | import { breakpoints } from "./foundations/breakpoints"; 12 | import { colors } from "./foundations/colors"; 13 | import { fonts } from "./foundations/fonts"; 14 | import { sizes } from "./foundations/sizes"; 15 | import { zIndices } from "./foundations/z-index"; 16 | 17 | // Component style overrides 18 | import { Input } from "./components/input"; 19 | import { Textarea } from "./components/textarea"; 20 | import { Checkbox } from "./components/checkbox"; 21 | import { Breadcrumb } from "./components/breadcrumb"; 22 | import { Card } from "./components/card"; 23 | import { Switch } from "./components/switch"; 24 | import { Pagination } from "./components/pagination"; 25 | import { Button } from "./components/button"; 26 | import { Heading } from "./components/heading"; 27 | import { FormLabel } from "./components/form-label"; 28 | import { Modal } from "./components/modal"; 29 | import { Alert } from "./components/alert"; 30 | 31 | const overrides = { 32 | direction: "rtl" as ThemeDirection, 33 | config: { 34 | initialColorMode: "light" as ColorMode, 35 | useSystemColorMode: false, 36 | }, 37 | semanticTokens, 38 | styles, 39 | breakpoints, 40 | colors, 41 | fonts, 42 | sizes, 43 | zIndices, 44 | components: { 45 | Alert, 46 | Input, 47 | Textarea, 48 | Checkbox, 49 | Breadcrumb, 50 | Card, 51 | Switch, 52 | Pagination, 53 | Button, 54 | Heading, 55 | FormLabel, 56 | Modal, 57 | Steps, 58 | }, 59 | }; 60 | 61 | // Workaround: https://github.com/chakra-ui/chakra-ui/issues/1424#issuecomment-743342944 62 | Tooltip.defaultProps = { 63 | ...Tooltip.defaultProps, 64 | hasArrow: false, 65 | placement: "top", 66 | }; 67 | 68 | export const theme = extendTheme(overrides, withDefaultColorScheme({ colorScheme: "brand" })); 69 | -------------------------------------------------------------------------------- /src/utils/string.ts: -------------------------------------------------------------------------------- 1 | import { truncate } from "lodash"; 2 | 3 | const ENGLISH_DIGITS = "0123456789"; 4 | const PERSIAN_DIGITS = "\u06f0\u06f1\u06f2\u06f3\u06f4\u06f5\u06f6\u06f7\u06f8\u06f9"; 5 | const ARABIC_DIGITS = "\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669"; 6 | 7 | export function persianDigits(value: string | number): string { 8 | if (typeof value === "number") return value.toLocaleString("fa-IR", { useGrouping: false }); 9 | return value 10 | .replace(new RegExp(`[${ENGLISH_DIGITS}]`, "g"), (d) => PERSIAN_DIGITS[ENGLISH_DIGITS.indexOf(d)]) 11 | .replace(new RegExp(`[${ARABIC_DIGITS}]`, "g"), (d) => PERSIAN_DIGITS[ARABIC_DIGITS.indexOf(d)]); 12 | } 13 | 14 | export function persianGroupedDigits(value: string | number): string { 15 | if (typeof value === "number") return value.toLocaleString("fa-IR", { useGrouping: true }).replace("٬", "٫"); 16 | return value 17 | .replace(new RegExp(`[${ENGLISH_DIGITS}]`, "g"), (d) => PERSIAN_DIGITS[ENGLISH_DIGITS.indexOf(d)]) 18 | .replace(new RegExp(`[${ARABIC_DIGITS}]`, "g"), (d) => PERSIAN_DIGITS[ARABIC_DIGITS.indexOf(d)]); 19 | } 20 | 21 | export function englishDigits(str: string): string { 22 | return str 23 | .replace(new RegExp(`[${PERSIAN_DIGITS}]`, "g"), (d) => ENGLISH_DIGITS[PERSIAN_DIGITS.indexOf(d)]) 24 | .replace(new RegExp(`[${ARABIC_DIGITS}]`, "g"), (d) => ENGLISH_DIGITS[ARABIC_DIGITS.indexOf(d)]); 25 | } 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 28 | export function humanizePersianDigits(str: string | number): string { 29 | throw new Error('Deprecated. Use number.toLocaleString("fa-IR", options) instead.'); 30 | // return persianDigits(str.toLocaleString()); 31 | } 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 34 | export function persianFloat(str: string | number): string { 35 | throw new Error('Deprecated. Use number.toLocaleString("fa-IR", options) instead.'); 36 | // return persianDigits(str).replace(".", "٫"); 37 | } 38 | 39 | export function truncateFilename(filename: string, length: number): string { 40 | const parts = filename.split(/\.(?=[^.]+$)/); 41 | if (parts[0].length <= length) return filename; 42 | parts[0] = truncate(parts[0], { length }); 43 | return parts.join(""); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/FaqAccordion.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { 4 | VStack, 5 | Text, 6 | HStack, 7 | Accordion, 8 | AccordionItem, 9 | AccordionButton, 10 | AccordionPanel, 11 | AccordionIcon, 12 | useColorModeValue, 13 | Divider, 14 | Box, 15 | } from "@chakra-ui/react"; 16 | 17 | import { Card, CardProps } from "./Card"; 18 | 19 | export interface IFaqItem { 20 | id: number | string; 21 | question: string; 22 | answer: string; 23 | } 24 | 25 | export interface FaqAccordionProps extends CardProps { 26 | faqItems: IFaqItem[]; 27 | fluid?: boolean; 28 | htmlSupport?: boolean; 29 | nextHead?: React.ReactNode; 30 | } 31 | 32 | /** 33 | * Hint: Don't hide this component in the view, even with styles. 34 | */ 35 | export const FaqAccordion = ({ 36 | faqItems, 37 | fluid = false, 38 | htmlSupport = false, 39 | nextHead = null, 40 | ...rest 41 | }: FaqAccordionProps) => { 42 | const selectedColor = useColorModeValue("brand.700", "brand.400"); 43 | return ( 44 | <> 45 | {nextHead} 46 | 47 | } {...rest}> 48 | {faqItems.map((faqItem) => ( 49 | 50 |

51 | 52 | 53 | 54 | {faqItem.question} 55 | 56 | 57 | 58 | 59 |

60 | 61 | {!htmlSupport ? ( 62 | {faqItem.answer} 63 | ) : ( 64 | 65 | )} 66 | 67 |
68 | ))} 69 |
70 |
71 | 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/components/FadedHorizontalScrollable.tsx: -------------------------------------------------------------------------------- 1 | import { Box, BoxProps, Flex, IconButton } from "@chakra-ui/react"; 2 | import { mdiChevronLeft, mdiChevronRight } from "@mdi/js"; 3 | import * as React from "react"; 4 | import { useScrollOnDrag } from "../hooks/useScrollOnDrag"; 5 | import { FadedHorizontalOneLine } from "./Faded"; 6 | import { MdIcon } from "./MdIcon"; 7 | 8 | export interface FadedHorizontalScrollableProps extends BoxProps { 9 | children: React.ReactNode; 10 | navigation?: boolean; 11 | } 12 | 13 | export const FadedHorizontalScrollable = ({ children, navigation, ...rest }: FadedHorizontalScrollableProps) => { 14 | const { ref, onMouseDown, scroll } = useScrollOnDrag(); 15 | 16 | return ( 17 | div::-webkit-scrollbar": { 21 | display: "none", 22 | }, 23 | "> div": { 24 | scrollbarWidth: "none", 25 | }, 26 | }} 27 | > 28 | {navigation && ( 29 | 30 | } 36 | pos="absolute" 37 | right={0} 38 | top="50%" 39 | transform="translateY(-50%)" 40 | zIndex="docked" 41 | onClick={() => scroll({ dx: 50, dy: 0, behavior: "smooth" })} 42 | /> 43 | } 49 | pos="absolute" 50 | left={0} 51 | top="50%" 52 | transform="translateY(-50%)" 53 | zIndex="docked" 54 | onClick={() => scroll({ dx: -50, dy: 0, behavior: "smooth" })} 55 | /> 56 | 57 | )} 58 | 67 | {children} 68 | 69 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /src/components/Faded.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { Meta, StoryObj } from "@storybook/react"; 3 | import { Box, Text } from "@chakra-ui/react"; 4 | 5 | import { FadedBox } from "./Faded"; 6 | 7 | const meta: Meta = { 8 | component: FadedBox, 9 | }; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Primary: Story = { 14 | args: { 15 | fadedMaxHeight: 100, 16 | fade: true, 17 | }, 18 | argTypes: { 19 | fadedMaxHeight: { 20 | type: "number", 21 | }, 22 | }, 23 | render: ({ ...args }) => ( 24 | 25 | 26 | 27 | لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون 28 | بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع 29 | با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و 30 | متخصصان را می طلبد، تا با نرم افزارها شناخت بیشتری را برای طراحان رایانه ای علی الخصوص طراحان خلاقی، و فرهنگ 31 | پیشرو در زبان فارسی ایجاد کرد، در این صورت می توان امید داشت که تمام لورم ایپسوم متن ساختگی با تولید سادگی 32 | نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان 33 | که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، 34 | کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را می طلبد، تا با نرم افزارها 35 | شناخت بیشتری را برای طراحان رایانه ای علی الخصوص طراحان خلاقی، و فرهنگ پیشرو در زبان فارسی ایجاد کرد، در این 36 | صورت می توان امید داشت که تماملورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از 37 | طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی 38 | تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد 39 | گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را می طلبد، تا با نرم افزارها شناخت بیشتری را برای طراحان 40 | رایانه ای علی الخصوص طراحان خلاقی، و فرهنگ پیشرو در زبان فارسی ایجاد کرد، در این صورت می توان امید داشت که 41 | تمام 42 | 43 | 44 | 45 | ), 46 | }; 47 | 48 | export default meta; 49 | -------------------------------------------------------------------------------- /src/components/FaqAccordion.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { Card, CardBody } from "@chakra-ui/react"; 4 | import type { Meta, StoryObj } from "@storybook/react"; 5 | import { FaqAccordion, FaqAccordionProps } from "./FaqAccordion"; 6 | 7 | const meta: Meta = { 8 | component: FaqAccordion, 9 | parameters: { 10 | controls: { 11 | exclude: /.*/g, 12 | }, 13 | }, 14 | decorators: [ 15 | (Story) => ( 16 | 17 | 18 | 19 | 20 | 21 | ), 22 | ], 23 | }; 24 | 25 | type Story = StoryObj; 26 | 27 | const SampleFAQItems: FaqAccordionProps["faqItems"] = [ 28 | { 29 | id: 1, 30 | question: "سوال 1", 31 | answer: `لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، 32 | چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، 33 | و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف,`, 34 | }, 35 | { 36 | id: 2, 37 | question: "سوال 2", 38 | answer: `لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، 39 | چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، 40 | و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف,`, 41 | }, 42 | { 43 | id: 3, 44 | question: "سوال 3", 45 | answer: `لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، 46 | چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، 47 | و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف,`, 48 | }, 49 | { 50 | id: 4, 51 | question: "سوال 4", 52 | answer: `لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، 53 | چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، 54 | و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف,`, 55 | }, 56 | { 57 | id: 5, 58 | question: "سوال 5", 59 | answer: `لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، 60 | چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، 61 | و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف,`, 62 | }, 63 | ]; 64 | 65 | export const Primary: Story = { 66 | args: { faqItems: SampleFAQItems, fluid: true, htmlSupport: false }, 67 | }; 68 | 69 | export default meta; 70 | -------------------------------------------------------------------------------- /src/components/SignInModalProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { mdiGithub, mdiLinkedin, mdiGoogle } from "@mdi/js"; 3 | import { 4 | Button, 5 | Modal, 6 | ModalBody, 7 | ModalContent, 8 | ModalOverlay, 9 | useDisclosure, 10 | VStack, 11 | useColorModeValue, 12 | } from "@chakra-ui/react"; 13 | import { MdIcon } from "./MdIcon"; 14 | 15 | type SignInModal = { 16 | open: () => void; 17 | close: () => void; 18 | toggle: () => void; 19 | }; 20 | 21 | export const SignInModalDisclosureContext = React.createContext({ 22 | open: () => {}, 23 | close: () => {}, 24 | toggle: () => {}, 25 | }); 26 | 27 | export const useSignInModal = () => React.useContext(SignInModalDisclosureContext); 28 | 29 | const SocialLoginButton = ({ name, icon, colorScheme }: { name: string; icon: string; colorScheme: string }) => ( 30 |
31 | 40 |
41 | ); 42 | 43 | export const SignInModalProvider = ({ children }: { children: React.ReactNode }) => { 44 | const disclosure = useDisclosure(); 45 | const githubColorScheme = useColorModeValue("blackAlpha", "white"); 46 | 47 | return ( 48 | 55 | {children} 56 | {disclosure.isOpen && ( 57 | 58 | 59 | 60 | 61 |

لطفاً ابتدا وارد حساب کاربری خود شوید.

62 | 63 | 64 | 65 | 66 | 67 | 68 |
69 |
70 |
71 | )} 72 |
73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /src/components/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Input, InputGroup, InputRightElement, Spinner, Card } from "@chakra-ui/react"; 3 | 4 | import { mdiMagnify, mdiClose } from "@mdi/js"; 5 | import { useDebouncedValue } from "../hooks/useDebouncedValue"; 6 | import { MdIcon } from "./MdIcon"; 7 | 8 | interface SearchBarIconProps { 9 | searching: boolean; 10 | canClearSearch: boolean; 11 | handleClearSearch: () => void; 12 | } 13 | 14 | const SearchBarIcon = ({ searching, canClearSearch, handleClearSearch }: SearchBarIconProps) => { 15 | let icon; 16 | if (searching) { 17 | icon = ; 18 | } else if (canClearSearch) { 19 | icon = ( 20 | { 25 | handleClearSearch(); 26 | }} 27 | /> 28 | ); 29 | } else { 30 | icon = ; 31 | } 32 | return {icon}; 33 | }; 34 | 35 | export interface SearchBarProps { 36 | placeholder: string; 37 | handleSearch: (debouncedSearchTerm: string) => void; 38 | isLoading: boolean; 39 | value: string; 40 | } 41 | 42 | export const SearchBar = ({ placeholder, handleSearch, isLoading, value }: SearchBarProps) => { 43 | const [searchTerm, setSearchTerm] = React.useState(value || ""); 44 | const debouncedSearchTerm = useDebouncedValue(searchTerm, 1000); 45 | 46 | React.useEffect(() => { 47 | handleSearch(debouncedSearchTerm); 48 | // eslint-disable-next-line react-hooks/exhaustive-deps 49 | }, [debouncedSearchTerm]); 50 | 51 | const handleClearSearch = () => { 52 | // instantly clear search 53 | setSearchTerm(""); 54 | handleSearch(""); 55 | }; 56 | 57 | const isSearching = searchTerm.length > 0 && isLoading; 58 | 59 | return ( 60 | 61 | 62 | 0} 65 | handleClearSearch={handleClearSearch} 66 | /> 67 | setSearchTerm(e.target.value)} 70 | onKeyDown={(e) => e.key === "Enter" && handleSearch(searchTerm)} 71 | placeholder={placeholder} 72 | fontSize="md" 73 | /> 74 | 75 | 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@querateam/qui", 3 | "version": "0.4.0-beta.0", 4 | "description": "", 5 | "sideEffects": false, 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "files": [ 9 | "/dist" 10 | ], 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 0", 13 | "lint": "eslint .", 14 | "lint:fix": "eslint . --fix", 15 | "build": "tsc --build tsconfig.pkg.json", 16 | "storybook": "storybook dev -p 6006", 17 | "build-storybook": "storybook build -o sb_dist" 18 | }, 19 | "keywords": [ 20 | "chakra-ui", 21 | "theme", 22 | "Quera", 23 | "storybook" 24 | ], 25 | "author": "me", 26 | "license": "ISC", 27 | "peerDependencies": { 28 | "@chakra-ui/icons": ">=2.0.17", 29 | "@chakra-ui/react": ">=2.5.2", 30 | "@chakra-ui/theme-tools": ">=2.0.16", 31 | "@emotion/react": ">=11.10.6", 32 | "@emotion/styled": ">=11.10.6", 33 | "@mdi/js": ">=7.1.96", 34 | "axios": ">=1.3.2", 35 | "chakra-ui-steps": ">=1.7.3", 36 | "framer-motion": ">=6.3.16", 37 | "lodash": ">=4.17.21", 38 | "react": ">=18.1.0", 39 | "react-dom": ">=18.1.0", 40 | "react-hook-form": ">=7.43.8", 41 | "react-schemaorg": ">=2.0.0", 42 | "react-select": ">=5.7.2", 43 | "schema-dts": ">=1.1.2" 44 | }, 45 | "devDependencies": { 46 | "@storybook/addon-actions": "^7.3.2", 47 | "@storybook/addon-essentials": "^7.3.2", 48 | "@storybook/addon-interactions": "^7.3.2", 49 | "@storybook/addon-links": "^7.3.2", 50 | "@storybook/addon-onboarding": "^1.0.8", 51 | "@storybook/blocks": "^7.3.2", 52 | "@storybook/jest": "^0.2.1", 53 | "@storybook/manager-api": "^7.3.2", 54 | "@storybook/react": "^7.3.2", 55 | "@storybook/react-vite": "^7.3.2", 56 | "@storybook/testing-library": "^0.2.0", 57 | "@storybook/theming": "^7.3.2", 58 | "@typescript-eslint/eslint-plugin": "^5.11.0", 59 | "@typescript-eslint/parser": "^5.11.0", 60 | "@vitejs/plugin-react": "^3.1.0", 61 | "eslint": "^8.19.0", 62 | "eslint-config-airbnb": "^19.0.4", 63 | "eslint-config-next": "^12.1.6", 64 | "eslint-config-prettier": "^8.3.0", 65 | "eslint-plugin-import": "^2.25.4", 66 | "eslint-plugin-jsx-a11y": "^6.5.1", 67 | "eslint-plugin-react": "^7.28.0", 68 | "eslint-plugin-react-hooks": "^4.3.0", 69 | "eslint-plugin-storybook": "^0.6.13", 70 | "file-loader": "^6.2.0", 71 | "prettier": "^3.0.1", 72 | "storybook": "^7.3.2", 73 | "typescript": "^4.8.4" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/components/SearchBox.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Button, HStack, Text } from "@chakra-ui/react"; 3 | import { StoryObj, Meta } from "@storybook/react"; 4 | import { ProblemIcon } from "./ProblemIcon"; 5 | import { SearchBox, SearchBoxProps } from "./SearchBox"; 6 | import { persianDigits } from "../utils/string"; 7 | 8 | const meta: Meta = { 9 | component: SearchBox, 10 | parameters: { 11 | controls: { 12 | exclude: /renderResultItem|searchHandler|setValue|value|emptyImage/g, 13 | }, 14 | }, 15 | }; 16 | 17 | const searchResults = { 18 | problems: [ 19 | { 20 | pk: 1, 21 | url: "", 22 | solved_percent: 43, 23 | name: "لورم ایپسوم متن ساختگی با تولید سادگی", 24 | solved_count: 134, 25 | }, 26 | { 27 | pk: 2, 28 | url: "", 29 | solved_percent: 55, 30 | name: "لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از", 31 | solved_count: 321, 32 | }, 33 | { 34 | pk: 3, 35 | url: "", 36 | solved_percent: 100, 37 | name: "لورم ایپسوم متن ساختگی", 38 | solved_count: 423, 39 | }, 40 | { 41 | pk: 4, 42 | url: "", 43 | solved_percent: 12, 44 | name: "لورم ایپسوم متن ساختگی با تولید سادگی", 45 | solved_count: 10, 46 | }, 47 | ], 48 | }; 49 | 50 | const renderResultItem = (result) => ( 51 | 72 | ); 73 | 74 | const CompleteSearchBox = (props: SearchBoxProps) => { 75 | const searchHandler = async () => searchResults.problems; 76 | 77 | const [value, setValue] = React.useState(""); 78 | 79 | return ( 80 | } 88 | {...props} 89 | /> 90 | ); 91 | }; 92 | 93 | export const Primary: StoryObj = { 94 | render: (args) => , 95 | argTypes: { onClearSearch: { action: "cleared" }, onShowAllResults: { action: "show all results" } }, 96 | }; 97 | 98 | export default meta; 99 | -------------------------------------------------------------------------------- /src/hooks/useScrollOnDrag.ts: -------------------------------------------------------------------------------- 1 | // Reference: https://github.com/dotcore64/react-scroll-ondrag 2 | 3 | import * as React from "react"; 4 | 5 | const maxHorizontalScroll = (dom) => dom.scrollWidth - dom.clientWidth; 6 | const maxVerticalScroll = (dom) => dom.scrollHeight - dom.clientHeight; 7 | 8 | /** 9 | Scroll your elements by dragging your mouse. 10 | 11 | Related Tips: 12 | - you can hide the scrollbar but keep it's functionality on your element using this: 13 | https://www.w3schools.com/howto/howto_css_hide_scrollbars.asp 14 | 15 | Usage: 16 | const App = () => { 17 | const { ref, onMouseDown } = useScrollOnDrag(ref); 18 | return
; 19 | 20 | @param options { onDragStart, onDragEnd } 21 | @returns { ref, onMouseDown } 22 | */ 23 | export const useScrollOnDrag = (onDragStart = () => {}, onDragEnd = () => {}) => { 24 | const ref = React.useRef(); 25 | const internalState = React.useRef({ 26 | lastMouseX: null, 27 | lastMouseY: null, 28 | isMouseDown: false, 29 | isScrolling: false, 30 | }); 31 | 32 | const scroll = React.useCallback(({ dx, dy, behavior = undefined }) => { 33 | const offsetX = Math.min(maxHorizontalScroll(ref.current), ref.current.scrollLeft + dx); 34 | const offsetY = Math.min(maxVerticalScroll(ref.current), ref.current.scrollTop + dy); 35 | 36 | ref.current.scroll({ left: offsetX, top: offsetY, behavior }); 37 | }, []); 38 | 39 | const onMouseDown = React.useCallback((e) => { 40 | internalState.current.isMouseDown = true; 41 | internalState.current.lastMouseX = e.clientX; 42 | internalState.current.lastMouseY = e.clientY; 43 | }, []); 44 | 45 | const onMouseUp = () => { 46 | internalState.current.isMouseDown = false; 47 | internalState.current.lastMouseX = null; 48 | internalState.current.lastMouseY = null; 49 | 50 | if (internalState.current.isScrolling) { 51 | internalState.current.isScrolling = false; 52 | ref.current.style.cursor = "grab"; 53 | onDragEnd(); 54 | } 55 | }; 56 | 57 | const onMouseMove = (e) => { 58 | if (!internalState.current.isMouseDown) return; 59 | 60 | if (!internalState.current.isScrolling) { 61 | internalState.current.isScrolling = true; 62 | ref.current.style.cursor = "grabbing"; 63 | onDragStart(); 64 | } 65 | 66 | // diff is negative because we want to scroll in the opposite direction of the movement 67 | const dx = -(e.clientX - internalState.current.lastMouseX); 68 | const dy = -(e.clientY - internalState.current.lastMouseY); 69 | internalState.current.lastMouseX = e.clientX; 70 | internalState.current.lastMouseY = e.clientY; 71 | 72 | scroll({ dx, dy }); 73 | }; 74 | 75 | React.useEffect(() => { 76 | window.addEventListener("mouseup", onMouseUp); 77 | window.addEventListener("mousemove", onMouseMove); 78 | 79 | return () => { 80 | window.removeEventListener("mouseup", onMouseUp); 81 | window.removeEventListener("mousemove", onMouseMove); 82 | }; 83 | // eslint-disable-next-line react-hooks/exhaustive-deps 84 | }, []); 85 | 86 | return { 87 | ref, 88 | onMouseDown, 89 | scroll, 90 | }; 91 | }; 92 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // chakra-ui theme configs 2 | export { theme } from "./theme"; 3 | 4 | // api exports 5 | export { axiosClient } from "./api/rest/axios-client"; 6 | 7 | // components exports 8 | export { AnimateCounter } from "./components/AnimateCounter"; 9 | export { 10 | Card, 11 | CardBody, 12 | CardButton, 13 | CardHeader, 14 | type CardProps, 15 | CardRow, 16 | CollapsibleCard, 17 | FixedBottomCard, 18 | } from "./components/Card"; 19 | 20 | export { Empty, type EmptyResultProps } from "./components/Empty"; 21 | export { FadedBox, FadedHorizontal, FadedHorizontalOneLine, FadedVertical } from "./components/Faded"; 22 | export { FadedHorizontalScrollable, type FadedHorizontalScrollableProps } from "./components/FadedHorizontalScrollable"; 23 | export { FaqAccordion, type FaqAccordionProps, type IFaqItem } from "./components/FaqAccordion"; 24 | export { FileInputField, type FileInputFieldType } from "./components/FileInputField"; 25 | export { GrayTag } from "./components/GrayTag"; 26 | export { 27 | HiringCompanyLogo, 28 | type HiringCompanyLogoBorderType, 29 | type HiringCompanyLogoProps, 30 | } from "./components/HiringCompanyLogo"; 31 | export { MdIcon, type MdIconProps } from "./components/MdIcon"; 32 | export { Pagination, type PaginationProps } from "./components/Pagination"; 33 | export { PinButton, type PinProps } from "./components/PinButton"; 34 | export { IconPinJob, PinJob } from "./components/PinJob"; 35 | export { ProblemIcon } from "./components/ProblemIcon"; 36 | export { QuoteItem, type QuoteItemProps } from "./components/QuoteItem"; 37 | export { RichText } from "./components/RichText"; 38 | export { AnchorLink, ScrollableCore, ScrollableHeader } from "./components/Scrollable"; 39 | export { SearchBar, type SearchBarProps } from "./components/SearchBar"; 40 | export { SearchBox, type SearchBoxProps } from "./components/SearchBox"; 41 | export { Select } from "./components/Select"; 42 | export { ShareLink } from "./components/ShareLink"; 43 | export { SidebarLabel, SidebarRow, SidebarValue } from "./components/Sidebar"; 44 | export { SignInModalDisclosureContext, SignInModalProvider, useSignInModal } from "./components/SignInModalProvider"; 45 | export { SocialNetworkIcon, type SocialNetworkTypes } from "./components/SocialNetworkIcon"; 46 | export { FadedTechnologyLabels, TechnologyLabel } from "./components/TechnologyLabel"; 47 | export { UploadResume, type UploadResumeProps } from "./components/UploadResume"; 48 | export { UserAvatar, type UserAvatarProps } from "./components/UserAvatar"; 49 | export { UserQCVProgress, type UserQCVProgressProps } from "./components/UserQCVProgress"; 50 | export { ReportButton, ReportRecipient, type FormDataType, initialFormState } from "./components/ReportButton"; 51 | 52 | // hooks exports 53 | export { useDebouncedValue } from "./hooks/useDebouncedValue"; 54 | export { useRemoteChoices } from "./hooks/useRemoteChoices"; 55 | export { useScrollOnDrag } from "./hooks/useScrollOnDrag"; 56 | 57 | // utils exports 58 | export { getBrowserCookie, getCookie } from "./utils/cookie"; 59 | export { getPageNumber, getSingleQueryParam } from "./utils/querystring"; 60 | export { 61 | englishDigits, 62 | humanizePersianDigits, 63 | persianDigits, 64 | persianFloat, 65 | persianGroupedDigits, 66 | truncateFilename, 67 | } from "./utils/string"; 68 | 69 | // contexts export 70 | export { ReachedPageBottom } from "./contexts"; 71 | -------------------------------------------------------------------------------- /src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | Box, 4 | BoxProps, 5 | Button, 6 | ButtonProps, 7 | chakra, 8 | Divider, 9 | HeadingProps, 10 | StylesProvider, 11 | useMultiStyleConfig, 12 | useStyles, 13 | forwardRef, 14 | HStack, 15 | Text, 16 | } from "@chakra-ui/react"; 17 | 18 | const ReachedPageBottom = React.createContext(false); 19 | 20 | interface CardHeaderProps extends HeadingProps { 21 | subtitle?: string; 22 | } 23 | 24 | export const CardHeader = forwardRef(({ as = "h2", subtitle, ...rest }, ref) => { 25 | const styles = useStyles(); 26 | const heading = ; 27 | 28 | if (subtitle) 29 | return ( 30 | 31 | {heading} 32 | {subtitle && ( 33 | 34 | {subtitle} 35 | 36 | )} 37 | 38 | ); 39 | 40 | return heading; 41 | }); 42 | 43 | export const CardRow = forwardRef((props, ref) => { 44 | const styles = useStyles(); 45 | return ; 46 | }); 47 | 48 | export const CardBody = forwardRef((props, ref) => { 49 | const styles = useStyles(); 50 | return ; 51 | }); 52 | 53 | export const CardButton = forwardRef((props, ref) => { 54 | const { card: cardStyles, button: buttonStyles } = useStyles(); 55 | buttonStyles.borderBottomRadius = cardStyles.borderRadius; 56 | return ( 57 | <> 58 | 59 | 101 | 102 | ); 103 | }; 104 | -------------------------------------------------------------------------------- /.github/workflows/publish_package.yml: -------------------------------------------------------------------------------- 1 | name: Release package 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | release-type: 6 | description: 'Release type' 7 | required: true 8 | type: choice 9 | options: 10 | - patch 11 | - minor 12 | - major 13 | - prepatch 14 | - preminor 15 | - premajor 16 | - prerelease 17 | jobs: 18 | release: 19 | runs-on: ubuntu-latest 20 | steps: 21 | # Checkout project repository 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | with: 25 | ref: main 26 | ssh-key: ${{secrets.DEPLOY_KEY}} 27 | 28 | # Setup Node.js environment 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v3 31 | with: 32 | registry-url: https://registry.npmjs.org/ 33 | node-version: '18' 34 | 35 | # Install dependencies (required by Run tests step) 36 | - name: Install dependencies 37 | run: npm install 38 | 39 | # Tests 40 | - name: Run tests 41 | run: npm run test 42 | 43 | # Build 44 | - name: Build Package 45 | run: npm run build 46 | 47 | # Configure Git 48 | - name: Git configuration 49 | run: | 50 | git config --global user.email "querateam+github-actions[bot]@users.noreply.github.com" 51 | git config --global user.name "GitHub Actions" 52 | 53 | # Bump package version 54 | # Use tag latest 55 | - name: Bump release version 56 | if: startsWith(github.event.inputs.release-type, 'pre') != true 57 | run: | 58 | echo "NEW_VERSION=$(npm --no-git-tag-version version $RELEASE_TYPE)" >> $GITHUB_ENV 59 | echo "RELEASE_TAG=latest" >> $GITHUB_ENV 60 | env: 61 | RELEASE_TYPE: ${{ github.event.inputs.release-type }} 62 | 63 | # Bump package pre-release version 64 | # Use tag beta for pre-release versions 65 | - name: Bump pre-release version 66 | if: startsWith(github.event.inputs.release-type, 'pre') 67 | run: | 68 | echo "NEW_VERSION=$(npm --no-git-tag-version --preid=beta version $RELEASE_TYPE)" >> $GITHUB_ENV 69 | echo "RELEASE_TAG=beta" >> $GITHUB_ENV 70 | env: 71 | RELEASE_TYPE: ${{ github.event.inputs.release-type }} 72 | # Commit changes 73 | - name: Commit package.json changes and create tag 74 | run: | 75 | git add "package.json" 76 | git commit -m "chore: release ${{ env.NEW_VERSION }}" 77 | git tag ${{ env.NEW_VERSION }} 78 | 79 | # Publish version to public repository 80 | - name: Publish 81 | run: npm publish --verbose --access=public --tag ${{ env.RELEASE_TAG }} 82 | env: 83 | NODE_AUTH_TOKEN: ${{ secrets.NPMJS_ACCESS_TOKEN }} 84 | 85 | # Push repository changes 86 | - name: Push changes to repository 87 | env: 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | run: | 90 | git push origin && git push --tags 91 | 92 | 93 | # Update GitHub release with changelog 94 | - name: Update GitHub release documentation 95 | uses: softprops/action-gh-release@v1 96 | with: 97 | tag_name: ${{ env.NEW_VERSION }} 98 | body: release ${{ env.NEW_VERSION }} 99 | prerelease: ${{ startsWith(github.event.inputs.release-type, 'pre') }} 100 | env: 101 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 102 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | jquery: true, 7 | }, 8 | globals: { 9 | JSX: "readonly", 10 | }, 11 | extends: ["plugin:react/recommended", "airbnb", "airbnb/hooks", "prettier"], 12 | parser: "@typescript-eslint/parser", 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | ecmaVersion: 12, 18 | sourceType: "module", 19 | }, 20 | plugins: ["react", "@typescript-eslint"], 21 | ignorePatterns: ["dist"], 22 | rules: { 23 | camelcase: "off", 24 | "max-len": ["error", { code: 120, ignorePattern: "^import .*" }], 25 | // indent: ["error", 2], --> conflicts with prettier 26 | "lines-between-class-members": "off", 27 | "no-use-before-define": "off", // https://stackoverflow.com/a/64024916 28 | "@typescript-eslint/no-use-before-define": ["error", { functions: false }], 29 | "no-unused-vars": "off", 30 | "class-methods-use-this": "off", 31 | "react/jsx-no-constructed-context-values": "warn", 32 | "@typescript-eslint/no-unused-vars": ["error"], 33 | "no-shadow": "off", // https://stackoverflow.com/a/63961972 34 | "@typescript-eslint/no-shadow": ["error"], 35 | "react/prop-types": "error", // works with typescript too 36 | "react/jsx-props-no-spreading": "off", 37 | "react/require-default-props": "off", 38 | "react/jsx-filename-extension": "off", 39 | "react-hooks/exhaustive-deps": "warn", 40 | "import/no-extraneous-dependencies": ["error", { packageDir: "." }], 41 | "import/prefer-default-export": "off", 42 | "import/no-unresolved": "off", 43 | "import/extensions": "off", 44 | "react/jsx-no-bind": "off", 45 | "react/react-in-jsx-scope": "off", 46 | "react/jsx-no-target-blank": "warn", 47 | "react/jsx-fragments": "off", 48 | "react/function-component-definition": [ 49 | "error", 50 | { 51 | namedComponents: "arrow-function", 52 | unnamedComponents: "arrow-function", 53 | }, 54 | ], 55 | "jsx-a11y/click-events-have-key-events": "off", 56 | "jsx-a11y/no-noninteractive-element-interactions": "warn", 57 | "jsx-a11y/no-static-element-interactions": "warn", 58 | "no-restricted-imports": [ 59 | "error", 60 | { 61 | patterns: [ 62 | { 63 | group: ["react-icons/*", "@react-icons/*", "@mdi/react", "@mdi/svg", "@mdi/font"], 64 | message: "Please use @/common/icons/MdIcon and @mdi/js for icons.", 65 | }, 66 | { 67 | group: ["moment", "luxon", "date-fns", "date-fns-jalali", "@date-io", "jalaali-js", "dayjs", "@js-joda"], 68 | message: "Please use Temporal for working with date and time.", 69 | }, 70 | ], 71 | paths: [ 72 | { 73 | name: "@chakra-ui/react", 74 | importNames: ["Image"], 75 | message: "Due to SSR problems, please use 'Img' component instead.", 76 | }, 77 | { 78 | name: "@/common/utils/string", 79 | importNames: ["humanizePersianDigits", "persianFloat"], 80 | message: 'Deprecated. Use number.toLocaleString("fa-IR", options) instead.', 81 | }, 82 | ], 83 | }, 84 | ], 85 | "jsx-a11y/label-has-associated-control": [ 86 | "error", 87 | { 88 | required: { 89 | some: ["nesting", "id"], 90 | }, 91 | }, 92 | ], 93 | "jsx-a11y/label-has-for": [ 94 | "error", 95 | { 96 | required: { 97 | some: ["nesting", "id"], 98 | }, 99 | }, 100 | ], 101 | }, 102 | }; 103 | -------------------------------------------------------------------------------- /src/components/ReportButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Meta, StoryObj } from "@storybook/react"; 3 | import { Flex, Card } from "@chakra-ui/react"; 4 | import { ReportButton, ReportRecipient, initialFormState } from "./ReportButton"; 5 | import { SignInModalProvider, useSignInModal } from "./SignInModalProvider"; 6 | 7 | const mockReportIssues = [ 8 | { 9 | id: 1, 10 | text: "اطلاعات غیرواقعی", 11 | children: [ 12 | { 13 | id: 8, 14 | text: "سوابق کاری", 15 | children: [], 16 | is_selectable: true, 17 | }, 18 | { 19 | id: 9, 20 | text: "سوابق تحصیلی", 21 | children: [], 22 | is_selectable: true, 23 | }, 24 | { 25 | id: 10, 26 | text: "پروژه‌های انجام شده", 27 | children: [], 28 | is_selectable: true, 29 | }, 30 | { 31 | id: 11, 32 | text: "اطلاعات تماس", 33 | children: [], 34 | is_selectable: true, 35 | }, 36 | { 37 | id: 12, 38 | text: "لینک‌ها", 39 | children: [], 40 | is_selectable: true, 41 | }, 42 | ], 43 | is_selectable: false, 44 | }, 45 | { 46 | id: 2, 47 | text: "اطلاعات‌ تماس", 48 | children: [], 49 | is_selectable: true, 50 | }, 51 | { 52 | id: 3, 53 | text: "محتوای متنی پروفایل", 54 | children: [], 55 | is_selectable: true, 56 | }, 57 | { 58 | id: 4, 59 | text: "محتوای تصویری پروفایل", 60 | children: [], 61 | is_selectable: true, 62 | }, 63 | { 64 | id: 5, 65 | text: "به روز نبودن", 66 | children: [ 67 | { 68 | id: 13, 69 | text: "سوابق کاری", 70 | children: [], 71 | is_selectable: true, 72 | }, 73 | { 74 | id: 14, 75 | text: "سوابق تحصیلی", 76 | children: [], 77 | is_selectable: true, 78 | }, 79 | { 80 | id: 15, 81 | text: "وضعیت اشتغال", 82 | children: [], 83 | is_selectable: true, 84 | }, 85 | ], 86 | is_selectable: false, 87 | }, 88 | { 89 | id: 6, 90 | text: "کلاهبرداری، نقض قانون و یا مالکیت معنوی", 91 | children: [], 92 | is_selectable: true, 93 | }, 94 | ]; 95 | 96 | const meta: Meta = { 97 | component: ReportButton, 98 | parameters: { 99 | controls: { 100 | exclude: /.*/g, 101 | }, 102 | actions: { argTypesRegex: "^on.*" }, 103 | }, 104 | args: { 105 | target: { name: "مشق باقر", recipient_slug: ReportRecipient.Problem, identifier: "12" }, 106 | isReported: false, 107 | isFetching: false, 108 | isSubmitting: false, 109 | onSubmit: () => console.log("submitted"), 110 | onClose: () => null, 111 | onButtonClick: () => null, 112 | isOpen: false, 113 | formState: { ...initialFormState, choices: mockReportIssues }, 114 | setFormState: null, 115 | }, 116 | argTypes: { 117 | onSubmit: { action: "submitted" }, 118 | }, 119 | decorators: [ 120 | (Story) => ( 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | ), 129 | ], 130 | }; 131 | 132 | type Story = StoryObj; 133 | 134 | export const Button: Story = { 135 | args: {}, 136 | }; 137 | 138 | export const LoggedOutButton: Story = { 139 | render: (args) => { 140 | // eslint-disable-next-line react-hooks/rules-of-hooks 141 | const { open } = useSignInModal(); 142 | return ; 143 | }, 144 | }; 145 | 146 | export const FetchingModal: Story = { 147 | args: { 148 | isOpen: true, 149 | isFetching: true, 150 | }, 151 | }; 152 | 153 | export const DoneFetchingModal: Story = { 154 | args: { 155 | isOpen: true, 156 | isFetching: false, 157 | }, 158 | }; 159 | 160 | export const AfterReportButton: Story = { 161 | args: { 162 | isReported: true, 163 | }, 164 | }; 165 | 166 | export default meta; 167 | -------------------------------------------------------------------------------- /src/components/Select.tsx: -------------------------------------------------------------------------------- 1 | import { useColorModeValue } from "@chakra-ui/react"; 2 | import * as React from "react"; 3 | import ReactSelect from "react-select"; 4 | 5 | // TODO: make it a chakra component (should accept chakra's style props, like Input) 6 | export const Select: typeof ReactSelect = (props) => { 7 | const borderColorHover = useColorModeValue("var(--chakra-colors-gray-300)", "var(--chakra-colors-whiteAlpha-400)"); 8 | const iconColor = useColorModeValue("var(--chakra-colors-gray-400)", "var(--chakra-colors-whiteAlpha-500)"); 9 | const iconColorHover = useColorModeValue("var(--chakra-colors-gray-700)", "var(--chakra-colors-whiteAlpha-700)"); 10 | const menuBgColor = useColorModeValue("var(--chakra-colors-gray-100)", "var(--chakra-colors-gray-700)"); 11 | const optionBgColor = useColorModeValue("white", "var(--chakra-colors-gray-700)"); 12 | const optionBgColorFocus = useColorModeValue("var(--chakra-colors-gray-100)", "var(--chakra-colors-whiteAlpha-100)"); 13 | const optionColorSelected = useColorModeValue("var(--chakra-colors-white-700)", "var(--chakra-colors-white-700)"); 14 | const optionColorDisabled = useColorModeValue("var(--chakra-colors-gray-400)", "var(--chakra-colors-gray-500)"); 15 | const getOptionColor = React.useCallback( 16 | (isSelected: boolean, isDisabled: boolean) => { 17 | if (isDisabled) { 18 | return optionColorDisabled; 19 | } 20 | return isSelected ? optionColorSelected : "inherit"; 21 | }, 22 | [optionColorDisabled, optionColorSelected], 23 | ); 24 | 25 | return ( 26 | null, 30 | IndicatorSeparator: () => null, 31 | }} 32 | placeholder="جستجو کنید..." 33 | loadingMessage={() => "در حال جستجو..."} 34 | noOptionsMessage={() => "نتیجه‌ای پیدا نشد"} 35 | // @ts-ignore 36 | theme={(theme) => ({ 37 | ...theme, 38 | borderRadius: "var(--chakra-radii-md)", 39 | colors: { 40 | ...theme.colors, 41 | primary: "var(--chakra-colors-brand-500)", 42 | }, 43 | })} 44 | styles={{ 45 | control: (provided, state) => ({ 46 | ...provided, 47 | minHeight: "initial", 48 | height: "auto", 49 | background: "var(--chakra-colors-input-bg)", 50 | borderColor: state.isFocused ? "var(--chakra-colors-brand-500)" : "var(--chakra-colors-border-gray)", 51 | "&:hover": { 52 | borderColor: state.isFocused ? "var(--chakra-colors-brand-500)" : borderColorHover, 53 | }, 54 | }), 55 | valueContainer: (provided) => ({ 56 | ...provided, 57 | cursor: "text", 58 | padding: "0 var(--chakra-space-4)", 59 | }), 60 | singleValue: (provided) => ({ 61 | ...provided, 62 | color: "inherit", 63 | }), 64 | multiValue: (provided) => ({ 65 | ...provided, 66 | borderRadius: "var(--chakra-radii-sm)", 67 | }), 68 | input: (provided) => ({ 69 | ...provided, 70 | color: "inherit", 71 | }), 72 | menu: (provided) => ({ 73 | ...provided, 74 | background: menuBgColor, 75 | border: "1px solid", 76 | borderColor: "var(--chakra-colors-border-gray)", 77 | }), 78 | option: (provided, { isFocused, isSelected, isDisabled }) => ({ 79 | ...provided, 80 | background: isFocused ? optionBgColorFocus : optionBgColor, 81 | color: getOptionColor(isSelected, isDisabled), 82 | "&:hover": { 83 | background: optionBgColorFocus, 84 | }, 85 | }), 86 | noOptionsMessage: (provided) => ({ 87 | ...provided, 88 | color: "inherit", 89 | }), 90 | clearIndicator: (provided) => ({ ...provided, cursor: "pointer" }), 91 | indicatorsContainer: (provided) => ({ 92 | ...provided, 93 | alignItems: "stretch", 94 | div: { 95 | paddingTop: 0, 96 | paddingBottom: 0, 97 | alignItems: "center", 98 | color: iconColor, 99 | "&:hover": { 100 | color: iconColorHover, 101 | }, 102 | }, 103 | }), 104 | }} 105 | {...props} 106 | /> 107 | ); 108 | }; 109 | -------------------------------------------------------------------------------- /src/components/UploadResume.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | Alert, 4 | AlertIcon, 5 | Box, 6 | BoxProps, 7 | FormControl, 8 | FormErrorMessage, 9 | Radio, 10 | RadioGroup, 11 | Text, 12 | VStack, 13 | } from "@chakra-ui/react"; 14 | import { Control, Controller, FieldValues, Path } from "react-hook-form"; 15 | import { persianDigits } from "../utils/string"; 16 | import { FileInputField, FileInputFieldType } from "./FileInputField"; 17 | import { UserQCVProgress } from "./UserQCVProgress"; 18 | 19 | type UploadResumeType = { 20 | UPLOAD: string; 21 | QCV: string; 22 | }; 23 | 24 | type UploadResumeUserProfileType = { 25 | avatar: string; 26 | fullName: string; 27 | progressPercent: number; 28 | }; 29 | 30 | export interface UploadResumeProps extends BoxProps { 31 | resumeTypes: UploadResumeType; 32 | field: Path; 33 | control: Control; 34 | previousFile: Omit; 35 | userProfile: UploadResumeUserProfileType; 36 | minProgressPercent: number; 37 | isMobile: boolean; 38 | showFileSize: boolean; 39 | } 40 | 41 | export const UploadResume = ({ 42 | field, 43 | control, 44 | previousFile, 45 | userProfile, 46 | minProgressPercent, 47 | resumeTypes, 48 | isMobile, 49 | showFileSize, 50 | ...rest 51 | }: UploadResumeProps) => { 52 | const previousFileExists = previousFile && previousFile.name && previousFile.size; 53 | const [uploadMode, setUploadMode] = React.useState(!previousFileExists); 54 | 55 | return ( 56 | ( 61 | 62 | 63 | 64 | ارسال فایل رزومه 65 | ( 72 | 77 | 87 | {fieldState.error && {fieldState.error.message}} 88 | 89 | )} 90 | /> 91 | 92 | 93 | ارسال رزومه کوئرایی 94 | 99 | 105 | {userProfile.progressPercent < minProgressPercent && ( 106 | 107 | 108 | 109 | حداقل 110 | 111 |  {persianDigits(minProgressPercent)}٪  112 | 113 | رزومه خود را تکمیل کنید. 114 | 115 | 116 | )} 117 | 118 | 119 | 120 | 121 | )} 122 | /> 123 | ); 124 | }; 125 | -------------------------------------------------------------------------------- /static/fonts/iranyekan/css/fontiran.css: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Name: IRANYekan Font 4 | * Version: 3.3 5 | * Author: Moslem Ebrahimi (moslemebrahimi.com) 6 | * Created on: Sep 17, 2020 7 | * Updated on: Sep 17, 2020 8 | * Website: http://fontiran.com 9 | * Copyright: Commercial/Proprietary Software 10 | -------------------------------------------------------------------------------------- 11 | فونت ایران یکان یک نرم افزار مالکیتی محسوب می شود. جهت آگاهی از قوانین استفاده از این فونت ها لطفا به وب سایت (فونت ایران دات کام) مراجعه نمایید 12 | -------------------------------------------------------------------------------------- 13 | IRANYekan fonts are considered a proprietary software. To gain information about the laws regarding the use of these fonts, please visit www.fontiran.com 14 | -------------------------------------------------------------------------------------- 15 | This set of fonts are used in this project under the license: (56AXFY) 16 | -------------------------------------------------------------------------------------- 17 | * 18 | **/ 19 | @font-face { 20 | font-family: IRANYekan; 21 | font-style: normal; 22 | font-weight: bold; 23 | font-display: swap; 24 | src: url('../fonts/eot/IRANYekanWebBold.eot'); 25 | src: url('../fonts/eot/IRANYekanWebBold.eot?#iefix') format('embedded-opentype'), /* IE6-8 */ 26 | url('../fonts/woff/IRANYekanWebBold.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ 27 | url('../fonts/woff2/IRANYekanWebBold.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/ 28 | url('../fonts/ttf/IRANYekanWebBold.ttf') format('truetype'); 29 | } 30 | 31 | @font-face { 32 | font-family: IRANYekan; 33 | font-style: normal; 34 | font-weight: 100; 35 | font-display: swap; 36 | src: url('../fonts/eot/IRANYekanWebThin.eot'); 37 | src: url('../fonts/eot/IRANYekanWebThin.eot?#iefix') format('embedded-opentype'), /* IE6-8 */ 38 | url('../fonts/woff/IRANYekanWebThin.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ 39 | url('../fonts/woff2/IRANYekanWebThin.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/ 40 | url('../fonts/ttf/IRANYekanWebThin.ttf') format('truetype'); 41 | } 42 | 43 | @font-face { 44 | font-family: IRANYekan; 45 | font-style: normal; 46 | font-weight: 300; 47 | font-display: swap; 48 | src: url('../fonts/eot/IRANYekanWebLight.eot'); 49 | src: url('../fonts/eot/IRANYekanWebLight.eot?#iefix') format('embedded-opentype'), /* IE6-8 */ 50 | url('../fonts/woff/IRANYekanWebLight.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ 51 | url('../fonts/woff2/IRANYekanWebLight.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/ 52 | url('../fonts/ttf/IRANYekanWebLight.ttf') format('truetype'); 53 | } 54 | 55 | @font-face { 56 | font-family: IRANYekan; 57 | font-style: normal; 58 | font-weight: normal; 59 | font-display: swap; 60 | src: url('../fonts/eot/IRANYekanWebRegular.eot'); 61 | src: url('../fonts/eot/IRANYekanWebRegular.eot?#iefix') format('embedded-opentype'), /* IE6-8 */ 62 | url('../fonts/woff/IRANYekanWebRegular.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ 63 | url('../fonts/woff2/IRANYekanWebRegular.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/ 64 | url('../fonts/ttf/IRANYekanWebRegular.ttf') format('truetype'); 65 | } 66 | 67 | @font-face { 68 | font-family: IRANYekan; 69 | font-style: normal; 70 | font-weight: 500; 71 | font-display: swap; 72 | src: url('../fonts/eot/IRANYekanWebMedium.eot'); 73 | src: url('../fonts/eot/IRANYekanWebMedium.eot?#iefix') format('embedded-opentype'), /* IE6-8 */ 74 | url('../fonts/woff/IRANYekanWebMedium.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ 75 | url('../fonts/woff2/IRANYekanWebMedium.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/ 76 | url('../fonts/ttf/IRANYekanWebMedium.ttf') format('truetype'); 77 | } 78 | 79 | @font-face { 80 | font-family: IRANYekan; 81 | font-style: normal; 82 | font-weight: 800; 83 | font-display: swap; 84 | src: url('../fonts/eot/IRANYekanWebExtraBold.eot'); 85 | src: url('../fonts/eot/IRANYekanWebExtraBold.eot?#iefix') format('embedded-opentype'), /* IE6-8 */ 86 | url('../fonts/woff/IRANYekanWebExtraBold.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ 87 | url('../fonts/woff2/IRANYekanWebExtraBold.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/ 88 | url('../fonts/ttf/IRANYekanWebExtraBold.ttf') format('truetype'); 89 | } 90 | 91 | @font-face { 92 | font-family: IRANYekan; 93 | font-style: normal; 94 | font-weight: 850; 95 | font-display: swap; 96 | src: url('../fonts/eot/IRANYekanWebBlack.eot'); 97 | src: url('../fonts/eot/IRANYekanWebBlack.eot?#iefix') format('embedded-opentype'), /* IE6-8 */ 98 | url('../fonts/woff/IRANYekanWebBlack.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ 99 | url('../fonts/woff2/IRANYekanWebBlack.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/ 100 | url('../fonts/ttf/IRANYekanWebBlack.ttf') format('truetype'); 101 | } 102 | 103 | @font-face { 104 | font-family: IRANYekan; 105 | font-style: normal; 106 | font-weight: 900; 107 | font-display: swap; 108 | src: url('../fonts/eot/IRANYekanWebExtraBlack.eot'); 109 | src: url('../fonts/eot/IRANYekanWebExtraBlack.eot?#iefix') format('embedded-opentype'), /* IE6-8 */ 110 | url('../fonts/woff/IRANYekanWebExtraBlack.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ 111 | url('../fonts/woff2/IRANYekanWebExtraBlack.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/ 112 | url('../fonts/ttf/IRANYekanWebExtraBlack.ttf') format('truetype'); 113 | } 114 | -------------------------------------------------------------------------------- /src/components/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Button, Flex, Tooltip, useTheme, useStyleConfig, HTMLChakraProps, ButtonProps } from "@chakra-ui/react"; 3 | import { range } from "lodash"; 4 | 5 | import { mdiChevronLeft, mdiChevronRight } from "@mdi/js"; 6 | import { persianDigits } from "../utils/string"; 7 | import { MdIcon } from "./MdIcon"; 8 | import { Card } from "./Card"; 9 | 10 | const calculatePages = ( 11 | page: number, 12 | total: number, 13 | pageSize: number, 14 | siblingRange: number, 15 | beginRange: number, 16 | endRange: number, 17 | ) => { 18 | const pageCount = Math.ceil(total / pageSize); 19 | 20 | const m1 = beginRange; 21 | const m2 = pageCount - endRange + 1; 22 | const p1 = Math.max(page - siblingRange, 1); 23 | const p2 = Math.min(page + siblingRange, pageCount); 24 | 25 | let groups; 26 | if (p1 > m1) { 27 | if (m2 > p2) { 28 | groups = [range(1, m1 + 1), range(p1, p2 + 1), range(m2, pageCount + 1)]; 29 | } else { 30 | groups = [range(1, m1 + 1), range(Math.min(p1, m2), pageCount + 1), []]; 31 | } 32 | } else if (m2 > p2) { 33 | groups = [range(1, Math.max(p2, m1) + 1), range(m2, pageCount + 1), []]; 34 | } else { 35 | groups = [range(1, pageCount + 1), [], []]; 36 | } 37 | 38 | return { pageCount, groups }; 39 | }; 40 | 41 | interface PaginationButtonProps { 42 | isDisabled?: boolean; 43 | onClick?: () => void; 44 | isActive?: boolean; 45 | tooltip?: string; 46 | display?: ButtonProps["display"]; 47 | children: React.ReactNode; 48 | } 49 | 50 | const PaginationButton = ({ isDisabled, onClick, children, isActive, tooltip, display }: PaginationButtonProps) => ( 51 | 52 | 67 | 68 | ); 69 | 70 | interface NumberGroupProps { 71 | page: number; 72 | group: number[]; 73 | renderNumber?: (page: number) => React.ReactNode; 74 | onPageChange: (page: number) => void; 75 | } 76 | 77 | const NumberGroup = ({ page, group, renderNumber, onPageChange }: NumberGroupProps) => ( 78 |
79 | {group.map((i) => ( 80 | onPageChange(i)}> 81 | {renderNumber ? renderNumber(i) : persianDigits(i)} 82 | 83 | ))} 84 |
85 | ); 86 | 87 | interface EllipsisProps { 88 | group1: number[]; 89 | group2: number[]; 90 | } 91 | 92 | const Ellipsis = ({ group1, group2 }: EllipsisProps) => { 93 | if (group1.length === 0) return null; 94 | if (group2.length === 0) return null; 95 | if (group2[0] <= group1[group1.length - 1] + 1) return null; 96 | return ...; 97 | }; 98 | 99 | export interface PaginationProps extends HTMLChakraProps<"div"> { 100 | /** 101 | * Current page number. 102 | */ 103 | page: number; 104 | /** 105 | * Total number of items. 106 | */ 107 | total: number; 108 | /** 109 | * Number of items per page. 110 | */ 111 | pageSize: number; 112 | /** 113 | * Handler for page change. 114 | */ 115 | onPageChange: (page: number) => any; 116 | /** 117 | * Number of always visible pages before and after the current one. 118 | */ 119 | siblingRange?: number; 120 | /** 121 | * Number of always visible pages at the beginning. 122 | */ 123 | beginRange?: number; 124 | /** 125 | * Number of always visible pages at the end. 126 | */ 127 | endRange?: number; 128 | /** 129 | * Optional function applied to page numbers before rendering. 130 | */ 131 | renderNumber?: (page: number) => React.ReactNode; 132 | 133 | isMobile: boolean; 134 | defaultSiblingRange: number; 135 | } 136 | 137 | export const Pagination = ({ 138 | page, 139 | total, 140 | pageSize, 141 | onPageChange, 142 | siblingRange = 1, 143 | beginRange = 1, 144 | endRange = 1, 145 | renderNumber, 146 | isMobile, 147 | defaultSiblingRange, 148 | ...rest 149 | }: PaginationProps) => { 150 | const { direction } = useTheme(); 151 | const styles = useStyleConfig("Pagination", {}); 152 | 153 | const finalSiblingsRange = siblingRange || defaultSiblingRange; 154 | 155 | const { 156 | pageCount, 157 | groups: [group1, group2, group3], 158 | } = React.useMemo( 159 | () => calculatePages(page, total, pageSize, finalSiblingsRange, beginRange, endRange), 160 | [page, total, pageSize, finalSiblingsRange, beginRange, endRange], 161 | ); 162 | 163 | if (pageCount < 2) return null; 164 | 165 | const prevIcon = direction === "rtl" ? mdiChevronRight : mdiChevronLeft; 166 | const nextIcon = direction === "rtl" ? mdiChevronLeft : mdiChevronRight; 167 | 168 | const hasPrev = page > 1; 169 | const hasNext = page < pageCount; 170 | 171 | return ( 172 | 173 | {!isMobile && ( 174 | onPageChange(page - 1)} tooltip="صفحه قبلی"> 175 | 176 | 177 | )} 178 | 179 | 180 | 181 | 182 | 183 | {!isMobile && ( 184 | onPageChange(page + 1)} tooltip="صفحه بعدی"> 185 | 186 | 187 | )} 188 | 189 | ); 190 | }; 191 | -------------------------------------------------------------------------------- /src/components/SearchBox.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Divider, 4 | Input, 5 | InputGroup, 6 | InputRightElement, 7 | Spinner, 8 | VStack, 9 | Text, 10 | useDisclosure, 11 | Card, 12 | } from "@chakra-ui/react"; 13 | import * as React from "react"; 14 | import { mdiMagnify, mdiClose } from "@mdi/js"; 15 | import { useDebouncedValue } from "../hooks/useDebouncedValue"; 16 | import { MdIcon } from "./MdIcon"; 17 | import { Empty } from "./Empty"; 18 | 19 | interface SearchBarIconProps { 20 | searching: boolean; 21 | canClearSearch: boolean; 22 | handleClearSearch: () => void; 23 | } 24 | 25 | const SearchBarIcon = ({ searching, canClearSearch, handleClearSearch }: SearchBarIconProps) => { 26 | let icon; 27 | if (searching) { 28 | icon = ; 29 | } else if (canClearSearch) { 30 | icon = ( 31 | { 36 | handleClearSearch(); 37 | }} 38 | /> 39 | ); 40 | } else { 41 | icon = ; 42 | } 43 | return {icon}; 44 | }; 45 | 46 | export interface SearchBoxProps { 47 | searchHandler: (searchTerm: string) => Promise; 48 | renderResultItem: (any) => React.ReactNode; 49 | emptyMessage: string; 50 | placeholder: string; 51 | value: string; 52 | setValue: React.Dispatch>; 53 | onShowAllResults: () => void; 54 | onClearSearch: () => void; 55 | emptyImage: React.ReactNode; 56 | } 57 | 58 | export const SearchBox = ({ 59 | searchHandler, 60 | renderResultItem, 61 | emptyMessage, 62 | placeholder, 63 | value, 64 | setValue, 65 | onShowAllResults, 66 | onClearSearch, 67 | emptyImage, 68 | }: SearchBoxProps) => { 69 | const [loading, setLoading] = React.useState(false); 70 | const resultsDisclosure = useDisclosure(); 71 | 72 | const debouncedSearchTerm = useDebouncedValue(value, 800); 73 | const [searchResults, setSearchResults] = React.useState(null); 74 | const containerRef = React.useRef(null); 75 | 76 | const setSearchTerm = (term: string) => { 77 | if (term) { 78 | setLoading(true); 79 | resultsDisclosure.onOpen(); 80 | } 81 | setValue(term); 82 | }; 83 | 84 | const onClickOutside = (event: Event) => { 85 | if (containerRef.current && !containerRef.current.contains(event.target)) { 86 | resultsDisclosure.onClose(); 87 | ["click", "keydown"].forEach((eventName) => { 88 | document.removeEventListener(eventName, onClickOutside); 89 | }); 90 | } 91 | }; 92 | 93 | const onFocusInput = () => { 94 | resultsDisclosure.onOpen(); 95 | ["click", "keydown"].forEach((eventName) => { 96 | document.addEventListener(eventName, onClickOutside); 97 | }); 98 | }; 99 | 100 | const handleShowAllResults = () => { 101 | resultsDisclosure.onClose(); 102 | onShowAllResults(); 103 | }; 104 | 105 | const handleClearSearch = () => { 106 | setSearchResults(null); 107 | resultsDisclosure.onClose(); 108 | setValue(""); 109 | if (setValue) { 110 | onClearSearch(); 111 | } 112 | }; 113 | 114 | const isInitialMount = React.useRef(true); 115 | 116 | React.useEffect(() => { 117 | // Don't do anything on initial mount 118 | if (isInitialMount.current) { 119 | isInitialMount.current = false; 120 | return; 121 | } 122 | if (!value && !resultsDisclosure.isOpen) { 123 | // Clear search in case of query param removal 124 | handleClearSearch(); 125 | } 126 | // eslint-disable-next-line react-hooks/exhaustive-deps 127 | }, [value]); 128 | 129 | React.useEffect(() => { 130 | if (debouncedSearchTerm.length === 0) return; 131 | 132 | searchHandler(debouncedSearchTerm).then((d) => { 133 | setSearchResults(d); 134 | setLoading(false); 135 | }); 136 | // eslint-disable-next-line react-hooks/exhaustive-deps 137 | }, [debouncedSearchTerm]); 138 | 139 | const isSearching = value.length > 0 && loading; 140 | const resultsIsOpen = resultsDisclosure.isOpen && debouncedSearchTerm.length > 0 && !loading; 141 | const finalResults = resultsIsOpen && searchResults ? searchResults : []; 142 | 143 | return ( 144 | 145 | 146 | 0} 149 | handleClearSearch={handleClearSearch} 150 | /> 151 | setSearchTerm(e.target.value)} 154 | onKeyDown={(e) => e.key === "Enter" && handleShowAllResults()} 155 | onFocus={onFocusInput} 156 | placeholder={placeholder} 157 | fontSize="md" 158 | borderBottomLeftRadius={resultsIsOpen ? 0 : undefined} 159 | borderBottomRightRadius={resultsIsOpen ? 0 : undefined} 160 | /> 161 | 162 | 171 | {finalResults.length > 0 ? ( 172 | <> 173 | 174 | 175 | {finalResults.map((result) => renderResultItem(result))} 176 | 177 | 178 | {finalResults.length > 0 && ( 179 | 198 | )} 199 | 200 | ) : ( 201 | 202 | )} 203 | 204 | 205 | ); 206 | }; 207 | -------------------------------------------------------------------------------- /src/components/ReportButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | Button, 4 | ButtonProps, 5 | Modal, 6 | ModalOverlay, 7 | ModalContent, 8 | ModalHeader, 9 | ModalFooter, 10 | ModalBody, 11 | ModalCloseButton, 12 | Checkbox, 13 | CheckboxGroup, 14 | Stack, 15 | Textarea, 16 | Box, 17 | Radio, 18 | RadioGroup, 19 | VStack, 20 | FormErrorMessage, 21 | FormControl, 22 | FormLabel, 23 | FormHelperText, 24 | Skeleton, 25 | } from "@chakra-ui/react"; 26 | import { createContext, useContext, Dispatch, SetStateAction } from "react"; 27 | import { mdiFlag } from "@mdi/js"; 28 | import { MdIcon } from "./MdIcon"; 29 | 30 | export type ReportIssuesType = { 31 | id: number; 32 | text: string; 33 | children: ReportIssuesType[]; 34 | is_selectable: boolean; 35 | }; 36 | 37 | export enum ReportRecipient { 38 | User = "user", 39 | Company = "company", 40 | Job = "job", 41 | Problem = "problem", 42 | } 43 | 44 | const RECIPIENT_TO_LABEL = { 45 | [ReportRecipient.User]: "کاربر", 46 | [ReportRecipient.Job]: "فرصت شغلی", 47 | [ReportRecipient.Company]: "شرکت", 48 | [ReportRecipient.Problem]: "سؤال", 49 | }; 50 | 51 | export type FormDataType = { 52 | choices: ReportIssuesType[]; 53 | selectError: string; 54 | checkedIssues: number[]; 55 | checkedRadio: string | null; 56 | description: string; 57 | }; 58 | 59 | export const initialFormState: FormDataType = { 60 | choices: [], 61 | checkedIssues: [], 62 | checkedRadio: null, 63 | description: "", 64 | selectError: null, 65 | }; 66 | 67 | const FormContext = createContext<{ formState: FormDataType; setFormState: Dispatch> }>({ 68 | formState: initialFormState, 69 | setFormState: () => {}, 70 | }); 71 | 72 | const ChoiceItem = ({ choice }: { choice: ReportIssuesType }) => { 73 | const { formState, setFormState } = useContext(FormContext); 74 | const onChange = (e) => { 75 | const targetID = parseInt(e.target.value, 10); 76 | const parentID = formState.choices.filter((c) => c.children.map((child) => child.id).includes(targetID))[0].id; 77 | if (formState.checkedRadio !== parentID.toString()) { 78 | setFormState({ ...formState, checkedRadio: parentID.toString(), checkedIssues: [targetID] }); 79 | return; 80 | } 81 | if (formState.checkedIssues.includes(targetID)) { 82 | setFormState({ 83 | ...formState, 84 | checkedIssues: formState.checkedIssues.filter((id) => id !== targetID), 85 | }); 86 | } else { 87 | setFormState({ 88 | ...formState, 89 | checkedIssues: [...formState.checkedIssues, targetID], 90 | }); 91 | } 92 | }; 93 | return ( 94 | <> 95 | 96 | {choice.text} 97 | 98 | {choice.children.length > 0 && ( 99 | 100 | 101 | {choice.children.map((child) => ( 102 | 103 | {child.text} 104 | 105 | ))} 106 | 107 | 108 | )} 109 | 110 | ); 111 | }; 112 | 113 | const ChoiceList = ({ isLoading }: { isLoading: boolean }) => { 114 | const { formState, setFormState } = useContext(FormContext); 115 | 116 | const onClick = (e) => { 117 | setFormState({ 118 | ...formState, 119 | checkedRadio: e, 120 | checkedIssues: formState.choices.filter((c) => c.id.toString() === e)[0].children.map((c) => c.id), 121 | }); 122 | }; 123 | 124 | return ( 125 | 126 | چه مشکلی وجود دارد؟ 127 | 128 | لطفاً نزدیک‌‌ترین گزینه را انتخاب کنید تا ما هر چه سریع‌تر و دقیق‌تر مشکل را بررسی کنیم. 129 | 130 | 131 | 142 | 143 | 144 | {formState.choices.map((choice) => ( 145 | 146 | ))} 147 | 148 | 149 | 150 | 151 | {formState.selectError} 152 | 153 | ); 154 | }; 155 | 156 | interface ReportButtonProps extends ButtonProps { 157 | target: { 158 | name: string; 159 | recipient_slug: ReportRecipient; 160 | identifier: number | string; 161 | }; 162 | isReported: boolean; 163 | onButtonClick: () => void; 164 | isOpen: boolean; 165 | onClose: () => void; 166 | isSubmitting: boolean; 167 | isFetching: boolean; 168 | formState: FormDataType; 169 | setFormState: Dispatch>; 170 | onSubmit: () => void; 171 | } 172 | 173 | export const ReportButton = ({ 174 | target, 175 | isReported, 176 | onButtonClick, 177 | isOpen, 178 | onClose, 179 | isSubmitting, 180 | isFetching, 181 | formState, 182 | setFormState, 183 | onSubmit, 184 | }: ReportButtonProps) => ( 185 | <> 186 | 199 | 200 | 201 | 202 | {`گزارش اشکال ${ 203 | RECIPIENT_TO_LABEL[target.recipient_slug] 204 | } "${target.name}"`} 205 | 206 | 207 | {/* eslint-disable-next-line react/jsx-no-constructed-context-values */} 208 | 209 | 210 | 211 | 212 | توضیحات بیشتر 213 |