mirror of
https://github.com/Funkoala14/CreatorCenter_OOIN.git
synced 2025-06-07 22:58:14 +08:00
[dev]campaign detail
This commit is contained in:
parent
7b0d0a109e
commit
9999e334fb
5
.sassrc.js
Normal file
5
.sassrc.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
quietDeps: true,
|
||||
outputStyle: 'compressed',
|
||||
sourceMap: false,
|
||||
};
|
215
DEVELOPMENT.md
Normal file
215
DEVELOPMENT.md
Normal file
@ -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
|
@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
60
package-lock.json
generated
60
package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
@ -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);
|
||||
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
Before Width: | Height: | Size: 4.0 KiB |
@ -28,7 +28,7 @@ export default function CampaignInfo() {
|
||||
</div>
|
||||
<div className='campaign-info-item-value'>
|
||||
{selectedCampaign?.category?.length > 0 &&
|
||||
selectedCampaign.category.map((cat) => <span className='category-tag'>{cat}</span>)}
|
||||
selectedCampaign.category.map((cat,index) => <span className='category-tag' key={index}>{cat}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='campaign-info-item'>
|
||||
@ -66,8 +66,8 @@ export default function CampaignInfo() {
|
||||
</div>
|
||||
<div className='campaign-info-item-value'>
|
||||
{selectedCampaign?.creator_level?.length > 0 &&
|
||||
selectedCampaign.creator_level.map((level) => (
|
||||
<span className='creator-level-tag'>{level}</span>
|
||||
selectedCampaign.creator_level.map((level,index) => (
|
||||
<span className='creator-level-tag' key={index}>{level}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -152,7 +152,7 @@ export default function DatabaseFilter({ path }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='shadow-sm mb-4 filter-card'>
|
||||
<div className='shadow-xs mb-4 filter-card'>
|
||||
<Card.Body>
|
||||
<h3 className='mb-4'>Filter</h3>
|
||||
|
||||
|
@ -86,7 +86,7 @@ export default function DatabaseList({ path }) {
|
||||
|
||||
return (
|
||||
<div className='creator-database-table'>
|
||||
<Table responsive hover className='bg-white shadow-sm rounded overflow-hidden'>
|
||||
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className='selector' style={{ width: '40px' }}>
|
||||
|
40
src/components/DiscoveryList.jsx
Normal file
40
src/components/DiscoveryList.jsx
Normal file
@ -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 (
|
||||
<div className='discovery-list'>
|
||||
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden border-1'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className='text-center'>Sessions</th>
|
||||
<th className='text-center'>#Creator</th>
|
||||
<th className='text-center'>#Shoppable Creators</th>
|
||||
<th className='text-center'>Avg.Followers</th>
|
||||
<th className='text-center'>Avg.GMV</th>
|
||||
<th className='text-center'>Avg.Video Views</th>
|
||||
<th className='text-center'>Date</th>
|
||||
<th className='text-center'>View Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{creators?.length > 0 && creators.map((creator) => (
|
||||
<tr key={creator.id}>
|
||||
<td className='text-center'>{creator.sessions}</td>
|
||||
<td className='text-center'>{creator.creator}</td>
|
||||
<td className='text-center'>{creator.shoppableCreators}</td>
|
||||
<td className='text-center'>{creator.avgFollowers}</td>
|
||||
<td className='text-center'>{creator.avgGMV}</td>
|
||||
<td className='text-center'>{creator.avgVideoViews}</td>
|
||||
<td className='text-center'>{creator.date}</td>
|
||||
<td className='text-center'><Link to={`/creator/${creator.id}`}>View</Link></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<div className='products-list'>
|
||||
<Table responsive hover className='bg-white shadow-xs rounded overflow-hidden'>
|
||||
<div className='products-list rounded shadow-xs'>
|
||||
<Table responsive hover className='bg-white rounded overflow-hidden m-0'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className='selector' style={{ width: '40px' }}>
|
||||
@ -111,7 +111,7 @@ export default function ProductsList() {
|
||||
/>
|
||||
</td>
|
||||
<td className='product-cell'>
|
||||
<div className='d-flex align-items-center'>
|
||||
<div className='d-flex align-items-center' onClick={() => onShowProductDetail(product.id)} style={{cursor: 'pointer'}}>
|
||||
<div className='product-logo'>{product.name.slice(0, 1)}</div>
|
||||
<div className='product-name'>{product.name}</div>
|
||||
</div>
|
||||
|
84
src/components/SlidePanel/SlidePanel.jsx
Normal file
84
src/components/SlidePanel/SlidePanel.jsx
Normal file
@ -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 (
|
||||
<div className={`slide-panel-container ${show ? 'show' : ''}`}>
|
||||
{backdrop && <div className={`slide-panel-backdrop ${show ? 'show' : ''}`} onClick={handleBackdropClick} />}
|
||||
|
||||
<div
|
||||
className={`slide-panel ${show ? 'show' : ''} slide-panel-${placement} slide-panel-${size}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className='slide-panel-header'>
|
||||
{title && <div className='slide-panel-title'>{title}</div>}
|
||||
{closeButton && (
|
||||
<Button variant='link' className='slide-panel-close' onClick={onClose} aria-label='Close'>
|
||||
<X size={20} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className='slide-panel-body'>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
145
src/components/SlidePanel/SlidePanel.scss
Normal file
145
src/components/SlidePanel/SlidePanel.scss
Normal file
@ -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;
|
||||
}
|
99
src/components/SlidePanel/SlidePanelExample.jsx
Normal file
99
src/components/SlidePanel/SlidePanelExample.jsx
Normal file
@ -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 = (
|
||||
<div>
|
||||
<p>This is an example of the SlidePanel component. You can place any content here.</p>
|
||||
<Form>
|
||||
<Form.Group className='mb-3'>
|
||||
<Form.Label>Example input</Form.Label>
|
||||
<Form.Control type='text' placeholder='Enter text' />
|
||||
</Form.Group>
|
||||
<Form.Group className='mb-3'>
|
||||
<Form.Label>Example select</Form.Label>
|
||||
<Form.Select>
|
||||
<option>Option 1</option>
|
||||
<option>Option 2</option>
|
||||
<option>Option 3</option>
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
<Button variant='primary'>Submit</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mb-3'>
|
||||
<h5>Panel Position</h5>
|
||||
<div className='d-flex gap-2'>
|
||||
<Button
|
||||
variant={placement === 'start' ? 'primary' : 'outline-primary'}
|
||||
onClick={() => setPlacement('start')}
|
||||
>
|
||||
Left
|
||||
</Button>
|
||||
<Button
|
||||
variant={placement === 'end' ? 'primary' : 'outline-primary'}
|
||||
onClick={() => setPlacement('end')}
|
||||
>
|
||||
Right
|
||||
</Button>
|
||||
<Button
|
||||
variant={placement === 'top' ? 'primary' : 'outline-primary'}
|
||||
onClick={() => setPlacement('top')}
|
||||
>
|
||||
Top
|
||||
</Button>
|
||||
<Button
|
||||
variant={placement === 'bottom' ? 'primary' : 'outline-primary'}
|
||||
onClick={() => setPlacement('bottom')}
|
||||
>
|
||||
Bottom
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<h5>Panel Size</h5>
|
||||
<div className='d-flex gap-2'>
|
||||
<Button variant={size === 'sm' ? 'primary' : 'outline-primary'} onClick={() => setSize('sm')}>
|
||||
Small
|
||||
</Button>
|
||||
<Button variant={size === 'md' ? 'primary' : 'outline-primary'} onClick={() => setSize('md')}>
|
||||
Medium
|
||||
</Button>
|
||||
<Button variant={size === 'lg' ? 'primary' : 'outline-primary'} onClick={() => setSize('lg')}>
|
||||
Large
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button variant='success' onClick={handleOpen}>
|
||||
Open SlidePanel
|
||||
</Button>
|
||||
|
||||
<SlidePanel
|
||||
show={showPanel}
|
||||
onClose={handleClose}
|
||||
title='SlidePanel Example'
|
||||
placement={placement}
|
||||
size={size}
|
||||
>
|
||||
{exampleContent}
|
||||
</SlidePanel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SlidePanelExample;
|
3
src/components/SlidePanel/index.js
Normal file
3
src/components/SlidePanel/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import SlidePanel from './SlidePanel';
|
||||
|
||||
export default SlidePanel;
|
@ -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';
|
||||
|
||||
|
@ -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 && (
|
||||
<div className='campaign-detail'>
|
||||
@ -39,7 +49,9 @@ export default function CampaignDetail() {
|
||||
<Link to={'/brands'} className='breadcrumb-item'>
|
||||
Brands
|
||||
</Link>
|
||||
<Link to={`/brands/${brandId}`} className='breadcrumb-item'>{selectedBrand.name}</Link>
|
||||
<Link to={`/brands/${brandId}`} className='breadcrumb-item'>
|
||||
{selectedBrand.name}
|
||||
</Link>
|
||||
<div className='breadcrumb-item'>{selectedCampaign.name}</div>
|
||||
</div>
|
||||
<CampaignInfo />
|
||||
@ -59,24 +71,30 @@ export default function CampaignDetail() {
|
||||
<div className='campaign-progress shadow-xs'>
|
||||
{progressList.map((item, index) =>
|
||||
index < progressList.length - 1 ? (
|
||||
<>
|
||||
<div className={`campaign-progress-item ${progressIndex === index ? 'active' : ''}`} key={index}>
|
||||
<React.Fragment key={index}>
|
||||
<div
|
||||
className={`campaign-progress-item ${progressIndex === index ? 'active' : ''}`}
|
||||
key={index}
|
||||
>
|
||||
<div className='campaign-progress-item-index'>{index + 1}</div>
|
||||
<div className='campaign-progress-item-label'>{item}</div>
|
||||
<div className='campaign-progress-item-desc'>xx Creators</div>
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<div className={`campaign-progress-item ${progressIndex === index ? 'active' : ''}`} key={index}>
|
||||
<div className='campaign-progress-item-index'>{index + 1}</div>
|
||||
<div
|
||||
className={`campaign-progress-item ${progressIndex === index ? 'active' : ''}`}
|
||||
key={index}
|
||||
>
|
||||
<div className='campaign-progress-item-index'>{index + 1}</div>
|
||||
<div className='campaign-progress-item-label'>{item}</div>
|
||||
<div className='campaign-progress-item-desc'>xx Creators</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className='campaign-tab-switches tab-switches'>
|
||||
<div className='campaign-tab-switches tab-switches'>
|
||||
<div
|
||||
className={`tab-switch-item ${activeTab === 'products' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('products')}
|
||||
@ -102,8 +120,57 @@ export default function CampaignDetail() {
|
||||
Email Draft
|
||||
</div>
|
||||
</div>
|
||||
{activeTab === 'products' && <ProductsList />}
|
||||
|
||||
{activeTab === 'products' && (
|
||||
<>
|
||||
<Button
|
||||
className='add-product-btn'
|
||||
variant='outline-primary'
|
||||
onClick={() => setShowAddProductModal(true)}
|
||||
>
|
||||
<Plus size={18} />
|
||||
Add Product
|
||||
</Button>
|
||||
<ProductsList onShowProductDetail={handleShowProductDetail} />
|
||||
<SlidePanel
|
||||
show={showProductDetail}
|
||||
onClose={() => setShowProductDetail(false)}
|
||||
title='Product Detail'
|
||||
size='xxl'
|
||||
>
|
||||
<CampaignScript />
|
||||
</SlidePanel>
|
||||
<AddProductModal show={showAddProductModal} onHide={() => setShowAddProductModal(false)} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AddProductModal({ show, onHide }) {
|
||||
return (
|
||||
<Modal show={show} onHide={onHide}>
|
||||
<Modal.Header closeButton className='fw-bold'>Add Product</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form className='add-product-form'>
|
||||
<Form.Group className='mb-3'>
|
||||
<Form.Label>Product PID</Form.Label>
|
||||
<Form.Select aria-label='Default select example'>
|
||||
<option>Select</option>
|
||||
<option value='1'>One</option>
|
||||
<option value='2'>Two</option>
|
||||
<option value='3'>Three</option>
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
<div className='button-group'>
|
||||
<Button variant='outline-light' className='text-primary'>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='primary'>Create</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
131
src/pages/CampaignScript.jsx
Normal file
131
src/pages/CampaignScript.jsx
Normal file
@ -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 && (
|
||||
<div className='product-script'>
|
||||
<div className='product-details shadow-xs'>
|
||||
<div className='product-details-header'>
|
||||
<div className='product-details-header-title'>{selectedProduct.name}</div>
|
||||
<div className='product-details-header-pid'>PID: {selectedProduct.id}</div>
|
||||
</div>
|
||||
<div className='product-details-body'>
|
||||
<div className='product-img'></div>
|
||||
<div className='product-detail-list'>
|
||||
<div className='product-detail-item'>
|
||||
<div className='product-detail-item-value'>{selectedProduct.commission}</div>
|
||||
<div className='product-detail-item-label'>Commission Rate</div>
|
||||
</div>
|
||||
<div className='product-detail-item'>
|
||||
<div className='product-detail-item-value'>{selectedProduct.availableSamples}</div>
|
||||
<div className='product-detail-item-label'>Available Samples</div>
|
||||
</div>
|
||||
<div className='product-detail-item'>
|
||||
<div className='product-detail-item-value'>{selectedProduct.price}</div>
|
||||
<div className='product-detail-item-label'>Sales Price</div>
|
||||
</div>
|
||||
<div className='product-detail-item'>
|
||||
<div className='product-detail-item-value'>{selectedProduct.stock}</div>
|
||||
<div className='product-detail-item-label'>Stock</div>
|
||||
</div>
|
||||
<div className='product-detail-item'>
|
||||
<div className='product-detail-item-value'>{selectedProduct.sold}</div>
|
||||
<div className='product-detail-item-label'>Items Sold</div>
|
||||
</div>
|
||||
<div className='product-detail-item'>
|
||||
<div className='product-detail-item-value'>{selectedProduct.rating}</div>
|
||||
<div className='product-detail-item-label'>Product Rating</div>
|
||||
</div>
|
||||
<div className='product-detail-item'>
|
||||
<div className='product-detail-item-value'>{selectedProduct.collabCreators}</div>
|
||||
<div className='product-detail-item-label'>Collab Creators</div>
|
||||
</div>
|
||||
<div className='product-detail-item'>
|
||||
<div className='product-detail-item-value'>{selectedProduct.gmv}</div>
|
||||
<div className='product-detail-item-label'>GMV Achieved</div>
|
||||
</div>
|
||||
<div className='product-detail-item'>
|
||||
<div className='product-detail-item-value'>{selectedProduct.reviews}</div>
|
||||
<div className='product-detail-item-label'>Views Achieved</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='product-script-switches tab-switches'>
|
||||
<div
|
||||
className={`tab-switch-item ${activeTab === 'collaborationInfo' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('collaborationInfo')}
|
||||
>
|
||||
Collaboration Info
|
||||
</div>
|
||||
<div
|
||||
className={`tab-switch-item ${activeTab === 'campaigns' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('campaigns')}
|
||||
>
|
||||
Campaigns & Collaborated Creators
|
||||
</div>
|
||||
</div>
|
||||
<div className='product-script-video-req'>
|
||||
<Form className='video-req-form'>
|
||||
<div className='form-header'>Video Posting Requirement</div>
|
||||
<Form.Group>
|
||||
<Form.Label>Product Selling Point</Form.Label>
|
||||
<Form.Control
|
||||
type='textarea'
|
||||
as='textarea'
|
||||
rows={3}
|
||||
placeholder='吸力强、有效除菌、香薰片功能、价格更优惠、两种颜色'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Example Videos</Form.Label>
|
||||
<Form.Control
|
||||
type='textarea'
|
||||
as='textarea'
|
||||
rows={3}
|
||||
placeholder='https://mcnmza4kafoj.feishu.cn/drive/folder/HzhWfGvWtlCBqIduwfLczQ9Cnyb'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Video Posting Suggestion</Form.Label>
|
||||
<Form.Control
|
||||
type='textarea'
|
||||
as='textarea'
|
||||
rows={3}
|
||||
placeholder='开头引人+产品亮点+CTA呼吁/时长30~50s左右'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Video Acceptance Standard</Form.Label>
|
||||
<Form.Control
|
||||
type='textarea'
|
||||
as='textarea'
|
||||
rows={3}
|
||||
placeholder='1.场景:包括不限于厨房、卫生间、衣柜;在橱柜下方和洗手台下方安装橱柜灯、衣柜等安全及方便需求。场景展示要有黑暗环境对比HOOK。
|
||||
2.红人必须要表达卖点(以下卖点按优先级排列,达人任意选择提到至少三个卖点即可)
|
||||
电池大续航能力强可以(必须提到)(可用字幕展示)
|
||||
容易安装(有磁铁)(必须提到)(要拍出安装过程)
|
||||
开启人体感应模式 (必须提到)(可视觉展示)
|
||||
可充电
|
||||
亮度高,且可调节
|
||||
加分卖点:
|
||||
视频需说明购买理由以及使用感受(例如:老人家晚上更安全,宝妈晚上起床更便捷安全等等购买理由)
|
||||
3.正确的hashtags 以及正确@官方账号'
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
62
src/pages/CreatorDiscovery.jsx
Normal file
62
src/pages/CreatorDiscovery.jsx
Normal file
@ -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 (
|
||||
<div className='creator-discovery-page'>
|
||||
<div className='top-search'>
|
||||
<div className='title'>Creator Discovery</div>
|
||||
<div className='description'>Select mode and discover new creators</div>
|
||||
<Form className='discovery-form' onSubmit={handleSubmit}>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
className='transparent-input'
|
||||
type='text'
|
||||
placeholder='Type a message'
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<div className='btn-tag-group' role='group' aria-label='Tag selection button group'>
|
||||
<input type='checkbox' className='btn-check' id='btncheck1' autocomplete='off' />
|
||||
<label className='rounded-pill btn btn-primary-subtle' for='btncheck1'>
|
||||
#Hashtag
|
||||
</label>
|
||||
|
||||
<input type='checkbox' className='btn-check' id='btncheck2' autocomplete='off' />
|
||||
<label className='rounded-pill btn btn-primary-subtle' for='btncheck2'>
|
||||
Trend
|
||||
</label>
|
||||
|
||||
<input type='checkbox' className='btn-check' id='btncheck3' autocomplete='off' />
|
||||
<label className='rounded-pill btn btn-primary-subtle' for='btncheck3'>
|
||||
Indivisual
|
||||
</label>
|
||||
</div>
|
||||
<Button className='rounded-pill submit-btn' type='submit'>
|
||||
<Send />
|
||||
</Button>
|
||||
</Form>
|
||||
<Button variant='outline-primary'>Upload from External Source</Button>
|
||||
</div>
|
||||
<DiscoveryList />
|
||||
</div>
|
||||
);
|
||||
}
|
36
src/pages/Login.jsx
Normal file
36
src/pages/Login.jsx
Normal file
@ -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 (
|
||||
<div className='login-container'>
|
||||
<div className='title'>Creator Center</div>
|
||||
<Form className='login-form' onSubmit={handleSubmit}>
|
||||
<Form.Group>
|
||||
<Form.Label>Username</Form.Label>
|
||||
<InputGroup>
|
||||
<InputGroup.Text>
|
||||
<User />
|
||||
</InputGroup.Text>
|
||||
<Form.Control type='text' placeholder='Enter username' />
|
||||
</InputGroup>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>Password</Form.Label>
|
||||
<InputGroup>
|
||||
<InputGroup.Text>
|
||||
<LockKeyhole />
|
||||
</InputGroup.Text>
|
||||
<Form.Control type='password' placeholder='Enter password' />
|
||||
</InputGroup>
|
||||
</Form.Group>
|
||||
<Button type='submit'>Sign In</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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: <Home />,
|
||||
element: <CreatorDiscovery />,
|
||||
},
|
||||
{
|
||||
path: '/creator-database',
|
||||
@ -72,6 +74,10 @@ const router = createBrowserRouter([
|
||||
element: <MainLayout />,
|
||||
children: routes,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
element: <Login />,
|
||||
},
|
||||
{
|
||||
path: '/creator-inbox',
|
||||
element: <DividLayout />,
|
||||
|
@ -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 = {
|
||||
|
20
src/store/slices/authSlice.js
Normal file
20
src/store/slices/authSlice.js
Normal file
@ -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;
|
@ -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;
|
||||
|
58
src/store/slices/discoverySlice.js
Normal file
58
src/store/slices/discoverySlice.js
Normal file
@ -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;
|
@ -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;
|
||||
|
86
src/styles/Campaign.scss
Normal file
86
src/styles/Campaign.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
53
src/styles/CreatorDiscovery.scss
Normal file
53
src/styles/CreatorDiscovery.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
@import './custom-theme.scss';
|
||||
// @import './custom-theme.scss';
|
||||
|
||||
// 导入变量
|
||||
@import './variables';
|
||||
|
||||
.filter-card {
|
||||
border-radius: 12px;
|
||||
|
@ -1,4 +1,7 @@
|
||||
@import './custom-theme.scss';
|
||||
// @import './custom-theme.scss';
|
||||
|
||||
// 导入变量
|
||||
@import './variables';
|
||||
|
||||
.creator-database-table {
|
||||
.creator-cell {
|
||||
|
@ -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;
|
||||
}
|
||||
|
30
src/styles/Login.scss
Normal file
30
src/styles/Login.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
41
src/styles/_variables.scss
Normal file
41
src/styles/_variables.scss
Normal file
@ -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);
|
@ -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;
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
@import 'custom-theme.scss';
|
||||
// 不再需要导入custom-theme.scss,因为已经在index.scss中统一导入了
|
||||
// @import 'custom-theme.scss';
|
||||
|
||||
// 导入变量
|
||||
@import './variables';
|
||||
|
||||
.breadcrumb {
|
||||
font-weight: 700;
|
||||
|
5
src/styles/index.scss
Normal file
5
src/styles/index.scss
Normal file
@ -0,0 +1,5 @@
|
||||
// 导入变量和Bootstrap
|
||||
@import './custom-theme.scss';
|
||||
|
||||
// 导入全局样式
|
||||
@import './global.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 {
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user