@@ -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
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
Submit
+
+
+ );
+
+ return (
+
+
+
Panel Position
+
+ setPlacement('start')}
+ >
+ Left
+
+ setPlacement('end')}
+ >
+ Right
+
+ setPlacement('top')}
+ >
+ Top
+
+ setPlacement('bottom')}
+ >
+ Bottom
+
+
+
+
+
+
Panel Size
+
+ setSize('sm')}>
+ Small
+
+ setSize('md')}>
+ Medium
+
+ setSize('lg')}>
+ Large
+
+
+
+
+
+ Open SlidePanel
+
+
+
+ {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' && (
+ <>
+
setShowAddProductModal(true)}
+ >
+
+ Add Product
+
+
+
setShowProductDetail(false)}
+ title='Product Detail'
+ size='xxl'
+ >
+
+
+
setShowAddProductModal(false)} />
+ >
+ )}
)
);
}
+
+function AddProductModal({ show, onHide }) {
+ return (
+
+ Add Product
+
+
+ Product PID
+
+ Select
+ One
+ Two
+ Three
+
+
+
+
+ Cancel
+
+ Create
+
+
+
+
+ );
+}
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
+
+
+
+
+ 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)}
+ />
+
+
+
+
+ #Hashtag
+
+
+
+
+ Trend
+
+
+
+
+ Indivisual
+
+
+
+
+
+
+
Upload from External Source
+
+
+
+ );
+}
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
+
+
+
+
+
+
+
+
Sign In
+
+
+ );
+}
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,
+ },
});