├── .env.prod ├── .env.test ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── i18n-ally-custom-framework.yml └── settings.json ├── README.md ├── bun.lockb ├── components.json ├── next.config.mjs ├── package.json ├── postcss.config.js ├── public ├── images │ └── auth-bg.webp ├── next.svg └── vercel.svg ├── src ├── @types │ ├── auth.ts │ ├── env.d.ts │ └── index.d.ts ├── apis │ └── auth │ │ └── use-login.mutation.ts ├── app │ ├── [locale] │ │ ├── (auth) │ │ │ ├── forgot-password │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── login │ │ │ │ └── page.tsx │ │ │ └── signup │ │ │ │ └── page.tsx │ │ ├── (dashboard) │ │ │ ├── dashboard │ │ │ │ ├── [...not-found] │ │ │ │ │ └── page.tsx │ │ │ │ ├── charts │ │ │ │ │ └── page.tsx │ │ │ │ ├── customers │ │ │ │ │ └── page.tsx │ │ │ │ ├── error │ │ │ │ │ └── 500 │ │ │ │ │ │ └── page.tsx │ │ │ │ ├── not-found.tsx │ │ │ │ └── page.tsx │ │ │ ├── error.tsx │ │ │ └── layout.tsx │ │ ├── [...not-found] │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── apple-icon.png │ ├── favicon.ico │ ├── layout.tsx │ ├── manifest.json │ ├── not-found.tsx │ ├── providers.tsx │ ├── robots.ts │ └── sitemap.ts ├── assets │ └── fonts │ │ └── README.md ├── components │ ├── common │ │ ├── index.ts │ │ ├── link.tsx │ │ ├── locale-switcher.tsx │ │ ├── responsive-pagination.tsx │ │ └── theme-switcher.tsx │ ├── errors │ │ └── dashboard │ │ │ ├── dashboard-404.tsx │ │ │ └── dashboard-500.tsx │ ├── layouts │ │ ├── auth-layout │ │ │ └── index.tsx │ │ ├── dashboard-layout │ │ │ ├── dashboard-header.tsx │ │ │ ├── index.tsx │ │ │ ├── logout-modal.tsx │ │ │ ├── mobile-sidebar.tsx │ │ │ ├── sidebar-item.tsx │ │ │ ├── sidebar.tsx │ │ │ └── user-nav.tsx │ │ └── locale-layout │ │ │ └── index.tsx │ ├── screens │ │ ├── auth │ │ │ ├── forgot-password-form.tsx │ │ │ ├── login-form.tsx │ │ │ └── signup-form.tsx │ │ ├── charts │ │ │ └── index.tsx │ │ ├── customers │ │ │ └── customers-list │ │ │ │ ├── customer-columns.tsx │ │ │ │ ├── customers-data-table.tsx │ │ │ │ ├── customers-table.tsx │ │ │ │ └── mock-customers.ts │ │ └── dashboard │ │ │ ├── overview-chart.tsx │ │ │ ├── recent-sales.tsx │ │ │ └── stats-widget.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── pagination.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── table.tsx │ │ └── tooltip.tsx ├── config.ts ├── constants │ └── sidebar-links.ts ├── hooks │ ├── use-dir.ts │ └── use-media-query.ts ├── i18n.ts ├── messages │ ├── ar.json │ └── en.json ├── middleware.ts ├── styles │ └── globals.css ├── utils │ ├── cn.ts │ ├── dir.ts │ ├── fetch │ │ ├── client.ts │ │ ├── config.ts │ │ └── server.ts │ └── wait.ts └── zustand │ └── sidebar-store │ └── index.tsx ├── tailwind.config.ts └── tsconfig.json /.env.prod: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ENV=production 2 | NEXT_PUBLIC_API_URL= 3 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ENV=testing 2 | NEXT_PUBLIC_API_URL= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["next", "next/core-web-vitals", "plugin:@typescript-eslint/recommended", "prettier"], 4 | "ignorePatterns": ["next.config.js", "tailwind.config.js"], 5 | "rules": { 6 | "quotes": ["error", "single", { "avoidEscape": true }], 7 | "no-unused-vars": "off", 8 | "@typescript-eslint/no-unused-vars": "warn", 9 | "@typescript-eslint/no-explicit-any": "off", 10 | "@typescript-eslint/ban-ts-comment": "off", 11 | "react/no-unescaped-entities": "off" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | package-lock.json 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | .cursorrules -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /cache 2 | data 3 | build 4 | coverage 5 | .yarn 6 | .next 7 | node_modules -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "trailingComma": "es5", 5 | "semi": true, 6 | "printWidth": 100, 7 | "endOfLine": "lf", 8 | "tabWidth": 2, 9 | "plugins": ["prettier-plugin-tailwindcss"], 10 | "bracketSameLine": true 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/i18n-ally-custom-framework.yml: -------------------------------------------------------------------------------- 1 | languageIds: 2 | - javascript 3 | - typescript 4 | - javascriptreact 5 | - typescriptreact 6 | 7 | usageMatchRegex: 8 | - "[^\\w\\d]t\\s*\\(\\s*['\"`]({key})['\"`]" 9 | - "[^\\w\\d]t\\s*\\.rich\\s*\\(\\s*['\"`]({key})['\"`]" 10 | - "[^\\w\\d]t\\s*\\.markup\\s*\\(\\s*['\"`]({key})['\"`]" 11 | - "[^\\w\\d]t\\s*\\.raw\\s*\\(\\s*['\"`]({key})['\"`]" 12 | 13 | scopeRangeRegex: "(?:getTranslations|useTranslations)\\((?:\\s*['\"`]|{\\s*namespace:\\s*['\"`])(.*?)['\"`]" 14 | 15 | monopoly: true 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": ["src/messages"], 3 | "i18n-ally.keystyle": "nested", 4 | "i18n-ally.sourceLanguage": "en" 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Specific Documentation here 2 | 3 | ## Get Started 4 | 5 | bun 6 | 7 | ``` 8 | bun install; bun run dev 9 | ``` 10 | 11 | yarn 12 | 13 | ``` 14 | yarn; yarn dev 15 | ``` 16 | 17 | npm 18 | 19 | ``` 20 | npm install; npm run start 21 | ``` 22 | 23 | > Will add more details to readme soon 24 | 25 | ## Recommendations 26 | 27 | If you are using vscode, install the following extensions, 28 | 29 | - i18n ally 30 | - Tailwind CSS Intelisense 31 | - Eslint 32 | - Prettier - Code Formatter 33 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohOulabi/nextjs-shadcn-admin/fb6f82d2f84532bf2e7b4bfb934e375a219af262/bun.lockb -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/utils/cn" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import nextBuildId from 'next-build-id'; 2 | import createNextIntlPlugin from 'next-intl/plugin'; 3 | import analyzer from '@next/bundle-analyzer'; 4 | import withPlugins from 'next-compose-plugins'; 5 | 6 | const withBundleAnalyzer = analyzer({ 7 | enabled: process.env.ANALYZE === 'true', 8 | }); 9 | 10 | const withNextIntl = createNextIntlPlugin(); 11 | 12 | /** @type {import('next').NextConfig} */ 13 | const nextConfig = { 14 | reactStrictMode: false, 15 | generateBuildId: async () => nextBuildId(), 16 | logging: { 17 | fetches: { 18 | fullUrl: true, 19 | }, 20 | }, 21 | }; 22 | 23 | export default withPlugins([withNextIntl, withBundleAnalyzer], nextConfig); 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-shadcn-admin", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "production": "env-cmd -f ./.env.prod next build", 10 | "production:start": "env-cmd -f ./.env.prod next start", 11 | "testing": "env-cmd -f ./.env.test next build", 12 | "testing:start": "env-cmd -f ./.env.test next start", 13 | "lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx --fix", 14 | "format": "prettier --check \"**/*.{js,jsx,ts,tsx,json}\"", 15 | "format:write": "prettier --write \"**/*.{js,jsx,ts,tsx,json}\"", 16 | "cn-add": "npx shadcn-ui@latest add", 17 | "pre:commit": "lint-staged" 18 | }, 19 | "dependencies": { 20 | "@faker-js/faker": "^8.4.1", 21 | "@hookform/resolvers": "^3.3.4", 22 | "@next/bundle-analyzer": "^14.1.4", 23 | "@radix-ui/react-accordion": "^1.1.2", 24 | "@radix-ui/react-avatar": "^1.0.4", 25 | "@radix-ui/react-checkbox": "^1.0.4", 26 | "@radix-ui/react-dialog": "^1.0.5", 27 | "@radix-ui/react-dropdown-menu": "^2.0.6", 28 | "@radix-ui/react-label": "^2.0.2", 29 | "@radix-ui/react-menubar": "^1.0.4", 30 | "@radix-ui/react-scroll-area": "^1.0.5", 31 | "@radix-ui/react-select": "^2.0.0", 32 | "@radix-ui/react-separator": "^1.0.3", 33 | "@radix-ui/react-slot": "^1.1.0", 34 | "@radix-ui/react-tooltip": "^1.0.7", 35 | "@tanstack/react-query": "^5.28.9", 36 | "@tanstack/react-query-devtools": "^5.28.10", 37 | "@tanstack/react-query-next-experimental": "^5.28.9", 38 | "@tanstack/react-table": "^8.20.5", 39 | "class-variance-authority": "^0.7.0", 40 | "clsx": "^2.1.0", 41 | "framer-motion": "^11.0.25", 42 | "js-cookie": "^3.0.5", 43 | "ky": "^1.2.3", 44 | "lodash": "^4.17.21", 45 | "lucide-react": "^0.437.0", 46 | "next": "^14.1.4", 47 | "next-build-id": "^3.0.0", 48 | "next-compose-plugins": "^2.2.1", 49 | "next-intl": "^3.10.0", 50 | "next-themes": "^0.3.0", 51 | "react": "^18", 52 | "react-country-flag": "^3.1.0", 53 | "react-dom": "^18", 54 | "react-error-boundary": "^4.0.13", 55 | "react-hook-form": "^7.51.2", 56 | "recharts": "^2.13.0-alpha.4", 57 | "sharp": "^0.33.3", 58 | "sonner": "^1.4.41", 59 | "tailwind-merge": "^2.2.2", 60 | "tailwindcss-animate": "^1.0.7", 61 | "vaul": "^0.9.0", 62 | "zod": "^3.22.4", 63 | "zustand": "^4.5.2" 64 | }, 65 | "devDependencies": { 66 | "@types/js-cookie": "^3.0.6", 67 | "@types/lodash": "^4.14.202", 68 | "@types/node": "^20", 69 | "@types/react": "^18", 70 | "@types/react-dom": "^18", 71 | "@typescript-eslint/eslint-plugin": "^6.13.2", 72 | "autoprefixer": "^10.0.1", 73 | "env-cmd": "^10.1.0", 74 | "eslint": "^8.55.0", 75 | "eslint-config-next": "14.1.1", 76 | "eslint-config-prettier": "^9.1.0", 77 | "eslint-plugin-prettier": "^5.0.1", 78 | "lint-staged": "^15.2.2", 79 | "postcss": "^8", 80 | "prettier": "^3.1.1", 81 | "prettier-plugin-tailwindcss": "^0.5.11", 82 | "tailwindcss": "^3.3.0", 83 | "typescript": "^5" 84 | }, 85 | "lint-staged": { 86 | "**/*.{ts,js,tsx,jsx}": [ 87 | "bun run lint:fix" 88 | ], 89 | "**/*.{ts,js,tsx,jsx,json,yml}": [ 90 | "bun run format:write" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/images/auth-bg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohOulabi/nextjs-shadcn-admin/fb6f82d2f84532bf2e7b4bfb934e375a219af262/public/images/auth-bg.webp -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/@types/auth.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | [x: string]: any; 3 | }; 4 | -------------------------------------------------------------------------------- /src/@types/env.d.ts: -------------------------------------------------------------------------------- 1 | import en from '../messages/en.json'; 2 | 3 | type Messages = typeof en; 4 | 5 | declare global { 6 | interface IntlMessages extends Messages {} 7 | } 8 | 9 | declare namespace NodeJS { 10 | interface ProcessEnv { 11 | // Add your environment variables here 12 | NEXT_PUBLIC_ENV: 'development' | 'testing' | 'production'; 13 | NEXT_PUBLIC_API_URL: string; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | type LocaleParams = { 2 | params: { 3 | locale: string; 4 | }; 5 | }; 6 | 7 | type NextPageProps = { 8 | searchParams?: Record; 9 | params: { 10 | locale: Languages; 11 | }; 12 | }; 13 | 14 | type IntlPath = Paths; 15 | 16 | type NextPage = ( 17 | props: NextPageProps & T 18 | ) => React.ReactElement | Promise | null; 19 | 20 | type LayoutProps = Readonly<{ 21 | children: React.ReactNode; 22 | params: { 23 | locale: string; 24 | }; 25 | }>; 26 | 27 | type ErrorFileProps = { 28 | reset: () => void; 29 | error: Error & { digest?: string }; 30 | }; 31 | 32 | type TranslationKey = MessageKeys; 33 | -------------------------------------------------------------------------------- /src/apis/auth/use-login.mutation.ts: -------------------------------------------------------------------------------- 1 | import type { LoginFormValues } from '@/components/screens/auth/login-form'; 2 | import { useMutation } from '@tanstack/react-query'; 3 | 4 | const _mock_response = { 5 | data: { 6 | token: '1234567890abcdef', 7 | }, 8 | }; 9 | 10 | type LoginAPIResponse = { 11 | data: { 12 | [key: string]: any; 13 | }; 14 | }; 15 | type LoginReturn = { 16 | token: string; 17 | [key: string]: any; 18 | }; 19 | 20 | const normalizeLogin = (response: LoginAPIResponse): LoginReturn => { 21 | return response.data as LoginReturn; 22 | }; 23 | 24 | const login = async (params: LoginFormValues) => { 25 | // Mock 26 | const response = new Promise((resolve, reject) => 27 | setTimeout(() => { 28 | if (params.email === 'admin@admin.com' && params.password === 'admin123') 29 | return resolve(_mock_response); 30 | reject({ message: 'Invalid email or password' }); 31 | }, 1000) 32 | ); 33 | 34 | const json = (await response) as LoginAPIResponse; 35 | return normalizeLogin(json); 36 | }; 37 | 38 | export const useLogin = () => { 39 | return useMutation({ 40 | mutationKey: ['login'], 41 | mutationFn: login, 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { getMessages, getTranslations } from 'next-intl/server'; 3 | import { NextIntlClientProvider } from 'next-intl'; 4 | import pick from 'lodash/pick'; 5 | import { ForgotPasswordForm } from '@/components/screens/auth/forgot-password-form'; 6 | import { NextLink } from '@/components/common'; 7 | import { ArrowLeft } from 'lucide-react'; 8 | import { Button } from '@/ui/button'; 9 | 10 | export async function generateMetadata(): Promise { 11 | const t = await getTranslations('Metadata'); 12 | return { 13 | title: t('forgot_password_page_title'), 14 | description: t('forgot_password_page_description'), 15 | robots: { 16 | follow: process.env.NEXT_PUBLIC_ENV === 'production', 17 | index: process.env.NEXT_PUBLIC_ENV === 'production', 18 | }, 19 | }; 20 | } 21 | 22 | const LoginPage: NextPage = async ({ params: { locale } }) => { 23 | const t = await getTranslations('ForgotPassword'); 24 | const messages = await getMessages(); 25 | 26 | return ( 27 | <> 28 | 34 |
35 |

{t('title')}

36 |

{t('description')}

37 |
38 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default LoginPage; 48 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { AuthLayout } from '@/components/layouts/auth-layout'; 2 | import { Metadata } from 'next'; 3 | // import { redirect } from '@/i18n'; 4 | // import { cookies } from 'next/headers'; 5 | 6 | export const metadata: Metadata = { 7 | robots: { 8 | follow: process.env.NEXT_PUBLIC_ENV === 'production', 9 | index: process.env.NEXT_PUBLIC_ENV === 'production', 10 | }, 11 | }; 12 | 13 | export default function Layout({ children }: LayoutProps) { 14 | // const token = cookies().get('token')?.value; 15 | // if (token) redirect('/dashboard'); 16 | 17 | return {children}; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm } from '@/components/screens/auth/login-form'; 2 | import { Metadata } from 'next'; 3 | import { getMessages, getTranslations } from 'next-intl/server'; 4 | import { NextIntlClientProvider } from 'next-intl'; 5 | import pick from 'lodash/pick'; 6 | 7 | export async function generateMetadata(): Promise { 8 | const t = await getTranslations('Metadata'); 9 | return { 10 | title: t('login_page_title'), 11 | description: t('login_page_description'), 12 | }; 13 | } 14 | 15 | const LoginPage: NextPage = async ({ params: { locale } }) => { 16 | const t = await getTranslations('Login'); 17 | const messages = await getMessages(); 18 | 19 | return ( 20 | <> 21 |
22 |

{t('title')}

23 |

{t('description')}

24 |
25 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default LoginPage; 35 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { getMessages, getTranslations } from 'next-intl/server'; 3 | import { NextIntlClientProvider } from 'next-intl'; 4 | import pick from 'lodash/pick'; 5 | import { SignupForm } from '@/components/screens/auth/signup-form'; 6 | import { NextLink } from '@/components/common'; 7 | import { ArrowLeft } from 'lucide-react'; 8 | import { Button } from '@/ui/button'; 9 | 10 | export async function generateMetadata(): Promise { 11 | const t = await getTranslations('Metadata'); 12 | return { 13 | title: t('signup_page_title'), 14 | description: t('signup_page_description'), 15 | robots: { 16 | follow: process.env.NEXT_PUBLIC_ENV === 'production', 17 | index: process.env.NEXT_PUBLIC_ENV === 'production', 18 | }, 19 | }; 20 | } 21 | 22 | const SignupPage: NextPage = async ({ params: { locale } }) => { 23 | const t = await getTranslations('Signup'); 24 | const messages = await getMessages(); 25 | 26 | return ( 27 | <> 28 | 34 |
35 |

{t('title')}

36 |

{t('description')}

37 |
38 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default SignupPage; 48 | -------------------------------------------------------------------------------- /src/app/[locale]/(dashboard)/dashboard/[...not-found]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation'; 2 | 3 | export default function CatchAllPage() { 4 | notFound(); 5 | } 6 | -------------------------------------------------------------------------------- /src/app/[locale]/(dashboard)/dashboard/charts/page.tsx: -------------------------------------------------------------------------------- 1 | import { ChartsPageContent } from '@/components/screens/charts'; 2 | import { pick } from 'lodash'; 3 | import { NextIntlClientProvider } from 'next-intl'; 4 | import { getMessages } from 'next-intl/server'; 5 | 6 | const ChartsPage: NextPage = async ({ params: { locale } }) => { 7 | const messages = await getMessages(); 8 | 9 | return ( 10 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default ChartsPage; 20 | -------------------------------------------------------------------------------- /src/app/[locale]/(dashboard)/dashboard/customers/page.tsx: -------------------------------------------------------------------------------- 1 | import { generateCustomers } from '@/components/screens/customers/customers-list/mock-customers'; 2 | import { CustomersTable } from '@/components/screens/customers/customers-list/customers-table'; 3 | import { NextIntlClientProvider, useMessages, useTranslations } from 'next-intl'; 4 | import pick from 'lodash/pick'; 5 | 6 | const CustomersPage: NextPage = ({ params: { locale } }) => { 7 | const t = useTranslations('Dashboard'); 8 | const messages = useMessages(); 9 | const customers = generateCustomers(100); 10 | 11 | return ( 12 |
13 |

{t('customers')}

14 | 15 | 16 | 17 |
18 | ); 19 | }; 20 | 21 | export default CustomersPage; 22 | -------------------------------------------------------------------------------- /src/app/[locale]/(dashboard)/dashboard/error/500/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page500() { 2 | throw new Error('This page does not work for the moment.'); 3 | } 4 | -------------------------------------------------------------------------------- /src/app/[locale]/(dashboard)/dashboard/not-found.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Dashboard404 } from '@/components/errors/dashboard/dashboard-404'; 4 | 5 | function NotFoundPage() { 6 | return ; 7 | } 8 | 9 | export default NotFoundPage; 10 | -------------------------------------------------------------------------------- /src/app/[locale]/(dashboard)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { OverviewChart } from '@/components/screens/dashboard/overview-chart'; 2 | import { RecentSales } from '@/components/screens/dashboard/recent-sales'; 3 | import { StatsWidget, StatsWidgetProps } from '@/components/screens/dashboard/stats-widget'; 4 | import { Button } from '@/ui/button'; 5 | import { DollarSignIcon, UsersIcon, ActivityIcon, CreditCardIcon } from 'lucide-react'; 6 | import { getTranslations } from 'next-intl/server'; 7 | 8 | const stats_widgets: StatsWidgetProps[] = [ 9 | { 10 | title: 'total_revenue', 11 | content: '+35,320', 12 | subContent: '+5.4% from last month', 13 | icon: , 14 | }, 15 | { 16 | title: 'subscriptions', 17 | content: '+1,200', 18 | subContent: '+12% from last month', 19 | icon: , 20 | }, 21 | { 22 | title: 'sales', 23 | content: '+17,901', 24 | subContent: '+8.2% from last month', 25 | icon: , 26 | }, 27 | { 28 | title: 'active_users', 29 | content: '510', 30 | subContent: '+61 since last hour', 31 | icon: , 32 | }, 33 | ]; 34 | 35 | const DashboardPage: NextPage = async () => { 36 | const t = await getTranslations('Common'); 37 | 38 | return ( 39 | <> 40 |
41 |

{t('dashboard')}

42 |
43 | 44 |
45 |
46 |
47 |
48 | {stats_widgets.map(({ title, ...widget }, index) => ( 49 | 50 | ))} 51 |
52 |
53 | 54 | 55 |
56 |
57 | 58 | ); 59 | }; 60 | 61 | export default DashboardPage; 62 | -------------------------------------------------------------------------------- /src/app/[locale]/(dashboard)/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Dashboard500 } from '@/components/errors/dashboard/dashboard-500'; 4 | 5 | export default function NotFoundDashboard(props: ErrorFileProps) { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/[locale]/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardLayout } from '@/components/layouts/dashboard-layout'; 2 | import pick from 'lodash/pick'; 3 | import { getMessages } from 'next-intl/server'; 4 | 5 | export default async function Layout({ children }: LayoutProps) { 6 | const messages = await getMessages(); 7 | 8 | return ( 9 | {children} 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/[locale]/[...not-found]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation'; 2 | 3 | export default function CatchAllPage() { 4 | notFound(); 5 | } 6 | -------------------------------------------------------------------------------- /src/app/[locale]/layout.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css'; 2 | import type { Metadata, Viewport } from 'next'; 3 | import { getTranslations } from 'next-intl/server'; 4 | import { Providers } from '../providers'; 5 | import { dir } from '@/utils/dir'; 6 | import { Cairo, Inter } from 'next/font/google'; 7 | import { cn } from '@/utils/cn'; 8 | import { Toaster } from '@/ui/sonner'; 9 | import { useMessages } from 'next-intl'; 10 | import pick from 'lodash/pick'; 11 | 12 | const cairo = Cairo({ 13 | weight: ['400', '500', '600', '700'], 14 | display: 'swap', 15 | subsets: ['latin'], 16 | variable: '--font-cairo', 17 | }); 18 | 19 | const inter = Inter({ 20 | weight: ['400', '500', '600', '700'], 21 | display: 'swap', 22 | subsets: ['latin'], 23 | variable: '--font-inter', 24 | }); 25 | 26 | export const viewport: Viewport = { 27 | maximumScale: 1, 28 | initialScale: 1, 29 | width: 'device-width', 30 | themeColor: [ 31 | { media: '(prefers-color-scheme: light)', color: 'white' }, 32 | { media: '(prefers-color-scheme: dark)', color: 'black' }, 33 | ], 34 | }; 35 | 36 | export async function generateMetadata(): Promise { 37 | const t = await getTranslations('Metadata'); 38 | return { 39 | title: { 40 | default: t('default_title'), 41 | template: `%s | ${t('default_title')}`, 42 | }, 43 | description: t('default_description'), 44 | 45 | robots: { 46 | follow: process.env.NEXT_PUBLIC_ENV === 'production', 47 | index: process.env.NEXT_PUBLIC_ENV === 'production', 48 | }, 49 | }; 50 | } 51 | 52 | export default function RootLocaleLayout({ children, params: { locale } }: LayoutProps) { 53 | const messages = useMessages(); 54 | return ( 55 | 56 | 63 | 64 | {children} 65 | 66 | 67 | 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/app/[locale]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound, redirect } from 'next/navigation'; 2 | import { cookies } from 'next/headers'; 3 | import { LOCALES } from '@/config'; 4 | 5 | const Home: NextPage = ({ params: { locale } }) => { 6 | 7 | 8 | if (!LOCALES.includes(locale)) return notFound(); 9 | 10 | const token = cookies().get('token')?.value; 11 | 12 | if (token) redirect(`/${locale}/dashboard`); 13 | else redirect(`/${locale}/login`); 14 | }; 15 | 16 | export default Home; 17 | -------------------------------------------------------------------------------- /src/app/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohOulabi/nextjs-shadcn-admin/fb6f82d2f84532bf2e7b4bfb934e375a219af262/src/app/apple-icon.png -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohOulabi/nextjs-shadcn-admin/fb6f82d2f84532bf2e7b4bfb934e375a219af262/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function Layout({ children }: { children: React.ReactNode }) { 2 | return <>{children}; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Application Name", 3 | "short_name": "App Name", 4 | "description": "An app to do amazing things", 5 | "start_url": "/", 6 | "background_color": "#3367D6", 7 | "theme_color": "#3367D6", 8 | "display": "standalone", 9 | "icons": [ 10 | { 11 | "src": "/favicon.ico", 12 | "sizes": "48x48", 13 | "type": "image/x-icon" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Error from 'next/error'; 4 | 5 | // This is a catch-all page for 404 errors other than the dashboard 6 | 7 | function NotFoundPage() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | export default NotFoundPage; 18 | -------------------------------------------------------------------------------- /src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { DEFAULT_QUERY_RETRY } from '@/config'; 4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 5 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 6 | import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'; 7 | import { useState } from 'react'; 8 | import { ThemeProvider } from 'next-themes'; 9 | import { AbstractIntlMessages, NextIntlClientProvider } from 'next-intl'; 10 | import { dir } from '@/utils/dir'; 11 | // We can add "user" props to get it from server on first load and pass it to AuthProvider 12 | 13 | type ProvidersProps = { 14 | children: React.ReactNode; 15 | locale: string; 16 | messages: AbstractIntlMessages; 17 | }; 18 | export function Providers({ children, messages, locale }: ProvidersProps) { 19 | const direction = dir(locale); 20 | const [queryClient] = useState( 21 | () => 22 | new QueryClient({ 23 | defaultOptions: { 24 | queries: { 25 | staleTime: 20 * 1000, 26 | retry: DEFAULT_QUERY_RETRY, 27 | }, 28 | }, 29 | }) 30 | ); 31 | 32 | return ( 33 | 34 | 35 | 36 | 37 | {children} 38 | 39 | 40 | {process.env.NODE_ENV === 'development' && ( 41 | 45 | )} 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from 'next'; 2 | 3 | export default function robots(): MetadataRoute.Robots { 4 | return { 5 | rules: { 6 | // userAgent: '*', 7 | // allow: '/', 8 | // disallow: '/private/', 9 | }, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from 'next'; 2 | 3 | export default function sitemap(): MetadataRoute.Sitemap { 4 | return []; 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/fonts/README.md: -------------------------------------------------------------------------------- 1 | # Add Your Fonts Here And Import Them With next/font Local 2 | -------------------------------------------------------------------------------- /src/components/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './link'; 2 | export * from './locale-switcher'; 3 | export * from './theme-switcher'; 4 | -------------------------------------------------------------------------------- /src/components/common/link.tsx: -------------------------------------------------------------------------------- 1 | import { DEFAULT_PREFETCH } from '@/config'; 2 | import { Link } from '@/i18n'; 3 | import { forwardRef } from 'react'; 4 | 5 | type Props = Parameters['0']; 6 | 7 | type NextLinkProps = Omit, keyof Props> & Props; 8 | 9 | export const NextLink = forwardRef( 10 | ({ prefetch, ...props }, ref) => { 11 | return ; 12 | } 13 | ); 14 | 15 | NextLink.displayName = 'NextLink'; 16 | -------------------------------------------------------------------------------- /src/components/common/locale-switcher.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { LOCALES } from '@/config'; 4 | import { usePathname, useRouter } from '@/i18n'; 5 | import { useParams } from 'next/navigation'; 6 | import { Select, SelectTrigger, SelectContent, SelectItem } from '@/ui/select'; 7 | import ReactCountryFlag from 'react-country-flag'; 8 | 9 | type LOCALE = (typeof LOCALES)[number]; 10 | 11 | const flags: Record< 12 | LOCALE, 13 | { 14 | flag: string; 15 | title: string; 16 | className?: string; 17 | } 18 | > = { 19 | ar: { 20 | flag: 'sa', 21 | title: 'عربي', 22 | className: 'font-cairo', 23 | }, 24 | en: { 25 | flag: 'us', 26 | title: 'EN', 27 | className: 'font-inter', 28 | }, 29 | }; 30 | 31 | export const LocaleSwitcher = () => { 32 | let pathname = usePathname(); 33 | const { locale }: { locale: LOCALE } = useParams(); 34 | const router = useRouter(); 35 | 36 | if (pathname.endsWith('/')) pathname = pathname.slice(0, -1); 37 | 38 | const updateLocale = (locale: LOCALE) => { 39 | router.push(pathname, { locale }); 40 | }; 41 | 42 | return ( 43 | 71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/components/common/responsive-pagination.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Pagination, 4 | PaginationContent, 5 | PaginationItem, 6 | PaginationLink, 7 | PaginationEllipsis, 8 | PaginationNext, 9 | PaginationPrevious, 10 | } from '@/components/ui/pagination'; 11 | 12 | interface PaginationItemsProps { 13 | totalPages: number; 14 | currentPage: number; 15 | maxVisiblePages?: number; 16 | onPageChange: (page: number) => void; 17 | onPreviousPage: () => void; 18 | onNextPage: () => void; 19 | canPreviousPage: boolean; 20 | canNextPage: boolean; 21 | } 22 | 23 | export function PaginationItems({ 24 | totalPages, 25 | currentPage, 26 | maxVisiblePages = 5, 27 | onPageChange, 28 | onPreviousPage, 29 | onNextPage, 30 | canPreviousPage, 31 | canNextPage, 32 | }: PaginationItemsProps) { 33 | const renderPaginationItems = () => { 34 | const items = []; 35 | 36 | if (totalPages <= maxVisiblePages) { 37 | for (let i = 1; i <= totalPages; i++) { 38 | items.push( 39 | 40 | onPageChange(i)} isActive={currentPage === i}> 41 | {i} 42 | 43 | 44 | ); 45 | } 46 | } else { 47 | items.push( 48 | 49 | onPageChange(1)} isActive={currentPage === 1}> 50 | 1 51 | 52 | 53 | ); 54 | 55 | if (currentPage > 3) { 56 | items.push(); 57 | } 58 | 59 | const start = Math.max(2, currentPage - 1); 60 | const end = Math.min(totalPages - 1, currentPage + 1); 61 | 62 | for (let i = start; i <= end; i++) { 63 | items.push( 64 | 65 | onPageChange(i)} isActive={currentPage === i}> 66 | {i} 67 | 68 | 69 | ); 70 | } 71 | 72 | if (currentPage < totalPages - 2) { 73 | items.push(); 74 | } 75 | 76 | items.push( 77 | 78 | onPageChange(totalPages)} 80 | isActive={currentPage === totalPages}> 81 | {totalPages} 82 | 83 | 84 | ); 85 | } 86 | 87 | return items; 88 | }; 89 | 90 | return ( 91 | 92 | 93 | 94 | 95 | 96 | {renderPaginationItems()} 97 | 98 | 99 | 100 | 101 | 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /src/components/common/theme-switcher.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useTheme } from 'next-themes'; 4 | import { Select, SelectContent, SelectItem, SelectTrigger } from '@/ui/select'; 5 | import { MoonStar, SunMedium } from 'lucide-react'; 6 | 7 | const themes = ['light', 'dark', 'system'] as const; 8 | 9 | export const ThemeSwitcher = () => { 10 | const { setTheme } = useTheme(); 11 | 12 | return ( 13 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/errors/dashboard/dashboard-404.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from 'next-intl'; 2 | 3 | export const Dashboard404 = () => { 4 | const t = useTranslations('Error'); 5 | 6 | return ( 7 |
8 |

{t('404_title')}

9 |

{t('404_message')}

10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/errors/dashboard/dashboard-500.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/ui/button'; 2 | import { useTranslations } from 'next-intl'; 3 | 4 | export const Dashboard500 = (props: ErrorFileProps) => { 5 | const t = useTranslations('Error'); 6 | 7 | return ( 8 |
9 |

{t('500_title')}

10 |

{JSON.stringify(props.error.message)}

11 | 14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/layouts/auth-layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { LocaleSwitcher, ThemeSwitcher } from '@/components/common'; 2 | import Image from 'next/image'; 3 | import AuthBG from '#/public/images/auth-bg.webp'; 4 | 5 | export const AuthLayout = ({ children }: { children: React.ReactNode }) => { 6 | return ( 7 |
8 |
9 |
10 |
11 |
{children}
12 |
13 | 14 | 15 |
16 |
17 |
18 |
19 | 28 |
29 |
30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/layouts/dashboard-layout/dashboard-header.tsx: -------------------------------------------------------------------------------- 1 | import { LocaleSwitcher, ThemeSwitcher } from '@/components/common'; 2 | import { UserNav } from './user-nav'; 3 | import { MobileSidebar } from './mobile-sidebar'; 4 | 5 | export const DashboardHeader = () => { 6 | return ( 7 |
8 |
9 |
10 | 11 |
12 |
13 |
14 | 15 | 16 | 17 |
18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/layouts/dashboard-layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Sidebar } from './sidebar'; 2 | import { SidebarProvider } from '@/zustand/sidebar-store'; 3 | import { cookies } from 'next/headers'; 4 | import { AbstractIntlMessages, NextIntlClientProvider } from 'next-intl'; 5 | import { DashboardHeader } from './dashboard-header'; 6 | 7 | type DashboardLayoutProps = { 8 | children: React.ReactNode; 9 | messages: AbstractIntlMessages; 10 | }; 11 | 12 | // getting messages from parent in case of caching, since cookies disable cache 13 | 14 | export const DashboardLayout = async ({ children, messages }: DashboardLayoutProps) => { 15 | const sidebar_collapsed = cookies().get('sidebar_collapsed')?.value === 'false' ? false : true; 16 | 17 | return ( 18 | 19 | 20 |
21 | 22 |
23 | 24 |
{children}
25 |
26 |
27 |
28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/layouts/dashboard-layout/logout-modal.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/ui/button'; 4 | import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@/ui/dialog'; 5 | import { Drawer, DrawerContent, DrawerFooter, DrawerHeader, DrawerTitle } from '@/ui/drawer'; 6 | import useMediaQuery from '@/hooks/use-media-query'; 7 | import { useRouter } from '@/i18n'; 8 | import { TriangleAlert } from 'lucide-react'; 9 | 10 | type LogoutModalProps = { 11 | isOpen: boolean; 12 | onClose: () => void; 13 | }; 14 | 15 | export const LogoutModal = ({ isOpen, onClose }: LogoutModalProps) => { 16 | const isDesktop = useMediaQuery('(min-width: 1024px)'); 17 | 18 | const router = useRouter(); 19 | 20 | const onLogout = () => { 21 | router.replace('/login'); 22 | }; 23 | 24 | if (isDesktop) 25 | return ( 26 | 27 | 28 | You're About to Logout 29 |
30 | 31 | 32 | Are you sure you would like to logout of your account? 33 | 34 |
35 | 36 | 39 | 40 | 41 |
42 |
43 | ); 44 | 45 | const closeDrawer = (v: boolean) => { 46 | if (!v) onClose(); 47 | }; 48 | 49 | return ( 50 | 51 | 52 | 53 | You're About to Logout 54 | 55 |
56 | 57 | 58 | Are you sure you would like to logout of your account? 59 | 60 |
61 | 62 | 65 | 66 | 67 |
68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/layouts/dashboard-layout/mobile-sidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Button } from '@/ui/button'; 3 | import { Sheet, SheetContent, SheetHeader, SheetTrigger } from '@/ui/sheet'; 4 | import { MenuIcon } from 'lucide-react'; 5 | import { SidebarNav } from './sidebar'; 6 | import { useDir } from '@/hooks/use-dir'; 7 | import { useEffect, useState } from 'react'; 8 | import { usePathname } from 'next/navigation'; 9 | 10 | export const MobileSidebar = () => { 11 | const dir = useDir(); 12 | const isRTL = dir === 'rtl'; 13 | const pathname = usePathname(); 14 | const [isOpen, setIsOpen] = useState(false); 15 | 16 | useEffect(() => setIsOpen(false), [pathname]); 17 | 18 | return ( 19 | 20 | 21 | 24 | 25 | 26 | 30 |
31 | 37 | 41 | 42 | NextJS & Shadcn 43 |
44 | 45 | } 46 | /> 47 |
48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/layouts/dashboard-layout/sidebar-item.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { NextLink } from '@/components/common'; 3 | import { SidebarLink, SidebarLinkItem } from '@/constants/sidebar-links'; 4 | import { useDir } from '@/hooks/use-dir'; 5 | import { usePathname } from '@/i18n'; 6 | import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/ui/accordion'; 7 | import { Button, buttonVariants } from '@/ui/button'; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuLabel, 13 | DropdownMenuSeparator, 14 | DropdownMenuTrigger, 15 | } from '@/ui/dropdown-menu'; 16 | import { Tooltip, TooltipContent } from '@/ui/tooltip'; 17 | import { cn } from '@/utils/cn'; 18 | import { useSidebar } from '@/zustand/sidebar-store'; 19 | import { TooltipTrigger } from '@radix-ui/react-tooltip'; 20 | import { motion } from 'framer-motion'; 21 | import { ChevronRight } from 'lucide-react'; 22 | import { useTranslations } from 'next-intl'; 23 | 24 | type SidebarItemProps = { 25 | link: SidebarLink; 26 | ignoreCollapse?: boolean; 27 | }; 28 | export const SidebarItem = ({ link, ignoreCollapse }: SidebarItemProps) => { 29 | const isOpen = useSidebar((state) => state.isOpen) || ignoreCollapse; 30 | const t = useTranslations('Dashboard'); 31 | 32 | if (link.type === 'divider') { 33 | if (!isOpen) return null; 34 | return ( 35 |
  • 36 | 37 | {t(link.title)} 38 | 39 |
  • 40 | ); 41 | } 42 | 43 | if (link.children) 44 | return ( 45 | 46 | 47 | 48 | ); 49 | 50 | return ( 51 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | type NavLinkProps = { link: SidebarLinkItem; ignoreCollapse?: boolean }; 58 | 59 | const SidebarNavLink = ({ link, ignoreCollapse }: NavLinkProps) => { 60 | const pathname = usePathname(); 61 | const isActive = `/dashboard${link.href}` === pathname; 62 | const isOpen = useSidebar((state) => state.isOpen) || ignoreCollapse; 63 | const t = useTranslations('Dashboard'); 64 | 65 | const href = link.hrefAsIs ? link.href : `/dashboard${link.href}`; 66 | 67 | if (!isOpen) 68 | return ( 69 | 70 | 71 | 78 | 79 | 80 | 81 | 82 | {t(link.title)} 83 | 84 | 85 | ); 86 | 87 | return ( 88 | 95 | 96 | {t(link.title)} 97 | 98 | ); 99 | }; 100 | 101 | const SidebarItemAccordion = ({ link, ignoreCollapse }: NavLinkProps) => { 102 | const isOpen = useSidebar((state) => state.isOpen) || ignoreCollapse; 103 | const dir = useDir(); 104 | const isRTL = dir === 'rtl'; 105 | const t = useTranslations('Dashboard'); 106 | 107 | if (!isOpen) { 108 | return ( 109 | 110 | 111 | 112 | 113 | 119 | 120 | 121 | 125 | {t(link.title)} 126 | 127 | 128 | 129 | 130 | {t(link.title)} 131 | 132 | {link.children!.map((child) => ( 133 | 134 | 135 | {t(child.title)} 136 | 137 | 138 | ))} 139 | 140 | 141 | ); 142 | } 143 | 144 | return ( 145 | 146 | 147 | 152 |
    153 | 154 |
    155 | {t(link.title)} 156 |
    157 | 158 | {link.children!.map((child) => ( 159 | 166 |
    167 | {t(child.title)} 168 | 169 | ))} 170 | 171 | 172 | 173 | ); 174 | }; 175 | -------------------------------------------------------------------------------- /src/components/layouts/dashboard-layout/sidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { cn } from '@/utils/cn'; 4 | import { ArrowRight, ArrowLeft } from 'lucide-react'; 5 | import { SidebarItem } from './sidebar-item'; 6 | import { useSidebar } from '@/zustand/sidebar-store'; 7 | import { TooltipProvider } from '@/ui/tooltip'; 8 | import { sidebarLinks } from '@/constants/sidebar-links'; 9 | 10 | export const Sidebar = () => { 11 | const { isOpen, toggle } = useSidebar((state) => state); 12 | return ( 13 | <> 14 | {/* To mock sidebar collapsing since aside is fixed for main tag */} 15 | 53 | } 54 | /> 55 | 56 | 57 | ); 58 | }; 59 | 60 | type SidebarNavProps = { 61 | heading: React.ReactNode; 62 | ignoreCollapse?: boolean; 63 | }; 64 | 65 | export const SidebarNav = ({ heading, ignoreCollapse }: SidebarNavProps) => { 66 | return ( 67 | 79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /src/components/layouts/dashboard-layout/user-nav.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { NextLink } from '@/components/common'; 3 | import { Button } from '@/ui/button'; 4 | import { useDir } from '@/hooks/use-dir'; 5 | import { Avatar, AvatarFallback } from '@/ui/avatar'; 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuGroup, 10 | DropdownMenuItem, 11 | DropdownMenuLabel, 12 | DropdownMenuSeparator, 13 | DropdownMenuShortcut, 14 | DropdownMenuTrigger, 15 | } from '@/ui/dropdown-menu'; 16 | import { useTranslations } from 'use-intl'; 17 | import { useState } from 'react'; 18 | import dynamic from 'next/dynamic'; 19 | const LogoutModal = dynamic(() => import('./logout-modal').then((mod) => mod.LogoutModal), { 20 | ssr: false, 21 | }); 22 | 23 | type NavItem = { 24 | title: TranslationKey; 25 | shortcut: string; 26 | href: string; 27 | }; 28 | 29 | const nav_items: NavItem[] = [ 30 | { 31 | title: 'myaccount', 32 | shortcut: '⇧⌘P', 33 | href: '/account', 34 | }, 35 | { 36 | title: 'settings', 37 | shortcut: '⇧⌘S', 38 | href: '/settings', 39 | }, 40 | 41 | { 42 | title: 'billing', 43 | shortcut: '⇧⌘B', 44 | href: '/billing', 45 | }, 46 | ]; 47 | 48 | export const UserNav = () => { 49 | const [isLogoutModalOpen, setLogoutModalOpen] = useState(false); 50 | const t = useTranslations('Dashboard'); 51 | const dir = useDir(); 52 | 53 | return ( 54 | <> 55 | setLogoutModalOpen(false)} isOpen={isLogoutModalOpen} /> 56 | 57 | 58 | 59 | 64 | 65 | 66 | 67 |
    68 |
    mohammadou
    69 |
    email.user@email.com
    70 |
    71 |
    72 | 73 | 74 | {nav_items.map((item) => ( 75 | 76 | 77 | {t(item.title)} 78 | {item.shortcut} 79 | 80 | 81 | ))} 82 | 83 | 84 | setLogoutModalOpen(true)}> 85 |
    86 | {t('logout')} 87 | ⇧⌘Q 88 |
    89 |
    90 |
    91 |
    92 | 93 | ); 94 | }; 95 | -------------------------------------------------------------------------------- /src/components/layouts/locale-layout/index.tsx: -------------------------------------------------------------------------------- 1 | import pick from 'lodash/pick'; 2 | import { NextIntlClientProvider, useMessages } from 'next-intl'; 3 | 4 | export default function LocaleLayout({ children, params: { locale } }: LayoutProps) { 5 | // ... 6 | const messages = useMessages(); 7 | 8 | return ( 9 | 10 | 11 | 12 | {children} 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/screens/auth/forgot-password-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Button } from '@/ui/button'; 3 | import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/ui/form'; 4 | import { Input } from '@/ui/input'; 5 | import { wait } from '@/utils/wait'; 6 | import { zodResolver } from '@hookform/resolvers/zod'; 7 | import { Loader2, MailIcon } from 'lucide-react'; 8 | import { MessageKeys, useTranslations } from 'next-intl'; 9 | import { useState } from 'react'; 10 | import { useForm } from 'react-hook-form'; 11 | import { toast } from 'sonner'; 12 | import { z } from 'zod'; 13 | 14 | const forgotPasswordSchema = z.object({ 15 | email: z 16 | .string() 17 | .min(1, { message: 'Validation.email_required' }) 18 | .email({ message: 'Validation.email_invalid' }), 19 | }); 20 | 21 | export type ForgotPasswordFormValues = z.infer; 22 | 23 | export const ForgotPasswordForm = () => { 24 | const t = useTranslations(); 25 | const [isPending, setIsPending] = useState(false); 26 | const form = useForm({ 27 | resolver: zodResolver(forgotPasswordSchema), 28 | defaultValues: { 29 | email: '', 30 | }, 31 | }); 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 34 | const onSubmit = async (values: ForgotPasswordFormValues) => { 35 | setIsPending(true); 36 | await wait(1); 37 | toast.success(`${t('ForgotPassword.success')}: ${values.email}`, { 38 | closeButton: true, 39 | }); 40 | setIsPending(false); 41 | form.reset(); 42 | }; 43 | 44 | return ( 45 |
    46 | 47 | ( 51 | 52 | {t('Common.email')} 53 | 54 | } 56 | error={!!error?.message} 57 | placeholder={t('Common.email')} 58 | {...field} 59 | autoComplete='off' 60 | /> 61 | 62 | 63 | {error?.message && t(error.message as MessageKeys)} 64 | 65 | 66 | )} 67 | /> 68 |
    69 | 73 |
    74 | 75 | 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/screens/auth/login-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useLogin } from '@/apis/auth/use-login.mutation'; 3 | import { Button } from '@/ui/button'; 4 | import { Checkbox } from '@/ui/checkbox'; 5 | import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/ui/form'; 6 | import { Input } from '@/ui/input'; 7 | import { zodResolver } from '@hookform/resolvers/zod'; 8 | import { Loader2, LockIcon, MailIcon } from 'lucide-react'; 9 | import { useTranslations } from 'next-intl'; 10 | import { useForm } from 'react-hook-form'; 11 | import { z } from 'zod'; 12 | import { NextLink } from '@/components/common'; 13 | import { toast } from 'sonner'; 14 | import { useRouter } from '@/i18n'; 15 | import jsCookie from 'js-cookie'; 16 | import { DEFAULT_REMEMBER_ME } from '@/config'; 17 | 18 | const loginSchema = (t: (key: IntlPath) => string) => 19 | z.object({ 20 | email: z 21 | .string() 22 | .min(1, { message: t('Validation.email_required') }) 23 | .email({ message: t('Validation.email_invalid') }), 24 | password: z.string().min(1, { message: t('Validation.password_required') }), 25 | remember: z.optional(z.boolean()), 26 | }); 27 | 28 | export type LoginFormValues = z.infer>; 29 | 30 | export const LoginForm = () => { 31 | const t = useTranslations(); 32 | const { mutate, isPending } = useLogin(); 33 | const router = useRouter(); 34 | 35 | const form = useForm({ 36 | resolver: zodResolver(loginSchema(t)), 37 | defaultValues: { 38 | email: 'admin@admin.com', 39 | password: 'admin123', 40 | remember: false, 41 | }, 42 | }); 43 | 44 | const onSubmit = (values: LoginFormValues) => { 45 | mutate(values, { 46 | onError: (e) => toast.error(e.message, { closeButton: true }), 47 | onSuccess: ({ token }) => { 48 | toast.success(t('Login.success'), { 49 | id: 'login-success', 50 | closeButton: true, 51 | }); 52 | jsCookie.set('token', token, { 53 | expires: DEFAULT_REMEMBER_ME || 1, 54 | path: '/', 55 | }); 56 | router.replace('/dashboard'); 57 | }, 58 | }); 59 | }; 60 | 61 | return ( 62 |
    63 | 64 |
    65 | ( 69 | 70 | {t('Common.email')} 71 | 72 | } 75 | error={!!error?.message} 76 | placeholder={t('Common.email')} 77 | {...field} 78 | autoComplete='email' 79 | /> 80 | 81 | {error?.message} 82 | 83 | )} 84 | /> 85 | ( 89 | 90 | {t('Common.password')} 91 | 92 | } 94 | error={!!error?.message} 95 | placeholder={t('Common.password')} 96 | type='password' 97 | {...field} 98 | autoComplete='current-password' 99 | /> 100 | 101 | {error?.message} 102 | 103 | )} 104 | /> 105 |
    106 |
    107 | ( 111 | 112 | 113 | 114 | 115 | {t('Login.remember_me')} 116 | 117 | )} 118 | /> 119 | 122 | {t('Common.forgot_password')} 123 | 124 |
    125 |
    126 | 130 |
    131 |
    132 |

    133 | {t('Login.no_account')}{' '} 134 | 137 | {t('Login.signup')} 138 | 139 |

    140 |
    141 |
    142 | 143 | ); 144 | }; 145 | -------------------------------------------------------------------------------- /src/components/screens/auth/signup-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useLogin } from '@/apis/auth/use-login.mutation'; 3 | import { Button } from '@/ui/button'; 4 | import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/ui/form'; 5 | import { Input } from '@/ui/input'; 6 | import { zodResolver } from '@hookform/resolvers/zod'; 7 | import { CaptionsIcon, Loader2, LockIcon, MailIcon, UserIcon } from 'lucide-react'; 8 | import { useTranslations } from 'next-intl'; 9 | import { useForm } from 'react-hook-form'; 10 | import { z } from 'zod'; 11 | import { toast } from 'sonner'; 12 | import { useRouter } from '@/i18n'; 13 | import jsCookie from 'js-cookie'; 14 | import { DEFAULT_REMEMBER_ME } from '@/config'; 15 | import { NextLink } from '@/components/common'; 16 | 17 | const loginSchema = (t: (key: IntlPath) => string) => 18 | z 19 | .object({ 20 | full_name: z.string().min(4, { message: t('Validation.full_name_required') }), 21 | username: z.string().min(4, { message: t('Validation.username_required') }), 22 | email: z 23 | .string() 24 | .min(1, { message: t('Validation.email_required') }) 25 | .email({ message: t('Validation.email_invalid') }), 26 | password: z.string().min(1, { message: t('Validation.password_required') }), 27 | password_confirmation: z 28 | .string() 29 | .min(1, { message: t('Validation.password_confirmation_required') }), 30 | }) 31 | .refine((data) => data.password === data.password_confirmation, { 32 | message: t('Validation.password_mismatch'), 33 | path: ['password_confirmation'], 34 | }); 35 | export type SignupFormValues = z.infer>; 36 | 37 | export const SignupForm = () => { 38 | const t = useTranslations(); 39 | const { mutate, isPending } = useLogin(); 40 | const router = useRouter(); 41 | 42 | const form = useForm({ 43 | resolver: zodResolver(loginSchema(t)), 44 | defaultValues: { 45 | email: '', 46 | password: '', 47 | password_confirmation: '', 48 | full_name: '', 49 | username: '', 50 | }, 51 | }); 52 | 53 | const onSubmit = (values: SignupFormValues) => { 54 | mutate(values, { 55 | onError: (e) => toast.error(e.message, { closeButton: true }), 56 | onSuccess: ({ token }) => { 57 | toast.success(t('Signup.success'), { 58 | id: 'signup-success', 59 | closeButton: true, 60 | }); 61 | jsCookie.set('token', token, { 62 | expires: DEFAULT_REMEMBER_ME || 1, 63 | path: '/', 64 | }); 65 | router.replace('/dashboard'); 66 | }, 67 | }); 68 | }; 69 | 70 | return ( 71 |
    72 | 73 |
    74 | ( 78 | 79 | {t('Common.username')} 80 | 81 | } 83 | error={!!error?.message} 84 | placeholder={t('Common.username')} 85 | {...field} 86 | autoComplete='username' 87 | /> 88 | 89 | {error?.message} 90 | 91 | )} 92 | /> 93 | ( 97 | 98 | {t('Common.full_name')} 99 | 100 | } 102 | error={!!error?.message} 103 | placeholder={t('Common.full_name')} 104 | {...field} 105 | autoComplete='full_name' 106 | /> 107 | 108 | {error?.message} 109 | 110 | )} 111 | /> 112 | ( 116 | 117 | {t('Common.email')} 118 | 119 | } 122 | error={!!error?.message} 123 | placeholder={t('Common.email')} 124 | {...field} 125 | autoComplete='email' 126 | /> 127 | 128 | {error?.message} 129 | 130 | )} 131 | /> 132 | ( 136 | 137 | {t('Common.password')} 138 | 139 | } 141 | error={!!error?.message} 142 | placeholder={t('Common.password')} 143 | type='password' 144 | autoComplete='new-password' 145 | {...field} 146 | /> 147 | 148 | {error?.message} 149 | 150 | )} 151 | /> 152 | ( 156 | 157 | {t('Common.password_confirmation')} 158 | 159 | } 161 | error={!!error?.message} 162 | placeholder={t('Common.password_confirmation')} 163 | type='password' 164 | {...field} 165 | /> 166 | 167 | {error?.message} 168 | 169 | )} 170 | /> 171 |
    172 |
    173 | 177 |
    178 |
    179 |
    180 | {t('Signup.already_have_account')} 181 | 184 |
    185 |
    186 | 187 | ); 188 | }; 189 | -------------------------------------------------------------------------------- /src/components/screens/charts/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { 3 | BarChart, 4 | LineChart, 5 | XAxis, 6 | YAxis, 7 | CartesianGrid, 8 | Line, 9 | Bar, 10 | PieChart, 11 | Pie, 12 | Cell, 13 | AreaChart, 14 | Area, 15 | RadarChart, 16 | PolarGrid, 17 | PolarAngleAxis, 18 | PolarRadiusAxis, 19 | Radar, 20 | ScatterChart, 21 | Scatter, 22 | ZAxis, 23 | } from 'recharts'; 24 | import { 25 | ChartContainer, 26 | ChartConfig, 27 | ChartTooltipContent, 28 | ChartTooltip, 29 | ChartLegend, 30 | ChartLegendContent, 31 | } from '@/components/ui/chart'; 32 | import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; 33 | import * as React from 'react'; 34 | 35 | export const ChartsPageContent = () => { 36 | const lineChartData = [ 37 | { name: 'January', Sales: 65, Expenses: 30 }, 38 | { name: 'February', Sales: 59, Expenses: 20 }, 39 | { name: 'March', Sales: 80, Expenses: 50 }, 40 | { name: 'April', Sales: 81, Expenses: 40 }, 41 | { name: 'May', Sales: 56, Expenses: 30 }, 42 | { name: 'June', Sales: 55, Expenses: 20 }, 43 | { name: 'July', Sales: 70, Expenses: 50 }, 44 | { name: 'August', Sales: 90, Expenses: 60 }, 45 | { name: 'September', Sales: 100, Expenses: 70 }, 46 | { name: 'October', Sales: 110, Expenses: 80 }, 47 | { name: 'November', Sales: 120, Expenses: 90 }, 48 | { name: 'December', Sales: 130, Expenses: 100 }, 49 | ]; 50 | 51 | const barChartData = [ 52 | { name: 'Red', Votes: 12 }, 53 | { name: 'Blue', Votes: 19 }, 54 | { name: 'Yellow', Votes: 3 }, 55 | { name: 'Green', Votes: 5 }, 56 | { name: 'Purple', Votes: 2 }, 57 | { name: 'Orange', Votes: 3 }, 58 | { name: 'Pink', Votes: 8 }, 59 | { name: 'Brown', Votes: 6 }, 60 | ]; 61 | 62 | const pieChartData = [ 63 | { name: 'Group A', value: 400, color: '#0088FE' }, 64 | { name: 'Group B', value: 300, color: '#00C49F' }, 65 | { name: 'Group C', value: 300, color: '#FFBB28' }, 66 | { name: 'Group D', value: 200, color: '#FF8042' }, 67 | { name: 'Group E', value: 100, color: '#FF6384' }, 68 | { name: 'Group F', value: 50, color: '#36A2EB' }, 69 | ]; 70 | 71 | const areaChartData = [ 72 | { month: 'Jan', uv: 4000, pv: 2400, amt: 2400 }, 73 | { month: 'Feb', uv: 3000, pv: 1398, amt: 2210 }, 74 | { month: 'Mar', uv: 2000, pv: 9800, amt: 2290 }, 75 | { month: 'Apr', uv: 2780, pv: 3908, amt: 2000 }, 76 | { month: 'May', uv: 1890, pv: 4800, amt: 2181 }, 77 | { month: 'Jun', uv: 2390, pv: 3800, amt: 2500 }, 78 | { month: 'Jul', uv: 3490, pv: 4300, amt: 2100 }, 79 | { month: 'Aug', uv: 4000, pv: 2400, amt: 2400 }, 80 | { month: 'Sep', uv: 3000, pv: 1398, amt: 2210 }, 81 | { month: 'Oct', uv: 2000, pv: 9800, amt: 2290 }, 82 | { month: 'Nov', uv: 2780, pv: 3908, amt: 2000 }, 83 | { month: 'Dec', uv: 1890, pv: 4800, amt: 2181 }, 84 | ]; 85 | 86 | const radarChartData = [ 87 | { subject: 'Math', A: 120, B: 110, fullMark: 150 }, 88 | { subject: 'Chinese', A: 98, B: 130, fullMark: 150 }, 89 | { subject: 'English', A: 86, B: 130, fullMark: 150 }, 90 | { subject: 'Geography', A: 99, B: 100, fullMark: 150 }, 91 | { subject: 'Physics', A: 85, B: 90, fullMark: 150 }, 92 | { subject: 'History', A: 65, B: 85, fullMark: 150 }, 93 | { subject: 'Biology', A: 90, B: 95, fullMark: 150 }, 94 | { subject: 'Chemistry', A: 80, B: 85, fullMark: 150 }, 95 | ]; 96 | 97 | const scatterChartData = [ 98 | { x: 100, y: 200, z: 200 }, 99 | { x: 120, y: 100, z: 260 }, 100 | { x: 170, y: 300, z: 400 }, 101 | { x: 140, y: 250, z: 280 }, 102 | { x: 150, y: 400, z: 500 }, 103 | { x: 110, y: 280, z: 200 }, 104 | { x: 130, y: 220, z: 300 }, 105 | { x: 160, y: 350, z: 450 }, 106 | ]; 107 | 108 | const pieChartConfig = pieChartData.reduce( 109 | (acc, item) => { 110 | acc[item.name] = { label: item.name, color: item.color }; 111 | return acc; 112 | }, 113 | {} as Record 114 | ); 115 | 116 | const chartConfig = { 117 | Sales: { label: 'Sales', color: 'hsl(var(--primary))' }, 118 | Expenses: { label: 'Expenses', color: 'hsl(var(--secondary))' }, 119 | Votes: { label: 'Votes', color: 'hsl(var(--primary))' }, 120 | uv: { label: 'UV', color: 'hsl(var(--primary))' }, 121 | pv: { label: 'PV', color: 'hsl(var(--secondary))' }, 122 | ...pieChartConfig, 123 | } satisfies ChartConfig; 124 | 125 | return ( 126 |
    127 | 128 | 129 | Line Chart 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | } /> 138 | } /> 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | Bar Chart 148 | 149 | 150 | 151 | 155 | 156 | 157 | 158 | } /> 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | Pie Chart 167 | 168 | 169 | 170 | 171 | 178 | {pieChartData.map((entry, index) => ( 179 | 180 | ))} 181 | 182 | } /> 183 | } /> 184 | 185 | 186 | 187 | 188 | 189 | 190 | Area Chart 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | } /> 199 | } /> 200 | 206 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | Radar Chart 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 233 | 240 | } /> 241 | 242 | 243 | 244 | 245 | 246 | 247 | Scatter Chart 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | } /> 257 | 258 | 259 | 260 | 261 | 262 |
    263 | ); 264 | }; 265 | -------------------------------------------------------------------------------- /src/components/screens/customers/customers-list/customer-columns.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowUp, ArrowDown, Edit, Eye, Trash, MoreHorizontal } from 'lucide-react'; 2 | import { Button } from '@/components/ui/button'; 3 | import { Checkbox } from '@/components/ui/checkbox'; 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuTrigger, 9 | } from '@/components/ui/dropdown-menu'; 10 | import { 11 | Dialog, 12 | DialogTrigger, 13 | DialogContent, 14 | DialogTitle, 15 | DialogDescription, 16 | DialogFooter, 17 | } from '@/components/ui/dialog'; 18 | import { ColumnDef } from '@tanstack/react-table'; 19 | import { Customer } from './mock-customers'; 20 | 21 | export const customer_columns: ColumnDef[] = [ 22 | { 23 | id: 'select', 24 | header: ({ table }) => ( 25 | table.toggleAllPageRowsSelected(!!value)} 28 | aria-label='Select all' 29 | /> 30 | ), 31 | cell: ({ row }) => ( 32 | row.toggleSelected(!!value)} 35 | aria-label='Select row' 36 | /> 37 | ), 38 | enableSorting: false, 39 | enableHiding: false, 40 | }, 41 | { 42 | accessorKey: 'id', 43 | header: 'ID', 44 | }, 45 | { 46 | accessorKey: 'name', 47 | header: ({ column }) => ( 48 |
    49 | Name 50 | 57 |
    58 | ), 59 | }, 60 | { 61 | accessorKey: 'email', 62 | header: ({ column }) => ( 63 |
    64 | Email 65 | 72 |
    73 | ), 74 | }, 75 | { 76 | id: 'actions', 77 | header: 'Actions', 78 | cell: ({ row }) => ( 79 |
    80 | 81 | 82 | 83 | 86 | 87 | 88 | alert(`Edit ${row.original.name}`)}> 89 | Edit 90 | 91 | alert(`View ${row.original.name}`)}> 92 | View 93 | 94 | 95 | 96 |
    97 | Delete 98 |
    99 |
    100 |
    101 |
    102 |
    103 | 104 | Confirm Delete 105 | 106 | Are you sure you want to delete {row.original.name}? 107 | 108 | 109 | 110 | 113 | 114 | 115 |
    116 |
    117 | ), 118 | }, 119 | ]; 120 | -------------------------------------------------------------------------------- /src/components/screens/customers/customers-list/customers-data-table.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | import { 5 | flexRender, 6 | getCoreRowModel, 7 | useReactTable, 8 | getPaginationRowModel, 9 | getSortedRowModel, 10 | } from '@tanstack/react-table'; 11 | import { 12 | Table, 13 | TableBody, 14 | TableCell, 15 | TableHead, 16 | TableHeader, 17 | TableRow, 18 | } from '@/components/ui/table'; 19 | 20 | import { Customer } from './mock-customers'; 21 | import { PaginationItems } from '@/components/common/responsive-pagination'; 22 | import { customer_columns } from './customer-columns'; 23 | 24 | interface DataTableProps { 25 | data: Customer[]; 26 | } 27 | 28 | export function DataTable({ data }: DataTableProps) { 29 | const [rowSelection, setRowSelection] = useState({}); 30 | const [sorting, setSorting] = useState([]); 31 | const table = useReactTable({ 32 | data, 33 | columns: customer_columns, 34 | getCoreRowModel: getCoreRowModel(), 35 | getPaginationRowModel: getPaginationRowModel(), 36 | getSortedRowModel: getSortedRowModel(), 37 | onRowSelectionChange: setRowSelection, 38 | onSortingChange: setSorting, 39 | state: { 40 | rowSelection, 41 | sorting, 42 | }, 43 | }); 44 | 45 | const currentPage = table.getState().pagination.pageIndex + 1; 46 | const totalPages = table.getPageCount(); 47 | 48 | return ( 49 |
    50 |
    51 | 52 | 53 | {table.getHeaderGroups().map((headerGroup) => ( 54 | 55 | {headerGroup.headers.map((header) => ( 56 | 57 | {header.isPlaceholder 58 | ? null 59 | : flexRender(header.column.columnDef.header, header.getContext())} 60 | 61 | ))} 62 | 63 | ))} 64 | 65 | 66 | {table.getRowModel().rows?.length ? ( 67 | table.getRowModel().rows.map((row) => ( 68 | 69 | {row.getVisibleCells().map((cell) => ( 70 | 71 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 72 | 73 | ))} 74 | 75 | )) 76 | ) : ( 77 | 78 | 79 | No results. 80 | 81 | 82 | )} 83 | 84 |
    85 |
    86 |
    87 | table.setPageIndex(page - 1)} 91 | onPreviousPage={() => table.previousPage()} 92 | onNextPage={() => table.nextPage()} 93 | canPreviousPage={table.getCanPreviousPage()} 94 | canNextPage={table.getCanNextPage()} 95 | /> 96 |
    97 |
    98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /src/components/screens/customers/customers-list/customers-table.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | import { Input } from '@/components/ui/input'; 5 | import { DataTable } from './customers-data-table'; 6 | import { Customer } from './mock-customers'; 7 | 8 | interface CustomersTableProps { 9 | customers: Customer[]; 10 | } 11 | 12 | export function CustomersTable({ customers }: CustomersTableProps) { 13 | const [filteredCustomers, setFilteredCustomers] = useState(customers); 14 | 15 | const handleFilter = (event: React.ChangeEvent) => { 16 | const value = event.target.value.toLowerCase(); 17 | const filtered = customers.filter( 18 | (customer) => 19 | customer.name.toLowerCase().includes(value) || customer.email.toLowerCase().includes(value) 20 | ); 21 | setFilteredCustomers(filtered); 22 | }; 23 | 24 | return ( 25 |
    26 | 27 | 28 |
    29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/screens/customers/customers-list/mock-customers.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | 3 | export interface Customer { 4 | id: number; 5 | name: string; 6 | email: string; 7 | } 8 | 9 | export function generateCustomers(count: number): Customer[] { 10 | return Array.from({ length: count }, (_, index) => ({ 11 | id: index + 1, 12 | name: faker.person.fullName(), 13 | email: faker.internet.email(), 14 | })); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/screens/dashboard/overview-chart.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect } from 'react'; 4 | import { Card, CardContent, CardHeader } from '../../ui/card'; 5 | import { 6 | ChartConfig, 7 | ChartContainer, 8 | ChartTooltip, 9 | ChartTooltipContent, 10 | } from '@/components/ui/chart'; 11 | import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer } from 'recharts'; 12 | import { useDir } from '@/hooks/use-dir'; 13 | 14 | const data = [ 15 | { month: 'Jan', sales: 4000 }, 16 | { month: 'Feb', sales: 3000 }, 17 | { month: 'Mar', sales: 2000 }, 18 | { month: 'Apr', sales: 2780 }, 19 | { month: 'May', sales: 1890 }, 20 | { month: 'Jun', sales: 2390 }, 21 | { month: 'Jul', sales: 3490 }, 22 | { month: 'Aug', sales: 4000 }, 23 | { month: 'Sep', sales: 3000 }, 24 | { month: 'Oct', sales: 2000 }, 25 | { month: 'Nov', sales: 2780 }, 26 | { month: 'Dec', sales: 1890 }, 27 | ]; 28 | 29 | type OverviewChartProps = { 30 | title: string; 31 | }; 32 | 33 | export const OverviewChart = ({ title }: OverviewChartProps) => { 34 | const direction = useDir(); 35 | 36 | useEffect(() => { 37 | // Suppressing defaultProps warning from recharts until it's fixed 38 | const error = console.error; 39 | console.error = (...args: any) => { 40 | if (/defaultProps/.test(args[0])) return; 41 | error(...args); 42 | }; 43 | }, []); 44 | 45 | const chartConfig = { 46 | sales: { label: 'Sales', color: 'hsl(var(--primary))' }, 47 | } satisfies ChartConfig; 48 | 49 | return ( 50 | 51 | {title} 52 | 53 | 54 | 55 | 61 | 72 | `$${v}`} 78 | style={{ 79 | fill: 'var(--accent-foreground)', 80 | }} 81 | /> 82 | } 86 | cursor={{ 87 | className: 'fill-accent', 88 | }} 89 | /> 90 | 91 | 92 | 93 | 94 | 95 | 96 | ); 97 | }; 98 | -------------------------------------------------------------------------------- /src/components/screens/dashboard/recent-sales.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations } from 'next-intl/server'; 2 | import { Avatar, AvatarFallback } from '../../ui/avatar'; 3 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card'; 4 | // import { ScrollArea } from '@/components/ui/scroll-area'; 5 | 6 | const sales = [ 7 | { 8 | id: '1', 9 | name: 'Olivia Martin', 10 | email: 'olivia.martins@email.com', 11 | amount: '+$1,999.00', 12 | avatar: '/avatars/01.png', 13 | fallback: 'OM', 14 | }, 15 | { 16 | id: '2', 17 | name: 'Jackson Lee', 18 | email: 'jackson.lee@email.com', 19 | amount: '+$39.00', 20 | avatar: '/avatars/02.png', 21 | fallback: 'JL', 22 | }, 23 | { 24 | id: '3', 25 | name: 'Isabella Nguyen', 26 | email: 'isabella.nguyen@email.com', 27 | amount: '+$299.00', 28 | avatar: '/avatars/03.png', 29 | fallback: 'IN', 30 | }, 31 | { 32 | id: '4', 33 | name: 'William Kim', 34 | email: 'will@email.com', 35 | amount: '+$99.00', 36 | avatar: '/avatars/04.png', 37 | fallback: 'WK', 38 | }, 39 | { 40 | id: '5', 41 | name: 'Sofia Davis', 42 | email: 'sofia.davis@email.com', 43 | amount: '+$39.00', 44 | avatar: '/avatars/05.png', 45 | fallback: 'SD', 46 | }, 47 | ]; 48 | 49 | export const RecentSales = async () => { 50 | const t = await getTranslations(); 51 | return ( 52 | 53 | 54 | {t('Common.recent_sales')} 55 | You made 5 sales in the last 24 hours. 56 | 57 | {/* */} 58 | 59 |
    60 | {sales.map((sale) => ( 61 |
    62 | 63 | {/* */} 64 | {sale.fallback} 65 | 66 |
    67 |

    {sale.name}

    68 |

    {sale.email}

    69 |
    70 |
    71 | {sale.amount} 72 |
    73 |
    74 | ))} 75 |
    76 |
    77 | {/*
    */} 78 |
    79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /src/components/screens/dashboard/stats-widget.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card'; 2 | 3 | export type StatsWidgetProps = { 4 | title: string; 5 | content: string; 6 | subContent?: string; 7 | icon: React.ReactNode; 8 | }; 9 | 10 | export const StatsWidget = ({ content, icon, subContent, title }: StatsWidgetProps) => { 11 | return ( 12 | 13 | 14 | {title} 15 |
    {icon}
    16 |
    17 | 18 |
    {content}
    19 | {subContent &&

    {subContent}

    } 20 |
    21 |
    22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as AccordionPrimitive from '@radix-ui/react-accordion'; 5 | import { ChevronDown } from 'lucide-react'; 6 | 7 | import { cn } from '@/utils/cn'; 8 | 9 | const Accordion = AccordionPrimitive.Root; 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 16 | )); 17 | AccordionItem.displayName = 'AccordionItem'; 18 | 19 | const AccordionTrigger = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef 22 | >(({ className, children, ...props }, ref) => ( 23 | 24 | svg]:rotate-180', 28 | className 29 | )} 30 | {...props}> 31 | <> 32 | {children} 33 | 34 | 35 | 36 | 37 | )); 38 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 39 | 40 | const AccordionContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, children, ...props }, ref) => ( 44 | 48 |
    {children}
    49 |
    50 | )); 51 | 52 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 53 | 54 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 55 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as AvatarPrimitive from '@radix-ui/react-avatar'; 5 | 6 | import { cn } from '@/utils/cn'; 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 17 | )); 18 | Avatar.displayName = AvatarPrimitive.Root.displayName; 19 | 20 | const AvatarImage = React.forwardRef< 21 | React.ElementRef, 22 | React.ComponentPropsWithoutRef 23 | >(({ className, ...props }, ref) => ( 24 | 29 | )); 30 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 31 | 32 | const AvatarFallback = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, ...props }, ref) => ( 36 | 44 | )); 45 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 46 | 47 | export { Avatar, AvatarImage, AvatarFallback }; 48 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Slot } from '@radix-ui/react-slot'; 3 | import { cva, type VariantProps } from 'class-variance-authority'; 4 | 5 | import { cn } from '@/utils/cn'; 6 | 7 | const buttonVariants = cva( 8 | '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', 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90', 13 | destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 14 | outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', 15 | secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', 16 | ghost: 'hover:bg-accent hover:text-accent-foreground', 17 | link: 'text-primary underline-offset-4 hover:underline', 18 | }, 19 | size: { 20 | default: 'h-10 px-4 py-2', 21 | sm: 'h-9 rounded-md px-3', 22 | lg: 'h-11 rounded-md px-8', 23 | icon: 'h-10 w-10', 24 | }, 25 | }, 26 | defaultVariants: { 27 | variant: 'default', 28 | size: 'default', 29 | }, 30 | } 31 | ); 32 | 33 | export interface ButtonProps 34 | extends React.ButtonHTMLAttributes, 35 | VariantProps { 36 | asChild?: boolean; 37 | } 38 | 39 | const Button = React.forwardRef( 40 | ({ className, variant, size, asChild = false, ...props }, ref) => { 41 | const Comp = asChild ? Slot : 'button'; 42 | return ( 43 | 44 | ); 45 | } 46 | ); 47 | Button.displayName = 'Button'; 48 | 49 | export { Button, buttonVariants }; 50 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/utils/cn'; 2 | import * as React from 'react'; 3 | 4 | const Card = React.forwardRef>( 5 | ({ className, ...props }, ref) => ( 6 |
    11 | ) 12 | ); 13 | Card.displayName = 'Card'; 14 | 15 | const CardHeader = React.forwardRef>( 16 | ({ className, ...props }, ref) => ( 17 |
    18 | ) 19 | ); 20 | CardHeader.displayName = 'CardHeader'; 21 | 22 | const CardTitle = React.forwardRef>( 23 | ({ className, ...props }, ref) => ( 24 |

    29 | ) 30 | ); 31 | CardTitle.displayName = 'CardTitle'; 32 | 33 | const CardDescription = React.forwardRef< 34 | HTMLParagraphElement, 35 | React.HTMLAttributes 36 | >(({ className, ...props }, ref) => ( 37 |

    38 | )); 39 | CardDescription.displayName = 'CardDescription'; 40 | 41 | const CardContent = React.forwardRef>( 42 | ({ className, ...props }, ref) => ( 43 |

    44 | ) 45 | ); 46 | CardContent.displayName = 'CardContent'; 47 | 48 | const CardFooter = React.forwardRef>( 49 | ({ className, ...props }, ref) => ( 50 |
    51 | ) 52 | ); 53 | CardFooter.displayName = 'CardFooter'; 54 | 55 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; 56 | -------------------------------------------------------------------------------- /src/components/ui/chart.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { cn } from '@/utils/cn'; 4 | import * as React from 'react'; 5 | import * as RechartsPrimitive from 'recharts'; 6 | 7 | // Format: { THEME_NAME: CSS_SELECTOR } 8 | const THEMES = { light: '', dark: '.dark' } as const; 9 | 10 | export type ChartConfig = { 11 | [k in string]: { 12 | label?: React.ReactNode; 13 | icon?: React.ComponentType; 14 | } & ( 15 | | { color?: string; theme?: never } 16 | | { color?: never; theme: Record } 17 | ); 18 | }; 19 | 20 | type ChartContextProps = { 21 | config: ChartConfig; 22 | }; 23 | 24 | const ChartContext = React.createContext(null); 25 | 26 | function useChart() { 27 | const context = React.useContext(ChartContext); 28 | 29 | if (!context) { 30 | throw new Error('useChart must be used within a '); 31 | } 32 | 33 | return context; 34 | } 35 | 36 | const ChartContainer = React.forwardRef< 37 | HTMLDivElement, 38 | React.ComponentProps<'div'> & { 39 | config: ChartConfig; 40 | children: React.ComponentProps['children']; 41 | } 42 | >(({ id, className, children, config, ...props }, ref) => { 43 | const uniqueId = React.useId(); 44 | const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`; 45 | 46 | return ( 47 | 48 |
    56 | 57 | {children} 58 |
    59 |
    60 | ); 61 | }); 62 | ChartContainer.displayName = 'Chart'; 63 | 64 | const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { 65 | const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color); 66 | 67 | if (!colorConfig.length) { 68 | return null; 69 | } 70 | 71 | return ( 72 |