├── public ├── _redirects ├── icon.png ├── logo.png └── logo.svg ├── src ├── interfaces │ ├── admin.tsx │ ├── review.tsx │ └── user.tsx ├── assets │ └── img │ │ └── login-bg.png ├── routes │ ├── web.tsx │ ├── api.tsx │ ├── requireAuth.tsx │ └── browserRouter.tsx ├── constants │ └── index.tsx ├── pages │ ├── errors │ │ ├── errorPage.tsx │ │ └── notfoundPage.tsx │ ├── auth │ │ └── loginPage.tsx │ ├── users │ │ └── userListPage.tsx │ ├── aboutPage.tsx │ └── dashboardPage.tsx ├── App.tsx ├── vite-env.d.ts ├── components │ ├── layout │ │ ├── redirect.tsx │ │ ├── sidebar.tsx │ │ ├── authLayout.tsx │ │ ├── pageContainer.tsx │ │ └── index.tsx │ ├── dashboard │ │ ├── statCard.module.css │ │ └── statCard.tsx │ ├── loader │ │ ├── progressBar.tsx │ │ ├── index.tsx │ │ └── progressBar.css │ └── lazy-image │ │ └── index.tsx ├── hooks │ └── breakpoint.tsx ├── store │ ├── slices │ │ └── adminSlice.tsx │ └── index.tsx ├── main.tsx ├── lib │ ├── http.tsx │ └── utils.tsx └── index.css ├── postcss.config.cjs ├── .github ├── dependabot.yml └── workflows │ └── test-deploy.yml ├── .prettierrc ├── .env.example ├── tsconfig.node.json ├── .eslintignore ├── .prettierignore ├── tailwind.config.mjs ├── .gitignore ├── tsconfig.json ├── .eslintrc.cjs ├── config.ts ├── global.d.ts ├── LICENSE ├── index.html ├── vite.config.ts ├── package.json ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arifszn/reforge/HEAD/public/icon.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arifszn/reforge/HEAD/public/logo.png -------------------------------------------------------------------------------- /src/interfaces/admin.tsx: -------------------------------------------------------------------------------- 1 | export interface Admin { 2 | token: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/img/login-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arifszn/reforge/HEAD/src/assets/img/login-bg.png -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'monthly' 7 | -------------------------------------------------------------------------------- /src/interfaces/review.tsx: -------------------------------------------------------------------------------- 1 | export interface Review { 2 | color: string; 3 | year: string; 4 | title: string; 5 | star: number; 6 | id: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/user.tsx: -------------------------------------------------------------------------------- 1 | export interface User { 2 | avatar: string; 3 | email: string; 4 | first_name: string; 5 | last_name: string; 6 | id: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/routes/web.tsx: -------------------------------------------------------------------------------- 1 | export const webRoutes = { 2 | home: '/', 3 | login: '/login', 4 | logout: '/logout', 5 | dashboard: '/dashboard', 6 | users: '/users', 7 | about: '/about', 8 | }; 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "arrowParens": "always", 4 | "bracketSpacing": true, 5 | "printWidth": 80, 6 | "singleQuote": true, 7 | "tabWidth": 2, 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | VITE_APP_NAME="Reforge" 2 | VITE_ENABLE_PWA=true 3 | VITE_THEME_ACCENT_COLOR='#18181b' 4 | VITE_THEME_SIDEBAR_LAYOUT='top' # mix | top | side 5 | VITE_SHOW_BREADCRUMB=false 6 | VITE_BACKEND_API_URL=https://reqres.in/api 7 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["config.ts", "vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/constants/index.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigProviderProps } from 'antd/es/config-provider'; 2 | import enUSIntl from 'antd/locale/en_US'; 3 | 4 | export const antdConfig: ConfigProviderProps = { 5 | theme: { 6 | token: { 7 | colorPrimary: CONFIG.theme.accentColor, 8 | }, 9 | }, 10 | locale: enUSIntl, 11 | }; 12 | -------------------------------------------------------------------------------- /src/routes/api.tsx: -------------------------------------------------------------------------------- 1 | export const BACKEND_API_URL = 2 | import.meta.env.VITE_BACKEND_API_URL || 'https://reqres.in/api'; 3 | 4 | export const apiRoutes = { 5 | login: `${BACKEND_API_URL}/login`, 6 | logout: `${BACKEND_API_URL}/logout`, 7 | users: `${BACKEND_API_URL}/users`, 8 | reviews: `${BACKEND_API_URL}/unknown`, 9 | }; 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /src/pages/errors/errorPage.tsx: -------------------------------------------------------------------------------- 1 | import { Result } from 'antd'; 2 | 3 | const ErrorPage = () => { 4 | return ( 5 |
6 | 11 |
12 | ); 13 | }; 14 | 15 | export default ErrorPage; 16 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from 'sonner'; 2 | import { RouterProvider } from 'react-router-dom'; 3 | import { browserRouter } from '@/routes/browserRouter'; 4 | 5 | function App() { 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | } 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /src/pages/errors/notfoundPage.tsx: -------------------------------------------------------------------------------- 1 | import { Result } from 'antd'; 2 | 3 | const NotFoundPage = () => { 4 | return ( 5 |
6 | 11 |
12 | ); 13 | }; 14 | 15 | export default NotFoundPage; 16 | -------------------------------------------------------------------------------- /tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | import CONFIG from './config'; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | corePlugins: { 6 | preflight: false, 7 | }, 8 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 9 | theme: { 10 | extend: { 11 | colors: { 12 | rfprimary: CONFIG.theme.accentColor, 13 | }, 14 | }, 15 | }, 16 | plugins: [], 17 | }; 18 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_APP_NAME: string; 5 | readonly VITE_ENABLE_PWA: string; 6 | readonly VITE_THEME_ACCENT_COLOR: string; 7 | readonly VITE_THEME_SIDEBAR_LAYOUT: string; 8 | readonly VITE_SHOW_BREADCRUMB: string; 9 | 10 | // more env variables... 11 | } 12 | 13 | interface ImportMeta { 14 | readonly env: ImportMetaEnv; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/layout/redirect.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { Navigate } from 'react-router-dom'; 3 | import { webRoutes } from '@/routes/web'; 4 | import { RootState } from '@/store'; 5 | 6 | const Redirect = () => { 7 | const admin = useSelector((state: RootState) => state.admin); 8 | 9 | return ( 10 | 11 | ); 12 | }; 13 | 14 | export default Redirect; 15 | -------------------------------------------------------------------------------- /.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 | # dotenv environment variable files 27 | .env 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | .env.local 32 | -------------------------------------------------------------------------------- /src/components/dashboard/statCard.module.css: -------------------------------------------------------------------------------- 1 | .statContent { 2 | width: 100%; 3 | padding-left: 60px; 4 | } 5 | 6 | .iconWrapper { 7 | font-size: 40px; 8 | float: left; 9 | } 10 | 11 | .statTitle { 12 | line-height: 16px; 13 | font-size: 16px; 14 | margin-bottom: 8px; 15 | height: 16px; 16 | white-space: nowrap; 17 | } 18 | 19 | .statNumber { 20 | margin-top: 2px; 21 | line-height: 32px; 22 | font-size: 24px; 23 | height: 32px; 24 | margin-bottom: 0; 25 | white-space: nowrap; 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/breakpoint.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const useBreakpoint = (breakPoint = 768) => { 4 | const [width, setWidth] = useState(window.innerWidth); 5 | 6 | useEffect(() => { 7 | const handleWindowResize = () => setWidth(window.innerWidth); 8 | window.addEventListener('resize', handleWindowResize); 9 | 10 | return () => window.removeEventListener('resize', handleWindowResize); 11 | }, []); 12 | 13 | return width < breakPoint; 14 | }; 15 | 16 | export default useBreakpoint; 17 | -------------------------------------------------------------------------------- /src/components/loader/progressBar.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useEffect } from 'react'; 2 | import NProgress from 'nprogress'; 3 | import '@/components/loader/progressBar.css'; 4 | 5 | export interface ProgressBarProps { 6 | spinner?: boolean; 7 | } 8 | 9 | const ProgressBar = ({ spinner = false }: ProgressBarProps) => { 10 | NProgress.configure({ showSpinner: spinner }); 11 | 12 | useEffect(() => { 13 | NProgress.start(); 14 | 15 | return () => { 16 | NProgress.done(); 17 | }; 18 | }); 19 | 20 | return ; 21 | }; 22 | 23 | export default ProgressBar; 24 | -------------------------------------------------------------------------------- /src/routes/requireAuth.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { Navigate, useLocation } from 'react-router-dom'; 3 | import { RootState } from '@/store'; 4 | import { webRoutes } from '@/routes/web'; 5 | 6 | export type RequireAuthProps = { 7 | children: JSX.Element; 8 | }; 9 | 10 | const RequireAuth = ({ children }: RequireAuthProps) => { 11 | const admin = useSelector((state: RootState) => state.admin); 12 | const location = useLocation(); 13 | 14 | if (!admin) { 15 | return ; 16 | } 17 | 18 | return children; 19 | }; 20 | 21 | export default RequireAuth; 22 | -------------------------------------------------------------------------------- /src/store/slices/adminSlice.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { Admin } from '@/interfaces/admin'; 3 | 4 | export type AdminState = Admin | null; 5 | 6 | const initialState: AdminState = null; 7 | 8 | export const adminSlice = createSlice({ 9 | name: 'admin', 10 | initialState: initialState, 11 | reducers: { 12 | login: (state, action) => { 13 | state = action.payload; 14 | 15 | return state; 16 | }, 17 | logout: (state) => { 18 | state = null; 19 | 20 | return state; 21 | }, 22 | }, 23 | }); 24 | 25 | export const { login, logout } = adminSlice.actions; 26 | 27 | export default adminSlice.reducer; 28 | -------------------------------------------------------------------------------- /src/components/layout/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { webRoutes } from '@/routes/web'; 2 | import { BiHomeAlt2 } from 'react-icons/bi'; 3 | import Icon, { UserOutlined, InfoCircleOutlined } from '@ant-design/icons'; 4 | 5 | export const sidebar = [ 6 | { 7 | path: webRoutes.dashboard, 8 | key: webRoutes.dashboard, 9 | name: 'Dashboard', 10 | icon: , 11 | }, 12 | { 13 | path: webRoutes.users, 14 | key: webRoutes.users, 15 | name: 'Users', 16 | icon: , 17 | }, 18 | { 19 | path: webRoutes.about, 20 | key: webRoutes.about, 21 | name: 'About', 22 | icon: , 23 | }, 24 | ]; 25 | -------------------------------------------------------------------------------- /src/components/lazy-image/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, Fragment, useEffect } from 'react'; 2 | 3 | export interface LazyImageProps { 4 | placeholder: React.ReactNode; 5 | src: string; 6 | [key: string]: string | React.ReactNode | undefined; 7 | } 8 | 9 | const LazyImage = ({ placeholder, src, ...rest }: LazyImageProps) => { 10 | const [loading, setLoading] = useState(true); 11 | 12 | useEffect(() => { 13 | const imageToLoad = new Image(); 14 | imageToLoad.src = src; 15 | 16 | imageToLoad.onload = () => { 17 | setLoading(false); 18 | }; 19 | }, [src]); 20 | 21 | return ( 22 | {loading ? placeholder : } 23 | ); 24 | }; 25 | 26 | export default LazyImage; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": ".", 19 | "paths": { 20 | "@/*": ["./src/*"] 21 | } 22 | }, 23 | "include": ["src", "global.d.ts"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // .eslintrc.cjs 2 | module.exports = { 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | node: true, 7 | }, 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:react/recommended', 11 | 'plugin:prettier/recommended', 12 | 'plugin:@typescript-eslint/recommended', 13 | ], 14 | settings: { 15 | react: { 16 | version: 'detect', 17 | }, 18 | }, 19 | parser: '@typescript-eslint/parser', 20 | parserOptions: { 21 | ecmaFeatures: { 22 | jsx: true, 23 | }, 24 | ecmaVersion: 'latest', 25 | sourceType: 'module', 26 | }, 27 | plugins: ['react'], 28 | rules: { 29 | 'react/react-in-jsx-scope': 'off', 30 | '@typescript-eslint/no-unused-vars': ['error'], 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/loader/index.tsx: -------------------------------------------------------------------------------- 1 | import { Spin } from 'antd'; 2 | import { ImSpinner2 } from 'react-icons/im'; 3 | 4 | const defaultSpinner = ( 5 | } /> 6 | ); 7 | 8 | export interface LoaderProps { 9 | text?: string; 10 | spinner?: React.ReactNode; 11 | } 12 | 13 | const Loader = ({ 14 | text = 'Loading...', 15 | spinner = defaultSpinner, 16 | }: LoaderProps) => { 17 | return ( 18 |
19 |
20 | {spinner} 21 | {text && {text}} 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default Loader; 28 | -------------------------------------------------------------------------------- /config.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv } from 'vite'; 2 | 3 | process.env = { ...process.env, ...loadEnv('all', process.cwd()) }; 4 | 5 | enum LayoutType { 6 | MIX = 'mix', 7 | TOP = 'top', 8 | SIDE = 'side', 9 | } 10 | 11 | const CONFIG = { 12 | appName: process.env.VITE_APP_NAME || 'Reforge', 13 | enablePWA: process.env.VITE_ENABLE_PWA === 'true', 14 | theme: { 15 | accentColor: process.env.VITE_THEME_ACCENT_COLOR || '#18181b', 16 | sidebarLayout: process.env.VITE_THEME_SIDEBAR_LAYOUT || LayoutType.MIX, 17 | showBreadcrumb: process.env.VITE_SHOW_BREADCRUMB === 'true', 18 | }, 19 | metaTags: { 20 | title: 'Reforge', 21 | description: 22 | 'An out-of-box UI solution for enterprise applications as a React boilerplate.', 23 | imageURL: 'logo.svg', 24 | }, 25 | }; 26 | 27 | export default CONFIG; 28 | -------------------------------------------------------------------------------- /.github/workflows/test-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Test Deployment 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | test-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v3 13 | 14 | - name: Set up Node 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 16.x 18 | cache: 'npm' 19 | 20 | - name: Restore cache 21 | uses: actions/cache@v3 22 | with: 23 | path: | 24 | **/node_modules 25 | key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} 26 | 27 | - name: Install dependencies 28 | run: npm ci 29 | 30 | - name: Run lint 31 | run: npm run lint 32 | 33 | - name: Run prettier 34 | run: npm run prettier 35 | 36 | - name: Build 37 | run: npm run build 38 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | enum LayoutType { 2 | MIX = 'mix', 3 | TOP = 'top', 4 | SIDE = 'side', 5 | } 6 | 7 | declare const CONFIG: { 8 | /** 9 | * App name 10 | */ 11 | appName: string; 12 | 13 | /** 14 | * Enable Progressive Web App 15 | */ 16 | enablePWA: boolean; 17 | 18 | /** 19 | * Theme config 20 | */ 21 | theme: { 22 | /** 23 | * Accent color 24 | */ 25 | accentColor: string; 26 | 27 | /** 28 | * Sidebar layout 29 | */ 30 | sidebarLayout: LayoutType; 31 | 32 | /** 33 | * Show breadcrumb 34 | */ 35 | showBreadcrumb: boolean; 36 | }; 37 | 38 | /** 39 | * Meta tags 40 | */ 41 | metaTags: { 42 | /** 43 | * Meta title 44 | */ 45 | title: string; 46 | 47 | /** 48 | * Meta description 49 | */ 50 | description: string; 51 | 52 | /** 53 | * Meta image 54 | */ 55 | imageURL: string; 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /src/store/index.tsx: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from '@reduxjs/toolkit'; 2 | import adminSlice, { AdminState } from '@/store/slices/adminSlice'; 3 | import { 4 | persistReducer, 5 | FLUSH, 6 | REHYDRATE, 7 | PAUSE, 8 | PERSIST, 9 | PURGE, 10 | REGISTER, 11 | } from 'redux-persist'; 12 | import storage from 'redux-persist/lib/storage'; 13 | 14 | const persistConfig = { 15 | key: CONFIG.appName, 16 | storage, 17 | }; 18 | 19 | const rootReducer = combineReducers({ 20 | admin: adminSlice, 21 | }); 22 | 23 | const persistedReducer = persistReducer(persistConfig, rootReducer); 24 | 25 | export const store = configureStore({ 26 | reducer: persistedReducer, 27 | middleware: (getDefaultMiddleware) => 28 | getDefaultMiddleware({ 29 | serializableCheck: { 30 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], 31 | }, 32 | }), 33 | }); 34 | 35 | export type RootState = { 36 | admin: AdminState; 37 | }; 38 | export type AppDispatch = typeof store.dispatch; 39 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { ConfigProvider } from 'antd'; 4 | import { antdConfig } from '@/constants'; 5 | import { Provider } from 'react-redux'; 6 | import { persistStore } from 'redux-persist'; 7 | import { PersistGate } from 'redux-persist/integration/react'; 8 | import Loader from '@/components/loader'; 9 | import { store } from '@/store'; 10 | import { injectStore } from '@/lib/http'; 11 | import App from '@/App'; 12 | import '@/index.css'; 13 | 14 | const persistor = persistStore(store); 15 | injectStore(store); 16 | 17 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 18 | 19 | 20 | 21 | } persistor={persistor}> 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | 29 | window?.addEventListener('vite:preloadError', () => { 30 | window?.location?.reload(); 31 | }); 32 | -------------------------------------------------------------------------------- /src/lib/http.tsx: -------------------------------------------------------------------------------- 1 | import { Store } from '@reduxjs/toolkit'; 2 | import axios from 'axios'; 3 | import { RootState } from '@/store'; 4 | import { logout } from '@/store/slices/adminSlice'; 5 | 6 | let store: Store; 7 | 8 | export const injectStore = (_store: Store) => { 9 | store = _store; 10 | }; 11 | 12 | export const defaultHttp = axios.create(); 13 | const http = axios.create(); 14 | 15 | http.interceptors.request.use( 16 | (config) => { 17 | const state: RootState = store.getState(); 18 | const apiToken = state.admin?.token; 19 | 20 | config.headers['x-api-key'] = 'reqres-free-v1'; 21 | 22 | if (apiToken) { 23 | config.headers.Authorization = `Bearer ${apiToken}`; 24 | } 25 | return config; 26 | }, 27 | (error) => { 28 | return Promise.reject(error); 29 | } 30 | ); 31 | 32 | http.interceptors.response.use( 33 | (response) => { 34 | return response; 35 | }, 36 | (error) => { 37 | if (error?.response?.status === 401) { 38 | store.dispatch(logout()); 39 | } 40 | return Promise.reject(error); 41 | } 42 | ); 43 | 44 | export default http; 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ariful Alam 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 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%- title %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/components/layout/authLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router'; 2 | 3 | const AuthLayout = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
21 |
22 | 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ); 31 | }; 32 | 33 | export default AuthLayout; 34 | -------------------------------------------------------------------------------- /src/lib/utils.tsx: -------------------------------------------------------------------------------- 1 | import { AxiosError } from 'axios'; 2 | import { toast } from 'sonner'; 3 | 4 | export enum NotificationType { 5 | ERROR = 'error', 6 | SUCCESS = 'success', 7 | } 8 | 9 | export const setPageTitle = (title: string) => { 10 | window.document.title = title; 11 | }; 12 | 13 | export const showNotification = ( 14 | message = 'Something went wrong', 15 | type: NotificationType = NotificationType.ERROR, 16 | description?: string 17 | ) => { 18 | toast[type](message, { 19 | description: description, 20 | }); 21 | }; 22 | 23 | export const handleErrorResponse = ( 24 | error: any, // eslint-disable-line @typescript-eslint/no-explicit-any 25 | callback?: () => void, 26 | errorMessage?: string 27 | ) => { 28 | console.error(error); 29 | 30 | if (!errorMessage) { 31 | errorMessage = 'Something went wrong'; 32 | 33 | if (typeof error === 'string') { 34 | try { 35 | error = JSON.parse(error); 36 | } catch (error) { 37 | // do nothing 38 | } 39 | } 40 | 41 | if (error instanceof AxiosError && error?.response?.data?.error) { 42 | errorMessage = error.response.data.error; 43 | } else if (error?.message) { 44 | errorMessage = error.message; 45 | } 46 | } 47 | 48 | showNotification( 49 | errorMessage && 50 | errorMessage.charAt(0).toUpperCase() + errorMessage.slice(1), 51 | NotificationType.ERROR 52 | ); 53 | 54 | if (callback) { 55 | return callback(); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 7 | 8 | font-synthesis: none; 9 | text-rendering: optimizeLegibility; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | -webkit-text-size-adjust: 100%; 13 | } 14 | 15 | body, 16 | p { 17 | margin: 0; 18 | } 19 | 20 | .fade-in { 21 | opacity: 1; 22 | animation-name: fadeIn; 23 | animation-iteration-count: 1; 24 | animation-timing-function: ease-in; 25 | animation-duration: 1s; 26 | } 27 | 28 | .icon-spin { 29 | -webkit-animation: icon-spin 1s infinite linear; 30 | animation: icon-spin 1s infinite linear; 31 | } 32 | 33 | .ant-pro-top-nav-header-logo > *:first-child, 34 | .ant-pro-sider-logo > a, 35 | .ant-pro-global-header-logo > a { 36 | opacity: 60%; 37 | } 38 | 39 | @keyframes fadeIn { 40 | 0% { 41 | opacity: 0; 42 | } 43 | 100% { 44 | opacity: 1; 45 | } 46 | } 47 | 48 | @-webkit-keyframes fadeIn { 49 | from { 50 | opacity: 0; 51 | } 52 | to { 53 | opacity: 1; 54 | } 55 | } 56 | 57 | @-webkit-keyframes icon-spin { 58 | 0% { 59 | -webkit-transform: rotate(0deg); 60 | transform: rotate(0deg); 61 | } 62 | 100% { 63 | -webkit-transform: rotate(359deg); 64 | transform: rotate(359deg); 65 | } 66 | } 67 | 68 | @keyframes icon-spin { 69 | 0% { 70 | -webkit-transform: rotate(0deg); 71 | transform: rotate(0deg); 72 | } 73 | 100% { 74 | -webkit-transform: rotate(359deg); 75 | transform: rotate(359deg); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/layout/pageContainer.tsx: -------------------------------------------------------------------------------- 1 | import { PageContainer, ProCard } from '@ant-design/pro-components'; 2 | import { Breadcrumb, Spin } from 'antd'; 3 | import useBreakpoint from '@/hooks/breakpoint'; 4 | import Loader from '@/components/loader'; 5 | import type { BreadcrumbProps } from 'antd/es/breadcrumb/Breadcrumb'; 6 | 7 | export interface BasePageContainerProps { 8 | title?: string; 9 | subTitle?: string; 10 | breadcrumb?: Partial | React.ReactElement; 11 | extra?: React.ReactNode; 12 | loading?: boolean; 13 | children: React.ReactNode; 14 | transparent?: boolean; 15 | } 16 | 17 | const BasePageContainer = (props: BasePageContainerProps) => { 18 | const isMobile = useBreakpoint(); 19 | 20 | return ( 21 | 30 | } /> 38 | ) : ( 39 | false 40 | ) 41 | } 42 | > 43 | {props.children} 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default BasePageContainer; 50 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import tailwind from 'tailwindcss'; 4 | import autoprefixer from 'autoprefixer'; 5 | import { createHtmlPlugin } from 'vite-plugin-html'; 6 | import tailwindConfig from './tailwind.config.mjs'; 7 | import CONFIG from './config'; 8 | import { VitePWA } from 'vite-plugin-pwa'; 9 | import path from 'path'; 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig({ 13 | plugins: [ 14 | react(), 15 | createHtmlPlugin({ 16 | inject: { 17 | data: { 18 | title: CONFIG.appName, 19 | metaTitle: CONFIG.metaTags.title, 20 | metaDescription: CONFIG.metaTags.description, 21 | metaImageURL: CONFIG.metaTags.imageURL, 22 | }, 23 | }, 24 | }), 25 | ...(CONFIG.enablePWA 26 | ? [ 27 | VitePWA({ 28 | registerType: 'autoUpdate', 29 | includeAssets: ['icon.png'], 30 | manifest: { 31 | name: CONFIG.appName, 32 | short_name: CONFIG.appName, 33 | description: CONFIG.metaTags.description, 34 | theme_color: CONFIG.theme.accentColor, 35 | icons: [ 36 | { 37 | src: 'icon.png', 38 | sizes: '64x64 32x32 24x24 16x16 192x192 512x512', 39 | type: 'image/png', 40 | }, 41 | ], 42 | }, 43 | }), 44 | ] 45 | : []), 46 | ], 47 | css: { 48 | postcss: { 49 | plugins: [tailwind(tailwindConfig), autoprefixer], 50 | }, 51 | }, 52 | define: { 53 | CONFIG: CONFIG, 54 | }, 55 | resolve: { 56 | alias: { 57 | '@': path.resolve(__dirname, './src'), 58 | }, 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /src/components/loader/progressBar.css: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: none; 4 | } 5 | 6 | #nprogress .bar { 7 | background: theme('colors.rfprimary'); 8 | 9 | position: fixed; 10 | z-index: 1031; 11 | top: 0; 12 | left: 0; 13 | 14 | width: 100%; 15 | height: 2px; 16 | } 17 | 18 | /* Fancy blur effect */ 19 | #nprogress .peg { 20 | display: block; 21 | position: absolute; 22 | right: 0px; 23 | width: 100px; 24 | height: 100%; 25 | box-shadow: 0 0 10px theme('colors.rfprimary'), 26 | 0 0 5px theme('colors.rfprimary'); 27 | opacity: 1; 28 | 29 | -webkit-transform: rotate(3deg) translate(0px, -4px); 30 | -ms-transform: rotate(3deg) translate(0px, -4px); 31 | transform: rotate(3deg) translate(0px, -4px); 32 | } 33 | 34 | /* Remove these to get rid of the spinner */ 35 | #nprogress .spinner { 36 | display: block; 37 | position: fixed; 38 | z-index: 1031; 39 | top: 15px; 40 | right: 15px; 41 | } 42 | 43 | #nprogress .spinner-icon { 44 | width: 18px; 45 | height: 18px; 46 | box-sizing: border-box; 47 | 48 | border: solid 2px transparent; 49 | border-top-color: theme('colors.rfprimary'); 50 | border-left-color: theme('colors.rfprimary'); 51 | border-radius: 50%; 52 | 53 | -webkit-animation: nprogress-spinner 400ms linear infinite; 54 | animation: nprogress-spinner 400ms linear infinite; 55 | } 56 | 57 | .nprogress-custom-parent { 58 | overflow: hidden; 59 | position: relative; 60 | } 61 | 62 | .nprogress-custom-parent #nprogress .spinner, 63 | .nprogress-custom-parent #nprogress .bar { 64 | position: absolute; 65 | } 66 | 67 | @-webkit-keyframes nprogress-spinner { 68 | 0% { 69 | -webkit-transform: rotate(0deg); 70 | } 71 | 100% { 72 | -webkit-transform: rotate(360deg); 73 | } 74 | } 75 | @keyframes nprogress-spinner { 76 | 0% { 77 | transform: rotate(0deg); 78 | } 79 | 100% { 80 | transform: rotate(360deg); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/routes/browserRouter.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter } from 'react-router-dom'; 2 | import AuthLayout from '@/components/layout/authLayout'; 3 | import ErrorPage from '@/pages/errors/errorPage'; 4 | import Layout from '@/components/layout'; 5 | import Redirect from '@/components/layout/redirect'; 6 | import NotFoundPage from '@/pages/errors/notfoundPage'; 7 | import { webRoutes } from '@/routes/web'; 8 | import loadable from '@loadable/component'; 9 | import ProgressBar from '@/components/loader/progressBar'; 10 | import RequireAuth from '@/routes/requireAuth'; 11 | import LoginPage from '@/pages/auth/loginPage'; 12 | 13 | const errorElement = ; 14 | const fallbackElement = ; 15 | 16 | const DashboardPage = loadable(() => import('@/pages/dashboardPage'), { 17 | fallback: fallbackElement, 18 | }); 19 | const UserListPage = loadable(() => import('@/pages/users/userListPage'), { 20 | fallback: fallbackElement, 21 | }); 22 | const AboutPage = loadable(() => import('@/pages/aboutPage'), { 23 | fallback: fallbackElement, 24 | }); 25 | 26 | export const browserRouter = createBrowserRouter([ 27 | { 28 | path: webRoutes.home, 29 | element: , 30 | errorElement: errorElement, 31 | }, 32 | 33 | // auth routes 34 | { 35 | element: , 36 | errorElement: errorElement, 37 | children: [ 38 | { 39 | path: webRoutes.login, 40 | element: , 41 | }, 42 | ], 43 | }, 44 | 45 | // protected routes 46 | { 47 | element: ( 48 | 49 | 50 | 51 | ), 52 | errorElement: errorElement, 53 | children: [ 54 | { 55 | path: webRoutes.dashboard, 56 | element: , 57 | }, 58 | { 59 | path: webRoutes.users, 60 | element: , 61 | }, 62 | { 63 | path: webRoutes.about, 64 | element: , 65 | }, 66 | ], 67 | }, 68 | 69 | // 404 70 | { 71 | path: '*', 72 | element: , 73 | errorElement: errorElement, 74 | }, 75 | ]); 76 | -------------------------------------------------------------------------------- /src/components/dashboard/statCard.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Skeleton, Typography } from 'antd'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import CountUp from 'react-countup'; 4 | import styles from '@/components/dashboard/statCard.module.css'; 5 | import React, { Fragment } from 'react'; 6 | 7 | const { Text } = Typography; 8 | 9 | interface StatCardProps { 10 | icon: React.ReactNode; 11 | title: string; 12 | number: number; 13 | loading: boolean; 14 | link?: string; 15 | isCard?: boolean; 16 | } 17 | 18 | const StatCard = ({ 19 | icon, 20 | title, 21 | number, 22 | link, 23 | loading = false, 24 | isCard = true, 25 | }: StatCardProps) => { 26 | const navigate = useNavigate(); 27 | 28 | const children = ( 29 |
30 | 31 | {icon} 32 | 33 |
34 |

35 | 39 | {title || ''} 40 | 41 |

42 |

43 | {loading ? ( 44 | 45 | ) : ( 46 | 53 | )} 54 |

55 |
56 |
57 | ); 58 | 59 | return ( 60 | 61 | {isCard ? ( 62 | { 64 | if (link) { 65 | navigate(link); 66 | } 67 | }} 68 | size="default" 69 | bordered={false} 70 | style={{ padding: '18px 0' }} 71 | > 72 | {children} 73 | 74 | ) : ( 75 | children 76 | )} 77 | 78 | ); 79 | }; 80 | 81 | export default StatCard; 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reforge", 3 | "private": true, 4 | "version": "1.3.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint --ext .js,.jsx,.ts,.tsx .", 11 | "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx --fix .", 12 | "prettier": "prettier --check \"./**/*.{js,jsx,ts,tsx,css,md,json}\"", 13 | "prettier:fix": "prettier --write \"./**/*.{js,jsx,ts,tsx,css,md,json}\"" 14 | }, 15 | "dependencies": { 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0" 18 | }, 19 | "devDependencies": { 20 | "@ant-design/pro-components": "^2.6.4", 21 | "@loadable/component": "^5.15.3", 22 | "@reduxjs/toolkit": "^1.9.5", 23 | "@types/loadable__component": "^5.13.4", 24 | "@types/node": "^20.14.8", 25 | "@types/nprogress": "^0.2.0", 26 | "@types/react": "^18.2.14", 27 | "@types/react-dom": "^18.2.6", 28 | "@typescript-eslint/eslint-plugin": "^5.61.0", 29 | "@typescript-eslint/parser": "^5.62.0", 30 | "@vitejs/plugin-react": "^4.0.1", 31 | "antd": "^5.6.4", 32 | "autoprefixer": "^10.4.13", 33 | "axios": "^1.4.0", 34 | "eslint": "^8.44.0", 35 | "eslint-config-prettier": "^9.1.0", 36 | "eslint-plugin-prettier": "^4.2.1", 37 | "eslint-plugin-react": "^7.32.2", 38 | "nprogress": "^0.2.0", 39 | "postcss": "^8.4.25", 40 | "prettier": "^2.8.8", 41 | "react-countup": "^6.4.2", 42 | "react-icons": "^4.10.1", 43 | "react-redux": "^8.1.1", 44 | "react-router-dom": "^6.16.0", 45 | "redux-persist": "^6.0.0", 46 | "sonner": "^1.5.0", 47 | "tailwindcss": "^3.3.2", 48 | "typescript": "^5.5.4", 49 | "vite": "^4.4.2", 50 | "vite-plugin-html": "^3.2.0", 51 | "vite-plugin-pwa": "^0.16.4" 52 | }, 53 | "keywords": [ 54 | "react", 55 | "reactjs", 56 | "admin", 57 | "dashboard", 58 | "ant admin", 59 | "antd admin", 60 | "tailwind admin", 61 | "tailwind template", 62 | "tailwind admin template", 63 | "ant design admin template", 64 | "antd admin template", 65 | "antd starter kit", 66 | "ant design starter kit", 67 | "ant design admin" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 4 | 5 | If you have found an issue or would like to request a new feature, simply create a new issue detailing the request. We also welcome pull requests. See below for information on getting started with development and submitting pull requests. 6 | 7 | Please note we have a [code of conduct](https://github.com/arifszn/reforge/blob/main/CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 8 | 9 | ## Found an Issue? 10 | 11 | If you find a bug in the source code or a mistake in the documentation, you can help us by 12 | submitting an issue to our [GitHub Repository](https://github.com/arifszn/reforge/issues/new). Even better you can submit a Pull Request with a fix. 13 | 14 | ## Submitting a Pull Request 15 | 16 | 1. Make sure that the contribution you want to make is explained or detailed in a GitHub issue! Find an [existing issue](https://github.com/arifszn/reforge/issues) or [open a new one](https://github.com/arifszn/reforge/issues/new). 17 | 2. Once done, [fork the repository](https://github.com/arifszn/reforge/fork) in your own GitHub account. 18 | 3. [Create a new Git branch](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-and-deleting-branches-within-your-repository). 19 | 4. Make the changes on your branch. 20 | 5. [Submit the branch as a PR](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) pointing to the `main` branch of the main repository.
21 | We do not enforce a naming convention for the PRs, but **please use something descriptive of your changes**. 22 | 23 | ## Development Workflow 24 | 25 | ### Install dependencies 26 | 27 | ```sh 28 | npm install 29 | ``` 30 | 31 | ### Run dev server 32 | 33 | ```sh 34 | npm run dev 35 | ``` 36 | 37 | ### Linter 38 | 39 | Each PR should pass the linter to be accepted. To fix lint and prettier errors, run `npm run lint:fix` and `npm run prettier:fix`. 40 | 41 | ### Commit Message 42 | 43 | As minimal requirements, your commit message should: 44 | 45 | - be capitalized 46 | - not finish by a dot or any other punctuation character (!,?) 47 | - start with a verb so that we can read your commit message this way: "This commit will ...", where "..." is the commit message. 48 | e.g.: "Fix the home page button" or "Add support for dark mode" 49 | -------------------------------------------------------------------------------- /src/components/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet, useLocation, useNavigate } from 'react-router-dom'; 2 | import { webRoutes } from '@/routes/web'; 3 | import { Dropdown } from 'antd'; 4 | import { ProLayout, ProLayoutProps } from '@ant-design/pro-components'; 5 | import Icon, { LogoutOutlined } from '@ant-design/icons'; 6 | import { useDispatch } from 'react-redux'; 7 | import { logout } from '@/store/slices/adminSlice'; 8 | import { memo } from 'react'; 9 | import { sidebar } from '@/components/layout/sidebar'; 10 | import { apiRoutes } from '@/routes/api'; 11 | import http from '@/lib/http'; 12 | import { handleErrorResponse } from '@/lib/utils'; 13 | import { RiShieldUserFill } from 'react-icons/ri'; 14 | 15 | const Layout = () => { 16 | const location = useLocation(); 17 | const navigate = useNavigate(); 18 | const dispatch = useDispatch(); 19 | 20 | const defaultProps: ProLayoutProps = { 21 | title: CONFIG.appName, 22 | pageTitleRender(props, defaultPageTitle) { 23 | return `${defaultPageTitle} - ${CONFIG.appName}`; 24 | }, 25 | logo: '/icon.png', 26 | fixedHeader: true, 27 | fixSiderbar: true, 28 | layout: CONFIG.theme.sidebarLayout, 29 | route: { 30 | routes: sidebar, 31 | }, 32 | }; 33 | 34 | const logoutAdmin = () => { 35 | dispatch(logout()); 36 | navigate(webRoutes.login, { 37 | replace: true, 38 | }); 39 | 40 | http.post(apiRoutes.logout).catch((error) => { 41 | handleErrorResponse(error); 42 | }); 43 | }; 44 | 45 | return ( 46 |
47 | navigate(webRoutes.dashboard)} 56 | menuItemRender={(item, dom) => ( 57 | { 59 | e.preventDefault(); 60 | item.path && navigate(item.path); 61 | }} 62 | href={item.path} 63 | > 64 | {dom} 65 | 66 | )} 67 | avatarProps={{ 68 | icon: , 69 | className: 70 | 'bg-rfprimary bg-opacity-20 text-rfprimary text-opacity-90', 71 | size: 'small', 72 | shape: 'square', 73 | title: 'Admin', 74 | render: (_, dom) => { 75 | return ( 76 | , 82 | label: 'Logout', 83 | onClick: () => { 84 | logoutAdmin(); 85 | }, 86 | }, 87 | ], 88 | }} 89 | > 90 | {dom} 91 | 92 | ); 93 | }, 94 | }} 95 | > 96 | 97 | 98 |
99 | ); 100 | }; 101 | 102 | export default memo(Layout); 103 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at fdkhadra@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | 5 |

6 | 7 |

8 |

An out-of-box UI solution for enterprise applications as a React boilerplate.

9 | 10 |

11 | Demo 12 | · 13 | Report Bug 14 | · 15 | Request Feature 16 |

17 |

18 | 19 |

20 | 21 | Preview 22 | 23 |
24 | Shadow 25 |

26 | 27 | ## Features 28 | 29 | - Elegant and customizable UI using `Tailwindcss` and `Ant Design`. 30 | - Single page application using `React Router`. 31 | - Mock API request using `reqres`. 32 | - Powerful layout and table using `@ant-design/pro-components`. 33 | - Code splitting and lazy loading component using `@loadable/component`. 34 | - State management using `react-redux` and `@reduxjs/toolkit`. 35 | - Persistent redux state using `redux-persist`. 36 | - Loading progress bar using `nprogress`. 37 | - `ESLint` and `Prettier` enabled. 38 | - Option to enable Progressive Web App (PWA). (Only available in production build) 39 | - Axios interceptor enabled to handle API authorization. 40 | - Automated workflow for checking new Pull Request. 41 | 42 | ## Demo 43 | 44 | https://reforge.netlify.app 45 | 46 | ### Credentials 47 | 48 | - **Email:** `eve.holt@reqres.in` 49 | - **Password:** `password` 50 | 51 | ## Usage 52 | 53 | - Clone the project and change directory. 54 | 55 | ```shell 56 | git clone https://github.com/arifszn/reforge.git 57 | cd reforge 58 | ``` 59 | 60 | - Install dependencies. 61 | 62 | ```shell 63 | npm install 64 | ``` 65 | 66 | - Run dev server. 67 | 68 | ```shell 69 | npm run dev 70 | ``` 71 | 72 | - Finally, visit [`http://localhost:5173`](http://localhost:5173) from your browser. Credentials can be found above. 73 | 74 | ## Config 75 | 76 | Settings including app name, theme color, meta tags, etc. can be controlled from one single file **`config.ts`** located at the project's root. 77 | 78 | ```ts 79 | //config.ts 80 | const CONFIG = { 81 | appName: 'Reforge', 82 | enablePWA: true, 83 | theme: { 84 | accentColor: '#818cf8', 85 | sidebarLayout: 'mix', 86 | showBreadcrumb: true, 87 | }, 88 | metaTags: { 89 | title: 'Reforge', 90 | description: 91 | 'An out-of-box UI solution for enterprise applications as a React boilerplate.', 92 | imageURL: 'logo.svg', 93 | }, 94 | }; 95 | 96 | export default CONFIG; 97 | ``` 98 | 99 | ## Support 100 | 101 |

You can show your support by starring this project. ★

102 | 103 | Github Star 104 | 105 | 106 | ## Contribute 107 | 108 | To contribute, see the [Contributing guide](https://github.com/arifszn/reforge/blob/main/CONTRIBUTING.md). 109 | 110 | ## License 111 | 112 | [MIT](https://github.com/arifszn/reforge/blob/main/LICENSE) 113 | -------------------------------------------------------------------------------- /src/pages/auth/loginPage.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Form, Input } from 'antd'; 2 | import { Fragment, useEffect, useState } from 'react'; 3 | import { apiRoutes } from '@/routes/api'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { login } from '@/store/slices/adminSlice'; 6 | import { RootState } from '@/store'; 7 | import { useLocation, useNavigate } from 'react-router-dom'; 8 | import { webRoutes } from '@/routes/web'; 9 | import { handleErrorResponse, setPageTitle } from '@/lib/utils'; 10 | import { Admin } from '@/interfaces/admin'; 11 | import { defaultHttp } from '@/lib/http'; 12 | 13 | interface FormValues { 14 | email: string; 15 | password: string; 16 | } 17 | 18 | const LoginPage = () => { 19 | const dispatch = useDispatch(); 20 | const navigate = useNavigate(); 21 | const location = useLocation(); 22 | const from = location.state?.from?.pathname || webRoutes.dashboard; 23 | const admin = useSelector((state: RootState) => state.admin); 24 | const [loading, setLoading] = useState(false); 25 | const [form] = Form.useForm(); 26 | 27 | useEffect(() => { 28 | setPageTitle(`Admin Login - ${CONFIG.appName}`); 29 | }, []); 30 | 31 | useEffect(() => { 32 | if (admin) { 33 | navigate(from, { replace: true }); 34 | } 35 | }, [admin]); 36 | 37 | const onSubmit = (values: FormValues) => { 38 | setLoading(true); 39 | 40 | defaultHttp 41 | .post( 42 | apiRoutes.login, 43 | { 44 | email: values.email, 45 | password: values.password, 46 | }, 47 | { 48 | headers: { 49 | 'x-api-key': 'reqres-free-v1', 50 | }, 51 | } 52 | ) 53 | .then((response) => { 54 | const admin: Admin = { 55 | token: response.data.token, 56 | }; 57 | dispatch(login(admin)); 58 | }) 59 | .catch((error) => { 60 | handleErrorResponse(error); 61 | setLoading(false); 62 | }); 63 | }; 64 | 65 | return ( 66 | 67 |
68 |

69 | Admin Login 70 |

71 |

72 | Enter your email below to login to your account 73 |

74 |
75 |
91 |
92 | Email

96 | } 97 | rules={[ 98 | { 99 | required: true, 100 | message: 'Please enter your email', 101 | }, 102 | { 103 | type: 'email', 104 | message: 'Invalid email address', 105 | }, 106 | ]} 107 | > 108 | 112 |
113 |
114 |
115 | 119 | Password 120 |

121 | } 122 | rules={[ 123 | { 124 | required: true, 125 | message: 'Please enter your password', 126 | }, 127 | ]} 128 | > 129 | 134 |
135 |
136 | 137 |
138 | 148 |
149 |
150 |
151 | ); 152 | }; 153 | 154 | export default LoginPage; 155 | -------------------------------------------------------------------------------- /src/pages/users/userListPage.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActionType, 3 | ProTable, 4 | ProColumns, 5 | RequestData, 6 | TableDropdown, 7 | ProDescriptions, 8 | } from '@ant-design/pro-components'; 9 | import { Avatar, BreadcrumbProps, Modal, Space } from 'antd'; 10 | import { useRef } from 'react'; 11 | import { FiUsers } from 'react-icons/fi'; 12 | import { CiCircleMore } from 'react-icons/ci'; 13 | import { Link } from 'react-router-dom'; 14 | import { User } from '@/interfaces/user'; 15 | import { apiRoutes } from '@/routes/api'; 16 | import { webRoutes } from '@/routes/web'; 17 | import { 18 | handleErrorResponse, 19 | NotificationType, 20 | showNotification, 21 | } from '@/lib/utils'; 22 | import http from '@/lib/http'; 23 | import BasePageContainer from '@/components/layout/pageContainer'; 24 | import LazyImage from '@/components/lazy-image'; 25 | import Icon, { 26 | ExclamationCircleOutlined, 27 | DeleteOutlined, 28 | } from '@ant-design/icons'; 29 | 30 | enum ActionKey { 31 | DELETE = 'delete', 32 | } 33 | 34 | const breadcrumb: BreadcrumbProps = { 35 | items: [ 36 | { 37 | key: webRoutes.dashboard, 38 | title: Dashboard, 39 | }, 40 | { 41 | key: webRoutes.users, 42 | title: Users, 43 | }, 44 | ], 45 | }; 46 | 47 | const UserListPage = () => { 48 | const actionRef = useRef(); 49 | const [modal, modalContextHolder] = Modal.useModal(); 50 | 51 | const columns: ProColumns[] = [ 52 | { 53 | title: 'Avatar', 54 | dataIndex: 'avatar', 55 | align: 'center', 56 | sorter: false, 57 | render: (_, row: User) => 58 | row.avatar ? ( 59 | } 66 | /> 67 | } 68 | /> 69 | ) : ( 70 | 71 | {row.first_name.charAt(0).toUpperCase()} 72 | 73 | ), 74 | }, 75 | { 76 | title: 'Name', 77 | dataIndex: 'name', 78 | sorter: false, 79 | align: 'center', 80 | ellipsis: true, 81 | render: (_, row: User) => `${row.first_name} ${row.last_name}`, 82 | }, 83 | { 84 | title: 'Email', 85 | dataIndex: 'email', 86 | sorter: false, 87 | align: 'center', 88 | ellipsis: true, 89 | }, 90 | { 91 | title: 'Action', 92 | align: 'center', 93 | key: 'option', 94 | fixed: 'right', 95 | render: (_, row: User) => [ 96 | handleActionOnSelect(key, row)} 99 | menus={[ 100 | { 101 | key: ActionKey.DELETE, 102 | name: ( 103 | 104 | 105 | Delete 106 | 107 | ), 108 | }, 109 | ]} 110 | > 111 | 112 | , 113 | ], 114 | }, 115 | ]; 116 | 117 | const handleActionOnSelect = (key: string, user: User) => { 118 | if (key === ActionKey.DELETE) { 119 | showDeleteConfirmation(user); 120 | } 121 | }; 122 | 123 | const showDeleteConfirmation = (user: User) => { 124 | modal.confirm({ 125 | title: 'Are you sure to delete this user?', 126 | icon: , 127 | content: ( 128 | 129 | 130 | {user.avatar} 131 | 132 | 133 | {user.first_name} {user.last_name} 134 | 135 | 136 | {user.email} 137 | 138 | 139 | ), 140 | onOk: () => { 141 | return http 142 | .delete(`${apiRoutes.users}/${user.id}`) 143 | .then(() => { 144 | showNotification( 145 | 'Success', 146 | NotificationType.SUCCESS, 147 | 'User is deleted.' 148 | ); 149 | 150 | actionRef.current?.reloadAndRest?.(); 151 | }) 152 | .catch((error) => { 153 | handleErrorResponse(error); 154 | }); 155 | }, 156 | }); 157 | }; 158 | 159 | return ( 160 | 161 | , 171 | }} 172 | bordered={true} 173 | showSorterTooltip={false} 174 | scroll={{ x: true }} 175 | tableLayout={'fixed'} 176 | rowSelection={false} 177 | pagination={{ 178 | showQuickJumper: true, 179 | pageSize: 10, 180 | }} 181 | actionRef={actionRef} 182 | request={(params) => { 183 | return http 184 | .get(apiRoutes.users, { 185 | params: { 186 | page: params.current, 187 | per_page: params.pageSize, 188 | }, 189 | }) 190 | .then((response) => { 191 | const users: [User] = response.data.data; 192 | 193 | return { 194 | data: users, 195 | success: true, 196 | total: response.data.total, 197 | } as RequestData; 198 | }) 199 | .catch((error) => { 200 | handleErrorResponse(error); 201 | 202 | return { 203 | data: [], 204 | success: false, 205 | } as RequestData; 206 | }); 207 | }} 208 | dateFormatter="string" 209 | search={false} 210 | rowKey="id" 211 | options={{ 212 | search: false, 213 | }} 214 | /> 215 | {modalContextHolder} 216 | 217 | ); 218 | }; 219 | 220 | export default UserListPage; 221 | -------------------------------------------------------------------------------- /src/pages/aboutPage.tsx: -------------------------------------------------------------------------------- 1 | import { BreadcrumbProps } from 'antd'; 2 | import BasePageContainer from '@/components/layout/pageContainer'; 3 | import { webRoutes } from '@/routes/web'; 4 | import { Link } from 'react-router-dom'; 5 | import { AiFillGithub, AiOutlineBug, AiOutlineHeart } from 'react-icons/ai'; 6 | import { FaRegLightbulb } from 'react-icons/fa'; 7 | import packageJson from '../../package.json'; 8 | 9 | const breadcrumb: BreadcrumbProps = { 10 | items: [ 11 | { 12 | key: webRoutes.dashboard, 13 | title: Dashboard, 14 | }, 15 | { 16 | key: webRoutes.about, 17 | title: About, 18 | }, 19 | ], 20 | }; 21 | 22 | const AboutPage = () => { 23 | const packageVersion = packageJson.version; 24 | 25 | return ( 26 | 27 |
28 |
29 |
30 |

31 | v{packageVersion} 32 |

33 |

34 | Reforge 35 |

36 |
37 |
38 |

39 | An out-of-box UI solution for enterprise applications as a React 40 | boilerplate.{' '} 41 |

42 |
43 |
44 |
45 |
46 | 47 |

48 | 54 | 55 | GitHub 56 | 57 |

58 |

59 | Source code of the website. 60 |

61 |
62 |
63 |
64 |
65 |
66 | 67 |

68 | 74 | 75 | Report Bug 76 | 77 |

78 |

79 | Something not working? Report a bug. 80 |

81 |
82 |
83 |
84 |
85 |
86 | 87 |

88 | 94 | 95 | Request Feature 96 | 97 |

98 |

99 | Need something? Request a new feature. 100 |

101 |
102 |
103 |
104 |
105 |
106 | 107 |

108 | 114 | 115 | Contribute 116 | 117 |

118 |

119 | Contribute to this project. 120 |

121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | ); 129 | }; 130 | 131 | export default AboutPage; 132 | -------------------------------------------------------------------------------- /src/pages/dashboardPage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import BasePageContainer from '@/components/layout/pageContainer'; 3 | import { 4 | Avatar, 5 | BreadcrumbProps, 6 | Card, 7 | Col, 8 | List, 9 | Progress, 10 | Rate, 11 | Row, 12 | Table, 13 | Tag, 14 | } from 'antd'; 15 | import { webRoutes } from '@/routes/web'; 16 | import { Link } from 'react-router-dom'; 17 | import StatCard from '@/components/dashboard/statCard'; 18 | import { AiOutlineStar, AiOutlineTeam } from 'react-icons/ai'; 19 | import Icon from '@ant-design/icons'; 20 | import { BiCommentDetail, BiPhotoAlbum } from 'react-icons/bi'; 21 | import { MdOutlineArticle, MdOutlinePhoto } from 'react-icons/md'; 22 | import { StatisticCard } from '@ant-design/pro-components'; 23 | import LazyImage from '@/components/lazy-image'; 24 | import { User } from '@/interfaces/user'; 25 | import http from '@/lib/http'; 26 | import { apiRoutes } from '@/routes/api'; 27 | import { handleErrorResponse } from '@/lib/utils'; 28 | import { Review } from '@/interfaces/review'; 29 | 30 | const breadcrumb: BreadcrumbProps = { 31 | items: [ 32 | { 33 | key: webRoutes.dashboard, 34 | title: Dashboard, 35 | }, 36 | ], 37 | }; 38 | 39 | const DashboardPage = () => { 40 | const [loading, setLoading] = useState(true); 41 | const [users, setUsers] = useState([]); 42 | const [reviews, setReviews] = useState([]); 43 | 44 | useEffect(() => { 45 | Promise.all([loadUsers(), loadReviews()]) 46 | .then(() => { 47 | setLoading(false); 48 | }) 49 | .catch((error) => { 50 | handleErrorResponse(error); 51 | }); 52 | }, []); 53 | 54 | const loadUsers = () => { 55 | return http 56 | .get(apiRoutes.users, { 57 | params: { 58 | per_page: 4, 59 | }, 60 | }) 61 | .then((response) => { 62 | setUsers(response.data.data); 63 | }) 64 | .catch((error) => { 65 | handleErrorResponse(error); 66 | }); 67 | }; 68 | 69 | const loadReviews = () => { 70 | return http 71 | .get(apiRoutes.reviews, { 72 | params: { 73 | per_page: 5, 74 | }, 75 | }) 76 | .then((response) => { 77 | setReviews( 78 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 79 | response.data.data.map((rawReview: any) => { 80 | const review: Review = { 81 | id: rawReview.id, 82 | title: rawReview.name, 83 | color: rawReview.color, 84 | year: rawReview.year, 85 | star: Math.floor(Math.random() * 5) + 1, 86 | }; 87 | 88 | return review; 89 | }) 90 | ); 91 | }) 92 | .catch((error) => { 93 | handleErrorResponse(error); 94 | }); 95 | }; 96 | 97 | return ( 98 | 99 | 100 | 101 | } 104 | title="Users" 105 | number={12} 106 | /> 107 | 108 | 109 | } 112 | title="Posts" 113 | number={100} 114 | /> 115 | 116 | 117 | } 120 | title="Albums" 121 | number={100} 122 | /> 123 | 124 | 125 | } 128 | title="Photos" 129 | number={500} 130 | /> 131 | 132 | 133 | } 136 | title="Comments" 137 | number={500} 138 | /> 139 | 140 | 141 | } 144 | title="Reviews" 145 | number={100} 146 | /> 147 | 148 | 156 | 157 | 158 | 164 | 177 | } 178 | chartPlacement="left" 179 | /> 180 | 181 | 182 | 183 | 191 | 192 | ( 197 | 198 | 208 | } 209 | /> 210 | } 211 | /> 212 | } 213 | title={`${user.first_name} ${user.last_name}`} 214 | description={user.email} 215 | /> 216 | 217 | )} 218 | /> 219 | 220 | 221 | 229 | 230 | ( 248 | {row.year} 249 | ), 250 | }, 251 | { 252 | title: 'Star', 253 | dataIndex: 'star', 254 | key: 'star', 255 | align: 'center', 256 | render: (_, row: Review) => ( 257 | 258 | ), 259 | }, 260 | ]} 261 | /> 262 | 263 | 264 | 265 | 266 | ); 267 | }; 268 | 269 | export default DashboardPage; 270 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | --------------------------------------------------------------------------------