ok
This commit is contained in:
@@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import Conversations from './Conversations';
|
import Conversations from './Conversations';
|
||||||
import DiscussedTopics from './DiscussedTopics';
|
import DiscussedTopics from './DiscussedTopics';
|
||||||
|
import StatCard from './StatCard'
|
||||||
|
|
||||||
import FollowUps from './FollowUps';
|
import FollowUps from './FollowUps';
|
||||||
|
|
||||||
@@ -36,17 +37,63 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleFiles = (files) => {
|
const handleFiles = async (files) => {
|
||||||
const newFiles = files.filter(file => {
|
const filteredFiles = [];
|
||||||
// Hindari duplikat berdasarkan nama (atau bisa pakai hash/md5 jika perlu)
|
|
||||||
return !selectedFiles.some(f => f.name === file.name);
|
for (const file of files) {
|
||||||
|
const lowerName = file.name.toLowerCase();
|
||||||
|
const nameWithoutExt = lowerName.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
|
// 1️⃣ Cegah duplikat dari file yang sudah dipilih sebelumnya
|
||||||
|
const alreadySelected = selectedFiles.some(f =>
|
||||||
|
f.name.toLowerCase() === file.name.toLowerCase()
|
||||||
|
);
|
||||||
|
if (alreadySelected) continue;
|
||||||
|
|
||||||
|
// 2️⃣ Cari file server yang mirip (berisi / mengandung)
|
||||||
|
const similarFile = fileList.find(f => {
|
||||||
|
const serverName = f.json.Key.toLowerCase();
|
||||||
|
const serverNameWithoutExt = serverName.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
serverName.includes(lowerName) ||
|
||||||
|
lowerName.includes(serverName) ||
|
||||||
|
serverNameWithoutExt.includes(nameWithoutExt) ||
|
||||||
|
nameWithoutExt.includes(serverNameWithoutExt)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
setSelectedFiles((prev) => [...prev, ...newFiles]);
|
if (similarFile) {
|
||||||
|
const confirmOverwrite = window.confirm(
|
||||||
|
`File "${file.name}" mirip atau mengandung "${similarFile.json.Key}" di server.\nIngin menimpa file tersebut?`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmOverwrite) {
|
||||||
|
// Ganti nama agar ditimpa
|
||||||
|
Object.defineProperty(file, 'name', {
|
||||||
|
writable: true,
|
||||||
|
value: similarFile.json.Key,
|
||||||
|
});
|
||||||
|
filteredFiles.push(file); // tetap tambahkan
|
||||||
|
} else {
|
||||||
|
// Tidak ditimpa, tetap pakai nama asli
|
||||||
|
filteredFiles.push(file); // tambahkan tanpa modifikasi nama
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Tidak ada kemiripan, langsung tambahkan
|
||||||
|
filteredFiles.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredFiles.length > 0) {
|
||||||
|
setSelectedFiles(prev => [...prev, ...filteredFiles]);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem('user');
|
||||||
@@ -491,10 +538,7 @@ const handleBatchDelete = async () => {
|
|||||||
<h2>{stats.botMessages}</h2>
|
<h2>{stats.botMessages}</h2>
|
||||||
<p>AI RESPONSE</p>
|
<p>AI RESPONSE</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.statCard} onClick={() => setModalContent(<FollowUps data={followUps} />)}>
|
<StatCard followUps={followUps} setModalContent={setModalContent} />
|
||||||
<h2>{followUps.length}</h2>
|
|
||||||
<p>BOOKING REQUEST</p>
|
|
||||||
</div>
|
|
||||||
<div className={styles.statCard} onClick={openTopicsModal}>
|
<div className={styles.statCard} onClick={openTopicsModal}>
|
||||||
<h2 style={{ fontSize: '17px' }}>{discussedTopics[0]?.topic}</h2>
|
<h2 style={{ fontSize: '17px' }}>{discussedTopics[0]?.topic}</h2>
|
||||||
<p>Top topic</p>
|
<p>Top topic</p>
|
||||||
@@ -585,7 +629,7 @@ const handleBatchDelete = async () => {
|
|||||||
|
|
||||||
{selectedFiles.length > 0 &&
|
{selectedFiles.length > 0 &&
|
||||||
selectedFiles.map((file, index) => (
|
selectedFiles.map((file, index) => (
|
||||||
<div>
|
<div key={index}>
|
||||||
<div key={index} className={styles.fileInfo}>
|
<div key={index} className={styles.fileInfo}>
|
||||||
<strong>{file.name}</strong>
|
<strong>{file.name}</strong>
|
||||||
</div>
|
</div>
|
||||||
@@ -626,7 +670,7 @@ const handleBatchDelete = async () => {
|
|||||||
|
|
||||||
|
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
© 2025 Kediri Technopark
|
© 2025 Dermalounge
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{modalContent && <Modal onClose={() => setModalContent(null)}>{modalContent}</Modal>}
|
{modalContent && <Modal onClose={() => setModalContent(null)}>{modalContent}</Modal>}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@
|
|||||||
.fileUpload {
|
.fileUpload {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: white;
|
color: #333;
|
||||||
background: #2bb438;
|
background: #2bb438;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
@@ -1,16 +1,37 @@
|
|||||||
|
|
||||||
// DiscussedTopics.js
|
// DiscussedTopics.js
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import styles from './DiscussedTopics.module.css';
|
||||||
|
|
||||||
const DiscussedTopics = ({ topics }) => {
|
const DiscussedTopics = ({ topics }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={styles.container}>
|
||||||
<h2>Top Topic</h2>
|
<div className={styles.header}>
|
||||||
<ul>
|
<h2 className={styles.title}>Top Topic</h2>
|
||||||
{topics.map((topic, idx) => (
|
<div className={styles.resultCount}>
|
||||||
<li key={idx}><strong>{topic.topic}</strong> - {topic.count} x</li>
|
{topics.length} topik
|
||||||
))}
|
</div>
|
||||||
</ul>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.grid}>
|
||||||
|
{topics.length === 0 ? (
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
<div className={styles.emptyIcon}>💬</div>
|
||||||
|
<p>Discussed Topic Is Empty</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
topics.map((topic, idx) => (
|
||||||
|
<div key={idx} className={styles.card}>
|
||||||
|
<div className={styles.cardContent}>
|
||||||
|
<h3 className={styles.topicName}>{topic.topic}</h3>
|
||||||
|
<div className={styles.countBadge}>
|
||||||
|
<span className={styles.countValue}>{topic.count}</span>
|
||||||
|
<span className={styles.countLabel}>times</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
187
src/DiscussedTopics.module.css
Normal file
187
src/DiscussedTopics.module.css
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
/* DiscussedTopics.module.css */
|
||||||
|
.container {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultCount {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards Grid */
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContent {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topicName {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countBadge {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 3px solid #0b7366;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countValue {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0b7366;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countLabel {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.emptyState {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyIcon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyState p {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 16px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContent {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topicName {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countValue {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContent {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topicName {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countBadge {
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countValue {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countLabel {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
194
src/FollowUps.js
194
src/FollowUps.js
@@ -1,36 +1,194 @@
|
|||||||
import React from 'react';
|
// FollowUps.js
|
||||||
|
import React, { useState } from 'react';
|
||||||
import styles from './FollowUps.module.css';
|
import styles from './FollowUps.module.css';
|
||||||
|
|
||||||
const FollowUps = ({ data }) => {
|
const FollowUps = ({ data: initialData }) => {
|
||||||
|
const [data, setData] = useState(initialData);
|
||||||
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
|
const [dateFilter, setDateFilter] = useState('all');
|
||||||
|
const [sortOrder, setSortOrder] = useState('latest');
|
||||||
|
|
||||||
|
const handleFollowUp = async (e, user) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch('https://bot.kediritechnopark.com/webhook/set-follow-up', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: user.id,
|
||||||
|
isfollowup: user.isfollowup ? 'success' : 'followup'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user.isfollowup) {
|
||||||
|
setData(prev =>
|
||||||
|
prev.map(u =>
|
||||||
|
u.id === user.id ? { ...u, isfollowup: false, issuccess: true } : u
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setData(prev =>
|
||||||
|
prev.map(u =>
|
||||||
|
u.id === user.id ? { ...u, isfollowup: true } : u
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.isfollowup) {
|
||||||
|
window.open(`https://api.whatsapp.com/send?phone=${user.contact_info}`, '_blank');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set follow-up:', error);
|
||||||
|
alert('Failed to send follow-up status.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter & Sort
|
||||||
|
const now = new Date();
|
||||||
|
const filteredData = data
|
||||||
|
.filter(user => {
|
||||||
|
switch (statusFilter) {
|
||||||
|
case 'pending':
|
||||||
|
return !user.isfollowup && !user.issuccess;
|
||||||
|
case 'inProgress':
|
||||||
|
return user.isfollowup && !user.issuccess;
|
||||||
|
case 'success':
|
||||||
|
return user.issuccess;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(user => {
|
||||||
|
const created = new Date(user.created_at);
|
||||||
|
switch (dateFilter) {
|
||||||
|
case 'today':
|
||||||
|
return created.toDateString() === now.toDateString();
|
||||||
|
case 'week':
|
||||||
|
const aWeekAgo = new Date();
|
||||||
|
aWeekAgo.setDate(now.getDate() - 7);
|
||||||
|
return created >= aWeekAgo;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const dateA = new Date(a.created_at);
|
||||||
|
const dateB = new Date(b.created_at);
|
||||||
|
return sortOrder === 'latest' ? dateB - dateA : dateA - dateB;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
{/* Filter Controls */}
|
||||||
|
<div className={styles.filterSection}>
|
||||||
|
<div className={styles.filterGroup}>
|
||||||
|
<div className={styles.filterItem}>
|
||||||
|
<label className={styles.filterLabel}>Status</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={e => setStatusFilter(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="all">All Statuses</option>
|
||||||
|
<option value="pending">Pending</option>
|
||||||
|
<option value="inProgress">In Progress</option>
|
||||||
|
<option value="success">Followed Up</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterItem}>
|
||||||
|
<label className={styles.filterLabel}>Period</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={dateFilter}
|
||||||
|
onChange={e => setDateFilter(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="all">All Time</option>
|
||||||
|
<option value="today">Today</option>
|
||||||
|
<option value="week">Last 7 Days</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterItem}>
|
||||||
|
<label className={styles.filterLabel}>Sort By</label>
|
||||||
|
<select
|
||||||
|
className={styles.filterSelect}
|
||||||
|
value={sortOrder}
|
||||||
|
onChange={e => setSortOrder(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="latest">Latest</option>
|
||||||
|
<option value="oldest">Oldest</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.resultCount}>
|
||||||
|
{filteredData.length} of {data.length} records
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cards Grid */}
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
{data.map(user => (
|
{filteredData.length === 0 ? (
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
<div className={styles.emptyIcon}>📋</div>
|
||||||
|
<p>No data matches the filters</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredData.map(user => (
|
||||||
<div key={user.id} className={styles.card}>
|
<div key={user.id} className={styles.card}>
|
||||||
<div className={styles.header}>
|
<div className={styles.cardHeader}>
|
||||||
<h3>{user.name}</h3>
|
<h3 className={styles.userName}>{user.name}</h3>
|
||||||
<span className={styles.date}>
|
<span className={styles.statusBadge}>
|
||||||
|
{user.issuccess ? (
|
||||||
|
<span className={styles.badgeSuccess}>✓ Followed Up</span>
|
||||||
|
) : user.isfollowup ? (
|
||||||
|
<span className={styles.badgeProgress}>⏳ In Progress</span>
|
||||||
|
) : (
|
||||||
|
<span className={styles.badgePending}>• Pending</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.cardContent}>
|
||||||
|
<p className={styles.notes}>{user.notes}</p>
|
||||||
|
<div className={styles.contactInfo}>
|
||||||
|
<span className={styles.contactLabel}>Contact:</span>
|
||||||
|
<span className={styles.contactValue}>{user.contact_info}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.dateInfo}>
|
||||||
{new Date(user.created_at).toLocaleString('id-ID', {
|
{new Date(user.created_at).toLocaleString('id-ID', {
|
||||||
dateStyle: 'medium',
|
dateStyle: 'medium',
|
||||||
timeStyle: 'short',
|
timeStyle: 'short',
|
||||||
timeZone: 'Asia/Jakarta'
|
timeZone: 'Asia/Jakarta'
|
||||||
})}
|
})}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p className={styles.notes}>{user.notes}</p>
|
</div>
|
||||||
<div className={styles.footer}>
|
|
||||||
<span className={styles.contact}>{user.contact_info}</span>
|
<div className={styles.cardActions}>
|
||||||
<a
|
<button
|
||||||
className={styles.chatBtn}
|
className={`${styles.actionBtn} ${
|
||||||
href={`https://api.whatsapp.com/send?phone=${user.contact_info}`}
|
user.issuccess
|
||||||
target="_blank"
|
? styles.btnSuccess
|
||||||
rel="noopener noreferrer"
|
: user.isfollowup
|
||||||
|
? styles.btnComplete
|
||||||
|
: styles.btnPrimary
|
||||||
|
}`}
|
||||||
|
onClick={(e) => handleFollowUp(e, user)}
|
||||||
>
|
>
|
||||||
Chat WhatsApp
|
{user.issuccess
|
||||||
</a>
|
? '✓ Follow-up Success'
|
||||||
|
: user.isfollowup
|
||||||
|
? '✓ Mark as Complete'
|
||||||
|
: '💬 Chat on WhatsApp'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,110 +1,316 @@
|
|||||||
|
/* FollowUps.module.css */
|
||||||
.container {
|
.container {
|
||||||
background-color: #f7f9fa;
|
background-color: #f8fafc;
|
||||||
font-family: 'Amazon Ember', sans-serif;
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
/* Filter Section */
|
||||||
font-size: 22px;
|
.filterSection {
|
||||||
font-weight: 600;
|
background: white;
|
||||||
color: #16191f;
|
border-radius: 12px;
|
||||||
margin-bottom: 20px;
|
padding: 20px;
|
||||||
text-align: center;
|
margin-bottom: 24px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filterGroup {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: end;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterItem {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterLabel {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterSelect {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: white;
|
||||||
|
color: #374151;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
|
||||||
|
background-position: right 8px center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 16px;
|
||||||
|
padding-right: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterSelect:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterSelect:hover {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultCount {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards Grid */
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
gap: 16px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background-color: #ffffff;
|
background: white;
|
||||||
border: 1px solid #d1d5db;
|
border-radius: 12px;
|
||||||
border-radius: 8px;
|
border: 1px solid #e2e8f0;
|
||||||
padding: 16px;
|
overflow: hidden;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
transition: all 0.2s ease;
|
||||||
display: flex;
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
transition: box-shadow 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
border-color: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.cardHeader {
|
||||||
|
padding: 20px 20px 12px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-wrap: wrap;
|
gap: 12px;
|
||||||
gap: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header h3 {
|
.userName {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #232f3e;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.statusBadge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeSuccess {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeProgress {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgePending {
|
||||||
|
background: #f3f4f6;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
white-space: nowrap;
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContent {
|
||||||
|
padding: 12px 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notes {
|
.notes {
|
||||||
margin: 12px 0;
|
color: #4b5563;
|
||||||
color: #374151;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.contactInfo {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
align-items: center;
|
gap: 4px;
|
||||||
flex-wrap: wrap;
|
margin-bottom: 12px;
|
||||||
gap: 8px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact {
|
.contactLabel {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: #374151;
|
color: #6b7280;
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatBtn {
|
|
||||||
background-color: #25d366;
|
|
||||||
color: #ffffff;
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatBtn:hover {
|
.contactValue {
|
||||||
background-color: #1da851;
|
font-size: 14px;
|
||||||
|
color: #374151;
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateInfo {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardActions {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #f1f5f9;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionBtn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrimary {
|
||||||
|
background: #25d366;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrimary:hover {
|
||||||
|
background: #1da851;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnComplete {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnComplete:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSuccess {
|
||||||
|
background: #10b981;
|
||||||
|
color: white;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.emptyState {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyIcon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyState p {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterSection {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterGroup {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterItem {
|
||||||
|
min-width: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardHeader {
|
||||||
|
padding: 16px 16px 10px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContent {
|
||||||
|
padding: 10px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardActions {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionBtn {
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.title {
|
.container {
|
||||||
font-size: 18px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.userName {
|
||||||
padding: 12px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notes {
|
.notes {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatBtn {
|
.statusBadge span {
|
||||||
padding: 6px 10px;
|
font-size: 11px;
|
||||||
font-size: 12px;
|
padding: 3px 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ const Login = () => {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
© 2025 Kediri Technopark
|
© 2025 Dermalounge
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,10 +16,9 @@
|
|||||||
background: white;
|
background: white;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
width: 70%;
|
width: 85%;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,25 @@ export default function NotificationPrompt({ onAllow, onDismiss }) {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.backdrop}>
|
<div className={styles.backdrop}>
|
||||||
<div className={styles.modal}>
|
<div className={styles.modal}>
|
||||||
<h2 className={styles.title}>Enable Notifications</h2>
|
<div className={styles.iconWrapper}>
|
||||||
|
<div className={styles.notificationIcon}>🔔</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.content}>
|
||||||
|
<h2 className={styles.title}>Aktifkan Notifikasi</h2>
|
||||||
<p className={styles.description}>
|
<p className={styles.description}>
|
||||||
Stay up to date with important updates and alerts. Enable push notifications to never miss a thing.
|
Tetap terhubung dengan update penting dan peringatan terbaru.
|
||||||
|
Aktifkan notifikasi push agar tidak ketinggalan informasi penting.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<button onClick={onAllow} className={styles.primaryButton}>
|
<button onClick={onAllow} className={styles.primaryButton}>
|
||||||
Enable Notifications
|
<span className={styles.buttonIcon}>✓</span>
|
||||||
|
Aktifkan Notifikasi
|
||||||
</button>
|
</button>
|
||||||
<button onClick={onDismiss} className={styles.secondaryButton}>
|
<button onClick={onDismiss} className={styles.secondaryButton}>
|
||||||
Maybe Later
|
Nanti Saja
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,35 +1,67 @@
|
|||||||
|
/* NotificationPrompt.module.css */
|
||||||
.backdrop {
|
.backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background-color: rgba(0, 0, 0, 0.45);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
|
animation: fadeIn 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconWrapper {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notificationIcon {
|
||||||
|
font-size: 48px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
font-size: 22px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
margin-bottom: 24px;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
line-height: 1.5;
|
line-height: 1.6;
|
||||||
|
max-width: 340px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
@@ -39,30 +71,129 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.primaryButton {
|
.primaryButton {
|
||||||
background-color: #0073bb;
|
background: #3b82f6;
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 12px 20px;
|
padding: 14px 24px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
font-size: 15px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primaryButton:hover {
|
.primaryButton:hover {
|
||||||
background-color: #005a99;
|
background: #2563eb;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primaryButton:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonIcon {
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondaryButton {
|
.secondaryButton {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: #374151;
|
color: #6b7280;
|
||||||
border: 1px solid #d1d5db;
|
border: 2px solid #e2e8f0;
|
||||||
padding: 12px 20px;
|
padding: 12px 24px;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondaryButton:hover {
|
.secondaryButton:hover {
|
||||||
background-color: #f3f4f6;
|
background-color: #f8fafc;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
color: #374151;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryButton:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.modal {
|
||||||
|
padding: 24px 20px;
|
||||||
|
margin: 16px;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconWrapper {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notificationIcon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primaryButton,
|
||||||
|
.secondaryButton {
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 320px) {
|
||||||
|
.modal {
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
48
src/StatCard.js
Normal file
48
src/StatCard.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import styles from './StatCard.module.css';
|
||||||
|
import FollowUps from './FollowUps';
|
||||||
|
|
||||||
|
const StatCard = ({ followUps, setModalContent }) => {
|
||||||
|
const [activeIndex, setActiveIndex] = useState(0); // 0 = booking request, 1 = sukses
|
||||||
|
const [direction, setDirection] = useState('right'); // for animation direction
|
||||||
|
|
||||||
|
const views = [
|
||||||
|
{
|
||||||
|
label: 'BOOKING REQUEST',
|
||||||
|
data: followUps.filter(u => !u.isfollowup && !u.issuccess),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'FOLLOWED UP',
|
||||||
|
data: followUps.filter(u => u.issuccess),
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Swipe timer
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setDirection(prev => (prev === 'right' ? 'left' : 'right'));
|
||||||
|
setActiveIndex(prev => 1 - prev);
|
||||||
|
}, 4000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
setModalContent(<FollowUps data={followUps} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.statCard} onClick={handleClick}>
|
||||||
|
<div
|
||||||
|
key={activeIndex} // re-render to trigger animation
|
||||||
|
className={`${styles.cardContent} ${
|
||||||
|
direction === 'right' ? styles.slideInRight : styles.slideInLeft
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<h2>{views[activeIndex].data.length}</h2>
|
||||||
|
<p>{views[activeIndex].label}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatCard;
|
||||||
57
src/StatCard.module.css
Normal file
57
src/StatCard.module.css
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
|
||||||
|
.statCard {
|
||||||
|
overflow: hidden;
|
||||||
|
background: #ece5dd;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.05);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statCard h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #075e54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statCard p {
|
||||||
|
margin: 5px 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.cardContent {
|
||||||
|
position: relative;
|
||||||
|
animation-duration: 0.6s;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInLeft {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slideInRight {
|
||||||
|
animation-name: slideInRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slideInLeft {
|
||||||
|
animation-name: slideInLeft;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user