diff --git a/apps/homepage/src/components/app-shell/SiteFooter.module.css b/apps/homepage/src/components/app-shell/SiteFooter.module.css index 1ebad0a..94aa903 100644 --- a/apps/homepage/src/components/app-shell/SiteFooter.module.css +++ b/apps/homepage/src/components/app-shell/SiteFooter.module.css @@ -27,6 +27,12 @@ font-weight: 800; } +.brandLogo { + flex-shrink: 0; + border-radius: 0.5rem; + object-fit: contain; +} + .mark { display: inline-grid; min-inline-size: var(--size-touch-min); diff --git a/apps/homepage/src/components/app-shell/SiteFooter.tsx b/apps/homepage/src/components/app-shell/SiteFooter.tsx index 3397a37..cd148c6 100644 --- a/apps/homepage/src/components/app-shell/SiteFooter.tsx +++ b/apps/homepage/src/components/app-shell/SiteFooter.tsx @@ -61,7 +61,7 @@ export function SiteFooter({ locale, messages }: SiteFooterProps) { width={40} height={40} unoptimized - style={{ borderRadius: '0.5rem', objectFit: 'contain', flexShrink: 0 }} + className={styles.brandLogo} /> RentalDriveGo diff --git a/apps/homepage/src/components/auth/AuthForms.module.css b/apps/homepage/src/components/auth/AuthForms.module.css new file mode 100644 index 0000000..a28aa50 --- /dev/null +++ b/apps/homepage/src/components/auth/AuthForms.module.css @@ -0,0 +1,142 @@ +.center { + text-align: center; +} + +.formStack { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.successIcon { + display: flex; + align-items: center; + justify-content: center; + inline-size: 3.5rem; + block-size: 3.5rem; + margin: 0 auto 1rem; + border-radius: 50%; + background: var(--color-success-bg, #ecfdf5); +} + +.successIconSvg { + inline-size: 1.75rem; + block-size: 1.75rem; + color: var(--color-success-text, #059669); +} + +.statusTitle { + color: var(--color-heading); + font-size: 1.25rem; + font-weight: 700; +} + +.statusBody { + margin-block-start: 0.5rem; + color: var(--color-text-secondary); + font-size: 0.875rem; +} + +.error { + border: 1px solid var(--color-error-border, #fecaca); + border-radius: 1.5rem; + padding: 0.75rem 1rem; + background: var(--color-error-bg, #fef2f2); + color: var(--color-error-text, #b91c1c); + font-size: 0.875rem; +} + +.spacedError { + margin-block-end: 1.25rem; +} + +.footer { + margin-block-start: 1.5rem; + border-block-start: 1px solid var(--color-border); + padding-block-start: 1.5rem; + font-size: 0.875rem; + text-align: center; +} + +.footerLink { + color: var(--color-heading); + font-weight: 600; + text-decoration: underline; + text-underline-offset: 4px; +} + +.mutedLink { + color: var(--color-text-tertiary); + font-size: 0.875rem; + text-decoration: underline; + text-underline-offset: 4px; +} + +.passwordField { + position: relative; + inline-size: 100%; +} + +.passwordInput { + inline-size: 100%; + min-block-size: 2.875rem; + border: 1px solid var(--border-strong); + border-radius: var(--radius-sm); + padding: var(--space-3) 2.5rem var(--space-3) var(--space-4); + background: var(--surface-primary); + color: var(--text-primary); + font-size: 0.875rem; +} + +.passwordToggle { + position: absolute; + inset-block: 0; + inset-inline-end: 0.5rem; + display: flex; + align-items: center; + border: 0; + padding: 0; + background: none; + color: var(--text-tertiary); + cursor: pointer; +} + +.forgotLinkWrap { + text-align: center; +} + +.infoBox { + border: 1px solid var(--color-border); + border-radius: 1.5rem; + padding: 1rem; + background: var(--color-surface-secondary); + color: var(--color-text-secondary); + font-size: 0.875rem; + text-align: center; +} + +.totpInput { + font-size: 1.25rem; + letter-spacing: 0.45em; + text-align: center; +} + +.plainButton { + inline-size: 100%; + border: 0; + background: none; + color: var(--color-text-tertiary); + cursor: pointer; + font-size: 0.875rem; +} + +.noToken { + color: var(--color-text-secondary); + font-size: 0.875rem; + text-align: center; +} + +.actionWrap { + margin-block-start: 1rem; + text-align: center; +} diff --git a/apps/homepage/src/components/auth/AuthShell.module.css b/apps/homepage/src/components/auth/AuthShell.module.css new file mode 100644 index 0000000..80504f9 --- /dev/null +++ b/apps/homepage/src/components/auth/AuthShell.module.css @@ -0,0 +1,51 @@ +.main { + display: flex; + flex: 1; + align-items: center; + justify-content: center; + min-block-size: calc(100vh - 80px); + padding: 2rem 1rem; +} + +.container { + inline-size: 100%; + max-inline-size: 28rem; +} + +.header { + display: flex; + flex-direction: column; + align-items: center; + margin-block-end: 2rem; +} + +.logo { + display: block; + border: 1px solid var(--color-border); + border-radius: 1.5rem; + padding: 0.375rem; + background: var(--color-surface); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +.title { + margin-block-start: 0.5rem; + color: var(--color-heading); + font-size: 1.75rem; + font-weight: 800; + letter-spacing: -0.03em; +} + +.subtitle { + margin-block-start: 0.5rem; + color: var(--color-text-secondary); + font-size: 0.875rem; +} + +.panel { + border: 1px solid var(--color-border); + border-radius: 2rem; + padding: 2rem; + background: var(--color-surface); + box-shadow: 0 30px 80px rgba(28, 25, 23, 0.06); +} diff --git a/apps/homepage/src/components/auth/AuthShell.tsx b/apps/homepage/src/components/auth/AuthShell.tsx index d298cd2..b527b5b 100644 --- a/apps/homepage/src/components/auth/AuthShell.tsx +++ b/apps/homepage/src/components/auth/AuthShell.tsx @@ -3,6 +3,7 @@ import { localizedPath, type Locale } from '@/lib/localization/config' import Image from 'next/image' import type { ReactNode } from 'react' +import styles from './AuthShell.module.css' interface AuthShellProps { locale: Locale @@ -16,17 +17,10 @@ export function AuthShell({ locale, title, subtitle, children }: AuthShellProps)
-
-
+
+
-

+

{title}

{subtitle ? ( -

+

{subtitle}

) : null}
-
+
{children}
diff --git a/apps/homepage/src/components/auth/ForgotPasswordForm.tsx b/apps/homepage/src/components/auth/ForgotPasswordForm.tsx index e61ab51..b0c6c87 100644 --- a/apps/homepage/src/components/auth/ForgotPasswordForm.tsx +++ b/apps/homepage/src/components/auth/ForgotPasswordForm.tsx @@ -7,6 +7,7 @@ import { AuthShell } from '@/components/auth/AuthShell' import { API_BASE } from '@/lib/api' import { localizedPath, type Locale } from '@/lib/localization/config' import { useState } from 'react' +import styles from './AuthForms.module.css' interface Dict { title: string @@ -98,21 +99,10 @@ export function ForgotPasswordForm({ locale }: { locale: Locale }) { return ( {sent ? ( -
-
+
+
-

+

{dict.successTitle}

-

+

{dict.successBody}

) : ( -
+ {error ? ( -
+
{error}
) : null} @@ -163,24 +144,8 @@ export function ForgotPasswordForm({ locale }: { locale: Locale }) { )} -
- + diff --git a/apps/homepage/src/components/auth/ResetPasswordForm.tsx b/apps/homepage/src/components/auth/ResetPasswordForm.tsx index 1d23cf7..772ddbd 100644 --- a/apps/homepage/src/components/auth/ResetPasswordForm.tsx +++ b/apps/homepage/src/components/auth/ResetPasswordForm.tsx @@ -7,6 +7,7 @@ import { API_BASE } from '@/lib/api'; import { localizedPath, type Locale } from '@/lib/localization/config'; import { useSearchParams } from 'next/navigation'; import { Suspense, useState } from 'react'; +import styles from './AuthForms.module.css'; interface Dict { title: string; @@ -144,16 +145,10 @@ function ResetPasswordContent({ locale }: { locale: Locale }) { if (!token) { return ( -

+

{dict.noToken}

-
+
@@ -165,25 +160,10 @@ function ResetPasswordContent({ locale }: { locale: Locale }) { if (done) { return ( -
-
+
+
-

+

{dict.successTitle}

-

+

{dict.successBody}

-
+
-
- + diff --git a/apps/homepage/src/components/auth/SignInForm.tsx b/apps/homepage/src/components/auth/SignInForm.tsx index e34e47c..b560d55 100644 --- a/apps/homepage/src/components/auth/SignInForm.tsx +++ b/apps/homepage/src/components/auth/SignInForm.tsx @@ -5,9 +5,11 @@ import { TextInput } from '@/components/forms/TextInput'; import { FormField } from '@/components/forms/FormField'; import { AuthShell } from '@/components/auth/AuthShell'; import { API_BASE, EMPLOYEE_PROFILE_KEY } from '@/lib/api'; +import { classNames } from '@/lib/components/classNames'; import { localizedPath, type Locale } from '@/lib/localization/config'; import { useSearchParams } from 'next/navigation'; import { useState } from 'react'; +import styles from './AuthForms.module.css'; interface Dict { title: string; @@ -240,15 +242,7 @@ export function SignInForm({ locale }: { locale: Locale }) { {error ? (
{error}
@@ -257,7 +251,7 @@ export function SignInForm({ locale }: { locale: Locale }) { {step === 'credentials' ? (
-
+
setPassword(e.target.value)} placeholder="••••••••" autoComplete="current-password" - style={{ - width: '100%', - minHeight: '2.875rem', - padding: 'var(--space-3) 2.5rem var(--space-3) var(--space-4)', - border: '1px solid var(--border-strong)', - borderRadius: 'var(--radius-sm)', - background: 'var(--surface-primary)', - color: 'var(--text-primary)', - fontSize: '0.875rem', - }} + className={styles.passwordInput} /> -
+
{dict.forgotPassword} @@ -345,19 +313,9 @@ export function SignInForm({ locale }: { locale: Locale }) { ) : ( -
+
{dict.enterCode}
@@ -373,7 +331,7 @@ export function SignInForm({ locale }: { locale: Locale }) { setTotpCode(e.target.value.replace(/[^0-9A-Za-z-]/g, '').toUpperCase()) } placeholder={dict.totpPlaceholder} - style={{ textAlign: 'center', fontSize: '1.25rem', letterSpacing: '0.45em' }} + className={styles.totpInput} /> @@ -388,14 +346,7 @@ export function SignInForm({ locale }: { locale: Locale }) { setTotpCode(''); setError(null); }} - style={{ - width: '100%', - fontSize: '0.875rem', - color: 'var(--color-text-tertiary)', - background: 'none', - border: 'none', - cursor: 'pointer', - }} + className={styles.plainButton} > {dict.back} diff --git a/apps/homepage/src/components/feedback/Loading.module.css b/apps/homepage/src/components/feedback/Loading.module.css index 53c588c..74c10a8 100644 --- a/apps/homepage/src/components/feedback/Loading.module.css +++ b/apps/homepage/src/components/feedback/Loading.module.css @@ -11,6 +11,7 @@ gap: var(--space-3); } .skeleton span { + inline-size: 100%; block-size: 0.9rem; border-radius: var(--radius-pill); background: linear-gradient( @@ -22,6 +23,9 @@ background-size: 200% 100%; animation: shimmer 1.5s linear infinite; } +.skeleton span:last-child { + inline-size: 68%; +} @keyframes spin { to { transform: rotate(360deg); diff --git a/apps/homepage/src/components/feedback/Loading.tsx b/apps/homepage/src/components/feedback/Loading.tsx index 3c96592..b77de97 100644 --- a/apps/homepage/src/components/feedback/Loading.tsx +++ b/apps/homepage/src/components/feedback/Loading.tsx @@ -31,7 +31,7 @@ export function Skeleton({ {...props} > {Array.from({ length: lines }, (_, index) => ( - + ))}
); diff --git a/production/scripts/docker-prod-common.sh b/production/scripts/docker-prod-common.sh index 7cc9bb6..1066c57 100755 --- a/production/scripts/docker-prod-common.sh +++ b/production/scripts/docker-prod-common.sh @@ -101,7 +101,7 @@ validate_prod_env_file() { redis_password="$(read_env_value REDIS_PASSWORD)" jwt_secret="$(read_env_value JWT_SECRET)" - if [[ "${acme_email}" != "${DEFAULT_ACME_EMAIL}" ]]; then + if [[ -z "${acme_email}" || "${acme_email}" != *@* || "${acme_email}" == *example.com* ]]; then echo "Invalid ACME_EMAIL in ${ENV_FILE}. Set ACME_EMAIL=${DEFAULT_ACME_EMAIL} for Let's Encrypt." >&2 exit 1 fi