Slicing UI

This commit is contained in:
Rizqika 2024-11-25 17:51:46 +07:00
parent 231c637fa9
commit 392a8a08fb
54 changed files with 8055 additions and 76 deletions

29
package-lock.json generated
View File

@ -19,6 +19,7 @@
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-drag-drop-files": "^2.4.0", "react-drag-drop-files": "^2.4.0",
"react-icons": "^5.3.0", "react-icons": "^5.3.0",
@ -5973,6 +5974,15 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
}, },
"node_modules/copy-to-clipboard": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
"integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
"license": "MIT",
"dependencies": {
"toggle-selection": "^1.0.6"
}
},
"node_modules/core-js": { "node_modules/core-js": {
"version": "3.39.0", "version": "3.39.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz",
@ -13344,6 +13354,19 @@
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
}, },
"node_modules/react-copy-to-clipboard": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
"integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
"license": "MIT",
"dependencies": {
"copy-to-clipboard": "^3.3.1",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"react": "^15.3.0 || 16 || 17 || 18"
}
},
"node_modules/react-dev-utils": { "node_modules/react-dev-utils": {
"version": "12.0.1", "version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
@ -15541,6 +15564,12 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
"license": "MIT"
},
"node_modules/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",

View File

@ -14,6 +14,7 @@
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-drag-drop-files": "^2.4.0", "react-drag-drop-files": "^2.4.0",
"react-icons": "^5.3.0", "react-icons": "^5.3.0",

View File

@ -13,53 +13,101 @@ import {
CreateApps CreateApps
} from './screens/Home'; } from './screens/Home';
import { import {
FaceVerify, Verify as FaceVerify,
FaceSummary, Summary as FaceSummary,
FaceTransaction Transaction as FaceTransaction
} from './screens/Biometric/FaceRecognition'; } from './screens/Biometric/FaceRecognition';
import { import {
Enroll, Enroll,
VerifySection, Verify as VerifySection,
Liveness, Liveness,
Compare, Compare,
Search Search
} from './screens/Biometric/FaceRecognition/Section'; } from './screens/Biometric/FaceRecognition/Section';
import { import {
ManageKtp, Manage as ManageKtp,
SummaryKtp, Summary as SummaryKtp,
VerifyKtp, Verify as VerifyKtp,
TransactionKtp Transaction as TransactionKtp
} from './screens/Biometric/OcrKtp'; } from './screens/Biometric/OcrKtp';
import { import {
SummaryNpwp, Summary as SummaryNpwp,
TransactionNpwp, Transaction as TransactionNpwp,
VerifyNpwp Verify as VerifyNpwp
} from './screens/Biometric/OcrNpwp'; } from './screens/Biometric/OcrNpwp';
import { import {
VerifySim, Verify as VerifySim,
SummarySim, Summary as SummarySim,
TransactionSim Transaction as TransactionSim
} from './screens/Biometric/OcrSim'; } from './screens/Biometric/OcrSim';
import { import {
VerifyDoc, Verify as VerifyDoc,
SummaryDoc, Summary as SummaryDoc,
TransactionDoc Transaction as TransactionDoc
} from './screens/Biometric/OcrDocument'; } from './screens/Biometric/OcrDocument';
import { import {
SmsVerify Verify as SmsVerify
} from './screens/Sms/Verification'; } from './screens/Sms/Verification';
import { import {
VerificationAnnoncement, Annoncement as VerificationAnnoncement,
VerificationOtp Otp as VerificationOtp
} from './screens/Sms/Verification/Section'; } from './screens/Sms/Verification/Section';
import {
Settings as SmsOtpSettings,
Summary as SmsOtpSummary,
Transaction as SmsOtpTransaction,
Detail as SmsOtpDetail
} from './screens/Sms/OtpManagement';
import {
Transaction as SmsOtpSummaryTransaction,
Announcement as SmsOtpSummaryAnnouncement
} from './screens/Sms/OtpManagement/Section';
import {
BulkMessage as SmsAnnouncementBulk,
Summary as SmsAnnouncementSummary,
Transaction as SmsAnnouncementTransaction
} from './screens/Sms/Announcements';
import {
Transaction as SmsAnnouncementSummaryTransaction,
Announcement as SmsAnnouncementSummaryAnnouncement
} from './screens/Sms/Announcements/Section';
import {
Blocked as BlockedSms,
BlockNumber
} from './screens/Sms/BlockedNumbers';
import {
Anomaly as AnomalySms
} from './screens/Sms/AnomalyReport';
import {
Verify as VerifyWa
} from './screens/Wa/Verify';
import {
Auth as AuthWa,
Message as MessageWa
} from './screens/Wa/Verify/Section';
import {
Registration as RegistrationWa,
Profile as ProfileWa,
Template as TemplateWa,
Integration as IntegrationWa
} from './screens/Wa/Manage';
import { CreateSettings } from './screens/Wa/Manage/Content';
const App = () => { const App = () => {
return ( return (
@ -89,12 +137,6 @@ const App = () => {
{/* Default route */} {/* Default route */}
<Route index element={<Navigate to="face-enroll" />} /> <Route index element={<Navigate to="face-enroll" />} />
</Route> </Route>
{/* Add routes for the verify section */}
{/* <Route path="/face-enroll" element={<Enroll />} />
<Route path="/face-verifysection" element={<VerifySection />} />
<Route path="/face-liveness" element={<Liveness />} />
<Route path="/face-compare" element={<Compare />} />
<Route path="/face-search" element={<Search />} /> */}
{/* Biometric - Face Recognition (Summary) */} {/* Biometric - Face Recognition (Summary) */}
<Route path="/face-summary" element={<FaceSummary />} /> <Route path="/face-summary" element={<FaceSummary />} />
@ -130,6 +172,54 @@ const App = () => {
<Route index element={<Navigate to="sms-otp" />} /> <Route index element={<Navigate to="sms-otp" />} />
</Route> </Route>
{/* Sms Services - OTP (Settings) */}
<Route path="/sms-otp-settings" element={<SmsOtpSettings />} />
{/* Sms Services - OTP (Summary) */}
<Route path="/sms-otp-summary/*" element={<SmsOtpSummary />}>
{/* Anak rute */}
<Route path="sms-otp-transaction" element={<SmsOtpSummaryTransaction />} />
<Route path="sms-otp-announcement" element={<SmsOtpSummaryAnnouncement />} />
{/* Default route */}
<Route index element={<Navigate to="sms-otp-transaction" />} />
</Route>
{/* Sms Services - OTP (Transaction) */}
<Route path="/sms-otp-transaction" element={<SmsOtpTransaction />} />
{/* Sms Services - OTP (Detail) */}
<Route path="/sms-otp-detail" element={<SmsOtpDetail />} />
{/* Sms Services - Announcements */}
<Route path="/sms-announcement-bulk" element={<SmsAnnouncementBulk />} />
<Route path="/sms-announcement-summary/*" element={<SmsAnnouncementSummary />}>
{/* Anak rute */}
<Route path="sms-announcement-transaction" element={<SmsAnnouncementSummaryTransaction />} />
<Route path="sms-announcement-announcement" element={<SmsAnnouncementSummaryAnnouncement />} />
{/* Default route */}
<Route index element={<Navigate to="sms-announcement-transaction" />} />
</Route>
<Route path="/sms-announcement-transaction" element={<SmsAnnouncementTransaction />} />
{/* Sms Services - Block Numbers */}
<Route path="/sms-block" element={<BlockedSms />}></Route>
<Route path="/block-number" element={<BlockNumber />} />
{/* Sms Services - Anomaly Report */}
<Route path="/sms-anomaly" element={<AnomalySms />}></Route>
{/* WhatsApp - Verify */}
<Route path="/wa-verify/*" element={<VerifyWa />}>
{/* Anak rute */}
<Route path="wa-auth" element={<AuthWa />} />
<Route path="wa-message" element={<MessageWa />} />
{/* Default route */}
<Route index element={<Navigate to="wa-auth" />} />
</Route>
{/* WhatsApp - Manage */}
<Route path="/wa-registration" element={<RegistrationWa />} />
<Route path="/wa-profile" element={<ProfileWa />} />
<Route path="/wa-template" element={<TemplateWa />} />
<Route path="/wa-integration" element={<IntegrationWa />} />
<Route path="/wa-createSettings" element={<CreateSettings />} />
{/* <Route path="/sms-otp-settings" element={<SmsOtpSettings />} /> */} {/* <Route path="/sms-otp-settings" element={<SmsOtpSettings />} /> */}
{/* Continue for each link */} {/* Continue for each link */}

View File

@ -11287,3 +11287,4 @@ body {
background-color: red; background-color: red;
font-size: 10px; font-size: 10px;
} }

BIN
src/assets/images/WAB-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src/assets/images/WAB-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -3,11 +3,15 @@ import Logo from './Logo.png';
import DashboardImg from './dashboard-img.png'; import DashboardImg from './dashboard-img.png';
import DummyKtp from './Dummy-Ktp.png'; import DummyKtp from './Dummy-Ktp.png';
import ServerDownAnimation from './server-down.gif'; import ServerDownAnimation from './server-down.gif';
import WAB_1 from './WAB-1.png';
import WAB_2 from './WAB-2.png';
export { export {
ProfileImage, ProfileImage,
Logo, Logo,
DashboardImg, DashboardImg,
DummyKtp, DummyKtp,
ServerDownAnimation ServerDownAnimation,
WAB_1,
WAB_2
} }

View File

@ -137,10 +137,10 @@ const dataMenu = [
name: 'WhatsApp Management', // Changed the name name: 'WhatsApp Management', // Changed the name
target: 'collapseWaManage', target: 'collapseWaManage',
subMenus: [ subMenus: [
{ name: 'Register Business Account', link: '/wa-manage-register'}, // Changed the name { name: 'Register Business Account', link: '/wa-registration'}, // Changed the name
{ name: 'WhatsApp Profile Settings', link: '/wa-manage-profile'}, // Changed the name { name: 'WhatsApp Profile Settings', link: '/wa-profile'}, // Changed the name
{ name: 'Message Templates', link: '/wa-manage-template'}, // Changed the name { name: 'Message Templates', link: '/wa-template'}, // Changed the name
{ name: 'Integration Settings', link: '/wa-manage-integration'}, // Changed the name { name: 'Integration Settings', link: '/wa-integration'}, // Changed the name
], ],
}, },
{ {

View File

@ -1,12 +1,12 @@
import Enroll from "./Enroll"; import Enroll from "./Enroll";
import VerifySection from "./Verify"; import Verify from "./Verify";
import Compare from "./Compare" import Compare from "./Compare"
import Liveness from "./Liveness" import Liveness from "./Liveness"
import Search from "./Search" import Search from "./Search"
export { export {
Enroll, Enroll,
VerifySection, Verify,
Compare, Compare,
Liveness, Liveness,
Search Search

View File

@ -5,7 +5,7 @@ import {
Compare, Compare,
Liveness, Liveness,
Search, Search,
VerifySection Verify as VerifySection
} from './Section'; } from './Section';
const Verify = () => { const Verify = () => {

View File

@ -1,9 +1,9 @@
import FaceVerify from "./Verify"; import Verify from "./Verify";
import FaceSummary from "./Summary"; import Summary from "./Summary";
import FaceTransaction from "./Transaction"; import Transaction from "./Transaction";
export { export {
FaceVerify, Verify,
FaceSummary, Summary,
FaceTransaction Transaction
} }

View File

@ -1,9 +1,9 @@
import VerifyDoc from "./Verify"; import Verify from "./Verify";
import SummaryDoc from "./Summary"; import Summary from "./Summary";
import TransactionDoc from "./Transaction"; import Transaction from "./Transaction";
export { export {
VerifyDoc, Verify,
SummaryDoc, Summary,
TransactionDoc Transaction
} }

View File

@ -353,7 +353,8 @@ const Verify = () => {
color: '#721c24', color: '#721c24',
fontSize: '14px', fontSize: '14px',
margin: '0', margin: '0',
},loadingOverlay: { },
loadingOverlay: {
position: 'fixed', // Gunakan fixed untuk overlay penuh layar position: 'fixed', // Gunakan fixed untuk overlay penuh layar
top: 0, top: 0,
left: 0, left: 0,

View File

@ -1,11 +1,11 @@
import VerifyKtp from "./Verify"; import Verify from "./Verify";
import ManageKtp from "./Manage"; import Manage from "./Manage";
import SummaryKtp from "./Summary"; import Summary from "./Summary";
import TransactionKtp from "./Transaction"; import Transaction from "./Transaction";
export { export {
VerifyKtp, Verify,
ManageKtp, Manage,
SummaryKtp, Summary,
TransactionKtp Transaction
} }

View File

@ -1,9 +1,9 @@
import VerifyNpwp from "./Verify"; import Verify from "./Verify";
import SummaryNpwp from "./Summary"; import Summary from "./Summary";
import TransactionNpwp from "./Transaction"; import Transaction from "./Transaction";
export { export {
VerifyNpwp, Verify,
SummaryNpwp, Summary,
TransactionNpwp Transaction
} }

View File

@ -1,9 +1,9 @@
import VerifySim from "./Verify"; import Verify from "./Verify";
import SummarySim from "./Summary"; import Summary from "./Summary";
import TransactionSim from "./Transaction"; import Transaction from "./Transaction";
export { export {
VerifySim, Verify,
SummarySim, Summary,
TransactionSim Transaction
} }

View File

@ -0,0 +1,295 @@
import React, { useState, useEffect } from 'react'
import { CloudDownload } from '../../../assets/icon';
const Bulk = () => {
const BASE_URL = process.env.REACT_APP_BASE_URL
const API_KEY = process.env.REACT_APP_API_KEY
const [selectedImageName, setSelectedImageName] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [applicationId, setApplicationId] = useState('');
const [applicationIds, setApplicationIds] = useState([]);
const [isSelectOpen, setIsSelectOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768); // Set mobile state based on window width
};
handleResize(); // Initial check on component mount
window.addEventListener('resize', handleResize); // Listen for resize events
return () => {
window.removeEventListener('resize', handleResize); // Cleanup on component unmount
};
}, []);
useEffect(() => {
const fetchApplicationIds = async () => {
try {
const url = `${BASE_URL}/application/list`;
console.log('Fetching URL:', url); // Log the URL
const response = await fetch(url, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': `${API_KEY}`,
},
});
const data = await response.json();
if (data.status_code === 200) {
const ids = data.details.data.map(app => app.id);
console.log('Application Id: ' + ids); // Log the IDs
setApplicationIds(data.details.data); // Update state with the fetched data
} else {
console.error('Failed to fetch data:', data.details.message);
}
} catch (error) {
console.error('Error fetching application IDs:', error);
}
};
fetchApplicationIds();
}, []);
const handleFocus = () => {
setIsSelectOpen(true);
};
const handleBlur = () => {
setIsSelectOpen(false);
};
const handleFileUpload = (event) => {
const file = event.target.files[0];
if (file && (file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg')) {
setSelectedImageName(file.name);
setErrorMessage('');
} else {
alert('Please upload a valid image file (JPG, JPEG, PNG).');
}
};
return (
<div style={isMobile ? styles.containerMobile : styles.container}>
<div style={{ ...styles.statusBox, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<img src={CloudDownload} alt="Download Icon" style={styles.iconStyle} />
<div style={{ marginLeft: '10px' }}>
<h3 style={styles.statusTitle}>Check Our Service Status</h3>
<p style={{ margin: '0' }}>
Access pre-approved message templates designed for various purposes, including customer support, appointment reminders, order updates, and more.
Simplify your communication by using these ready-to-go formats.
</p>
</div>
</div>
<a href="#" style={{ ...styles.buttonStyle, backgroundColor: '#fff', color: '#0542cc', display: 'flex', alignItems: 'center', padding: '10px 15px', borderRadius: '5px' }}>
<i className="fas fa-download" style={{ marginRight: '5px' }}></i>
Download Template
</a>
</div>
<div style={styles.formContainer}>
<div
style={{
...styles.formRow,
flexDirection: isMobile ? 'column' : 'row', // Set to column in mobile mode
}}
>
<div style={{ ...styles.formColumn, marginRight: isMobile ? '0' : '20px' }}>
<label htmlFor="appId" style={styles.label}>Application ID</label>
<select
id="applicationId"
className="form-control"
style={styles.select}
value={applicationId}
onChange={(e) => setApplicationId(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
>
<option value="">Select Application ID</option>
{applicationIds.map((app) => (
<option key={app.id} value={app.id}>
{app.name}
</option>
))}
</select>
<label htmlFor="messageInfo" style={styles.label}>Message Info</label>
<textarea
id="messageInfo"
rows="5"
placeholder="Type your message here..."
style={styles.textarea}
/>
<p style={styles.charCounter}>Character: 0 (Max: 459), SMS Count: 0</p>
</div>
<div
style={{
...styles.uploadBox,
marginBottom: isMobile ? '20px' : '0', // Add margin for spacing in mobile
}}
>
<p style={{ fontWeight: 'bold', color: '#666' }}>Upload Template</p>
<div
style={styles.uploadArea}
onClick={() => document.getElementById('fileUpload').click()}
>
<i className="fas fa-cloud-upload-alt" style={styles.uploadIcon}></i>
<p style={styles.uploadText}>Drag and Drop Here</p>
<p>Or</p>
<button style={styles.browseButton}>Browse</button>
<p style={styles.charCounter}>Accepted file: .csv<br />Maximum file size: 5MB</p>
</div>
<input
type="file"
id="fileUpload"
style={{ display: 'none' }}
accept=".csv"
onChange={handleFileUpload}
/>
</div>
</div>
<button style={styles.sendButton}>Send Bulk SMS</button>
</div>
</div>
);
};
export default Bulk;
const styles = {
container: {
padding: '20px',
fontFamily: 'Arial, sans-serif',
},
containerMobile: {
padding: '10px',
fontFamily: 'Arial, sans-serif',
},
statusBox: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
marginBottom: '1rem',
},
iconStyle: {
width: '36px',
height: '36px',
},
buttonStyle: {
textDecoration: 'none',
border: 'none',
cursor: 'pointer',
transition: 'background-color 0.3s',
padding: '10px 15px',
borderRadius: '5px',
},
statusTitle: {
color: '#0066cc',
fontWeight: '600',
},
formContainer: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
marginBottom: '1rem',
},
formRow: {
display: 'flex',
justifyContent: 'space-between',
flexWrap: 'wrap', // Allow wrapping for smaller screens
},
formColumn: {
flex: 1,
marginRight: '20px',
marginBottom: '20px', // Add spacing between rows in mobile
},
uploadBox: {
flex: 1,
textAlign: 'center',
padding: '20px',
marginBottom: '1rem',
},
label: {
fontWeight: 'bold',
display: 'block',
margin: '0.5rem 0',
},
select: {
width: '100%',
padding: '8px',
borderRadius: '4px',
border: '1px solid #ccc',
},
textarea: {
width: '100%',
padding: '8px',
borderRadius: '4px',
border: '1px solid #ccc',
marginTop: '10px',
height: '150px', // Adjust height for mobile
},
uploadArea: {
backgroundColor: '#e6f2ff',
cursor: 'pointer',
padding: '15px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
border: '1px solid #ced4da',
borderRadius: '0.25rem',
height: 'auto', // Let content dictate height
minHeight: '200px',
},
uploadIcon: {
fontSize: '36px',
color: '#0542cc',
marginBottom: '7px',
},
uploadText: {
color: '#1f2d3d',
fontWeight: '400',
fontSize: '14px',
lineHeight: '13px',
textAlign: 'center',
},
browseButton: {
color: '#0066cc',
border: 'none',
background: 'none',
cursor: 'pointer',
marginTop: '5px',
},
charCounter: {
fontSize: '12px',
color: '#888',
marginTop: '5px',
},
sendButton: {
backgroundColor: '#0066cc',
color: 'white',
padding: '10px 20px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginTop: '20px',
width: '100%', // Full width for mobile
maxWidth: '200px', // Limited width for larger screens
alignSelf: 'center',
},
};

View File

@ -0,0 +1,265 @@
import React, { useState, useEffect } from 'react';
import {
Extract,
Transaction as TransactionIcon,
} from '../../../../assets/icon';
const Announcement = () => {
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [application, setApplication] = useState('');
const [isMobile, setIsMobile] = useState(false);
const menuData = [
{ id: 1, value: '150', label: 'Total Transaction', icon: TransactionIcon },
{ id: 2, value: '4', label: 'Total Extract', icon: Extract },
];
const handleApply = () => {
console.log({ startDate, endDate, application });
};
const handleCancel = () => {
setStartDate('');
setEndDate('');
setApplication('');
};
// Detect if the device is mobile based on window width
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768); // Define mobile screen as width <= 768px
};
// Initialize on component mount
handleResize();
// Add event listener to update state on resize
window.addEventListener('resize', handleResize);
// Cleanup event listener on component unmount
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<div style={styles.container}>
{/* Welcome Message */}
<div
className="row-card border-left border-primary shadow mb-4"
style={{ backgroundColor: '#E2FBEA' }}
>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<div>
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
</div>
</div>
</div>
<div style={styles.summaryContainer}>
<div style={isMobile ? styles.filtersMobile : styles.filters}>
{/* Row 1: Start Date and End Date */}
<div style={styles.filterRow}>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
style={styles.filterInput}
placeholder="Start Date"
/>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
style={styles.filterInput}
placeholder="End Date"
/>
</div>
{/* Row 2: Application Select */}
<div style={styles.filterRow}>
<select
value={application}
onChange={(e) => setApplication(e.target.value)}
style={styles.filterInput}
>
<option value="">Application</option>
<option value="App1">App1</option>
<option value="App2">App2</option>
</select>
</div>
{/* Row 3: Apply and Cancel Buttons */}
<div style={styles.filterRow}>
<button style={styles.applyButton} onClick={handleApply}>
Apply
</button>
<button style={styles.cancelButton} onClick={handleCancel}>
Cancel
</button>
</div>
</div>
<div style={isMobile ? styles.summaryCardsMobile : styles.summaryCards}>
{menuData.map((item) => (
<div key={item.id} style={styles.card}>
<div style={styles.textContainer}>
<h2 style={styles.cardTitle}>{item.value}</h2>
<p style={styles.cardText}>{item.label}</p>
</div>
<div style={styles.iconContainer}>
<img src={item.icon} alt={item.label} style={styles.icon} />
</div>
</div>
))}
</div>
</div>
</div>
);
};
const styles = {
container: {
margin: '3rem',
},
filters: {
display: 'flex',
justifyContent: 'space-between',
marginBottom: '20px',
flexDirection: 'row', // Horizontal layout on desktop
gap: '10px',
},
filtersMobile: {
display: 'flex',
flexDirection: 'column', // Vertical layout on mobile
gap: '15px',
},
filterRow: {
display: 'flex',
justifyContent: 'space-between',
gap: '10px',
width: '100%',
},
filterInput: {
padding: '10px',
border: '1px solid #ccc',
borderRadius: '5px',
fontSize: '16px',
width: '48%', // Adjust width to take up 48% of the row for input elements
},
applyButton: {
backgroundColor: '#0542cc',
color: '#fff',
padding: '10px 20px',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
width: '48%', // Adjust button width to take half of the row
},
cancelButton: {
backgroundColor: '#fff',
color: '#000',
padding: '10px 20px',
border: '1px solid gray',
borderRadius: '5px',
cursor: 'pointer',
width: '48%', // Adjust button width to take half of the row
},
summaryContainer: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
},
summaryCards: {
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)', // 4 columns on desktop
gap: '20px',
marginTop: '20px',
},
summaryCardsMobile: {
display: 'grid',
gridTemplateColumns: '1fr', // 1 column on mobile (full width)
gap: '15px',
marginTop: '20px',
},
card: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: '15px', // Reduced padding for a more compact card
backgroundColor: 'white',
borderRadius: '8px', // Slightly smaller border-radius for a sharper look
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
textAlign: 'left',
minWidth: '200px', // Ensure minimum width for mobile responsiveness
maxWidth: '300px', // Limit the maximum width to prevent cards from becoming too wide
width: '100%',
transition: 'transform 0.3s ease', // Smooth transition for hover effects
},
cardHover: {
transform: 'scale(1.05)', // Slight zoom on hover for interactivity
},
iconContainer: {
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
},
textContainer: {
display: 'flex',
flexDirection: 'column',
},
icon: {
width: '36px', // Smaller icon size for a more compact card
height: '36px', // Matching height for the icon
},
cardTitle: {
fontSize: '18px', // Smaller font size for title to prevent overflow
margin: '0',
fontWeight: 'bold',
},
cardText: {
fontSize: '12px', // Smaller text size for description to keep it balanced
color: '#555',
},
// Optional: Hover effect for the cards to enhance interactivity
cardHover: {
transform: 'scale(1.05)', // Slight zoom effect when hovering over card
},
// Media query for smaller screens (e.g., phones in portrait mode)
"@media (max-width: 768px)": {
summaryCards: {
gridTemplateColumns: '1fr', // 1 column on mobile screens
},
card: {
padding: '10px', // Smaller padding for smaller screens
minWidth: '180px', // Adjust min width for mobile devices
maxWidth: '250px', // Adjust max width for mobile devices
},
icon: {
width: '30px', // Slightly smaller icon size for mobile
height: '30px', // Matching height for mobile icon
},
cardTitle: {
fontSize: '16px', // Smaller font size for the card title
},
cardText: {
fontSize: '10px', // Smaller font size for card description
},
},
};
export default Announcement;

View File

@ -0,0 +1,271 @@
import React, { useState, useEffect } from 'react';
import {
Extract,
Quality,
AsyncIcon,
QualityAsync,
Transaction as TransactionIcon,
} from '../../../../assets/icon';
const Transaction = () => {
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [application, setApplication] = useState('');
const [isMobile, setIsMobile] = useState(false);
const menuData = [
{ id: 1, value: '150', label: 'Total Transaction', icon: TransactionIcon },
{ id: 2, value: '4', label: 'Total Extract', icon: Extract },
{ id: 3, value: '65', label: 'Total Quality', icon: Quality },
{ id: 6, value: '900', label: 'Total Extract Async', icon: AsyncIcon },
{ id: 4, value: '22', label: 'Total Quality Async', icon: QualityAsync },
];
const handleApply = () => {
console.log({ startDate, endDate, application });
};
const handleCancel = () => {
setStartDate('');
setEndDate('');
setApplication('');
};
// Detect if the device is mobile based on window width
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768); // Define mobile screen as width <= 768px
};
// Initialize on component mount
handleResize();
// Add event listener to update state on resize
window.addEventListener('resize', handleResize);
// Cleanup event listener on component unmount
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<div style={styles.container}>
{/* Welcome Message */}
<div
className="row-card border-left border-primary shadow mb-4"
style={{ backgroundColor: '#E2FBEA' }}
>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<div>
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
</div>
</div>
</div>
<div style={styles.summaryContainer}>
<div style={isMobile ? styles.filtersMobile : styles.filters}>
{/* Row 1: Start Date and End Date */}
<div style={styles.filterRow}>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
style={styles.filterInput}
placeholder="Start Date"
/>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
style={styles.filterInput}
placeholder="End Date"
/>
</div>
{/* Row 2: Application Select */}
<div style={styles.filterRow}>
<select
value={application}
onChange={(e) => setApplication(e.target.value)}
style={styles.filterInput}
>
<option value="">Application</option>
<option value="App1">App1</option>
<option value="App2">App2</option>
</select>
</div>
{/* Row 3: Apply and Cancel Buttons */}
<div style={styles.filterRow}>
<button style={styles.applyButton} onClick={handleApply}>
Apply
</button>
<button style={styles.cancelButton} onClick={handleCancel}>
Cancel
</button>
</div>
</div>
<div style={isMobile ? styles.summaryCardsMobile : styles.summaryCards}>
{menuData.map((item) => (
<div key={item.id} style={styles.card}>
<div style={styles.textContainer}>
<h2 style={styles.cardTitle}>{item.value}</h2>
<p style={styles.cardText}>{item.label}</p>
</div>
<div style={styles.iconContainer}>
<img src={item.icon} alt={item.label} style={styles.icon} />
</div>
</div>
))}
</div>
</div>
</div>
);
};
const styles = {
container: {
margin: '3rem',
},
filters: {
display: 'flex',
justifyContent: 'space-between',
marginBottom: '20px',
flexDirection: 'row', // Horizontal layout on desktop
gap: '10px',
},
filtersMobile: {
display: 'flex',
flexDirection: 'column', // Vertical layout on mobile
gap: '15px',
},
filterRow: {
display: 'flex',
justifyContent: 'space-between',
gap: '10px',
width: '100%',
},
filterInput: {
padding: '10px',
border: '1px solid #ccc',
borderRadius: '5px',
fontSize: '16px',
width: '48%', // Adjust width to take up 48% of the row for input elements
},
applyButton: {
backgroundColor: '#0542cc',
color: '#fff',
padding: '10px 20px',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
width: '48%', // Adjust button width to take half of the row
},
cancelButton: {
backgroundColor: '#fff',
color: '#000',
padding: '10px 20px',
border: '1px solid gray',
borderRadius: '5px',
cursor: 'pointer',
width: '48%', // Adjust button width to take half of the row
},
summaryContainer: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
},
summaryCards: {
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)', // 4 columns on desktop
gap: '20px',
marginTop: '20px',
},
summaryCardsMobile: {
display: 'grid',
gridTemplateColumns: '1fr', // 1 column on mobile (full width)
gap: '15px',
marginTop: '20px',
},
card: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: '15px', // Reduced padding for a more compact card
backgroundColor: 'white',
borderRadius: '8px', // Slightly smaller border-radius for a sharper look
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
textAlign: 'left',
minWidth: '200px', // Ensure minimum width for mobile responsiveness
maxWidth: '300px', // Limit the maximum width to prevent cards from becoming too wide
width: '100%',
transition: 'transform 0.3s ease', // Smooth transition for hover effects
},
cardHover: {
transform: 'scale(1.05)', // Slight zoom on hover for interactivity
},
iconContainer: {
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
},
textContainer: {
display: 'flex',
flexDirection: 'column',
},
icon: {
width: '36px', // Smaller icon size for a more compact card
height: '36px', // Matching height for the icon
},
cardTitle: {
fontSize: '18px', // Smaller font size for title to prevent overflow
margin: '0',
fontWeight: 'bold',
},
cardText: {
fontSize: '12px', // Smaller text size for description to keep it balanced
color: '#555',
},
// Optional: Hover effect for the cards to enhance interactivity
cardHover: {
transform: 'scale(1.05)', // Slight zoom effect when hovering over card
},
// Media query for smaller screens (e.g., phones in portrait mode)
"@media (max-width: 768px)": {
summaryCards: {
gridTemplateColumns: '1fr', // 1 column on mobile screens
},
card: {
padding: '10px', // Smaller padding for smaller screens
minWidth: '180px', // Adjust min width for mobile devices
maxWidth: '250px', // Adjust max width for mobile devices
},
icon: {
width: '30px', // Slightly smaller icon size for mobile
height: '30px', // Matching height for mobile icon
},
cardTitle: {
fontSize: '16px', // Smaller font size for the card title
},
cardText: {
fontSize: '10px', // Smaller font size for card description
},
},
};
export default Transaction;

View File

@ -0,0 +1,7 @@
import Transaction from "./Transaction";
import Announcement from "./Announcement";
export {
Transaction,
Announcement
}

View File

@ -0,0 +1,114 @@
import React, { useEffect, useState } from 'react';
import { Link, Routes, Route, useNavigate } from 'react-router-dom';
import {
Transaction as SmsAnnouncementSummaryTransaction,
Announcement as SmsAnnouncementSummaryAnnouncement
} from './Section';
const Summary = () => {
const verifyTabs = [
{ name: 'Transaction', link: 'sms-announcement-transaction' },
{ name: 'Announcement', link: 'sms-announcement-announcement' },
];
const [isMobile, setIsMobile] = useState(false);
const navigate = useNavigate();
// Redirect otomatis ke rute default saat akses ke /sms-announcement-transaction
useEffect(() => {
if (window.location.pathname === '/sms-announcement-summary') {
navigate('sms-announcement-transaction', { replace: true });
}
}, [navigate]);
// Update state isMobile berdasarkan ukuran layar
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<div className="container" style={styles.container}>
{/* Static Content */}
<div className="row-card border-left border-primary shadow mb-4" style={styles.welcomeCard}>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
<div className="d-flex flex-row mt-3">
<Link to="/createApps" style={{ textDecoration: 'none' }}>
<button className="btn d-flex justify-content-center align-items-center me-2" style={styles.createButton}>
<i className="fas fa-plus text-white me-2"></i>
<p className="text-white mb-0">Create New App ID</p>
</button>
</Link>
</div>
</div>
</div>
{/* Tab Navigation */}
<div style={styles.section}>
<div className={`d-flex ${isMobile ? 'flex-column' : 'flex-row'} justify-content-between align-items-center mb-3`}>
<div className="d-flex flex-wrap">
{verifyTabs.map((tab) => (
<Link
key={tab.link}
to={tab.link}
className={`btn ${window.location.pathname.includes(tab.link) ? 'btn-primary' : 'btn-light'} me-2 mb-2`}
style={styles.tabLink}
>
{tab.name}
</Link>
))}
</div>
</div>
{/* Dynamic Tab Content */}
<div className="tab-content">
<Routes>
<Route path="sms-announcement-transaction" element={<SmsAnnouncementSummaryTransaction />} />
<Route path="sms-announcement-announcement" element={<SmsAnnouncementSummaryAnnouncement />} />
</Routes>
</div>
</div>
</div>
);
};
export default Summary;
const styles = {
container: {
marginTop: '3%',
padding: '0 15px',
},
welcomeCard: {
backgroundColor: '#E2FBEA',
borderLeft: '4px solid #0542CC',
borderRadius: '5px',
marginBottom: '20px',
},
createButton: {
backgroundColor: '#0542CC',
},
section: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542CC',
borderRadius: '10px',
width: '100%',
},
tabLink: {
padding: '10px 20px',
},
};

View File

@ -0,0 +1,454 @@
import React, { useState, useEffect } from 'react';
import { FaChevronLeft, FaChevronRight, FaFastBackward, FaFastForward, FaSort, FaSortUp, FaSortDown } from 'react-icons/fa'; // Icons for sorting
import { NoAvailable } from '../../../assets/icon';
// Pagination Component
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
const handlePrev = () => {
if (currentPage > 1) {
onPageChange(currentPage - 1);
}
};
const handleNext = () => {
if (currentPage < totalPages) {
onPageChange(currentPage + 1);
}
};
const handleFirst = () => {
onPageChange(1); // Go to first page
};
const handleLast = () => {
onPageChange(totalPages); // Go to last page
};
// Logic to display only 3 pages in pagination
const getPaginationRange = () => {
const range = [];
const totalPagesCount = totalPages;
let start = currentPage - 1;
let end = currentPage + 1;
// Adjust start and end if near the boundaries
if (currentPage === 1) {
start = 1;
end = Math.min(3, totalPagesCount);
} else if (currentPage === totalPages) {
start = Math.max(totalPagesCount - 2, 1);
end = totalPagesCount;
}
for (let i = start; i <= end; i++) {
range.push(i);
}
return range;
};
const pageRange = getPaginationRange();
return (
<div className="pagination-container d-flex justify-content-end mt-4">
{/* First Page Button */}
<button
className="btn"
onClick={handleFirst}
disabled={currentPage === 1}
>
<FaFastBackward /> {/* Double Arrow Left */}
</button>
<button
className="btn"
onClick={handlePrev}
disabled={currentPage === 1}
>
<FaChevronLeft /> {/* Single Arrow Left */}
</button>
{/* Page Numbers */}
{pageRange.map((pageNum) => (
<button
key={pageNum}
className={`btn ${pageNum === currentPage ? 'btn-primary' : ''}`}
onClick={() => onPageChange(pageNum)}
>
{pageNum}
</button>
))}
<button
className="btn"
onClick={handleNext}
disabled={currentPage === totalPages}
>
<FaChevronRight /> {/* Single Arrow Right */}
</button>
{/* Last Page Button */}
<button
className="btn"
onClick={handleLast}
disabled={currentPage === totalPages}
>
<FaFastForward /> {/* Double Arrow Right */}
</button>
</div>
);
};
const Transaction = () => {
const [currentPage, setCurrentPage] = useState(1);
const [isMobile, setIsMobile] = useState(false); // State to detect mobile view
const [transactionData, setTransactionData] = useState([]);
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state
const dataPerPage = 10; // Data per page (10 data per page)
const buttonData = [
{ label: 'Copy', enabled: true },
{ label: 'CSV', enabled: true },
{ label: 'Excel', enabled: true },
{ label: 'PDF', enabled: true },
{ label: 'Print', enabled: true },
{ label: 'Column Visibility', enabled: true },
];
// Generate 691 dummy transactions
const generateDummyData = (numOfItems) => {
const transactionData = [];
for (let i = 1; i <= numOfItems; i++) {
transactionData.push({
transactionId: `TX${String(i).padStart(3, '0')}`, // Transaction ID
applicationName: `App ${Math.floor(Math.random() * 5) + 1}`, // Application Name
phoneNumber: `+62${Math.floor(Math.random() * 900000000) + 800000000}`, // Random phone number
operator: ['Telkomsel', 'Indosat', 'XL', 'Tri', 'Smartfren'][Math.floor(Math.random() * 5)], // Random operator
senderId: `SENDER${String(i).padStart(3, '0')}`, // Sender ID
segment: ['Retail', 'Finance', 'Health', 'Education', 'Technology'][Math.floor(Math.random() * 5)], // Random segment
dateSent: new Date(2023, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toLocaleDateString(), // Random date
status: ['Completed', 'Pending', 'Failed'][Math.floor(Math.random() * 3)], // Random status
mode: Math.random() > 0.5 ? 'Online' : 'Offline', // Random mode
});
}
return transactionData;
};
// Set the generated transaction data
useEffect(() => {
setTransactionData(generateDummyData(31)); // count data dummy transactions
}, []);
// Sorting function
const sortData = (data, config) => {
const { key, direction } = config;
return [...data].sort((a, b) => {
if (a[key] < b[key]) {
return direction === 'asc' ? -1 : 1;
}
if (a[key] > b[key]) {
return direction === 'asc' ? 1 : -1;
}
return 0;
});
};
// Handle column header sort click
const handleSort = (key) => {
let direction = 'asc';
if (sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc'; // Toggle direction if the same column is clicked
}
setSortConfig({ key, direction });
};
// Get the paginated data
const getPaginatedData = (data, page, perPage) => {
const sortedData = sortData(data, sortConfig);
const startIndex = (page - 1) * perPage;
const endIndex = startIndex + perPage;
return sortedData.slice(startIndex, endIndex);
};
// Handle page change
const handlePageChange = (page) => {
setCurrentPage(page);
};
// Calculate total pages based on the data and data per page
const totalPages = Math.ceil(transactionData.length / dataPerPage);
// Paginated data
const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage);
// Detect screen size and update isMobile state
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768); // Change 768 to your breakpoint
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<div className="container mt-5">
{/* Welcome Message */}
<div className="row-card border-left border-primary shadow mb-4" style={{ backgroundColor: '#E2FBEA' }}>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<div>
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
</div>
</div>
</div>
<div style={styles.contentContainer}>
{/* Filter Form */}
<div className="card p-3 mb-4">
<div className="row">
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Start Date</label>
<input type="date" className="form-control" />
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>End Date</label>
<input type="date" className="form-control" />
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Application</label>
<select className="form-control">
<option>Select Application</option>
<option>App 1</option>
<option>App 2</option>
<option>App 3</option>
<option>App 4</option>
<option>App 5</option>
</select>
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Operator</label>
<select className="form-control">
<option>Select Operator</option>
<option>Telkomsel</option>
<option>Indosat</option>
<option>XL</option>
<option>Tri</option>
<option>Smartfren</option>
</select>
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Status</label>
<select className="form-control">
<option>Select Status</option>
<option>Completed</option>
<option>Pending</option>
<option>Failed</option>
</select>
</div>
<div className={`col-12 ${isMobile ? 'd-flex justify-content-between' : 'col-md-2 d-flex align-items-end'}`} style={{ gap: '10px' }}>
<button className="btn btn-primary w-48">Apply</button>
<button className="btn btn-secondary w-48">Cancel</button>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="d-flex justify-content-between align-items-center mb-3">
<div>
{buttonData.map((button, index) =>
button.enabled ? (
<button
key={index}
className={`btn btn-light ${isMobile ? 'mb-2' : ''}`} // Add margin on mobile
style={styles.actionButton}
>
{button.label}
</button>
) : null
)}
</div>
{/* Search Bar with Icon */}
<div className="input-group" style={{ width: '250px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
<input
type="text"
placeholder="Search..."
className="form-control"
/>
<span className="input-group-text">
<i className="fas fa-search"></i> {/* FontAwesome search icon */}
</span>
</div>
</div>
{/* Table */}
<div className="table-responsive">
<table className="table table-bordered" style={styles.tableContainer}>
<thead>
<tr>
<th>No.</th> {/* Kolom untuk Nomor Urut */}
<th>
<button className="btn text-sm" onClick={() => handleSort('transactionId')}>
Transaction ID
{sortConfig.key === 'transactionId' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'transactionId' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn text-sm" onClick={() => handleSort('applicationName')}>
Application Name
{sortConfig.key === 'applicationName' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'applicationName' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn text-sm" onClick={() => handleSort('phoneNumber')}>
Phone Number
{sortConfig.key === 'phoneNumber' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'phoneNumber' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn text-sm" onClick={() => handleSort('operator')}>
Operator
{sortConfig.key === 'operator' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'operator' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn text-sm" onClick={() => handleSort('senderId')}>
Sender ID
{sortConfig.key === 'senderId' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'senderId' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn text-sm" onClick={() => handleSort('segment')}>
Segment
{sortConfig.key === 'segment' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'segment' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn text-sm" onClick={() => handleSort('dateSent')}>
Date Sent
{sortConfig.key === 'dateSent' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'dateSent' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn text-sm" onClick={() => handleSort('status')}>
Status
{sortConfig.key === 'status' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'status' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn text-sm" onClick={() => handleSort('mode')}>
Mode
{sortConfig.key === 'mode' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'mode' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
</tr>
</thead>
<tbody>
{paginatedData.length > 0 ? (
paginatedData.map((transaction, index) => (
<tr key={index}>
{/* Kolom Nomor Urut */}
<td>{(currentPage - 1) * dataPerPage + index + 1}</td>
<td>{transaction.transactionId}</td>
<td>{transaction.applicationName}</td>
<td>{transaction.phoneNumber}</td>
<td>{transaction.operator}</td>
<td>{transaction.senderId}</td>
<td>{transaction.segment}</td>
<td>{transaction.dateSent}</td>
<td>{transaction.status}</td>
<td>{transaction.mode}</td>
</tr>
))
) : (
<tr>
<td colSpan="7" className="text-center">
<div className="d-flex flex-column align-items-center mt-5">
<img src={NoAvailable} alt="No Data Available" className="mb-3" style={styles.iconStyle} />
<p>Data not available</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
/>
</div>
</div>
);
};
export default Transaction;
const styles = {
contentContainer: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
},
tableContainer: {
minHeight: '300px',
maxHeight: '1500px',
overflowY: 'auto',
},
iconStyle: {
width: '50px',
height: '50px',
},
// Add margin-left style for icons
iconMarginLeft: {
marginRight: '-1rem', // Adjust as needed
},
};

View File

@ -0,0 +1,9 @@
import BulkMessage from './Bulk';
import Summary from './Summary';
import Transaction from './Transaction';
export {
BulkMessage,
Summary,
Transaction
}

View File

@ -0,0 +1,565 @@
import React, { useState, useEffect } from 'react';
import { FaChevronLeft, FaChevronRight, FaFastBackward, FaFastForward, FaSort, FaSortUp, FaSortDown } from 'react-icons/fa'; // Icons for sorting
import { NoAvailable } from '../../../assets/icon';
import Select from 'react-select'
import { ServerDownAnimation } from '../../../assets/images';
const BASE_URL = process.env.REACT_APP_BASE_URL;
const API_KEY = process.env.REACT_APP_API_KEY;
// Pagination Component
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
const handlePrev = () => {
if (currentPage > 1) {
onPageChange(currentPage - 1);
}
};
const handleNext = () => {
if (currentPage < totalPages) {
onPageChange(currentPage + 1);
}
};
const handleFirst = () => {
onPageChange(1); // Go to first page
};
const handleLast = () => {
onPageChange(totalPages); // Go to last page
};
// Logic to display only 3 pages in pagination
const getPaginationRange = () => {
const range = [];
const totalPagesCount = totalPages;
let start = currentPage - 1;
let end = currentPage + 1;
// Adjust start and end if near the boundaries
if (currentPage === 1) {
start = 1;
end = Math.min(3, totalPagesCount);
} else if (currentPage === totalPages) {
start = Math.max(totalPagesCount - 2, 1);
end = totalPagesCount;
}
for (let i = start; i <= end; i++) {
range.push(i);
}
return range;
};
const pageRange = getPaginationRange();
return (
<div className="pagination-container d-flex justify-content-end mt-4">
{/* First Page Button */}
<button
className="btn"
onClick={handleFirst}
disabled={currentPage === 1}
>
<FaFastBackward /> {/* Double Arrow Left */}
</button>
<button
className="btn"
onClick={handlePrev}
disabled={currentPage === 1}
>
<FaChevronLeft /> {/* Single Arrow Left */}
</button>
{/* Page Numbers */}
{pageRange.map((pageNum) => (
<button
key={pageNum}
className={`btn ${pageNum === currentPage ? 'btn-primary' : ''}`}
onClick={() => onPageChange(pageNum)}
>
{pageNum}
</button>
))}
<button
className="btn"
onClick={handleNext}
disabled={currentPage === totalPages}
>
<FaChevronRight /> {/* Single Arrow Right */}
</button>
{/* Last Page Button */}
<button
className="btn"
onClick={handleLast}
disabled={currentPage === totalPages}
>
<FaFastForward /> {/* Double Arrow Right */}
</button>
</div>
);
};
const Anomaly = () => {
const [currentPage, setCurrentPage] = useState(1);
const [phoneNumber, setPhoneNumber] = useState('');
const [isMobile, setIsMobile] = useState(false); // State to detect mobile view
const [transactionData, setTransactionData] = useState([]);
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state
const dataPerPage = 10; // Data per page (10 data per page)
const [applicationId, setApplicationId] = useState('');
const [applicationIds, setApplicationIds] = useState([]);
const [inputValueApplication, setInputValueApplication] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isServer, setIsServer] = useState(true);
const [errorMessage, setErrorMessage] = useState('');
const applicationOptions = applicationIds.map(app => ({
value: app.id, // This is what will be sent when an option is selected
label: app.name // This is what will be displayed in the dropdown
}));
// Validation state
const [validationErrors, setValidationErrors] = useState({
applicationId: '',
file: ''
});
const handleApplicationChange = (selectedOption) => {
const selectedId = selectedOption.value;
const selectedApp = applicationIds.find(app => app.id === parseInt(selectedId));
setApplicationId(selectedOption ? selectedOption.value : '');
};
const handleInputChangeApplication = (inputValue) => {
setInputValueApplication(inputValue);
};
const buttonData = [
{ label: 'Copy', enabled: true },
{ label: 'CSV', enabled: true },
{ label: 'Excel', enabled: true },
{ label: 'PDF', enabled: true },
{ label: 'Print', enabled: true },
{ label: 'Column Visibility', enabled: true },
];
// Generate 691 dummy transactions
const generateDummyData = (numOfItems) => {
const transactionData = [];
for (let i = 1; i <= numOfItems; i++) {
transactionData.push({
date: new Date(2023, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toLocaleDateString(), // Random date
applicationId: `APP${String(Math.floor(Math.random() * 1000)).padStart(3, '0')}`, // Random Application ID
applicationName: `App ${Math.floor(Math.random() * 5) + 1}`, // Application Name
smsType: ['Promotional', 'Transactional'][Math.floor(Math.random() * 2)], // Random SMS Type
phoneNumber: `+62${Math.floor(Math.random() * 900000000) + 800000000}`, // Random phone number
tryCount: Math.floor(Math.random() * 5) + 1, // Random try count between 1 and 5
});
}
return transactionData;
};
useEffect(() => {
const fetchApplicationIds = async () => {
try {
setIsLoading(true);
const response = await fetch(`${BASE_URL}/application/list`, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': API_KEY,
},
});
if (!response.ok) {
throw new Error('Failed to fetch application IDs');
}
const data = await response.json();
console.log('Response Data:', data);
if (data.status_code === 200) {
setApplicationIds(data.details.data);
setIsServer(true);
} else {
setIsServer(false);
throw new Error(data.details.message || 'Failed to fetch application IDs');
}
} catch (error) {
setErrorMessage(error.message || 'Error fetching application IDs');
setIsServer(false);
} finally {
setIsLoading(false);
}
};
fetchApplicationIds();
setTransactionData(generateDummyData(31)); // count data dummy transactions
}, []);
// Sorting function
const sortData = (data, config) => {
const { key, direction } = config;
return [...data].sort((a, b) => {
if (a[key] < b[key]) {
return direction === 'asc' ? -1 : 1;
}
if (a[key] > b[key]) {
return direction === 'asc' ? 1 : -1;
}
return 0;
});
};
// Handle column header sort click
const handleSort = (key) => {
let direction = 'asc';
if (sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc'; // Toggle direction if the same column is clicked
}
setSortConfig({ key, direction });
};
// Get the paginated data
const getPaginatedData = (data, page, perPage) => {
const sortedData = sortData(data, sortConfig);
const startIndex = (page - 1) * perPage;
const endIndex = startIndex + perPage;
return sortedData.slice(startIndex, endIndex);
};
// Handle page change
const handlePageChange = (page) => {
setCurrentPage(page);
};
// Calculate total pages based on the data and data per page
const totalPages = Math.ceil(transactionData.length / dataPerPage);
// Paginated data
const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage);
// Detect screen size and update isMobile state
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768); // Change 768 to your breakpoint
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
if (!isServer) {
return (
<div style={{ textAlign: 'center', marginTop: '50px' }}>
<img
src={ServerDownAnimation}
alt="Server Down Animation"
style={{ width: '18rem', height: '18rem', marginBottom: '20px' }}
/>
<h2 style={{ color: 'red' }}>Server tidak dapat diakses</h2>
<p>{errorMessage || 'Silakan periksa koneksi internet Anda atau coba lagi nanti.'}</p>
<button
onClick={() => window.location.reload()}
style={{
padding: '10px 20px',
backgroundColor: '#0542cc',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer'
}}>
Coba Lagi
</button>
</div>
);
}
return (
<div className="container mt-5">
{/* Inject keyframes for the spinner */}
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
{isLoading && (
<div style={styles.loadingOverlay}>
<div style={styles.spinner}></div>
<p style={styles.loadingText}>Loading...</p>
</div>
)}
{/* Welcome Message */}
<div className="row-card border-left border-primary shadow mb-4" style={{ backgroundColor: '#E2FBEA' }}>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<div>
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
</div>
</div>
</div>
<div style={styles.contentContainer}>
<strong>Please input the phone number first to show data</strong>
<div className="col-md-8 mt-2">
<div className="d-flex align-items-center">
<div style={styles.selectWrapper}>
<Select
id="applicationId"
value={applicationOptions.find(option => option.value === applicationId)}
onChange={handleApplicationChange}
options={applicationOptions}
placeholder="Select Application ID"
isSearchable
menuPortalTarget={document.body}
menuPlacement="auto"
inputValue={inputValueApplication}
onInputChange={handleInputChangeApplication} // Limit input length for Application ID
/>
</div>
{validationErrors.applicationId && <p style={styles.errorText}>{validationErrors.applicationId}</p>}
<button className="btn text-white" style={{ backgroundColor: '#0542cc', marginLeft: '2vh', width: '20%' }}>Search</button>
</div>
</div>
</div>
<div style={styles.contentContainer}>
{/* Filter Form */}
<div className="card p-3 mb-4">
<div className="row">
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Start Date</label>
<input type="date" className="form-control" />
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>End Date</label>
<input type="date" className="form-control" />
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Services</label>
<select className="form-control">
<option>Select Services</option>
<option>Service 1</option>
<option>Service 2</option>
<option>Service 3</option>
<option>Service 4</option>
<option>Service 5</option>
</select>
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Feature</label>
<select className="form-control">
<option>Select Feature</option>
<option>Feature 1</option>
<option>Feature 2</option>
<option>Feature 3</option>
</select>
</div>
<div className={`col-12 ${isMobile ? 'd-flex justify-content-between' : 'col-md-2 d-flex align-items-end'}`} style={{ gap: '10px' }}>
<button className="btn btn-primary w-48">Apply</button>
<button className="btn btn-secondary w-48">Cancel</button>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="d-flex justify-content-between align-items-center mb-3">
<div>
{buttonData.map((button, index) =>
button.enabled ? (
<button
key={index}
className={`btn btn-light ${isMobile ? 'mb-2' : ''}`} // Add margin on mobile
style={styles.actionButton}
>
{button.label}
</button>
) : null
)}
</div>
{/* Search Bar with Icon */}
<div className="input-group" style={{ width: '250px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
<input
type="text"
placeholder="Search..."
className="form-control"
/>
<span className="input-group-text">
<i className="fas fa-search"></i> {/* FontAwesome search icon */}
</span>
</div>
</div>
{/* Table */}
<div className="table-responsive">
<table className="table table-bordered" style={styles.tableContainer}>
<thead>
<tr>
<th>No.</th> {/* Kolom untuk Nomor Urut */}
<th>
<button className="btn" onClick={() => handleSort('applicationId')}>
Application Id
{sortConfig.key === 'applicationId' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'applicationId' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('applicationName')}>
Application Name
{sortConfig.key === 'applicationName' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'applicationName' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('smsType')}>
SMS Type
{sortConfig.key === 'smsType' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'smsType' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('phoneNumber')}>
Phone Number
{sortConfig.key === 'phoneNumber' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'phoneNumber' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('tryCount')}>
Try Count
{sortConfig.key === 'tryCount' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'tryCount' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
</tr>
</thead>
<tbody>
{paginatedData.length > 0 ? (
paginatedData.map((transaction, index) => (
<tr key={index}>
{/* Kolom Nomor Urut */}
<td>{(currentPage - 1) * dataPerPage + index + 1}</td>
<td>{transaction.applicationId}</td>
<td>{transaction.applicationName}</td>
<td>{transaction.smsType}</td>
<td>{transaction.phoneNumber}</td>
<td>{transaction.tryCount}</td>
</tr>
))
) : (
<tr>
<td colSpan="7" className="text-center">
<div className="d-flex flex-column align-items-center mt-5">
<img src={NoAvailable} alt="No Data Available" className="mb-3" style={styles.iconStyle} />
<p>Data not available</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
/>
</div>
</div>
);
};
export default Anomaly;
const styles = {
contentContainer: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
},
selectWrapper: {
position: 'relative',
display: 'inline-block',
width: '100%',
},
tableContainer: {
minHeight: '300px',
maxHeight: '1500px',
overflowY: 'auto',
},
iconStyle: {
width: '50px',
height: '50px',
},
// Add margin-left style for icons
iconMarginLeft: {
marginLeft: '0.7rem', // Adjust as needed
},
loadingOverlay: {
position: 'fixed', // Gunakan fixed untuk overlay penuh layar
top: 0,
left: 0,
right: 0,
bottom: 0, // Pastikan menutupi seluruh layar
display: 'flex',
justifyContent: 'center', // Posisikan spinner di tengah secara horizontal
alignItems: 'center', // Posisikan spinner di tengah secara vertikal
backgroundColor: 'rgba(0, 0, 0, 0.5)', // Semitransparan di background
color: 'white',
fontSize: '24px',
zIndex: 1000, // Pastikan overlay muncul di atas konten lainnya
},
spinner: {
border: '4px solid #f3f3f3', // Border gray untuk spinner
borderTop: '4px solid #3498db', // Border biru untuk spinner
borderRadius: '50%', // Membuat spinner bulat
width: '50px', // Ukuran spinner
height: '50px', // Ukuran spinner
animation: 'spin 2s linear infinite', // Menambahkan animasi spin
},
loadingText: {
marginLeft: '20px',
},
};

View File

@ -0,0 +1,5 @@
import Anomaly from "./Anomaly";
export {
Anomaly
}

View File

@ -0,0 +1,130 @@
import React, { useState, useRef, useEffect } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronLeft, faChevronDown } from '@fortawesome/free-solid-svg-icons';
const BlockNumber = () => {
const [applicationId, setApplicationId] = useState('');
const [applicationIds, setApplicationIds] = useState([]);
const [isSelectOpen, setIsSelectOpen] = useState(false);
const [phoneId, setPhoneId] = useState('');
const [description, setDescription] = useState('');
const handleFocus = () => {
setIsSelectOpen(true);
};
const handleBlur = () => {
setIsSelectOpen(false);
};
const handleClick = async () => {
console.log('Make OTP Demo')
};
return (
<div className='container mt-5'>
<div style={styles.contentContainer}>
<div className="form-group col align-items-center">
{/* Application ID Selection */}
<div className="col-md-6 my-2">
<div style={styles.selectWrapper}>
<select
id="applicationId"
className="form-control"
style={{
...styles.select,
border: '1px solid lightgray'
}}
value={applicationId}
onChange={(e) => setApplicationId(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
>
<option value="">Select Application ID</option>
{applicationIds.map((app) => (
<option key={app.id} value={app.id}>
{app.name}
</option>
))}
</select>
<FontAwesomeIcon
icon={isSelectOpen ? faChevronDown : faChevronLeft}
style={styles.chevronIcon}
/>
</div>
</div>
{/* Phone Number Input */}
<div className="col-md-6 my-2">
<div className="input-group" style={{ marginTop: '1rem' }}>
<span className="input-group-prepend">
<span className="input-group-text">+62</span>
</span>
<input
type="text"
id="phoneId"
className="form-control"
placeholder="Phone Number"
value={phoneId}
onChange={(e) => setPhoneId(e.target.value)}
/>
</div>
</div>
{/* Description Text Area */}
<div className="col-md-6 my-3">
<textarea
id="description"
className="form-control"
rows="4"
placeholder="Enter description here..."
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
{/* Submit Button */}
<div>
<button onClick={handleClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Submit Now</p>
</button>
</div>
</div>
</div>
</div>
)
}
export default BlockNumber
const styles = {
contentContainer: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
},
selectWrapper: {
position: 'relative',
marginTop: '0',
},
select: {
width: '100%',
paddingRight: '30px',
flex: 1,
fontSize: '16px',
border: 'none',
outline: 'none',
},
chevronIcon: {
position: 'absolute',
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'none',
},
}

View File

@ -0,0 +1,476 @@
import React, { useState, useEffect } from 'react';
import {
FaChevronLeft,
FaChevronRight,
FaFastBackward,
FaFastForward,
FaSort,
FaSortUp,
FaSortDown,
FaEdit,
FaEye,
FaTrash
} from 'react-icons/fa'; // Icons for sorting
import { NoAvailable } from '../../../assets/icon';
import { Link } from 'react-router-dom';
// Pagination Component
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
const handlePrev = () => {
if (currentPage > 1) {
onPageChange(currentPage - 1);
}
};
const handleNext = () => {
if (currentPage < totalPages) {
onPageChange(currentPage + 1);
}
};
const handleFirst = () => {
onPageChange(1); // Go to first page
};
const handleLast = () => {
onPageChange(totalPages); // Go to last page
};
// Logic to display only 3 pages in pagination
const getPaginationRange = () => {
const range = [];
const totalPagesCount = totalPages;
let start = currentPage - 1;
let end = currentPage + 1;
// Adjust start and end if near the boundaries
if (currentPage === 1) {
start = 1;
end = Math.min(3, totalPagesCount);
} else if (currentPage === totalPages) {
start = Math.max(totalPagesCount - 2, 1);
end = totalPagesCount;
}
for (let i = start; i <= end; i++) {
range.push(i);
}
return range;
};
const pageRange = getPaginationRange();
return (
<div className="pagination-container d-flex justify-content-end mt-4">
{/* First Page Button */}
<button
className="btn"
onClick={handleFirst}
disabled={currentPage === 1}
>
<FaFastBackward /> {/* Double Arrow Left */}
</button>
<button
className="btn"
onClick={handlePrev}
disabled={currentPage === 1}
>
<FaChevronLeft /> {/* Single Arrow Left */}
</button>
{/* Page Numbers */}
{pageRange.map((pageNum) => (
<button
key={pageNum}
className={`btn ${pageNum === currentPage ? 'btn-primary' : ''}`}
onClick={() => onPageChange(pageNum)}
>
{pageNum}
</button>
))}
<button
className="btn"
onClick={handleNext}
disabled={currentPage === totalPages}
>
<FaChevronRight /> {/* Single Arrow Right */}
</button>
{/* Last Page Button */}
<button
className="btn"
onClick={handleLast}
disabled={currentPage === totalPages}
>
<FaFastForward /> {/* Double Arrow Right */}
</button>
</div>
);
};
const BlockedSms = () => {
const [currentPage, setCurrentPage] = useState(1);
const [isMobile, setIsMobile] = useState(false); // State to detect mobile view
const [transactionData, setTransactionData] = useState([]);
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state
const dataPerPage = 10; // Data per page (10 data per page)
const buttonData = [
{ label: 'Copy', enabled: true },
{ label: 'CSV', enabled: true },
{ label: 'Excel', enabled: true },
{ label: 'PDF', enabled: true },
{ label: 'Print', enabled: true },
{ label: 'Column Visibility', enabled: true },
];
// Generate 691 dummy transactions
const generateDummyData = (numOfItems) => {
const transactionData = [];
for (let i = 1; i <= numOfItems; i++) {
transactionData.push({
transactionId: `TX${String(i).padStart(3, '0')}`,
applicationName: `App ${Math.floor(Math.random() * 5) + 1}`,
phoneNumber: `+62${Math.floor(Math.random() * 900000000) + 800000000}`, // Random phone number
description: [
"Transaction processed successfully.",
"Payment is pending confirmation.",
"Failed to process transaction due to network error.",
"Sender ID verification in progress.",
"Operator approval required for completion.",
"Transaction marked for manual review.",
"Processed via offline mode.",
"Transaction successfully completed online."
][Math.floor(Math.random() * 8)] // Random description
});
}
return transactionData;
};
// Set the generated transaction data
useEffect(() => {
setTransactionData(generateDummyData(3113)); // count data dummy transactions
}, []);
// Sorting function
const sortData = (data, config) => {
const { key, direction } = config;
return [...data].sort((a, b) => {
if (a[key] < b[key]) {
return direction === 'asc' ? -1 : 1;
}
if (a[key] > b[key]) {
return direction === 'asc' ? 1 : -1;
}
return 0;
});
};
// Handle column header sort click
const handleSort = (key) => {
let direction = 'asc';
if (sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc'; // Toggle direction if the same column is clicked
}
setSortConfig({ key, direction });
};
// Get the paginated data
const getPaginatedData = (data, page, perPage) => {
const sortedData = sortData(data, sortConfig);
const startIndex = (page - 1) * perPage;
const endIndex = startIndex + perPage;
return sortedData.slice(startIndex, endIndex);
};
// Handle page change
const handlePageChange = (page) => {
setCurrentPage(page);
};
// Calculate total pages based on the data and data per page
const totalPages = Math.ceil(transactionData.length / dataPerPage);
// Paginated data
const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage);
// Detect screen size and update isMobile state
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768); // Change 768 to your breakpoint
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Define your handle functions
const handleEdit = (transactionId) => {
console.log(`Edit transaction with ID: ${transactionId}`);
// Add your edit logic here
};
const handleShow = (transactionId) => {
console.log(`Show transaction with ID: ${transactionId}`);
// Add your show logic here
};
const handleDelete = (transactionId) => {
console.log(`Delete transaction with ID: ${transactionId}`);
// Add your delete logic here
};
return (
<div className="container mt-5">
{/* Welcome Message */}
<div className="row-card border-left border-primary shadow mb-4" style={{ backgroundColor: '#E2FBEA' }}>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<div>
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
</div>
</div>
</div>
<div style={styles.contentContainer}>
{/* Submit Block Number Button */}
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '1rem' }}>
<Link to="/block-number" className="btn text-white" style={{ backgroundColor: '#0542cc', textDecoration: 'none' }}>
Submit Block Number
</Link>
</div>
{/* Filter Form */}
<div className="card p-3 mb-4">
<div className="row">
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Start Date</label>
<input type="date" className="form-control" />
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>End Date</label>
<input type="date" className="form-control" />
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Application</label>
<select className="form-control">
<option>Select Application</option>
<option>App 1</option>
<option>App 2</option>
<option>App 3</option>
<option>App 4</option>
<option>App 5</option>
</select>
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Endpoint</label>
<select className="form-control">
<option>Select Endpoint</option>
<option>Endpoint 1</option>
<option>Endpoint 2</option>
<option>Endpoint 3</option>
<option>Endpoint 4</option>
<option>Endpoint 5</option>
</select>
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Service Charge</label>
<select className="form-control">
<option>Select Service Charge</option>
<option>$10</option>
<option>$20</option>
<option>$30</option>
<option>$40</option>
<option>$50</option>
</select>
</div>
<div className={`col-12 ${isMobile ? 'd-flex justify-content-between' : 'col-md-2 d-flex align-items-end'}`} style={{ gap: '10px' }}>
<button className="btn btn-primary w-48">Apply</button>
<button className="btn btn-secondary w-48">Cancel</button>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="d-flex justify-content-between align-items-center mb-3">
<div>
{buttonData.map((button, index) =>
button.enabled ? (
<button
key={index}
className={`btn btn-light ${isMobile ? 'mb-2' : ''}`} // Add margin on mobile
style={styles.actionButton}
>
{button.label}
</button>
) : null
)}
</div>
{/* Search Bar with Icon */}
<div className="input-group" style={{ width: '250px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
<input
type="text"
placeholder="Search..."
className="form-control"
/>
<span className="input-group-text">
<i className="fas fa-search"></i> {/* FontAwesome search icon */}
</span>
</div>
</div>
{/* Table */}
<div className="table-responsive">
<table className="table table-bordered" style={styles.tableContainer}>
<thead>
<tr>
<th>No.</th> {/* Kolom untuk Nomor Urut */}
<th>
<button className="btn" onClick={() => handleSort('transactionId')}>
Transaction ID
{sortConfig.key === 'transactionId' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'transactionId' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('applicationName')}>
Application Name
{sortConfig.key === 'applicationName' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'applicationName' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn text-sm" onClick={() => handleSort('phoneNumber')}>
Phone Number
{sortConfig.key === 'phoneNumber' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'phoneNumber' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn text-sm" onClick={() => handleSort('description')}>
Description
{sortConfig.key === 'description' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'description' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th style={{ textAlign: 'center' }}>Action</th>
</tr>
</thead>
<tbody>
{paginatedData.length > 0 ? (
paginatedData.map((transaction, index) => (
<tr key={index}>
{/* Kolom Nomor Urut */}
<td>{(currentPage - 1) * dataPerPage + index + 1}</td> {/* Nomor urut berdasarkan halaman dan index */}
<td>{transaction.transactionId}</td>
<td>{transaction.applicationName}</td>
<td>{transaction.phoneNumber}</td>
<td>{transaction.description}</td>
<td style={{ textAlign: 'center' }}>
<div style={{ display: 'flex', justifyContent: 'center', gap: '10px' }}>
<button
className="btn btn-warning"
style={{ display: 'flex', alignItems: 'center' }}
onClick={() => handleEdit(transaction.transactionId)}
>
<FaEdit style={{ marginRight: '5px' }} />
Edit
</button>
<button
className="btn btn-success"
style={{ display: 'flex', alignItems: 'center' }}
onClick={() => handleShow(transaction.transactionId)}
>
<FaEye style={{ marginRight: '5px' }} />
Show
</button>
<button
className="btn btn-danger"
style={{ display: 'flex', alignItems: 'center' }}
onClick={() => handleDelete(transaction.transactionId)}
>
<FaTrash style={{ marginRight: '5px' }} />
Delete
</button>
</div>
</td>
</tr>
))
) : (
<tr>
<td colSpan="7" className="text-center">
<div className="d-flex flex-column align-items-center mt-5">
<img src={NoAvailable} alt="No Data Available" className="mb-3" style={styles.iconStyle} />
<p>Data not available</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
/>
</div>
</div>
);
};
export default BlockedSms;
const styles = {
contentContainer: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
},
tableContainer: {
minHeight: '300px',
maxHeight: '1500px',
overflowY: 'auto',
},
iconStyle: {
width: '50px',
height: '50px',
},
// Add margin-left style for icons
iconMarginLeft: {
marginLeft: '0.7rem', // Adjust as needed
},
};

View File

@ -0,0 +1,7 @@
import Blocked from './BlockedSms';
import BlockNumber from './BlockNumber';
export {
Blocked,
BlockNumber
}

View File

@ -0,0 +1,419 @@
import React, { useState, useEffect } from 'react';
import { FaChevronLeft, FaChevronRight, FaFastBackward, FaFastForward, FaSort, FaSortUp, FaSortDown } from 'react-icons/fa'; // Icons for sorting
import { NoAvailable } from '../../../assets/icon';
// Pagination Component
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
const handlePrev = () => {
if (currentPage > 1) {
onPageChange(currentPage - 1);
}
};
const handleNext = () => {
if (currentPage < totalPages) {
onPageChange(currentPage + 1);
}
};
const handleFirst = () => {
onPageChange(1); // Go to first page
};
const handleLast = () => {
onPageChange(totalPages); // Go to last page
};
// Logic to display only 3 pages in pagination
const getPaginationRange = () => {
const range = [];
const totalPagesCount = totalPages;
let start = currentPage - 1;
let end = currentPage + 1;
// Adjust start and end if near the boundaries
if (currentPage === 1) {
start = 1;
end = Math.min(3, totalPagesCount);
} else if (currentPage === totalPages) {
start = Math.max(totalPagesCount - 2, 1);
end = totalPagesCount;
}
for (let i = start; i <= end; i++) {
range.push(i);
}
return range;
};
const pageRange = getPaginationRange();
return (
<div className="pagination-container d-flex justify-content-end mt-4">
{/* First Page Button */}
<button
className="btn"
onClick={handleFirst}
disabled={currentPage === 1}
>
<FaFastBackward /> {/* Double Arrow Left */}
</button>
<button
className="btn"
onClick={handlePrev}
disabled={currentPage === 1}
>
<FaChevronLeft /> {/* Single Arrow Left */}
</button>
{/* Page Numbers */}
{pageRange.map((pageNum) => (
<button
key={pageNum}
className={`btn ${pageNum === currentPage ? 'btn-primary' : ''}`}
onClick={() => onPageChange(pageNum)}
>
{pageNum}
</button>
))}
<button
className="btn"
onClick={handleNext}
disabled={currentPage === totalPages}
>
<FaChevronRight /> {/* Single Arrow Right */}
</button>
{/* Last Page Button */}
<button
className="btn"
onClick={handleLast}
disabled={currentPage === totalPages}
>
<FaFastForward /> {/* Double Arrow Right */}
</button>
</div>
);
};
const DetailView = () => {
const [currentPage, setCurrentPage] = useState(1);
const [phoneNumber, setPhoneNumber] = useState('');
const [isMobile, setIsMobile] = useState(false); // State to detect mobile view
const [transactionData, setTransactionData] = useState([]);
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state
const dataPerPage = 10; // Data per page (10 data per page)
const buttonData = [
{ label: 'Copy', enabled: true },
{ label: 'CSV', enabled: true },
{ label: 'Excel', enabled: true },
{ label: 'PDF', enabled: true },
{ label: 'Print', enabled: true },
{ label: 'Column Visibility', enabled: true },
];
// Generate 691 dummy transactions
const generateDummyData = (numOfItems) => {
const transactionData = [];
for (let i = 1; i <= numOfItems; i++) {
transactionData.push({
applicationName: `App ${Math.floor(Math.random() * 5) + 1}`, // Application Name
phoneNumber: `+62${Math.floor(Math.random() * 900000000) + 800000000}`, // Random phone number
otp: Math.floor(100000 + Math.random() * 900000),
dateSent: new Date(2023, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toLocaleDateString(), // Random date
status: ['Completed', 'Pending', 'Failed'][Math.floor(Math.random() * 3)], // Random status
});
}
return transactionData;
};
// Set the generated transaction data
useEffect(() => {
setTransactionData(generateDummyData(31)); // count data dummy transactions
}, []);
// Sorting function
const sortData = (data, config) => {
const { key, direction } = config;
return [...data].sort((a, b) => {
if (a[key] < b[key]) {
return direction === 'asc' ? -1 : 1;
}
if (a[key] > b[key]) {
return direction === 'asc' ? 1 : -1;
}
return 0;
});
};
// Handle column header sort click
const handleSort = (key) => {
let direction = 'asc';
if (sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc'; // Toggle direction if the same column is clicked
}
setSortConfig({ key, direction });
};
// Get the paginated data
const getPaginatedData = (data, page, perPage) => {
const sortedData = sortData(data, sortConfig);
const startIndex = (page - 1) * perPage;
const endIndex = startIndex + perPage;
return sortedData.slice(startIndex, endIndex);
};
// Handle page change
const handlePageChange = (page) => {
setCurrentPage(page);
};
// Calculate total pages based on the data and data per page
const totalPages = Math.ceil(transactionData.length / dataPerPage);
// Paginated data
const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage);
// Detect screen size and update isMobile state
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768); // Change 768 to your breakpoint
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<div className="container mt-5">
{/* Welcome Message */}
<div className="row-card border-left border-primary shadow mb-4" style={{ backgroundColor: '#E2FBEA' }}>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<div>
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
</div>
</div>
</div>
<div style={styles.contentContainer}>
<strong>Please input the phone number first to show data</strong>
<div className="col-md-8 mt-2">
<div className="d-flex align-items-center">
<span className="input-group-prepend">
<span className="input-group-text">+62</span>
</span>
<input
type="text"
id="phoneNumber"
className="form-control"
placeholder="Phone Number"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
/>
<button className="btn text-white" style={{ backgroundColor: '#0542cc', marginLeft: '2vh', width: '20%' }}>Show Data</button>
</div>
</div>
</div>
<div style={styles.contentContainer}>
{/* Filter Form */}
<div className="card p-3 mb-4">
<div className="row">
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Start Date</label>
<input type="date" className="form-control" />
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>End Date</label>
<input type="date" className="form-control" />
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Application</label>
<select className="form-control">
<option>Select Application</option>
<option>App 1</option>
<option>App 2</option>
<option>App 3</option>
<option>App 4</option>
<option>App 5</option>
</select>
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Status</label>
<select className="form-control">
<option>Select Status</option>
<option>Status 1</option>
<option>Status 2</option>
<option>Status 3</option>
</select>
</div>
<div className={`col-12 ${isMobile ? 'd-flex justify-content-between' : 'col-md-2 d-flex align-items-end'}`} style={{ gap: '10px' }}>
<button className="btn btn-primary w-48">Apply</button>
<button className="btn btn-secondary w-48">Cancel</button>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="d-flex justify-content-between align-items-center mb-3">
<div>
{buttonData.map((button, index) =>
button.enabled ? (
<button
key={index}
className={`btn btn-light ${isMobile ? 'mb-2' : ''}`} // Add margin on mobile
style={styles.actionButton}
>
{button.label}
</button>
) : null
)}
</div>
{/* Search Bar with Icon */}
<div className="input-group" style={{ width: '250px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
<input
type="text"
placeholder="Search..."
className="form-control"
/>
<span className="input-group-text">
<i className="fas fa-search"></i> {/* FontAwesome search icon */}
</span>
</div>
</div>
{/* Table */}
<div className="table-responsive">
<table className="table table-bordered" style={styles.tableContainer}>
<thead>
<tr>
<th>No.</th> {/* Kolom untuk Nomor Urut */}
<th>
<button className="btn" onClick={() => handleSort('applicationName')}>
Application Name
{sortConfig.key === 'applicationName' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'applicationName' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('phoneNumber')}>
Phone Number
{sortConfig.key === 'phoneNumber' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'phoneNumber' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('otp')}>
OTP
{sortConfig.key === 'otp' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'otp' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('dateSent')}>
Date Sent
{sortConfig.key === 'dateSent' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'dateSent' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('status')}>
Status
{sortConfig.key === 'status' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'status' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
</tr>
</thead>
<tbody>
{paginatedData.length > 0 ? (
paginatedData.map((transaction, index) => (
<tr key={index}>
{/* Kolom Nomor Urut */}
<td>{(currentPage - 1) * dataPerPage + index + 1}</td>
<td>{transaction.applicationName}</td>
<td>{transaction.phoneNumber}</td>
<td>{transaction.otp}</td>
<td>{transaction.dateSent}</td>
<td>{transaction.status}</td>
</tr>
))
) : (
<tr>
<td colSpan="7" className="text-center">
<div className="d-flex flex-column align-items-center mt-5">
<img src={NoAvailable} alt="No Data Available" className="mb-3" style={styles.iconStyle} />
<p>Data not available</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
/>
</div>
</div>
);
};
export default DetailView;
const styles = {
contentContainer: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
},
tableContainer: {
minHeight: '300px',
maxHeight: '1500px',
overflowY: 'auto',
},
iconStyle: {
width: '50px',
height: '50px',
},
// Add margin-left style for icons
iconMarginLeft: {
marginLeft: '0.7rem', // Adjust as needed
},
};

View File

@ -0,0 +1,265 @@
import React, { useState, useEffect } from 'react';
import {
Extract,
Transaction as TransactionIcon,
} from '../../../../assets/icon';
const Announcement = () => {
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [application, setApplication] = useState('');
const [isMobile, setIsMobile] = useState(false);
const menuData = [
{ id: 1, value: '150', label: 'Total Transaction', icon: TransactionIcon },
{ id: 2, value: '4', label: 'Total Extract', icon: Extract },
];
const handleApply = () => {
console.log({ startDate, endDate, application });
};
const handleCancel = () => {
setStartDate('');
setEndDate('');
setApplication('');
};
// Detect if the device is mobile based on window width
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768); // Define mobile screen as width <= 768px
};
// Initialize on component mount
handleResize();
// Add event listener to update state on resize
window.addEventListener('resize', handleResize);
// Cleanup event listener on component unmount
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<div style={styles.container}>
{/* Welcome Message */}
<div
className="row-card border-left border-primary shadow mb-4"
style={{ backgroundColor: '#E2FBEA' }}
>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<div>
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
</div>
</div>
</div>
<div style={styles.summaryContainer}>
<div style={isMobile ? styles.filtersMobile : styles.filters}>
{/* Row 1: Start Date and End Date */}
<div style={styles.filterRow}>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
style={styles.filterInput}
placeholder="Start Date"
/>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
style={styles.filterInput}
placeholder="End Date"
/>
</div>
{/* Row 2: Application Select */}
<div style={styles.filterRow}>
<select
value={application}
onChange={(e) => setApplication(e.target.value)}
style={styles.filterInput}
>
<option value="">Application</option>
<option value="App1">App1</option>
<option value="App2">App2</option>
</select>
</div>
{/* Row 3: Apply and Cancel Buttons */}
<div style={styles.filterRow}>
<button style={styles.applyButton} onClick={handleApply}>
Apply
</button>
<button style={styles.cancelButton} onClick={handleCancel}>
Cancel
</button>
</div>
</div>
<div style={isMobile ? styles.summaryCardsMobile : styles.summaryCards}>
{menuData.map((item) => (
<div key={item.id} style={styles.card}>
<div style={styles.textContainer}>
<h2 style={styles.cardTitle}>{item.value}</h2>
<p style={styles.cardText}>{item.label}</p>
</div>
<div style={styles.iconContainer}>
<img src={item.icon} alt={item.label} style={styles.icon} />
</div>
</div>
))}
</div>
</div>
</div>
);
};
const styles = {
container: {
margin: '3rem',
},
filters: {
display: 'flex',
justifyContent: 'space-between',
marginBottom: '20px',
flexDirection: 'row', // Horizontal layout on desktop
gap: '10px',
},
filtersMobile: {
display: 'flex',
flexDirection: 'column', // Vertical layout on mobile
gap: '15px',
},
filterRow: {
display: 'flex',
justifyContent: 'space-between',
gap: '10px',
width: '100%',
},
filterInput: {
padding: '10px',
border: '1px solid #ccc',
borderRadius: '5px',
fontSize: '16px',
width: '48%', // Adjust width to take up 48% of the row for input elements
},
applyButton: {
backgroundColor: '#0542cc',
color: '#fff',
padding: '10px 20px',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
width: '48%', // Adjust button width to take half of the row
},
cancelButton: {
backgroundColor: '#fff',
color: '#000',
padding: '10px 20px',
border: '1px solid gray',
borderRadius: '5px',
cursor: 'pointer',
width: '48%', // Adjust button width to take half of the row
},
summaryContainer: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
},
summaryCards: {
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)', // 4 columns on desktop
gap: '20px',
marginTop: '20px',
},
summaryCardsMobile: {
display: 'grid',
gridTemplateColumns: '1fr', // 1 column on mobile (full width)
gap: '15px',
marginTop: '20px',
},
card: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: '15px', // Reduced padding for a more compact card
backgroundColor: 'white',
borderRadius: '8px', // Slightly smaller border-radius for a sharper look
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
textAlign: 'left',
minWidth: '200px', // Ensure minimum width for mobile responsiveness
maxWidth: '300px', // Limit the maximum width to prevent cards from becoming too wide
width: '100%',
transition: 'transform 0.3s ease', // Smooth transition for hover effects
},
cardHover: {
transform: 'scale(1.05)', // Slight zoom on hover for interactivity
},
iconContainer: {
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
},
textContainer: {
display: 'flex',
flexDirection: 'column',
},
icon: {
width: '36px', // Smaller icon size for a more compact card
height: '36px', // Matching height for the icon
},
cardTitle: {
fontSize: '18px', // Smaller font size for title to prevent overflow
margin: '0',
fontWeight: 'bold',
},
cardText: {
fontSize: '12px', // Smaller text size for description to keep it balanced
color: '#555',
},
// Optional: Hover effect for the cards to enhance interactivity
cardHover: {
transform: 'scale(1.05)', // Slight zoom effect when hovering over card
},
// Media query for smaller screens (e.g., phones in portrait mode)
"@media (max-width: 768px)": {
summaryCards: {
gridTemplateColumns: '1fr', // 1 column on mobile screens
},
card: {
padding: '10px', // Smaller padding for smaller screens
minWidth: '180px', // Adjust min width for mobile devices
maxWidth: '250px', // Adjust max width for mobile devices
},
icon: {
width: '30px', // Slightly smaller icon size for mobile
height: '30px', // Matching height for mobile icon
},
cardTitle: {
fontSize: '16px', // Smaller font size for the card title
},
cardText: {
fontSize: '10px', // Smaller font size for card description
},
},
};
export default Announcement;

View File

@ -0,0 +1,271 @@
import React, { useState, useEffect } from 'react';
import {
Extract,
Quality,
AsyncIcon,
QualityAsync,
Transaction as TransactionIcon,
} from '../../../../assets/icon';
const Transaction = () => {
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [application, setApplication] = useState('');
const [isMobile, setIsMobile] = useState(false);
const menuData = [
{ id: 1, value: '150', label: 'Total Transaction', icon: TransactionIcon },
{ id: 2, value: '4', label: 'Total Extract', icon: Extract },
{ id: 3, value: '65', label: 'Total Quality', icon: Quality },
{ id: 6, value: '900', label: 'Total Extract Async', icon: AsyncIcon },
{ id: 4, value: '22', label: 'Total Quality Async', icon: QualityAsync },
];
const handleApply = () => {
console.log({ startDate, endDate, application });
};
const handleCancel = () => {
setStartDate('');
setEndDate('');
setApplication('');
};
// Detect if the device is mobile based on window width
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768); // Define mobile screen as width <= 768px
};
// Initialize on component mount
handleResize();
// Add event listener to update state on resize
window.addEventListener('resize', handleResize);
// Cleanup event listener on component unmount
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<div style={styles.container}>
{/* Welcome Message */}
<div
className="row-card border-left border-primary shadow mb-4"
style={{ backgroundColor: '#E2FBEA' }}
>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<div>
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
</div>
</div>
</div>
<div style={styles.summaryContainer}>
<div style={isMobile ? styles.filtersMobile : styles.filters}>
{/* Row 1: Start Date and End Date */}
<div style={styles.filterRow}>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
style={styles.filterInput}
placeholder="Start Date"
/>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
style={styles.filterInput}
placeholder="End Date"
/>
</div>
{/* Row 2: Application Select */}
<div style={styles.filterRow}>
<select
value={application}
onChange={(e) => setApplication(e.target.value)}
style={styles.filterInput}
>
<option value="">Application</option>
<option value="App1">App1</option>
<option value="App2">App2</option>
</select>
</div>
{/* Row 3: Apply and Cancel Buttons */}
<div style={styles.filterRow}>
<button style={styles.applyButton} onClick={handleApply}>
Apply
</button>
<button style={styles.cancelButton} onClick={handleCancel}>
Cancel
</button>
</div>
</div>
<div style={isMobile ? styles.summaryCardsMobile : styles.summaryCards}>
{menuData.map((item) => (
<div key={item.id} style={styles.card}>
<div style={styles.textContainer}>
<h2 style={styles.cardTitle}>{item.value}</h2>
<p style={styles.cardText}>{item.label}</p>
</div>
<div style={styles.iconContainer}>
<img src={item.icon} alt={item.label} style={styles.icon} />
</div>
</div>
))}
</div>
</div>
</div>
);
};
const styles = {
container: {
margin: '3rem',
},
filters: {
display: 'flex',
justifyContent: 'space-between',
marginBottom: '20px',
flexDirection: 'row', // Horizontal layout on desktop
gap: '10px',
},
filtersMobile: {
display: 'flex',
flexDirection: 'column', // Vertical layout on mobile
gap: '15px',
},
filterRow: {
display: 'flex',
justifyContent: 'space-between',
gap: '10px',
width: '100%',
},
filterInput: {
padding: '10px',
border: '1px solid #ccc',
borderRadius: '5px',
fontSize: '16px',
width: '48%', // Adjust width to take up 48% of the row for input elements
},
applyButton: {
backgroundColor: '#0542cc',
color: '#fff',
padding: '10px 20px',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
width: '48%', // Adjust button width to take half of the row
},
cancelButton: {
backgroundColor: '#fff',
color: '#000',
padding: '10px 20px',
border: '1px solid gray',
borderRadius: '5px',
cursor: 'pointer',
width: '48%', // Adjust button width to take half of the row
},
summaryContainer: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
},
summaryCards: {
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)', // 4 columns on desktop
gap: '20px',
marginTop: '20px',
},
summaryCardsMobile: {
display: 'grid',
gridTemplateColumns: '1fr', // 1 column on mobile (full width)
gap: '15px',
marginTop: '20px',
},
card: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: '15px', // Reduced padding for a more compact card
backgroundColor: 'white',
borderRadius: '8px', // Slightly smaller border-radius for a sharper look
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
textAlign: 'left',
minWidth: '200px', // Ensure minimum width for mobile responsiveness
maxWidth: '300px', // Limit the maximum width to prevent cards from becoming too wide
width: '100%',
transition: 'transform 0.3s ease', // Smooth transition for hover effects
},
cardHover: {
transform: 'scale(1.05)', // Slight zoom on hover for interactivity
},
iconContainer: {
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
},
textContainer: {
display: 'flex',
flexDirection: 'column',
},
icon: {
width: '36px', // Smaller icon size for a more compact card
height: '36px', // Matching height for the icon
},
cardTitle: {
fontSize: '18px', // Smaller font size for title to prevent overflow
margin: '0',
fontWeight: 'bold',
},
cardText: {
fontSize: '12px', // Smaller text size for description to keep it balanced
color: '#555',
},
// Optional: Hover effect for the cards to enhance interactivity
cardHover: {
transform: 'scale(1.05)', // Slight zoom effect when hovering over card
},
// Media query for smaller screens (e.g., phones in portrait mode)
"@media (max-width: 768px)": {
summaryCards: {
gridTemplateColumns: '1fr', // 1 column on mobile screens
},
card: {
padding: '10px', // Smaller padding for smaller screens
minWidth: '180px', // Adjust min width for mobile devices
maxWidth: '250px', // Adjust max width for mobile devices
},
icon: {
width: '30px', // Slightly smaller icon size for mobile
height: '30px', // Matching height for mobile icon
},
cardTitle: {
fontSize: '16px', // Smaller font size for the card title
},
cardText: {
fontSize: '10px', // Smaller font size for card description
},
},
};
export default Transaction;

View File

@ -0,0 +1,7 @@
import Transaction from "./Transaction";
import Announcement from "./Announcement";
export {
Transaction,
Announcement
}

View File

@ -0,0 +1,146 @@
import React, { useState } from 'react';
const Settings = () => {
const [spamCheckEnabled, setSpamCheckEnabled] = useState(false);
const [sequentialPhoneEnabled, setSequentialPhoneEnabled] = useState(false);
const [rateLimit, setRateLimit] = useState('');
return (
<div className='container' style={{ marginTop: '3%' }}>
<div style={{
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
}}>
{/* Application ID, OTP Length, Expiry Time, and Message Info */}
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}>
<div style={{ flex: '1', marginRight: '10px' }}>
<label>Application ID</label>
<input type="text" style={inputStyle} placeholder="Application ID" />
</div>
<div style={{ flex: '1', marginLeft: '10px' }}>
<label>OTP Length</label>
<input type="text" style={inputStyle} placeholder="OTP Length" />
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}>
<div style={{ flex: '1', marginRight: '10px' }}>
<label>Expiry Time</label>
<input type="text" style={inputStyle} placeholder="Expiry Time" />
</div>
<div style={{ flex: '1', marginLeft: '10px' }}>
<label>Message Info</label>
<textarea
style={{ ...inputStyle, height: '80px', resize: 'none' }} // Add height and prevent resizing if desired
placeholder="Message Info"
/>
</div>
</div>
<div className='column'>
{/* Spam Check & Rate Limit Section */}
<h3>Spam Check</h3>
<p>
When this feature is enabled, any requests that exceed the limit within the specified time period will return error code 429 and a status of 6 (Rejected). These requests will not incur any charges.
</p>
{/* Spam Check Section */}
<div className="col-md-6" style={{ justifyContent: 'flex-start', padding: '20px', marginBottom: '20px', marginLeft: '-1.2rem' }}>
<div className="input-group align-items-center">
<span className="input-group-prepend">
<span className="input-group-text">Enable</span>
</span>
{/* Tampilkan Rate Limit jika spamCheckEnabled adalah true */}
{spamCheckEnabled && (
<input
type="number"
value={rateLimit}
onChange={(e) => setRateLimit(e.target.value)}
className="form-control text-center"
placeholder="Rate Limit"
style={{ marginRight: '10px' }}
/>
)}
{/* Enable Switch */}
<div className="form-check form-switch" style={{ display: 'flex', alignItems: 'center', marginLeft: '1rem' }}>
<input
type="checkbox"
className="form-check-input"
checked={spamCheckEnabled}
onChange={() => {
setSpamCheckEnabled(!spamCheckEnabled);
console.log(`Spam Check is now ${!spamCheckEnabled ? 'Enabled' : 'Disabled'}`);
}}
id="spamCheck"
/>
<label className="form-check-label" htmlFor="spamCheck" style={{ marginLeft: '5px' }}>
Enable Spam Check
</label>
</div>
</div>
</div>
</div>
{/* Sequential Phone Number Section */}
<h3>Sequential Phone Number</h3>
<p>
When activated, all incoming request logs will be analyzed using the Verihubs algorithm. If any anomalies are detected, the
request will be rejected and will not be forwarded to the operator. The rejected request will receive a status of 6 (Rejected).
</p>
{/* Spam Check Section */}
<div className="col-md-6" style={{ justifyContent: 'flex-start', padding: '20px', marginBottom: '20px', marginLeft: '-1.2rem' }}>
<div className="input-group align-items-center">
<span className="input-group-prepend">
<span className="input-group-text">Enable</span>
</span>
{/* Enable Switch */}
<div className="form-check form-switch" style={{ display: 'flex', alignItems: 'center', marginLeft: '1rem' }}>
<input
type="checkbox"
className="form-check-input"
checked={sequentialPhoneEnabled}
onChange={() => {
setSequentialPhoneEnabled(!sequentialPhoneEnabled);
console.log(`Phone Number is now ${!sequentialPhoneEnabled ? 'Enabled' : 'Disabled'}`);
}}
id="spamCheck"
/>
<label className="form-check-label" htmlFor="spamCheck" style={{ marginLeft: '5px' }}>
Enable Spam Check
</label>
</div>
</div>
</div>
{/* Save Button */}
<div>
<button style={buttonStyle}>Save Settings</button>
</div>
</div>
</div>
);
};
// Styles
const inputStyle = {
width: '100%',
padding: '8px',
marginBottom: '10px',
borderRadius: '4px',
border: '1px solid #ccc',
fontSize: '16px',
};
const buttonStyle = {
backgroundColor: '#007BFF',
color: '#fff',
padding: '10px 20px',
fontSize: '16px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
};
export default Settings;

View File

@ -0,0 +1,114 @@
import React, { useEffect, useState } from 'react';
import { Link, Routes, Route, useNavigate } from 'react-router-dom';
import {
Transaction as SmsOtpSummaryTransaction,
Announcement as SmsOtpSummaryAnnouncement
} from './Section';
const Summary = () => {
const verifyTabs = [
{ name: 'Transaction', link: 'sms-otp-transaction' },
{ name: 'Announcement', link: 'sms-otp-announcement' },
];
const [isMobile, setIsMobile] = useState(false);
const navigate = useNavigate();
// Redirect otomatis ke rute default saat akses ke /sms-otp-transaction
useEffect(() => {
if (window.location.pathname === '/sms-otp-summary') {
navigate('sms-otp-transaction', { replace: true });
}
}, [navigate]);
// Update state isMobile berdasarkan ukuran layar
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<div className="container" style={styles.container}>
{/* Static Content */}
<div className="row-card border-left border-primary shadow mb-4" style={styles.welcomeCard}>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
<div className="d-flex flex-row mt-3">
<Link to="/createApps" style={{ textDecoration: 'none' }}>
<button className="btn d-flex justify-content-center align-items-center me-2" style={styles.createButton}>
<i className="fas fa-plus text-white me-2"></i>
<p className="text-white mb-0">Create New App ID</p>
</button>
</Link>
</div>
</div>
</div>
{/* Tab Navigation */}
<div style={styles.section}>
<div className={`d-flex ${isMobile ? 'flex-column' : 'flex-row'} justify-content-between align-items-center mb-3`}>
<div className="d-flex flex-wrap">
{verifyTabs.map((tab) => (
<Link
key={tab.link}
to={tab.link}
className={`btn ${window.location.pathname.includes(tab.link) ? 'btn-primary' : 'btn-light'} me-2 mb-2`}
style={styles.tabLink}
>
{tab.name}
</Link>
))}
</div>
</div>
{/* Dynamic Tab Content */}
<div className="tab-content">
<Routes>
<Route path="sms-otp-transaction" element={<SmsOtpSummaryTransaction />} />
<Route path="sms-otp-announcement" element={<SmsOtpSummaryAnnouncement />} />
</Routes>
</div>
</div>
</div>
);
};
export default Summary;
const styles = {
container: {
marginTop: '3%',
padding: '0 15px',
},
welcomeCard: {
backgroundColor: '#E2FBEA',
borderLeft: '4px solid #0542CC',
borderRadius: '5px',
marginBottom: '20px',
},
createButton: {
backgroundColor: '#0542CC',
},
section: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542CC',
borderRadius: '10px',
width: '100%',
},
tabLink: {
padding: '10px 20px',
},
};

View File

@ -0,0 +1,422 @@
import React, { useState, useEffect } from 'react';
import { FaChevronLeft, FaChevronRight, FaFastBackward, FaFastForward, FaSort, FaSortUp, FaSortDown } from 'react-icons/fa'; // Icons for sorting
import { NoAvailable } from '../../../assets/icon';
// Pagination Component
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
const handlePrev = () => {
if (currentPage > 1) {
onPageChange(currentPage - 1);
}
};
const handleNext = () => {
if (currentPage < totalPages) {
onPageChange(currentPage + 1);
}
};
const handleFirst = () => {
onPageChange(1); // Go to first page
};
const handleLast = () => {
onPageChange(totalPages); // Go to last page
};
// Logic to display only 3 pages in pagination
const getPaginationRange = () => {
const range = [];
const totalPagesCount = totalPages;
let start = currentPage - 1;
let end = currentPage + 1;
// Adjust start and end if near the boundaries
if (currentPage === 1) {
start = 1;
end = Math.min(3, totalPagesCount);
} else if (currentPage === totalPages) {
start = Math.max(totalPagesCount - 2, 1);
end = totalPagesCount;
}
for (let i = start; i <= end; i++) {
range.push(i);
}
return range;
};
const pageRange = getPaginationRange();
return (
<div className="pagination-container d-flex justify-content-end mt-4">
{/* First Page Button */}
<button
className="btn"
onClick={handleFirst}
disabled={currentPage === 1}
>
<FaFastBackward /> {/* Double Arrow Left */}
</button>
<button
className="btn"
onClick={handlePrev}
disabled={currentPage === 1}
>
<FaChevronLeft /> {/* Single Arrow Left */}
</button>
{/* Page Numbers */}
{pageRange.map((pageNum) => (
<button
key={pageNum}
className={`btn ${pageNum === currentPage ? 'btn-primary' : ''}`}
onClick={() => onPageChange(pageNum)}
>
{pageNum}
</button>
))}
<button
className="btn"
onClick={handleNext}
disabled={currentPage === totalPages}
>
<FaChevronRight /> {/* Single Arrow Right */}
</button>
{/* Last Page Button */}
<button
className="btn"
onClick={handleLast}
disabled={currentPage === totalPages}
>
<FaFastForward /> {/* Double Arrow Right */}
</button>
</div>
);
};
const Transaction = () => {
const [currentPage, setCurrentPage] = useState(1);
const [isMobile, setIsMobile] = useState(false); // State to detect mobile view
const [transactionData, setTransactionData] = useState([]);
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state
const dataPerPage = 10; // Data per page (10 data per page)
const buttonData = [
{ label: 'Copy', enabled: true },
{ label: 'CSV', enabled: true },
{ label: 'Excel', enabled: true },
{ label: 'PDF', enabled: true },
{ label: 'Print', enabled: true },
{ label: 'Column Visibility', enabled: true },
];
// Generate 691 dummy transactions
const generateDummyData = (numOfItems) => {
const transactionData = [];
for (let i = 1; i <= numOfItems; i++) {
transactionData.push({
transactionId: `TX${String(i).padStart(3, '0')}`, // Transaction ID
applicationName: `App ${Math.floor(Math.random() * 5) + 1}`, // Application Name
phoneNumber: `+62${Math.floor(Math.random() * 900000000) + 800000000}`, // Random phone number
operator: ['Telkomsel', 'Indosat', 'XL', 'Tri', 'Smartfren'][Math.floor(Math.random() * 5)], // Random operator
senderId: `SENDER${String(i).padStart(3, '0')}`, // Sender ID
dateSent: new Date(2023, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toLocaleDateString(), // Random date
status: ['Completed', 'Pending', 'Failed'][Math.floor(Math.random() * 3)], // Random status
mode: Math.random() > 0.5 ? 'Online' : 'Offline', // Random mode
});
}
return transactionData;
};
// Set the generated transaction data
useEffect(() => {
setTransactionData(generateDummyData(31)); // count data dummy transactions
}, []);
// Sorting function
const sortData = (data, config) => {
const { key, direction } = config;
return [...data].sort((a, b) => {
if (a[key] < b[key]) {
return direction === 'asc' ? -1 : 1;
}
if (a[key] > b[key]) {
return direction === 'asc' ? 1 : -1;
}
return 0;
});
};
// Handle column header sort click
const handleSort = (key) => {
let direction = 'asc';
if (sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc'; // Toggle direction if the same column is clicked
}
setSortConfig({ key, direction });
};
// Get the paginated data
const getPaginatedData = (data, page, perPage) => {
const sortedData = sortData(data, sortConfig);
const startIndex = (page - 1) * perPage;
const endIndex = startIndex + perPage;
return sortedData.slice(startIndex, endIndex);
};
// Handle page change
const handlePageChange = (page) => {
setCurrentPage(page);
};
// Calculate total pages based on the data and data per page
const totalPages = Math.ceil(transactionData.length / dataPerPage);
// Paginated data
const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage);
// Detect screen size and update isMobile state
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768); // Change 768 to your breakpoint
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<div className="container mt-5">
{/* Welcome Message */}
<div className="row-card border-left border-primary shadow mb-4" style={{ backgroundColor: '#E2FBEA' }}>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<div>
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
</div>
</div>
</div>
<div style={styles.contentContainer}>
{/* Filter Form */}
<div className="card p-3 mb-4">
<div className="row">
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Start Date</label>
<input type="date" className="form-control" />
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>End Date</label>
<input type="date" className="form-control" />
</div>
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<label>Application</label>
<select className="form-control">
<option>Select Application</option>
<option>App 1</option>
<option>App 2</option>
<option>App 3</option>
<option>App 4</option>
<option>App 5</option>
</select>
</div>
<div className={`col-12 ${isMobile ? 'd-flex justify-content-between' : 'col-md-2 d-flex align-items-end'}`} style={{ gap: '10px' }}>
<button className="btn btn-primary w-48">Apply</button>
<button className="btn btn-secondary w-48">Cancel</button>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="d-flex justify-content-between align-items-center mb-3">
<div>
{buttonData.map((button, index) =>
button.enabled ? (
<button
key={index}
className={`btn btn-light ${isMobile ? 'mb-2' : ''}`} // Add margin on mobile
style={styles.actionButton}
>
{button.label}
</button>
) : null
)}
</div>
{/* Search Bar with Icon */}
<div className="input-group" style={{ width: '250px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
<input
type="text"
placeholder="Search..."
className="form-control"
/>
<span className="input-group-text">
<i className="fas fa-search"></i> {/* FontAwesome search icon */}
</span>
</div>
</div>
{/* Table */}
<div className="table-responsive">
<table className="table table-bordered" style={styles.tableContainer}>
<thead>
<tr>
<th>No.</th> {/* Kolom untuk Nomor Urut */}
<th>
<button className="btn" onClick={() => handleSort('transactionId')}>
Transaction ID
{sortConfig.key === 'transactionId' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'transactionId' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('applicationName')}>
Application Name
{sortConfig.key === 'applicationName' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'applicationName' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('phoneNumber')}>
Phone Number
{sortConfig.key === 'phoneNumber' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'phoneNumber' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('operator')}>
Operator
{sortConfig.key === 'operator' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'operator' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('senderId')}>
Sender ID
{sortConfig.key === 'senderId' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'senderId' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('dateSent')}>
Date Sent
{sortConfig.key === 'dateSent' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'dateSent' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('status')}>
Status
{sortConfig.key === 'status' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'status' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('mode')}>
Mode
{sortConfig.key === 'mode' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'mode' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
</tr>
</thead>
<tbody>
{paginatedData.length > 0 ? (
paginatedData.map((transaction, index) => (
<tr key={index}>
{/* Kolom Nomor Urut */}
<td>{(currentPage - 1) * dataPerPage + index + 1}</td>
<td>{transaction.transactionId}</td>
<td>{transaction.applicationName}</td>
<td>{transaction.phoneNumber}</td>
<td>{transaction.operator}</td>
<td>{transaction.senderId}</td>
<td>{transaction.dateSent}</td>
<td>{transaction.status}</td>
<td>{transaction.mode}</td>
</tr>
))
) : (
<tr>
<td colSpan="7" className="text-center">
<div className="d-flex flex-column align-items-center mt-5">
<img src={NoAvailable} alt="No Data Available" className="mb-3" style={styles.iconStyle} />
<p>Data not available</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
/>
</div>
</div>
);
};
export default Transaction;
const styles = {
contentContainer: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
},
tableContainer: {
minHeight: '300px',
maxHeight: '1500px',
overflowY: 'auto',
},
iconStyle: {
width: '50px',
height: '50px',
},
// Add margin-left style for icons
iconMarginLeft: {
marginLeft: '0.7rem', // Adjust as needed
},
};

View File

@ -0,0 +1,11 @@
import Settings from './Settings';
import Summary from './Summary';
import Transaction from './Transaction';
import Detail from './DetailView';
export {
Settings,
Summary,
Transaction,
Detail
}

View File

@ -1,7 +1,7 @@
import VerificationAnnoncement from "./Announcement"; import Annoncement from "./Announcement";
import VerificationOtp from "./Otp"; import Otp from "./Otp";
export { export {
VerificationAnnoncement, Annoncement,
VerificationOtp Otp
} }

View File

@ -1,8 +1,8 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Link, Routes, Route, useNavigate } from 'react-router-dom'; import { Link, Routes, Route, useNavigate } from 'react-router-dom';
import { import {
VerificationOtp, Otp as VerificationOtp,
VerificationAnnoncement Annoncement as VerificationAnnoncement
} from './Section'; } from './Section';
const Verify = () => { const Verify = () => {

View File

@ -1,5 +1,5 @@
import SmsVerify from "./Verify" import Verify from "./Verify"
export { export {
SmsVerify Verify
} }

View File

@ -0,0 +1,212 @@
import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
const CreateSettings = () => {
const location = useLocation();
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 768);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const Breadcrumb = ({ path }) => {
return (
<nav style={styles.breadcrumb}>
{path.map((item, index) => (
<span key={index}>
{index > 0 && ' > '}
<a
href={item.link}
style={{
...styles.breadcrumbLink,
fontWeight: location.pathname === item.link ? 'bold' : 'normal',
}}
>
{item.name}
</a>
</span>
))}
</nav>
);
};
const breadcrumbPath = [
{ name: 'Wa Integration', link: '/wa-integration' },
{ name: 'Add Setting', link: '/wa-createSettings' },
];
return (
<div style={styles.container}>
<div style={isMobile ? styles.wrapperMobile : styles.wrapper}>
<Breadcrumb path={breadcrumbPath} />
<h2 style={styles.header}>Create Settings</h2>
<div style={isMobile ? styles.formWrapperMobile : styles.formWrapper}>
{/* Left Form Section */}
<div style={styles.formSection}>
<div style={styles.inputGroup}>
<label style={styles.label}>Application ID</label>
<select style={styles.input}>
<option value="">Select Application</option>
</select>
</div>
<div style={styles.inputGroup}>
<label style={styles.label}>Inbound Configuration</label>
<select style={styles.input}>
<option value="">Select Configuration</option>
</select>
</div>
<div style={styles.inputGroup}>
<label style={styles.label}>Webhook URL</label>
<input
type="text"
placeholder="Enter Webhook URL"
style={styles.input}
/>
</div>
<div style={styles.inputGroup}>
<label style={styles.label}>Event</label>
<div style={styles.checkboxGroup}>
<label>
<input type="checkbox" />
Message Created
</label>
<label>
<input type="checkbox" />
Message Updated
</label>
</div>
</div>
<button style={styles.button}>Add Now</button>
</div>
{/* Right WhatsApp Details Section */}
<div style={styles.detailSection}>
<h3 style={styles.detailHeader}>WhatsApp Channel Detail</h3>
<div style={styles.detailItem}>
<strong>Business Account Name:</strong> RekanVeri
</div>
<div style={styles.detailItem}>
<strong>WABA ID:</strong> 112917284928438
</div>
<div style={styles.detailItem}>
<strong>WhatsApp Number:</strong> 6287736766208
</div>
<div style={styles.detailItem}>
<strong>Channel Name:</strong> RekanVeri
</div>
<div style={styles.detailItem}>
<strong>Eligible for Inbound:</strong> YES
</div>
</div>
</div>
</div>
</div>
);
};
export default CreateSettings;
const styles = {
container: {
padding: '1rem',
width: '100%',
margin: '0 auto',
boxSizing: 'border-box',
},
wrapper: {
display: 'flex',
flexDirection: 'column',
border: '1px solid #ddd',
borderRadius: '8px',
padding: '1.5rem',
boxShadow: '0 2px 5px rgba(0, 0, 0, 0.1)',
backgroundColor: '#fff',
maxWidth: '1200px',
margin: '0 auto',
},
wrapperMobile: {
display: 'flex',
flexDirection: 'column',
border: '1px solid #ddd',
borderRadius: '8px',
padding: '1rem',
boxShadow: '0 2px 5px rgba(0, 0, 0, 0.1)',
backgroundColor: '#fff',
},
breadcrumb: {
marginBottom: '1rem',
fontSize: '0.9rem',
},
breadcrumbLink: {
textDecoration: 'none',
color: '#0542cc',
},
header: {
marginBottom: '1rem',
fontSize: '1.5rem',
},
formWrapper: {
display: 'flex',
gap: '2rem',
marginTop: '1rem',
},
formWrapperMobile: {
display: 'flex',
flexDirection: 'column',
gap: '1rem',
marginTop: '1rem',
},
formSection: {
flex: 2,
display: 'flex',
flexDirection: 'column',
gap: '1rem',
},
inputGroup: {
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
},
label: {
fontWeight: 'bold',
},
input: {
padding: '0.5rem',
border: '1px solid #ddd',
borderRadius: '4px',
width: '100%',
},
checkboxGroup: {
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
},
button: {
padding: '0.8rem 1.5rem',
backgroundColor: '#0542cc',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
alignSelf: 'flex-start',
},
detailSection: {
flex: 1,
backgroundColor: '#f5f9ff',
padding: '1rem',
border: '1px solid #ddd',
borderRadius: '8px',
},
detailHeader: {
fontSize: '1.2rem',
marginBottom: '1rem',
color: '#0542cc',
},
detailItem: {
marginBottom: '0.5rem',
fontSize: '0.9rem',
},
};

View File

@ -0,0 +1,256 @@
import React, { useState, useEffect } from 'react';
import { WAB_1, WAB_2 } from '../../../../assets/images';
import { RegisterStep } from '../Content';
// Header component
const HeaderComponent = ({ isMobile }) => {
return (
<div style={isMobile ? styles.headerContainerMobile : styles.headerContainer}>
<img src={WAB_1} alt="WAB_1" style={styles.image(isMobile ? 200 : 350, isMobile ? 100 : 170)} />
<div style={styles.textContainer}>
<h2 style={styles.textTitle}>API For Your WhatsApp Business</h2>
<p style={styles.textBody}>
WhatsApp is one of the most popular communication platforms globally. As a Facebook-owned company,
setting up WhatsApp Business API requires a Facebook account and Facebook Business Manager.
Start leveraging this powerful tool for seamless communication with your customers.
</p>
</div>
</div>
);
};
// Step Component (untuk menampilkan 4 langkah yang ada)
const StepComponent = ({ isMigration, isMobile }) => {
const stepsData = isMigration
? [
{ number: '01', description: 'A Viable Phone Number for WhatsApp' },
{ number: '02', description: 'Your Business Legal address and details' },
]
: [
{ number: '01', description: 'A Viable Phone Number for WhatsApp' },
{ number: '02', description: 'Your Business Legal address and details' },
{ number: '03', description: 'Select Number you want to connect' },
{ number: '04', description: 'Verify your Facebook Business' },
];
return (
<div style={isMobile ? styles.stepsContainerMobile : styles.stepsContainer}>
{stepsData.map((step, index) => (
<div key={index} style={styles.step}>
<div style={styles.stepNumberContainer}>
<span style={styles.stepNumber}>{step.number}</span>
</div>
<p style={styles.stepDescription}>{step.description}</p>
</div>
))}
</div>
);
};
// Info Box Component untuk setiap informasi
const InfoComponent = ({ title, description, onStartNow, isMobile }) => {
return (
<div style={isMobile ? styles.infoBoxMobile : styles.infoBox}>
<h3>{title}</h3>
<p>{description}</p>
<button style={styles.startButton} onClick={onStartNow}>Start Now</button>
</div>
);
};
// Need Component (Menggabungkan Step dan Info Pertama)
const Need = ({ onStartNow, isMobile }) => {
return (
<div style={styles.regisBusinessContainer}>
<div style={styles.rowContainer}>
<div style={styles.col6}>
<StepComponent isMobile={isMobile} />
</div>
<div style={styles.col6}>
<InfoComponent
title="What You Need?"
description="To register for WhatsApp Business API, you'll need an active phone number,
your business's legal address and details, select the number you want to connect, and
verify your Facebook Business account to complete the setup."
onStartNow={onStartNow}
isMobile={isMobile}
/>
</div>
</div>
</div>
);
};
// Migration Component (Menggabungkan Step dan Info Kedua)
const Migration = ({ isMobile }) => {
return (
<div style={styles.regisBusinessContainer}>
<div style={styles.rowContainer}>
<div style={styles.col6}>
<div style={styles.leftSideContainer}>
<img src={WAB_2} alt="WAB_2" style={styles.image(isMobile ? 100 : 180, isMobile ? 100 : 180)} />
<StepComponent isMigration={true} isMobile={isMobile} />
</div>
</div>
<div style={styles.col6}>
<InfoComponent
title="What You Need for Migration?"
description="To complete the migration, you'll need a verified Facebook Business account
and access to the phone number you wish to migrate."
isMobile={isMobile}
/>
</div>
</div>
</div>
);
};
// Main component
const RegisterContent = () => {
const [isStarted, setIsStarted] = useState(false);
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
// Update isMobile state on window resize
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 768);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const handleStartNow = () => {
setIsStarted(true);
};
return (
<div style={styles.regisBusinessContainer}>
{!isStarted && <HeaderComponent isMobile={isMobile} />}
{!isStarted && <Need onStartNow={handleStartNow} isMobile={isMobile} />}
{!isStarted && <Migration isMobile={isMobile} />}
{isStarted && <RegisterStep />}
</div>
);
};
const styles = {
regisBusinessContainer: {
padding: '1rem',
margin: 'auto',
fontFamily: 'Arial, sans-serif'
},
// Header Content Style
headerContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginBottom: '20px',
},
leftSideContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: '10px',
},
image: (width, height) => ({
width: width,
height: height,
flex: '1 1 33%',
alignSelf: 'flex-start',
}),
textContainer: {
flex: '1 1 66%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
paddingLeft: '20px',
},
textTitle: {
fontSize: '24px',
fontWeight: 'bold',
marginBottom: '10px',
},
textBody: {
fontSize: '16px',
lineHeight: '1.5',
},
rowContainer: {
display: 'flex',
flexDirection: 'row',
marginTop: '20px',
flexWrap: 'wrap',
borderRadius: '8px',
border: '1px solid #ddd',
boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.1)',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
minHeight: '200px',
},
col6: {
flex: '1 1 50%',
padding: '10px',
boxSizing: 'border-box',
},
// Step Style
stepsContainer: {
display: 'flex',
flexWrap: 'wrap',
gap: '10px', // Jarak antar langkah
position: 'relative', // Posisi relatif agar connectorLine bisa diposisikan dengan benar
},
step: {
flex: '1 1 16.6%', // Menyusun setiap langkah untuk mengambil 1/6 dari lebar container
padding: '10px',
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
marginBottom: '10px',
position: 'relative', // Agar connectorLine bisa diposisikan di dalam step
},
stepNumberContainer: {
width: '40px', // Menentukan lebar oval
height: '40px', // Menentukan tinggi oval
borderRadius: '50%', // Membuat bentuk oval
backgroundColor: '#0542CC', // Warna latar belakang oval
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '8px',
},
stepNumber: {
fontSize: '16px',
color: '#fff', // Warna teks putih di dalam oval
fontWeight: 'bold',
},
stepDescription: {
fontSize: '14px',
lineHeight: '1.5',
color: '#333',
textAlign: 'center',
margin: '10px 0',
},
// Info Style
infoBox: {
padding: '20px',
marginBottom: '20px',
},
startButton: {
backgroundColor: '#0542cc',
color: '#fff',
padding: '10px 20px',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
marginTop: '15px',
},
};
export default RegisterContent;

View File

@ -0,0 +1,455 @@
import React, { useState, useEffect } from 'react'
import { WAB_2 } from '../../../../assets/images';
const HeaderComponent = () => {
const isMobile = useIsMobile();
return (
<div style={{ ...styles.headerContainer, flexDirection: isMobile ? 'column' : 'row' }}>
<div style={styles.textContainer} className='p-3'>
<h2 style={styles.textTitle}>Registration Step</h2>
<p style={styles.textBody}>
WhatsApp is one of the most popular communication platforms globally. As a Facebook-owned company,
setting up WhatsApp Business API requires a Facebook account and Facebook Business Manager.
Start leveraging this powerful tool for seamless communication with your customers.
</p>
</div>
<img src={WAB_2} alt="WAB_2" style={styles.image(190, 190)} />
</div>
);
};
const useIsMobile = () => {
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 768);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return isMobile;
};
const InfoComponent = ({ title, description, onStartNow }) => {
const [isChecked, setIsChecked] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const handleCheckboxChange = (event) => {
setIsChecked(event.target.checked);
setErrorMessage('');
};
const handleStartNow = () => {
if (!isChecked) {
setErrorMessage('Please confirm agreement on above!');
} else {
onStartNow();
}
};
return (
<div style={styles.contentContainer}>
<h3>{title}</h3>
<p>{description}</p>
<ul style={styles.descriptionList}>
<li>The number must be owned by your company.</li>
<li>Do not use a personal number.</li>
<li>It should not be actively registered on WhatsApp Messenger or WhatsApp Business App.</li>
<li>You must be able to receive phone calls or SMS on the number during the setup process.</li>
</ul>
<div style={styles.checkboxContainer}>
<input
type="checkbox"
id="agreement"
style={styles.checkbox}
checked={isChecked}
onChange={handleCheckboxChange}
/>
<label htmlFor="agreement" style={styles.checkboxLabel}>
I have a number with the above requirements
</label>
</div>
{errorMessage && (
<p style={{ color: 'red', fontSize: '0.875rem' }}>{errorMessage}</p>
)}
<button style={styles.startButton} onClick={handleStartNow}>
Start Now
</button>
</div>
);
};
// StepComponent
const StepComponent = ({ activeStep }) => {
const stepsData = [
{ number: '01', description: 'Phone Number to WhatsApp' },
{ number: '02', description: 'WhatsApp Business Channel' },
{ number: '03', description: 'Verify Your Business on Facebook' },
];
return (
<div style={styles.stepsContainer}>
{stepsData.map((step, index) => {
const isCurrent = activeStep === index;
return (
<div key={index} style={styles.step}>
<div
style={{
...styles.stepNumberContainer,
backgroundColor: isCurrent ? '#0542cc' : '#d3d3d3',
}}
>
<span
style={{
...styles.stepNumber,
color: isCurrent ? '#fff' : '#808080',
}}
>
{step.number}
</span>
</div>
<p
style={{
...styles.stepDescription,
color: isCurrent ? '#000' : 'gray',
}}
>
{step.description}
</p>
</div>
);
})}
</div>
);
};
// FirstContentComponent
const FirstContentComponent = ({ onStartNow }) => (
<div style={styles.rowContainer}>
<div style={styles.col6}>
<StepComponent activeStep={0} /> {/* Displays step 1 as active */}
</div>
<div style={styles.col6}>
<InfoComponent
title="Following Specifications Step"
onStartNow={onStartNow} // Handles "Start Now" button click
/>
</div>
</div>
);
// SecondContentComponent
const SecondContentComponent = ({ onNextStep, onBack }) => {
const [isChecked, setIsChecked] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const handleCheckboxChange = (event) => {
setIsChecked(event.target.checked);
setErrorMessage(''); // Reset error message if checkbox is changed
};
const handleNextStep = () => {
if (!isChecked) {
setErrorMessage('Please confirm agreement on above!');
} else {
onNextStep(); // Proceed to next step if checkbox is checked
}
};
return (
<div style={styles.rowContainer}>
<div style={styles.col6}>
<StepComponent activeStep={1} /> {/* Displays step 2 as active */}
</div>
<div style={styles.col6}>
<p>You'll need the following information for your WhatsApp Business Channel, Be sure to have them on hand.</p>
<ul style={styles.descriptionList}>
<li>Your brand's Display Name, shown to customers you chat with.</li>
<li>Your company's Legal business name.</li>
<li>Your company's official address.</li>
</ul>
<p>Create your WhatsApp Business Channel</p>
<p>You'll be directed to Facebook Business Manager. Complete all the steps in the pop-up in order to create your WhatsApp Business Channel</p>
<div style={styles.checkboxContainer}>
<input
type="checkbox"
id="agreement"
style={styles.checkbox}
checked={isChecked}
onChange={handleCheckboxChange}
/>
<label htmlFor="agreement" style={styles.checkboxLabel}>
Create Your Own Channel
</label>
</div>
{errorMessage && (
<p style={{ color: 'red', fontSize: '0.875rem' }}>{errorMessage}</p>
)}
<button onClick={onBack} style={styles.backButton}>
Back
</button>
<button onClick={handleNextStep} style={styles.startButton}>
Next Step
</button>
</div>
</div>
);
};
// LastContentComponent (ThirdComponent)
const LastContentComponent = ({ onBack }) => {
const [isChecked, setIsChecked] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const handleCheckboxChange = (event) => {
setIsChecked(event.target.checked);
setErrorMessage(''); // Reset error message if checkbox is changed
};
const handleFinish = () => {
if (!isChecked) {
setErrorMessage('Please confirm agreement on above!');
} else {
// Perform the final action here (e.g., submit form, complete registration)
alert('Registration Complete');
}
};
return (
<div style={styles.rowContainer}>
<div style={styles.col6}>
<StepComponent activeStep={2} /> {/* Displays step 3 as active */}
</div>
<div style={styles.col6}>
<p>Ensure your business is fully verified by completing the required steps on Facebook. This process is essential to activate and manage your WhatsApp Business API effectively.</p>
<div style={styles.checkboxContainer}>
<input
type="checkbox"
id="agreement"
style={styles.checkbox}
checked={isChecked}
onChange={handleCheckboxChange}
/>
<label htmlFor="agreement" style={styles.checkboxLabel}>
Verify Your Business on Facebook
</label>
</div>
{errorMessage && (
<p style={{ color: 'red', fontSize: '0.875rem' }}>{errorMessage}</p>
)}
<button onClick={onBack} style={styles.backButton}>
Back
</button>
<button onClick={handleFinish} style={styles.startButton}>
Finish
</button>
</div>
</div>
);
};
// Main Component
const RegisterStep = () => {
const [activeStep, setActiveStep] = useState(0);
const isMobile = useIsMobile();
const handleStartNow = () => {
setActiveStep(1); // Move to the second step
};
const handleNextStep = () => {
setActiveStep(activeStep + 1); // Move to the next step
};
const handleBack = () => {
setActiveStep(activeStep - 1); // Move to the previous step
};
const responsiveStyles = isMobile
? { flexDirection: 'column', alignItems: 'flex-start' }
: { flexDirection: 'row' };
return (
<div>
<HeaderComponent />
<div style={{ ...styles.rowContainer, ...responsiveStyles }}>
{activeStep === 0 && (
<FirstContentComponent onStartNow={handleStartNow} />
)}
{activeStep === 1 && (
<SecondContentComponent onNextStep={handleNextStep} onBack={handleBack} />
)}
{activeStep === 2 && (
<LastContentComponent onBack={handleBack} />
)}
</div>
</div>
);
};
export default RegisterStep
const styles = {
rowContainer: {
display: 'flex',
flexDirection: 'row',
marginTop: '20px',
flexWrap: 'wrap',
borderRadius: '8px',
border: '1px solid #ddd',
boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.1)',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
minHeight: '200px',
},
col6: {
flex: '1 1 48%', // Membagi ruang menjadi 2 kolom
padding: '10px',
boxSizing: 'border-box',
},
headerContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginBottom: '20px',
borderRadius: '8px',
border: '1px solid #ddd',
boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.1)',
justifyContent: 'center',
width: '100%',
minHeight: '250px',
},
leftSideContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: '10px',
},
image: (width, height) => ({
width: width,
height: height,
flex: '1 1 33%',
}),
textContainer: {
flex: '1 1 66%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
paddingLeft: '20px',
},
textTitle: {
fontSize: '24px',
fontWeight: 'bold',
marginBottom: '10px',
},
textBody: {
fontSize: '16px',
lineHeight: '1.5',
},
// Step Styles
stepsContainer: {
display: 'flex',
flexWrap: 'wrap',
gap: '10px', // Jarak antar langkah
position: 'relative',
},
step: {
flex: '1 1 16.6%', // Membagi langkah dalam 1/6 bagian dari lebar container
padding: '10px',
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
marginBottom: '10px',
position: 'relative',
},
stepNumberContainer: {
width: '40px',
height: '40px',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '8px',
transition: 'background-color 0.3s ease',
},
stepNumber: {
fontSize: '16px',
color: '#fff',
fontWeight: 'bold',
},
stepDescription: {
fontSize: '14px',
lineHeight: '1.5',
color: '#333',
textAlign: 'center',
margin: '10px 0',
},
// Content Styles
contentContainer: {
marginTop: '20px',
padding: '20px',
},
infoBox: {
padding: '20px',
marginBottom: '20px',
},
descriptionList: {
listStyleType: 'disc',
paddingLeft: '20px',
color: '#333',
fontSize: '14px',
},
descriptionText: {
marginLeft: '0',
paddingLeft: '0',
fontSize: '14px',
color: '#333',
},
startButton: {
backgroundColor: '#0542cc',
color: '#fff',
padding: '10px 20px',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
marginTop: '15px',
},
checkboxContainer: {
display: 'flex',
alignItems: 'center',
marginBottom: '10px',
},
checkbox: {
marginRight: '10px',
},
checkboxLabel: {
color: '#0542cc',
fontSize: '14px',
},
backButton: {
padding: '10px 20px',
backgroundColor: '#fff', // Background color updated to white
color: '#000', // Text color set to black
border: '2px solid #0542cc', // Border color set to #0542cc
borderRadius: '5px',
cursor: 'pointer',
marginRight: '10px',
},
};

View File

@ -0,0 +1,9 @@
import RegisterContent from "./RegisterContent";
import RegisterStep from "./RegisterStep";
import CreateSettings from "./CreateSettings";
export {
RegisterContent,
RegisterStep,
CreateSettings
}

View File

@ -0,0 +1,437 @@
import React, { useState, useEffect } from 'react';
import {
FaChevronLeft,
FaChevronRight,
FaFastBackward,
FaFastForward,
FaSort,
FaSortUp,
FaSortDown,
FaEdit,
FaEye,
FaTrash
} from 'react-icons/fa'; // Icons for sorting
import { NoAvailable } from '../../../assets/icon';
import { Link } from 'react-router-dom';
// Pagination Component
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
const handlePrev = () => {
if (currentPage > 1) {
onPageChange(currentPage - 1);
}
};
const handleNext = () => {
if (currentPage < totalPages) {
onPageChange(currentPage + 1);
}
};
const handleFirst = () => {
onPageChange(1); // Go to first page
};
const handleLast = () => {
onPageChange(totalPages); // Go to last page
};
// Logic to display only 3 pages in pagination
const getPaginationRange = () => {
const range = [];
const totalPagesCount = totalPages;
let start = currentPage - 1;
let end = currentPage + 1;
// Adjust start and end if near the boundaries
if (currentPage === 1) {
start = 1;
end = Math.min(3, totalPagesCount);
} else if (currentPage === totalPages) {
start = Math.max(totalPagesCount - 2, 1);
end = totalPagesCount;
}
for (let i = start; i <= end; i++) {
range.push(i);
}
return range;
};
const pageRange = getPaginationRange();
return (
<div className="pagination-container d-flex justify-content-end mt-4">
{/* First Page Button */}
<button
className="btn"
onClick={handleFirst}
disabled={currentPage === 1}
>
<FaFastBackward /> {/* Double Arrow Left */}
</button>
<button
className="btn"
onClick={handlePrev}
disabled={currentPage === 1}
>
<FaChevronLeft /> {/* Single Arrow Left */}
</button>
{/* Page Numbers */}
{pageRange.map((pageNum) => (
<button
key={pageNum}
className={`btn ${pageNum === currentPage ? 'btn-primary' : ''}`}
onClick={() => onPageChange(pageNum)}
>
{pageNum}
</button>
))}
<button
className="btn"
onClick={handleNext}
disabled={currentPage === totalPages}
>
<FaChevronRight /> {/* Single Arrow Right */}
</button>
{/* Last Page Button */}
<button
className="btn"
onClick={handleLast}
disabled={currentPage === totalPages}
>
<FaFastForward /> {/* Double Arrow Right */}
</button>
</div>
);
};
const Integration = () => {
const [currentPage, setCurrentPage] = useState(1);
const [isMobile, setIsMobile] = useState(false); // State to detect mobile view
const [transactionData, setTransactionData] = useState([]);
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state
const dataPerPage = 10; // Data per page (10 data per page)
const buttonData = [
{ label: 'Copy', enabled: true },
{ label: 'CSV', enabled: true },
{ label: 'Excel', enabled: true },
{ label: 'PDF', enabled: true },
{ label: 'Print', enabled: true },
{ label: 'Column Visibility', enabled: true },
];
// Generate 691 dummy transactions
const generateDummyData = (numOfItems) => {
const transactionData = [];
for (let i = 1; i <= numOfItems; i++) {
transactionData.push({
channelName: `Channel ${Math.floor(Math.random() * 5) + 1}`, // Random channel name (e.g., Channel 1, Channel 2, ...)
channelId: `CH${String(i).padStart(3, '0')}`, // Random channel ID (e.g., CH001, CH002, ...)
phoneNumber: `+1${Math.floor(Math.random() * 1000000000)}`, // Random phone number (e.g., +1234567890)
status: ['Active', 'Inactive', 'Suspended'][Math.floor(Math.random() * 3)], // Random status
creationDate: new Date(2023, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toLocaleDateString(), // Random date
tier: ['Tier 1', 'Tier 2', 'Tier 3', 'Tier 4', 'Tier 5'][Math.floor(Math.random() * 5)], // Random tier
officialBusinessAccount: `Business ${Math.floor(Math.random() * 5) + 1}`, // Random business account (e.g., Business 1, Business 2, ...)
});
}
return transactionData;
};
// Set the generated transaction data
useEffect(() => {
setTransactionData(generateDummyData(52)); // count data dummy transactions
}, []);
// Sorting function
const sortData = (data, config) => {
const { key, direction } = config;
return [...data].sort((a, b) => {
if (a[key] < b[key]) {
return direction === 'asc' ? -1 : 1;
}
if (a[key] > b[key]) {
return direction === 'asc' ? 1 : -1;
}
return 0;
});
};
// Handle column header sort click
const handleSort = (key) => {
let direction = 'asc';
if (sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc'; // Toggle direction if the same column is clicked
}
setSortConfig({ key, direction });
};
// Get the paginated data
const getPaginatedData = (data, page, perPage) => {
const sortedData = sortData(data, sortConfig);
const startIndex = (page - 1) * perPage;
const endIndex = startIndex + perPage;
return sortedData.slice(startIndex, endIndex);
};
// Handle page change
const handlePageChange = (page) => {
setCurrentPage(page);
};
// Calculate total pages based on the data and data per page
const totalPages = Math.ceil(transactionData.length / dataPerPage);
// Paginated data
const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage);
// Detect screen size and update isMobile state
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768); // Change 768 to your breakpoint
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Define your handle functions
const handleEdit = (transactionId) => {
console.log(`Edit transaction with ID: ${transactionId}`);
// Add your edit logic here
};
const handleShow = (transactionId) => {
console.log(`Show transaction with ID: ${transactionId}`);
// Add your show logic here
};
const handleDelete = (transactionId) => {
console.log(`Delete transaction with ID: ${transactionId}`);
// Add your delete logic here
};
return (
<div className="container mt-5">
{/* Welcome Message */}
<div className="row-card border-left border-primary shadow mb-4" style={{ backgroundColor: '#E2FBEA' }}>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<div>
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
</div>
</div>
</div>
<div style={styles.contentContainer}>
{/* Filter Form */}
<div className="card p-3 mb-4">
<div className="row d-flex justify-content-around"> {/* Using Flexbox */}
{/* Dropdown: Business Account */}
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<select className="form-control">
<option>Select Application</option>
<option>App 1</option>
<option>App 2</option>
<option>App 3</option>
<option>App 4</option>
<option>App 5</option>
</select>
</div>
{/* Dropdown: Status */}
<div className={`col-sm-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<select className="form-control">
<option>Select Status</option>
<option>Status 1</option>
<option>Status 2</option>
<option>Status 3</option>
<option>Status 4</option>
<option>Status 5</option>
</select>
</div>
{/* Apply and Cancel Buttons */}
<div className={`col-12 ${isMobile ? 'd-flex justify-content-between' : 'col-md-2 d-flex align-items-end'}`} style={{ gap: '10px' }}>
<button className="btn btn-primary w-48">Apply</button>
<button className="btn btn-secondary w-48">Cancel</button>
</div>
{/* Create Settings Button with Icon */}
<div className={`col-12 ${isMobile ? 'd-flex justify-content-end' : 'col-md-2 d-flex justify-content-end align-items-end'}`} style={{ gap: '10px' }}>
<Link to="/wa-createSettings" style={{ textDecoration: 'none' }}>
<button className="btn btn-primary">
<i className="fas fa-cogs me-2"></i> {/* Font Awesome icon */}
Create Settings
</button>
</Link>
</div>
</div>
</div>
{/* Table */}
<div className="table-responsive">
<table className="table table-bordered" style={styles.tableContainer}>
<thead>
<tr>
<th>No.</th> {/* Kolom untuk Nomor Urut */}
<th>
<button className="btn" onClick={() => handleSort('channelName')} style={styles.buttonStyle}>
<span>Channel Name</span>
{sortConfig.key === 'channelName' &&
(sortConfig.direction === 'asc' ?
<FaSortUp style={styles.iconMarginLeft} /> :
<FaSortDown style={styles.iconMarginLeft} />
)
}
{sortConfig.key !== 'channelName' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('channelId')} style={styles.buttonStyle}>
Channel ID
{sortConfig.key === 'channelId' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'channelId' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('phoneNumber')} style={styles.buttonStyle}>
Phone Number
{sortConfig.key === 'phoneNumber' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'phoneNumber' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('status')} style={styles.buttonStyle}>
Status
{sortConfig.key === 'status' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'status' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('creationDate')} style={styles.buttonStyle}>
Creation Date
{sortConfig.key === 'creationDate' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'creationDate' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('tier')} style={styles.buttonStyle}>
Tier
{sortConfig.key === 'tier' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'tier' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('officialBusinessAccount')} style={styles.buttonStyle}>
Official Business Account
{sortConfig.key === 'officialBusinessAccount' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'officialBusinessAccount' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
</tr>
</thead>
<tbody>
{paginatedData.length > 0 ? (
paginatedData.map((transaction, index) => (
<tr key={index}>
{/* Kolom Nomor Urut */}
<td>{(currentPage - 1) * dataPerPage + index + 1}</td> {/* Nomor urut berdasarkan halaman dan index */}
<td>{transaction.channelName}</td>
<td>{transaction.channelId}</td>
<td>{transaction.phoneNumber}</td>
<td>{transaction.status}</td>
<td>{transaction.creationDate}</td>
<td>{transaction.tier}</td>
<td>{transaction.officialBusinessAccount}</td>
</tr>
))
) : (
<tr>
<td colSpan="7" className="text-center">
<div className="d-flex flex-column align-items-center mt-5">
<img src={NoAvailable} alt="No Data Available" className="mb-3" style={styles.iconStyle} />
<p>Data not available</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
/>
</div>
</div>
);
};
export default Integration;
const styles = {
contentContainer: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
},
tableContainer: {
minHeight: '300px',
maxHeight: '1500px',
overflowY: 'auto',
},
iconStyle: {
width: '50px',
height: '50px',
},
// Add margin-left style for icons
iconMarginLeft: {
marginLeft: '0.5rem',
verticalAlign: 'middle',
},
buttonStyle: {
display: 'inline-flex',
justifyContent: 'flex-start',
alignItems: 'center',
padding: '0.5rem 1rem',
},
};

View File

@ -0,0 +1,486 @@
import React, { useState, useEffect } from 'react';
import {
FaChevronLeft,
FaChevronRight,
FaFastBackward,
FaFastForward,
FaSort,
FaSortUp,
FaSortDown,
FaEdit,
FaEye,
FaTrash
} from 'react-icons/fa'; // Icons for sorting
import { NoAvailable } from '../../../assets/icon';
// Pagination Component
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
const handlePrev = () => {
if (currentPage > 1) {
onPageChange(currentPage - 1);
}
};
const handleNext = () => {
if (currentPage < totalPages) {
onPageChange(currentPage + 1);
}
};
const handleFirst = () => {
onPageChange(1); // Go to first page
};
const handleLast = () => {
onPageChange(totalPages); // Go to last page
};
// Logic to display only 3 pages in pagination
const getPaginationRange = () => {
const range = [];
const totalPagesCount = totalPages;
let start = currentPage - 1;
let end = currentPage + 1;
// Adjust start and end if near the boundaries
if (currentPage === 1) {
start = 1;
end = Math.min(3, totalPagesCount);
} else if (currentPage === totalPages) {
start = Math.max(totalPagesCount - 2, 1);
end = totalPagesCount;
}
for (let i = start; i <= end; i++) {
range.push(i);
}
return range;
};
const pageRange = getPaginationRange();
return (
<div className="pagination-container d-flex justify-content-end mt-4">
{/* First Page Button */}
<button
className="btn"
onClick={handleFirst}
disabled={currentPage === 1}
>
<FaFastBackward /> {/* Double Arrow Left */}
</button>
<button
className="btn"
onClick={handlePrev}
disabled={currentPage === 1}
>
<FaChevronLeft /> {/* Single Arrow Left */}
</button>
{/* Page Numbers */}
{pageRange.map((pageNum) => (
<button
key={pageNum}
className={`btn ${pageNum === currentPage ? 'btn-primary' : ''}`}
onClick={() => onPageChange(pageNum)}
>
{pageNum}
</button>
))}
<button
className="btn"
onClick={handleNext}
disabled={currentPage === totalPages}
>
<FaChevronRight /> {/* Single Arrow Right */}
</button>
{/* Last Page Button */}
<button
className="btn"
onClick={handleLast}
disabled={currentPage === totalPages}
>
<FaFastForward /> {/* Double Arrow Right */}
</button>
</div>
);
};
const Profile = () => {
const [currentPage, setCurrentPage] = useState(1);
const [isMobile, setIsMobile] = useState(false); // State to detect mobile view
const [transactionData, setTransactionData] = useState([]);
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state
const dataPerPage = 10; // Data per page (10 data per page)
const buttonData = [
{ label: 'Copy', enabled: true },
{ label: 'CSV', enabled: true },
{ label: 'Excel', enabled: true },
{ label: 'PDF', enabled: true },
{ label: 'Print', enabled: true },
{ label: 'Column Visibility', enabled: true },
];
// Generate 691 dummy transactions
const generateDummyData = (numOfItems) => {
const transactionData = [];
for (let i = 1; i <= numOfItems; i++) {
transactionData.push({
channelName: `Channel ${Math.floor(Math.random() * 5) + 1}`, // Random channel name (e.g., Channel 1, Channel 2, ...)
channelId: `CH${String(i).padStart(3, '0')}`, // Random channel ID (e.g., CH001, CH002, ...)
phoneNumber: `+1${Math.floor(Math.random() * 1000000000)}`, // Random phone number (e.g., +1234567890)
status: ['Active', 'Inactive', 'Suspended'][Math.floor(Math.random() * 3)], // Random status
creationDate: new Date(2023, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toLocaleDateString(), // Random date
officialBusinessAccount: `Business ${Math.floor(Math.random() * 5) + 1}`, // Random business account (e.g., Business 1, Business 2, ...)
});
}
return transactionData;
};
// Set the generated transaction data
useEffect(() => {
setTransactionData(generateDummyData(52)); // count data dummy transactions
}, []);
// Sorting function
const sortData = (data, config) => {
const { key, direction } = config;
return [...data].sort((a, b) => {
if (a[key] < b[key]) {
return direction === 'asc' ? -1 : 1;
}
if (a[key] > b[key]) {
return direction === 'asc' ? 1 : -1;
}
return 0;
});
};
// Handle column header sort click
const handleSort = (key) => {
let direction = 'asc';
if (sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc'; // Toggle direction if the same column is clicked
}
setSortConfig({ key, direction });
};
// Get the paginated data
const getPaginatedData = (data, page, perPage) => {
const sortedData = sortData(data, sortConfig);
const startIndex = (page - 1) * perPage;
const endIndex = startIndex + perPage;
return sortedData.slice(startIndex, endIndex);
};
// Handle page change
const handlePageChange = (page) => {
setCurrentPage(page);
};
// Calculate total pages based on the data and data per page
const totalPages = Math.ceil(transactionData.length / dataPerPage);
// Paginated data
const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage);
// Detect screen size and update isMobile state
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768); // Change 768 to your breakpoint
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Define your handle functions
const handleEdit = (transactionId) => {
console.log(`Edit transaction with ID: ${transactionId}`);
// Add your edit logic here
};
const handleShow = (transactionId) => {
console.log(`Show transaction with ID: ${transactionId}`);
// Add your show logic here
};
const handleDelete = (transactionId) => {
console.log(`Delete transaction with ID: ${transactionId}`);
// Add your delete logic here
};
return (
<div className="container mt-5">
{/* Welcome Message */}
<div className="row-card border-left border-primary shadow mb-4" style={{ backgroundColor: '#E2FBEA' }}>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<div>
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
</div>
</div>
</div>
<div style={styles.contentContainer}>
{/* Filter Form */}
<div className="card p-3 mb-4">
<div className="row d-flex justify-content-arround"> {/* Menggunakan Flexbox */}
{/* Dropdown: Business Account */}
<div className={`col-sm-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<select className="form-control">
<option>Select Business Account</option>
<option>Account 1</option>
<option>Account 2</option>
<option>Account 3</option>
<option>Account 4</option>
<option>Account 5</option>
</select>
</div>
{/* Input: Business Account Name */}
<div className={`col-sm-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<input type="text" className="form-control" placeholder="Enter Business Account Name" />
</div>
{/* Dropdown: Meta Manager ID */}
<div className={`col-sm-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<select className="form-control">
<option>Select Meta Manager</option>
<option>Manager 1</option>
<option>Manager 2</option>
<option>Manager 3</option>
<option>Manager 4</option>
<option>Manager 5</option>
</select>
</div>
{/* Dropdown: WABA ID */}
<div className={`col-sm-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
<select className="form-control">
<option>Select WABA ID</option>
<option>WABA 1</option>
<option>WABA 2</option>
<option>WABA 3</option>
<option>WABA 4</option>
<option>WABA 5</option>
</select>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="d-flex justify-content-between align-items-center mb-3">
<div>
{buttonData.map((button, index) =>
button.enabled ? (
<button
key={index}
className={`btn btn-light ${isMobile ? 'mb-2' : ''}`} // Add margin on mobile
style={styles.actionButton}
>
{button.label}
</button>
) : null
)}
</div>
{/* Search Bar with Icon */}
<div className="input-group" style={{ width: '250px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
<input
type="text"
placeholder="Search..."
className="form-control"
/>
<span className="input-group-text">
<i className="fas fa-search"></i> {/* FontAwesome search icon */}
</span>
</div>
</div>
{/* Table */}
<div className="table-responsive">
<table className="table table-bordered" style={styles.tableContainer}>
<thead>
<tr>
<th>No.</th> {/* Kolom untuk Nomor Urut */}
<th>
<button className="btn" onClick={() => handleSort('channelName')} style={styles.buttonStyle}>
<span>Channel Name</span>
{sortConfig.key === 'channelName' &&
(sortConfig.direction === 'asc' ?
<FaSortUp style={styles.iconMarginLeft} /> :
<FaSortDown style={styles.iconMarginLeft} />
)
}
{sortConfig.key !== 'channelName' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('channelId')} style={styles.buttonStyle}>
Channel ID
{sortConfig.key === 'channelId' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'channelId' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('phoneNumber')} style={styles.buttonStyle}>
Phone Number
{sortConfig.key === 'phoneNumber' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'phoneNumber' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('status')} style={styles.buttonStyle}>
Status
{sortConfig.key === 'status' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'status' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('creationDate')} style={styles.buttonStyle}>
Creation Date
{sortConfig.key === 'creationDate' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'creationDate' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('officialBusinessAccount')} style={styles.buttonStyle}>
Official Business Account
{sortConfig.key === 'officialBusinessAccount' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'officialBusinessAccount' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th style={{ textAlign: 'center', verticalAlign: 'middle' }}>Action</th>
</tr>
</thead>
<tbody>
{paginatedData.length > 0 ? (
paginatedData.map((transaction, index) => (
<tr key={index}>
{/* Kolom Nomor Urut */}
<td>{(currentPage - 1) * dataPerPage + index + 1}</td> {/* Nomor urut berdasarkan halaman dan index */}
<td>{transaction.channelName}</td>
<td>{transaction.channelId}</td>
<td>{transaction.phoneNumber}</td>
<td>{transaction.status}</td>
<td>{transaction.creationDate}</td>
<td>{transaction.officialBusinessAccount}</td>
<td style={{ textAlign: 'center' }}>
<div style={{ display: 'flex', justifyContent: 'center', gap: '10px' }}>
<button
className="btn btn-warning"
style={{ display: 'flex', alignItems: 'center' }}
onClick={() => handleEdit(transaction.transactionId)}
>
<FaEdit style={{ marginRight: '5px' }} />
Edit
</button>
<button
className="btn btn-success"
style={{ display: 'flex', alignItems: 'center' }}
onClick={() => handleShow(transaction.transactionId)}
>
<FaEye style={{ marginRight: '5px' }} />
Show
</button>
<button
className="btn btn-danger"
style={{ display: 'flex', alignItems: 'center' }}
onClick={() => handleDelete(transaction.transactionId)}
>
<FaTrash style={{ marginRight: '5px' }} />
Delete
</button>
</div>
</td>
</tr>
))
) : (
<tr>
<td colSpan="7" className="text-center">
<div className="d-flex flex-column align-items-center mt-5">
<img src={NoAvailable} alt="No Data Available" className="mb-3" style={styles.iconStyle} />
<p>Data not available</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
/>
</div>
</div>
);
};
export default Profile;
const styles = {
contentContainer: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
},
tableContainer: {
minHeight: '300px',
maxHeight: '1500px',
overflowY: 'auto',
},
iconStyle: {
width: '50px',
height: '50px',
},
// Add margin-left style for icons
iconMarginLeft: {
marginLeft: '0.5rem',
verticalAlign: 'middle',
},
buttonStyle: {
display: 'inline-flex',
justifyContent: 'flex-start',
alignItems: 'center',
padding: '0.5rem 1rem',
},
};

View File

@ -0,0 +1,29 @@
import React from 'react'
import { RegisterContent } from './Content'
const Registration = () => {
return (
<div className='container' style={styles.container}>
<div style={styles.section}>
<h1>WA Manage</h1>
<RegisterContent />
</div>
</div>
)
}
export default Registration
const styles = {
container: {
marginTop: '3%',
padding: '0 15px',
},
section: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
},
}

View File

@ -0,0 +1,346 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { CopyToClipboard } from 'react-copy-to-clipboard';
const Template = () => {
const BASE_URL = process.env.REACT_APP_BASE_URL;
const API_KEY = process.env.REACT_APP_API_KEY;
const [ownership, setOwnership] = useState([]);
const [buttonStates, setButtonStates] = useState([]);
const [copiedIndex, setCopiedIndex] = useState(null);
const [searchTerm, setSearchTerm] = useState(''); // state for search term
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(''); // state for debounced search term
const [errorMessage, setErrorMessage] = useState(null); // state for error message
// Debounce search term input
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchTerm(searchTerm);
}, 500); // Delay of 500ms before search term is sent
return () => clearTimeout(timer); // Clear timeout when searchTerm changes
}, [searchTerm]);
// Fetch applications based on the search term
useEffect(() => {
let isMounted = true;
const fetchApplications = async () => {
if (debouncedSearchTerm) {
// Fetch by search term
try {
const response = await fetch(`${BASE_URL}/application/get-by-name/${debouncedSearchTerm}`, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': API_KEY,
},
});
const result = await response.json();
if (response.ok) {
if (result.details && result.details.data) {
if (isMounted) {
setOwnership([result.details.data]);
setButtonStates([{ isHovered: false, isActive: false }]);
setErrorMessage(null); // Reset error if data found
}
} else {
if (isMounted) {
setOwnership([]);
setErrorMessage('Data is not found.');
}
}
} else {
if (isMounted) {
setOwnership([]);
setErrorMessage('Data is not found.');
}
}
} catch (error) {
console.error('Error fetching applications:', error);
if (isMounted) {
setOwnership([]);
setErrorMessage('Data is not found.');
}
}
} else {
// Fetch all applications when there's no search term
const fetchAllApplications = async () => {
try {
const response = await fetch(`${BASE_URL}/application/list`, {
method: 'GET',
headers: {
'x-api-key': API_KEY,
},
});
const result = await response.json();
if (response.ok && result.status_code === 200) {
if (isMounted) {
setOwnership(result.details.data);
setButtonStates(result.details.data.map(() => ({ isHovered: false, isActive: false })));
setErrorMessage(null); // Reset error if data found
}
} else {
if (isMounted) {
setOwnership([]);
setErrorMessage('Error fetching data. Please try again.');
}
}
} catch (error) {
console.error('Error fetching all applications:', error);
if (isMounted) {
setOwnership([]);
setErrorMessage('Error fetching data. Please try again.');
}
}
};
fetchAllApplications();
}
};
fetchApplications();
return () => {
isMounted = false; // Set the flag to false on cleanup
};
}, [debouncedSearchTerm]); // Trigger the effect when `debouncedSearchTerm` changes
const handleMouseEnter = (index) => {
setButtonStates((prevStates) => {
const newStates = [...prevStates];
newStates[index].isHovered = true;
return newStates;
});
};
const handleMouseLeave = (index) => {
setButtonStates((prevStates) => {
const newStates = [...prevStates];
newStates[index].isHovered = false;
return newStates;
});
};
const handleChange = (event) => {
setSearchTerm(event.target.value);
};
return (
<div style={styles.container}>
{/* Welcome Message */}
<div className="row-card border-left border-primary shadow mb-4" style={styles.welcomeCard}>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<div>
<h4 className="mb-3 text-start">
<i className="fas fa-message fa-bold me-3"></i>Create WhatsApp Business Templates
</h4>
<p className="mb-0 text-start">
Design pre-approved message templates tailored for various business needs, such as customer support, appointment reminders, order updates, and more. Streamline communication and ensure consistency with ready-to-use templates.
</p>
</div>
<div className="d-flex flex-row mt-3">
<Link to="/" style={{ textDecoration: 'none' }}>
<button className="btn d-flex justify-content-center align-items-center me-2" style={styles.createButton}>
<i className="fas fa-plus text-white me-2"></i>
<p className="text-white mb-0">Create Template</p>
</button>
</Link>
</div>
</div>
</div>
<div style={styles.wrapperCard}>
<div className="card mt-3">
<div className="row">
{/* Filtering Options */}
<div className="col-md-2">
<select className="form-control">
<option value="">Pilih WABA IDI</option>
<option value="waba1">WABA 1</option>
<option value="waba2">WABA 2</option>
<option value="waba3">WABA 3</option>
</select>
</div>
<div className="col-md-2">
<select className="form-control">
<option value="">Pilih Kategori</option>
<option value="kategori1">Kategori 1</option>
<option value="kategori2">Kategori 2</option>
<option value="kategori3">Kategori 3</option>
</select>
</div>
<div className="col-md-2">
<select className="form-control">
<option value="">Pilih Status</option>
<option value="aktif">Aktif</option>
<option value="non-aktif">Non-Aktif</option>
<option value="pending">Pending</option>
</select>
</div>
<div className="col-md-2 d-flex align-items-end" style={{ gap: '10px' }}>
<button className="btn btn-primary w-100">Apply</button>
<button className="btn btn-secondary w-100">Cancel</button>
</div>
</div>
</div>
{/* Search Bar */}
<div style={styles.searchInputWrapper}>
<input
type="text"
placeholder="Type your keywords here"
style={styles.searchInput}
value={searchTerm}
onChange={handleChange}
/>
<button style={styles.searchButton}>
<i className="fas fa-search" style={styles.searchIcon}></i>
</button>
</div>
{/* Application Cards */}
<div style={styles.cardContainer}>
{/* Show message if no data or search result */}
{ownership.length === 0 && !errorMessage && <p style={styles.noDataMessage}>Data is not found.</p>}
{ownership.map((item, index) => (
<div key={item.id} style={styles.card}>
<div style={styles.cardHeader}>
<span style={styles.cardTitle}>{item.name}</span>
</div>
<div style={styles.cardContent}>
<div style={styles.applicationIdWrapper}>
<p><strong>WABA Ownership:</strong></p>
<p>{item.name}</p>
</div>
{/* Copy to Clipboard Button */}
<CopyToClipboard
key={index}
text={item.name}
onCopy={() => {
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null), 2000); // Hide "Copied" status after 2 seconds
}}
>
<button
style={{
backgroundColor: copiedIndex === index ? '#4BA5E7' : '#0542cc',
padding: '10px 20px',
color: 'white',
border: 'none',
cursor: 'pointer',
}}
onMouseEnter={() => handleMouseEnter(index)}
onMouseLeave={() => handleMouseLeave(index)}
>
{copiedIndex === index ? (
<i className="fas fa-check"></i> // Show check icon if copied
) : (
<i className="fas fa-copy"></i> // Show copy icon
)}
{copiedIndex === index ? 'Copied' : 'Copy'}
</button>
</CopyToClipboard>
</div>
</div>
))}
</div>
{/* Show error message */}
{errorMessage && <p style={styles.errorMessage}>{errorMessage}</p>}
</div>
</div>
);
};
const styles = {
container: {
margin: '20px auto',
maxWidth: '1200px',
},
welcomeCard: {
backgroundColor: '#F1F3F6',
borderColor: '#1e2b34',
borderRadius: '5px',
marginBottom: '20px',
},
createButton: {
backgroundColor: '#1d3f68',
color: '#fff',
borderRadius: '5px',
padding: '12px 20px',
width: '150px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
wrapperCard: {
padding: '20px',
},
searchInputWrapper: {
display: 'flex',
alignItems: 'center',
marginTop: '20px',
marginBottom: '20px',
},
searchInput: {
padding: '10px',
border: '1px solid #ccc',
borderRadius: '5px',
width: '300px',
marginRight: '10px',
},
searchButton: {
backgroundColor: '#1d3f68',
border: 'none',
color: 'white',
padding: '10px 15px',
cursor: 'pointer',
},
searchIcon: {
fontSize: '16px',
},
cardContainer: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
gap: '20px',
},
card: {
backgroundColor: '#fff',
border: '1px solid #ddd',
borderRadius: '10px',
padding: '20px',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
},
cardHeader: {
marginBottom: '10px',
},
cardTitle: {
fontSize: '18px',
fontWeight: 'bold',
},
cardContent: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
applicationIdWrapper: {
display: 'flex',
flexDirection: 'column',
},
noDataMessage: {
color: 'gray',
},
errorMessage: {
color: 'red',
}
};
export default Template;

View File

@ -0,0 +1,11 @@
import Registration from './Registration';
import Profile from './Profile';
import Template from './Template';
import Integration from './Integration';
export {
Registration,
Profile,
Template,
Integration
}

View File

@ -0,0 +1,542 @@
import React, { useState, useRef, useEffect } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronLeft, faChevronDown } from '@fortawesome/free-solid-svg-icons';
import Select from 'react-select'
const BASE_URL = process.env.REACT_APP_BASE_URL
const API_KEY = process.env.REACT_APP_API_KEY
const Verify = ({ phoneId, onVerify }) => {
const [otpCode, setOtpCode] = useState('');
const [generateCode, setGenerateCode] = useState('');
const [error, setError] = useState('');
const handleClick = async () => {
console.log('Verify Click');
console.log('otp code: ', otpCode);
console.log('generate code: ', generateCode);
console.log('phone number: ', phoneId);
// Validation logic
if (!otpCode) {
setError("OTP Code is required."); // Set error message directly
return; // Exit the function if there are errors
} else if (otpCode.length !== 6) {
setError("OTP Code must be exactly 6 characters long."); // Set error message directly
return; // Exit the function if there are errors
}
setError(''); // Clear error if validation passes
onVerify(); // Call the onVerify function passed from Auth
};
return (
<div style={styles.section}>
{/* Results Data */}
<div className="form-group row align-items-center mt-4">
<div className="col-md-6">
<div style={styles.selectWrapper}>
<input
type="text"
id="generateCode"
className="form-control"
placeholder="e133d538-dc40-4c43-b042-06aad864d634"
value={generateCode}
onChange={(e) => setGenerateCode(e.target.value)}
readOnly
/>
</div>
</div>
<div className="col-md-6">
<div className="input-group">
<span className="input-group-prepend">
<span className="input-group-text">+62</span>
</span>
<input
type="text"
id="phoneId"
className="form-control"
placeholder="Phone Number"
value={phoneId}
readOnly
/>
</div>
</div>
</div>
{/* OTP Code */}
<div className="form-group row align-items-center mt-3">
<div className="col-md-6">
<input
type="text"
id="otpCode"
className="form-control"
placeholder="OTP Code"
value={otpCode}
onChange={(e) => {
const value = e.target.value;
// Allow only digits and enforce exactly 6 digits
if (/^\d{0,6}$/.test(value)) {
setOtpCode(value);
}
}}
maxLength={6} // Prevents entering more than 6 digits
/>
{error && <small className="text-danger">{error}</small>}
</div>
</div>
{/* Verify Button */}
<div style={styles.submitButton}>
<button onClick={handleClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Verify</p>
</button>
</div>
</div>
);
}
const Preview = () => {
const [inputValue, setInputValue] = useState('');
const handleCopy = () => {
navigator.clipboard.writeText(inputValue);
console.log('Copied');
};
return (
<div className="form-group row align-items-center mt-4">
<div className="col-10">
<div style={styles.selectWrapper}>
<input
type="text"
id="inputValue"
className="form-control"
placeholder="7976 is your verification code. For your security, do not share this code."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
readOnly
/>
</div>
</div>
<div className="col-2 d-flex">
<div style={styles.selectWrapper}>
<button className="btn btn-primary" onClick={handleCopy}>
Copy
</button>
</div>
</div>
</div>
)
}
const Auth = () => {
const [isSelectOpen, setIsSelectOpen] = useState(false);
const [applicationId, setApplicationId] = useState('');
const [expiryId, setExpiryId] = useState(0);
const [otpId, setOtpId] = useState('');
const [phoneId, setPhoneId] = useState('');
const [templateId, setTemplateId] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [applicationIds, setApplicationIds] = useState([]);
const [showVerify, setShowVerify] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [errors, setErrors] = useState({});
const [inputValueApplication, setInputValueApplication] = useState(''); // Controlled input value for Application ID
const [isServer, setIsServer] = useState(true);
const [selectedQuota, setSelectedQuota] = useState(0);
const applicationOptions = applicationIds.map(app => ({
value: app.id,
label: app.name
}));
const handleApplicationChange = (selectedOption) => {
const selectedId = selectedOption.value;
const selectedApp = applicationIds.find(app => app.id === parseInt(selectedId));
setApplicationId(selectedOption ? selectedOption.value : '');
if (selectedApp) {
setSelectedQuota(selectedApp.quota);
}
};
const handleInputChangeApplication = (newInputValue) => {
// Limit input to 15 characters for Application ID
if (newInputValue.length <= 15) {
setInputValueApplication(newInputValue);
}
};
useEffect(() => {
const fetchApplicationIds = async () => {
try {
setIsLoading(true);
const response = await fetch(`${BASE_URL}/application/list`, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': API_KEY,
},
});
if (!response.ok) {
throw new Error('Failed to fetch application IDs');
}
const data = await response.json();
console.log('Response Data:', data);
if (data.status_code === 200) {
setApplicationIds(data.details.data);
setIsServer(true);
} else {
setIsServer(false);
throw new Error(data.details.message || 'Failed to fetch application IDs');
}
} catch (error) {
setErrors(error.message || 'Error fetching application IDs');
setIsServer(false);
} finally {
setIsLoading(false);
}
};
fetchApplicationIds();
}, []);
const handleFocus = () => {
setIsSelectOpen(true);
};
const handleBlur = () => {
setIsSelectOpen(false);
};
const handleVerify = () => {
setShowPreview(true); // Show the preview when verified
};
const validate = () => {
const newErrors = {};
if (!applicationId) newErrors.applicationId = "Application ID is required.";
if (expiryId <= 0) newErrors.expiryId = "Expiry Time must be greater than 0.";
if (!otpId) newErrors.otpId = "OTP Length is required.";
if (!phoneId) newErrors.phoneId = "Phone Number is required.";
if (!templateId) newErrors.templateId = "Template Name is required.";
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleClick = async () => {
if (validate()) {
// Log the input data
console.log('Application ID:', applicationId);
console.log('Expiry ID:', expiryId);
console.log('OTP Length:', otpId);
console.log('Phone Number:', '08' + phoneId); // Prepend "08" to the phone number
console.log('Template ID:', templateId);
console.log('Make Auth Demo');
setShowVerify(true);
}
};
return (
<>
<div style={styles.section}>
{/* Inject keyframes for the spinner */}
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
{isLoading && (
<div style={styles.loadingOverlay}>
<div style={styles.spinner}></div>
</div>
)}
{/* Application ID Selection */}
<div className="form-group row align-items-center">
<div className="col-md-6">
<Select
id="applicationId"
value={applicationOptions.find(option => option.value === applicationId)}
onChange={handleApplicationChange}
options={applicationOptions}
placeholder="Select Application ID"
isSearchable
menuPortalTarget={document.body}
menuPlacement="auto"
inputValue={inputValueApplication}
onInputChange={handleInputChangeApplication} // Limit input length for Application ID
/>
</div>
<div className="col-md-6">
<p className="text-secondary" style={{ fontSize: '16px', fontWeight: '400', margin: '0', marginTop: '8px' }}>
Remaining Quota
</p>
<div style={styles.remainingQuota}>
<span style={styles.quotaText}>{selectedQuota}</span>
<span style={styles.timesText}>(times)</span>
</div>
</div>
</div>
{/* Expiry and OTP */}
<div className="form-group row align-items-center">
<div className="col-md-6">
<div style={styles.selectWrapper}>
<input
type="number"
id="expiryId"
className="form-control"
style={styles.select}
value={expiryId}
onChange={(e) => {
const value = e.target.value;
// Check if the value is empty or a valid number
if (value === '' || /^[0-9]+$/.test(value)) {
setExpiryId(value === '' ? '' : parseInt(value, 10)); // Update state as number
}
}}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder="Expiry Time"
/>
{errors.expiryId && <small className="text-danger">{errors.expiryId}</small>}
</div>
</div>
<div className="col-md-6">
<input
type="text"
id="otpId"
className="form-control"
placeholder="OTP Length"
value={otpId}
onChange={(e) => {
const value = e.target.value;
// Allow only digits and enforce length constraints
if (/^\d{0,6}$/.test(value)) {
setOtpId(value);
}
}}
minLength={6} // Enforce minimum length (for validation)
maxLength={6} // Enforce maximum length
/>
{errors.otpId && <small className="text-danger">{errors.otpId}</small>}
</div>
</div>
{/* Message and Phone */}
<div className="form-group row align-items-center mt-4">
<div className="col-md-6">
<div style={styles.selectWrapper}>
<input
type="text"
id="templateId"
className="form-control"
placeholder="Template Name"
value={templateId}
onChange={(e) => setTemplateId(e.target.value)}
/>
{errors.templateId && <small className="text-danger">{errors.templateId}</small>}
</div>
</div>
<div className="col-md-6">
<div className="input-group">
<span className="input-group-prepend">
<span className="input-group-text">+62</span>
</span>
<input
type="text"
id="phoneId"
className="form-control"
placeholder="Phone Number"
value={phoneId}
onChange={(e) => {
const value = e.target.value;
// Allow only digits and enforce length constraints
if (/^\d*$/.test(value) && value.length <= 14) {
setPhoneId(value);
}
}}
minLength={9} // Enforce minimum length
maxLength={14} // Enforce maximum length
/>
</div>
{errors.phoneId && <small className="text-danger">{errors.phoneId}</small>}
</div>
</div>
{/* Submit Button */}
<div style={styles.submitButton}>
<button onClick={handleClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Make Authentication</p>
</button>
</div>
</div>
{showVerify && <Verify phoneId={phoneId} onVerify={handleVerify} />}
{showPreview && <Preview />}
</>
);
}
export default Auth;
const styles = {
section: {
margin: '1rem 0 1rem 0'
},
formGroup: {
marginTop: '-45px',
},
selectWrapper: {
position: 'relative',
marginTop: '0',
},
select: {
width: '100%',
paddingRight: '30px',
flex: 1,
fontSize: '16px',
border: 'none',
outline: 'none',
},
chevronIcon: {
position: 'absolute',
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'none',
},
remainingQuota: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginTop: '4px',
},
quotaText: {
fontSize: '40px',
color: '#0542cc',
fontWeight: '600',
},
timesText: {
marginLeft: '8px',
verticalAlign: 'super',
fontSize: '20px',
},
buttonContainer: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center', // Center vertically within container
position: 'absolute',
right: '10px',
top: '0',
bottom: '0',
height: '47px',
margin: 'auto 0',
},
button: {
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '0', // Kurangi padding tombol
margin: '0', // Hilangkan margin antar tombol
fontSize: '12px',
},
icon: {
fontSize: '14px',
},
submitButton: {
marginLeft: 'auto',
marginTop: '1.2rem',
textAlign: 'start',
position: 'relative',
zIndex: 1,
},
uploadError: {
color: 'red',
fontSize: '12px',
marginTop: '5px',
},
containerResultStyle: {
padding: '20px',
border: '1px solid #0053b3',
borderRadius: '5px',
width: '100%',
margin: '20px auto',
},
resultContainer: {
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
width: '100%',
},
tableStyle: {
width: '60%',
borderCollapse: 'collapse',
},
imageContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
width: '30%',
marginLeft: '10px'
},
imageStyle: {
width: '193px',
height: '242px',
borderRadius: '5px',
},
loadingOverlay: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.2)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
},
spinner: {
border: '4px solid rgba(0, 0, 0, 0.1)',
borderLeftColor: '#0542cc',
borderRadius: '50%',
width: '90px',
height: '90px',
animation: 'spin 1s ease-in-out infinite',
},
preview: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
marginTop: '1rem',
backgroundColor: '#fff',
},
};

View File

@ -0,0 +1,691 @@
import React, { useState, useRef, useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronDown, faChevronLeft, faCloudUploadAlt, faTimes, faImage } from '@fortawesome/free-solid-svg-icons';
import { FileUploader } from 'react-drag-drop-files';
const BASE_URL = process.env.REACT_APP_BASE_URL
const API_KEY = process.env.REACT_APP_API_KEY
const fileTypes = ['JPG', 'JPEG'];
const CustomLabel = ({ children, ...props }) => {
return <label {...props}>{children}</label>;
};
const SingleMessage = ({ isSelectOpen, handleFocus, handleBlur }) => {
const [applicationId, setApplicationId] = useState('');
const [phoneId, setPhoneId] = useState('');
const [variable_1, setVariable_1] = useState('');
const [templateName, setTemplateName] = useState('');
const [variable_2, setVariable_2] = useState('');
const [preview, setPreview] = useState(null);
const [errors, setErrors] = useState({});
const [applicationIds, setApplicationIds] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchApplicationIds = async () => {
setIsLoading(true);
const url = `${BASE_URL}/application/list`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': API_KEY,
},
});
const data = await response.json();
if (data.status_code === 200) {
setApplicationIds(data.details.data);
} else {
console.error('Failed to fetch data:', data.details.message);
}
} catch (error) {
console.error('Error fetching application IDs:', error);
} finally {
setIsLoading(false);
}
};
fetchApplicationIds();
}, []);
const handleValidation = () => {
const newErrors = {};
if (!applicationId) newErrors.applicationId = "Applications ID is required.";
if (!phoneId) newErrors.phoneId = "Phone Number is required.";
if (!variable_1) newErrors.variable_1 = "Variable 1 is required.";
if (!templateName) newErrors.templateName = "Template Name is required.";
if (!variable_2) newErrors.variable_2 = "Variable 2 is required.";
setErrors(newErrors);
return Object.keys(newErrors).length === 0; // Return true if no errors
};
const handleButtonClick = () => {
if (handleValidation()) {
const formattedPhoneId = `08${phoneId}`;
// Log the input data
console.log({
applicationId,
formattedPhoneId,
variable_1,
templateName,
variable_2
});
setPreview({
message: `Thank you for RSVP, ${variable_1}! We're excited to see you soon.`,
details: `Check the details and mark on your calendar:\nDate: Thursday, October 10, 2024 | 6:00 PM - 9:00 PM\nLocation: Stories SCBD, South Jakarta, Fairground Building, SCBD Lot No.14`
});
}
};
return (
<div>
{/* Inject keyframes for the spinner */}
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
{isLoading && (
<div style={styles.loadingOverlay}>
<div style={styles.spinner}></div>
</div>
)}
<div className="form-group row align-items-center">
<div className="col-md-6">
<div style={styles.selectWrapper}>
<select
id="applicationId"
className="form-control"
style={styles.select}
value={applicationId}
onChange={(e) => setApplicationId(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
>
<option value="">Select Application ID</option>
{applicationIds.map((app) => (
<option key={app.id} value={app.id}>
{app.name}
</option>
))}
</select>
<FontAwesomeIcon
icon={isSelectOpen ? faChevronDown : faChevronLeft}
style={styles.chevronIcon}
/>
</div>
{errors.applicationId && <p className="text-danger">{errors.applicationId}</p>}
</div>
<div className="col-md-6">
<p className="text-secondary" style={{ fontSize: '16px', fontWeight: '400', margin: '0', marginTop: '8px' }}>
Remaining Quota
</p>
<div style={styles.remainingQuota}>
<span style={styles.quotaText}>0</span>
<span style={styles.timesText}>(times)</span>
</div>
</div>
</div>
<div className="form-group row align-items-center mt-4">
<div className="col-md-6">
<div className="input-group">
<span className="input-group-prepend">
<span className="input-group-text">+62</span>
</span>
<input
type="text"
id="phoneId"
className="form-control"
placeholder="Phone Number"
value={phoneId}
onChange={(e) => {
const value = e.target.value;
// Allow only digits and enforce length constraints
if (/^\d*$/.test(value) && value.length <= 14) {
setPhoneId(value);
}
}}
minLength={9}
maxLength={14}
/>
</div>
{errors.phoneId && <p className="text-danger">{errors.phoneId}</p>}
</div>
<div className="col-md-6">
<div className="input-group">
<input
type="text"
id="variable_1"
className="form-control"
placeholder="Variables 1"
value={variable_1}
onChange={(e) => setVariable_1(e.target.value)}
maxLength={15}
/>
</div>
{errors.variable_1 && <p className="text-danger">{errors.variable_1}</p>}
</div>
</div>
<div className="form-group row align-items-center mt-4">
<div className="col-md-6">
<div className="input-group">
<input
type="text"
id="templateName"
className="form-control"
placeholder="Template Name"
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
maxLength={15}
/>
</div>
{errors.templateName && <p className="text-danger">{errors.templateName}</p>}
</div>
<div className="col-md-6">
<div className="input-group">
<input
type="text"
id="variable_2"
className="form-control"
placeholder="Variables 2"
value={variable_2}
onChange={(e) => setVariable_2(e.target.value)}
maxLength={15}
/>
</div>
{errors.variable_2 && <p className="text-danger">{errors.variable_2}</p>}
</div>
</div>
<div style={styles.submitButton}>
<button onClick={handleButtonClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Make SMS Demo</p>
</button>
</div>
{/* Preview Section */}
{preview && (
<div style={styles.preview}>
<div style={styles.waMessagePreview}>
<p><strong>[Sips & Synergy Executive]</strong></p>
<p>{preview.message}</p>
<p>{preview.details}</p>
<button className="btn btn-primary mt-3">Maps Event Location</button>
</div>
</div>
)}
</div>
);
};
const BulkMessage = ({ isSelectOpen, handleFocus, handleBlur, handleClick, onImageUpload }) => {
const [applicationId, setApplicationId] = useState('');
const [templateName, setTemplateName] = useState('');
const [imageData, setImageData] = useState(null);
const [selectedImageName, setSelectedImageName] = useState('');
const [errors, setErrors] = useState({});
const fileInputRef = useRef(null);
const [applicationIds, setApplicationIds] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchApplicationIds = async () => {
setIsLoading(true);
const url = `${BASE_URL}/application/list`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': API_KEY,
},
});
const data = await response.json();
if (data.status_code === 200) {
setApplicationIds(data.details.data);
} else {
console.error('Failed to fetch data:', data.details.message);
}
} catch (error) {
console.error('Error fetching application IDs:', error);
} finally {
setIsLoading(false);
}
};
fetchApplicationIds();
}, []);
const handleValidation = () => {
const newErrors = {};
if (!applicationId) newErrors.applicationId = "Application ID is required.";
if (!templateName) newErrors.templateName = "Template Name is required.";
if (!imageData) newErrors.imageData = "Image is required.";
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleImageUpload = (file) => {
if (file) {
setImageData(file);
setSelectedImageName(file.name);
if (onImageUpload) {
onImageUpload(file);
}
} else {
setErrors((prevErrors) => ({
...prevErrors,
imageData: "Image upload failed."
}));
}
};
const handleButtonClick = () => {
if (handleValidation()) {
handleClick();
console.log('Form Data:', {
applicationId,
templateName,
imageData
});
} else {
console.log('Validation errors:', errors);
}
};
const handleImageCancel = () => {
setImageData(null);
setSelectedImageName('');
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
return (
<div>
{/* Inject keyframes for the spinner */}
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
<div className="form-group row align-items-center">
<div className="col-md-6">
<div style={styles.selectWrapper}>
<select
id="applicationId"
className="form-control"
style={styles.select}
value={applicationId}
onChange={(e) => setApplicationId(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
>
<option value="">Select Application ID</option>
{applicationIds.map((app) => (
<option key={app.id} value={app.id}>
{app.name}
</option>
))}
</select>
<FontAwesomeIcon
icon={isSelectOpen ? faChevronDown : faChevronLeft}
style={styles.chevronIcon}
/>
</div>
{errors.applicationId && <p className="text-danger">{errors.applicationId}</p>}
</div>
<div className="col-md-6">
<p className="text-secondary" style={{ fontSize: '16px', fontWeight: '400', margin: '0', marginTop: '8px' }}>
Remaining Quota
</p>
<div style={styles.remainingQuota}>
<span style={styles.quotaText}>0</span>
<span style={styles.timesText}>(times)</span>
</div>
</div>
</div>
<div className="form-group row align-items-center mt-4">
<div className="col-md-6">
<div className="input-group">
<input
type="text"
id="templateName"
className="form-control"
placeholder="Template Name"
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
maxLength={15}
/>
</div>
{errors.templateName && <p className="text-danger">{errors.templateName}</p>}
</div>
</div>
{/* Upload Section */}
<div className='col-md-6'>
<div className="row form-group mt-4">
<CustomLabel htmlFor="uploadPhoto" style={styles.customLabel}>
Upload Face Photo
</CustomLabel>
<FileUploader
handleChange={handleImageUpload}
name="file"
types={fileTypes}
multiple={false}
onDrop={(files) => {
// Ensure files array is not empty
if (files.length > 0) {
handleImageUpload(files[0]);
}
}}
children={
<div style={styles.uploadArea}>
<FontAwesomeIcon icon={faCloudUploadAlt} style={styles.uploadIcon} />
<p style={styles.uploadText}>Drag and Drop Here</p>
<p>Or</p>
<a href="#" onClick={() => fileInputRef.current.click()}>Browse</a>
<p className="text-muted">Recommended size: 300x300 (Max File Size: 2MB)</p>
<p className="text-muted">Supported file types: JPG, JPEG</p>
</div>
}
/>
<input
type="file"
id="fileUpload"
ref={fileInputRef}
style={{ display: 'none' }}
accept="image/jpeg, image/jpg"
onChange={e => {
if (e.target.files.length > 0) {
handleImageUpload(e.target.files[0]);
}
}}
/>
{errors.imageData && <small style={{ color: 'red' }}>{errors.imageData}</small>}
</div>
</div>
{/* Display uploaded image name */}
{selectedImageName && (
<div className="col-md-6 mt-4">
<div style={styles.fileWrapper}>
<FontAwesomeIcon icon={faImage} style={styles.imageIcon} />
<div style={{ marginRight: '18rem', marginTop: '0.2rem' }}>
<h5>Uploaded File:</h5>
<p>{selectedImageName}</p>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<button
style={{ background: 'none', border: 'none', cursor: 'pointer' }}
onClick={handleImageCancel}
aria-label="Remove uploaded image"
>
<FontAwesomeIcon
icon={faTimes}
style={styles.closeIcon}
/>
</button>
</div>
</div>
</div>
)}
<div style={styles.selectWrapper}>
<div style={styles.submitButton}>
<button onClick={handleButtonClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Make SMS Demo</p>
</button>
</div>
</div>
</div>
);
};
const Message = () => {
const [isSelectOpen, setIsSelectOpen] = useState(false);
const [activeTab, setActiveTab] = useState('single');
const fileInputRef = useRef(null);
const handleFocus = () => {
setIsSelectOpen(true);
};
const handleBlur = () => {
setIsSelectOpen(false);
};
const handleClick = async () => {
console.log('Make SMS Demo');
};
const handleImageUpload = (event) => {
const file = event.target.files[0];
if (file && (file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg')) {
console.log('File uploaded:', file.name); // Handle the file upload
} else {
alert('Please upload a valid image file (JPG, JPEG).');
}
};
return (
<div>
{/* Tab buttons */}
<div>
<button
onClick={() => setActiveTab('single')}
style={activeTab === 'single' ? styles.tabButton : styles.tabInactiveButton}
>
Single Message
</button>
<button
onClick={() => setActiveTab('bulk')}
style={activeTab === 'bulk' ? styles.tabButton : styles.tabInactiveButton}
>
Bulk Message
</button>
</div>
{/* Conditional content based on active tab */}
{activeTab === 'single' && <SingleMessage
isSelectOpen={isSelectOpen}
handleFocus={handleFocus}
handleBlur={handleBlur}
handleClick={handleClick}
/>}
{activeTab === 'bulk' && <BulkMessage
isSelectOpen={isSelectOpen}
handleFocus={handleFocus}
handleBlur={handleBlur}
handleClick={handleClick}
handleImageUpload={handleImageUpload}
fileInputRef={fileInputRef}
/>}
</div>
);
};
export default Message;
const styles = {
selectWrapper: {
position: 'relative',
marginTop: '0',
},
select: {
width: '100%',
paddingRight: '30px',
flex: 1,
fontSize: '16px',
outline: 'none',
},
chevronIcon: {
position: 'absolute',
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'none',
},
remainingQuota: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginTop: '4px',
},
quotaText: {
fontSize: '40px',
color: '#0542cc',
fontWeight: '600',
},
timesText: {
marginLeft: '8px',
verticalAlign: 'super',
fontSize: '20px',
},
tabButton: {
display: 'inline-block',
padding: '10px 20px',
fontSize: '16px',
cursor: 'pointer',
backgroundColor: '#0542cc',
color: '#fff',
border: 'none',
marginRight: '10px',
borderRadius: '7px'
},
tabInactiveButton: {
display: 'inline-block',
padding: '10px 20px',
fontSize: '16px',
cursor: 'pointer',
backgroundColor: '#fff',
color: '#0542cc',
border: '1px solid #0542cc',
marginRight: '10px',
borderRadius: '7px'
},
submitButton: {
marginLeft: 'auto',
marginTop: '1rem',
textAlign: 'start',
position: 'relative',
zIndex: 1,
},
uploadArea: {
backgroundColor: '#e6f2ff',
height: '250px',
cursor: 'pointer',
paddingTop: '22px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
border: '1px solid #ced4da',
borderRadius: '0.25rem',
},
uploadIcon: {
fontSize: '40px',
color: '#0542cc',
marginBottom: '7px',
},
uploadText: {
color: '#1f2d3d',
fontWeight: '400',
fontSize: '16px',
lineHeight: '13px',
},
uploadError: {
color: 'red',
},
waMessagePreview: {
backgroundColor: '#F8F9FA',
padding: '15px',
borderRadius: '8px',
border: '1px solid #dcdcdc',
marginBottom: '20px',
},
preview: {
marginTop: '20px',
},
// Bulk Message
uploadArea: {
backgroundColor: '#e6f2ff',
height: '250px',
cursor: 'pointer',
marginTop: '1rem',
paddingTop: '22px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
border: '1px solid #ced4da',
borderRadius: '0.25rem',
},
uploadIcon: {
fontSize: '40px',
color: '#0542cc',
marginBottom: '7px',
},
uploadText: {
color: '#1f2d3d',
fontWeight: '400',
fontSize: '16px',
lineHeight: '13px',
},
fileWrapper: {
backgroundColor: '#fff',
border: '0.2px solid gray',
padding: '15px 0 0 17px',
borderRadius: '5px',
position: 'relative',
display: 'flex',
alignItems: 'center',
gap: '10px',
justifyContent: 'space-between',
},
imageIcon: {
color: '#0542cc',
fontSize: '24px',
marginBottom: '1rem'
},
closeIcon: {
color: 'red',
cursor: 'pointer',
fontSize: '26px',
marginRight: '1rem',
marginBottom: '1rem'
},
customLabel: {
fontWeight: 600,
fontSize: '14px',
color: '#212529'
},
submitButton: {
marginTop: '20px',
},
};

View File

@ -0,0 +1,7 @@
import Auth from "./Auth";
import Message from "./Message";
export {
Auth,
Message
}

View File

@ -0,0 +1,114 @@
import React, { useEffect, useState } from 'react';
import { Link, Routes, Route, useNavigate } from 'react-router-dom';
import {
Auth as AuthWa,
Message as MessageWa
} from './Section';
const Verify = () => {
const verifyTabs = [
{ name: 'Auth', link: 'wa-auth' },
{ name: 'Message', link: 'wa-message' }
];
const [isMobile, setIsMobile] = useState(false);
const navigate = useNavigate();
// Redirect otomatis ke rute default saat akses ke /wa-verify
useEffect(() => {
if (window.location.pathname === '/wa-verify') {
navigate('wa-auth', { replace: true });
}
}, [navigate]);
// Update state isMobile berdasarkan ukuran layar
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<div className="container" style={styles.container}>
{/* Static Content */}
<div className="row-card border-left border-primary shadow mb-4" style={styles.welcomeCard}>
<div className="d-flex flex-column justify-content-start align-items-start p-4">
<h4 className="mb-3 text-start">
<i className="fas fa-warning fa-bold me-3"></i>Alert
</h4>
<p className="mb-0 text-start">
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
Experience the ease and flexibility of trying out all our features firsthand.
</p>
<div className="d-flex flex-row mt-3">
<Link to="/createApps" style={{ textDecoration: 'none' }}>
<button className="btn d-flex justify-content-center align-items-center me-2" style={styles.createButton}>
<i className="fas fa-plus text-white me-2"></i>
<p className="text-white mb-0">Create New App ID</p>
</button>
</Link>
</div>
</div>
</div>
{/* Tab Navigation */}
<div style={styles.section}>
<div className={`d-flex ${isMobile ? 'flex-column' : 'flex-row'} justify-content-between align-items-center mb-3`}>
<div className="d-flex flex-wrap">
{verifyTabs.map((tab) => (
<Link
key={tab.link}
to={tab.link}
className={`btn ${window.location.pathname.includes(tab.link) ? 'btn-primary' : 'btn-light'} me-2 mb-2`}
style={styles.tabLink}
>
{tab.name}
</Link>
))}
</div>
</div>
{/* Dynamic Tab Content */}
<div className="tab-content">
<Routes>
<Route path="wa-auth" element={<AuthWa />} />
<Route path="wa-message" element={<MessageWa />} />
</Routes>
</div>
</div>
</div>
);
};
export default Verify;
const styles = {
container: {
marginTop: '3%',
padding: '0 15px',
},
welcomeCard: {
backgroundColor: '#E2FBEA',
borderLeft: '4px solid #0542CC',
borderRadius: '5px',
marginBottom: '20px',
},
createButton: {
backgroundColor: '#0542CC',
},
section: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542CC',
borderRadius: '10px',
width: '100%',
},
tabLink: {
padding: '10px 20px',
},
};

View File

@ -0,0 +1,5 @@
import Verify from "./Verify";
export {
Verify
}