@geenius/feedback 0.1.0
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.
- package/.changeset/config.json +11 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/release.yml +29 -0
- package/.nvmrc +1 -0
- package/.project/ACCOUNT.yaml +4 -0
- package/.project/IDEAS.yaml +7 -0
- package/.project/PROJECT.yaml +11 -0
- package/.project/ROADMAP.yaml +15 -0
- package/CHANGELOG.md +8 -0
- package/CODE_OF_CONDUCT.md +16 -0
- package/CONTRIBUTING.md +26 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/SECURITY.md +15 -0
- package/SUPPORT.md +8 -0
- package/package.json +75 -0
- package/packages/convex/package.json +42 -0
- package/packages/convex/src/index.ts +3 -0
- package/packages/convex/src/mutations.ts +88 -0
- package/packages/convex/src/queries.ts +78 -0
- package/packages/convex/src/schema.ts +47 -0
- package/packages/convex/tsconfig.json +18 -0
- package/packages/convex/tsup.config.ts +17 -0
- package/packages/react/README.md +1 -0
- package/packages/react/package.json +49 -0
- package/packages/react/src/components/FeedbackCard.tsx +51 -0
- package/packages/react/src/components/FeedbackForm.tsx +43 -0
- package/packages/react/src/components/FeedbackWidget.tsx +32 -0
- package/packages/react/src/components/NPSSurvey.tsx +62 -0
- package/packages/react/src/components/index.ts +4 -0
- package/packages/react/src/hooks/index.ts +5 -0
- package/packages/react/src/hooks/useFeedback.ts +23 -0
- package/packages/react/src/hooks/useFeedbackAdmin.ts +24 -0
- package/packages/react/src/hooks/useFeedbackForm.ts +35 -0
- package/packages/react/src/hooks/useNPS.ts +26 -0
- package/packages/react/src/index.tsx +13 -0
- package/packages/react/src/pages/FeedbackAdminPage.tsx +71 -0
- package/packages/react/src/pages/FeedbackPublicPage.tsx +42 -0
- package/packages/react/src/pages/FeedbackWidgetPage.tsx +25 -0
- package/packages/react/src/pages/index.ts +3 -0
- package/packages/react/tsconfig.json +19 -0
- package/packages/react/tsup.config.ts +12 -0
- package/packages/react-css/README.md +1 -0
- package/packages/react-css/package.json +36 -0
- package/packages/react-css/src/components/index.ts +5 -0
- package/packages/react-css/src/components/index.tsx +107 -0
- package/packages/react-css/src/hooks/index.ts +2 -0
- package/packages/react-css/src/index.tsx +5 -0
- package/packages/react-css/src/pages/FeedbackAdminPage.tsx +112 -0
- package/packages/react-css/src/pages/FeedbackPage.tsx +76 -0
- package/packages/react-css/src/styles.css +281 -0
- package/packages/react-css/tsconfig.json +19 -0
- package/packages/react-css/tsup.config.ts +10 -0
- package/packages/shared/README.md +1 -0
- package/packages/shared/package.json +44 -0
- package/packages/shared/src/__tests__/feedback.test.ts +72 -0
- package/packages/shared/src/config.ts +49 -0
- package/packages/shared/src/index.ts +111 -0
- package/packages/shared/src/types.ts +59 -0
- package/packages/shared/tsconfig.json +18 -0
- package/packages/shared/tsup.config.ts +11 -0
- package/packages/shared/vitest.config.ts +4 -0
- package/packages/solidjs/README.md +1 -0
- package/packages/solidjs/package.json +45 -0
- package/packages/solidjs/src/components.tsx +72 -0
- package/packages/solidjs/src/index.tsx +3 -0
- package/packages/solidjs/src/primitives.ts +49 -0
- package/packages/solidjs/tsconfig.json +20 -0
- package/packages/solidjs/tsup.config.ts +12 -0
- package/packages/solidjs-css/README.md +1 -0
- package/packages/solidjs-css/package.json +32 -0
- package/packages/solidjs-css/src/index.tsx +4 -0
- package/packages/solidjs-css/src/pages/FeedbackAdminPage.tsx +78 -0
- package/packages/solidjs-css/src/pages/FeedbackPage.tsx +65 -0
- package/packages/solidjs-css/src/primitives/index.ts +1 -0
- package/packages/solidjs-css/src/styles.css +281 -0
- package/packages/solidjs-css/tsconfig.json +20 -0
- package/packages/solidjs-css/tsup.config.ts +10 -0
- package/pnpm-workspace.yaml +2 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import type { FeedbackItem, FeedbackType, FeedbackStatus, FeedbackPriority, NPSResponse } from '@geenius-feedback/shared'
|
|
3
|
+
import { STATUS_CONFIG, PRIORITY_CONFIG, TYPE_CONFIG, FEEDBACK_TYPES, formatRelativeTime, calcNPSStats, getNPSCategory } from '@geenius-feedback/shared'
|
|
4
|
+
import type { RuleOperator } from '@geenius-feedback/shared'
|
|
5
|
+
|
|
6
|
+
export function StatusBadge({ status }: { status: FeedbackStatus }) {
|
|
7
|
+
return <span className={`feedback__status-badge feedback__status-badge--${status}`}>{STATUS_CONFIG[status].emoji} {STATUS_CONFIG[status].label}</span>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function PriorityBadge({ priority }: { priority: FeedbackPriority }) {
|
|
11
|
+
return <span className={`feedback__priority-badge feedback__priority-badge--${priority}`}>{PRIORITY_CONFIG[priority].label}</span>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function TypeBadge({ type }: { type: FeedbackType }) {
|
|
15
|
+
return <span className={`feedback__type-badge feedback__type-badge--${type}`}>{TYPE_CONFIG[type].icon} {TYPE_CONFIG[type].label}</span>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function FeedbackCard({ item, onVote, hasVoted, onClick }: { item: FeedbackItem; onVote?: (id: string) => void; hasVoted?: boolean; onClick?: (id: string) => void }) {
|
|
19
|
+
return (
|
|
20
|
+
<div className={`feedback__card feedback__card--${item.type}`} onClick={() => onClick?.(item.id)}>
|
|
21
|
+
{onVote && (
|
|
22
|
+
<button type="button" className={`feedback__vote-btn ${hasVoted ? 'feedback__vote-btn--active' : 'feedback__vote-btn--inactive'}`} onClick={e => { e.stopPropagation(); onVote(item.id) }}>
|
|
23
|
+
<span className="feedback__vote-btn-arrow">{hasVoted ? '▲' : '△'}</span>
|
|
24
|
+
<span>{item.votes}</span>
|
|
25
|
+
</button>
|
|
26
|
+
)}
|
|
27
|
+
<div className="feedback__card-info">
|
|
28
|
+
<div className="feedback__card-badges"><TypeBadge type={item.type} /><StatusBadge status={item.status} /><PriorityBadge priority={item.priority} /></div>
|
|
29
|
+
<div className="feedback__card-title">{item.title}</div>
|
|
30
|
+
<div className="feedback__card-desc">{item.description}</div>
|
|
31
|
+
<div className="feedback__card-meta">{item.userName && <span>{item.userName}</span>}<span>{formatRelativeTime(item.createdAt)}</span>{item.tags.map(t => <span key={t} className="feedback__tag">{t}</span>)}</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function FeedbackTypeSelector({ value, onChange }: { value: FeedbackType; onChange: (t: FeedbackType) => void }) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="feedback__type-selector">
|
|
40
|
+
{FEEDBACK_TYPES.map(t => <button key={t} type="button" onClick={() => onChange(t)}
|
|
41
|
+
className={`feedback__type-tab ${value === t ? '' : 'feedback__type-tab--inactive'}`}
|
|
42
|
+
style={value === t ? { background: TYPE_CONFIG[t].color, color: 'white' } : undefined}>{TYPE_CONFIG[t].icon} {TYPE_CONFIG[t].label}</button>)}
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function FeedbackForm({ onSubmit, onClose }: { onSubmit: (data: { type: FeedbackType; title: string; description: string }) => Promise<string>; onClose?: () => void }) {
|
|
48
|
+
const [type, setType] = useState<FeedbackType>('general')
|
|
49
|
+
const [title, setTitle] = useState(''); const [desc, setDesc] = useState('')
|
|
50
|
+
const [submitting, setSubmitting] = useState(false); const [error, setError] = useState<string | null>(null); const [success, setSuccess] = useState(false)
|
|
51
|
+
const submit = async () => {
|
|
52
|
+
if (!title.trim()) { setError('Title required'); return }; if (desc.length < 10) { setError('10+ chars'); return }
|
|
53
|
+
setSubmitting(true); setError(null); try { await onSubmit({ type, title, description: desc }); setSuccess(true) } catch (e) { setError(String(e)) } finally { setSubmitting(false) }
|
|
54
|
+
}
|
|
55
|
+
if (success) return <div className="feedback__success"><div className="feedback__success-icon">🎉</div><div className="feedback__success-text">Thank you!</div><div className="feedback__success-sub">Feedback submitted</div></div>
|
|
56
|
+
return (
|
|
57
|
+
<div className="feedback__form">
|
|
58
|
+
<FeedbackTypeSelector value={type} onChange={setType} />
|
|
59
|
+
<input type="text" className="feedback__form-input" placeholder="Title…" value={title} onChange={e => setTitle(e.target.value)} />
|
|
60
|
+
<textarea className="feedback__form-input feedback__form-textarea" placeholder="Description…" value={desc} onChange={e => setDesc(e.target.value)} />
|
|
61
|
+
{error && <div className="feedback__form-error">{error}</div>}
|
|
62
|
+
<div className="feedback__form-actions">
|
|
63
|
+
{onClose && <button type="button" className="feedback__btn feedback__btn--outline" onClick={onClose}>Cancel</button>}
|
|
64
|
+
<button type="button" className="feedback__btn feedback__btn--primary" onClick={submit} disabled={submitting}>{submitting ? 'Submitting…' : 'Submit'}</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function FeedbackWidget({ onSubmit, position = 'right' }: { onSubmit: (data: { type: FeedbackType; title: string; description: string }) => Promise<string>; position?: 'left' | 'right' }) {
|
|
71
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
72
|
+
return (<>
|
|
73
|
+
<button type="button" className="feedback__widget-trigger" onClick={() => setIsOpen(true)}>Feedback</button>
|
|
74
|
+
{isOpen && <div className="feedback__widget-overlay" onClick={() => setIsOpen(false)}>
|
|
75
|
+
<div className="feedback__widget-backdrop" />
|
|
76
|
+
<div className="feedback__widget-panel" onClick={e => e.stopPropagation()}>
|
|
77
|
+
<div className="feedback__widget-header"><span className="feedback__widget-title">Send Feedback</span><button type="button" className="feedback__close-btn" onClick={() => setIsOpen(false)}>✕</button></div>
|
|
78
|
+
<FeedbackForm onSubmit={onSubmit} onClose={() => setIsOpen(false)} />
|
|
79
|
+
</div>
|
|
80
|
+
</div>}
|
|
81
|
+
</>)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function NPSSurvey({ onSubmit, onDismiss }: { onSubmit: (score: number, comment?: string) => void; onDismiss: () => void }) {
|
|
85
|
+
const [score, setScore] = useState<number | null>(null); const [comment, setComment] = useState(''); const [step, setStep] = useState<'score'|'comment'|'thanks'>('score')
|
|
86
|
+
return (
|
|
87
|
+
<div className="feedback__nps">
|
|
88
|
+
{step === 'score' && <div><div className="feedback__nps-header"><span className="feedback__nps-title">How likely to recommend us?</span><button type="button" className="feedback__close-btn" onClick={onDismiss}>✕</button></div><div className="feedback__nps-subtitle">0 = Not at all · 10 = Extremely likely</div>
|
|
89
|
+
<div className="feedback__nps-scores">{Array.from({length:11}).map((_,i) => <button key={i} type="button" className="feedback__nps-score-btn" onClick={() => { setScore(i); setStep('comment') }}>{i}</button>)}</div><div className="feedback__nps-labels"><span>Not likely</span><span>Very likely</span></div></div>}
|
|
90
|
+
{step === 'comment' && <div><div className="feedback__nps-title" style={{marginBottom: '0.75rem'}}>Score: {score}</div><textarea className="feedback__form-input feedback__form-textarea" value={comment} onChange={e => setComment(e.target.value)} placeholder="Optional…" /><button type="button" className="feedback__btn feedback__btn--primary" style={{width:'100%',marginTop:'0.75rem'}} onClick={() => { onSubmit(score!, comment||undefined); setStep('thanks') }}>Submit</button></div>}
|
|
91
|
+
{step === 'thanks' && <div className="feedback__success"><div className="feedback__success-icon">🎉</div><div className="feedback__success-text">Thank you!</div></div>}
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function NPSResults({ responses }: { responses: NPSResponse[] }) {
|
|
97
|
+
const stats = calcNPSStats(responses); const total = stats.totalResponses || 1
|
|
98
|
+
const color = stats.npsScore >= 50 ? 'oklch(0.72 0.18 155)' : stats.npsScore >= 0 ? 'oklch(0.72 0.18 60)' : 'oklch(0.60 0.25 25)'
|
|
99
|
+
return (
|
|
100
|
+
<div className="feedback__nps-results">
|
|
101
|
+
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:'0.5rem'}}><span>NPS Score</span><span className="feedback__nps-results-score" style={{color}}>{stats.npsScore}</span></div>
|
|
102
|
+
<div className="feedback__nps-bar"><div className="feedback__nps-bar-detractors" style={{width:`${(stats.detractors/total)*100}%`}} /><div className="feedback__nps-bar-passives" style={{width:`${(stats.passives/total)*100}%`}} /><div className="feedback__nps-bar-promoters" style={{width:`${(stats.promoters/total)*100}%`}} /></div>
|
|
103
|
+
<div className="feedback__nps-breakdown"><div><span style={{color:'oklch(0.60 0.25 25)',fontWeight:700}}>{stats.detractors}</span><br/><span style={{color:'oklch(1 0 0/0.3)'}}>Detractors</span></div><div><span style={{color:'oklch(0.72 0.18 60)',fontWeight:700}}>{stats.passives}</span><br/><span style={{color:'oklch(1 0 0/0.3)'}}>Passives</span></div><div><span style={{color:'oklch(0.72 0.18 155)',fontWeight:700}}>{stats.promoters}</span><br/><span style={{color:'oklch(1 0 0/0.3)'}}>Promoters</span></div></div>
|
|
104
|
+
<div style={{marginTop:'0.75rem',paddingTop:'0.75rem',borderTop:'1px solid oklch(1 0 0/0.05)',display:'flex',justifyContent:'space-between',fontSize:'0.6875rem',color:'oklch(1 0 0/0.4)'}}><span>Avg: {stats.averageScore}/10</span><span>{stats.totalResponses} responses</span></div>
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { useFeedback, useFeedbackForm, useNPS, useFeedbackAdmin } from './hooks'
|
|
2
|
+
export type { FeedbackFilter } from './hooks'
|
|
3
|
+
export { StatusBadge, PriorityBadge, TypeBadge, FeedbackCard, FeedbackTypeSelector, FeedbackForm, FeedbackWidget, NPSSurvey, NPSResults } from './components'
|
|
4
|
+
import './styles.css'
|
|
5
|
+
export type { FeedbackItem, NPSResponse, FeedbackConfig, FeedbackStats, NPSStats, FeedbackType, FeedbackStatus, FeedbackPriority } from '@geenius-feedback/shared'
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import '../styles.css';
|
|
3
|
+
|
|
4
|
+
const FeedbackAdminPage: React.FC = () => {
|
|
5
|
+
return (
|
|
6
|
+
<div style={{ padding: '1.5rem' }}>
|
|
7
|
+
<div className="feedback__breadcrumb">
|
|
8
|
+
<span className="feedback__breadcrumb-item">Feedback</span>
|
|
9
|
+
<span className="feedback__breadcrumb-sep">/</span>
|
|
10
|
+
<span className="feedback__breadcrumb-item--active">Admin</span>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div className="feedback__stats-grid">
|
|
14
|
+
<div className="feedback__stat-card">
|
|
15
|
+
<div className="feedback__stat-icon">📊</div>
|
|
16
|
+
<div className="feedback__stat-value">7.8</div>
|
|
17
|
+
<div className="feedback__stat-label">NPS Score</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div className="feedback__stat-card">
|
|
20
|
+
<div className="feedback__stat-icon">⭐</div>
|
|
21
|
+
<div className="feedback__stat-value">4.2</div>
|
|
22
|
+
<div className="feedback__stat-label">Avg Rating</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div className="feedback__stat-card">
|
|
25
|
+
<div className="feedback__stat-icon">⏱️</div>
|
|
26
|
+
<div className="feedback__stat-value">2.1h</div>
|
|
27
|
+
<div className="feedback__stat-label">Avg Response</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div className="feedback__filter-panel">
|
|
32
|
+
<div className="feedback__filter-section">
|
|
33
|
+
<div className="feedback__filter-section-title">Status</div>
|
|
34
|
+
<div className="feedback__filter-options">
|
|
35
|
+
<label className="feedback__filter-option">
|
|
36
|
+
<input type="checkbox" defaultChecked />
|
|
37
|
+
<span className="feedback__filter-option-label">Open</span>
|
|
38
|
+
</label>
|
|
39
|
+
<label className="feedback__filter-option">
|
|
40
|
+
<input type="checkbox" defaultChecked />
|
|
41
|
+
<span className="feedback__filter-option-label">Under Review</span>
|
|
42
|
+
</label>
|
|
43
|
+
<label className="feedback__filter-option">
|
|
44
|
+
<input type="checkbox" defaultChecked />
|
|
45
|
+
<span className="feedback__filter-option-label">Planned</span>
|
|
46
|
+
</label>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div className="feedback__filter-section">
|
|
50
|
+
<div className="feedback__filter-section-title">Priority</div>
|
|
51
|
+
<div className="feedback__filter-options">
|
|
52
|
+
<label className="feedback__filter-option">
|
|
53
|
+
<input type="checkbox" defaultChecked />
|
|
54
|
+
<span className="feedback__filter-option-label">Critical</span>
|
|
55
|
+
</label>
|
|
56
|
+
<label className="feedback__filter-option">
|
|
57
|
+
<input type="checkbox" defaultChecked />
|
|
58
|
+
<span className="feedback__filter-option-label">High</span>
|
|
59
|
+
</label>
|
|
60
|
+
<label className="feedback__filter-option">
|
|
61
|
+
<input type="checkbox" />
|
|
62
|
+
<span className="feedback__filter-option-label">Low</span>
|
|
63
|
+
</label>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div style={{ marginTop: '2rem' }}>
|
|
69
|
+
<h3 style={{ fontSize: '0.875rem', fontWeight: '600', marginBottom: '1rem', color: 'oklch(1 0 0 / 0.8)' }}>
|
|
70
|
+
Recent Activity
|
|
71
|
+
</h3>
|
|
72
|
+
<table className="feedback__admin-table">
|
|
73
|
+
<thead>
|
|
74
|
+
<tr>
|
|
75
|
+
<th>Item</th>
|
|
76
|
+
<th>Status</th>
|
|
77
|
+
<th>Priority</th>
|
|
78
|
+
<th>Votes</th>
|
|
79
|
+
</tr>
|
|
80
|
+
</thead>
|
|
81
|
+
<tbody>
|
|
82
|
+
<tr>
|
|
83
|
+
<td>Add Dark Mode</td>
|
|
84
|
+
<td>
|
|
85
|
+
<select className="feedback__admin-select">
|
|
86
|
+
<option>In Progress</option>
|
|
87
|
+
<option>Planned</option>
|
|
88
|
+
<option>Open</option>
|
|
89
|
+
</select>
|
|
90
|
+
</td>
|
|
91
|
+
<td>High</td>
|
|
92
|
+
<td>42</td>
|
|
93
|
+
</tr>
|
|
94
|
+
<tr>
|
|
95
|
+
<td>Login Timeout Issue</td>
|
|
96
|
+
<td>
|
|
97
|
+
<select className="feedback__admin-select">
|
|
98
|
+
<option>Open</option>
|
|
99
|
+
<option>In Progress</option>
|
|
100
|
+
</select>
|
|
101
|
+
</td>
|
|
102
|
+
<td>Critical</td>
|
|
103
|
+
<td>12</td>
|
|
104
|
+
</tr>
|
|
105
|
+
</tbody>
|
|
106
|
+
</table>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default FeedbackAdminPage;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import '../styles.css';
|
|
3
|
+
|
|
4
|
+
const FeedbackPage: React.FC = () => {
|
|
5
|
+
const feedbackItems = [
|
|
6
|
+
{ id: 1, title: 'Add Dark Mode', type: 'feature', status: 'in-progress', votes: 42, priority: 'high' },
|
|
7
|
+
{ id: 2, title: 'Bug: Login timeout issue', type: 'bug', status: 'open', votes: 12, priority: 'critical' },
|
|
8
|
+
{ id: 3, title: 'Improve API documentation', type: 'suggestion', status: 'planned', votes: 28, priority: 'medium' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div style={{ padding: '1.5rem' }}>
|
|
13
|
+
<div className="feedback__breadcrumb">
|
|
14
|
+
<span className="feedback__breadcrumb-item">Feedback</span>
|
|
15
|
+
<span className="feedback__breadcrumb-sep">/</span>
|
|
16
|
+
<span className="feedback__breadcrumb-item--active">View All</span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div className="feedback__stats-grid">
|
|
20
|
+
<div className="feedback__stat-card">
|
|
21
|
+
<div className="feedback__stat-icon">💬</div>
|
|
22
|
+
<div className="feedback__stat-value">145</div>
|
|
23
|
+
<div className="feedback__stat-label">Total Feedback</div>
|
|
24
|
+
</div>
|
|
25
|
+
<div className="feedback__stat-card">
|
|
26
|
+
<div className="feedback__stat-icon">👍</div>
|
|
27
|
+
<div className="feedback__stat-value">89</div>
|
|
28
|
+
<div className="feedback__stat-label">In Progress</div>
|
|
29
|
+
</div>
|
|
30
|
+
<div className="feedback__stat-card">
|
|
31
|
+
<div className="feedback__stat-icon">✅</div>
|
|
32
|
+
<div className="feedback__stat-value">34</div>
|
|
33
|
+
<div className="feedback__stat-label">Implemented</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div className="feedback__filter-tabs" style={{ marginBottom: '1.5rem' }}>
|
|
38
|
+
<button className="feedback__filter-tab feedback__filter-tab--active">All</button>
|
|
39
|
+
<button className="feedback__filter-tab feedback__filter-tab--inactive">Features</button>
|
|
40
|
+
<button className="feedback__filter-tab feedback__filter-tab--inactive">Bugs</button>
|
|
41
|
+
<button className="feedback__filter-tab feedback__filter-tab--inactive">Suggestions</button>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
|
45
|
+
{feedbackItems.map(item => (
|
|
46
|
+
<div key={item.id} className={`feedback__card feedback__card--${item.type}`}>
|
|
47
|
+
<div className="feedback__vote-btn feedback__vote-btn--inactive">
|
|
48
|
+
<span className="feedback__vote-btn-arrow">▲</span>
|
|
49
|
+
<span>{item.votes}</span>
|
|
50
|
+
</div>
|
|
51
|
+
<div className="feedback__card-info">
|
|
52
|
+
<div className="feedback__card-badges">
|
|
53
|
+
<span className={`feedback__type-badge feedback__type-badge--${item.type}`}>{item.type}</span>
|
|
54
|
+
<span className={`feedback__status-badge feedback__status-badge--${item.status}`}>{item.status}</span>
|
|
55
|
+
<span className="feedback__priority-badge feedback__priority-badge--high">{item.priority}</span>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="feedback__card-title">{item.title}</div>
|
|
58
|
+
<div className="feedback__card-meta">
|
|
59
|
+
<span>Posted by user</span>
|
|
60
|
+
<span>2 days ago</span>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="feedback__pagination">
|
|
68
|
+
<button className="feedback__pagination-btn">Previous</button>
|
|
69
|
+
<span className="feedback__pagination-info">Page 1 of 5</span>
|
|
70
|
+
<button className="feedback__pagination-btn">Next</button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default FeedbackPage;
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/* ─── Feedback Design Tokens (OKLCH) ──────────────────── */
|
|
2
|
+
:root {
|
|
3
|
+
--feedback-bg: oklch(0.12 0.01 250);
|
|
4
|
+
--feedback-surface: oklch(0.16 0.01 250);
|
|
5
|
+
--feedback-border: oklch(0.22 0.01 250);
|
|
6
|
+
--feedback-text: oklch(0.95 0.01 250);
|
|
7
|
+
--feedback-text-muted: oklch(0.58 0.02 250);
|
|
8
|
+
--feedback-accent: oklch(0.65 0.20 265);
|
|
9
|
+
--feedback-open: oklch(0.65 0.20 245);
|
|
10
|
+
--feedback-review: oklch(0.72 0.18 60);
|
|
11
|
+
--feedback-planned: oklch(0.70 0.22 280);
|
|
12
|
+
--feedback-progress: oklch(0.65 0.20 265);
|
|
13
|
+
--feedback-done: oklch(0.72 0.18 155);
|
|
14
|
+
--feedback-declined: oklch(0.50 0.10 250);
|
|
15
|
+
--feedback-bug: oklch(0.60 0.25 25);
|
|
16
|
+
--feedback-feature: oklch(0.70 0.22 280);
|
|
17
|
+
--feedback-suggestion: oklch(0.72 0.18 60);
|
|
18
|
+
--feedback-general: oklch(0.65 0.20 245);
|
|
19
|
+
--feedback-radius: 0.75rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* ─── Feedback Card ──────────────────────────────────── */
|
|
23
|
+
.feedback__card {
|
|
24
|
+
display: flex;
|
|
25
|
+
gap: 0.75rem;
|
|
26
|
+
padding: 1rem;
|
|
27
|
+
border: 1px solid var(--feedback-border);
|
|
28
|
+
border-radius: var(--feedback-radius);
|
|
29
|
+
background: oklch(1 0 0 / 0.02);
|
|
30
|
+
transition: all 0.2s;
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
}
|
|
33
|
+
.feedback__card:hover { border-color: oklch(0.65 0.20 265 / 0.2); background: oklch(1 0 0 / 0.04); }
|
|
34
|
+
.feedback__card-info { flex: 1; min-width: 0; }
|
|
35
|
+
.feedback__card-badges { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.25rem; }
|
|
36
|
+
.feedback__card-title { font-size: 0.875rem; font-weight: 600; color: oklch(1 0 0 / 0.9); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 0.125rem; }
|
|
37
|
+
.feedback__card-desc { font-size: 0.6875rem; color: oklch(1 0 0 / 0.4); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
38
|
+
.feedback__card-meta { display: flex; align-items: center; gap: 0.75rem; margin-top: 0.5rem; font-size: 0.625rem; color: oklch(1 0 0 / 0.25); }
|
|
39
|
+
.feedback__card--bug { border-left: 3px solid var(--feedback-bug); }
|
|
40
|
+
.feedback__card--feature { border-left: 3px solid var(--feedback-feature); }
|
|
41
|
+
.feedback__card--suggestion { border-left: 3px solid var(--feedback-suggestion); }
|
|
42
|
+
.feedback__card--general { border-left: 3px solid var(--feedback-general); }
|
|
43
|
+
|
|
44
|
+
/* ─── Vote Button ────────────────────────────────────── */
|
|
45
|
+
.feedback__vote-btn {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: 0.125rem;
|
|
50
|
+
padding: 0.375rem 0.5rem;
|
|
51
|
+
border-radius: calc(var(--feedback-radius) * 0.8);
|
|
52
|
+
border: none;
|
|
53
|
+
font-size: 0.6875rem;
|
|
54
|
+
font-weight: 700;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
transition: all 0.15s;
|
|
57
|
+
font-variant-numeric: tabular-nums;
|
|
58
|
+
}
|
|
59
|
+
.feedback__vote-btn--inactive { background: oklch(1 0 0 / 0.05); color: oklch(1 0 0 / 0.3); }
|
|
60
|
+
.feedback__vote-btn--inactive:hover { background: oklch(1 0 0 / 0.1); color: oklch(1 0 0 / 0.5); }
|
|
61
|
+
.feedback__vote-btn--active { background: oklch(0.65 0.20 265 / 0.15); color: oklch(0.65 0.20 265); }
|
|
62
|
+
.feedback__vote-btn-arrow { font-size: 0.875rem; }
|
|
63
|
+
|
|
64
|
+
/* ─── Status Badge ───────────────────────────────────── */
|
|
65
|
+
.feedback__status-badge { display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.125rem 0.625rem; border-radius: 9999px; font-size: 0.625rem; font-weight: 500; }
|
|
66
|
+
.feedback__status-badge--open { background: oklch(0.65 0.20 245 / 0.15); color: oklch(0.65 0.20 245); }
|
|
67
|
+
.feedback__status-badge--under-review { background: oklch(0.72 0.18 60 / 0.15); color: oklch(0.72 0.18 60); }
|
|
68
|
+
.feedback__status-badge--planned { background: oklch(0.70 0.22 280 / 0.15); color: oklch(0.70 0.22 280); }
|
|
69
|
+
.feedback__status-badge--in-progress { background: oklch(0.65 0.20 265 / 0.15); color: oklch(0.65 0.20 265); }
|
|
70
|
+
.feedback__status-badge--done { background: oklch(0.72 0.18 155 / 0.15); color: oklch(0.72 0.18 155); }
|
|
71
|
+
.feedback__status-badge--declined { background: oklch(0.50 0.10 250 / 0.15); color: oklch(0.50 0.10 250); }
|
|
72
|
+
|
|
73
|
+
/* ─── Priority Badge ─────────────────────────────────── */
|
|
74
|
+
.feedback__priority-badge { padding: 0.125rem 0.5rem; border-radius: 9999px; font-size: 0.625rem; font-weight: 500; }
|
|
75
|
+
.feedback__priority-badge--low { background: oklch(0.58 0.10 250 / 0.15); color: oklch(0.58 0.10 250); }
|
|
76
|
+
.feedback__priority-badge--medium { background: oklch(0.72 0.18 60 / 0.15); color: oklch(0.72 0.18 60); }
|
|
77
|
+
.feedback__priority-badge--high { background: oklch(0.68 0.22 35 / 0.15); color: oklch(0.68 0.22 35); }
|
|
78
|
+
.feedback__priority-badge--critical { background: oklch(0.60 0.25 25 / 0.15); color: oklch(0.60 0.25 25); }
|
|
79
|
+
|
|
80
|
+
/* ─── Type Badge ─────────────────────────────────────── */
|
|
81
|
+
.feedback__type-badge { display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.125rem 0.5rem; border-radius: 9999px; font-size: 0.625rem; font-weight: 500; }
|
|
82
|
+
.feedback__type-badge--bug { background: oklch(0.60 0.25 25 / 0.15); color: oklch(0.60 0.25 25); }
|
|
83
|
+
.feedback__type-badge--feature { background: oklch(0.70 0.22 280 / 0.15); color: oklch(0.70 0.22 280); }
|
|
84
|
+
.feedback__type-badge--suggestion { background: oklch(0.72 0.18 60 / 0.15); color: oklch(0.72 0.18 60); }
|
|
85
|
+
.feedback__type-badge--general { background: oklch(0.65 0.20 245 / 0.15); color: oklch(0.65 0.20 245); }
|
|
86
|
+
|
|
87
|
+
/* ─── Widget ─────────────────────────────────────────── */
|
|
88
|
+
.feedback__widget-trigger {
|
|
89
|
+
position: fixed; top: 50%; right: 0; transform: translateY(-50%);
|
|
90
|
+
z-index: 40; padding: 0.5rem 1rem; border-radius: var(--feedback-radius) 0 0 var(--feedback-radius);
|
|
91
|
+
background: var(--feedback-accent); color: white; border: none; cursor: pointer;
|
|
92
|
+
writing-mode: vertical-rl; font-size: 0.6875rem; font-weight: 500; letter-spacing: 0.05em;
|
|
93
|
+
box-shadow: -2px 0 12px oklch(0.65 0.20 265 / 0.3);
|
|
94
|
+
}
|
|
95
|
+
.feedback__widget-trigger:hover { background: oklch(0.70 0.22 265); }
|
|
96
|
+
.feedback__widget-overlay { position: fixed; inset: 0; z-index: 50; display: flex; justify-content: flex-end; }
|
|
97
|
+
.feedback__widget-backdrop { position: absolute; inset: 0; background: oklch(0 0 0 / 0.5); backdrop-filter: blur(4px); }
|
|
98
|
+
.feedback__widget-panel { position: relative; width: 100%; max-width: 28rem; background: oklch(0.06 0.01 250); border-left: 1px solid var(--feedback-border); padding: 1.5rem; overflow-y: auto; }
|
|
99
|
+
.feedback__widget-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem; }
|
|
100
|
+
.feedback__widget-title { font-size: 1.125rem; font-weight: 700; color: var(--feedback-text); }
|
|
101
|
+
|
|
102
|
+
/* ─── Form ───────────────────────────────────────────── */
|
|
103
|
+
.feedback__form { display: flex; flex-direction: column; gap: 1rem; }
|
|
104
|
+
.feedback__form-input { width: 100%; padding: 0.75rem 1rem; border: 1px solid var(--feedback-border); border-radius: var(--feedback-radius); background: oklch(1 0 0 / 0.03); font-size: 0.875rem; color: var(--feedback-text); outline: none; }
|
|
105
|
+
.feedback__form-input::placeholder { color: oklch(1 0 0 / 0.3); }
|
|
106
|
+
.feedback__form-input:focus { border-color: oklch(0.65 0.20 265 / 0.4); }
|
|
107
|
+
.feedback__form-textarea { resize: none; min-height: 6rem; }
|
|
108
|
+
.feedback__form-error { font-size: 0.6875rem; color: oklch(0.60 0.25 25); }
|
|
109
|
+
.feedback__form-actions { display: flex; justify-content: flex-end; gap: 0.5rem; }
|
|
110
|
+
|
|
111
|
+
/* ─── Type Selector ──────────────────────────────────── */
|
|
112
|
+
.feedback__type-selector { display: flex; gap: 0.375rem; }
|
|
113
|
+
.feedback__type-tab { display: flex; align-items: center; gap: 0.375rem; padding: 0.5rem 0.75rem; border-radius: calc(var(--feedback-radius) * 0.8); border: none; font-size: 0.6875rem; font-weight: 500; cursor: pointer; transition: all 0.15s; }
|
|
114
|
+
.feedback__type-tab--inactive { background: oklch(1 0 0 / 0.05); color: oklch(1 0 0 / 0.5); }
|
|
115
|
+
.feedback__type-tab--inactive:hover { background: oklch(1 0 0 / 0.1); }
|
|
116
|
+
|
|
117
|
+
/* ─── NPS ────────────────────────────────────────────── */
|
|
118
|
+
.feedback__nps { position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 50; width: 24rem; border: 1px solid oklch(1 0 0 / 0.1); border-radius: calc(var(--feedback-radius) * 1.5); background: oklch(0.08 0.01 250); padding: 1.5rem; box-shadow: 0 8px 32px oklch(0 0 0 / 0.5); }
|
|
119
|
+
.feedback__nps-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.25rem; }
|
|
120
|
+
.feedback__nps-title { font-size: 0.875rem; font-weight: 600; color: oklch(1 0 0 / 0.9); }
|
|
121
|
+
.feedback__nps-subtitle { font-size: 0.6875rem; color: oklch(1 0 0 / 0.4); margin-bottom: 1rem; }
|
|
122
|
+
.feedback__nps-scores { display: flex; gap: 0.25rem; }
|
|
123
|
+
.feedback__nps-score-btn { flex: 1; padding: 0.625rem 0; border: none; border-radius: calc(var(--feedback-radius) * 0.8); background: oklch(1 0 0 / 0.05); color: oklch(1 0 0 / 0.5); font-size: 0.6875rem; font-weight: 700; cursor: pointer; transition: all 0.15s; }
|
|
124
|
+
.feedback__nps-score-btn:hover { background: oklch(1 0 0 / 0.1); }
|
|
125
|
+
.feedback__nps-labels { display: flex; justify-content: space-between; margin-top: 0.5rem; font-size: 0.625rem; color: oklch(1 0 0 / 0.2); }
|
|
126
|
+
.feedback__nps-results { padding: 1.25rem; border: 1px solid var(--feedback-border); border-radius: var(--feedback-radius); background: oklch(1 0 0 / 0.02); }
|
|
127
|
+
.feedback__nps-results-score { font-size: 1.875rem; font-weight: 900; font-variant-numeric: tabular-nums; }
|
|
128
|
+
.feedback__nps-bar { display: flex; height: 1.25rem; overflow: hidden; border-radius: 9999px; margin: 1rem 0; }
|
|
129
|
+
.feedback__nps-bar-detractors { background: oklch(0.60 0.25 25 / 0.7); }
|
|
130
|
+
.feedback__nps-bar-passives { background: oklch(0.72 0.18 60 / 0.7); }
|
|
131
|
+
.feedback__nps-bar-promoters { background: oklch(0.72 0.18 155 / 0.7); }
|
|
132
|
+
.feedback__nps-breakdown { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; text-align: center; font-size: 0.6875rem; }
|
|
133
|
+
|
|
134
|
+
/* ─── Board ──────────────────────────────────────────── */
|
|
135
|
+
.feedback__board { display: flex; flex-direction: column; gap: 0.625rem; }
|
|
136
|
+
|
|
137
|
+
/* ─── Admin Table ────────────────────────────────────── */
|
|
138
|
+
.feedback__admin-table { width: 100%; font-size: 0.875rem; border-collapse: collapse; }
|
|
139
|
+
.feedback__admin-table th { padding: 0.75rem 1rem; text-align: left; font-size: 0.625rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; color: oklch(1 0 0 / 0.4); border-bottom: 1px solid oklch(1 0 0 / 0.08); }
|
|
140
|
+
.feedback__admin-table td { padding: 0.75rem 1rem; border-bottom: 1px solid oklch(1 0 0 / 0.05); }
|
|
141
|
+
.feedback__admin-table tr:hover { background: oklch(1 0 0 / 0.02); }
|
|
142
|
+
.feedback__admin-select { padding: 0.25rem 0.5rem; border: 1px solid var(--feedback-border); border-radius: calc(var(--feedback-radius) * 0.6); background: oklch(1 0 0 / 0.05); font-size: 0.6875rem; color: var(--feedback-text); outline: none; }
|
|
143
|
+
|
|
144
|
+
/* ─── Stats Grid ─────────────────────────────────────── */
|
|
145
|
+
.feedback__stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
146
|
+
.feedback__stat-card { padding: 1rem; border: 1px solid var(--feedback-border); border-radius: var(--feedback-radius); background: oklch(1 0 0 / 0.02); }
|
|
147
|
+
.feedback__stat-icon { font-size: 1.125rem; }
|
|
148
|
+
.feedback__stat-value { font-size: 1.5rem; font-weight: 700; font-variant-numeric: tabular-nums; color: oklch(1 0 0 / 0.9); }
|
|
149
|
+
.feedback__stat-label { font-size: 0.6875rem; color: oklch(1 0 0 / 0.4); }
|
|
150
|
+
|
|
151
|
+
/* ─── Search ─────────────────────────────────────────── */
|
|
152
|
+
.feedback__search { width: 100%; max-width: 16rem; padding: 0.625rem 1rem; border: 1px solid var(--feedback-border); border-radius: var(--feedback-radius); background: oklch(1 0 0 / 0.03); font-size: 0.875rem; color: var(--feedback-text); outline: none; }
|
|
153
|
+
.feedback__search::placeholder { color: oklch(1 0 0 / 0.3); }
|
|
154
|
+
.feedback__search:focus { border-color: oklch(0.65 0.20 265 / 0.4); }
|
|
155
|
+
|
|
156
|
+
/* ─── Filter Tabs ────────────────────────────────────── */
|
|
157
|
+
.feedback__filter-tabs { display: flex; flex-wrap: wrap; gap: 0.375rem; }
|
|
158
|
+
.feedback__filter-tab { padding: 0.375rem 0.75rem; border: none; border-radius: calc(var(--feedback-radius) * 0.8); font-size: 0.6875rem; font-weight: 500; cursor: pointer; }
|
|
159
|
+
.feedback__filter-tab--active { background: var(--feedback-accent); color: white; }
|
|
160
|
+
.feedback__filter-tab--inactive { background: oklch(1 0 0 / 0.05); color: oklch(1 0 0 / 0.5); }
|
|
161
|
+
.feedback__filter-tab--inactive:hover { background: oklch(1 0 0 / 0.1); }
|
|
162
|
+
|
|
163
|
+
/* ─── Btn ────────────────────────────────────────────── */
|
|
164
|
+
.feedback__btn { padding: 0.5rem 1rem; border-radius: calc(var(--feedback-radius) * 0.8); font-size: 0.6875rem; font-weight: 500; cursor: pointer; border: none; transition: all 0.15s; }
|
|
165
|
+
.feedback__btn--primary { background: var(--feedback-accent); color: white; }
|
|
166
|
+
.feedback__btn--primary:hover { background: oklch(0.70 0.22 265); }
|
|
167
|
+
.feedback__btn--outline { border: 1px solid var(--feedback-border); background: transparent; color: oklch(1 0 0 / 0.6); }
|
|
168
|
+
.feedback__btn--danger { background: oklch(0.60 0.25 25 / 0.2); color: oklch(0.60 0.25 25); }
|
|
169
|
+
|
|
170
|
+
/* ─── Skeleton ───────────────────────────────────────── */
|
|
171
|
+
.feedback__skeleton { background: oklch(1 0 0 / 0.05); border-radius: var(--feedback-radius); animation: feedback-pulse 1.5s ease-in-out infinite; }
|
|
172
|
+
@keyframes feedback-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
173
|
+
|
|
174
|
+
/* ─── Empty ──────────────────────────────────────────── */
|
|
175
|
+
.feedback__empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 4rem 1rem; text-align: center; }
|
|
176
|
+
.feedback__empty-icon { font-size: 3rem; opacity: 0.2; margin-bottom: 0.75rem; }
|
|
177
|
+
.feedback__empty-text { font-size: 0.875rem; color: oklch(1 0 0 / 0.4); }
|
|
178
|
+
|
|
179
|
+
/* ─── Success ────────────────────────────────────────── */
|
|
180
|
+
.feedback__success { display: flex; flex-direction: column; align-items: center; padding: 2rem; text-align: center; }
|
|
181
|
+
.feedback__success-icon { font-size: 2.5rem; margin-bottom: 0.75rem; }
|
|
182
|
+
.feedback__success-text { font-size: 1rem; font-weight: 600; color: oklch(1 0 0 / 0.9); margin-bottom: 0.25rem; }
|
|
183
|
+
.feedback__success-sub { font-size: 0.875rem; color: oklch(1 0 0 / 0.5); }
|
|
184
|
+
|
|
185
|
+
/* ─── Tag ────────────────────────────────────────────── */
|
|
186
|
+
.feedback__tag { padding: 0.125rem 0.375rem; border-radius: 0.25rem; background: oklch(1 0 0 / 0.05); font-size: 0.625rem; color: oklch(1 0 0 / 0.3); }
|
|
187
|
+
|
|
188
|
+
/* ─── Close Button ───────────────────────────────────── */
|
|
189
|
+
.feedback__close-btn { padding: 0.375rem; border: none; background: transparent; color: oklch(1 0 0 / 0.3); cursor: pointer; border-radius: calc(var(--feedback-radius) * 0.6); }
|
|
190
|
+
.feedback__close-btn:hover { color: oklch(1 0 0 / 0.5); background: oklch(1 0 0 / 0.05); }
|
|
191
|
+
|
|
192
|
+
/* ─── Comment Thread ─────────────────────────────────── */
|
|
193
|
+
.feedback__comment-thread { display: flex; flex-direction: column; gap: 1rem; padding: 1rem; border: 1px solid var(--feedback-border); border-radius: var(--feedback-radius); background: oklch(1 0 0 / 0.01); }
|
|
194
|
+
.feedback__comment { display: flex; gap: 0.75rem; }
|
|
195
|
+
.feedback__comment-avatar { width: 2rem; height: 2rem; border-radius: 50%; background: oklch(0.65 0.20 265 / 0.2); display: flex; align-items: center; justify-content: center; font-size: 0.875rem; flex-shrink: 0; }
|
|
196
|
+
.feedback__comment-body { flex: 1; }
|
|
197
|
+
.feedback__comment-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem; }
|
|
198
|
+
.feedback__comment-author { font-size: 0.75rem; font-weight: 600; color: oklch(1 0 0 / 0.8); }
|
|
199
|
+
.feedback__comment-time { font-size: 0.625rem; color: oklch(1 0 0 / 0.3); }
|
|
200
|
+
.feedback__comment-text { font-size: 0.6875rem; color: oklch(1 0 0 / 0.7); line-height: 1.4; }
|
|
201
|
+
.feedback__comment-reply-btn { margin-top: 0.5rem; padding: 0.25rem 0.5rem; font-size: 0.625rem; border: none; background: transparent; color: oklch(0.65 0.20 265); cursor: pointer; transition: all 0.1s; }
|
|
202
|
+
.feedback__comment-reply-btn:hover { color: oklch(0.70 0.22 265); }
|
|
203
|
+
|
|
204
|
+
/* ─── Rating Component ───────────────────────────────── */
|
|
205
|
+
.feedback__rating { display: flex; gap: 0.25rem; }
|
|
206
|
+
.feedback__rating-star { font-size: 1.5rem; cursor: pointer; opacity: 0.3; transition: all 0.15s; }
|
|
207
|
+
.feedback__rating-star--filled { opacity: 1; color: oklch(0.72 0.18 60); }
|
|
208
|
+
.feedback__rating-star:hover { opacity: 1; transform: scale(1.1); }
|
|
209
|
+
|
|
210
|
+
/* ─── Sentiment Indicator ────────────────────────────── */
|
|
211
|
+
.feedback__sentiment-card { padding: 1rem; border: 1px solid var(--feedback-border); border-radius: var(--feedback-radius); background: oklch(1 0 0 / 0.02); text-align: center; }
|
|
212
|
+
.feedback__sentiment-icon { font-size: 2rem; margin-bottom: 0.5rem; }
|
|
213
|
+
.feedback__sentiment-label { font-size: 0.875rem; font-weight: 600; color: oklch(1 0 0 / 0.8); margin-bottom: 0.25rem; }
|
|
214
|
+
.feedback__sentiment-count { font-size: 0.6875rem; color: oklch(1 0 0 / 0.4); }
|
|
215
|
+
|
|
216
|
+
/* ─── Response Panel ─────────────────────────────────── */
|
|
217
|
+
.feedback__response-panel { border: 1px solid var(--feedback-border); border-radius: var(--feedback-radius); background: oklch(1 0 0 / 0.02); padding: 1rem; }
|
|
218
|
+
.feedback__response-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem; }
|
|
219
|
+
.feedback__response-title { font-size: 0.875rem; font-weight: 600; color: oklch(1 0 0 / 0.8); }
|
|
220
|
+
.feedback__response-time { font-size: 0.625rem; color: oklch(1 0 0 / 0.3); }
|
|
221
|
+
.feedback__response-body { font-size: 0.6875rem; color: oklch(1 0 0 / 0.7); line-height: 1.5; }
|
|
222
|
+
.feedback__response-actions { display: flex; gap: 0.5rem; margin-top: 0.75rem; }
|
|
223
|
+
.feedback__response-btn { padding: 0.25rem 0.5rem; font-size: 0.625rem; border: none; background: oklch(1 0 0 / 0.05); color: oklch(1 0 0 / 0.5); cursor: pointer; border-radius: calc(var(--feedback-radius) * 0.6); transition: all 0.1s; }
|
|
224
|
+
.feedback__response-btn:hover { background: oklch(1 0 0 / 0.1); color: oklch(1 0 0 / 0.6); }
|
|
225
|
+
|
|
226
|
+
/* ─── Analytics Chart ────────────────────────────────── */
|
|
227
|
+
.feedback__analytics { border: 1px solid var(--feedback-border); border-radius: var(--feedback-radius); background: oklch(1 0 0 / 0.02); padding: 1rem; }
|
|
228
|
+
.feedback__analytics-title { font-size: 0.875rem; font-weight: 600; color: oklch(1 0 0 / 0.8); margin-bottom: 1rem; }
|
|
229
|
+
.feedback__analytics-chart { display: flex; align-items: flex-end; gap: 0.5rem; height: 10rem; }
|
|
230
|
+
.feedback__analytics-bar { flex: 1; border-radius: 0.25rem 0.25rem 0 0; background: linear-gradient(to top, oklch(0.65 0.20 265), oklch(0.65 0.20 265 / 0.4)); transition: all 0.2s; position: relative; }
|
|
231
|
+
.feedback__analytics-bar:hover { background: linear-gradient(to top, oklch(0.70 0.22 265), oklch(0.70 0.22 265)); }
|
|
232
|
+
.feedback__analytics-label { position: absolute; bottom: -1.5rem; left: 50%; transform: translateX(-50%); font-size: 0.625rem; color: oklch(1 0 0 / 0.4); white-space: nowrap; }
|
|
233
|
+
|
|
234
|
+
/* ─── Filter Control Panel ───────────────────────────── */
|
|
235
|
+
.feedback__filter-panel { border: 1px solid var(--feedback-border); border-radius: var(--feedback-radius); background: oklch(1 0 0 / 0.02); padding: 1rem; }
|
|
236
|
+
.feedback__filter-section { margin-bottom: 1rem; }
|
|
237
|
+
.feedback__filter-section:last-child { margin-bottom: 0; }
|
|
238
|
+
.feedback__filter-section-title { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; color: oklch(1 0 0 / 0.4); margin-bottom: 0.5rem; letter-spacing: 0.05em; }
|
|
239
|
+
.feedback__filter-options { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
240
|
+
.feedback__filter-option { display: flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0; font-size: 0.6875rem; }
|
|
241
|
+
.feedback__filter-option input { cursor: pointer; }
|
|
242
|
+
.feedback__filter-option-label { color: oklch(1 0 0 / 0.7); cursor: pointer; }
|
|
243
|
+
|
|
244
|
+
/* ─── Tag Selector ───────────────────────────────────── */
|
|
245
|
+
.feedback__tag-selector { padding: 1rem; border: 1px solid var(--feedback-border); border-radius: var(--feedback-radius); background: oklch(1 0 0 / 0.02); }
|
|
246
|
+
.feedback__tag-selector-label { display: block; font-size: 0.6875rem; font-weight: 500; color: oklch(1 0 0 / 0.6); margin-bottom: 0.5rem; text-transform: uppercase; }
|
|
247
|
+
.feedback__tag-list { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
|
248
|
+
.feedback__tag-item { display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.375rem 0.75rem; border-radius: 9999px; background: oklch(0.65 0.20 265 / 0.15); color: oklch(0.65 0.20 265); font-size: 0.6875rem; font-weight: 500; }
|
|
249
|
+
.feedback__tag-remove { margin-left: 0.25rem; cursor: pointer; font-weight: 700; opacity: 0.6; transition: opacity 0.1s; }
|
|
250
|
+
.feedback__tag-remove:hover { opacity: 1; }
|
|
251
|
+
|
|
252
|
+
/* ─── Feedback Form Actions ──────────────────────────── */
|
|
253
|
+
.feedback__form-footer { display: flex; justify-content: space-between; align-items: center; padding-top: 1rem; border-top: 1px solid var(--feedback-border); }
|
|
254
|
+
.feedback__form-counter { font-size: 0.625rem; color: oklch(1 0 0 / 0.3); }
|
|
255
|
+
.feedback__form-actions-group { display: flex; gap: 0.5rem; }
|
|
256
|
+
|
|
257
|
+
/* ─── Feedback Status Indicator ──────────────────────── */
|
|
258
|
+
.feedback__status-indicator { display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.375rem 0.75rem; border-radius: 9999px; font-size: 0.625rem; font-weight: 500; }
|
|
259
|
+
.feedback__status-indicator--new { background: oklch(0.65 0.20 245 / 0.15); color: oklch(0.65 0.20 245); }
|
|
260
|
+
.feedback__status-indicator--in-review { background: oklch(0.72 0.18 60 / 0.15); color: oklch(0.72 0.18 60); }
|
|
261
|
+
.feedback__status-indicator--implemented { background: oklch(0.72 0.18 155 / 0.15); color: oklch(0.72 0.18 155); }
|
|
262
|
+
|
|
263
|
+
/* ─── Detailed Stats Row ─────────────────────────────── */
|
|
264
|
+
.feedback__detailed-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 0.5rem; }
|
|
265
|
+
.feedback__detailed-stat-item { padding: 0.75rem; border: 1px solid var(--feedback-border); border-radius: var(--feedback-radius); background: oklch(1 0 0 / 0.01); text-align: center; }
|
|
266
|
+
.feedback__detailed-stat-icon { font-size: 1.25rem; margin-bottom: 0.25rem; }
|
|
267
|
+
.feedback__detailed-stat-value { font-size: 1rem; font-weight: 700; color: oklch(1 0 0 / 0.9); }
|
|
268
|
+
.feedback__detailed-stat-label { font-size: 0.625rem; color: oklch(1 0 0 / 0.4); margin-top: 0.25rem; }
|
|
269
|
+
|
|
270
|
+
/* ─── Breadcrumb Navigation ──────────────────────────── */
|
|
271
|
+
.feedback__breadcrumb { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; margin-bottom: 1rem; }
|
|
272
|
+
.feedback__breadcrumb-item { color: oklch(1 0 0 / 0.5); }
|
|
273
|
+
.feedback__breadcrumb-item--active { color: oklch(1 0 0 / 0.8); font-weight: 500; }
|
|
274
|
+
.feedback__breadcrumb-sep { color: oklch(1 0 0 / 0.2); margin: 0 0.25rem; }
|
|
275
|
+
|
|
276
|
+
/* ─── Pagination Controls ────────────────────────────── */
|
|
277
|
+
.feedback__pagination { display: flex; align-items: center; gap: 0.5rem; justify-content: center; padding: 1rem; }
|
|
278
|
+
.feedback__pagination-btn { padding: 0.375rem 0.75rem; border: 1px solid var(--feedback-border); border-radius: calc(var(--feedback-radius) * 0.6); background: oklch(1 0 0 / 0.05); color: oklch(1 0 0 / 0.6); font-size: 0.6875rem; cursor: pointer; transition: all 0.1s; }
|
|
279
|
+
.feedback__pagination-btn:hover:not(:disabled) { background: oklch(1 0 0 / 0.1); color: oklch(1 0 0 / 0.8); }
|
|
280
|
+
.feedback__pagination-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
281
|
+
.feedback__pagination-info { font-size: 0.6875rem; color: oklch(1 0 0 / 0.5); }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"target": "ES2022",
|
|
13
|
+
"module": "ESNext",
|
|
14
|
+
"moduleResolution": "bundler"
|
|
15
|
+
},
|
|
16
|
+
"include": [
|
|
17
|
+
"src"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# ✦ @geenius-feedback/shared\n\n> Geenius Feedback — Shared types & Convex schema\n\n---\n\n## Overview\nBuilt with Steve Jobs-level minimalism and Jony Ive-level craftsmanship, this package is designed to deliver unparalleled developer experience (DX) and rock-solid performance.\n\n## Installation\n\n```bash\npnpm add @geenius-feedback/shared\n```\n\n## Usage\n\n```typescript\nimport { init } from '@geenius-feedback/shared';\n\n// Initialize the module with absolute precision\ninit({\n mode: 'premium',\n});\n```\n\n## Architecture\n- **Zero-config**: It just works.\n- **Strictly Typed**: Fully written in TypeScript for flawless IntelliSense.\n- **Framework Agnostic**: seamlessly integrates into the Geenius ecosystem.\n\n---\n\n*Designed by Antigravity HQ*\n
|