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
"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"
]
}

View file

@ -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,

View file

@ -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: {

View file

@ -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"

View file

@ -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>

View file

@ -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">

View file

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

View file

@ -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')

View file

@ -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,

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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'),

View file

@ -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.

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`?
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
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',
})

View file

@ -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"

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 { 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)),

View file

@ -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({

View file

@ -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({

View file

@ -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}`)

View file

@ -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

View file

@ -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

View file

@ -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) {

View file

@ -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, '')

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 { SelectStatement } from 'sql-bricks'
import type { z } from 'zod'
export type Query = z.infer<typeof QuerySchema>
export type Filter = z.infer<typeof FilterSchema>

View file

@ -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})`)
}