257 lines
11 KiB
Vue
257 lines
11 KiB
Vue
<script setup>
|
||
import { Link, router } from '@inertiajs/vue3';
|
||
import { computed, ref } from 'vue';
|
||
|
||
import ConfirmDialog from '../../Components/ConfirmDialog.vue';
|
||
import TagPill from '../../Components/TagPill.vue';
|
||
import { formatDate } from '../../lib/format.js';
|
||
|
||
const props = defineProps({
|
||
prompts: { type: Object, required: true },
|
||
tags: { type: Array, required: true },
|
||
filters: { type: Object, required: true },
|
||
sort: { type: String, required: true },
|
||
direction: { type: String, required: true },
|
||
});
|
||
|
||
const search = ref(props.filters.search ?? '');
|
||
const deleting = ref(null);
|
||
|
||
const rows = computed(() => props.prompts.data ?? []);
|
||
const currentPage = computed(() => props.prompts.current_page ?? 1);
|
||
const lastPage = computed(() => props.prompts.last_page ?? 1);
|
||
const total = computed(() => props.prompts.total ?? 0);
|
||
|
||
const params = (extra = {}) => ({
|
||
search: props.filters.search || undefined,
|
||
tag: props.filters.tag || undefined,
|
||
favorites: props.filters.favorites ? '1' : undefined,
|
||
sort: props.sort,
|
||
direction: props.direction,
|
||
...extra,
|
||
});
|
||
|
||
const refresh = (extra = {}) =>
|
||
router.get(
|
||
'/prompts',
|
||
params(extra),
|
||
{ preserveState: true, preserveScroll: true },
|
||
{ only: ['prompts', 'filters', 'sort', 'direction'] }
|
||
);
|
||
|
||
const onSearch = (event) => {
|
||
event.preventDefault();
|
||
refresh({ search: search.value });
|
||
};
|
||
|
||
const onFilter = (key, value) => {
|
||
refresh({ [key]: value });
|
||
};
|
||
|
||
const favoritePrompt = (prompt) => {
|
||
router.post(`/prompts/${prompt.id}/toggle-favorite`, {}, {
|
||
preserveScroll: true,
|
||
only: ['prompts'],
|
||
});
|
||
};
|
||
|
||
const duplicatePrompt = (prompt) => {
|
||
router.post(`/prompts/${prompt.id}/duplicate`);
|
||
};
|
||
|
||
const confirmDelete = () => {
|
||
const target = deleting.value;
|
||
|
||
if (!target) {
|
||
return;
|
||
}
|
||
|
||
deleting.value = null;
|
||
router.delete(`/prompts/${target.id}`, {
|
||
preserveScroll: true,
|
||
onFinish: () => refresh({}),
|
||
});
|
||
};
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<div class="mb-5 flex flex-wrap items-center justify-between gap-3">
|
||
<div>
|
||
<h1 class="text-xl font-semibold tracking-tight">Prompts</h1>
|
||
<p class="text-sm text-slate-500">{{ total }} prompt(s) in your vault</p>
|
||
</div>
|
||
<form class="relative" @submit="onSearch">
|
||
<input
|
||
v-model="search"
|
||
type="search"
|
||
name="search"
|
||
placeholder="Search name, description, body…"
|
||
class="w-72 rounded-md border border-slate-300 bg-white py-2 pl-3 pr-3 text-sm shadow-sm focus:border-slate-500 focus:outline-none"
|
||
/>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="mb-4 flex flex-wrap items-center gap-2 text-sm">
|
||
<label class="flex items-center gap-1.5 text-slate-600">
|
||
Tag
|
||
<select
|
||
:value="filters.tag ?? ''"
|
||
class="rounded-md border border-slate-300 bg-white py-1.5 pl-2 pr-7 text-sm shadow-sm"
|
||
@change="onFilter('tag', $event.target.value)"
|
||
>
|
||
<option value="">All tags</option>
|
||
<option v-for="tag in tags" :key="tag" :value="tag">
|
||
{{ tag }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="flex items-center gap-1.5 text-slate-600">
|
||
<input
|
||
type="checkbox"
|
||
class="h-4 w-4 rounded border-slate-300"
|
||
:checked="filters.favorites"
|
||
@change="onFilter('favorites', $event.target.checked ? '1' : undefined)"
|
||
/>
|
||
★ Favorites
|
||
</label>
|
||
<label class="flex items-center gap-1.5 text-slate-600">
|
||
Sort
|
||
<select
|
||
:value="sort"
|
||
class="rounded-md border border-slate-300 bg-white py-1.5 pl-2 pr-7 text-sm shadow-sm"
|
||
@change="onFilter('sort', $event.target.value)"
|
||
>
|
||
<option value="created_at">Newest</option>
|
||
<option value="name">Name</option>
|
||
<option value="usage_count">Most used</option>
|
||
<option value="last_used_at">Recently used</option>
|
||
<option value="updated_at">Recently updated</option>
|
||
</select>
|
||
<select
|
||
:value="direction"
|
||
class="rounded-md border border-slate-300 bg-white py-1.5 pl-2 pr-7 text-sm shadow-sm"
|
||
@change="onFilter('direction', $event.target.value)"
|
||
>
|
||
<option value="asc">A→Z</option>
|
||
<option value="desc">Z→A</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm">
|
||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||
<thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500">
|
||
<tr>
|
||
<th class="px-4 py-3 font-medium">Name</th>
|
||
<th class="px-4 py-3 font-medium">Tags</th>
|
||
<th class="px-4 py-3 text-right font-medium">Usage</th>
|
||
<th class="px-4 py-3 font-medium">Last used</th>
|
||
<th class="px-4 py-3 text-right font-medium">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-slate-100">
|
||
<tr v-if="rows.length === 0" class="bg-white">
|
||
<td colspan="5" class="px-4 py-10 text-center text-slate-500">
|
||
No prompts match.
|
||
<Link href="/prompts/create" class="text-slate-900 underline">Create one</Link>.
|
||
</td>
|
||
</tr>
|
||
<tr v-for="prompt in rows" :key="prompt.id" class="hover:bg-slate-50">
|
||
<td class="max-w-xs px-4 py-3">
|
||
<div class="flex items-center gap-1.5">
|
||
<button
|
||
type="button"
|
||
class="shrink-0 text-base leading-none"
|
||
:class="prompt.is_favorite ? 'text-amber-500' : 'text-slate-300 hover:text-slate-400'"
|
||
:aria-label="prompt.is_favorite ? 'Unfavorite' : 'Favorite'"
|
||
@click="favoritePrompt(prompt)"
|
||
>
|
||
★
|
||
</button>
|
||
<Link
|
||
:href="`/prompts/${prompt.id}`"
|
||
class="font-medium text-slate-900 hover:underline"
|
||
>
|
||
{{ prompt.name }}
|
||
</Link>
|
||
</div>
|
||
<p v-if="prompt.description" class="mt-0.5 truncate text-xs text-slate-500">
|
||
{{ prompt.description }}
|
||
</p>
|
||
</td>
|
||
<td class="px-4 py-3">
|
||
<div class="flex flex-wrap gap-1">
|
||
<TagPill v-for="tag in prompt.tags" :key="tag.id" :name="tag.name" />
|
||
</div>
|
||
</td>
|
||
<td class="px-4 py-3 text-right tabular-nums text-slate-600">
|
||
{{ prompt.usage_count }}×
|
||
</td>
|
||
<td class="px-4 py-3 text-slate-600">
|
||
{{ formatDate(prompt.last_used_at) }}
|
||
</td>
|
||
<td class="px-4 py-3">
|
||
<div class="flex items-center justify-end gap-1 text-xs">
|
||
<Link
|
||
:href="`/prompts/${prompt.id}/edit`"
|
||
class="rounded px-2 py-1 text-slate-600 hover:bg-slate-100"
|
||
>
|
||
Edit
|
||
</Link>
|
||
<button
|
||
type="button"
|
||
class="rounded px-2 py-1 text-slate-600 hover:bg-slate-100"
|
||
@click="duplicatePrompt(prompt)"
|
||
>
|
||
Duplicate
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="rounded px-2 py-1 text-red-600 hover:bg-red-50"
|
||
@click="deleting = prompt"
|
||
>
|
||
Delete
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-if="lastPage > 1" class="mt-4 flex items-center justify-between text-sm">
|
||
<span class="text-slate-500">
|
||
Page {{ currentPage }} of {{ lastPage }}
|
||
</span>
|
||
<div class="flex gap-2">
|
||
<button
|
||
type="button"
|
||
class="rounded-md border border-slate-300 bg-white px-3 py-1.5 shadow-sm disabled:cursor-not-allowed disabled:opacity-40"
|
||
:disabled="currentPage <= 1"
|
||
@click="refresh({ page: currentPage - 1 })"
|
||
>
|
||
← Prev
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="rounded-md border border-slate-300 bg-white px-3 py-1.5 shadow-sm disabled:cursor-not-allowed disabled:opacity-40"
|
||
:disabled="currentPage >= lastPage"
|
||
@click="refresh({ page: currentPage + 1 })"
|
||
>
|
||
Next →
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<ConfirmDialog
|
||
:open="deleting !== null"
|
||
title="Delete prompt?"
|
||
:message="deleting ? `“${deleting.name}” will be permanently deleted.` : ''"
|
||
confirm-label="Delete"
|
||
@confirm="confirmDelete"
|
||
@cancel="deleting = null"
|
||
/>
|
||
</div>
|
||
</template>
|