mirror of
https://github.com/Funkoala14/CreatorCenter_OOIN.git
synced 2025-06-08 06:38:14 +08:00
[dev]inbox & templates & discovery
This commit is contained in:
parent
cf1a9d10f6
commit
bcbc8fafab
@ -3,9 +3,10 @@ import { library } from '@fortawesome/fontawesome-svg-core';
|
|||||||
import { fas } from '@fortawesome/free-solid-svg-icons';
|
import { fas } from '@fortawesome/free-solid-svg-icons';
|
||||||
import Router from './router';
|
import Router from './router';
|
||||||
import './styles/Campaign.scss';
|
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
|
// Add Font Awesome icons to library
|
||||||
library.add(faTiktok, fas, faYoutube, faInstagram);
|
library.add(faTiktok, fas, faYoutube, faInstagram);
|
||||||
|
71
src/components/ChatDetails.jsx
Normal file
71
src/components/ChatDetails.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -5,7 +5,7 @@ import { Ellipsis, Send } from 'lucide-react';
|
|||||||
import { Button, Form } from 'react-bootstrap';
|
import { Button, Form } from 'react-bootstrap';
|
||||||
import ChatInput from './ChatInput';
|
import ChatInput from './ChatInput';
|
||||||
|
|
||||||
export default function ChatWindow() {
|
export default function ChatWindow({ onOpenChatDetails }) {
|
||||||
const { selectedChat, chatStatus } = useSelector((state) => state.inbox);
|
const { selectedChat, chatStatus } = useSelector((state) => state.inbox);
|
||||||
const [activePlatform, setActivePlatform] = useState('email');
|
const [activePlatform, setActivePlatform] = useState('email');
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -45,7 +45,12 @@ export default function ChatWindow() {
|
|||||||
<img src={selectedChat.avatar} alt='avatar' />
|
<img src={selectedChat.avatar} alt='avatar' />
|
||||||
</div>
|
</div>
|
||||||
<div className='chat-window-header-left-info'>
|
<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>
|
</div>
|
||||||
<div className='chat-window-header-right'>
|
<div className='chat-window-header-right'>
|
||||||
|
@ -73,7 +73,7 @@ export default function CreatorList({ path, pageType = 'database' }) {
|
|||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
<div className='text-center p-5'>
|
<div className='text-center p-5'>
|
||||||
<Spinner animation='border' role='status'>
|
<Spinner animation='border' role='status' variant='primary'>
|
||||||
<span className='visually-hidden'>Loading...</span>
|
<span className='visually-hidden'>Loading...</span>
|
||||||
</Spinner>
|
</Spinner>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,8 +6,7 @@ export default function DividLayout() {
|
|||||||
return (
|
return (
|
||||||
<div className='d-flex'>
|
<div className='d-flex'>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
<main className='main-content w-100 d-flex flex-row gap-3 p-0' style={{ backgroundColor: '#f5f3ff' }}>
|
||||||
<main className='main-content w-100 d-flex flex-row gap-3' style={{ backgroundColor: '#f5f3ff' }}>
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
@ -110,7 +110,7 @@ const menuItems = [
|
|||||||
{
|
{
|
||||||
id: 'templates',
|
id: 'templates',
|
||||||
title: 'Templates',
|
title: 'Templates',
|
||||||
path: '/creator-inbox/templates',
|
path: '/inbox-templates',
|
||||||
icon: <FileText />,
|
icon: <FileText />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
26
src/components/LoadingOverlay.jsx
Normal file
26
src/components/LoadingOverlay.jsx
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
11
src/components/Spinning.jsx
Normal file
11
src/components/Spinning.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
71
src/components/TemplateList.jsx
Normal file
71
src/components/TemplateList.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,10 +1,11 @@
|
|||||||
import { ArrowLeft, Instagram, Link, Mail, MapPin } from 'lucide-react';
|
import { ArrowLeft, Crown, Eye, Heart, Instagram, Link, Mail, MapPin } from 'lucide-react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Card } from 'react-bootstrap';
|
import { Card, Table } from 'react-bootstrap';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { selectCreator, clearCreator } from '../store/slices/creatorsSlice';
|
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 = {
|
const data = {
|
||||||
labels: ['Red', 'Blue', 'Green', 'Purple'],
|
labels: ['Red', 'Blue', 'Green', 'Purple'],
|
||||||
@ -18,22 +19,35 @@ const data = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
cutout: 70,
|
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
position: 'bottom',
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
align: 'end',
|
||||||
labels: {
|
labels: {
|
||||||
generateLabels: function (chart) {
|
usePointStyle: true,
|
||||||
return chart.data.labels.map((label, index) => {
|
pointStyle: 'circle',
|
||||||
const value = chart.data.datasets[0].data[index];
|
boxWidth: 10,
|
||||||
return {
|
padding: 10,
|
||||||
text: `${label} ${value}%`,
|
font: {
|
||||||
fillStyle: chart.data.datasets[0].backgroundColor[index],
|
size: 14,
|
||||||
strokeStyle: chart.data.datasets[0].backgroundColor[index],
|
|
||||||
index: index,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
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 navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { selectedCreator } = useSelector((state) => state.creators);
|
const { selectedCreator } = useSelector((state) => state.creators);
|
||||||
|
const [activeTab, setActiveTab] = useState('basic');
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
navigate(-1);
|
navigate(-1);
|
||||||
@ -62,7 +77,7 @@ export default function CreatorDetail({}) {
|
|||||||
</div>
|
</div>
|
||||||
{selectedCreator ? (
|
{selectedCreator ? (
|
||||||
<div className='creator-info-detail-container'>
|
<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-info-1'>
|
||||||
<div className='creator-avatar'>
|
<div className='creator-avatar'>
|
||||||
<img src={selectedCreator.avatar} alt={selectedCreator.name} />
|
<img src={selectedCreator.avatar} alt={selectedCreator.name} />
|
||||||
@ -117,7 +132,7 @@ export default function CreatorDetail({}) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='creator-data'>
|
<div className='creator-data card'>
|
||||||
<div className='levels'>
|
<div className='levels'>
|
||||||
<div className='level-item'>
|
<div className='level-item'>
|
||||||
<div className='name'>E-commerce Level</div>
|
<div className='name'>E-commerce Level</div>
|
||||||
@ -157,14 +172,36 @@ export default function CreatorDetail({}) {
|
|||||||
<div className='data-charts'>
|
<div className='data-charts'>
|
||||||
<div className='data-chart'>
|
<div className='data-chart'>
|
||||||
<div className='chart-title'>GMV per sales channel</div>
|
<div className='chart-title'>GMV per sales channel</div>
|
||||||
<Doughnut data={data} options={options} />
|
<Doughnut data={data} options={{ ...options, cutout: 80 }} />
|
||||||
</div>
|
</div>
|
||||||
<div className='data-chart'>
|
<div className='data-chart'>
|
||||||
<div className='chart-title'>GMV by product category</div>
|
<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>
|
||||||
</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>
|
||||||
) : (
|
) : (
|
||||||
<div>No creator found</div>
|
<div>No creator found</div>
|
||||||
@ -172,3 +209,312 @@ export default function CreatorDetail({}) {
|
|||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import InboxList from '@/components/InboxList';
|
import InboxList from '@/components/InboxList';
|
||||||
import ChatWindow from '@/components/ChatWindow';
|
import ChatWindow from '@/components/ChatWindow';
|
||||||
import '@/styles/Inbox.scss';
|
import '@/styles/Inbox.scss';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { resetSelectedChat } from '@/store/slices/inboxSlice';
|
import { resetSelectedChat } from '@/store/slices/inboxSlice';
|
||||||
|
import ChatDetails from '@/components/ChatDetails';
|
||||||
|
|
||||||
export default function CreatorInbox() {
|
export default function CreatorInbox() {
|
||||||
const { selectedChat } = useSelector((state) => state.inbox);
|
const { selectedChat } = useSelector((state) => state.inbox);
|
||||||
|
const [openChatDetails, setOpenChatDetails] = useState(false);
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@ -19,7 +22,8 @@ export default function CreatorInbox() {
|
|||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<InboxList />
|
<InboxList />
|
||||||
{selectedChat?.id && <ChatWindow />}
|
{selectedChat?.id && <ChatWindow onOpenChatDetails={() => setOpenChatDetails(true)} />}
|
||||||
|
{openChatDetails && <ChatDetails onCloseChatDetails={() => setOpenChatDetails(false)} />}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
64
src/pages/InboxTemplate.jsx
Normal file
64
src/pages/InboxTemplate.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -9,8 +9,9 @@ import BrandsDetail from '@/pages/BrandsDetail';
|
|||||||
import CampaignDetail from '@/pages/CampaignDetail';
|
import CampaignDetail from '@/pages/CampaignDetail';
|
||||||
import Login from '@/pages/Login';
|
import Login from '@/pages/Login';
|
||||||
import CreatorDiscovery from '@/pages/CreatorDiscovery';
|
import CreatorDiscovery from '@/pages/CreatorDiscovery';
|
||||||
import PrivateCreator from '../pages/PrivateCreator';
|
import PrivateCreator from '@/pages/PrivateCreator';
|
||||||
import CreatorDetail from '../pages/CreatorDetail';
|
import CreatorDetail from '@/pages/CreatorDetail';
|
||||||
|
import InboxTemplate from '@/pages/InboxTemplate';
|
||||||
|
|
||||||
// Routes configuration object
|
// Routes configuration object
|
||||||
const routes = [
|
const routes = [
|
||||||
@ -88,6 +89,10 @@ const routes = [
|
|||||||
path: '/creator/:id',
|
path: '/creator/:id',
|
||||||
element: <CreatorDetail />,
|
element: <CreatorDetail />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/inbox-templates',
|
||||||
|
element: <InboxTemplate />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Create router with routes wrapped in the layout
|
// Create router with routes wrapped in the layout
|
||||||
@ -108,11 +113,7 @@ const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
element: <CreatorInbox />,
|
element: <CreatorInbox />,
|
||||||
},
|
}
|
||||||
{
|
|
||||||
path: 'templates',
|
|
||||||
element: <Home />,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -1,5 +1,30 @@
|
|||||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
const mockVideos = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Collagen + Biotin = your beauty routine’s 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 routine’s 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 routine’s 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获取
|
// 模拟创作者数据,实际项目中会从API获取
|
||||||
const mockCreators = [
|
const mockCreators = [
|
||||||
{
|
{
|
||||||
@ -16,6 +41,8 @@ const mockCreators = [
|
|||||||
hasEcommerce: true,
|
hasEcommerce: true,
|
||||||
hasTiktok: true,
|
hasTiktok: true,
|
||||||
verified: true,
|
verified: true,
|
||||||
|
videos: mockVideos,
|
||||||
|
videosWithProduct: mockVideos,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@ -31,6 +58,8 @@ const mockCreators = [
|
|||||||
hasEcommerce: false,
|
hasEcommerce: false,
|
||||||
hasTiktok: true,
|
hasTiktok: true,
|
||||||
verified: false,
|
verified: false,
|
||||||
|
videos: mockVideos,
|
||||||
|
videosWithProduct: mockVideos,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
@ -46,6 +75,8 @@ const mockCreators = [
|
|||||||
hasEcommerce: true,
|
hasEcommerce: true,
|
||||||
hasTiktok: true,
|
hasTiktok: true,
|
||||||
verified: false,
|
verified: false,
|
||||||
|
videos: mockVideos,
|
||||||
|
videosWithProduct: mockVideos,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
@ -63,6 +94,8 @@ const mockCreators = [
|
|||||||
hasInstagram: true,
|
hasInstagram: true,
|
||||||
hasYoutube: true,
|
hasYoutube: true,
|
||||||
verified: true,
|
verified: true,
|
||||||
|
videos: mockVideos,
|
||||||
|
videosWithProduct: mockVideos,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
@ -80,6 +113,8 @@ const mockCreators = [
|
|||||||
hasInstagram: true,
|
hasInstagram: true,
|
||||||
hasYoutube: true,
|
hasYoutube: true,
|
||||||
verified: true,
|
verified: true,
|
||||||
|
videos: mockVideos,
|
||||||
|
videosWithProduct: mockVideos,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
@ -97,6 +132,8 @@ const mockCreators = [
|
|||||||
hasInstagram: true,
|
hasInstagram: true,
|
||||||
hasYoutube: true,
|
hasYoutube: true,
|
||||||
verified: false,
|
verified: false,
|
||||||
|
videos: mockVideos,
|
||||||
|
videosWithProduct: mockVideos,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,6 +1,87 @@
|
|||||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||||
import { format, isToday, parseISO } from 'date-fns';
|
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 = [
|
const mockInboxList = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -102,12 +183,22 @@ export const fetchChatHistory = createAsyncThunk('inbox/fetchChatHistory', async
|
|||||||
return { chatHistory: mockChatHistory, chatId: id };
|
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 = {
|
const initialState = {
|
||||||
inboxList: [],
|
inboxList: [],
|
||||||
inboxStatus: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
inboxStatus: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||||
chatStatus: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
chatStatus: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||||
error: null,
|
error: null,
|
||||||
selectedChat: {},
|
selectedChat: {},
|
||||||
|
templates: [],
|
||||||
|
templatesStatus: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||||
};
|
};
|
||||||
|
|
||||||
const inboxSlice = createSlice({
|
const inboxSlice = createSlice({
|
||||||
@ -150,6 +241,17 @@ const inboxSlice = createSlice({
|
|||||||
.addCase(fetchChatHistory.rejected, (state, action) => {
|
.addCase(fetchChatHistory.rejected, (state, action) => {
|
||||||
state.chatStatus = 'failed';
|
state.chatStatus = 'failed';
|
||||||
state.error = action.error.message;
|
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;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -56,18 +56,15 @@
|
|||||||
.creator-info-detail-container {
|
.creator-info-detail-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
.creator-info-container {
|
.creator-info-container {
|
||||||
width: 40%;
|
flex: 4;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
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;
|
gap: 1rem;
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
.creator-info-1 {
|
.creator-info-1 {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -145,11 +142,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.creator-data {
|
.creator-data {
|
||||||
width: 65%;
|
padding: 1.5rem;
|
||||||
background: #ffffffff; /* white */
|
flex: 6;
|
||||||
border-radius: 8px; /* border-l */
|
|
||||||
box-shadow: 0px 0px 1px #171a1f12, 0px 0px 2px #171a1f1f; /* shadow-xs */
|
|
||||||
padding: 1rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@ -170,7 +164,7 @@
|
|||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
background-color: $primary-100;
|
background-color: $primary-100;
|
||||||
color: $primary;
|
color: $primary;
|
||||||
font-size: .75rem;
|
font-size: 0.75rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
}
|
}
|
||||||
@ -179,7 +173,7 @@
|
|||||||
.data-cards {
|
.data-cards {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: .5rem;
|
gap: 0.5rem;
|
||||||
.data-card {
|
.data-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
@ -200,6 +194,7 @@
|
|||||||
.data-charts {
|
.data-charts {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
|
gap: 1rem;
|
||||||
.data-chart {
|
.data-chart {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
.chart-title {
|
.chart-title {
|
||||||
@ -207,12 +202,122 @@
|
|||||||
color: $neutral-900;
|
color: $neutral-900;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
// 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 {
|
canvas {
|
||||||
max-width: 260px;
|
|
||||||
max-height: 260px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.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%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,6 +164,7 @@
|
|||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
.chat-window-header-left-avatar {
|
.chat-window-header-left-avatar {
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
@ -255,3 +256,168 @@
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border: 1px solid #ccc;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
.btn-outline-primary {
|
.btn-outline-primary {
|
||||||
--bs-btn-color: #6366f1 !important;
|
--bs-btn-color: #6366f1 !important;
|
||||||
--bs-btn-hover-color: white !important;
|
--bs-btn-hover-color: white !important;
|
||||||
|
--bs-btn-active-color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary-subtle {
|
.btn-primary-subtle {
|
||||||
@ -114,3 +115,11 @@ a {
|
|||||||
border-color: transparent !important;
|
border-color: transparent !important;
|
||||||
box-shadow: none !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;
|
||||||
|
}
|
||||||
|
@ -28,3 +28,7 @@
|
|||||||
.back-button {
|
.back-button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user