From fe7d67a2d87b9c6beef3bdd781234025a1204b2e Mon Sep 17 00:00:00 2001 From: Vassshhh Date: Wed, 2 Jul 2025 17:32:13 +0700 Subject: [PATCH] ok --- src/Dashboard.js | 258 +++++++++++++---------- src/Dashboard.module.css | 2 +- src/DiscussedTopics.js | 37 +++- src/DiscussedTopics.module.css | 187 +++++++++++++++++ src/FollowUps.js | 214 ++++++++++++++++--- src/FollowUps.module.css | 332 ++++++++++++++++++++++++------ src/Login.js | 2 +- src/Modal.module.css | 3 +- src/NotificationPrompt.js | 23 ++- src/NotificationPrompt.module.css | 165 +++++++++++++-- src/StatCard.js | 48 +++++ src/StatCard.module.css | 57 +++++ 12 files changed, 1094 insertions(+), 234 deletions(-) create mode 100644 src/DiscussedTopics.module.css create mode 100644 src/StatCard.js create mode 100644 src/StatCard.module.css diff --git a/src/Dashboard.js b/src/Dashboard.js index 08401c6..132b946 100644 --- a/src/Dashboard.js +++ b/src/Dashboard.js @@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom'; import Modal from './Modal'; import Conversations from './Conversations'; import DiscussedTopics from './DiscussedTopics'; +import StatCard from './StatCard' import FollowUps from './FollowUps'; @@ -36,14 +37,60 @@ const Dashboard = () => { const navigate = useNavigate(); - const handleFiles = (files) => { - const newFiles = files.filter(file => { - // Hindari duplikat berdasarkan nama (atau bisa pakai hash/md5 jika perlu) - return !selectedFiles.some(f => f.name === file.name); +const handleFiles = async (files) => { + const filteredFiles = []; + + 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]); + } +}; + + @@ -364,89 +411,89 @@ const Dashboard = () => { } } }; -const handleBatchUpload = async () => { - const token = localStorage.getItem('token'); - const newFiles = []; + const handleBatchUpload = async () => { + const token = localStorage.getItem('token'); + const newFiles = []; - for (const file of selectedFiles) { - const formData = new FormData(); - formData.append('file', file); + for (const file of selectedFiles) { + const formData = new FormData(); + formData.append('file', file); - const response = await fetch('https://bot.kediritechnopark.com/webhook/files/upload', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}` - }, - body: formData - }); - - if (response.ok) { - newFiles.push({ - json: { - Key: file.name, - LastModified: new Date().toISOString(), - Size: file.size, - StorageClass: 'STANDARD' - } + const response = await fetch('https://bot.kediritechnopark.com/webhook/files/upload', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}` + }, + body: formData }); - } else { - console.error(`Upload gagal untuk file ${file.name}`); - } - } - - // ✅ Set fileList sekaligus - setFileList((prev) => [...prev, ...newFiles]); - alert('Upload selesai'); - setSelectedFiles([]); -}; - - - -const handleBatchDelete = async () => { - if (!window.confirm(`Yakin ingin menghapus ${selectedKeys.length} file?`)) return; - - const token = localStorage.getItem('token'); - const successKeys = []; - const failedKeys = []; - - for (const key of selectedKeys) { - try { - const response = await fetch( - `https://bot.kediritechnopark.com/webhook/files/delete?key=${encodeURIComponent(key)}`, - { - method: 'DELETE', - headers: { - 'Authorization': `Bearer ${token}` - } - } - ); if (response.ok) { - successKeys.push(key); + newFiles.push({ + json: { + Key: file.name, + LastModified: new Date().toISOString(), + Size: file.size, + StorageClass: 'STANDARD' + } + }); } else { + console.error(`Upload gagal untuk file ${file.name}`); + } + } + + // ✅ Set fileList sekaligus + setFileList((prev) => [...prev, ...newFiles]); + alert('Upload selesai'); + setSelectedFiles([]); + }; + + + + const handleBatchDelete = async () => { + if (!window.confirm(`Yakin ingin menghapus ${selectedKeys.length} file?`)) return; + + const token = localStorage.getItem('token'); + const successKeys = []; + const failedKeys = []; + + for (const key of selectedKeys) { + try { + const response = await fetch( + `https://bot.kediritechnopark.com/webhook/files/delete?key=${encodeURIComponent(key)}`, + { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}` + } + } + ); + + if (response.ok) { + successKeys.push(key); + } else { + failedKeys.push(key); + } + } catch (err) { + console.error(`Gagal menghapus ${key}`, err); failedKeys.push(key); } - } catch (err) { - console.error(`Gagal menghapus ${key}`, err); - failedKeys.push(key); } - } - // ✅ Update fileList sekaligus - setFileList((prev) => - prev.filter((file) => !successKeys.includes(file.json.Key)) - ); + // ✅ Update fileList sekaligus + setFileList((prev) => + prev.filter((file) => !successKeys.includes(file.json.Key)) + ); - // ✅ Kosongkan selected - setSelectedKeys([]); + // ✅ Kosongkan selected + setSelectedKeys([]); - // ✅ Beri feedback ke user - if (failedKeys.length === 0) { - alert('File berhasil dihapus.'); - } else { - alert(`Sebagian gagal dihapus:\n${failedKeys.join('\n')}`); - } -}; + // ✅ Beri feedback ke user + if (failedKeys.length === 0) { + alert('File berhasil dihapus.'); + } else { + alert(`Sebagian gagal dihapus:\n${failedKeys.join('\n')}`); + } + }; // ⬇️ Jika masih loading, tampilkan full white screen @@ -491,10 +538,7 @@ const handleBatchDelete = async () => {

{stats.botMessages}

AI RESPONSE

-
setModalContent()}> -

{followUps.length}

-

BOOKING REQUEST

-
+

{discussedTopics[0]?.topic}

Top topic

@@ -581,33 +625,33 @@ const handleBatchDelete = async () => {

document.getElementById("fileInput").click()}>Click to upload

-
- - {selectedFiles.length > 0 && - selectedFiles.map((file, index) => ( -
-
- {file.name} -
-
- setSelectedFiles((prev) => prev.filter((_, i) => i !== index)) - } - > - X -
-
-))} +
-{selectedFiles.length > 0 && -
-
handleBatchUpload()} className={styles.fileUpload}> - Upload -
-
- } + {selectedFiles.length > 0 && + selectedFiles.map((file, index) => ( +
+
+ {file.name} +
+
+ setSelectedFiles((prev) => prev.filter((_, i) => i !== index)) + } + > + X +
+
+ ))} + + {selectedFiles.length > 0 && +
+
handleBatchUpload()} className={styles.fileUpload}> + Upload +
+ } +
{
- © 2025 Kediri Technopark + © 2025 Dermalounge
{modalContent && setModalContent(null)}>{modalContent}} diff --git a/src/Dashboard.module.css b/src/Dashboard.module.css index e0f1c9e..ddb0eb4 100644 --- a/src/Dashboard.module.css +++ b/src/Dashboard.module.css @@ -137,7 +137,7 @@ .fileUpload { margin-top: 16px; font-size: 14px; - color: white; + color: #333; background: #2bb438; padding: 8px 12px; border-radius: 8px; diff --git a/src/DiscussedTopics.js b/src/DiscussedTopics.js index 723b10d..7735ced 100644 --- a/src/DiscussedTopics.js +++ b/src/DiscussedTopics.js @@ -1,16 +1,37 @@ - // DiscussedTopics.js import React from 'react'; +import styles from './DiscussedTopics.module.css'; const DiscussedTopics = ({ topics }) => { return ( -
-

Top Topic

-
    - {topics.map((topic, idx) => ( -
  • {topic.topic} - {topic.count} x
  • - ))} -
+
+
+

Top Topic

+
+ {topics.length} topik +
+
+ +
+ {topics.length === 0 ? ( +
+
💬
+

Discussed Topic Is Empty

+
+ ) : ( + topics.map((topic, idx) => ( +
+
+

{topic.topic}

+
+ {topic.count} + times +
+
+
+ )) + )} +
); }; diff --git a/src/DiscussedTopics.module.css b/src/DiscussedTopics.module.css new file mode 100644 index 0000000..86ae0e5 --- /dev/null +++ b/src/DiscussedTopics.module.css @@ -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; + } +} \ No newline at end of file diff --git a/src/FollowUps.js b/src/FollowUps.js index 16d177a..6b6cf83 100644 --- a/src/FollowUps.js +++ b/src/FollowUps.js @@ -1,36 +1,194 @@ -import React from 'react'; +// FollowUps.js +import React, { useState } from 'react'; 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 (
-
- {data.map(user => ( -
-
-

{user.name}

- - {new Date(user.created_at).toLocaleString('id-ID', { - dateStyle: 'medium', - timeStyle: 'short', - timeZone: 'Asia/Jakarta' - })} - -
-

{user.notes}

-
- {user.contact_info} - - Chat WhatsApp - -
+ {/* Filter Controls */} +
+
+
+ +
- ))} + +
+ + +
+ +
+ + +
+
+ +
+ {filteredData.length} of {data.length} records +
+
+ + {/* Cards Grid */} +
+ {filteredData.length === 0 ? ( +
+
📋
+

No data matches the filters

+
+ ) : ( + filteredData.map(user => ( +
+
+

{user.name}

+ + {user.issuccess ? ( + ✓ Followed Up + ) : user.isfollowup ? ( + ⏳ In Progress + ) : ( + • Pending + )} + +
+ +
+

{user.notes}

+
+ Contact: + {user.contact_info} +
+
+ {new Date(user.created_at).toLocaleString('id-ID', { + dateStyle: 'medium', + timeStyle: 'short', + timeZone: 'Asia/Jakarta' + })} +
+
+ +
+ +
+
+ )) + )}
); diff --git a/src/FollowUps.module.css b/src/FollowUps.module.css index 8b88130..9c566c4 100644 --- a/src/FollowUps.module.css +++ b/src/FollowUps.module.css @@ -1,110 +1,316 @@ +/* FollowUps.module.css */ .container { - background-color: #f7f9fa; - font-family: 'Amazon Ember', sans-serif; + background-color: #f8fafc; + min-height: 100vh; + padding: 20px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; } -.title { - font-size: 22px; - font-weight: 600; - color: #16191f; - margin-bottom: 20px; - text-align: center; +/* Filter Section */ +.filterSection { + 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); } +.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 { display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 16px; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 20px; } .card { - background-color: #ffffff; - border: 1px solid #d1d5db; - border-radius: 8px; - padding: 16px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - display: flex; - flex-direction: column; - justify-content: space-between; - transition: box-shadow 0.2s ease; + 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 { - 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; justify-content: space-between; align-items: flex-start; - flex-wrap: wrap; - gap: 4px; + gap: 12px; } -.header h3 { +.userName { font-size: 18px; + font-weight: 600; + color: #1f2937; 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-weight: 500; +} + +.badgeProgress { + background: #fef3c7; + color: #92400e; + padding: 4px 8px; + border-radius: 6px; + font-size: 12px; + font-weight: 500; +} + +.badgePending { + background: #f3f4f6; color: #6b7280; - white-space: nowrap; + padding: 4px 8px; + border-radius: 6px; + font-size: 12px; + font-weight: 500; +} + +.cardContent { + padding: 12px 20px 20px; } .notes { - margin: 12px 0; - color: #374151; + color: #4b5563; font-size: 14px; + line-height: 1.5; + margin: 0 0 16px 0; + word-break: break-word; } -.footer { +.contactInfo { display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 8px; - margin-top: 8px; + flex-direction: column; + gap: 4px; + margin-bottom: 12px; } -.contact { - font-size: 13px; - color: #374151; - word-break: break-all; -} - -.chatBtn { - background-color: #25d366; - color: #ffffff; - text-decoration: none; - padding: 6px 12px; - border-radius: 4px; - font-size: 13px; +.contactLabel { + font-size: 12px; + color: #6b7280; font-weight: 500; - transition: background-color 0.2s ease; - white-space: nowrap; } -.chatBtn:hover { - background-color: #1da851; +.contactValue { + 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) { - .title { - font-size: 18px; - } - - .card { + .container { padding: 12px; } - + + .userName { + font-size: 16px; + } + .notes { font-size: 13px; } - - .chatBtn { - padding: 6px 10px; - font-size: 12px; + + .statusBadge span { + font-size: 11px; + padding: 3px 6px; } -} +} \ No newline at end of file diff --git a/src/Login.js b/src/Login.js index c5c8db9..c306e11 100644 --- a/src/Login.js +++ b/src/Login.js @@ -69,7 +69,7 @@ const Login = () => {
- © 2025 Kediri Technopark + © 2025 Dermalounge
diff --git a/src/Modal.module.css b/src/Modal.module.css index ef1ce36..0e2476e 100644 --- a/src/Modal.module.css +++ b/src/Modal.module.css @@ -16,10 +16,9 @@ background: white; border-radius: 10px; max-width: 700px; - width: 70%; + width: 85%; max-height: 80vh; overflow-y: auto; - padding: 20px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); position: relative; } diff --git a/src/NotificationPrompt.js b/src/NotificationPrompt.js index 3bc4984..45cc6e7 100644 --- a/src/NotificationPrompt.js +++ b/src/NotificationPrompt.js @@ -5,19 +5,28 @@ export default function NotificationPrompt({ onAllow, onDismiss }) { return (
-

Enable Notifications

-

- Stay up to date with important updates and alerts. Enable push notifications to never miss a thing. -

+
+
🔔
+
+ +
+

Aktifkan Notifikasi

+

+ Tetap terhubung dengan update penting dan peringatan terbaru. + Aktifkan notifikasi push agar tidak ketinggalan informasi penting. +

+
+
); -} +} \ No newline at end of file diff --git a/src/NotificationPrompt.module.css b/src/NotificationPrompt.module.css index 5e26da1..fafd9fc 100644 --- a/src/NotificationPrompt.module.css +++ b/src/NotificationPrompt.module.css @@ -1,35 +1,67 @@ +/* NotificationPrompt.module.css */ .backdrop { position: fixed; inset: 0; - background-color: rgba(0, 0, 0, 0.45); + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); display: flex; justify-content: center; align-items: center; z-index: 9999; + animation: fadeIn 0.2s ease-out; } .modal { background-color: #fff; - border-radius: 8px; + border-radius: 12px; padding: 32px; max-width: 420px; 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; + 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 { margin-bottom: 16px; - font-size: 22px; + font-size: 24px; font-weight: 600; color: #1f2937; + line-height: 1.3; } .description { - margin-bottom: 24px; + margin: 0; font-size: 16px; color: #4b5563; - line-height: 1.5; + line-height: 1.6; + max-width: 340px; + margin: 0 auto; } .actions { @@ -39,30 +71,129 @@ } .primaryButton { - background-color: #0073bb; + background: #3b82f6; color: white; font-weight: 500; - padding: 12px 20px; + padding: 14px 24px; border: none; - border-radius: 6px; + border-radius: 8px; 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 { - 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 { background-color: transparent; - color: #374151; - border: 1px solid #d1d5db; - padding: 12px 20px; - border-radius: 6px; + color: #6b7280; + border: 2px solid #e2e8f0; + padding: 12px 24px; + border-radius: 8px; cursor: pointer; - transition: background-color 0.2s; + font-size: 15px; + font-weight: 500; + transition: all 0.2s ease; + font-family: inherit; } .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; + } +} \ No newline at end of file diff --git a/src/StatCard.js b/src/StatCard.js new file mode 100644 index 0000000..7a4df2c --- /dev/null +++ b/src/StatCard.js @@ -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(); + }; + + return ( +
+
+

{views[activeIndex].data.length}

+

{views[activeIndex].label}

+
+
+ ); +}; + +export default StatCard; diff --git a/src/StatCard.module.css b/src/StatCard.module.css new file mode 100644 index 0000000..cfbf42f --- /dev/null +++ b/src/StatCard.module.css @@ -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; +}