[dev]inbox & templates & discovery

This commit is contained in:
susie-laptop 2025-05-21 10:49:54 -04:00
parent cf1a9d10f6
commit bcbc8fafab
19 changed files with 1078 additions and 56 deletions

View File

@ -3,9 +3,10 @@ import { library } from '@fortawesome/fontawesome-svg-core';
import { fas } from '@fortawesome/free-solid-svg-icons';
import Router from './router';
import './styles/Campaign.scss';
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
import '@/styles/custom-theme.scss';
import { Chart as ChartJS, ArcElement, Tooltip, Legend, BarElement, LinearScale, CategoryScale } from 'chart.js';
ChartJS.register(ArcElement, Tooltip, Legend);
ChartJS.register(ArcElement, Tooltip, Legend, BarElement, LinearScale, CategoryScale);
// Add Font Awesome icons to library
library.add(faTiktok, fas, faYoutube, faInstagram);

View File

@ -0,0 +1,71 @@
import { Send, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { selectCreator } from '../store/slices/creatorsSlice';
import { Button, Form } from 'react-bootstrap';
export default function ChatDetails({ onCloseChatDetails }) {
const [search, setSearch] = useState('');
const { selectedChat } = useSelector((state) => state.inbox);
const { selectedCreator } = useSelector((state) => state.creators);
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted');
};
useEffect(() => {
dispatch(selectCreator(selectedChat.id));
}, [dispatch, selectedChat]);
return (
<div className='chat-details'>
<div className='chat-details-header'>
<div className='chat-header-title'>
Chat Details
<div className='chat-detail-close-btn' onClick={onCloseChatDetails}>
<X />
</div>
</div>
<div className='chat-detail-header-creator'>
<div className='chat-detail-header-creator-avatar'>
<img src={selectedChat?.avatar} alt='avatar' />
</div>
<div className='chat-detail-header-creator-name'>{selectedChat?.name}</div>
</div>
</div>
<div className='chat-detail-header-infos'>
<div className='chat-detail-header-info-item'>
<div className='chat-detail-header-info-item-label'>Category</div>
<div className='chat-detail-header-info-item-value'>{selectedCreator?.category}</div>
</div>
<div className='chat-detail-header-info-item'>
<div className='chat-detail-header-info-item-label'>MCN</div>
<div className='chat-detail-header-info-item-value'>{selectedCreator?.mcn || '--'}</div>
</div>
<div className='chat-detail-header-info-item'>
<div className='chat-detail-header-info-item-label'>Pricing</div>
<div className='chat-detail-header-info-item-value'>{selectedCreator?.pricing || '--'}</div>
</div>
<div className='chat-detail-header-info-item'>
<div className='chat-detail-header-info-item-label'>Collab.</div>
<div className='chat-detail-header-info-item-value'>{selectedCreator?.collab || '--'}</div>
</div>
</div>
<div className='chat-detail-summary'>
<div className='chat-detail-summary-title'>Chat Summary</div>
<Form.Control as='textarea' rows={6} className='chat-detail-summary-input' />
</div>
<div className='chat-detail-generate'>
<div className='chat-detail-generate-title'>Chat Generate</div>
<Form className='generate-form' onSubmit={handleSubmit}>
<Form.Control as='textarea' rows={4} className='generate-input' />
<Button className='rounded-pill submit-btn btn-sm' type='submit'>
<Send size={16} />
</Button>
</Form>
</div>
</div>
);
}

View File

@ -5,7 +5,7 @@ import { Ellipsis, Send } from 'lucide-react';
import { Button, Form } from 'react-bootstrap';
import ChatInput from './ChatInput';
export default function ChatWindow() {
export default function ChatWindow({ onOpenChatDetails }) {
const { selectedChat, chatStatus } = useSelector((state) => state.inbox);
const [activePlatform, setActivePlatform] = useState('email');
const dispatch = useDispatch();
@ -45,7 +45,12 @@ export default function ChatWindow() {
<img src={selectedChat.avatar} alt='avatar' />
</div>
<div className='chat-window-header-left-info'>
<div className='chat-window-header-left-info-name fw-bold'>{selectedChat.name}</div>
<div
className='chat-window-header-left-info-name fw-bold'
onClick={onOpenChatDetails}
>
{selectedChat.name}
</div>
</div>
</div>
<div className='chat-window-header-right'>

View File

@ -73,7 +73,7 @@ export default function CreatorList({ path, pageType = 'database' }) {
if (status === 'loading') {
return (
<div className='text-center p-5'>
<Spinner animation='border' role='status'>
<Spinner animation='border' role='status' variant='primary'>
<span className='visually-hidden'>Loading...</span>
</Spinner>
</div>

View File

@ -6,8 +6,7 @@ export default function DividLayout() {
return (
<div className='d-flex'>
<Sidebar />
<main className='main-content w-100 d-flex flex-row gap-3' style={{ backgroundColor: '#f5f3ff' }}>
<main className='main-content w-100 d-flex flex-row gap-3 p-0' style={{ backgroundColor: '#f5f3ff' }}>
<Outlet />
</main>
</div>

View File

@ -110,7 +110,7 @@ const menuItems = [
{
id: 'templates',
title: 'Templates',
path: '/creator-inbox/templates',
path: '/inbox-templates',
icon: <FileText />,
},
],

View File

@ -0,0 +1,26 @@
import { Spinner } from 'react-bootstrap';
export default function LoadingOverlay({ status }) {
if (status !== 'loading') return null;
return (
<div style={styles.overlay}>
<Spinner animation='border' role='status' variant='primary' style={{ width: '3rem', height: '3rem' }}>
<span className='visually-hidden'>Loading...</span>
</Spinner>
</div>
);
}
const styles = {
overlay: {
position: 'absolute', // fixed
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(255, 255, 255, 0.6)', //
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 9999,
},
};

View File

@ -0,0 +1,11 @@
import { Spinner } from "react-bootstrap";
export default function Spinning() {
return (
<div className='text-center p-5'>
<Spinner animation='border' role='status' variant='primary'>
<span className='visually-hidden'>Loading...</span>
</Spinner>
</div>
);
}

View File

@ -0,0 +1,71 @@
import { Copy, Edit, FileText, LayoutTemplate } from 'lucide-react';
import { Button } from 'react-bootstrap';
import { useSelector } from 'react-redux';
import Spinning from './Spinning';
export default function TemplateList({ activeTab }) {
const { templates, templatesStatus } = useSelector((state) => state.inbox);
//
if (templatesStatus === 'loading') {
return <Spinning />;
}
if (templates.length === 0) {
return (
<div className='template-list-empty'>
<span className='template-list-empty-text'>No templates found</span>
</div>
);
}
return (
<div className='template-list'>
{templates.map((template) => (
<div className='template-item' key={template.id}>
<div className='template-item-name'>
<span className='template-item-name-text'>
{template.type === 'initial' && (
<span className='template-item-name-text-initial'>初步建联</span>
)}
{template.type === 'bargain' && (
<span className='template-item-name-text-bargain'>砍价邮件</span>
)}
{template.type === 'script' && (
<span className='template-item-name-text-script'>脚本邮件</span>
)}
{template.type === 'cooperation' && (
<span className='template-item-name-text-cooperation'>合作追踪</span>
)}{' '}
- {template.name}
</span>
<div className='template-item-name-actions'>
<Button variant='outline-primary' className='border-0' size='sm'>
<Edit size={16} />
Edit
</Button>
</div>
</div>
<div className='template-item-item template-platform'>
<div className='label'>
<LayoutTemplate size={20} />
Platform
</div>
<div className='value'>{template.platform}</div>
</div>
<div className='template-item-item template-service'>
<div className='label'>
<Copy size={20} />
Service
</div>
<div className='value'>{template.service}</div>
</div>
<div className='template-item-item template-message'>
<div className='label'>
<FileText size={20} />
Message
</div>
<div className='value'>{template.message}</div>
</div>
</div>
))}
</div>
);
}

View File

@ -1,10 +1,11 @@
import { ArrowLeft, Instagram, Link, Mail, MapPin } from 'lucide-react';
import { useEffect } from 'react';
import { Card } from 'react-bootstrap';
import { ArrowLeft, Crown, Eye, Heart, Instagram, Link, Mail, MapPin } from 'lucide-react';
import { useEffect, useState } from 'react';
import { Card, Table } from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
import { selectCreator, clearCreator } from '../store/slices/creatorsSlice';
import { Doughnut } from 'react-chartjs-2';
import { Bar, Doughnut } from 'react-chartjs-2';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
const data = {
labels: ['Red', 'Blue', 'Green', 'Purple'],
@ -18,22 +19,35 @@ const data = {
};
const options = {
cutout: 70,
plugins: {
legend: {
position: 'bottom',
display: true,
position: 'right',
align: 'end',
labels: {
generateLabels: function (chart) {
return chart.data.labels.map((label, index) => {
const value = chart.data.datasets[0].data[index];
return {
text: `${label} ${value}%`,
fillStyle: chart.data.datasets[0].backgroundColor[index],
strokeStyle: chart.data.datasets[0].backgroundColor[index],
index: index,
};
});
usePointStyle: true,
pointStyle: 'circle',
boxWidth: 10,
padding: 10,
font: {
size: 14,
},
color: '#eee',
generateLabels: (chart) => {
const dataset = chart.data.datasets[0];
return chart.data.labels.map((label, i) => ({
text: `${label} ${dataset.data[i]}%`,
fillStyle: dataset.backgroundColor[i],
strokeStyle: dataset.backgroundColor[i],
fontColor: '#000',
lineWidth: 0,
}));
},
},
},
tooltip: {
callbacks: {
label: (context) => `${context.label}: ${context.raw}%`,
},
},
},
@ -44,6 +58,7 @@ export default function CreatorDetail({}) {
const navigate = useNavigate();
const dispatch = useDispatch();
const { selectedCreator } = useSelector((state) => state.creators);
const [activeTab, setActiveTab] = useState('basic');
const handleBack = () => {
navigate(-1);
@ -62,7 +77,7 @@ export default function CreatorDetail({}) {
</div>
{selectedCreator ? (
<div className='creator-info-detail-container'>
<div className='creator-info-container'>
<div className='creator-info-container card'>
<div className='creator-info-1'>
<div className='creator-avatar'>
<img src={selectedCreator.avatar} alt={selectedCreator.name} />
@ -117,7 +132,7 @@ export default function CreatorDetail({}) {
</div>
</div>
</div>
<div className='creator-data'>
<div className='creator-data card'>
<div className='levels'>
<div className='level-item'>
<div className='name'>E-commerce Level</div>
@ -157,14 +172,36 @@ export default function CreatorDetail({}) {
<div className='data-charts'>
<div className='data-chart'>
<div className='chart-title'>GMV per sales channel</div>
<Doughnut data={data} options={options} />
<Doughnut data={data} options={{ ...options, cutout: 80 }} />
</div>
<div className='data-chart'>
<div className='chart-title'>GMV by product category</div>
<Doughnut data={data} options={options} />
<Doughnut data={data} options={{ ...options, cutout: 80 }} />
</div>
</div>
</div>
<div className='creator-detail-tab-switches tab-switches'>
<div
className={`tab-switch-item ${activeTab === 'basic' ? 'active' : ''}`}
onClick={() => setActiveTab('basic')}
>
Basic Info
</div>
<div
className={`tab-switch-item ${activeTab === 'deepAnalysis' ? 'active' : ''}`}
onClick={() => setActiveTab('deepAnalysis')}
>
Deep Analysis
</div>
<div
className={`tab-switch-item ${activeTab === 'collab' ? 'active' : ''}`}
onClick={() => setActiveTab('collab')}
>
Collaboration Info
</div>
</div>
{activeTab === 'basic' && <CreatorBasicInfo selectedCreator={selectedCreator} />}
{activeTab === 'collab' && <CollabInfo selectedCreator={selectedCreator} />}
</div>
) : (
<div>No creator found</div>
@ -172,3 +209,312 @@ export default function CreatorDetail({}) {
</div>
);
}
function CreatorBasicInfo({ selectedCreator }) {
const [activeTab, setActiveTab] = useState('gmv');
const barOptions = {
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: (context) => `${context.raw}%`,
},
},
datalabels: {
display: true,
color: '#fff',
anchor: 'end',
align: 'start',
formatter: (value) => `${value}%`,
},
},
scales: {
x: {
grid: { display: false },
ticks: {
color: '#888',
font: {
size: 14,
},
},
},
y: {
display: false,
},
},
};
const barData = {
labels: ['TX', 'FL', 'NY', 'GE', 'CA'],
datasets: [
{
label: 'Top 5 Locations',
data: [11, 8, 6, 5.5, 5],
backgroundColor: '#6C63FF',
borderRadius: 10,
barPercentage: 0.6,
},
],
};
return (
<div className='creator-basic-info card'>
<div className='basic-info-list'>
<div className='basic-info-title'>Collaboration Metrics</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgCommissionRate || '--'}</div>
<div className='name'>Avg. Commission Rate</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.products || '--'}</div>
<div className='name'>Products</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.brandCollaborations || '--'}</div>
<div className='name'>Brand Collaborations</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.productPrice || '--'}</div>
<div className='name'>Product Price</div>
</div>
</div>
<div className='basic-info-list'>
<div className='basic-info-title'>
Video<span className='time-range'>{selectedCreator.videoTimeRange || '--'}</span>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgVideoGpm || '--'}</div>
<div className='name'>Video GPM</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.videos.length || '--'}</div>
<div className='name'>Videos</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgVideoViews || '--'}</div>
<div className='name'>Avg. Video Views</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgVideoEngagements || '--'}</div>
<div className='name'>Avg. Video Engagement</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgVideoLikes || '--'}</div>
<div className='name'>Avg. Video Likes</div>
</div>
</div>
<div className='basic-info-list'>
<div className='basic-info-title'>
Shoppable Video<span className='time-range'>{selectedCreator.videoTimeRange || '--'}</span>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgVideoGpm || '--'}</div>
<div className='name'>Video GPM</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.videos.length || '--'}</div>
<div className='name'>Videos</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgVideoViews || '--'}</div>
<div className='name'>Avg. Video Views</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgVideoEngagements || '--'}</div>
<div className='name'>Avg. Video Engagement</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgVideoLikes || '--'}</div>
<div className='name'>Avg. Video Likes</div>
</div>
</div>
<div className='basic-info-list'>
<div className='basic-info-title'>
LIVE<span className='time-range'>{selectedCreator.liveTimeRange || '--'}</span>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgLiveGpm || '--'}</div>
<div className='name'>LIVE GPM</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.liveVideos || '--'}</div>
<div className='name'>LIVE Videos</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgLiveViews || '--'}</div>
<div className='name'>Avg. LIVE Views</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgLiveEngagements || '--'}</div>
<div className='name'>Avg. LIVE Engagement</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgLiveLikes || '--'}</div>
<div className='name'>Avg. LIVE Likes</div>
</div>
</div>
<div className='basic-info-list'>
<div className='basic-info-title'>
Shoppable LIVE<span className='time-range'>{selectedCreator.liveTimeRange || '--'}</span>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgLiveGpm || '--'}</div>
<div className='name'>LIVE GPM</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.liveVideos || '--'}</div>
<div className='name'>LIVE Videos</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgLiveViews || '--'}</div>
<div className='name'>Avg. LIVE Views</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgLiveEngagements || '--'}</div>
<div className='name'>Avg. LIVE Engagement</div>
</div>
<div className='basic-info-item'>
<div className='value'>{selectedCreator.avgLiveLikes || '--'}</div>
<div className='name'>Avg. LIVE Likes</div>
</div>
</div>
<div className='basic-info-list'>
<div className='basic-info-title'>
Follwers<span className='time-range'>{selectedCreator.liveTimeRange || '--'}</span>
</div>
<div className='followers-data-charts'>
<div className='data-chart'>
<div className='chart-title'>Follower Gender</div>
<Doughnut data={data} options={{ ...options, cutout: 90 }} />
</div>
<div className='data-chart'>
<div className='chart-title'>Follower Age</div>
<Doughnut data={data} options={{ ...options, cutout: 90 }} />
</div>
<div className='data-chart'>
<div className='chart-title'>Top 5 Locations</div>
<Bar data={barData} options={barOptions} />
</div>
</div>
</div>
<div className='basic-info-list'>
<div className='basic-info-title'>
Trends<span className='time-range'>{selectedCreator.liveTimeRange || '--'}</span>
</div>
<div className='tab-switches'>
<div
className={`tab-switch-item ${activeTab === 'gmv' ? 'active' : ''}`}
onClick={() => setActiveTab('gmv')}
>
GMV
</div>
<div
className={`tab-switch-item ${activeTab === 'sold' ? 'active' : ''}`}
onClick={() => setActiveTab('sold')}
>
Items Sold
</div>
<div
className={`tab-switch-item ${activeTab === 'followers' ? 'active' : ''}`}
onClick={() => setActiveTab('followers')}
>
Followers
</div>
<div
className={`tab-switch-item ${activeTab === 'views' ? 'active' : ''}`}
onClick={() => setActiveTab('views')}
>
Video Views
</div>
</div>
</div>
<div className='basic-info-list video-list'>
<div className='basic-info-title'>Videos</div>
{selectedCreator?.videos?.length > 0 &&
selectedCreator?.videos.map((video) => (
<div className='basic-info-item'>
<div className='picture' style={{ backgroundImage: `url(${video.picture})` }}></div>
<div className='right-side-info'>
<Crown size={16} />
<div className='video-title'>{video.title}</div>
<div className='release-time item-info'>
Release Time<span className='time'>{video.releaseTime}</span>
</div>
<div className='views item-info'>
<FontAwesomeIcon icon='fa-solid fa-eye' style={{ color: '#636AE8FF' }} />
{video.views}
</div>
<div className='like item-info'>
<FontAwesomeIcon icon='fa-solid fa-heart' style={{ color: '#E8618CFF' }} />
{video.likes}
</div>
</div>
</div>
))}
<div className='basic-info-title'>Videos with Product</div>
{selectedCreator?.videosWithProduct?.length > 0 &&
selectedCreator?.videosWithProduct.map((video) => (
<div className='basic-info-item'>
<div className='picture' style={{ backgroundImage: `url(${video.picture})` }}></div>
<div className='right-side-info'>
<Crown size={16} className='crown-icon' />
<div className='video-title'>{video.title}</div>
<div className='release-time item-info'>
Release Time<span className='time'>{video.releaseTime}</span>
</div>
<div className='views item-info'>
<FontAwesomeIcon icon='fa-solid fa-eye' style={{ color: '#636AE8FF' }} />
{video.views}
</div>
<div className='like item-info'>
<FontAwesomeIcon icon='fa-solid fa-heart' style={{ color: '#E8618CFF' }} />
{video.likes}
</div>
</div>
</div>
))}
</div>
</div>
);
}
function CollabInfo({ selectedCreator }) {
return (
<div className='collab-info'>
<Table responsive className='bg-white shadow-xs rounded overflow-hidden'>
<thead>
<tr>
<th>Brand</th>
<th>Pricing Detail</th>
<th>Start Date</th>
<th>End Date</th>
<th>Status</th>
<th>GMV Achieved</th>
<th>Views Achieved</th>
<th>Video Link</th>
</tr>
</thead>
<tbody>
{selectedCreator?.collabInfo?.length > 0 ? (
selectedCreator?.collabInfo?.map((collab) => (
<tr>
<td>{collab.brand}</td>
<td>{collab.pricingDetail}</td>
<td>{collab.startDate}</td>
<td>{collab.endDate}</td>
<td>{collab.status}</td>
</tr>
))
) : (
<tr>
<td colSpan={8} className='text-center'>
No data
</td>
</tr>
)}
</tbody>
</Table>
</div>
);
}

View File

@ -1,12 +1,15 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import InboxList from '@/components/InboxList';
import ChatWindow from '@/components/ChatWindow';
import '@/styles/Inbox.scss';
import { useSelector, useDispatch } from 'react-redux';
import { resetSelectedChat } from '@/store/slices/inboxSlice';
import ChatDetails from '@/components/ChatDetails';
export default function CreatorInbox() {
const { selectedChat } = useSelector((state) => state.inbox);
const [openChatDetails, setOpenChatDetails] = useState(false);
const dispatch = useDispatch();
useEffect(() => {
return () => {
@ -19,7 +22,8 @@ export default function CreatorInbox() {
return (
<React.Fragment>
<InboxList />
{selectedChat?.id && <ChatWindow />}
{selectedChat?.id && <ChatWindow onOpenChatDetails={() => setOpenChatDetails(true)} />}
{openChatDetails && <ChatDetails onCloseChatDetails={() => setOpenChatDetails(false)} />}
</React.Fragment>
);
}

View File

@ -0,0 +1,64 @@
import { Button } from 'react-bootstrap';
import SearchBar from '../components/SearchBar';
import { Plus } from 'lucide-react';
import React, { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { fetchTemplates } from '../store/slices/inboxSlice';
import TemplateList from '../components/TemplateList';
export default function InboxTemplate() {
const [activeTab, setActiveTab] = useState('all');
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchTemplates(activeTab));
}, [dispatch, activeTab]);
return (
<React.Fragment>
<div className='function-bar'>
<SearchBar />
<Button onClick={() => setShowAddBrandModal(true)}>
<Plus />
Add Template
</Button>
</div>
<div className='breadcrumb'>
<div className='breadcrumb-item'>Templates</div>
</div>
<div className='tab-switches'>
<div
className={`tab-switch-item ${activeTab === 'all' ? 'active' : ''}`}
onClick={() => setActiveTab('all')}
>
全部
</div>
<div
className={`tab-switch-item ${activeTab === 'initial' ? 'active' : ''}`}
onClick={() => setActiveTab('initial')}
>
初步建联
</div>
<div
className={`tab-switch-item ${activeTab === 'bargain' ? 'active' : ''}`}
onClick={() => setActiveTab('bargain')}
>
砍价邮件
</div>
<div
className={`tab-switch-item ${activeTab === 'script' ? 'active' : ''}`}
onClick={() => setActiveTab('script')}
>
脚本邮件
</div>
<div
className={`tab-switch-item ${activeTab === 'cooperation' ? 'active' : ''}`}
onClick={() => setActiveTab('cooperation')}
>
合作追踪
</div>
</div>
<TemplateList activeTab={activeTab} />
</React.Fragment>
);
}

View File

@ -9,8 +9,9 @@ import BrandsDetail from '@/pages/BrandsDetail';
import CampaignDetail from '@/pages/CampaignDetail';
import Login from '@/pages/Login';
import CreatorDiscovery from '@/pages/CreatorDiscovery';
import PrivateCreator from '../pages/PrivateCreator';
import CreatorDetail from '../pages/CreatorDetail';
import PrivateCreator from '@/pages/PrivateCreator';
import CreatorDetail from '@/pages/CreatorDetail';
import InboxTemplate from '@/pages/InboxTemplate';
// Routes configuration object
const routes = [
@ -88,6 +89,10 @@ const routes = [
path: '/creator/:id',
element: <CreatorDetail />,
},
{
path: '/inbox-templates',
element: <InboxTemplate />,
},
];
// Create router with routes wrapped in the layout
@ -108,11 +113,7 @@ const router = createBrowserRouter([
{
path: '',
element: <CreatorInbox />,
},
{
path: 'templates',
element: <Home />,
},
}
],
},
]);

View File

@ -1,5 +1,30 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
const mockVideos = [
{
id: 1,
title: 'Collagen + Biotin = your beauty routines new besties. For hair, skin, nails, and join...',
picture: 'https://api.dicebear.com/7.x/micah/svg?seed=1',
releaseTime: '2025-01-01',
views: 1000,
likes: 100,
},
{
id: 2,
title: 'Collagen + Biotin = your beauty routines new besties. For hair, skin, nails, and join...',
picture: 'https://api.dicebear.com/7.x/micah/svg?seed=2',
releaseTime: '2025-01-01',
views: 1000,
likes: 100,
},
{
id: 3,
title: 'Collagen + Biotin = your beauty routines new besties. For hair, skin, nails, and join...',
picture: 'https://api.dicebear.com/7.x/micah/svg?seed=3',
releaseTime: '2025-01-01',
views: 1000,
likes: 100,
},
];
// 模拟创作者数据实际项目中会从API获取
const mockCreators = [
{
@ -16,6 +41,8 @@ const mockCreators = [
hasEcommerce: true,
hasTiktok: true,
verified: true,
videos: mockVideos,
videosWithProduct: mockVideos,
},
{
id: 2,
@ -31,6 +58,8 @@ const mockCreators = [
hasEcommerce: false,
hasTiktok: true,
verified: false,
videos: mockVideos,
videosWithProduct: mockVideos,
},
{
id: 3,
@ -46,6 +75,8 @@ const mockCreators = [
hasEcommerce: true,
hasTiktok: true,
verified: false,
videos: mockVideos,
videosWithProduct: mockVideos,
},
{
id: 4,
@ -63,6 +94,8 @@ const mockCreators = [
hasInstagram: true,
hasYoutube: true,
verified: true,
videos: mockVideos,
videosWithProduct: mockVideos,
},
{
id: 5,
@ -80,6 +113,8 @@ const mockCreators = [
hasInstagram: true,
hasYoutube: true,
verified: true,
videos: mockVideos,
videosWithProduct: mockVideos,
},
{
id: 6,
@ -97,6 +132,8 @@ const mockCreators = [
hasInstagram: true,
hasYoutube: true,
verified: false,
videos: mockVideos,
videosWithProduct: mockVideos,
},
];

View File

@ -1,6 +1,87 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { format, isToday, parseISO } from 'date-fns';
const mockTemplates = [
{
id: 1,
name: 'TikTok 达播',
type: 'initial',
platform: 'TikTok',
service: '直播 - 达播',
message: `Hi {tiktok id},
This is {sender name} from OOIN Media. We've been impressed by your amazing content on TikTok, and several of our partner brands are interested in collaborating with you on paid campaigns.
Could you please share your rate for a single TikTok video? Additionally, do you offer any bundle discounts if we're looking to collaborate on multiple videos?`,
},
{
id: 2,
name: 'TikTok 达播',
type: 'initial',
platform: 'TikTok',
service: '直播 - 达播',
message: `Hi {tiktok id},
This is {sender name} from OOIN Media. We've been impressed by your amazing content on TikTok, and several of our partner brands are interested in collaborating with you on paid campaigns.
Could you please share your rate for a single TikTok video? Additionally, do you offer any bundle discounts if we're looking to collaborate on multiple videos?`,
},
{
id: 3,
name: 'TikTok 达播',
type: 'initial',
platform: 'TikTok',
service: '直播 - 达播',
message: `Hi {tiktok id},
This is {sender name} from OOIN Media. We've been impressed by your amazing content on TikTok, and several of our partner brands are interested in collaborating with you on paid campaigns.
Could you please share your rate for a single TikTok video? Additionally, do you offer any bundle discounts if we're looking to collaborate on multiple videos?`,
},
{
id: 4,
name: 'TikTok 达播',
type: 'initial',
platform: 'TikTok',
service: '直播 - 达播',
message: `Hi {tiktok id},
This is {sender name} from OOIN Media. We've been impressed by your amazing content on TikTok, and several of our partner brands are interested in collaborating with you on paid campaigns.
Could you please share your rate for a single TikTok video? Additionally, do you offer any bundle discounts if we're looking to collaborate on multiple videos?`,
},
{
id: 5,
name: 'TikTok 达播',
type: 'initial',
platform: 'TikTok',
service: '直播 - 达播',
message: `Hi {tiktok id},
This is {sender name} from OOIN Media. We've been impressed by your amazing content on TikTok, and several of our partner brands are interested in collaborating with you on paid campaigns.
Could you please share your rate for a single TikTok video? Additionally, do you offer any bundle discounts if we're looking to collaborate on multiple videos?`,
},
{
id: 6,
name: 'TikTok 达播',
type: 'bargain',
platform: 'TikTok',
service: '直播 - 达播',
message: `Hi {tiktok id},
This is {sender name} from OOIN Media. We've been impressed by your amazing content on TikTok, and several of our partner brands are interested in collaborating with you on paid campaigns.
Could you please share your rate for a single TikTok video? Additionally, do you offer any bundle discounts if we're looking to collaborate on multiple videos?`,
},
{
id: 7,
name: 'TikTok 达播',
type: 'script',
platform: 'TikTok',
service: '直播 - 达播',
message: `Hi {tiktok id},
This is {sender name} from OOIN Media. We've been impressed by your amazing content on TikTok, and several of our partner brands are interested in collaborating with you on paid campaigns.
Could you please share your rate for a single TikTok video? Additionally, do you offer any bundle discounts if we're looking to collaborate on multiple videos?`,
},
{
id: 8,
name: 'TikTok 达播',
type: 'cooperation',
platform: 'TikTok',
service: '直播 - 达播',
message: `Hi {tiktok id},
This is {sender name} from OOIN Media. We've been impressed by your amazing content on TikTok, and several of our partner brands are interested in collaborating with you on paid campaigns.
Could you please share your rate for a single TikTok video? Additionally, do you offer any bundle discounts if we're looking to collaborate on multiple videos?`,
},
];
const mockInboxList = [
{
id: 1,
@ -102,12 +183,22 @@ export const fetchChatHistory = createAsyncThunk('inbox/fetchChatHistory', async
return { chatHistory: mockChatHistory, chatId: id };
});
export const fetchTemplates = createAsyncThunk('inbox/fetchTemplates', async (type) => {
await new Promise((resolve) => setTimeout(resolve, 500));
if (type === 'all') {
return mockTemplates;
}
return mockTemplates.filter((item) => item.type === type);
});
const initialState = {
inboxList: [],
inboxStatus: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
chatStatus: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
selectedChat: {},
templates: [],
templatesStatus: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
};
const inboxSlice = createSlice({
@ -150,6 +241,17 @@ const inboxSlice = createSlice({
.addCase(fetchChatHistory.rejected, (state, action) => {
state.chatStatus = 'failed';
state.error = action.error.message;
})
.addCase(fetchTemplates.rejected, (state, action) => {
state.templatesStatus = 'failed';
state.error = action.error.message;
})
.addCase(fetchTemplates.pending, (state) => {
state.templatesStatus = 'loading';
})
.addCase(fetchTemplates.fulfilled, (state, action) => {
state.templatesStatus = 'succeeded';
state.templates = action.payload;
});
},
});

View File

@ -56,18 +56,15 @@
.creator-info-detail-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 1rem;
justify-content: space-between;
.creator-info-container {
width: 40%;
flex: 4;
display: flex;
flex-flow: column nowrap;
background: #ffffffff; /* white */
border-radius: 8px; /* border-l */
box-shadow: 0px 0px 1px #171a1f12, 0px 0px 2px #171a1f1f; /* shadow-xs */
gap: 1rem;
padding: 1rem;
.creator-info-1 {
display: flex;
@ -145,11 +142,8 @@
}
}
.creator-data {
width: 65%;
background: #ffffffff; /* white */
border-radius: 8px; /* border-l */
box-shadow: 0px 0px 1px #171a1f12, 0px 0px 2px #171a1f1f; /* shadow-xs */
padding: 1rem;
padding: 1.5rem;
flex: 6;
display: flex;
flex-flow: column nowrap;
gap: 1rem;
@ -170,7 +164,7 @@
border-radius: 1rem;
background-color: $primary-100;
color: $primary;
font-size: .75rem;
font-size: 0.75rem;
line-height: 1.5;
padding: 0.25rem 0.5rem;
}
@ -179,7 +173,7 @@
.data-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: .5rem;
gap: 0.5rem;
.data-card {
display: flex;
flex-flow: column nowrap;
@ -200,6 +194,7 @@
.data-charts {
display: flex;
flex-flow: row nowrap;
gap: 1rem;
.data-chart {
flex: 1;
.chart-title {
@ -207,12 +202,122 @@
color: $neutral-900;
margin-bottom: 1rem;
}
canvas {
max-width: 260px;
max-height: 260px;
}
// canvas {
// max-width: 260px;
// max-height: 260px;
// }
}
}
}
.creator-detail-tab-switches {
}
.creator-basic-info {
padding: 1.5rem;
flex: 1;
display: flex;
flex-flow: column nowrap;
gap: 1rem;
.basic-info-list {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: space-between;
gap: 1rem;
border-bottom: 2px solid $neutral-200;
padding-bottom: 1rem;
.basic-info-title {
font-weight: 700;
width: 100%;
font-size: 1.125rem;
display: flex;
align-items: center;
gap: 1.5rem;
.time-range {
font-size: 0.75rem;
color: $neutral-600;
}
}
.basic-info-item {
display: flex;
flex-flow: column nowrap;
align-items: center;
justify-content: center;
flex: 1;
background-color: $neutral-150;
border-radius: 6px;
padding: 1.5rem;
.value {
font-weight: 700;
}
.name {
color: $neutral-500;
font-size: 0.875rem;
}
}
// Charts
.followers-data-charts {
display: flex;
flex-flow: row nowrap;
gap: 1rem;
width: 100%;
.data-chart {
flex: 1;
display: inline-flex;
flex-flow: column nowrap;
justify-content: space-between;
canvas {
}
}
}
}
.video-list {
.basic-info-item {
flex-flow: row nowrap;
padding: 0;
gap: 0.5rem;
background-color: transparent;
.picture {
width: 100px;
height: 178px;
background-size: cover;
background-position: center;
background-color: $neutral-200;
border-radius: 8px;
}
.right-side-info {
flex: 1;
display: flex;
flex-flow: column nowrap;
gap: 0.5rem;
font-size: .875rem;
.video-title {
font-weight: 700;
text-overflow: ellipsis;
overflow: hidden;
line-clamp: 3;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.release-time {
color: $neutral-500;
.time {
color: $neutral-900;
}
}
.item-info {
display: flex;
flex-flow: row nowrap;
gap: 0.25rem;
align-items: center;
}
}
}
}
}
.collab-info {
width: 100%;
}
}
}

View File

@ -164,6 +164,7 @@
flex-flow: row nowrap;
gap: 0.5rem;
align-items: center;
cursor: pointer;
.chat-window-header-left-avatar {
width: 2.5rem;
@ -255,3 +256,168 @@
box-shadow: none;
border: 1px solid #ccc;
}
.chat-details {
display: flex;
flex-flow: column nowrap;
flex-grow: 1;
background-color: #fff;
border-radius: 0.4rem;
padding: 1rem;
width: 300px;
flex-shrink: 0;
overflow-y: auto;
.chat-details-header {
display: flex;
flex-flow: column nowrap;
font-weight: 700;
gap: 1rem;
border-bottom: 1px solid $neutral-200;
padding-bottom: 1rem;
.chat-header-title {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
.chat-detail-close-btn {
cursor: pointer;
}
}
.chat-detail-header-creator {
display: flex;
flex-flow: column nowrap;
align-items: center;
gap: 0.5rem;
.chat-detail-header-creator-avatar {
width: 115px;
height: 115px;
border-radius: 50%;
overflow: hidden;
background: $primary-150;
}
}
}
.chat-detail-header-infos {
display: flex;
flex-flow: column nowrap;
gap: 0.5rem;
padding: 1rem 0;
font-size: 0.875rem;
border-bottom: 1px solid $neutral-200;
.chat-detail-header-info-item {
display: flex;
flex-flow: row nowrap;
.chat-detail-header-info-item-label {
color: $neutral-900;
font-weight: 700;
width: 80px;
}
}
}
.chat-detail-summary {
display: flex;
flex-flow: column nowrap;
gap: 0.5rem;
padding: 1rem 0;
border-bottom: 1px solid $neutral-200;
.chat-detail-summary-title {
font-weight: 700;
}
.chat-detail-summary-input {
background-color: $neutral-200;
border: 0 solid transparent;
border-radius: 0.25rem;
padding: 0.5rem 1rem;
}
}
.chat-detail-generate {
display: flex;
flex-flow: column nowrap;
padding: 1rem 0;
.chat-detail-generate-title {
font-weight: 700;
}
.chat-detail-generate-input {
background-color: $neutral-200;
border: 0 solid transparent;
border-radius: 0.25rem;
padding: 0.5rem 1rem;
}
.generate-form {
position: relative;
.generate-input {
}
.submit-btn {
position: absolute;
bottom: 0.5rem;
right: 0.75rem;
}
}
}
}
.template-list {
width: 100%;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
margin-top: 1rem;
overflow-y: auto;
height: calc(100% - 85px);
padding-bottom: 1rem;
.template-item {
background-color: #fff;
border-radius: 6px;
padding: 1.5rem;
display: flex;
flex-flow: column nowrap;
gap: .875rem;
box-shadow: 0px 0px 1px #171a1f12, 0px 0px 2px #171a1f1F;
max-height: 380px;
height: fit-content;
.template-item-name {
font-size: 1.25rem;
font-weight: 700;
color: $primary;
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
}
.template-item-item {
display: flex;
flex-flow: row nowrap;
font-size: 0.875rem;
align-items: flex-start;
.label {
width: 95px;
font-weight: 600;
color: $neutral-600;
flex-shrink: 0;
display: flex;
flex-flow: row nowrap;
align-items: center;
gap: 8px;
}
.value {
color: $neutral-900;
font-weight: 400;
}
}
.template-platform {
.value {
font-size: 0.875rem;
background: $neutral-200;
line-height: 1.5rem;
padding: 0 6px;
border-radius: 12px;
color: $neutral-700;
}
}
}
}

View File

@ -19,6 +19,7 @@
.btn-outline-primary {
--bs-btn-color: #6366f1 !important;
--bs-btn-hover-color: white !important;
--bs-btn-active-color: white !important;
}
.btn-primary-subtle {
@ -114,3 +115,11 @@ a {
border-color: transparent !important;
box-shadow: none !important;
}
.card {
background: #ffffffff; /* white */
border: none;
border-radius: 8px; /* border-l */
box-shadow: 0px 0px 1px #171a1f12, 0px 0px 2px #171a1f1f; /* shadow-xs */
padding: 1rem;
}

View File

@ -28,3 +28,7 @@
.back-button {
cursor: pointer;
}
.btn {
display: inline-flex !important;
align-items: center;
}