diff --git a/app/appearance.css b/app/appearance.css index 6303d1a..437ec0d 100644 --- a/app/appearance.css +++ b/app/appearance.css @@ -83,6 +83,24 @@ html[data-theme="light"] :is(h1, h2, h3) { html[data-theme="light"] .hero h1 span { text-shadow: none; } +html[data-theme="light"] :is(.compass-art, .floating-tag) { + background: var(--panel); + color: var(--text); + border-color: var(--line); + box-shadow: + inset 0 1px #fff, + 0 4px 12px #243a1814; +} +html[data-theme="light"] .floating-tag strong { + color: var(--text); +} +html[data-theme="light"] .floating-tag > span { + color: var(--muted); +} +html[data-theme="light"] .card-type { + background: var(--panel2); + color: var(--text); +} html[data-theme="light"] :is(.site-header, .site-footer) { background: var(--bg); border-color: var(--line); @@ -387,7 +405,12 @@ html[data-theme="light"] .site-cursor-dot span { .header-inner { max-width: 1600px; } -@media (max-width: 1500px) { +.nav-toggle, +.site-navigation .nav-support, +.account-symbol { + display: none; +} +@media (max-width: 1500px) and (min-width: 1201px) { .site-header { height: auto; } @@ -407,76 +430,140 @@ html[data-theme="light"] .site-cursor-dot span { padding: 12px 0 2px; } } -@media (max-width: 500px) { - .header-inner { - grid-template-columns: minmax(0, 1fr) auto; - gap: 10px; +@media (max-width: 1200px) { + .site-header { + height: auto; } - .header-actions { - grid-row: 1; - grid-column: 2; - justify-self: end; + .header-inner { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + gap: 10px; + padding: 12px 24px; } .header-inner > .brand { - grid-row: 1; - grid-column: 1; - justify-self: start; + grid-area: 1 / 1; + } + .header-inner .header-actions { + grid-area: 1 / 2; + margin: 0; + justify-self: end; + gap: 8px; + } + .header-inner .nav-toggle { + grid-area: 1 / 3; + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border: 1px solid var(--line); + border-radius: var(--radius-control); + color: var(--text); + background: var(--panel); + } + .header-inner nav.site-navigation { + display: none; + grid-area: 2 / 1 / auto / -1; + width: 100%; + padding: 12px 0 0; + margin: 2px 0 0; + border-top: 1px solid var(--line); + gap: 4px; + font: + 500 14px/1.5 Arial, + Helvetica, + sans-serif; + } + .header-inner nav.site-navigation[data-open="true"] { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .site-navigation a { + padding: 12px; + border-radius: 6px; + } + .site-navigation a[aria-current="page"] { + color: var(--green); + background: var(--panel2); + } + .site-navigation a[aria-current="page"]::after { + display: none; + } + .site-navigation .nav-support { + display: block; + } + .site-navigation a:hover, + .nav-toggle:hover { + background: var(--panel2); + } +} +@media (max-width: 600px) { + .header-inner { + padding: 10px 16px; + gap: 8px; + } + .header-inner .brand { + gap: 5px; } .header-inner .brand .brand-wordmark { font-size: 21px; } .header-inner .brand .brand-mark { - width: 30px; - height: 36px; + width: 28px; + height: 34px; } - .header-inner .brand { - gap: 5px; + .header-inner .brand sup { + display: none; } - .header-actions { - gap: 4px; + .header-inner .header-actions { + gap: 6px; + } + .header-actions .header-report, + .header-account > span, + .header-account .account-arrow { + display: none; + } + .header-actions .header-account { + width: 40px; + height: 40px; + padding: 0; + justify-content: center; + border-radius: var(--radius-control); + } + .header-account .account-symbol { + display: block; + } + .header-actions .theme-toggle, + .header-inner .nav-toggle { + width: 40px; + height: 40px; } .game-header .brand .brand-wordmark { font-size: 17px; } } -@media (max-width: 600px) { - .header-actions { - grid-row: 2; - grid-column: 1 / -1; - justify-self: start; - margin-left: 0; - } - .header-inner nav { - grid-row: 3; - } -} -@media (max-width: 500px) { - html[lang="ru"] .header-actions { - grid-row: 2; - grid-column: 1 / -1; - justify-self: start; - margin-left: 0; - } - html[lang="ru"] .header-inner nav { - grid-row: 3; - } -} .difficulty-options button.difficulty-void { border-color: #a855f7; background: linear-gradient(145deg, #21122f, #100c18); color: #e9d5ff; - box-shadow: 0 0 16px #a855f738, inset 0 0 20px #a855f710; + box-shadow: + 0 0 16px #a855f738, + inset 0 0 20px #a855f710; } .difficulty-options button.difficulty-void strong { color: #e9d5ff; - text-shadow: 0 0 10px #c084fc99, 0 0 22px #a855f780; + text-shadow: + 0 0 10px #c084fc99, + 0 0 22px #a855f780; } .difficulty-options button.difficulty-void.selected, html[data-theme="light"] .difficulty-options button.difficulty-void.selected { color: #f3e8ff; background: linear-gradient(145deg, #482363, #21122f); border-color: #d8b4fe; - box-shadow: 0 0 24px #a855f760, inset 0 0 20px #c084fc18; + box-shadow: + 0 0 24px #a855f760, + inset 0 0 20px #c084fc18; } html[data-theme="light"] .difficulty-options button.difficulty-void { background: #f3e8ff; @@ -486,6 +573,9 @@ html[data-theme="light"] .difficulty-options button.difficulty-void { html[data-theme="light"] .difficulty-options button.difficulty-void strong { color: #7e22ce; } -html[data-theme="light"] .difficulty-options button.difficulty-void.selected strong { +html[data-theme="light"] + .difficulty-options + button.difficulty-void.selected + strong { color: #f3e8ff; } diff --git a/app/layout.tsx b/app/layout.tsx index b5936c9..be4cdf1 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -26,6 +26,7 @@ import { SiteSettingsProvider } from "@/components/SiteSettingsProvider"; import { initialSiteSettings } from "@/lib/site-settings"; import { rawUser, accountState } from "@/lib/supabase"; import { LegacyPasswordNotice } from "@/components/LegacyPasswordNotice"; +import { Suspense } from "react"; export async function generateMetadata(): Promise { const { t } = await getI18n(); return { @@ -43,11 +44,10 @@ export default async function RootLayout({ }: { children: React.ReactNode; }) { - const [{ locale, preference, detectedLocale }, settings, account] = - await Promise.all([getI18n(), initialSiteSettings(), rawUser()]); - const active = account - ? (await accountState(account.id)) === "active" - : false; + const [{ locale, preference, detectedLocale }, settings] = await Promise.all([ + getI18n(), + initialSiteSettings(), + ]); return ( @@ -62,7 +62,9 @@ export default async function RootLayout({ {children} - + + + @@ -70,3 +72,10 @@ export default async function RootLayout({ ); } +async function AccountPasswordNotice() { + const account = await rawUser(); + const active = account + ? (await accountState(account.id)) === "active" + : false; + return ; +} diff --git a/app/page.tsx b/app/page.tsx index eae0ea0..ac749a9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -18,13 +18,12 @@ import { LeaderboardPreview } from "@/components/Leaderboard"; import desktopRelease from "@/shared/desktop-release.json"; import { FirstVisitIntro } from "@/components/FirstVisitIntro"; import { SiteTrailer } from "@/components/SiteTrailer"; +import { Suspense } from "react"; export const dynamic = "force-dynamic"; export default async function Home() { const { t, formatNumber } = await getI18n(); - const [maps, rankings] = await Promise.all([ - catalog(), - leaderboard({ limit: 5 }), - ]); + const rankings = leaderboard({ limit: 5 }); + const maps = await catalog(); const servers = new Set(maps.map((map) => map.server_id)).size; const locations = maps.reduce((total, map) => total + map.panorama_count, 0); return ( @@ -78,6 +77,7 @@ export default async function Home() { width={1024} height={1024} loading="eager" + fetchPriority="high" unoptimized /> @@ -142,7 +142,13 @@ export default async function Home() { {maps.length ? (
{maps.slice(0, 4).map((map, index) => ( - + ))}
) : ( @@ -175,7 +181,19 @@ export default async function Home() { )} - + +

{t("THE EXPLORERS’ BOARD")}

+
+ + } + > + +
{t("HOW TO PLAY")} @@ -217,7 +235,7 @@ export default async function Home() {
{t("CHOOSE YOUR CHALLENGE")} -

{t("One world. Three ways.")}

+

{t("Find your challenge.")}

{[ { @@ -238,6 +256,12 @@ export default async function Home() { detail: t("Think fast. No zoom allowed."), color: "red", }, + { + name: t("Void"), + time: t("10 seconds"), + detail: t("The ultimate test. Fixed view, strict scoring."), + color: "void", + }, ].map((mode) => ( @@ -278,3 +302,10 @@ export default async function Home() { ); } +async function HomeLeaderboard({ + data, +}: { + data: ReturnType; +}) { + return ; +} diff --git a/app/polish.css b/app/polish.css index ef5d399..8f7c960 100644 --- a/app/polish.css +++ b/app/polish.css @@ -28,6 +28,12 @@ select:focus-visible { font-style: normal; font-display: swap; } +/* Fallback for user-provided Cyrillic names on English pages. */ +h1, +h2, +h3 { + font-family: MineGeoPixel, MineGeoPixelCyrillic, monospace; +} html[lang="ru"] h1, html[lang="ru"] h2, html[lang="ru"] h3, @@ -127,7 +133,7 @@ html[lang="ru"] .mode-row h3 span { } /* Keep catalog cards readable instead of squeezing long map names. */ .cards-grid { - grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); + grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); } .map-card .card-body .map-credit { display: block; @@ -251,6 +257,259 @@ html[lang="ru"] .mode-row h3 span { gap: 20px; margin: 24px 0 32px; } + +/* Shared layout repairs: readable content, predictable cards, and touch targets. */ +:is(a, button, input, select, textarea, summary):focus-visible { + outline: 2px solid var(--green); + outline-offset: 4px; +} +.page-content > h1 { + text-wrap: balance; + overflow-wrap: anywhere; +} +.lead { + text-wrap: pretty; +} +.map-card { + display: flex; + flex-direction: column; + min-width: 0; +} +.map-card .card-body { + flex: 1; + display: flex; + flex-direction: column; + padding: 20px; + gap: 12px; +} +.map-card .card-body .eyebrow { + font: 11px/1.5 monospace; + margin: 0; +} +.map-card .card-body h3, +html[lang="ru"] .map-card .card-body h3 { + font: + 400 18px/1.6 MineGeoPixelCyrillic, + monospace; + letter-spacing: -0.3px; + text-shadow: none; + min-height: 4.8em; + margin: 0; + gap: 12px; + align-items: flex-start; +} +.map-card .card-body h3 > span { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + min-width: 0; + overflow-wrap: anywhere; +} +.map-card .card-body h3 svg { + margin-top: 4px; +} +.map-card .card-body p { + font-size: 13px; + line-height: 1.65; + margin: 0; + min-height: 3.3em; +} +.map-card .card-body .map-credit { + margin-top: auto; + padding-top: 4px; +} +.map-card .card-meta { + font-size: 11px; + gap: 8px; + padding-top: 14px; +} +.catalog-count { + margin: 0 0 20px; + font-size: 13px; + font-variant-numeric: tabular-nums; +} +.catalog-empty { + padding: 36px 20px; + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} +.catalog-empty p { + margin: 0; +} +.catalog-controls .tabs button { + min-height: 44px; +} +.map-back-link { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--muted); + margin-bottom: 28px; + min-height: 32px; +} +.map-back-link:hover { + color: var(--green); +} +.map-detail-page .map-detail-title { + font-family: MineGeoPixelCyrillic, monospace; + font-size: clamp(24px, 3vw, 34px); + line-height: 1.5; + max-width: 30ch; + text-wrap: pretty; +} +.map-detail-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; + margin: 16px 0; +} +.map-detail-actions .map-export { + margin: 0; +} +.map-detail-id { + font-size: 12px; + overflow-wrap: anywhere; +} +.detail-grid > * { + min-width: 0; +} +.start-panel .difficulty-options button { + min-width: 0; + overflow-wrap: anywhere; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; +} +.detail-grid .card-meta span:last-child { + display: flex; +} +.stat-cube.void-cube { + background: #9e6bc4; + border-color: #cca6ef; +} +.mode-row:has(.void-cube) { + border-color: #9163b080; +} +.world-stats strong, +.card-meta { + font-variant-numeric: tabular-nums; +} +.loading-surface { + background: var(--panel2); + border: 1px solid var(--line); + border-radius: var(--radius-control); +} +.loading-title { + width: min(440px, 75%); + height: 48px; + margin: 24px 0 36px; +} +.loading-card { + height: 420px; +} +.route-loading { + min-height: 85dvh; +} +.leaderboard-preview-placeholder { + min-height: 420px; +} +.leaderboard-preview-placeholder .loading-surface { + height: 240px; +} +@media (max-width: 960px) { + .map-detail-page .detail-grid { + grid-template-columns: minmax(0, 1fr); + } + .detail-grid > div:first-child { + width: 100%; + max-width: 520px; + margin-inline: auto; + } + .start-panel .difficulty-options { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +@media (min-width: 701px) and (max-width: 960px) { + .home-page .hero { + grid-template-columns: minmax(0, 1fr); + gap: 32px; + } + .hero-copy { + max-width: 620px; + } + .home-page .hero h1 { + font-size: clamp(40px, 5.8vw, 54px); + line-height: 1.12; + text-wrap: balance; + } + .hero-art { + min-height: 350px; + width: 100%; + max-width: 440px; + margin-inline: auto; + } + .hero-art .compass-art { + right: calc(50% - 150px); + top: 0; + } +} +@media (max-width: 600px) { + .section.page-content { + padding: 32px 20px 48px; + } + .catalog-search { + width: 100%; + } + .catalog-search .search-field { + min-height: 46px; + padding-inline: 12px; + } + .catalog-search input { + font-size: 16px; + } + .catalog-controls { + gap: 16px; + } + .catalog-controls .tabs { + gap: 4px; + } + .catalog-controls .tabs button { + padding-inline: 10px; + } + .map-card .card-body { + padding: 18px; + } + .map-detail-page .map-detail-title { + font-size: 22px; + } + .map-back-link { + margin-bottom: 20px; + } + .detail-grid { + gap: 28px; + margin-top: 28px; + } + .detail-grid .start-panel { + padding: 20px; + } + .start-panel .difficulty-options { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .start-panel .difficulty-options span { + font-size: 12px; + } + .hero-actions { + align-items: stretch; + } + .hero-actions > .button { + width: 100%; + } +} .moderator-setting-row { display: flex; gap: 24px; diff --git a/app/servers/[id]/loading.tsx b/app/servers/[id]/loading.tsx new file mode 100644 index 0000000..183d38a --- /dev/null +++ b/app/servers/[id]/loading.tsx @@ -0,0 +1 @@ +export { PageLoading as default } from "@/components/PageLoading"; diff --git a/app/servers/[id]/page.tsx b/app/servers/[id]/page.tsx index 6af96f6..996f890 100644 --- a/app/servers/[id]/page.tsx +++ b/app/servers/[id]/page.tsx @@ -7,10 +7,11 @@ import { StartGame } from "@/components/StartGame"; import { getMap } from "@/lib/catalog"; import { ContributionInstructions } from "@/components/ContributionInstructions"; import { ReportContent } from "@/components/ReportContent"; -import { user } from "@/lib/supabase"; -import { accountProfile } from "@/lib/profile"; +import { user, admin, required } from "@/lib/supabase"; import { ExportMap } from "@/components/MapTransfer"; import { MapCommunity } from "@/components/MapCommunity"; +import Link from "next/link"; +import { ArrowLeft } from "lucide-react"; export default async function ServerPage({ params, }: { @@ -18,29 +19,42 @@ export default async function ServerPage({ }) { const { t } = await getI18n(); const { id } = await params; - const map = await getMap(id); + const [map, account] = await Promise.all([getMap(id), user()]); if (!map) notFound(); - const account = await user(); - const profile = account ? await accountProfile(account) : null; + const profileResult = account + ? await admin() + .from("profiles") + .select("display_name") + .eq("id", account.id) + .maybeSingle() + : null; + if (profileResult?.error) required(profileResult); + const profile = profileResult?.data; return ( <>
-
+
+ +
- +
{map.allow_contributions && ( -

+

{t("Worlds to explore: {count}", { count: shown.length })}

@@ -63,7 +63,20 @@ export function CatalogBrowser({ ))}
{!shown.length && ( -

{t("No worlds match that search.")}

+
+
)} ); diff --git a/components/MapCard.tsx b/components/MapCard.tsx index 92efd55..d5cf823 100644 --- a/components/MapCard.tsx +++ b/components/MapCard.tsx @@ -21,10 +21,12 @@ export function MapCard({ map, index = 0, minecraftPaper = false, + loading, }: { map: CardMap; index?: number; minecraftPaper?: boolean; + loading?: "eager" | "lazy"; }) { const { t } = useI18n(); return ( @@ -35,7 +37,7 @@ export function MapCard({ src={map.image || "/images/example-map.webp"} alt={t("Terrain of {name}", { name: map.name })} fill - loading={index < 3 ? "eager" : "lazy"} + loading={loading ?? (index < 3 ? "eager" : "lazy")} unoptimized sizes="(max-width: 640px) 100vw, 33vw" /> @@ -48,8 +50,8 @@ export function MapCard({
{map.server?.name || t("Overworld")}

- {map.name} - + {map.name} +

{map.description}

{map.credit && ( diff --git a/components/PageLoading.tsx b/components/PageLoading.tsx new file mode 100644 index 0000000..def7509 --- /dev/null +++ b/components/PageLoading.tsx @@ -0,0 +1,22 @@ +import { getI18n } from "@/lib/i18n"; + +export async function PageLoading() { + const { t } = await getI18n(); + return ( +
+

+ {t("Loading page")}… +

+
+ ); +} diff --git a/components/Shell.tsx b/components/Shell.tsx index 0b1a512..2fd4631 100644 --- a/components/Shell.tsx +++ b/components/Shell.tsx @@ -1,7 +1,7 @@ import { getI18n } from "@/lib/i18n"; import "server-only"; import Link from "next/link"; -import { Compass, ArrowUpRight } from "lucide-react"; +import { Compass, ArrowUpRight, UserRound } from "lucide-react"; import { LanguageSwitcher } from "./LanguageSwitcher"; import { rawUser, admin, required } from "@/lib/supabase"; import { canReviewMaps, isAdministrator } from "@/shared/permissions"; @@ -25,8 +25,19 @@ export async function Header() { - {account ? t("My account") : t("Log in")} +
diff --git a/components/SiteNav.tsx b/components/SiteNav.tsx index e35ce26..8740876 100644 --- a/components/SiteNav.tsx +++ b/components/SiteNav.tsx @@ -3,6 +3,8 @@ import { useI18n } from "@/components/I18nProvider"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; +import { Menu, X } from "lucide-react"; const links = [ ["/servers", "Play & maps"], @@ -17,26 +19,84 @@ const links = [ export function SiteNav({ forumUrl }: { forumUrl?: string | null }) { const { t } = useI18n(); const pathname = usePathname(); + const [openPath, setOpenPath] = useState(null); + const open = openPath === pathname; + const toggle = useRef(null); + const navigation = useRef(null); + useEffect(() => { + if (!open) return; + function dismiss(event: PointerEvent) { + if ( + event.target instanceof Node && + !navigation.current?.contains(event.target) && + !toggle.current?.contains(event.target) + ) + setOpenPath(null); + } + function escape(event: KeyboardEvent) { + if (event.key === "Escape") { + setOpenPath(null); + toggle.current?.focus(); + } + } + document.addEventListener("pointerdown", dismiss); + document.addEventListener("keydown", escape); + return () => { + document.removeEventListener("pointerdown", dismiss); + document.removeEventListener("keydown", escape); + }; + }, [open]); return ( - + ); } diff --git a/components/SiteSettingsProvider.tsx b/components/SiteSettingsProvider.tsx index 8b4f369..77a7083 100644 --- a/components/SiteSettingsProvider.tsx +++ b/components/SiteSettingsProvider.tsx @@ -32,6 +32,7 @@ export function SiteSettingsProvider({ const version = useRef(0); const pending = useRef(false); const pathname = usePathname(); + const previousPathname = useRef(pathname); const updateSettings = useCallback((next: SiteSettings) => { version.current++; setSettings(next); @@ -51,6 +52,9 @@ export function SiteSettingsProvider({ } }, []); useEffect(() => { + // SSR already supplied fresh settings for the initial page. + if (previousPathname.current === pathname) return; + previousPathname.current = pathname; void refresh(); }, [pathname, refresh]); useEffect(() => { diff --git a/lib/catalog.ts b/lib/catalog.ts index 3d30200..a4bbc3f 100644 --- a/lib/catalog.ts +++ b/lib/catalog.ts @@ -49,45 +49,50 @@ export async function mediaUrls(paths: (string | null)[], expires = 3600) { } export async function catalog() { const client = admin(); - const maps = required( - await client + const [mapResult, serverResult] = await Promise.all([ + client .from("datasets") .select("*") .eq("state", "published") .order("created_at", { ascending: false }), - ) as unknown as MapListing[]; - const servers = required( - await client.from("catalog_servers").select("*"), - ) as { id: string; name: string; address: string | null }[]; - const owners = maps.length - ? required( - await client + client.from("catalog_servers").select("*"), + ]); + const maps = required(mapResult) as unknown as MapListing[]; + const servers = required(serverResult) as { + id: string; + name: string; + address: string | null; + }[]; + const ownerIds = [ + ...new Set(maps.flatMap((map) => (map.owner_id ? [map.owner_id] : []))), + ]; + const [ownersResult, signed] = await Promise.all([ + ownerIds.length + ? client .from("profiles") .select( "id,display_name,avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon", ) - .in("id", [ - ...new Set( - maps - .map((m) => m.owner_id) - .filter((id): id is string => id !== null), - ), - ]), - ) - : []; - const signed = await mediaUrls( - maps.flatMap((map) => [map.map_path || map.cover_path, map.map_path]), + .in("id", ownerIds) + : Promise.resolve({ data: [], error: null }), + mediaUrls( + maps.flatMap((map) => [map.map_path || map.cover_path, map.map_path]), + ), + ]); + const owners = new Map( + required(ownersResult).map((owner) => [owner.id, owner]), ); + const serverById = new Map(servers.map((server) => [server.id, server])); return maps.map((m, index) => ({ ...m, credit: - owners.find((p) => p.id === m.owner_id)?.display_name ?? + owners.get(m.owner_id ?? "")?.display_name ?? (m.owner_id ? "Explorer" : "Deleted player"), - creditGlow: owners.find((p) => p.id === m.owner_id)?.name_glow ?? false, - creditAppearance: owners.find((p) => p.id === m.owner_id), + creditGlow: owners.get(m.owner_id ?? "")?.name_glow ?? false, + creditAppearance: owners.get(m.owner_id ?? ""), image: signed[index * 2] ?? undefined, mapImage: signed[index * 2 + 1] ?? undefined, - server: servers.find((s) => s.id === m.server_id), + server: serverById.get(m.server_id), })); } export async function getMap(id: string) { diff --git a/proxy.ts b/proxy.ts index 281b9b8..861afae 100644 --- a/proxy.ts +++ b/proxy.ts @@ -64,6 +64,6 @@ export async function proxy(request: NextRequest) { } export const config = { matcher: [ - "/((?!_next/static|_next/image|favicon.ico|\\.well-known/workflow/|tools/|desktop/|fonts/|images/|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|ttf|woff|woff2|jar)$).*)", + "/((?!_next/static|_next/image|favicon.ico|\\.well-known/workflow/|tools/|desktop/|fonts/|images/|media/|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|ttf|woff|woff2|jar)$).*)", ], }; diff --git a/shared/locales/ru.json b/shared/locales/ru.json index 84bfcee..b1182b5 100644 --- a/shared/locales/ru.json +++ b/shared/locales/ru.json @@ -1,5 +1,12 @@ { "MineGeo": "MineGeo", + "Open navigation": "Открыть меню", + "Close navigation": "Закрыть меню", + "Find your challenge.": "Найди свой вызов.", + "10 seconds": "10 секунд", + "The ultimate test. Fixed view, strict scoring.": "Главное испытание. Фиксированный вид, строгий подсчёт.", + "Clear filters": "Сбросить фильтры", + "Loading page": "Загрузка страницы", "Find your place in a blocky world.": "Найди своё место в мире блоков.", "Skip intro": "Пропустить вступление", "THE EXPEDITION AWAITS": "ЭКСПЕДИЦИЯ ЖДЁТ",