2227 lines
76 KiB
HTML
2227 lines
76 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="darkreader-lock">
|
|
|
|
<!-- Primary Meta Tags -->
|
|
<title>E-Dir - Dhiraagu Phone Directory Lookup | Find Maldives Phone Numbers</title>
|
|
<meta name="title" content="E-Dir - Dhiraagu Phone Directory Lookup | Find Maldives Phone Numbers">
|
|
<meta name="description" content="Search Dhiraagu phone directory instantly. Find Maldives phone numbers, addresses, and contact details. Fast, free, no captcha. Alternative to official Dhiraagu E-Directory.">
|
|
<meta name="keywords" content="dhiraagu, e-directory, edir, e-dir, phone directory, maldives phone numbers, dhiraagu directory, phone lookup, maldives directory, dhiraagu phone search, find phone number maldives, dhiraagu number search, maldives telephone directory">
|
|
<meta name="author" content="E-Dir">
|
|
<meta name="robots" content="index, follow">
|
|
<meta name="language" content="English">
|
|
<meta name="revisit-after" content="7 days">
|
|
|
|
<!-- Canonical URL -->
|
|
<link rel="canonical" href="https://dhiraagu-edir.shihaam.me/">
|
|
|
|
<!-- Favicon -->
|
|
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
<link rel="icon" type="image/png" href="favicon.png">
|
|
<link rel="apple-touch-icon" href="apple-touch-icon.png">
|
|
|
|
<!-- Open Graph / Facebook -->
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:url" content="https://dhiraagu-edir.shihaam.me/">
|
|
<meta property="og:title" content="E-Dir - Dhiraagu Phone Directory Lookup | Find Maldives Phone Numbers">
|
|
<meta property="og:description" content="Search Dhiraagu phone directory instantly. Find Maldives phone numbers, addresses, and contact details. Fast, free, no captcha required.">
|
|
<meta property="og:image" content="https://dhiraagu-edir.shihaam.me/og-image.png">
|
|
<meta property="og:site_name" content="E-Dir">
|
|
<meta property="og:locale" content="en_US">
|
|
|
|
<!-- Twitter -->
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:url" content="https://dhiraagu-edir.shihaam.me/">
|
|
<meta name="twitter:title" content="E-Dir - Dhiraagu Phone Directory Lookup">
|
|
<meta name="twitter:description" content="Search Dhiraagu phone directory instantly. Find Maldives phone numbers fast, free, no captcha.">
|
|
<meta name="twitter:image" content="https://dhiraagu-edir.shihaam.me/og-image.png">
|
|
|
|
<!-- Additional SEO -->
|
|
<meta name="application-name" content="E-Dir">
|
|
<meta name="theme-color" content="#ff8c00">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<meta name="apple-mobile-web-app-title" content="E-Dir">
|
|
|
|
<!-- Structured Data -->
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "WebApplication",
|
|
"name": "E-Dir",
|
|
"alternateName": "Dhiraagu E-Directory Alternative",
|
|
"description": "Search Dhiraagu phone directory instantly. Find Maldives phone numbers, addresses, and contact details without captcha.",
|
|
"url": "https://dhiraagu-edir.shihaam.me/",
|
|
"applicationCategory": "UtilitiesApplication",
|
|
"operatingSystem": "Any",
|
|
"offers": {
|
|
"@type": "Offer",
|
|
"price": "0",
|
|
"priceCurrency": "USD"
|
|
}
|
|
}
|
|
</script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:root {
|
|
--orange: #ff8c00;
|
|
--bg-primary: #0a0a0a;
|
|
--bg-secondary: #1a1a1a;
|
|
--bg-card: #111111;
|
|
--text-primary: #ffffff;
|
|
--text-secondary: #666666;
|
|
--border: #2a2a2a;
|
|
--border-hover: #ff8c00;
|
|
--success: #00ff66;
|
|
--error: #ff3333;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Roboto', sans-serif;
|
|
background-color: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 40px 20px;
|
|
border-top: 4px solid var(--orange);
|
|
}
|
|
|
|
.container {
|
|
width: 100%;
|
|
max-width: 1400px;
|
|
}
|
|
|
|
.main-layout {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
align-items: start;
|
|
}
|
|
|
|
.main-layout > .left-column,
|
|
.main-layout > .right-column {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.main-layout {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.card {
|
|
background-color: var(--bg-card);
|
|
border: 2px solid var(--border);
|
|
padding: 28px;
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
margin-bottom: 28px;
|
|
}
|
|
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 16px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.logo-icon {
|
|
width: 56px;
|
|
height: 56px;
|
|
background-color: var(--orange);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 2px solid var(--orange);
|
|
}
|
|
|
|
.logo-icon .material-icons {
|
|
font-size: 32px;
|
|
color: white;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 48px;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 4px;
|
|
}
|
|
|
|
.subtitle {
|
|
color: var(--text-secondary);
|
|
font-size: 12px;
|
|
margin-top: 8px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 2px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.input-group {
|
|
position: relative;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.search-form-row {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.search-form-row .input-group {
|
|
flex: 1;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.search-form-row .btn-icon {
|
|
padding: 16px;
|
|
}
|
|
|
|
.input-group .material-icons {
|
|
position: absolute;
|
|
left: 16px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: var(--text-secondary);
|
|
transition: color 0.2s ease;
|
|
}
|
|
|
|
.input-group input {
|
|
width: 100%;
|
|
padding: 20px 20px 20px 56px;
|
|
font-size: 18px;
|
|
font-family: 'Roboto', sans-serif;
|
|
border: 2px solid var(--border);
|
|
background-color: var(--bg-secondary);
|
|
color: var(--text-primary);
|
|
outline: none;
|
|
transition: all 0.15s ease;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.input-group input::placeholder {
|
|
color: var(--text-secondary);
|
|
text-transform: none;
|
|
}
|
|
|
|
.input-group input:focus {
|
|
border-color: var(--orange);
|
|
background-color: var(--bg-card);
|
|
}
|
|
|
|
.input-group input:focus + .material-icons,
|
|
.input-group:focus-within .material-icons {
|
|
color: var(--orange);
|
|
}
|
|
|
|
.btn {
|
|
width: 100%;
|
|
padding: 20px 24px;
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
font-family: 'Roboto', sans-serif;
|
|
text-transform: uppercase;
|
|
letter-spacing: 2px;
|
|
border: 2px solid var(--orange);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 12px;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.btn-primary {
|
|
background-color: var(--orange);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background-color: var(--border-hover);
|
|
border-color: var(--border-hover);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.btn-primary:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.btn-primary:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.btn-danger {
|
|
color: var(--error);
|
|
font-size: 14px;
|
|
padding: 14px 24px;
|
|
margin-top: 16px;
|
|
border: 2px solid var(--border);
|
|
background-color: transparent;
|
|
font-weight: 700;
|
|
letter-spacing: 1.5px;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background-color: var(--error);
|
|
color: white;
|
|
border-color: var(--error);
|
|
}
|
|
|
|
.btn-outline {
|
|
color: var(--orange);
|
|
font-size: 14px;
|
|
padding: 14px 24px;
|
|
border: 2px solid var(--orange);
|
|
background-color: transparent;
|
|
font-weight: 700;
|
|
letter-spacing: 1.5px;
|
|
}
|
|
|
|
.btn-outline:hover {
|
|
background-color: var(--orange);
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger-filled {
|
|
background-color: var(--error);
|
|
color: white;
|
|
font-size: 14px;
|
|
padding: 14px 24px;
|
|
border: 2px solid var(--error);
|
|
font-weight: 700;
|
|
letter-spacing: 1.5px;
|
|
}
|
|
|
|
.btn-danger-filled:hover {
|
|
background-color: #ff1744;
|
|
border-color: #ff1744;
|
|
}
|
|
|
|
.btn-icon {
|
|
padding: 8px;
|
|
margin-top: 0;
|
|
width: auto;
|
|
min-width: unset;
|
|
flex-shrink: 0;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.btn-icon .material-icons {
|
|
font-size: 20px;
|
|
}
|
|
|
|
.spinner {
|
|
width: 20px;
|
|
height: 20px;
|
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
border-top-color: white;
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 24px;
|
|
padding-bottom: 16px;
|
|
border-bottom: 2px solid var(--border);
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 2px;
|
|
}
|
|
|
|
.search-box {
|
|
position: relative;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.search-box .material-icons {
|
|
position: absolute;
|
|
left: 16px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: var(--text-secondary);
|
|
transition: color 0.2s ease;
|
|
}
|
|
|
|
.search-box input {
|
|
width: 100%;
|
|
padding: 16px 16px 16px 56px;
|
|
font-size: 16px;
|
|
font-family: 'Roboto', sans-serif;
|
|
border: 2px solid var(--border);
|
|
background-color: var(--bg-secondary);
|
|
color: var(--text-primary);
|
|
outline: none;
|
|
transition: all 0.15s ease;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.search-box input::placeholder {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.search-box input:focus {
|
|
border-color: var(--orange);
|
|
background-color: var(--bg-card);
|
|
}
|
|
|
|
.search-box input:focus + .material-icons {
|
|
color: var(--orange);
|
|
}
|
|
|
|
.search-box .clear-search {
|
|
position: absolute;
|
|
right: 0;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
color: var(--text-secondary);
|
|
display: none;
|
|
padding: 4px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.search-box .clear-search:hover {
|
|
color: var(--error);
|
|
background-color: rgba(244, 67, 54, 0.1);
|
|
}
|
|
|
|
.search-box .clear-search.show {
|
|
display: block;
|
|
}
|
|
|
|
.results-count {
|
|
font-size: 12px;
|
|
font-family: 'Roboto Mono', monospace;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 16px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.mobile-table {
|
|
display: none;
|
|
}
|
|
|
|
.mobile-table .table-container {
|
|
border: 2px solid var(--border);
|
|
overflow: hidden;
|
|
max-height: 500px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.mobile-table table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.mobile-table thead {
|
|
position: sticky;
|
|
top: 0;
|
|
background-color: var(--bg-secondary);
|
|
z-index: 10;
|
|
border-bottom: 2px solid var(--border);
|
|
}
|
|
|
|
.mobile-table th {
|
|
padding: 12px 10px;
|
|
text-align: left;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
color: var(--text-secondary);
|
|
font-family: 'Roboto Mono', monospace;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.mobile-table th:hover {
|
|
color: var(--orange);
|
|
background-color: var(--bg-card);
|
|
}
|
|
|
|
.mobile-table th.sortable::after {
|
|
content: '↕';
|
|
font-size: 9px;
|
|
margin-left: 4px;
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.mobile-table th.sort-asc::after {
|
|
content: '↑';
|
|
opacity: 1;
|
|
}
|
|
|
|
.mobile-table th.sort-desc::after {
|
|
content: '↓';
|
|
opacity: 1;
|
|
}
|
|
|
|
.mobile-table td {
|
|
padding: 12px 10px;
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
border-bottom: 1px solid var(--border);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.mobile-table .phone-number {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.mobile-table .result {
|
|
max-width: 150px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.mobile-table .datetime {
|
|
font-size: 11px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.mobile-table .datetime .date-part {
|
|
display: block;
|
|
}
|
|
|
|
.mobile-table .datetime .date-part::after {
|
|
content: none;
|
|
}
|
|
|
|
.mobile-table tbody tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.mobile-table tbody tr {
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.mobile-table tbody tr:hover {
|
|
background-color: var(--bg-secondary);
|
|
}
|
|
|
|
.mobile-table .phone-icon-btn .material-icons {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.mobile-table .phone-text {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.table-container {
|
|
border: 2px solid var(--border);
|
|
overflow: hidden;
|
|
max-height: 500px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.table-container::-webkit-scrollbar {
|
|
width: 12px;
|
|
}
|
|
|
|
.table-container::-webkit-scrollbar-track {
|
|
background: var(--bg-secondary);
|
|
border-left: 2px solid var(--border);
|
|
}
|
|
|
|
.table-container::-webkit-scrollbar-thumb {
|
|
background: var(--border-hover);
|
|
border: 2px solid var(--border);
|
|
}
|
|
|
|
.table-container::-webkit-scrollbar-thumb:hover {
|
|
background: var(--border-hover);
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
thead {
|
|
position: sticky;
|
|
top: 0;
|
|
background-color: var(--bg-secondary);
|
|
z-index: 10;
|
|
border-bottom: 2px solid var(--border);
|
|
}
|
|
|
|
th {
|
|
padding: 16px 14px;
|
|
text-align: left;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1.5px;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: all 0.15s ease;
|
|
font-family: 'Roboto Mono', monospace;
|
|
}
|
|
|
|
th:hover {
|
|
color: var(--border-hover);
|
|
background-color: var(--bg-card);
|
|
}
|
|
|
|
th.sortable::after {
|
|
content: '↕';
|
|
font-size: 10px;
|
|
margin-left: 4px;
|
|
opacity: 0.3;
|
|
}
|
|
|
|
th.sort-asc::after {
|
|
content: '↑';
|
|
opacity: 1;
|
|
}
|
|
|
|
th.sort-desc::after {
|
|
content: '↓';
|
|
opacity: 1;
|
|
}
|
|
|
|
td {
|
|
padding: 16px 14px;
|
|
font-size: 14px;
|
|
color: var(--text-primary);
|
|
border-bottom: 1px solid var(--border);
|
|
font-weight: 500;
|
|
}
|
|
|
|
tbody tr {
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
tbody tr:hover {
|
|
background-color: var(--bg-secondary);
|
|
}
|
|
|
|
tbody tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.phone-number {
|
|
font-weight: 700;
|
|
font-family: 'Roboto Mono', monospace;
|
|
color: var(--orange);
|
|
font-size: 16px;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
td.phone-number {
|
|
padding: 16px 14px;
|
|
}
|
|
|
|
.phone-cell {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 6px;
|
|
}
|
|
|
|
.phone-icons {
|
|
display: flex;
|
|
flex-direction: row;
|
|
gap: 12px;
|
|
}
|
|
|
|
.phone-icon-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--orange);
|
|
text-decoration: none;
|
|
transition: all 0.15s ease;
|
|
cursor: pointer;
|
|
background: none;
|
|
border: none;
|
|
padding: 0;
|
|
}
|
|
|
|
.phone-icon-btn:hover {
|
|
color: #ffaa44;
|
|
}
|
|
|
|
.phone-icon-btn .material-icons {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.phone-text {
|
|
color: var(--orange);
|
|
font-weight: 700;
|
|
font-family: 'Roboto Mono', monospace;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.result {
|
|
word-break: break-word;
|
|
max-width: 300px;
|
|
color: var(--text-primary);
|
|
font-weight: 500;
|
|
font-size: 15px;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.result.copyable {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.result.copyable:hover {
|
|
color: var(--orange);
|
|
}
|
|
|
|
.result.copyable:active {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.datetime {
|
|
font-size: 12px;
|
|
font-family: 'Roboto Mono', monospace;
|
|
color: var(--text-secondary);
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.datetime .date-part::after {
|
|
content: ' ';
|
|
}
|
|
|
|
.datetime .time-part {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.status-badge {
|
|
display: inline-block;
|
|
padding: 6px 16px;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
font-family: 'Roboto Mono', monospace;
|
|
border: 2px solid;
|
|
}
|
|
|
|
.status-success {
|
|
background-color: transparent;
|
|
color: var(--success);
|
|
border-color: var(--success);
|
|
}
|
|
|
|
.status-not-found {
|
|
background-color: transparent;
|
|
color: var(--error);
|
|
border-color: var(--error);
|
|
}
|
|
|
|
.status-error {
|
|
background-color: transparent;
|
|
color: var(--error);
|
|
border-color: var(--error);
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 80px 20px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.empty-state .material-icons {
|
|
font-size: 72px;
|
|
margin-bottom: 20px;
|
|
opacity: 0.2;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.empty-state h3 {
|
|
font-size: 20px;
|
|
margin-bottom: 8px;
|
|
color: var(--text-primary);
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 2px;
|
|
}
|
|
|
|
.empty-state p {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.toast {
|
|
position: fixed;
|
|
bottom: 30px;
|
|
left: 50%;
|
|
transform: translateX(-50%) translateY(100px);
|
|
background-color: var(--bg-card);
|
|
border: 2px solid var(--border);
|
|
color: var(--text-primary);
|
|
padding: 16px 28px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
opacity: 0;
|
|
transition: all 0.15s ease;
|
|
z-index: 1000;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.toast.show {
|
|
transform: translateX(-50%) translateY(0);
|
|
opacity: 1;
|
|
}
|
|
|
|
.toast .material-icons {
|
|
font-size: 20px;
|
|
}
|
|
|
|
.toast.success .material-icons {
|
|
color: var(--success);
|
|
}
|
|
|
|
.toast.error .material-icons {
|
|
color: var(--error);
|
|
}
|
|
|
|
.confirm-modal {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
animation: fadeIn 0.2s ease;
|
|
}
|
|
|
|
.confirm-modal.show {
|
|
display: flex;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
.confirm-card {
|
|
background-color: var(--bg-card);
|
|
border: 2px solid var(--border);
|
|
padding: 32px;
|
|
max-width: 400px;
|
|
width: 90%;
|
|
animation: scaleIn 0.15s ease;
|
|
}
|
|
|
|
.confirm-title {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
margin-bottom: 16px;
|
|
color: var(--text-primary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 2px;
|
|
}
|
|
|
|
.confirm-message {
|
|
font-size: 14px;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 28px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
@keyframes scaleIn {
|
|
from {
|
|
transform: scale(0.9);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: scale(1);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.confirm-title {
|
|
font-size: 20px;
|
|
font-weight: 500;
|
|
margin-bottom: 12px;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.confirm-message {
|
|
font-size: 14px;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.confirm-actions {
|
|
display: flex;
|
|
gap: 16px;
|
|
}
|
|
|
|
.confirm-actions .btn {
|
|
flex: 1;
|
|
font-size: 14px;
|
|
padding: 16px;
|
|
}
|
|
|
|
.qr-card {
|
|
text-align: center;
|
|
}
|
|
|
|
.qr-card .confirm-title {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.qr-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.qr-container canvas {
|
|
border: 2px solid var(--border);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.container {
|
|
padding: 0;
|
|
}
|
|
|
|
.card {
|
|
border-left: none;
|
|
border-right: none;
|
|
padding: 24px;
|
|
}
|
|
|
|
.mobile-table {
|
|
display: block;
|
|
}
|
|
|
|
.desktop-table {
|
|
display: none;
|
|
}
|
|
|
|
.section-header {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
gap: 16px;
|
|
border-bottom: none;
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
.btn-danger:not(.btn-icon) {
|
|
width: 100%;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 32px;
|
|
letter-spacing: 2px;
|
|
}
|
|
|
|
.logo-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
}
|
|
|
|
.logo-icon .material-icons {
|
|
font-size: 28px;
|
|
}
|
|
|
|
.search-box {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.results-count {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.emergency-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.emergency-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
|
|
.emergency-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 16px;
|
|
background-color: var(--bg-secondary);
|
|
border: 2px solid var(--border);
|
|
text-decoration: none;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.emergency-card:hover {
|
|
border-color: var(--orange);
|
|
background-color: var(--bg-card);
|
|
}
|
|
|
|
.emergency-card .material-icons {
|
|
font-size: 28px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.emergency-card:hover .material-icons {
|
|
color: var(--orange);
|
|
}
|
|
|
|
.emergency-critical {
|
|
border-color: var(--error);
|
|
}
|
|
|
|
.emergency-critical .material-icons {
|
|
color: var(--error);
|
|
}
|
|
|
|
.emergency-critical:hover {
|
|
background-color: rgba(255, 51, 51, 0.1);
|
|
border-color: var(--error);
|
|
}
|
|
|
|
.emergency-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.emergency-name {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.emergency-number {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
font-family: 'Roboto Mono', monospace;
|
|
color: var(--text-primary);
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.emergency-section {
|
|
margin-top: 20px;
|
|
border: 2px solid var(--border);
|
|
height: fit-content;
|
|
}
|
|
|
|
.emergency-section summary {
|
|
padding: 16px 20px;
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1.5px;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
background-color: var(--bg-secondary);
|
|
transition: all 0.15s ease;
|
|
list-style: none;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.emergency-section summary::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
|
|
.emergency-section summary::after {
|
|
content: '▼';
|
|
font-size: 12px;
|
|
transition: transform 0.2s ease;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.emergency-section[open] summary::after {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.emergency-section summary:hover::after {
|
|
opacity: 1;
|
|
}
|
|
|
|
.emergency-section summary:hover {
|
|
color: var(--orange);
|
|
background-color: var(--bg-card);
|
|
}
|
|
|
|
.emergency-section > .emergency-grid {
|
|
padding: 16px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="main-layout">
|
|
<div class="left-column">
|
|
<div class="card">
|
|
<div class="header">
|
|
<div class="logo">
|
|
<div class="logo-icon">
|
|
<span class="material-icons">contacts</span>
|
|
</div>
|
|
<h1>E-Dir</h1>
|
|
</div>
|
|
<p class="subtitle">Dhiraagu Directory Lookup</p>
|
|
</div>
|
|
|
|
<form id="searchForm" class="search-form-row">
|
|
<div class="input-group">
|
|
<input
|
|
type="tel"
|
|
id="phoneNumber"
|
|
placeholder="Enter phone number"
|
|
autocomplete="tel"
|
|
required
|
|
>
|
|
<span class="material-icons">phone</span>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary btn-icon" id="searchBtn" title="Search">
|
|
<span class="material-icons">search</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="section-header">
|
|
<h2 class="section-title">Search History</h2>
|
|
<button class="btn btn-danger btn-icon" id="clearHistoryBtn" title="Clear History">
|
|
<span class="material-icons">delete</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="search-box">
|
|
<input type="text" id="tableSearch" placeholder="Search history...">
|
|
<span class="material-icons">filter_list</span>
|
|
<button class="clear-search" id="clearSearchBtn">
|
|
<span class="material-icons">close</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="results-count" id="resultsCount"></div>
|
|
|
|
<div class="table-container desktop-table">
|
|
<table id="historyTable">
|
|
<thead>
|
|
<tr>
|
|
<th class="sortable" data-sort="phone">Phone</th>
|
|
<th class="sortable" data-sort="result">Result</th>
|
|
<th class="sortable" data-sort="timestamp">Date/Time</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="historyTableBody">
|
|
</tbody>
|
|
</table>
|
|
<div id="emptyState" class="empty-state">
|
|
<span class="material-icons">history</span>
|
|
<h3>No search history yet</h3>
|
|
<p>Your searches will appear here</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mobile-table" id="mobileTableContainer">
|
|
<div class="table-container">
|
|
<table id="mobileHistoryTable">
|
|
<thead>
|
|
<tr>
|
|
<th class="sortable" data-sort="phone">Phone</th>
|
|
<th class="sortable" data-sort="result">Result</th>
|
|
<th class="sortable" data-sort="timestamp">Date/Time</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="mobileTableBody">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div id="emptyStateMobile" class="empty-state mobile-table">
|
|
<span class="material-icons">history</span>
|
|
<h3>No search history yet</h3>
|
|
<p>Your searches will appear here</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="right-column">
|
|
<div class="card">
|
|
<div class="section-header">
|
|
<h2 class="section-title">Emergency Numbers</h2>
|
|
</div>
|
|
<div class="emergency-grid">
|
|
<a href="tel:119" class="emergency-card emergency-critical">
|
|
<span class="material-icons">local_police</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">Police</span>
|
|
<span class="emergency-number">119</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:118" class="emergency-card emergency-critical">
|
|
<span class="material-icons">local_fire_department</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">Fire</span>
|
|
<span class="emergency-number">118</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:102" class="emergency-card emergency-critical">
|
|
<span class="material-icons">emergency</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">Ambulance</span>
|
|
<span class="emergency-number">102</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:191" class="emergency-card emergency-critical">
|
|
<span class="material-icons">sailing</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">Coast Guard</span>
|
|
<span class="emergency-number">191</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:1676" class="emergency-card">
|
|
<span class="material-icons">health_and_safety</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">HPA</span>
|
|
<span class="emergency-number">1676</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:1412" class="emergency-card">
|
|
<span class="material-icons">child_care</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">Child Helpline</span>
|
|
<span class="emergency-number">1412</span>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
|
|
<details class="emergency-section">
|
|
<summary>Public Services</summary>
|
|
<div class="emergency-grid">
|
|
<a href="tel:104" class="emergency-card">
|
|
<span class="material-icons">bolt</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">STELCO</span>
|
|
<span class="emergency-number">104</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:105" class="emergency-card">
|
|
<span class="material-icons">water_drop</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">MWSC</span>
|
|
<span class="emergency-number">105</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:1402" class="emergency-card">
|
|
<span class="material-icons">account_balance</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">NSPA</span>
|
|
<span class="emergency-number">1402</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:1400" class="emergency-card">
|
|
<span class="material-icons">medical_services</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">Aasandha</span>
|
|
<span class="emergency-number">1400</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:1415" class="emergency-card">
|
|
<span class="material-icons">receipt_long</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">MIRA</span>
|
|
<span class="emergency-number">1415</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:1416" class="emergency-card">
|
|
<span class="material-icons">inventory</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">Customs</span>
|
|
<span class="emergency-number">1416</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:1414" class="emergency-card">
|
|
<span class="material-icons">how_to_vote</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">Elections</span>
|
|
<span class="emergency-number">1414</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:1410" class="emergency-card">
|
|
<span class="material-icons">psychology</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">Drug Rehab</span>
|
|
<span class="emergency-number">1410</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:110" class="emergency-card">
|
|
<span class="material-icons">contact_phone</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">Directory Enquiries</span>
|
|
<span class="emergency-number">110</span>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</details>
|
|
|
|
<details class="emergency-section">
|
|
<summary>Medical</summary>
|
|
<div class="emergency-grid">
|
|
<a href="tel:3335335" class="emergency-card">
|
|
<span class="material-icons">local_hospital</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">IGMH</span>
|
|
<span class="emergency-number">3335335</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:3313553" class="emergency-card">
|
|
<span class="material-icons">local_hospital</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">ADK</span>
|
|
<span class="emergency-number">3313553</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:3351610" class="emergency-card">
|
|
<span class="material-icons">local_hospital</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">Tree Top</span>
|
|
<span class="emergency-number">3351610</span>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</details>
|
|
|
|
<details class="emergency-section">
|
|
<summary>Travel</summary>
|
|
<div class="emergency-grid">
|
|
<a href="tel:3322211" class="emergency-card">
|
|
<span class="material-icons">flight</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">Flight Info</span>
|
|
<span class="emergency-number">3322211</span>
|
|
</div>
|
|
</a>
|
|
<a href="tel:3335544" class="emergency-card">
|
|
<span class="material-icons">flight_takeoff</span>
|
|
<div class="emergency-info">
|
|
<span class="emergency-name">Maldivian</span>
|
|
<span class="emergency-number">3335544</span>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast" id="toast">
|
|
<span class="material-icons" id="toastIcon"></span>
|
|
<span id="toastMessage"></span>
|
|
</div>
|
|
|
|
<div class="confirm-modal" id="confirmModal">
|
|
<div class="confirm-card">
|
|
<h3 class="confirm-title">Clear History?</h3>
|
|
<p class="confirm-message">Are you sure you want to clear all search history? This action cannot be undone.</p>
|
|
<div class="confirm-actions">
|
|
<button class="btn btn-outline" id="cancelClearBtn">Cancel</button>
|
|
<button class="btn btn-danger-filled" id="confirmClearBtn">Clear</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="confirm-modal" id="qrModal">
|
|
<div class="confirm-card qr-card">
|
|
<h3 class="confirm-title" id="qrTitle">QR Code</h3>
|
|
<div class="qr-container" id="qrContainer"></div>
|
|
<div class="confirm-actions">
|
|
<button class="btn btn-outline" id="closeQrBtn">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const form = document.getElementById('searchForm');
|
|
const phoneInput = document.getElementById('phoneNumber');
|
|
const searchBtn = document.getElementById('searchBtn');
|
|
const tableSearch = document.getElementById('tableSearch');
|
|
const clearSearchBtn = document.getElementById('clearSearchBtn');
|
|
const clearHistoryBtn = document.getElementById('clearHistoryBtn');
|
|
const historyTableBody = document.getElementById('historyTableBody');
|
|
const emptyState = document.getElementById('emptyState');
|
|
const resultsCount = document.getElementById('resultsCount');
|
|
const toast = document.getElementById('toast');
|
|
const toastMessage = document.getElementById('toastMessage');
|
|
const toastIcon = document.getElementById('toastIcon');
|
|
const confirmModal = document.getElementById('confirmModal');
|
|
const cancelClearBtn = document.getElementById('cancelClearBtn');
|
|
const confirmClearBtn = document.getElementById('confirmClearBtn');
|
|
const mobileTableContainer = document.getElementById('mobileTableContainer');
|
|
const mobileTableBody = document.getElementById('mobileTableBody');
|
|
const emptyStateMobile = document.getElementById('emptyStateMobile');
|
|
const qrModal = document.getElementById('qrModal');
|
|
const qrContainer = document.getElementById('qrContainer');
|
|
const qrTitle = document.getElementById('qrTitle');
|
|
const closeQrBtn = document.getElementById('closeQrBtn');
|
|
|
|
let searchHistory = [];
|
|
let currentSort = { column: 'timestamp', direction: 'desc' };
|
|
|
|
const HISTORY_KEY = 'edir_search_history';
|
|
|
|
function loadHistory() {
|
|
const stored = localStorage.getItem(HISTORY_KEY);
|
|
if (stored) {
|
|
searchHistory = JSON.parse(stored);
|
|
}
|
|
renderTable();
|
|
}
|
|
|
|
function saveHistory() {
|
|
localStorage.setItem(HISTORY_KEY, JSON.stringify(searchHistory));
|
|
}
|
|
|
|
function addToHistory(phone, result, status) {
|
|
const entry = {
|
|
id: Date.now(),
|
|
phone,
|
|
result,
|
|
status,
|
|
timestamp: Date.now()
|
|
};
|
|
searchHistory.unshift(entry);
|
|
saveHistory();
|
|
renderTable();
|
|
}
|
|
|
|
function clearHistory() {
|
|
searchHistory = [];
|
|
localStorage.removeItem(HISTORY_KEY);
|
|
renderTable();
|
|
showToast('History cleared', 'success');
|
|
}
|
|
|
|
function formatDate(timestamp) {
|
|
const date = new Date(timestamp);
|
|
const datePart = date.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric'
|
|
});
|
|
const timePart = date.toLocaleTimeString('en-US', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: true
|
|
});
|
|
return `<span class="date-part">${datePart}</span><span class="time-part">${timePart}</span>`;
|
|
}
|
|
|
|
function getStatusBadge(status) {
|
|
const badges = {
|
|
success: '<span class="status-badge status-success">Found</span>',
|
|
not_found: '<span class="status-badge status-not-found">Not Found</span>',
|
|
error: '<span class="status-badge status-error">Error</span>'
|
|
};
|
|
return badges[status] || badges.error;
|
|
}
|
|
|
|
function renderTable(filteredHistory = null) {
|
|
const data = filteredHistory || getSortedHistory();
|
|
historyTableBody.innerHTML = '';
|
|
mobileTableBody.innerHTML = '';
|
|
|
|
if (data.length === 0) {
|
|
emptyState.style.display = 'block';
|
|
mobileTableContainer.style.display = 'none';
|
|
emptyStateMobile.style.display = ''; // Let CSS media queries handle visibility
|
|
resultsCount.textContent = filteredHistory ? 'No results found' : '';
|
|
return;
|
|
}
|
|
|
|
emptyState.style.display = 'none';
|
|
mobileTableContainer.style.display = '';
|
|
emptyStateMobile.style.display = 'none';
|
|
resultsCount.textContent = `Showing ${data.length} ${data.length === 1 ? 'result' : 'results'}`;
|
|
|
|
data.forEach((entry, index) => {
|
|
let resultHtml;
|
|
if (entry.status === 'not_found') {
|
|
resultHtml = getStatusBadge(entry.status);
|
|
} else if (entry.status === 'error') {
|
|
resultHtml = `${escapeHtml(entry.result)}<div style="margin-top: 8px;">${getStatusBadge(entry.status)}</div>`;
|
|
} else {
|
|
resultHtml = escapeHtml(entry.result);
|
|
}
|
|
|
|
// Desktop table row
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td class="phone-number">
|
|
<div class="phone-cell">
|
|
<div class="phone-icons">
|
|
<a href="tel:${escapeHtml(entry.phone)}" class="phone-icon-btn" title="Call">
|
|
<span class="material-icons">phone</span>
|
|
</a>
|
|
<button class="phone-icon-btn qr-btn" title="QR Code">
|
|
<span class="material-icons">qr_code_2</span>
|
|
</button>
|
|
</div>
|
|
<span class="phone-text">${escapeHtml(entry.phone)}</span>
|
|
</div>
|
|
</td>
|
|
<td class="result">${resultHtml}</td>
|
|
<td class="datetime">${formatDate(entry.timestamp)}</td>
|
|
`;
|
|
row.querySelector('.qr-btn').addEventListener('click', () => showQrCode(entry.phone));
|
|
const resultCell = row.querySelector('.result');
|
|
if (entry.status === 'success') {
|
|
resultCell.classList.add('copyable');
|
|
resultCell.addEventListener('click', () => copyToClipboard(entry.result));
|
|
}
|
|
historyTableBody.appendChild(row);
|
|
|
|
// Mobile table row
|
|
const mobileRow = document.createElement('tr');
|
|
mobileRow.innerHTML = `
|
|
<td class="phone-number">
|
|
<div class="phone-cell">
|
|
<div class="phone-icons">
|
|
<a href="tel:${escapeHtml(entry.phone)}" class="phone-icon-btn" title="Call">
|
|
<span class="material-icons">phone</span>
|
|
</a>
|
|
<button class="phone-icon-btn qr-btn" title="QR Code">
|
|
<span class="material-icons">qr_code_2</span>
|
|
</button>
|
|
</div>
|
|
<span class="phone-text">${escapeHtml(entry.phone)}</span>
|
|
</div>
|
|
</td>
|
|
<td class="result">${resultHtml}</td>
|
|
<td class="datetime">${formatDate(entry.timestamp)}</td>
|
|
`;
|
|
mobileRow.querySelector('.qr-btn').addEventListener('click', () => showQrCode(entry.phone));
|
|
const mobileResultCell = mobileRow.querySelector('.result');
|
|
if (entry.status === 'success') {
|
|
mobileResultCell.classList.add('copyable');
|
|
mobileResultCell.addEventListener('click', () => copyToClipboard(entry.result));
|
|
}
|
|
mobileTableBody.appendChild(mobileRow);
|
|
});
|
|
}
|
|
|
|
function getSortedHistory() {
|
|
const sorted = [...searchHistory];
|
|
sorted.sort((a, b) => {
|
|
let comparison = 0;
|
|
if (currentSort.column === 'phone') {
|
|
comparison = a.phone.localeCompare(b.phone);
|
|
} else if (currentSort.column === 'result') {
|
|
comparison = a.result.localeCompare(b.result);
|
|
} else if (currentSort.column === 'timestamp') {
|
|
comparison = a.timestamp - b.timestamp;
|
|
} else if (currentSort.column === 'status') {
|
|
comparison = a.status.localeCompare(b.status);
|
|
}
|
|
return currentSort.direction === 'asc' ? comparison : -comparison;
|
|
});
|
|
return sorted;
|
|
}
|
|
|
|
function filterHistory(searchTerm) {
|
|
if (!searchTerm.trim()) {
|
|
renderTable();
|
|
clearSearchBtn.classList.remove('show');
|
|
return;
|
|
}
|
|
|
|
const term = searchTerm.toLowerCase();
|
|
const filtered = searchHistory.filter(entry =>
|
|
entry.phone.toLowerCase().includes(term) ||
|
|
entry.result.toLowerCase().includes(term)
|
|
);
|
|
|
|
renderTable(filtered);
|
|
clearSearchBtn.classList.add('show');
|
|
}
|
|
|
|
function handleSort(column) {
|
|
if (currentSort.column === column) {
|
|
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
currentSort.column = column;
|
|
currentSort.direction = 'asc';
|
|
}
|
|
|
|
document.querySelectorAll('th').forEach(th => {
|
|
th.classList.remove('sort-asc', 'sort-desc');
|
|
});
|
|
document.querySelectorAll(`th[data-sort="${column}"]`).forEach(th => {
|
|
th.classList.add(currentSort.direction === 'asc' ? 'sort-asc' : 'sort-desc');
|
|
});
|
|
|
|
const searchTerm = tableSearch.value;
|
|
if (searchTerm.trim()) {
|
|
filterHistory(searchTerm);
|
|
} else {
|
|
renderTable();
|
|
}
|
|
}
|
|
|
|
function showToast(message, type = 'success') {
|
|
toastMessage.textContent = message;
|
|
toast.className = `toast ${type}`;
|
|
toastIcon.textContent = type === 'success' ? 'check_circle' : 'error';
|
|
toast.classList.add('show');
|
|
|
|
setTimeout(() => {
|
|
toast.classList.remove('show');
|
|
}, 3000);
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
async function copyToClipboard(text) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
showToast('Copied to clipboard', 'success');
|
|
} catch (err) {
|
|
showToast('Failed to copy', 'error');
|
|
}
|
|
}
|
|
|
|
// Minimal QR Code Generator
|
|
const QRCode = (function() {
|
|
const EC_L = 1, EC_M = 0, EC_Q = 3, EC_H = 2;
|
|
|
|
function generateQR(text, ecl = EC_M) {
|
|
const data = encodeData(text, ecl);
|
|
const size = data.size;
|
|
const modules = data.modules;
|
|
return { size, modules };
|
|
}
|
|
|
|
function encodeData(text, ecl) {
|
|
const bytes = new TextEncoder().encode(text);
|
|
let version = 1;
|
|
for (; version <= 40; version++) {
|
|
const capacity = getCapacity(version, ecl);
|
|
if (bytes.length <= capacity) break;
|
|
}
|
|
if (version > 40) throw new Error('Data too long');
|
|
|
|
const size = version * 4 + 17;
|
|
const modules = Array(size).fill(null).map(() => Array(size).fill(null));
|
|
const ecInfo = getECInfo(version, ecl);
|
|
|
|
// Place finder patterns
|
|
placeFinderPattern(modules, 0, 0);
|
|
placeFinderPattern(modules, size - 7, 0);
|
|
placeFinderPattern(modules, 0, size - 7);
|
|
|
|
// Place timing patterns
|
|
for (let i = 8; i < size - 8; i++) {
|
|
modules[6][i] = modules[i][6] = i % 2 === 0;
|
|
}
|
|
|
|
// Place alignment patterns
|
|
if (version >= 2) {
|
|
const positions = getAlignmentPositions(version);
|
|
for (const row of positions) {
|
|
for (const col of positions) {
|
|
if (modules[row][col] === null) {
|
|
placeAlignmentPattern(modules, row, col);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reserve format info areas
|
|
for (let i = 0; i < 9; i++) {
|
|
if (modules[8][i] === null) modules[8][i] = false;
|
|
if (modules[i][8] === null) modules[i][8] = false;
|
|
if (i < 8) {
|
|
if (modules[8][size - 1 - i] === null) modules[8][size - 1 - i] = false;
|
|
if (modules[size - 1 - i][8] === null) modules[size - 1 - i][8] = false;
|
|
}
|
|
}
|
|
modules[size - 8][8] = true; // Dark module
|
|
|
|
// Reserve version info for version >= 7
|
|
if (version >= 7) {
|
|
for (let i = 0; i < 6; i++) {
|
|
for (let j = 0; j < 3; j++) {
|
|
modules[i][size - 11 + j] = false;
|
|
modules[size - 11 + j][i] = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Encode data
|
|
const dataCodewords = encodeDataCodewords(bytes, version, ecl);
|
|
const ecCodewords = generateECCodewords(dataCodewords, ecInfo);
|
|
const allCodewords = interleaveCodewords(dataCodewords, ecCodewords, ecInfo);
|
|
|
|
// Place data
|
|
placeData(modules, allCodewords, size);
|
|
|
|
// Apply mask and format info
|
|
const mask = selectBestMask(modules, size, ecl);
|
|
applyMask(modules, mask, size);
|
|
placeFormatInfo(modules, ecl, mask, size);
|
|
|
|
if (version >= 7) {
|
|
placeVersionInfo(modules, version, size);
|
|
}
|
|
|
|
return { size, modules };
|
|
}
|
|
|
|
function getCapacity(version, ecl) {
|
|
const capacities = [
|
|
[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],
|
|
[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],
|
|
[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],
|
|
[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],
|
|
[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],
|
|
[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],
|
|
[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],
|
|
[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]
|
|
];
|
|
return capacities[version - 1][[EC_L, EC_M, EC_Q, EC_H].indexOf(ecl)] - 3;
|
|
}
|
|
|
|
function getECInfo(version, ecl) {
|
|
const table = {
|
|
1: { 0: [1,26,19], 1: [1,26,16], 2: [1,26,13], 3: [1,26,9] },
|
|
2: { 0: [1,44,34], 1: [1,44,28], 2: [1,44,22], 3: [1,44,16] },
|
|
3: { 0: [1,70,55], 1: [1,70,44], 2: [2,35,17], 3: [2,35,13] },
|
|
4: { 0: [1,100,80], 1: [2,50,32], 2: [2,50,24], 3: [4,25,9] },
|
|
5: { 0: [1,134,108], 1: [2,67,43], 2: [2,33,15,2,34,16], 3: [2,33,11,2,34,12] }
|
|
};
|
|
if (table[version] && table[version][ecl]) {
|
|
const info = table[version][ecl];
|
|
if (info.length === 3) {
|
|
return { blocks: [{ count: info[0], total: info[1], data: info[2] }] };
|
|
} else {
|
|
return { blocks: [
|
|
{ count: info[0], total: info[1], data: info[2] },
|
|
{ count: info[3], total: info[4], data: info[5] }
|
|
]};
|
|
}
|
|
}
|
|
return { blocks: [{ count: 1, total: 26, data: 19 }] };
|
|
}
|
|
|
|
function placeFinderPattern(modules, row, col) {
|
|
for (let r = -1; r <= 7; r++) {
|
|
for (let c = -1; c <= 7; c++) {
|
|
const tr = row + r, tc = col + c;
|
|
if (tr < 0 || tc < 0 || tr >= modules.length || tc >= modules.length) continue;
|
|
const isBorder = r === -1 || r === 7 || c === -1 || c === 7;
|
|
const isOuter = r === 0 || r === 6 || c === 0 || c === 6;
|
|
const isInner = r >= 2 && r <= 4 && c >= 2 && c <= 4;
|
|
modules[tr][tc] = !isBorder && (isOuter || isInner);
|
|
}
|
|
}
|
|
}
|
|
|
|
function placeAlignmentPattern(modules, row, col) {
|
|
for (let r = -2; r <= 2; r++) {
|
|
for (let c = -2; c <= 2; c++) {
|
|
modules[row + r][col + c] = Math.abs(r) === 2 || Math.abs(c) === 2 || (r === 0 && c === 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getAlignmentPositions(version) {
|
|
if (version === 1) return [];
|
|
const positions = [6];
|
|
const step = Math.floor(version / 7) + 1;
|
|
const last = version * 4 + 10;
|
|
const diff = last - 6;
|
|
const stepSize = Math.ceil(diff / step);
|
|
for (let i = last - stepSize * (step - 1); i <= last; i += stepSize) {
|
|
if (!positions.includes(i)) positions.push(i);
|
|
}
|
|
return positions.sort((a, b) => a - b);
|
|
}
|
|
|
|
function encodeDataCodewords(bytes, version, ecl) {
|
|
const bits = [];
|
|
bits.push(0, 1, 0, 0); // Byte mode indicator
|
|
const countBits = version < 10 ? 8 : 16;
|
|
for (let i = countBits - 1; i >= 0; i--) {
|
|
bits.push((bytes.length >> i) & 1);
|
|
}
|
|
for (const byte of bytes) {
|
|
for (let i = 7; i >= 0; i--) {
|
|
bits.push((byte >> i) & 1);
|
|
}
|
|
}
|
|
|
|
const ecInfo = getECInfo(version, ecl);
|
|
let totalData = 0;
|
|
for (const block of ecInfo.blocks) {
|
|
totalData += block.count * block.data;
|
|
}
|
|
const totalBits = totalData * 8;
|
|
|
|
// Add terminator
|
|
for (let i = 0; i < 4 && bits.length < totalBits; i++) bits.push(0);
|
|
while (bits.length % 8 !== 0) bits.push(0);
|
|
|
|
// Add padding
|
|
let pad = true;
|
|
while (bits.length < totalBits) {
|
|
const padByte = pad ? 0xEC : 0x11;
|
|
for (let i = 7; i >= 0; i--) bits.push((padByte >> i) & 1);
|
|
pad = !pad;
|
|
}
|
|
|
|
const codewords = [];
|
|
for (let i = 0; i < bits.length; i += 8) {
|
|
let byte = 0;
|
|
for (let j = 0; j < 8; j++) byte = (byte << 1) | bits[i + j];
|
|
codewords.push(byte);
|
|
}
|
|
return codewords;
|
|
}
|
|
|
|
function generateECCodewords(data, ecInfo) {
|
|
const allEC = [];
|
|
let dataIndex = 0;
|
|
for (const block of ecInfo.blocks) {
|
|
for (let i = 0; i < block.count; i++) {
|
|
const blockData = data.slice(dataIndex, dataIndex + block.data);
|
|
dataIndex += block.data;
|
|
const ecCount = block.total - block.data;
|
|
const ec = reedSolomon(blockData, ecCount);
|
|
allEC.push(ec);
|
|
}
|
|
}
|
|
return allEC;
|
|
}
|
|
|
|
function reedSolomon(data, ecCount) {
|
|
const gf = {
|
|
exp: new Uint8Array(512),
|
|
log: new Uint8Array(256)
|
|
};
|
|
let x = 1;
|
|
for (let i = 0; i < 255; i++) {
|
|
gf.exp[i] = x;
|
|
gf.log[x] = i;
|
|
x <<= 1;
|
|
if (x & 0x100) x ^= 0x11d;
|
|
}
|
|
for (let i = 255; i < 512; i++) gf.exp[i] = gf.exp[i - 255];
|
|
|
|
const gen = [1];
|
|
for (let i = 0; i < ecCount; i++) {
|
|
const newGen = new Array(gen.length + 1).fill(0);
|
|
for (let j = 0; j < gen.length; j++) {
|
|
newGen[j] ^= gen[j];
|
|
newGen[j + 1] ^= gf.exp[(gf.log[gen[j]] + i) % 255];
|
|
}
|
|
gen.length = 0;
|
|
gen.push(...newGen);
|
|
}
|
|
|
|
const result = new Uint8Array(ecCount);
|
|
const msg = new Uint8Array(data.length + ecCount);
|
|
msg.set(data);
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
const coef = msg[i];
|
|
if (coef !== 0) {
|
|
for (let j = 0; j < gen.length; j++) {
|
|
msg[i + j] ^= gf.exp[(gf.log[gen[j]] + gf.log[coef]) % 255];
|
|
}
|
|
}
|
|
}
|
|
result.set(msg.slice(data.length));
|
|
return Array.from(result);
|
|
}
|
|
|
|
function interleaveCodewords(data, ec, ecInfo) {
|
|
const dataBlocks = [];
|
|
const ecBlocks = ec;
|
|
let dataIndex = 0;
|
|
|
|
for (const block of ecInfo.blocks) {
|
|
for (let i = 0; i < block.count; i++) {
|
|
dataBlocks.push(data.slice(dataIndex, dataIndex + block.data));
|
|
dataIndex += block.data;
|
|
}
|
|
}
|
|
|
|
const result = [];
|
|
const maxDataLen = Math.max(...dataBlocks.map(b => b.length));
|
|
for (let i = 0; i < maxDataLen; i++) {
|
|
for (const block of dataBlocks) {
|
|
if (i < block.length) result.push(block[i]);
|
|
}
|
|
}
|
|
|
|
const maxEcLen = Math.max(...ecBlocks.map(b => b.length));
|
|
for (let i = 0; i < maxEcLen; i++) {
|
|
for (const block of ecBlocks) {
|
|
if (i < block.length) result.push(block[i]);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function placeData(modules, codewords, size) {
|
|
let bitIndex = 0;
|
|
let up = true;
|
|
|
|
for (let col = size - 1; col >= 0; col -= 2) {
|
|
if (col === 6) col = 5;
|
|
|
|
for (let i = 0; i < size; i++) {
|
|
const row = up ? size - 1 - i : i;
|
|
|
|
for (let j = 0; j < 2; j++) {
|
|
const c = col - j;
|
|
if (modules[row][c] === null) {
|
|
const byteIndex = Math.floor(bitIndex / 8);
|
|
const bitPos = 7 - (bitIndex % 8);
|
|
modules[row][c] = byteIndex < codewords.length && ((codewords[byteIndex] >> bitPos) & 1) === 1;
|
|
bitIndex++;
|
|
}
|
|
}
|
|
}
|
|
up = !up;
|
|
}
|
|
}
|
|
|
|
function selectBestMask(modules, size, ecl) {
|
|
let bestMask = 0;
|
|
let bestPenalty = Infinity;
|
|
|
|
for (let mask = 0; mask < 8; mask++) {
|
|
const copy = modules.map(row => [...row]);
|
|
applyMask(copy, mask, size);
|
|
const penalty = calculatePenalty(copy, size);
|
|
if (penalty < bestPenalty) {
|
|
bestPenalty = penalty;
|
|
bestMask = mask;
|
|
}
|
|
}
|
|
return bestMask;
|
|
}
|
|
|
|
function applyMask(modules, mask, size) {
|
|
const maskFns = [
|
|
(r, c) => (r + c) % 2 === 0,
|
|
(r, c) => r % 2 === 0,
|
|
(r, c) => c % 3 === 0,
|
|
(r, c) => (r + c) % 3 === 0,
|
|
(r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0,
|
|
(r, c) => ((r * c) % 2) + ((r * c) % 3) === 0,
|
|
(r, c) => (((r * c) % 2) + ((r * c) % 3)) % 2 === 0,
|
|
(r, c) => (((r + c) % 2) + ((r * c) % 3)) % 2 === 0
|
|
];
|
|
|
|
for (let r = 0; r < size; r++) {
|
|
for (let c = 0; c < size; c++) {
|
|
if (isDataModule(r, c, size)) {
|
|
if (maskFns[mask](r, c)) {
|
|
modules[r][c] = !modules[r][c];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function isDataModule(row, col, size) {
|
|
// Check finder patterns
|
|
if (row < 9 && col < 9) return false;
|
|
if (row < 9 && col >= size - 8) return false;
|
|
if (row >= size - 8 && col < 9) return false;
|
|
// Check timing patterns
|
|
if (row === 6 || col === 6) return false;
|
|
return true;
|
|
}
|
|
|
|
function calculatePenalty(modules, size) {
|
|
let penalty = 0;
|
|
|
|
// Rule 1: consecutive same-color modules
|
|
for (let r = 0; r < size; r++) {
|
|
let count = 1;
|
|
for (let c = 1; c < size; c++) {
|
|
if (modules[r][c] === modules[r][c - 1]) {
|
|
count++;
|
|
} else {
|
|
if (count >= 5) penalty += 3 + (count - 5);
|
|
count = 1;
|
|
}
|
|
}
|
|
if (count >= 5) penalty += 3 + (count - 5);
|
|
}
|
|
|
|
for (let c = 0; c < size; c++) {
|
|
let count = 1;
|
|
for (let r = 1; r < size; r++) {
|
|
if (modules[r][c] === modules[r - 1][c]) {
|
|
count++;
|
|
} else {
|
|
if (count >= 5) penalty += 3 + (count - 5);
|
|
count = 1;
|
|
}
|
|
}
|
|
if (count >= 5) penalty += 3 + (count - 5);
|
|
}
|
|
|
|
return penalty;
|
|
}
|
|
|
|
function placeFormatInfo(modules, ecl, mask, size) {
|
|
const formatBits = ((ecl << 3) | mask);
|
|
const poly = 0x537;
|
|
let rem = formatBits << 10;
|
|
for (let i = 4; i >= 0; i--) {
|
|
if (rem & (1 << (i + 10))) {
|
|
rem ^= poly << i;
|
|
}
|
|
}
|
|
const format = ((formatBits << 10) | rem) ^ 0x5412;
|
|
|
|
const bits = [];
|
|
for (let i = 14; i >= 0; i--) bits.push((format >> i) & 1);
|
|
|
|
// Place format info
|
|
for (let i = 0; i < 6; i++) modules[8][i] = bits[14 - i] === 1;
|
|
modules[8][7] = bits[8] === 1;
|
|
modules[8][8] = bits[7] === 1;
|
|
modules[7][8] = bits[6] === 1;
|
|
for (let i = 0; i < 6; i++) modules[5 - i][8] = bits[i] === 1;
|
|
|
|
for (let i = 0; i < 8; i++) modules[8][size - 1 - i] = bits[14 - i] === 1;
|
|
for (let i = 0; i < 7; i++) modules[size - 7 + i][8] = bits[i] === 1;
|
|
}
|
|
|
|
function placeVersionInfo(modules, version, size) {
|
|
if (version < 7) return;
|
|
const poly = 0x1F25;
|
|
let rem = version << 12;
|
|
for (let i = 5; i >= 0; i--) {
|
|
if (rem & (1 << (i + 12))) {
|
|
rem ^= poly << i;
|
|
}
|
|
}
|
|
const versionInfo = (version << 12) | rem;
|
|
|
|
for (let i = 0; i < 18; i++) {
|
|
const bit = (versionInfo >> i) & 1;
|
|
const row = Math.floor(i / 3);
|
|
const col = i % 3;
|
|
modules[row][size - 11 + col] = bit === 1;
|
|
modules[size - 11 + col][row] = bit === 1;
|
|
}
|
|
}
|
|
|
|
function renderToCanvas(qr, cellSize = 8, margin = 4) {
|
|
const size = qr.size * cellSize + margin * 2 * cellSize;
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = size;
|
|
canvas.height = size;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillRect(0, 0, size, size);
|
|
|
|
ctx.fillStyle = '#000000';
|
|
for (let r = 0; r < qr.size; r++) {
|
|
for (let c = 0; c < qr.size; c++) {
|
|
if (qr.modules[r][c]) {
|
|
ctx.fillRect(
|
|
(c + margin) * cellSize,
|
|
(r + margin) * cellSize,
|
|
cellSize,
|
|
cellSize
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return canvas;
|
|
}
|
|
|
|
return { generate: generateQR, render: renderToCanvas };
|
|
})();
|
|
|
|
function showQrCode(phone) {
|
|
const telUrl = `tel:${phone}`;
|
|
qrTitle.textContent = phone;
|
|
qrContainer.innerHTML = '';
|
|
const qr = QRCode.generate(telUrl);
|
|
const canvas = QRCode.render(qr, 8, 4);
|
|
qrContainer.appendChild(canvas);
|
|
qrModal.classList.add('show');
|
|
}
|
|
|
|
// Only allow numbers and + in phone input
|
|
phoneInput.addEventListener('input', (e) => {
|
|
e.target.value = e.target.value.replace(/[^0-9+]/g, '');
|
|
});
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const phoneNumber = phoneInput.value.trim();
|
|
if (!phoneNumber) return;
|
|
|
|
// Strip +960 or 960 prefix for API call if it's a valid Maldivian number
|
|
let apiNumber = phoneNumber;
|
|
if (/^\+960\d{7}$/.test(phoneNumber)) {
|
|
apiNumber = phoneNumber.slice(4); // Remove +960
|
|
} else if (/^960\d{7}$/.test(phoneNumber)) {
|
|
apiNumber = phoneNumber.slice(3); // Remove 960
|
|
}
|
|
|
|
searchBtn.disabled = true;
|
|
searchBtn.innerHTML = '<div class="spinner"></div>';
|
|
|
|
try {
|
|
const response = await fetch(`https://dhiraagu-edir-proxy.shihaam.me/${encodeURIComponent(apiNumber)}`);
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data && data.dirEnquiryEntry) {
|
|
if (data.dirEnquiryEntry === 'Number not found') {
|
|
addToHistory(phoneNumber, 'Number not found', 'not_found');
|
|
} else {
|
|
addToHistory(phoneNumber, data.dirEnquiryEntry, 'success');
|
|
}
|
|
} else {
|
|
addToHistory(phoneNumber, 'No results found', 'error');
|
|
}
|
|
} catch (error) {
|
|
addToHistory(phoneNumber, 'Failed to fetch data', 'error');
|
|
} finally {
|
|
searchBtn.disabled = false;
|
|
searchBtn.innerHTML = '<span class="material-icons">search</span>';
|
|
phoneInput.value = '';
|
|
}
|
|
});
|
|
|
|
tableSearch.addEventListener('input', (e) => {
|
|
filterHistory(e.target.value);
|
|
});
|
|
|
|
clearSearchBtn.addEventListener('click', () => {
|
|
tableSearch.value = '';
|
|
filterHistory('');
|
|
tableSearch.focus();
|
|
});
|
|
|
|
document.querySelectorAll('th.sortable').forEach(th => {
|
|
th.addEventListener('click', () => {
|
|
handleSort(th.dataset.sort);
|
|
});
|
|
});
|
|
|
|
clearHistoryBtn.addEventListener('click', () => {
|
|
if (searchHistory.length === 0) {
|
|
showToast('No history to clear', 'error');
|
|
return;
|
|
}
|
|
confirmModal.classList.add('show');
|
|
});
|
|
|
|
cancelClearBtn.addEventListener('click', () => {
|
|
confirmModal.classList.remove('show');
|
|
});
|
|
|
|
confirmClearBtn.addEventListener('click', () => {
|
|
confirmModal.classList.remove('show');
|
|
clearHistory();
|
|
});
|
|
|
|
confirmModal.addEventListener('click', (e) => {
|
|
if (e.target === confirmModal) {
|
|
confirmModal.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
closeQrBtn.addEventListener('click', () => {
|
|
qrModal.classList.remove('show');
|
|
});
|
|
|
|
qrModal.addEventListener('click', (e) => {
|
|
if (e.target === qrModal) {
|
|
qrModal.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.ctrlKey && e.key === 'f') {
|
|
e.preventDefault();
|
|
tableSearch.focus();
|
|
}
|
|
});
|
|
|
|
loadHistory();
|
|
</script>
|
|
</body>
|
|
</html>
|