Merge branch 'dev' into preview
This commit is contained in:
commit
97e154dd0e
29 changed files with 3593 additions and 1777 deletions
50
.vscode/settings.json
vendored
50
.vscode/settings.json
vendored
|
|
@ -1,4 +1,50 @@
|
|||
{
|
||||
// Enable ESlint flat config support
|
||||
"eslint.useFlatConfig": true
|
||||
// Disable the default formatter, use eslint instead
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
|
||||
// Auto fix
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
|
||||
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "format/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-indent", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-spacing", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-spaces", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-order", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-dangle", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-newline", "severity": "off", "fixable": true },
|
||||
{ "rule": "*quotes", "severity": "off", "fixable": true },
|
||||
{ "rule": "*semi", "severity": "off", "fixable": true }
|
||||
],
|
||||
|
||||
// Enable eslint for all supported languages
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml",
|
||||
"toml",
|
||||
"xml",
|
||||
"gql",
|
||||
"graphql",
|
||||
"astro",
|
||||
"svelte",
|
||||
"css",
|
||||
"less",
|
||||
"scss",
|
||||
"pcss",
|
||||
"postcss"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup>
|
||||
import { Flame, MousePointerClick, Users } from 'lucide-vue-next'
|
||||
import NumberFlow from '@number-flow/vue'
|
||||
import { Flame, MousePointerClick, Users } from 'lucide-vue-next'
|
||||
|
||||
const defaultData = Object.freeze({
|
||||
visits: 0,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script setup>
|
||||
import { z } from 'zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { Shuffle, Sparkles } from 'lucide-vue-next'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { DependencyType } from '@/components/ui/auto-form/interface'
|
||||
import { LinkSchema, nanoid } from '@/schemas/link'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { Shuffle, Sparkles } from 'lucide-vue-next'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { z } from 'zod'
|
||||
|
||||
const props = defineProps({
|
||||
link: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup>
|
||||
import { Loader } from 'lucide-vue-next'
|
||||
import { useInfiniteScroll } from '@vueuse/core'
|
||||
import { Loader } from 'lucide-vue-next'
|
||||
|
||||
const links = ref([])
|
||||
const limit = 24
|
||||
|
|
@ -80,7 +80,7 @@ function updateLinkList(link, type) {
|
|||
v-if="!isLoading && listComplete"
|
||||
class="flex items-center justify-center text-sm"
|
||||
>
|
||||
No more
|
||||
No more links
|
||||
</div>
|
||||
<div
|
||||
v-if="listError"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<script setup>
|
||||
import { CalendarPlus2, Copy, CopyCheck, Eraser, Hourglass, Link as LinkIcon, QrCode, SquareChevronDown, SquarePen } from 'lucide-vue-next'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { CalendarPlus2, Copy, CopyCheck, Eraser, Hourglass, Link as LinkIcon, QrCode, SquareChevronDown, SquarePen } from 'lucide-vue-next'
|
||||
import { parseURL } from 'ufo'
|
||||
import { toast } from 'vue-sonner'
|
||||
import QRCode from './QRCode.vue'
|
||||
|
||||
const props = defineProps({
|
||||
|
|
@ -23,7 +23,7 @@ function getLinkHost(url) {
|
|||
}
|
||||
|
||||
const shortLink = computed(() => `${origin}/${props.link.slug}`)
|
||||
const linkIcon = computed(() => `https://unavatar.io/${getLinkHost(props.link.url)}?fallback=https://sink.cool/sink.png`)
|
||||
const linkIcon = computed(() => `https://unavatar.io/${getLinkHost(props.link.url)}?fallback=https://sink.cool/icon.png`)
|
||||
|
||||
const { copy, copied } = useClipboard({ source: shortLink.value, copiedDuring: 400 })
|
||||
|
||||
|
|
@ -44,11 +44,13 @@ function updateLink(link, type) {
|
|||
<AvatarImage
|
||||
:src="linkIcon"
|
||||
alt="@radix-vue"
|
||||
loading="lazy"
|
||||
/>
|
||||
<AvatarFallback>
|
||||
<img
|
||||
src="/sink.png"
|
||||
src="/icon.png"
|
||||
alt="Sink"
|
||||
loading="lazy"
|
||||
>
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ onMounted(() => {
|
|||
</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">
|
||||
<Command v-model:search-term="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">
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ defineProps({
|
|||
</div>
|
||||
</div>
|
||||
<VList
|
||||
v-slot="metric"
|
||||
v-slot="{ item: metric }"
|
||||
:data="metrics"
|
||||
:style="{ height: '342px' }"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup>
|
||||
import { VisSingleContainer, VisTopoJSONMap, VisTopoJSONMapSelectors } from '@unovis/vue'
|
||||
import { ChartTooltip } from '@/components/ui/chart'
|
||||
import { VisSingleContainer, VisTopoJSONMap, VisTopoJSONMapSelectors } from '@unovis/vue'
|
||||
|
||||
const id = inject('id')
|
||||
const startAt = inject('startAt')
|
||||
|
|
|
|||
|
|
@ -1,4 +1,13 @@
|
|||
<script setup>
|
||||
import {
|
||||
Globe,
|
||||
Laptop,
|
||||
MonitorCheck,
|
||||
Smartphone,
|
||||
Tablet,
|
||||
Terminal,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
// https://vue3-simple-icons.wyatt-herkamp.dev/
|
||||
import {
|
||||
AndroidIcon,
|
||||
|
|
@ -20,23 +29,14 @@ import {
|
|||
SamsungIcon,
|
||||
UbuntuIcon,
|
||||
VivoIcon,
|
||||
WeChatIcon,
|
||||
WearOsIcon,
|
||||
WeChatIcon,
|
||||
XiaomiIcon,
|
||||
// WindowsIcon,
|
||||
XIcon,
|
||||
XiaomiIcon,
|
||||
YandexCloudIcon,
|
||||
} from 'vue3-simple-icons'
|
||||
|
||||
import {
|
||||
Globe,
|
||||
Laptop,
|
||||
MonitorCheck,
|
||||
Smartphone,
|
||||
Tablet,
|
||||
Terminal,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
|
|
|
|||
|
|
@ -17,11 +17,13 @@ defineProps({
|
|||
<AvatarImage
|
||||
:src="`https://unavatar.io/${name}?fallback=false`"
|
||||
alt="@radix-vue"
|
||||
loading="lazy"
|
||||
/>
|
||||
<AvatarFallback>
|
||||
<img
|
||||
src="/sink.png"
|
||||
src="/icon.png"
|
||||
alt="Sink"
|
||||
loading="lazy"
|
||||
>
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup>
|
||||
import heroImg from '@/assets/images/hero.svg?raw'
|
||||
import { AreaChart } from 'lucide-vue-next'
|
||||
import { GitHubIcon } from 'vue3-simple-icons'
|
||||
import heroImg from '@/assets/images/hero.svg?raw'
|
||||
|
||||
const { title, description, github } = useAppConfig()
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup>
|
||||
import { XIcon } from 'vue3-simple-icons'
|
||||
import { ArrowRight } from 'lucide-vue-next'
|
||||
import { XIcon } from 'vue3-simple-icons'
|
||||
|
||||
const { twitter } = useAppConfig()
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup>
|
||||
import { AlertCircle } from 'lucide-vue-next'
|
||||
import { z } from 'zod'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { z } from 'zod'
|
||||
|
||||
const LoginSchema = z.object({
|
||||
token: z.string().describe('SiteToken'),
|
||||
|
|
|
|||
|
|
@ -43,3 +43,11 @@ Default prompt:
|
|||
```txt
|
||||
You are a URL shortening assistant, please shorten the URL provided by the user into a SLUG. The SLUG information must come from the URL itself, do not make any assumptions. A SLUG is human-readable and should not exceed three words and can be validated using regular expressions {slugRegex} . Only the best one is returned, the format must be JSON reference {"slug": "example-slug"}
|
||||
```
|
||||
|
||||
## `NUXT_CASE_SENSITIVE`
|
||||
|
||||
Set URL case sensitivity.
|
||||
|
||||
## `NUXT_LIST_QUERY_LIMIT`
|
||||
|
||||
Set the maximum query data volume for the Metric list.
|
||||
|
|
|
|||
|
|
@ -42,3 +42,7 @@ However, you can disable this feature by setting the `NUXT_CASE_SENSITIVE` envir
|
|||
### What happens when `NUXT_CASE_SENSITIVE` is `true`?
|
||||
|
||||
Newly generated links will be case-sensitive, treating `MyLink` and `mylink` as distinct. Randomly generated slugs will include both uppercase and lowercase characters, offering a larger pool of unique combinations (but not user-friendly that why we default to non-case-sensitive).
|
||||
|
||||
## 7. Why does the Metric list only show the top 500 data entries?
|
||||
|
||||
To improve query performance, we have limited the amount of data. If you need to query more data, you can adjust it through `NUXT_LIST_QUERY_LIMIT`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
devtools: { enabled: true },
|
||||
|
||||
modules: [
|
||||
'@nuxthub/core',
|
||||
|
|
@ -9,11 +8,31 @@ export default defineNuxtConfig({
|
|||
'@nuxtjs/tailwindcss',
|
||||
'@nuxtjs/color-mode',
|
||||
],
|
||||
devtools: { enabled: true },
|
||||
|
||||
colorMode: {
|
||||
classSuffix: '',
|
||||
},
|
||||
|
||||
runtimeConfig: {
|
||||
siteToken: 'SinkCool',
|
||||
redirectStatusCode: '301',
|
||||
linkCacheTtl: 60,
|
||||
redirectWithQuery: false,
|
||||
homeURL: '',
|
||||
cfAccountId: '',
|
||||
cfApiToken: '',
|
||||
dataset: 'sink',
|
||||
aiModel: '@cf/meta/llama-3.1-8b-instruct',
|
||||
aiPrompt: `You are a URL shortening assistant, please shorten the URL provided by the user into a SLUG. The SLUG information must come from the URL itself, do not make any assumptions. A SLUG is human-readable and should not exceed three words and can be validated using regular expressions {slugRegex} . Only the best one is returned, the format must be JSON reference {"slug": "example-slug"}`,
|
||||
caseSensitive: false,
|
||||
listQueryLimit: 500,
|
||||
public: {
|
||||
previewMode: '',
|
||||
slugDefaultLength: '6',
|
||||
},
|
||||
},
|
||||
|
||||
routeRules: {
|
||||
'/': {
|
||||
prerender: true,
|
||||
|
|
@ -26,6 +45,15 @@ export default defineNuxtConfig({
|
|||
},
|
||||
},
|
||||
|
||||
compatibilityDate: '2024-07-08',
|
||||
|
||||
nitro: {
|
||||
experimental: {
|
||||
// Enable Server API documentation within NuxtHub
|
||||
openAPI: true,
|
||||
},
|
||||
},
|
||||
|
||||
hub: {
|
||||
ai: true,
|
||||
analytics: true,
|
||||
|
|
@ -41,31 +69,4 @@ export default defineNuxtConfig({
|
|||
standalone: false,
|
||||
},
|
||||
},
|
||||
|
||||
nitro: {
|
||||
experimental: {
|
||||
// Enable Server API documentation within NuxtHub
|
||||
openAPI: true,
|
||||
},
|
||||
},
|
||||
|
||||
runtimeConfig: {
|
||||
siteToken: 'SinkCool',
|
||||
redirectStatusCode: '301',
|
||||
linkCacheTtl: 60,
|
||||
redirectWithQuery: false,
|
||||
homeURL: '',
|
||||
cfAccountId: '',
|
||||
cfApiToken: '',
|
||||
dataset: 'sink',
|
||||
aiModel: '@cf/meta/llama-3.1-8b-instruct',
|
||||
aiPrompt: `You are a URL shortening assistant, please shorten the URL provided by the user into a SLUG. The SLUG information must come from the URL itself, do not make any assumptions. A SLUG is human-readable and should not exceed three words and can be validated using regular expressions {slugRegex} . Only the best one is returned, the format must be JSON reference {"slug": "example-slug"}`,
|
||||
caseSensitive: false,
|
||||
public: {
|
||||
previewMode: '',
|
||||
slugDefaultLength: '6',
|
||||
},
|
||||
},
|
||||
|
||||
compatibilityDate: '2024-07-08',
|
||||
})
|
||||
|
|
|
|||
52
package.json
52
package.json
|
|
@ -3,7 +3,7 @@
|
|||
"type": "module",
|
||||
"version": "0.1.7",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.7.1",
|
||||
"packageManager": "pnpm@9.15.1",
|
||||
"engines": {
|
||||
"node": ">=20.11"
|
||||
},
|
||||
|
|
@ -22,44 +22,44 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@number-flow/vue": "^0.3.3",
|
||||
"@unovis/ts": "^1.4.4",
|
||||
"@unovis/vue": "^1.4.4",
|
||||
"@vee-validate/zod": "^4.13.2",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@unovis/ts": "^1.5.0",
|
||||
"@unovis/vue": "^1.5.0",
|
||||
"@vee-validate/zod": "^4.15.0",
|
||||
"@vueuse/core": "^12.2.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"intl-parse-accept-language": "^1.0.0",
|
||||
"lucide-vue-next": "^0.428.0",
|
||||
"lucide-vue-next": "^0.469.0",
|
||||
"mysql-bricks": "^1.1.2",
|
||||
"nanoid": "^5.0.7",
|
||||
"nanoid": "^5.0.9",
|
||||
"pluralize": "^8.0.0",
|
||||
"qr-code-styling": "1.6.0-rc.1",
|
||||
"radix-vue": "^1.9.4",
|
||||
"radix-vue": "^1.9.11",
|
||||
"ua-parser-js": "next",
|
||||
"vee-validate": "^4.13.2",
|
||||
"virtua": "^0.33.7",
|
||||
"vue-sonner": "^1.1.4",
|
||||
"vee-validate": "^4.15.0",
|
||||
"virtua": "^0.39.2",
|
||||
"vue-sonner": "^1.3.0",
|
||||
"vue3-simple-icons": "^13.2.0",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.26.0",
|
||||
"@nuxt/eslint": "^0.5.0",
|
||||
"@nuxt/eslint-config": "^0.5.0",
|
||||
"@nuxthub/core": "^0.7.3",
|
||||
"@nuxtjs/color-mode": "^3.4.4",
|
||||
"@nuxtjs/tailwindcss": "^6.12.1",
|
||||
"@antfu/eslint-config": "^3.12.1",
|
||||
"@nuxt/eslint": "^0.7.4",
|
||||
"@nuxt/eslint-config": "^0.7.4",
|
||||
"@nuxthub/core": "0.7.11",
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@vueuse/integrations": "^12.2.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.9.0",
|
||||
"lint-staged": "^15.2.9",
|
||||
"nuxt": "^3.12.4",
|
||||
"shadcn-nuxt": "^0.10.4",
|
||||
"eslint": "^9.17.0",
|
||||
"lint-staged": "^15.2.11",
|
||||
"nuxt": "~3.13.2",
|
||||
"shadcn-nuxt": "^0.11.3",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vue-tsc": "^2.0.29",
|
||||
"wrangler": "^3.72.0"
|
||||
"vue-tsc": "^2.2.0",
|
||||
"wrangler": "^3.99.0"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "npm run lint-staged"
|
||||
|
|
|
|||
5086
pnpm-lock.yaml
5086
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
BIN
public/icon.png
Normal file
BIN
public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -1,16 +1,11 @@
|
|||
import { z } from 'zod'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
import { z } from 'zod'
|
||||
|
||||
const { slugRegex } = useAppConfig()
|
||||
const { caseSensitive } = useRuntimeConfig()
|
||||
|
||||
const slugDefaultLength = +useRuntimeConfig().public.slugDefaultLength
|
||||
|
||||
export function nanoid(length: number = slugDefaultLength) {
|
||||
return caseSensitive
|
||||
? customAlphabet('23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ', length)
|
||||
: customAlphabet('23456789abcdefghjkmnpqrstuvwxyz', length)
|
||||
}
|
||||
export const nanoid = (length: number = slugDefaultLength) => customAlphabet('23456789abcdefghjkmnpqrstuvwxyz', length)
|
||||
|
||||
export const LinkSchema = z.object({
|
||||
id: z.string().trim().max(26).default(nanoid(10)),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { z } from 'zod'
|
||||
|
||||
const listQueryLimit = +useRuntimeConfig().listQueryLimit
|
||||
|
||||
export const QuerySchema = z.object({
|
||||
id: z.string().optional(),
|
||||
startAt: z.coerce.number().int().safe().optional(),
|
||||
|
|
@ -17,7 +19,7 @@ export const QuerySchema = z.object({
|
|||
browserType: z.string().optional(),
|
||||
device: z.string().optional(),
|
||||
deviceType: z.string().optional(),
|
||||
limit: z.coerce.number().int().safe().default(500),
|
||||
limit: z.coerce.number().int().safe().default(listQueryLimit),
|
||||
})
|
||||
|
||||
export const FilterSchema = QuerySchema.omit({ id: true, startAt: true, endAt: true, limit: true }).extend({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { z } from 'zod'
|
||||
import { destr } from 'destr'
|
||||
import { z } from 'zod'
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const url = (await getValidatedQuery(event, z.object({
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@ import { LinkSchema } from '@/schemas/link'
|
|||
export default eventHandler(async (event) => {
|
||||
const link = await readValidatedBody(event, LinkSchema.parse)
|
||||
|
||||
const { caseSensitive } = useRuntimeConfig(event)
|
||||
|
||||
if (!caseSensitive) {
|
||||
link.slug = link.slug.toLowerCase()
|
||||
}
|
||||
|
||||
const { cloudflare } = event.context
|
||||
const { KV } = cloudflare.env
|
||||
const existingLink = await KV.get(`link:${link.slug}`)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { H3Event } from 'h3'
|
||||
import { z } from 'zod'
|
||||
import { QuerySchema } from '@/schemas/query'
|
||||
import { z } from 'zod'
|
||||
|
||||
const { select } = SqlBricks
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { H3Event } from 'h3'
|
||||
import { z } from 'zod'
|
||||
import { QuerySchema } from '@/schemas/query'
|
||||
import { z } from 'zod'
|
||||
|
||||
const { select } = SqlBricks
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { LinkSchema } from '@/schemas/link'
|
||||
import type { z } from 'zod'
|
||||
import { parsePath, withQuery } from 'ufo'
|
||||
import type { LinkSchema } from '@/schemas/link'
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const { pathname: slug } = parsePath(event.path.replace(/^\/|\/$/g, '')) // remove leading and trailing slashes
|
||||
|
|
@ -19,13 +19,13 @@ export default eventHandler(async (event) => {
|
|||
const getLink = async (key: string) =>
|
||||
await KV.get(`link:${key}`, { type: 'json', cacheTtl: linkCacheTtl })
|
||||
|
||||
link = await getLink(slug)
|
||||
|
||||
// fallback to lowercase slug if caseSensitive is false and the slug is not found
|
||||
const lowerCaseSlug = slug.toLowerCase()
|
||||
link = await getLink(caseSensitive ? slug : lowerCaseSlug)
|
||||
|
||||
// fallback to original slug if caseSensitive is false and the slug is not found
|
||||
if (!caseSensitive && !link && lowerCaseSlug !== slug) {
|
||||
console.log('lowerCaseSlug fallback:', `slug:${slug} lowerCaseSlug:${lowerCaseSlug}`)
|
||||
link = await getLink(lowerCaseSlug)
|
||||
console.log('original slug fallback:', `slug:${slug} lowerCaseSlug:${lowerCaseSlug}`)
|
||||
link = await getLink(slug)
|
||||
}
|
||||
|
||||
if (link) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { H3Event } from 'h3'
|
||||
import { parseURL } from 'ufo'
|
||||
import { getFlag } from '@/utils/flag'
|
||||
import { parseAcceptLanguage } from 'intl-parse-accept-language'
|
||||
import { UAParser } from 'ua-parser-js'
|
||||
import {
|
||||
CLIs,
|
||||
|
|
@ -11,8 +12,7 @@ import {
|
|||
MediaPlayers,
|
||||
Modules,
|
||||
} from 'ua-parser-js/extensions'
|
||||
import { parseAcceptLanguage } from 'intl-parse-accept-language'
|
||||
import { getFlag } from '@/utils/flag'
|
||||
import { parseURL } from 'ufo'
|
||||
|
||||
function toBlobNumber(blob: string) {
|
||||
return +blob.replace(/\D/g, '')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { z } from 'zod'
|
||||
import type { SelectStatement } from 'sql-bricks'
|
||||
import type { FilterSchema, QuerySchema } from '@/schemas/query'
|
||||
import type { SelectStatement } from 'sql-bricks'
|
||||
import type { z } from 'zod'
|
||||
|
||||
export type Query = z.infer<typeof QuerySchema>
|
||||
export type Filter = z.infer<typeof FilterSchema>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export function colorGradation(count: number) {
|
||||
return Array.from(Array(count).keys()).map(i => `hsl(var(--vis-secondary-color) / ${1 - (1 / count) * i})`)
|
||||
return Array.from({ length: count }, (_, i) => `hsl(var(--vis-secondary-color) / ${1 - (1 / count) * i})`)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue