diff --git a/.sassrc.js b/.sassrc.js new file mode 100644 index 0000000..7cda289 --- /dev/null +++ b/.sassrc.js @@ -0,0 +1,5 @@ +module.exports = { + quietDeps: true, + outputStyle: 'compressed', + sourceMap: false, +}; diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..44a8cc8 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,215 @@ +# OOIN Creator Center - Development Documentation + +## Project Overview +OOIN Creator Center is a React application built with Vite that allows users to discover, analyze, and manage content creators for marketing campaigns. The application provides tools for browsing creators from different social media platforms, managing brands, and facilitating creator communication. + +## Tech Stack + +### Core Technologies +- **React 19**: UI library +- **Vite 6**: Development and build tool +- **Redux Toolkit**: State management +- **React Router v7**: Routing +- **Bootstrap 5 + React Bootstrap**: UI components +- **SASS**: Advanced styling + +### Key Dependencies +- **Redux Persist**: Persists state to sessionStorage +- **Font Awesome**: Icon library +- **Lucide React**: Additional icon components +- **Date-fns**: Date utilities + +## Project Structure + +``` +/ +├── public/ # Static assets +├── src/ # Source code +│ ├── assets/ # Assets (images, etc.) +│ ├── components/ # Reusable components +│ │ └── Layouts/ # Layout components +│ ├── lib/ # Utility functions +│ ├── pages/ # Page components +│ ├── router/ # Routing configuration +│ ├── store/ # Redux store configuration +│ │ └── slices/ # Redux slices +│ ├── styles/ # Global styles +│ ├── App.jsx # Main application component +│ ├── index.css # Global CSS +│ └── main.jsx # Entry point +├── index.html # HTML template +└── vite.config.js # Vite configuration +``` + +## Application Architecture + +### State Management +- **Redux Toolkit** is used for global state management +- State is divided into several slices: + - `creators`: Manages creator data and selection state + - `filters`: Manages filter options for creator discovery + - `brands`: Manages brand information + - `inbox`: Manages creator communication +- Redux Persist stores state in sessionStorage for persistence across page reloads + +### Routing +- The application uses React Router v7 with a nested route structure +- Main routes are wrapped in `MainLayout` which includes the sidebar navigation +- The Creator Inbox uses a dedicated `DividLayout` for its specialized interface + +## Feature Modules + +### Creator Database +- Displays creator information from different social media platforms (TikTok, Instagram, YouTube) +- Supports filtering by various attributes like category, followers, etc. +- Creators can be selected and added to campaigns + +### Brands Management +- Allows viewing and management of brand partnerships +- Displays brand information and status + +### Creator Inbox +- Communication interface for interacting with creators +- Includes chat functionality and message templates + +## Data Model + +### Creator +```javascript +{ + id: number, + name: string, + avatar: string, + category: string, + ecommerceLevel: string, + exposureLevel: string, + followers: string, + gmv: string, + soldPercentage: string, + avgViews: string, + hasEcommerce: boolean, + hasTiktok: boolean, + hasInstagram: boolean, // optional + hasYoutube: boolean, // optional + verified: boolean +} +``` + +### Brand +```javascript +{ + id: number, + name: string, + logo: string, + status: string, + category: string, + lastUpdated: string, + lastAction: string +} +``` + +### Message +```javascript +{ + id: number, + text: string, + timestamp: string, + sender: 'user' | 'creator' +} +``` + +## Development Workflow + +### Setup +1. Clone the repository +2. Install dependencies: `npm install` +3. Start development server: `npm run dev` + +### Build +- Create production build: `npm run build` +- Preview production build: `npm run preview` + +### Code Linting +- Run ESLint: `npm run lint` + +## Component Guidelines + +### Layout Components +- `MainLayout`: Provides the main application layout with sidebar navigation +- `DividLayout`: Used for the Creator Inbox with its specialized layout needs + +### UI Components +- `SearchBar`: Reusable search component used across different pages +- `DatabaseFilter`: Complex filter component for creator discovery +- `DatabaseList`: Displays creator data in a tabular format +- `ChatWindow` and `ChatInput`: Used for creator communications +- `RangeSlider`: Custom slider component for range-based filtering + +## Mock Data +Currently, the application uses mock data defined in the Redux slices. In a production environment, these would be replaced with API calls to a backend service. + +## Future Development Areas + +### API Integration +- Replace mock data with actual API calls +- Implement authentication and user management + +### Feature Enhancements +- Complete the unfinished pages (Creator Discovery, Deep Analysis, Settings) +- Enhance filtering capabilities +- Add data visualization for creator analytics + +### Technical Improvements +- Implement unit and integration testing +- Add TypeScript for improved type safety +- Optimize performance for large datasets + +## UI/UX Guidelines +- The application uses Bootstrap 5 for consistent styling +- Follow the existing component patterns for new UI elements +- Use Font Awesome icons from the available library + +## Redux Store + +### Store Configuration +The Redux store is configured in `src/store/index.js` with Redux Persist for state persistence. + +### Key Slices +1. **creatorsSlice** + - Manages creator data and selection state + - Handles filtering logic based on selected filters + - Provides actions for selecting/deselecting creators + +2. **filtersSlice** + - Manages the filter criteria for creator discovery + - Includes category filters, rating filters, and range filters + +3. **brandsSlice** + - Manages brand data + - Includes mock brand information for display + +4. **inboxSlice** + - Manages creator communication + - Handles message history and conversation state + +## Adding New Features + +### New Page +1. Create a new component in the `src/pages` directory +2. Add the route to `src/router/index.jsx` +3. Update the sidebar navigation in `src/components/Layouts/Sidebar.jsx` if needed + +### New Component +1. Create the component in the `src/components` directory +2. Follow existing naming conventions and styling approaches +3. Reuse existing components where possible + +### New Redux Slice +1. Create a new slice file in `src/store/slices` +2. Define initial state, reducers, and any async thunks needed +3. Add the reducer to the root reducer in `src/store/index.js` + +## Performance Considerations +- Use memoization for expensive computations +- Implement virtualized lists for large data sets +- Optimize Redux selectors for efficient state access \ No newline at end of file diff --git a/index.html b/index.html index af06e28..8ff14c7 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + diff --git a/package-lock.json b/package-lock.json index 9ff8419..21c7b69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", "sass": "^1.87.0", + "terser": "^5.39.1", "vite": "^6.3.5" } }, @@ -1030,6 +1031,16 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -2043,6 +2054,12 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2164,6 +2181,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4391,6 +4414,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4400,6 +4432,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -4433,6 +4475,24 @@ "node": ">=8" } }, + "node_modules/terser": { + "version": "5.39.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.1.tgz", + "integrity": "sha512-Mm6+uad0ZuDtcV8/4uOZQDQ8RuiC5Pu+iZRedJtF7yA/27sPL7d++In/AJKpWZlU3SYMPPkVfwetn6sgZ66pUA==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tinyglobby": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", diff --git a/package.json b/package.json index a1f7475..a21b503 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", "sass": "^1.87.0", + "terser": "^5.39.1", "vite": "^6.3.5" } } diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..f695f2a Binary files /dev/null and b/public/logo.png differ diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 39a4794..1170607 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,6 +2,7 @@ import { faTiktok, faYoutube, faInstagram } from '@fortawesome/free-brands-svg-i import { library } from '@fortawesome/fontawesome-svg-core'; import { fas } from '@fortawesome/free-solid-svg-icons'; import Router from './router'; +import './styles/Campaign.scss'; // Add Font Awesome icons to library library.add(faTiktok, fas, faYoutube, faInstagram); diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/CampaignInfo.jsx b/src/components/CampaignInfo.jsx index e155bcb..6163ccf 100644 --- a/src/components/CampaignInfo.jsx +++ b/src/components/CampaignInfo.jsx @@ -28,7 +28,7 @@ export default function CampaignInfo() {
{selectedCampaign?.category?.length > 0 && - selectedCampaign.category.map((cat) => {cat})} + selectedCampaign.category.map((cat,index) => {cat})}
@@ -66,8 +66,8 @@ export default function CampaignInfo() {
{selectedCampaign?.creator_level?.length > 0 && - selectedCampaign.creator_level.map((level) => ( - {level} + selectedCampaign.creator_level.map((level,index) => ( + {level} ))}
diff --git a/src/components/DatabaseFilter.jsx b/src/components/DatabaseFilter.jsx index 9e9dd61..6a8c206 100644 --- a/src/components/DatabaseFilter.jsx +++ b/src/components/DatabaseFilter.jsx @@ -152,7 +152,7 @@ export default function DatabaseFilter({ path }) { }; return ( -
+

Filter

diff --git a/src/components/DatabaseList.jsx b/src/components/DatabaseList.jsx index 3dc85f2..991c987 100644 --- a/src/components/DatabaseList.jsx +++ b/src/components/DatabaseList.jsx @@ -86,7 +86,7 @@ export default function DatabaseList({ path }) { return (
- +
diff --git a/src/components/DiscoveryList.jsx b/src/components/DiscoveryList.jsx new file mode 100644 index 0000000..2dc85e4 --- /dev/null +++ b/src/components/DiscoveryList.jsx @@ -0,0 +1,40 @@ +import { Table } from 'react-bootstrap'; +import { useSelector } from 'react-redux'; +import { Link } from 'react-router-dom'; + +export default function DiscoveryList() { + const creators = useSelector((state) => state.discovery.creators); + + return ( +
+ + + + + + + + + + + + + + + {creators?.length > 0 && creators.map((creator) => ( + + + + + + + + + + + ))} + +
Sessions#Creator#Shoppable CreatorsAvg.FollowersAvg.GMVAvg.Video ViewsDateView Details
{creator.sessions}{creator.creator}{creator.shoppableCreators}{creator.avgFollowers}{creator.avgGMV}{creator.avgVideoViews}{creator.date}View
+
+ ); +} diff --git a/src/components/ProductsList.jsx b/src/components/ProductsList.jsx index 9b6163f..36c5e52 100644 --- a/src/components/ProductsList.jsx +++ b/src/components/ProductsList.jsx @@ -5,7 +5,7 @@ import { useParams } from 'react-router-dom'; import '../styles/Products.scss'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -export default function ProductsList() { +export default function ProductsList({ onShowProductDetail }) { const { brandId } = useParams(); const { brands } = useSelector((state) => state.brands); const [products, setProducts] = useState([]); @@ -53,8 +53,8 @@ export default function ProductsList() { }; return ( -
- +
+
@@ -111,7 +111,7 @@ export default function ProductsList() { /> -
+
onShowProductDetail(product.id)} style={{cursor: 'pointer'}}>
{product.name.slice(0, 1)}
{product.name}
diff --git a/src/components/SlidePanel/SlidePanel.jsx b/src/components/SlidePanel/SlidePanel.jsx new file mode 100644 index 0000000..7c0ef69 --- /dev/null +++ b/src/components/SlidePanel/SlidePanel.jsx @@ -0,0 +1,84 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'react-bootstrap'; +import { X } from 'lucide-react'; +import './SlidePanel.scss'; + +/** + * SlidePanel - A reusable offcanvas-like component + * @param {Object} props + * @param {boolean} props.show - Controls visibility of the panel + * @param {function} props.onClose - Function to call when panel is closed + * @param {string} props.placement - Side from which the panel appears (start, end, top, bottom) + * @param {string} props.title - Title to display in the panel header + * @param {ReactNode} props.children - Content to display in the panel body + * @param {string} props.size - Size of the panel (sm, md, lg) + * @param {boolean} props.backdrop - Whether to show backdrop + * @param {boolean} props.closeButton - Whether to show close button in header + */ +const SlidePanel = ({ + show, + onClose, + placement = 'end', + title, + children, + size = 'md', + backdrop = true, + closeButton = true, +}) => { + const [isVisible, setIsVisible] = useState(false); + + // Handle animation timing + useEffect(() => { + if (show) { + setIsVisible(true); + } else { + const timer = setTimeout(() => setIsVisible(false), 300); + return () => clearTimeout(timer); + } + }, [show]); + + if (!isVisible && !show) { + return null; + } + + const handleBackdropClick = () => { + if (backdrop && onClose) { + onClose(); + } + }; + + return ( +
+ {backdrop &&
} + +
e.stopPropagation()} + > +
+ {title &&
{title}
} + {closeButton && ( + + )} +
+
{children}
+
+
+ ); +}; + +SlidePanel.propTypes = { + show: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + placement: PropTypes.oneOf(['start', 'end', 'top', 'bottom']), + title: PropTypes.node, + children: PropTypes.node, + size: PropTypes.oneOf(['sm', 'md', 'lg']), + backdrop: PropTypes.bool, + closeButton: PropTypes.bool, +}; + +export default SlidePanel; diff --git a/src/components/SlidePanel/SlidePanel.scss b/src/components/SlidePanel/SlidePanel.scss new file mode 100644 index 0000000..63cf02d --- /dev/null +++ b/src/components/SlidePanel/SlidePanel.scss @@ -0,0 +1,145 @@ +.slide-panel-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1050; + display: none; + + &.show { + display: block; + } +} + +.slide-panel-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1050; + opacity: 0; + transition: opacity 0.3s ease; + + &.show { + opacity: 1; + } +} + +.slide-panel { + position: fixed; + background-color: #f8f9fa; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + z-index: 1051; + transition: transform 0.3s ease; + overflow-y: auto; + + // Sizes + &.slide-panel-sm { + width: 280px; + max-width: 100%; + } + + &.slide-panel-md { + width: 380px; + max-width: 100%; + } + + &.slide-panel-lg { + width: 480px; + max-width: 100%; + } + + &.slide-panel-xl { + width: 680px; + max-width: 100%; + } + + &.slide-panel-xxl { + width: 880px; + max-width: 100%; + } + + // Placement - End (Right) + &.slide-panel-end { + top: 0; + right: 0; + height: 100%; + transform: translateX(100%); + + &.show { + transform: translateX(0); + } + } + + // Placement - Start (Left) + &.slide-panel-start { + top: 0; + left: 0; + height: 100%; + transform: translateX(-100%); + + &.show { + transform: translateX(0); + } + } + + // Placement - Top + &.slide-panel-top { + top: 0; + left: 0; + width: 100%; + transform: translateY(-100%); + + &.show { + transform: translateY(0); + } + } + + // Placement - Bottom + &.slide-panel-bottom { + bottom: 0; + left: 0; + width: 100%; + transform: translateY(100%); + + &.show { + transform: translateY(0); + } + } +} + +.slide-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem; + border-bottom: 1px solid #dee2e6; +} + +.slide-panel-title { + margin: 0; + font-size: 1.25rem; + font-weight: 500; +} + +.slide-panel-close { + padding: 0.25rem; + color: #6c757d; + opacity: 0.75; + transition: opacity 0.15s; + + &:hover { + opacity: 1; + } +} + +.slide-panel-body { + flex: 1 1 auto; + padding: 1rem; + overflow-y: auto; +} \ No newline at end of file diff --git a/src/components/SlidePanel/SlidePanelExample.jsx b/src/components/SlidePanel/SlidePanelExample.jsx new file mode 100644 index 0000000..c357a59 --- /dev/null +++ b/src/components/SlidePanel/SlidePanelExample.jsx @@ -0,0 +1,99 @@ +import React, { useState } from 'react'; +import { Button, Form } from 'react-bootstrap'; +import SlidePanel from './SlidePanel'; + +const SlidePanelExample = () => { + const [showPanel, setShowPanel] = useState(false); + const [placement, setPlacement] = useState('end'); + const [size, setSize] = useState('md'); + + const handleOpen = () => setShowPanel(true); + const handleClose = () => setShowPanel(false); + + // Example content for the panel + const exampleContent = ( +
+

This is an example of the SlidePanel component. You can place any content here.

+
+ + Example input + + + + Example select + + + + + + + +
+
+ ); + + return ( +
+
+
Panel Position
+
+ + + + +
+
+ +
+
Panel Size
+
+ + + +
+
+ + + + + {exampleContent} + +
+ ); +}; + +export default SlidePanelExample; diff --git a/src/components/SlidePanel/index.js b/src/components/SlidePanel/index.js new file mode 100644 index 0000000..b72c593 --- /dev/null +++ b/src/components/SlidePanel/index.js @@ -0,0 +1,3 @@ +import SlidePanel from './SlidePanel'; + +export default SlidePanel; diff --git a/src/main.jsx b/src/main.jsx index e479611..e8014f9 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -2,7 +2,7 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import store from './store'; -import './styles/global.scss'; +import './styles/index.scss'; import './index.css'; import App from './App.jsx'; diff --git a/src/pages/CampaignDetail.jsx b/src/pages/CampaignDetail.jsx index 2303f70..328e5ed 100644 --- a/src/pages/CampaignDetail.jsx +++ b/src/pages/CampaignDetail.jsx @@ -1,12 +1,15 @@ import React, { useEffect, useState } from 'react'; import { Link, useParams } from 'react-router-dom'; import SearchBar from '../components/SearchBar'; -import { Button, Form } from 'react-bootstrap'; +import { Button, Form, Modal } from 'react-bootstrap'; import { useSelector, useDispatch } from 'react-redux'; -import { fetchBrands, findCampaignById } from '../store/slices/brandsSlice'; +import { fetchBrands, findCampaignById, findProductById } from '../store/slices/brandsSlice'; import CampaignInfo from '../components/CampaignInfo'; -import { ChevronRight, Send } from 'lucide-react'; +import { ChevronRight, Send, Plus } from 'lucide-react'; import ProductsList from '../components/ProductsList'; +import SlidePanel from '../components/SlidePanel'; +import CampaignScript from './CampaignScript'; + export default function CampaignDetail() { const { brandId, campaignId } = useParams(); const dispatch = useDispatch(); @@ -14,6 +17,8 @@ export default function CampaignDetail() { const progressList = ['Find', 'Review', 'Confirmed', 'Draft Ready', 'Published']; const [progressIndex, setProgressIndex] = useState(2); const [activeTab, setActiveTab] = useState('products'); + const [showProductDetail, setShowProductDetail] = useState(false); + const [showAddProductModal, setShowAddProductModal] = useState(false); useEffect(() => { dispatch(fetchBrands()); @@ -25,6 +30,11 @@ export default function CampaignDetail() { } }, [brandId, campaignId]); + const handleShowProductDetail = (productId) => { + dispatch(findProductById({ brandId, campaignId, productId })); + setShowProductDetail(true); + }; + return ( selectedCampaign?.id && (
@@ -39,7 +49,9 @@ export default function CampaignDetail() { Brands - {selectedBrand.name} + + {selectedBrand.name} +
{selectedCampaign.name}
@@ -59,24 +71,30 @@ export default function CampaignDetail() {
{progressList.map((item, index) => index < progressList.length - 1 ? ( - <> -
+ +
{index + 1}
{item}
xx Creators
- +
) : ( -
-
{index + 1}
+
+
{index + 1}
{item}
xx Creators
) )}
-
+
setActiveTab('products')} @@ -102,8 +120,57 @@ export default function CampaignDetail() { Email Draft
- {activeTab === 'products' && } + + {activeTab === 'products' && ( + <> + + + setShowProductDetail(false)} + title='Product Detail' + size='xxl' + > + + + setShowAddProductModal(false)} /> + + )}
) ); } + +function AddProductModal({ show, onHide }) { + return ( + + Add Product + +
+ + Product PID + + + + + + + +
+ + +
+
+
+
+ ); +} diff --git a/src/pages/CampaignScript.jsx b/src/pages/CampaignScript.jsx new file mode 100644 index 0000000..3152304 --- /dev/null +++ b/src/pages/CampaignScript.jsx @@ -0,0 +1,131 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { useEffect, useState } from 'react'; +import { Form } from 'react-bootstrap'; + +export default function CampaignScript() { + const dispatch = useDispatch(); + const selectedProduct = useSelector((state) => state.brands.selectedProduct); + const [activeTab, setActiveTab] = useState('collaborationInfo'); + + useEffect(() => { + console.log(selectedProduct); + }, [selectedProduct]); + + return ( + selectedProduct?.id && ( +
+
+
+
{selectedProduct.name}
+
PID: {selectedProduct.id}
+
+
+
+
+
+
{selectedProduct.commission}
+
Commission Rate
+
+
+
{selectedProduct.availableSamples}
+
Available Samples
+
+
+
{selectedProduct.price}
+
Sales Price
+
+
+
{selectedProduct.stock}
+
Stock
+
+
+
{selectedProduct.sold}
+
Items Sold
+
+
+
{selectedProduct.rating}
+
Product Rating
+
+
+
{selectedProduct.collabCreators}
+
Collab Creators
+
+
+
{selectedProduct.gmv}
+
GMV Achieved
+
+
+
{selectedProduct.reviews}
+
Views Achieved
+
+
+
+
+
+
setActiveTab('collaborationInfo')} + > + Collaboration Info +
+
setActiveTab('campaigns')} + > + Campaigns & Collaborated Creators +
+
+
+
+
Video Posting Requirement
+ + Product Selling Point + + + + Example Videos + + + + Video Posting Suggestion + + + + Video Acceptance Standard + + +
+
+
+ ) + ); +} diff --git a/src/pages/CreatorDiscovery.jsx b/src/pages/CreatorDiscovery.jsx new file mode 100644 index 0000000..b46961f --- /dev/null +++ b/src/pages/CreatorDiscovery.jsx @@ -0,0 +1,62 @@ +import { Send } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { Button, Form } from 'react-bootstrap'; +import '@/styles/CreatorDiscovery.scss'; +import DiscoveryList from '../components/DiscoveryList'; +import { useDispatch } from 'react-redux'; +import { fetchDiscovery } from '../store/slices/discoverySlice'; + +export default function CreatorDiscovery() { + const [search, setSearch] = useState(''); + + const dispatch = useDispatch(); + + useEffect(() => {}, [dispatch]); + + const handleSubmit = (e) => { + e.preventDefault(); + console.log('Form submitted'); + dispatch(fetchDiscovery(search)); + }; + + return ( +
+
+
Creator Discovery
+
Select mode and discover new creators
+
+ + setSearch(e.target.value)} + /> + +
+ + + + + + + + +
+ +
+ +
+ +
+ ); +} diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx new file mode 100644 index 0000000..dfcc874 --- /dev/null +++ b/src/pages/Login.jsx @@ -0,0 +1,36 @@ +import '@/styles/Login.scss'; +import { Button, Form, InputGroup } from 'react-bootstrap'; +import { LockKeyhole, User } from 'lucide-react'; +export default function Login() { + + const handleSubmit = (e) => { + e.preventDefault(); + console.log('Form submitted'); + }; + return ( +
+
Creator Center
+
+ + Username + + + + + + + + + Password + + + + + + + + +
+
+ ); +} diff --git a/src/router/index.jsx b/src/router/index.jsx index 2c47bc8..1b8d8f3 100644 --- a/src/router/index.jsx +++ b/src/router/index.jsx @@ -1,12 +1,14 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom'; -import Home from '../pages/Home'; -import Database from '../pages/Database'; -import MainLayout from '../components/Layouts/MainLayout'; -import Brands from '../pages/Brands'; +import Home from '@/pages/Home'; +import Database from '@/pages/Database'; +import MainLayout from '@/components/Layouts/MainLayout'; +import Brands from '@/pages/Brands'; import CreatorInbox from '@/pages/CreatorInbox'; import DividLayout from '@/components/Layouts/DividLayout'; import BrandsDetail from '@/pages/BrandsDetail'; -import CampaignDetail from '../pages/CampaignDetail'; +import CampaignDetail from '@/pages/CampaignDetail'; +import Login from '@/pages/Login'; +import CreatorDiscovery from '@/pages/CreatorDiscovery'; // Routes configuration object const routes = [ @@ -16,7 +18,7 @@ const routes = [ }, { path: '/creator-discovery', - element: , + element: , }, { path: '/creator-database', @@ -72,6 +74,10 @@ const router = createBrowserRouter([ element: , children: routes, }, + { + path: '/login', + element: , + }, { path: '/creator-inbox', element: , diff --git a/src/store/index.js b/src/store/index.js index f60d6ca..523893d 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -5,11 +5,16 @@ import brandsReducer from './slices/brandsSlice'; import { persistReducer, persistStore } from 'redux-persist'; import sessionStorage from 'redux-persist/es/storage/session'; import inboxReducer from './slices/inboxSlice'; +import authReducer from './slices/authSlice'; +import discoveryReducer from './slices/discoverySlice'; + const reducers = combineReducers({ creators: creatorsReducer, filters: filtersReducer, brands: brandsReducer, inbox: inboxReducer, + auth: authReducer, + discovery: discoveryReducer, }); const persistConfig = { diff --git a/src/store/slices/authSlice.js b/src/store/slices/authSlice.js new file mode 100644 index 0000000..c01a309 --- /dev/null +++ b/src/store/slices/authSlice.js @@ -0,0 +1,20 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const initialState = { + user: null, + token: null, + isAuthenticated: false, +}; + +const authSlice = createSlice({ + name: 'auth', + initialState, + reducers: { + setUser: (state, action) => { + state.user = action.payload; + }, + }, +}); + +export const { setUser } = authSlice.actions; +export default authSlice.reducer; diff --git a/src/store/slices/brandsSlice.js b/src/store/slices/brandsSlice.js index d6a61f5..19f2889 100644 --- a/src/store/slices/brandsSlice.js +++ b/src/store/slices/brandsSlice.js @@ -1,34 +1,5 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; -const mockCampaigns = [ - { - id: 1, - name: 'SUNLINK拍拍灯', - service: '达人短视频-付费', - creators: 10, - creator_type: '带货类达人', - creator_level: ['L2', 'L3'], - category: ['家居', '生活', '数码'], - gmv: '$4k - $10k', - followers: 10000, - views: '500 - 10.5k', - budget: '$10 - $150', - }, - { - id: 2, - name: 'MINISO', - service: '达人短视频-付费', - creators: 10, - creator_type: '带货类达人', - creator_level: ['L2', 'L3'], - category: ['家居', '生活', '数码'], - gmv: '$4k - $10k', - followers: 10000, - views: '500 - 10.5k', - budget: '$10 - $150', - }, -]; - const mockProducts = [ { id: 1, @@ -43,6 +14,7 @@ const mockProducts = [ reviews: 58, collabCreators: 40, tiktokShop: true, + gmv: '$4k - $10k', }, { id: 2, @@ -57,6 +29,7 @@ const mockProducts = [ reviews: 58, collabCreators: 40, tiktokShop: true, + gmv: '$4k - $10k', }, { id: 3, @@ -71,6 +44,38 @@ const mockProducts = [ reviews: 58, collabCreators: 40, tiktokShop: true, + gmv: '$4k - $10k', + }, +]; + +const mockCampaigns = [ + { + id: 1, + name: 'SUNLINK拍拍灯', + service: '达人短视频-付费', + creators: 10, + creator_type: '带货类达人', + creator_level: ['L2', 'L3'], + category: ['家居', '生活', '数码'], + gmv: '$4k - $10k', + followers: 10000, + views: '500 - 10.5k', + budget: '$10 - $150', + products: mockProducts, + }, + { + id: 2, + name: 'MINISO', + service: '达人短视频-付费', + creators: 10, + creator_type: '带货类达人', + creator_level: ['L2', 'L3'], + category: ['家居', '生活', '数码'], + gmv: '$4k - $10k', + followers: 10000, + views: '500 - 10.5k', + budget: '$10 - $150', + products: mockProducts, }, ]; @@ -115,6 +120,7 @@ const initialState = { error: null, selectedBrand: {}, selectedCampaign: {}, + selectedProduct: {}, }; const brandsSlice = createSlice({ @@ -129,13 +135,19 @@ const brandsSlice = createSlice({ }, findCampaignById: (state, action) => { const { brandId, campaignId } = action.payload; - console.log(brandId, campaignId); - console.log(state.brands); const brand = state.brands?.find((b) => b.id.toString() === brandId); state.selectedBrand = brand; state.selectedCampaign = brand?.campaigns?.find((c) => c.id.toString() === campaignId) || {}; }, + findProductById: (state, action) => { + const { brandId, campaignId, productId } = action.payload; + const brand = state.brands?.find((b) => b.id.toString() === brandId); + const campaign = brand?.campaigns?.find((c) => c.id.toString() === campaignId); + const product = campaign?.products?.find((p) => p.id.toString() === productId.toString()); + console.log(brand, campaign, product); + state.selectedProduct = product; + }, }, extraReducers: (builder) => { builder @@ -153,6 +165,6 @@ const brandsSlice = createSlice({ }, }); -export const { selectBrand, findBrandById, findCampaignById } = brandsSlice.actions; +export const { selectBrand, findBrandById, findCampaignById, findProductById } = brandsSlice.actions; export default brandsSlice.reducer; diff --git a/src/store/slices/discoverySlice.js b/src/store/slices/discoverySlice.js new file mode 100644 index 0000000..9258733 --- /dev/null +++ b/src/store/slices/discoverySlice.js @@ -0,0 +1,58 @@ +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; + +const mockCreators = [ + { + id: 1, + sessions: 1, + creator: 12, + shoppableCreators: 12, + avgFollowers: 12, + avgGMV: 12, + avgVideoViews: 12, + date: '2021-01-01', + }, + { + id: 2, + sessions: 2, + creator: 12, + shoppableCreators: 12, + avgFollowers: 12, + avgGMV: 12, + avgVideoViews: 12, + date: '2021-01-01', + }, +]; +export const fetchDiscovery = createAsyncThunk('discovery/fetchDiscovery', async (search) => { + // const response = await fetch('/api/discovery'); + // return response.json(); + return mockCreators; +}); +const initialState = { + creators: [], + status: 'idle', + error: null, +}; + +const discoverySlice = createSlice({ + name: 'discovery', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(fetchDiscovery.pending, (state) => { + state.status = 'loading'; + }) + .addCase(fetchDiscovery.fulfilled, (state, action) => { + state.status = 'succeeded'; + state.creators = action.payload; + }) + .addCase(fetchDiscovery.rejected, (state, action) => { + state.status = 'failed'; + state.error = action.error.message; + }); + }, +}); + +export const {} = discoverySlice.actions; + +export default discoverySlice.reducer; diff --git a/src/styles/Brands.scss b/src/styles/Brands.scss index 6fe5db3..ae5d34e 100644 --- a/src/styles/Brands.scss +++ b/src/styles/Brands.scss @@ -1,4 +1,6 @@ -@import './custom-theme.scss'; +// @import './custom-theme.scss'; +@import './variables'; + .brands-list { display: flex; flex-direction: row; @@ -21,7 +23,7 @@ width: 2.25rem; height: 2.25rem; border-radius: 0.5rem; - background-color: $indigo-500; + background-color: $primary-500; color: white; line-height: 2.25rem; text-align: center; @@ -59,6 +61,15 @@ } } +.add-product-form { + .button-group { + display: flex; + flex-flow: row nowrap; + gap: 0.5rem; + justify-content: flex-end; + } +} + .brand-detail-info { display: flex; flex-flow: row wrap; @@ -137,7 +148,6 @@ .tab-switches { width: 100%; display: flex; - margin: 1rem 0; flex-flow: row nowrap; justify-content: space-between; align-items: center; @@ -174,6 +184,13 @@ } } +.add-product-btn { + display: inline-flex !important; + flex-flow: row nowrap; + align-items: center; + gap: 0.25rem; +} + .campaigns-list { display: flex; gap: 0.875rem; diff --git a/src/styles/Campaign.scss b/src/styles/Campaign.scss new file mode 100644 index 0000000..53430d7 --- /dev/null +++ b/src/styles/Campaign.scss @@ -0,0 +1,86 @@ +// @import '@/styles/custom-theme.scss'; +// 导入变量 +@import './variables'; + +.product-script { + display: flex; + flex-flow: column nowrap; + gap: 1rem; + + .product-details { + background-color: #fff; + padding: 1.5rem; + border-radius: 0.5rem; + + .product-details-header { + display: flex; + flex-flow: column nowrap; + align-items: flex-start; + .product-details-header-title { + font-size: 1rem; + color: $primary; + font-weight: 800; + } + .product-details-header-pid { + font-size: 0.75rem; + color: $neutral-600; + } + } + .product-details-body { + display: flex; + flex-flow: row wrap; + justify-content: space-between; + + .product-img { + flex-shrink: 0; + width: 16rem; + padding-right: 1rem; + height: 15rem; + background-color: $neutral-200; + border-radius: 0.5rem; + } + .product-detail-list { + display: grid; + grid-template-columns: repeat(3, 1fr); + justify-content: space-between; + gap: 0.5rem; + + .product-detail-item { + display: flex; + flex-flow: column nowrap; + align-items: center; + background-color: $neutral-150; + border-radius: 0.375rem; + padding: 0.75rem 0; + width: 10rem; + + .product-detail-item-value { + font-size: 1rem; + font-weight: 600; + } + .product-detail-item-label { + font-size: 0.875rem; + color: $neutral-600; + } + } + } + } + } + .product-script-video-req { + .video-req-form { + display: flex; + flex-flow: column nowrap; + gap: 0.5rem; + + .form-header { + font-size: 1.25rem; + font-weight: 700; + } + .form-label { + color: $neutral-700; + font-weight: 700; + margin: 0; + } + } + } +} diff --git a/src/styles/CreatorDiscovery.scss b/src/styles/CreatorDiscovery.scss new file mode 100644 index 0000000..9395b51 --- /dev/null +++ b/src/styles/CreatorDiscovery.scss @@ -0,0 +1,53 @@ +// 不再需要导入custom-theme.scss,因为已经在index.scss中统一导入了 +// @import 'custom-theme.scss'; + +// 导入变量 +@import './variables'; +.creator-discovery-page { + display: flex; + flex-flow: column nowrap; + gap: 1rem; + + .top-search { + display: flex; + flex-flow: column nowrap; + gap: 0.5rem; + align-items: center; + width: 100%; + + .title { + font-size: 2rem; + font-weight: 700; + color: $primary; + } + .description { + font-size: 0.875rem; + color: $neutral-900; + } + + .discovery-form { + position: relative; + background-color: #fff; + border-radius: 0.5rem; + padding: 1rem; + display: flex; + flex-flow: column nowrap; + gap: 0.5rem; + width: 40%; + min-width: 420px; + .btn-tag-group { + display: flex; + flex-flow: row nowrap; + gap: 0.5rem; + } + .submit-btn { + position: absolute; + right: 1rem; + bottom: 1rem; + } + } + } + table { + border: 1px solid #171a1f12; + } +} diff --git a/src/styles/DatabaseFilter.scss b/src/styles/DatabaseFilter.scss index f0395e5..d78e2af 100644 --- a/src/styles/DatabaseFilter.scss +++ b/src/styles/DatabaseFilter.scss @@ -1,4 +1,7 @@ -@import './custom-theme.scss'; +// @import './custom-theme.scss'; + +// 导入变量 +@import './variables'; .filter-card { border-radius: 12px; diff --git a/src/styles/DatabaseList.scss b/src/styles/DatabaseList.scss index e620a01..348509c 100644 --- a/src/styles/DatabaseList.scss +++ b/src/styles/DatabaseList.scss @@ -1,4 +1,7 @@ -@import './custom-theme.scss'; +// @import './custom-theme.scss'; + +// 导入变量 +@import './variables'; .creator-database-table { .creator-cell { diff --git a/src/styles/Inbox.scss b/src/styles/Inbox.scss index 12bda28..b4febf7 100644 --- a/src/styles/Inbox.scss +++ b/src/styles/Inbox.scss @@ -1,4 +1,7 @@ -@import './custom-theme.scss'; +// @import './custom-theme.scss'; + +// 导入变量 +@import './variables'; .inbox-list-container { background-color: #fff; @@ -218,7 +221,7 @@ } .message.user { - background-color: $violet-150; + background-color: $primary-150; align-self: flex-end; margin-left: auto; } diff --git a/src/styles/Login.scss b/src/styles/Login.scss new file mode 100644 index 0000000..52e7d28 --- /dev/null +++ b/src/styles/Login.scss @@ -0,0 +1,30 @@ +// @import '@/styles/custom-theme.scss'; + +// 导入变量 +@import './variables'; + +.login-container { + width: 100vw; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + flex-flow: column nowrap; + + .title { + font-size: 2rem; + font-weight: 700; + margin-bottom: 2rem; + color: $primary; + } + .login-form { + background-color: #fff; + border-radius: 0.5rem; + padding: 2rem; + display: flex; + flex-flow: column nowrap; + gap: 1rem; + width: 25%; + min-width: 20rem; + } +} diff --git a/src/styles/Products.scss b/src/styles/Products.scss index 3c45d60..3cf8811 100644 --- a/src/styles/Products.scss +++ b/src/styles/Products.scss @@ -1,3 +1,6 @@ +// 导入变量 +@import './variables'; + .products-list { width: 100%; @@ -11,6 +14,6 @@ background-color: #7f55e0; color: white; font-size: 0.875rem; - margin-right: .25rem; + margin-right: .25rem; } } diff --git a/src/styles/RangeSlider.scss b/src/styles/RangeSlider.scss index 59ef848..5bdf45b 100644 --- a/src/styles/RangeSlider.scss +++ b/src/styles/RangeSlider.scss @@ -1,6 +1,8 @@ -@import './custom-theme.scss'; +// 导入变量 +@import './variables'; -$primary: #6366f1; // 使用与主题一致的颜色 +// 不再需要导入custom-theme.scss,因为所有变量都在index.scss中定义了 +// @import './custom-theme.scss'; .range-slider { width: 70%; @@ -42,7 +44,7 @@ $primary: #6366f1; // 使用与主题一致的颜色 position: absolute; height: 5px; border-radius: 3px; - background-color: #6366f1; + background-color: $indigo-500; } &__steps { @@ -63,8 +65,8 @@ $primary: #6366f1; // 使用与主题一致的颜色 transition: all 0.2s ease; &.active { - background-color: #6366f1; - border-color: #6366f1; + background-color: $indigo-500; + border-color: $indigo-500; transform: scale(1.2); } } @@ -81,7 +83,7 @@ $primary: #6366f1; // 使用与主题一致的颜色 transform: translateX(-50%); font-size: 0.75rem; color: white; - background-color: #6366f1; + background-color: $indigo-500; padding: 2px 6px; border-radius: 10px; white-space: nowrap; @@ -98,7 +100,7 @@ $primary: #6366f1; // 使用与主题一致的颜色 height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; - border-bottom: 5px solid #6366f1; + border-bottom: 5px solid $indigo-500; } } } @@ -121,13 +123,13 @@ $primary: #6366f1; // 使用与主题一致的颜色 height: 20px; border-radius: 50%; background-color: white; - border: 2px solid #6366f1; + border: 2px solid $indigo-500; cursor: pointer; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); transition: all 0.2s ease; &:hover { - background-color: #6366f1; + background-color: $indigo-500; transform: scale(1.1); } @@ -144,13 +146,13 @@ $primary: #6366f1; // 使用与主题一致的颜色 height: 20px; border-radius: 50%; background-color: white; - border: 2px solid #6366f1; + border: 2px solid $indigo-500; cursor: pointer; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); transition: all 0.2s ease; &:hover { - background-color: #6366f1; + background-color: $indigo-500; transform: scale(1.1); } @@ -188,7 +190,7 @@ $primary: #6366f1; // 使用与主题一致的颜色 /* For Chrome browsers */ .thumb::-webkit-slider-thumb { background-color: #fff; - border: 2px solid $primary; + border: 2px solid $indigo-500; border-radius: 50%; box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); cursor: pointer; @@ -202,7 +204,7 @@ $primary: #6366f1; // 使用与主题一致的颜色 /* For Firefox browsers */ .thumb::-moz-range-thumb { background-color: #fff; - border: 2px solid $primary; + border: 2px solid $indigo-500; border-radius: 50%; box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); cursor: pointer; diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss new file mode 100644 index 0000000..ed4733c --- /dev/null +++ b/src/styles/_variables.scss @@ -0,0 +1,41 @@ +// 主题颜色变量 +$primary: #636AE8FF; // 靛蓝色 +$secondary: #6c757d; // 灰色 +$success: #198754; // 绿色 +$info: #0dcaf0; // 浅蓝色 +$warning: #ffc107; // 黄色 +$danger: #dc3545; // 红色 +$light: #f8f9fa; // 浅色 +$dark: #212529; // 深色 + +// 自定义颜色变量 +$primary-100: #F2F2FDFF; +$primary-150: #E0E1FAFF; +$primary-500: #636AE8FF; +$indigo-50: #eef2ff; +$indigo-100: #e0e7ff; +$indigo-500: #6366f1; +$violet-50: #f5f3ff; +$violet-100: #ede9fe; +$violet-400: #a78bfa; +$neutral-150: #f8f9faff; +$neutral-200: #f3f4f6ff; +$neutral-350: #cfd2daff; +$neutral-600: #565e6cff; +$neutral-700: #323842ff; +$neutral-900: #171a1fff; +$zinc-600: #52525b; + +// Gray 系列变量 +$gray-600: #6c757d; +$gray-700: #495057; +$gray-800: #343a40; + +// 字体 +$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); \ No newline at end of file diff --git a/src/styles/custom-theme.scss b/src/styles/custom-theme.scss index eb7416c..997c774 100644 --- a/src/styles/custom-theme.scss +++ b/src/styles/custom-theme.scss @@ -1,41 +1,10 @@ -// 在引入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; -$violet-150: #e0e1faff; -$violet-400: #a78bfa; -$neutral-150: #f8f9faff; -$neutral-200: #f3f4f6ff; -$neutral-350: #cfd2daff; -$neutral-600: #565e6cff; -$neutral-700: #323842ff; -$neutral-900: #171a1fff; -$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); +// 导入变量 +@import './variables'; // 导入Bootstrap @import 'bootstrap/scss/bootstrap'; +// 自定义Bootstrap组件样式 :root { --bs-breadcrumb-font-size: 1.5rem; --bs-body-color: #171a1fff; @@ -53,14 +22,20 @@ $box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); } .btn-primary-subtle { - background-color: $violet-150; - color: $indigo-500; + background-color: $primary-100; + color: $primary; &:hover { - background-color: $primary !important; - color: white; + background-color: $primary-150 !important; + color: $primary !important; } } +.btn-check:checked + .btn-primary-subtle { + background-color: $primary-150 !important; + color: $primary !important; + border-color: $primary-500 !important; +} + #root { font-weight: 500; background-color: #f5f3ff; @@ -131,3 +106,11 @@ a { } } } + +.transparent-input { + border-color: transparent !important; +} +.transparent-input:focus { + border-color: transparent !important; + box-shadow: none !important; +} diff --git a/src/styles/global.scss b/src/styles/global.scss index b084f96..166e5b4 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -1,4 +1,8 @@ -@import 'custom-theme.scss'; +// 不再需要导入custom-theme.scss,因为已经在index.scss中统一导入了 +// @import 'custom-theme.scss'; + +// 导入变量 +@import './variables'; .breadcrumb { font-weight: 700; diff --git a/src/styles/index.scss b/src/styles/index.scss new file mode 100644 index 0000000..e6850f5 --- /dev/null +++ b/src/styles/index.scss @@ -0,0 +1,5 @@ +// 导入变量和Bootstrap +@import './custom-theme.scss'; + +// 导入全局样式 +@import './global.scss'; \ No newline at end of file diff --git a/src/styles/sidebar.scss b/src/styles/sidebar.scss index f65a834..2062263 100644 --- a/src/styles/sidebar.scss +++ b/src/styles/sidebar.scss @@ -1,4 +1,7 @@ -@import 'custom-theme.scss'; +// @import 'custom-theme.scss'; + +// 导入变量 +@import './variables'; .sidebar { width: 220px; @@ -11,7 +14,7 @@ z-index: 1000; transition: all 0.3s ease; overflow-y: auto; - background: $violet-50; + background: $primary-100; // Collapsed sidebar style &.sidebar-collapsed { diff --git a/vite.config.js b/vite.config.js index e9091eb..531f180 100644 --- a/vite.config.js +++ b/vite.config.js @@ -10,4 +10,17 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), }, }, + css: { + preprocessorOptions: { + scss: { + quietDeps: true, + outputStyle: 'compressed', + }, + }, + devSourcemap: false, + }, + build: { + cssCodeSplit: false, + minify: true, + }, });