Merge branch 'search' into dev

This commit is contained in:
ccbikai 2024-12-23 20:57:24 +08:00
commit b520d4006c
22 changed files with 698 additions and 15 deletions

View file

@ -101,6 +101,7 @@ We welcome your contributions and PRs.
- Bind the variable name `ANALYTICS` to the `sink` dataset.
7. Redeploy the project.
8. Update code, refer to the official GitHub documentation [Syncing a fork branch from the web UI](https://docs.github.com/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork#syncing-a-fork-branch-from-the-web-ui).
## ⚒️ Configuration

View file

@ -3,7 +3,7 @@ const route = useRoute()
</script>
<template>
<nav class="flex justify-between">
<section class="flex justify-between">
<Tabs
v-if="route.path !== '/dashboard/link'"
:default-value="route.path"
@ -24,5 +24,5 @@ const route = useRoute()
<div>
<slot />
</div>
</nav>
</section>
</template>

View file

@ -102,12 +102,10 @@ onMounted(() => {
}
})
const { caseSensitive } = useRuntimeConfig()
async function onSubmit(formData) {
const link = {
url: formData.url,
slug: caseSensitive ? formData.slug : formData.slug.toLowerCase(),
slug: formData.slug,
...(formData.optional || []),
expiration: formData.optional?.expiration ? date2unix(formData.optional?.expiration, 'end') : undefined,
}

View file

@ -42,9 +42,12 @@ function updateLinkList(link, type) {
<template>
<main class="space-y-6">
<DashboardNav>
<DashboardLinksEditor @update:link="updateLinkList" />
</DashboardNav>
<div class="flex flex-col gap-6 sm:gap-2 sm:flex-row sm:justify-between">
<DashboardNav class="flex-1">
<DashboardLinksEditor @update:link="updateLinkList" />
</DashboardNav>
<DashboardLinksSearch />
</div>
<section class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<DashboardLinksLink
v-for="link in links"

View file

@ -0,0 +1,88 @@
<script setup>
import { useMagicKeys } from '@vueuse/core'
import { useFuse } from '@vueuse/integrations/useFuse'
const router = useRouter()
const isOpen = ref(false)
const searchTerm = ref('')
const selectedLink = ref(null)
const links = await useAPI('/api/link/search')
const { results: filteredLinks } = useFuse(searchTerm, links, {
fuseOptions: {
keys: ['slug', 'url', 'comment'],
},
resultLimit: 20,
})
const { Meta_K, Ctrl_K } = useMagicKeys({
passive: false,
onEventFired(e) {
if (e.key === 'k' && (e.metaKey || e.ctrlKey))
e.preventDefault()
},
})
watch([Meta_K, Ctrl_K], (v) => {
if (v[0] || v[1])
isOpen.value = true
})
function selectLink(link) {
isOpen.value = false
router.push({
path: '/dashboard/link',
query: { slug: link.slug },
})
}
</script>
<template>
<div>
<Button
variant="outline"
size="sm"
class="relative h-10 w-full justify-start bg-background text-muted-foreground sm:w-32 md:w-48"
@click="isOpen = true"
>
<span class="hidden md:inline-flex">Search Links...</span>
<span class="inline-flex md:hidden">Search</span>
<kbd class="pointer-events-none absolute right-[0.3rem] top-[0.6rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
<span class="text-xs"></span>K
</kbd>
</Button>
<Dialog :open="isOpen" @update:open="isOpen = !isOpen">
<DialogContent class="overflow-hidden p-0 shadow-lg">
<Command v-model:searchTerm="searchTerm" v-model="selectedLink" class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<CommandInput placeholder="Type to search..." />
<CommandList>
<CommandEmpty v-if="searchTerm">
No links found.
</CommandEmpty>
<CommandGroup heading="Links">
<CommandItem v-for="link in filteredLinks" :key="link.item?.id" class="cursor-pointer" :value="link.item" @select="selectLink(link.item)">
<div class="flex gap-1 w-full">
<div class="flex-1 overflow-hidden inline-flex gap-1 items-center">
<div class="text-sm font-medium">
{{ link.item?.slug }}
</div>
<div class="text-xs text-muted-foreground flex-1 truncate">
({{ link.item?.url }})
</div>
</div>
<Badge v-if="link.item?.comment" variant="secondary">
<div class="w-24 truncate">
{{ link.item?.comment }}
</div>
</Badge>
</div>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</DialogContent>
</Dialog>
</div>
</template>

View file

@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
import { type BadgeVariants, badgeVariants } from '.'
const props = defineProps<{
variant?: BadgeVariants['variant']
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn(badgeVariants({ variant }), props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,25 @@
import { cva, type VariantProps } from 'class-variance-authority'
export { default as Badge } from './Badge.vue'
export const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
)
export type BadgeVariants = VariantProps<typeof badgeVariants>

View file

@ -0,0 +1,30 @@
<script setup lang="ts">
import type { ComboboxRootEmits, ComboboxRootProps } from 'radix-vue'
import { cn } from '@/utils'
import { ComboboxRoot, useForwardPropsEmits } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = withDefaults(defineProps<ComboboxRootProps & { class?: HTMLAttributes['class'] }>(), {
open: true,
modelValue: '',
})
const emits = defineEmits<ComboboxRootEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<ComboboxRoot
v-bind="forwarded"
:class="cn('flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground', props.class)"
>
<slot />
</ComboboxRoot>
</template>

View file

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from 'radix-vue'
import { Dialog, DialogContent } from '@/components/ui/dialog'
import { useForwardPropsEmits } from 'radix-vue'
import Command from './Command.vue'
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<Dialog v-bind="forwarded">
<DialogContent class="overflow-hidden p-0 shadow-lg">
<Command class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<slot />
</Command>
</DialogContent>
</Dialog>
</template>

View file

@ -0,0 +1,20 @@
<script setup lang="ts">
import type { ComboboxEmptyProps } from 'radix-vue'
import { cn } from '@/utils'
import { ComboboxEmpty } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<ComboboxEmptyProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ComboboxEmpty v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)">
<slot />
</ComboboxEmpty>
</template>

View file

@ -0,0 +1,29 @@
<script setup lang="ts">
import type { ComboboxGroupProps } from 'radix-vue'
import { cn } from '@/utils'
import { ComboboxGroup, ComboboxLabel } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<ComboboxGroupProps & {
class?: HTMLAttributes['class']
heading?: string
}>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ComboboxGroup
v-bind="delegatedProps"
:class="cn('overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground', props.class)"
>
<ComboboxLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
{{ heading }}
</ComboboxLabel>
<slot />
</ComboboxGroup>
</template>

View file

@ -0,0 +1,33 @@
<script setup lang="ts">
import { cn } from '@/utils'
import { Search } from 'lucide-vue-next'
import { ComboboxInput, type ComboboxInputProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
defineOptions({
inheritAttrs: false,
})
const props = defineProps<ComboboxInputProps & {
class?: HTMLAttributes['class']
}>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<div class="flex items-center border-b px-3" cmdk-input-wrapper>
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
<ComboboxInput
v-bind="{ ...forwardedProps, ...$attrs }"
auto-focus
:class="cn('flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50', props.class)"
/>
</div>
</template>

View file

@ -0,0 +1,26 @@
<script setup lang="ts">
import type { ComboboxItemEmits, ComboboxItemProps } from 'radix-vue'
import { cn } from '@/utils'
import { ComboboxItem, useForwardPropsEmits } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<ComboboxItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<ComboboxItemEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<ComboboxItem
v-bind="forwarded"
:class="cn('relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', props.class)"
>
<slot />
</ComboboxItem>
</template>

View file

@ -0,0 +1,27 @@
<script setup lang="ts">
import type { ComboboxContentEmits, ComboboxContentProps } from 'radix-vue'
import { cn } from '@/utils'
import { ComboboxContent, useForwardPropsEmits } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = withDefaults(defineProps<ComboboxContentProps & { class?: HTMLAttributes['class'] }>(), {
dismissable: false,
})
const emits = defineEmits<ComboboxContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<ComboboxContent v-bind="forwarded" :class="cn('max-h-[300px] overflow-y-auto overflow-x-hidden', props.class)">
<div role="presentation">
<slot />
</div>
</ComboboxContent>
</template>

View file

@ -0,0 +1,23 @@
<script setup lang="ts">
import type { ComboboxSeparatorProps } from 'radix-vue'
import { cn } from '@/utils'
import { ComboboxSeparator } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<ComboboxSeparatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ComboboxSeparator
v-bind="delegatedProps"
:class="cn('-mx-1 h-px bg-border', props.class)"
>
<slot />
</ComboboxSeparator>
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<span :class="cn('ml-auto text-xs tracking-widest text-muted-foreground', props.class)">
<slot />
</span>
</template>

View file

@ -0,0 +1,9 @@
export { default as Command } from './Command.vue'
export { default as CommandDialog } from './CommandDialog.vue'
export { default as CommandEmpty } from './CommandEmpty.vue'
export { default as CommandGroup } from './CommandGroup.vue'
export { default as CommandInput } from './CommandInput.vue'
export { default as CommandItem } from './CommandItem.vue'
export { default as CommandList } from './CommandList.vue'
export { default as CommandSeparator } from './CommandSeparator.vue'
export { default as CommandShortcut } from './CommandShortcut.vue'

View file

@ -1,7 +1,7 @@
{
"name": "sink",
"type": "module",
"version": "0.1.5",
"version": "0.1.6",
"private": true,
"packageManager": "pnpm@9.7.1",
"engines": {
@ -25,6 +25,7 @@
"@unovis/vue": "^1.4.4",
"@vee-validate/zod": "^4.13.2",
"@vueuse/core": "^11.0.0",
"fuse.js": "^7.0.0",
"intl-parse-accept-language": "^1.0.0",
"lucide-vue-next": "^0.428.0",
"mysql-bricks": "^1.1.2",
@ -46,6 +47,7 @@
"@nuxthub/core": "^0.7.3",
"@nuxtjs/color-mode": "^3.4.4",
"@nuxtjs/tailwindcss": "^6.12.1",
"@vueuse/integrations": "^12.2.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"eslint": "^9.9.0",

View file

@ -20,6 +20,9 @@ importers:
'@vueuse/core':
specifier: ^11.0.0
version: 11.0.0(vue@3.4.38(typescript@5.4.5))
fuse.js:
specifier: ^7.0.0
version: 7.0.0
intl-parse-accept-language:
specifier: ^1.0.0
version: 1.0.0
@ -62,7 +65,7 @@ importers:
devDependencies:
'@antfu/eslint-config':
specifier: ^2.26.0
version: 2.26.0(@typescript-eslint/utils@8.1.0(eslint@9.9.0(jiti@1.21.6))(typescript@5.4.5))(@vue/compiler-sfc@3.4.38)(eslint@9.9.0(jiti@1.21.6))(typescript@5.4.5)
version: 2.26.0(@typescript-eslint/utils@8.1.0(eslint@9.9.0(jiti@1.21.6))(typescript@5.4.5))(@vue/compiler-sfc@3.5.13)(eslint@9.9.0(jiti@1.21.6))(typescript@5.4.5)
'@nuxt/eslint':
specifier: ^0.5.0
version: 0.5.0(eslint@9.9.0(jiti@1.21.6))(magicast@0.3.4)(rollup@4.18.0)(typescript@5.4.5)(vite@5.4.1(@types/node@20.12.11)(terser@5.31.0))
@ -78,6 +81,9 @@ importers:
'@nuxtjs/tailwindcss':
specifier: ^6.12.1
version: 6.12.1(magicast@0.3.4)(rollup@4.18.0)
'@vueuse/integrations':
specifier: ^12.2.0
version: 12.2.0(fuse.js@7.0.0)(typescript@5.4.5)
class-variance-authority:
specifier: ^0.7.0
version: 0.7.0
@ -268,10 +274,18 @@ packages:
resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.25.9':
resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.24.7':
resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.25.9':
resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.24.7':
resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==}
engines: {node: '>=6.9.0'}
@ -289,6 +303,11 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/parser@7.26.3':
resolution: {integrity: sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/plugin-proposal-decorators@7.24.1':
resolution: {integrity: sha512-zPEvzFijn+hRvJuX2Vu3KbEBN39LN3f7tW3MQO2LsIs57B26KU+kUc82BdAktS1VCM6libzh45eKGI65lg0cpA==}
engines: {node: '>=6.9.0'}
@ -350,6 +369,10 @@ packages:
resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==}
engines: {node: '>=6.9.0'}
'@babel/types@7.26.3':
resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==}
engines: {node: '>=6.9.0'}
'@clack/core@0.3.4':
resolution: {integrity: sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw==}
@ -1974,15 +1997,27 @@ packages:
'@vue/compiler-core@3.4.38':
resolution: {integrity: sha512-8IQOTCWnLFqfHzOGm9+P8OPSEDukgg3Huc92qSG49if/xI2SAwLHQO2qaPQbjCWPBcQoO1WYfXfTACUrWV3c5A==}
'@vue/compiler-core@3.5.13':
resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
'@vue/compiler-dom@3.4.38':
resolution: {integrity: sha512-Osc/c7ABsHXTsETLgykcOwIxFktHfGSUDkb05V61rocEfsFDcjDLH/IHJSNJP+/Sv9KeN2Lx1V6McZzlSb9EhQ==}
'@vue/compiler-dom@3.5.13':
resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==}
'@vue/compiler-sfc@3.4.38':
resolution: {integrity: sha512-s5QfZ+9PzPh3T5H4hsQDJtI8x7zdJaew/dCGgqZ2630XdzaZ3AD8xGZfBqpT8oaD/p2eedd+pL8tD5vvt5ZYJQ==}
'@vue/compiler-sfc@3.5.13':
resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==}
'@vue/compiler-ssr@3.4.38':
resolution: {integrity: sha512-YXznKFQ8dxYpAz9zLuVvfcXhc31FSPFDcqr0kyujbOwNhlmaNvL2QfIy+RZeJgSn5Fk54CWoEUeW+NVBAogGaw==}
'@vue/compiler-ssr@3.5.13':
resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==}
'@vue/compiler-vue2@2.7.16':
resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
@ -2009,38 +2044,105 @@ packages:
'@vue/reactivity@3.4.38':
resolution: {integrity: sha512-4vl4wMMVniLsSYYeldAKzbk72+D3hUnkw9z8lDeJacTxAkXeDAP1uE9xr2+aKIN0ipOL8EG2GPouVTH6yF7Gnw==}
'@vue/reactivity@3.5.13':
resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==}
'@vue/runtime-core@3.4.38':
resolution: {integrity: sha512-21z3wA99EABtuf+O3IhdxP0iHgkBs1vuoCAsCKLVJPEjpVqvblwBnTj42vzHRlWDCyxu9ptDm7sI2ZMcWrQqlA==}
'@vue/runtime-core@3.5.13':
resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==}
'@vue/runtime-dom@3.4.38':
resolution: {integrity: sha512-afZzmUreU7vKwKsV17H1NDThEEmdYI+GCAK/KY1U957Ig2NATPVjCROv61R19fjZNzMmiU03n79OMnXyJVN0UA==}
'@vue/runtime-dom@3.5.13':
resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==}
'@vue/server-renderer@3.4.38':
resolution: {integrity: sha512-NggOTr82FbPEkkUvBm4fTGcwUY8UuTsnWC/L2YZBmvaQ4C4Jl/Ao4HHTB+l7WnFCt5M/dN3l0XLuyjzswGYVCA==}
peerDependencies:
vue: 3.4.38
'@vue/server-renderer@3.5.13':
resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==}
peerDependencies:
vue: 3.5.13
'@vue/shared@3.4.38':
resolution: {integrity: sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw==}
'@vue/shared@3.5.13':
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
'@vueuse/core@10.11.1':
resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
'@vueuse/core@11.0.0':
resolution: {integrity: sha512-shibzNGjmRjZucEm97B8V0NO5J3vPHMCE/mltxQ3vHezbDoFQBMtK11XsfwfPionxSbo+buqPmsCljtYuXIBpw==}
'@vueuse/core@12.2.0':
resolution: {integrity: sha512-jksyNu+5EGwggNkRWd6xX+8qBkYbmrwdFQMgCABsz+wq8bKF6w3soPFLB8vocFp3wFIzn0OYkSPM9JP+AFKwsg==}
'@vueuse/integrations@12.2.0':
resolution: {integrity: sha512-Bc0unXiGNZ0w7xqSvzCuP7AflBRKcZX6ib7tGi7vAjOxhLd6GtN3J8qizIbSPnI62XyPy5fauOkq2i2HUPq6MQ==}
peerDependencies:
async-validator: ^4
axios: ^1
change-case: ^5
drauu: ^0.4
focus-trap: ^7
fuse.js: ^7
idb-keyval: ^6
jwt-decode: ^4
nprogress: ^0.2
qrcode: ^1.5
sortablejs: ^1
universal-cookie: ^7
peerDependenciesMeta:
async-validator:
optional: true
axios:
optional: true
change-case:
optional: true
drauu:
optional: true
focus-trap:
optional: true
fuse.js:
optional: true
idb-keyval:
optional: true
jwt-decode:
optional: true
nprogress:
optional: true
qrcode:
optional: true
sortablejs:
optional: true
universal-cookie:
optional: true
'@vueuse/metadata@10.11.1':
resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
'@vueuse/metadata@11.0.0':
resolution: {integrity: sha512-0TKsAVT0iUOAPWyc9N79xWYfovJVPATiOPVKByG6jmAYdDiwvMVm9xXJ5hp4I8nZDxpCcYlLq/Rg9w1Z/jrGcg==}
'@vueuse/metadata@12.2.0':
resolution: {integrity: sha512-x6zynZtTh1l52m0y8d/EgzpshnMjg8cNZ2KWoncJ62Z5qPSGoc4FUunmMVrrRM/I/5542rTEY89CGftngZvrkQ==}
'@vueuse/shared@10.11.1':
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
'@vueuse/shared@11.0.0':
resolution: {integrity: sha512-i4ZmOrIEjSsL94uAEt3hz88UCz93fMyP/fba9S+vypX90fKg3uYX9cThqvWc9aXxuTzR0UGhOKOTQd//Goh1nQ==}
'@vueuse/shared@12.2.0':
resolution: {integrity: sha512-SRr4AZwv/giS+EmyA1ZIzn3/iALjjnWAGaBNmoDTMEob9JwQaevAocuaMDnPAvU7Z35Y5g3CFRusCWgp1gVJ3Q==}
abbrev@1.1.1:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
@ -3299,6 +3401,10 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
fuse.js@7.0.0:
resolution: {integrity: sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==}
engines: {node: '>=10'}
gauge@3.0.2:
resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
engines: {node: '>=10'}
@ -4352,6 +4458,9 @@ packages:
picocolors@1.0.1:
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
@ -4593,6 +4702,10 @@ packages:
resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.4.49:
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
engines: {node: ^10 || ^12 || >=14}
potpack@1.0.2:
resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==}
@ -4927,6 +5040,10 @@ packages:
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
engines: {node: '>=0.10.0'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
@ -5578,6 +5695,14 @@ packages:
typescript:
optional: true
vue@3.5.13:
resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@ -5726,7 +5851,7 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
'@antfu/eslint-config@2.26.0(@typescript-eslint/utils@8.1.0(eslint@9.9.0(jiti@1.21.6))(typescript@5.4.5))(@vue/compiler-sfc@3.4.38)(eslint@9.9.0(jiti@1.21.6))(typescript@5.4.5)':
'@antfu/eslint-config@2.26.0(@typescript-eslint/utils@8.1.0(eslint@9.9.0(jiti@1.21.6))(typescript@5.4.5))(@vue/compiler-sfc@3.5.13)(eslint@9.9.0(jiti@1.21.6))(typescript@5.4.5)':
dependencies:
'@antfu/install-pkg': 0.3.5
'@clack/prompts': 0.7.0
@ -5754,7 +5879,7 @@ snapshots:
eslint-plugin-unused-imports: 4.1.3(@typescript-eslint/eslint-plugin@8.1.0(@typescript-eslint/parser@8.1.0(eslint@9.9.0(jiti@1.21.6))(typescript@5.4.5))(eslint@9.9.0(jiti@1.21.6))(typescript@5.4.5))(eslint@9.9.0(jiti@1.21.6))
eslint-plugin-vue: 9.27.0(eslint@9.9.0(jiti@1.21.6))
eslint-plugin-yml: 1.14.0(eslint@9.9.0(jiti@1.21.6))
eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.4.38)(eslint@9.9.0(jiti@1.21.6))
eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.5.13)(eslint@9.9.0(jiti@1.21.6))
globals: 15.9.0
jsonc-eslint-parser: 2.4.0
local-pkg: 0.5.0
@ -5922,8 +6047,12 @@ snapshots:
'@babel/helper-string-parser@7.24.7': {}
'@babel/helper-string-parser@7.25.9': {}
'@babel/helper-validator-identifier@7.24.7': {}
'@babel/helper-validator-identifier@7.25.9': {}
'@babel/helper-validator-option@7.24.7': {}
'@babel/helpers@7.24.7':
@ -5942,6 +6071,10 @@ snapshots:
dependencies:
'@babel/types': 7.24.7
'@babel/parser@7.26.3':
dependencies:
'@babel/types': 7.26.3
'@babel/plugin-proposal-decorators@7.24.1(@babel/core@7.24.7)':
dependencies:
'@babel/core': 7.24.7
@ -6019,6 +6152,11 @@ snapshots:
'@babel/helper-validator-identifier': 7.24.7
to-fast-properties: 2.0.0
'@babel/types@7.26.3':
dependencies:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@clack/core@0.3.4':
dependencies:
picocolors: 1.0.1
@ -7780,11 +7918,24 @@ snapshots:
estree-walker: 2.0.2
source-map-js: 1.2.0
'@vue/compiler-core@3.5.13':
dependencies:
'@babel/parser': 7.26.3
'@vue/shared': 3.5.13
entities: 4.5.0
estree-walker: 2.0.2
source-map-js: 1.2.0
'@vue/compiler-dom@3.4.38':
dependencies:
'@vue/compiler-core': 3.4.38
'@vue/shared': 3.4.38
'@vue/compiler-dom@3.5.13':
dependencies:
'@vue/compiler-core': 3.5.13
'@vue/shared': 3.5.13
'@vue/compiler-sfc@3.4.38':
dependencies:
'@babel/parser': 7.24.7
@ -7797,11 +7948,28 @@ snapshots:
postcss: 8.4.41
source-map-js: 1.2.0
'@vue/compiler-sfc@3.5.13':
dependencies:
'@babel/parser': 7.26.3
'@vue/compiler-core': 3.5.13
'@vue/compiler-dom': 3.5.13
'@vue/compiler-ssr': 3.5.13
'@vue/shared': 3.5.13
estree-walker: 2.0.2
magic-string: 0.30.11
postcss: 8.4.49
source-map-js: 1.2.0
'@vue/compiler-ssr@3.4.38':
dependencies:
'@vue/compiler-dom': 3.4.38
'@vue/shared': 3.4.38
'@vue/compiler-ssr@3.5.13':
dependencies:
'@vue/compiler-dom': 3.5.13
'@vue/shared': 3.5.13
'@vue/compiler-vue2@2.7.16':
dependencies:
de-indent: 1.0.2
@ -7851,11 +8019,20 @@ snapshots:
dependencies:
'@vue/shared': 3.4.38
'@vue/reactivity@3.5.13':
dependencies:
'@vue/shared': 3.5.13
'@vue/runtime-core@3.4.38':
dependencies:
'@vue/reactivity': 3.4.38
'@vue/shared': 3.4.38
'@vue/runtime-core@3.5.13':
dependencies:
'@vue/reactivity': 3.5.13
'@vue/shared': 3.5.13
'@vue/runtime-dom@3.4.38':
dependencies:
'@vue/reactivity': 3.4.38
@ -7863,14 +8040,29 @@ snapshots:
'@vue/shared': 3.4.38
csstype: 3.1.3
'@vue/runtime-dom@3.5.13':
dependencies:
'@vue/reactivity': 3.5.13
'@vue/runtime-core': 3.5.13
'@vue/shared': 3.5.13
csstype: 3.1.3
'@vue/server-renderer@3.4.38(vue@3.4.38(typescript@5.4.5))':
dependencies:
'@vue/compiler-ssr': 3.4.38
'@vue/shared': 3.4.38
vue: 3.4.38(typescript@5.4.5)
'@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.4.5))':
dependencies:
'@vue/compiler-ssr': 3.5.13
'@vue/shared': 3.5.13
vue: 3.5.13(typescript@5.4.5)
'@vue/shared@3.4.38': {}
'@vue/shared@3.5.13': {}
'@vueuse/core@10.11.1(vue@3.4.38(typescript@5.4.5))':
dependencies:
'@types/web-bluetooth': 0.0.20
@ -7891,10 +8083,31 @@ snapshots:
- '@vue/composition-api'
- vue
'@vueuse/core@12.2.0(typescript@5.4.5)':
dependencies:
'@types/web-bluetooth': 0.0.20
'@vueuse/metadata': 12.2.0
'@vueuse/shared': 12.2.0(typescript@5.4.5)
vue: 3.5.13(typescript@5.4.5)
transitivePeerDependencies:
- typescript
'@vueuse/integrations@12.2.0(fuse.js@7.0.0)(typescript@5.4.5)':
dependencies:
'@vueuse/core': 12.2.0(typescript@5.4.5)
'@vueuse/shared': 12.2.0(typescript@5.4.5)
vue: 3.5.13(typescript@5.4.5)
optionalDependencies:
fuse.js: 7.0.0
transitivePeerDependencies:
- typescript
'@vueuse/metadata@10.11.1': {}
'@vueuse/metadata@11.0.0': {}
'@vueuse/metadata@12.2.0': {}
'@vueuse/shared@10.11.1(vue@3.4.38(typescript@5.4.5))':
dependencies:
vue-demi: 0.14.10(vue@3.4.38(typescript@5.4.5))
@ -7909,6 +8122,12 @@ snapshots:
- '@vue/composition-api'
- vue
'@vueuse/shared@12.2.0(typescript@5.4.5)':
dependencies:
vue: 3.5.13(typescript@5.4.5)
transitivePeerDependencies:
- typescript
abbrev@1.1.1: {}
abort-controller@3.0.0:
@ -9078,9 +9297,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-processor-vue-blocks@0.1.2(@vue/compiler-sfc@3.4.38)(eslint@9.9.0(jiti@1.21.6)):
eslint-processor-vue-blocks@0.1.2(@vue/compiler-sfc@3.5.13)(eslint@9.9.0(jiti@1.21.6)):
dependencies:
'@vue/compiler-sfc': 3.4.38
'@vue/compiler-sfc': 3.5.13
eslint: 9.9.0(jiti@1.21.6)
eslint-scope@7.2.2:
@ -9310,6 +9529,8 @@ snapshots:
function-bind@1.1.2: {}
fuse.js@7.0.0: {}
gauge@3.0.2:
dependencies:
aproba: 2.0.0
@ -10569,6 +10790,8 @@ snapshots:
picocolors@1.0.1: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
picomatch@4.0.2: {}
@ -10788,6 +11011,12 @@ snapshots:
picocolors: 1.0.1
source-map-js: 1.2.0
postcss@8.4.49:
dependencies:
nanoid: 3.3.7
picocolors: 1.1.1
source-map-js: 1.2.1
potpack@1.0.2: {}
prelude-ls@1.2.1: {}
@ -11151,6 +11380,8 @@ snapshots:
source-map-js@1.2.0: {}
source-map-js@1.2.1: {}
source-map-support@0.5.21:
dependencies:
buffer-from: 1.1.2
@ -11818,6 +12049,16 @@ snapshots:
optionalDependencies:
typescript: 5.4.5
vue@3.5.13(typescript@5.4.5):
dependencies:
'@vue/compiler-dom': 3.5.13
'@vue/compiler-sfc': 3.5.13
'@vue/runtime-dom': 3.5.13
'@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.4.5))
'@vue/shared': 3.5.13
optionalDependencies:
typescript: 5.4.5
webidl-conversions@3.0.1: {}
webpack-sources@3.2.3: {}

View file

@ -20,6 +20,8 @@ export default eventHandler(async (event) => {
expiration,
metadata: {
expiration,
url: link.url,
comment: link.comment,
},
})
setResponseStatus(event, 201)

View file

@ -27,10 +27,12 @@ export default eventHandler(async (event) => {
expiration,
metadata: {
expiration,
url: newLink.url,
comment: newLink.comment,
},
})
setResponseStatus(event, 201)
const shortLink = `${getRequestProtocol(event)}://${getRequestHost(event)}/${link.slug}`
const shortLink = `${getRequestProtocol(event)}://${getRequestHost(event)}/${newLink.slug}`
return { link: newLink, shortLink }
}
})

View file

@ -0,0 +1,73 @@
interface Link {
slug: string
url: string
comment?: string
}
export default eventHandler(async (event) => {
const { cloudflare } = event.context
const { KV } = cloudflare.env
const list: Link[] = []
let finalCursor: string | undefined
try {
while (true) {
const { keys, list_complete, cursor } = await KV.list({
prefix: `link:`,
limit: 1,
cursor: finalCursor,
})
finalCursor = cursor
if (Array.isArray(keys)) {
for (const key of keys) {
try {
if (key.metadata?.url) {
list.push({
slug: key.name.replace('link:', ''),
url: key.metadata.url,
comment: key.metadata.comment,
})
}
else {
// Forward compatible with links without metadata
const { metadata, value: link } = await KV.getWithMetadata(key.name, { type: 'json' })
if (link) {
list.push({
slug: key.name.replace('link:', ''),
url: link.url,
comment: link.comment,
})
await KV.put(key.name, JSON.stringify(link), {
expiration: metadata?.expiration,
metadata: {
...metadata,
url: link.url,
comment: link.comment,
},
})
}
}
}
catch (err) {
console.error(`Error processing key ${key.name}:`, err)
continue // Skip this key and continue with the next one
}
}
}
if (!keys || list_complete) {
break
}
}
return list
}
catch (err) {
console.error('Error fetching link list:', err)
throw createError({
statusCode: 500,
message: 'Failed to fetch link list',
})
}
})