diff --git a/bun.lock b/bun.lock index 199a4c26..d12b58d8 100644 --- a/bun.lock +++ b/bun.lock @@ -1805,7 +1805,7 @@ "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="], - "@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="], + "@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="], "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], diff --git a/internal/site/app/(public)/layout.tsx b/internal/site/app/(public)/layout.tsx index 8d436dc4..cdac856d 100644 --- a/internal/site/app/(public)/layout.tsx +++ b/internal/site/app/(public)/layout.tsx @@ -5,12 +5,30 @@ import { LogoBlink } from "@/components/icons"; import { ThemeProvider } from "@/components/theme-provider"; import { Button } from "@/components/ui/button"; import { Geist } from "next/font/google"; -import Head from "next/head"; +import localFont from "next/font/local"; import Link from "next/link"; import type { ReactNode } from "react"; import { useEffect, useState } from "react"; import "./styles.css"; +// Load LayGrotesk via next/font/local for proper preloading +const layGrotesk = localFont({ + src: [ + { + path: "../../public/fonts/LayGrotesk-Regular.woff2", + weight: "400", + style: "normal", + }, + { + path: "../../public/fonts/LayGrotesk-Medium.woff2", + weight: "500", + style: "normal", + }, + ], + display: "swap", + variable: "--font-laygrotesk", +}); + // Keep Geist as fallback font const geist = Geist({ subsets: ["latin"], @@ -120,152 +138,134 @@ export default function Layout({ children }: { children: ReactNode }) { }, [isMobileMenuOpen]); return ( - <> - - {/* Preload LayGrotesk fonts for faster loading */} - - - - -
-
-
-
- - - - {/* Mobile: Single line */} - - {typedTextMobile} - - {/* Desktop: Two lines */} - -
{typedText.split("\n")[0] || "\u00A0"}
-
{typedText.split("\n")[1] || "\u00A0"}
-
-
+ +
+
+
+ - {/* Navigation - Hidden on mobile, visible on large screens */} - {/*
+ {/* Navigation - Hidden on mobile, visible on large screens */} + {/*
*/} - {/* Mobile menu button */} - + + {/* Auth buttons */} + +
- {/* Mobile Navigation Menu */} - {isMobileMenuOpen && ( -
-
- -
- setIsMobileMenuOpen(false)} +
+ setIsMobileMenuOpen(false)} + > + - -
+ Login + +
- )} - -
- {children}
+ )} -
+
+ {children}
+ +
- - +
+ ); } diff --git a/internal/site/app/(public)/login/page.tsx b/internal/site/app/(public)/login/page.tsx index f12e1a43..5ce63f67 100644 --- a/internal/site/app/(public)/login/page.tsx +++ b/internal/site/app/(public)/login/page.tsx @@ -1,8 +1,10 @@ -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; import type { Metadata } from "next"; import { cookies } from "next/headers"; import Link from "next/link"; +import { redirect } from "next/navigation"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { getQuerier } from "@/lib/database"; import { LoginForm } from "./form"; export const metadata: Metadata = { @@ -22,6 +24,12 @@ interface LoginPageProps { } export default async function LoginPage({ searchParams }: LoginPageProps) { + const db = await getQuerier(); + const teamOrgs = await db.selectTeamOrganizations(); + if (teamOrgs.length === 0) { + redirect("/setup"); + } + const { error, waitlist, diff --git a/internal/site/app/(public)/page.tsx b/internal/site/app/(public)/page.tsx index 6cd9c05a..3d38fadb 100644 --- a/internal/site/app/(public)/page.tsx +++ b/internal/site/app/(public)/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; + import Home from "./home/page"; export const metadata: Metadata = { diff --git a/internal/site/app/(public)/signup/form.tsx b/internal/site/app/(public)/signup/form.tsx index 3a3630dc..427946cd 100644 --- a/internal/site/app/(public)/signup/form.tsx +++ b/internal/site/app/(public)/signup/form.tsx @@ -16,12 +16,18 @@ export function SignupForm({ redirect }: { redirect?: string }) { const [fieldErrors, setFieldErrors] = useState({}); const [serverError, setServerError] = useState(); const [isSubmitting, setIsSubmitting] = useState(false); - const [touched, setTouched] = useState<{ email: boolean; password: boolean }>( - { - email: false, - password: false, + + const clearEmailError = () => { + if (fieldErrors.email) { + setFieldErrors((prev) => ({ ...prev, email: undefined })); } - ); + }; + + const clearPasswordError = () => { + if (fieldErrors.password) { + setFieldErrors((prev) => ({ ...prev, password: undefined })); + } + }; const validateEmail = (email: string): string | undefined => { if (!email) return "Email is required"; @@ -37,34 +43,6 @@ export function SignupForm({ redirect }: { redirect?: string }) { return undefined; }; - const handleEmailChange = (e: React.ChangeEvent) => { - const email = e.target.value; - if (touched.email) { - const error = validateEmail(email); - setFieldErrors((prev) => ({ ...prev, email: error })); - } - }; - - const handlePasswordChange = (e: React.ChangeEvent) => { - const password = e.target.value; - if (touched.password) { - const error = validatePassword(password); - setFieldErrors((prev) => ({ ...prev, password: error })); - } - }; - - const handleEmailBlur = (e: React.FocusEvent) => { - setTouched((prev) => ({ ...prev, email: true })); - const error = validateEmail(e.target.value); - setFieldErrors((prev) => ({ ...prev, email: error })); - }; - - const handlePasswordBlur = (e: React.FocusEvent) => { - setTouched((prev) => ({ ...prev, password: true })); - const error = validatePassword(e.target.value); - setFieldErrors((prev) => ({ ...prev, password: error })); - }; - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setServerError(undefined); @@ -73,7 +51,7 @@ export function SignupForm({ redirect }: { redirect?: string }) { const email = formData.get("email") as string; const password = formData.get("password") as string; - // Validate both fields + // Validate all fields const emailError = validateEmail(email); const passwordError = validatePassword(password); @@ -82,13 +60,17 @@ export function SignupForm({ redirect }: { redirect?: string }) { email: emailError, password: passwordError, }); - setTouched({ email: true, password: true }); return; } + setFieldErrors({}); setIsSubmitting(true); try { - const result = await client.auth.signup({ email, password, redirect }); + const result = await client.auth.signup({ + email, + password, + redirect, + }); if (result.ok && result.redirect_url) { window.location.href = result.redirect_url; } @@ -117,14 +99,13 @@ export function SignupForm({ redirect }: { redirect?: string }) { placeholder="Enter your email" className={cn( "w-full", - fieldErrors.email && touched.email + fieldErrors.email ? "border-red-500 focus-visible:ring-red-500 dark:border-red-500 dark:focus-visible:ring-red-500" : "" )} - onChange={handleEmailChange} - onBlur={handleEmailBlur} + onChange={clearEmailError} /> - {fieldErrors.email && touched.email && ( + {fieldErrors.email && (

{fieldErrors.email}

@@ -145,22 +126,17 @@ export function SignupForm({ redirect }: { redirect?: string }) { placeholder="Enter your password" className={cn( "w-full", - fieldErrors.password && touched.password + fieldErrors.password ? "border-red-500 focus-visible:ring-red-500 dark:border-red-500 dark:focus-visible:ring-red-500" : "" )} minLength={8} - onChange={handlePasswordChange} - onBlur={handlePasswordBlur} + onChange={clearPasswordError} /> - {fieldErrors.password && touched.password ? ( + {fieldErrors.password && (

{fieldErrors.password}

- ) : ( -

- Password must be at least 8 characters long -

)}
@@ -175,7 +151,7 @@ export function SignupForm({ redirect }: { redirect?: string }) {