|
139 | 139 | let seriesSearch = $state(''); |
140 | 140 | let seriesBookNum = $state(''); |
141 | 141 | let showSeriesDropdown = $state(false); |
| 142 | + let creatingSeries = $state(false); |
| 143 | + let availableSeries = $state(options.series); |
142 | 144 | let filteredSeries = $derived( |
143 | | - options.series.filter((s: any) => |
| 145 | + availableSeries.filter((s: any) => |
144 | 146 | s.title.toLowerCase().includes(seriesSearch.toLowerCase()) && |
145 | 147 | !selectedSeries.some(ss => ss.id === s.id) |
146 | 148 | ).slice(0, 10) |
147 | 149 | ); |
| 150 | + let canCreateSeries = $derived( |
| 151 | + seriesSearch.trim().length > 0 && |
| 152 | + !availableSeries.some((s: any) => s.title.toLowerCase() === seriesSearch.trim().toLowerCase()) |
| 153 | + ); |
148 | 154 |
|
149 | 155 | const tabs = [ |
150 | 156 | { id: 'basic', label: 'Basic', icon: Info }, |
|
176 | 182 | showSeriesDropdown = false; |
177 | 183 | } |
178 | 184 |
|
| 185 | + async function createNewSeries() { |
| 186 | + const title = seriesSearch.trim(); |
| 187 | + if (!title) return; |
| 188 | +
|
| 189 | + creatingSeries = true; |
| 190 | + try { |
| 191 | + const res = await fetch('/api/series', { |
| 192 | + method: 'POST', |
| 193 | + headers: { 'Content-Type': 'application/json' }, |
| 194 | + body: JSON.stringify({ title }) |
| 195 | + }); |
| 196 | +
|
| 197 | + if (res.ok) { |
| 198 | + const newSeries = await res.json(); |
| 199 | + availableSeries = [...availableSeries, { id: newSeries.id, title: newSeries.title }]; |
| 200 | + addSeries({ id: newSeries.id, title: newSeries.title }); |
| 201 | + } |
| 202 | + } catch (e) { |
| 203 | + console.error('Failed to create series:', e); |
| 204 | + } finally { |
| 205 | + creatingSeries = false; |
| 206 | + } |
| 207 | + } |
| 208 | +
|
179 | 209 | function removeSeries(id: number) { |
180 | 210 | selectedSeries = selectedSeries.filter(s => s.id !== id); |
181 | 211 | } |
|
1013 | 1043 | placeholder="Search series..." |
1014 | 1044 | bind:value={seriesSearch} |
1015 | 1045 | onfocus={() => showSeriesDropdown = true} |
| 1046 | + oninput={() => showSeriesDropdown = true} |
1016 | 1047 | class="w-full px-3 py-2 rounded-md text-sm" |
1017 | 1048 | style="background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary);" |
1018 | 1049 | /> |
1019 | | - {#if showSeriesDropdown && filteredSeries.length > 0} |
| 1050 | + {#if showSeriesDropdown} |
1020 | 1051 | <div |
1021 | 1052 | class="absolute z-20 w-full mt-1 rounded-lg shadow-xl max-h-40 overflow-y-auto" |
1022 | 1053 | style="background-color: var(--bg-secondary); border: 1px solid var(--border-color);" |
|
1034 | 1065 | {s.title} |
1035 | 1066 | </button> |
1036 | 1067 | {/each} |
| 1068 | + {#if canCreateSeries} |
| 1069 | + <button |
| 1070 | + type="button" |
| 1071 | + class="w-full px-3 py-2 text-left text-sm transition-colors flex items-center gap-2 font-medium" |
| 1072 | + style="color: var(--success); border-top: 1px solid var(--border-color);" |
| 1073 | + onmouseenter={(e) => e.currentTarget.style.backgroundColor = 'var(--bg-hover)'} |
| 1074 | + onmouseleave={(e) => e.currentTarget.style.backgroundColor = 'transparent'} |
| 1075 | + onclick={createNewSeries} |
| 1076 | + disabled={creatingSeries} |
| 1077 | + > |
| 1078 | + {#if creatingSeries} |
| 1079 | + <Loader2 class="w-3.5 h-3.5 animate-spin" /> |
| 1080 | + Creating... |
| 1081 | + {:else} |
| 1082 | + <Plus class="w-3.5 h-3.5" /> |
| 1083 | + Create "{seriesSearch.trim()}" |
| 1084 | + {/if} |
| 1085 | + </button> |
| 1086 | + {:else if filteredSeries.length === 0 && seriesSearch.trim().length === 0} |
| 1087 | + <div class="px-3 py-2 text-sm" style="color: var(--text-muted);"> |
| 1088 | + Type to search or create a new series |
| 1089 | + </div> |
| 1090 | + {:else if filteredSeries.length === 0} |
| 1091 | + <div class="px-3 py-2 text-sm" style="color: var(--text-muted);"> |
| 1092 | + No matching series found |
| 1093 | + </div> |
| 1094 | + {/if} |
1037 | 1095 | </div> |
1038 | 1096 | {/if} |
1039 | 1097 | </div> |
|
0 commit comments