@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,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geenius-feedback/shared",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Geenius Feedback \u2014 Shared types & Convex schema",
|
|
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
|
+
"./convex": "./src/convex.ts"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup",
|
|
28
|
+
"clean": "rm -rf dist",
|
|
29
|
+
"type-check": "tsc --noEmit",
|
|
30
|
+
"prepublishOnly": "pnpm clean && pnpm build",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest",
|
|
33
|
+
"test:coverage": "vitest run --coverage"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"convex": "^1.34.0",
|
|
37
|
+
"tsup": "^8.5.1",
|
|
38
|
+
"typescript": "~6.0.2",
|
|
39
|
+
"vitest": "^4.0.0"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
STATUS_CONFIG, PRIORITY_CONFIG, TYPE_CONFIG,
|
|
4
|
+
FEEDBACK_TYPES, FEEDBACK_STATUSES, FEEDBACK_PRIORITIES,
|
|
5
|
+
getStatusTransitions, getNPSCategory, calcNPSScore,
|
|
6
|
+
} from '../index'
|
|
7
|
+
|
|
8
|
+
describe('Feedback Constants', () => {
|
|
9
|
+
it('FEEDBACK_TYPES has 4 types', () => {
|
|
10
|
+
expect(FEEDBACK_TYPES).toHaveLength(4)
|
|
11
|
+
expect(FEEDBACK_TYPES).toContain('bug')
|
|
12
|
+
expect(FEEDBACK_TYPES).toContain('feature')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('FEEDBACK_STATUSES has 6 statuses', () => {
|
|
16
|
+
expect(FEEDBACK_STATUSES).toHaveLength(6)
|
|
17
|
+
expect(FEEDBACK_STATUSES).toContain('open')
|
|
18
|
+
expect(FEEDBACK_STATUSES).toContain('done')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('STATUS_CONFIG maps every status to label+color', () => {
|
|
22
|
+
FEEDBACK_STATUSES.forEach(s => {
|
|
23
|
+
expect(STATUS_CONFIG[s]).toBeDefined()
|
|
24
|
+
expect(STATUS_CONFIG[s].label).toBeDefined()
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('PRIORITY_CONFIG has low through critical', () => {
|
|
29
|
+
FEEDBACK_PRIORITIES.forEach(p => {
|
|
30
|
+
expect(PRIORITY_CONFIG[p]).toBeDefined()
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('Status Transitions', () => {
|
|
36
|
+
it('open can transition to under-review', () => {
|
|
37
|
+
const transitions = getStatusTransitions('open')
|
|
38
|
+
expect(transitions).toContain('under-review')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('done has limited transitions', () => {
|
|
42
|
+
const transitions = getStatusTransitions('done')
|
|
43
|
+
expect(Array.isArray(transitions)).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('NPS Scoring', () => {
|
|
48
|
+
it('getNPSCategory detractor for 0-6', () => {
|
|
49
|
+
expect(getNPSCategory(0)).toBe('detractor')
|
|
50
|
+
expect(getNPSCategory(6)).toBe('detractor')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('getNPSCategory passive for 7-8', () => {
|
|
54
|
+
expect(getNPSCategory(7)).toBe('passive')
|
|
55
|
+
expect(getNPSCategory(8)).toBe('passive')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('getNPSCategory promoter for 9-10', () => {
|
|
59
|
+
expect(getNPSCategory(9)).toBe('promoter')
|
|
60
|
+
expect(getNPSCategory(10)).toBe('promoter')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('calcNPSScore computes score from responses', () => {
|
|
64
|
+
const responses = [
|
|
65
|
+
{ score: 10 }, { score: 9 }, { score: 5 }, { score: 3 },
|
|
66
|
+
] as any[]
|
|
67
|
+
const nps = calcNPSScore(responses)
|
|
68
|
+
expect(typeof nps).toBe('number')
|
|
69
|
+
expect(nps).toBeGreaterThanOrEqual(-100)
|
|
70
|
+
expect(nps).toBeLessThanOrEqual(100)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Configuration factory for Geenius Feedback
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FeedbackConfig } from './types'
|
|
6
|
+
|
|
7
|
+
export interface ConfigureFeedbackOptions {
|
|
8
|
+
floatingWidget?: boolean
|
|
9
|
+
npsEnabled?: boolean
|
|
10
|
+
npsInterval?: number
|
|
11
|
+
categories?: Array<'bug' | 'feature' | 'general' | 'suggestion'>
|
|
12
|
+
allowAnonymous?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configure the feedback system with custom options
|
|
17
|
+
* @param options Configuration options for the feedback system
|
|
18
|
+
* @returns FeedbackConfig object ready for use
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const config = configureFeedback({
|
|
22
|
+
* floatingWidget: true,
|
|
23
|
+
* npsEnabled: true,
|
|
24
|
+
* npsInterval: 7,
|
|
25
|
+
* categories: ['bug', 'feature', 'general'],
|
|
26
|
+
* allowAnonymous: true,
|
|
27
|
+
* })
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function configureFeedback(options: ConfigureFeedbackOptions = {}): FeedbackConfig {
|
|
31
|
+
return {
|
|
32
|
+
floatingWidget: options.floatingWidget ?? true,
|
|
33
|
+
npsEnabled: options.npsEnabled ?? true,
|
|
34
|
+
npsInterval: options.npsInterval ?? 7,
|
|
35
|
+
categories: options.categories ?? ['bug', 'feature', 'general', 'suggestion'],
|
|
36
|
+
allowAnonymous: options.allowAnonymous ?? true,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Default feedback configuration
|
|
42
|
+
*/
|
|
43
|
+
export const defaultFeedbackConfig: FeedbackConfig = {
|
|
44
|
+
floatingWidget: true,
|
|
45
|
+
npsEnabled: true,
|
|
46
|
+
npsInterval: 7,
|
|
47
|
+
categories: ['bug', 'feature', 'general', 'suggestion'],
|
|
48
|
+
allowAnonymous: true,
|
|
49
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
FeedbackType, FeedbackStatus, FeedbackPriority, NPSCategory,
|
|
3
|
+
Attachment, FeedbackItem, NPSResponse, FeedbackConfig,
|
|
4
|
+
FeedbackStats, NPSStats,
|
|
5
|
+
} from './types'
|
|
6
|
+
|
|
7
|
+
export { configureFeedback, defaultFeedbackConfig } from './config'
|
|
8
|
+
export type { ConfigureFeedbackOptions } from './config'
|
|
9
|
+
|
|
10
|
+
import type { FeedbackStatus, FeedbackPriority, FeedbackType, NPSResponse, FeedbackStats, NPSStats, FeedbackItem } from './types'
|
|
11
|
+
|
|
12
|
+
// ─── Status Config ───────────────────────────────────
|
|
13
|
+
export const STATUS_CONFIG: Record<FeedbackStatus, { label: string; color: string; emoji: string }> = {
|
|
14
|
+
open: { label: 'Open', color: 'oklch(0.65 0.20 245)', emoji: '📬' },
|
|
15
|
+
'under-review': { label: 'Under Review', color: 'oklch(0.72 0.18 60)', emoji: '🔍' },
|
|
16
|
+
planned: { label: 'Planned', color: 'oklch(0.70 0.22 280)', emoji: '📋' },
|
|
17
|
+
'in-progress': { label: 'In Progress', color: 'oklch(0.65 0.20 265)', emoji: '🚧' },
|
|
18
|
+
done: { label: 'Done', color: 'oklch(0.72 0.18 155)', emoji: '✅' },
|
|
19
|
+
declined: { label: 'Declined', color: 'oklch(0.50 0.10 250)', emoji: '🚫' },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const PRIORITY_CONFIG: Record<FeedbackPriority, { label: string; color: string }> = {
|
|
23
|
+
low: { label: 'Low', color: 'oklch(0.58 0.10 250)' },
|
|
24
|
+
medium: { label: 'Medium', color: 'oklch(0.72 0.18 60)' },
|
|
25
|
+
high: { label: 'High', color: 'oklch(0.68 0.22 35)' },
|
|
26
|
+
critical: { label: 'Critical', color: 'oklch(0.60 0.25 25)' },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const TYPE_CONFIG: Record<FeedbackType, { label: string; icon: string; color: string }> = {
|
|
30
|
+
bug: { label: 'Bug', icon: '🐛', color: 'oklch(0.60 0.25 25)' },
|
|
31
|
+
feature: { label: 'Feature', icon: '✨', color: 'oklch(0.70 0.22 280)' },
|
|
32
|
+
suggestion: { label: 'Suggestion', icon: '💡', color: 'oklch(0.72 0.18 60)' },
|
|
33
|
+
general: { label: 'General', icon: '💬', color: 'oklch(0.65 0.20 245)' },
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const FEEDBACK_TYPES: FeedbackType[] = ['bug', 'feature', 'suggestion', 'general']
|
|
37
|
+
export const FEEDBACK_STATUSES: FeedbackStatus[] = ['open', 'under-review', 'planned', 'in-progress', 'done', 'declined']
|
|
38
|
+
export const FEEDBACK_PRIORITIES: FeedbackPriority[] = ['low', 'medium', 'high', 'critical']
|
|
39
|
+
|
|
40
|
+
// ─── Status Transitions ─────────────────────────────
|
|
41
|
+
const STATUS_TRANSITIONS: Record<FeedbackStatus, FeedbackStatus[]> = {
|
|
42
|
+
open: ['under-review', 'planned', 'declined'],
|
|
43
|
+
'under-review': ['planned', 'in-progress', 'declined'],
|
|
44
|
+
planned: ['in-progress', 'declined'],
|
|
45
|
+
'in-progress': ['done', 'planned'],
|
|
46
|
+
done: ['open'],
|
|
47
|
+
declined: ['open'],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getStatusTransitions(status: FeedbackStatus): FeedbackStatus[] {
|
|
51
|
+
return STATUS_TRANSITIONS[status] ?? []
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── NPS Utilities ───────────────────────────────────
|
|
55
|
+
export function getNPSCategory(score: number): 'detractor' | 'passive' | 'promoter' {
|
|
56
|
+
if (score <= 6) return 'detractor'
|
|
57
|
+
if (score <= 8) return 'passive'
|
|
58
|
+
return 'promoter'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function calcNPSScore(responses: NPSResponse[]): number {
|
|
62
|
+
if (responses.length === 0) return 0
|
|
63
|
+
let promoters = 0, detractors = 0
|
|
64
|
+
for (const r of responses) {
|
|
65
|
+
const cat = getNPSCategory(r.score)
|
|
66
|
+
if (cat === 'promoter') promoters++
|
|
67
|
+
else if (cat === 'detractor') detractors++
|
|
68
|
+
}
|
|
69
|
+
return Math.round(((promoters - detractors) / responses.length) * 100)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function calcNPSStats(responses: NPSResponse[]): NPSStats {
|
|
73
|
+
let promoters = 0, passives = 0, detractors = 0, totalScore = 0
|
|
74
|
+
for (const r of responses) {
|
|
75
|
+
totalScore += r.score
|
|
76
|
+
const cat = getNPSCategory(r.score)
|
|
77
|
+
if (cat === 'promoter') promoters++
|
|
78
|
+
else if (cat === 'passive') passives++
|
|
79
|
+
else detractors++
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
averageScore: responses.length > 0 ? Math.round((totalScore / responses.length) * 10) / 10 : 0,
|
|
83
|
+
totalResponses: responses.length,
|
|
84
|
+
promoters, passives, detractors,
|
|
85
|
+
npsScore: calcNPSScore(responses),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function calcFeedbackStats(items: FeedbackItem[]): FeedbackStats {
|
|
90
|
+
const byType = { bug: 0, feature: 0, suggestion: 0, general: 0 }
|
|
91
|
+
const byStatus = { open: 0, 'under-review': 0, planned: 0, 'in-progress': 0, done: 0, declined: 0 }
|
|
92
|
+
const byPriority = { low: 0, medium: 0, high: 0, critical: 0 }
|
|
93
|
+
for (const item of items) {
|
|
94
|
+
byType[item.type]++
|
|
95
|
+
byStatus[item.status]++
|
|
96
|
+
byPriority[item.priority]++
|
|
97
|
+
}
|
|
98
|
+
return { total: items.length, byType, byStatus, byPriority }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function formatRelativeTime(dateStr: string): string {
|
|
102
|
+
const diff = Date.now() - new Date(dateStr).getTime()
|
|
103
|
+
const mins = Math.floor(diff / 60000)
|
|
104
|
+
if (mins < 1) return 'just now'
|
|
105
|
+
if (mins < 60) return `${mins}m ago`
|
|
106
|
+
const hrs = Math.floor(mins / 60)
|
|
107
|
+
if (hrs < 24) return `${hrs}h ago`
|
|
108
|
+
const days = Math.floor(hrs / 24)
|
|
109
|
+
if (days < 30) return `${days}d ago`
|
|
110
|
+
return new Date(dateStr).toLocaleDateString()
|
|
111
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type FeedbackType = 'bug' | 'feature' | 'general' | 'suggestion'
|
|
2
|
+
export type FeedbackStatus = 'open' | 'under-review' | 'planned' | 'in-progress' | 'done' | 'declined'
|
|
3
|
+
export type FeedbackPriority = 'low' | 'medium' | 'high' | 'critical'
|
|
4
|
+
export type NPSCategory = 'detractor' | 'passive' | 'promoter'
|
|
5
|
+
|
|
6
|
+
export interface Attachment { url: string; name: string; size: number }
|
|
7
|
+
|
|
8
|
+
export interface FeedbackItem {
|
|
9
|
+
id: string
|
|
10
|
+
type: FeedbackType
|
|
11
|
+
title: string
|
|
12
|
+
description: string
|
|
13
|
+
status: FeedbackStatus
|
|
14
|
+
priority: FeedbackPriority
|
|
15
|
+
userId?: string
|
|
16
|
+
userEmail?: string
|
|
17
|
+
userName?: string
|
|
18
|
+
url?: string
|
|
19
|
+
browser?: string
|
|
20
|
+
os?: string
|
|
21
|
+
tags: string[]
|
|
22
|
+
votes: number
|
|
23
|
+
createdAt: string
|
|
24
|
+
updatedAt: string
|
|
25
|
+
attachments?: Attachment[]
|
|
26
|
+
adminNote?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface NPSResponse {
|
|
30
|
+
id: string
|
|
31
|
+
score: number
|
|
32
|
+
comment?: string
|
|
33
|
+
userId?: string
|
|
34
|
+
submittedAt: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface FeedbackConfig {
|
|
38
|
+
floatingWidget: boolean
|
|
39
|
+
npsEnabled: boolean
|
|
40
|
+
npsInterval: number
|
|
41
|
+
categories: FeedbackType[]
|
|
42
|
+
allowAnonymous: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface FeedbackStats {
|
|
46
|
+
total: number
|
|
47
|
+
byType: Record<FeedbackType, number>
|
|
48
|
+
byStatus: Record<FeedbackStatus, number>
|
|
49
|
+
byPriority: Record<FeedbackPriority, number>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface NPSStats {
|
|
53
|
+
averageScore: number
|
|
54
|
+
totalResponses: number
|
|
55
|
+
promoters: number
|
|
56
|
+
passives: number
|
|
57
|
+
detractors: number
|
|
58
|
+
npsScore: number
|
|
59
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# ✦ @geenius-feedback/solidjs\n\n> Geenius Feedback — SolidJS components & primitives\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/solidjs\n```\n\n## Usage\n\n```typescript\nimport { init } from '@geenius-feedback/solidjs';\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
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geenius-feedback/solidjs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Geenius Feedback — SolidJS components & primitives",
|
|
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
|
+
"solid-js": "^1.9.0",
|
|
36
|
+
"tsup": "^8.5.1",
|
|
37
|
+
"typescript": "~6.0.2"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"solid-js": "^1.8.0 || ^1.9.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Show, For, createSignal } from 'solid-js'
|
|
2
|
+
import type { FeedbackItem, FeedbackType, FeedbackStatus, FeedbackPriority } from '@geenius-feedback/shared'
|
|
3
|
+
import { STATUS_CONFIG, PRIORITY_CONFIG, TYPE_CONFIG, FEEDBACK_TYPES, formatRelativeTime } from '@geenius-feedback/shared'
|
|
4
|
+
|
|
5
|
+
export function StatusBadge(props: { status: FeedbackStatus }) {
|
|
6
|
+
const cfg = () => STATUS_CONFIG[props.status]
|
|
7
|
+
return <span class="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(props: { priority: FeedbackPriority }) {
|
|
11
|
+
const cfg = () => PRIORITY_CONFIG[props.priority]
|
|
12
|
+
return <span class="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(props: { type: FeedbackType }) {
|
|
16
|
+
const cfg = () => TYPE_CONFIG[props.type]
|
|
17
|
+
return <span class="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(props: { item: FeedbackItem; onVote?: (id: string) => void; hasVoted?: boolean }) {
|
|
21
|
+
return (
|
|
22
|
+
<div class="group flex gap-3 rounded-xl border border-white/8 bg-white/[0.02] p-4 hover:border-indigo-500/20">
|
|
23
|
+
<Show when={props.onVote}><button type="button" onClick={() => props.onVote?.(props.item.id)}
|
|
24
|
+
class={`flex flex-col items-center gap-0.5 rounded-lg px-2 py-1.5 text-xs font-bold ${props.hasVoted ? 'bg-indigo-500/15 text-indigo-400' : 'bg-white/5 text-white/30 hover:bg-white/10'}`}>
|
|
25
|
+
<span class="text-sm">{props.hasVoted ? '▲' : '△'}</span><span>{props.item.votes}</span>
|
|
26
|
+
</button></Show>
|
|
27
|
+
<div class="flex-1 min-w-0">
|
|
28
|
+
<div class="flex items-center gap-2 mb-1 flex-wrap"><TypeBadge type={props.item.type} /><StatusBadge status={props.item.status} /></div>
|
|
29
|
+
<h3 class="text-sm font-semibold text-white/90 mb-0.5 truncate">{props.item.title}</h3>
|
|
30
|
+
<p class="text-xs text-white/40 line-clamp-2">{props.item.description}</p>
|
|
31
|
+
<div class="mt-2 text-[10px] text-white/25">{formatRelativeTime(props.item.createdAt)}</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function FeedbackWidget(props: { onSubmit: (data: { type: FeedbackType; title: string; description: string }) => Promise<string> }) {
|
|
38
|
+
const [isOpen, setIsOpen] = createSignal(false)
|
|
39
|
+
const [type, setType] = createSignal<FeedbackType>('general')
|
|
40
|
+
const [title, setTitle] = createSignal(''); const [desc, setDesc] = createSignal('')
|
|
41
|
+
const [submitting, setSubmitting] = createSignal(false); const [done, setDone] = createSignal(false)
|
|
42
|
+
const submit = async () => { setSubmitting(true); try { await props.onSubmit({ type: type(), title: title(), description: desc() }); setDone(true) } finally { setSubmitting(false) } }
|
|
43
|
+
return (<>
|
|
44
|
+
<button type="button" onClick={() => setIsOpen(true)} class="fixed top-1/2 right-0 -translate-y-1/2 z-40 rounded-l-xl bg-indigo-600 px-2 py-4 text-white shadow-lg hover:bg-indigo-500" style={{ "writing-mode": "vertical-rl" }}><span class="text-xs font-medium tracking-wider">Feedback</span></button>
|
|
45
|
+
<Show when={isOpen()}>
|
|
46
|
+
<div class="fixed inset-0 z-50 flex justify-end" onClick={() => setIsOpen(false)}>
|
|
47
|
+
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" />
|
|
48
|
+
<div class="relative w-full max-w-md bg-[#0f0f17] border-l border-white/10 p-6" onClick={e => e.stopPropagation()}>
|
|
49
|
+
<div class="mb-6 flex items-center justify-between"><h2 class="text-lg font-bold text-white">Send Feedback</h2><button type="button" onClick={() => setIsOpen(false)} class="text-white/30 hover:text-white/50">✕</button></div>
|
|
50
|
+
<Show when={!done()} fallback={<div class="text-center py-8"><div class="text-4xl mb-3">🎉</div><p class="text-sm text-white/80">Thank you!</p></div>}>
|
|
51
|
+
<div class="flex gap-1.5 mb-4"><For each={FEEDBACK_TYPES}>{t => <button type="button" onClick={() => setType(t)} class={`flex items-center gap-1 rounded-lg px-3 py-2 text-xs font-medium ${type() === t ? 'text-white' : 'bg-white/5 text-white/50'}`} style={type() === t ? { background: TYPE_CONFIG[t].color } : undefined}>{TYPE_CONFIG[t].icon} {TYPE_CONFIG[t].label}</button>}</For></div>
|
|
52
|
+
<input type="text" placeholder="Title…" value={title()} onInput={e => setTitle((e.target as HTMLInputElement).value)} class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white placeholder-white/30 outline-none mb-3" />
|
|
53
|
+
<textarea placeholder="Description…" value={desc()} onInput={e => setDesc((e.target as HTMLTextAreaElement).value)} rows={4} class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white placeholder-white/30 outline-none resize-none mb-4" />
|
|
54
|
+
<button type="button" onClick={submit} disabled={submitting()} class="w-full rounded-lg bg-indigo-600 py-2.5 text-sm font-medium text-white hover:bg-indigo-500 disabled:opacity-50">{submitting() ? 'Submitting…' : 'Submit'}</button>
|
|
55
|
+
</Show>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</Show>
|
|
59
|
+
</>)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function NPSSurvey(props: { onSubmit: (score: number, comment?: string) => void; onDismiss: () => void }) {
|
|
63
|
+
const [score, setScore] = createSignal<number | null>(null)
|
|
64
|
+
const [comment, setComment] = createSignal(''); const [step, setStep] = createSignal<'score'|'comment'|'thanks'>('score')
|
|
65
|
+
return (
|
|
66
|
+
<div class="fixed bottom-6 right-6 z-50 w-96 rounded-2xl border border-white/10 bg-[#111118] p-6 shadow-2xl">
|
|
67
|
+
<Show when={step() === 'score'}><div><div class="mb-1 flex items-center justify-between"><h3 class="text-sm font-semibold text-white/90">How likely to recommend us?</h3><button type="button" onClick={props.onDismiss} class="text-white/20">✕</button></div><div class="flex gap-1 mt-3"><For each={Array.from({length:11},(_,i)=>i)}>{i => <button type="button" onClick={() => { setScore(i); setStep('comment') }} class="flex-1 rounded-lg py-2.5 text-xs font-bold bg-white/5 text-white/50 hover:bg-white/10">{i}</button>}</For></div></div></Show>
|
|
68
|
+
<Show when={step() === 'comment'}><div><p class="text-sm font-semibold text-white/90 mb-3">Score: {score()}</p><textarea value={comment()} onInput={e => setComment((e.target as HTMLTextAreaElement).value)} rows={3} class="w-full rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-sm text-white outline-none resize-none mb-3" /><button type="button" onClick={() => { props.onSubmit(score()!, comment() || undefined); setStep('thanks') }} class="w-full rounded-lg bg-indigo-600 py-2 text-xs text-white">Submit</button></div></Show>
|
|
69
|
+
<Show when={step() === 'thanks'}><div class="text-center py-4"><div class="text-3xl mb-2">🎉</div><p class="text-sm text-white/80">Thank you!</p></div></Show>
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createFeedback, createFeedbackForm, createNPS, createFeedbackAdmin } from './primitives'
|
|
2
|
+
export { StatusBadge, PriorityBadge, TypeBadge, FeedbackCard, FeedbackWidget, NPSSurvey } from './components'
|
|
3
|
+
export type { FeedbackItem, NPSResponse, FeedbackConfig, FeedbackStats, NPSStats, FeedbackType, FeedbackStatus, FeedbackPriority } from '@geenius-feedback/shared'
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createSignal, createMemo } from 'solid-js'
|
|
2
|
+
import type { FeedbackItem, FeedbackType, FeedbackStatus, FeedbackPriority, FeedbackStats, NPSResponse } from '@geenius-feedback/shared'
|
|
3
|
+
import { calcFeedbackStats, calcNPSStats } from '@geenius-feedback/shared'
|
|
4
|
+
|
|
5
|
+
export function createFeedback(items: () => FeedbackItem[] | undefined) {
|
|
6
|
+
const [typeFilter, setTypeFilter] = createSignal<FeedbackType | undefined>()
|
|
7
|
+
const [search, setSearch] = createSignal('')
|
|
8
|
+
const [sort, setSort] = createSignal<'newest' | 'votes'>('newest')
|
|
9
|
+
const isLoading = createMemo(() => items() === undefined)
|
|
10
|
+
const stats = createMemo(() => calcFeedbackStats(items() ?? []))
|
|
11
|
+
const filtered = createMemo(() => {
|
|
12
|
+
let r = items() ?? []; const tf = typeFilter(); if (tf) r = r.filter(i => i.type === tf)
|
|
13
|
+
const q = search().toLowerCase(); if (q) r = r.filter(i => i.title.toLowerCase().includes(q) || i.description.toLowerCase().includes(q))
|
|
14
|
+
if (sort() === 'votes') r = [...r].sort((a, b) => b.votes - a.votes)
|
|
15
|
+
return r
|
|
16
|
+
})
|
|
17
|
+
return { items: filtered, stats, isLoading, typeFilter, setTypeFilter, search, setSearch, sort, setSort }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createFeedbackForm(submitFn: (data: { type: FeedbackType; title: string; description: string }) => Promise<string>) {
|
|
21
|
+
const [type, setType] = createSignal<FeedbackType>('general')
|
|
22
|
+
const [title, setTitle] = createSignal(''); const [desc, setDesc] = createSignal('')
|
|
23
|
+
const [isSubmitting, setIsSubmitting] = createSignal(false); const [error, setError] = createSignal<string | null>(null); const [success, setSuccess] = createSignal(false)
|
|
24
|
+
const submit = async () => {
|
|
25
|
+
if (!title().trim()) { setError('Title required'); return }; if (desc().length < 10) { setError('Description must be 10+ chars'); return }
|
|
26
|
+
setIsSubmitting(true); setError(null)
|
|
27
|
+
try { await submitFn({ type: type(), title: title(), description: desc() }); setSuccess(true); setTitle(''); setDesc('') }
|
|
28
|
+
catch (e) { setError(e instanceof Error ? e.message : 'Failed') } finally { setIsSubmitting(false) }
|
|
29
|
+
}
|
|
30
|
+
const reset = () => { setType('general'); setTitle(''); setDesc(''); setError(null); setSuccess(false) }
|
|
31
|
+
return { type, setType, title, setTitle, description: desc, setDescription: setDesc, submit, isSubmitting, error, success, reset }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createNPS(submitFn: (score: number, comment?: string) => Promise<void>) {
|
|
35
|
+
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
|
36
|
+
const [showSurvey, setShowSurvey] = createSignal(typeof window !== 'undefined' && !localStorage.getItem('geenius-nps-last-shown'))
|
|
37
|
+
const submit = async (score: number, comment?: string) => {
|
|
38
|
+
setIsSubmitting(true); try { await submitFn(score, comment); localStorage.setItem('geenius-nps-last-shown', String(Date.now())); setShowSurvey(false) } finally { setIsSubmitting(false) }
|
|
39
|
+
}
|
|
40
|
+
const dismiss = () => { setShowSurvey(false); localStorage.setItem('geenius-nps-last-shown', String(Date.now())) }
|
|
41
|
+
return { submit, isSubmitting, showSurvey, dismiss }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createFeedbackAdmin(items: () => FeedbackItem[] | undefined, mutations: { updateStatus: (id: string, s: FeedbackStatus, note?: string) => Promise<void>; updatePriority: (id: string, p: FeedbackPriority) => Promise<void>; deleteFeedback: (id: string) => Promise<void> }) {
|
|
45
|
+
const [search, setSearch] = createSignal('')
|
|
46
|
+
const stats = createMemo(() => calcFeedbackStats(items() ?? []))
|
|
47
|
+
const filtered = createMemo(() => { const q = search().toLowerCase(); return q ? (items() ?? []).filter(i => i.title.toLowerCase().includes(q)) : (items() ?? []) })
|
|
48
|
+
return { items: filtered, stats, search, setSearch, isLoading: createMemo(() => items() === undefined), ...mutations }
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"jsx": "preserve",
|
|
7
|
+
"jsxImportSource": "solid-js",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"target": "ES2022",
|
|
14
|
+
"module": "ESNext",
|
|
15
|
+
"moduleResolution": "bundler"
|
|
16
|
+
},
|
|
17
|
+
"include": [
|
|
18
|
+
"src"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# ✦ @geenius-feedback/solidjs-css\n\n> Geenius Feedback — SolidJS primitives (vanilla CSS variant)\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/solidjs-css\n```\n\n## Usage\n\n```typescript\nimport { init } from '@geenius-feedback/solidjs-css';\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
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geenius-feedback/solidjs-css",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Geenius Feedback — SolidJS primitives (vanilla CSS variant)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "restricted"
|
|
9
|
+
},
|
|
10
|
+
"main": "./src/index.tsx",
|
|
11
|
+
"types": "./src/index.tsx",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/index.tsx"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"type-check": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"solid-js": ">=1.0.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@geenius-feedback/shared": "workspace:*"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"solid-js": "^1.9.4",
|
|
26
|
+
"typescript": "~6.0.2"
|
|
27
|
+
},
|
|
28
|
+
"author": "Antigravity HQ",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createFeedback, createFeedbackForm, createNPS, createFeedbackAdmin } from '@geenius-feedback/solidjs'
|
|
2
|
+
export { StatusBadge, PriorityBadge, TypeBadge, FeedbackCard, FeedbackWidget, NPSSurvey } from '@geenius-feedback/solidjs'
|
|
3
|
+
import './styles.css'
|
|
4
|
+
export type { FeedbackItem, NPSResponse, FeedbackConfig, FeedbackStats, NPSStats, FeedbackType, FeedbackStatus, FeedbackPriority } from '@geenius-feedback/shared'
|