Fix the homepage CSP inline-style violations by removing React style attributes from the homepage app and moving them into CSS modules.
Build & Deploy / Build & Push Docker Image (push) Successful in 2m55s
Test / Type Check (all packages) (push) Successful in 59s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Successful in 51s
Test / Homepage Unit Tests (push) Successful in 43s
Test / Storefront Unit Tests (push) Successful in 42s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 44s
Test / API Integration Tests (push) Successful in 1m0s

This commit is contained in:
root
2026-07-01 23:33:37 -04:00
parent 30631504f1
commit bcfe518af2
11 changed files with 256 additions and 265 deletions
@@ -27,6 +27,12 @@
font-weight: 800; font-weight: 800;
} }
.brandLogo {
flex-shrink: 0;
border-radius: 0.5rem;
object-fit: contain;
}
.mark { .mark {
display: inline-grid; display: inline-grid;
min-inline-size: var(--size-touch-min); min-inline-size: var(--size-touch-min);
@@ -61,7 +61,7 @@ export function SiteFooter({ locale, messages }: SiteFooterProps) {
width={40} width={40}
height={40} height={40}
unoptimized unoptimized
style={{ borderRadius: '0.5rem', objectFit: 'contain', flexShrink: 0 }} className={styles.brandLogo}
/> />
<span>RentalDriveGo</span> <span>RentalDriveGo</span>
</div> </div>
@@ -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;
}
@@ -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);
}
@@ -3,6 +3,7 @@
import { localizedPath, type Locale } from '@/lib/localization/config' import { localizedPath, type Locale } from '@/lib/localization/config'
import Image from 'next/image' import Image from 'next/image'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import styles from './AuthShell.module.css'
interface AuthShellProps { interface AuthShellProps {
locale: Locale locale: Locale
@@ -16,17 +17,10 @@ export function AuthShell({ locale, title, subtitle, children }: AuthShellProps)
<main <main
id="main-content" id="main-content"
tabIndex={-1} tabIndex={-1}
style={{ className={styles.main}
display: 'flex',
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: '2rem 1rem',
minHeight: 'calc(100vh - 80px)',
}}
> >
<div style={{ width: '100%', maxWidth: '28rem' }}> <div className={styles.container}>
<div style={{ marginBottom: '2rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}> <div className={styles.header}>
<a href={localizedPath('home', locale)}> <a href={localizedPath('home', locale)}>
<Image <Image
src="/rentaldrivego.png" src="/rentaldrivego.png"
@@ -35,49 +29,20 @@ export function AuthShell({ locale, title, subtitle, children }: AuthShellProps)
height={96} height={96}
priority priority
unoptimized unoptimized
style={{ className={styles.logo}
display: 'block',
borderRadius: '1.5rem',
border: '1px solid var(--color-border)',
background: 'var(--color-surface)',
padding: '0.375rem',
boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
}}
/> />
</a> </a>
<h1 <h1 className={styles.title}>
style={{
marginTop: '0.5rem',
fontSize: '1.75rem',
fontWeight: 800,
letterSpacing: '-0.03em',
color: 'var(--color-heading)',
}}
>
{title} {title}
</h1> </h1>
{subtitle ? ( {subtitle ? (
<p <p className={styles.subtitle}>
style={{
marginTop: '0.5rem',
fontSize: '0.875rem',
color: 'var(--color-text-secondary)',
}}
>
{subtitle} {subtitle}
</p> </p>
) : null} ) : null}
</div> </div>
<section <section className={styles.panel}>
style={{
borderRadius: '2rem',
border: '1px solid var(--color-border)',
background: 'var(--color-surface)',
padding: '2rem',
boxShadow: '0 30px 80px rgba(28,25,23,0.06)',
}}
>
{children} {children}
</section> </section>
</div> </div>
@@ -7,6 +7,7 @@ import { AuthShell } from '@/components/auth/AuthShell'
import { API_BASE } from '@/lib/api' import { API_BASE } from '@/lib/api'
import { localizedPath, type Locale } from '@/lib/localization/config' import { localizedPath, type Locale } from '@/lib/localization/config'
import { useState } from 'react' import { useState } from 'react'
import styles from './AuthForms.module.css'
interface Dict { interface Dict {
title: string title: string
@@ -98,21 +99,10 @@ export function ForgotPasswordForm({ locale }: { locale: Locale }) {
return ( return (
<AuthShell locale={locale} title={dict.title} subtitle={dict.subtitle}> <AuthShell locale={locale} title={dict.title} subtitle={dict.subtitle}>
{sent ? ( {sent ? (
<div style={{ textAlign: 'center' }}> <div className={styles.center}>
<div <div className={styles.successIcon}>
style={{
margin: '0 auto 1rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '3.5rem',
height: '3.5rem',
borderRadius: '50%',
background: 'var(--color-success-bg, #ecfdf5)',
}}
>
<svg <svg
style={{ width: '1.75rem', height: '1.75rem', color: 'var(--color-success-text, #059669)' }} className={styles.successIconSvg}
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -121,26 +111,17 @@ export function ForgotPasswordForm({ locale }: { locale: Locale }) {
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg> </svg>
</div> </div>
<h2 style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-heading)' }}> <h2 className={styles.statusTitle}>
{dict.successTitle} {dict.successTitle}
</h2> </h2>
<p style={{ marginTop: '0.5rem', fontSize: '0.875rem', color: 'var(--color-text-secondary)' }}> <p className={styles.statusBody}>
{dict.successBody} {dict.successBody}
</p> </p>
</div> </div>
) : ( ) : (
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}> <form onSubmit={handleSubmit} className={styles.formStack}>
{error ? ( {error ? (
<div <div className={styles.error}>
style={{
borderRadius: '1.5rem',
border: '1px solid var(--color-error-border, #fecaca)',
background: 'var(--color-error-bg, #fef2f2)',
padding: '0.75rem 1rem',
fontSize: '0.875rem',
color: 'var(--color-error-text, #b91c1c)',
}}
>
{error} {error}
</div> </div>
) : null} ) : null}
@@ -163,24 +144,8 @@ export function ForgotPasswordForm({ locale }: { locale: Locale }) {
</form> </form>
)} )}
<div <div className={styles.footer}>
style={{ <a href={signInHref} className={styles.footerLink}>
marginTop: '1.5rem',
borderTop: '1px solid var(--color-border)',
paddingTop: '1.5rem',
textAlign: 'center',
fontSize: '0.875rem',
}}
>
<a
href={signInHref}
style={{
fontWeight: 600,
color: 'var(--color-heading)',
textDecoration: 'underline',
textUnderlineOffset: '4px',
}}
>
{dict.backToLogin} {dict.backToLogin}
</a> </a>
</div> </div>
@@ -7,6 +7,7 @@ import { API_BASE } from '@/lib/api';
import { localizedPath, type Locale } from '@/lib/localization/config'; import { localizedPath, type Locale } from '@/lib/localization/config';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { Suspense, useState } from 'react'; import { Suspense, useState } from 'react';
import styles from './AuthForms.module.css';
interface Dict { interface Dict {
title: string; title: string;
@@ -144,16 +145,10 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
if (!token) { if (!token) {
return ( return (
<AuthShell locale={locale} title={dict.title}> <AuthShell locale={locale} title={dict.title}>
<p <p className={styles.noToken}>
style={{
textAlign: 'center',
fontSize: '0.875rem',
color: 'var(--color-text-secondary)',
}}
>
{dict.noToken} {dict.noToken}
</p> </p>
<div style={{ marginTop: '1rem', textAlign: 'center' }}> <div className={styles.actionWrap}>
<a href={forgotHref}> <a href={forgotHref}>
<Button intent="primary">{dict.requestNew}</Button> <Button intent="primary">{dict.requestNew}</Button>
</a> </a>
@@ -165,25 +160,10 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
if (done) { if (done) {
return ( return (
<AuthShell locale={locale} title={dict.title}> <AuthShell locale={locale} title={dict.title}>
<div style={{ textAlign: 'center' }}> <div className={styles.center}>
<div <div className={styles.successIcon}>
style={{
margin: '0 auto 1rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '3.5rem',
height: '3.5rem',
borderRadius: '50%',
background: 'var(--color-success-bg, #ecfdf5)',
}}
>
<svg <svg
style={{ className={styles.successIconSvg}
width: '1.75rem',
height: '1.75rem',
color: 'var(--color-success-text, #059669)',
}}
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -192,19 +172,13 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg> </svg>
</div> </div>
<h2 style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-heading)' }}> <h2 className={styles.statusTitle}>
{dict.successTitle} {dict.successTitle}
</h2> </h2>
<p <p className={styles.statusBody}>
style={{
marginTop: '0.5rem',
fontSize: '0.875rem',
color: 'var(--color-text-secondary)',
}}
>
{dict.successBody} {dict.successBody}
</p> </p>
<div style={{ marginTop: '1rem' }}> <div className={styles.actionWrap}>
<a href={signInHref}> <a href={signInHref}>
<Button intent="primary" fullWidth> <Button intent="primary" fullWidth>
{dict.signIn} {dict.signIn}
@@ -220,25 +194,16 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
<AuthShell locale={locale} title={dict.title} subtitle={dict.subtitle}> <AuthShell locale={locale} title={dict.title} subtitle={dict.subtitle}>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }} className={styles.formStack}
> >
{error ? ( {error ? (
<div <div className={styles.error}>
style={{
borderRadius: '1.5rem',
border: '1px solid var(--color-error-border, #fecaca)',
background: 'var(--color-error-bg, #fef2f2)',
padding: '0.75rem 1rem',
fontSize: '0.875rem',
color: 'var(--color-error-text, #b91c1c)',
}}
>
{error} {error}
</div> </div>
) : null} ) : null}
<FormField id="reset-password" label={dict.newPassword} required> <FormField id="reset-password" label={dict.newPassword} required>
<div style={{ position: 'relative', width: '100%' }}> <div className={styles.passwordField}>
<input <input
id="reset-password" id="reset-password"
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
@@ -248,34 +213,13 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••" placeholder="••••••••"
autoComplete="new-password" autoComplete="new-password"
style={{ className={styles.passwordInput}
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',
}}
/> />
<button <button
type="button" type="button"
onClick={() => setShowPassword((v) => !v)} onClick={() => setShowPassword((v) => !v)}
tabIndex={-1} tabIndex={-1}
style={{ className={styles.passwordToggle}
position: 'absolute',
top: 0,
bottom: 0,
right: '0.5rem',
display: 'flex',
alignItems: 'center',
background: 'none',
border: 'none',
cursor: 'pointer',
color: 'var(--text-tertiary)',
padding: 0,
}}
> >
{showPassword ? ( {showPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
@@ -292,7 +236,7 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
</FormField> </FormField>
<FormField id="reset-confirm" label={dict.confirmPassword} required> <FormField id="reset-confirm" label={dict.confirmPassword} required>
<div style={{ position: 'relative', width: '100%' }}> <div className={styles.passwordField}>
<input <input
id="reset-confirm" id="reset-confirm"
type={showConfirm ? 'text' : 'password'} type={showConfirm ? 'text' : 'password'}
@@ -302,34 +246,13 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
onChange={(e) => setConfirm(e.target.value)} onChange={(e) => setConfirm(e.target.value)}
placeholder="••••••••" placeholder="••••••••"
autoComplete="new-password" autoComplete="new-password"
style={{ className={styles.passwordInput}
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',
}}
/> />
<button <button
type="button" type="button"
onClick={() => setShowConfirm((v) => !v)} onClick={() => setShowConfirm((v) => !v)}
tabIndex={-1} tabIndex={-1}
style={{ className={styles.passwordToggle}
position: 'absolute',
top: 0,
bottom: 0,
right: '0.5rem',
display: 'flex',
alignItems: 'center',
background: 'none',
border: 'none',
cursor: 'pointer',
color: 'var(--text-tertiary)',
padding: 0,
}}
> >
{showConfirm ? ( {showConfirm ? (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
@@ -350,24 +273,8 @@ function ResetPasswordContent({ locale }: { locale: Locale }) {
</Button> </Button>
</form> </form>
<div <div className={styles.footer}>
style={{ <a href={signInHref} className={styles.footerLink}>
marginTop: '1.5rem',
borderTop: '1px solid var(--color-border)',
paddingTop: '1.5rem',
textAlign: 'center',
fontSize: '0.875rem',
}}
>
<a
href={signInHref}
style={{
fontWeight: 600,
color: 'var(--color-heading)',
textDecoration: 'underline',
textUnderlineOffset: '4px',
}}
>
{dict.backToLogin} {dict.backToLogin}
</a> </a>
</div> </div>
@@ -5,9 +5,11 @@ import { TextInput } from '@/components/forms/TextInput';
import { FormField } from '@/components/forms/FormField'; import { FormField } from '@/components/forms/FormField';
import { AuthShell } from '@/components/auth/AuthShell'; import { AuthShell } from '@/components/auth/AuthShell';
import { API_BASE, EMPLOYEE_PROFILE_KEY } from '@/lib/api'; import { API_BASE, EMPLOYEE_PROFILE_KEY } from '@/lib/api';
import { classNames } from '@/lib/components/classNames';
import { localizedPath, type Locale } from '@/lib/localization/config'; import { localizedPath, type Locale } from '@/lib/localization/config';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import styles from './AuthForms.module.css';
interface Dict { interface Dict {
title: string; title: string;
@@ -240,15 +242,7 @@ export function SignInForm({ locale }: { locale: Locale }) {
<AuthShell locale={locale} title={dict.title} subtitle={dict.subtitle}> <AuthShell locale={locale} title={dict.title} subtitle={dict.subtitle}>
{error ? ( {error ? (
<div <div
style={{ className={classNames(styles.error, styles.spacedError)}
marginBottom: '1.25rem',
borderRadius: '1.5rem',
border: '1px solid var(--color-error-border, #fecaca)',
background: 'var(--color-error-bg, #fef2f2)',
padding: '0.75rem 1rem',
fontSize: '0.875rem',
color: 'var(--color-error-text, #b91c1c)',
}}
> >
{error} {error}
</div> </div>
@@ -257,7 +251,7 @@ export function SignInForm({ locale }: { locale: Locale }) {
{step === 'credentials' ? ( {step === 'credentials' ? (
<form <form
onSubmit={handleCredentials} onSubmit={handleCredentials}
style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }} className={styles.formStack}
> >
<FormField id="signin-email" label={dict.email} required> <FormField id="signin-email" label={dict.email} required>
<TextInput <TextInput
@@ -272,7 +266,7 @@ export function SignInForm({ locale }: { locale: Locale }) {
</FormField> </FormField>
<FormField id="signin-password" label={dict.password} required> <FormField id="signin-password" label={dict.password} required>
<div style={{ position: 'relative', width: '100%' }}> <div className={styles.passwordField}>
<input <input
id="signin-password" id="signin-password"
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
@@ -281,34 +275,13 @@ export function SignInForm({ locale }: { locale: Locale }) {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••" placeholder="••••••••"
autoComplete="current-password" autoComplete="current-password"
style={{ className={styles.passwordInput}
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',
}}
/> />
<button <button
type="button" type="button"
onClick={() => setShowPassword((v) => !v)} onClick={() => setShowPassword((v) => !v)}
tabIndex={-1} tabIndex={-1}
style={{ className={styles.passwordToggle}
position: 'absolute',
top: 0,
bottom: 0,
right: '0.5rem',
display: 'flex',
alignItems: 'center',
background: 'none',
border: 'none',
cursor: 'pointer',
color: 'var(--text-tertiary)',
padding: 0,
}}
> >
{showPassword ? ( {showPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
@@ -328,15 +301,10 @@ export function SignInForm({ locale }: { locale: Locale }) {
{loading ? dict.signingIn : dict.signIn} {loading ? dict.signingIn : dict.signIn}
</Button> </Button>
<div style={{ textAlign: 'center' }}> <div className={styles.forgotLinkWrap}>
<a <a
href={hrefBase} href={hrefBase}
style={{ className={styles.mutedLink}
fontSize: '0.875rem',
color: 'var(--color-text-tertiary)',
textDecoration: 'underline',
textUnderlineOffset: '4px',
}}
> >
{dict.forgotPassword} {dict.forgotPassword}
</a> </a>
@@ -345,19 +313,9 @@ export function SignInForm({ locale }: { locale: Locale }) {
) : ( ) : (
<form <form
onSubmit={handleTotp} onSubmit={handleTotp}
style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }} className={styles.formStack}
>
<div
style={{
borderRadius: '1.5rem',
border: '1px solid var(--color-border)',
background: 'var(--color-surface-secondary)',
padding: '1rem',
textAlign: 'center',
fontSize: '0.875rem',
color: 'var(--color-text-secondary)',
}}
> >
<div className={styles.infoBox}>
{dict.enterCode} {dict.enterCode}
</div> </div>
@@ -373,7 +331,7 @@ export function SignInForm({ locale }: { locale: Locale }) {
setTotpCode(e.target.value.replace(/[^0-9A-Za-z-]/g, '').toUpperCase()) setTotpCode(e.target.value.replace(/[^0-9A-Za-z-]/g, '').toUpperCase())
} }
placeholder={dict.totpPlaceholder} placeholder={dict.totpPlaceholder}
style={{ textAlign: 'center', fontSize: '1.25rem', letterSpacing: '0.45em' }} className={styles.totpInput}
/> />
</FormField> </FormField>
@@ -388,14 +346,7 @@ export function SignInForm({ locale }: { locale: Locale }) {
setTotpCode(''); setTotpCode('');
setError(null); setError(null);
}} }}
style={{ className={styles.plainButton}
width: '100%',
fontSize: '0.875rem',
color: 'var(--color-text-tertiary)',
background: 'none',
border: 'none',
cursor: 'pointer',
}}
> >
{dict.back} {dict.back}
</button> </button>
@@ -11,6 +11,7 @@
gap: var(--space-3); gap: var(--space-3);
} }
.skeleton span { .skeleton span {
inline-size: 100%;
block-size: 0.9rem; block-size: 0.9rem;
border-radius: var(--radius-pill); border-radius: var(--radius-pill);
background: linear-gradient( background: linear-gradient(
@@ -22,6 +23,9 @@
background-size: 200% 100%; background-size: 200% 100%;
animation: shimmer 1.5s linear infinite; animation: shimmer 1.5s linear infinite;
} }
.skeleton span:last-child {
inline-size: 68%;
}
@keyframes spin { @keyframes spin {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
@@ -31,7 +31,7 @@ export function Skeleton({
{...props} {...props}
> >
{Array.from({ length: lines }, (_, index) => ( {Array.from({ length: lines }, (_, index) => (
<span key={index} style={{ inlineSize: index === lines - 1 ? '68%' : '100%' }} /> <span key={index} />
))} ))}
</div> </div>
); );
+1 -1
View File
@@ -101,7 +101,7 @@ validate_prod_env_file() {
redis_password="$(read_env_value REDIS_PASSWORD)" redis_password="$(read_env_value REDIS_PASSWORD)"
jwt_secret="$(read_env_value JWT_SECRET)" 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 echo "Invalid ACME_EMAIL in ${ENV_FILE}. Set ACME_EMAIL=${DEFAULT_ACME_EMAIL} for Let's Encrypt." >&2
exit 1 exit 1
fi fi