spine-framework-portal 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,149 @@
1
+ import { useState } from 'react'
2
+ import { NavLink, useNavigate } from 'react-router-dom'
3
+ import { Ticket, Users, BookOpen, GraduationCap, Store, LayoutGrid, User, LogOut, Save } from 'lucide-react'
4
+ import { useAuth } from '@core/contexts/AuthContext'
5
+ import { Button } from '@core/components/ui/button'
6
+ import { Avatar, AvatarFallback } from '@core/components/ui/avatar'
7
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@core/components/ui/dialog'
8
+ import { Input } from '@core/components/ui/input'
9
+ import { Label } from '@core/components/ui/label'
10
+ import { Separator } from '@core/components/ui/separator'
11
+
12
+ const NAV_ITEMS = [
13
+ { label: 'Tickets', path: '/portal/tickets', icon: Ticket },
14
+ { label: 'Knowledge Base', path: '/portal/kb', icon: BookOpen },
15
+ { label: 'Courses', path: '/portal/courses', icon: GraduationCap },
16
+ { label: 'Community', path: '/portal/community', icon: Users },
17
+ { label: 'Marketplace', path: '/portal/marketplace', icon: Store },
18
+ ]
19
+
20
+ export function PortalHeader() {
21
+ const { user, logout } = useAuth()
22
+ const navigate = useNavigate()
23
+ const [accountOpen, setAccountOpen] = useState(false)
24
+ const [displayName, setDisplayName] = useState(user?.full_name || user?.email?.split('@')[0] || '')
25
+
26
+ const initials = (displayName || user?.email || 'U')
27
+ .split(' ')
28
+ .map((w: string) => w[0])
29
+ .join('')
30
+ .toUpperCase()
31
+ .slice(0, 2)
32
+
33
+ const handleSave = () => {
34
+ setAccountOpen(false)
35
+ }
36
+
37
+ const handleSignOut = async () => {
38
+ await logout()
39
+ navigate('/login')
40
+ }
41
+
42
+ return (
43
+ <>
44
+ <header className="sticky top-0 z-50 bg-background border-b border-border shadow-sm">
45
+ <div className="flex items-center h-14 px-6 gap-6">
46
+ <NavLink to="/portal" className="flex items-center gap-2 shrink-0 text-foreground hover:text-primary transition-colors">
47
+ <LayoutGrid size={18} className="text-primary" />
48
+ <span className="font-semibold tracking-tight text-sm">Customer Portal</span>
49
+ </NavLink>
50
+
51
+ <nav className="flex items-center gap-1 flex-1">
52
+ {NAV_ITEMS.map(({ label, path, icon: Icon }) => (
53
+ <NavLink
54
+ key={path}
55
+ to={path}
56
+ className={({ isActive }) =>
57
+ [
58
+ 'flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm font-medium transition-colors',
59
+ isActive
60
+ ? 'bg-accent text-accent-foreground'
61
+ : 'text-muted-foreground hover:text-foreground hover:bg-accent/50',
62
+ ].join(' ')
63
+ }
64
+ >
65
+ <Icon size={14} />
66
+ {label}
67
+ </NavLink>
68
+ ))}
69
+ </nav>
70
+
71
+ <Button
72
+ variant="ghost"
73
+ size="sm"
74
+ className="shrink-0 gap-2"
75
+ onClick={() => setAccountOpen(true)}
76
+ >
77
+ <Avatar className="h-6 w-6">
78
+ <AvatarFallback className="text-xs bg-primary text-primary-foreground">{initials}</AvatarFallback>
79
+ </Avatar>
80
+ <span className="text-sm font-medium hidden sm:inline">{displayName || user?.email}</span>
81
+ </Button>
82
+ </div>
83
+ </header>
84
+
85
+ <Dialog open={accountOpen} onOpenChange={setAccountOpen}>
86
+ <DialogContent className="sm:max-w-md">
87
+ <DialogHeader>
88
+ <DialogTitle>Account</DialogTitle>
89
+ </DialogHeader>
90
+
91
+ <div className="space-y-6 py-2">
92
+ <div className="flex items-center gap-4">
93
+ <Avatar className="h-14 w-14">
94
+ <AvatarFallback className="text-lg bg-primary text-primary-foreground">{initials}</AvatarFallback>
95
+ </Avatar>
96
+ <div>
97
+ <p className="font-medium">{displayName || '—'}</p>
98
+ <p className="text-sm text-muted-foreground">{user?.email}</p>
99
+ </div>
100
+ </div>
101
+
102
+ <Separator />
103
+
104
+ <div className="space-y-4">
105
+ <div className="space-y-1.5">
106
+ <Label htmlFor="display-name">Display Name</Label>
107
+ <Input
108
+ id="display-name"
109
+ value={displayName}
110
+ onChange={e => setDisplayName(e.target.value)}
111
+ placeholder="Your name"
112
+ />
113
+ </div>
114
+ <div className="space-y-1.5">
115
+ <Label>Email</Label>
116
+ <Input value={user?.email || ''} disabled className="text-muted-foreground" />
117
+ </div>
118
+ </div>
119
+
120
+ <Separator />
121
+
122
+ <div className="space-y-1.5">
123
+ <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Account Info</p>
124
+ <div className="text-sm text-muted-foreground space-y-1">
125
+ <p>Role: <span className="text-foreground font-medium capitalize">{user?.roles?.[0] || 'member'}</span></p>
126
+ <p>Account: <span className="text-foreground font-medium">{user?.account?.display_name || '—'}</span></p>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ <DialogFooter className="flex-col sm:flex-row gap-2">
132
+ <Button
133
+ variant="outline"
134
+ className="text-destructive hover:text-destructive gap-2 sm:mr-auto"
135
+ onClick={handleSignOut}
136
+ >
137
+ <LogOut size={14} />
138
+ Sign Out
139
+ </Button>
140
+ <Button onClick={handleSave} className="gap-2">
141
+ <Save size={14} />
142
+ Save Changes
143
+ </Button>
144
+ </DialogFooter>
145
+ </DialogContent>
146
+ </Dialog>
147
+ </>
148
+ )
149
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @module PortalSidebar
3
+ * @audience installer
4
+ * @layer frontend-component
5
+ * @stability stable
6
+ *
7
+ * Sidebar for Customer Portal with ticket/content navigation.
8
+ */
9
+
10
+ import * as React from "react"
11
+ import { PlusIcon, Ticket, BookOpen, GraduationCap, MessageSquare, HelpCircle, Settings, Layout } from "lucide-react"
12
+ import {
13
+ Sidebar,
14
+ SidebarContent,
15
+ SidebarFooter,
16
+ SidebarGroup,
17
+ SidebarGroupContent,
18
+ SidebarGroupLabel,
19
+ SidebarHeader,
20
+ SidebarMenu,
21
+ SidebarMenuButton,
22
+ SidebarMenuItem,
23
+ SidebarRail,
24
+ } from "@core/components/ui/sidebar"
25
+ import { Button } from "@core/components/ui/button"
26
+
27
+ const navItems = [
28
+ {
29
+ title: "Tickets",
30
+ url: "/portal/tickets",
31
+ icon: Ticket,
32
+ },
33
+ {
34
+ title: "Knowledge Base",
35
+ url: "/portal/kb",
36
+ icon: BookOpen,
37
+ },
38
+ {
39
+ title: "Courses",
40
+ url: "/portal/courses",
41
+ icon: GraduationCap,
42
+ },
43
+ {
44
+ title: "Community",
45
+ url: "/portal/community",
46
+ icon: MessageSquare,
47
+ },
48
+ ]
49
+
50
+ const supportItems = [
51
+ {
52
+ title: "Help Center",
53
+ url: "/portal/help",
54
+ icon: HelpCircle,
55
+ },
56
+ {
57
+ title: "Settings",
58
+ url: "/portal/settings",
59
+ icon: Settings,
60
+ },
61
+ ]
62
+
63
+ export function PortalSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
64
+ return (
65
+ <Sidebar {...props}>
66
+ <SidebarHeader>
67
+ {/* Brand Header */}
68
+ <SidebarMenu>
69
+ <SidebarMenuItem>
70
+ <SidebarMenuButton size="lg" asChild>
71
+ <a href="/portal">
72
+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
73
+ <span className="text-sm font-bold">P</span>
74
+ </div>
75
+ <div className="flex flex-col gap-0.5 leading-none">
76
+ <span className="font-semibold">Portal</span>
77
+ <span className="text-xs text-muted-foreground">Support</span>
78
+ </div>
79
+ </a>
80
+ </SidebarMenuButton>
81
+ </SidebarMenuItem>
82
+ </SidebarMenu>
83
+
84
+ {/* Quick Create Button */}
85
+ <SidebarGroup className="py-2">
86
+ <Button className="w-full" size="sm">
87
+ <PlusIcon className="mr-2 h-4 w-4" />
88
+ New Ticket
89
+ </Button>
90
+ </SidebarGroup>
91
+ </SidebarHeader>
92
+
93
+ <SidebarContent>
94
+ <SidebarGroup>
95
+ <SidebarGroupLabel>Support</SidebarGroupLabel>
96
+ <SidebarGroupContent>
97
+ <SidebarMenu>
98
+ {navItems.map((item) => (
99
+ <SidebarMenuItem key={item.title}>
100
+ <SidebarMenuButton asChild>
101
+ <a href={item.url}>
102
+ <item.icon className="h-4 w-4" />
103
+ <span>{item.title}</span>
104
+ </a>
105
+ </SidebarMenuButton>
106
+ </SidebarMenuItem>
107
+ ))}
108
+ </SidebarMenu>
109
+ </SidebarGroupContent>
110
+ </SidebarGroup>
111
+
112
+ <SidebarGroup>
113
+ <SidebarGroupLabel>Resources</SidebarGroupLabel>
114
+ <SidebarGroupContent>
115
+ <SidebarMenu>
116
+ {supportItems.map((item) => (
117
+ <SidebarMenuItem key={item.title}>
118
+ <SidebarMenuButton asChild>
119
+ <a href={item.url}>
120
+ <item.icon className="h-4 w-4" />
121
+ <span>{item.title}</span>
122
+ </a>
123
+ </SidebarMenuButton>
124
+ </SidebarMenuItem>
125
+ ))}
126
+ </SidebarMenu>
127
+ </SidebarGroupContent>
128
+ </SidebarGroup>
129
+ </SidebarContent>
130
+
131
+ <SidebarFooter className="p-4">
132
+ <div className="text-xs text-muted-foreground">
133
+ Customer Portal
134
+ </div>
135
+ </SidebarFooter>
136
+
137
+ <SidebarRail />
138
+ </Sidebar>
139
+ )
140
+ }
@@ -0,0 +1,41 @@
1
+ import { useState } from 'react'
2
+ import { CheckCircle, Circle } from 'lucide-react'
3
+ import { Button } from '@core/components/ui/button'
4
+
5
+ interface ProgressTrackerProps {
6
+ sequence?: number
7
+ onComplete?: () => void
8
+ completed?: boolean
9
+ }
10
+
11
+ export function ProgressTracker({ sequence, onComplete, completed = false }: ProgressTrackerProps) {
12
+ const [isCompleted, setIsCompleted] = useState(completed)
13
+
14
+ const handleComplete = () => {
15
+ if (isCompleted) return
16
+ setIsCompleted(true)
17
+ onComplete?.()
18
+ }
19
+
20
+ return (
21
+ <div className="flex items-center gap-3">
22
+ {sequence && (
23
+ <span className="text-sm text-muted-foreground">Lesson {sequence}</span>
24
+ )}
25
+
26
+ <Button
27
+ variant={isCompleted ? 'default' : 'outline'}
28
+ size="sm"
29
+ onClick={handleComplete}
30
+ disabled={isCompleted}
31
+ className="gap-1.5"
32
+ >
33
+ {isCompleted ? <><CheckCircle size={13} /> Completed</> : <><Circle size={13} /> Mark Complete</>}
34
+ </Button>
35
+
36
+ {isCompleted && (
37
+ <span className="text-xs text-muted-foreground">Progress saved!</span>
38
+ )}
39
+ </div>
40
+ )
41
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react'
2
+ import { Search } from 'lucide-react'
3
+ import { Input } from '@core/components/ui/input'
4
+
5
+ interface SearchFilterBarProps {
6
+ placeholder?: string
7
+ value: string
8
+ onChange: (value: string) => void
9
+ children?: React.ReactNode
10
+ }
11
+
12
+ export function SearchFilterBar({ placeholder = 'Search…', value, onChange, children }: SearchFilterBarProps) {
13
+ return (
14
+ <div className="relative flex-1">
15
+ <Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none" />
16
+ <Input
17
+ type="text"
18
+ value={value}
19
+ onChange={(e) => onChange(e.target.value)}
20
+ placeholder={placeholder}
21
+ className="pl-9 h-9"
22
+ />
23
+ {children}
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,44 @@
1
+ import { Badge } from '@core/components/ui/badge'
2
+
3
+ type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline'
4
+
5
+ const STATUS_VARIANTS: Record<string, BadgeVariant> = {
6
+ new: 'default',
7
+ open: 'default',
8
+ working: 'outline',
9
+ pending: 'outline',
10
+ in_progress: 'outline',
11
+ returned: 'destructive',
12
+ resolved: 'secondary',
13
+ completed: 'secondary',
14
+ closed: 'secondary',
15
+ not_started: 'secondary',
16
+ }
17
+
18
+ const STATUS_LABELS: Record<string, string> = {
19
+ new: 'New',
20
+ working: 'Working',
21
+ pending: 'Pending',
22
+ returned: 'Returned',
23
+ resolved: 'Resolved',
24
+ closed: 'Closed',
25
+ open: 'Open',
26
+ completed: 'Completed',
27
+ not_started: 'Not Started',
28
+ in_progress: 'In Progress',
29
+ }
30
+
31
+ interface StatusBadgeProps {
32
+ status: string
33
+ }
34
+
35
+ export function StatusBadge({ status }: StatusBadgeProps) {
36
+ const variant = STATUS_VARIANTS[status] ?? 'secondary'
37
+ const label = STATUS_LABELS[status] ?? status
38
+
39
+ return (
40
+ <Badge variant={variant} className="text-xs">
41
+ {label}
42
+ </Badge>
43
+ )
44
+ }
@@ -0,0 +1,102 @@
1
+ import { useState } from 'react'
2
+ import { Send } from 'lucide-react'
3
+ import { usePortalThreads, usePortalMessages } from '../hooks/usePortalData'
4
+ import { Button } from '@core/components/ui/button'
5
+ import { Input } from '@core/components/ui/input'
6
+ import { Skeleton } from '@core/components/ui/skeleton'
7
+ import { ScrollArea } from '@core/components/ui/scroll-area'
8
+
9
+ interface ThreadPanelProps {
10
+ itemId: string
11
+ itemType: string
12
+ context: string
13
+ collapsible?: boolean
14
+ }
15
+
16
+ export function ThreadPanel({ itemId, itemType, context, collapsible = false }: ThreadPanelProps) {
17
+ const [expanded, setExpanded] = useState(!collapsible)
18
+ const [newMessage, setNewMessage] = useState('')
19
+
20
+ const { threads, loading: threadsLoading } = usePortalThreads(itemId)
21
+ const { messages, loading: messagesLoading } = usePortalMessages(threads[0]?.id || '')
22
+
23
+ const handleSendMessage = async () => {
24
+ if (!newMessage.trim()) return
25
+ console.log('Sending message:', newMessage)
26
+ setNewMessage('')
27
+ }
28
+
29
+ if (threadsLoading) {
30
+ return (
31
+ <div className="p-4 space-y-2">
32
+ <Skeleton className="h-10 w-2/3" />
33
+ <Skeleton className="h-10 w-1/2 ml-auto" />
34
+ </div>
35
+ )
36
+ }
37
+
38
+ if (threads.length === 0) {
39
+ return (
40
+ <div className="text-center py-8 text-sm text-muted-foreground">
41
+ No conversation yet. Be the first to start the discussion!
42
+ </div>
43
+ )
44
+ }
45
+
46
+ return (
47
+ <div className="space-y-4">
48
+ {collapsible && (
49
+ <div className="flex items-center justify-between">
50
+ <h4 className="font-medium text-sm">Discussion</h4>
51
+ <Button variant="ghost" size="sm" onClick={() => setExpanded(!expanded)}>
52
+ {expanded ? 'Hide' : 'Show'}
53
+ </Button>
54
+ </div>
55
+ )}
56
+
57
+ {expanded && (
58
+ <>
59
+ <ScrollArea className="max-h-96">
60
+ <div className="space-y-3">
61
+ {messages.map((message) => (
62
+ <div
63
+ key={message.id}
64
+ className={`flex ${message.direction === 'in' ? 'justify-start' : 'justify-end'}`}
65
+ >
66
+ <div className={`max-w-xs lg:max-w-md px-4 py-2 rounded-lg text-sm border ${
67
+ message.direction === 'in'
68
+ ? 'bg-muted text-foreground border-border'
69
+ : 'bg-primary text-primary-foreground border-primary'
70
+ }`}>
71
+ <div className="flex items-center gap-2 mb-1">
72
+ <span className="text-xs font-medium">
73
+ {(message as any).author_name || (message.direction === 'in' ? 'Customer' : 'Support')}
74
+ </span>
75
+ <span className="text-xs opacity-75">
76
+ {new Date(message.created_at).toLocaleTimeString()}
77
+ </span>
78
+ </div>
79
+ <p className="whitespace-pre-wrap">{message.content}</p>
80
+ </div>
81
+ </div>
82
+ ))}
83
+ </div>
84
+ </ScrollArea>
85
+
86
+ <div className="flex gap-2">
87
+ <Input
88
+ value={newMessage}
89
+ onChange={(e) => setNewMessage(e.target.value)}
90
+ placeholder="Type your message…"
91
+ onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
92
+ className="flex-1"
93
+ />
94
+ <Button onClick={handleSendMessage} disabled={!newMessage.trim()} size="icon">
95
+ <Send size={14} />
96
+ </Button>
97
+ </div>
98
+ </>
99
+ )}
100
+ </div>
101
+ )
102
+ }
@@ -0,0 +1,107 @@
1
+ import { useState } from 'react'
2
+ import { PortalItem } from '../hooks/usePortalData'
3
+ import { ThreadPanel } from './ThreadPanel'
4
+ import { Button } from '@core/components/ui/button'
5
+ import { Badge } from '@core/components/ui/badge'
6
+ import { StatusBadge } from './StatusBadge'
7
+
8
+ interface UnifiedItemCardProps {
9
+ item: PortalItem
10
+ compact?: boolean
11
+ onVote?: (helpful: boolean) => void
12
+ onProgress?: () => void
13
+ onAIResponse?: () => void
14
+ onClick?: () => void
15
+ showThread?: boolean
16
+ }
17
+
18
+ export function UnifiedItemCard({
19
+ item,
20
+ compact = false,
21
+ onVote,
22
+ onProgress,
23
+ onAIResponse,
24
+ onClick,
25
+ showThread = false,
26
+ }: UnifiedItemCardProps) {
27
+ const [expanded, setExpanded] = useState(false)
28
+
29
+ const getContextIcon = (context: string) => {
30
+ switch (context) {
31
+ case 'support': return '🎫'
32
+ case 'community': return '💬'
33
+ case 'kb': return '📚'
34
+ case 'course': return '🎓'
35
+ default: return '📄'
36
+ }
37
+ }
38
+
39
+ const renderContent = () => {
40
+ if (compact) return null
41
+ const text = String(item.data?.description || item.data?.summary || 'No description provided.')
42
+ return <p className="text-sm text-muted-foreground mt-2">{text}</p>
43
+ }
44
+
45
+ const renderActions = () => {
46
+ if (compact) return null
47
+ return (
48
+ <div className="flex items-center gap-2 mt-4">
49
+ {item.context === 'support' && onAIResponse && (
50
+ <Button variant="outline" size="sm" onClick={onAIResponse}>Get AI Help</Button>
51
+ )}
52
+ {(item.context === 'community' || item.context === 'kb') && onVote && (
53
+ <>
54
+ <Button variant="ghost" size="sm" onClick={() => onVote(true)}>👍 Helpful</Button>
55
+ <Button variant="ghost" size="sm" onClick={() => onVote(false)}>👎 Not Helpful</Button>
56
+ </>
57
+ )}
58
+ {item.context === 'course' && onProgress && (
59
+ <Button variant="outline" size="sm" onClick={onProgress}>Mark Complete</Button>
60
+ )}
61
+ {showThread && (
62
+ <Button variant="ghost" size="sm" onClick={() => setExpanded(!expanded)}>
63
+ {expanded ? 'Hide' : 'Show'} Discussion
64
+ </Button>
65
+ )}
66
+ </div>
67
+ )
68
+ }
69
+
70
+ return (
71
+ <div
72
+ className={`border border-border rounded-lg ${compact ? 'p-3' : 'p-4'} hover:shadow-sm transition-shadow cursor-pointer bg-card`}
73
+ onClick={onClick}
74
+ >
75
+ <div className="space-y-3">
76
+ <div className="flex items-start justify-between">
77
+ <div className="flex-1">
78
+ <h3 className="font-medium text-sm">{item.title}</h3>
79
+ <div className="flex items-center gap-2 mt-1.5 flex-wrap">
80
+ <span className="text-xs text-muted-foreground flex items-center gap-1">
81
+ {getContextIcon(item.context)} {item.context}
82
+ </span>
83
+ <StatusBadge status={item.status} />
84
+ {item.data?.priority && (
85
+ <Badge variant="outline" className="text-xs">
86
+ Priority: {String(item.data.priority)}
87
+ </Badge>
88
+ )}
89
+ <span className="text-xs text-muted-foreground">
90
+ {new Date(item.created_at).toLocaleDateString()}
91
+ </span>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ {renderContent()}
97
+ {renderActions()}
98
+ </div>
99
+
100
+ {expanded && showThread && (
101
+ <div className="mt-4 pt-4 border-t border-border">
102
+ <ThreadPanel itemId={item.id} itemType="content" context="portal" />
103
+ </div>
104
+ )}
105
+ </div>
106
+ )
107
+ }
@@ -0,0 +1,51 @@
1
+ import { useState } from 'react'
2
+ import { ThumbsUp, ThumbsDown } from 'lucide-react'
3
+ import { Button } from '@core/components/ui/button'
4
+
5
+ interface VotingComponentProps {
6
+ helpfulCount: number
7
+ notHelpfulCount?: number
8
+ onVote?: (helpful: boolean) => void
9
+ }
10
+
11
+ export function VotingComponent({ helpfulCount, notHelpfulCount = 0, onVote }: VotingComponentProps) {
12
+ const [hasVoted, setHasVoted] = useState(false)
13
+ const [voteType, setVoteType] = useState<'helpful' | 'not-helpful' | null>(null)
14
+
15
+ const handleVote = (helpful: boolean) => {
16
+ if (hasVoted) return
17
+ setHasVoted(true)
18
+ setVoteType(helpful ? 'helpful' : 'not-helpful')
19
+ onVote?.(helpful)
20
+ }
21
+
22
+ return (
23
+ <div className="flex items-center gap-2">
24
+ <Button
25
+ variant={voteType === 'helpful' ? 'default' : 'outline'}
26
+ size="sm"
27
+ onClick={() => handleVote(true)}
28
+ disabled={hasVoted}
29
+ className="gap-1.5"
30
+ >
31
+ <ThumbsUp size={13} /> Helpful {helpfulCount > 0 && `(${helpfulCount})`}
32
+ </Button>
33
+
34
+ {notHelpfulCount > 0 && (
35
+ <Button
36
+ variant={voteType === 'not-helpful' ? 'default' : 'outline'}
37
+ size="sm"
38
+ onClick={() => handleVote(false)}
39
+ disabled={hasVoted}
40
+ className="gap-1.5"
41
+ >
42
+ <ThumbsDown size={13} /> Not Helpful ({notHelpfulCount})
43
+ </Button>
44
+ )}
45
+
46
+ {hasVoted && (
47
+ <span className="text-xs text-muted-foreground">Thanks for your feedback!</span>
48
+ )}
49
+ </div>
50
+ )
51
+ }