|
| 1 | +<template> |
| 2 | + <div class="card-preview-list"> |
| 3 | + <!-- Delete Confirmation Dialog --> |
| 4 | + <v-dialog v-model="showDeleteConfirm" max-width="400px"> |
| 5 | + <v-card> |
| 6 | + <v-card-title class="text-h5">Remove Card</v-card-title> |
| 7 | + <v-card-text> |
| 8 | + <p>Are you sure you want to remove this card?</p> |
| 9 | + <v-checkbox |
| 10 | + v-model="dontAskAgain" |
| 11 | + label="Don't ask me again" |
| 12 | + hide-details |
| 13 | + class="mt-2" |
| 14 | + ></v-checkbox> |
| 15 | + </v-card-text> |
| 16 | + <v-card-actions> |
| 17 | + <v-spacer></v-spacer> |
| 18 | + <v-btn color="grey-darken-1" variant="text" @click="cancelDelete">Cancel</v-btn> |
| 19 | + <v-btn color="error" @click="confirmDelete">Remove</v-btn> |
| 20 | + </v-card-actions> |
| 21 | + </v-card> |
| 22 | + </v-dialog> |
| 23 | + |
| 24 | + <v-card v-if="parsedCards.length > 0" class="mb-4"> |
| 25 | + <v-card-title class="d-flex align-center justify-space-between"> |
| 26 | + <span>Card Preview</span> |
| 27 | + <div class="d-flex align-center"> |
| 28 | + <div class="text-subtitle-1 mr-2">{{ currentIndex + 1 }} of {{ parsedCards.length }}</div> |
| 29 | + <sk-mouse-trap /> |
| 30 | + </div> |
| 31 | + </v-card-title> |
| 32 | + |
| 33 | + <v-card-text> |
| 34 | + <div v-if="loading" class="d-flex justify-center align-center my-4"> |
| 35 | + <v-progress-circular indeterminate color="primary"></v-progress-circular> |
| 36 | + </div> |
| 37 | + <div v-else> |
| 38 | + <div v-if="currentCard"> |
| 39 | + <v-sheet class="card-content pa-4 mb-4" rounded border> |
| 40 | + <!-- Rendered card content --> |
| 41 | + <div v-if="viewComponents && viewComponents.length > 0"> |
| 42 | + <card-browser :views="viewComponents" :data="[currentViewData]" :suppress-spinner="true" /> |
| 43 | + </div> |
| 44 | + <!-- Fallback markdown display when no view components are available --> |
| 45 | + <div v-else> |
| 46 | + <div class="mb-2 font-weight-bold">Card content:</div> |
| 47 | + <div class="markdown-content">{{ currentCard.markdown }}</div> |
| 48 | + </div> |
| 49 | + </v-sheet> |
| 50 | + |
| 51 | + <div class="card-metadata mt-2"> |
| 52 | + <div class="d-flex flex-wrap gap-1"> |
| 53 | + <v-chip |
| 54 | + v-for="tag in currentCard.tags" |
| 55 | + :key="tag" |
| 56 | + size="small" |
| 57 | + color="primary" |
| 58 | + variant="outlined" |
| 59 | + class="mr-1 mb-1" |
| 60 | + > |
| 61 | + {{ tag }} |
| 62 | + </v-chip> |
| 63 | + </div> |
| 64 | + <div v-if="currentCard.elo !== undefined" class="mt-2 text-caption">ELO: {{ currentCard.elo }}</div> |
| 65 | + </div> |
| 66 | + </div> |
| 67 | + <div v-else class="text-center pa-4">No card selected or available to preview.</div> |
| 68 | + </div> |
| 69 | + </v-card-text> |
| 70 | + |
| 71 | + <v-card-actions class="px-4 pb-4"> |
| 72 | + <v-btn variant="outlined" icon :disabled="currentIndex === 0 || loading" @click="prevCard"> |
| 73 | + <v-icon>mdi-chevron-left</v-icon> |
| 74 | + </v-btn> |
| 75 | + |
| 76 | + <v-spacer></v-spacer> |
| 77 | + |
| 78 | + <v-btn |
| 79 | + variant="tonal" |
| 80 | + color="error" |
| 81 | + prepend-icon="mdi-delete" |
| 82 | + class="mx-2" |
| 83 | + @click="promptDelete" |
| 84 | + :disabled="!currentCard || loading" |
| 85 | + > |
| 86 | + Remove |
| 87 | + </v-btn> |
| 88 | + |
| 89 | + <v-btn |
| 90 | + variant="tonal" |
| 91 | + color="primary" |
| 92 | + prepend-icon="mdi-pencil" |
| 93 | + class="mx-2" |
| 94 | + @click="editCurrentCard" |
| 95 | + :disabled="!currentCard || loading" |
| 96 | + > |
| 97 | + Edit |
| 98 | + </v-btn> |
| 99 | + |
| 100 | + <v-spacer></v-spacer> |
| 101 | + |
| 102 | + <v-btn variant="outlined" icon :disabled="currentIndex >= parsedCards.length - 1 || loading" @click="nextCard"> |
| 103 | + <v-icon>mdi-chevron-right</v-icon> |
| 104 | + </v-btn> |
| 105 | + </v-card-actions> |
| 106 | + </v-card> |
| 107 | + </div> |
| 108 | +</template> |
| 109 | + |
| 110 | +<script lang="ts"> |
| 111 | +import { ViewComponent, SkldrMouseTrap, HotKey, SkMouseTrap } from '@vue-skuilder/common-ui'; |
| 112 | +import { DataShape, ParsedCard, ViewData } from '@vue-skuilder/common'; |
| 113 | +import { defineComponent, PropType } from 'vue'; |
| 114 | +import CardBrowser from '../CardBrowser.vue'; |
| 115 | +
|
| 116 | +export default defineComponent({ |
| 117 | + name: 'CardPreviewList', |
| 118 | +
|
| 119 | + components: { |
| 120 | + CardBrowser, |
| 121 | + SkMouseTrap, |
| 122 | + }, |
| 123 | +
|
| 124 | + props: { |
| 125 | + parsedCards: { |
| 126 | + type: Array as PropType<ParsedCard[]>, |
| 127 | + required: true, |
| 128 | + default: () => [], |
| 129 | + }, |
| 130 | + dataShape: { |
| 131 | + type: Object as PropType<DataShape>, |
| 132 | + required: true, |
| 133 | + }, |
| 134 | + viewComponents: { |
| 135 | + type: Array as PropType<ViewComponent[]>, |
| 136 | + required: false, |
| 137 | + default: () => [], |
| 138 | + }, |
| 139 | + }, |
| 140 | +
|
| 141 | + emits: ['update:parsedCards', 'edit-card', 'delete-card'], |
| 142 | +
|
| 143 | + data() { |
| 144 | + return { |
| 145 | + currentIndex: 0, |
| 146 | + loading: false, |
| 147 | + keyBindings: [] as HotKey[], |
| 148 | + showDeleteConfirm: false, |
| 149 | + dontAskAgain: false, |
| 150 | + skipDeleteConfirmation: false, |
| 151 | + shortcutsEnabled: true, |
| 152 | + }; |
| 153 | + }, |
| 154 | +
|
| 155 | + computed: { |
| 156 | + currentCard(): ParsedCard | null { |
| 157 | + if (this.parsedCards.length === 0 || this.currentIndex >= this.parsedCards.length) { |
| 158 | + return null; |
| 159 | + } |
| 160 | + return this.parsedCards[this.currentIndex]; |
| 161 | + }, |
| 162 | +
|
| 163 | + currentViewData(): ViewData { |
| 164 | + if (!this.currentCard) { |
| 165 | + return { Input: '' }; |
| 166 | + } |
| 167 | +
|
| 168 | + // Convert ParsedCard to ViewData |
| 169 | + return { |
| 170 | + Input: this.currentCard.markdown, |
| 171 | + // Any additional fields the view might need |
| 172 | + }; |
| 173 | + }, |
| 174 | + }, |
| 175 | +
|
| 176 | + methods: { |
| 177 | + nextCard() { |
| 178 | + if (this.currentIndex < this.parsedCards.length - 1) { |
| 179 | + this.currentIndex++; |
| 180 | + } |
| 181 | + }, |
| 182 | +
|
| 183 | + prevCard() { |
| 184 | + if (this.currentIndex > 0) { |
| 185 | + this.currentIndex--; |
| 186 | + } |
| 187 | + }, |
| 188 | +
|
| 189 | + showCard(index: number) { |
| 190 | + if (index >= 0 && index < this.parsedCards.length) { |
| 191 | + this.currentIndex = index; |
| 192 | + } |
| 193 | + }, |
| 194 | +
|
| 195 | + setupKeyBindings() { |
| 196 | + // Define key bindings for navigation |
| 197 | + this.keyBindings = [ |
| 198 | + { |
| 199 | + command: 'Previous Card', |
| 200 | + hotkey: 'left', |
| 201 | + callback: () => { |
| 202 | + this.prevCard(); |
| 203 | + return false; // Prevent default |
| 204 | + }, |
| 205 | + }, |
| 206 | + { |
| 207 | + command: 'Next Card', |
| 208 | + hotkey: 'right', |
| 209 | + callback: () => { |
| 210 | + this.nextCard(); |
| 211 | + return false; // Prevent default |
| 212 | + }, |
| 213 | + }, |
| 214 | + { |
| 215 | + command: 'Delete Card', |
| 216 | + hotkey: 'del', |
| 217 | + callback: () => { |
| 218 | + this.promptDelete(); |
| 219 | + return false; // Prevent default |
| 220 | + }, |
| 221 | + }, |
| 222 | + { |
| 223 | + command: 'Edit Card', |
| 224 | + hotkey: 'e', |
| 225 | + callback: () => { |
| 226 | + this.editCurrentCard(); |
| 227 | + return false; // Prevent default |
| 228 | + }, |
| 229 | + }, |
| 230 | + ]; |
| 231 | +
|
| 232 | + if (this.shortcutsEnabled) { |
| 233 | + // Register keyboard shortcuts |
| 234 | + SkldrMouseTrap.bind(this.keyBindings); |
| 235 | + } |
| 236 | + }, |
| 237 | + |
| 238 | + enableShortcuts() { |
| 239 | + if (!this.shortcutsEnabled) { |
| 240 | + this.shortcutsEnabled = true; |
| 241 | + SkldrMouseTrap.bind(this.keyBindings); |
| 242 | + console.log('[CardPreviewList] Keyboard shortcuts enabled'); |
| 243 | + } |
| 244 | + }, |
| 245 | +
|
| 246 | + disableShortcuts() { |
| 247 | + if (this.shortcutsEnabled) { |
| 248 | + this.shortcutsEnabled = false; |
| 249 | + SkldrMouseTrap.reset(); |
| 250 | + console.log('[CardPreviewList] Keyboard shortcuts disabled'); |
| 251 | + } |
| 252 | + }, |
| 253 | + |
| 254 | + toggleShortcuts(enable: boolean) { |
| 255 | + if (enable) { |
| 256 | + this.enableShortcuts(); |
| 257 | + } else { |
| 258 | + this.disableShortcuts(); |
| 259 | + } |
| 260 | + }, |
| 261 | +
|
| 262 | + promptDelete() { |
| 263 | + if (!this.currentCard) return; |
| 264 | + |
| 265 | + if (this.skipDeleteConfirmation) { |
| 266 | + this.deleteCurrentCard(); |
| 267 | + } else { |
| 268 | + this.showDeleteConfirm = true; |
| 269 | + } |
| 270 | + }, |
| 271 | + |
| 272 | + cancelDelete() { |
| 273 | + this.showDeleteConfirm = false; |
| 274 | + }, |
| 275 | + |
| 276 | + confirmDelete() { |
| 277 | + if (this.dontAskAgain) { |
| 278 | + this.skipDeleteConfirmation = true; |
| 279 | + } |
| 280 | + |
| 281 | + this.showDeleteConfirm = false; |
| 282 | + this.deleteCurrentCard(); |
| 283 | + }, |
| 284 | + |
| 285 | + deleteCurrentCard() { |
| 286 | + if (!this.currentCard) return; |
| 287 | +
|
| 288 | + // Create a new array without the current card |
| 289 | + const updatedCards = [...this.parsedCards]; |
| 290 | + updatedCards.splice(this.currentIndex, 1); |
| 291 | +
|
| 292 | + // Emit event with updated array |
| 293 | + this.$emit('update:parsedCards', updatedCards); |
| 294 | +
|
| 295 | + // Also emit a specific event for parent component to handle |
| 296 | + this.$emit('delete-card', this.currentIndex); |
| 297 | +
|
| 298 | + // Adjust current index if needed |
| 299 | + if (this.currentIndex >= updatedCards.length && updatedCards.length > 0) { |
| 300 | + this.currentIndex = updatedCards.length - 1; |
| 301 | + } |
| 302 | + }, |
| 303 | +
|
| 304 | + editCurrentCard() { |
| 305 | + if (!this.currentCard) return; |
| 306 | +
|
| 307 | + // Emit event with current card and index |
| 308 | + this.$emit('edit-card', this.currentCard, this.currentIndex); |
| 309 | + }, |
| 310 | + }, |
| 311 | + mounted() { |
| 312 | + this.setupKeyBindings(); |
| 313 | + }, |
| 314 | + |
| 315 | + beforeUnmount() { |
| 316 | + // Clean up key bindings when component is unmounted |
| 317 | + SkldrMouseTrap.reset(); |
| 318 | + }, |
| 319 | +}); |
| 320 | +</script> |
| 321 | + |
| 322 | +<style scoped> |
| 323 | +.card-preview-list { |
| 324 | + width: 100%; |
| 325 | +} |
| 326 | +
|
| 327 | +.card-content { |
| 328 | + min-height: 150px; |
| 329 | + background-color: #f8f9fa; |
| 330 | +} |
| 331 | +
|
| 332 | +.markdown-content { |
| 333 | + white-space: pre-wrap; |
| 334 | + word-break: break-word; |
| 335 | + line-height: 1.5; |
| 336 | + font-family: var(--v-font-family); |
| 337 | +} |
| 338 | +
|
| 339 | +.gap-1 { |
| 340 | + gap: 4px; |
| 341 | +} |
| 342 | +</style> |
0 commit comments