Skip to content

Commit 1ca77bb

Browse files
authored
Update certManager.js
1 parent b028878 commit 1ca77bb

File tree

1 file changed

+208
-56
lines changed

1 file changed

+208
-56
lines changed

web/certManager.js

Lines changed: 208 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,224 @@
1-
const README_URL =
2-
"https://raw.githubusercontent.com/ProStore-iOS/certificates/refs/heads/main/README.md";
1+
// certManager.js
2+
const README_URL = "https://raw.githubusercontent.com/ProStore-iOS/certificates/refs/heads/main/README.md";
3+
4+
async function init() {
5+
try {
6+
const res = await fetch(README_URL);
7+
if (!res.ok) throw new Error("Failed to fetch README");
8+
const md = await res.text();
39

4-
fetch(README_URL)
5-
.then(res => res.text())
6-
.then(md => {
710
renderRecommended(md);
8-
renderTable(md);
11+
const certs = parseCertTable(md);
12+
renderCertCards(certs);
913
renderUpdates(md);
10-
});
14+
} catch (err) {
15+
console.error(err);
16+
document.getElementById("certList").innerHTML = `<p style="color:#ef4444">Failed to load certificate data.</p>`;
17+
}
18+
}
19+
20+
/* ---------- Parsing helpers ---------- */
21+
22+
function parseCertTable(md) {
23+
const lines = md.split("\n");
24+
let start = -1;
25+
for (let i = 0; i < lines.length; i++) {
26+
if (lines[i].trim().toLowerCase().startsWith("| company |")) {
27+
start = i;
28+
break;
29+
}
30+
}
31+
if (start === -1) return [];
32+
33+
// skip header + separator
34+
const rows = [];
35+
for (let i = start + 2; i < lines.length; i++) {
36+
const line = lines[i].trim();
37+
if (!line.startsWith("|")) break;
38+
// split by '|' and remove first/last empty segments
39+
const parts = line.split("|").map(p => p.trim());
40+
// parts usually: ["", "Company", "Type", ... , ""]
41+
// discard any leading/trailing empty strings
42+
if (parts.length < 6) continue;
43+
// filter out possible empty at start
44+
const cols = parts.filter((c, idx) => c !== "" || (c === "" && idx > 0 && idx < parts.length - 1));
45+
// safer: take last 6 non-empty-ish entries from the line
46+
// but easier: take indices 1..6 in normal layout
47+
const company = parts[1] || "";
48+
const type = parts[2] || "";
49+
const statusRaw = parts[3] || "";
50+
const validFrom = parts[4] || "";
51+
const validTo = parts[5] || "";
52+
const downloadRaw = parts[6] || "";
53+
54+
const status = statusRaw.replace(/\*\*/g, "").trim();
55+
const downloadUrlMatch = downloadRaw.match(/\((https?:\/\/[^\)]+)\)/);
56+
const downloadUrl = downloadUrlMatch ? decodeURIComponent(downloadUrlMatch[1]) : (downloadRaw.match(/https?:\/\//) ? downloadRaw : "");
57+
58+
rows.push({
59+
company: company,
60+
type: stripMd(type),
61+
status: stripMd(status),
62+
validFrom: stripMd(validFrom),
63+
validTo: stripMd(validTo),
64+
download: downloadUrl
65+
});
66+
}
67+
68+
return rows;
69+
}
70+
71+
function stripMd(s) {
72+
return s.replace(/\*\*/g, "").replace(/\[|\]/g, "").trim();
73+
}
74+
75+
/* ---------- Rendering ---------- */
1176

1277
function renderRecommended(md) {
13-
const match = md.match(/# Recommend Certificate\s+\*\*(.+?)\*\*/);
14-
if (!match) return;
78+
const m = md.match(/# Recommend Certificate\s+([\s\S]*?)\n\n/);
79+
let rec = "";
80+
if (m) rec = m[1].trim();
81+
else {
82+
// fallback: look for header then bold on next line
83+
const m2 = md.match(/# Recommend Certificate\s*\n\*\*(.+?)\*\*/s);
84+
if (m2) rec = m2[1].trim();
85+
}
1586

16-
document.getElementById("recommended").innerHTML = `
17-
<h2>⭐ Recommended Certificate</h2>
18-
<p>${match[1]}</p>
19-
`;
87+
const el = document.getElementById("recommended");
88+
if (!rec) {
89+
el.style.display = "none";
90+
return;
91+
}
92+
el.innerHTML = `<h3>⭐ Recommended Certificate</h3><p>${escapeHtml(rec)}</p>`;
2093
}
2194

22-
function renderTable(md) {
23-
const tableMatch = md.match(/\| Company \|[\s\S]*?\n\n/);
24-
if (!tableMatch) return;
25-
26-
const lines = tableMatch[0].trim().split("\n").slice(2);
27-
let rows = "";
28-
29-
lines.forEach(line => {
30-
const cols = line.split("|").map(c => c.trim()).filter(Boolean);
31-
if (cols.length < 6) return;
32-
33-
rows += `
34-
<tr>
35-
<td>${cols[0]}</td>
36-
<td>${cols[1]}</td>
37-
<td class="status revoked">❌ Revoked</td>
38-
<td>${cols[3]}</td>
39-
<td>${cols[4]}</td>
40-
<td>${cols[5]}</td>
41-
</tr>
95+
function renderCertCards(certs) {
96+
const container = document.getElementById("certList");
97+
container.innerHTML = "";
98+
99+
if (!certs.length) {
100+
container.innerHTML = `<p style="color:var(--muted)">No certificates found in the source README.</p>`;
101+
return;
102+
}
103+
104+
certs.forEach((c, idx) => {
105+
const statusLower = c.status.toLowerCase();
106+
const isRevoked = statusLower.includes("revok") || statusLower.includes("❌");
107+
const badgeClass = isRevoked ? "revoked" : "valid";
108+
const badgeText = c.status || (isRevoked ? "Revoked" : "Unknown");
109+
110+
const card = document.createElement("div");
111+
card.className = "cert-card";
112+
card.setAttribute("role","button");
113+
card.setAttribute("tabindex","0");
114+
card.innerHTML = `
115+
<div class="card-top">
116+
<div>
117+
<div class="card-title">${escapeHtml(c.company)}</div>
118+
<div class="card-meta">${escapeHtml(c.type)}</div>
119+
</div>
120+
<div>
121+
<span class="badge ${badgeClass}">${escapeHtml(badgeText)}</span>
122+
</div>
123+
</div>
124+
125+
<div class="card-footer">
126+
<div class="small">From: ${escapeHtml(c.validFrom)}</div>
127+
<div class="small">To: ${escapeHtml(c.validTo)}</div>
128+
</div>
42129
`;
43-
});
44130

45-
document.getElementById("certTable").innerHTML = `
46-
<table class="cert-table">
47-
<thead>
48-
<tr>
49-
<th>Company</th>
50-
<th>Type</th>
51-
<th>Status</th>
52-
<th>Valid From</th>
53-
<th>Valid To</th>
54-
<th>Download</th>
55-
</tr>
56-
</thead>
57-
<tbody>${rows}</tbody>
58-
</table>
59-
`;
131+
// click handler opens modal with details
132+
card.addEventListener("click", () => openModal(c));
133+
card.addEventListener("keypress", (e) => { if (e.key === "Enter") openModal(c); });
134+
135+
container.appendChild(card);
136+
});
60137
}
61138

62139
function renderUpdates(md) {
63-
const section = md.split("# Updates")[1];
64-
if (!section) return;
140+
const idx = md.indexOf("# Updates");
141+
const container = document.getElementById("updatesInner");
142+
container.innerHTML = "";
65143

66-
const lines = section.split("\n").filter(l => l.startsWith("**"));
144+
if (idx === -1) {
145+
container.innerHTML = `<div class="update-item">No updates section found.</div>`;
146+
return;
147+
}
67148

68-
document.getElementById("updates").innerHTML = `
69-
<h2>📰 Updates</h2>
70-
${lines.map(l => `<div class="update-item">${l}</div>`).join("")}
71-
`;
149+
// capture content after "# Updates" until next heading that starts with '# ' or EOF
150+
const after = md.substring(idx + "# Updates".length);
151+
const lines = after.split("\n");
152+
const updates = [];
153+
for (let i = 0; i < lines.length; i++) {
154+
const line = lines[i].trim();
155+
if (!line) continue;
156+
if (line.startsWith("#")) break;
157+
// lines that start with ** are update entries in this README
158+
if (line.startsWith("**") && line.endsWith("**")) {
159+
updates.push(line.replace(/\*\*/g, ""));
160+
} else if (line.startsWith("**")) {
161+
updates.push(line.replace(/\*\*/g, ""));
162+
} else {
163+
// sometimes they aren't bold — include them too
164+
updates.push(line);
165+
}
166+
}
167+
168+
if (!updates.length) {
169+
container.innerHTML = `<div class="update-item">No updates found.</div>`;
170+
return;
171+
}
172+
173+
container.innerHTML = updates.map(u => `<div class="update-item">${escapeHtml(u)}</div>`).join("");
72174
}
175+
176+
/* ---------- Modal ---------- */
177+
function openModal(c) {
178+
const modal = document.getElementById("certModal");
179+
document.getElementById("modalName").textContent = c.company;
180+
document.getElementById("modalMeta").textContent = `${c.type} • Status: ${c.status}`;
181+
document.getElementById("modalDates").textContent = `Valid: ${c.validFrom}${c.validTo}`;
182+
183+
const dl = document.getElementById("modalDownload");
184+
if (c.download) {
185+
const a = document.createElement("a");
186+
a.href = c.download;
187+
a.target = "_blank";
188+
a.rel = "noopener noreferrer";
189+
a.textContent = "Download";
190+
dl.innerHTML = "";
191+
dl.appendChild(a);
192+
} else {
193+
dl.innerHTML = `<div style="color:var(--muted);">No download link found.</div>`;
194+
}
195+
196+
modal.classList.add("show");
197+
modal.setAttribute("aria-hidden","false");
198+
}
199+
200+
// close modal handlers
201+
document.addEventListener("click", (e) => {
202+
const modal = document.getElementById("certModal");
203+
if (!modal) return;
204+
if (e.target.matches("#modalClose")) closeModal();
205+
if (e.target === modal) closeModal();
206+
});
207+
document.addEventListener("keydown", (e) => {
208+
if (e.key === "Escape") closeModal();
209+
});
210+
function closeModal() {
211+
const modal = document.getElementById("certModal");
212+
if (!modal) return;
213+
modal.classList.remove("show");
214+
modal.setAttribute("aria-hidden","true");
215+
}
216+
217+
/* ---------- Utilities ---------- */
218+
function escapeHtml(s){
219+
if (!s) return "";
220+
return s.replace(/[&<>"']/g, function(m){ return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]); });
221+
}
222+
223+
/* Start */
224+
init();

0 commit comments

Comments
 (0)