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
|
// 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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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' }"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
||||||
|
|
|
||||||
|
|
@ -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',
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
52
package.json
52
package.json
|
|
@ -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"
|
||||||
|
|
|
||||||
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 { 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)),
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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}`)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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, '')
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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})`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue