Velocity UI
Loading…
Menu

Interactive Feedback Widget

Interactive Feedback Widget component with smooth animations and modern UI.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add interactive-feedback-widget

Source Code

interactive-feedback-widget.tsx
'use client'

import React, { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { 
  MessageSquarePlus, 
  Send, 
  X, 
  Star, 
  Smile, 
  Meh, 
  Frown, 
  ThumbsUp, 
  Loader2,
  CheckCircle2
} from 'lucide-react'
import { cn } from '@/lib/utils'
// @ts-ignore
import confetti from 'canvas-confetti'

type FeedbackType = 'bug' | 'idea' | 'other'
type Mood = 'happy' | 'neutral' | 'sad'

export default function InteractiveFeedbackWidget() {
  const [isOpen, setIsOpen] = useState(false)
  const [step, setStep] = useState<'type' | 'details' | 'success'>('type')
  const [selectedType, setSelectedType] = useState<FeedbackType | null>(null)
  const [mood, setMood] = useState<Mood | null>(null)
  const [comment, setComment] = useState('')
  const [isSubmitting, setIsSubmitting] = useState(false)

  const handleSubmit = () => {
    setIsSubmitting(true)
    // Simulate API call
    setTimeout(() => {
      setIsSubmitting(false)
      setStep('success')
      confetti({
        particleCount: 100,
        spread: 70,
        origin: { y: 0.8 },
        colors: ['#4F46E5', '#10B981', '#F59E0B']
      })
      
      // Reset after delay
      setTimeout(() => {
        setIsOpen(false)
        setTimeout(() => {
          setStep('type')
          setSelectedType(null)
          setMood(null)
          setComment('')
        }, 300)
      }, 3000)
    }, 1500)
  }

  return (
    <div className="min-h-[400px] w-full flex items-center justify-center bg-neutral-100 dark:bg-neutral-950 p-4 font-sans relative">
        <div className="absolute inset-0 bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:16px_16px] opacity-50 dark:opacity-10"></div>
        
        <div className="relative z-10">
            <AnimatePresence mode="wait">
                {!isOpen ? (
                    <motion.button
                        layoutId="feedback-trigger"
                        className="group flex items-center gap-2 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 px-4 py-2.5 rounded-full shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-300"
                        onClick={() => setIsOpen(true)}
                        initial={{ scale: 0.9, opacity: 0 }}
                        animate={{ scale: 1, opacity: 1 }}
                        exit={{ scale: 0.9, opacity: 0 }}
                        whileHover={{ y: -2 }}
                    >
                        <div className="bg-gradient-to-tr from-indigo-500 to-purple-500 p-1.5 rounded-full text-white group-hover:rotate-12 transition-transform duration-300">
                            <MessageSquarePlus className="w-4 h-4" />
                        </div>
                        <span className="text-sm font-medium text-neutral-700 dark:text-neutral-200 pr-1">Feedback</span>
                    </motion.button>
                ) : (
                    <motion.div
                        layoutId="feedback-trigger"
                        className="w-[320px] bg-white dark:bg-neutral-900 rounded-2xl shadow-2xl border border-neutral-200 dark:border-neutral-800 overflow-hidden"
                        initial={{ opacity: 0, scale: 0.9, y: 20 }}
                        animate={{ opacity: 1, scale: 1, y: 0 }}
                        exit={{ opacity: 0, scale: 0.9, y: 20 }}
                        transition={{ type: "spring", duration: 0.5, bounce: 0.3 }}
                    >
                        {/* Header */}
                        <div className="flex items-center justify-between px-5 py-4 border-b border-neutral-100 dark:border-neutral-800 bg-neutral-50/50 dark:bg-neutral-900/50 backdrop-blur-sm">
                            <div className="flex items-center gap-2">
                                <div className="w-2 h-2 rounded-full bg-indigo-500 animate-pulse" />
                                <span className="text-sm font-semibold text-neutral-900 dark:text-white">Share Feedback</span>
                            </div>
                            <button 
                                onClick={() => setIsOpen(false)}
                                className="text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-200 transition-colors rounded-full p-1 hover:bg-neutral-100 dark:hover:bg-neutral-800"
                            >
                                <X className="w-4 h-4" />
                            </button>
                        </div>

                        {/* Content */}
                        <div className="p-5">
                            <AnimatePresence mode="wait">
                                {step === 'type' && (
                                    <motion.div
                                        key="type"
                                        initial={{ opacity: 0, x: 20 }}
                                        animate={{ opacity: 1, x: 0 }}
                                        exit={{ opacity: 0, x: -20 }}
                                        className="space-y-4"
                                    >
                                        <p className="text-sm text-neutral-500 dark:text-neutral-400 text-center mb-4">
                                            What's on your mind?
                                        </p>
                                        <div className="grid grid-cols-3 gap-3">
                                            {[
                                                { id: 'bug', icon: Frown, label: 'Issue', color: 'text-red-500', bg: 'bg-red-50 dark:bg-red-500/10 hover:border-red-200 dark:hover:border-red-500/30' },
                                                { id: 'idea', icon: SparklesIcon, label: 'Idea', color: 'text-amber-500', bg: 'bg-amber-50 dark:bg-amber-500/10 hover:border-amber-200 dark:hover:border-amber-500/30' },
                                                { id: 'other', icon: MessageSquarePlus, label: 'Other', color: 'text-blue-500', bg: 'bg-blue-50 dark:bg-blue-500/10 hover:border-blue-200 dark:hover:border-blue-500/30' }
                                            ].map((item) => (
                                                <button
                                                    key={item.id}
                                                    onClick={() => {
                                                        setSelectedType(item.id as FeedbackType)
                                                        setStep('details')
                                                    }}
                                                    className={cn(
                                                        "flex flex-col items-center gap-2 p-3 rounded-xl border border-transparent transition-all duration-200 group",
                                                        item.bg
                                                    )}
                                                >
                                                    <item.icon className={cn("w-6 h-6 transition-transform group-hover:scale-110", item.color)} />
                                                    <span className="text-xs font-medium text-neutral-600 dark:text-neutral-300">{item.label}</span>
                                                </button>
                                            ))}
                                        </div>
                                    </motion.div>
                                )}

                                {step === 'details' && (
                                    <motion.div
                                        key="details"
                                        initial={{ opacity: 0, x: 20 }}
                                        animate={{ opacity: 1, x: 0 }}
                                        exit={{ opacity: 0, x: -20 }}
                                        className="space-y-4"
                                    >
                                        <div className="flex justify-center gap-4 mb-4">
                                            {[
                                                { id: 'sad', icon: Frown, color: 'hover:text-red-500' },
                                                { id: 'neutral', icon: Meh, color: 'hover:text-amber-500' },
                                                { id: 'happy', icon: Smile, color: 'hover:text-green-500' }
                                            ].map((m) => (
                                                <button
                                                    key={m.id}
                                                    onClick={() => setMood(m.id as Mood)}
                                                    className={cn(
                                                        "p-2 rounded-full transition-all duration-200 hover:bg-neutral-100 dark:hover:bg-neutral-800",
                                                        mood === m.id ? "scale-110 " + (m.id === 'sad' ? 'text-red-500' : m.id === 'neutral' ? 'text-amber-500' : 'text-green-500') : "text-neutral-400 " + m.color
                                                    )}
                                                >
                                                    <m.icon className="w-8 h-8" />
                                                </button>
                                            ))}
                                        </div>
                                        
                                        <textarea
                                            value={comment}
                                            onChange={(e) => setComment(e.target.value)}
                                            placeholder="Tell us more..."
                                            className="w-full h-24 p-3 rounded-xl bg-neutral-50 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700 focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 transition-all resize-none text-sm placeholder:text-neutral-400 outline-none"
                                            autoFocus
                                        />

                                        <div className="flex gap-2 pt-2">
                                            <button
                                                onClick={() => setStep('type')}
                                                className="px-4 py-2 rounded-lg text-xs font-medium text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300 transition-colors"
                                            >
                                                Back
                                            </button>
                                            <button
                                                onClick={handleSubmit}
                                                disabled={!comment || !mood || isSubmitting}
                                                className="flex-1 flex items-center justify-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg text-xs font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-indigo-500/20 active:scale-95"
                                            >
                                                {isSubmitting ? (
                                                    <Loader2 className="w-3 h-3 animate-spin" />
                                                ) : (
                                                    <>
                                                        Send Feedback
                                                        <Send className="w-3 h-3" />
                                                    </>
                                                )}
                                            </button>
                                        </div>
                                    </motion.div>
                                )}

                                {step === 'success' && (
                                    <motion.div
                                        key="success"
                                        initial={{ opacity: 0, scale: 0.8 }}
                                        animate={{ opacity: 1, scale: 1 }}
                                        className="flex flex-col items-center justify-center py-8 text-center"
                                    >
                                        <div className="w-16 h-16 rounded-full bg-green-100 dark:bg-green-500/10 flex items-center justify-center mb-4 text-green-500">
                                            <CheckCircle2 className="w-8 h-8" />
                                        </div>
                                        <h3 className="text-lg font-semibold text-neutral-900 dark:text-white mb-1">Thank You!</h3>
                                        <p className="text-sm text-neutral-500 dark:text-neutral-400">
                                            Your feedback helps us improve.
                                        </p>
                                    </motion.div>
                                )}
                            </AnimatePresence>
                        </div>
                    </motion.div>
                )}
            </AnimatePresence>
        </div>
    </div>
  )
}

function SparklesIcon(props: any) {
    return (
        <svg
            {...props}
            xmlns="http://www.w3.org/2000/svg"
            width="24"
            height="24"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
        >
            <path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />
        </svg>
    )
}