Validate screenshot URLs with inline hints

This commit is contained in:
2026-02-06 22:52:24 +01:00
parent c64ac7480d
commit ae7cf1ff64
2 changed files with 33 additions and 1 deletions

View File

@@ -63,6 +63,8 @@ const translations = {
"form.placeholder.youtube": "YouTube URL",
"form.placeholder.gameUrl": "Game website URL",
"form.playersInvalid": "Players must be between 1 and 32, and min cannot exceed max.",
"form.screenshotHint": "Use a public direct image link (http/https), max 5 MB. Avoid shortlinks/redirects.",
"form.screenshotInvalid": "Screenshot must be a direct http/https image URL (png, jpg, jpeg, gif, webp, avif) under 5 MB and not a redirect/shortlink.",
"section.mySuggestions": "Your suggestions",
"section.allSuggestions": "All suggestions",
@@ -229,6 +231,8 @@ const translations = {
"form.placeholder.youtube": "YouTube-URL",
"form.placeholder.gameUrl": "Spiel-Webseite",
"form.playersInvalid": "Spielerzahl muss zwischen 1 und 32 liegen, und Min darf Max nicht überschreiten.",
"form.screenshotHint": "Nutze einen öffentlichen Bildlink (http/https), max. 5 MB. Keine Kurzlinks/Weiterleitungen.",
"form.screenshotInvalid": "Screenshot muss eine direkte http/https-Bild-URL sein (png, jpg, jpeg, gif, webp, avif), unter 5 MB und ohne Weiterleitung/Kurzlink.",
"section.mySuggestions": "Deine Vorschläge",
"section.allSuggestions": "Alle Vorschläge",

View File

@@ -520,6 +520,8 @@ function buildSuggestionForm(initial = {}, lockTitle = false) {
<span class="char-counter" data-for="screenshotUrl"></span>
</span>
<input name="screenshotUrl" maxlength="2048" />
<p class="hint" data-i18n="form.screenshotHint">${t("form.screenshotHint")}</p>
<div class="form-error hidden" data-error="screenshot"></div>
</label>
<label class="stack">
<span class="label-row">
@@ -618,6 +620,8 @@ function openSuggestionModal({ title, submitLabel, initial = {}, onSubmit, lockT
const errorBox = form.querySelector('[data-error="players"]');
const minInput = form.querySelector('input[name="minPlayers"]');
const maxInput = form.querySelector('input[name="maxPlayers"]');
const screenshotError = form.querySelector('[data-error="screenshot"]');
const screenshotInput = form.querySelector('input[name="screenshotUrl"]');
const markError = (msg) => {
if (errorBox) {
errorBox.textContent = msg;
@@ -626,6 +630,7 @@ function openSuggestionModal({ title, submitLabel, initial = {}, onSubmit, lockT
};
const clearError = () => {
if (errorBox) errorBox.classList.add("hidden");
if (screenshotError) screenshotError.classList.add("hidden");
};
clearError();
const min = data.minPlayers;
@@ -643,8 +648,14 @@ function openSuggestionModal({ title, submitLabel, initial = {}, onSubmit, lockT
return;
}
if (data.screenshotUrl && !isValidImageUrl(data.screenshotUrl)) {
return toast(t("toast.invalidImageUrl"), true);
if (screenshotError) {
screenshotError.textContent = t("form.screenshotInvalid");
screenshotError.classList.remove("hidden");
}
screenshotInput?.classList.add("input-error");
return;
}
screenshotInput?.classList.remove("input-error");
if (!data.name?.trim()) return toast(t("toast.nameRequired"), true);
try {
await onSubmit(data, close, submitBtn);
@@ -976,6 +987,23 @@ function isValidImageUrl(url) {
const u = new URL(url);
const allowed = ["http:", "https:"];
if (!allowed.includes(u.protocol)) return false;
const host = u.hostname.toLowerCase();
const bannedHosts = [
"bit.ly",
"tinyurl.com",
"t.co",
"goo.gl",
"ow.ly",
"is.gd",
"buff.ly",
"rebrand.ly",
"steamcommunity.com",
"store.steampowered.com",
"imgur.com",
];
if (bannedHosts.some((h) => host === h)) return false;
if (host === "imgur.com" && !u.pathname.startsWith("/a/") && !u.pathname.startsWith("/gallery/")) return false;
if (host === "steamcommunity.com") return false;
const path = u.pathname.toLowerCase();
return [".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif"].some((ext) =>
path.endsWith(ext),