Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/apps
diff options
context:
space:
mode:
authorLeitwolf112026-09-09 19:41:09 +0300
committerLeitwolf112026-09-09 19:41:09 +0300
commitfa3b1f249f390ecd2248e1c17e70ede1364ff034 (patch)
tree53fa892d62bba3b2e272a23d73f43ba34bcdcf9a /apps
parentb7f2dceebdee7e9d13644a1f80c661cfcc265bd2 (diff)
downloadprogmaweb-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.
Diffstat (limited to 'apps')
-rw-r--r--apps/web/app/[accountName]/dashboard/page.tsx12
-rw-r--r--apps/web/app/account/page.tsx13
-rw-r--r--apps/web/app/globals.css25
-rw-r--r--apps/web/app/layout.tsx16
-rw-r--r--apps/web/app/login/page.tsx17
-rw-r--r--apps/web/app/page.tsx13
-rw-r--r--apps/web/app/recover/page.tsx15
-rw-r--r--apps/web/app/register/page.tsx27
-rw-r--r--apps/web/components/Brand.tsx8
-rw-r--r--apps/web/components/DashboardClient.tsx24
-rw-r--r--apps/web/components/SiteHeader.tsx2
-rw-r--r--apps/web/lib/localization.ts42
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…",