Skip to content

Commit a124e93

Browse files
BytePaulMakisH
andauthored
FAQ browser page with static fetching (#612)
Co-authored-by: Gerasimos Chourdakis <gerasimos.chourdakis@ipvs.uni-stuttgart.de>
1 parent 9a058d9 commit a124e93

File tree

5 files changed

+247
-4
lines changed

5 files changed

+247
-4
lines changed

.github/workflows/update-discourse-data.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ jobs:
2828
with:
2929
python-version: "3.14"
3030

31-
- name: Fetch latest news from Discourse
32-
run: python tools/fetch-news.py
31+
- name: Fetch latest data from Discourse
32+
run: |
33+
python tools/fetch-news.py
34+
python tools/fetch-faq.py
3335
3436
- name: Configure Git author
3537
run: |
@@ -39,7 +41,8 @@ jobs:
3941
- name: Commit changes (if any)
4042
run: |
4143
git add assets/data/news.json
42-
git commit -m "Update Discourse news data [skip ci]" || echo "No changes to commit"
44+
git add assets/data/faq.json
45+
git commit -m "Update Discourse data data [skip ci]" || echo "No changes to commit"
4346
4447
- name: Push commit
4548
uses: ad-m/github-push-action@master

_data/sidebars/docs_sidebar.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ entries:
4343
- title: Roadmap
4444
url: /fundamentals-roadmap.html
4545
output: web, pdf
46-
46+
47+
- title: FAQ
48+
url: /fundamentals-faq.html
49+
output: web, pdf
50+
tags: faq
51+
4752
- title: Previous versions
4853
url: /fundamentals-previous-versions.html
4954
output: web, pdf
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
title: Frequently asked questions
3+
permalink: /fundamentals-faq.html
4+
keywords: faq, forum, questions
5+
editme: false
6+
toc: false
7+
redirect_from:
8+
- faq.html
9+
---
10+
11+
Search and filter frequently asked questions on the [preCICE Discourse forum](https://precice.discourse.group/tag/faq).
12+
13+
<div style="display:flex;flex-wrap:wrap;gap:8px;margin:12px 0;">
14+
<input id="q" placeholder="Search questions..." style="padding:8px;border:1px solid #ddd;border-radius:8px;min-width:240px;">
15+
<select id="sort" style="padding:8px;border:1px solid #ddd;border-radius:8px;">
16+
<option value="last_posted_at">Sort by last activity</option>
17+
<option value="created_at">Sort by creation date</option>
18+
<option value="posts_count">Sort by replies</option>
19+
<option value="views">Sort by views</option>
20+
<option value="like_count">Sort by likes</option>
21+
</select>
22+
</div>
23+
24+
<p id="meta" style="margin:0 0 8px 0;color:#666;"></p>
25+
<p id="stats" style="margin:0 0 16px 0;color:#666;"></p>
26+
27+
<div id="list" style="display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));"></div>
28+
<div id="empty" style="display:none;color:#666;padding:16px 0;text-align:center;">No results found.</div>
29+
30+
<script src="/js/forum-fetch.js"></script>

js/forum-fetch.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
console.log("forum-fetch.js loaded!");
2+
3+
document.addEventListener("DOMContentLoaded", async function () {
4+
console.log("FAQ loader running...");
5+
6+
const list = document.getElementById("list");
7+
const empty = document.getElementById("empty");
8+
const searchInput = document.getElementById("q");
9+
const sortSelect = document.getElementById("sort");
10+
11+
const loadingText = document.createElement("p");
12+
loadingText.textContent = "Loading FAQs...";
13+
list.parentElement.insertBefore(loadingText, list);
14+
15+
try {
16+
const res = await fetch("/assets/data/faq.json");
17+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
18+
19+
const data = await res.json();
20+
const topics = data.topics || [];
21+
loadingText.remove();
22+
23+
if (!topics.length) {
24+
empty.style.display = "block";
25+
return;
26+
}
27+
28+
let visibleCount = 10;
29+
let filteredTopics = [...topics];
30+
31+
function filterAndSort() {
32+
const q = searchInput.value.trim().toLowerCase();
33+
const sortKey = sortSelect.value;
34+
35+
// Filter
36+
filteredTopics = topics.filter((t) =>
37+
(t.title + " " + (t.excerpt || "")).toLowerCase().includes(q)
38+
);
39+
40+
// Sort
41+
filteredTopics.sort((a, b) => {
42+
if (sortKey === "created_at" || sortKey === "last_posted_at") {
43+
return new Date(b[sortKey]) - new Date(a[sortKey]);
44+
}
45+
return (b[sortKey] || 0) - (a[sortKey] || 0);
46+
});
47+
48+
visibleCount = 10;
49+
renderTopics();
50+
}
51+
52+
function renderTopics() {
53+
list.innerHTML = "";
54+
55+
const shown = filteredTopics.slice(0, visibleCount);
56+
57+
if (!shown.length) {
58+
empty.style.display = "block";
59+
return;
60+
} else {
61+
empty.style.display = "none";
62+
}
63+
64+
shown.forEach((t) => {
65+
const card = document.createElement("div");
66+
card.className = "faq-card";
67+
card.style.cssText =
68+
"border:1px solid #ddd;padding:12px;border-radius:8px;background:#fff;";
69+
70+
const excerpt =
71+
t.excerpt && t.excerpt.trim().length > 0
72+
? t.excerpt
73+
: "No description available.";
74+
75+
card.innerHTML = `
76+
<h4 style="margin-bottom:8px;">
77+
<strong>${t.title}</strong>
78+
</h4>
79+
<p style="color:#333;line-height:1.4;">
80+
${excerpt}
81+
<a href="${t.url}" target="_blank" rel="noopener noreferrer"
82+
style="text-decoration:none;color:#0069c2;" data-noicon>
83+
Read more
84+
</a>
85+
</p>
86+
<p style="color:#666;font-size:0.9em;">
87+
Last updated: ${new Date(t.last_posted_at).toLocaleDateString("en-GB")} |
88+
Replies: ${t.posts_count} | Views: ${t.views}
89+
</p>
90+
`;
91+
list.appendChild(card);
92+
});
93+
94+
if (visibleCount < filteredTopics.length) {
95+
const btn = document.createElement("button");
96+
btn.textContent = "Load more";
97+
btn.style.cssText =
98+
"padding:8px 16px;margin:12px 0 12px auto;display:block;border:1px solid #ccc;border-radius:8px;background:#f9f9f9;cursor:pointer;";
99+
btn.addEventListener("click", () => {
100+
visibleCount += 10;
101+
renderTopics();
102+
});
103+
list.appendChild(btn);
104+
}
105+
106+
// 🧽 JS-based CSS injection to remove external icon pseudo-element
107+
if (!document.getElementById("noicon-style")) {
108+
const style = document.createElement("style");
109+
style.id = "noicon-style";
110+
style.textContent = `
111+
[data-noicon]::after {
112+
content: none !important;
113+
background-image: none !important;
114+
}
115+
`;
116+
document.head.appendChild(style);
117+
}
118+
}
119+
120+
// Event listeners
121+
searchInput.addEventListener("input", filterAndSort);
122+
sortSelect.addEventListener("change", filterAndSort);
123+
124+
// Initial render
125+
filterAndSort();
126+
} catch (err) {
127+
console.error("Error loading FAQ:", err);
128+
loadingText.textContent = "Failed to load FAQs.";
129+
}
130+
});

tools/fetch-faq.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import json
4+
import re
5+
import urllib.request
6+
from datetime import datetime
7+
8+
DISCOURSE_BASE = "https://precice.discourse.group"
9+
OUTPUT_FILE = "./assets/data/faq.json"
10+
11+
12+
def http_get_json(url: str):
13+
"""GET URL and return parsed JSON using only stdlib."""
14+
with urllib.request.urlopen(url) as r:
15+
return json.load(r)
16+
17+
18+
def strip_html(text: str) -> str:
19+
return re.sub(r"<[^>]+>", "", text)
20+
21+
22+
def fetch_excerpt(topic_id: int) -> str:
23+
try:
24+
topic_data = http_get_json(f"{DISCOURSE_BASE}/t/{topic_id}.json")
25+
raw = topic_data.get("post_stream", {}).get("posts", [{}])[0].get("cooked", "")
26+
cleaned = strip_html(raw)
27+
return cleaned[:250] + ("…" if len(cleaned) > 250 else "")
28+
except Exception as e:
29+
print(f"Could not fetch excerpt for topic {topic_id}: {e}")
30+
return ""
31+
32+
33+
def fetch_faq():
34+
try:
35+
print("Fetching FAQ topics from Discourse...")
36+
37+
data = http_get_json(f"{DISCOURSE_BASE}/tag/faq/l/latest.json")
38+
topic_list = data.get("topic_list", {}).get("topics", [])
39+
print(f"Found {len(topic_list)} FAQ topics. Fetching excerpts...")
40+
41+
topics = []
42+
for t in topic_list:
43+
excerpt = fetch_excerpt(t["id"])
44+
topics.append({
45+
"id": t["id"],
46+
"title": t["title"],
47+
"slug": t["slug"],
48+
"url": f"{DISCOURSE_BASE}/t/{t['slug']}/{t['id']}",
49+
"created_at": t.get("created_at"),
50+
"last_posted_at": t.get("last_posted_at"),
51+
"views": t.get("views"),
52+
"posts_count": t.get("posts_count"),
53+
"like_count": t.get("like_count"),
54+
"excerpt": excerpt,
55+
})
56+
57+
payload = {
58+
"source": "preCICE Discourse (FAQ)",
59+
"generated_at": datetime.utcnow().isoformat(),
60+
"topics": topics,
61+
}
62+
63+
os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
64+
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
65+
json.dump(payload, f, indent=2)
66+
67+
print(f"Saved {len(topics)} FAQ topics to {OUTPUT_FILE}")
68+
69+
except Exception as e:
70+
print("Failed to fetch FAQ data:", e)
71+
exit(1)
72+
73+
74+
if __name__ == "__main__":
75+
fetch_faq()

0 commit comments

Comments
 (0)