Merge branch 'dev' into preview

This commit is contained in:
ccbikai 2024-12-25 12:47:53 +08:00
commit 97e154dd0e
29 changed files with 3593 additions and 1777 deletions

50
.vscode/settings.json vendored
View file

@ -1,4 +1,50 @@
{ {
// Enable ESlint flat config support // Disable the default formatter, use eslint instead
"eslint.useFlatConfig": true "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"
]
} }

View file

@ -1,6 +1,6 @@
<script setup> <script setup>
import { Flame, MousePointerClick, Users } from 'lucide-vue-next'
import NumberFlow from '@number-flow/vue' import NumberFlow from '@number-flow/vue'
import { Flame, MousePointerClick, Users } from 'lucide-vue-next'
const defaultData = Object.freeze({ const defaultData = Object.freeze({
visits: 0, visits: 0,

View file

@ -1,11 +1,11 @@
<script setup> <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 { DependencyType } from '@/components/ui/auto-form/interface'
import { LinkSchema, nanoid } from '@/schemas/link' 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({ const props = defineProps({
link: { link: {

View file

@ -1,6 +1,6 @@
<script setup> <script setup>
import { Loader } from 'lucide-vue-next'
import { useInfiniteScroll } from '@vueuse/core' import { useInfiniteScroll } from '@vueuse/core'
import { Loader } from 'lucide-vue-next'
const links = ref([]) const links = ref([])
const limit = 24 const limit = 24
@ -80,7 +80,7 @@ function updateLinkList(link, type) {
v-if="!isLoading && listComplete" v-if="!isLoading && listComplete"
class="flex items-center justify-center text-sm" class="flex items-center justify-center text-sm"
> >
No more No more links
</div> </div>
<div <div
v-if="listError" v-if="listError"

View file

@ -1,8 +1,8 @@
<script setup> <script setup>
import { CalendarPlus2, Copy, CopyCheck, Eraser, Hourglass, Link as LinkIcon, QrCode, SquareChevronDown, SquarePen } from 'lucide-vue-next'
import { useClipboard } from '@vueuse/core' 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 { parseURL } from 'ufo'
import { toast } from 'vue-sonner'
import QRCode from './QRCode.vue' import QRCode from './QRCode.vue'
const props = defineProps({ const props = defineProps({
@ -23,7 +23,7 @@ function getLinkHost(url) {
} }
const shortLink = computed(() => `${origin}/${props.link.slug}`) 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 }) const { copy, copied } = useClipboard({ source: shortLink.value, copiedDuring: 400 })
@ -44,11 +44,13 @@ function updateLink(link, type) {
<AvatarImage <AvatarImage
:src="linkIcon" :src="linkIcon"
alt="@radix-vue" alt="@radix-vue"
loading="lazy"
/> />
<AvatarFallback> <AvatarFallback>
<img <img
src="/sink.png" src="/icon.png"
alt="Sink" alt="Sink"
loading="lazy"
> >
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>

View file

@ -63,7 +63,7 @@ onMounted(() => {
</Button> </Button>
<Dialog :open="isOpen" @update:open="isOpen = !isOpen"> <Dialog :open="isOpen" @update:open="isOpen = !isOpen">
<DialogContent class="overflow-hidden p-0 shadow-lg"> <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..." /> <CommandInput placeholder="Type to search..." />
<CommandList> <CommandList>
<CommandEmpty v-if="searchTerm"> <CommandEmpty v-if="searchTerm">

View file

@ -30,7 +30,7 @@ defineProps({
</div> </div>
</div> </div>
<VList <VList
v-slot="metric" v-slot="{ item: metric }"
:data="metrics" :data="metrics"
:style="{ height: '342px' }" :style="{ height: '342px' }"
> >

View file

@ -1,6 +1,6 @@
<script setup> <script setup>
import { VisSingleContainer, VisTopoJSONMap, VisTopoJSONMapSelectors } from '@unovis/vue'
import { ChartTooltip } from '@/components/ui/chart' import { ChartTooltip } from '@/components/ui/chart'
import { VisSingleContainer, VisTopoJSONMap, VisTopoJSONMapSelectors } from '@unovis/vue'
const id = inject('id') const id = inject('id')
const startAt = inject('startAt') const startAt = inject('startAt')

View file

@ -1,4 +1,13 @@
<script setup> <script setup>
import {
Globe,
Laptop,
MonitorCheck,
Smartphone,
Tablet,
Terminal,
} from 'lucide-vue-next'
// https://vue3-simple-icons.wyatt-herkamp.dev/ // https://vue3-simple-icons.wyatt-herkamp.dev/
import { import {
AndroidIcon, AndroidIcon,
@ -20,23 +29,14 @@ import {
SamsungIcon, SamsungIcon,
UbuntuIcon, UbuntuIcon,
VivoIcon, VivoIcon,
WeChatIcon,
WearOsIcon, WearOsIcon,
WeChatIcon,
XiaomiIcon,
// WindowsIcon, // WindowsIcon,
XIcon, XIcon,
XiaomiIcon,
YandexCloudIcon, YandexCloudIcon,
} from 'vue3-simple-icons' } from 'vue3-simple-icons'
import {
Globe,
Laptop,
MonitorCheck,
Smartphone,
Tablet,
Terminal,
} from 'lucide-vue-next'
defineProps({ defineProps({
name: { name: {
type: String, type: String,

View file

@ -17,11 +17,13 @@ defineProps({
<AvatarImage <AvatarImage
:src="`https://unavatar.io/${name}?fallback=false`" :src="`https://unavatar.io/${name}?fallback=false`"
alt="@radix-vue" alt="@radix-vue"
loading="lazy"
/> />
<AvatarFallback> <AvatarFallback>
<img <img
src="/sink.png" src="/icon.png"
alt="Sink" alt="Sink"
loading="lazy"
> >
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>

View file

@ -1,7 +1,7 @@
<script setup> <script setup>
import heroImg from '@/assets/images/hero.svg?raw'
import { AreaChart } from 'lucide-vue-next' import { AreaChart } from 'lucide-vue-next'
import { GitHubIcon } from 'vue3-simple-icons' import { GitHubIcon } from 'vue3-simple-icons'
import heroImg from '@/assets/images/hero.svg?raw'
const { title, description, github } = useAppConfig() const { title, description, github } = useAppConfig()
</script> </script>

View file

@ -1,6 +1,6 @@
<script setup> <script setup>
import { XIcon } from 'vue3-simple-icons'
import { ArrowRight } from 'lucide-vue-next' import { ArrowRight } from 'lucide-vue-next'
import { XIcon } from 'vue3-simple-icons'
const { twitter } = useAppConfig() const { twitter } = useAppConfig()
</script> </script>

View file

@ -1,7 +1,7 @@
<script setup> <script setup>
import { AlertCircle } from 'lucide-vue-next' import { AlertCircle } from 'lucide-vue-next'
import { z } from 'zod'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { z } from 'zod'
const LoginSchema = z.object({ const LoginSchema = z.object({
token: z.string().describe('SiteToken'), token: z.string().describe('SiteToken'),

View file

@ -43,3 +43,11 @@ Default prompt:
```txt ```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"} 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.

View file

@ -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`? ### 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). 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`.

View file

@ -1,6 +1,5 @@
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
devtools: { enabled: true },
modules: [ modules: [
'@nuxthub/core', '@nuxthub/core',
@ -9,11 +8,31 @@ export default defineNuxtConfig({
'@nuxtjs/tailwindcss', '@nuxtjs/tailwindcss',
'@nuxtjs/color-mode', '@nuxtjs/color-mode',
], ],
devtools: { enabled: true },
colorMode: { colorMode: {
classSuffix: '', 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: { routeRules: {
'/': { '/': {
prerender: true, prerender: true,
@ -26,6 +45,15 @@ export default defineNuxtConfig({
}, },
}, },
compatibilityDate: '2024-07-08',
nitro: {
experimental: {
// Enable Server API documentation within NuxtHub
openAPI: true,
},
},
hub: { hub: {
ai: true, ai: true,
analytics: true, analytics: true,
@ -41,31 +69,4 @@ export default defineNuxtConfig({
standalone: false, 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',
}) })

View file

@ -3,7 +3,7 @@
"type": "module", "type": "module",
"version": "0.1.7", "version": "0.1.7",
"private": true, "private": true,
"packageManager": "pnpm@9.7.1", "packageManager": "pnpm@9.15.1",
"engines": { "engines": {
"node": ">=20.11" "node": ">=20.11"
}, },
@ -22,44 +22,44 @@
}, },
"dependencies": { "dependencies": {
"@number-flow/vue": "^0.3.3", "@number-flow/vue": "^0.3.3",
"@unovis/ts": "^1.4.4", "@unovis/ts": "^1.5.0",
"@unovis/vue": "^1.4.4", "@unovis/vue": "^1.5.0",
"@vee-validate/zod": "^4.13.2", "@vee-validate/zod": "^4.15.0",
"@vueuse/core": "^11.0.0", "@vueuse/core": "^12.2.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"intl-parse-accept-language": "^1.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", "mysql-bricks": "^1.1.2",
"nanoid": "^5.0.7", "nanoid": "^5.0.9",
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"qr-code-styling": "1.6.0-rc.1", "qr-code-styling": "1.6.0-rc.1",
"radix-vue": "^1.9.4", "radix-vue": "^1.9.11",
"ua-parser-js": "next", "ua-parser-js": "next",
"vee-validate": "^4.13.2", "vee-validate": "^4.15.0",
"virtua": "^0.33.7", "virtua": "^0.39.2",
"vue-sonner": "^1.1.4", "vue-sonner": "^1.3.0",
"vue3-simple-icons": "^13.2.0", "vue3-simple-icons": "^13.2.0",
"zod": "^3.23.8" "zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^2.26.0", "@antfu/eslint-config": "^3.12.1",
"@nuxt/eslint": "^0.5.0", "@nuxt/eslint": "^0.7.4",
"@nuxt/eslint-config": "^0.5.0", "@nuxt/eslint-config": "^0.7.4",
"@nuxthub/core": "^0.7.3", "@nuxthub/core": "0.7.11",
"@nuxtjs/color-mode": "^3.4.4", "@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/tailwindcss": "^6.12.1", "@nuxtjs/tailwindcss": "^6.12.2",
"@vueuse/integrations": "^12.2.0", "@vueuse/integrations": "^12.2.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eslint": "^9.9.0", "eslint": "^9.17.0",
"lint-staged": "^15.2.9", "lint-staged": "^15.2.11",
"nuxt": "^3.12.4", "nuxt": "~3.13.2",
"shadcn-nuxt": "^0.10.4", "shadcn-nuxt": "^0.11.3",
"simple-git-hooks": "^2.11.1", "simple-git-hooks": "^2.11.1",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vue-tsc": "^2.0.29", "vue-tsc": "^2.2.0",
"wrangler": "^3.72.0" "wrangler": "^3.99.0"
}, },
"simple-git-hooks": { "simple-git-hooks": {
"pre-commit": "npm run lint-staged" "pre-commit": "npm run lint-staged"

File diff suppressed because it is too large Load diff

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,16 +1,11 @@
import { z } from 'zod'
import { customAlphabet } from 'nanoid' import { customAlphabet } from 'nanoid'
import { z } from 'zod'
const { slugRegex } = useAppConfig() const { slugRegex } = useAppConfig()
const { caseSensitive } = useRuntimeConfig()
const slugDefaultLength = +useRuntimeConfig().public.slugDefaultLength const slugDefaultLength = +useRuntimeConfig().public.slugDefaultLength
export function nanoid(length: number = slugDefaultLength) { export const nanoid = (length: number = slugDefaultLength) => customAlphabet('23456789abcdefghjkmnpqrstuvwxyz', length)
return caseSensitive
? customAlphabet('23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ', length)
: customAlphabet('23456789abcdefghjkmnpqrstuvwxyz', length)
}
export const LinkSchema = z.object({ export const LinkSchema = z.object({
id: z.string().trim().max(26).default(nanoid(10)), id: z.string().trim().max(26).default(nanoid(10)),

View file

@ -1,5 +1,7 @@
import { z } from 'zod' import { z } from 'zod'
const listQueryLimit = +useRuntimeConfig().listQueryLimit
export const QuerySchema = z.object({ export const QuerySchema = z.object({
id: z.string().optional(), id: z.string().optional(),
startAt: z.coerce.number().int().safe().optional(), startAt: z.coerce.number().int().safe().optional(),
@ -17,7 +19,7 @@ export const QuerySchema = z.object({
browserType: z.string().optional(), browserType: z.string().optional(),
device: z.string().optional(), device: z.string().optional(),
deviceType: 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({ export const FilterSchema = QuerySchema.omit({ id: true, startAt: true, endAt: true, limit: true }).extend({

View file

@ -1,5 +1,5 @@
import { z } from 'zod'
import { destr } from 'destr' import { destr } from 'destr'
import { z } from 'zod'
export default eventHandler(async (event) => { export default eventHandler(async (event) => {
const url = (await getValidatedQuery(event, z.object({ const url = (await getValidatedQuery(event, z.object({

View file

@ -3,6 +3,12 @@ import { LinkSchema } from '@/schemas/link'
export default eventHandler(async (event) => { export default eventHandler(async (event) => {
const link = await readValidatedBody(event, LinkSchema.parse) const link = await readValidatedBody(event, LinkSchema.parse)
const { caseSensitive } = useRuntimeConfig(event)
if (!caseSensitive) {
link.slug = link.slug.toLowerCase()
}
const { cloudflare } = event.context const { cloudflare } = event.context
const { KV } = cloudflare.env const { KV } = cloudflare.env
const existingLink = await KV.get(`link:${link.slug}`) const existingLink = await KV.get(`link:${link.slug}`)

View file

@ -1,6 +1,6 @@
import type { H3Event } from 'h3' import type { H3Event } from 'h3'
import { z } from 'zod'
import { QuerySchema } from '@/schemas/query' import { QuerySchema } from '@/schemas/query'
import { z } from 'zod'
const { select } = SqlBricks const { select } = SqlBricks

View file

@ -1,6 +1,6 @@
import type { H3Event } from 'h3' import type { H3Event } from 'h3'
import { z } from 'zod'
import { QuerySchema } from '@/schemas/query' import { QuerySchema } from '@/schemas/query'
import { z } from 'zod'
const { select } = SqlBricks const { select } = SqlBricks

View file

@ -1,6 +1,6 @@
import type { LinkSchema } from '@/schemas/link'
import type { z } from 'zod' import type { z } from 'zod'
import { parsePath, withQuery } from 'ufo' import { parsePath, withQuery } from 'ufo'
import type { LinkSchema } from '@/schemas/link'
export default eventHandler(async (event) => { export default eventHandler(async (event) => {
const { pathname: slug } = parsePath(event.path.replace(/^\/|\/$/g, '')) // remove leading and trailing slashes 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) => const getLink = async (key: string) =>
await KV.get(`link:${key}`, { type: 'json', cacheTtl: linkCacheTtl }) 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() 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) { if (!caseSensitive && !link && lowerCaseSlug !== slug) {
console.log('lowerCaseSlug fallback:', `slug:${slug} lowerCaseSlug:${lowerCaseSlug}`) console.log('original slug fallback:', `slug:${slug} lowerCaseSlug:${lowerCaseSlug}`)
link = await getLink(lowerCaseSlug) link = await getLink(slug)
} }
if (link) { if (link) {

View file

@ -1,5 +1,6 @@
import type { H3Event } from 'h3' 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 { UAParser } from 'ua-parser-js'
import { import {
CLIs, CLIs,
@ -11,8 +12,7 @@ import {
MediaPlayers, MediaPlayers,
Modules, Modules,
} from 'ua-parser-js/extensions' } from 'ua-parser-js/extensions'
import { parseAcceptLanguage } from 'intl-parse-accept-language' import { parseURL } from 'ufo'
import { getFlag } from '@/utils/flag'
function toBlobNumber(blob: string) { function toBlobNumber(blob: string) {
return +blob.replace(/\D/g, '') return +blob.replace(/\D/g, '')

View file

@ -1,6 +1,6 @@
import type { z } from 'zod'
import type { SelectStatement } from 'sql-bricks'
import type { FilterSchema, QuerySchema } from '@/schemas/query' 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 Query = z.infer<typeof QuerySchema>
export type Filter = z.infer<typeof FilterSchema> export type Filter = z.infer<typeof FilterSchema>

View file

@ -1,3 +1,3 @@
export function colorGradation(count: number) { 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})`)
} }