1+ import { useMemo , useRef , useState } from "react" ;
2+ import ArrowPathIcon from "@heroicons/react/24/solid/ArrowPathIcon" ;
3+ import { zodResolver } from "@hookform/resolvers/zod" ;
4+ import { AnimatePresence , motion } from "framer-motion" ;
5+ import { useForm } from "react-hook-form" ;
6+ import { z } from "zod" ;
7+
8+ import { BackendResponse } from "@/api/models/Response" ;
9+ import perks from "@/assets/state/perks" ;
10+ import Button from "@/components/controls/Button" ;
11+ import Error from "@/components/controls/Error" ;
12+ import Link from "@/components/navigation/Link" ;
13+ import { backend } from "@/utils/wretch" ;
14+
15+
16+ const signupSchema = z . object ( {
17+ email : z . string ( ) . email ( ) . min ( 3 )
18+ } ) ;
19+ const signupSchemaResolver = zodResolver ( signupSchema ) ;
20+
21+ type SignupSchema = z . infer < typeof signupSchema > ;
22+
23+ const fadeAnim = {
24+ in : {
25+ opacity : 0
26+ } ,
27+ anim : {
28+ opacity : 1 ,
29+ transition : {
30+ duration : 0.35
31+ }
32+ } ,
33+ exit : {
34+ opacity : 0 ,
35+ transition : {
36+ duration : 0.35
37+ }
38+ }
39+ } as const ;
40+
41+ const SignupSection = ( ) => {
42+ const { handleSubmit, register, formState } = useForm < SignupSchema > ( {
43+ resolver : signupSchemaResolver ,
44+ mode : "onChange"
45+ } ) ;
46+
47+ const [ response , setResponse ] = useState < null | BackendResponse > ( null ) ;
48+ const [ loading , setLoading ] = useState ( false ) ;
49+
50+ const loadingRef = useRef ( loading ) ;
51+ loadingRef . current = loading ;
52+
53+ const submit = useMemo ( ( ) => handleSubmit ( ( { email } ) => {
54+ if ( loadingRef . current ) return ;
55+ setLoading ( true ) ;
56+
57+ backend . url ( "/email/subscribe" )
58+ . post ( { email } )
59+ . json ( ( res : BackendResponse ) => setResponse ( res ) ) ;
60+ } ) , [ handleSubmit ] ) ;
61+
62+ return (
63+ < section
64+ aria-labelledby = "try-it-yourself"
65+ className = "flex flex-col gap-12 py-16 mx-auto text-center max-w-7xl"
66+ >
67+ < h2
68+ id = "try-it-yourself"
69+ className = "text-4xl font-bold md:text-5xl text-secondary"
70+ >
71+ Try it Yourself
72+ </ h2 >
73+ < section className = "flex flex-col gap-4" >
74+ < h3 className = "text-2xl text-primary md:text-3xl" > Commit Rocket is not out yet!</ h3 >
75+ < p className = "text-xl" >
76+ Do you want to join in on this adventure and help develop Commit Rocket?
77+ We value your input and look forward to involving you in the process of making Commit Rocket as optimal as possible.
78+ </ p >
79+ </ section >
80+ < section className = "flex flex-col gap-4" >
81+ < h3 className = "text-xl font-bold md:text-2xl text-primary" > Perks</ h3 >
82+ < p className = "text-xl" >
83+ Sign up to stay in the loop on our progress,
84+ get early access to beta versions, and participate in surveys that help shape the development of our open-source and cross-platform Git client.
85+ </ p >
86+ < div className = "flex flex-wrap justify-center w-full gap-4" aria-hidden >
87+ { perks . map ( ( { title, icon : Icon } , i ) => (
88+ < div key = { i } className = "flex flex-col items-center w-32 gap-2" >
89+ < Icon className = "w-12 sm:w-16 text-primary" />
90+ < div className = "font-bold" > { title } </ div >
91+ </ div >
92+ ) ) }
93+ </ div >
94+ </ section >
95+ < form aria-label = "Sign up" className = "flex flex-col items-center gap-4" onSubmit = { submit } >
96+ < AnimatePresence mode = "wait" >
97+ { ( ! response || response . success === false ) && < motion . div
98+ key = "form"
99+ className = "flex flex-col items-center w-full gap-4"
100+ variants = { fadeAnim }
101+ initial = "in"
102+ animate = "anim"
103+ exit = "exit"
104+ >
105+ < div className = "w-full" >
106+ < input
107+ className = "w-full p-4 text-lg bg-white border-2 rounded-md md:text-xl border-primary"
108+ placeholder = "your@email.com"
109+ { ...register ( "email" ) }
110+ />
111+ < Error className = "w-full px-2 text-start" state = { formState } name = "email" />
112+ </ div >
113+ < Button type = "submit" disabled = { loading } color = "secondary" className = "px-5 py-3 text-lg md:text-xl w-fit" >
114+ { ! loading
115+ ? "Keep me up-to-date!"
116+ : < ArrowPathIcon className = "w-6 h-6 animate-spin" />
117+ }
118+ </ Button >
119+ </ motion . div > }
120+ { ( response && response . success ) && < motion . div
121+ key = "success"
122+ className = "py-4 mb-8 text-xl font-semibold text-green-500"
123+ variants = { fadeAnim }
124+ initial = "in"
125+ animate = "anim"
126+ exit = "exit"
127+ >
128+ 🎉 { response . message } 🎉
129+ </ motion . div > }
130+ </ AnimatePresence >
131+ < p role = "note" > You can always unsubscribe by going to < Link color = "primary" href = "/mail/unsubscribe" underline > this link</ Link > </ p >
132+ </ form >
133+ </ section >
134+ ) ;
135+ } ;
136+
137+ export default SignupSection ;
0 commit comments