Skip to content

Commit d684f0d

Browse files
committed
feat: add basic scaffold for send gift from link
1 parent 118664e commit d684f0d

File tree

5 files changed

+360
-0
lines changed

5 files changed

+360
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { QuotesRotate } from "~/components/quotes-rotate";
2+
import { Footer } from "../components/footer";
3+
import { Header } from "~/components/header";
4+
5+
export default function Layout({ children }: { children: React.ReactNode }) {
6+
return (
7+
<div className="relative flex min-h-dvh flex-col">
8+
<Header />
9+
10+
<main className="mx-auto flex max-w-7xl w-full flex-col gap-4 px-4 py-14 sm:px-6 sm:py-24 lg:px-8 lg:py-40">
11+
{children}
12+
</main>
13+
14+
<QuotesRotate />
15+
<Footer />
16+
</div>
17+
);
18+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"use server";
2+
3+
import { ZSAError, createServerAction } from "zsa";
4+
import { z } from "zod";
5+
import { createClient } from "~/supabase/server";
6+
import { client as fedexClientLocations } from "@theliaison/fedex/fetch/locations";
7+
import { getAccessToken } from "@theliaison/fedex/fetch/authorization";
8+
import { env } from "~/env";
9+
import { sendByLinkSchema } from "../validators";
10+
11+
export const sendGiftFromLinkAction = createServerAction()
12+
.input(sendByLinkSchema)
13+
.handler(async ({ input }) => {
14+
const supabase = createClient();
15+
16+
const {
17+
data: { user },
18+
error,
19+
} = await supabase.auth.getUser();
20+
21+
if (error || !user) {
22+
return new ZSAError("NOT_AUTHORIZED", {
23+
status: 401,
24+
});
25+
}
26+
27+
// const accessToken = await getAccessToken(
28+
// env.FEDEX_TEST_API_KEY,
29+
// env.FEDEX_TEST_SECRET_KEY,
30+
// );
31+
32+
// if (!accessToken) {
33+
// return new ZSAError("INTERNAL_SERVER_ERROR", {
34+
// status: 500,
35+
// });
36+
// }
37+
38+
const {
39+
giftLink,
40+
giftSpecifications,
41+
recipientName,
42+
recipientEmail,
43+
recipientSocial,
44+
recipientSocialHandle,
45+
recipientPhone,
46+
} = input;
47+
48+
console.log({
49+
giftLink,
50+
giftSpecifications,
51+
recipientName,
52+
recipientEmail,
53+
recipientSocial,
54+
recipientSocialHandle,
55+
recipientPhone,
56+
});
57+
58+
return {
59+
giftLink,
60+
giftSpecifications,
61+
recipientName,
62+
recipientEmail,
63+
recipientSocial,
64+
recipientSocialHandle,
65+
recipientPhone,
66+
};
67+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Toaster } from "@theliaison/ui/sonner";
2+
import { SendByLinkForm } from "./send-by-link-form";
3+
4+
export default function SendByLink() {
5+
return (
6+
<div className="max-w-screen-2xl mx-auto px-4 sm:px-6 md:px-8 w-full">
7+
<SendByLinkForm />
8+
<Toaster />
9+
</div>
10+
);
11+
}
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { Button } from "@theliaison/ui/button";
5+
import {
6+
Form,
7+
FormControl,
8+
FormField,
9+
FormItem,
10+
FormLabel,
11+
FormMessage,
12+
} from "@theliaison/ui/form";
13+
import { RadioGroup, RadioGroupItem } from "@theliaison/ui/radio-group";
14+
import {
15+
Select,
16+
SelectContent,
17+
SelectItem,
18+
SelectTrigger,
19+
SelectValue,
20+
} from "@theliaison/ui/select";
21+
import { Label } from "@theliaison/ui/label";
22+
import { Input } from "@theliaison/ui/input";
23+
import { zodResolver } from "@hookform/resolvers/zod";
24+
import { useForm } from "react-hook-form";
25+
import type { z } from "zod";
26+
import { useServerAction } from "zsa-react";
27+
import { sendGiftFromLinkAction } from "./actions";
28+
import { sendByLinkSchema } from "../validators";
29+
import { GiftIcon, LinkIcon, MailIcon, PhoneIcon } from "lucide-react";
30+
import { toast } from "sonner";
31+
32+
export function SendByLinkForm() {
33+
const { isPending, execute, data, error } = useServerAction(
34+
sendGiftFromLinkAction,
35+
);
36+
37+
const [contactMethod, setContactMethod] = useState<
38+
"email" | "phone" | "social"
39+
>("social");
40+
41+
const form = useForm<z.infer<typeof sendByLinkSchema>>({
42+
resolver: zodResolver(sendByLinkSchema),
43+
mode: "onChange",
44+
});
45+
46+
async function onSubmit(values: z.infer<typeof sendByLinkSchema>) {
47+
const [data, err] = await execute(values);
48+
49+
toast.message(<pre>{JSON.stringify(data, null, 2)}</pre>);
50+
51+
if (err) {
52+
// show a toast or something
53+
toast.error(err.message);
54+
return;
55+
}
56+
57+
// form.reset({ giftLink: "" });
58+
}
59+
60+
return (
61+
<Form {...form}>
62+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 w-full">
63+
<FormField
64+
control={form.control}
65+
name="giftLink"
66+
render={({ field }) => (
67+
<FormItem>
68+
<FormLabel>Gift Link</FormLabel>
69+
<FormControl>
70+
<Input placeholder="John Doe" {...field} />
71+
</FormControl>
72+
<FormMessage />
73+
</FormItem>
74+
)}
75+
/>
76+
<FormField
77+
control={form.control}
78+
name="giftSpecifications"
79+
render={({ field }) => (
80+
<FormItem>
81+
<FormLabel>Gift Specifications</FormLabel>
82+
<FormControl>
83+
<Input placeholder="Red" {...field} />
84+
</FormControl>
85+
<FormMessage />
86+
</FormItem>
87+
)}
88+
/>
89+
<FormField
90+
control={form.control}
91+
name="recipientName"
92+
render={({ field }) => (
93+
<FormItem>
94+
<FormLabel>Recipient Name</FormLabel>
95+
<FormControl>
96+
<Input placeholder="name" {...field} />
97+
</FormControl>
98+
<FormMessage />
99+
</FormItem>
100+
)}
101+
/>
102+
103+
<Label htmlFor="social">Recipient Contact Method</Label>
104+
<RadioGroup
105+
defaultValue={contactMethod}
106+
className="grid grid-cols-3 gap-4"
107+
onValueChange={(value) =>
108+
setContactMethod(value as "email" | "phone" | "social")
109+
}
110+
>
111+
<div>
112+
<RadioGroupItem
113+
value="social"
114+
id="social"
115+
className="peer sr-only"
116+
/>
117+
<Label
118+
htmlFor="social"
119+
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
120+
>
121+
<svg
122+
xmlns="http:www.w3.org/2000/svg"
123+
className="mb-3 size-6"
124+
width="44"
125+
height="44"
126+
viewBox="0 0 24 24"
127+
strokeWidth="1.5"
128+
stroke="#2c3e50"
129+
fill="none"
130+
strokeLinecap="round"
131+
strokeLinejoin="round"
132+
>
133+
<title>Social</title>
134+
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
135+
<path d="M12 5m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
136+
<path d="M5 19m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
137+
<path d="M19 19m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
138+
<path d="M12 14m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" />
139+
<path d="M12 7l0 4" />
140+
<path d="M6.7 17.8l2.8 -2" />
141+
<path d="M17.3 17.8l-2.8 -2" />
142+
</svg>
143+
Social
144+
</Label>
145+
</div>
146+
<div>
147+
<RadioGroupItem value="email" id="email" className="peer sr-only" />
148+
<Label
149+
htmlFor="email"
150+
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
151+
>
152+
<MailIcon className="mb-3 size-6" />
153+
Email
154+
</Label>
155+
</div>
156+
<div>
157+
<RadioGroupItem value="phone" id="phone" className="peer sr-only" />
158+
<Label
159+
htmlFor="phone"
160+
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
161+
>
162+
<PhoneIcon className="mb-3 size-6" />
163+
Phone
164+
</Label>
165+
</div>
166+
</RadioGroup>
167+
168+
{contactMethod === "social" ? (
169+
<div className="grid gap-2">
170+
<Label htmlFor="social-network">Social Network</Label>
171+
<Select
172+
onValueChange={(value) => {
173+
form.setValue("recipientSocial", value);
174+
}}
175+
required
176+
>
177+
<SelectTrigger id="social-network">
178+
<SelectValue placeholder="Social Network" />
179+
</SelectTrigger>
180+
<SelectContent>
181+
<SelectItem value="facebook">Facebook</SelectItem>
182+
<SelectItem value="twitter">X (Twitter)</SelectItem>
183+
<SelectItem value="instagram">Instagram</SelectItem>
184+
<SelectItem value="linkedin">LinkedIn</SelectItem>
185+
<SelectItem value="github">Github</SelectItem>
186+
<SelectItem value="tiktok">TikTok</SelectItem>
187+
<SelectItem value="twitch">Twitch</SelectItem>
188+
<SelectItem value="youtube">YouTube</SelectItem>
189+
<SelectItem value="reddit">Reddit</SelectItem>
190+
<SelectItem value="kick">Kick</SelectItem>
191+
</SelectContent>
192+
</Select>
193+
<FormField
194+
control={form.control}
195+
name="recipientSocialHandle"
196+
render={({ field }) => (
197+
<FormItem className="w-full">
198+
<FormControl>
199+
<Input
200+
placeholder="@recipient_social_handle"
201+
className=""
202+
required
203+
{...field}
204+
/>
205+
</FormControl>
206+
<FormMessage />
207+
</FormItem>
208+
)}
209+
/>
210+
</div>
211+
) : null}
212+
213+
{contactMethod === "email" ? (
214+
<FormField
215+
control={form.control}
216+
name="recipientEmail"
217+
render={({ field }) => (
218+
<FormItem>
219+
<FormLabel>Recipient Email</FormLabel>
220+
<FormControl>
221+
<Input placeholder="email" required {...field} />
222+
</FormControl>
223+
<FormMessage />
224+
</FormItem>
225+
)}
226+
/>
227+
) : null}
228+
229+
{contactMethod === "phone" ? (
230+
<FormField
231+
control={form.control}
232+
name="recipientPhone"
233+
render={({ field }) => (
234+
<FormItem>
235+
<FormLabel>Recipient Phone</FormLabel>
236+
<FormControl>
237+
<Input placeholder="phone" required {...field} />
238+
</FormControl>
239+
<FormMessage />
240+
</FormItem>
241+
)}
242+
/>
243+
) : null}
244+
<Button disabled={isPending} type="submit" className="w-full">
245+
{isPending ? "Saving..." : "Save"}
246+
</Button>
247+
{error && <div>Error: {JSON.stringify(error.fieldErrors)}</div>}
248+
</form>
249+
</Form>
250+
);
251+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { z } from "zod";
2+
3+
export const sendByLinkSchema = z.object({
4+
recipientName: z.string().min(1, { message: "Recipient name is required" }),
5+
recipientEmail: z.string().email({ message: "Invalid email" }).optional(),
6+
recipientSocial: z.string().optional(),
7+
recipientSocialHandle: z.string().optional(),
8+
recipientPhone: z.string().optional(),
9+
giftLink: z.string().url({ message: "Invalid URL" }),
10+
giftSpecifications: z
11+
.string()
12+
.min(5, { message: "Gift specifications are required" }),
13+
});

0 commit comments

Comments
 (0)