feat(trash): TrashView page, nav links, and g+x shortcut
This commit is contained in:
@@ -82,6 +82,7 @@ function onGlobalKeydown(e: KeyboardEvent) {
|
|||||||
case "r": router.push("/rules"); break;
|
case "r": router.push("/rules"); break;
|
||||||
case "g": router.push("/graph"); break;
|
case "g": router.push("/graph"); break;
|
||||||
case "l": router.push("/calendar"); break;
|
case "l": router.push("/calendar"); break;
|
||||||
|
case "x": router.push("/trash"); break;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -195,6 +196,12 @@ onUnmounted(() => {
|
|||||||
<kbd class="shortcut-key">l</kbd>
|
<kbd class="shortcut-key">l</kbd>
|
||||||
<span class="shortcut-desc">Calendar</span>
|
<span class="shortcut-desc">Calendar</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="shortcut-row">
|
||||||
|
<kbd class="shortcut-key">g</kbd>
|
||||||
|
<span class="shortcut-key-sep">+</span>
|
||||||
|
<kbd class="shortcut-key">x</kbd>
|
||||||
|
<span class="shortcut-desc">Trash</span>
|
||||||
|
</div>
|
||||||
<div class="shortcut-row">
|
<div class="shortcut-row">
|
||||||
<kbd class="shortcut-key">Esc</kbd>
|
<kbd class="shortcut-key">Esc</kbd>
|
||||||
<span class="shortcut-desc">Unfocus field → go home</span>
|
<span class="shortcut-desc">Unfocus field → go home</span>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useShortcuts } from "@/composables/useShortcuts";
|
|||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import AppLogo from "@/components/AppLogo.vue";
|
import AppLogo from "@/components/AppLogo.vue";
|
||||||
import NotificationBell from "@/components/NotificationBell.vue";
|
import NotificationBell from "@/components/NotificationBell.vue";
|
||||||
import { Sun, Moon, Settings } from "lucide-vue-next";
|
import { Sun, Moon, Settings, Trash2 } from "lucide-vue-next";
|
||||||
|
|
||||||
const { theme, toggleTheme } = useTheme();
|
const { theme, toggleTheme } = useTheme();
|
||||||
const { toggleShortcuts } = useShortcuts();
|
const { toggleShortcuts } = useShortcuts();
|
||||||
@@ -63,6 +63,11 @@ router.afterEach(() => {
|
|||||||
<Moon v-else :size="16" />
|
<Moon v-else :size="16" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Trash link -->
|
||||||
|
<router-link to="/trash" class="btn-icon" aria-label="Trash" title="Trash">
|
||||||
|
<Trash2 :size="16" />
|
||||||
|
</router-link>
|
||||||
|
|
||||||
<!-- Settings link -->
|
<!-- Settings link -->
|
||||||
<router-link to="/settings" class="btn-icon" aria-label="Settings" title="Settings">
|
<router-link to="/settings" class="btn-icon" aria-label="Settings" title="Settings">
|
||||||
<Settings :size="16" />
|
<Settings :size="16" />
|
||||||
@@ -91,6 +96,7 @@ router.afterEach(() => {
|
|||||||
<router-link to="/rules" class="nav-link">Rulebooks</router-link>
|
<router-link to="/rules" class="nav-link">Rulebooks</router-link>
|
||||||
<router-link to="/shared" class="nav-link">Shared</router-link>
|
<router-link to="/shared" class="nav-link">Shared</router-link>
|
||||||
<div class="mobile-divider"></div>
|
<div class="mobile-divider"></div>
|
||||||
|
<router-link to="/trash" class="nav-link">Trash</router-link>
|
||||||
<router-link to="/settings" class="nav-link">Settings</router-link>
|
<router-link to="/settings" class="nav-link">Settings</router-link>
|
||||||
<div class="mobile-divider"></div>
|
<div class="mobile-divider"></div>
|
||||||
<div class="mobile-actions">
|
<div class="mobile-actions">
|
||||||
|
|||||||
@@ -113,6 +113,11 @@ const router = createRouter({
|
|||||||
name: "settings",
|
name: "settings",
|
||||||
component: () => import("@/views/SettingsView.vue"),
|
component: () => import("@/views/SettingsView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/trash",
|
||||||
|
name: "trash",
|
||||||
|
component: () => import("@/views/TrashView.vue"),
|
||||||
|
},
|
||||||
{ path: "/admin/users", redirect: "/settings" },
|
{ path: "/admin/users", redirect: "/settings" },
|
||||||
{ path: "/admin/logs", redirect: "/settings" },
|
{ path: "/admin/logs", redirect: "/settings" },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import { useTrashStore } from "@/stores/trash";
|
||||||
|
import { relativeTime } from "@/composables/useRelativeTime";
|
||||||
|
|
||||||
|
const store = useTrashStore();
|
||||||
|
|
||||||
|
async function purge(batchId: string) {
|
||||||
|
if (!confirm("Permanently delete this? This cannot be undone.")) return;
|
||||||
|
await store.purge(batchId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function empty() {
|
||||||
|
if (!store.batches.length) return;
|
||||||
|
if (!confirm("Permanently delete everything in the trash? This cannot be undone.")) return;
|
||||||
|
await store.emptyTrash();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => store.fetchTrash());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main class="trash-page">
|
||||||
|
<header class="trash-header">
|
||||||
|
<h1>Trash</h1>
|
||||||
|
<button
|
||||||
|
v-if="store.batches.length"
|
||||||
|
class="btn-empty"
|
||||||
|
@click="empty"
|
||||||
|
>Empty trash</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<p class="trash-note">
|
||||||
|
Deleted items are kept here and can be restored. They're permanently removed
|
||||||
|
after the retention window set in Settings.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-if="store.loading" class="trash-loading">Loading…</div>
|
||||||
|
|
||||||
|
<p v-else-if="!store.batches.length" class="trash-empty">Trash is empty.</p>
|
||||||
|
|
||||||
|
<ul v-else class="batch-list">
|
||||||
|
<li v-for="b in store.batches" :key="b.batch_id" class="batch">
|
||||||
|
<div class="batch-main">
|
||||||
|
<div class="batch-summary">
|
||||||
|
{{ b.summary }}
|
||||||
|
<span v-if="b.count > 1" class="batch-count">+ {{ b.count - 1 }} item{{ b.count - 1 === 1 ? '' : 's' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="batch-meta">
|
||||||
|
deleted {{ b.deleted_at ? relativeTime(b.deleted_at) : '' }}
|
||||||
|
<span class="batch-types">· {{ b.items.map(i => i.type).join(', ') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="batch-actions">
|
||||||
|
<button class="btn-restore" @click="store.restore(b.batch_id)">Restore</button>
|
||||||
|
<button class="btn-purge" @click="purge(b.batch_id)">Delete permanently</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.trash-page { max-width: 900px; margin: 0 auto; padding: 1.5rem; }
|
||||||
|
.trash-header { display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.trash-header h1 { font-family: Fraunces, serif; font-style: italic; margin: 0; }
|
||||||
|
.btn-empty {
|
||||||
|
background: none; border: 1px solid var(--color-border, #2a2a2e);
|
||||||
|
color: inherit; border-radius: 6px; padding: 0.4rem 0.8rem; cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-empty:hover { border-color: var(--color-danger, #ef4444); color: var(--color-danger, #ef4444); }
|
||||||
|
.trash-note { opacity: 0.7; font-size: 0.9em; margin: 0.5rem 0 1.5rem; }
|
||||||
|
.trash-loading, .trash-empty { opacity: 0.6; font-style: italic; padding: 2rem 0; }
|
||||||
|
.batch-list { list-style: none; padding: 0; margin: 0; }
|
||||||
|
.batch {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
gap: 1rem; padding: 0.85rem 1rem; margin-bottom: 0.5rem;
|
||||||
|
background: var(--color-surface, #18181b); border-radius: 8px;
|
||||||
|
border-left: 2px solid var(--color-border, #2a2a2e);
|
||||||
|
}
|
||||||
|
.batch-summary { font-weight: 500; }
|
||||||
|
.batch-count { opacity: 0.6; font-weight: 400; font-size: 0.9em; margin-left: 0.35rem; }
|
||||||
|
.batch-meta { font-size: 0.82em; opacity: 0.6; margin-top: 0.25rem; }
|
||||||
|
.batch-actions { display: flex; gap: 0.5rem; flex-shrink: 0; }
|
||||||
|
.batch-actions button { border-radius: 6px; padding: 0.35rem 0.7rem; cursor: pointer; border: 1px solid var(--color-border, #2a2a2e); background: none; color: inherit; }
|
||||||
|
.btn-restore:hover { border-color: var(--color-primary, #6366f1); color: var(--color-primary, #6366f1); }
|
||||||
|
.btn-purge:hover { border-color: var(--color-danger, #ef4444); color: var(--color-danger, #ef4444); }
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user