From 5bcdcd6568c28bbcbcdb98105b3cbb28e064303b Mon Sep 17 00:00:00 2001 From: Kevin Midboe Date: Fri, 27 Feb 2026 18:43:38 +0100 Subject: [PATCH] Add command palette with smart usage tracking and content search - Implement keyboard shortcut (Cmd/Ctrl+K) to open command palette - Add smart ranking algorithm (70% frequency + 30% recency) - Track both route navigation and content (movies/shows) usage - Support parameter input for dynamic routes (e.g., /movie/:id) - Add query parameter support for search routes - Integrate ElasticSearch fallback for content search - Include rate limiting and error handling for API calls - Store usage data in localStorage (commandPalette_stats) - Auto-scroll selected items into view with keyboard navigation --- src/App.vue | 5 +- src/components/ui/CommandPalette.vue | 835 +++++++++++++++++++++++++++ src/routes.ts | 8 +- src/utils/commandTracking.ts | 103 ++++ 4 files changed, 948 insertions(+), 3 deletions(-) create mode 100644 src/components/ui/CommandPalette.vue create mode 100644 src/utils/commandTracking.ts diff --git a/src/App.vue b/src/App.vue index fe9c9a6..3bf3726 100644 --- a/src/App.vue +++ b/src/App.vue @@ -14,6 +14,9 @@ + + + @@ -23,6 +26,7 @@ import NavigationIcons from "@/components/header/NavigationIcons.vue"; import Popup from "@/components/Popup.vue"; import DarkmodeToggle from "@/components/ui/DarkmodeToggle.vue"; + import CommandPalette from "@/components/ui/CommandPalette.vue"; const router = useRouter(); @@ -61,7 +65,6 @@ grid-column: 2 / 3; width: calc(100% - var(--header-size)); grid-row: 2; - z-index: 5; @include mobile { grid-column: 1 / 3; diff --git a/src/components/ui/CommandPalette.vue b/src/components/ui/CommandPalette.vue new file mode 100644 index 0000000..9bca94a --- /dev/null +++ b/src/components/ui/CommandPalette.vue @@ -0,0 +1,835 @@ + + + + + diff --git a/src/routes.ts b/src/routes.ts index 1854504..bcf4f86 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -56,8 +56,7 @@ const routes: RouteRecordRaw[] = [ }, { name: "signin", - path: "/signin", - alias: "/login", + path: "/login", component: () => import("./pages/SigninPage.vue") }, { @@ -83,6 +82,11 @@ const routes: RouteRecordRaw[] = [ path: "/admin", meta: { requiresAuth: true }, component: () => import("./pages/AdminPage.vue") + }, + { + path: "/:pathMatch(.*)*", + name: "NotFound", + component: () => import("./pages/404Page.vue") } // { // path: "*", diff --git a/src/utils/commandTracking.ts b/src/utils/commandTracking.ts new file mode 100644 index 0000000..bc81502 --- /dev/null +++ b/src/utils/commandTracking.ts @@ -0,0 +1,103 @@ +interface CommandData { + count: number; + lastUsed: string; // ISO timestamp + routePath?: string; + type: "route" | "content"; +} + +interface CommandStats { + commands: { + [key: string]: CommandData; + }; + version: number; +} + +const STORAGE_KEY = "commandPalette_stats"; +const CURRENT_VERSION = 1; + +function getStats(): CommandStats { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (!stored) { + return { commands: {}, version: CURRENT_VERSION }; + } + const parsed = JSON.parse(stored) as CommandStats; + return parsed; + } catch (error) { + console.error("Failed to parse command stats:", error); + return { commands: {}, version: CURRENT_VERSION }; + } +} + +function saveStats(stats: CommandStats): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(stats)); + } catch (error) { + console.error("Failed to save command stats:", error); + } +} + +export function trackCommand( + id: string, + type: "route" | "content", + metadata?: { routePath?: string } +): void { + const stats = getStats(); + + if (!stats.commands[id]) { + stats.commands[id] = { + count: 0, + lastUsed: new Date().toISOString(), + type, + routePath: metadata?.routePath + }; + } + + stats.commands[id].count++; + stats.commands[id].lastUsed = new Date().toISOString(); + + if (metadata?.routePath) { + stats.commands[id].routePath = metadata.routePath; + } + + saveStats(stats); +} + +export function getCommandScore(commandId: string): number { + const stats = getStats(); + const command = stats.commands[commandId]; + + if (!command) return 0; + + const now = new Date().getTime(); + const lastUsed = new Date(command.lastUsed).getTime(); + const daysSinceLastUse = (now - lastUsed) / (1000 * 60 * 60 * 24); + + // Recency bonus: 10 points for today, decreasing to 0 after 10 days + const recencyBonus = Math.max(0, 10 - daysSinceLastUse); + + // Combined score: 70% frequency, 30% recency + return command.count * 0.7 + recencyBonus * 0.3; +} + +export function getTopCommands( + limit = 10 +): Array<{ id: string; score: number }> { + const stats = getStats(); + + const scored = Object.keys(stats.commands).map(id => ({ + id, + score: getCommandScore(id) + })); + + return scored.sort((a, b) => b.score - a.score).slice(0, limit); +} + +export function clearCommandHistory(): void { + localStorage.removeItem(STORAGE_KEY); +} + +export function getCommandStats(commandId: string): CommandData | null { + const stats = getStats(); + return stats.commands[commandId] || null; +}