Improve authentication recovery and mobile gameplay

This commit is contained in:
2026-10-07 11:10:29 +05:00
parent 233e4d57f0
commit ce26e6fdbc
22 changed files with 763 additions and 97 deletions

View File

@@ -284,3 +284,10 @@
gap: 8px;
}
}
.legacy-password-dialog { width: min(540px, calc(100vw - 24px)); }
.legacy-password-dialog > svg { color: var(--green); }
.legacy-password-dialog h2 { margin-top: 16px; }
.legacy-password-dialog .field input { width: 100%; font: 14px monospace; padding: 12px; background: var(--bg); color: var(--text); border: 1px solid var(--line); }
.legacy-password-dialog > .text-link { margin-top: 18px; }
.password-saved { display: flex; gap: 10px; align-items: flex-start; margin: 20px 0; }
.password-saved input { flex-shrink: 0; margin-top: 5px; accent-color: var(--green); }

View File

@@ -11,11 +11,13 @@ export async function GET(request: NextRequest) {
const legacyToken = request.nextUrl.searchParams.get("legacy_recovery_token");
const type = request.nextUrl.searchParams.get("type");
const next = authDestination(request.nextUrl.searchParams.get("next"));
if (type === "magiclink")
return NextResponse.redirect(new URL("/log-in?auth=failed", origin));
if (
code ||
legacyToken ||
(tokenHash &&
(type === "email" || type === "recovery" || type === "magiclink"))
(type === "email" || type === "signup" || type === "recovery"))
) {
const client = await authClient();
let { data, error } = code
@@ -24,7 +26,7 @@ export async function GET(request: NextRequest) {
? await client.auth.exchangeCodeForSession(legacyToken)
: await client.auth.verifyOtp({
token_hash: tokenHash!,
type: type as "email" | "recovery" | "magiclink",
type: type as "email" | "signup" | "recovery",
});
// Older email templates could place a recovery token hash in /code=...
// instead of a PKCE code. Only Supabase can decide whether it is valid.

View File

@@ -12,8 +12,11 @@ import { SiteCursor } from "@/components/SiteCursor";
import { I18nProvider } from "@/components/I18nProvider";
import { getI18n } from "@/lib/i18n";
import "./polish.css";
import "./mobile-game.css";
import { SiteSettingsProvider } from "@/components/SiteSettingsProvider";
import { initialSiteSettings } from "@/lib/site-settings";
import { user } from "@/lib/supabase";
import { LegacyPasswordNotice } from "@/components/LegacyPasswordNotice";
export async function generateMetadata(): Promise<Metadata> {
const { t } = await getI18n();
return {
@@ -31,10 +34,8 @@ export default async function RootLayout({
}: {
children: React.ReactNode;
}) {
const [{ locale, preference, detectedLocale }, settings] = await Promise.all([
getI18n(),
initialSiteSettings(),
]);
const [{ locale, preference, detectedLocale }, settings, account] =
await Promise.all([getI18n(), initialSiteSettings(), user()]);
return (
<html lang={locale}>
<body>
@@ -46,6 +47,7 @@ export default async function RootLayout({
<SiteSettingsProvider initialSettings={settings}>
{children}
<SiteCursor />
<LegacyPasswordNotice enabled={!!account?.email} />
</SiteSettingsProvider>
</I18nProvider>
</body>

192
app/mobile-game.css Normal file
View File

@@ -0,0 +1,192 @@
.mobile-map-toggle,
.report-icon {
display: none;
}
@media (max-width: 700px) {
.game-page {
min-height: 0;
}
.game-header {
display: grid;
grid-template-columns: minmax(0, 1fr) 44px;
gap: 8px;
padding: max(10px, env(safe-area-inset-top))
max(10px, env(safe-area-inset-right)) 12px
max(10px, env(safe-area-inset-left));
}
.game-header .brand {
font-size: 17px;
white-space: nowrap;
gap: 6px;
}
.game-header .brand-cube {
flex-shrink: 0;
}
.game-header .report-trigger {
width: 44px;
height: 44px;
padding: 0;
}
.game-header .report-trigger > span {
display: none;
}
.game-header .report-icon {
display: block;
}
.game-info {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
width: 100%;
gap: 8px;
padding: 8px;
border-radius: 10px;
}
.game-info > div {
min-width: 0;
}
.game-info small {
white-space: nowrap;
font-size: 8px;
letter-spacing: 0;
}
.game-info strong {
white-space: nowrap;
font-size: 17px;
line-height: 1.4;
}
.panorama-controls {
top: calc(max(10px, env(safe-area-inset-top)) + 112px);
left: max(10px, env(safe-area-inset-left));
gap: 6px;
}
.panorama-controls button {
width: 44px;
height: 44px;
padding: 0;
}
.game-page .game-map,
.game-page .game-map.expanded {
right: max(8px, env(safe-area-inset-right));
left: max(8px, env(safe-area-inset-left));
bottom: max(10px, env(safe-area-inset-bottom));
width: auto;
max-width: none;
box-shadow: 0 8px 24px #0006;
}
.game-map:not(.expanded) .game-map-content {
position: absolute;
bottom: 100%;
left: 0;
right: 0;
visibility: hidden;
pointer-events: none;
}
.mobile-map-toggle {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
min-height: 56px;
padding: 12px;
border: 0;
color: #fff1d0;
background: #241e18;
font: 14px var(--font-pixel, monospace);
}
.mobile-map-toggle small {
font: 11px monospace;
}
.game-map.expanded .mobile-map-toggle {
display: none;
}
.game-map .map-topline {
min-height: 44px;
padding: 0 8px 0 12px;
}
.game-map .map-topline button {
display: grid;
place-items: center;
width: 44px;
height: 44px;
}
.game-page .game-map .map-surface {
height: min(46dvh, 360px);
border-width: 12px;
border-image-width: 12px;
}
.game-map .map-actions {
padding: 8px 10px 10px;
}
.game-map .map-actions .button {
min-height: 48px;
padding: 10px 12px;
font-size: 13px;
white-space: nowrap;
}
.game-map .map-actions small {
margin-bottom: 8px;
}
.game-map .leaflet-bar a {
width: 40px;
height: 40px;
line-height: 40px;
}
.look-help {
display: none;
}
.round-result {
max-width: calc(100% - 24px);
max-height: calc(100dvh - 140px);
overflow-y: auto;
padding: 20px;
}
html[lang] .round-result h2 {
font-size: 21px;
}
.round-result .score {
font-size: 42px;
}
}
@media (max-height: 500px) and (max-width: 950px) {
.game-page {
min-height: 0;
}
.game-header {
display: flex;
padding: 8px 12px;
gap: 10px;
}
.game-header .brand {
font-size: 16px;
}
.game-header .report-trigger {
order: 3;
}
.game-info {
width: auto;
flex: 1;
max-width: 300px;
margin-left: auto;
gap: 16px;
padding: 6px 10px;
}
.panorama-controls {
top: 72px;
left: 12px;
}
.game-page .game-map,
.game-page .game-map.expanded {
left: auto;
width: min(440px, calc(100vw - 80px));
right: 8px;
bottom: max(8px, env(safe-area-inset-bottom));
}
.game-page .game-map .map-surface {
height: max(100px, calc(100dvh - 190px));
}
.round-result {
max-height: calc(100dvh - 80px);
}
}