├── 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 |
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 |
22 |
23 |
24 |
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 |
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 |
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 |
37 |
38 |
39 | An out-of-box UI solution for enterprise applications as a React
40 | boilerplate.{' '}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
58 |
59 | Source code of the website.
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
78 |
79 | Something not working? Report a bug.
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
98 |
99 | Need something? Request a new feature.
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
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 |
--------------------------------------------------------------------------------