ReviewEditor.tsx raw

   1  import { Button } from '@/components/ui/button'
   2  import { Textarea } from '@/components/ui/textarea'
   3  import { createRelayReviewDraftEvent } from '@/lib/draft-event'
   4  import { useNostr } from '@/providers/NostrProvider'
   5  import { Loader2, Star } from 'lucide-react'
   6  import { NostrEvent } from 'nostr-tools'
   7  import { useMemo, useState } from 'react'
   8  import { useTranslation } from 'react-i18next'
   9  import { toast } from 'sonner'
  10  
  11  export default function ReviewEditor({
  12    relayUrl,
  13    onReviewed
  14  }: {
  15    relayUrl: string
  16    onReviewed: (evt: NostrEvent) => void
  17  }) {
  18    const { t } = useTranslation()
  19    const { publish } = useNostr()
  20    const [stars, setStars] = useState(0)
  21    const [hoverStars, setHoverStars] = useState(0)
  22    const [review, setReview] = useState('')
  23    const [submitting, setSubmitting] = useState(false)
  24    const canSubmit = useMemo(() => stars > 0 && !!review.trim(), [stars, review])
  25  
  26    const submit = async () => {
  27      if (!canSubmit) return
  28  
  29      setSubmitting(true)
  30      try {
  31        const draftEvent = createRelayReviewDraftEvent(relayUrl, review, stars)
  32        const evt = await publish(draftEvent)
  33        onReviewed(evt)
  34      } catch (error) {
  35        if (error instanceof AggregateError) {
  36          error.errors.forEach((e) => toast.error(`${t('Failed to review')}: ${e.message}`))
  37        } else if (error instanceof Error) {
  38          toast.error(`${t('Failed to review')}: ${error.message}`)
  39        }
  40        console.error(error)
  41        return
  42      } finally {
  43        setSubmitting(false)
  44      }
  45    }
  46  
  47    return (
  48      <div className="px-4 space-y-2">
  49        <Textarea
  50          className="min-h-36"
  51          placeholder={t('Write a review and pick a star rating')}
  52          value={review}
  53          onChange={(e) => setReview(e.target.value)}
  54        />
  55        <div className="flex justify-between items-center">
  56          <div className="flex items-center">
  57            {Array.from({ length: 5 }).map((_, index) => (
  58              <div
  59                key={index}
  60                className="pr-2 cursor-pointer"
  61                onMouseEnter={() => setHoverStars(index + 1)}
  62                onMouseLeave={() => setHoverStars(0)}
  63              >
  64                {index < (hoverStars || stars) ? (
  65                  <Star
  66                    className="size-6 text-yellow-400 fill-yellow-400"
  67                    onClick={() => setStars(index + 1)}
  68                  />
  69                ) : (
  70                  <Star
  71                    className="size-6 text-muted-foreground"
  72                    onClick={() => setStars(index + 1)}
  73                  />
  74                )}
  75              </div>
  76            ))}
  77          </div>
  78          <Button
  79            disabled={!canSubmit}
  80            variant={canSubmit ? 'default' : 'secondary'}
  81            onClick={submit}
  82          >
  83            {submitting && <Loader2 className="animate-spin" />}
  84            {t('Submit')}
  85          </Button>
  86        </div>
  87      </div>
  88    )
  89  }
  90