Velocity UI
Loading…
Menu

Interactive Feedback Widget

Interactive Feedback Widget component with smooth animations and modern UI.

Preview

Live interactive preview.

Installation

Add this component to your project using the CLI:

terminal
npx -y vui-registry-cli-v1@latest 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>
    )
}