Skip to content

Commit 122311b

Browse files
committed
website: init website
1 parent a9bccf0 commit 122311b

6 files changed

Lines changed: 1346 additions & 0 deletions

File tree

.github/workflows/deploy-pages.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Deploy Pages
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- master
8+
- develop
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: read
13+
pages: write
14+
id-token: write
15+
16+
concurrency:
17+
group: pages
18+
cancel-in-progress: true
19+
20+
jobs:
21+
build:
22+
runs-on: ubuntu-latest
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@v4
26+
27+
- name: Setup Pages
28+
uses: actions/configure-pages@v5
29+
30+
- name: Upload artifact
31+
uses: actions/upload-pages-artifact@v4
32+
with:
33+
path: .
34+
35+
deploy:
36+
needs: build
37+
runs-on: ubuntu-latest
38+
environment:
39+
name: github-pages
40+
url: ${{ steps.deployment.outputs.page_url }}
41+
steps:
42+
- name: Deploy to GitHub Pages
43+
id: deployment
44+
uses: actions/deploy-pages@v4

website/app.js

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
document.documentElement.classList.add("motion-ready");
2+
3+
const RELEASE_URL = "https://github.com/monogram-android/monogram/releases";
4+
const RELEASES_API_URL = "https://api.github.com/repos/monogram-android/monogram/releases?per_page=10";
5+
const LANG_KEY = "monogram-site-language";
6+
const translations = window.MONOGRAM_TRANSLATIONS || {};
7+
8+
const root = document.documentElement;
9+
const topBar = document.querySelector(".top-bar");
10+
const revealNodes = document.querySelectorAll("[data-reveal]");
11+
const latestReleaseNodes = document.querySelectorAll("[data-latest-release]");
12+
const releaseMeta = document.querySelector(".release-meta");
13+
const releaseVersionNodes = document.querySelectorAll("[data-release-version]");
14+
const releaseDateNodes = document.querySelectorAll("[data-release-date]");
15+
const releaseChannelNodes = document.querySelectorAll("[data-release-channel]");
16+
const langButtons = document.querySelectorAll("[data-lang]");
17+
18+
let currentLang = "en";
19+
let currentRelease = null;
20+
let hasAnimatedRelease = false;
21+
22+
function safeStorageGet(key) {
23+
try {
24+
return localStorage.getItem(key);
25+
} catch {
26+
return null;
27+
}
28+
}
29+
30+
function safeStorageSet(key, value) {
31+
try {
32+
localStorage.setItem(key, value);
33+
} catch {
34+
// Ignore storage failures.
35+
}
36+
}
37+
38+
function detectPreferredLanguage() {
39+
const browserLanguage = (navigator.language || navigator.userLanguage || "en").toLowerCase();
40+
41+
if (browserLanguage.startsWith("ru")) {
42+
return "ru";
43+
}
44+
45+
if (browserLanguage.startsWith("zh")) {
46+
return "zh";
47+
}
48+
49+
return "en";
50+
}
51+
52+
function getDictionary(lang) {
53+
return translations[lang] || translations.en;
54+
}
55+
56+
function updateTopBar() {
57+
if (!topBar) {
58+
return;
59+
}
60+
61+
topBar.dataset.elevated = window.scrollY > 10 ? "true" : "false";
62+
}
63+
64+
function setLatestReleaseTargets(url) {
65+
latestReleaseNodes.forEach((node) => {
66+
if (node instanceof HTMLAnchorElement) {
67+
node.href = url || RELEASE_URL;
68+
}
69+
});
70+
}
71+
72+
function formatReleaseDate(value, lang) {
73+
const date = new Date(value);
74+
75+
if (Number.isNaN(date.getTime())) {
76+
return "";
77+
}
78+
79+
const localeMap = {
80+
en: "en-US",
81+
ru: "ru-RU",
82+
zh: "zh-CN"
83+
};
84+
85+
return new Intl.DateTimeFormat(localeMap[lang] || "en-US", {
86+
day: "numeric",
87+
month: "long",
88+
year: "numeric"
89+
}).format(date);
90+
}
91+
92+
function applyTranslations(lang) {
93+
const dictionary = getDictionary(lang);
94+
95+
document.querySelectorAll("[data-i18n]").forEach((node) => {
96+
const key = node.getAttribute("data-i18n");
97+
98+
if (key && dictionary[key]) {
99+
node.textContent = dictionary[key];
100+
}
101+
});
102+
103+
document.querySelectorAll("[data-i18n-attr]").forEach((node) => {
104+
const pairs = node.getAttribute("data-i18n-attr").split(",");
105+
106+
pairs.forEach((pair) => {
107+
const [attribute, key] = pair.split(":").map((part) => part.trim());
108+
109+
if (attribute && key && dictionary[key]) {
110+
node.setAttribute(attribute, dictionary[key]);
111+
}
112+
});
113+
});
114+
115+
root.lang = lang === "zh" ? "zh-CN" : lang;
116+
document.title = dictionary["meta.title"];
117+
}
118+
119+
function renderRelease() {
120+
if (!releaseMeta || !currentRelease) {
121+
return;
122+
}
123+
124+
const dictionary = getDictionary(currentLang);
125+
const formattedDate = formatReleaseDate(currentRelease.publishedAt, currentLang);
126+
const stateKey = currentRelease.prerelease ? "release.preview" : "release.latest";
127+
128+
releaseVersionNodes.forEach((node) => {
129+
node.textContent = currentRelease.version;
130+
});
131+
132+
releaseDateNodes.forEach((node) => {
133+
node.textContent = formattedDate;
134+
node.setAttribute("datetime", currentRelease.publishedAt);
135+
});
136+
137+
releaseChannelNodes.forEach((node) => {
138+
node.textContent = dictionary[stateKey];
139+
});
140+
}
141+
142+
function setReleaseVisibility(isVisible) {
143+
if (!releaseMeta) {
144+
return;
145+
}
146+
147+
releaseMeta.hidden = !isVisible;
148+
149+
if (isVisible) {
150+
releaseMeta.classList.add("is-visible");
151+
152+
if (!hasAnimatedRelease) {
153+
releaseMeta.classList.remove("is-appearing");
154+
void releaseMeta.offsetWidth;
155+
releaseMeta.classList.add("is-appearing");
156+
hasAnimatedRelease = true;
157+
}
158+
}
159+
}
160+
161+
function updateLanguageButtons(lang) {
162+
langButtons.forEach((button) => {
163+
const isActive = button.dataset.lang === lang;
164+
button.classList.toggle("is-active", isActive);
165+
button.setAttribute("aria-pressed", isActive ? "true" : "false");
166+
});
167+
}
168+
169+
function applyLanguage(lang) {
170+
currentLang = translations[lang] ? lang : "en";
171+
applyTranslations(currentLang);
172+
renderRelease();
173+
updateLanguageButtons(currentLang);
174+
safeStorageSet(LANG_KEY, currentLang);
175+
}
176+
177+
function setupLanguageSwitcher() {
178+
langButtons.forEach((button) => {
179+
button.addEventListener("click", () => {
180+
applyLanguage(button.dataset.lang || "en");
181+
});
182+
});
183+
}
184+
185+
function setupRevealAnimations() {
186+
if (!("IntersectionObserver" in window) || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
187+
revealNodes.forEach((node) => node.classList.add("is-visible"));
188+
return;
189+
}
190+
191+
const observer = new IntersectionObserver(
192+
(entries, activeObserver) => {
193+
entries.forEach((entry) => {
194+
if (!entry.isIntersecting) {
195+
return;
196+
}
197+
198+
entry.target.classList.add("is-visible");
199+
activeObserver.unobserve(entry.target);
200+
});
201+
},
202+
{
203+
threshold: 0.18,
204+
rootMargin: "0px 0px -8% 0px"
205+
}
206+
);
207+
208+
revealNodes.forEach((node) => observer.observe(node));
209+
}
210+
211+
async function loadLatestRelease() {
212+
try {
213+
const response = await fetch(RELEASES_API_URL, {
214+
headers: {
215+
Accept: "application/vnd.github+json",
216+
"X-GitHub-Api-Version": "2026-03-10"
217+
}
218+
});
219+
220+
if (!response.ok) {
221+
throw new Error(`GitHub API returned ${response.status}`);
222+
}
223+
224+
const releases = await response.json();
225+
226+
if (!Array.isArray(releases)) {
227+
throw new Error("GitHub API returned an unexpected payload.");
228+
}
229+
230+
const release = releases
231+
.filter((item) => item && !item.draft && item.published_at)
232+
.sort((left, right) => new Date(right.published_at) - new Date(left.published_at))[0];
233+
234+
if (!release) {
235+
throw new Error("No published releases were found.");
236+
}
237+
238+
currentRelease = {
239+
version: (release.tag_name || release.name || "").replace(/^v/i, ""),
240+
publishedAt: release.published_at,
241+
prerelease: Boolean(release.prerelease)
242+
};
243+
244+
setLatestReleaseTargets(release.html_url || RELEASE_URL);
245+
setReleaseVisibility(true);
246+
renderRelease();
247+
} catch (error) {
248+
currentRelease = null;
249+
setLatestReleaseTargets(RELEASE_URL);
250+
setReleaseVisibility(false);
251+
console.warn("Unable to load latest release metadata.", error);
252+
}
253+
}
254+
255+
updateTopBar();
256+
setupLanguageSwitcher();
257+
setupRevealAnimations();
258+
setLatestReleaseTargets(RELEASE_URL);
259+
setReleaseVisibility(false);
260+
applyLanguage(safeStorageGet(LANG_KEY) || detectPreferredLanguage());
261+
loadLatestRelease();
262+
263+
window.addEventListener("scroll", updateTopBar, { passive: true });

0 commit comments

Comments
 (0)