Revamp sign up page (#288)

* New cleaner signup/login screen
This commit is contained in:
Johannes
2023-05-19 12:08:24 +02:00
committed by GitHub
parent b9cdf329e3
commit c6aabc77b4
24 changed files with 430 additions and 230 deletions

View File

@@ -88,4 +88,7 @@ NEXT_PUBLIC_SENTRY_DSN=
# Configure Github Login
NEXT_PUBLIC_GITHUB_AUTH_ENABLED=0
GITHUB_ID=
GITHUB_SECRET=
GITHUB_SECRET=
# Configure Google Login
NEXT_PUBLIC_GOOGLE_AUTH_ENABLED=0

View File

@@ -90,6 +90,10 @@ NEXT_PUBLIC_GITHUB_AUTH_ENABLED=0
GITHUB_ID=
GITHUB_SECRET=
# Configure Google Login
NEXT_PUBLIC_GOOGLE_AUTH_ENABLED=0
# Stripe Billing Variables
NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID=
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=

View File

@@ -68,7 +68,7 @@ export default function AppPage({}) {
<button
className="mr-2 flex max-w-xs items-center rounded-full bg-white text-sm font-medium text-slate-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2 lg:rounded-md lg:p-2 lg:hover:bg-slate-50"
onClick={() => {
formbricks.track("Feedback Button Click");
formbricks.track("Cancel Subscription");
}}>
Feedback
</button>

View File

@@ -1,17 +1,20 @@
import BackToLoginButton from "@/components/auth/BackToLoginButton";
import FormWrapper from "@/components/auth/FormWrapper";
const SignInPage: React.FC = () => {
return (
<div>
<h1 className="leading-2 mb-4 text-center font-bold">Password reset successfully requested</h1>
<p className="text-center">
Check your email for a link to reset your password. If it doesn&apos;t appear within a few minutes,
check your spam folder.
</p>
<div className="mt-5 text-center">
<BackToLoginButton />
<FormWrapper>
<div>
<h1 className="leading-2 mb-4 text-center font-bold">Password reset successfully requested</h1>
<p className="text-center">
Check your email for a link to reset your password. If it doesn&apos;t appear within a few minutes,
check your spam folder.
</p>
<div className="mt-5 text-center">
<BackToLoginButton />
</div>
</div>
</div>
</FormWrapper>
);
};

View File

@@ -1,7 +1,12 @@
import { PasswordResetForm } from "../../../components/auth/PasswordResetForm";
import FormWrapper from "@/components/auth/FormWrapper";
const ForgotPasswordPage: React.FC = () => {
return <PasswordResetForm />;
return (
<FormWrapper>
<PasswordResetForm />
</FormWrapper>
);
};
export default ForgotPasswordPage;

View File

@@ -1,7 +1,12 @@
import { ResetPasswordForm } from "@/components/auth/ResetPasswordForm";
import FormWrapper from "@/components/auth/FormWrapper";
const ResetPasswordPage: React.FC = () => {
return <ResetPasswordForm />;
return (
<FormWrapper>
<ResetPasswordForm />
</FormWrapper>
);
};
export default ResetPasswordPage;

View File

@@ -1,13 +1,16 @@
import BackToLoginButton from "@/components/auth/BackToLoginButton";
import FormWrapper from "@/components/auth/FormWrapper";
export default function ResetPasswordSuccessPage() {
return (
<div>
<h1 className="leading-2 mb-4 text-center font-bold">Password successfully reset</h1>
<p className="text-center">You can now log in with your new password</p>
<div className="mt-3 text-center">
<BackToLoginButton />
<FormWrapper>
<div>
<h1 className="leading-2 mb-4 text-center font-bold">Password successfully reset</h1>
<p className="text-center">You can now log in with your new password</p>
<div className="mt-3 text-center">
<BackToLoginButton />
</div>
</div>
</div>
</FormWrapper>
);
}

View File

@@ -1,4 +1,3 @@
import { Logo } from "@/components/Logo";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { PosthogClientWrapper } from "../PosthogClientWrapper";
@@ -12,16 +11,7 @@ export default async function AuthLayout({ children }: { children: React.ReactNo
<PosthogClientWrapper>
<div className="min-h-screen bg-slate-50">
<div className="isolate bg-white">
<div className="bg-gradient-radial flex min-h-screen from-slate-200 to-slate-50">
<div className="mx-auto flex flex-1 flex-col justify-center px-4 py-12 sm:px-6 lg:flex-none lg:px-20 xl:px-24">
<div className="mx-auto w-full max-w-sm rounded-xl bg-white p-8 shadow-xl lg:w-96">
<div className="mb-8 text-center">
<Logo className="mx-auto w-3/4" />
</div>
{children}
</div>
</div>
</div>
<div className="bg-gradient-radial flex min-h-screen from-slate-200 to-slate-50">{children}</div>
</div>
</div>
</PosthogClientWrapper>

View File

@@ -1,19 +1,16 @@
import { SigninForm } from "@/components/auth/SigninForm";
import Link from "next/link";
import Testimonial from "@/components/auth/Testimonial";
import FormWrapper from "@/components/auth/FormWrapper";
export default function SignInPage() {
return (
<div>
<SigninForm />
{process.env.NEXT_PUBLIC_SIGNUP_DISABLED !== "1" && (
<div>
<Link
href="/auth/signup"
className="hover:text-brand-dark mt-3 grid grid-cols-1 space-y-2 text-center text-xs text-slate-700">
Create an account
</Link>
</div>
)}
<div className="grid min-h-screen w-full bg-gradient-to-tr from-slate-200 to-slate-50 lg:grid-cols-2">
<div className="hidden lg:flex">
<Testimonial />
</div>
<FormWrapper>
<SigninForm />
</FormWrapper>
</div>
);
}

View File

@@ -1,8 +1,9 @@
import BackToLoginButton from "@/components/auth/BackToLoginButton";
import FormWrapper from "@/components/auth/FormWrapper";
export default function SignupWithoutVerificationSuccess() {
return (
<div>
<FormWrapper>
<h1 className="leading-2 mb-4 text-center font-bold">User successfully created</h1>
<p className="text-center">
Your new user has been created successfully. Please click the button below and sign in to your
@@ -10,6 +11,6 @@ export default function SignupWithoutVerificationSuccess() {
</p>
<hr className="my-4" />
<BackToLoginButton />
</div>
</FormWrapper>
);
}

View File

@@ -1,26 +1,33 @@
import Link from "next/link";
import { SignupForm } from "@/components/auth/SignupForm";
import FormWrapper from "@/components/auth/FormWrapper";
import Testimonial from "@/components/auth/Testimonial";
export default function SignUpPage() {
return (
<div>
{process.env.NEXT_PUBLIC_SIGNUP_DISABLED === "1" ? (
<>
<h1 className="leading-2 mb-4 text-center font-bold">Sign up disabled</h1>
<p className="text-center">
The account creation is disabled in this instance. Please contact the site administrator to create
an account.
</p>
<hr className="my-4" />
<Link
href="/"
className="mt-5 flex w-full justify-center rounded-md border border-slate-400 bg-white px-4 py-2 text-sm font-medium text-slate-600 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
Login
</Link>
</>
) : (
<SignupForm />
)}
<div className="grid min-h-screen w-full bg-gradient-to-tr from-slate-200 to-slate-50 lg:grid-cols-2">
<div className="hidden lg:flex">
<Testimonial />
</div>
<FormWrapper>
{process.env.NEXT_PUBLIC_SIGNUP_DISABLED === "1" ? (
<>
<h1 className="leading-2 mb-4 text-center font-bold">Sign up disabled</h1>
<p className="text-center">
The account creation is disabled in this instance. Please contact the site administrator to
create an account.
</p>
<hr className="my-4" />
<Link
href="/"
className="mt-5 flex w-full justify-center rounded-md border border-slate-400 bg-white px-4 py-2 text-sm font-medium text-slate-600 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
Login
</Link>
</>
) : (
<SignupForm />
)}
</FormWrapper>
</div>
);
}

View File

@@ -2,11 +2,12 @@
import { RequestVerificationEmail } from "@/components/auth/RequestVerificationEmail";
import { useSearchParams } from "next/navigation";
import FormWrapper from "@/components/auth/FormWrapper";
export default function VerficationPage() {
const searchParams = useSearchParams();
return (
<div>
<FormWrapper>
{searchParams && searchParams?.get("email") ? (
<>
<h1 className="leading-2 mb-4 text-center text-lg font-semibold text-slate-900">
@@ -27,6 +28,6 @@ export default function VerficationPage() {
) : (
<p className="text-center">No E-Mail Address provided</p>
)}
</div>
</FormWrapper>
);
}

View File

@@ -2,14 +2,15 @@
import { SignIn } from "@/components/auth/SignIn";
import { useSearchParams } from "next/navigation";
import FormWrapper from "@/components/auth/FormWrapper";
export default function Verify() {
const searchParams = useSearchParams();
return searchParams && searchParams?.get("token") ? (
<div>
<FormWrapper>
<p className="text-center">Verifying...</p>
<SignIn token={searchParams.get("token")} />
</div>
</FormWrapper>
) : (
<p className="text-center">No Token provided</p>
);

View File

@@ -135,7 +135,7 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
<Select
value={triggerEventClassId}
onValueChange={(eventClassId) => setTriggerEvent(idx, eventClassId)}>
<SelectTrigger className="w-[180px]">
<SelectTrigger className="w-[240px]">
<SelectValue />
</SelectTrigger>
<SelectContent>

View File

@@ -0,0 +1,14 @@
import { Logo } from "@/components/Logo";
export default function FormWrapper({ children }: { children: React.ReactNode }) {
return (
<div className="mx-auto flex flex-1 flex-col justify-center px-4 py-12 sm:px-6 lg:flex-none lg:px-20 xl:px-24">
<div className="mx-auto w-full max-w-sm rounded-xl bg-white p-8 shadow-xl lg:w-96">
<div className="mb-6 text-center">
<Logo className="mx-auto w-3/4" />
</div>
{children}
</div>
</div>
);
}

View File

@@ -4,7 +4,7 @@ import { Button } from "@formbricks/ui";
import { signIn } from "next-auth/react";
import { FaGithub } from "react-icons/fa";
export const GithubButton = ({ text = "Login with Github" }) => {
export const GithubButton = ({ text = "Continue with Github" }) => {
const handleLogin = async () => {
await signIn("github", {
redirect: true,
@@ -15,8 +15,8 @@ export const GithubButton = ({ text = "Login with Github" }) => {
return (
<Button
type="button"
StartIcon={FaGithub}
startIconClassName="mr-2"
EndIcon={FaGithub}
startIconClassName="ml-2"
onClick={handleLogin}
variant="secondary"
className="w-full justify-center">

View File

@@ -0,0 +1,26 @@
"use client";
import { Button } from "@formbricks/ui";
import { signIn } from "next-auth/react";
import { FaGoogle } from "react-icons/fa";
export const GoogleButton = ({ text = "Continue with Google" }) => {
const handleLogin = async () => {
await signIn("google", {
redirect: true,
callbackUrl: "/", // redirect after login to /
});
};
return (
<Button
type="button"
EndIcon={FaGoogle}
startIconClassName="ml-3"
onClick={handleLogin}
variant="secondary"
className="w-full justify-center">
{text}
</Button>
);
};

View File

@@ -1,11 +1,12 @@
"use client";
import { GoogleButton } from "@/components/auth/GoogleButton";
import { Button } from "@formbricks/ui";
import { XCircleIcon } from "@heroicons/react/24/solid";
import { signIn } from "next-auth/react";
import Link from "next/dist/client/link";
import { useSearchParams } from "next/navigation";
import { useState } from "react";
import { useRef, useState } from "react";
import { GithubButton } from "./GithubButton";
export const SigninForm = () => {
@@ -22,9 +23,105 @@ export const SigninForm = () => {
};
const [loggingIn, setLoggingIn] = useState(false);
const [showLogin, setShowLogin] = useState(false);
const [isButtonEnabled, setButtonEnabled] = useState(true);
const [isPasswordFocused, setIsPasswordFocused] = useState(false);
const formRef = useRef<HTMLFormElement>(null);
const checkFormValidity = () => {
// If both fields are filled, enable the button
if (formRef.current) {
setButtonEnabled(formRef.current.checkValidity());
}
};
return (
<>
<div className="text-center">
<p className="mb-8 text-lg text-slate-700">Log in to your account</p>
<div className="space-y-2">
<form onSubmit={handleSubmit} ref={formRef} className="space-y-2" onChange={checkFormValidity}>
{showLogin && (
<div>
<div className="mb-2 transition-all duration-500 ease-in-out">
<label htmlFor="email" className="sr-only">
Email address
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
placeholder="work@email.com"
defaultValue={searchParams?.get("email") || ""}
className="focus:border-brand focus:ring-brand block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
</div>
<div className="transition-all duration-500 ease-in-out">
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
placeholder="*******"
aria-placeholder="password"
onFocus={() => setIsPasswordFocused(true)}
required
className="focus:border-brand focus:ring-brand block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
</div>
{process.env.NEXT_PUBLIC_PASSWORD_RESET_DISABLED !== "1" && isPasswordFocused && (
<div className="ml-1 text-right transition-all duration-500 ease-in-out">
<Link
href="/auth/forgot-password"
className="hover:text-brand-dark text-xs text-slate-500">
Forgot your password?
</Link>
</div>
)}
</div>
)}
<Button
onClick={() => {
if (!showLogin) {
setShowLogin(true);
setButtonEnabled(false);
} else if (formRef.current) {
formRef.current.requestSubmit();
}
}}
variant="darkCTA"
className="w-full justify-center"
loading={loggingIn}
disabled={!isButtonEnabled}>
Continue with Email
</Button>
</form>
{process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED === "1" && (
<>
<GoogleButton />
</>
)}
{process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED === "1" && (
<>
<GithubButton />
</>
)}
</div>
{process.env.NEXT_PUBLIC_SIGNUP_DISABLED !== "1" && (
<div className="mt-3 text-center text-xs text-slate-600">
Need an account?{" "}
<Link href="/auth/signup" className="font-semibold underline">
Register.
</Link>
</div>
)}
</div>
{searchParams?.get("error") && (
<div className="absolute top-10 rounded-md bg-red-50 p-4">
<div className="flex">
@@ -40,67 +137,6 @@ export const SigninForm = () => {
</div>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-800">
Email address
</label>
<div className="mt-1">
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
defaultValue={searchParams?.get("email") || ""}
className="focus:border-brand focus:ring-brand block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
</div>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-800">
Password
</label>
<div className="mt-1">
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="focus:border-brand focus:ring-brand block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
</div>
</div>
<div>
<Button type="submit" className="w-full justify-center" loading={loggingIn}>
Sign in
</Button>
</div>
{process.env.NEXT_PUBLIC_PASSWORD_RESET_DISABLED !== "1" && (
<div>
<Link
href="/auth/forgot-password"
className="hover:text-brand-dark mt-3 grid grid-cols-1 space-y-2 text-center text-xs text-slate-700">
Forgot your password?
</Link>
</div>
)}
{process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED === "1" && (
<>
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-slate-300" />
</div>
<div className="relative flex justify-center">
<span className="bg-white px-2 text-sm text-slate-500">Sign in with</span>
</div>
</div>
<GithubButton text="Sign in with GitHub" />{" "}
</>
)}
</form>
</>
);
};

View File

@@ -5,8 +5,9 @@ import { createUser } from "@/lib/users/users";
import { XCircleIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { useRef, useState } from "react";
import { GithubButton } from "./GithubButton";
import { GoogleButton } from "@/components/auth/GoogleButton";
export const SignupForm = () => {
const searchParams = useSearchParams();
@@ -36,6 +37,18 @@ export const SignupForm = () => {
}
};
const [showLogin, setShowLogin] = useState(false);
const [isButtonEnabled, setButtonEnabled] = useState(true);
const [isPasswordFocused, setIsPasswordFocused] = useState(false);
const formRef = useRef<HTMLFormElement>(null);
const checkFormValidity = () => {
// If all fields are filled, enable the button
if (formRef.current) {
setButtonEnabled(formRef.current.checkValidity());
}
};
return (
<>
{error && (
@@ -53,106 +66,136 @@ export const SignupForm = () => {
</div>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-slate-800">
Full Name
</label>
<div className="mt-1">
<input
id="name"
name="name"
type="text"
autoComplete="given-name"
required
className="focus:border-brand focus:ring-brand block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
</div>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-800">
Email address
</label>
<div className="mt-1">
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
defaultValue={searchParams?.get("email") || ""}
className="focus:border-brand focus:ring-brand block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
</div>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-800">
Password
</label>
<div className="mt-1">
<input
id="password"
name="password"
type="password"
required
className="focus:border-brand focus:ring-brand block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
</div>
</div>
<div className="text-center">
<p className="mb-8 text-lg text-slate-700">Create your Formbricks account</p>
<div className="space-y-2">
<form onSubmit={handleSubmit} ref={formRef} className="space-y-2" onChange={checkFormValidity}>
{showLogin && (
<div>
<div className="mb-2 transition-all duration-500 ease-in-out">
<label htmlFor="name" className="sr-only">
Full Name
</label>
<div className="mt-1">
<input
id="name"
name="name"
type="text"
autoComplete="given-name"
placeholder="Full Name"
aria-placeholder="Full Name"
required
className="focus:border-brand focus:ring-brand block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
</div>
</div>
<div className="mb-2 transition-all duration-500 ease-in-out">
<label htmlFor="email" className="sr-only">
Email address
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
placeholder="work@email.com"
defaultValue={searchParams?.get("email") || ""}
className="focus:border-brand focus:ring-brand block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
</div>
<div className="transition-all duration-500 ease-in-out">
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
placeholder="*******"
aria-placeholder="password"
onFocus={() => setIsPasswordFocused(true)}
required
className="focus:border-brand focus:ring-brand block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
</div>
{process.env.NEXT_PUBLIC_PASSWORD_RESET_DISABLED !== "1" && isPasswordFocused && (
<div className="ml-1 text-right transition-all duration-500 ease-in-out">
<Link
href="/auth/forgot-password"
className="hover:text-brand-dark text-xs text-slate-500">
Forgot your password?
</Link>
</div>
)}
</div>
)}
<Button
onClick={() => {
if (!showLogin) {
setShowLogin(true);
setButtonEnabled(false);
} else if (formRef.current) {
formRef.current.requestSubmit();
}
}}
variant="darkCTA"
className="w-full justify-center"
loading={signingUp}
disabled={!isButtonEnabled}>
Continue with Email
</Button>
</form>
<div>
<Button type="submit" className="w-full justify-center" loading={signingUp}>
Sign up
</Button>
<div className="mt-3 text-center text-xs text-slate-600">
Already have an account?{" "}
<Link href="/auth/login" className="text-brand hover:text-brand-light">
Log in.
</Link>
</div>
{(process.env.NEXT_PUBLIC_TERMS_URL || process.env.NEXT_PUBLIC_PRIVACY_URL) && (
<div className="mt-3 text-center text-xs text-slate-400">
By clicking &quot;Sign Up&quot;, you agree to our
<br />
{process.env.NEXT_PUBLIC_TERMS_URL && (
<Link
className="text-brand hover:text-brand-light underline"
href={process.env.NEXT_PUBLIC_TERMS_URL}
rel="noreferrer"
target="_blank">
terms of service
</Link>
)}
{process.env.NEXT_PUBLIC_TERMS_URL && process.env.NEXT_PUBLIC_PRIVACY_URL && <span> and </span>}
{process.env.NEXT_PUBLIC_PRIVACY_URL && (
<Link
className="text-brand hover:text-brand-light underline"
href={process.env.NEXT_PUBLIC_PRIVACY_URL}
rel="noreferrer"
target="_blank">
privacy policy
</Link>
)}
.<br />
We&apos;ll occasionally send you account related emails.
</div>
{process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED === "1" && (
<>
<GoogleButton />
</>
)}
{process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED === "1" && (
<>
<GithubButton />{" "}
</>
)}
</div>
{process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED === "1" && (
<>
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-slate-300" />
</div>
<div className="relative flex justify-center">
<span className="bg-white px-2 text-sm text-slate-500">Sign up with</span>
</div>
</div>
<GithubButton text="Sign up with GitHub" />
</>
{(process.env.NEXT_PUBLIC_TERMS_URL || process.env.NEXT_PUBLIC_PRIVACY_URL) && (
<div className="mt-3 text-center text-xs text-slate-500">
By signing up, you agree to our
<br />
{process.env.NEXT_PUBLIC_TERMS_URL && (
<Link
className="font-semibold"
href="google.com" /* {process.env.NEXT_PUBLIC_TERMS_URL} */
rel="noreferrer"
target="_blank">
Terms of Service
</Link>
)}
{process.env.NEXT_PUBLIC_TERMS_URL && process.env.NEXT_PUBLIC_PRIVACY_URL && <span> and </span>}
{process.env.NEXT_PUBLIC_PRIVACY_URL && (
<Link
className="font-semibold"
href="google.com" /* {/* process.env.NEXT_PUBLIC_PRIVACY_URL }*/
rel="noreferrer"
target="_blank">
Privacy Policy.
</Link>
)}
{/* <br />
We&apos;ll occasionally send you account related emails. */}
<hr className="mx-6 mt-3"></hr>
</div>
)}
</form>
<div className="mt-3 text-center text-xs text-slate-600">
Have an account?{" "}
<Link href="/auth/login" className="font-semibold underline">
Log in.
</Link>
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,51 @@
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import Image from "next/image";
import Peer from "@/images/peer.webp";
import CalComLogo from "@/images/cal-logo-light.svg";
export default function Testimonial() {
return (
<div className="flex flex-col items-center justify-center">
<div className="mb-10 w-3/4 space-y-8 2xl:w-1/2">
<div>
<h2 className="text-3xl font-bold text-slate-600">Survey any segment</h2>
<h3 className="text-3xl font-light text-slate-600">No coding required.</h3>
</div>
{/* <p className="text-slate-600">
Make customer-centric decisions based on data.
<br /> Keep 100% data ownership.
</p> */}
<div className="space-y-2">
<div className="flex space-x-2">
<CheckCircleIcon className="text-brand-dark h-6 w-6" />
<p className="inline text-lg text-slate-900">All features included</p>
</div>
<div className="flex space-x-2">
<CheckCircleIcon className="text-brand-dark h-6 w-6" />
<p className="inline text-lg text-slate-900">No Creditcard required</p>
</div>
<div className="flex space-x-2">
<CheckCircleIcon className="text-brand-dark h-6 w-6" />
<p className="inline text-lg text-slate-900">Trusted by 400+ product teams</p>
</div>
</div>
<div className="border-1 relative flex rounded-xl border-slate-200 bg-slate-50 shadow-sm">
<Image
src={Peer}
alt="Cal.com Co-CEO Peer Richelsen"
className="absolute -bottom-9 right-10 h-28 w-28 rounded-full border border-slate-200 shadow-sm"
/>
<div className="space-y-2 p-6">
<p className=" italic text-slate-700">
We measure the clarity of our docs and learn from churn all on one platform. Great product, very
responsive team!
</p>
<p className="text-sm text-slate-500">Peer Richelsen, Co-CEO Cal.com</p>
<Image src={CalComLogo} alt="Cal.com Logo" />
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,9 @@
<svg width="101" height="22" viewBox="0 0 101 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0582 20.817C4.32115 20.817 0 16.2763 0 10.6704C0 5.04589 4.1005 0.467773 10.0582 0.467773C13.2209 0.467773 15.409 1.43945 17.1191 3.66311L14.3609 5.96151C13.2025 4.72822 11.805 4.11158 10.0582 4.11158C6.17833 4.11158 4.04533 7.08268 4.04533 10.6704C4.04533 14.2582 6.38059 17.1732 10.0582 17.1732C11.7866 17.1732 13.2577 16.5566 14.4161 15.3233L17.1375 17.7151C15.501 19.8453 13.2577 20.817 10.0582 20.817Z" fill="#292929"/>
<path d="M29.0161 5.88601H32.7304V20.4612H29.0161V18.331C28.2438 19.8446 26.9566 20.8536 24.4927 20.8536C20.5577 20.8536 17.4133 17.4341 17.4133 13.2297C17.4133 9.02528 20.5577 5.60571 24.4927 5.60571C26.9383 5.60571 28.2438 6.61477 29.0161 8.12835V5.88601ZM29.1264 13.2297C29.1264 10.95 27.5634 9.06266 25.0995 9.06266C22.7274 9.06266 21.1828 10.9686 21.1828 13.2297C21.1828 15.4346 22.7274 17.3967 25.0995 17.3967C27.5451 17.3967 29.1264 15.4907 29.1264 13.2297Z" fill="#292929"/>
<path d="M35.3599 0H39.0742V20.4427H35.3599V0Z" fill="#292929"/>
<path d="M40.7291 18.5182C40.7291 17.3223 41.6853 16.3132 42.9908 16.3132C44.2964 16.3132 45.2158 17.3223 45.2158 18.5182C45.2158 19.7515 44.278 20.7605 42.9908 20.7605C41.7037 20.7605 40.7291 19.7515 40.7291 18.5182Z" fill="#292929"/>
<path d="M59.4296 18.1068C58.0505 19.7885 55.9543 20.8536 53.4719 20.8536C49.0404 20.8536 45.7858 17.4341 45.7858 13.2297C45.7858 9.02528 49.0404 5.60571 53.4719 5.60571C55.8623 5.60571 57.9402 6.61477 59.3193 8.20309L56.4508 10.6136C55.7336 9.71667 54.7958 9.04397 53.4719 9.04397C51.0999 9.04397 49.5553 10.95 49.5553 13.211C49.5553 15.472 51.0999 17.378 53.4719 17.378C54.9062 17.378 55.8991 16.6306 56.6346 15.6215L59.4296 18.1068Z" fill="#292929"/>
<path d="M59.7422 13.2297C59.7422 9.02528 62.9968 5.60571 67.4283 5.60571C71.8598 5.60571 75.1144 9.02528 75.1144 13.2297C75.1144 17.4341 71.8598 20.8536 67.4283 20.8536C62.9968 20.8349 59.7422 17.4341 59.7422 13.2297ZM71.3449 13.2297C71.3449 10.95 69.8003 9.06266 67.4283 9.06266C65.0563 9.04397 63.5117 10.95 63.5117 13.2297C63.5117 15.4907 65.0563 17.3967 67.4283 17.3967C69.8003 17.3967 71.3449 15.4907 71.3449 13.2297Z" fill="#292929"/>
<path d="M100.232 11.5482V20.4428H96.518V12.4638C96.518 9.94119 95.3412 8.85739 93.576 8.85739C91.921 8.85739 90.7442 9.67958 90.7442 12.4638V20.4428H87.0299V12.4638C87.0299 9.94119 85.8346 8.85739 84.0878 8.85739C82.4329 8.85739 80.9802 9.67958 80.9802 12.4638V20.4428H77.2659V5.8676H80.9802V7.88571C81.7525 6.31607 83.15 5.53125 85.3014 5.53125C87.3425 5.53125 89.0525 6.5403 89.9903 8.24074C90.9281 6.50293 92.3072 5.53125 94.8079 5.53125C97.8603 5.54994 100.232 7.86702 100.232 11.5482Z" fill="#292929"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
apps/web/images/peer.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -61,7 +61,7 @@ export const Button: React.ForwardRefExoticComponent<
"inline-flex items-center appearance-none",
// different styles depending on size
size === "sm" && "px-3 py-2 text-sm leading-4 font-medium rounded-md",
size === "base" && "px-6 py-2 text-sm font-medium rounded-md",
size === "base" && "px-6 py-3 text-sm font-medium rounded-md",
size === "lg" && "px-4 py-2 text-base font-medium rounded-md",
size === "icon" &&
"w-10 h-10 justify-center group p-2 border rounded-lg border-transparent text-neutral-400 hover:border-slate-200 transition",

View File

@@ -13,6 +13,7 @@
"INTERNAL_SECRET",
"MAIL_FROM",
"NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED",
"NEXT_PUBLIC_GOOGLE_AUTH_ENABLED",
"NEXT_PUBLIC_GITHUB_AUTH_ENABLED",
"NEXT_PUBLIC_IS_FORMBRICKS_CLOUD",
"NEXT_PUBLIC_PASSWORD_RESET_DISABLED",