mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat: add save-as-lens from HomeView filter panel
- add SaveAsLensDialog component that takes current filter and sort as props - wire Save as Lens button in HomeView filter panel header - on save, creates lens with the active filter/sort and navigates to it
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { useLenses } from '@/features/lens/composables/useLenses'
|
||||
import IconPicker from '@/components/IconPicker.vue'
|
||||
import type { GroupRule, SortSpec } from '@projectx/types'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
filter: GroupRule | undefined
|
||||
sort: SortSpec[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ close: [] }>()
|
||||
|
||||
const router = useRouter()
|
||||
const { createLens } = useLenses()
|
||||
|
||||
const name = ref('')
|
||||
const icon = ref('')
|
||||
const saving = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function submit() {
|
||||
if (!name.value.trim()) return
|
||||
saving.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const lens = await createLens({
|
||||
name: name.value.trim(),
|
||||
icon: icon.value.trim() || undefined,
|
||||
filter: props.filter,
|
||||
defaultSort: props.sort,
|
||||
})
|
||||
name.value = ''
|
||||
icon.value = ''
|
||||
emit('close')
|
||||
router.push({ name: 'lens', params: { id: lens.id } })
|
||||
} catch {
|
||||
error.value = 'Failed to save lens'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="open" class="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" @click="emit('close')" />
|
||||
<div class="relative z-10 w-full max-w-md mx-4 bg-card border border-border rounded-xl shadow-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h2 class="text-base font-semibold text-foreground">Save as Lens</h2>
|
||||
<button @click="emit('close')" class="text-muted-foreground hover:text-foreground transition-colors">
|
||||
<X :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit" class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-sm font-medium text-foreground">Name</label>
|
||||
<input
|
||||
v-model="name"
|
||||
type="text"
|
||||
placeholder="e.g. Unread Sci-Fi"
|
||||
autofocus
|
||||
class="h-9 rounded-md border border-input bg-background text-foreground text-sm px-3 focus:outline-none focus:ring-2 focus:ring-primary placeholder:text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-sm font-medium text-foreground"> Icon <span class="text-muted-foreground font-normal">(optional)</span> </label>
|
||||
<IconPicker v-model="icon" placeholder="Choose an icon..." />
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="text-sm text-destructive">{{ error }}</p>
|
||||
|
||||
<div class="flex justify-end gap-2 mt-1">
|
||||
<button
|
||||
type="button"
|
||||
@click="emit('close')"
|
||||
class="h-9 px-4 rounded-md border border-input bg-background text-sm text-foreground hover:bg-muted transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="!name.trim() || saving"
|
||||
class="h-9 px-4 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{{ saving ? 'Saving...' : 'Save Lens' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ArrowUpDown, Bookmark, BookmarkCheck, Filter, X } from 'lucide-vue-next'
|
||||
import { ArrowUpDown, Bookmark, BookmarkCheck, Filter, Telescope, X } from 'lucide-vue-next'
|
||||
import BookCoverCard from '@/features/book/components/BookCoverCard.vue'
|
||||
import BookListRow from '@/features/book/components/BookListRow.vue'
|
||||
import BookQuickView from '@/features/book/components/BookQuickView.vue'
|
||||
@@ -14,6 +14,7 @@ import AppSidebar from '@/components/AppSidebar.vue'
|
||||
import SelectionActionBar from '@/components/SelectionActionBar.vue'
|
||||
import AddToCollectionSheet from '@/features/collection/components/AddToCollectionSheet.vue'
|
||||
import SendBookDialog from '@/features/email/components/SendBookDialog.vue'
|
||||
import SaveAsLensDialog from '@/features/lens/components/SaveAsLensDialog.vue'
|
||||
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { api } from '@/lib/api'
|
||||
@@ -212,6 +213,7 @@ function toggleSelectionMode() {
|
||||
|
||||
const addToCollectionOpen = ref(false)
|
||||
const sendBookOpen = ref(false)
|
||||
const saveAsLensOpen = ref(false)
|
||||
|
||||
async function handleDeleteSelected() {
|
||||
const ids = [...selectedIds.value]
|
||||
@@ -409,6 +411,19 @@ function handleBookAction(book: BookCard, action: BookActionType) {
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-xs font-medium text-muted-foreground">Filter rules</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
v-if="activeFilterCount > 0"
|
||||
@click="saveAsLensOpen = true"
|
||||
class="flex items-center gap-1.5 h-7 px-3 rounded-md border border-input text-xs font-medium text-muted-foreground bg-background hover:text-foreground hover:bg-muted transition-colors"
|
||||
>
|
||||
<Telescope :size="13" />
|
||||
Save as Lens
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Save this filter as a named lens</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
@@ -510,4 +525,6 @@ function handleBookAction(book: BookCard, action: BookActionType) {
|
||||
/>
|
||||
|
||||
<SendBookDialog :open="sendBookOpen" :book-ids="[...selectedIds]" @update:open="sendBookOpen = $event" @sent="exitSelectionMode" />
|
||||
|
||||
<SaveAsLensDialog :open="saveAsLensOpen" :filter="filter" :sort="sort" @close="saveAsLensOpen = false" />
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user