[dev]sidebar

This commit is contained in:
susie-laptop 2025-05-08 19:03:19 -04:00
parent 817f334b5d
commit 0a74e9a59c
21 changed files with 2198 additions and 594 deletions

View File

@ -4,7 +4,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Hubot+Sans:ital,wght@0,200..900;1,200..900&family=Josefin+Sans:ital,wght@0,100..700;1,100..700&family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Roboto+Slab:wght@100..900&family=Source+Sans+3:ital,wght@0,800;1,800&display=swap" rel="stylesheet">
<title>Creator Center</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

1821
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,40 @@
{ {
"name": "mvp_ooin", "name": "creator-center-ooin",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^2.8.1", "@fortawesome/fontawesome-free": "^6.7.2",
"axios": "^1.9.0", "@fortawesome/fontawesome-svg-core": "^6.7.2",
"react": "^19.1.0", "@fortawesome/free-brands-svg-icons": "^6.7.2",
"react-dom": "^19.1.0", "@fortawesome/free-regular-svg-icons": "^6.7.2",
"react-redux": "^9.2.0", "@fortawesome/free-solid-svg-icons": "^6.7.2",
"react-router-dom": "^7.5.3", "@fortawesome/react-fontawesome": "^0.2.2",
"redux": "^5.0.1", "bootstrap": "^5.3.3",
"sass": "^1.87.0" "lucide-react": "^0.508.0",
}, "react": "^19.1.0",
"devDependencies": { "react-bootstrap": "^2.10.1",
"@eslint/js": "^9.25.0", "react-dom": "^19.1.0",
"@types/react": "^19.1.2", "react-redux": "^9.2.0",
"@types/react-dom": "^19.1.2", "react-router-dom": "^7.6.0"
"@vitejs/plugin-react": "^4.4.1", },
"eslint": "^9.25.0", "devDependencies": {
"eslint-plugin-react-hooks": "^5.2.0", "@eslint/js": "^9.25.0",
"eslint-plugin-react-refresh": "^0.4.19", "@types/node": "^22.15.17",
"globals": "^16.0.0", "@types/react": "^19.1.2",
"vite": "^6.3.5" "@types/react-dom": "^19.1.2",
} "@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"sass": "^1.87.0",
"vite": "^6.3.5"
}
} }

View File

@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@ -1,27 +1,13 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { faTiktok, faYoutube, faInstagram } from '@fortawesome/free-brands-svg-icons';
import { Provider } from 'react-redux'; import { library } from '@fortawesome/fontawesome-svg-core';
import store from './store'; import { fas } from '@fortawesome/free-solid-svg-icons';
import './App.css'; import Router from './router';
// Import pages here // Add Font Awesome icons to library
import Home from './pages/Home'; library.add(faTiktok, fas, faYoutube, faInstagram);
import About from './pages/About';
import NotFound from './pages/NotFound';
function App() { function App() {
return ( return <Router />;
<Provider store={store}>
<Router>
<div className='app-container'>
<Routes>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
<Route path='*' element={<NotFound />} />
</Routes>
</div>
</Router>
</Provider>
);
} }
export default App; export default App;

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

29
src/components/Layout.jsx Normal file
View File

@ -0,0 +1,29 @@
import React, { useState } from 'react';
import { Outlet } from 'react-router-dom';
import { Button } from 'react-bootstrap';
import { List } from 'lucide-react';
import Sidebar from './Sidebar';
export default function Layout() {
const [showSidebar, setShowSidebar] = useState(true);
const toggleSidebar = () => {
setShowSidebar(!showSidebar);
};
return (
<div className='d-flex'>
<div className={`sidebar ${showSidebar ? 'show' : ''}`}>
<Sidebar />
</div>
<Button variant='light' size='sm' className='sidebar-toggle' onClick={toggleSidebar}>
<List size={20} />
</Button>
<main className='main-content w-100'>
<Outlet />
</main>
</div>
);
}

169
src/components/Sidebar.jsx Normal file
View File

@ -0,0 +1,169 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Nav, Accordion } from 'react-bootstrap';
import { Settings, ChevronDown, Blocks, SquareActivity, LayoutDashboard, Mail, UserSearch, Heart } from 'lucide-react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import logo from '@/assets/logo.png';
import '../styles/sidebar.scss';
// Organized menu items
const menuItems = [
{
id: 'creator-discovery',
title: 'Creator Discovery',
path: '/creator-discovery',
icon: <UserSearch style={{ width: 20, height: 20 }} />,
hasSubmenu: false,
},
{
id: 'creator-database',
title: 'Creator Database',
path: '/creator-database',
icon: <Blocks style={{ width: 20, height: 20 }} />,
hasSubmenu: true,
submenuItems: [
{
id: 'tiktok',
title: 'TikTok',
path: '/creator-database/tiktok',
icon: <FontAwesomeIcon style={{ width: 20, height: 20 }} icon='fa-brands fa-tiktok' />,
},
{
id: 'instagram',
title: 'Instagram',
path: '/creator-database/instagram',
icon: <FontAwesomeIcon style={{ width: 20, height: 20 }} icon='fa-brands fa-instagram' />,
},
{
id: 'youtube',
title: 'YouTube',
path: '/creator-database/youtube',
icon: <FontAwesomeIcon style={{ width: 20, height: 20 }} icon='fa-brands fa-youtube' />,
},
],
},
{
id: 'private-creators',
title: 'Private Creators',
path: '/private-creators',
icon: <Heart style={{ width: 20, height: 20 }} />,
hasSubmenu: true,
submenuItems: [],
},
{
id: 'deep-analysis',
title: 'Deep Analysis',
path: '/deep-analysis',
icon: <SquareActivity style={{ width: 20, height: 20 }} />,
hasSubmenu: false,
},
{
id: 'brands',
title: 'Brands',
path: '/brands',
icon: <LayoutDashboard style={{ width: 20, height: 20 }} />,
hasSubmenu: false,
},
{
id: 'creator-inbox',
title: 'Creator Inbox',
path: '/creator-inbox',
icon: <Mail style={{ width: 20, height: 20 }} />,
hasSubmenu: true,
submenuItems: [],
},
{
id: 'settings',
title: 'Settings',
path: '/settings',
icon: <Settings style={{ width: 20, height: 20 }} />,
hasSubmenu: false,
},
];
export default function Sidebar() {
const location = useLocation();
const [expanded, setExpanded] = useState({});
//
const isActive = (path) => {
return location.pathname === path || location.pathname.startsWith(`${path}/`);
};
//
const hasActiveChild = (item) => {
if (!item.submenuItems) return false;
return item.submenuItems.some((subItem) => isActive(subItem.path));
};
// /
const handleAccordionToggle = (id) => {
setExpanded((prev) => ({
...prev,
[id]: !prev[id],
}));
};
return (
<div className='sidebar'>
<div className='sidebar-header p-3 d-flex align-items-center'>
<img src={logo} alt='OOIN Logo' width={48} height={48} className='me-2' />
<div>
<div className='fw-bold'>OOIN Media</div>
<div className='small text-muted'>Creator Center</div>
</div>
</div>
<Nav className='flex-column sidebar-nav'>
{menuItems.map((item) => {
if (item.hasSubmenu) {
const isItemActive = isActive(item.path) || hasActiveChild(item);
const isOpen = expanded[item.id] || isItemActive;
return (
<Accordion key={item.id} className='sidebar-accordion' activeKey={isOpen ? item.id : null}>
<Accordion.Item eventKey={item.id} className='border-0 bg-transparent'>
<Accordion.Header
onClick={() => handleAccordionToggle(item.id)}
className={isItemActive ? 'active' : ''}
>
<div className='d-flex align-items-center'>
<span className='sidebar-icon me-2'>{item.icon}</span>
{item.title}
</div>
</Accordion.Header>
<Accordion.Body className='p-0'>
<Nav className='flex-column sidebar-submenu'>
{item.submenuItems &&
item.submenuItems.map((subItem) => (
<Nav.Item key={`${item.id}-${subItem.id}`}>
<Nav.Link
as={Link}
to={subItem.path}
className={isActive(subItem.path) ? 'active' : ''}
>
<span className='sidebar-icon me-2'>{subItem.icon}</span>
{subItem.title}
</Nav.Link>
</Nav.Item>
))}
</Nav>
</Accordion.Body>
</Accordion.Item>
</Accordion>
);
} else {
return (
<Nav.Item key={item.id}>
<Nav.Link as={Link} to={item.path} className={isActive(item.path) ? 'active' : ''}>
<span className='sidebar-icon me-2'>{item.icon}</span>
{item.title}
</Nav.Link>
</Nav.Item>
);
}
})}
</Nav>
</div>
);
}

View File

@ -1,125 +1,49 @@
:root { :root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; --bs-primary-rgb: 99, 102, 241; /* 必须匹配custom-theme.scss中的$primary */
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
} }
body { body {
font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin: 0; margin: 0;
display: flex; padding: 0;
place-items: center; font-size: 16px;
min-width: 320px; line-height: 1.5;
color: #212529;
background-color: #f5f3ff;
min-height: 100vh; min-height: 100vh;
} }
a { a {
font-weight: 500; text-decoration: none;
color: #646cff;
text-decoration: inherit;
} }
a:hover { a:hover {
color: #535bf2; text-decoration: underline;
} }
h1 { /* Override some Bootstrap defaults */
font-size: 2.2em; .btn:focus, .btn:active:focus {
line-height: 1.1; box-shadow: none;
margin-bottom: 1rem;
} }
h2 { /* Custom scrollbar */
font-size: 1.5em; ::-webkit-scrollbar {
margin-bottom: 0.75rem; width: 8px;
height: 8px;
} }
button { ::-webkit-scrollbar-track {
border-radius: 8px; background: #f1f1f1;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
} }
.app-container { ::-webkit-scrollbar-thumb {
max-width: 1280px; background: #ccc;
margin: 0 auto; border-radius: 4px;
padding: 2rem;
text-align: center;
} }
.button-group { ::-webkit-scrollbar-thumb:hover {
display: flex; background: #aaa;
gap: 0.5rem; }
justify-content: center;
margin: 1rem 0;
}
.navigation {
margin-top: 2rem;
}
.home-container, .about-container, .not-found-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.counter-section {
margin: 2rem 0;
padding: 1.5rem;
border-radius: 8px;
background-color: #1a1a1a;
}
.tech-stack {
margin: 2rem 0;
text-align: left;
}
.tech-stack ul {
list-style-position: inside;
margin-left: 1rem;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
.counter-section {
background-color: #f1f1f1;
}
}

40
src/lib/utils.js Normal file
View File

@ -0,0 +1,40 @@
/**
* 格式化日期
* @param {Date} date - 要格式化的日期对象
* @param {string} format - 格式字符串 (默认为 'YYYY-MM-DD')
* @returns {string} 格式化后的日期字符串
*/
export function formatDate(date, format = 'YYYY-MM-DD') {
if (!date) return '';
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const seconds = String(d.getSeconds()).padStart(2, '0');
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds);
}
/**
* 格式化数字
* @param {number} number - 要格式化的数字
* @param {number} decimals - 小数位数
* @param {string} decimalPoint - 小数点符号
* @param {string} thousandsSeparator - 千位分隔符
* @returns {string} 格式化后的数字字符串
*/
export function formatNumber(number, decimals = 0, decimalPoint = '.', thousandsSeparator = ',') {
if (isNaN(number)) return '0';
const n = Number(number).toFixed(decimals);
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSeparator);
}

View File

@ -1,10 +1,11 @@
import React from 'react'; import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import App from './App.jsx'; import '../src/styles/custom-theme.scss';
import './index.css'; import './index.css';
import App from './App.jsx';
ReactDOM.createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<React.StrictMode> <StrictMode>
<App /> <App />
</React.StrictMode> </StrictMode>
); );

View File

@ -1,29 +0,0 @@
import { Link } from 'react-router-dom';
function About() {
return (
<div className='about-container'>
<h1>About 达人工具 MVP</h1>
<p>This is a React application built with Vite, Redux, and React Router.</p>
<p>The application serves as an MVP for talent tools.</p>
<div className='tech-stack'>
<h2>Tech Stack</h2>
<ul>
<li>React</li>
<li>Redux (with Redux Toolkit)</li>
<li>React Router</li>
<li>Vite</li>
<li>Axios</li>
<li>SASS</li>
</ul>
</div>
<div className='navigation'>
<Link to='/'>Back to Home</Link>
</div>
</div>
);
}
export default About;

View File

@ -1,30 +1,15 @@
import { useSelector, useDispatch } from 'react-redux'; import React from 'react';
import { increment, decrement } from '../store/counterSlice'; import { Container, Row, Col } from 'react-bootstrap';
import { Link } from 'react-router-dom';
function Home() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
export default function Home() {
return ( return (
<div className='home-container'> <Container className='py-4'>
<h1>达人工具 MVP</h1> <Row>
<p>Welcome to the Talent Tool MVP application</p> <Col>
<h1 className='fw-bold mb-4'>Welcome to OOIN Creator Center</h1>
<div className='counter-section'> <p>Select an option from the sidebar to get started.</p>
<h2>Counter Demo</h2> </Col>
<p>Current count: {count}</p> </Row>
<div className='button-group'> </Container>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(increment())}>+</button>
</div>
</div>
<div className='navigation'>
<Link to='/about'>About Page</Link>
</div>
</div>
); );
} }
export default Home;

View File

@ -1,16 +0,0 @@
import { Link } from 'react-router-dom';
function NotFound() {
return (
<div className='not-found-container'>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<div className='navigation'>
<Link to='/'>Back to Home</Link>
</div>
</div>
);
}
export default NotFound;

71
src/router/index.jsx Normal file
View File

@ -0,0 +1,71 @@
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Home from '../pages/Home';
import BootstrapLayout from '../components/Layout';
// Routes configuration object
const routes = [
{
path: '/',
element: <Home />,
},
{
path: '/creator-discovery',
element: <Home />,
},
{
path: '/creator-database',
children: [
{
path: '',
element: <Home />,
},
{
path: 'tiktok',
element: <Home />,
},
{
path: 'instagram',
element: <Home />,
},
{
path: 'youtube',
element: <Home />,
},
],
},
{
path: '/private-creators/*',
element: <Home />,
},
{
path: '/deep-analysis',
element: <Home />,
},
{
path: '/brands',
element: <Home />,
},
{
path: '/creator-inbox/*',
element: <Home />,
},
{
path: '/settings',
element: <Home />,
},
];
// Create router with routes wrapped in the layout
const router = createBrowserRouter([
{
path: '/',
element: <BootstrapLayout />,
children: routes,
},
]);
export default function Router() {
return <RouterProvider router={router} />;
}
export { routes };

View File

@ -1,55 +0,0 @@
import axios from 'axios';
const API_URL = 'https://api.example.com'; // Replace with your actual API URL
const apiClient = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Add request interceptor for authentication
apiClient.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Add response interceptor for error handling
apiClient.interceptors.response.use(
(response) => response,
(error) => {
// Handle common errors here
if (error.response && error.response.status === 401) {
// Handle unauthorized access
localStorage.removeItem('token');
// Redirect to login or show notification
}
return Promise.reject(error);
}
);
// Example API methods
export const userService = {
login: (credentials) => apiClient.post('/auth/login', credentials),
register: (userData) => apiClient.post('/auth/register', userData),
getProfile: () => apiClient.get('/user/profile'),
};
export const contentService = {
getItems: (params) => apiClient.get('/items', { params }),
getItemById: (id) => apiClient.get(`/items/${id}`),
createItem: (data) => apiClient.post('/items', data),
updateItem: (id, data) => apiClient.put(`/items/${id}`, data),
deleteItem: (id) => apiClient.delete(`/items/${id}`),
};
export default apiClient;

View File

@ -1,25 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
value: 0,
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

View File

@ -1,21 +0,0 @@
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
// Import reducers
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
// Add reducers here
counter: counterReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat([
// Add middleware here if needed
]),
});
// Enable refetchOnFocus and refetchOnReconnect behaviors
setupListeners(store.dispatch);
export default store;

View File

@ -0,0 +1,34 @@
// 在引入Bootstrap前自定义变量
// 主题颜色 - 根据需要修改这些值
$primary: #6366f1; // 靛蓝色 (Indigo)
$secondary: #6c757d; // 灰色
$success: #198754; // 绿色
$info: #0dcaf0; // 浅蓝色
$warning: #ffc107; // 黄色
$danger: #dc3545; // 红色
$light: #f8f9fa; // 浅色
$dark: #212529; // 深色
$indigo-50: #eef2ff;
$indigo-100: #e0e7ff;
$indigo-500: #6366f1;
$violet-50: #f5f3ff;
$violet-100: #ede9fe;
$neutral-600: #525252;
$zinc-600: #52525b;
// 字体
$font-family-sans-serif: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif;
$font-size-base: 1rem;
// 其他自定义
$border-radius: 0.375rem;
$box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
// 导入Bootstrap
@import 'bootstrap/scss/bootstrap';
#root {
background-color: #f5f3ff;
}

142
src/styles/sidebar.scss Normal file
View File

@ -0,0 +1,142 @@
@import 'custom-theme.scss';
.sidebar {
width: 220px;
min-height: 100vh;
position: fixed;
left: 0;
top: 0;
bottom: 0;
box-shadow: none;
z-index: 1000;
transition: all 0.3s ease;
overflow-y: auto;
background: $violet-50;
.sidebar-header {
}
.sidebar-nav {
padding: 1rem 0;
.nav-link {
padding: 0.75rem 1rem;
color: $zinc-600; // neutral-600
font-size: 1rem;
border-radius: 0;
display: flex;
align-items: center;
text-decoration: none;
font-weight: 500;
&:hover {
text-decoration: none;
color: var(--bs-primary);
background-color: rgba(var(--bs-primary-rgb), 0.05);
}
&.active {
color: var(--bs-primary);
font-weight: 600;
border-left: 4px solid var(--bs-primary);
}
.sidebar-icon {
display: flex;
align-items: center;
justify-content: center;
}
}
}
.sidebar-accordion {
width: 100%;
.accordion-item {
background-color: transparent;
}
.accordion-header {
.accordion-button {
padding: 0.75rem 1rem;
background-color: transparent;
color: $zinc-600; // neutral-600
font-size: 1rem;
box-shadow: none;
font-weight: 500;
&:not(.collapsed) {
color: var(--bs-primary);
background-color: rgba(var(--bs-primary-rgb), 0.05);
font-weight: 500;
}
&:hover {
color: var(--bs-primary);
background-color: rgba(var(--bs-primary-rgb), 0.05);
}
&:focus {
box-shadow: none;
border-color: transparent;
}
&::after {
width: 1rem;
height: 1rem;
background-size: 1rem;
}
&.active {
font-weight: 600;
color: var(--bs-primary);
}
}
}
.sidebar-submenu {
.nav-link {
padding: 0.5rem 1rem;
padding-left: 2rem;
}
}
}
}
// Adjust main content when sidebar is present
.main-content {
padding: 1rem;
margin: 1rem;
margin-left: 220px;
height: 100vh;
transition: all 0.3s ease;
background: #f8f9fa;
border-radius: 8px;
}
// Responsive sidebar for mobile
@media (max-width: 768px) {
.sidebar {
width: 0;
overflow: hidden;
&.show {
width: 250px;
}
}
.main-content {
margin-left: 0;
}
}
// Toggle button for mobile
.sidebar-toggle {
position: fixed;
top: 1rem;
left: 1rem;
z-index: 1100;
display: none;
@media (max-width: 768px) {
display: block;
}
}

View File

@ -1,7 +1,13 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react';
import path from 'path';
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
}) resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});