Compare commits

...

5 Commits

Author SHA1 Message Date
90a81f353d chore: fix eslint and make nuxt typecheck happpy
All checks were successful
ci / ci (22, ubuntu-latest) (push) Successful in 9m44s
Build and Push Docker Image / build (push) Successful in 1m29s
2026-03-25 21:42:20 +01:00
8f4be48fd1 feat: page titles and favicon 2026-03-25 21:39:00 +01:00
ffba95a411 feat: ui tweaks 2026-03-25 21:31:39 +01:00
5d5d691bab feat: improve theme 2026-03-25 21:12:03 +01:00
b763eb70db feat: custom error page 2026-03-25 20:54:58 +01:00
14 changed files with 121 additions and 19 deletions

10
app/app.config.ts Normal file
View File

@@ -0,0 +1,10 @@
export default defineAppConfig({
ui: {
colors: {
primary: "neutral",
neutral: "zinc",
success: "emerald",
error: "rose",
}
}
})

View File

@@ -1,2 +1,6 @@
@import "tailwindcss"; @import "tailwindcss";
@import "@nuxt/ui"; @import "@nuxt/ui";
:root {
--ui-radius: 0rem;
}

View File

@@ -15,7 +15,7 @@ const emit = defineEmits<{ close: [confirmed: boolean] }>();
<template #footer> <template #footer>
<div class="flex gap-2 justify-end w-full"> <div class="flex gap-2 justify-end w-full">
<UButton variant="ghost" color="neutral" @click="emit('close', false)">Cancel</UButton> <UButton variant="ghost" color="neutral" @click="emit('close', false)">Cancel</UButton>
<UButton color="error" @click="emit('close', true)">Delete</UButton> <UButton color="error" icon="i-lucide-trash-2" @click="emit('close', true)">Delete</UButton>
</div> </div>
</template> </template>
</UModal> </UModal>

View File

@@ -73,7 +73,7 @@ const onSubmit = async (event: FormSubmitEvent<Schema>) => {
<div class="flex justify-end gap-2 pt-2"> <div class="flex justify-end gap-2 pt-2">
<UButton variant="ghost" @click="emit('close', false)">Cancel</UButton> <UButton variant="ghost" @click="emit('close', false)">Cancel</UButton>
<UButton type="submit" :loading="submitting">{{ link ? "Save" : "Create" }}</UButton> <UButton type="submit" variant="subtle" :loading="submitting" :icon="link ? 'i-lucide-save' : 'i-lucide-plus'">{{ link ? "Save" : "Create" }}</UButton>
</div> </div>
</UForm> </UForm>
</template> </template>

20
app/error.vue Normal file
View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { NuxtError } from "#app"
defineProps<{ error: NuxtError }>();
useHead({ title: 'Not found' });
</script>
<template>
<UApp>
<div class="min-h-screen flex items-center justify-center">
<div class="text-center space-y-2">
<p class="text-8xl font-black tracking-tight">404</p>
<p class="text-lg font-medium">This link doesn't exist.</p>
<p class="text-muted text-sm">
Maybe check out <a href="https://pihkaal.me" class="underline underline-offset-2 hover:text-default transition-colors">pihkaal.me</a> instead?
</p>
</div>
</div>
</UApp>
</template>

View File

@@ -2,6 +2,8 @@
import { z } from "zod"; import { z } from "zod";
import type { AuthFormField, FormSubmitEvent } from "@nuxt/ui"; import type { AuthFormField, FormSubmitEvent } from "@nuxt/ui";
useHead({ title: 'Sign in' });
definePageMeta({ definePageMeta({
middleware: () => { middleware: () => {
const { loggedIn } = useUserSession(); const { loggedIn } = useUserSession();

View File

@@ -23,6 +23,13 @@ const category = computed(() => {
return "all"; return "all";
}); });
const categoryTitle = computed(() => {
if (category.value === "active") return "Active links";
if (category.value === "disabled") return "Disabled links";
return "All links";
});
useHead({ title: categoryTitle });
const filteredLinks = computed(() => { const filteredLinks = computed(() => {
if (!links.value) return []; if (!links.value) return [];
if (category.value === "active") return links.value.filter((l) => !l.disabled); if (category.value === "active") return links.value.filter((l) => !l.disabled);
@@ -54,9 +61,9 @@ const deleteLink = async (link: Link) => {
<template> <template>
<UDashboardGroup> <UDashboardGroup>
<UDashboardSidebar :toggle="false"> <UDashboardSidebar :toggle="false" :ui="{ header: 'border-b border-default' }">
<template #header> <template #header>
<UDashboardNavbar title="pihka.al" :toggle="false" /> <span class="font-semibold text-highlighted text-lg">pihka.al</span>
</template> </template>
<UNavigationMenu <UNavigationMenu
@@ -88,9 +95,7 @@ const deleteLink = async (link: Link) => {
class="px-2" class="px-2"
/> />
<template #footer> <template #footer>
<UButton variant="ghost" icon="i-lucide-log-out" block @click="signOut"> <UButton variant="link" icon="i-lucide-log-out" block @click="signOut">Sign out</UButton>
Sign out
</UButton>
</template> </template>
</UDashboardSidebar> </UDashboardSidebar>
@@ -98,9 +103,7 @@ const deleteLink = async (link: Link) => {
<template #header> <template #header>
<UDashboardNavbar :title="category === 'all' ? 'All links' : category === 'active' ? 'Active links' : 'Disabled links'"> <UDashboardNavbar :title="category === 'all' ? 'All links' : category === 'active' ? 'Active links' : 'Disabled links'">
<template #right> <template #right>
<UButton icon="i-lucide-plus" @click="openModal(null)"> <UButton icon="i-lucide-plus" variant="subtle" @click="openModal(null)">New link</UButton>
New link
</UButton>
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
</template> </template>

View File

@@ -0,0 +1,6 @@
export default defineNuxtPlugin(() => {
const event = useRequestEvent()
if (event?.context.redirectNotFound) {
showError({ statusCode: 404, message: 'Link not found' })
}
})

View File

@@ -1,4 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import type { InternalApi } from "nitropack/types";
export type Link = InternalApi["/api/links"]["get"][number];
const apiErrorSchema = z.object({ const apiErrorSchema = z.object({
data: z.object({ data: z.object({

View File

@@ -10,6 +10,13 @@ export default defineNuxtConfig({
} }
}, },
app: {
head: {
titleTemplate: '%s · pihka.al',
link: [{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }],
},
},
devtools: { devtools: {
enabled: true enabled: true
}, },

51
public/favicon.svg Normal file
View File

@@ -0,0 +1,51 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 18">
<style>
.bg { fill: #000000; }
.fg { fill: #ffffff; }
</style>
<rect x="0" y="11" width="5" height="5" class="bg" />
<rect x="1" y="10" width="7" height="5" class="bg" />
<rect x="6" y="10" width="22" height="6" class="bg" />
<rect x="9" y="5" width="18" height="12" class="bg" />
<rect x="10" y="16" width="16" height="2" class="bg" />
<rect x="7" y="7" width="16" height="4" class="bg" />
<rect x="8" y="6" width="2" height="2" class="bg" />
<rect x="26" y="9" width="2" height="2" class="bg" />
<rect x="11" y="4" width="16" height="2" class="bg" />
<rect x="12" y="3" width="15" height="2" class="bg" />
<rect x="13" y="2" width="6" height="2" class="bg" />
<rect x="14" y="1" width="5" height="2" class="bg" />
<rect x="15" y="0" width="3" height="2" class="bg" />
<rect x="21" y="2" width="6" height="2" class="bg" />
<rect x="22" y="1" width="5" height="2" class="bg" />
<rect x="23" y="0" width="3" height="2" class="bg" />
<rect x="2" y="11" width="5" height="3" class="fg" />
<rect x="1" y="12" width="3" height="3" class="fg" />
<rect x="10" y="6" width="16" height="10" class="fg" />
<rect x="8" y="10" width="19" height="5" class="fg" />
<rect x="7" y="12" width="2" height="3" class="fg" />
<rect x="6" y="12" width="2" height="2" class="fg" />
<rect x="8" y="8" width="18" height="3" class="fg" />
<rect x="9" y="7" width="2" height="2" class="fg" />
<rect x="12" y="15" width="13" height="2" class="fg" />
<rect x="25" y="10" width="2" height="5" class="fg" />
<rect x="15" y="11" width="3" height="1" class="bg" />
<rect x="22" y="11" width="3" height="1" class="bg" />
<rect x="12" y="5" width="14" height="2" class="fg" />
<rect x="13" y="4" width="6" height="2" class="fg" />
<rect x="14" y="3" width="4" height="2" class="fg" />
<rect x="15" y="2" width="3" height="2" class="fg" />
<rect x="16" y="1" width="1" height="2" class="fg" />
<rect x="21" y="4" width="5" height="2" class="fg" />
<rect x="22" y="3" width="4" height="2" class="fg" />
<rect x="23" y="2" width="3" height="2" class="fg" />
<rect x="24" y="1" width="1" height="2" class="fg" />
<rect x="16" y="3" width="1" height="2" class="bg" />
<rect x="15" y="4" width="1" height="2" class="bg" />
<rect x="14" y="5" width="2" height="1" class="bg" />
<rect x="15" y="4" width="2" height="1" class="bg" />
<rect x="24" y="3" width="1" height="3" class="bg" />
<rect x="23" y="4" width="2" height="1" class="bg" />
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -19,12 +19,9 @@ export default defineEventHandler(async (event) => {
where: eq(tables.links.path, path), where: eq(tables.links.path, path),
}); });
if (!link) { if (!link || link.disabled) {
throw createError({ statusCode: 404, message: "Not found" }); event.context.redirectNotFound = true;
} return;
if (link.disabled) {
throw createError({ statusCode: 410, message: "This link has been disabled" });
} }
return sendRedirect(event, link.url, 302); return sendRedirect(event, link.url, 302);

View File

@@ -1,3 +0,0 @@
import type { InternalApi } from "nitropack/types";
export type Link = InternalApi["/api/links"]["get"][number];