From f9040f8beda10b3969572cf8764e49f9b24b222a Mon Sep 17 00:00:00 2001
From: Sebastien Castiel <sebastien@castiel.me>
Date: Thu, 18 Jan 2024 15:46:50 -0500
Subject: Merge feedback and support dialogs

---
 src/components/feedback-button/feedback-button.tsx | 294 +++++++++++++++++++--
 1 file changed, 272 insertions(+), 22 deletions(-)

(limited to 'src/components/feedback-button/feedback-button.tsx')

diff --git a/src/components/feedback-button/feedback-button.tsx b/src/components/feedback-button/feedback-button.tsx
index d43f8a7..b1b6ae2 100644
--- a/src/components/feedback-button/feedback-button.tsx
+++ b/src/components/feedback-button/feedback-button.tsx
@@ -1,27 +1,277 @@
-import { FeedbackButtonClient } from '@/components/feedback-button/feedback-button-client'
+'use client'
+import { sendFeedback } from '@/components/feedback-button/feedback-button-actions'
 import { formSchema } from '@/components/feedback-button/feedback-button-common'
-import { FeedbackButtonEmail } from '@/components/feedback-button/feedback-button-email'
-import { env } from '@/lib/env'
-import { getResend } from '@/lib/resend'
-
-export function FeedbackButton() {
-  async function sendFeedback(values: unknown) {
-    'use server'
-    const { email, message } = formSchema.parse(values)
-    const resend = getResend()
-    if (!resend || !env.FEEDBACK_EMAIL_FROM || !env.FEEDBACK_EMAIL_TO) {
-      console.warn(
-        'Resend is not properly configured. Feedback email won’t be sent.',
-      )
-      return
-    }
-    await resend.emails.send({
-      from: env.FEEDBACK_EMAIL_FROM,
-      to: env.FEEDBACK_EMAIL_TO,
-      subject: `Spliit: new feedback from ${email || 'anonymous user'}`,
-      react: <FeedbackButtonEmail email={email} message={message} />,
+import { Button, ButtonProps } from '@/components/ui/button'
+import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'
+import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'
+import {
+  Form,
+  FormControl,
+  FormDescription,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from '@/components/ui/form'
+import { Input } from '@/components/ui/input'
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { Textarea } from '@/components/ui/textarea'
+import { useToast } from '@/components/ui/use-toast'
+import { useMediaQuery } from '@/lib/hooks'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { Heart, HeartIcon, Loader2, MessageCircle } from 'lucide-react'
+import { PropsWithChildren, ReactNode, SetStateAction, useState } from 'react'
+import { useForm } from 'react-hook-form'
+import * as z from 'zod'
+
+type FormValues = z.infer<typeof formSchema>
+
+type Props = {
+  donationUrl: string
+  defaultTab?: 'feedback' | 'support'
+}
+
+export function FeedbackModal({
+  donationUrl,
+  defaultTab = 'feedback',
+  children,
+}: PropsWithChildren<Props>) {
+  const { toast } = useToast()
+  const isDesktop = useMediaQuery('(min-width: 640px)')
+  const [open, setOpen] = useState(false)
+
+  async function onSubmit(values: FormValues) {
+    await sendFeedback(values)
+    toast({
+      title: 'Thank you for your feedback!',
+      description:
+        'We will have a look at it as soon as possible, and will get back to you if needed.',
     })
   }
 
-  return <FeedbackButtonClient sendFeedback={sendFeedback} />
+  const Wrapper = isDesktop ? FeedbackDialog : FeedbackDrawer
+
+  return (
+    <Wrapper open={open} setOpen={setOpen} button={children}>
+      <FeedbackContent
+        onSubmit={async (values) => {
+          await onSubmit(values)
+          setOpen(false)
+        }}
+        donationUrl={donationUrl}
+        defaultTab={defaultTab}
+      />
+    </Wrapper>
+  )
+}
+
+function FeedbackDrawer({
+  children,
+  open,
+  setOpen,
+  button,
+}: PropsWithChildren<{
+  open: boolean
+  setOpen: (open: SetStateAction<boolean>) => void
+  button: ReactNode
+}>) {
+  return (
+    <Drawer open={open} onOpenChange={setOpen}>
+      <DrawerTrigger asChild>{button}</DrawerTrigger>
+      <DrawerContent>
+        <div className="p-4">{children}</div>
+      </DrawerContent>
+    </Drawer>
+  )
+}
+
+function FeedbackDialog({
+  children,
+  open,
+  setOpen,
+  button,
+}: PropsWithChildren<{
+  open: boolean
+  setOpen: (open: SetStateAction<boolean>) => void
+  button: ReactNode
+}>) {
+  return (
+    <Dialog open={open} onOpenChange={setOpen}>
+      <DialogTrigger asChild>{button}</DialogTrigger>
+      <DialogContent>
+        <div className="pt-4">{children}</div>
+      </DialogContent>
+    </Dialog>
+  )
+}
+
+function FeedbackContent({
+  onSubmit,
+  donationUrl,
+  defaultTab,
+}: {
+  onSubmit: (values: FormValues) => Promise<void>
+  donationUrl: string
+  defaultTab: 'feedback' | 'support'
+}) {
+  return (
+    <Tabs defaultValue={defaultTab}>
+      <div className="mt-2 mb-6">
+        <TabsList className="grid w-full grid-cols-2">
+          <TabsTrigger value="feedback">Give feedback</TabsTrigger>
+          <TabsTrigger value="support">Support us</TabsTrigger>
+        </TabsList>
+      </div>
+      <TabsContent value="feedback">
+        <FeedbackForm onSubmit={onSubmit} />
+      </TabsContent>
+      <TabsContent value="support">
+        <DonationForm donationUrl={donationUrl} />
+      </TabsContent>
+    </Tabs>
+  )
+}
+
+function FeedbackForm({
+  onSubmit,
+}: {
+  onSubmit: (values: FormValues) => Promise<void>
+}) {
+  const form = useForm<FormValues>({
+    resolver: zodResolver(formSchema),
+    defaultValues: { email: '', message: '' },
+  })
+
+  const isSubmitting = form.formState.isSubmitting
+  return (
+    <Form {...form}>
+      <form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-4">
+        <div>
+          <h2 className="text-lg font-semibold leading-none tracking-tight pb-1.5">
+            Give us your feedback
+          </h2>
+          <p className="text-sm text-muted-foreground">
+            We are always working to improve the user experience, and your
+            feedback helps us a lot.
+          </p>
+        </div>
+        <div className="grid gap-4">
+          <FormField
+            control={form.control}
+            name="email"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>Your email address</FormLabel>
+                <FormControl>
+                  <Input
+                    placeholder="your@email.com"
+                    className="text-base"
+                    {...field}
+                  />
+                </FormControl>
+                <FormDescription>
+                  Optional. Provide it if you want us to get back to you.
+                </FormDescription>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+          <FormField
+            control={form.control}
+            name="message"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>Your feedback</FormLabel>
+                <FormControl>
+                  <Textarea
+                    placeholder="Enter your feedback"
+                    className="text-base"
+                    {...field}
+                  />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+        </div>
+        <div className="text-center mt-1">
+          <Button type="submit" disabled={isSubmitting}>
+            {isSubmitting ? (
+              <>
+                <Loader2 className="w-4 h-4 mr-2 animate-spin" /> Submitting…
+              </>
+            ) : (
+              <>
+                <MessageCircle className="w-4 h-4 mr-2" /> Send
+              </>
+            )}
+          </Button>
+        </div>
+      </form>
+    </Form>
+  )
+}
+
+function DonationForm({ donationUrl }: { donationUrl: string }) {
+  return (
+    <div className="flex flex-col gap-4">
+      <div>
+        <h2 className="text-lg font-semibold leading-none tracking-tight pb-1.5">
+          Support us
+        </h2>
+        <p className="text-sm text-muted-foreground">
+          Help keep <strong>Spliit</strong> free and without ads!
+        </p>
+      </div>
+      <div className="prose prose-sm dark:prose-invert">
+        <p>
+          Spliit is offered for free, but costs money and energy. If you like
+          the app, you can choose to support it by buying me (Sebastien) a
+          coffee with a one-time small donation.
+        </p>
+        <p>By supporting Spliit:</p>
+        <ul>
+          <li>
+            You contribute to the <strong>hosting costs</strong> for the app
+            (currently ~$150/year).
+          </li>
+          <li>
+            You help us keeping the application{' '}
+            <strong>free and without ads</strong>.
+          </li>
+          <li>
+            You give me energy to build <strong>new features</strong> and
+            improve the application.
+          </li>
+        </ul>
+        <p>
+          You will be redirected to <strong>Stripe</strong>, our payment
+          provider, where you can choose an amount to donate and complete the
+          payment.
+        </p>
+      </div>
+      <div className="text-center">
+        <Button
+          asChild
+          className="bg-pink-700 hover:bg-pink-600 dark:bg-pink-500 dark:hover:bg-pink-600"
+        >
+          <a href={donationUrl} target="_blank">
+            <Heart className="w-4 h-4 mr-2" /> Support us
+          </a>
+        </Button>
+      </div>
+    </div>
+  )
+}
+
+export function FeedbackButton({ ...props }: ButtonProps) {
+  return (
+    <Button
+      className="bg-pink-700 hover:bg-pink-600 dark:bg-pink-500 dark:hover:bg-pink-600 fixed right-0 bottom-4 rounded-r-none gap-2"
+      {...props}
+    >
+      <MessageCircle className="w-4 h-4" />
+      <HeartIcon className="w-4 h-4" />
+    </Button>
+  )
 }
-- 
cgit v1.3

