Files
MineGeo/app/moderator-apply/apply.tsx
pavel444-byte 9aa7fd6774
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m52s
Fix Telegram application theme contrast
2026-11-01 15:40:13 +05:00

280 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
type FormEvent,
} from "react";
interface TelegramUser {
id: number;
username?: string;
first_name?: string;
last_name?: string;
}
interface ThemeParams {
bg_color?: string;
text_color?: string;
hint_color?: string;
link_color?: string;
button_color?: string;
button_text_color?: string;
secondary_bg_color?: string;
}
interface TelegramWebApp {
initData: string;
initDataUnsafe: { user?: TelegramUser };
colorScheme?: string;
themeParams?: ThemeParams;
ready: () => void;
expand: () => void;
close: () => void;
openLink: (url: string) => void;
setHeaderColor?: (color: string) => void;
setBackgroundColor?: (color: string) => void;
MainButton?: {
text: string;
color: string;
textColor: string;
isVisible: boolean;
setText: (text: string) => void;
show: () => void;
hide: () => void;
enable: () => void;
disable: () => void;
showProgress: (leaveActive?: boolean) => void;
hideProgress: () => void;
onClick: (handler: () => void) => void;
offClick: (handler: () => void) => void;
};
BackButton?: { show: () => void; hide: () => void; onClick: (handler: () => void) => void };
}
declare global {
interface Window {
Telegram?: { WebApp?: TelegramWebApp };
}
}
type Status = "loading" | "form" | "success" | "outside";
const TELEGRAM_SCRIPT = "https://telegram.org/js/telegram-web-app.js";
const BOT_LINK = "https://t.me/MineGeoModeratorBot";
function loadTelegramScript(): Promise<void> {
if (window.Telegram?.WebApp) return Promise.resolve();
return new Promise((resolve, reject) => {
const existing = document.querySelector<HTMLScriptElement>(
`script[src="${TELEGRAM_SCRIPT}"]`,
);
if (existing) {
existing.addEventListener("load", () => resolve(), { once: true });
existing.addEventListener("error", () => reject(new Error("load failed")), {
once: true,
});
return;
}
const script = document.createElement("script");
script.src = TELEGRAM_SCRIPT;
script.async = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error("load failed"));
document.head.appendChild(script);
});
}
export function ModeratorApplyApp() {
const [status, setStatus] = useState<Status>("loading");
const [username, setUsername] = useState("");
const [error, setError] = useState<string | null>(null);
const [confirmationUrl, setConfirmationUrl] = useState<string | null>(null);
const [theme, setTheme] = useState<ThemeParams>({});
const tgRef = useRef<TelegramWebApp | null>(null);
const submitRef = useRef<() => void>(() => {});
const submittingRef = useRef(false);
useEffect(() => {
let cancelled = false;
loadTelegramScript()
.then(() => {
if (cancelled) return;
const tg = window.Telegram?.WebApp;
if (!tg) {
setStatus("outside");
return;
}
tgRef.current = tg;
tg.ready();
tg.expand();
setTheme(tg.themeParams ?? {});
const background = tg.themeParams?.bg_color ?? "#0e1621";
tg.setBackgroundColor?.(background);
tg.setHeaderColor?.(tg.themeParams?.secondary_bg_color ?? background);
if (tg.initData) setStatus("form");
else setStatus("outside");
})
.catch(() => !cancelled && setStatus("outside"));
return () => {
cancelled = true;
};
}, []);
const submit = useCallback(async () => {
const tg = tgRef.current;
if (!tg?.initData || !/^[A-Za-z0-9_]{3,32}$/.test(username.trim()) || submittingRef.current) return;
submittingRef.current = true;
const mainButton = tg.MainButton;
setError(null);
mainButton?.disable();
mainButton?.showProgress(true);
try {
const response = await fetch("/api/moderator-apply", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ initData: tg.initData, username: username.trim() }),
});
const data = (await response.json().catch(() => null)) as {
url?: string;
error?: string;
} | null;
if (!response.ok || !data?.url) {
setError(data?.error ?? "Something went wrong. Please try again.");
return;
}
setConfirmationUrl(data.url);
setStatus("success");
mainButton?.hide();
} catch {
setError("Network error. Please try again.");
} finally {
submittingRef.current = false;
mainButton?.hideProgress();
mainButton?.enable();
}
}, [username]);
submitRef.current = submit;
useEffect(() => {
if (status !== "form") return;
const tg = tgRef.current;
const mainButton = tg?.MainButton;
if (!tg || !mainButton) return;
mainButton.text = "Submit application";
if (tg.themeParams?.button_color) mainButton.color = tg.themeParams.button_color;
if (tg.themeParams?.button_text_color)
mainButton.textColor = tg.themeParams.button_text_color;
const handler = () => submitRef.current();
mainButton.onClick(handler);
mainButton.show();
return () => {
mainButton.offClick(handler);
mainButton.hide();
};
}, [status]);
const onSubmit = (event: FormEvent) => {
event.preventDefault();
void submit();
};
const style = useMemo(
() =>
({
"--tg-bg": theme.bg_color ?? "#ffffff",
"--tg-secondary-bg": theme.secondary_bg_color ?? "#f4f4f5",
"--tg-text": theme.text_color ?? "#000000",
"--tg-hint": theme.hint_color ?? "#707579",
"--tg-link": theme.link_color ?? "#3390ec",
"--tg-button": theme.button_color ?? "#3390ec",
"--tg-button-text": theme.button_text_color ?? "#ffffff",
}) as CSSProperties,
[theme],
);
const displayName = useMemo(() => {
const user = tgRef.current?.initDataUnsafe?.user;
if (!user) return null;
const name = [user.first_name, user.last_name].filter(Boolean).join(" ").trim();
return name || user.username || null;
}, [status]);
return (
<main className="tg-apply" style={style}>
<div className="tg-apply-card">
{status === "loading" && <p className="tg-apply-hint">Loading…</p>}
{status === "outside" && (
<div className="tg-apply-body">
<h1 className="tg-apply-title">Moderator application</h1>
<p className="tg-apply-hint">
This form opens inside Telegram. Use the MineGeo bot to apply.
</p>
<a className="tg-apply-button" href={BOT_LINK} target="_blank" rel="noreferrer">
Open @MineGeoModeratorBot
</a>
</div>
)}
{status === "form" && (
<form className="tg-apply-body" onSubmit={onSubmit}>
<h1 className="tg-apply-title">Apply for moderator</h1>
<p className="tg-apply-hint">
{displayName ? `Signed in as ${displayName}. ` : ""}
Enter your MineGeo username. You will confirm the application from that
account.
</p>
<label className="tg-apply-label" htmlFor="minegeo-username">
MineGeo username
</label>
<input
id="minegeo-username"
className="tg-apply-input"
value={username}
onChange={(event) => setUsername(event.target.value)}
placeholder="your_username"
autoCapitalize="none"
autoCorrect="off"
autoComplete="username"
minLength={3}
maxLength={32}
pattern="[A-Za-z0-9_]{3,32}"
required
/>
<p className="tg-apply-hint">3–32 letters, numbers or underscores.</p>
{error && (
<p className="tg-apply-error" role="alert">
{error}
</p>
)}
<button className="tg-apply-button" type="submit">
Submit application
</button>
</form>
)}
{status === "success" && confirmationUrl && (
<div className="tg-apply-body">
<h1 className="tg-apply-title">Application created</h1>
<p className="tg-apply-hint">
Confirm it from your MineGeo account within 30 minutes. The staff will
review it after confirmation.
</p>
<button
className="tg-apply-button"
type="button"
onClick={() => tgRef.current?.openLink(confirmationUrl)}
>
Confirm application
</button>
<p className="tg-apply-url">{confirmationUrl}</p>
</div>
)}
</div>
</main>
);
}