Skip to content

Commit edd2cf9

Browse files
committed
Make unsubscribe page
1 parent be858a3 commit edd2cf9

File tree

7 files changed

+423
-267
lines changed

7 files changed

+423
-267
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { motion } from "framer-motion";
2+
3+
import { IMember } from "@/assets/state/team";
4+
import LinkButton from "@/components/controls/LinkButton";
5+
6+
const memberAnim = {
7+
hidden: {
8+
translateY: "5%",
9+
transition: {
10+
duration: 0.15,
11+
ease: "easeOut"
12+
}
13+
},
14+
show: {
15+
translateY: "0%",
16+
transition: {
17+
duration: 0.15,
18+
ease: "easeOut"
19+
}
20+
}
21+
} as const;
22+
23+
const Member = ({ image, name, title, text, links, animate }: IMember & { animate: boolean; }) => (
24+
<motion.div
25+
variants={animate ? memberAnim : {}}
26+
className="flex flex-col items-center w-full max-w-full gap-2 p-4 rounded-md shadow motion-safe:transition-all sm:p-6 md:w-fit shadow-primary"
27+
aria-label="Member"
28+
>
29+
<div className="flex flex-col items-center max-w-full gap-2 sm:flex-row md:flex-col">
30+
<img
31+
className="object-cover w-40 h-40 rounded-full shadow-md aspect-auto md:w-60 md:h-60"
32+
aria-label="Link"
33+
src={image.src}
34+
width={image.width}
35+
height={image.height}
36+
/>
37+
<div className="flex flex-col max-w-full gap-2 py-4 text-center w-72 md:py-0">
38+
<p className="text-2xl font-semibold text-secondary" aria-label="Name">{name}</p>
39+
<p className="font-semibold text-primary" aria-label="Title / Role">{title}</p>
40+
<p className="text-fill-contrast " aria-label="Text">{text}</p>
41+
</div>
42+
</div>
43+
<div className="grid w-full grid-cols-1 gap-2 sm:grid-cols-2" aria-label="Links">
44+
{links.map((link, i) => (
45+
<LinkButton
46+
key={i}
47+
href={link.href}
48+
color="secondary"
49+
aria-label="Link"
50+
>
51+
{link.name}
52+
</LinkButton>
53+
))}
54+
</div>
55+
</motion.div>
56+
);
57+
58+
export default Member;
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import ArrowPathIcon from "@heroicons/react/24/solid/ArrowPathIcon";
2+
import { zodResolver } from "@hookform/resolvers/zod";
3+
import { AnimatePresence, motion } from "framer-motion";
4+
import { useMemo, useRef, useState } from "react";
5+
import { useForm } from "react-hook-form";
6+
import { z } from "zod";
7+
8+
import { BackendResponse } from "@/api/models/Response";
9+
import Button from "@/components/controls/Button";
10+
import Error from "@/components/controls/Error";
11+
import Link from "@/components/navigation/Link";
12+
import { backend } from "@/utils/wretch";
13+
14+
const feedbackSchema = z.object({
15+
text: z.string()
16+
});
17+
18+
const feedbackSchemaResolver = zodResolver(feedbackSchema);
19+
20+
type FeedbackSchema = z.infer<typeof feedbackSchema>;
21+
22+
const fadeAnim = {
23+
in: {
24+
opacity: 0
25+
},
26+
anim: {
27+
opacity: 1,
28+
transition: {
29+
duration: 0.35
30+
}
31+
},
32+
exit: {
33+
opacity: 0,
34+
transition: {
35+
duration: 0.35
36+
}
37+
}
38+
} as const;
39+
40+
const FeedbackSection = () => {
41+
const { handleSubmit, register, formState } = useForm<FeedbackSchema>({
42+
resolver: feedbackSchemaResolver,
43+
mode: "onChange"
44+
});
45+
46+
const [response, setResponse] = useState<null | BackendResponse>(null);
47+
const [loading, setLoading] = useState(false);
48+
49+
const loadingRef = useRef(loading);
50+
loadingRef.current = loading;
51+
52+
const submit = useMemo(() => handleSubmit(({ text }) => {
53+
if (loadingRef.current) return;
54+
setLoading(true);
55+
56+
backend.url("/feedback")
57+
.post({ text })
58+
.json((res: BackendResponse) => setResponse(res));
59+
}), [handleSubmit]);
60+
61+
return (
62+
<section
63+
aria-labelledby="feedback"
64+
className="flex flex-col w-full gap-4 py-16 mx-auto text-center max-w-7xl"
65+
>
66+
<h2
67+
id="feedback"
68+
className="text-4xl font-bold md:text-5xl text-secondary"
69+
>
70+
Feedback
71+
</h2>
72+
<section aria-labelledby="anon-feedback" className="flex flex-col gap-4">
73+
<h3 id="anon-feedback" className="text-2xl text-primary md:text-3xl">Anonymous feedback</h3>
74+
<p className="text-xl">
75+
Want to help us out without signing up? You can send us feedback directly by filling in your feedback below!
76+
</p>
77+
</section>
78+
<form aria-label="feedback-inbox" className="flex flex-col items-center gap-4" onSubmit={submit}>
79+
<AnimatePresence mode="wait">
80+
{(!response || response.success === false) && <motion.div className="flex flex-col items-center w-full gap-4">
81+
<textarea
82+
className="w-full p-4 text-lg bg-white border-2 rounded-md md:text-xl border-primary h-fit"
83+
placeholder="I would like this feature!"
84+
{...register("text")}
85+
rows={5}
86+
/>
87+
<Error className="w-full px-2 text-start" state={formState} name="text" />
88+
<Button type="submit" color="secondary" className="px-5 py-3 text-lg md:text-xl w-fit">
89+
{!loading
90+
? "Submit Feedback"
91+
: <ArrowPathIcon className="w-6 h-6 animate-spin" />
92+
}
93+
</Button>
94+
</motion.div>}
95+
{(response && response.success) && <motion.div
96+
key="success"
97+
className="py-8 text-xl font-semibold text-green-500"
98+
variants={fadeAnim}
99+
initial="in"
100+
animate="anim"
101+
exit="exit"
102+
>
103+
🎉 {response.message} 🎉
104+
</motion.div>}
105+
</AnimatePresence>
106+
<p role="note">Alternatively you can send an email to <Link color="primary" href="mailto:feedback@commitrocket.com" underline>feedback@commitrocket.com</Link></p>
107+
</form>
108+
</section>
109+
);
110+
};
111+
112+
export default FeedbackSection;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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;

src/pages/404.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from "react";
2+
import Head from "next/head";
23
import { useRouter } from "next/router";
34
import ArrowUturnLeftIcon from "@heroicons/react/24/solid/ArrowUturnLeftIcon";
45
import HomeModernIcon from "@heroicons/react/24/solid/HomeModernIcon";
@@ -12,6 +13,9 @@ const NotFound: Page = ({ }) => {
1213

1314
return (
1415
<>
16+
<Head>
17+
<title>404 - Commit Rocket</title>
18+
</Head>
1519
<main className="flex items-center justify-center flex-1 w-full" aria-labelledby="not-found">
1620
<div className="flex flex-col gap-2 p-4 text-center rounded-md shadow shadow-black/25 bg-primary/25">
1721
<h1 id="not-found" className="text-5xl text-secondary">Not Found.</h1>

0 commit comments

Comments
 (0)