Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ dist-ssr

.env*
!.env.example

build/
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20.11.1
74 changes: 38 additions & 36 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,56 +5,58 @@
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint './src/**/*.{js,ts,tsx}' --fix",
"lint": "eslint './src/**/*.{ts,tsx}' --fix",
"prettify": "prettier -c --write ./src/**/* ",
"install:clean": "rm -rf node_modules/ && yarn",
"prepare": "husky install",
"pre-commit": "lint-staged"
},
"dependencies": {
"@reduxjs/toolkit": "^1.9.1",
"axios": "^1.2.1",
"@reduxjs/toolkit": "^2.2.1",
"axios": "^1.6.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"redux": "^4.2.0",
"redux-saga": "^1.2.2",
"reselect": "^4.1.7"
"react-redux": "^9.1.0",
"react-router-dom": "^6.22.3",
"react-toastify": "^10.0.5",
"redux": "^5.0.1",
"redux-saga": "^1.3.0",
"reselect": "^5.1.0"
},
"devDependencies": {
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"@types/redux-logger": "^3.0.9",
"@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.47.0",
"@vitejs/plugin-react": "^3.0.0",
"autoprefixer": "^10.4.13",
"eslint": "^8.30.0",
"@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22",
"@types/redux-logger": "^3.0.13",
"@typescript-eslint/eslint-plugin": "^7.3.0",
"@typescript-eslint/parser": "^7.3.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.18",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.11",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-redux": "^4.0.0",
"eslint-plugin-simple-import-sort": "^8.0.0",
"husky": "^8.0.2",
"immer": "^9.0.16",
"lint-staged": "^13.1.0",
"postcss": "^8.4.20",
"prettier": "^2.8.1",
"sass": "^1.57.1",
"tailwindcss": "^3.2.4",
"typescript": "^4.9.4",
"vite": "^4.0.3",
"vite-tsconfig-paths": "^4.0.3"
"eslint-plugin-react-redux": "^4.1.0",
"eslint-plugin-simple-import-sort": "^12.0.0",
"husky": "^9.0.11",
"immer": "^10.0.4",
"lint-staged": "^15.2.2",
"postcss": "^8.4.36",
"prettier": "^3.2.5",
"sass": "^1.72.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2",
"vite": "^5.1.6",
"vite-tsconfig-paths": "^4.3.2"
},
"engines": {
"npm": "8.19.2",
"node": "18.12.1"
"npm": "please-use-yarn",
"node": ">=20"
},
"husky": {
"hooks": {
Expand Down
42 changes: 23 additions & 19 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.min.css';
// HOC
import UnauthenticatedRouteHOC from 'HOC/UnauthenticatedRoute';
import AuthenticatedRouteHOC from 'HOC/AuthenticatedRoute';
// store
import store from 'store';
// components
import Dashboard from 'views/Dashboard';

const App: React.FC = () => (
<Provider store={store}>
<div className='flex flex-col h-full'>
<div className='flex flex-col items-center justify-center h-4/5'>
<span className='text-4xl'>Basic Setup</span>
<span className='text-4xl'>Vite + React + Tailwind + Redux</span>
</div>
<div className='flex flex-col items-center justify-center'>
<span className='text-4xl'>
Made with <span className='text-red-500'> &#10084;</span> by{' '}
<a
className='text-blue-400 underline'
href='https://madhav.dev'
target='_blank'
rel='noopener noreferrer'
>
Madhav
</a>
</span>
</div>
</div>
<ToastContainer
theme='dark'
limit={5}
closeButton={false}
pauseOnFocusLoss={false}
toastClassName='relative flex p-4 rounded-10 tracking-wider justify-between overflow-hidden cursor-pointer font-bold text-sm'
/>
<BrowserRouter>
<Routes>
<Route path='/login' Component={UnauthenticatedRouteHOC(Dashboard)} />
<Route path='/' Component={AuthenticatedRouteHOC(Dashboard)} />
<Route path='*' element={<Navigate to='/' />} />
</Routes>
</BrowserRouter>
</Provider>
);

Expand Down
40 changes: 40 additions & 0 deletions src/HOC/AuthenticatedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Navigate } from 'react-router-dom';
// services
import { localStorageService } from 'services/LocalStorageService';
// actions
import { authFetchMeAction } from 'store/actions/auth.action';
// selectors
import {
isAuthLoadingSelector,
isAuthenticatedSelector,
} from 'store/selectors/auth.selector';

const AuthenticatedRouteHOC = <P extends object>(
Component: React.ComponentType<P>
): React.FC<P> => {
const AuthenticatedRoute: React.FC<P> = ({ ...props }) => {
const dispatch = useDispatch();

const isAuthenticated = useSelector(isAuthenticatedSelector);
const isLoading = useSelector(isAuthLoadingSelector);

useEffect(() => {
const token = localStorageService.getAuthToken();
if (token && !isAuthenticated && !isLoading) {
dispatch(authFetchMeAction());
}
}, [dispatch, isAuthenticated, isLoading]);

return isAuthenticated ? (
<Component {...(props as P)} />
) : (
<Navigate to='/login' />
);
};

return AuthenticatedRoute;
};

export default AuthenticatedRouteHOC;
40 changes: 40 additions & 0 deletions src/HOC/UnauthenticatedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Navigate } from 'react-router-dom';
// services
import { localStorageService } from 'services/LocalStorageService';
// actions
import { authFetchMeAction } from 'store/actions/auth.action';
// selectors
import {
isAuthLoadingSelector,
isAuthenticatedSelector,
} from 'store/selectors/auth.selector';

const UnauthenticatedRouteHOC = <P extends {}>(
Component: React.ComponentType<P>
): React.FC<P> => {
const UnauthenticatedRoute: React.FC<P> = ({ ...props }) => {
const dispatch = useDispatch();

const isAuthenticated = useSelector(isAuthenticatedSelector);
const isLoading = useSelector(isAuthLoadingSelector);

useEffect(() => {
const token = localStorageService.getAuthToken();
if (token && !isAuthenticated && !isLoading) {
dispatch(authFetchMeAction());
}
}, [dispatch, isAuthenticated, isLoading]);

return !isAuthenticated ? (
<Component {...(props as P)} />
) : (
<Navigate to='/' />
);
};

return UnauthenticatedRoute;
};

export default UnauthenticatedRouteHOC;
54 changes: 54 additions & 0 deletions src/services/ToastService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { toast, ToastContent, ToastOptions } from 'react-toastify';

class ToastService {
private static _instance: ToastService;

static getInstance(): ToastService {
if (!this._instance) {
this._instance = new ToastService();
}
return this._instance;
}

showError(content: ToastContent, config?: ToastOptions) {
toast(content, {
...config,
type: 'error',
position: config?.position || 'bottom-right',
autoClose: config?.delay || 4000,
});
}

showInfo(content: ToastContent, config?: ToastOptions) {
toast(content, {
...config,
type: 'info',
position: config?.position || 'bottom-right',
autoClose: config?.autoClose || 4000,
});
}

showSuccess(content: ToastContent, config?: ToastOptions) {
toast(content, {
...config,
type: 'success',
position: config?.position || 'bottom-right',
autoClose: config?.autoClose || 4000,
});
}

showWarning(content: ToastContent, config?: ToastOptions) {
toast(content, {
...config,
type: 'warning',
position: config?.position || 'bottom-right',
autoClose: config?.autoClose || 4000,
});
}

dismiss(toastRef: any) {
toast.dismiss(toastRef);
}
}

export const toastService = ToastService.getInstance();
2 changes: 1 addition & 1 deletion src/store/reducers/auth.reducer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import produce from 'immer';
import { produce } from 'immer';
import { Reducer } from 'redux';
import { AuthActionType } from 'store/actions/actions.constants';

Expand Down
2 changes: 1 addition & 1 deletion src/store/reducers/user.reducer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import produce from 'immer';
import { produce } from 'immer';
import { Reducer } from 'redux';
import { AuthActionType } from 'store/actions/actions.constants';
import { addOne } from 'store/base/base.reducer';
Expand Down
15 changes: 15 additions & 0 deletions src/store/selectors/auth.selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createSelector } from 'reselect';
import { AppState } from 'store/reducers';
import { AuthState } from 'store/reducers/auth.reducer';

const authState = (state: AppState) => state.auth;

export const isAuthenticatedSelector = createSelector(
[authState],
(state: AuthState) => Boolean(state.userID)
);

export const isAuthLoadingSelector = createSelector(
[authState],
(state: AuthState) => Boolean(state.loading)
);
27 changes: 27 additions & 0 deletions src/views/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { FC } from 'react';

interface DashboardProps {}

const Dashboard: FC<DashboardProps> = () => (
<div className='flex flex-col h-full'>
<div className='flex flex-col items-center justify-center h-4/5'>
<span className='text-4xl'>Basic Setup</span>
<span className='text-4xl'>Vite + React + Tailwind + Redux</span>
</div>
<div className='flex flex-col items-center justify-center'>
<span className='text-4xl'>
Made with <span className='text-red-500'> &#10084;</span> by{' '}
<a
className='text-blue-400 underline'
href='https://madhav.dev'
target='_blank'
rel='noopener noreferrer'
>
Madhav
</a>
</span>
</div>
</div>
);

export default Dashboard;
2 changes: 1 addition & 1 deletion tailwind.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module.exports = {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
content: ['./index.html', './src/**/*.{ts,tsx}'],
};
9 changes: 5 additions & 4 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import tsconfigPaths from 'vite-tsconfig-paths';

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), tsconfigPaths()],
server: {
open: true,
},
plugins: [react(), tsconfigPaths()],
server: {
open: true,
},
build: { outDir: 'build' },
});
Loading