├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── app ├── assets │ └── vite.svg ├── components │ ├── coming-soon.tsx │ ├── command-menu.tsx │ ├── confirm-dialog.tsx │ ├── layout │ │ ├── app-sidebar.tsx │ │ ├── header.tsx │ │ ├── main.tsx │ │ ├── nav-group.tsx │ │ ├── nav-user.tsx │ │ ├── team-switcher.tsx │ │ ├── top-nav.tsx │ │ └── types.ts │ ├── long-text.tsx │ ├── password-input.tsx │ ├── pin-input.tsx │ ├── profile-dropdown.tsx │ ├── search.tsx │ ├── theme-provider.tsx │ ├── theme-switch.tsx │ └── ui │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── radio-group.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── stack.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx ├── context │ └── search-context.tsx ├── data │ └── sidebar-data.ts ├── hooks │ ├── use-debounce.ts │ ├── use-dialog-state.tsx │ ├── use-mobile.ts │ ├── use-mobile.tsx │ └── use-smart-navigation.ts ├── index.css ├── lib │ └── utils.ts ├── root.tsx ├── routes.ts └── routes │ ├── _auth+ │ ├── _layout │ │ └── route.tsx │ ├── forgot-password │ │ ├── components │ │ │ └── forgot-password-form.tsx │ │ └── route.tsx │ ├── otp │ │ ├── components │ │ │ └── otp-form.tsx │ │ └── route.tsx │ ├── sign-in-2 │ │ ├── components │ │ │ └── user-auth-form.tsx │ │ └── route.tsx │ ├── sign-in │ │ ├── components │ │ │ └── user-auth-form.tsx │ │ └── route.tsx │ └── sign-up │ │ ├── components │ │ └── sign-up-form.tsx │ │ └── route.tsx │ ├── _authenticated+ │ ├── _index │ │ ├── components │ │ │ ├── overview.tsx │ │ │ └── recent-sales.tsx │ │ └── route.tsx │ ├── _layout │ │ └── route.tsx │ ├── apps │ │ ├── data │ │ │ └── apps.tsx │ │ └── route.tsx │ ├── chats │ │ ├── data │ │ │ └── convo.json │ │ └── route.tsx │ ├── help-center │ │ └── route.tsx │ ├── settings+ │ │ ├── _index │ │ │ ├── profile-form.tsx │ │ │ └── route.tsx │ │ ├── _layout │ │ │ ├── components │ │ │ │ ├── content-section.tsx │ │ │ │ └── sidebar-nav.tsx │ │ │ └── route.tsx │ │ ├── account │ │ │ ├── account-form.tsx │ │ │ └── route.tsx │ │ ├── appearance │ │ │ ├── appearance-form.tsx │ │ │ └── route.tsx │ │ ├── display │ │ │ ├── display-form.tsx │ │ │ └── route.tsx │ │ └── notifications │ │ │ ├── notifications-form.tsx │ │ │ └── route.tsx │ ├── tasks+ │ │ ├── $task._index │ │ │ └── route.tsx │ │ ├── $task.delete │ │ │ └── route.tsx │ │ ├── $task.label │ │ │ └── route.ts │ │ ├── _index │ │ │ ├── components │ │ │ │ ├── data-table-column-header.tsx │ │ │ │ ├── data-table-faceted-filter.tsx │ │ │ │ ├── data-table-pagination.tsx │ │ │ │ ├── data-table-row-actions.tsx │ │ │ │ ├── data-table-toolbar.tsx │ │ │ │ ├── data-table-view-options.tsx │ │ │ │ ├── data-table.tsx │ │ │ │ └── search-input.tsx │ │ │ ├── config │ │ │ │ ├── columns.tsx │ │ │ │ ├── constants.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── types.ts │ │ │ ├── hooks │ │ │ │ └── use-data-table-state.ts │ │ │ ├── queries.server.ts │ │ │ └── route.tsx │ │ ├── _layout │ │ │ ├── hooks │ │ │ │ └── use-breadcrumbs.tsx │ │ │ └── route.tsx │ │ ├── _shared │ │ │ ├── components │ │ │ │ └── tasks-mutate-form.tsx │ │ │ └── data │ │ │ │ ├── data.tsx │ │ │ │ ├── schema.ts │ │ │ │ └── tasks.ts │ │ ├── create │ │ │ └── route.tsx │ │ └── import │ │ │ └── route.tsx │ └── users+ │ │ ├── $user.delete │ │ ├── components │ │ │ └── users-delete-dialog.tsx │ │ └── route.tsx │ │ ├── $user.update │ │ └── route.tsx │ │ ├── _layout │ │ ├── components │ │ │ ├── data-table-column-header.tsx │ │ │ ├── data-table-faceted-filter.tsx │ │ │ ├── data-table-pagination.tsx │ │ │ ├── data-table-row-actions.tsx │ │ │ ├── data-table-toolbar.tsx │ │ │ ├── data-table-view-options.tsx │ │ │ ├── search-input.tsx │ │ │ ├── users-columns.tsx │ │ │ └── users-table.tsx │ │ ├── hooks │ │ │ └── use-data-table-state.ts │ │ ├── queries.server.ts │ │ └── route.tsx │ │ ├── _shared │ │ ├── components │ │ │ └── users-action-dialog.tsx │ │ └── data │ │ │ ├── data.ts │ │ │ ├── schema.ts │ │ │ └── users.ts │ │ ├── add │ │ └── route.tsx │ │ └── invite │ │ ├── components │ │ └── users-invite-dialog.tsx │ │ └── route.tsx │ └── _errors+ │ ├── 401.tsx │ ├── 403.tsx │ ├── 404.tsx │ ├── 500.tsx │ └── 503.tsx ├── biome.json ├── components.json ├── package.json ├── pnpm-lock.yaml ├── prettier.config.js ├── public ├── avatars │ ├── 01.png │ ├── 02.png │ ├── 03.png │ ├── 04.png │ ├── 05.png │ └── shadcn.jpg ├── favicon.ico └── images │ ├── favicon.png │ ├── favicon.svg │ └── shadcn-admin.png ├── react-router.config.ts ├── server └── app.ts ├── tsconfig.json └── vite.config.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [satnaing] 2 | buy_me_a_coffee: satnaing 3 | # patreon: # Replace with a single Patreon username 4 | # open_collective: # Replace with a single Open Collective username 5 | # ko_fi: # Replace with a single Ko-fi username 6 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 7 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 8 | # liberapay: # Replace with a single Liberapay username 9 | # issuehunt: # Replace with a single IssueHunt username 10 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 11 | # polar: # Replace with a single Polar username 12 | # thanks_dev: # Replace with a single thanks.dev username 13 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.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: 18 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:check 36 | 37 | - name: Build the project 38 | run: pnpm build 39 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | build 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.0.0 (2024-12-17) 2 | 3 | ### Refactor 4 | 5 | - Rewritten to use React Router v7 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Coji Mizoguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shadcn Admin Dashboard 2 | 3 | Admin Dashboard UI built with Shadcn and React Router v7. Built with responsiveness and accessibility in mind. 4 | 5 | ![alt text](public/images/shadcn-admin.png) 6 | 7 | I've been creating dashboard UIs at work and for my personal projects. I always wanted to make a reusable collection of dashboard UI for future projects; and here it is now. While I've created a few custom components, some of the code is directly adapted from ShadcnUI examples. 8 | 9 | > This is not a starter project (template) though. I'll probably make one in the future. 10 | 11 | ## Features 12 | 13 | - Light/dark mode 14 | - Responsive 15 | - Accessible 16 | - With built-in Sidebar component 17 | - Global Search Command 18 | - 10+ pages 19 | - Extra custom components 20 | 21 | ## Tech Stack 22 | 23 | **UI:** [ShadcnUI](https://ui.shadcn.com) (TailwindCSS + RadixUI) 24 | 25 | **Build Tool:** [Vite](https://vitejs.dev/) 26 | 27 | **Routing:** [React Router v7](https://reactrouter.com/en/main) (Framework) 28 | 29 | **Form Validation:** [Conform](https://conform.guide/) 30 | 31 | **Type Checking:** [TypeScript](https://www.typescriptlang.org/) 32 | 33 | **Linting/Formatting:** [Biome](https://biomejs.dev/) & [Prettier](https://prettier.io/) 34 | 35 | **Icons:** [Tabler Icons](https://tabler.io/icons) 36 | 37 | ## Run Locally 38 | 39 | Clone the project 40 | 41 | ```bash 42 | git clone https://github.com/coji/shadcn-admin-react-router.git 43 | ``` 44 | 45 | Go to the project directory 46 | 47 | ```bash 48 | cd shadcn-admin-react-router 49 | ``` 50 | 51 | Install dependencies 52 | 53 | ```bash 54 | pnpm install 55 | ``` 56 | 57 | Start the server 58 | 59 | ```bash 60 | pnpm run dev 61 | ``` 62 | 63 | ## Author 64 | 65 | Crafted with 🤍 by [@coji](https://github.com/coji) 66 | 67 | This project is a fork of [shadcn-admin](https://github.com/satnaing/shadcn-admin) by [@satnaing](https://github.com/satnaing). Thanks for the great original work! 68 | 69 | ## License 70 | 71 | Licensed under the [MIT License](https://choosealicense.com/licenses/mit/) 72 | -------------------------------------------------------------------------------- /app/assets/vite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /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/components/command-menu.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | IconArrowRightDashed, 3 | IconDeviceLaptop, 4 | IconMoon, 5 | IconSun, 6 | } from '@tabler/icons-react' 7 | import { useTheme } from 'next-themes' 8 | import React from 'react' 9 | import { useNavigate } from 'react-router' 10 | import { 11 | CommandDialog, 12 | CommandEmpty, 13 | CommandGroup, 14 | CommandInput, 15 | CommandItem, 16 | CommandList, 17 | CommandSeparator, 18 | } from '~/components/ui/command' 19 | import { useSearch } from '~/context/search-context' 20 | import { sidebarData } from '~/data/sidebar-data' 21 | import { ScrollArea } from './ui/scroll-area' 22 | 23 | export function CommandMenu() { 24 | const navigate = useNavigate() 25 | const { setTheme } = useTheme() 26 | const { open, setOpen } = useSearch() 27 | 28 | const runCommand = React.useCallback( 29 | (command: () => unknown) => { 30 | setOpen(false) 31 | command() 32 | }, 33 | [setOpen], 34 | ) 35 | 36 | return ( 37 | 38 | 39 | 40 | 41 | No results found. 42 | {sidebarData.navGroups.map((group) => ( 43 | 44 | {group.items.map((navItem, i) => { 45 | if (navItem.url) 46 | return ( 47 | { 51 | runCommand(() => navigate(navItem.url as string)) 52 | }} 53 | > 54 |
55 | 56 |
57 | {navItem.title} 58 |
59 | ) 60 | 61 | return navItem.items?.map((subItem, i) => ( 62 | { 66 | runCommand(() => navigate(subItem.url as string)) 67 | }} 68 | > 69 |
70 | 71 |
72 | {subItem.title} 73 |
74 | )) 75 | })} 76 |
77 | ))} 78 | 79 | 80 | runCommand(() => setTheme('light'))}> 81 | Light 82 | 83 | runCommand(() => setTheme('dark'))}> 84 | 85 | Dark 86 | 87 | runCommand(() => setTheme('system'))}> 88 | 89 | System 90 | 91 | 92 |
93 |
94 |
95 | ) 96 | } 97 | -------------------------------------------------------------------------------- /app/components/confirm-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { Form, useNavigation, type FetcherWithComponents } from 'react-router' 2 | import { 3 | AlertDialog, 4 | AlertDialogCancel, 5 | AlertDialogContent, 6 | AlertDialogDescription, 7 | AlertDialogFooter, 8 | AlertDialogHeader, 9 | AlertDialogTitle, 10 | } from '~/components/ui/alert-dialog' 11 | import { Button } from '~/components/ui/button' 12 | import { cn } from '~/lib/utils' 13 | 14 | interface ConfirmDialogProps { 15 | open: boolean 16 | onOpenChange: (open: boolean) => void 17 | title: React.ReactNode 18 | disabled?: boolean 19 | desc: React.JSX.Element | string 20 | cancelBtnText?: string 21 | confirmText?: React.ReactNode 22 | destructive?: boolean 23 | isLoading?: boolean 24 | fetcher?: FetcherWithComponents 25 | action?: string 26 | className?: string 27 | children?: React.ReactNode 28 | } 29 | 30 | export function ConfirmDialog(props: ConfirmDialogProps) { 31 | const { 32 | title, 33 | desc, 34 | children, 35 | className, 36 | confirmText, 37 | cancelBtnText, 38 | destructive, 39 | isLoading, 40 | disabled = false, 41 | fetcher, 42 | action, 43 | ...actions 44 | } = props 45 | const navigation = useNavigation() 46 | 47 | return ( 48 | 49 | 50 | 51 | {title} 52 | 53 |
{desc}
54 |
55 |
56 | {children} 57 | 58 | 59 | {cancelBtnText ?? 'Cancel'} 60 | 61 | {fetcher ? ( 62 | 63 | 72 | 73 | ) : ( 74 |
75 | 84 |
85 | )} 86 |
87 |
88 |
89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /app/components/layout/app-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Sidebar, 3 | SidebarContent, 4 | SidebarFooter, 5 | SidebarHeader, 6 | SidebarRail, 7 | } from '~/components/ui/sidebar' 8 | import { sidebarData } from '~/data/sidebar-data' 9 | import { NavGroup } from './nav-group' 10 | import { NavUser } from './nav-user' 11 | import { TeamSwitcher } from './team-switcher' 12 | 13 | export function AppSidebar({ ...props }: React.ComponentProps) { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | {sidebarData.navGroups.map((props) => ( 21 | 22 | ))} 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /app/components/layout/header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Separator } from '~/components/ui/separator' 3 | import { SidebarTrigger } from '~/components/ui/sidebar' 4 | import { cn } from '~/lib/utils' 5 | 6 | interface HeaderProps extends React.ComponentPropsWithRef<'header'> { 7 | fixed?: boolean 8 | } 9 | 10 | export const Header = ({ 11 | className, 12 | fixed, 13 | children, 14 | ...props 15 | }: HeaderProps) => { 16 | const [offset, setOffset] = React.useState(0) 17 | 18 | React.useEffect(() => { 19 | const onScroll = () => { 20 | setOffset(document.body.scrollTop || document.documentElement.scrollTop) 21 | } 22 | 23 | // Add scroll listener to the body 24 | document.addEventListener('scroll', onScroll, { passive: true }) 25 | 26 | // Clean up the event listener on unmount 27 | return () => document.removeEventListener('scroll', onScroll) 28 | }, []) 29 | 30 | return ( 31 |
10 && fixed ? 'shadow-sm' : 'shadow-none', 36 | className, 37 | )} 38 | {...props} 39 | > 40 | 41 | 42 | {children} 43 |
44 | ) 45 | } 46 | 47 | Header.displayName = 'Header' 48 | -------------------------------------------------------------------------------- /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/layout/team-switcher.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronsUpDown, Plus } from 'lucide-react' 2 | import * as React from 'react' 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuItem, 7 | DropdownMenuLabel, 8 | DropdownMenuSeparator, 9 | DropdownMenuShortcut, 10 | DropdownMenuTrigger, 11 | } from '~/components/ui/dropdown-menu' 12 | import { 13 | SidebarMenu, 14 | SidebarMenuButton, 15 | SidebarMenuItem, 16 | useSidebar, 17 | } from '~/components/ui/sidebar' 18 | 19 | export function TeamSwitcher({ 20 | teams, 21 | }: { 22 | teams: { 23 | name: string 24 | logo: React.ElementType 25 | plan: string 26 | }[] 27 | }) { 28 | const { isMobile } = useSidebar() 29 | // biome-ignore lint/style/noNonNullAssertion: 30 | const [activeTeam, setActiveTeam] = React.useState(teams[0]!) 31 | 32 | return ( 33 | 34 | 35 | 36 | 37 | 41 |
42 | 43 |
44 |
45 | 46 | {activeTeam.name} 47 | 48 | {activeTeam.plan} 49 |
50 | 51 |
52 |
53 | 59 | 60 | Teams 61 | 62 | {teams.map((team, index) => ( 63 | setActiveTeam(team)} 66 | className="gap-2 p-2" 67 | > 68 |
69 | 70 |
71 | {team.name} 72 | ⌘{index + 1} 73 |
74 | ))} 75 | 76 | 77 |
78 | 79 |
80 |
Add team
81 |
82 |
83 |
84 |
85 |
86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /app/components/layout/top-nav.tsx: -------------------------------------------------------------------------------- 1 | import { IconMenu } from '@tabler/icons-react' 2 | import { NavLink } from 'react-router' 3 | import { Button } from '~/components/ui/button' 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuTrigger, 9 | } from '~/components/ui/dropdown-menu' 10 | import { cn } from '~/lib/utils' 11 | 12 | interface TopNavProps extends React.HTMLAttributes { 13 | links: { 14 | title: string 15 | href: string 16 | disabled?: boolean 17 | }[] 18 | } 19 | 20 | export function TopNav({ className, links, ...props }: TopNavProps) { 21 | return ( 22 | <> 23 |
24 | 25 | 26 | 29 | 30 | 31 | {links.map(({ title, href }) => ( 32 | 33 | 37 | {title} 38 | 39 | 40 | ))} 41 | 42 | 43 |
44 | 45 | 64 | 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /app/components/layout/types.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | name: string 3 | email: string 4 | avatar: string 5 | } 6 | 7 | interface Team { 8 | name: string 9 | logo: React.ElementType 10 | plan: string 11 | } 12 | 13 | interface BaseNavItem { 14 | title: string 15 | badge?: string 16 | icon?: React.ElementType 17 | } 18 | 19 | type NavLink = BaseNavItem & { 20 | url: string 21 | items?: never 22 | } 23 | 24 | type NavCollapsible = BaseNavItem & { 25 | items: (BaseNavItem & { url: string })[] 26 | url?: never 27 | } 28 | 29 | type NavItem = NavCollapsible | NavLink 30 | 31 | interface NavGroup { 32 | title: string 33 | items: NavItem[] 34 | } 35 | 36 | interface SidebarData { 37 | user: User 38 | teams: Team[] 39 | navGroups: NavGroup[] 40 | } 41 | 42 | export type { 43 | NavCollapsible, 44 | NavGroup as NavGroupProps, 45 | NavItem, 46 | NavLink, 47 | SidebarData, 48 | } 49 | -------------------------------------------------------------------------------- /app/components/long-text.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react' 2 | import { 3 | Popover, 4 | PopoverContent, 5 | PopoverTrigger, 6 | } from '~/components/ui/popover' 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipProvider, 11 | TooltipTrigger, 12 | } from '~/components/ui/tooltip' 13 | import { cn } from '~/lib/utils' 14 | 15 | interface Props { 16 | children: React.ReactNode 17 | className?: string 18 | contentClassName?: string 19 | } 20 | 21 | export default function LongText({ 22 | children, 23 | className = '', 24 | contentClassName = '', 25 | }: Props) { 26 | const ref = useRef(null) 27 | const [isOverflown, setIsOverflown] = useState(false) 28 | 29 | useEffect(() => { 30 | if (checkOverflow(ref.current)) { 31 | setIsOverflown(true) 32 | return 33 | } 34 | 35 | setIsOverflown(false) 36 | }, []) 37 | 38 | if (!isOverflown) 39 | return ( 40 |
41 | {children} 42 |
43 | ) 44 | 45 | return ( 46 | <> 47 |
48 | 49 | 50 | 51 |
52 | {children} 53 |
54 |
55 | 56 |

{children}

57 |
58 |
59 |
60 |
61 |
62 | 63 | 64 |
65 | {children} 66 |
67 |
68 | 69 |

{children}

70 |
71 |
72 |
73 | 74 | ) 75 | } 76 | 77 | const checkOverflow = (textContainer: HTMLDivElement | null) => { 78 | if (textContainer) { 79 | return ( 80 | textContainer.offsetHeight < textContainer.scrollHeight || 81 | textContainer.offsetWidth < textContainer.scrollWidth 82 | ) 83 | } 84 | return false 85 | } 86 | -------------------------------------------------------------------------------- /app/components/password-input.tsx: -------------------------------------------------------------------------------- 1 | import { IconEye, IconEyeOff } from '@tabler/icons-react' 2 | import * as React from 'react' 3 | import { cn } from '~/lib/utils' 4 | import { Button } from './ui/button' 5 | 6 | type PasswordInputProps = Omit, 'type'> 7 | 8 | const PasswordInput = ({ 9 | className, 10 | disabled, 11 | ref, 12 | ...props 13 | }: PasswordInputProps) => { 14 | const [showPassword, setShowPassword] = React.useState(false) 15 | return ( 16 |
17 | 24 | 34 |
35 | ) 36 | } 37 | PasswordInput.displayName = 'PasswordInput' 38 | 39 | export { PasswordInput } 40 | -------------------------------------------------------------------------------- /app/components/profile-dropdown.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar' 2 | import { Button } from '~/components/ui/button' 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuGroup, 7 | DropdownMenuItem, 8 | DropdownMenuLabel, 9 | DropdownMenuSeparator, 10 | DropdownMenuShortcut, 11 | DropdownMenuTrigger, 12 | } from '~/components/ui/dropdown-menu' 13 | 14 | export function ProfileDropdown() { 15 | return ( 16 | 17 | 18 | 24 | 25 | 26 | 27 |
28 |

Mizoguchi Coji

29 |

30 | coji@techtalk.jp 31 |

32 |
33 |
34 | 35 | 36 | 37 | Profile 38 | ⇧⌘P 39 | 40 | 41 | Billing 42 | ⌘B 43 | 44 | 45 | Settings 46 | ⌘S 47 | 48 | New Team 49 | 50 | 51 | 52 | Log out 53 | ⇧⌘Q 54 | 55 |
56 |
57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /app/components/search.tsx: -------------------------------------------------------------------------------- 1 | import { IconSearch } from '@tabler/icons-react' 2 | import { Button } from '~/components/ui/button' 3 | import { useSearch } from '~/context/search-context' 4 | import { cn } from '~/lib/utils' 5 | 6 | interface Props { 7 | className?: string 8 | type?: React.HTMLInputTypeAttribute 9 | placeholder?: string 10 | } 11 | 12 | export function Search({ className = '', placeholder = 'Search' }: Props) { 13 | const { setOpen } = useSearch() 14 | return ( 15 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /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/components/theme-switch.tsx: -------------------------------------------------------------------------------- 1 | import { IconCheck, IconMoon, IconSun } from '@tabler/icons-react' 2 | import { useTheme } from 'next-themes' 3 | import { useEffect } from 'react' 4 | import { Button } from '~/components/ui/button' 5 | import { 6 | DropdownMenu, 7 | DropdownMenuContent, 8 | DropdownMenuItem, 9 | DropdownMenuTrigger, 10 | } from '~/components/ui/dropdown-menu' 11 | import { cn } from '~/lib/utils' 12 | 13 | export function ThemeSwitch() { 14 | const { theme, setTheme } = useTheme() 15 | 16 | /* Update theme-color meta tag 17 | * when theme is updated */ 18 | useEffect(() => { 19 | const themeColor = theme === 'dark' ? '#020817' : '#fff' 20 | const metaThemeColor = document.querySelector("meta[name='theme-color']") 21 | if (metaThemeColor) metaThemeColor.setAttribute('content', themeColor) 22 | }, [theme]) 23 | 24 | return ( 25 | 26 | 27 | 32 | 33 | 34 | setTheme('light')}> 35 | Light{' '} 36 | 40 | 41 | setTheme('dark')}> 42 | Dark 43 | 47 | 48 | setTheme('system')}> 49 | System 50 | 54 | 55 | 56 | 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /app/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority' 2 | import type * as React from 'react' 3 | 4 | import { cn } from '~/lib/utils' 5 | 6 | const alertVariants = cva( 7 | 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', 8 | { 9 | variants: { 10 | variant: { 11 | default: 'bg-background text-foreground', 12 | destructive: 13 | 'text-destructive-foreground [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-foreground/80', 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: 'default', 18 | }, 19 | }, 20 | ) 21 | 22 | function Alert({ 23 | className, 24 | variant, 25 | ...props 26 | }: React.ComponentProps<'div'> & VariantProps) { 27 | return ( 28 |
34 | ) 35 | } 36 | 37 | function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { 38 | return ( 39 |
47 | ) 48 | } 49 | 50 | function AlertDescription({ 51 | className, 52 | ...props 53 | }: React.ComponentProps<'div'>) { 54 | return ( 55 |
63 | ) 64 | } 65 | 66 | export { Alert, AlertDescription, AlertTitle } 67 | -------------------------------------------------------------------------------- /app/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as AvatarPrimitive from '@radix-ui/react-avatar' 2 | import type * as React from 'react' 3 | 4 | import { cn } from '~/lib/utils' 5 | 6 | function Avatar({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ) 20 | } 21 | 22 | function AvatarImage({ 23 | className, 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 32 | ) 33 | } 34 | 35 | function AvatarFallback({ 36 | className, 37 | ...props 38 | }: React.ComponentProps) { 39 | return ( 40 | 48 | ) 49 | } 50 | 51 | export { Avatar, AvatarFallback, AvatarImage } 52 | -------------------------------------------------------------------------------- /app/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from '@radix-ui/react-slot' 2 | import { cva, type VariantProps } from 'class-variance-authority' 3 | import type * as React from 'react' 4 | 5 | import { cn } from '~/lib/utils' 6 | 7 | const badgeVariants = cva( 8 | 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-auto', 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', 14 | secondary: 15 | 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', 16 | destructive: 17 | 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40', 18 | outline: 19 | 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: 'default', 24 | }, 25 | }, 26 | ) 27 | 28 | function Badge({ 29 | className, 30 | variant, 31 | asChild = false, 32 | ...props 33 | }: React.ComponentProps<'span'> & 34 | VariantProps & { asChild?: boolean }) { 35 | const Comp = asChild ? Slot : 'span' 36 | 37 | return ( 38 | 43 | ) 44 | } 45 | 46 | export { Badge, badgeVariants } 47 | -------------------------------------------------------------------------------- /app/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from '@radix-ui/react-slot' 2 | import { ChevronRight, MoreHorizontal } from 'lucide-react' 3 | import type * as React from 'react' 4 | 5 | import { cn } from '~/lib/utils' 6 | 7 | function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) { 8 | return