mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 03:20:46 +02:00
changes to the login and signup, similar to in the saas repo (#6577)
Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
co-authored by
Anthony Stirling
parent
3675db5907
commit
f15e405759
@@ -4471,7 +4471,7 @@ alreadyHaveAccount = "Already have an account?"
|
||||
choosePassword = "Choose a password"
|
||||
confirmPassword = "Confirm password"
|
||||
confirmPasswordPlaceholder = "Re-enter your password"
|
||||
createAccount = "Create Account"
|
||||
createAccount = "Create account"
|
||||
creating = "Creating Account..."
|
||||
email = "Email address"
|
||||
emailPlaceholder = "Enter your email address"
|
||||
@@ -4554,6 +4554,7 @@ sending = "Sending…"
|
||||
sendMagicLink = "Send Magic Link"
|
||||
sendResetLink = "Send reset link"
|
||||
sessionExpired = "Your session has expired. Please sign in again."
|
||||
createAccount = "Create an account"
|
||||
signin = "Sign in"
|
||||
signInAnonymously = "Sign Up as a Guest"
|
||||
signingIn = "Signing in..."
|
||||
@@ -6885,6 +6886,8 @@ passwordsDoNotMatch = "Passwords do not match"
|
||||
passwordTooShort = "Password must be at least 6 characters long"
|
||||
pleaseFillAllFields = "Please fill in all fields"
|
||||
signUp = "Sign Up"
|
||||
signUpWith = "Sign up with"
|
||||
skip = "Skip"
|
||||
subtitle = "Join Stirling PDF to get started"
|
||||
title = "Create an account"
|
||||
unexpectedError = "Unexpected error: {{message}}"
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.8 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.8 KiB |
@@ -19,12 +19,7 @@ test.describe("1. Authentication and Login", () => {
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
|
||||
// Step 3: Confirm the heading for "Sign In" / "Login" is visible
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /sign in|login|masuk/i }),
|
||||
).toBeVisible();
|
||||
|
||||
// Step 4: Confirm a "Username" text input field is present and empty
|
||||
// Step 3: Confirm a "Username" text input field is present and empty
|
||||
const usernameInput = page.locator("#email");
|
||||
await expect(usernameInput).toBeVisible();
|
||||
await expect(usernameInput).toHaveValue("");
|
||||
|
||||
@@ -17,12 +17,11 @@ import { useDocumentMeta } from "@app/hooks/useDocumentMeta";
|
||||
import AuthLayout from "@app/routes/authShared/AuthLayout";
|
||||
import { useBackendProbe } from "@app/hooks/useBackendProbe";
|
||||
import apiClient from "@app/services/apiClient";
|
||||
import { BASE_PATH } from "@app/constants/app";
|
||||
import { BASE_PATH, withBasePath } from "@app/constants/app";
|
||||
import { type OAuthProvider } from "@app/auth/oauthTypes";
|
||||
import { updateSupportedLanguages } from "@app/i18n";
|
||||
|
||||
// Import login components
|
||||
import LoginHeader from "@app/routes/login/LoginHeader";
|
||||
import ErrorMessage from "@app/routes/login/ErrorMessage";
|
||||
import EmailPasswordForm from "@app/routes/login/EmailPasswordForm";
|
||||
import OAuthButtons, {
|
||||
@@ -74,7 +73,6 @@ export default function Login() {
|
||||
const autoLoginErrorRecorded = useRef(false);
|
||||
const isUserPassAllowed = loginMethod === "all" || loginMethod === "normal";
|
||||
const isSsoOnlyMode = loginMethod !== "all" && loginMethod !== "normal";
|
||||
const isSingleSsoOnly = !isUserPassAllowed && enabledProviders.length === 1;
|
||||
|
||||
const AUTO_LOGIN_ATTEMPTS_KEY = "stirling_sso_auto_login_attempts";
|
||||
const AUTO_LOGIN_ERRORS_KEY = "stirling_sso_auto_login_errors";
|
||||
@@ -461,7 +459,6 @@ export default function Login() {
|
||||
|
||||
// If backend isn't ready yet, show a lightweight status screen instead of the form
|
||||
if (backendProbe.status !== "up" && !loginDisabled) {
|
||||
const backendTitle = t("backendStartup.notFoundTitle", "Backend not found");
|
||||
const handleRetry = async () => {
|
||||
const result = await backendProbe.probe();
|
||||
if (result.status === "up") {
|
||||
@@ -471,7 +468,18 @@ export default function Login() {
|
||||
};
|
||||
return (
|
||||
<AuthLayout>
|
||||
<LoginHeader title={backendTitle} />
|
||||
<div className="auth-logo-block">
|
||||
<img
|
||||
src={withBasePath("/modern-logo/LoginLightModeHeader.svg")}
|
||||
alt="Stirling PDF"
|
||||
className="auth-logo-header auth-logo-header--light"
|
||||
/>
|
||||
<img
|
||||
src={withBasePath("/modern-logo/LoginDarkModeHeader.svg")}
|
||||
alt="Stirling PDF"
|
||||
className="auth-logo-header auth-logo-header--dark"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="auth-section"
|
||||
style={{
|
||||
@@ -555,10 +563,18 @@ export default function Login() {
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<LoginHeader
|
||||
title={isSingleSsoOnly ? "" : t("login.login") || "Sign in"}
|
||||
centerOnly={isSingleSsoOnly}
|
||||
/>
|
||||
<div className="auth-logo-block">
|
||||
<img
|
||||
src={withBasePath("/modern-logo/LoginLightModeHeader.svg")}
|
||||
alt="Stirling PDF"
|
||||
className="auth-logo-header auth-logo-header--light"
|
||||
/>
|
||||
<img
|
||||
src={withBasePath("/modern-logo/LoginDarkModeHeader.svg")}
|
||||
alt="Stirling PDF"
|
||||
className="auth-logo-header auth-logo-header--dark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Success message */}
|
||||
{successMessage && (
|
||||
|
||||
@@ -5,10 +5,9 @@ import { useDocumentMeta } from "@app/hooks/useDocumentMeta";
|
||||
import { useAuth } from "@app/auth/UseSession";
|
||||
import AuthLayout from "@app/routes/authShared/AuthLayout";
|
||||
import "@app/routes/authShared/auth.css";
|
||||
import { BASE_PATH } from "@app/constants/app";
|
||||
import { BASE_PATH, withBasePath } from "@app/constants/app";
|
||||
|
||||
// Import signup components
|
||||
import LoginHeader from "@app/routes/login/LoginHeader";
|
||||
import ErrorMessage from "@app/routes/login/ErrorMessage";
|
||||
import DividerWithText from "@app/components/shared/DividerWithText";
|
||||
import SignupForm from "@app/routes/signup/SignupForm";
|
||||
@@ -92,10 +91,18 @@ export default function Signup() {
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<LoginHeader
|
||||
title={t("signup.title", "Create an account")}
|
||||
subtitle={t("signup.subtitle", "Join Stirling PDF")}
|
||||
/>
|
||||
<div className="auth-logo-block">
|
||||
<img
|
||||
src={withBasePath("/modern-logo/LoginLightModeHeader.svg")}
|
||||
alt="Stirling PDF"
|
||||
className="auth-logo-header auth-logo-header--light"
|
||||
/>
|
||||
<img
|
||||
src={withBasePath("/modern-logo/LoginDarkModeHeader.svg")}
|
||||
alt="Stirling PDF"
|
||||
className="auth-logo-header auth-logo-header--dark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ErrorMessage error={error} />
|
||||
|
||||
|
||||
@@ -615,3 +615,90 @@
|
||||
background-color: #af3434 !important;
|
||||
opacity: 0.6 !important;
|
||||
}
|
||||
|
||||
/* ── Logo block ─────────────────────────────────────────────────────── */
|
||||
.auth-logo-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 0.5rem;
|
||||
animation: authFadeUp 0.4s ease both;
|
||||
}
|
||||
|
||||
.auth-logo-header {
|
||||
height: 8rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.auth-logo-header--dark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── Page entrance animation ────────────────────────────────────────── */
|
||||
@keyframes authFadeUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Expandable trigger button state ────────────────────────────────── */
|
||||
.auth-expandable-trigger {
|
||||
transition:
|
||||
background-color 180ms ease,
|
||||
box-shadow 180ms ease,
|
||||
border-color 180ms ease;
|
||||
}
|
||||
|
||||
.auth-expandable-trigger--active {
|
||||
border-color: #af3434 !important;
|
||||
box-shadow: 0 0 0 3px rgba(175, 52, 52, 0.12);
|
||||
}
|
||||
|
||||
/* ── Animated expand/collapse via grid-template-rows ───────────────── */
|
||||
.auth-expand-grid {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition:
|
||||
grid-template-rows 280ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 220ms ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.auth-expand-grid--open {
|
||||
grid-template-rows: 1fr;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.auth-expand-inner {
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ── Icon+label group — keeps icons vertically aligned across buttons ── */
|
||||
.oauth-btn-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 12.5rem;
|
||||
}
|
||||
|
||||
.oauth-btn-label {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── Icon helpers inside oauth-button-fullwidth ─────────────────────── */
|
||||
.auth-at-icon {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
margin-right: 0.5rem;
|
||||
display: inline-block;
|
||||
width: 1.75rem;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -7,17 +7,18 @@ import { useDocumentMeta } from "@app/hooks/useDocumentMeta";
|
||||
import AuthLayout from "@app/routes/authShared/AuthLayout";
|
||||
import "@app/routes/authShared/auth.css";
|
||||
import "@app/routes/authShared/saas-auth.css";
|
||||
import GuestSignInButton from "@app/routes/authShared/GuestSignInButton";
|
||||
import {
|
||||
absoluteWithBasePath,
|
||||
getBaseUrl,
|
||||
withBasePath,
|
||||
} from "@app/constants/app";
|
||||
import LinkRoundedIcon from "@mui/icons-material/LinkRounded";
|
||||
|
||||
// Import login components
|
||||
import LoginHeader from "@app/routes/login/LoginHeader";
|
||||
import ErrorMessage from "@app/routes/login/ErrorMessage";
|
||||
import EmailPasswordForm from "@app/routes/login/EmailPasswordForm";
|
||||
import MagicLinkForm from "@app/routes/login/MagicLinkForm";
|
||||
import OAuthButtons from "@app/routes/login/OAuthButtons";
|
||||
import DividerWithText from "@app/components/shared/DividerWithText";
|
||||
import LoggedInState from "@app/routes/login/LoggedInState";
|
||||
import { absoluteWithBasePath, getBaseUrl } from "@app/constants/app";
|
||||
|
||||
export default function Login() {
|
||||
const navigate = useNavigate();
|
||||
@@ -25,11 +26,13 @@ export default function Login() {
|
||||
const { t } = useTranslation();
|
||||
const [isSigningIn, setIsSigningIn] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showMagicLink, setShowMagicLink] = useState(false);
|
||||
const [showMagicLinkForm, setShowMagicLinkForm] = useState(false);
|
||||
const [showEmailForm, setShowEmailForm] = useState(false);
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [magicLinkEmail, setMagicLinkEmail] = useState("");
|
||||
const [magicLinkSent, setMagicLinkSent] = useState(false);
|
||||
|
||||
// Prefill email from query param (e.g. after password reset)
|
||||
useEffect(() => {
|
||||
try {
|
||||
@@ -37,6 +40,7 @@ export default function Login() {
|
||||
const emailFromQuery = url.searchParams.get("email");
|
||||
if (emailFromQuery) {
|
||||
setEmail(emailFromQuery);
|
||||
setShowEmailForm(true);
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore
|
||||
@@ -174,12 +178,9 @@ export default function Login() {
|
||||
setError(error.message);
|
||||
} else {
|
||||
setError(null);
|
||||
alert(t("login.magicLinkSent", { email: magicLinkEmail }));
|
||||
setMagicLinkEmail("");
|
||||
setShowMagicLink(false);
|
||||
setMagicLinkSent(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Login] Unexpected error:", err);
|
||||
setError(
|
||||
t("login.unexpectedError", {
|
||||
message: err instanceof Error ? err.message : "Unknown error",
|
||||
@@ -190,10 +191,6 @@ export default function Login() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleForgotPassword = () => {
|
||||
navigate("/auth/reset");
|
||||
};
|
||||
|
||||
const handleAnonymousSignIn = async () => {
|
||||
try {
|
||||
setIsSigningIn(true);
|
||||
@@ -227,115 +224,210 @@ export default function Login() {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleEmailForm = () => {
|
||||
setShowEmailForm((v) => !v);
|
||||
setShowMagicLinkForm(false);
|
||||
setMagicLinkSent(false);
|
||||
};
|
||||
|
||||
const toggleMagicLink = () => {
|
||||
setShowMagicLinkForm((v) => !v);
|
||||
setShowEmailForm(false);
|
||||
setMagicLinkSent(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthLayout isEmailFormExpanded={showEmailForm}>
|
||||
<LoginHeader
|
||||
title={t("login.login")}
|
||||
subtitle={t("login.subtitle", "Sign back in to Stirling PDF")}
|
||||
/>
|
||||
<AuthLayout isEmailFormExpanded={showEmailForm || showMagicLinkForm}>
|
||||
{/* Centered logo */}
|
||||
<div className="auth-logo-block">
|
||||
<img
|
||||
src={withBasePath("/modern-logo/LoginLightModeHeader.svg")}
|
||||
alt="Stirling PDF"
|
||||
className="auth-logo-header auth-logo-header--light"
|
||||
/>
|
||||
<img
|
||||
src={withBasePath("/modern-logo/LoginDarkModeHeader.svg")}
|
||||
alt="Stirling PDF"
|
||||
className="auth-logo-header auth-logo-header--dark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ErrorMessage error={error} />
|
||||
|
||||
{/* OAuth first */}
|
||||
<OAuthButtons
|
||||
onProviderClick={signInWithProvider}
|
||||
isSubmitting={isSigningIn}
|
||||
layout="fullwidth"
|
||||
/>
|
||||
{/* OAuth + magic link group — single flex column so gap is uniform */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "0.75rem",
|
||||
marginBottom: "2.5rem",
|
||||
}}
|
||||
>
|
||||
<OAuthButtons
|
||||
onProviderClick={signInWithProvider}
|
||||
isSubmitting={isSigningIn}
|
||||
layout="fullwidth"
|
||||
labelPrefix={`${t("login.signInWith", "Sign in with")} `}
|
||||
/>
|
||||
|
||||
{/* Divider between OAuth and Email */}
|
||||
<DividerWithText
|
||||
text={t("signup.or", "or")}
|
||||
respondsToDarkMode={false}
|
||||
opacity={0.4}
|
||||
/>
|
||||
{/* Magic link button + its expandable form as one unit */}
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSigningIn}
|
||||
onClick={toggleMagicLink}
|
||||
className={`oauth-button-fullwidth auth-expandable-trigger ${showMagicLinkForm ? "auth-expandable-trigger--active" : ""}`}
|
||||
>
|
||||
<span className="oauth-btn-group">
|
||||
<LinkRoundedIcon
|
||||
style={{
|
||||
width: "1.75rem",
|
||||
height: "1.75rem",
|
||||
marginRight: "0.5rem",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<span className="oauth-btn-label">
|
||||
{t("login.useMagicLink", "Use magic link")}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Sign in with email button (primary color to match signup CTA) */}
|
||||
<div className="auth-section">
|
||||
<div
|
||||
className={`auth-expand-grid ${showMagicLinkForm ? "auth-expand-grid--open" : ""}`}
|
||||
>
|
||||
<div className="auth-expand-inner">
|
||||
<div style={{ paddingTop: "0.25rem" }}>
|
||||
{magicLinkSent ? (
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
color: "#059669",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{t("login.magicLinkSent", { email: magicLinkEmail })}
|
||||
</p>
|
||||
) : (
|
||||
<div className="auth-magic-row">
|
||||
<input
|
||||
type="email"
|
||||
placeholder={t(
|
||||
"login.enterEmailForMagicLink",
|
||||
"Enter your email",
|
||||
)}
|
||||
value={magicLinkEmail}
|
||||
onChange={(e) => setMagicLinkEmail(e.target.value)}
|
||||
onKeyDown={(e) =>
|
||||
e.key === "Enter" &&
|
||||
!isSigningIn &&
|
||||
signInWithMagicLink()
|
||||
}
|
||||
className="auth-input"
|
||||
/>
|
||||
<button
|
||||
onClick={signInWithMagicLink}
|
||||
disabled={isSigningIn || !magicLinkEmail}
|
||||
className="auth-magic-button"
|
||||
>
|
||||
{isSigningIn
|
||||
? t("login.sending")
|
||||
: t("login.sendMagicLink")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email & Password button */}
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSigningIn}
|
||||
onClick={toggleEmailForm}
|
||||
className={`oauth-button-fullwidth auth-expandable-trigger ${showEmailForm ? "auth-expandable-trigger--active" : ""}`}
|
||||
style={{ marginBottom: "0.75rem" }}
|
||||
>
|
||||
<span className="oauth-btn-group">
|
||||
<span className="auth-at-icon">@</span>
|
||||
<span className="oauth-btn-label">{`${t("login.signInWith", "Sign in with")} email`}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Email form — animated expand */}
|
||||
<div
|
||||
className={`auth-expand-grid ${showEmailForm ? "auth-expand-grid--open" : ""}`}
|
||||
>
|
||||
<div className="auth-expand-inner">
|
||||
<div style={{ paddingBottom: "0.5rem" }}>
|
||||
<EmailPasswordForm
|
||||
email={email}
|
||||
password={password}
|
||||
setEmail={setEmail}
|
||||
setPassword={setPassword}
|
||||
onSubmit={signInWithEmail}
|
||||
isSubmitting={isSigningIn}
|
||||
submitButtonText={
|
||||
isSigningIn ? t("login.loggingIn") : t("login.login")
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate("/auth/reset")}
|
||||
className="auth-link-black"
|
||||
style={{ fontSize: "0.8125rem", marginTop: "0.25rem" }}
|
||||
>
|
||||
{t("login.forgotPassword", "Forgot your password?")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skip */}
|
||||
<div style={{ textAlign: "center", margin: "1rem 0" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowEmailForm((v) => !v)}
|
||||
onClick={handleAnonymousSignIn}
|
||||
disabled={isSigningIn}
|
||||
className="w-full px-4 py-[0.75rem] rounded-[0.625rem] text-base font-semibold mb-2 cursor-pointer border-0 disabled:opacity-50 disabled:cursor-not-allowed auth-cta-button"
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
fontSize: "1rem",
|
||||
fontWeight: 700,
|
||||
color: "#000000",
|
||||
}}
|
||||
>
|
||||
{t("login.useEmailInstead", "Sign in with email")}
|
||||
{isSigningIn
|
||||
? t("login.signingIn", "Signing in...")
|
||||
: `${t("signup.skip", "Skip")} →`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showEmailForm && (
|
||||
<EmailPasswordForm
|
||||
email={email}
|
||||
password={password}
|
||||
setEmail={setEmail}
|
||||
setPassword={setPassword}
|
||||
onSubmit={signInWithEmail}
|
||||
isSubmitting={isSigningIn}
|
||||
submitButtonText={
|
||||
isSigningIn ? t("login.loggingIn") : t("login.login")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showEmailForm && (
|
||||
<div className="auth-section-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleForgotPassword}
|
||||
className="auth-link-black"
|
||||
>
|
||||
{t("login.forgotPassword", "Forgot your password?")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Divider then Guest */}
|
||||
<DividerWithText
|
||||
text={t("signup.or", "or")}
|
||||
respondsToDarkMode={false}
|
||||
opacity={0.4}
|
||||
/>
|
||||
|
||||
<GuestSignInButton
|
||||
onClick={handleAnonymousSignIn}
|
||||
disabled={isSigningIn}
|
||||
label={
|
||||
isSigningIn
|
||||
? t("login.signingIn", "Signing in...")
|
||||
: t("login.signInAnonymously", "Sign in as a Guest")
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="auth-bottom-row">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowMagicLink(true)}
|
||||
className="auth-link-black"
|
||||
>
|
||||
{t("login.useMagicLink", "Sign in with magic link")}
|
||||
</button>
|
||||
|
||||
{/* Bottom */}
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: "auto",
|
||||
paddingTop: "1rem",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate("/signup")}
|
||||
className="auth-link-black"
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.875rem",
|
||||
color: "#9ca3af",
|
||||
}}
|
||||
>
|
||||
{t("signup.signUp", "Sign up")}
|
||||
{t("login.createAccount", "Create an account")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Magic link form renders on demand */}
|
||||
{showMagicLink && (
|
||||
<div style={{ marginTop: "0.5rem" }}>
|
||||
<MagicLinkForm
|
||||
showMagicLink={showMagicLink}
|
||||
magicLinkEmail={magicLinkEmail}
|
||||
setMagicLinkEmail={setMagicLinkEmail}
|
||||
setShowMagicLink={setShowMagicLink}
|
||||
onSubmit={signInWithMagicLink}
|
||||
isSubmitting={isSigningIn}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,18 +4,15 @@ import { signInAnonymously } from "@app/auth/supabase";
|
||||
import { useAuth } from "@app/auth/UseSession";
|
||||
import { useTranslation } from "@app/hooks/useTranslation";
|
||||
import { useDocumentMeta } from "@app/hooks/useDocumentMeta";
|
||||
import { getBaseUrl } from "@app/constants/app";
|
||||
import { getBaseUrl, withBasePath } from "@app/constants/app";
|
||||
import AuthLayout from "@app/routes/authShared/AuthLayout";
|
||||
import "@app/routes/authShared/auth.css";
|
||||
import "@app/routes/authShared/saas-auth.css";
|
||||
import GuestSignInButton from "@app/routes/authShared/GuestSignInButton";
|
||||
import { alert } from "@app/components/toast";
|
||||
|
||||
// Import signup components
|
||||
import LoginHeader from "@app/routes/login/LoginHeader";
|
||||
import ErrorMessage from "@app/routes/login/ErrorMessage";
|
||||
import OAuthButtons from "@app/routes/login/OAuthButtons";
|
||||
import DividerWithText from "@app/components/shared/DividerWithText";
|
||||
import SignupForm from "@app/routes/signup/SignupForm";
|
||||
import {
|
||||
useSignupFormValidation,
|
||||
@@ -184,84 +181,112 @@ export default function Signup() {
|
||||
|
||||
return (
|
||||
<AuthLayout isEmailFormExpanded={showEmailForm}>
|
||||
<LoginHeader title={t("signup.title")} subtitle={t("signup.subtitle")} />
|
||||
{/* Centered logo */}
|
||||
<div className="auth-logo-block">
|
||||
<img
|
||||
src={withBasePath("/modern-logo/LoginLightModeHeader.svg")}
|
||||
alt="Stirling PDF"
|
||||
className="auth-logo-header auth-logo-header--light"
|
||||
/>
|
||||
<img
|
||||
src={withBasePath("/modern-logo/LoginDarkModeHeader.svg")}
|
||||
alt="Stirling PDF"
|
||||
className="auth-logo-header auth-logo-header--dark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ErrorMessage error={error} />
|
||||
|
||||
{/* OAuth first */}
|
||||
<div style={{ marginBottom: "0.5rem" }}>
|
||||
{/* OAuth providers */}
|
||||
<div>
|
||||
<OAuthButtons
|
||||
onProviderClick={handleProviderSignIn}
|
||||
isSubmitting={isSigningUp}
|
||||
layout="fullwidth"
|
||||
labelPrefix={`${t("signup.signUpWith", "Sign up with")} `}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Divider between OAuth and Email */}
|
||||
<div style={{ margin: "0.5rem 0" }}>
|
||||
<DividerWithText
|
||||
text={t("signup.or", "or")}
|
||||
respondsToDarkMode={false}
|
||||
opacity={0.4}
|
||||
/>
|
||||
{/* Email & Password button */}
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSigningUp}
|
||||
onClick={() => setShowEmailForm((v) => !v)}
|
||||
className={`oauth-button-fullwidth auth-expandable-trigger ${showEmailForm ? "auth-expandable-trigger--active" : ""}`}
|
||||
style={{ marginTop: "2.5rem", marginBottom: "0.75rem" }}
|
||||
>
|
||||
<span className="oauth-btn-group">
|
||||
<span className="auth-at-icon">@</span>
|
||||
<span className="oauth-btn-label">{`${t("signup.signUpWith", "Sign up with")} email`}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Email form — animated expand */}
|
||||
<div
|
||||
className={`auth-expand-grid ${showEmailForm ? "auth-expand-grid--open" : ""}`}
|
||||
>
|
||||
<div className="auth-expand-inner">
|
||||
<div style={{ paddingBottom: "0.5rem" }}>
|
||||
<SignupForm
|
||||
name={name}
|
||||
email={email}
|
||||
password={password}
|
||||
confirmPassword={confirmPassword}
|
||||
agree={agree}
|
||||
setName={setName}
|
||||
setEmail={setEmail}
|
||||
setPassword={setPassword}
|
||||
setConfirmPassword={setConfirmPassword}
|
||||
setAgree={setAgree}
|
||||
onSubmit={handleSignUp}
|
||||
isSubmitting={isSigningUp}
|
||||
fieldErrors={fieldErrors}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Use Email Instead button (toggles email form) */}
|
||||
<div className="auth-section">
|
||||
{/* Skip */}
|
||||
<div style={{ textAlign: "center", margin: "1rem 0" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAnonymousSignIn}
|
||||
disabled={isSigningUp}
|
||||
onClick={() => setShowEmailForm((v) => !v)}
|
||||
className="w-full px-4 py-[0.75rem] rounded-[0.625rem] text-base font-semibold mb-2 cursor-pointer border-0 disabled:opacity-50 disabled:cursor-not-allowed auth-cta-button"
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
fontSize: "1rem",
|
||||
fontWeight: 700,
|
||||
color: "#000000",
|
||||
}}
|
||||
>
|
||||
{t("signup.useEmailInstead", "Use Email Instead")}
|
||||
{isSigningUp
|
||||
? t("login.signingIn", "Signing in...")
|
||||
: `${t("signup.skip", "Skip")} →`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showEmailForm && (
|
||||
<SignupForm
|
||||
name={name}
|
||||
email={email}
|
||||
password={password}
|
||||
confirmPassword={confirmPassword}
|
||||
agree={agree}
|
||||
setName={setName}
|
||||
setEmail={setEmail}
|
||||
setPassword={setPassword}
|
||||
setConfirmPassword={setConfirmPassword}
|
||||
setAgree={setAgree}
|
||||
onSubmit={handleSignUp}
|
||||
isSubmitting={isSigningUp}
|
||||
fieldErrors={fieldErrors}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="auth-section-sm">
|
||||
<DividerWithText
|
||||
text={t("signup.or", "or")}
|
||||
respondsToDarkMode={false}
|
||||
opacity={0.4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<GuestSignInButton
|
||||
onClick={handleAnonymousSignIn}
|
||||
disabled={isSigningUp}
|
||||
label={
|
||||
isSigningUp
|
||||
? t("login.signingIn", "Signing in...")
|
||||
: t("login.signInAnonymously", "Sign in as a Guest")
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Bottom row */}
|
||||
<div className="auth-bottom-right">
|
||||
{/* Bottom */}
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: "auto",
|
||||
paddingTop: "1rem",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate("/login")}
|
||||
className="auth-link-black"
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.875rem",
|
||||
color: "#9ca3af",
|
||||
}}
|
||||
>
|
||||
{t("login.logIn", "Log In")}
|
||||
{t("signup.alreadyHaveAccount", "I already have an account")}
|
||||
</button>
|
||||
</div>
|
||||
</AuthLayout>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
justify-content: center;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.625rem;
|
||||
border-radius: 100px;
|
||||
background-color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
@@ -22,6 +22,10 @@
|
||||
cursor: pointer;
|
||||
gap: 0.5rem;
|
||||
box-shadow: 0 0.125rem 0.375rem rgba(0, 0, 0, 0.04);
|
||||
transition:
|
||||
background-color 150ms ease,
|
||||
box-shadow 150ms ease,
|
||||
border-color 150ms ease;
|
||||
}
|
||||
|
||||
.oauth-button-fullwidth:disabled {
|
||||
@@ -29,6 +33,11 @@
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.oauth-button-fullwidth:hover:not(:disabled) {
|
||||
background-color: #fafafa;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.auth-dropdown-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
@@ -72,3 +81,90 @@
|
||||
color: #9c2f30;
|
||||
border: 2px solid currentColor;
|
||||
}
|
||||
|
||||
/* ── Logo block ─────────────────────────────────────────────────────── */
|
||||
.auth-logo-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 0.5rem;
|
||||
animation: authFadeUp 0.4s ease both;
|
||||
}
|
||||
|
||||
.auth-logo-header {
|
||||
height: 8rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.auth-logo-header--dark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── Page entrance animation ────────────────────────────────────────── */
|
||||
@keyframes authFadeUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Expandable trigger button state ────────────────────────────────── */
|
||||
.auth-expandable-trigger {
|
||||
transition:
|
||||
background-color 180ms ease,
|
||||
box-shadow 180ms ease,
|
||||
border-color 180ms ease;
|
||||
}
|
||||
|
||||
.auth-expandable-trigger--active {
|
||||
border-color: #af3434 !important;
|
||||
box-shadow: 0 0 0 3px rgba(175, 52, 52, 0.12);
|
||||
}
|
||||
|
||||
/* ── Animated expand/collapse via grid-template-rows ───────────────── */
|
||||
.auth-expand-grid {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition:
|
||||
grid-template-rows 280ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 220ms ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.auth-expand-grid--open {
|
||||
grid-template-rows: 1fr;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.auth-expand-inner {
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ── Icon+label group — keeps icons vertically aligned across buttons ── */
|
||||
.oauth-btn-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 12.5rem;
|
||||
}
|
||||
|
||||
.oauth-btn-label {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── Icon helpers inside oauth-button-fullwidth ─────────────────────── */
|
||||
.auth-at-icon {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
margin-right: 0.5rem;
|
||||
display: inline-block;
|
||||
width: 1.75rem;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ interface OAuthButtonsProps {
|
||||
isSubmitting: boolean;
|
||||
layout?: "vertical" | "grid" | "icons" | "fullwidth";
|
||||
enabledProviders?: string[]; // List of enabled provider IDs from backend
|
||||
labelPrefix?: string;
|
||||
}
|
||||
|
||||
export default function OAuthButtons({
|
||||
@@ -24,6 +25,7 @@ export default function OAuthButtons({
|
||||
isSubmitting,
|
||||
layout = "vertical",
|
||||
enabledProviders: _enabledProviders = [],
|
||||
labelPrefix = "",
|
||||
}: OAuthButtonsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -92,12 +94,18 @@ export default function OAuthButtons({
|
||||
className="oauth-button-fullwidth"
|
||||
title={p.label}
|
||||
>
|
||||
<img
|
||||
src={withBasePath(`/Login/${p.file}`)}
|
||||
alt={p.label}
|
||||
className={`oauth-icon-medium ${p.isDisabled ? "opacity-20" : ""}`}
|
||||
/>
|
||||
{p.label}
|
||||
<span className="oauth-btn-group">
|
||||
<img
|
||||
src={withBasePath(`/Login/${p.file}`)}
|
||||
alt={p.label}
|
||||
className={`oauth-icon-medium ${p.isDisabled ? "opacity-20" : ""}`}
|
||||
style={{ marginRight: "0.5rem", flexShrink: 0 }}
|
||||
/>
|
||||
<span className="oauth-btn-label">
|
||||
{labelPrefix}
|
||||
{p.label}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
--auth-label-text: #2b3230;
|
||||
--auth-button-bg: #af3434;
|
||||
--auth-button-text: #ffffff;
|
||||
--auth-magic-button-bg: #8b5cf6;
|
||||
--auth-magic-button-bg: #af3434;
|
||||
--auth-magic-button-text: #ffffff;
|
||||
|
||||
/* Light-only auth colors (no dark mode equivalents) used for login/signup */
|
||||
@@ -45,7 +45,7 @@
|
||||
--auth-label-text-light-only: #2b3230;
|
||||
--auth-button-bg-light-only: #af3434;
|
||||
--auth-button-text-light-only: #ffffff;
|
||||
--auth-magic-button-bg-light-only: #8b5cf6;
|
||||
--auth-magic-button-bg-light-only: #af3434;
|
||||
--auth-magic-button-text-light-only: #ffffff;
|
||||
--auth-bg-color-light-only: #ffffff;
|
||||
--auth-card-bg-light-only: #ffffff;
|
||||
|
||||
Reference in New Issue
Block a user