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-widgetSource 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>
)
}

