Compare commits
4
Commits
f0b60717a0
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fbf22a29b
|
||
|
|
004130d2a6
|
||
|
|
7af571e55a
|
||
|
|
cda6557e42
|
@@ -1,12 +1,16 @@
|
|||||||
# E-Dir
|
# E-Dir
|
||||||
|
|
||||||
A simple web frontend for Dhiraagu directory lookup.
|
A better web frontend for [Dhiraagu E-Directory](https://www.dhiraagu.com.mv/e-directory).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Phone number lookup via Dhiraagu E-Dir API
|
- Phone number lookup via Dhiraagu E-Dir API
|
||||||
- Material Design UI
|
- Tap-to-call with `tel:` links
|
||||||
|
- Search history stored in LocalStorage
|
||||||
|
- Accepts country code prefix (+960)
|
||||||
|
- QR code generation to share contact numbers
|
||||||
- Automatic dark/light theme (follows system preference)
|
- Automatic dark/light theme (follows system preference)
|
||||||
|
- No Recaptcha required
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from flask import Flask, request, Response
|
||||||
|
from curl_cffi import requests as curl_requests
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
BASE_URL = "https://app-production.dhiraagu.com.mv"
|
||||||
|
|
||||||
|
|
||||||
|
def proxy_request(target_url: str) -> Response:
|
||||||
|
headers = {"Host": "app-production.dhiraagu.com.mv"}
|
||||||
|
|
||||||
|
for h in ["Authorization", "Content-Type", "Accept"]:
|
||||||
|
if h in request.headers:
|
||||||
|
headers[h] = request.headers[h]
|
||||||
|
|
||||||
|
resp = curl_requests.request(
|
||||||
|
method=request.method,
|
||||||
|
url=target_url,
|
||||||
|
headers=headers,
|
||||||
|
data=request.get_data() or None,
|
||||||
|
impersonate="chrome",
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(resp.content, resp.status_code, {"Content-Type": resp.headers.get("content-type", "application/json")})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/<int:subscriber_id>")
|
||||||
|
def subscriber_lookup(subscriber_id: int):
|
||||||
|
return proxy_request(f"{BASE_URL}/io/v1/info/subscribers/{subscriber_id}/dir")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/", defaults={"path": ""})
|
||||||
|
@app.route("/<path:path>")
|
||||||
|
def catch_all(path: str):
|
||||||
|
return proxy_request(f"{BASE_URL}/{path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", port=8080)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
flask>=3.0.0
|
||||||
|
curl_cffi>=0.6.0
|
||||||
+2
-1
@@ -41,7 +41,8 @@
|
|||||||
|
|
||||||
<!-- Additional SEO -->
|
<!-- Additional SEO -->
|
||||||
<meta name="application-name" content="E-Dir">
|
<meta name="application-name" content="E-Dir">
|
||||||
<meta name="theme-color" content="#ff8c00">
|
<meta name="theme-color" content="#0a0a0a" media="(prefers-color-scheme: dark)">
|
||||||
|
<meta name="theme-color" content="#f5f5f5" media="(prefers-color-scheme: light)">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-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-status-bar-style" content="black-translucent">
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ function renderTable(filteredHistory = null) {
|
|||||||
if (entry.status === 'not_found') {
|
if (entry.status === 'not_found') {
|
||||||
resultHtml = getStatusBadge(entry.status);
|
resultHtml = getStatusBadge(entry.status);
|
||||||
} else if (entry.status === 'error') {
|
} else if (entry.status === 'error') {
|
||||||
resultHtml = `${escapeHtml(entry.result)}<div style="margin-top: 8px;">${getStatusBadge(entry.status)}</div>`;
|
resultHtml = `${escapeHtml(entry.result)}<div style="margin-top: 8px;">${getStatusBadge(entry.status)}<button class="retry-btn" data-phone="${escapeHtml(entry.phone)}" data-id="${entry.id}" title="Retry"><span class="material-icons">refresh</span></button></div>`;
|
||||||
} else {
|
} else {
|
||||||
resultHtml = escapeHtml(entry.result);
|
resultHtml = escapeHtml(entry.result);
|
||||||
}
|
}
|
||||||
@@ -135,6 +135,10 @@ function renderTable(filteredHistory = null) {
|
|||||||
resultCell.classList.add('copyable');
|
resultCell.classList.add('copyable');
|
||||||
resultCell.addEventListener('click', () => copyToClipboard(entry.result));
|
resultCell.addEventListener('click', () => copyToClipboard(entry.result));
|
||||||
}
|
}
|
||||||
|
const retryBtn = row.querySelector('.retry-btn');
|
||||||
|
if (retryBtn) {
|
||||||
|
retryBtn.addEventListener('click', () => retrySearch(entry.id, entry.phone, retryBtn));
|
||||||
|
}
|
||||||
historyTableBody.appendChild(row);
|
historyTableBody.appendChild(row);
|
||||||
|
|
||||||
// Mobile table row
|
// Mobile table row
|
||||||
@@ -162,6 +166,10 @@ function renderTable(filteredHistory = null) {
|
|||||||
mobileResultCell.classList.add('copyable');
|
mobileResultCell.classList.add('copyable');
|
||||||
mobileResultCell.addEventListener('click', () => copyToClipboard(entry.result));
|
mobileResultCell.addEventListener('click', () => copyToClipboard(entry.result));
|
||||||
}
|
}
|
||||||
|
const mobileRetryBtn = mobileRow.querySelector('.retry-btn');
|
||||||
|
if (mobileRetryBtn) {
|
||||||
|
mobileRetryBtn.addEventListener('click', () => retrySearch(entry.id, entry.phone, mobileRetryBtn));
|
||||||
|
}
|
||||||
mobileTableBody.appendChild(mobileRow);
|
mobileTableBody.appendChild(mobileRow);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -250,16 +258,56 @@ async function copyToClipboard(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function retrySearch(entryId, phoneNumber, button) {
|
||||||
|
button.classList.add('loading');
|
||||||
|
|
||||||
|
let apiNumber = phoneNumber;
|
||||||
|
if (/^\+960\d{7}$/.test(phoneNumber)) {
|
||||||
|
apiNumber = phoneNumber.slice(4);
|
||||||
|
} else if (/^960\d{7}$/.test(phoneNumber)) {
|
||||||
|
apiNumber = phoneNumber.slice(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://dhiraagu-edir-proxy.shihaam.me/${encodeURIComponent(apiNumber)}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const entryIndex = searchHistory.findIndex(e => e.id === entryId);
|
||||||
|
if (entryIndex === -1) return;
|
||||||
|
|
||||||
|
if (response.ok && data && data.dirEnquiryEntry) {
|
||||||
|
if (data.dirEnquiryEntry === 'Number not found') {
|
||||||
|
searchHistory[entryIndex].result = 'Number not found';
|
||||||
|
searchHistory[entryIndex].status = 'not_found';
|
||||||
|
} else {
|
||||||
|
searchHistory[entryIndex].result = data.dirEnquiryEntry;
|
||||||
|
searchHistory[entryIndex].status = 'success';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
searchHistory[entryIndex].result = 'No results found';
|
||||||
|
searchHistory[entryIndex].status = 'error';
|
||||||
|
}
|
||||||
|
searchHistory[entryIndex].timestamp = Date.now();
|
||||||
|
saveHistory();
|
||||||
|
renderTable();
|
||||||
|
showToast('Retry successful', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
button.classList.remove('loading');
|
||||||
|
showToast('Retry failed', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showQrCode(phone) {
|
function showQrCode(phone) {
|
||||||
const telUrl = `tel:${phone}`;
|
const telUrl = `tel:${phone}`;
|
||||||
qrTitle.textContent = phone;
|
qrTitle.textContent = phone;
|
||||||
qrContainer.innerHTML = '';
|
qrContainer.innerHTML = '';
|
||||||
|
const isLightMode = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||||
new QRCode(qrContainer, {
|
new QRCode(qrContainer, {
|
||||||
text: telUrl,
|
text: telUrl,
|
||||||
width: 200,
|
width: 200,
|
||||||
height: 200,
|
height: 200,
|
||||||
colorDark: '#ff8c00',
|
colorDark: isLightMode ? '#e07800' : '#ff8c00',
|
||||||
colorLight: '#111111',
|
colorLight: isLightMode ? '#ffffff' : '#111111',
|
||||||
correctLevel: QRCode.CorrectLevel.H
|
correctLevel: QRCode.CorrectLevel.H
|
||||||
});
|
});
|
||||||
qrModal.classList.add('show');
|
qrModal.classList.add('show');
|
||||||
|
|||||||
+48
@@ -17,6 +17,21 @@
|
|||||||
--error: #ff3333;
|
--error: #ff3333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--orange: #e07800;
|
||||||
|
--bg-primary: #f5f5f5;
|
||||||
|
--bg-secondary: #ffffff;
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--text-primary: #1b1b1b;
|
||||||
|
--text-secondary: #808080;
|
||||||
|
--border: #e0e0e0;
|
||||||
|
--border-hover: #e07800;
|
||||||
|
--success: #00a844;
|
||||||
|
--error: #d32f2f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Roboto', sans-serif;
|
font-family: 'Roboto', sans-serif;
|
||||||
background-color: var(--bg-primary);
|
background-color: var(--bg-primary);
|
||||||
@@ -689,6 +704,39 @@ td.phone-number {
|
|||||||
border-color: var(--error);
|
border-color: var(--error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.retry-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 2px solid var(--orange);
|
||||||
|
color: var(--orange);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn:hover {
|
||||||
|
background-color: var(--orange);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn .material-icons {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn.loading {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn.loading .material-icons {
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 80px 20px;
|
padding: 80px 20px;
|
||||||
|
|||||||
Reference in New Issue
Block a user