@miketromba/issy-app 0.1.1 → 0.1.4

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.
@@ -1,201 +0,0 @@
1
- import type React from 'react'
2
- import { useState } from 'react'
3
-
4
- interface CreateIssueModalProps {
5
- isOpen: boolean
6
- onClose: () => void
7
- onCreated: () => void
8
- }
9
-
10
- export function CreateIssueModal({
11
- isOpen,
12
- onClose,
13
- onCreated,
14
- }: CreateIssueModalProps) {
15
- const [title, setTitle] = useState('')
16
- const [description, setDescription] = useState('')
17
- const [type, setType] = useState<'bug' | 'improvement'>('improvement')
18
- const [priority, setPriority] = useState<'high' | 'medium' | 'low'>('medium')
19
- const [labels, setLabels] = useState('')
20
- const [isSubmitting, setIsSubmitting] = useState(false)
21
- const [error, setError] = useState<string | null>(null)
22
-
23
- if (!isOpen) return null
24
-
25
- const handleSubmit = async (e: React.FormEvent) => {
26
- e.preventDefault()
27
-
28
- if (!title.trim()) {
29
- setError('Title is required')
30
- return
31
- }
32
-
33
- setIsSubmitting(true)
34
- setError(null)
35
-
36
- try {
37
- const response = await fetch('/api/issues/create', {
38
- method: 'POST',
39
- headers: { 'Content-Type': 'application/json' },
40
- body: JSON.stringify({
41
- title: title.trim(),
42
- description: description.trim() || title.trim(),
43
- type,
44
- priority,
45
- labels: labels.trim() || undefined,
46
- }),
47
- })
48
-
49
- if (!response.ok) {
50
- const data = await response.json()
51
- throw new Error(data.error || 'Failed to create issue')
52
- }
53
-
54
- // Reset form and close
55
- setTitle('')
56
- setDescription('')
57
- setType('improvement')
58
- setPriority('medium')
59
- setLabels('')
60
- onCreated()
61
- onClose()
62
- } catch (e) {
63
- setError(e instanceof Error ? e.message : 'Unknown error')
64
- } finally {
65
- setIsSubmitting(false)
66
- }
67
- }
68
-
69
- const handleBackdropClick = (e: React.MouseEvent) => {
70
- if (e.target === e.currentTarget) {
71
- onClose()
72
- }
73
- }
74
-
75
- return (
76
- <div
77
- className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4"
78
- onClick={handleBackdropClick}
79
- >
80
- <div className="bg-surface-elevated border border-border rounded-xl w-full max-w-lg shadow-2xl">
81
- <div className="flex items-center justify-between px-5 py-4 border-b border-border">
82
- <h2 className="text-lg font-semibold text-text-primary">New Issue</h2>
83
- <button
84
- onClick={onClose}
85
- className="text-text-muted hover:text-text-primary transition-colors"
86
- >
87
- <svg
88
- width="20"
89
- height="20"
90
- viewBox="0 0 24 24"
91
- fill="none"
92
- stroke="currentColor"
93
- strokeWidth="2"
94
- >
95
- <path d="M18 6L6 18M6 6l12 12" />
96
- </svg>
97
- </button>
98
- </div>
99
-
100
- <form onSubmit={handleSubmit} className="p-5 space-y-4">
101
- {error && (
102
- <div className="px-3 py-2 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
103
- {error}
104
- </div>
105
- )}
106
-
107
- <div>
108
- <label className="block text-sm text-text-secondary mb-1.5">
109
- Title *
110
- </label>
111
- <input
112
- type="text"
113
- value={title}
114
- onChange={(e) => setTitle(e.target.value)}
115
- placeholder="Brief description of the issue"
116
- className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm placeholder:text-text-muted focus:outline-none focus:border-accent"
117
- />
118
- </div>
119
-
120
- <div>
121
- <label className="block text-sm text-text-secondary mb-1.5">
122
- Description
123
- </label>
124
- <input
125
- type="text"
126
- value={description}
127
- onChange={(e) => setDescription(e.target.value)}
128
- placeholder="One-line summary (defaults to title)"
129
- className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm placeholder:text-text-muted focus:outline-none focus:border-accent"
130
- />
131
- </div>
132
-
133
- <div className="grid grid-cols-2 gap-4">
134
- <div>
135
- <label className="block text-sm text-text-secondary mb-1.5">
136
- Type
137
- </label>
138
- <select
139
- value={type}
140
- onChange={(e) =>
141
- setType(e.target.value as 'bug' | 'improvement')
142
- }
143
- className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm focus:outline-none focus:border-accent"
144
- >
145
- <option value="improvement">Improvement</option>
146
- <option value="bug">Bug</option>
147
- </select>
148
- </div>
149
-
150
- <div>
151
- <label className="block text-sm text-text-secondary mb-1.5">
152
- Priority
153
- </label>
154
- <select
155
- value={priority}
156
- onChange={(e) =>
157
- setPriority(e.target.value as 'high' | 'medium' | 'low')
158
- }
159
- className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm focus:outline-none focus:border-accent"
160
- >
161
- <option value="high">High</option>
162
- <option value="medium">Medium</option>
163
- <option value="low">Low</option>
164
- </select>
165
- </div>
166
- </div>
167
-
168
- <div>
169
- <label className="block text-sm text-text-secondary mb-1.5">
170
- Labels
171
- </label>
172
- <input
173
- type="text"
174
- value={labels}
175
- onChange={(e) => setLabels(e.target.value)}
176
- placeholder="Comma-separated: ui, backend, api"
177
- className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm placeholder:text-text-muted focus:outline-none focus:border-accent"
178
- />
179
- </div>
180
-
181
- <div className="flex justify-end gap-3 pt-2">
182
- <button
183
- type="button"
184
- onClick={onClose}
185
- className="px-4 py-2 text-sm text-text-secondary hover:text-text-primary transition-colors"
186
- >
187
- Cancel
188
- </button>
189
- <button
190
- type="submit"
191
- disabled={isSubmitting}
192
- className="px-4 py-2 bg-accent hover:bg-accent-hover text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
193
- >
194
- {isSubmitting ? 'Creating...' : 'Create Issue'}
195
- </button>
196
- </div>
197
- </form>
198
- </div>
199
- </div>
200
- )
201
- }
@@ -1,215 +0,0 @@
1
- import type React from 'react'
2
- import { useEffect, useState } from 'react'
3
- import type { Issue } from '../App'
4
-
5
- interface EditIssueModalProps {
6
- issue: Issue | null
7
- isOpen: boolean
8
- onClose: () => void
9
- onUpdated: () => void
10
- }
11
-
12
- export function EditIssueModal({
13
- issue,
14
- isOpen,
15
- onClose,
16
- onUpdated,
17
- }: EditIssueModalProps) {
18
- const [title, setTitle] = useState('')
19
- const [description, setDescription] = useState('')
20
- const [type, setType] = useState<'bug' | 'improvement'>('improvement')
21
- const [priority, setPriority] = useState<'high' | 'medium' | 'low'>('medium')
22
- const [labels, setLabels] = useState('')
23
- const [isSubmitting, setIsSubmitting] = useState(false)
24
- const [error, setError] = useState<string | null>(null)
25
-
26
- // Populate form when issue changes
27
- useEffect(() => {
28
- if (issue) {
29
- setTitle(issue.frontmatter.title || '')
30
- setDescription(issue.frontmatter.description || '')
31
- setType(
32
- (issue.frontmatter.type as 'bug' | 'improvement') || 'improvement',
33
- )
34
- setPriority(
35
- (issue.frontmatter.priority as 'high' | 'medium' | 'low') || 'medium',
36
- )
37
- setLabels(issue.frontmatter.labels || '')
38
- }
39
- }, [issue])
40
-
41
- if (!isOpen || !issue) return null
42
-
43
- const handleSubmit = async (e: React.FormEvent) => {
44
- e.preventDefault()
45
-
46
- if (!title.trim()) {
47
- setError('Title is required')
48
- return
49
- }
50
-
51
- setIsSubmitting(true)
52
- setError(null)
53
-
54
- try {
55
- const response = await fetch(`/api/issues/${issue.id}`, {
56
- method: 'PATCH',
57
- headers: { 'Content-Type': 'application/json' },
58
- body: JSON.stringify({
59
- title: title.trim(),
60
- description: description.trim() || title.trim(),
61
- type,
62
- priority,
63
- labels: labels.trim() || undefined,
64
- }),
65
- })
66
-
67
- if (!response.ok) {
68
- const data = await response.json()
69
- throw new Error(data.error || 'Failed to update issue')
70
- }
71
-
72
- onUpdated()
73
- onClose()
74
- } catch (e) {
75
- setError(e instanceof Error ? e.message : 'Unknown error')
76
- } finally {
77
- setIsSubmitting(false)
78
- }
79
- }
80
-
81
- const handleBackdropClick = (e: React.MouseEvent) => {
82
- if (e.target === e.currentTarget) {
83
- onClose()
84
- }
85
- }
86
-
87
- return (
88
- <div
89
- className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4"
90
- onClick={handleBackdropClick}
91
- >
92
- <div className="bg-surface-elevated border border-border rounded-xl w-full max-w-lg shadow-2xl">
93
- <div className="flex items-center justify-between px-5 py-4 border-b border-border">
94
- <h2 className="text-lg font-semibold text-text-primary">
95
- Edit Issue #{issue.id}
96
- </h2>
97
- <button
98
- onClick={onClose}
99
- className="text-text-muted hover:text-text-primary transition-colors"
100
- >
101
- <svg
102
- width="20"
103
- height="20"
104
- viewBox="0 0 24 24"
105
- fill="none"
106
- stroke="currentColor"
107
- strokeWidth="2"
108
- >
109
- <path d="M18 6L6 18M6 6l12 12" />
110
- </svg>
111
- </button>
112
- </div>
113
-
114
- <form onSubmit={handleSubmit} className="p-5 space-y-4">
115
- {error && (
116
- <div className="px-3 py-2 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
117
- {error}
118
- </div>
119
- )}
120
-
121
- <div>
122
- <label className="block text-sm text-text-secondary mb-1.5">
123
- Title *
124
- </label>
125
- <input
126
- type="text"
127
- value={title}
128
- onChange={(e) => setTitle(e.target.value)}
129
- placeholder="Brief description of the issue"
130
- className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm placeholder:text-text-muted focus:outline-none focus:border-accent"
131
- />
132
- </div>
133
-
134
- <div>
135
- <label className="block text-sm text-text-secondary mb-1.5">
136
- Description
137
- </label>
138
- <input
139
- type="text"
140
- value={description}
141
- onChange={(e) => setDescription(e.target.value)}
142
- placeholder="One-line summary"
143
- className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm placeholder:text-text-muted focus:outline-none focus:border-accent"
144
- />
145
- </div>
146
-
147
- <div className="grid grid-cols-2 gap-4">
148
- <div>
149
- <label className="block text-sm text-text-secondary mb-1.5">
150
- Type
151
- </label>
152
- <select
153
- value={type}
154
- onChange={(e) =>
155
- setType(e.target.value as 'bug' | 'improvement')
156
- }
157
- className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm focus:outline-none focus:border-accent"
158
- >
159
- <option value="improvement">Improvement</option>
160
- <option value="bug">Bug</option>
161
- </select>
162
- </div>
163
-
164
- <div>
165
- <label className="block text-sm text-text-secondary mb-1.5">
166
- Priority
167
- </label>
168
- <select
169
- value={priority}
170
- onChange={(e) =>
171
- setPriority(e.target.value as 'high' | 'medium' | 'low')
172
- }
173
- className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm focus:outline-none focus:border-accent"
174
- >
175
- <option value="high">High</option>
176
- <option value="medium">Medium</option>
177
- <option value="low">Low</option>
178
- </select>
179
- </div>
180
- </div>
181
-
182
- <div>
183
- <label className="block text-sm text-text-secondary mb-1.5">
184
- Labels
185
- </label>
186
- <input
187
- type="text"
188
- value={labels}
189
- onChange={(e) => setLabels(e.target.value)}
190
- placeholder="Comma-separated: ui, backend, api"
191
- className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm placeholder:text-text-muted focus:outline-none focus:border-accent"
192
- />
193
- </div>
194
-
195
- <div className="flex justify-end gap-3 pt-2">
196
- <button
197
- type="button"
198
- onClick={onClose}
199
- className="px-4 py-2 text-sm text-text-secondary hover:text-text-primary transition-colors"
200
- >
201
- Cancel
202
- </button>
203
- <button
204
- type="submit"
205
- disabled={isSubmitting}
206
- className="px-4 py-2 bg-accent hover:bg-accent-hover text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
207
- >
208
- {isSubmitting ? 'Saving...' : 'Save Changes'}
209
- </button>
210
- </div>
211
- </form>
212
- </div>
213
- </div>
214
- )
215
- }
@@ -1,209 +0,0 @@
1
- import { parseQuery } from '@miketromba/issy-core'
2
- import { useEffect, useMemo, useRef, useState } from 'react'
3
- import type { Issue } from '../App'
4
-
5
- interface FilterBarProps {
6
- query: string
7
- onQueryChange: (query: string) => void
8
- issues: Issue[]
9
- }
10
-
11
- interface DropdownProps {
12
- label: string
13
- value: string | null
14
- options: { value: string; label: string }[]
15
- onSelect: (value: string | null) => void
16
- }
17
-
18
- function Dropdown({ label, value, options, onSelect }: DropdownProps) {
19
- const [isOpen, setIsOpen] = useState(false)
20
- const ref = useRef<HTMLDivElement>(null)
21
-
22
- useEffect(() => {
23
- function handleClickOutside(e: MouseEvent) {
24
- if (ref.current && !ref.current.contains(e.target as Node)) {
25
- setIsOpen(false)
26
- }
27
- }
28
- document.addEventListener('mousedown', handleClickOutside)
29
- return () => document.removeEventListener('mousedown', handleClickOutside)
30
- }, [])
31
-
32
- const selectedOption = options.find((o) => o.value === value)
33
-
34
- return (
35
- <div ref={ref} className="relative">
36
- <button
37
- onClick={() => setIsOpen(!isOpen)}
38
- className={`inline-flex items-center gap-1 px-2 py-1 text-xs rounded border transition-colors ${
39
- value
40
- ? 'bg-accent/10 border-accent/30 text-accent'
41
- : 'bg-transparent border-border text-text-muted hover:text-text-secondary hover:border-border'
42
- }`}
43
- >
44
- <span>{selectedOption ? selectedOption.label : label}</span>
45
- <svg
46
- width="12"
47
- height="12"
48
- viewBox="0 0 24 24"
49
- fill="none"
50
- stroke="currentColor"
51
- strokeWidth="2"
52
- >
53
- <path d="M6 9l6 6 6-6" />
54
- </svg>
55
- </button>
56
-
57
- {isOpen && (
58
- <div className="absolute top-full left-0 mt-1 min-w-[120px] bg-surface-elevated border border-border rounded-lg shadow-lg z-50 py-1">
59
- {value && (
60
- <>
61
- <button
62
- onClick={() => {
63
- onSelect(null)
64
- setIsOpen(false)
65
- }}
66
- className="w-full px-3 py-1.5 text-left text-xs text-text-muted hover:bg-surface hover:text-text-primary"
67
- >
68
- Clear
69
- </button>
70
- <div className="border-t border-border my-1" />
71
- </>
72
- )}
73
- {options.map((option) => (
74
- <button
75
- key={option.value}
76
- onClick={() => {
77
- onSelect(option.value)
78
- setIsOpen(false)
79
- }}
80
- className={`w-full px-3 py-1.5 text-left text-xs hover:bg-surface ${
81
- value === option.value
82
- ? 'text-accent'
83
- : 'text-text-secondary hover:text-text-primary'
84
- }`}
85
- >
86
- {option.label}
87
- </button>
88
- ))}
89
- </div>
90
- )}
91
- </div>
92
- )
93
- }
94
-
95
- // Sort options are fixed since they're not data-dependent
96
- const SORT_OPTIONS = [
97
- { value: 'created', label: 'Newest' },
98
- { value: 'created-asc', label: 'Oldest' },
99
- { value: 'priority', label: 'Priority' },
100
- ]
101
-
102
- // Priority sort order for consistent display
103
- const PRIORITY_ORDER: Record<string, number> = { high: 0, medium: 1, low: 2 }
104
-
105
- function capitalize(str: string): string {
106
- return str.charAt(0).toUpperCase() + str.slice(1)
107
- }
108
-
109
- function extractUniqueValues(
110
- issues: Issue[],
111
- key: keyof Issue['frontmatter'],
112
- ): { value: string; label: string }[] {
113
- const values = new Set<string>()
114
-
115
- for (const issue of issues) {
116
- const val = issue.frontmatter[key]
117
- if (val && typeof val === 'string') {
118
- values.add(val.toLowerCase())
119
- }
120
- }
121
-
122
- return Array.from(values)
123
- .sort((a, b) => {
124
- // Special sort for priority
125
- if (key === 'priority') {
126
- return (PRIORITY_ORDER[a] ?? 99) - (PRIORITY_ORDER[b] ?? 99)
127
- }
128
- return a.localeCompare(b)
129
- })
130
- .map((v) => ({ value: v, label: capitalize(v) }))
131
- }
132
-
133
- export function FilterBar({ query, onQueryChange, issues }: FilterBarProps) {
134
- const parsed = parseQuery(query)
135
-
136
- // Dynamically extract options from actual issue data
137
- const statusOptions = useMemo(
138
- () => extractUniqueValues(issues, 'status'),
139
- [issues],
140
- )
141
- const priorityOptions = useMemo(
142
- () => extractUniqueValues(issues, 'priority'),
143
- [issues],
144
- )
145
- const typeOptions = useMemo(
146
- () => extractUniqueValues(issues, 'type'),
147
- [issues],
148
- )
149
-
150
- const updateQualifier = (key: string, value: string | null) => {
151
- const newQualifiers = { ...parsed.qualifiers }
152
-
153
- if (value === null) {
154
- delete newQualifiers[key]
155
- } else {
156
- newQualifiers[key] = value
157
- }
158
-
159
- // Rebuild query string
160
- const parts: string[] = []
161
-
162
- // Add qualifiers in consistent order
163
- if (newQualifiers.is) parts.push(`is:${newQualifiers.is}`)
164
- if (newQualifiers.priority) parts.push(`priority:${newQualifiers.priority}`)
165
- if (newQualifiers.type) parts.push(`type:${newQualifiers.type}`)
166
- if (newQualifiers.label) parts.push(`label:${newQualifiers.label}`)
167
- if (newQualifiers.sort) parts.push(`sort:${newQualifiers.sort}`)
168
-
169
- // Add search text at the end
170
- if (parsed.searchText) parts.push(parsed.searchText)
171
-
172
- onQueryChange(parts.join(' '))
173
- }
174
-
175
- return (
176
- <div className="flex items-center gap-2 flex-wrap">
177
- {statusOptions.length > 0 && (
178
- <Dropdown
179
- label="Status"
180
- value={parsed.qualifiers.is || null}
181
- options={statusOptions}
182
- onSelect={(v) => updateQualifier('is', v)}
183
- />
184
- )}
185
- {priorityOptions.length > 0 && (
186
- <Dropdown
187
- label="Priority"
188
- value={parsed.qualifiers.priority || null}
189
- options={priorityOptions}
190
- onSelect={(v) => updateQualifier('priority', v)}
191
- />
192
- )}
193
- {typeOptions.length > 0 && (
194
- <Dropdown
195
- label="Type"
196
- value={parsed.qualifiers.type || null}
197
- options={typeOptions}
198
- onSelect={(v) => updateQualifier('type', v)}
199
- />
200
- )}
201
- <Dropdown
202
- label="Sort"
203
- value={parsed.qualifiers.sort || null}
204
- options={SORT_OPTIONS}
205
- onSelect={(v) => updateQualifier('sort', v)}
206
- />
207
- </div>
208
- )
209
- }