grapes-andrewdingus
Version:
GRAPES OS static site — CDN-ready via unpkg
432 lines (393 loc) • 13.8 kB
JavaScript
const supabaseUrl = "https://hqlgppguxhqeaonjzinv.supabase.co";
const supabaseKey =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhxbGdwcGd1eGhxZWFvbmp6aW52Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzI2MjYwNDQsImV4cCI6MjA0ODIwMjA0NH0.4LuWk4qxp0NRZ5_erEIJq5BHq5qZiSE4zTUFS1ioZw8";
const supabaseClient = supabase.createClient(supabaseUrl, supabaseKey);
function getDescriptionFromMarkdown(markdown) {
if (!markdown) return "";
const descParts = markdown.split("## Description");
if (!descParts[1]) return "";
let built = descParts[1];
const creatorParts = built.split("## Creator");
built = creatorParts[0];
return built.replaceAll("\n", "");
}
function create_in_article_ad() {
/**
* <div class="ad">
<ins
class="adsbygoogle"
style="display: block"
data-ad-client="ca-pub-8362959866002557"
data-ad-slot="8239998772"
data-ad-format="auto"
data-full-width-responsive="true"
></ins>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script>
</div>
*/
const adDiv = document.createElement("div");
adDiv.className = "ad";
const ins = document.createElement("ins");
ins.className = "adsbygoogle";
ins.style.display = "block";
ins.setAttribute("data-ad-client", "ca-pub-8362959866002557");
ins.setAttribute("data-ad-slot", "8239998772");
ins.setAttribute("data-ad-format", "auto");
ins.setAttribute("data-full-width-responsive", "true");
const script = document.createElement("script");
script.innerHTML = "(adsbygoogle = window.adsbygoogle || []).push({});";
adDiv.appendChild(ins);
adDiv.appendChild(script);
return adDiv;
}
const APP_VER = "apps11";
const REQUERY_TIME = 5; // in days
function gamePageHref(title) {
const inGameRoute = window.location.pathname.includes("/g4m3s/");
const page = inGameRoute ? "index.html" : "g4m3s/index.html";
return `${page}?title=${encodeURIComponent(String(title || ""))}`;
}
async function get_all_apps() {
const date_last_queryed = JSON.parse(localStorage.getItem("dlq"));
const local_apps = JSON.parse(localStorage.getItem(APP_VER));
// console.log((new Date().getTime() - date_last_queryed) / 1000 / 60 / 60 / 24);
const onLocalHost = false; // window.location.includes("localhost");
if (
!onLocalHost &&
Array.isArray(local_apps) &&
date_last_queryed &&
(new Date().getTime() - date_last_queryed) / 1000 / 60 / 60 / 24 <=
REQUERY_TIME &&
local_apps?.[0] !== null
) {
console.log("Using local apps data.");
return local_apps;
} else {
console.log("Fetching apps data from Supabase.");
const { data, error } = await supabaseClient.rpc(
"get_apps_ordered_by_title"
);
if (error) {
console.error("Error fetching apps:", error);
window.alert(
"Error fetching apps. Please try again later. If the problem persists, please contact support."
);
return null;
}
// Store the data in local storage with a timestamp
localStorage.setItem(APP_VER, JSON.stringify(data));
localStorage.setItem("dlq", JSON.stringify(new Date().getTime()));
return data;
}
}
function remove_all_children(element) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
/**
*
* @param {Element} element
*/
async function list_all_apps(element) {
const query = await get_all_apps();
if (!Array.isArray(query)) {
return;
}
remove_all_children(element);
for (let i = 0; i < query.length; i++) {
/**
* <a id="Retro Bowl" class="Sports 2D" href="games/retro_bowl.html"
><img
onmouseover="viewFig(this)"
onmouseout="hideFig(this)"
src="assets/img/icons/retro_bowl_icon.webp"
alt="Icon"
loading="lazy"
/>
<figcaption>Retro Bowl</figcaption></a
>
re create this code with the data from the query
*/
const app = query[i];
const appTitle = app.title;
const appCategory = app.categories;
const appId = app.id;
const appIcon = app.icon;
// now create the element
const a = document.createElement("a");
a.id = appTitle;
a.className = appCategory;
a.href = gamePageHref(appTitle);
const img = document.createElement("img");
img.onmouseover = function () {
viewFig(this);
};
img.onmouseout = function () {
hideFig(this);
};
img.src = appIcon;
img.alt = appTitle;
img.loading = "lazy";
const figcaption = document.createElement("figcaption");
figcaption.innerText = appTitle.replaceAll("-", " ");
a.appendChild(img);
a.appendChild(figcaption);
element.appendChild(a);
if (i % 40 === 0 && i !== 0) {
const adDiv = create_in_article_ad();
element.appendChild(adDiv);
}
}
}
async function get_app_by_title(title) {
const apps = await get_all_apps();
if (!Array.isArray(apps)) {
console.error("Error fetching apps list");
return null;
}
const app = apps.find(
(app) => app.title.toLowerCase() === title.toLowerCase()
);
if (!app) {
console.error("App not found:", title);
return null;
}
return app;
}
function renderGameNotFound(message) {
try {
const reason =
message ||
"We couldn't find that game. It may have been moved or removed.";
const pagePrefix = window.location.hostname.split(".")[0];
try {
window.document.title = `Game Not Found - ${pagePrefix}`;
} catch (_) {}
const titleEl = document.getElementById("game-title");
if (titleEl) {
titleEl.textContent = "Game not found";
}
const iframeWrap = document.querySelector(".game-iframe-container");
if (iframeWrap) {
iframeWrap.innerHTML =
'<div style="padding:24px; text-align:center; min-height:200px; display:flex; align-items:center; justify-content:center;">' +
`<div>` +
`<div style="font-size:1.25rem; font-weight:600; margin-bottom:8px;">Game not found</div>` +
`<div style="opacity:0.9; margin-bottom:12px;">${reason}</div>` +
`<a href="./" style="color:#ff6b6b; text-decoration:none;">\u2190 Back to games</a>` +
`</div>` +
`</div>`;
}
const descTarget = document.getElementById("game-description");
if (descTarget) {
descTarget.textContent = "";
}
const relatedWrap = document.getElementById("related-games");
if (relatedWrap) {
try {
get_all_apps()
.then((all) => {
if (!Array.isArray(all)) return;
remove_all_children(relatedWrap);
const picks = all.slice(0, 3);
for (const rel of picks) {
const a = document.createElement("a");
a.href = gamePageHref(rel.title);
const img = document.createElement("img");
img.src = rel.icon;
img.alt = rel.title;
img.loading = "lazy";
img.style.width = "120px";
img.style.height = "120px";
a.appendChild(img);
relatedWrap.appendChild(a);
}
})
.catch(() => {});
} catch (_) {}
}
} catch (e) {
// As a last resort, fall back to 404 page
try {
window.location.href = "g404.html";
} catch (_) {}
}
}
async function hydrateAppPage() {
const urlParams = new URLSearchParams(window.location.search);
const appTitle = urlParams.get("title");
if (!appTitle) {
console.error("No app title provided in the URL.");
renderGameNotFound("No game specified in the URL.");
return;
}
const appData = await get_app_by_title(appTitle);
if (!appData) {
console.error("Game not found:", appTitle);
renderGameNotFound(
`We couldn't find "${appTitle.replaceAll(
"-",
" "
)}". It may have been moved or renamed.`
);
return;
}
// remove current canonical and add new one
const existingCanonical = document.querySelector('link[rel="canonical"]');
if (existingCanonical) {
existingCanonical.remove();
}
const canonicalLink = document.createElement("link");
canonicalLink.rel = "canonical";
canonicalLink.href = `https://grapes-os.org/g4m3s/?title=${appData.title}`;
document.head.appendChild(canonicalLink);
window.document.title =
appTitle.replaceAll("-", " ") +
` - ${window.location.hostname.split(".")[0]}`;
// Update meta description using app.desc
if (appData.desc) {
const existingMetaDesc = document.querySelector('meta[name="description"]');
if (existingMetaDesc) {
existingMetaDesc.remove();
}
const metaDescMeta = document.createElement("meta");
metaDescMeta.setAttribute("name", "description");
metaDescMeta.setAttribute(
"content",
getDescriptionFromMarkdown(appData.desc)
);
document.head.appendChild(metaDescMeta);
}
// Set image meta tags for social media sharing
const existingOgImage = document.querySelector('meta[property="og:image"]');
if (existingOgImage) {
existingOgImage.remove();
}
const existingTwitterImage = document.querySelector(
'meta[name="twitter:image"]'
);
if (existingTwitterImage) {
existingTwitterImage.remove();
}
const ogImageMeta = document.createElement("meta");
ogImageMeta.setAttribute("property", "og:image");
ogImageMeta.setAttribute("content", appData.icon);
ogImageMeta.setAttribute("alt", `${appData.title} unblocked game icon`);
document.head.appendChild(ogImageMeta);
const twitterImageMeta = document.createElement("meta");
twitterImageMeta.setAttribute("name", "twitter:image");
twitterImageMeta.setAttribute("content", appData.icon);
twitterImageMeta.setAttribute("alt", `${appData.title} unblocked game icon`);
document.head.appendChild(twitterImageMeta);
// Populate the page with app data
// Render markdown description safely under the game
try {
const descTarget = document.getElementById("game-description");
if (descTarget && appData.desc) {
const rawHtml =
typeof window !== "undefined" && window.marked && window.marked.parse
? window.marked.parse(appData.desc)
: appData.desc;
const safeHtml =
typeof window !== "undefined" &&
window.DOMPurify &&
window.DOMPurify.sanitize
? window.DOMPurify.sanitize(rawHtml)
: rawHtml;
descTarget.innerHTML = safeHtml;
}
} catch (e) {
console.warn(
"Failed to render markdown description; falling back to text.",
e
);
const descTarget = document.getElementById("game-description");
if (descTarget && appData.desc) {
descTarget.innerText = appData.desc;
}
}
document
.getElementById("game-title")
.prepend(appData.title.replaceAll("-", " ") + " Unblocked");
// Add small text under title about .top_message property
if (appData?.top_message) {
const titleElement = document.getElementById("game-title");
const infoText = document.createElement("div");
infoText.style.fontSize = ".6rem";
infoText.style.color = "#666";
infoText.style.marginTop = ".5rem";
infoText.textContent = appData?.top_message;
titleElement.appendChild(infoText);
}
const GAMES_PAGE_URL = "https://maddox05.github.io/basic-ruffle-player";
let app_link = appData.link;
if (window.location.hostname === "grapes-os.org") {
app_link = app_link.replace(GAMES_PAGE_URL, "https://db.grapes-os.org");
}
document.getElementById("gameFrame").src = app_link;
// Populate minimal related games (3 items) after fullscreen button
try {
const allApps = await get_all_apps();
const relatedWrap = document.getElementById("related-games");
if (relatedWrap && Array.isArray(allApps)) {
remove_all_children(relatedWrap);
const currentCats = new Set(
Array.isArray(appData.categories)
? appData.categories
: typeof appData.categories === "string"
? appData.categories.split(" ")
: []
);
const scored = allApps
.filter((a) => a.title !== appData.title)
.map((a) => {
const aCats = Array.isArray(a.categories)
? a.categories
: typeof a.categories === "string"
? a.categories.split(" ")
: [];
const overlap = aCats.some((c) => currentCats.has(c));
return { app: a, score: overlap ? 1 : 0 };
})
.filter((x) => x.score > 0)
.slice(0, 6);
const finalList =
scored.length > 0 ? scored.map((x) => x.app) : allApps.slice(0, 3);
for (const rel of finalList) {
const a = document.createElement("a");
a.href = gamePageHref(rel.title);
const img = document.createElement("img");
img.src = rel.icon;
img.alt = rel.title;
img.loading = "lazy";
img.style.width = "120px";
img.style.height = "120px";
img.style.objectFit = "cover";
img.style.borderRadius = "10px";
a.appendChild(img);
relatedWrap.appendChild(a);
}
}
} catch (e) {
console.warn("Unable to populate related games", e);
}
}
document.addEventListener("DOMContentLoaded", function () {
const appListElement = document.getElementById("icon_image");
if (appListElement) {
list_all_apps(appListElement).then(() => {
document.dispatchEvent(new Event("GamesLoaded"));
});
}
if (
window.location.pathname.includes("g4m3s") &&
window.location.search.includes("title")
) {
hydrateAppPage();
} else if (window.location.pathname.includes("g4m3s")) {
renderGameNotFound("No game specified in the URL.");
}
});