├── public ├── avatars │ ├── 01.png │ ├── 02.png │ ├── 03.png │ ├── 04.png │ ├── 05.png │ └── shadcn.jpg ├── favicon.ico └── images │ ├── favicon.png │ ├── shadcn-admin.png │ └── favicon.svg ├── .prettierignore ├── CHANGELOG.md ├── app ├── routes │ ├── _authenticated+ │ │ ├── help-center │ │ │ └── route.tsx │ │ ├── tasks+ │ │ │ ├── _index │ │ │ │ ├── config │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── components │ │ │ │ │ ├── data-table-view-options.tsx │ │ │ │ │ ├── search-input.tsx │ │ │ │ │ ├── data-table-toolbar.tsx │ │ │ │ │ ├── data-table-column-header.tsx │ │ │ │ │ ├── data-table.tsx │ │ │ │ │ └── data-table-row-actions.tsx │ │ │ │ ├── route.tsx │ │ │ │ └── queries.server.ts │ │ │ ├── _shared │ │ │ │ └── data │ │ │ │ │ ├── data.tsx │ │ │ │ │ └── schema.ts │ │ │ ├── _layout │ │ │ │ ├── route.tsx │ │ │ │ └── hooks │ │ │ │ │ └── use-breadcrumbs.tsx │ │ │ ├── $task.label │ │ │ │ └── route.ts │ │ │ ├── create │ │ │ │ └── route.tsx │ │ │ ├── $task.delete │ │ │ │ └── route.tsx │ │ │ ├── $task._index │ │ │ │ └── route.tsx │ │ │ └── import │ │ │ │ └── route.tsx │ │ ├── settings+ │ │ │ ├── _layout │ │ │ │ ├── components │ │ │ │ │ ├── content-section.tsx │ │ │ │ │ └── sidebar-nav.tsx │ │ │ │ └── route.tsx │ │ │ ├── display │ │ │ │ ├── route.tsx │ │ │ │ └── display-form.tsx │ │ │ ├── appearance │ │ │ │ └── route.tsx │ │ │ ├── notifications │ │ │ │ └── route.tsx │ │ │ ├── account │ │ │ │ └── route.tsx │ │ │ └── _index │ │ │ │ └── route.tsx │ │ ├── users+ │ │ │ ├── _shared │ │ │ │ └── data │ │ │ │ │ ├── users.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── data.ts │ │ │ ├── _layout │ │ │ │ ├── components │ │ │ │ │ ├── search-input.tsx │ │ │ │ │ ├── data-table-view-options.tsx │ │ │ │ │ ├── data-table-row-actions.tsx │ │ │ │ │ ├── data-table-column-header.tsx │ │ │ │ │ └── data-table-toolbar.tsx │ │ │ │ ├── queries.server.ts │ │ │ │ └── route.tsx │ │ │ ├── invite │ │ │ │ └── route.tsx │ │ │ ├── $user.delete │ │ │ │ ├── route.tsx │ │ │ │ └── components │ │ │ │ │ └── users-delete-dialog.tsx │ │ │ ├── add │ │ │ │ └── route.tsx │ │ │ └── $user.update │ │ │ │ └── route.tsx │ │ ├── _layout │ │ │ └── route.tsx │ │ ├── _index │ │ │ └── components │ │ │ │ ├── overview.tsx │ │ │ │ └── recent-sales.tsx │ │ └── apps │ │ │ └── data │ │ │ └── apps.tsx │ ├── _errors+ │ │ ├── 503.tsx │ │ ├── 403.tsx │ │ ├── 401.tsx │ │ ├── 404.tsx │ │ └── 500.tsx │ └── _auth+ │ │ ├── _layout │ │ └── route.tsx │ │ ├── otp │ │ ├── route.tsx │ │ └── components │ │ │ └── otp-form.tsx │ │ ├── forgot-password │ │ ├── components │ │ │ └── forgot-password-form.tsx │ │ └── route.tsx │ │ ├── sign-in │ │ ├── route.tsx │ │ └── components │ │ │ └── user-auth-form.tsx │ │ └── sign-up │ │ └── route.tsx ├── components │ ├── ui │ │ ├── stack.tsx │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── textarea.tsx │ │ ├── collapsible.tsx │ │ ├── sonner.tsx │ │ ├── input.tsx │ │ ├── switch.tsx │ │ ├── checkbox.tsx │ │ ├── avatar.tsx │ │ ├── radio-group.tsx │ │ ├── popover.tsx │ │ ├── card.tsx │ │ ├── badge.tsx │ │ ├── scroll-area.tsx │ │ ├── alert.tsx │ │ ├── tabs.tsx │ │ ├── tooltip.tsx │ │ ├── button.tsx │ │ ├── input-otp.tsx │ │ ├── table.tsx │ │ └── breadcrumb.tsx │ ├── theme-provider.tsx │ ├── layout │ │ ├── main.tsx │ │ ├── types.ts │ │ ├── app-sidebar.tsx │ │ ├── header.tsx │ │ ├── top-nav.tsx │ │ └── team-switcher.tsx │ ├── coming-soon.tsx │ ├── search.tsx │ ├── password-input.tsx │ ├── profile-dropdown.tsx │ ├── theme-switch.tsx │ ├── long-text.tsx │ ├── confirm-dialog.tsx │ └── command-menu.tsx ├── lib │ └── utils.ts ├── routes.ts ├── hooks │ ├── use-debounce.ts │ ├── use-dialog-state.tsx │ ├── use-mobile.ts │ ├── use-mobile.tsx │ └── use-smart-navigation.ts ├── context │ └── search-context.tsx ├── assets │ └── vite.svg └── root.tsx ├── prettier.config.js ├── react-router.config.ts ├── vite.config.ts ├── .gitignore ├── components.json ├── biome.json ├── .github ├── workflows │ └── ci.yml └── FUNDING.yml ├── LICENSE ├── tsconfig.json ├── README.md └── package.json /public/avatars/01.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/avatars/02.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/avatars/03.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/avatars/04.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/avatars/05.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/avatars/shadcn.jpg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | build 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coji/shadcn-admin-react-router/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.0.0 (2024-12-17) 2 | 3 | ### Refactor 4 | 5 | - Rewritten to use React Router v7 6 | -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coji/shadcn-admin-react-router/HEAD/public/images/favicon.png -------------------------------------------------------------------------------- /public/images/shadcn-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coji/shadcn-admin-react-router/HEAD/public/images/shadcn-admin.png -------------------------------------------------------------------------------- /app/routes/_authenticated+/help-center/route.tsx: -------------------------------------------------------------------------------- 1 | import ComingSoon from '~/components/coming-soon' 2 | 3 | export default ComingSoon 4 | -------------------------------------------------------------------------------- /app/routes/_authenticated+/tasks+/_index/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './columns' 2 | export * from './constants' 3 | export * from './schema' 4 | export * from './types' 5 | -------------------------------------------------------------------------------- /app/components/ui/stack.tsx: -------------------------------------------------------------------------------- 1 | import { twc } from 'react-twc' 2 | 3 | export const Stack = twc.div`flex flex-col gap-2` 4 | export const HStack = twc.div`flex flex-row gap-2 items-center` 5 | -------------------------------------------------------------------------------- /app/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | semi: false, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'], 6 | tailwindFunctions: ['twMerge'], 7 | } 8 | -------------------------------------------------------------------------------- /react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@react-router/dev/config' 2 | import { vercelPreset } from '@vercel/react-router/vite' 3 | 4 | export default { 5 | ssr: true, 6 | presets: [vercelPreset()], 7 | } satisfies Config 8 | -------------------------------------------------------------------------------- /app/routes/_authenticated+/tasks+/_shared/data/data.tsx: -------------------------------------------------------------------------------- 1 | export const labels = [ 2 | { 3 | value: 'bug', 4 | label: 'Bug', 5 | }, 6 | { 7 | value: 'feature', 8 | label: 'Feature', 9 | }, 10 | { 11 | value: 'documentation', 12 | label: 'Documentation', 13 | }, 14 | ] 15 | -------------------------------------------------------------------------------- /app/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | import type { ThemeProviderProps } from 'next-themes' 2 | import { ThemeProvider as NextThemesProvider } from 'next-themes' 3 | 4 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 5 | return {children} 6 | } 7 | -------------------------------------------------------------------------------- /app/routes.ts: -------------------------------------------------------------------------------- 1 | import { remixRoutesOptionAdapter } from '@react-router/remix-routes-option-adapter' 2 | import { flatRoutes } from 'remix-flat-routes' 3 | 4 | export default remixRoutesOptionAdapter((defineRotue) => 5 | flatRoutes('routes', defineRotue, { 6 | ignoredRouteFiles: ['**/_shared/**', '**/index.ts'], 7 | }), 8 | ) 9 | -------------------------------------------------------------------------------- /app/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '~/lib/utils' 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from '@react-router/dev/vite' 2 | import tailwindcss from '@tailwindcss/vite' 3 | import { defineConfig } from 'vite' 4 | import devtoolsJson from 'vite-plugin-devtools-json' 5 | import tsconfigPaths from 'vite-tsconfig-paths' 6 | 7 | export default defineConfig({ 8 | plugins: [devtoolsJson(), tailwindcss(), reactRouter(), tsconfigPaths()], 9 | }) 10 | -------------------------------------------------------------------------------- /.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 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .react-router/ 27 | build/ 28 | .vercel 29 | -------------------------------------------------------------------------------- /app/routes/_authenticated+/tasks+/_shared/data/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | // We're keeping a simple non-relational schema here. 4 | // IRL, you will have a schema for your data models. 5 | export const taskSchema = z.object({ 6 | id: z.string(), 7 | title: z.string(), 8 | status: z.string(), 9 | label: z.string(), 10 | priority: z.string(), 11 | }) 12 | 13 | export type Task = z.infer 14 | -------------------------------------------------------------------------------- /app/routes/_authenticated+/tasks+/_index/config/types.ts: -------------------------------------------------------------------------------- 1 | import type { z } from 'zod' 2 | import type { 3 | FilterSchema, 4 | PaginationSchema, 5 | SearchSchema, 6 | SortSchema, 7 | } from './schema' 8 | 9 | export type Search = z.infer 10 | export type Filters = z.infer 11 | export type Sort = z.infer 12 | export type Pagination = z.infer 13 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "~/components", 15 | "utils": "~/lib/utils", 16 | "ui": "~/components/ui", 17 | "lib": "~/lib", 18 | "hooks": "~/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /public/images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/hooks/use-debounce.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react' 2 | 3 | type Debounce = (fn: () => void) => void 4 | 5 | export const useDebounce = (timeout = 500): Debounce => { 6 | const timer = useRef | null>(null) 7 | const debounce: Debounce = useCallback( 8 | (fn) => { 9 | if (timer.current) { 10 | clearTimeout(timer.current) 11 | } 12 | timer.current = setTimeout(() => { 13 | fn() 14 | }, timeout) 15 | }, 16 | [timeout], 17 | ) 18 | return debounce 19 | } 20 | -------------------------------------------------------------------------------- /app/components/layout/main.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import { cn } from '~/lib/utils' 3 | 4 | interface MainProps extends React.ComponentPropsWithRef<'main'> { 5 | fixed?: boolean 6 | } 7 | 8 | export const Main = ({ fixed, ...props }: MainProps) => { 9 | return ( 10 |
18 | ) 19 | } 20 | 21 | Main.displayName = 'Main' 22 | -------------------------------------------------------------------------------- /app/components/coming-soon.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlanet } from '@tabler/icons-react' 2 | 3 | export default function ComingSoon() { 4 | return ( 5 |
6 |
7 | 8 |

Coming Soon 👀

9 |

10 | This page has not been created yet.
11 | Stay tuned though! 12 |

13 |
14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/hooks/use-dialog-state.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | /** 4 | * Custom hook for confirm dialog 5 | * @param initialState string | null 6 | * @returns A stateful value, and a function to update it. 7 | * @example const [open, setOpen] = useDialogState<"approve" | "reject">() 8 | */ 9 | export default function useDialogState( 10 | initialState: T | null = null, 11 | ) { 12 | const [open, _setOpen] = useState(initialState) 13 | 14 | const setOpen = (str: T | null) => 15 | _setOpen((prev) => (prev === str ? null : str)) 16 | 17 | return [open, setOpen] as const 18 | } 19 | -------------------------------------------------------------------------------- /app/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener('change', onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener('change', onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /app/hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener('change', onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener('change', onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /app/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import { Label as LabelPrimitive } from 'radix-ui' 2 | import type * as React from 'react' 3 | 4 | import { cn } from '~/lib/utils' 5 | 6 | function Label({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ) 20 | } 21 | 22 | export { Label } 23 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json", 3 | "files": { 4 | "includes": ["**", "!app/index.css"] 5 | }, 6 | "assist": { "actions": { "source": { "organizeImports": "off" } } }, 7 | "vcs": { 8 | "enabled": true, 9 | "clientKind": "git", 10 | "useIgnoreFile": true 11 | }, 12 | "linter": { 13 | "enabled": true, 14 | "rules": { 15 | "recommended": true, 16 | "suspicious": { 17 | "useAwait": "error", 18 | "noArrayIndexKey": "off" 19 | }, 20 | "correctness": { 21 | "useUniqueElementIds": "off", 22 | "noNestedComponentDefinitions": "off" 23 | }, 24 | "a11y": { 25 | "noSvgWithoutTitle": "off" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/routes/_errors+/503.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '~/components/ui/button' 2 | 3 | export default function MaintenanceError() { 4 | return ( 5 |
6 |
7 |

503

8 | Website is under maintenance! 9 |

10 | The site is not available at the moment.
11 | We'll be back online shortly. 12 |

13 |
14 | 15 |
16 |
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import { Separator as SeparatorPrimitive } from 'radix-ui' 2 | import type * as React from 'react' 3 | 4 | import { cn } from '~/lib/utils' 5 | 6 | function Separator({ 7 | className, 8 | orientation = 'horizontal', 9 | decorative = true, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 23 | ) 24 | } 25 | 26 | export { Separator } 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | install-lint-build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | 25 | - name: Install pnpm 26 | run: npm install -g pnpm 27 | 28 | - name: Install dependencies 29 | run: pnpm install 30 | 31 | - name: Lint the code 32 | run: pnpm lint 33 | 34 | - name: Run Prettier check 35 | run: pnpm format 36 | 37 | - name: Build the project 38 | run: pnpm build 39 | -------------------------------------------------------------------------------- /app/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from 'react' 2 | 3 | import { cn } from '~/lib/utils' 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) { 6 | return ( 7 |