Merge pull request #32 from ricardo-campos-org/feat/31-define-functions-and-screens
feat: define functions and screens
@@ -22,3 +22,5 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.env
|
||||
|
||||
@@ -3,19 +3,30 @@
|
||||
"version": "1.0.0",
|
||||
"license": "GPL-3.0-only",
|
||||
"description": "TaskNote App - Your daily-basis friend to help you with TODOs and Notes",
|
||||
"keywords": ["react", "typescript", "node", "nestjs"],
|
||||
"keywords": [
|
||||
"react",
|
||||
"typescript",
|
||||
"node",
|
||||
"nestjs"
|
||||
],
|
||||
"repository": "https://github.com/ricardo-campos-org/react-typescript-todolist",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^22.5.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-router-bootstrap": "^0.26.6",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"bootstrap": "^5.3.3",
|
||||
"react": "^18.3.1",
|
||||
"react-bootstrap": "^2.10.4",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-bootstrap": "^0.26.3",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^5.4.2"
|
||||
},
|
||||
@@ -58,6 +69,7 @@
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"jsdom": "^24.1.1",
|
||||
"sass": "^1.77.8",
|
||||
"source-map-support": "^0.5.21",
|
||||
"vitest": "^2.0.5"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 341 KiB After Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 651 B After Width: | Height: | Size: 831 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,9 +0,0 @@
|
||||
.main {
|
||||
min-height: 60vh;
|
||||
text-align: center;
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.main h2 {
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
@@ -1,84 +1,57 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
// Components
|
||||
import Footer from './components/Footer';
|
||||
import Header from './components/Header';
|
||||
import TaskForm from './components/TaskForm';
|
||||
import TaskList from './components/TaskList';
|
||||
import Modal from './components/Modal';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
|
||||
// Styles
|
||||
import styles from './App.module.css';
|
||||
import './styles/custom.scss';
|
||||
|
||||
// Interfaces
|
||||
import { ITask } from './interfaces/Task';
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
import Landing from './views/Landing';
|
||||
import ProtectedRoute from './routes/ProtectedRoute';
|
||||
import Layout from './layout/PrivateLayout';
|
||||
import browserRoutes from './routes';
|
||||
import NotFound from './views/NotFound';
|
||||
import AuthContext from './context/AuthContext';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [taskList, setTaskList] = useState<ITask[]>([]);
|
||||
const [taskToUpdate, setTaskToUpdate] = useState<ITask | null>(null);
|
||||
const { signed, checkCurrentAuthUser } = useContext(AuthContext);
|
||||
|
||||
const deleteTask = (id: number) => {
|
||||
setTaskList(
|
||||
taskList.filter((task) => task.id !== id)
|
||||
);
|
||||
};
|
||||
|
||||
const hideOrShowModal = (display: boolean) => {
|
||||
const modal = document.querySelector('#modal');
|
||||
if (display) {
|
||||
modal!.classList.remove('hide');
|
||||
} else {
|
||||
modal!.classList.add('hide');
|
||||
const notSignedRouter = createBrowserRouter([
|
||||
{
|
||||
path: '*',
|
||||
element: <Landing />
|
||||
}
|
||||
]);
|
||||
|
||||
const signedRouter = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <ProtectedRoute />,
|
||||
children: [
|
||||
{
|
||||
element: <Layout />,
|
||||
children: browserRoutes
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <NotFound />
|
||||
}
|
||||
]);
|
||||
|
||||
const getBrowserRoutes = () => {
|
||||
if (signed) {
|
||||
return signedRouter;
|
||||
}
|
||||
return notSignedRouter;
|
||||
};
|
||||
|
||||
const editTask = (task: ITask): void => {
|
||||
hideOrShowModal(true);
|
||||
setTaskToUpdate(task);
|
||||
};
|
||||
|
||||
const updateTask = (id: number, title: string, difficulty: number) => {
|
||||
const updatedTask: ITask = { id, title, difficulty };
|
||||
const updatedItems = taskList.map((task) => (task.id === updatedTask.id ? updatedTask : task));
|
||||
|
||||
setTaskList(updatedItems);
|
||||
hideOrShowModal(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
checkCurrentAuthUser(window.location.pathname);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal>
|
||||
<TaskForm
|
||||
btnText="Update task"
|
||||
taskList={taskList}
|
||||
task={taskToUpdate}
|
||||
handleUpdate={updateTask}
|
||||
/>
|
||||
</Modal>
|
||||
<Header />
|
||||
|
||||
<main className={styles.main}>
|
||||
<div>
|
||||
<h2>What are you going to do?</h2>
|
||||
<TaskForm
|
||||
btnText="New task"
|
||||
taskList={taskList}
|
||||
setTaskList={setTaskList}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Your tasks</h2>
|
||||
<TaskList
|
||||
taskList={taskList}
|
||||
handleDelete={deleteTask}
|
||||
handleEdit={editTask}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
<RouterProvider router={getBrowserRoutes()} />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const getHeaders = (): Headers => {
|
||||
const headers = new Headers();
|
||||
return headers;
|
||||
};
|
||||
|
||||
const api = {
|
||||
get: (url: string, params?: object) => fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getHeaders(),
|
||||
...params
|
||||
}),
|
||||
|
||||
post: (url: string, data: BodyInit) => fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: data
|
||||
}),
|
||||
|
||||
put: (url: string, data: BodyInit) => fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: data
|
||||
}),
|
||||
|
||||
patch: (url: string, data: any) => fetch(url, {
|
||||
method: 'PATCH',
|
||||
headers: getHeaders(),
|
||||
body: data
|
||||
}),
|
||||
|
||||
delete: (url: string) => fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
})
|
||||
};
|
||||
|
||||
export default api;
|
||||
@@ -0,0 +1,36 @@
|
||||
import { env } from '../env';
|
||||
import { User } from '../types/User';
|
||||
|
||||
const server = env.VITE_BACKEND_SERVER;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function privateSessionFake(signed: boolean): Promise<User | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (signed) {
|
||||
resolve({
|
||||
name: 'User',
|
||||
email: 'email@domain.com'
|
||||
});
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const ApiConfig = {
|
||||
login: `${server}/login`,
|
||||
|
||||
loginFake: async (): Promise<void> => new Promise((resolve) => {
|
||||
resolve();
|
||||
}),
|
||||
|
||||
logoutFake: async (): Promise<void> => new Promise((resolve) => {
|
||||
resolve();
|
||||
}),
|
||||
|
||||
currentSessionFake: privateSessionFake
|
||||
};
|
||||
|
||||
export default ApiConfig;
|
||||
@@ -0,0 +1 @@
|
||||
export const REDIRECT_PATH = 'TASKNOTE-REDIRECT';
|
||||
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 113 KiB |
@@ -1,18 +1,27 @@
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import style from './Footer.module.css';
|
||||
import { env } from '../env';
|
||||
import AuthContext from '../context/AuthContext';
|
||||
|
||||
const Footer = () => {
|
||||
const build = env.BUILD;
|
||||
const { signOut } = useContext(AuthContext);
|
||||
const build = env.VITE_BUILD;
|
||||
|
||||
return (
|
||||
<footer className={style.footer}>
|
||||
<p>
|
||||
<span>React + TS Todo</span>
|
||||
{' '}
|
||||
@ 2022
|
||||
{` Build: ${build}`}
|
||||
</p>
|
||||
|
||||
<span>React + TS Todoo</span>
|
||||
{' '}
|
||||
@ 2024
|
||||
{` Build: ${build}`}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => signOut()}
|
||||
>
|
||||
Sair
|
||||
</Button>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
.header {
|
||||
background-color: #282c34;
|
||||
color: #61dafb;
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './Header.module.css';
|
||||
|
||||
const Header = () => (
|
||||
<header className={styles.header}>
|
||||
<h1>React + TS Todo</h1>
|
||||
</header>
|
||||
);
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import Container from 'react-bootstrap/Container';
|
||||
import Nav from 'react-bootstrap/Nav';
|
||||
import Navbar from 'react-bootstrap/Navbar';
|
||||
import { LinkContainer } from 'react-router-bootstrap';
|
||||
import Logo from '../../assets/logo2-450-450.png';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function Header() {
|
||||
return (
|
||||
<header>
|
||||
<Navbar expand="lg" className="bg-body-secondary">
|
||||
<Container>
|
||||
<LinkContainer to="/home">
|
||||
<Navbar.Brand>
|
||||
<img width="30" src={Logo} alt="TaskNote logo" />
|
||||
</Navbar.Brand>
|
||||
</LinkContainer>
|
||||
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||
<Navbar.Collapse id="basic-navbar-nav">
|
||||
<Nav className="me-auto">
|
||||
<LinkContainer to="/home">
|
||||
<Nav.Link>Home</Nav.Link>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/about">
|
||||
<Nav.Link>About</Nav.Link>
|
||||
</LinkContainer>
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
</Container>
|
||||
</Navbar>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createContext } from 'react';
|
||||
import { User } from '../types/User';
|
||||
|
||||
export interface AuthContextData {
|
||||
signed: boolean;
|
||||
user: User | undefined;
|
||||
checkCurrentAuthUser: (pathname: string) => Promise<void>;
|
||||
signIn: () => void;
|
||||
signOut: () => void;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextData>({} as AuthContextData);
|
||||
|
||||
export default AuthContext;
|
||||
@@ -0,0 +1,117 @@
|
||||
/* eslint-disable no-console */
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { User } from '../types/User';
|
||||
import { env } from '../env';
|
||||
import ApiConfig from '../api-service/apiConfig';
|
||||
import AuthContext, { AuthContextData } from './AuthContext';
|
||||
import { REDIRECT_PATH } from '../app-constants/app-constants';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Props) => {
|
||||
const [signed, setSigned] = useState<boolean>(false);
|
||||
const [user, setUser] = useState<User | undefined>();
|
||||
const [isAdmin, setIsAdmin] = useState<boolean>(false);
|
||||
const [intervalInstance, setIntervalInstance] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
const fetchCurrentSession = async (pathname: string): Promise<User | undefined> => {
|
||||
try {
|
||||
const currentUser = await ApiConfig.currentSessionFake(signed);
|
||||
if (currentUser) {
|
||||
setSigned(true);
|
||||
}
|
||||
return currentUser;
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
console.warn(e.message);
|
||||
} else if (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
// Clear stored client id and name
|
||||
localStorage.clear();
|
||||
localStorage.setItem(REDIRECT_PATH, pathname);
|
||||
setUser(undefined);
|
||||
setSigned(false);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const updateUserSession = (userPriv: User) => {
|
||||
localStorage.setItem('TaskNote-token', userPriv.email);
|
||||
};
|
||||
|
||||
const checkCurrentAuthUser = async (pathname: string): Promise<void> => {
|
||||
const currentUser = await fetchCurrentSession(pathname);
|
||||
if (currentUser) {
|
||||
updateUserSession(currentUser);
|
||||
setUser(currentUser);
|
||||
}
|
||||
};
|
||||
|
||||
const signIn = async (): Promise<void> => {
|
||||
const appEnv = env.VITE_ENV || 'dev';
|
||||
await ApiConfig.loginFake();
|
||||
setSigned(true);
|
||||
const currentUser: User = {
|
||||
name: `Ricardo ${appEnv}`,
|
||||
email: 'ricardompcampos@gmail.com'
|
||||
};
|
||||
|
||||
setUser(currentUser);
|
||||
};
|
||||
|
||||
const signOut = async (): Promise<void> => {
|
||||
await ApiConfig.logoutFake();
|
||||
setSigned(false);
|
||||
setUser(undefined);
|
||||
setIsAdmin(false);
|
||||
if (intervalInstance) {
|
||||
clearInterval(intervalInstance);
|
||||
setIntervalInstance(null);
|
||||
}
|
||||
localStorage.clear();
|
||||
};
|
||||
|
||||
const refreshTokenPvt = async () => {
|
||||
const currentUser = await fetchCurrentSession('/');
|
||||
if (currentUser) {
|
||||
updateUserSession(currentUser);
|
||||
}
|
||||
};
|
||||
|
||||
// 2 minutes
|
||||
const second = 1000;
|
||||
const minute = second * 60;
|
||||
const REFRESH_TIMER = minute * 2;
|
||||
|
||||
if (intervalInstance == null && signed) {
|
||||
const instance = setInterval(() => {
|
||||
refreshTokenPvt()
|
||||
.then(() => {
|
||||
console.log('User session successfully refreshed!');
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}, REFRESH_TIMER);
|
||||
|
||||
setIntervalInstance(instance);
|
||||
}
|
||||
|
||||
const contextValue: AuthContextData = useMemo(() => ({
|
||||
signed,
|
||||
user,
|
||||
checkCurrentAuthUser,
|
||||
signIn,
|
||||
signOut,
|
||||
isAdmin
|
||||
}), [signed, user, checkCurrentAuthUser, signIn, signOut, isAdmin]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={contextValue}>
|
||||
{ children }
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthProvider;
|
||||
@@ -1,9 +0,0 @@
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: Helvetica;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
import './index.css';
|
||||
import AuthProvider from './context/AuthProvider';
|
||||
|
||||
window.global ||= window;
|
||||
|
||||
@@ -12,6 +11,8 @@ const root = ReactDOM.createRoot(
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<AuthProvider>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Container } from 'react-bootstrap';
|
||||
import Header from '../../components/Header';
|
||||
import Footer from '../../components/Footer';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function Layout() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
|
||||
<Container>
|
||||
<Outlet />
|
||||
</Container>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,15 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
import AuthContext from '../../context/AuthContext';
|
||||
|
||||
const ProtectedRoute = (): React.JSX.Element => {
|
||||
const { signed, signOut } = useContext(AuthContext);
|
||||
|
||||
if (!signed) {
|
||||
signOut();
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { Navigate, RouteObject } from 'react-router-dom';
|
||||
import getStoredPath from '../utils/PathUtils';
|
||||
import Home from '../views/Home';
|
||||
import About from '../views/About';
|
||||
import NotFound from '../views/NotFound';
|
||||
|
||||
const browserRoutes: RouteObject[] = [
|
||||
{
|
||||
path: '/',
|
||||
element: (
|
||||
<Navigate to={getStoredPath()} replace />
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
element: (
|
||||
<Navigate to="/home" replace />
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
element: (
|
||||
<Home />
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
element: (
|
||||
<About />
|
||||
)
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
element: (
|
||||
<NotFound />
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
export default browserRoutes;
|
||||
@@ -0,0 +1,10 @@
|
||||
$theme-colors: (
|
||||
'info': tomato,
|
||||
'danger': teal
|
||||
);
|
||||
|
||||
@import '~bootstrap/scss/bootstrap';
|
||||
|
||||
.bg-body-secondary {
|
||||
background-color: red;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type User = {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { REDIRECT_PATH } from '../app-constants/app-constants';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function getStoredPath() {
|
||||
const root: string = '/';
|
||||
const home: string = '/home';
|
||||
|
||||
const storedPath: string | null = localStorage.getItem(REDIRECT_PATH);
|
||||
if (storedPath) {
|
||||
localStorage.removeItem(REDIRECT_PATH);
|
||||
return storedPath === root ? home : storedPath;
|
||||
}
|
||||
return home;
|
||||
}
|
||||
|
||||
export default getStoredPath;
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function About() {
|
||||
return (
|
||||
<h1>This is about!</h1>
|
||||
);
|
||||
}
|
||||
|
||||
export default About;
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function Home() {
|
||||
return (
|
||||
<h1>This is home!</h1>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
@@ -0,0 +1,25 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import './styles.scss';
|
||||
import AuthContext from '../../context/AuthContext';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function Landing() {
|
||||
const { signIn } = useContext(AuthContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>This is landing page</h1>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => signIn()}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Landing;
|
||||
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
background-color: #ccc;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function NotFound() {
|
||||
return (
|
||||
<h2>Not found!</h2>
|
||||
);
|
||||
}
|
||||
|
||||
export default NotFound;
|
||||
@@ -1 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module '*.png';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ConfigEnv, defineConfig, UserConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig(({ mode }: ConfigEnv) => {
|
||||
const config: UserConfig = {
|
||||
@@ -38,7 +39,8 @@ export default defineConfig(({ mode }: ConfigEnv) => {
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'~bootstrap': path.resolve(__dirname, 'node_modules/bootstrap')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||