Skip to content

Commit c642509

Browse files
committed
feat: add settings functionality
1 parent 59b4616 commit c642509

File tree

15 files changed

+2153
-940
lines changed

15 files changed

+2153
-940
lines changed

app/components/header.vue

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,47 @@
11
<script setup lang="ts">
2+
import { useRoute } from 'vue-router'
23
import Theme from '~/components/theme.vue'
4+
5+
const route = useRoute()
6+
const navLinks = [
7+
{ label: 'Tasks', to: '/tasks' },
8+
{ label: 'Settings', to: '/settings' }
9+
]
10+
11+
const isActiveRoute = (path: string) => {
12+
if (path === '/') {
13+
return route.path === '/'
14+
}
15+
16+
return route.path === path || route.path.startsWith(`${path}/`)
17+
}
318
</script>
419
<template>
520
<header class="h-14 text-foreground border-b-2 flex">
6-
<div class="container m-auto flex justify-between">
7-
<NuxtLink
8-
class="no-underline"
9-
to="/"
10-
><h3 class="text-2xl font-bold">Taskiq Admin</h3></NuxtLink
11-
>
21+
<div class="container m-auto flex justify-between items-center">
22+
<div class="flex items-center gap-8">
23+
<NuxtLink
24+
class="no-underline"
25+
to="/"
26+
><h3 class="text-2xl font-bold">Taskiq Admin</h3></NuxtLink
27+
>
28+
<nav class="flex gap-4 text-sm">
29+
<NuxtLink
30+
v-for="link in navLinks"
31+
:key="link.to"
32+
class="tracking-wide"
33+
:class="[
34+
'pb-1 transition-colors no-underline',
35+
isActiveRoute(link.to)
36+
? 'text-foreground border-primary'
37+
: 'text-muted-foreground hover:text-foreground'
38+
]"
39+
:to="link.to"
40+
>
41+
{{ link.label }}
42+
</NuxtLink>
43+
</nav>
44+
</div>
1245
<Theme />
1346
</div>
1447
</header>

app/pages/settings/index.vue

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<script setup lang="ts">
2+
import { computed, ref, watchEffect } from 'vue'
3+
import { useAsyncData } from '#app'
4+
import { toast } from 'vue-sonner'
5+
import { Button } from '~/components/ui/button'
6+
import {
7+
Card,
8+
CardContent,
9+
CardDescription,
10+
CardFooter,
11+
CardHeader,
12+
CardTitle
13+
} from '~/components/ui/card'
14+
import { Input } from '~/components/ui/input'
15+
import { SETTINGS } from '~~/shared/constants/settings'
16+
import type { SettingsSchema } from '~~/shared/schemas/settings'
17+
18+
const DELETE_OLD_TTL = SETTINGS.delete_old_ttl_minutes
19+
const TTL_MINUTES_MIN = DELETE_OLD_TTL.min
20+
const TTL_MINUTES_MAX = DELETE_OLD_TTL.max
21+
22+
const saving = ref(false)
23+
const { data, pending, refresh } = useAsyncData<SettingsSchema>(
24+
'settings',
25+
() => $fetch('/api/settings')
26+
)
27+
28+
const ttlInput = computed({
29+
get: () => data.value?.delete_old_ttl_minutes ?? '',
30+
set: (raw: string) => {
31+
const parsed = parseInt(raw, 10)
32+
if (isNaN(parsed)) {
33+
data.value!.delete_old_ttl_minutes = null
34+
} else {
35+
data.value!.delete_old_ttl_minutes = parsed
36+
}
37+
}
38+
})
39+
40+
const hasValidationError = ref(false)
41+
42+
async function handleSubmit() {
43+
try {
44+
saving.value = true
45+
const response = await $fetch('/api/settings', {
46+
method: 'PUT',
47+
body: data.value
48+
})
49+
saving.value = false
50+
} catch (error) {
51+
saving.value = false
52+
toast.error('Failed to save settings. Please try again.')
53+
return
54+
}
55+
56+
toast.success('Settings saved successfully!')
57+
await refresh()
58+
}
59+
</script>
60+
61+
<template>
62+
<div class="container py-6">
63+
<Card class="max-w-xl">
64+
<form @submit.prevent="handleSubmit">
65+
<CardHeader>
66+
<CardTitle>Settings</CardTitle>
67+
<CardDescription>
68+
Control how long task metadata should be retained before the
69+
automated cleanup runs the delete-old job.
70+
</CardDescription>
71+
</CardHeader>
72+
<CardContent
73+
v-if="data"
74+
class="flex flex-col gap-4"
75+
>
76+
<label class="flex flex-col gap-2">
77+
<span class="text-sm font-medium text-foreground"
78+
>TTL (minutes)</span
79+
>
80+
<Input
81+
type="number"
82+
v-model="ttlInput"
83+
:disabled="pending"
84+
placeholder="Enter number of minutes"
85+
/>
86+
<span class="text-xs text-muted-foreground">
87+
Minimum {{ TTL_MINUTES_MIN }} minute. Maximum
88+
{{ TTL_MINUTES_MAX }} minutes (~1 year). Leave this field empty to
89+
disable automatic cleanup.
90+
</span>
91+
</label>
92+
<p
93+
v-if="hasValidationError"
94+
class="text-xs text-red-500"
95+
>
96+
Value must be between {{ TTL_MINUTES_MIN }} and
97+
{{ TTL_MINUTES_MAX }}.
98+
</p>
99+
</CardContent>
100+
<CardFooter class="flex justify-end">
101+
<Button
102+
type="submit"
103+
:disabled="saving"
104+
class="cursor-pointer"
105+
>
106+
Save
107+
</Button>
108+
</CardFooter>
109+
</form>
110+
</Card>
111+
</div>
112+
</template>

nuxt.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,13 @@ export default defineNuxtConfig({
1414
typescript: {
1515
strict: true
1616
},
17+
nitro: {
18+
experimental: {
19+
tasks: true
20+
},
21+
scheduledTasks: {
22+
'* * * * *': ['settings-dispatcher']
23+
}
24+
},
1725
modules: ['@nuxt/fonts']
1826
})

package.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "taskiq-admin",
33
"private": true,
44
"type": "module",
5-
"version": "1.8.1",
5+
"version": "1.9.0",
66
"scripts": {
77
"build": "nuxt build",
88
"dev": "nuxt dev",
@@ -16,36 +16,36 @@
1616
"generate:deprecated:sql": "drizzle-kit export --sql | sed 's/CREATE TABLE/CREATE TABLE IF NOT EXISTS/g; s/CREATE INDEX/CREATE INDEX IF NOT EXISTS/g' > dbschema.sql"
1717
},
1818
"dependencies": {
19-
"@internationalized/date": "^3.10.0",
19+
"@internationalized/date": "^3.10.1",
2020
"@nuxt/fonts": "0.12.1",
2121
"@tailwindcss/vite": "^4.1.17",
2222
"@tanstack/vue-table": "^8.21.3",
2323
"@vueuse/core": "^14.1.0",
24-
"better-sqlite3": "^12.5.0",
24+
"better-sqlite3": "^12.6.2",
2525
"class-variance-authority": "^0.7.1",
2626
"clsx": "^2.1.1",
2727
"dayjs": "^1.11.19",
2828
"dotenv": "^17.2.3",
29-
"drizzle-orm": "^0.44.7",
29+
"drizzle-orm": "^0.45.1",
3030
"lucide-vue-next": "^0.524.0",
31-
"nuxt": "^4.2.1",
31+
"nuxt": "^4.2.2",
3232
"reka-ui": "^2.6.1",
3333
"tailwind-merge": "^3.4.0",
3434
"tailwindcss": "^4.1.17",
3535
"tw-animate-css": "^1.4.0",
36-
"vue": "^3.5.17",
37-
"vue-router": "^4.5.1",
36+
"vue": "^3.5.27",
37+
"vue-router": "^4.6.4",
3838
"vue-sonner": "^2.0.9",
39-
"zod": "^4.1.13"
39+
"zod": "^4.3.5"
4040
},
4141
"packageManager": "pnpm@8.7.6+sha1.a428b12202bc4f23b17e6dffe730734dae5728e2",
4242
"devDependencies": {
4343
"@iconify-json/radix-icons": "^1.2.5",
4444
"@iconify/vue": "^5.0.0",
45-
"@nuxt/test-utils": "^3.20.1",
45+
"@nuxt/test-utils": "^3.23.0",
4646
"@types/better-sqlite3": "^7.6.12",
4747
"@vue/test-utils": "^2.4.6",
48-
"drizzle-kit": "^0.31.7",
48+
"drizzle-kit": "^0.31.8",
4949
"happy-dom": "^18.0.1",
5050
"playwright-core": "^1.57.0",
5151
"prettier": "^3.7.3",

0 commit comments

Comments
 (0)