diff options
| author | Leitwolf11 | 2026-09-09 19:41:09 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-09-09 19:41:09 +0300 |
| commit | fa3b1f249f390ecd2248e1c17e70ede1364ff034 (patch) | |
| tree | 53fa892d62bba3b2e272a23d73f43ba34bcdcf9a | |
| parent | b7f2dceebdee7e9d13644a1f80c661cfcc265bd2 (diff) | |
| download | progmaweb-fa3b1f249f390ecd2248e1c17e70ede1364ff034.tar.gz | |
Extend Progmaweb dark-theme and German localization coverage across account, authentication, recovery, and dashboard routes; localize page metadata and OAuth feedback; expose persistent preference controls in dashboard states; and apply saved themes before first paint.
| -rw-r--r-- | apps/web/app/[accountName]/dashboard/page.tsx | 12 | ||||
| -rw-r--r-- | apps/web/app/account/page.tsx | 13 | ||||
| -rw-r--r-- | apps/web/app/globals.css | 25 | ||||
| -rw-r--r-- | apps/web/app/layout.tsx | 16 | ||||
| -rw-r--r-- | apps/web/app/login/page.tsx | 17 | ||||
| -rw-r--r-- | apps/web/app/page.tsx | 13 | ||||
| -rw-r--r-- | apps/web/app/recover/page.tsx | 15 | ||||
| -rw-r--r-- | apps/web/app/register/page.tsx | 27 | ||||
| -rw-r--r-- | apps/web/components/Brand.tsx | 8 | ||||
| -rw-r--r-- | apps/web/components/DashboardClient.tsx | 24 | ||||
| -rw-r--r-- | apps/web/components/SiteHeader.tsx | 2 | ||||
| -rw-r--r-- | apps/web/lib/localization.ts | 42 |
12 files changed, 165 insertions, 49 deletions
diff --git a/apps/web/app/[accountName]/dashboard/page.tsx b/apps/web/app/[accountName]/dashboard/page.tsx index 32dc6e7..0d9ed7d 100644 --- a/apps/web/app/[accountName]/dashboard/page.tsx +++ b/apps/web/app/[accountName]/dashboard/page.tsx @@ -4,11 +4,15 @@ import type { Metadata } from "next"; import { DashboardClient } from "@/components/DashboardClient"; import { getLocale } from "@/lib/locale.server"; +import { getMessages } from "@/lib/localization"; -export const metadata: Metadata = { - title: "Dashboard", - robots: { index: false, follow: false }, -}; +export async function generateMetadata(): Promise<Metadata> { + const { metadata } = getMessages(await getLocale()); + return { + title: metadata.dashboardTitle, + robots: { index: false, follow: false }, + }; +} interface DashboardPageProps { params: Promise<{ accountName: string }>; diff --git a/apps/web/app/account/page.tsx b/apps/web/app/account/page.tsx index 0776ee4..2d36236 100644 --- a/apps/web/app/account/page.tsx +++ b/apps/web/app/account/page.tsx @@ -8,11 +8,14 @@ import { SiteHeader } from "@/components/SiteHeader"; import { getLocale } from "@/lib/locale.server"; import { getMessages } from "@/lib/localization"; -export const metadata: Metadata = { - title: "Account", - description: "Access and manage your Progmasoft account.", - robots: { index: false, follow: false }, -}; +export async function generateMetadata(): Promise<Metadata> { + const { metadata } = getMessages(await getLocale()); + return { + title: metadata.accountTitle, + description: metadata.accountDescription, + robots: { index: false, follow: false }, + }; +} export default async function AccountHomepage() { const locale = await getLocale(); diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 342974b..936a53b 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -1038,6 +1038,17 @@ a { text-decoration: none; } +.dashboard-sidebar-preferences { + width: 100%; + margin-top: 24px; + padding-top: 20px; + border-top: 1px solid var(--line); +} + +.dashboard-sidebar-preferences .preference-controls { + padding-left: 0; +} + .text-button { margin-top: auto; border: 0; @@ -1173,8 +1184,8 @@ a { color: #004e8c; } -.dashboard-loading, -.dashboard-error { +.dashboard-state { + position: relative; min-height: 100vh; display: grid; place-items: center; @@ -1183,6 +1194,16 @@ a { color: var(--ink-650); } +.dashboard-state > p { + margin: 0; +} + +.dashboard-state-preferences { + position: absolute; + top: 24px; + right: 24px; +} + .dashboard-error { color: var(--danger); } diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 8eb99e4..82159e8 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -6,6 +6,19 @@ import type { ReactNode } from "react"; import { getLocale } from "@/lib/locale.server"; import "./globals.css"; +// Apply the stored theme before the body is painted. PreferenceControls keeps +// this value synchronized after hydration, while this small bootstrap prevents +// a light flash on every route—including dashboard loading and error states. +const themeBootstrap = ` + (() => { + try { + const theme = localStorage.getItem("progmasoft_theme") === "dark" ? "dark" : "light"; + document.documentElement.dataset.theme = theme; + document.documentElement.style.colorScheme = theme; + } catch {} + })(); +`; + export const metadata: Metadata = { // The apex host is the only public organization canonical. The www host is a // transport-level redirect and account pages opt out of indexing below. @@ -29,6 +42,9 @@ export default async function RootLayout({ const locale = await getLocale(); return ( <html lang={locale} data-theme="light" suppressHydrationWarning> + <head> + <script dangerouslySetInnerHTML={{ __html: themeBootstrap }} /> + </head> <body>{children}</body> </html> ); diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index 87748b0..6e57963 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -7,10 +7,13 @@ import { AuthForm } from "@/components/AuthForm"; import { getLocale } from "@/lib/locale.server"; import { getMessages } from "@/lib/localization"; -export const metadata: Metadata = { - title: "Sign in", - robots: { index: false, follow: false }, -}; +export async function generateMetadata(): Promise<Metadata> { + const { metadata } = getMessages(await getLocale()); + return { + title: metadata.loginTitle, + robots: { index: false, follow: false }, + }; +} interface LoginPageProps { searchParams: Promise<{ google?: string }>; @@ -19,11 +22,9 @@ interface LoginPageProps { export default async function LoginPage({ searchParams }: LoginPageProps) { const { google } = await searchParams; const locale = await getLocale(); - const { account } = getMessages(locale); + const { account, auth } = getMessages(locale); const initialMessage = - google === "failed" - ? "Google sign-in could not be completed. Try again." - : undefined; + google === "failed" ? auth.googleSignInFailed : undefined; return ( <AccountShell diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 158f1b1..345c835 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -8,10 +8,15 @@ import { SiteHeader } from "@/components/SiteHeader"; import { getLocale } from "@/lib/locale.server"; import { getMessages } from "@/lib/localization"; -export const metadata: Metadata = { - alternates: { canonical: "https://progmasoft.com/" }, - robots: { index: true, follow: true }, -}; +export async function generateMetadata(): Promise<Metadata> { + const { metadata } = getMessages(await getLocale()); + return { + title: metadata.homeTitle, + description: metadata.homeDescription, + alternates: { canonical: "https://progmasoft.com/" }, + robots: { index: true, follow: true }, + }; +} const productLinks = [ { diff --git a/apps/web/app/recover/page.tsx b/apps/web/app/recover/page.tsx index c01d9c5..ce31c1b 100644 --- a/apps/web/app/recover/page.tsx +++ b/apps/web/app/recover/page.tsx @@ -8,11 +8,14 @@ import { AccountShell } from "@/components/AccountShell"; import { getLocale } from "@/lib/locale.server"; import { getMessages } from "@/lib/localization"; -export const metadata: Metadata = { - title: "Recover your account", - description: "Get help regaining access to a Progmasoft account.", - robots: { index: false, follow: false }, -}; +export async function generateMetadata(): Promise<Metadata> { + const { metadata } = getMessages(await getLocale()); + return { + title: metadata.recoveryTitle, + description: metadata.recoveryDescription, + robots: { index: false, follow: false }, + }; +} export default async function RecoverPage() { const locale = await getLocale(); @@ -35,7 +38,7 @@ export default async function RecoverPage() { </div> <a className="button auth-submit" - href="mailto:support@progmasoft.com?subject=Progmasoft%20account%20recovery" + href={`mailto:support@progmasoft.com?subject=${encodeURIComponent(account.recoveryEmailSubject)}`} > {account.contactSupport} </a> diff --git a/apps/web/app/register/page.tsx b/apps/web/app/register/page.tsx index 796f163..fa774bd 100644 --- a/apps/web/app/register/page.tsx +++ b/apps/web/app/register/page.tsx @@ -7,30 +7,29 @@ import { AuthForm } from "@/components/AuthForm"; import { getLocale } from "@/lib/locale.server"; import { getMessages } from "@/lib/localization"; -export const metadata: Metadata = { - title: "Create account", - robots: { index: false, follow: false }, -}; +export async function generateMetadata(): Promise<Metadata> { + const { metadata } = getMessages(await getLocale()); + return { + title: metadata.registerTitle, + robots: { index: false, follow: false }, + }; +} interface RegisterPageProps { searchParams: Promise<{ google?: string }>; } -const googleMessages: Record<string, string> = { - "account-name-required": - "Choose an Account name before continuing with Google.", - "account-name-unavailable": - "That Account name is unavailable. Choose another name.", - "invalid-account-name": - "Use a valid Account name before continuing with Google.", -}; - export default async function RegisterPage({ searchParams, }: RegisterPageProps) { const { google } = await searchParams; const locale = await getLocale(); - const { account } = getMessages(locale); + const { account, auth } = getMessages(locale); + const googleMessages: Record<string, string> = { + "account-name-required": auth.googleAccountNameRequired, + "account-name-unavailable": auth.googleAccountNameUnavailable, + "invalid-account-name": auth.googleInvalidAccountName, + }; const initialMessage = google ? googleMessages[google] : undefined; return ( diff --git a/apps/web/components/Brand.tsx b/apps/web/components/Brand.tsx index 756162b..925ef61 100644 --- a/apps/web/components/Brand.tsx +++ b/apps/web/components/Brand.tsx @@ -5,9 +5,13 @@ import Link from "next/link"; interface BrandProps { account?: boolean; + accountLabel?: string; } -export function Brand({ account = false }: BrandProps) { +export function Brand({ + account = false, + accountLabel = "Account", +}: BrandProps) { return ( <Link className="brand" @@ -22,7 +26,7 @@ export function Brand({ account = false }: BrandProps) { <span className="brand-tile brand-tile-orange" /> </span> <strong className="brand-name">Progmasoft</strong> - {account && <span className="brand-product">Account</span>} + {account && <span className="brand-product">{accountLabel}</span>} </Link> ); } diff --git a/apps/web/components/DashboardClient.tsx b/apps/web/components/DashboardClient.tsx index e190705..3f9c749 100644 --- a/apps/web/components/DashboardClient.tsx +++ b/apps/web/components/DashboardClient.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from "react"; import { getMessages, type Locale } from "@/lib/localization"; +import { PreferenceControls } from "./PreferenceControls"; interface AccountSummary { accountName: string; @@ -26,7 +27,7 @@ export function DashboardClient({ locale, routeAccountName, }: DashboardClientProps) { - const { dashboard } = getMessages(locale); + const { dashboard, navigation } = getMessages(locale); const [state, setState] = useState<LoadState>({ kind: "loading" }); useEffect(() => { @@ -86,11 +87,25 @@ export function DashboardClient({ } if (state.kind === "loading") { - return <div className="dashboard-loading">{dashboard.loading}</div>; + return ( + <div className="dashboard-state dashboard-loading"> + <div className="dashboard-state-preferences"> + <PreferenceControls locale={locale} labels={navigation} /> + </div> + <p>{dashboard.loading}</p> + </div> + ); } if (state.kind === "error") { - return <div className="dashboard-error">{state.message}</div>; + return ( + <div className="dashboard-state dashboard-error"> + <div className="dashboard-state-preferences"> + <PreferenceControls locale={locale} labels={navigation} /> + </div> + <p role="alert">{state.message}</p> + </div> + ); } const { account } = state; @@ -109,6 +124,9 @@ export function DashboardClient({ <a href="#security">{dashboard.security}</a> <a href="#services">{dashboard.services}</a> </nav> + <div className="dashboard-sidebar-preferences"> + <PreferenceControls locale={locale} labels={navigation} /> + </div> <button className="text-button" type="button" onClick={signOut}> {dashboard.signOut} </button> diff --git a/apps/web/components/SiteHeader.tsx b/apps/web/components/SiteHeader.tsx index b42c7c9..7ca6e8e 100644 --- a/apps/web/components/SiteHeader.tsx +++ b/apps/web/components/SiteHeader.tsx @@ -21,7 +21,7 @@ export function SiteHeader({ return ( <header className="site-header"> <div className="shell header-inner"> - <Brand account={account} /> + <Brand account={account} accountLabel={navigation.account} /> <nav className="primary-nav" aria-label={navigation.primary}> {account ? ( <> diff --git a/apps/web/lib/localization.ts b/apps/web/lib/localization.ts index 99bd437..ae4702b 100644 --- a/apps/web/lib/localization.ts +++ b/apps/web/lib/localization.ts @@ -10,6 +10,18 @@ export function isLocale(value: string | undefined): value is Locale { export const messages = { "en-US": { + metadata: { + homeTitle: "Progmasoft", + homeDescription: + "Progmasoft builds programming-language, package-management, and developer-tooling systems.", + accountTitle: "Account", + accountDescription: "Access and manage your Progmasoft account.", + loginTitle: "Sign in", + registerTitle: "Create account", + recoveryTitle: "Recover your account", + recoveryDescription: "Get help regaining access to a Progmasoft account.", + dashboardTitle: "Dashboard", + }, navigation: { primary: "Primary navigation", organization: "Organization", @@ -162,6 +174,7 @@ export const messages = { recoveryPanelTitle: "Regain access safely", recoveryPanelDescription: "Automated password recovery is not available during the initial account-system rollout. Contact Progmasoft support from the email address registered to your account so ownership can be verified.", + recoveryEmailSubject: "Progmasoft account recovery", contactSupport: "Contact support", rememberedPassword: "Remembered your password?", returnToSignIn: "Return to sign in", @@ -198,6 +211,13 @@ export const messages = { missingName: "The server did not return an account name.", unreadableName: "The Account name field could not be read.", requestFailed: "The request could not be completed.", + googleSignInFailed: "Google sign-in could not be completed. Try again.", + googleAccountNameRequired: + "Choose an Account name before continuing with Google.", + googleAccountNameUnavailable: + "That Account name is unavailable. Choose another name.", + googleInvalidAccountName: + "Use a valid Account name before continuing with Google.", }, dashboard: { loading: "Loading account…", @@ -229,6 +249,19 @@ export const messages = { }, }, "de-DE": { + metadata: { + homeTitle: "Progmasoft", + homeDescription: + "Progmasoft entwickelt Systeme für Programmiersprachen, Paketverwaltung und Entwicklerwerkzeuge.", + accountTitle: "Konto", + accountDescription: "Öffnen und verwalten Sie Ihr Progmasoft-Konto.", + loginTitle: "Anmelden", + registerTitle: "Konto erstellen", + recoveryTitle: "Konto wiederherstellen", + recoveryDescription: + "Erhalten Sie Hilfe beim Wiederherstellen des Zugriffs auf ein Progmasoft-Konto.", + dashboardTitle: "Kontoübersicht", + }, navigation: { primary: "Hauptnavigation", organization: "Organisation", @@ -382,6 +415,7 @@ export const messages = { recoveryPanelTitle: "Zugriff sicher wiederherstellen", recoveryPanelDescription: "Die automatische Passwortwiederherstellung ist während der ersten Einführung des Kontosystems nicht verfügbar. Kontaktieren Sie den Progmasoft-Support von der im Konto registrierten E-Mail-Adresse, damit die Inhaberschaft geprüft werden kann.", + recoveryEmailSubject: "Wiederherstellung des Progmasoft-Kontos", contactSupport: "Support kontaktieren", rememberedPassword: "Passwort wieder eingefallen?", returnToSignIn: "Zur Anmeldung", @@ -419,6 +453,14 @@ export const messages = { unreadableName: "Das Feld für den Kontonamen konnte nicht gelesen werden.", requestFailed: "Die Anfrage konnte nicht abgeschlossen werden.", + googleSignInFailed: + "Die Anmeldung mit Google konnte nicht abgeschlossen werden. Versuchen Sie es erneut.", + googleAccountNameRequired: + "Wählen Sie einen Kontonamen, bevor Sie mit Google fortfahren.", + googleAccountNameUnavailable: + "Dieser Kontoname ist nicht verfügbar. Wählen Sie einen anderen Namen.", + googleInvalidAccountName: + "Verwenden Sie einen gültigen Kontonamen, bevor Sie mit Google fortfahren.", }, dashboard: { loading: "Konto wird geladen…", |