├── src ├── enums │ └── .gitkeep ├── domains │ ├── auth │ │ ├── auth.view.tsx │ │ ├── index.ts │ │ ├── stories │ │ │ └── signup.stories.tsx │ │ └── sign-up.tsx │ ├── home │ │ ├── index.ts │ │ ├── home.domains.tsx │ │ └── type.ts │ ├── users │ │ ├── user.domain.tsx │ │ ├── stories │ │ │ └── UserItem.stories.tsx │ │ ├── userItem.tsx │ │ └── userList.tsx │ ├── index.ts │ └── app │ │ └── app.domains.tsx ├── interfaces │ ├── index.ts │ └── User.interface.ts ├── providers │ ├── index.ts │ └── auth-provider.tsx ├── services │ ├── index.ts │ ├── user.services.ts │ └── api-client.ts ├── constant │ ├── regex-patterns.ts │ └── themes.tsx ├── types │ ├── helper.ts │ ├── global.d.ts │ └── process-env.d.ts ├── components │ ├── index.ts │ ├── ui │ │ ├── toast │ │ │ ├── index.ts │ │ │ ├── toaster.tsx │ │ │ ├── use-toast.tsx │ │ │ └── toast.tsx │ │ ├── form │ │ │ ├── index.ts │ │ │ ├── stories │ │ │ │ ├── checkbox.stories.tsx │ │ │ │ ├── label.stories.tsx │ │ │ │ └── input.stories.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── label.tsx │ │ │ ├── input-controller.tsx │ │ │ ├── checkbox-controller.tsx │ │ │ ├── input.tsx │ │ │ └── form.tsx │ │ ├── icon.tsx │ │ ├── button │ │ │ ├── icon-button.tsx │ │ │ ├── stories │ │ │ │ ├── icon-button.stories.tsx │ │ │ │ └── button.stories.tsx │ │ │ └── button.tsx │ │ ├── badge.tsx │ │ ├── avatar.tsx │ │ ├── alert.tsx │ │ ├── card │ │ │ ├── card.tsx │ │ │ └── Card.stories.tsx │ │ ├── accordion.tsx │ │ ├── breadcrumb.tsx │ │ └── alert-dialog.tsx │ ├── common │ │ ├── code.tsx │ │ ├── list.tsx │ │ ├── portal.tsx │ │ └── query-components.tsx │ └── development.tsx ├── views │ ├── faq.view.tsx │ ├── about.view.tsx │ ├── home.view.tsx │ ├── license.view.tsx │ ├── contribute.view.tsx │ ├── status.view.tsx │ ├── contact.view.tsx │ ├── services.view.tsx │ └── index.ts ├── lib │ ├── date.ts │ ├── type-helper.ts │ ├── utils.ts │ └── validation.ts ├── containers │ ├── index.ts │ ├── ProtectedRoute.tsx │ ├── DahboardLayout.tsx │ ├── layout.tsx │ ├── client-only.tsx │ ├── loading.tsx │ └── error.tsx ├── vite-env.d.ts ├── hooks │ ├── useIsomorphicLayoutEffect.ts │ ├── useUnmount.ts │ ├── useIsMounted.ts │ ├── index.ts │ ├── useData.ts │ ├── useIterator.ts │ ├── useDebounceCallback.ts │ ├── useMediaQuery.ts │ ├── useWindowSize.ts │ └── useEventListener.ts ├── schemas │ └── product.ts ├── store │ └── index.ts ├── main.tsx ├── routes │ └── index.tsx └── styles │ └── App.css ├── docs ├── Patterns.mdx ├── FUNDING.yml ├── CODEOWNERS ├── SECURITY.md ├── StoryRoadmap.mdx ├── SUPPORT.md ├── CODE_OF_CONDUCT.md ├── Introduction.mdx ├── Todo.mdx ├── README.md └── CONTRIBUTING.md ├── .prettierignore ├── .github ├── nodejs.version ├── assets │ ├── project-logo.png │ └── project-logo-vertical copy.png └── workflows │ ├── cr.yml │ ├── vitest.yml │ └── check.yml ├── .env.local ├── .husky ├── pre-commit ├── pre-push └── commit-msg ├── .env ├── .lintstagedrc.json ├── commitlint.config.js ├── chromatic.config.json ├── reset.d.ts ├── .eslintignore ├── postcss.config.js ├── tests ├── mocks │ ├── handlers.ts │ ├── server.ts │ ├── data.ts │ └── db.ts ├── components │ └── main.test.tsx └── setup.ts ├── .prettierrc.cjs ├── public ├── assets │ ├── OIG2.2goIdbKloQEs0xtB2RE4.jpg │ ├── OIG2.H7B7oyw.XKsTlgK_HDjN.jpg │ ├── OIG2.HRGAY847i07g.oxSEsnE.jpg │ ├── OIG2.XmgK4KNrPDYZcIv0_ed7.jpg │ ├── OIG2.ga9YmsAYmdotDENs7mTY.jpg │ ├── OIG2.qyqXO2kYvThCqcyRVxbJ.jpg │ ├── OIG2.uHdAnObqGHuRdTSjsaYE.jpg │ └── OIG2.ucWUO4j4NbxU_rS1zpEF.jpg └── logo.svg ├── tsconfig.node.json ├── vitest.config.ts ├── components.json ├── index.html ├── .gitignore ├── vite.config.ts ├── tsconfig.json ├── .storybook ├── main.ts └── preview.ts ├── LICENSE.md ├── tailwind.config.ts ├── .eslintrc.cjs └── package.json /src/enums/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/Patterns.mdx: -------------------------------------------------------------------------------- 1 | #HOC -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.github/nodejs.version: -------------------------------------------------------------------------------- 1 | v21.6.1 -------------------------------------------------------------------------------- /.env.local: -------------------------------------------------------------------------------- 1 | VITE_APP_TITLE=applebeuoynd -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpx lint-staged 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | pnpm vitest run 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | pnpm commitlint ${1} 2 | -------------------------------------------------------------------------------- /src/domains/auth/auth.view.tsx: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_APP_TITLE=react-launchpad 2 | VITE_APP_PROT=4000 3 | -------------------------------------------------------------------------------- /src/domains/auth/index.ts: -------------------------------------------------------------------------------- 1 | export { SignUp } from './sign-up'; 2 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export type { User } from './User.interface'; 2 | -------------------------------------------------------------------------------- /src/domains/home/index.ts: -------------------------------------------------------------------------------- 1 | export { HomeDomains } from './home.domains'; 2 | -------------------------------------------------------------------------------- /src/providers/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthProvider } from './auth-provider'; 2 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.+(js|ts)": ["prettier --write", "eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | export { default as userClient } from './user.services'; 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | export default { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /chromatic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectId": "Project:660c05957a86fdc97edfb8cf", 3 | "zip": true 4 | } 5 | -------------------------------------------------------------------------------- /reset.d.ts: -------------------------------------------------------------------------------- 1 | // Do not add any other lines of code to this file! 2 | import '@total-typescript/ts-reset'; 3 | -------------------------------------------------------------------------------- /src/domains/home/home.domains.tsx: -------------------------------------------------------------------------------- 1 | export function HomeDomains() { 2 | return <>Home Domains; 3 | } 4 | -------------------------------------------------------------------------------- /src/domains/users/user.domain.tsx: -------------------------------------------------------------------------------- 1 | export function UserDomain() { 2 | return
UserDomain
; 3 | } 4 | -------------------------------------------------------------------------------- /src/constant/regex-patterns.ts: -------------------------------------------------------------------------------- 1 | export const RegexPatterns = { 2 | Email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, 3 | }; 4 | -------------------------------------------------------------------------------- /src/types/helper.ts: -------------------------------------------------------------------------------- 1 | export type OverrideProps = Omit & 2 | TOverridden; 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | vitest.config.ts 4 | tailwind.config.ts 5 | reset.d.ts 6 | 7 | commitlint.config.js -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Button } from './ui/button/button'; 2 | export { Development } from './development'; 3 | -------------------------------------------------------------------------------- /tests/mocks/handlers.ts: -------------------------------------------------------------------------------- 1 | import { db } from './db'; 2 | 3 | export const handlers = [...db.product.toHandlers('rest')]; 4 | -------------------------------------------------------------------------------- /.github/assets/project-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alireza-akbarzadeh/react-launchpad/HEAD/.github/assets/project-logo.png -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | // updated window types 2 | declare global { 3 | interface Window { 4 | test: string; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | }; 7 | -------------------------------------------------------------------------------- /public/assets/OIG2.2goIdbKloQEs0xtB2RE4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alireza-akbarzadeh/react-launchpad/HEAD/public/assets/OIG2.2goIdbKloQEs0xtB2RE4.jpg -------------------------------------------------------------------------------- /public/assets/OIG2.H7B7oyw.XKsTlgK_HDjN.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alireza-akbarzadeh/react-launchpad/HEAD/public/assets/OIG2.H7B7oyw.XKsTlgK_HDjN.jpg -------------------------------------------------------------------------------- /public/assets/OIG2.HRGAY847i07g.oxSEsnE.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alireza-akbarzadeh/react-launchpad/HEAD/public/assets/OIG2.HRGAY847i07g.oxSEsnE.jpg -------------------------------------------------------------------------------- /public/assets/OIG2.XmgK4KNrPDYZcIv0_ed7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alireza-akbarzadeh/react-launchpad/HEAD/public/assets/OIG2.XmgK4KNrPDYZcIv0_ed7.jpg -------------------------------------------------------------------------------- /public/assets/OIG2.ga9YmsAYmdotDENs7mTY.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alireza-akbarzadeh/react-launchpad/HEAD/public/assets/OIG2.ga9YmsAYmdotDENs7mTY.jpg -------------------------------------------------------------------------------- /public/assets/OIG2.qyqXO2kYvThCqcyRVxbJ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alireza-akbarzadeh/react-launchpad/HEAD/public/assets/OIG2.qyqXO2kYvThCqcyRVxbJ.jpg -------------------------------------------------------------------------------- /public/assets/OIG2.uHdAnObqGHuRdTSjsaYE.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alireza-akbarzadeh/react-launchpad/HEAD/public/assets/OIG2.uHdAnObqGHuRdTSjsaYE.jpg -------------------------------------------------------------------------------- /public/assets/OIG2.ucWUO4j4NbxU_rS1zpEF.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alireza-akbarzadeh/react-launchpad/HEAD/public/assets/OIG2.ucWUO4j4NbxU_rS1zpEF.jpg -------------------------------------------------------------------------------- /src/domains/index.ts: -------------------------------------------------------------------------------- 1 | export { HomeDomains } from './home'; 2 | export { default as App } from './app/app.domains'; 3 | export { SignUp } from './auth'; 4 | -------------------------------------------------------------------------------- /.github/assets/project-logo-vertical copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alireza-akbarzadeh/react-launchpad/HEAD/.github/assets/project-logo-vertical copy.png -------------------------------------------------------------------------------- /src/services/user.services.ts: -------------------------------------------------------------------------------- 1 | import { User } from 'interfaces'; 2 | import APICLient from './api-client'; 3 | 4 | export default new APICLient('/users'); 5 | -------------------------------------------------------------------------------- /src/views/faq.view.tsx: -------------------------------------------------------------------------------- 1 | import { Development } from 'components'; 2 | 3 | function FaqView() { 4 | return ; 5 | } 6 | 7 | export default FaqView; 8 | -------------------------------------------------------------------------------- /tests/mocks/server.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | import { handlers } from './handlers'; 3 | 4 | export const server = setupServer(...handlers); 5 | -------------------------------------------------------------------------------- /src/views/about.view.tsx: -------------------------------------------------------------------------------- 1 | import { Development } from 'components'; 2 | 3 | function AboutView() { 4 | return ; 5 | } 6 | 7 | export default AboutView; 8 | -------------------------------------------------------------------------------- /src/views/home.view.tsx: -------------------------------------------------------------------------------- 1 | import { Development } from 'components'; 2 | 3 | function HomeView() { 4 | return ; 5 | } 6 | 7 | export default HomeView; 8 | -------------------------------------------------------------------------------- /src/views/license.view.tsx: -------------------------------------------------------------------------------- 1 | import { Development } from 'components'; 2 | 3 | export default function Licenseview(): JSX.Element { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/views/contribute.view.tsx: -------------------------------------------------------------------------------- 1 | import { Development } from 'components'; 2 | 3 | export default function Contributeview(): JSX.Element { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/views/status.view.tsx: -------------------------------------------------------------------------------- 1 | import { Development } from 'components'; 2 | 3 | function StatusView() { 4 | return ; 5 | } 6 | 7 | export default StatusView; 8 | -------------------------------------------------------------------------------- /src/views/contact.view.tsx: -------------------------------------------------------------------------------- 1 | import { Development } from 'components'; 2 | 3 | function ContactView() { 4 | return ; 5 | } 6 | 7 | export default ContactView; 8 | -------------------------------------------------------------------------------- /src/views/services.view.tsx: -------------------------------------------------------------------------------- 1 | import { Development } from 'components'; 2 | 3 | function ServicesView() { 4 | return ; 5 | } 6 | 7 | export default ServicesView; 8 | -------------------------------------------------------------------------------- /src/lib/date.ts: -------------------------------------------------------------------------------- 1 | import { format, isValid } from 'date-fns'; 2 | 3 | export function formatDate(date: number | Date) { 4 | return isValid(date) ? format(date, 'do MMMM yyyy') : 'N/A'; 5 | } 6 | -------------------------------------------------------------------------------- /src/containers/index.ts: -------------------------------------------------------------------------------- 1 | export { RootLayout } from './layout'; 2 | export { ErrorBoundray } from './error'; 3 | export { DashboardLayout } from './DahboardLayout'; 4 | export { Loading } from './loading'; 5 | -------------------------------------------------------------------------------- /tests/components/main.test.tsx: -------------------------------------------------------------------------------- 1 | describe('isRouteErrorResponse', () => { 2 | it('signal error response', () => { 3 | const valuse = { name: 'mosh' }; 4 | expect(valuse).toEqual({ name: 'mosh' }); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_APP_TITLE: string; 5 | // more env variables... 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv; 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/type-helper.ts: -------------------------------------------------------------------------------- 1 | declare const brand: unique symbol; 2 | 3 | export type Brand = T & { [brand]: TBrand }; 4 | 5 | export type Expect = T; 6 | export type IsTrue = T; 7 | export type IsFalse = T; 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/hooks/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from 'react'; 2 | 3 | // "In the browser, I'm an `useLayoutEffect`, but in SSR, I'm an `useEffect`.", 4 | 5 | export const useIsomorphicLayoutEffect = 6 | typeof window !== 'undefined' ? useLayoutEffect : useEffect; 7 | -------------------------------------------------------------------------------- /src/types/process-env.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | [key: string]: string | undefined; 5 | PORT: string; 6 | DATABASE_URL: string; 7 | // add more environment variables and their types here 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/mocks/data.ts: -------------------------------------------------------------------------------- 1 | export const products = [ 2 | { 3 | id: 1, 4 | name: 'Product 1', 5 | price: 10, 6 | }, 7 | { 8 | id: 2, 9 | name: 'Product 2', 10 | price: 20, 11 | }, 12 | { 13 | id: 3, 14 | name: 'Product 3', 15 | price: 30, 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /src/hooks/useUnmount.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export function useUnmount(func: () => void) { 4 | const funcRef = useRef(func); 5 | funcRef.current = func; 6 | useEffect( 7 | () => () => { 8 | funcRef.current(); 9 | }, 10 | [] 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/components/ui/toast/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Toast, 3 | ToastAction, 4 | ToastClose, 5 | ToastDescription, 6 | ToastProvider, 7 | ToastTitle, 8 | ToastViewport, 9 | type ToastActionElement, 10 | type ToastProps, 11 | } from './toast'; 12 | 13 | export { toast, useToast } from './use-toast'; 14 | -------------------------------------------------------------------------------- /docs/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # Funding 2 | # https://help.github.com/en/github/administering-a-repository/displaying-a-sponsor-button-in-your-repository 3 | 4 | github: alireza-akbarzadeh 5 | liberapay: alireza-akbarzdeh 6 | # remember to create an funding account 7 | # custom: ["paypal.me/jessesquires", "cash.app/$jsq", "venmo.com/u/jsq1312"] 8 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, mergeConfig } from 'vitest/config'; 2 | import viteConfig from './vite.config.ts'; 3 | 4 | export default mergeConfig( 5 | viteConfig, 6 | defineConfig({ 7 | test: { 8 | environment: 'jsdom', 9 | globals: true, 10 | setupFiles: 'tests/setup.ts', 11 | }, 12 | }) 13 | ); 14 | -------------------------------------------------------------------------------- /src/components/ui/form/index.ts: -------------------------------------------------------------------------------- 1 | export { InputController } from './input-controller'; 2 | export {} from './checkbox'; 3 | export { type InputProps, Input } from './input'; 4 | export { 5 | Form, 6 | FormControl, 7 | FormDescription, 8 | FormField, 9 | FormItem, 10 | FormLabel, 11 | FormMessage, 12 | useFormField, 13 | } from './form'; 14 | -------------------------------------------------------------------------------- /src/containers/ProtectedRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet } from 'react-router-dom'; 2 | 3 | type TProtectedRoute = { 4 | isPublic: boolean; 5 | isAuthorized: boolean; 6 | }; 7 | 8 | export function ProtectedRoute({ isPublic, isAuthorized }: TProtectedRoute) { 9 | return isPublic || isAuthorized ? : ; 10 | } 11 | -------------------------------------------------------------------------------- /tests/mocks/db.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/unbound-method */ 2 | import { faker } from '@faker-js/faker'; 3 | import { factory, primaryKey } from '@mswjs/data'; 4 | 5 | export const db = factory({ 6 | product: { 7 | id: primaryKey(faker.number.int), 8 | name: faker.commerce.productName, 9 | price: () => faker.number.int({ min: 1, max: 100 }), 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/hooks/useIsMounted.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | 3 | export function useIsMounted(): () => boolean { 4 | const isMounted = useRef(false); 5 | 6 | useEffect(() => { 7 | isMounted.current = true; 8 | 9 | return () => { 10 | isMounted.current = false; 11 | }; 12 | }, []); 13 | return useCallback(() => isMounted.current, []); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/common/code.tsx: -------------------------------------------------------------------------------- 1 | export function Code(props: { value: TCode; space?: number }) { 2 | const { value, space } = props; 3 | return ( 4 |
 5 |       {JSON.stringify(value, null, space)}
 6 |     
7 | ); 8 | } 9 | 10 | Code.defaultProps = { 11 | space: 4, 12 | }; 13 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; 2 | export { useMediaQuery } from './useMediaQuery'; 3 | export { useEventListener } from './useEventListener'; 4 | export { useIsMounted } from './useIsMounted'; 5 | export { useWindowSize } from './useWindowSize'; 6 | export { useUnmount } from './useUnmount'; 7 | export { useDebounceCallback } from './useDebounceCallback'; 8 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React launchpad 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/constant/themes.tsx: -------------------------------------------------------------------------------- 1 | const VARIANT = { 2 | primary: 'primary', 3 | destructive: 'destructive', 4 | outline: 'outline', 5 | secondary: 'secondary', 6 | link: 'link', 7 | info: 'info', 8 | ghost: 'ghost', 9 | }; 10 | 11 | const SIZES = { 12 | default: 'default', 13 | icon: 'icon', 14 | xs: 'xs', 15 | sm: 'sm', 16 | md: 'md', 17 | lg: 'lg', 18 | xl: 'lg', 19 | }; 20 | 21 | export { VARIANT, SIZES }; 22 | -------------------------------------------------------------------------------- /.github/workflows/cr.yml: -------------------------------------------------------------------------------- 1 | name: Code Review 2 | permissions: 3 | contents: read 4 | pull-requests: write 5 | on: 6 | pull_request: 7 | types: 8 | - opened 9 | - reopened 10 | - synchronize 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: anc95/ChatGPT-CodeReview@main 16 | env: 17 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 18 | OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}" 19 | -------------------------------------------------------------------------------- /src/views/index.ts: -------------------------------------------------------------------------------- 1 | export { default as HomeView } from './home.view'; 2 | export { default as IphoneView } from './faq.view'; 3 | export { default as AboutView } from './about.view'; 4 | export { default as IpadView } from './contact.view'; 5 | export { default as ServicesView } from './services.view'; 6 | export { default as LicenseView } from './license.view'; 7 | export { default as StatusView } from './status.view'; 8 | export { default as ContributeView } from './contribute.view'; 9 | -------------------------------------------------------------------------------- /src/interfaces/User.interface.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | name: string; 4 | username: string; 5 | email: string; 6 | address: { 7 | street: string; 8 | suite: string; 9 | city: string; 10 | zipcode: string; 11 | geo: { 12 | lat: string; 13 | lng: string; 14 | }; 15 | }; 16 | phone: string; 17 | website: string; 18 | company: { 19 | name: string; 20 | catchPhrase: string; 21 | bs: string; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/containers/DahboardLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import { Loading } from './loading'; 3 | import { ProtectedRoute } from './ProtectedRoute'; 4 | 5 | export function DashboardLayout() { 6 | return ( 7 | 11 | } 12 | > 13 |
14 | 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | # FIXME: checkout 14 | # *.local 15 | 16 | coverage/ 17 | 18 | # Editor directories and files 19 | .vscode/* 20 | !.vscode/extensions.json 21 | .idea 22 | .DS_Store 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | 29 | # Sentry Config File 30 | .env.sentry-build-plugin 31 | 32 | *storybook.log 33 | /.npm-only-allow -------------------------------------------------------------------------------- /docs/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://github.dev/alireza-akbarzadeh/react-launchpad 2 | 3 | # These owners will be the default owners for everything in 4 | # the repo. Unless a later match takes precedence, they will 5 | # be requested for review when someone opens a PR. 6 | * @alireza-akbarzadeh 7 | 8 | # Order is important; the last matching pattern takes the most 9 | # precedence. When someone opens a PR that only modifies 10 | # .yml files, only the following people and NOT the global 11 | # owner(s) will be requested for a review. -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sentryVitePlugin } from '@sentry/vite-plugin'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | import { defineConfig } from 'vite'; 4 | import tsconfigPaths from 'vite-tsconfig-paths'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | react(), 10 | tsconfigPaths(), 11 | sentryVitePlugin({ 12 | org: 'devtools-pk', 13 | project: 'apple-beyound', 14 | }), 15 | ], 16 | 17 | build: { 18 | sourcemap: true, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/schemas/product.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | 3 | export const productFormSchema = z.object({ 4 | id: z.number().optional(), 5 | name: z.string().min(1, 'Name is required').max(255), 6 | price: z.coerce 7 | .number({ 8 | required_error: 'Price is required', 9 | invalid_type_error: 'Price is required', 10 | }) 11 | .min(1) 12 | .max(1000), 13 | categoryId: z.number({ 14 | required_error: 'Category is required', 15 | }), 16 | }); 17 | 18 | export type ProductFormData = z.infer; 19 | -------------------------------------------------------------------------------- /src/containers/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import { Loading } from './loading'; 3 | import { ProtectedRoute } from './ProtectedRoute'; 4 | 5 | export function RootLayout() { 6 | return ( 7 | 11 | } 12 | > 13 |
14 | 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/useData.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export type Result = 4 | | ['loading', undefined?] 5 | | ['success', T] 6 | | ['error', Error]; 7 | 8 | export const useData = (url: string): Result => { 9 | const [result, setResult] = useState>(['loading']); 10 | 11 | useEffect(() => { 12 | fetch(url) 13 | .then((response) => response.json()) 14 | .then((data) => setResult(['success', data])) 15 | .catch((error) => setResult(['error', error])); 16 | }, [url]); 17 | 18 | return result; 19 | }; 20 | -------------------------------------------------------------------------------- /docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you discover a security issue, please bring it to our attention right away! 4 | 5 | ## Reporting a Vulnerability 6 | 7 | Please **DO NOT** file a public issue to report a security vulberability, instead send your report privately to **oss@jessesquires.com**. This will help ensure that any vulnerabilities that are found can be [disclosed responsibly](https://en.wikipedia.org/wiki/Responsible_disclosure) to any affected parties. 8 | 9 | ## Supported Versions 10 | 11 | Project versions that are currently being supported with security updates vary per project. 12 | Please see specific project repositories for details. 13 | If nothing is specified, only the latest major versions are supported. -------------------------------------------------------------------------------- /src/containers/client-only.tsx: -------------------------------------------------------------------------------- 1 | import { useIsMounted } from 'hooks'; 2 | import { PropsWithChildren } from 'react'; 3 | 4 | function ClientOnly(props: PropsWithChildren) { 5 | const { children, ...rest } = props; 6 | const isMounted = useIsMounted(); 7 | if (!isMounted) return
; 8 | return children; 9 | } 10 | export default ClientOnly; 11 | 12 | // ? another version 13 | // export default function ClientOnly(props: PropsWithChildren) { 14 | // const { children, ...rest } = props 15 | // const [hasMounted, setHasMounted] = useState(false) 16 | // useEffect(() => { 17 | // setHasMounted(true) 18 | // }, []) 19 | // if (!hasMounted) return
20 | // return <>{props.children} 21 | // } 22 | -------------------------------------------------------------------------------- /src/components/development.tsx: -------------------------------------------------------------------------------- 1 | export function Development(): JSX.Element { 2 | return ( 3 |
4 |
5 |

Contribute View

6 |

7 | This page is currently under development. Stay tuned! 8 |

9 |
10 | Under Construction 15 |
16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/containers/loading.tsx: -------------------------------------------------------------------------------- 1 | export function Loading() { 2 | return ( 3 | <> 4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/ui/toast/toaster.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | useToast, 11 | } from 'components/ui/toast'; 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast(); 15 | 16 | return ( 17 | 18 | {toasts.map(({ id, title, description, action, ...props }) => ( 19 | 20 |
21 | {title && {title}} 22 | {description && {description}} 23 |
24 | {action} 25 | 26 |
27 | ))} 28 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/domains/users/stories/UserItem.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import fetch from 'node-fetch'; 4 | 5 | import { UserITem } from '../userItem'; 6 | 7 | /* 8 | *👇 Render functions are a framework specific feature to allow you control on how the component renders. 9 | * See https://storybook.js.org/docs/api/csf 10 | * to learn how to use render functions. 11 | */ 12 | const meta: Meta = { 13 | component: UserITem, 14 | render: (args, { loaded: { user } }) => , 15 | }; 16 | 17 | export default meta; 18 | type Story = StoryObj; 19 | 20 | export const Primary: Story = { 21 | loaders: [ 22 | async () => ({ 23 | todo: await ( 24 | await fetch('https://jsonplaceholder.typicode.com/todos/1') 25 | ).json(), 26 | }), 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "paths": { 5 | // //add paths line 6 | "@/*": ["src/*"] 7 | }, 8 | "target": "ES2020", 9 | "useDefineForClassFields": true, 10 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 11 | "module": "ESNext", 12 | "skipLibCheck": true, 13 | "types": ["vitest/globals"], 14 | 15 | /* Bundler mode */ 16 | "moduleResolution": "bundler", 17 | "allowImportingTsExtensions": true, 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | 23 | /* Linting */ 24 | "strict": true, 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": false, 27 | "noFallthroughCasesInSwitch": true 28 | }, 29 | "include": ["src", "tests", "src/environment.d.ts"], 30 | "references": [{ "path": "./tsconfig.node.json" }] 31 | } 32 | 33 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite'; 2 | 3 | const config: StorybookConfig = { 4 | stories: [ 5 | '../src/**/*.mdx', 6 | '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)', 7 | '../docs/**/*.mdx', 8 | ], 9 | addons: [ 10 | '@storybook/addon-onboarding', 11 | '@storybook/addon-links', 12 | '@storybook/addon-essentials', 13 | '@chromatic-com/storybook', 14 | '@storybook/addon-interactions', 15 | ], 16 | framework: { 17 | name: '@storybook/react-vite', 18 | options: {}, 19 | }, 20 | docs: { 21 | autodocs: 'tag', 22 | }, 23 | typescript: { 24 | check: false, 25 | reactDocgen: false, 26 | reactDocgenTypescriptOptions: { 27 | shouldExtractLiteralValuesFromEnum: true, 28 | propFilter: (prop) => 29 | prop.parent ? !/node_modules/.test(prop.parent.fileName) : true, 30 | }, 31 | }, 32 | }; 33 | export default config; 34 | -------------------------------------------------------------------------------- /src/components/ui/form/stories/checkbox.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useArgs } from '@storybook/preview-api'; 2 | import { Meta, StoryObj } from '@storybook/react'; 3 | import { Checkbox } from '../checkbox'; 4 | 5 | const meta: Meta = { 6 | title: 'Forms/Checkbox', 7 | component: Checkbox, 8 | }; 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Example: Story = { 14 | args: { 15 | checked: true, 16 | }, 17 | /** 18 | * 👇 To avoid linting issues, it is recommended to use a function with a capitalized name. 19 | * If you are not concerned with linting, you may use an arrow function. 20 | */ 21 | render: function Render(args) { 22 | const [{ checked }, updateArgs] = useArgs(); 23 | 24 | const onChange = () => { 25 | updateArgs({ checked: !checked }); 26 | }; 27 | 28 | return ; 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/domains/app/app.domains.tsx: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/react'; 2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 3 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 4 | import { Toaster } from 'components/ui/toast/toaster'; 5 | import { AuthProvider } from 'providers'; 6 | import React from 'react'; 7 | import { RouterProvider } from 'react-router-dom'; 8 | import { router } from 'routes'; 9 | import 'styles/App.css'; 10 | 11 | export const queryClient = new QueryClient(); 12 | 13 | function App() { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | export default Sentry.withProfiler(App); 28 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest'; 2 | import ResizeObserver from 'resize-observer-polyfill'; 3 | import { server } from './mocks/server'; 4 | 5 | beforeAll(() => server.listen()); 6 | afterEach(() => server.resetHandlers()); 7 | afterAll(() => server.close()); 8 | 9 | global.ResizeObserver = ResizeObserver; 10 | 11 | window.HTMLElement.prototype.scrollIntoView = vi.fn(); 12 | window.HTMLElement.prototype.hasPointerCapture = vi.fn(); 13 | window.HTMLElement.prototype.releasePointerCapture = vi.fn(); 14 | 15 | Object.defineProperty(window, 'matchMedia', { 16 | writable: true, 17 | value: vi.fn().mockImplementation((query) => ({ 18 | matches: false, 19 | media: query as unknown, 20 | onchange: null, 21 | addListener: vi.fn(), // deprecated 22 | removeListener: vi.fn(), // deprecated 23 | addEventListener: vi.fn(), 24 | removeEventListener: vi.fn(), 25 | dispatchEvent: vi.fn(), 26 | })), 27 | }); 28 | -------------------------------------------------------------------------------- /src/services/api-client.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from 'axios'; 2 | 3 | export interface FetchResponse { 4 | data: T[]; 5 | } 6 | 7 | const axiosInstance = axios.create({ 8 | baseURL: 'https://jsonplaceholder.typicode.com', 9 | // params: { 10 | // key, 11 | // }, 12 | }); 13 | 14 | class APICLient { 15 | endpoint: string; 16 | 17 | constructor(endpoint: string) { 18 | this.endpoint = endpoint; 19 | } 20 | 21 | getAll = (config: AxiosRequestConfig) => 22 | axiosInstance 23 | .get>(this.endpoint, config) 24 | .then((res) => res.data); 25 | 26 | get = (id: number | string) => 27 | axiosInstance.get(`${this.endpoint}/${id}`).then((res) => res.data); 28 | 29 | // eslint-disable-next-line class-methods-use-this 30 | post = (data: T) => 31 | axiosInstance 32 | .post>('/genres', data) 33 | .then((res) => res.data); 34 | } 35 | 36 | export default APICLient; 37 | -------------------------------------------------------------------------------- /src/hooks/useIterator.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useState } from 'react'; 2 | 3 | type UseIterator = { 4 | initialIndex?: number; 5 | items: TIndex[]; 6 | }; 7 | 8 | type UseIteratorReturnType = [TIndex, () => void, () => void]; 9 | export const useIterator = ( 10 | props: UseIterator 11 | ): UseIteratorReturnType => { 12 | const { initialIndex = 0, items = [] } = props; 13 | const [i, setIndex] = useState(initialIndex); 14 | 15 | const prev = useCallback(() => { 16 | if (i === 0) { 17 | setIndex(items.length - 1); 18 | return; 19 | } 20 | setIndex(i - 1); 21 | }, [i, items]); 22 | 23 | const next = useCallback(() => { 24 | if (i === items.length - 1) { 25 | setIndex(0); 26 | return; 27 | } 28 | setIndex(i + 1); 29 | }, [i, items]); 30 | 31 | const item = useMemo(() => items[i], [i, items]); 32 | 33 | return [item || items[0], prev, next]; 34 | }; 35 | -------------------------------------------------------------------------------- /src/domains/home/type.ts: -------------------------------------------------------------------------------- 1 | import { SyntheticEvent } from 'react'; 2 | 3 | type TVideoState = { 4 | isEnd: boolean; 5 | startPlay: boolean; 6 | videoId: number; 7 | isLast: boolean; 8 | isPlaying: boolean; 9 | }; 10 | 11 | type TProsessState = 12 | | 'video-end' 13 | | 'play' 14 | | 'pause' 15 | | 'video-last' 16 | | 'video-reset'; 17 | 18 | type TReturnUseCarouselController = { 19 | handleProsess: ( 20 | prosess: TProsessState, 21 | i?: number 22 | ) => TVideoState | undefined; 23 | videoRef: React.MutableRefObject; 24 | videoSpanRef: React.MutableRefObject; 25 | videoDivRef: React.MutableRefObject; 26 | setVideo: React.Dispatch>; 27 | video: TVideoState; 28 | handleLoadedMetaData: ( 29 | index: number, 30 | event: SyntheticEvent 31 | ) => void; 32 | }; 33 | 34 | export type { TProsessState, TReturnUseCarouselController, TVideoState }; 35 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | export interface GameQuery { 4 | genreId?: number; 5 | platformId?: number; 6 | sortOrder?: string; 7 | searchText?: string; 8 | } 9 | 10 | interface GameQueryStore { 11 | gameQuery: GameQuery; 12 | setSearchText: (searchText: string) => void; 13 | setPlatformId: (platformId: number) => void; 14 | setGenreId: (genreId: number) => void; 15 | setSortOrder: (sortOrder: string) => void; 16 | } 17 | 18 | const useGameQueryStore = create((set) => ({ 19 | gameQuery: {}, 20 | setSearchText: (searchText) => set(() => ({ gameQuery: { searchText } })), 21 | setGenreId: (genreId) => 22 | set((store) => ({ gameQuery: { ...store.gameQuery, genreId } })), 23 | setPlatformId: (platform) => 24 | set((store) => ({ gameQuery: { ...store.gameQuery, platform } })), 25 | setSortOrder: (sortOrder) => 26 | set((store) => ({ gameQuery: { ...store.gameQuery, sortOrder } })), 27 | })); 28 | export default useGameQueryStore; 29 | -------------------------------------------------------------------------------- /src/components/common/list.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable react/no-unknown-property 2 | 3 | import { FC } from 'react'; 4 | import { Fragment } from 'react/jsx-runtime'; 5 | 6 | interface IPersonList { 7 | items: T[]; 8 | itemComponents?: FC; 9 | renderList?: (props: T) => JSX.Element; 10 | } 11 | 12 | export function List(props: IPersonList) { 13 | const { items, itemComponents: ItemComponents, renderList } = props; 14 | return ( 15 | <> 16 | {/* this is called render props patterns */} 17 | {renderList && 18 | items.map((item, index) => ( 19 | // eslint-disable-next-line react/no-array-index-key 20 | {renderList(item)} 21 | ))} 22 | {ItemComponents && 23 | // eslint-disable-next-line react/no-array-index-key 24 | items.map((item, index) => )} 25 | 26 | ); 27 | } 28 | 29 | List.defaultProps = { 30 | itemComponents: {}, 31 | renderList: {}, 32 | }; 33 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/react'; 2 | import { App } from 'domains'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom/client'; 5 | 6 | Sentry.init({ 7 | dsn: 'https://c2fd28389b65abd38cb1d3b403d20184@o1330959.ingest.us.sentry.io/4506939243495424', 8 | integrations: [ 9 | Sentry.browserTracingIntegration(), 10 | Sentry.metrics.metricsAggregatorIntegration(), 11 | // @ts-expect-error i should find the problem here 12 | Sentry.reactRouterV6BrowserTracingIntegration({ 13 | useEffect: React.useEffect, 14 | }), 15 | // Sentry.replayIntegration({ 16 | // maskAllText: false, 17 | // blockAllMedia: false, 18 | // }), 19 | Sentry.replayIntegration(), 20 | ], 21 | 22 | tracesSampleRate: 1.0, 23 | 24 | tracePropagationTargets: ['localhost', /^https:\/\/yourserver\.io\/api/], 25 | replaysSessionSampleRate: 0.1, 26 | replaysOnErrorSampleRate: 1.0, 27 | }); 28 | 29 | ReactDOM.createRoot(document.getElementById('root')!).render(); 30 | -------------------------------------------------------------------------------- /src/domains/users/userItem.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import axios from 'axios'; 3 | import { User } from './userList'; 4 | 5 | const fetchUsers = (userId: string): Promise => 6 | axios 7 | .get(`https://jsonplaceholder.typicode.com/users/${userId}`) 8 | .then((response) => response.data); 9 | 10 | export function UserITem({ userId }: { userId: string }) { 11 | const { 12 | data: user, 13 | isLoading, 14 | isError, 15 | } = useQuery({ 16 | queryKey: ['user', userId], 17 | queryFn: () => fetchUsers(userId), 18 | }); 19 | 20 | if (isLoading) return
Loading...
; 21 | if (isError) return
Error fetching data
; 22 | 23 | return ( 24 |
25 |

{user?.name}

26 |

27 | Username: {user?.username} 28 |

29 |

30 | Email: {user?.email} 31 |

32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/domains/users/userList.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import axios from 'axios'; 3 | 4 | export interface User { 5 | id: number; 6 | name: string; 7 | username: string; 8 | email: string; 9 | } 10 | const fetchUsers = (): Promise => 11 | axios 12 | .get(`https://jsonplaceholder.typicode.com/users`) 13 | .then((response) => response.data); 14 | 15 | export function Users() { 16 | const { 17 | data: users, 18 | isLoading, 19 | isError, 20 | } = useQuery({ 21 | queryKey: ['users'], 22 | queryFn: fetchUsers, 23 | }); 24 | 25 | if (isLoading) return
Loading...
; 26 | if (isError) return
Error fetching data
; 27 | 28 | return users?.map((user) => ( 29 |
30 |

{user?.name}

31 |

32 | Username: {user?.username} 33 |

34 |

35 | Email: {user?.email} 36 |

37 |
38 | )); 39 | } 40 | -------------------------------------------------------------------------------- /docs/StoryRoadmap.mdx: -------------------------------------------------------------------------------- 1 | # Story Roadmap 2 | 3 | Welcome to the Story Roadmap 4 | 5 | ## Instructions 6 | 7 | Follow the sections below to create components and stories. Each section provides a list of components along with their respective stories. 8 | 9 | ## Buttons 10 | 11 | - Button 12 | - IconButton 13 | - ButtonGroup 14 | 15 | ## Display 16 | 17 | - Avatar 18 | - Badge 19 | - Chip 20 | - List 21 | - Tag 22 | - TagGroup 23 | - Timeline 24 | 25 | ## Feedback 26 | 27 | - Loader 28 | - Progress 29 | - Toasts 30 | 31 | ## Forms 32 | 33 | - Checkbox 34 | - CheckGroup 35 | - Input 36 | - Label 37 | - Dropdown 38 | - Radio 39 | - Select 40 | - Textarea 41 | - Slider 42 | - InputPassword 43 | - InputGroup 44 | 45 | ## Navigation 46 | 47 | - Link 48 | - Pagination 49 | - Navigation 50 | 51 | ## Surfaces 52 | 53 | - Accordion 54 | - Card 55 | - Dropdown 56 | - Footer 57 | - Header 58 | - Modals 59 | - Panels 60 | - Paper 61 | - Popover 62 | - Tabs 63 | - Tooltip 64 | - Alert 65 | 66 | ## Utilities 67 | 68 | - Accessibility 69 | - Divider 70 | - Flex 71 | - Portal 72 | - Size 73 | - Typography 74 | - InteractiveList 75 | 76 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'; 2 | 3 | import type { Preview } from '@storybook/react'; 4 | import '../src/styles/App.css'; 5 | 6 | const MY_VIEWPORTS = { 7 | FHD: { 8 | name: 'FHD _ 1080P', 9 | styles: { width: '1900px', height: '1080px' }, 10 | }, 11 | ...INITIAL_VIEWPORTS, 12 | }; 13 | 14 | const preview: Preview = { 15 | parameters: { 16 | viewport: { 17 | viewports: MY_VIEWPORTS, 18 | }, 19 | backgrounds: { 20 | values: [ 21 | { name: 'Light', value: '#ffffff' }, 22 | { name: 'Dark', value: '#333333' }, 23 | { name: 'Primary', value: '#007bff' }, 24 | { name: 'Secondary', value: '#6c757d' }, 25 | { name: 'Success', value: '#28a745' }, 26 | { name: 'Danger', value: '#dc3545' }, 27 | { name: 'Warning', value: '#ffc107' }, 28 | { name: 'Info', value: '#17a2b8' }, 29 | ], 30 | }, 31 | controls: { 32 | matchers: { 33 | color: /(background|color)$/i, 34 | date: /Date$/i, 35 | }, 36 | }, 37 | }, 38 | }; 39 | 40 | export default preview; 41 | -------------------------------------------------------------------------------- /src/components/ui/form/stories/label.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Card, CardContent } from 'components/ui/card/card'; 3 | import { Label, LabelProps } from 'components/ui/form/label'; 4 | import { Checkbox } from '../checkbox'; 5 | 6 | const meta: Meta = { 7 | component: Label, 8 | title: 'Forms/Label', 9 | tags: ['autodocs'], 10 | args: { children: 'your email Address here', htmlFor: 'email' }, 11 | parameters: { 12 | controls: { expanded: true }, 13 | layout: 'centered', 14 | }, 15 | }; 16 | 17 | export default meta; 18 | 19 | type Story = StoryObj; 20 | 21 | const labelDefaultArgs = { 22 | fullWidth: true, 23 | } as LabelProps; 24 | 25 | export const Default: Story = { 26 | args: { 27 | ...labelDefaultArgs, 28 | }, 29 | }; 30 | 31 | export const LabelCheked: Story = { 32 | args: { 33 | ...labelDefaultArgs, 34 | }, 35 | decorators: [ 36 | (story) => ( 37 | 38 | 39 | 43 | 44 | ), 45 | ], 46 | }; 47 | -------------------------------------------------------------------------------- /.github/workflows/vitest.yml: -------------------------------------------------------------------------------- 1 | name: Vitest Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | - develop 9 | pull_request: null 10 | workflow_dispatch: null 11 | 12 | jobs: 13 | test: 14 | timeout-minutes: 60 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Install pnpm 22 | run: npm install -g pnpm 23 | 24 | - run: echo "node_version=$(cat .github/nodejs.version)" >> $GITHUB_ENV 25 | 26 | - name: "use node ${{ env.node_version }}" 27 | uses: actions/setup-node@v3 28 | with: 29 | node-version: "${{ env.node_version }}" 30 | registry-url: 'https://registry.npmjs.org/' 31 | cache: 'pnpm' 32 | always-auth: false 33 | 34 | - name: Install dependencies 35 | run: pnpm install --frozen-lockfile 36 | 37 | - name: Run Vitest tests 38 | run: pnpm exec vitest 39 | 40 | - name: Upload Vitest Report 41 | if: always() 42 | uses: actions/upload-artifact@v3 43 | with: 44 | name: vitest-report 45 | path: vitest-report/ 46 | retention-days: 30 -------------------------------------------------------------------------------- /src/components/ui/form/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; 2 | 3 | import { cn } from 'lib/utils'; 4 | import { Check } from 'lucide-react'; 5 | import * as React from 'react'; 6 | 7 | export type CheckboxPrimitiveRoot = typeof CheckboxPrimitive.Root; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )); 28 | 29 | Checkbox.displayName = 'Checkbox'; 30 | 31 | export { Checkbox }; 32 | -------------------------------------------------------------------------------- /src/components/ui/form/label.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from '@radix-ui/react-slot'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | import { cn } from 'lib/utils'; 4 | import * as React from 'react'; 5 | 6 | const labelVariants = cva( 7 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' 8 | ); 9 | 10 | // Define props interface for the Label component 11 | export interface LabelProps 12 | extends React.LabelHTMLAttributes { 13 | asChild?: boolean; // Indicate if the label should be rendered as a child element 14 | } 15 | 16 | // ForwardRefExoticComponent allows the component to receive a ref 17 | const Label: React.ForwardRefExoticComponent< 18 | LabelProps & 19 | VariantProps & 20 | React.RefAttributes 21 | > = React.forwardRef(({ className, asChild, htmlFor, ...props }, ref) => { 22 | const Comp = asChild ? Slot : 'label'; 23 | const labelProps = htmlFor ? { htmlFor } : {}; 24 | 25 | return ( 26 | 32 | ); 33 | }); 34 | 35 | Label.displayName = 'Label'; 36 | 37 | export { Label }; 38 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | - develop 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | check: 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Use Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ env.node_version }} 21 | 22 | - name: Cache dependencies 23 | uses: actions/cache@v2 24 | with: 25 | path: ~/.pnpm-store 26 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 27 | restore-keys: | 28 | ${{ runner.os }}- 29 | 30 | - name: Install dependencies 31 | run: pnpm install 32 | 33 | - name: Lint 34 | run: pnpm run lint 35 | 36 | - name: Format check 37 | run: pnpm run format 38 | 39 | - name: Test 40 | run: pnpm test 41 | 42 | - name: Build storybook 43 | run: pnpm run build-storybook 44 | 45 | - name: Smoke tests 46 | run: | 47 | npx http-server storybook-static --port 6006 --silent 48 | npx wait-on tcp:127.0.0.1:6006 49 | pnpm run test-storybook 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/components/common/portal.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, useLayoutEffect, useState } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | 4 | interface IPortalProps { 5 | id?: string; 6 | elements?: keyof HTMLElementTagNameMap; // Allow specifying specific HTML element types 7 | } 8 | 9 | export function Portal(props: PropsWithChildren) { 10 | const { children, id = 'portal', elements = 'div' } = props; 11 | const [modalRoot, setModalRoot] = useState(null); 12 | 13 | useLayoutEffect(() => { 14 | let root = document.getElementById(id); 15 | if (!root) { 16 | root = document.createElement(elements); // Create element of specified type 17 | root.id = id; 18 | document.body.appendChild(root); 19 | } 20 | setModalRoot(root); 21 | return () => { 22 | // Cleanup when component unmounts 23 | if (root && root.parentNode) { 24 | root.parentNode.removeChild(root); 25 | } 26 | }; 27 | }, [id, elements]); 28 | 29 | if (!modalRoot) { 30 | return null; // Render nothing until modalRoot is initialized 31 | } 32 | 33 | return createPortal(
{children}
, modalRoot); 34 | } 35 | 36 | Portal.defaultProps = { 37 | id: undefined, 38 | elements: {}, 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/ui/form/stories/input.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from '@storybook/react'; 2 | import { VARIANT } from 'constant/themes'; 3 | import { Input } from '../input'; 4 | 5 | const InputStories: Meta = { 6 | title: 'Forms/Input', 7 | component: Input, 8 | tags: ['autodocs'], 9 | args: { 10 | name: 'somthing', 11 | disabled: false, 12 | required: false, 13 | label: 'Field', 14 | variant: 'primary', 15 | placeholder: 'Enter Somthing....', 16 | }, 17 | argTypes: { 18 | name: { control: 'text' }, 19 | value: { control: 'text' }, 20 | label: { control: 'text' }, 21 | disabled: { control: 'boolean' }, 22 | variant: { control: 'select', options: Object.keys(VARIANT) }, 23 | required: { control: 'boolean' }, 24 | type: { control: 'select', options: ['text', 'email'] }, 25 | }, 26 | parameters: { 27 | layout: 'centered', 28 | }, 29 | }; 30 | 31 | export default InputStories; 32 | 33 | type Story = StoryObj; 34 | 35 | export const TextField: Story = { 36 | args: { 37 | name: 'email', 38 | id: 'email', 39 | }, 40 | }; 41 | 42 | export const Primary: Story = { 43 | args: { 44 | name: 'userName', 45 | type: 'text', 46 | id: 'email', 47 | fullWidth: true, 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | 3 | import { twMerge } from 'tailwind-merge'; 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | 9 | interface ICountDown { 10 | value: number; 11 | fn: (val: number) => void; 12 | delay?: number; 13 | } 14 | 15 | // cooldown recursive functionality 16 | // recorsion stops when value reaches 0 17 | const countDown = ({ value, fn, delay = 1000 }: ICountDown) => { 18 | fn(value); 19 | return value > 0 20 | ? setTimeout(() => { 21 | countDown({ value: value - 1, fn, delay }); 22 | }, delay) 23 | : value; 24 | }; 25 | 26 | const deepPick = (fields: string, object: T): T[K] => { 27 | const [first, ...remaining] = fields.split('.'); 28 | const value = object[first as keyof T]; 29 | if (typeof value === 'undefined') { 30 | throw new Error(`Property '${first}' not found in object`); 31 | } 32 | if (remaining.length === 0) { 33 | return value as T[K]; 34 | } 35 | if (typeof value !== 'object' || value === null) { 36 | throw new Error(`Invalid property '${first}'`); 37 | } 38 | return deepPick(remaining.join('.'), value as unknown) as T[K]; 39 | }; 40 | 41 | const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); 42 | export { capitalize, countDown, deepPick }; 43 | -------------------------------------------------------------------------------- /src/components/ui/icon.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority'; 2 | import { cn } from 'lib/utils'; 3 | import { icons, LucideProps } from 'lucide-react'; 4 | import { forwardRef } from 'react'; 5 | 6 | // Define icon variants 7 | const iconVariants = cva('px-2', { 8 | variants: { 9 | sizes: { 10 | default: 'size-10', 11 | xs: 'size-8', 12 | sm: 'size-12', 13 | md: 'size-14', 14 | lg: 'size-17', 15 | }, 16 | colors: { 17 | muted: 'text-muted-foreground', 18 | primary: 'text-primary', 19 | secondary: 'text-secondary', 20 | }, 21 | }, 22 | defaultVariants: { 23 | sizes: 'default', 24 | colors: 'primary', 25 | }, 26 | }); 27 | 28 | export interface IconProps 29 | extends Omit, 30 | VariantProps { 31 | name: keyof typeof icons; 32 | } 33 | 34 | const Icon = forwardRef( 35 | ({ name, colors, className, sizes, ...props }, ref) => { 36 | const LucideIcon = icons[name]; 37 | 38 | return ( 39 | 47 | ); 48 | } 49 | ); 50 | 51 | Icon.displayName = 'Icon'; 52 | 53 | export { iconVariants, Icon }; 54 | -------------------------------------------------------------------------------- /src/components/ui/button/icon-button.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from 'lib/utils'; 2 | import * as React from 'react'; 3 | import { Button, ButtonProps, buttonVariants } from './button'; 4 | import { Icon, IconProps } from '../icon'; 5 | 6 | // TODO: see if you combine size props to one with this patterns 7 | // IconProps["size"] | (string & {}) 8 | export interface IconButtonProps extends ButtonProps { 9 | iconName: IconProps['name']; 10 | iconSize?: IconProps['size'] | (string & object); 11 | iconClassName?: IconProps['className']; 12 | sizes?: IconProps['sizes']; 13 | iconProps?: Omit; 14 | } 15 | 16 | export const IconButton = React.forwardRef( 17 | ( 18 | { className, iconName, iconClassName, iconProps, variant, ...props }, 19 | ref 20 | ) => ( 21 | 34 | ) 35 | ); 36 | 37 | IconButton.displayName = 'IconButton'; 38 | 39 | IconButton.defaultProps = { 40 | iconSize: undefined, 41 | iconClassName: '', 42 | sizes: undefined, 43 | iconProps: {}, 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/ui/button/stories/icon-button.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { IconButton, IconButtonProps } from 'components/ui/button/icon-button'; 3 | import { SIZES, VARIANT } from 'constant/themes'; 4 | 5 | const meta: Meta = { 6 | component: IconButton, 7 | title: 'Buttons/IconButton', 8 | tags: ['autodocs'], 9 | args: { 10 | iconName: 'User', 11 | iconSize: 'lg', 12 | sizes: 'lg', 13 | }, 14 | argTypes: { 15 | onClick: { action: 'clicked' }, 16 | disabled: { control: 'boolean' }, 17 | variant: { 18 | options: Object.keys(VARIANT), 19 | control: { type: 'select' }, 20 | }, 21 | roundedFull: { 22 | control: { type: 'boolean' }, 23 | }, 24 | size: { 25 | control: { type: 'text' }, 26 | }, 27 | sizes: { 28 | options: Object.keys(SIZES), 29 | control: { type: 'radio' }, 30 | }, 31 | iconSize: { 32 | options: Object.keys(SIZES), 33 | control: { type: 'radio' }, 34 | }, 35 | }, 36 | }; 37 | 38 | type Story = StoryObj; 39 | 40 | export const Outline: Story = { 41 | args: { 42 | variant: 'outline', 43 | iconClassName: 'text-black', 44 | }, 45 | }; 46 | export const Ghost: Story = { 47 | args: { 48 | variant: 'ghost', 49 | roundedFull: true, 50 | }, 51 | }; 52 | 53 | export default meta; 54 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority'; 2 | import { cn } from 'lib/utils'; 3 | import * as React from 'react'; 4 | 5 | const badgeVariants = cva( 6 | 'inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 7 | { 8 | variants: { 9 | variant: { 10 | default: 11 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', 12 | secondary: 13 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 14 | destructive: 15 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', 16 | outline: 'text-foreground', 17 | }, 18 | }, 19 | defaultVariants: { 20 | variant: 'default', 21 | }, 22 | } 23 | ); 24 | 25 | export interface BadgeProps 26 | extends React.HTMLAttributes, 27 | VariantProps {} 28 | 29 | const Badge = React.forwardRef( 30 | ({ className, variant, ...props }, ref) => ( 31 |
36 | ) 37 | ); 38 | 39 | Badge.displayName = 'Badge'; 40 | 41 | export { Badge, badgeVariants }; 42 | -------------------------------------------------------------------------------- /src/containers/error.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'components'; 2 | import { 3 | Card, 4 | CardContent, 5 | CardFooter, 6 | CardHeader, 7 | } from 'components/ui/card/card'; 8 | 9 | import { isRouteErrorResponse, Link, useRouteError } from 'react-router-dom'; 10 | 11 | export function ErrorBoundray() { 12 | const error = useRouteError() as Error; 13 | 14 | return ( 15 |
19 | {isRouteErrorResponse(error) ? ( 20 |

This page does not exist..

21 | ) : ( 22 | 23 | Ops.. 24 | 25 |

26 | We are sorry... something went wrong 27 |

28 |

29 | We cannot process your request at this moment. 30 |

31 | {error && ( 32 |

33 | ERROR: {error.message} 34 |

35 | )} 36 |
37 | 38 | 41 | 42 |
43 | )} 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/validation.ts: -------------------------------------------------------------------------------- 1 | import { RegexPatterns } from 'constant/regex-patterns'; 2 | import { Brand } from './type-helper'; 3 | 4 | type EmailAddress = Brand; 5 | 6 | function isValidEmail(email: string): email is EmailAddress { 7 | return RegexPatterns.Email.test(email); 8 | } 9 | 10 | function isValid(input: string, regex: RegExp): boolean { 11 | return regex.test(input); 12 | } 13 | function assertValid(input: T, regex: RegExp): asserts input is T { 14 | if (typeof input !== 'string' && typeof input !== 'number') { 15 | throw new Error('Input must be a string or number'); 16 | } 17 | if (!regex.test(String(input))) { 18 | throw new Error('Input does not match the expected pattern'); 19 | } 20 | } 21 | 22 | type PasswordValues = { 23 | password: string; 24 | confirmPassword: string; 25 | }; 26 | 27 | type Valid = Brand; 28 | 29 | function isValidPassword( 30 | valuse: PasswordValues 31 | ): valuse is Valid { 32 | if (valuse.password !== valuse.confirmPassword) return false; 33 | return true; 34 | } 35 | 36 | function assertIsValidPassword( 37 | values: PasswordValues 38 | ): asserts values is Valid { 39 | if (values.password !== values.confirmPassword) { 40 | throw new Error('Password is invalid'); 41 | } 42 | } 43 | 44 | export { 45 | assertValid, 46 | isValidEmail, 47 | isValid, 48 | isValidPassword, 49 | assertIsValidPassword, 50 | }; 51 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | Copyright © 2024 https://react-launchpad.vercel.app/ 4 | 5 | The content of this repository is bound by the following licenses: 6 | 7 | - The computer software is licensed under the [BSD-3-Clause](LICENSE.md) license. 8 | - The learning resources in the [`/curriculum`](/curriculum) directory including their subdirectories thereon are copyright © 2024 freeCodeCamp.org 9 | 10 | 11 | This project is licensed under the [MIT License](https://gist.github.com/lukas-h/2a5d00690736b4c3a7ba). 12 | 13 | ## MIT License 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as AvatarPrimitive from '@radix-ui/react-avatar'; 2 | import { cn } from 'lib/utils'; 3 | import * as React from 'react'; 4 | 5 | const Avatar = React.forwardRef< 6 | React.ElementRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, ...props }, ref) => ( 9 | 17 | )); 18 | 19 | Avatar.displayName = AvatarPrimitive.Root.displayName; 20 | 21 | const AvatarImage = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 30 | )); 31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 32 | 33 | const AvatarFallback = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | )); 46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 47 | 48 | export { Avatar, AvatarImage, AvatarFallback }; 49 | -------------------------------------------------------------------------------- /src/providers/auth-provider.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | PropsWithChildren, 4 | useContext, 5 | useMemo, 6 | useState, 7 | } from 'react'; 8 | 9 | // Define the shape of the user object 10 | interface User { 11 | id: string; 12 | name: string; 13 | // Add more fields as needed 14 | } 15 | 16 | // Define the shape of the authentication context 17 | interface AuthContextType { 18 | user: User | null; 19 | login: (userData: User) => void; 20 | logout: () => void; 21 | } 22 | 23 | // Create a context for authentication 24 | const AuthContext = createContext(undefined); 25 | 26 | // Custom hook to access the authentication context 27 | export const useAuth = () => { 28 | const context = useContext(AuthContext); 29 | if (!context) { 30 | throw new Error('useAuth must be used within an AuthProvider'); 31 | } 32 | return context; 33 | }; 34 | 35 | // Authentication provider component 36 | export function AuthProvider({ children }: PropsWithChildren) { 37 | const [user, setUser] = useState(null); 38 | 39 | const login = (userData: User) => { 40 | // Logic for logging in the user 41 | setUser(userData); 42 | }; 43 | 44 | const logout = () => { 45 | // Logic for logging out the user 46 | setUser(null); 47 | }; 48 | 49 | // Value provided by the authentication context 50 | 51 | const authValues = useMemo( 52 | () => ({ 53 | user, 54 | login, 55 | logout, 56 | }), 57 | [user] 58 | ); 59 | 60 | return ( 61 | {children} 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/components/ui/form/input-controller.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | FormDescription, 4 | FormField, 5 | FormItem, 6 | FormLabel, 7 | FormMessage, 8 | } from 'components/ui/form/form'; 9 | import { Input, InputProps } from 'components/ui/form/input'; 10 | import { ControllerProps, FieldPath, FieldValues } from 'react-hook-form'; 11 | 12 | interface InputControllerProps< 13 | TFieldValues extends FieldValues = FieldValues, 14 | TName extends FieldPath = FieldPath, 15 | > { 16 | label: string; 17 | description?: string; 18 | inputControll: Omit, 'render'>; 19 | inputProps?: InputProps; 20 | 21 | // render?: (field: ControllerRenderProps) => ReactNode; 22 | } 23 | 24 | export function InputController< 25 | TFieldValues extends FieldValues = FieldValues, 26 | TName extends FieldPath = FieldPath, 27 | >(props: InputControllerProps) { 28 | const { inputControll, inputProps, label, description, ...rest } = props; 29 | 30 | return ( 31 | ( 33 | 34 | {label && {label}} 35 | 36 | 37 | 38 | {description && {description}} 39 | 40 | 41 | )} 42 | {...inputControll} 43 | {...rest} 44 | /> 45 | ); 46 | } 47 | 48 | InputController.defaultProps = { 49 | description: false, 50 | inputProps: {}, 51 | }; 52 | -------------------------------------------------------------------------------- /src/hooks/useDebounceCallback.ts: -------------------------------------------------------------------------------- 1 | import { useUnmount } from 'hooks'; 2 | import debounce from 'lodash.debounce'; 3 | import { useEffect, useMemo, useRef } from 'react'; 4 | 5 | type DebounceOptions = { 6 | loading?: boolean; 7 | trailing?: boolean; 8 | maxWait?: number; 9 | }; 10 | 11 | type ControllFunctions = { 12 | cancel: () => void; 13 | flush: () => void; 14 | isPending: () => boolean; 15 | }; 16 | 17 | export type DebounceState ReturnType> = (( 18 | ...args: Parameters 19 | ) => ReturnType | undefined) & 20 | ControllFunctions; 21 | 22 | export function useDebounceCallback ReturnType>( 23 | func: T, 24 | delay: number = 500, 25 | options?: DebounceOptions 26 | ): DebounceState { 27 | const debouncedFunc = useRef>(); 28 | 29 | useUnmount(() => { 30 | if (debouncedFunc.current) { 31 | debouncedFunc.current.cancel(); 32 | } 33 | }); 34 | const debounced = useMemo(() => { 35 | const debouncedFuncInstance = debounce(func, delay, options); 36 | 37 | const wrappedFunc: DebounceState = (...args: Parameters) => 38 | debouncedFuncInstance(...args); 39 | wrappedFunc.cancel = () => { 40 | debouncedFuncInstance.cancel(); 41 | }; 42 | 43 | wrappedFunc.isPending = () => !!debouncedFunc.current; 44 | 45 | wrappedFunc.flush = () => debouncedFuncInstance.flush(); 46 | 47 | return wrappedFunc; 48 | }, [delay, func, options]); 49 | 50 | // Update the debounced function ref whenever func, wait, or options change 51 | useEffect(() => { 52 | debouncedFunc.current = debounce(func, delay, options); 53 | }, [func, delay, options]); 54 | 55 | return debounced; 56 | } 57 | -------------------------------------------------------------------------------- /src/hooks/useMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; 4 | 5 | type UseMediaQueryOptions = { 6 | defaultValue?: boolean; 7 | initializeWithValue?: boolean; 8 | }; 9 | 10 | const IS_SERVER = typeof window === 'undefined'; 11 | 12 | export function useMediaQuery( 13 | query: string, 14 | { 15 | defaultValue = false, 16 | initializeWithValue = true, 17 | }: UseMediaQueryOptions = {} 18 | ): boolean { 19 | const getMatches = (matchQuery: string): boolean => { 20 | if (IS_SERVER) { 21 | return defaultValue; 22 | } 23 | return window.matchMedia(matchQuery).matches; 24 | }; 25 | 26 | const [matches, setMatches] = useState(() => { 27 | if (initializeWithValue) { 28 | return getMatches(query); 29 | } 30 | return defaultValue; 31 | }); 32 | 33 | // Handles the change event of the media query. 34 | function handleChange() { 35 | setMatches(getMatches(query)); 36 | } 37 | 38 | useIsomorphicLayoutEffect(() => { 39 | const matchMedia = window.matchMedia(query); 40 | 41 | // Triggered at the first client-side load and if query changes 42 | handleChange(); 43 | 44 | // Use deprecated `addListener` and `removeListener` to support Safari < 14 (#135) 45 | if (matchMedia.addListener) { 46 | matchMedia.addListener(handleChange); 47 | } else { 48 | matchMedia.addEventListener('change', handleChange); 49 | } 50 | 51 | return () => { 52 | if (matchMedia.removeListener) { 53 | matchMedia.removeListener(handleChange); 54 | } else { 55 | matchMedia.removeEventListener('change', handleChange); 56 | } 57 | }; 58 | }, [query]); 59 | 60 | return matches; 61 | } 62 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority'; 2 | import { cn } from 'lib/utils'; 3 | import * as React from 'react'; 4 | 5 | const alertVariants = cva( 6 | 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', 7 | { 8 | variants: { 9 | variant: { 10 | default: 'bg-background text-foreground', 11 | destructive: 12 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', 13 | }, 14 | defaultVariants: { 15 | variant: 'default', 16 | }, 17 | }, 18 | } 19 | ); 20 | 21 | const Alert = React.forwardRef< 22 | HTMLDivElement, 23 | React.HTMLAttributes & VariantProps 24 | >(({ className, variant, ...props }, ref) => ( 25 |
31 | )); 32 | Alert.displayName = 'Alert'; 33 | 34 | const AlertTitle = React.forwardRef< 35 | HTMLParagraphElement, 36 | React.HTMLAttributes 37 | >(({ className, children, ...props }, ref) => ( 38 |
43 | {children} 44 |
45 | )); 46 | AlertTitle.displayName = 'AlertTitle'; 47 | 48 | const AlertDescription = React.forwardRef< 49 | HTMLParagraphElement, 50 | React.HTMLAttributes 51 | >(({ className, ...props }, ref) => ( 52 |

57 | )); 58 | 59 | AlertDescription.displayName = 'AlertTitle'; 60 | 61 | export { Alert, AlertDescription, AlertTitle }; 62 | -------------------------------------------------------------------------------- /src/domains/auth/stories/signup.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { expect, userEvent, within } from '@storybook/test'; 4 | 5 | import { SignUp } from '../sign-up'; 6 | 7 | const meta: Meta = { 8 | title: 'Auth/SignUp', 9 | component: SignUp, 10 | }; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | /* @see https://storybook.js.org/docs/writing-stories/play-function */ 16 | 17 | export const SignForm: Story = { 18 | play: async ({ canvasElement }) => { 19 | const canvas = within(canvasElement); 20 | 21 | await userEvent.type(canvas.getByTestId('email'), 'email@provider.com'); 22 | await userEvent.type(canvas.getByTestId('username'), 'alireza'); 23 | 24 | await userEvent.type(canvas.getByTestId('password'), 'a-random-password'); 25 | await userEvent.type( 26 | canvas.getByTestId('confirmPassword'), 27 | 'a-random-password' 28 | ); 29 | await userEvent.click(canvas.getByRole('checkbox')); 30 | await userEvent.click(canvas.getByRole('button')); 31 | 32 | // 👇 Assert DOM structure 33 | await expect( 34 | canvas.getByText(/Everything is perfect/i) 35 | ).toBeInTheDocument(); 36 | }, 37 | }; 38 | 39 | export const ErrorForm: Story = { 40 | play: async ({ canvasElement }) => { 41 | const canvas = within(canvasElement); 42 | 43 | await userEvent.type(canvas.getByTestId('email'), 'email@provider.com'); 44 | await userEvent.type(canvas.getByTestId('username'), 'alireza'); 45 | 46 | await userEvent.type(canvas.getByTestId('password'), 'a-random-password'); 47 | await userEvent.type( 48 | canvas.getByTestId('confirmPassword'), 49 | 'a-random-password sad' 50 | ); 51 | await userEvent.click(canvas.getByRole('checkbox')); 52 | 53 | // 👇 Assert DOM structure 54 | await expect(canvas.getByRole('button')).toBeDisabled(); 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /src/hooks/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useDebounceCallback } from './useDebounceCallback'; 3 | import { useEventListener } from './useEventListener'; 4 | import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; 5 | 6 | type WindowSize = { 7 | width: T; 8 | height: T; 9 | }; 10 | 11 | type UseWindowSizeOptions = { 12 | initializeWithValue: InitializeWithValue; 13 | debounceDelay?: number; 14 | }; 15 | 16 | const IS_SERVER = typeof window === 'undefined'; 17 | 18 | // SSR version of useWindowSize. 19 | export function useWindowSize(options: UseWindowSizeOptions): WindowSize; 20 | // CSR version of useWindowSize. 21 | export function useWindowSize( 22 | options?: Partial> 23 | ): WindowSize; 24 | export function useWindowSize( 25 | options: Partial> = {} 26 | ): WindowSize | WindowSize { 27 | let { initializeWithValue = true } = options; 28 | if (IS_SERVER) { 29 | initializeWithValue = false; 30 | } 31 | 32 | const [windowSize, setWindowSize] = useState(() => { 33 | if (initializeWithValue) { 34 | return { 35 | width: window.innerWidth, 36 | height: window.innerHeight, 37 | }; 38 | } 39 | return { 40 | width: undefined, 41 | height: undefined, 42 | }; 43 | }); 44 | 45 | const debouncedSetWindowSize = useDebounceCallback( 46 | setWindowSize, 47 | options.debounceDelay 48 | ); 49 | 50 | function handleSize() { 51 | const setSize = options.debounceDelay 52 | ? debouncedSetWindowSize 53 | : setWindowSize; 54 | 55 | setSize({ 56 | width: window.innerWidth, 57 | height: window.innerHeight, 58 | }); 59 | } 60 | 61 | useEventListener('resize', handleSize); 62 | 63 | // Set size at the first client-side load 64 | useIsomorphicLayoutEffect(() => { 65 | handleSize(); 66 | }, []); 67 | 68 | return windowSize; 69 | } 70 | -------------------------------------------------------------------------------- /src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardLayout, ErrorBoundray, RootLayout } from 'containers'; 2 | import { lazy } from 'react'; 3 | import { createBrowserRouter } from 'react-router-dom'; 4 | 5 | const FaqView = lazy(() => import('views/faq.view')); 6 | const ContactView = lazy(() => import('views/contact.view')); 7 | const AboutView = lazy(() => import('views/about.view')); 8 | const ServicesView = lazy(() => import('views/services.view')); 9 | const HomeView = lazy(() => import('views/home.view')); 10 | const ServiesView = lazy(() => import('views/services.view')); 11 | const LicenseView = lazy(() => import('views/license.view')); 12 | const ContributeView = lazy(() => import('views/contribute.view')); 13 | const StatusView = lazy(() => import('views/status.view')); 14 | 15 | export const router = createBrowserRouter([ 16 | { 17 | path: '/', 18 | element: , 19 | errorElement: , 20 | children: [ 21 | { 22 | index: true, 23 | element: , 24 | }, 25 | { 26 | path: 'faq', 27 | element: , 28 | }, 29 | { 30 | path: 'contribute', 31 | element: , 32 | }, 33 | { 34 | path: 'status', 35 | element: , 36 | }, 37 | { 38 | path: 'license', 39 | element: , 40 | }, 41 | { 42 | path: 'services', 43 | element: , 44 | }, 45 | { 46 | path: 'contact', 47 | element: , 48 | }, 49 | { 50 | path: 'about', 51 | element: , 52 | }, 53 | { 54 | path: 'services', 55 | element: , 56 | }, 57 | ], 58 | }, 59 | { 60 | path: '/dashboard', 61 | element: , 62 | errorElement: , 63 | children: [ 64 | { 65 | index: true, 66 | path: 'patterns', 67 | element:

dashboard
, 68 | }, 69 | ], 70 | }, 71 | ]); 72 | -------------------------------------------------------------------------------- /src/components/ui/form/checkbox-controller.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | FormDescription, 4 | FormField, 5 | FormItem, 6 | FormLabel, 7 | FormMessage, 8 | } from 'components/ui/form/form'; 9 | import { cn } from 'lib/utils'; 10 | import { ComponentPropsWithoutRef } from 'react'; 11 | import { ControllerProps, FieldPath, FieldValues } from 'react-hook-form'; 12 | import { Checkbox, CheckboxPrimitiveRoot } from './checkbox'; 13 | 14 | interface InputControllerProps< 15 | TFieldValues extends FieldValues = FieldValues, 16 | TName extends FieldPath = FieldPath, 17 | > { 18 | label: string; 19 | description?: string; 20 | itemClassName?: string; 21 | checkboxControll: Omit, 'render'>; 22 | checkboxProps?: ComponentPropsWithoutRef; 23 | } 24 | 25 | export function CheckboxController< 26 | TFieldValues extends FieldValues = FieldValues, 27 | TName extends FieldPath = FieldPath, 28 | >(props: InputControllerProps) { 29 | const { 30 | checkboxControll, 31 | checkboxProps, 32 | label, 33 | itemClassName, 34 | description, 35 | ...rest 36 | } = props; 37 | 38 | return ( 39 | ( 41 | 47 | 48 | 54 | 55 | {label && ( 56 |
57 | {label} 58 |
59 | )} 60 | {description && {description}} 61 | 62 |
63 | )} 64 | {...checkboxControll} 65 | {...rest} 66 | /> 67 | ); 68 | } 69 | 70 | CheckboxController.defaultProps = { 71 | description: false, 72 | itemClassName: false, 73 | checkboxProps: {}, 74 | }; 75 | -------------------------------------------------------------------------------- /docs/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support and Help 2 | 3 | Need help getting started or using a project? Here's how. 4 | 5 | ## How to get help 6 | 7 | Generally, we do not use GitHub as a support forum. For any usage questions that are not specific to the project itself, please ask on [Stack Overflow](https://stackoverflow.com) instead. By doing so, you are more likely to quickly solve your problem, and you will allow anyone else with the same question to find the answer. This also allows maintainers to focus on improving the project for others. 8 | 9 | Please seek support in the following ways: 10 | 11 | 1. :book: **Read the documentation and other guides** for the project to see if you can figure it out on your own. These should be located in a root `docs/` directory. If there is an example project, explore that to learn how it works to see if you can answer your question. 12 | 13 | 1. :bulb: **Search for answers and ask questions on [Stack Overflow](https://stackoverflow.com).** This is the most appropriate place for debugging issues specific to your use of the project, or figuring out how to use the project in a specific way. 14 | 15 | 1. :memo: As a **last resort**, you may open an issue on GitHub to ask for help. However, please clearly explain what you are trying to do, and list what you have already attempted to solve the problem. Provide code samples, but **do not** attach your entire project for someone else to debug. Review our [contributing guidelines](https://github.com/jessesquires/.github/blob/master/CONTRIBUTING.md). 16 | 17 | ## What NOT to do 18 | 19 | Please **do not** do any the following: 20 | 21 | 1. :x: Do not reach out to the author or contributor on Twitter (or other social media) by tweeting or sending a direct message. 22 | 23 | 1. :x: Do not email the author or contributor. 24 | 25 | 1. :x: Do not open duplicate issues or litter an existing issue with +1's. 26 | 27 | These are not appropriate avenues for seeking help or support with an open-source project. Please follow the guidelines in the previous section. Public questions get public answers, which benefits everyone in the community. ✌️ 28 | 29 | ## Customer Support 30 | 31 | I do not provide any sort of "customer support" for open-source projects. However, I am available for hire. For details on contracting and consulting, visit [jessesquires.com/hire-me](https://www.jessesquires.com/hire-me/). -------------------------------------------------------------------------------- /src/components/ui/card/card.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from 'lib/utils'; 2 | import * as React from 'react'; 3 | 4 | const Card = React.forwardRef< 5 | HTMLDivElement, 6 | React.HTMLAttributes 7 | >(({ className, ...props }, ref) => ( 8 |
16 | )); 17 | Card.displayName = 'Card'; 18 | 19 | const CardHeader = React.forwardRef< 20 | HTMLDivElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 |
28 | )); 29 | CardHeader.displayName = 'CardHeader'; 30 | 31 | const CardTitle = React.forwardRef< 32 | HTMLParagraphElement, 33 | React.HTMLAttributes 34 | >(({ className, children, ...props }, ref) => ( 35 |

43 | {children} 44 |

45 | )); 46 | 47 | CardTitle.displayName = 'CardTitle'; 48 | 49 | const CardDescription = React.forwardRef< 50 | HTMLParagraphElement, 51 | React.HTMLAttributes 52 | >(({ className, ...props }, ref) => ( 53 |

58 | )); 59 | 60 | CardDescription.displayName = 'CardDescription'; 61 | 62 | const CardFooter = React.forwardRef< 63 | HTMLDivElement, 64 | React.HTMLAttributes 65 | >(({ className, ...props }, ref) => ( 66 |

71 | )); 72 | 73 | CardFooter.displayName = 'CardFooter'; 74 | 75 | const CardContent = React.forwardRef< 76 | HTMLDivElement, 77 | React.HTMLAttributes 78 | >(({ className, ...props }, ref) => ( 79 |
80 | )); 81 | 82 | CardContent.displayName = 'CardContent'; 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardTitle, 88 | CardContent, 89 | CardDescription, 90 | CardFooter, 91 | }; 92 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | import * as AccordionPrimitive from '@radix-ui/react-accordion'; 2 | import { cn } from 'lib/utils'; 3 | import { ChevronDown } from 'lucide-react'; 4 | 5 | import * as React from 'react'; 6 | 7 | const Accordion = AccordionPrimitive.Root; 8 | 9 | type AccordionPrimitiveItem = typeof AccordionPrimitive.Item; 10 | const AccordionItem = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 19 | )); 20 | 21 | AccordionItem.displayName = 'AccordionItem'; 22 | 23 | type AccordionPrimitiverigger = typeof AccordionPrimitive.Trigger; 24 | 25 | const AccordionTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, children, ...props }, ref) => ( 29 | 30 | svg]:rotate-180', 34 | className 35 | )} 36 | {...props} 37 | > 38 | {children} 39 | 40 | 41 | 42 | )); 43 | 44 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 45 | 46 | type AccordionContentPrimitive = typeof AccordionPrimitive.Content; 47 | 48 | const AccordionContent = React.forwardRef< 49 | React.ElementRef, 50 | React.ComponentPropsWithoutRef 51 | >(({ className, children, ...props }, ref) => ( 52 | 57 |
{children}
58 |
59 | )); 60 | 61 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 62 | 63 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 64 | -------------------------------------------------------------------------------- /src/components/ui/button/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from '@radix-ui/react-slot'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | import { cn } from 'lib/utils'; 4 | import * as React from 'react'; 5 | 6 | const buttonVariants = cva( 7 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md', 8 | { 9 | variants: { 10 | variant: { 11 | primary: 'bg-primary text-primary-foreground hover:bg-primary/90', 12 | destructive: 13 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 14 | outline: 15 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground ', 16 | secondary: 17 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80', 18 | info: 'bg-sky-500/100 text-primary-foreground hover:bg-sky-600', 19 | ghost: 'bg-transparent', 20 | link: 'text-primary underline-offset-4 hover:underline', 21 | }, 22 | size: { 23 | default: 'h-10 px-4 py-2', 24 | icon: 'h-10 w-10', 25 | xs: 'h-6 px-2', 26 | sm: 'h-9 px-3', 27 | md: 'h-11 px-5', 28 | lg: 'h-13 px-7', 29 | xl: 'h-15 px-9', 30 | }, 31 | rounded: { 32 | default: 'rounded-md', 33 | xs: 'rounded-xs', 34 | sm: 'rounded-sm', 35 | md: 'rounded-md', 36 | lg: 'rounded-lg', 37 | xl: 'rounded-xl', 38 | }, 39 | fullWidth: { 40 | true: 'w-full', 41 | }, 42 | roundedFull: { 43 | true: 'rounded-full', 44 | }, 45 | }, 46 | defaultVariants: { 47 | variant: 'primary', 48 | size: 'default', 49 | fullWidth: false, 50 | roundedFull: false, 51 | }, 52 | } 53 | ); 54 | 55 | export interface ButtonProps 56 | extends React.ButtonHTMLAttributes, 57 | VariantProps { 58 | asChild?: boolean; 59 | } 60 | 61 | const Button = React.forwardRef( 62 | ({ className, fullWidth, variant, size, asChild = false, ...props }, ref) => { 63 | const Comp = asChild ? Slot : 'button'; 64 | return ( 65 | 70 | ); 71 | } 72 | ); 73 | Button.displayName = 'Button'; 74 | Button.defaultProps = { asChild: false }; 75 | 76 | export { Button, buttonVariants }; 77 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | const config = { 4 | darkMode: ['class'], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | prefix: '', 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: '2rem', 16 | screens: { 17 | '2xl': '1400px', 18 | }, 19 | }, 20 | extend: { 21 | colors: { 22 | blue: '#2997FF', 23 | gray: { 24 | DEFAULT: '#86868b', 25 | 100: '#94928d', 26 | 200: '#afafaf', 27 | 300: '#42424570', 28 | }, 29 | zinc: '#101010', 30 | border: 'hsl(var(--border))', 31 | input: 'hsl(var(--input))', 32 | ring: 'hsl(var(--ring))', 33 | background: 'hsl(var(--background))', 34 | foreground: 'hsl(var(--foreground))', 35 | primary: { 36 | DEFAULT: 'hsl(var(--primary))', 37 | foreground: 'hsl(var(--primary-foreground))', 38 | }, 39 | secondary: { 40 | DEFAULT: 'hsl(var(--secondary))', 41 | foreground: 'hsl(var(--secondary-foreground))', 42 | }, 43 | destructive: { 44 | DEFAULT: 'hsl(var(--destructive))', 45 | foreground: 'hsl(var(--destructive-foreground))', 46 | }, 47 | muted: { 48 | DEFAULT: 'hsl(var(--muted))', 49 | foreground: 'hsl(var(--muted-foreground))', 50 | }, 51 | accent: { 52 | DEFAULT: 'hsl(var(--accent))', 53 | foreground: 'hsl(var(--accent-foreground))', 54 | }, 55 | popover: { 56 | DEFAULT: 'hsl(var(--popover))', 57 | foreground: 'hsl(var(--popover-foreground))', 58 | }, 59 | card: { 60 | DEFAULT: 'hsl(var(--card))', 61 | foreground: 'hsl(var(--card-foreground))', 62 | }, 63 | }, 64 | borderRadius: { 65 | lg: 'var(--radius)', 66 | md: 'calc(var(--radius) - 2px)', 67 | sm: 'calc(var(--radius) - 4px)', 68 | }, 69 | keyframes: { 70 | 'accordion-down': { 71 | from: { height: '0' }, 72 | to: { height: 'var(--radix-accordion-content-height)' }, 73 | }, 74 | 'accordion-up': { 75 | from: { height: 'var(--radix-accordion-content-height)' }, 76 | to: { height: '0' }, 77 | }, 78 | }, 79 | animation: { 80 | 'accordion-down': 'accordion-down 0.2s ease-out', 81 | 'accordion-up': 'accordion-up 0.2s ease-out', 82 | }, 83 | }, 84 | }, 85 | plugins: [require('tailwindcss-animate')], 86 | } satisfies Config; 87 | 88 | export default config; 89 | -------------------------------------------------------------------------------- /src/components/ui/button/stories/button.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { fn } from '@storybook/test'; 3 | import { Button, ButtonProps } from 'components/ui/button/button'; 4 | import { SIZES, VARIANT } from 'constant/themes'; 5 | 6 | type ButtonPageProps = React.ComponentProps & { 7 | footer?: string; 8 | }; 9 | 10 | const meta = { 11 | component: Button, 12 | title: 'Buttons/Button', 13 | render: ({ footer, ...args }) => ( 14 | <> 15 | 16 |
{footer}
17 | 18 | ), 19 | tags: ['autodocs'], 20 | // decorators: [ 21 | // (story) => {story()}, 22 | // ], 23 | args: { children: 'button' }, 24 | argTypes: { 25 | variant: { 26 | options: Object.keys(VARIANT), 27 | control: { type: 'radio' }, 28 | }, 29 | children: { control: 'text' }, 30 | onClick: { action: fn() }, 31 | fullWidth: { active: { control: 'boolean' } }, 32 | disabled: { control: 'boolean' }, 33 | asChild: { table: { disable: true } }, 34 | size: { 35 | options: Object.keys(SIZES), 36 | control: { type: 'radio' }, 37 | }, 38 | }, 39 | parameters: { 40 | controls: { expanded: true }, 41 | layout: 'centered', 42 | }, 43 | } satisfies Meta; 44 | 45 | export default meta; 46 | 47 | type Story = StoryObj; 48 | 49 | export const buttonDefaultArgs = { 50 | disabled: false, 51 | } as ButtonProps; 52 | 53 | export const Default: Story = { 54 | args: { 55 | ...buttonDefaultArgs, 56 | footer: 'Built with Storybook', 57 | }, 58 | }; 59 | 60 | export const Destructive: Story = { 61 | args: { 62 | ...buttonDefaultArgs, 63 | variant: 'destructive', 64 | }, 65 | }; 66 | 67 | export const Ghost: Story = { 68 | args: { 69 | ...buttonDefaultArgs, 70 | variant: 'ghost', 71 | }, 72 | }; 73 | export const Link: Story = { 74 | render: (args) => ( 75 | 78 | ), 79 | args: { 80 | ...buttonDefaultArgs, 81 | variant: 'link', 82 | }, 83 | }; 84 | export const Secondary: Story = { 85 | args: { 86 | ...buttonDefaultArgs, 87 | variant: 'secondary', 88 | }, 89 | }; 90 | export const Outline: Story = { 91 | args: { 92 | ...buttonDefaultArgs, 93 | variant: 'outline', 94 | }, 95 | }; 96 | export const Info: Story = { 97 | args: { 98 | ...buttonDefaultArgs, 99 | variant: 'info', 100 | }, 101 | }; 102 | 103 | Info.parameters = { 104 | viewport: { 105 | viewports: 'FHD', 106 | }, 107 | }; 108 | -------------------------------------------------------------------------------- /src/hooks/useEventListener.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | import type { RefObject } from 'react'; 4 | 5 | import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; 6 | 7 | // MediaQueryList Event based useEventListener interface 8 | function useEventListener( 9 | eventName: K, 10 | handler: (event: MediaQueryListEventMap[K]) => void, 11 | element: RefObject, 12 | options?: boolean | AddEventListenerOptions 13 | ): void; 14 | 15 | // Window Event based useEventListener interface 16 | function useEventListener( 17 | eventName: K, 18 | handler: (event: WindowEventMap[K]) => void, 19 | element?: undefined, 20 | options?: boolean | AddEventListenerOptions 21 | ): void; 22 | 23 | // Element Event based useEventListener interface 24 | function useEventListener< 25 | K extends keyof HTMLElementEventMap, 26 | T extends HTMLElement = HTMLDivElement, 27 | >( 28 | eventName: K, 29 | handler: (event: HTMLElementEventMap[K]) => void, 30 | element: RefObject, 31 | options?: boolean | AddEventListenerOptions 32 | ): void; 33 | 34 | // Document Event based useEventListener interface 35 | function useEventListener( 36 | eventName: K, 37 | handler: (event: DocumentEventMap[K]) => void, 38 | element: RefObject, 39 | options?: boolean | AddEventListenerOptions 40 | ): void; 41 | 42 | function useEventListener< 43 | KW extends keyof WindowEventMap, 44 | KH extends keyof HTMLElementEventMap, 45 | KM extends keyof MediaQueryListEventMap, 46 | T extends HTMLElement | MediaQueryList = HTMLElement, 47 | >( 48 | eventName: KW | KH | KM, 49 | handler: ( 50 | event: 51 | | WindowEventMap[KW] 52 | | HTMLElementEventMap[KH] 53 | | MediaQueryListEventMap[KM] 54 | | Event 55 | ) => void, 56 | element?: RefObject, 57 | options?: boolean | AddEventListenerOptions 58 | ) { 59 | // Create a ref that stores handler 60 | const savedHandler = useRef(handler); 61 | 62 | useIsomorphicLayoutEffect(() => { 63 | savedHandler.current = handler; 64 | }, [handler]); 65 | 66 | useEffect(() => { 67 | // Define the listening target 68 | const targetElement: T | Window = element?.current ?? window; 69 | 70 | if (!(targetElement && targetElement.addEventListener)) return; 71 | 72 | // Create event listener that calls handler function stored in ref 73 | const listener: typeof handler = (event) => { 74 | savedHandler.current(event); 75 | }; 76 | 77 | targetElement.addEventListener(eventName, listener, options); 78 | 79 | // Remove event listener on cleanup 80 | // eslint-disable-next-line consistent-return 81 | return () => { 82 | targetElement.removeEventListener(eventName, listener, options); 83 | }; 84 | }, [eventName, element, options]); 85 | } 86 | 87 | export { useEventListener }; 88 | -------------------------------------------------------------------------------- /src/components/common/query-components.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultError, 3 | QueryClient, 4 | QueryKey, 5 | useQuery, 6 | UseQueryOptions, 7 | UseQueryResult, 8 | } from '@tanstack/react-query'; 9 | import { Loading } from 'containers'; 10 | import { ReactNode } from 'react'; 11 | import { Code } from './code'; 12 | 13 | interface QueryComponentsProps< 14 | TData = unknown, 15 | TError = DefaultError, 16 | TQueryFnData = TData, // Adjusted the order of type parameters 17 | TQueryKey extends QueryKey = QueryKey, 18 | > { 19 | onSuccess?: ( 20 | data: TData, 21 | queryResult: UseQueryResult 22 | ) => ReactNode; 23 | onError?: (error: TError) => ReactNode; 24 | options: UseQueryOptions; // Adjusted the order of types 25 | queryClient?: QueryClient; 26 | showError?: boolean; 27 | } 28 | 29 | /** 30 | * QueryComponents is a reusable React component that handles data fetching using React Query. 31 | * It provides a flexible and consistent way to fetch data from an API and render it based on the success or failure of the query. 32 | * 33 | * @template TData The type of data returned by the query. 34 | * @template TError The type of error that can occur during the query. 35 | * @template TQueryFnData The type of data used by the query function. 36 | * @template TQueryKey The type of query key used to identify the query in React Query. 37 | * 38 | * @param {QueryComponentsProps} props The properties passed to the QueryComponents component. 39 | * @param {TData} props.onSuccess The function to render the UI when the query is successful. 40 | * @param {TError} [props.onError] Optional function to handle UI rendering when the query encounters an error. 41 | * @param {UseQueryOptions} props.options The options object used to configure the query, including the query key and query function. 42 | * @param {QueryClient} [props.queryClient] Optional query client instance to use for the query. 43 | * @param {boolean} [props.showError=false] Optional boolean flag to determine whether to show error messages. 44 | * 45 | * @returns {ReactNode} The JSX to render based on the query state. 46 | */ 47 | 48 | export function QueryComponents< 49 | TData = unknown, 50 | TError = DefaultError, 51 | TQueryKey extends QueryKey = QueryKey, 52 | >(props: QueryComponentsProps) { 53 | const { onSuccess, onError, options, queryClient, showError = false } = props; 54 | 55 | const query = useQuery(options, queryClient); 56 | 57 | const fallbackLoading = ; 58 | 59 | if (query.isLoading) return fallbackLoading; 60 | if (query.error && onError) return onError(query.error); 61 | if (query.error && showError) return ; 62 | if (query.data) return onSuccess?.(query.data, query); 63 | } 64 | 65 | QueryComponents.defaultProps = { 66 | onSuccess: {}, 67 | onError: {}, 68 | showError: false, 69 | queryClient: {}, 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/ui/card/Card.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { 3 | Card, 4 | CardContent, 5 | CardDescription, 6 | CardFooter, 7 | CardHeader, 8 | CardTitle, 9 | } from 'components/ui/card/card'; 10 | import { BellRing, Check } from 'lucide-react'; 11 | import { Button } from '../button/button'; 12 | 13 | const meta: Meta = { 14 | title: 'Ui/Card', 15 | tags: ['autodocs'], 16 | component: Card, 17 | args: { 18 | children: 'Card', 19 | }, 20 | argTypes: {}, 21 | }; 22 | 23 | const notifications = [ 24 | { 25 | title: 'Your call has been confirmed.', 26 | description: '1 hour ago', 27 | }, 28 | { 29 | title: 'You have a new message!', 30 | description: '1 hour ago', 31 | }, 32 | { 33 | title: 'Your subscription is expiring soon!', 34 | description: '2 hours ago', 35 | }, 36 | ]; 37 | 38 | type Story = StoryObj; 39 | 40 | export const Default: Story = { 41 | render: (args) => ( 42 | <> 43 | 44 | 45 | Notifications 46 | You have 3 unread messages. 47 | 48 | 49 |
50 | 51 |
52 |

53 | Push Notifications 54 |

55 |

56 | Send notifications to device. 57 |

58 |
59 |
60 |
61 | {notifications.map((notification) => ( 62 |
66 | 67 |
68 |

69 | {notification.title} 70 |

71 |

72 | {notification.description} 73 |

74 |
75 |
76 | ))} 77 |
78 |
79 | 80 | 83 | 84 |
85 | 95 | 96 | ), 97 | }; 98 | 99 | export default meta; 100 | -------------------------------------------------------------------------------- /src/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from '@radix-ui/react-slot'; 2 | import { cn } from 'lib/utils'; 3 | import { ChevronRight, MoreHorizontal } from 'lucide-react'; 4 | 5 | import * as React from 'react'; 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<'nav'> & { 10 | separator?: React.ReactNode; 11 | } 12 | >(({ ...props }, ref) =>