Validate screenshot URLs with inline hints
This commit is contained in:
@@ -63,6 +63,8 @@ const translations = {
|
|||||||
"form.placeholder.youtube": "YouTube URL",
|
"form.placeholder.youtube": "YouTube URL",
|
||||||
"form.placeholder.gameUrl": "Game website URL",
|
"form.placeholder.gameUrl": "Game website URL",
|
||||||
"form.playersInvalid": "Players must be between 1 and 32, and min cannot exceed max.",
|
"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.mySuggestions": "Your suggestions",
|
||||||
"section.allSuggestions": "All suggestions",
|
"section.allSuggestions": "All suggestions",
|
||||||
@@ -229,6 +231,8 @@ const translations = {
|
|||||||
"form.placeholder.youtube": "YouTube-URL",
|
"form.placeholder.youtube": "YouTube-URL",
|
||||||
"form.placeholder.gameUrl": "Spiel-Webseite",
|
"form.placeholder.gameUrl": "Spiel-Webseite",
|
||||||
"form.playersInvalid": "Spielerzahl muss zwischen 1 und 32 liegen, und Min darf Max nicht überschreiten.",
|
"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.mySuggestions": "Deine Vorschläge",
|
||||||
"section.allSuggestions": "Alle Vorschläge",
|
"section.allSuggestions": "Alle Vorschläge",
|
||||||
|
|||||||
@@ -520,6 +520,8 @@ function buildSuggestionForm(initial = {}, lockTitle = false) {
|
|||||||
<span class="char-counter" data-for="screenshotUrl"></span>
|
<span class="char-counter" data-for="screenshotUrl"></span>
|
||||||
</span>
|
</span>
|
||||||
<input name="screenshotUrl" maxlength="2048" />
|
<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>
|
||||||
<label class="stack">
|
<label class="stack">
|
||||||
<span class="label-row">
|
<span class="label-row">
|
||||||
@@ -618,6 +620,8 @@ function openSuggestionModal({ title, submitLabel, initial = {}, onSubmit, lockT
|
|||||||
const errorBox = form.querySelector('[data-error="players"]');
|
const errorBox = form.querySelector('[data-error="players"]');
|
||||||
const minInput = form.querySelector('input[name="minPlayers"]');
|
const minInput = form.querySelector('input[name="minPlayers"]');
|
||||||
const maxInput = form.querySelector('input[name="maxPlayers"]');
|
const maxInput = form.querySelector('input[name="maxPlayers"]');
|
||||||
|
const screenshotError = form.querySelector('[data-error="screenshot"]');
|
||||||
|
const screenshotInput = form.querySelector('input[name="screenshotUrl"]');
|
||||||
const markError = (msg) => {
|
const markError = (msg) => {
|
||||||
if (errorBox) {
|
if (errorBox) {
|
||||||
errorBox.textContent = msg;
|
errorBox.textContent = msg;
|
||||||
@@ -626,6 +630,7 @@ function openSuggestionModal({ title, submitLabel, initial = {}, onSubmit, lockT
|
|||||||
};
|
};
|
||||||
const clearError = () => {
|
const clearError = () => {
|
||||||
if (errorBox) errorBox.classList.add("hidden");
|
if (errorBox) errorBox.classList.add("hidden");
|
||||||
|
if (screenshotError) screenshotError.classList.add("hidden");
|
||||||
};
|
};
|
||||||
clearError();
|
clearError();
|
||||||
const min = data.minPlayers;
|
const min = data.minPlayers;
|
||||||
@@ -643,8 +648,14 @@ function openSuggestionModal({ title, submitLabel, initial = {}, onSubmit, lockT
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.screenshotUrl && !isValidImageUrl(data.screenshotUrl)) {
|
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);
|
if (!data.name?.trim()) return toast(t("toast.nameRequired"), true);
|
||||||
try {
|
try {
|
||||||
await onSubmit(data, close, submitBtn);
|
await onSubmit(data, close, submitBtn);
|
||||||
@@ -976,6 +987,23 @@ function isValidImageUrl(url) {
|
|||||||
const u = new URL(url);
|
const u = new URL(url);
|
||||||
const allowed = ["http:", "https:"];
|
const allowed = ["http:", "https:"];
|
||||||
if (!allowed.includes(u.protocol)) return false;
|
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();
|
const path = u.pathname.toLowerCase();
|
||||||
return [".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif"].some((ext) =>
|
return [".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif"].some((ext) =>
|
||||||
path.endsWith(ext),
|
path.endsWith(ext),
|
||||||
|
|||||||
Reference in New Issue
Block a user