@geenius/feedback 0.1.0 → 0.3.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.
Files changed (104) hide show
  1. package/package.json +16 -3
  2. package/packages/convex/dist/index.d.ts +192 -0
  3. package/packages/convex/dist/index.js +239 -0
  4. package/packages/convex/dist/index.js.map +1 -0
  5. package/packages/react/README.md +1 -1
  6. package/packages/react/dist/index.d.ts +146 -0
  7. package/packages/react/dist/index.js +545 -0
  8. package/packages/react/dist/index.js.map +1 -0
  9. package/packages/react-css/README.md +1 -1
  10. package/packages/react-css/dist/index.css +965 -0
  11. package/packages/react-css/dist/index.css.map +1 -0
  12. package/packages/react-css/dist/index.d.ts +49 -0
  13. package/packages/react-css/dist/index.js +228 -0
  14. package/packages/react-css/dist/index.js.map +1 -0
  15. package/packages/shared/README.md +1 -1
  16. package/packages/shared/dist/index.d.ts +115 -0
  17. package/packages/shared/dist/index.js +112 -0
  18. package/packages/shared/dist/index.js.map +1 -0
  19. package/packages/solidjs/README.md +1 -1
  20. package/packages/solidjs/dist/index.d.ts +128 -0
  21. package/packages/solidjs/dist/index.js +289 -0
  22. package/packages/solidjs/dist/index.js.map +1 -0
  23. package/packages/solidjs-css/README.md +1 -1
  24. package/packages/solidjs-css/dist/index.css +965 -0
  25. package/packages/solidjs-css/dist/index.css.map +1 -0
  26. package/packages/solidjs-css/dist/index.d.ts +2 -0
  27. package/packages/solidjs-css/dist/index.js +29 -0
  28. package/packages/solidjs-css/dist/index.js.map +1 -0
  29. package/.changeset/config.json +0 -11
  30. package/.github/CODEOWNERS +0 -1
  31. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -16
  32. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -11
  33. package/.github/PULL_REQUEST_TEMPLATE.md +0 -10
  34. package/.github/dependabot.yml +0 -11
  35. package/.github/workflows/ci.yml +0 -23
  36. package/.github/workflows/release.yml +0 -29
  37. package/.nvmrc +0 -1
  38. package/.project/ACCOUNT.yaml +0 -4
  39. package/.project/IDEAS.yaml +0 -7
  40. package/.project/PROJECT.yaml +0 -11
  41. package/.project/ROADMAP.yaml +0 -15
  42. package/CODE_OF_CONDUCT.md +0 -16
  43. package/CONTRIBUTING.md +0 -26
  44. package/SECURITY.md +0 -15
  45. package/SUPPORT.md +0 -8
  46. package/packages/convex/package.json +0 -42
  47. package/packages/convex/src/index.ts +0 -3
  48. package/packages/convex/src/mutations.ts +0 -88
  49. package/packages/convex/src/queries.ts +0 -78
  50. package/packages/convex/src/schema.ts +0 -47
  51. package/packages/convex/tsconfig.json +0 -18
  52. package/packages/convex/tsup.config.ts +0 -17
  53. package/packages/react/package.json +0 -49
  54. package/packages/react/src/components/FeedbackCard.tsx +0 -51
  55. package/packages/react/src/components/FeedbackForm.tsx +0 -43
  56. package/packages/react/src/components/FeedbackWidget.tsx +0 -32
  57. package/packages/react/src/components/NPSSurvey.tsx +0 -62
  58. package/packages/react/src/components/index.ts +0 -4
  59. package/packages/react/src/hooks/index.ts +0 -5
  60. package/packages/react/src/hooks/useFeedback.ts +0 -23
  61. package/packages/react/src/hooks/useFeedbackAdmin.ts +0 -24
  62. package/packages/react/src/hooks/useFeedbackForm.ts +0 -35
  63. package/packages/react/src/hooks/useNPS.ts +0 -26
  64. package/packages/react/src/index.tsx +0 -13
  65. package/packages/react/src/pages/FeedbackAdminPage.tsx +0 -71
  66. package/packages/react/src/pages/FeedbackPublicPage.tsx +0 -42
  67. package/packages/react/src/pages/FeedbackWidgetPage.tsx +0 -25
  68. package/packages/react/src/pages/index.ts +0 -3
  69. package/packages/react/tsconfig.json +0 -19
  70. package/packages/react/tsup.config.ts +0 -12
  71. package/packages/react-css/package.json +0 -36
  72. package/packages/react-css/src/components/index.ts +0 -5
  73. package/packages/react-css/src/components/index.tsx +0 -107
  74. package/packages/react-css/src/hooks/index.ts +0 -2
  75. package/packages/react-css/src/index.tsx +0 -5
  76. package/packages/react-css/src/pages/FeedbackAdminPage.tsx +0 -112
  77. package/packages/react-css/src/pages/FeedbackPage.tsx +0 -76
  78. package/packages/react-css/src/styles.css +0 -281
  79. package/packages/react-css/tsconfig.json +0 -19
  80. package/packages/react-css/tsup.config.ts +0 -10
  81. package/packages/shared/package.json +0 -44
  82. package/packages/shared/src/__tests__/feedback.test.ts +0 -72
  83. package/packages/shared/src/config.ts +0 -49
  84. package/packages/shared/src/index.ts +0 -111
  85. package/packages/shared/src/types.ts +0 -59
  86. package/packages/shared/tsconfig.json +0 -18
  87. package/packages/shared/tsup.config.ts +0 -11
  88. package/packages/shared/vitest.config.ts +0 -4
  89. package/packages/solidjs/package.json +0 -45
  90. package/packages/solidjs/src/components.tsx +0 -72
  91. package/packages/solidjs/src/index.tsx +0 -3
  92. package/packages/solidjs/src/primitives.ts +0 -49
  93. package/packages/solidjs/tsconfig.json +0 -20
  94. package/packages/solidjs/tsup.config.ts +0 -12
  95. package/packages/solidjs-css/package.json +0 -32
  96. package/packages/solidjs-css/src/index.tsx +0 -4
  97. package/packages/solidjs-css/src/pages/FeedbackAdminPage.tsx +0 -78
  98. package/packages/solidjs-css/src/pages/FeedbackPage.tsx +0 -65
  99. package/packages/solidjs-css/src/primitives/index.ts +0 -1
  100. package/packages/solidjs-css/src/styles.css +0 -281
  101. package/packages/solidjs-css/tsconfig.json +0 -20
  102. package/packages/solidjs-css/tsup.config.ts +0 -10
  103. package/pnpm-workspace.yaml +0 -2
  104. package/tsconfig.json +0 -23
@@ -1,18 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src",
6
- "strict": true,
7
- "skipLibCheck": true,
8
- "forceConsistentCasingInFileNames": true,
9
- "resolveJsonModule": true,
10
- "isolatedModules": true,
11
- "target": "ES2022",
12
- "module": "ESNext",
13
- "moduleResolution": "bundler"
14
- },
15
- "include": [
16
- "src"
17
- ]
18
- }
@@ -1,17 +0,0 @@
1
- import { defineConfig } from 'tsup'
2
-
3
- export default defineConfig({
4
- entry: {
5
- index: 'src/index.ts',
6
- },
7
- format: ['esm'],
8
- dts: true,
9
- clean: true,
10
- sourcemap: true,
11
- external: [
12
- 'convex',
13
- 'convex/server',
14
- 'convex/values',
15
- 'convex/react',
16
- ],
17
- })
@@ -1,49 +0,0 @@
1
- {
2
- "name": "@geenius-feedback/react",
3
- "version": "0.1.0",
4
- "private": false,
5
- "type": "module",
6
- "description": "Geenius Feedback — React components & hooks",
7
- "author": "Antigravity HQ",
8
- "license": "MIT",
9
- "publishConfig": {
10
- "access": "public"
11
- },
12
- "main": "./dist/index.js",
13
- "module": "./dist/index.js",
14
- "types": "./dist/index.d.ts",
15
- "exports": {
16
- ".": {
17
- "types": "./dist/index.d.ts",
18
- "import": "./dist/index.js"
19
- }
20
- },
21
- "files": [
22
- "dist",
23
- "src"
24
- ],
25
- "scripts": {
26
- "build": "tsup",
27
- "clean": "rm -rf dist",
28
- "type-check": "tsc --noEmit",
29
- "prepublishOnly": "pnpm clean && pnpm build"
30
- },
31
- "dependencies": {
32
- "@geenius-feedback/shared": "workspace:*"
33
- },
34
- "devDependencies": {
35
- "@types/react": "^19.0.0",
36
- "@types/react-dom": "^19.0.0",
37
- "react": "^19.2.4",
38
- "react-dom": "^19.2.4",
39
- "tsup": "^8.5.1",
40
- "typescript": "~6.0.2"
41
- },
42
- "peerDependencies": {
43
- "react": "^18.0.0 || ^19.0.0",
44
- "react-dom": "^18.0.0 || ^19.0.0"
45
- },
46
- "engines": {
47
- "node": ">=20.0.0"
48
- }
49
- }
@@ -1,51 +0,0 @@
1
- import React from 'react'
2
- import type { FeedbackStatus, FeedbackPriority, FeedbackType, FeedbackItem } from '@geenius-feedback/shared'
3
- import { STATUS_CONFIG, PRIORITY_CONFIG, TYPE_CONFIG, formatRelativeTime } from '@geenius-feedback/shared'
4
-
5
- export function StatusBadge({ status }: { status: FeedbackStatus }) {
6
- const cfg = STATUS_CONFIG[status]
7
- return <span className="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-[10px] font-medium" style={{ background: `${cfg.color}22`, color: cfg.color }}><span>{cfg.emoji}</span>{cfg.label}</span>
8
- }
9
-
10
- export function PriorityBadge({ priority }: { priority: FeedbackPriority }) {
11
- const cfg = PRIORITY_CONFIG[priority]
12
- return <span className="rounded-full px-2 py-0.5 text-[10px] font-medium" style={{ background: `${cfg.color}22`, color: cfg.color }}>{cfg.label}</span>
13
- }
14
-
15
- export function TypeBadge({ type }: { type: FeedbackType }) {
16
- const cfg = TYPE_CONFIG[type]
17
- return <span className="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium" style={{ background: `${cfg.color}22`, color: cfg.color }}><span>{cfg.icon}</span>{cfg.label}</span>
18
- }
19
-
20
- export function FeedbackCard({ item, onVote, onClick, hasVoted }: { item: FeedbackItem; onVote?: (id: string) => void; onClick?: (id: string) => void; hasVoted?: boolean }) {
21
- return (
22
- <div className="group flex gap-3 rounded-xl border border-white/8 bg-white/[0.02] p-4 transition-all hover:border-indigo-500/20 hover:bg-white/[0.04] cursor-pointer" onClick={() => onClick?.(item.id)}>
23
- {onVote && (
24
- <button type="button" onClick={e => { e.stopPropagation(); onVote(item.id) }}
25
- className={`flex flex-col items-center gap-0.5 rounded-lg px-2 py-1.5 text-xs font-bold transition-colors ${hasVoted ? 'bg-indigo-500/15 text-indigo-400' : 'bg-white/5 text-white/30 hover:bg-white/10 hover:text-white/50'}`}>
26
- <span className="text-sm">{hasVoted ? '▲' : '△'}</span>
27
- <span className="tabular-nums">{item.votes}</span>
28
- </button>
29
- )}
30
- <div className="flex-1 min-w-0">
31
- <div className="flex items-center gap-2 mb-1 flex-wrap">
32
- <TypeBadge type={item.type} />
33
- <StatusBadge status={item.status} />
34
- <PriorityBadge priority={item.priority} />
35
- </div>
36
- <h3 className="text-sm font-semibold text-white/90 mb-0.5 truncate">{item.title}</h3>
37
- <p className="text-xs text-white/40 line-clamp-2">{item.description}</p>
38
- <div className="mt-2 flex items-center gap-3 text-[10px] text-white/25">
39
- {item.userName && <span>{item.userName}</span>}
40
- <span>{formatRelativeTime(item.createdAt)}</span>
41
- {item.tags.length > 0 && item.tags.map(t => <span key={t} className="rounded bg-white/5 px-1.5 py-0.5">{t}</span>)}
42
- </div>
43
- </div>
44
- </div>
45
- )
46
- }
47
-
48
- export function FeedbackList({ items, onVote, votedIds, onStatusChange }: { items: FeedbackItem[]; onVote?: (id: string) => void; votedIds?: string[]; onStatusChange?: (id: string, status: FeedbackStatus) => void }) {
49
- if (items.length === 0) return <div className="flex flex-col items-center py-16"><div className="mb-3 text-5xl opacity-20">💬</div><p className="text-sm text-white/40">No feedback yet</p></div>
50
- return <div className="space-y-2">{items.map(item => <FeedbackCard key={item.id} item={item} onVote={onVote} hasVoted={votedIds?.includes(item.id)} />)}</div>
51
- }
@@ -1,43 +0,0 @@
1
- import React, { useState } from 'react'
2
- import type { FeedbackType } from '@geenius-feedback/shared'
3
- import { TYPE_CONFIG, FEEDBACK_TYPES } from '@geenius-feedback/shared'
4
- import { useFeedbackForm } from '../hooks/useFeedbackForm'
5
-
6
- export function FeedbackTypeSelector({ value, onChange }: { value: FeedbackType; onChange: (t: FeedbackType) => void }) {
7
- return (
8
- <div className="flex gap-1.5">
9
- {FEEDBACK_TYPES.map(t => {
10
- const cfg = TYPE_CONFIG[t]
11
- return <button key={t} type="button" onClick={() => onChange(t)}
12
- className={`flex items-center gap-1.5 rounded-lg px-3 py-2 text-xs font-medium transition-all ${value === t ? 'text-white' : 'bg-white/5 text-white/50 hover:bg-white/10'}`}
13
- style={value === t ? { background: cfg.color } : undefined}><span>{cfg.icon}</span>{cfg.label}</button>
14
- })}
15
- </div>
16
- )
17
- }
18
-
19
- export function FeedbackForm({ onSubmit, onClose }: { onSubmit: (data: { type: FeedbackType; title: string; description: string }) => Promise<string>; onClose?: () => void }) {
20
- const form = useFeedbackForm(onSubmit)
21
-
22
- if (form.success) return (
23
- <div className="flex flex-col items-center py-8 text-center"><div className="mb-3 text-4xl">🎉</div><h3 className="text-base font-semibold text-white/90 mb-1">Thank you!</h3><p className="text-sm text-white/50 mb-4">Your feedback has been submitted.</p>
24
- <button type="button" onClick={() => { form.reset(); onClose?.() }} className="rounded-lg bg-indigo-600 px-4 py-2 text-xs font-medium text-white">Done</button>
25
- </div>
26
- )
27
-
28
- return (
29
- <div className="space-y-4">
30
- <FeedbackTypeSelector value={form.formData.type} onChange={t => form.setFormData(d => ({ ...d, type: t }))} />
31
- <input type="text" placeholder="Short title for your feedback…" value={form.formData.title} onChange={e => form.setFormData(d => ({ ...d, title: e.target.value }))}
32
- className="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white placeholder-white/30 outline-none focus:border-indigo-500/40" />
33
- <textarea placeholder="Describe your feedback in detail…" value={form.formData.description} onChange={e => form.setFormData(d => ({ ...d, description: e.target.value }))} rows={4}
34
- className="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white placeholder-white/30 outline-none focus:border-indigo-500/40 resize-none" />
35
- {form.error && <p className="text-xs text-red-400">{form.error}</p>}
36
- <div className="flex justify-end gap-2">
37
- {onClose && <button type="button" onClick={onClose} className="rounded-lg border border-white/10 px-4 py-2 text-xs text-white/50 hover:bg-white/5">Cancel</button>}
38
- <button type="button" onClick={form.submit} disabled={form.isSubmitting}
39
- className="rounded-lg bg-indigo-600 px-5 py-2 text-xs font-medium text-white hover:bg-indigo-500 disabled:opacity-50">{form.isSubmitting ? 'Submitting…' : 'Submit'}</button>
40
- </div>
41
- </div>
42
- )
43
- }
@@ -1,32 +0,0 @@
1
- import React, { useState } from 'react'
2
- import type { FeedbackType } from '@geenius-feedback/shared'
3
- import { FeedbackForm } from './FeedbackForm'
4
-
5
- export function FeedbackWidget({ onSubmit, position = 'right' }: { onSubmit: (data: { type: FeedbackType; title: string; description: string }) => Promise<string>; position?: 'left' | 'right' }) {
6
- const [isOpen, setIsOpen] = useState(false)
7
-
8
- return (
9
- <>
10
- {/* Floating trigger tab */}
11
- <button type="button" onClick={() => setIsOpen(true)}
12
- className={`fixed top-1/2 -translate-y-1/2 z-40 rounded-l-xl bg-indigo-600 px-2 py-4 text-white shadow-lg hover:bg-indigo-500 transition-all ${position === 'right' ? 'right-0' : 'left-0 rounded-l-none rounded-r-xl'}`}
13
- style={{ writingMode: 'vertical-rl' }}>
14
- <span className="text-xs font-medium tracking-wider">Feedback</span>
15
- </button>
16
-
17
- {/* Slide-out panel */}
18
- {isOpen && (
19
- <div className="fixed inset-0 z-50 flex justify-end" onClick={() => setIsOpen(false)}>
20
- <div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
21
- <div className="relative w-full max-w-md bg-[#0f0f17] border-l border-white/10 p-6 overflow-y-auto" onClick={e => e.stopPropagation()}>
22
- <div className="mb-6 flex items-center justify-between">
23
- <h2 className="text-lg font-bold text-white">Send Feedback</h2>
24
- <button type="button" onClick={() => setIsOpen(false)} className="rounded-lg p-1.5 text-white/30 hover:bg-white/5 hover:text-white/50">✕</button>
25
- </div>
26
- <FeedbackForm onSubmit={onSubmit} onClose={() => setIsOpen(false)} />
27
- </div>
28
- </div>
29
- )}
30
- </>
31
- )
32
- }
@@ -1,62 +0,0 @@
1
- import React, { useState } from 'react'
2
- import type { NPSResponse, NPSStats } from '@geenius-feedback/shared'
3
- import { getNPSCategory, calcNPSStats } from '@geenius-feedback/shared'
4
-
5
- export function NPSSurvey({ onSubmit, onDismiss }: { onSubmit: (score: number, comment?: string) => void; onDismiss: () => void }) {
6
- const [score, setScore] = useState<number | null>(null)
7
- const [comment, setComment] = useState('')
8
- const [step, setStep] = useState<'score' | 'comment' | 'thanks'>('score')
9
-
10
- const handleScore = (s: number) => { setScore(s); setStep('comment') }
11
- const handleSubmit = () => { if (score !== null) { onSubmit(score, comment || undefined); setStep('thanks') } }
12
-
13
- return (
14
- <div className="fixed bottom-6 right-6 z-50 w-96 rounded-2xl border border-white/10 bg-[#111118] p-6 shadow-2xl">
15
- {step === 'score' && (
16
- <div>
17
- <div className="mb-1 flex items-center justify-between"><h3 className="text-sm font-semibold text-white/90">How likely are you to recommend us?</h3><button type="button" onClick={onDismiss} className="text-white/20 hover:text-white/40">✕</button></div>
18
- <p className="text-xs text-white/40 mb-4">0 = Not at all, 10 = Extremely likely</p>
19
- <div className="flex gap-1">{Array.from({ length: 11 }).map((_, i) => (
20
- <button key={i} type="button" onClick={() => handleScore(i)}
21
- className={`flex-1 rounded-lg py-2.5 text-xs font-bold transition-all ${i <= 6 ? 'hover:bg-red-500/20 hover:text-red-400' : i <= 8 ? 'hover:bg-amber-500/20 hover:text-amber-400' : 'hover:bg-emerald-500/20 hover:text-emerald-400'} bg-white/5 text-white/50`}>{i}</button>
22
- ))}</div>
23
- <div className="mt-2 flex justify-between text-[10px] text-white/20"><span>Not likely</span><span>Very likely</span></div>
24
- </div>
25
- )}
26
- {step === 'comment' && (
27
- <div>
28
- <h3 className="text-sm font-semibold text-white/90 mb-1">Thanks! Any additional thoughts?</h3>
29
- <p className="text-xs text-white/40 mb-3">Score: <span className={`font-bold ${score! <= 6 ? 'text-red-400' : score! <= 8 ? 'text-amber-400' : 'text-emerald-400'}`}>{score}</span></p>
30
- <textarea value={comment} onChange={e => setComment(e.target.value)} placeholder="Optional feedback…" rows={3} className="w-full rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-sm text-white placeholder-white/30 outline-none resize-none mb-3" />
31
- <div className="flex gap-2"><button type="button" onClick={handleSubmit} className="flex-1 rounded-lg bg-indigo-600 py-2 text-xs font-medium text-white hover:bg-indigo-500">Submit</button><button type="button" onClick={() => { onSubmit(score!, undefined); setStep('thanks') }} className="rounded-lg border border-white/10 px-3 py-2 text-xs text-white/50">Skip</button></div>
32
- </div>
33
- )}
34
- {step === 'thanks' && (
35
- <div className="text-center py-4"><div className="text-3xl mb-2">🎉</div><p className="text-sm font-semibold text-white/80">Thank you for your feedback!</p></div>
36
- )}
37
- </div>
38
- )
39
- }
40
-
41
- export function NPSResults({ responses }: { responses: NPSResponse[] }) {
42
- const stats = calcNPSStats(responses)
43
- const total = stats.totalResponses || 1
44
- const scoreColor = stats.npsScore >= 50 ? 'text-emerald-400' : stats.npsScore >= 0 ? 'text-amber-400' : 'text-red-400'
45
-
46
- return (
47
- <div className="rounded-xl border border-white/8 bg-white/[0.02] p-5">
48
- <div className="mb-4 flex items-center justify-between"><h4 className="text-sm font-semibold text-white/80">NPS Score</h4><span className={`text-3xl font-black tabular-nums ${scoreColor}`}>{stats.npsScore}</span></div>
49
- <div className="mb-4 flex h-5 overflow-hidden rounded-full">
50
- <div className="bg-red-500/70" style={{ width: `${(stats.detractors / total) * 100}%` }} title={`Detractors: ${stats.detractors}`} />
51
- <div className="bg-amber-500/70" style={{ width: `${(stats.passives / total) * 100}%` }} title={`Passives: ${stats.passives}`} />
52
- <div className="bg-emerald-500/70" style={{ width: `${(stats.promoters / total) * 100}%` }} title={`Promoters: ${stats.promoters}`} />
53
- </div>
54
- <div className="grid grid-cols-3 gap-2 text-center text-xs">
55
- <div><span className="block text-red-400 font-bold">{stats.detractors}</span><span className="text-white/30">Detractors</span></div>
56
- <div><span className="block text-amber-400 font-bold">{stats.passives}</span><span className="text-white/30">Passives</span></div>
57
- <div><span className="block text-emerald-400 font-bold">{stats.promoters}</span><span className="text-white/30">Promoters</span></div>
58
- </div>
59
- <div className="mt-3 pt-3 border-t border-white/5 flex justify-between text-xs text-white/40"><span>Avg: {stats.averageScore}/10</span><span>{stats.totalResponses} responses</span></div>
60
- </div>
61
- )
62
- }
@@ -1,4 +0,0 @@
1
- export { StatusBadge, PriorityBadge, TypeBadge, FeedbackCard, FeedbackList } from './FeedbackCard'
2
- export { FeedbackTypeSelector, FeedbackForm } from './FeedbackForm'
3
- export { FeedbackWidget } from './FeedbackWidget'
4
- export { NPSSurvey, NPSResults } from './NPSSurvey'
@@ -1,5 +0,0 @@
1
- export { useFeedback } from './useFeedback'
2
- export type { FeedbackFilter } from './useFeedback'
3
- export { useFeedbackForm } from './useFeedbackForm'
4
- export { useNPS } from './useNPS'
5
- export { useFeedbackAdmin } from './useFeedbackAdmin'
@@ -1,23 +0,0 @@
1
- import { useMemo, useState } from 'react'
2
- import type { FeedbackItem, FeedbackType, FeedbackStatus, FeedbackPriority, FeedbackStats } from '@geenius-feedback/shared'
3
- import { calcFeedbackStats } from '@geenius-feedback/shared'
4
-
5
- export interface FeedbackFilter { type?: FeedbackType; status?: FeedbackStatus; priority?: FeedbackPriority; search?: string; sort?: 'newest' | 'votes' }
6
-
7
- export function useFeedback(items: FeedbackItem[] | undefined) {
8
- const [filter, setFilter] = useState<FeedbackFilter>({ sort: 'newest' })
9
- const isLoading = items === undefined
10
- const stats = useMemo<FeedbackStats>(() => calcFeedbackStats(items ?? []), [items])
11
-
12
- const filteredItems = useMemo(() => {
13
- let result = items ?? []
14
- if (filter.type) result = result.filter(i => i.type === filter.type)
15
- if (filter.status) result = result.filter(i => i.status === filter.status)
16
- if (filter.priority) result = result.filter(i => i.priority === filter.priority)
17
- if (filter.search) { const q = filter.search.toLowerCase(); result = result.filter(i => i.title.toLowerCase().includes(q) || i.description.toLowerCase().includes(q)) }
18
- if (filter.sort === 'votes') result = [...result].sort((a, b) => b.votes - a.votes)
19
- return result
20
- }, [items, filter])
21
-
22
- return { items: filteredItems, allItems: items ?? [], stats, filter, setFilter, isLoading }
23
- }
@@ -1,24 +0,0 @@
1
- import { useMemo, useState } from 'react'
2
- import type { FeedbackItem, FeedbackStatus, FeedbackPriority } from '@geenius-feedback/shared'
3
- import { calcFeedbackStats } from '@geenius-feedback/shared'
4
-
5
- export function useFeedbackAdmin(
6
- items: FeedbackItem[] | undefined,
7
- mutations: {
8
- updateStatus: (id: string, status: FeedbackStatus, note?: string) => Promise<void>
9
- updatePriority: (id: string, priority: FeedbackPriority) => Promise<void>
10
- deleteFeedback: (id: string) => Promise<void>
11
- }
12
- ) {
13
- const [search, setSearch] = useState('')
14
- const isLoading = items === undefined
15
- const stats = useMemo(() => calcFeedbackStats(items ?? []), [items])
16
-
17
- const filteredItems = useMemo(() => {
18
- if (!search) return items ?? []
19
- const q = search.toLowerCase()
20
- return (items ?? []).filter(i => i.title.toLowerCase().includes(q) || i.description.toLowerCase().includes(q) || i.userEmail?.toLowerCase().includes(q))
21
- }, [items, search])
22
-
23
- return { items: filteredItems, allItems: items ?? [], stats, search, setSearch, isLoading, ...mutations }
24
- }
@@ -1,35 +0,0 @@
1
- import { useState, useCallback } from 'react'
2
- import type { FeedbackType } from '@geenius-feedback/shared'
3
-
4
- interface FormData { type: FeedbackType; title: string; description: string; url?: string }
5
-
6
- export function useFeedbackForm(submitFn: (data: FormData & { browser?: string; os?: string }) => Promise<string>) {
7
- const [isSubmitting, setIsSubmitting] = useState(false)
8
- const [error, setError] = useState<string | null>(null)
9
- const [success, setSuccess] = useState(false)
10
- const [formData, setFormData] = useState<FormData>({ type: 'general', title: '', description: '' })
11
-
12
- const validate = useCallback((): string | null => {
13
- if (!formData.title.trim()) return 'Title is required'
14
- if (formData.title.trim().length < 3) return 'Title must be at least 3 characters'
15
- if (!formData.description.trim()) return 'Description is required'
16
- if (formData.description.trim().length < 10) return 'Description must be at least 10 characters'
17
- return null
18
- }, [formData])
19
-
20
- const submit = useCallback(async () => {
21
- const err = validate()
22
- if (err) { setError(err); return }
23
- setIsSubmitting(true); setError(null)
24
- try {
25
- const browser = typeof navigator !== 'undefined' ? navigator.userAgent : undefined
26
- await submitFn({ ...formData, browser, url: formData.url || (typeof window !== 'undefined' ? window.location.href : undefined) })
27
- setSuccess(true); setFormData({ type: 'general', title: '', description: '' })
28
- } catch (e) { setError(e instanceof Error ? e.message : 'Submission failed') }
29
- finally { setIsSubmitting(false) }
30
- }, [formData, submitFn, validate])
31
-
32
- const reset = useCallback(() => { setFormData({ type: 'general', title: '', description: '' }); setError(null); setSuccess(false) }, [])
33
-
34
- return { formData, setFormData, submit, isSubmitting, error, success, reset, validate }
35
- }
@@ -1,26 +0,0 @@
1
- import { useState, useCallback, useEffect } from 'react'
2
-
3
- const NPS_STORAGE_KEY = 'geenius-nps-last-shown'
4
-
5
- export function useNPS(submitFn: (score: number, comment?: string) => Promise<void>, intervalDays = 90) {
6
- const [isSubmitting, setIsSubmitting] = useState(false)
7
- const [showSurvey, setShowSurvey] = useState(false)
8
-
9
- useEffect(() => {
10
- if (typeof window === 'undefined') return
11
- const last = localStorage.getItem(NPS_STORAGE_KEY)
12
- if (!last) { setShowSurvey(true); return }
13
- const daysSince = (Date.now() - Number(last)) / (1000 * 60 * 60 * 24)
14
- if (daysSince >= intervalDays) setShowSurvey(true)
15
- }, [intervalDays])
16
-
17
- const submit = useCallback(async (score: number, comment?: string) => {
18
- setIsSubmitting(true)
19
- try { await submitFn(score, comment); localStorage.setItem(NPS_STORAGE_KEY, String(Date.now())); setShowSurvey(false) }
20
- finally { setIsSubmitting(false) }
21
- }, [submitFn])
22
-
23
- const dismiss = useCallback(() => { setShowSurvey(false); localStorage.setItem(NPS_STORAGE_KEY, String(Date.now())) }, [])
24
-
25
- return { submit, isSubmitting, showSurvey, dismiss }
26
- }
@@ -1,13 +0,0 @@
1
- export { useFeedback } from './hooks/useFeedback'
2
- export type { FeedbackFilter } from './hooks/useFeedback'
3
- export { useFeedbackForm } from './hooks/useFeedbackForm'
4
- export { useNPS } from './hooks/useNPS'
5
- export { useFeedbackAdmin } from './hooks/useFeedbackAdmin'
6
- export { StatusBadge, PriorityBadge, TypeBadge, FeedbackCard, FeedbackList } from './components/FeedbackCard'
7
- export { FeedbackTypeSelector, FeedbackForm } from './components/FeedbackForm'
8
- export { FeedbackWidget } from './components/FeedbackWidget'
9
- export { NPSSurvey, NPSResults } from './components/NPSSurvey'
10
- export { FeedbackPublicPage } from './pages/FeedbackPublicPage'
11
- export { FeedbackAdminPage } from './pages/FeedbackAdminPage'
12
- export { FeedbackWidgetPage } from './pages/FeedbackWidgetPage'
13
- export type { FeedbackItem, NPSResponse, FeedbackConfig, FeedbackStats, NPSStats, FeedbackType, FeedbackStatus, FeedbackPriority, NPSCategory } from '@geenius-feedback/shared'
@@ -1,71 +0,0 @@
1
- import React from 'react'
2
- import type { FeedbackItem, FeedbackStatus, FeedbackPriority, NPSResponse } from '@geenius-feedback/shared'
3
- import { STATUS_CONFIG, PRIORITY_CONFIG, FEEDBACK_STATUSES, FEEDBACK_PRIORITIES, formatRelativeTime, TYPE_CONFIG } from '@geenius-feedback/shared'
4
- import { useFeedbackAdmin } from '../hooks/useFeedbackAdmin'
5
- import { StatusBadge, PriorityBadge, TypeBadge } from '../components/FeedbackCard'
6
- import { NPSResults } from '../components/NPSSurvey'
7
-
8
- interface FeedbackAdminPageProps {
9
- items: FeedbackItem[] | undefined; npsResponses?: NPSResponse[]
10
- mutations: Parameters<typeof useFeedbackAdmin>[1]
11
- }
12
-
13
- export function FeedbackAdminPage({ items, npsResponses, mutations }: FeedbackAdminPageProps) {
14
- const admin = useFeedbackAdmin(items, mutations)
15
-
16
- if (admin.isLoading) return (
17
- <div className="min-h-screen bg-[#090a0f] px-6 py-12"><div className="mx-auto max-w-6xl">
18
- <div className="mb-8 h-10 w-48 animate-pulse rounded-lg bg-white/5" />
19
- <div className="mb-6 grid grid-cols-4 gap-4">{Array.from({ length: 4 }).map((_, i) => <div key={i} className="h-20 animate-pulse rounded-xl bg-white/5" />)}</div>
20
- <div className="h-96 animate-pulse rounded-xl bg-white/5" />
21
- </div></div>
22
- )
23
-
24
- return (
25
- <div className="min-h-screen bg-[#090a0f] text-white"><div className="mx-auto max-w-6xl px-6 py-12">
26
- <h1 className="text-2xl font-bold tracking-tight mb-8">Feedback Admin</h1>
27
- <div className="mb-8 grid grid-cols-2 gap-4 md:grid-cols-5">
28
- <StatCard label="Total" value={admin.stats.total} icon="📊" />
29
- <StatCard label="Open" value={admin.stats.byStatus.open} icon="📬" color="text-blue-400" />
30
- <StatCard label="Planned" value={admin.stats.byStatus.planned} icon="📋" color="text-purple-300" />
31
- <StatCard label="In Progress" value={admin.stats.byStatus['in-progress']} icon="🚧" color="text-indigo-300" />
32
- <StatCard label="Done" value={admin.stats.byStatus.done} icon="✅" color="text-emerald-400" />
33
- </div>
34
-
35
- <div className="grid gap-6 lg:grid-cols-3">
36
- <div className="lg:col-span-2">
37
- <div className="mb-4 flex items-center gap-3">
38
- <input type="text" placeholder="Search…" value={admin.search} onChange={e => admin.setSearch(e.target.value)} className="w-64 rounded-xl border border-white/10 bg-white/5 px-4 py-2.5 text-sm text-white placeholder-white/30 outline-none focus:border-indigo-500/40" />
39
- <span className="text-xs text-white/30">{admin.items.length} items</span>
40
- </div>
41
- <div className="overflow-x-auto rounded-xl border border-white/8">
42
- <table className="w-full text-sm"><thead><tr className="border-b border-white/8 bg-white/[0.02]"><th className="px-4 py-3 text-left text-xs font-medium text-white/40 uppercase">Type</th><th className="px-4 py-3 text-left text-xs font-medium text-white/40 uppercase">Title</th><th className="px-4 py-3 text-left text-xs font-medium text-white/40 uppercase">Status</th><th className="px-4 py-3 text-left text-xs font-medium text-white/40 uppercase">Priority</th><th className="px-4 py-3 text-left text-xs font-medium text-white/40 uppercase">Votes</th><th className="px-4 py-3 text-left text-xs font-medium text-white/40 uppercase">Date</th><th className="px-4 py-3 text-right text-xs font-medium text-white/40 uppercase">Actions</th></tr></thead>
43
- <tbody className="divide-y divide-white/5">{admin.items.map(item => (
44
- <tr key={item.id} className="group hover:bg-white/[0.02]">
45
- <td className="px-4 py-3"><TypeBadge type={item.type} /></td>
46
- <td className="px-4 py-3 max-w-xs truncate text-white/80 font-medium">{item.title}</td>
47
- <td className="px-4 py-3"><select value={item.status} onChange={e => admin.updateStatus(item.id, e.target.value as FeedbackStatus)} className="rounded border border-white/10 bg-white/5 px-1.5 py-0.5 text-xs text-white outline-none">{FEEDBACK_STATUSES.map(s => <option key={s} value={s}>{STATUS_CONFIG[s].label}</option>)}</select></td>
48
- <td className="px-4 py-3"><select value={item.priority} onChange={e => admin.updatePriority(item.id, e.target.value as FeedbackPriority)} className="rounded border border-white/10 bg-white/5 px-1.5 py-0.5 text-xs text-white outline-none">{FEEDBACK_PRIORITIES.map(p => <option key={p} value={p}>{PRIORITY_CONFIG[p].label}</option>)}</select></td>
49
- <td className="px-4 py-3 tabular-nums text-white/50">{item.votes}</td>
50
- <td className="px-4 py-3 text-xs text-white/30">{formatRelativeTime(item.createdAt)}</td>
51
- <td className="px-4 py-3 text-right"><button type="button" onClick={() => admin.deleteFeedback(item.id)} className="rounded px-2 py-1 text-xs text-white/20 hover:text-red-400 hover:bg-red-500/10 opacity-0 group-hover:opacity-100">Delete</button></td>
52
- </tr>
53
- ))}</tbody></table>
54
- </div>
55
- </div>
56
-
57
- <div className="space-y-6">
58
- {npsResponses && <NPSResults responses={npsResponses} />}
59
- <div className="rounded-xl border border-white/8 bg-white/[0.02] p-5">
60
- <h4 className="text-sm font-semibold text-white/80 mb-3">By Type</h4>
61
- {Object.entries(admin.stats.byType).map(([t, c]) => <div key={t} className="flex justify-between mb-1.5"><span className="text-xs text-white/50 capitalize">{TYPE_CONFIG[t as keyof typeof TYPE_CONFIG]?.icon} {t}</span><span className="text-xs font-bold text-white/70">{c}</span></div>)}
62
- </div>
63
- </div>
64
- </div>
65
- </div></div>
66
- )
67
- }
68
-
69
- function StatCard({ label, value, icon, color }: { label: string; value: number; icon: string; color?: string }) {
70
- return <div className="rounded-xl border border-white/8 bg-white/[0.02] p-4"><div className="mb-2 flex items-center justify-between"><span className="text-lg">{icon}</span><span className={`text-2xl font-bold tabular-nums ${color ?? 'text-white/90'}`}>{value}</span></div><p className="text-xs text-white/40">{label}</p></div>
71
- }
@@ -1,42 +0,0 @@
1
- import React, { useState } from 'react'
2
- import type { FeedbackItem, FeedbackType } from '@geenius-feedback/shared'
3
- import { FEEDBACK_TYPES, TYPE_CONFIG, STATUS_CONFIG } from '@geenius-feedback/shared'
4
- import { useFeedback, type FeedbackFilter } from '../hooks/useFeedback'
5
- import { FeedbackList, FeedbackCard, StatusBadge } from '../components/FeedbackCard'
6
-
7
- export function FeedbackPublicPage({ items, onVote, votedIds }: { items: FeedbackItem[] | undefined; onVote?: (id: string) => void; votedIds?: string[] }) {
8
- const feedback = useFeedback(items)
9
- const [activeType, setActiveType] = useState<FeedbackType | 'all'>('all')
10
-
11
- if (feedback.isLoading) return (
12
- <div className="min-h-screen bg-[#090a0f] px-6 py-12"><div className="mx-auto max-w-3xl">
13
- <div className="mb-8 h-10 w-48 animate-pulse rounded-lg bg-white/5" />
14
- <div className="mb-6 flex gap-2">{Array.from({ length: 5 }).map((_, i) => <div key={i} className="h-9 w-24 animate-pulse rounded-lg bg-white/5" />)}</div>
15
- <div className="space-y-3">{Array.from({ length: 4 }).map((_, i) => <div key={i} className="h-24 animate-pulse rounded-xl bg-white/5" />)}</div>
16
- </div></div>
17
- )
18
-
19
- return (
20
- <div className="min-h-screen bg-[#090a0f] text-white">
21
- <div className="mx-auto max-w-3xl px-6 py-12">
22
- <div className="mb-8"><h1 className="text-2xl font-bold tracking-tight mb-1">Feedback Board</h1><p className="text-sm text-white/40">{feedback.stats.total} submissions · {feedback.stats.byStatus.open} open</p></div>
23
- <div className="mb-6 flex flex-wrap items-center gap-3">
24
- <input type="text" placeholder="Search feedback…" value={feedback.filter.search ?? ''} onChange={e => feedback.setFilter({ ...feedback.filter, search: e.target.value || undefined })}
25
- className="w-56 rounded-xl border border-white/10 bg-white/5 px-4 py-2.5 text-sm text-white placeholder-white/30 outline-none focus:border-indigo-500/40" />
26
- <div className="flex gap-1.5">
27
- <button type="button" onClick={() => { setActiveType('all'); feedback.setFilter({ ...feedback.filter, type: undefined }) }}
28
- className={`rounded-lg px-3 py-1.5 text-xs font-medium ${activeType === 'all' ? 'bg-indigo-600 text-white' : 'bg-white/5 text-white/50 hover:bg-white/10'}`}>All ({feedback.stats.total})</button>
29
- {FEEDBACK_TYPES.map(t => <button key={t} type="button" onClick={() => { setActiveType(t); feedback.setFilter({ ...feedback.filter, type: t }) }}
30
- className={`rounded-lg px-3 py-1.5 text-xs font-medium ${activeType === t ? 'text-white' : 'bg-white/5 text-white/50 hover:bg-white/10'}`}
31
- style={activeType === t ? { background: TYPE_CONFIG[t].color } : undefined}>{TYPE_CONFIG[t].icon} {TYPE_CONFIG[t].label} ({feedback.stats.byType[t]})</button>)}
32
- </div>
33
- <div className="ml-auto flex gap-1.5">
34
- <button type="button" onClick={() => feedback.setFilter({ ...feedback.filter, sort: 'newest' })} className={`rounded-lg px-3 py-1.5 text-xs ${feedback.filter.sort === 'newest' ? 'bg-white/10 text-white/80' : 'text-white/40'}`}>Newest</button>
35
- <button type="button" onClick={() => feedback.setFilter({ ...feedback.filter, sort: 'votes' })} className={`rounded-lg px-3 py-1.5 text-xs ${feedback.filter.sort === 'votes' ? 'bg-white/10 text-white/80' : 'text-white/40'}`}>Top voted</button>
36
- </div>
37
- </div>
38
- <FeedbackList items={feedback.items} onVote={onVote} votedIds={votedIds} />
39
- </div>
40
- </div>
41
- )
42
- }
@@ -1,25 +0,0 @@
1
- import React from 'react'
2
- import type { FeedbackType } from '@geenius-feedback/shared'
3
- import { FeedbackWidget } from '../components/FeedbackWidget'
4
-
5
- export function FeedbackWidgetPage({ onSubmit }: { onSubmit: (data: { type: FeedbackType; title: string; description: string }) => Promise<string> }) {
6
- return (
7
- <div className="min-h-screen bg-[#090a0f] text-white flex items-center justify-center">
8
- <div className="text-center max-w-md">
9
- <h1 className="text-2xl font-bold mb-2">Feedback Widget</h1>
10
- <p className="text-sm text-white/40 mb-8">Click the floating "Feedback" tab on the right edge to open the feedback form.</p>
11
- <div className="rounded-xl border border-white/8 bg-white/[0.02] p-6 text-left">
12
- <h3 className="text-sm font-semibold text-white/80 mb-3">Features</h3>
13
- <ul className="space-y-2 text-xs text-white/50">
14
- <li className="flex items-center gap-2"><span className="text-emerald-400">✓</span> Floating edge tab trigger</li>
15
- <li className="flex items-center gap-2"><span className="text-emerald-400">✓</span> Type selector (Bug/Feature/Suggestion/General)</li>
16
- <li className="flex items-center gap-2"><span className="text-emerald-400">✓</span> Title + description form with validation</li>
17
- <li className="flex items-center gap-2"><span className="text-emerald-400">✓</span> Auto-captures page URL + browser</li>
18
- <li className="flex items-center gap-2"><span className="text-emerald-400">✓</span> Success confirmation</li>
19
- </ul>
20
- </div>
21
- </div>
22
- <FeedbackWidget onSubmit={onSubmit} />
23
- </div>
24
- )
25
- }
@@ -1,3 +0,0 @@
1
- export { FeedbackPublicPage } from './FeedbackPublicPage'
2
- export { FeedbackAdminPage } from './FeedbackAdminPage'
3
- export { FeedbackWidgetPage } from './FeedbackWidgetPage'
@@ -1,19 +0,0 @@
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
- }
@@ -1,12 +0,0 @@
1
- import { defineConfig } from 'tsup'
2
-
3
- export default defineConfig({
4
- entry: { index: 'src/index.tsx' },
5
- outDir: 'dist',
6
- format: ['esm'],
7
- dts: false,
8
- sourcemap: true,
9
- clean: true,
10
- treeshake: true,
11
- external: ['react', 'react-dom'],
12
- })