From ea14904730b0007176fc39436cc889b7e40d387d Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Tue, 16 Sep 2025 11:22:54 +0100
Subject: [PATCH 1/2] feat: Add signup page and update navbar
---
apps/web/app/(org)/login/form.tsx | 14 +-
apps/web/app/(org)/signup/form.tsx | 494 +++++++++++++++++++++++++++++
apps/web/app/(org)/signup/page.tsx | 29 ++
apps/web/app/(site)/Navbar.tsx | 459 ++++++++++++++-------------
4 files changed, 773 insertions(+), 223 deletions(-)
create mode 100644 apps/web/app/(org)/signup/form.tsx
create mode 100644 apps/web/app/(org)/signup/page.tsx
diff --git a/apps/web/app/(org)/login/form.tsx b/apps/web/app/(org)/login/form.tsx
index 70b375e4b87..0601ded15d2 100644
--- a/apps/web/app/(org)/login/form.tsx
+++ b/apps/web/app/(org)/login/form.tsx
@@ -437,7 +437,19 @@ const NormalLogin = ({
)} */}
-
+
+ Don't have an account?{" "}
+
+ Sign up here
+
+
+
OR
diff --git a/apps/web/app/(org)/signup/form.tsx b/apps/web/app/(org)/signup/form.tsx
new file mode 100644
index 00000000000..16bc429bb5d
--- /dev/null
+++ b/apps/web/app/(org)/signup/form.tsx
@@ -0,0 +1,494 @@
+"use client";
+
+import { Button, Input, LogoBadge } from "@cap/ui";
+import {
+ faArrowLeft,
+ faEnvelope,
+ faExclamationCircle,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { AnimatePresence, motion } from "framer-motion";
+import Cookies from "js-cookie";
+import { LucideArrowUpRight } from "lucide-react";
+import Image from "next/image";
+import Link from "next/link";
+import { useRouter, useSearchParams } from "next/navigation";
+import { signIn } from "next-auth/react";
+import { Suspense, useEffect, useState } from "react";
+import { toast } from "sonner";
+import { getOrganizationSSOData } from "@/actions/organization/get-organization-sso-data";
+import { trackEvent } from "@/app/utils/analytics";
+
+const MotionInput = motion(Input);
+const MotionLogoBadge = motion(LogoBadge);
+const MotionLink = motion(Link);
+const MotionButton = motion(Button);
+
+export function SignupForm() {
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const next = searchParams?.get("next");
+ const [email, setEmail] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [emailSent, setEmailSent] = useState(false);
+ const [oauthError, setOauthError] = useState(false);
+ const [showOrgInput, setShowOrgInput] = useState(false);
+ const [organizationId, setOrganizationId] = useState("");
+ const [organizationName, setOrganizationName] = useState
(null);
+ const [lastEmailSentTime, setLastEmailSentTime] = useState(
+ null
+ );
+ const theme = Cookies.get("theme") || "light";
+
+ useEffect(() => {
+ theme === "dark"
+ ? (document.body.className = "dark")
+ : (document.body.className = "light");
+ //remove the dark mode when we leave the dashboard
+ return () => {
+ document.body.className = "light";
+ };
+ }, [theme]);
+
+ useEffect(() => {
+ const error = searchParams?.get("error");
+ const errorDesc = searchParams?.get("error_description");
+
+ const handleErrors = () => {
+ if (error === "OAuthAccountNotLinked" && !errorDesc) {
+ setOauthError(true);
+ return toast.error(
+ "This email is already associated with a different sign-in method"
+ );
+ } else if (
+ error === "profile_not_allowed_outside_organization" &&
+ !errorDesc
+ ) {
+ return toast.error(
+ "Your email domain is not authorized for SSO access. Please use your work email or contact your administrator."
+ );
+ } else if (error && errorDesc) {
+ return toast.error(errorDesc);
+ }
+ };
+ handleErrors();
+ }, [searchParams]);
+
+ useEffect(() => {
+ const pendingPriceId = localStorage.getItem("pendingPriceId");
+ const pendingQuantity = localStorage.getItem("pendingQuantity") ?? "1";
+ if (emailSent && pendingPriceId) {
+ localStorage.removeItem("pendingPriceId");
+ localStorage.removeItem("pendingQuantity");
+
+ // Wait a bit to ensure the user is created
+ setTimeout(async () => {
+ const response = await fetch(`/api/settings/billing/subscribe`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ priceId: pendingPriceId,
+ quantity: parseInt(pendingQuantity),
+ }),
+ });
+ const data = await response.json();
+
+ if (data.url) {
+ window.location.href = data.url;
+ }
+ }, 2000);
+ }
+ }, [emailSent]);
+
+ const handleGoogleSignIn = () => {
+ trackEvent("auth_started", { method: "google", is_signup: true });
+ signIn("google", {
+ ...(next && next.length > 0 ? { callbackUrl: next } : {}),
+ });
+ };
+
+ const handleOrganizationLookup = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!organizationId) {
+ toast.error("Please enter an organization ID");
+ return;
+ }
+
+ try {
+ const data = await getOrganizationSSOData(organizationId);
+ setOrganizationName(data.name);
+
+ signIn("workos", undefined, {
+ organization: data.organizationId,
+ connection: data.connectionId,
+ });
+ } catch (error) {
+ console.error("Lookup Error:", error);
+ toast.error("Organization not found or SSO not configured");
+ }
+ };
+
+ return (
+
+ setShowOrgInput(false)}
+ className="absolute overflow-hidden top-5 rounded-full left-5 z-20 hover:bg-gray-1 gap-2 items-center py-1.5 px-3 text-gray-12 bg-transparent border border-gray-4 transition-colors duration-300 cursor-pointer"
+ >
+
+
+ Back
+
+
+
+
+
+
+
+ Sign up to Cap
+
+
+ Beautiful screen recordings, owned by you.
+
+
+
+
+
+
+
+ >
+ }
+ >
+
+
+
+ {showOrgInput ? (
+
+
+
+ ) : (
+ {
+ e.preventDefault();
+ if (!email) return;
+
+ // Check if we're rate limited on the client side
+ if (lastEmailSentTime) {
+ const timeSinceLastRequest =
+ Date.now() - lastEmailSentTime;
+ const waitTime = 30000; // 30 seconds
+ if (timeSinceLastRequest < waitTime) {
+ const remainingSeconds = Math.ceil(
+ (waitTime - timeSinceLastRequest) / 1000
+ );
+ toast.error(
+ `Please wait ${remainingSeconds} seconds before requesting a new code`
+ );
+ return;
+ }
+ }
+
+ setLoading(true);
+ trackEvent("auth_started", {
+ method: "email",
+ is_signup: true,
+ });
+ signIn("email", {
+ email,
+ redirect: false,
+ ...(next && next.length > 0
+ ? { callbackUrl: next }
+ : {}),
+ })
+ .then((res) => {
+ setLoading(false);
+
+ if (res?.ok && !res?.error) {
+ setEmailSent(true);
+ setLastEmailSentTime(Date.now());
+ trackEvent("auth_email_sent", {
+ email_domain: email.split("@")[1],
+ });
+ const params = new URLSearchParams({
+ email,
+ ...(next && { next }),
+ lastSent: Date.now().toString(),
+ });
+ router.push(`/verify-otp?${params.toString()}`);
+ } else {
+ // NextAuth always returns "EmailSignin" for all email provider errors
+ // Since we already check rate limiting on the client side before sending,
+ // if we get an error here, it's likely rate limiting from the server
+ toast.error(
+ "Please wait 30 seconds before requesting a new code"
+ );
+ }
+ })
+ .catch((error) => {
+ setEmailSent(false);
+ setLoading(false);
+ // Catch block is rarely triggered with NextAuth
+ toast.error("Error sending email - try again?");
+ });
+ }}
+ className="flex flex-col space-y-3"
+ >
+
+
+ )}
+
+
+
+ Already have an account?{" "}
+
+ Log in here
+
+
+
+ By typing your email and clicking continue, you acknowledge that
+ you have both read and agree to Cap's{" "}
+
+ Terms of Service
+ {" "}
+ and{" "}
+
+ Privacy Policy
+
+ .
+
+
+
+
+
+ );
+}
+
+const SignupWithSSO = ({
+ handleOrganizationLookup,
+ organizationId,
+ setOrganizationId,
+ organizationName,
+}: {
+ handleOrganizationLookup: (e: React.FormEvent) => void;
+ organizationId: string;
+ setOrganizationId: (organizationId: string) => void;
+ organizationName: string | null;
+}) => {
+ return (
+
+ setOrganizationId(e.target.value)}
+ className="w-full max-w-full"
+ />
+ {organizationName && (
+
+ Signing up with: {organizationName}
+
+ )}
+
+
+
+
+ );
+};
+
+const NormalSignup = ({
+ setShowOrgInput,
+ email,
+ emailSent,
+ setEmail,
+ loading,
+ oauthError,
+ handleGoogleSignIn,
+}: {
+ setShowOrgInput: (show: boolean) => void;
+ email: string;
+ emailSent: boolean;
+ setEmail: (email: string) => void;
+ loading: boolean;
+ oauthError: boolean;
+ handleGoogleSignIn: () => void;
+}) => {
+ return (
+
+
+ {
+ setEmail(e.target.value);
+ }}
+ />
+ }
+ >
+ Sign up with email
+
+
+
+
+ {!oauthError && (
+ <>
+
+
+ Sign up with Google
+
+ >
+ )}
+
+ {oauthError && (
+
+
+
+ It looks like you've previously used this email to sign up via
+ email. Please enter your email below to receive a sign up link.
+
+
+ )}
+ setShowOrgInput(true)}
+ disabled={loading}
+ >
+
+ Sign up with SAML SSO
+
+
+
+ );
+};
diff --git a/apps/web/app/(org)/signup/page.tsx b/apps/web/app/(org)/signup/page.tsx
new file mode 100644
index 00000000000..aa4736bfc09
--- /dev/null
+++ b/apps/web/app/(org)/signup/page.tsx
@@ -0,0 +1,29 @@
+import { getCurrentUser } from "@cap/database/auth/session";
+import { faArrowLeft } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import Link from "next/link";
+import { redirect } from "next/navigation";
+import { SignupForm } from "./form";
+
+export const dynamic = "force-dynamic";
+
+export default async function SignupPage() {
+ const session = await getCurrentUser();
+ if (session) {
+ redirect("/dashboard");
+ }
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/app/(site)/Navbar.tsx b/apps/web/app/(site)/Navbar.tsx
index 34ea845f172..39ed887a7f8 100644
--- a/apps/web/app/(site)/Navbar.tsx
+++ b/apps/web/app/(site)/Navbar.tsx
@@ -1,20 +1,18 @@
"use client";
import {
- Button,
- ListItem,
- Logo,
- NavigationMenu,
- NavigationMenuContent,
- NavigationMenuItem,
- NavigationMenuLink,
- NavigationMenuList,
- NavigationMenuTrigger,
- navigationMenuTriggerStyle,
+ Button,
+ ListItem,
+ Logo,
+ NavigationMenu,
+ NavigationMenuContent,
+ NavigationMenuItem,
+ NavigationMenuLink,
+ NavigationMenuList,
+ NavigationMenuTrigger,
+ navigationMenuTriggerStyle,
} from "@cap/ui";
import { classNames } from "@cap/utils";
-import { faGithub } from "@fortawesome/free-brands-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import Link from "next/link";
import { usePathname } from "next/navigation";
@@ -23,221 +21,238 @@ import MobileMenu from "@/components/ui/MobileMenu";
import { useAuthContext } from "../Layout/AuthContext";
const Links = [
- {
- label: "Product",
- dropdown: [
- {
- label: "Download App",
- sub: "Downloads for macOS & Windows",
- href: "/download",
- },
- {
- label: "Open Source",
- sub: "Cap is open source and available on GitHub",
- href: "https://github.com/CapSoftware/Cap",
- },
- {
- label: "Self-host Cap",
- sub: "Self-host Cap on your own infrastructure",
- href: "/self-hosting",
- },
- {
- label: "Join the community",
- sub: "Join the Cap community on Discord",
- href: "https://cap.link/discord",
- },
- ],
- },
- {
- label: "Download",
- href: "/download",
- },
- {
- label: "Testimonials",
- href: "/testimonials",
- },
- {
- label: "Help",
- dropdown: [
- {
- label: "Documentation",
- sub: "Documentation for using Cap",
- href: "/docs",
- },
- {
- label: "FAQs",
- sub: "Frequently asked questions about Cap",
- href: "/faq",
- },
- {
- label: "Email support",
- sub: "Support via email",
- href: "mailto:hello@cap.so",
- },
- {
- label: "Chat support",
- sub: "Support via chat",
- href: "https://discord.gg/y8gdQ3WRN3",
- },
- ],
- },
- {
- label: "Pricing",
- href: "/pricing",
- },
- {
- label: "About",
- href: "/about",
- },
- {
- label: "Blog",
- href: "/blog",
- },
+ {
+ label: "Product",
+ dropdown: [
+ {
+ label: "Download App",
+ sub: "Downloads for macOS & Windows",
+ href: "/download",
+ },
+ {
+ label: "Open Source",
+ sub: "Cap is open source and available on GitHub",
+ href: "https://github.com/CapSoftware/Cap",
+ },
+ {
+ label: "Self-host Cap",
+ sub: "Self-host Cap on your own infrastructure",
+ href: "/self-hosting",
+ },
+ {
+ label: "Join the community",
+ sub: "Join the Cap community on Discord",
+ href: "https://cap.link/discord",
+ },
+ ],
+ },
+ {
+ label: "Download",
+ href: "/download",
+ },
+ {
+ label: "Testimonials",
+ href: "/testimonials",
+ },
+ {
+ label: "Help",
+ dropdown: [
+ {
+ label: "Documentation",
+ sub: "Documentation for using Cap",
+ href: "/docs",
+ },
+ {
+ label: "FAQs",
+ sub: "Frequently asked questions about Cap",
+ href: "/faq",
+ },
+ {
+ label: "Email support",
+ sub: "Support via email",
+ href: "mailto:hello@cap.so",
+ },
+ {
+ label: "Chat support",
+ sub: "Support via chat",
+ href: "https://discord.gg/y8gdQ3WRN3",
+ },
+ ],
+ },
+ {
+ label: "Pricing",
+ href: "/pricing",
+ },
+ {
+ label: "About",
+ href: "/about",
+ },
+ {
+ label: "Blog",
+ href: "/blog",
+ },
+];
+
+const AuthLinks = [
+ {
+ label: "Log In",
+ href: "/login",
+ },
];
export const Navbar = () => {
- const pathname = usePathname();
- const [showMobileMenu, setShowMobileMenu] = useState(false);
- const auth = use(useAuthContext().user);
+ const pathname = usePathname();
+ const [showMobileMenu, setShowMobileMenu] = useState(false);
+ const auth = use(useAuthContext().user);
- return (
- <>
-
-
-
- {showMobileMenu && (
-
- )}
- >
- );
+ return (
+ <>
+
+
+
+ {showMobileMenu && (
+
+ )}
+ >
+ );
};
function LoginOrDashboard() {
- const auth = use(useAuthContext().user);
- return (
-
- );
+ const auth = use(useAuthContext().user);
+
+ return (
+
+ );
}
From a758467c5d8b8ea9a87cfd041ca4f0525e55ab8a Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Tue, 16 Sep 2025 11:25:09 +0100
Subject: [PATCH 2/2] format
---
apps/web/app/(org)/signup/form.tsx | 888 +++++++++++-----------
apps/web/app/(org)/signup/page.tsx | 2 +-
apps/web/app/(site)/Navbar.tsx | 466 ++++++------
apps/web/components/ReadyToGetStarted.tsx | 96 +--
4 files changed, 726 insertions(+), 726 deletions(-)
diff --git a/apps/web/app/(org)/signup/form.tsx b/apps/web/app/(org)/signup/form.tsx
index 16bc429bb5d..d5817ac0736 100644
--- a/apps/web/app/(org)/signup/form.tsx
+++ b/apps/web/app/(org)/signup/form.tsx
@@ -2,9 +2,9 @@
import { Button, Input, LogoBadge } from "@cap/ui";
import {
- faArrowLeft,
- faEnvelope,
- faExclamationCircle,
+ faArrowLeft,
+ faEnvelope,
+ faExclamationCircle,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
@@ -25,470 +25,470 @@ const MotionLink = motion(Link);
const MotionButton = motion(Button);
export function SignupForm() {
- const searchParams = useSearchParams();
- const router = useRouter();
- const next = searchParams?.get("next");
- const [email, setEmail] = useState("");
- const [loading, setLoading] = useState(false);
- const [emailSent, setEmailSent] = useState(false);
- const [oauthError, setOauthError] = useState(false);
- const [showOrgInput, setShowOrgInput] = useState(false);
- const [organizationId, setOrganizationId] = useState("");
- const [organizationName, setOrganizationName] = useState(null);
- const [lastEmailSentTime, setLastEmailSentTime] = useState(
- null
- );
- const theme = Cookies.get("theme") || "light";
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const next = searchParams?.get("next");
+ const [email, setEmail] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [emailSent, setEmailSent] = useState(false);
+ const [oauthError, setOauthError] = useState(false);
+ const [showOrgInput, setShowOrgInput] = useState(false);
+ const [organizationId, setOrganizationId] = useState("");
+ const [organizationName, setOrganizationName] = useState(null);
+ const [lastEmailSentTime, setLastEmailSentTime] = useState(
+ null,
+ );
+ const theme = Cookies.get("theme") || "light";
- useEffect(() => {
- theme === "dark"
- ? (document.body.className = "dark")
- : (document.body.className = "light");
- //remove the dark mode when we leave the dashboard
- return () => {
- document.body.className = "light";
- };
- }, [theme]);
+ useEffect(() => {
+ theme === "dark"
+ ? (document.body.className = "dark")
+ : (document.body.className = "light");
+ //remove the dark mode when we leave the dashboard
+ return () => {
+ document.body.className = "light";
+ };
+ }, [theme]);
- useEffect(() => {
- const error = searchParams?.get("error");
- const errorDesc = searchParams?.get("error_description");
+ useEffect(() => {
+ const error = searchParams?.get("error");
+ const errorDesc = searchParams?.get("error_description");
- const handleErrors = () => {
- if (error === "OAuthAccountNotLinked" && !errorDesc) {
- setOauthError(true);
- return toast.error(
- "This email is already associated with a different sign-in method"
- );
- } else if (
- error === "profile_not_allowed_outside_organization" &&
- !errorDesc
- ) {
- return toast.error(
- "Your email domain is not authorized for SSO access. Please use your work email or contact your administrator."
- );
- } else if (error && errorDesc) {
- return toast.error(errorDesc);
- }
- };
- handleErrors();
- }, [searchParams]);
+ const handleErrors = () => {
+ if (error === "OAuthAccountNotLinked" && !errorDesc) {
+ setOauthError(true);
+ return toast.error(
+ "This email is already associated with a different sign-in method",
+ );
+ } else if (
+ error === "profile_not_allowed_outside_organization" &&
+ !errorDesc
+ ) {
+ return toast.error(
+ "Your email domain is not authorized for SSO access. Please use your work email or contact your administrator.",
+ );
+ } else if (error && errorDesc) {
+ return toast.error(errorDesc);
+ }
+ };
+ handleErrors();
+ }, [searchParams]);
- useEffect(() => {
- const pendingPriceId = localStorage.getItem("pendingPriceId");
- const pendingQuantity = localStorage.getItem("pendingQuantity") ?? "1";
- if (emailSent && pendingPriceId) {
- localStorage.removeItem("pendingPriceId");
- localStorage.removeItem("pendingQuantity");
+ useEffect(() => {
+ const pendingPriceId = localStorage.getItem("pendingPriceId");
+ const pendingQuantity = localStorage.getItem("pendingQuantity") ?? "1";
+ if (emailSent && pendingPriceId) {
+ localStorage.removeItem("pendingPriceId");
+ localStorage.removeItem("pendingQuantity");
- // Wait a bit to ensure the user is created
- setTimeout(async () => {
- const response = await fetch(`/api/settings/billing/subscribe`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- priceId: pendingPriceId,
- quantity: parseInt(pendingQuantity),
- }),
- });
- const data = await response.json();
+ // Wait a bit to ensure the user is created
+ setTimeout(async () => {
+ const response = await fetch(`/api/settings/billing/subscribe`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ priceId: pendingPriceId,
+ quantity: parseInt(pendingQuantity),
+ }),
+ });
+ const data = await response.json();
- if (data.url) {
- window.location.href = data.url;
- }
- }, 2000);
- }
- }, [emailSent]);
+ if (data.url) {
+ window.location.href = data.url;
+ }
+ }, 2000);
+ }
+ }, [emailSent]);
- const handleGoogleSignIn = () => {
- trackEvent("auth_started", { method: "google", is_signup: true });
- signIn("google", {
- ...(next && next.length > 0 ? { callbackUrl: next } : {}),
- });
- };
+ const handleGoogleSignIn = () => {
+ trackEvent("auth_started", { method: "google", is_signup: true });
+ signIn("google", {
+ ...(next && next.length > 0 ? { callbackUrl: next } : {}),
+ });
+ };
- const handleOrganizationLookup = async (e: React.FormEvent) => {
- e.preventDefault();
- if (!organizationId) {
- toast.error("Please enter an organization ID");
- return;
- }
+ const handleOrganizationLookup = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!organizationId) {
+ toast.error("Please enter an organization ID");
+ return;
+ }
- try {
- const data = await getOrganizationSSOData(organizationId);
- setOrganizationName(data.name);
+ try {
+ const data = await getOrganizationSSOData(organizationId);
+ setOrganizationName(data.name);
- signIn("workos", undefined, {
- organization: data.organizationId,
- connection: data.connectionId,
- });
- } catch (error) {
- console.error("Lookup Error:", error);
- toast.error("Organization not found or SSO not configured");
- }
- };
+ signIn("workos", undefined, {
+ organization: data.organizationId,
+ connection: data.connectionId,
+ });
+ } catch (error) {
+ console.error("Lookup Error:", error);
+ toast.error("Organization not found or SSO not configured");
+ }
+ };
- return (
-
- setShowOrgInput(false)}
- className="absolute overflow-hidden top-5 rounded-full left-5 z-20 hover:bg-gray-1 gap-2 items-center py-1.5 px-3 text-gray-12 bg-transparent border border-gray-4 transition-colors duration-300 cursor-pointer"
- >
-
-
- Back
-
-
-
-
-
-
-
- Sign up to Cap
-
-
- Beautiful screen recordings, owned by you.
-
-
-
-
-
-
-
- >
- }
- >
-
-
-
- {showOrgInput ? (
-
-
-
- ) : (
- {
- e.preventDefault();
- if (!email) return;
+ return (
+
+ setShowOrgInput(false)}
+ className="absolute overflow-hidden top-5 rounded-full left-5 z-20 hover:bg-gray-1 gap-2 items-center py-1.5 px-3 text-gray-12 bg-transparent border border-gray-4 transition-colors duration-300 cursor-pointer"
+ >
+
+
+ Back
+
+
+
+
+
+
+
+ Sign up to Cap
+
+
+ Beautiful screen recordings, owned by you.
+
+
+
+
+
+
+
+ >
+ }
+ >
+
+
+
+ {showOrgInput ? (
+
+
+
+ ) : (
+ {
+ e.preventDefault();
+ if (!email) return;
- // Check if we're rate limited on the client side
- if (lastEmailSentTime) {
- const timeSinceLastRequest =
- Date.now() - lastEmailSentTime;
- const waitTime = 30000; // 30 seconds
- if (timeSinceLastRequest < waitTime) {
- const remainingSeconds = Math.ceil(
- (waitTime - timeSinceLastRequest) / 1000
- );
- toast.error(
- `Please wait ${remainingSeconds} seconds before requesting a new code`
- );
- return;
- }
- }
+ // Check if we're rate limited on the client side
+ if (lastEmailSentTime) {
+ const timeSinceLastRequest =
+ Date.now() - lastEmailSentTime;
+ const waitTime = 30000; // 30 seconds
+ if (timeSinceLastRequest < waitTime) {
+ const remainingSeconds = Math.ceil(
+ (waitTime - timeSinceLastRequest) / 1000,
+ );
+ toast.error(
+ `Please wait ${remainingSeconds} seconds before requesting a new code`,
+ );
+ return;
+ }
+ }
- setLoading(true);
- trackEvent("auth_started", {
- method: "email",
- is_signup: true,
- });
- signIn("email", {
- email,
- redirect: false,
- ...(next && next.length > 0
- ? { callbackUrl: next }
- : {}),
- })
- .then((res) => {
- setLoading(false);
+ setLoading(true);
+ trackEvent("auth_started", {
+ method: "email",
+ is_signup: true,
+ });
+ signIn("email", {
+ email,
+ redirect: false,
+ ...(next && next.length > 0
+ ? { callbackUrl: next }
+ : {}),
+ })
+ .then((res) => {
+ setLoading(false);
- if (res?.ok && !res?.error) {
- setEmailSent(true);
- setLastEmailSentTime(Date.now());
- trackEvent("auth_email_sent", {
- email_domain: email.split("@")[1],
- });
- const params = new URLSearchParams({
- email,
- ...(next && { next }),
- lastSent: Date.now().toString(),
- });
- router.push(`/verify-otp?${params.toString()}`);
- } else {
- // NextAuth always returns "EmailSignin" for all email provider errors
- // Since we already check rate limiting on the client side before sending,
- // if we get an error here, it's likely rate limiting from the server
- toast.error(
- "Please wait 30 seconds before requesting a new code"
- );
- }
- })
- .catch((error) => {
- setEmailSent(false);
- setLoading(false);
- // Catch block is rarely triggered with NextAuth
- toast.error("Error sending email - try again?");
- });
- }}
- className="flex flex-col space-y-3"
- >
-
-
- )}
-
-
-
- Already have an account?{" "}
-
- Log in here
-
-
-
- By typing your email and clicking continue, you acknowledge that
- you have both read and agree to Cap's{" "}
-
- Terms of Service
- {" "}
- and{" "}
-
- Privacy Policy
-
- .
-
-
-
-
-
- );
+ if (res?.ok && !res?.error) {
+ setEmailSent(true);
+ setLastEmailSentTime(Date.now());
+ trackEvent("auth_email_sent", {
+ email_domain: email.split("@")[1],
+ });
+ const params = new URLSearchParams({
+ email,
+ ...(next && { next }),
+ lastSent: Date.now().toString(),
+ });
+ router.push(`/verify-otp?${params.toString()}`);
+ } else {
+ // NextAuth always returns "EmailSignin" for all email provider errors
+ // Since we already check rate limiting on the client side before sending,
+ // if we get an error here, it's likely rate limiting from the server
+ toast.error(
+ "Please wait 30 seconds before requesting a new code",
+ );
+ }
+ })
+ .catch((error) => {
+ setEmailSent(false);
+ setLoading(false);
+ // Catch block is rarely triggered with NextAuth
+ toast.error("Error sending email - try again?");
+ });
+ }}
+ className="flex flex-col space-y-3"
+ >
+
+
+ )}
+
+
+
+ Already have an account?{" "}
+
+ Log in here
+
+
+
+ By typing your email and clicking continue, you acknowledge that
+ you have both read and agree to Cap's{" "}
+
+ Terms of Service
+ {" "}
+ and{" "}
+
+ Privacy Policy
+
+ .
+
+
+
+
+
+ );
}
const SignupWithSSO = ({
- handleOrganizationLookup,
- organizationId,
- setOrganizationId,
- organizationName,
+ handleOrganizationLookup,
+ organizationId,
+ setOrganizationId,
+ organizationName,
}: {
- handleOrganizationLookup: (e: React.FormEvent) => void;
- organizationId: string;
- setOrganizationId: (organizationId: string) => void;
- organizationName: string | null;
+ handleOrganizationLookup: (e: React.FormEvent) => void;
+ organizationId: string;
+ setOrganizationId: (organizationId: string) => void;
+ organizationName: string | null;
}) => {
- return (
-
- setOrganizationId(e.target.value)}
- className="w-full max-w-full"
- />
- {organizationName && (
-
- Signing up with: {organizationName}
-
- )}
-
-
-
-
- );
+ return (
+
+ setOrganizationId(e.target.value)}
+ className="w-full max-w-full"
+ />
+ {organizationName && (
+
+ Signing up with: {organizationName}
+
+ )}
+
+
+
+
+ );
};
const NormalSignup = ({
- setShowOrgInput,
- email,
- emailSent,
- setEmail,
- loading,
- oauthError,
- handleGoogleSignIn,
+ setShowOrgInput,
+ email,
+ emailSent,
+ setEmail,
+ loading,
+ oauthError,
+ handleGoogleSignIn,
}: {
- setShowOrgInput: (show: boolean) => void;
- email: string;
- emailSent: boolean;
- setEmail: (email: string) => void;
- loading: boolean;
- oauthError: boolean;
- handleGoogleSignIn: () => void;
+ setShowOrgInput: (show: boolean) => void;
+ email: string;
+ emailSent: boolean;
+ setEmail: (email: string) => void;
+ loading: boolean;
+ oauthError: boolean;
+ handleGoogleSignIn: () => void;
}) => {
- return (
-
-
- {
- setEmail(e.target.value);
- }}
- />
- }
- >
- Sign up with email
-
-
-
-
- {!oauthError && (
- <>
-
-
- Sign up with Google
-
- >
- )}
+ return (
+
+
+ {
+ setEmail(e.target.value);
+ }}
+ />
+ }
+ >
+ Sign up with email
+
+
+
+
+ {!oauthError && (
+ <>
+
+
+ Sign up with Google
+
+ >
+ )}
- {oauthError && (
-
-
-
- It looks like you've previously used this email to sign up via
- email. Please enter your email below to receive a sign up link.
-
-
- )}
- setShowOrgInput(true)}
- disabled={loading}
- >
-
- Sign up with SAML SSO
-
-
-
- );
+ {oauthError && (
+
+
+
+ It looks like you've previously used this email to sign up via
+ email. Please enter your email below to receive a sign up link.
+
+
+ )}
+ setShowOrgInput(true)}
+ disabled={loading}
+ >
+
+ Sign up with SAML SSO
+
+
+
+ );
};
diff --git a/apps/web/app/(org)/signup/page.tsx b/apps/web/app/(org)/signup/page.tsx
index aa4736bfc09..de68b63d08c 100644
--- a/apps/web/app/(org)/signup/page.tsx
+++ b/apps/web/app/(org)/signup/page.tsx
@@ -26,4 +26,4 @@ export default async function SignupPage() {
);
-}
\ No newline at end of file
+}
diff --git a/apps/web/app/(site)/Navbar.tsx b/apps/web/app/(site)/Navbar.tsx
index 39ed887a7f8..f16dd135c2e 100644
--- a/apps/web/app/(site)/Navbar.tsx
+++ b/apps/web/app/(site)/Navbar.tsx
@@ -1,16 +1,16 @@
"use client";
import {
- Button,
- ListItem,
- Logo,
- NavigationMenu,
- NavigationMenuContent,
- NavigationMenuItem,
- NavigationMenuLink,
- NavigationMenuList,
- NavigationMenuTrigger,
- navigationMenuTriggerStyle,
+ Button,
+ ListItem,
+ Logo,
+ NavigationMenu,
+ NavigationMenuContent,
+ NavigationMenuItem,
+ NavigationMenuLink,
+ NavigationMenuList,
+ NavigationMenuTrigger,
+ navigationMenuTriggerStyle,
} from "@cap/ui";
import { classNames } from "@cap/utils";
import { motion } from "framer-motion";
@@ -21,238 +21,238 @@ import MobileMenu from "@/components/ui/MobileMenu";
import { useAuthContext } from "../Layout/AuthContext";
const Links = [
- {
- label: "Product",
- dropdown: [
- {
- label: "Download App",
- sub: "Downloads for macOS & Windows",
- href: "/download",
- },
- {
- label: "Open Source",
- sub: "Cap is open source and available on GitHub",
- href: "https://github.com/CapSoftware/Cap",
- },
- {
- label: "Self-host Cap",
- sub: "Self-host Cap on your own infrastructure",
- href: "/self-hosting",
- },
- {
- label: "Join the community",
- sub: "Join the Cap community on Discord",
- href: "https://cap.link/discord",
- },
- ],
- },
- {
- label: "Download",
- href: "/download",
- },
- {
- label: "Testimonials",
- href: "/testimonials",
- },
- {
- label: "Help",
- dropdown: [
- {
- label: "Documentation",
- sub: "Documentation for using Cap",
- href: "/docs",
- },
- {
- label: "FAQs",
- sub: "Frequently asked questions about Cap",
- href: "/faq",
- },
- {
- label: "Email support",
- sub: "Support via email",
- href: "mailto:hello@cap.so",
- },
- {
- label: "Chat support",
- sub: "Support via chat",
- href: "https://discord.gg/y8gdQ3WRN3",
- },
- ],
- },
- {
- label: "Pricing",
- href: "/pricing",
- },
- {
- label: "About",
- href: "/about",
- },
- {
- label: "Blog",
- href: "/blog",
- },
+ {
+ label: "Product",
+ dropdown: [
+ {
+ label: "Download App",
+ sub: "Downloads for macOS & Windows",
+ href: "/download",
+ },
+ {
+ label: "Open Source",
+ sub: "Cap is open source and available on GitHub",
+ href: "https://github.com/CapSoftware/Cap",
+ },
+ {
+ label: "Self-host Cap",
+ sub: "Self-host Cap on your own infrastructure",
+ href: "/self-hosting",
+ },
+ {
+ label: "Join the community",
+ sub: "Join the Cap community on Discord",
+ href: "https://cap.link/discord",
+ },
+ ],
+ },
+ {
+ label: "Download",
+ href: "/download",
+ },
+ {
+ label: "Testimonials",
+ href: "/testimonials",
+ },
+ {
+ label: "Help",
+ dropdown: [
+ {
+ label: "Documentation",
+ sub: "Documentation for using Cap",
+ href: "/docs",
+ },
+ {
+ label: "FAQs",
+ sub: "Frequently asked questions about Cap",
+ href: "/faq",
+ },
+ {
+ label: "Email support",
+ sub: "Support via email",
+ href: "mailto:hello@cap.so",
+ },
+ {
+ label: "Chat support",
+ sub: "Support via chat",
+ href: "https://discord.gg/y8gdQ3WRN3",
+ },
+ ],
+ },
+ {
+ label: "Pricing",
+ href: "/pricing",
+ },
+ {
+ label: "About",
+ href: "/about",
+ },
+ {
+ label: "Blog",
+ href: "/blog",
+ },
];
const AuthLinks = [
- {
- label: "Log In",
- href: "/login",
- },
+ {
+ label: "Log In",
+ href: "/login",
+ },
];
export const Navbar = () => {
- const pathname = usePathname();
- const [showMobileMenu, setShowMobileMenu] = useState(false);
- const auth = use(useAuthContext().user);
+ const pathname = usePathname();
+ const [showMobileMenu, setShowMobileMenu] = useState(false);
+ const auth = use(useAuthContext().user);
- return (
- <>
-
-
-
- {showMobileMenu && (
-
- )}
- >
- );
+ return (
+ <>
+
+
+
+ {showMobileMenu && (
+
+ )}
+ >
+ );
};
function LoginOrDashboard() {
- const auth = use(useAuthContext().user);
+ const auth = use(useAuthContext().user);
- return (
-
- );
+ return (
+
+ );
}
diff --git a/apps/web/components/ReadyToGetStarted.tsx b/apps/web/components/ReadyToGetStarted.tsx
index 822ac0723cf..0d4cecd06d6 100644
--- a/apps/web/components/ReadyToGetStarted.tsx
+++ b/apps/web/components/ReadyToGetStarted.tsx
@@ -1,55 +1,55 @@
"use client";
import { Button } from "@cap/ui";
-import { homepageCopy } from "../data/homepage-copy";
import Link from "next/link";
+import { homepageCopy } from "../data/homepage-copy";
export function ReadyToGetStarted() {
- return (
-
-
-
-
- {homepageCopy.readyToGetStarted.title}
-
-
-
-
-
-
-
-
- or,{" "}
-
- Switch from Loom
-
-
-
-
-
- );
+ return (
+
+
+
+
+ {homepageCopy.readyToGetStarted.title}
+
+
+
+
+
+
+
+
+ or,{" "}
+
+ Switch from Loom
+
+
+
+
+
+ );
}