Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12e3e7c7bc | ||
|
|
233d366d6e | ||
|
|
05eb3bfca3 | ||
|
|
d8c92aa7a1 | ||
|
|
f2a2e00ce6 | ||
|
|
20eed15967 | ||
|
|
53a6b5d403 | ||
|
|
36e7962b83 | ||
|
|
b116d4c007 | ||
|
|
55783573ef | ||
|
|
ef3e704d79 | ||
|
|
5cf1e89ce4 | ||
|
|
6d0a67d9b5 | ||
|
|
b2e859107d | ||
|
|
08ed8da522 | ||
|
|
2856c4013f | ||
|
|
d7fce2eac0 | ||
|
|
576eee43ca | ||
|
|
52187d1ff6 | ||
|
|
2876385f20 | ||
|
|
3c0a7be6eb | ||
|
|
8f8865801a | ||
|
|
7bcc5b27be | ||
|
|
6b3dd8d47e | ||
|
|
ca12fdd876 | ||
|
|
21d8352de0 | ||
|
|
09a97070d3 | ||
|
|
0d10da9b04 | ||
|
|
01be05c0fc | ||
|
|
e16fb88c08 | ||
|
|
bac9abb9a8 | ||
|
|
de411396e2 | ||
|
|
528f5ddc48 | ||
|
|
503e62aa9e | ||
|
|
a98aa7c4e9 | ||
|
|
99f1c95cf9 | ||
|
|
14ff257216 | ||
|
|
08359a773b | ||
|
|
40fa9d1581 | ||
|
|
686ecd151a | ||
|
|
9355ff5503 | ||
|
|
997d58f42d | ||
|
|
2b4724f39c | ||
|
|
3f08cf0439 | ||
|
|
036265544f | ||
|
|
426db8d149 | ||
|
|
97d6848b5e | ||
|
|
4d35186ffe | ||
|
|
07c11954c7 | ||
|
|
c894fde09f | ||
|
|
d2c83e43d4 | ||
|
|
5a3f3fbeff | ||
|
|
74a2309b44 | ||
|
|
de730b4a04 | ||
|
|
3e2878c107 | ||
|
|
d0d0fc65e5 | ||
|
|
5df2adc5b0 | ||
|
|
bdabd9d65f | ||
|
|
d620d1f84e | ||
|
|
1127d0a4e9 | ||
|
|
95cbb64b22 | ||
|
|
cd012676b4 | ||
|
|
3b6121e64f | ||
|
|
9a00ed3086 | ||
|
|
12f2576c0a | ||
|
|
03ff995257 | ||
|
|
e66c21634e | ||
|
|
f2a4a5963d | ||
|
|
245a0ea997 | ||
|
|
3ff84f61cb | ||
|
|
5c61271fcf | ||
|
|
dda19697d6 |
@ -2,6 +2,8 @@ NUXT_PUBLIC_PREVIEW_MODE=true
|
|||||||
NUXT_PUBLIC_SLUG_DEFAULT_LENGTH=5
|
NUXT_PUBLIC_SLUG_DEFAULT_LENGTH=5
|
||||||
NUXT_SITE_TOKEN=SinkCool
|
NUXT_SITE_TOKEN=SinkCool
|
||||||
NUXT_REDIRECT_STATUS_CODE=308
|
NUXT_REDIRECT_STATUS_CODE=308
|
||||||
|
NUXT_LINK_CACHE_TTL=60
|
||||||
|
NUXT_REDIRECT_WITH_QUERY=false
|
||||||
NUXT_HOME_URL="https://sink.cool"
|
NUXT_HOME_URL="https://sink.cool"
|
||||||
NUXT_CF_ACCOUNT_ID=123456
|
NUXT_CF_ACCOUNT_ID=123456
|
||||||
NUXT_CF_API_TOKEN=CloudflareAPIToken
|
NUXT_CF_API_TOKEN=CloudflareAPIToken
|
||||||
|
|||||||
16
.github/FUNDING.yml
vendored
16
.github/FUNDING.yml
vendored
@ -1,14 +1,2 @@
|
|||||||
# These are supported funding model platforms
|
github: ccbikai
|
||||||
|
buy_me_a_coffee: ccbikai
|
||||||
github: ccbikai # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
|
||||||
patreon: # Replace with a single Patreon username
|
|
||||||
open_collective: # Replace with a single Open Collective username
|
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username
|
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
||||||
polar: # Replace with a single Polar username
|
|
||||||
buy_me_a_coffee: ccbikai # Replace with a single Buy Me a Coffee username
|
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,3 +25,4 @@ logs
|
|||||||
.wrangler
|
.wrangler
|
||||||
site
|
site
|
||||||
cache
|
cache
|
||||||
|
public/world.json
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
v20
|
v20.11
|
||||||
|
|||||||
34
README.md
34
README.md
@ -2,6 +2,30 @@
|
|||||||
|
|
||||||
**A Simple / Speedy / Secure Link Shortener with Analytics, 100% run on Cloudflare.**
|
**A Simple / Speedy / Secure Link Shortener with Analytics, 100% run on Cloudflare.**
|
||||||
|
|
||||||
|
<a href="https://trendshift.io/repositories/10421" target="_blank">
|
||||||
|
<img
|
||||||
|
src="https://trendshift.io/api/badge/repositories/10421"
|
||||||
|
alt="ccbikai/Sink | Trendshift"
|
||||||
|
style="width: 250px; height: 55px;"
|
||||||
|
width="250"
|
||||||
|
height="55"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://news.ycombinator.com/item?id=40843683">
|
||||||
|
<img
|
||||||
|
src="https://hackernews-badge.vercel.app/api?id=40843683"
|
||||||
|
alt="Featured on Hacker News"
|
||||||
|
style="width: 250px; height: 55px;"
|
||||||
|
width="250"
|
||||||
|
height="55"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
----
|
----
|
||||||
@ -73,6 +97,14 @@ We welcome your contributions and PRs.
|
|||||||
|
|
||||||
[Configuration Docs](./docs/configuration.md)
|
[Configuration Docs](./docs/configuration.md)
|
||||||
|
|
||||||
|
## 🔌 API
|
||||||
|
|
||||||
|
[API Docs](./docs/api.md)
|
||||||
|
|
||||||
|
## 🙋🏻 FAQs
|
||||||
|
|
||||||
|
[FAQs](./docs/faqs.md)
|
||||||
|
|
||||||
## 💖 Credits
|
## 💖 Credits
|
||||||
|
|
||||||
1. [**Cloudflare**](https://www.cloudflare.com/)
|
1. [**Cloudflare**](https://www.cloudflare.com/)
|
||||||
@ -81,5 +113,5 @@ We welcome your contributions and PRs.
|
|||||||
|
|
||||||
## ☕ Sponsor
|
## ☕ Sponsor
|
||||||
|
|
||||||
1. [Follow Me on X(Twitter)](https://x.com/ccbikai).
|
1. [Follow Me on X(Twitter)](https://x.com/0xKaiBi).
|
||||||
2. [Become a sponsor to on GitHub](https://github.com/sponsors/ccbikai).
|
2. [Become a sponsor to on GitHub](https://github.com/sponsors/ccbikai).
|
||||||
|
|||||||
5
app.vue
5
app.vue
@ -1,8 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const { title, description, image } = useAppConfig()
|
const { title, description, image } = useAppConfig()
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title,
|
title: `${title} - ${description}`,
|
||||||
description,
|
description,
|
||||||
|
ogType: 'website',
|
||||||
ogTitle: title,
|
ogTitle: title,
|
||||||
ogDescription: description,
|
ogDescription: description,
|
||||||
ogImage: image,
|
ogImage: image,
|
||||||
@ -20,7 +21,7 @@ useHead({
|
|||||||
{
|
{
|
||||||
rel: 'icon',
|
rel: 'icon',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
href: '/sink.png',
|
href: '/icon-192.png',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Sun, Moon } from 'lucide-vue-next'
|
import { Laptop, Moon, Sun } from 'lucide-vue-next'
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
</script>
|
</script>
|
||||||
@ -9,10 +9,10 @@ const colorMode = useColorMode()
|
|||||||
<DropdownMenuTrigger as-child>
|
<DropdownMenuTrigger as-child>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<Sun
|
<Sun
|
||||||
class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
|
class="absolute w-5 h-5 transition-all scale-100 dark:scale-0"
|
||||||
/>
|
/>
|
||||||
<Moon
|
<Moon
|
||||||
class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
|
class="w-5 h-5 transition-all scale-0 dark:scale-100"
|
||||||
/>
|
/>
|
||||||
<span class="sr-only">Toggle theme</span>
|
<span class="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
@ -21,13 +21,25 @@ const colorMode = useColorMode()
|
|||||||
align="end"
|
align="end"
|
||||||
class="min-w-min"
|
class="min-w-min"
|
||||||
>
|
>
|
||||||
<DropdownMenuItem @click="colorMode.preference = 'light'">
|
<DropdownMenuItem
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="colorMode.preference = 'light'"
|
||||||
|
>
|
||||||
|
<Sun class="w-4 h-4 mr-1" />
|
||||||
Light
|
Light
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem @click="colorMode.preference = 'dark'">
|
<DropdownMenuItem
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="colorMode.preference = 'dark'"
|
||||||
|
>
|
||||||
|
<Moon class="w-4 h-4 mr-1" />
|
||||||
Dark
|
Dark
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem @click="colorMode.preference = 'system'">
|
<DropdownMenuItem
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="colorMode.preference = 'system'"
|
||||||
|
>
|
||||||
|
<Laptop class="w-4 h-4 mr-1" />
|
||||||
System
|
System
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@ -10,14 +10,14 @@ const route = useRoute()
|
|||||||
@update:model-value="navigateTo"
|
@update:model-value="navigateTo"
|
||||||
>
|
>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="/dashboard">
|
|
||||||
Analysis
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="/dashboard/links"
|
value="/dashboard/links"
|
||||||
>
|
>
|
||||||
Links
|
Links
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="/dashboard/analysis">
|
||||||
|
Analysis
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<slot name="left" />
|
<slot name="left" />
|
||||||
|
|||||||
@ -114,8 +114,13 @@ async function onSubmit(formData) {
|
|||||||
body: link,
|
body: link,
|
||||||
})
|
})
|
||||||
dialogOpen.value = false
|
dialogOpen.value = false
|
||||||
emit('update:link', newLink, isEdit ? 'edit' : 'create')
|
emit('update:link', newLink)
|
||||||
isEdit ? toast('Link updated successfully') : toast('Link created successfully')
|
if (isEdit) {
|
||||||
|
toast('Link updated successfully')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast('Link created successfully')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { previewMode } = useRuntimeConfig().public
|
const { previewMode } = useRuntimeConfig().public
|
||||||
|
|||||||
@ -14,7 +14,7 @@ async function getLinks() {
|
|||||||
cursor,
|
cursor,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
links.value = links.value.concat(data.links)
|
links.value = links.value.concat(data.links).filter(Boolean) // Sometimes cloudflare will return null, filter out
|
||||||
cursor = data.cursor
|
cursor = data.cursor
|
||||||
listComplete = data.list_complete
|
listComplete = data.list_complete
|
||||||
}
|
}
|
||||||
@ -22,7 +22,7 @@ async function getLinks() {
|
|||||||
const { isLoading } = useInfiniteScroll(
|
const { isLoading } = useInfiniteScroll(
|
||||||
document,
|
document,
|
||||||
getLinks,
|
getLinks,
|
||||||
{ distance: 10, interval: 1000, canLoadMore: () => !listComplete },
|
{ distance: 150, interval: 1000, canLoadMore: () => !listComplete },
|
||||||
)
|
)
|
||||||
|
|
||||||
function updateLinkList(link, type) {
|
function updateLinkList(link, type) {
|
||||||
@ -59,5 +59,11 @@ function updateLinkList(link, type) {
|
|||||||
>
|
>
|
||||||
<Loader class="animate-spin" />
|
<Loader class="animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!isLoading && listComplete"
|
||||||
|
class="flex items-center justify-center text-sm"
|
||||||
|
>
|
||||||
|
No more
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="grid gap-8 lg:grid-cols-12">
|
<main class="grid gap-8 lg:grid-cols-12">
|
||||||
<DashboardMetricsLocations class="col-span-1 lg:col-span-8" />
|
<LazyDashboardMetricsLocations class="col-span-1 lg:col-span-8" />
|
||||||
<DashboardMetricsGroup
|
<DashboardMetricsGroup
|
||||||
class="lg:col-span-4"
|
class="lg:col-span-4"
|
||||||
:tabs="['country', 'region', 'city']"
|
:tabs="['country', 'region', 'city']"
|
||||||
|
|||||||
@ -1,26 +1,19 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { VisSingleContainer, VisTopoJSONMap, VisTopoJSONMapSelectors } from '@unovis/vue'
|
import { VisSingleContainer, VisTopoJSONMap, VisTopoJSONMapSelectors } from '@unovis/vue'
|
||||||
import { WorldMapSimplestTopoJSON } from '@unovis/ts/maps'
|
|
||||||
import WorldMapTopoJSON from '@/assets/location/world-topo.json' // https://github.com/apache/echarts/blob/master/test/data/map/json/world.json
|
|
||||||
import { ChartTooltip } from '@/components/ui/chart'
|
import { ChartTooltip } from '@/components/ui/chart'
|
||||||
|
|
||||||
WorldMapTopoJSON.objects.states.geometries.map((state) => {
|
|
||||||
const name = state.properties.name
|
|
||||||
const country = WorldMapSimplestTopoJSON.objects.countries.geometries.find(country => country.properties.name === name)
|
|
||||||
state.id = state.name || ''
|
|
||||||
if (country) {
|
|
||||||
state.id = country.id || ''
|
|
||||||
state.properties = country.properties
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
|
|
||||||
const id = inject('id')
|
const id = inject('id')
|
||||||
const startAt = inject('startAt')
|
const startAt = inject('startAt')
|
||||||
const endAt = inject('endAt')
|
const endAt = inject('endAt')
|
||||||
|
|
||||||
|
const worldMapTopoJSON = ref({})
|
||||||
const areaData = ref([])
|
const areaData = ref([])
|
||||||
|
|
||||||
|
async function getWorldMapJSON() {
|
||||||
|
const data = await $fetch('/world.json')
|
||||||
|
worldMapTopoJSON.value = data
|
||||||
|
}
|
||||||
|
|
||||||
async function getMapData() {
|
async function getMapData() {
|
||||||
areaData.value = []
|
areaData.value = []
|
||||||
const { data } = await useAPI('/api/stats/metrics', {
|
const { data } = await useAPI('/api/stats/metrics', {
|
||||||
@ -42,6 +35,7 @@ async function getMapData() {
|
|||||||
const stopWatchTime = watch([startAt, endAt], getMapData)
|
const stopWatchTime = watch([startAt, endAt], getMapData)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
getWorldMapJSON()
|
||||||
getMapData()
|
getMapData()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -72,12 +66,13 @@ const Tooltip = {
|
|||||||
<CardContent class="flex-1 flex [&_[data-radix-aspect-ratio-wrapper]]:flex-1">
|
<CardContent class="flex-1 flex [&_[data-radix-aspect-ratio-wrapper]]:flex-1">
|
||||||
<AspectRatio :ratio="65 / 30">
|
<AspectRatio :ratio="65 / 30">
|
||||||
<VisSingleContainer
|
<VisSingleContainer
|
||||||
|
v-if="worldMapTopoJSON.type"
|
||||||
:data="{ areas: areaData }"
|
:data="{ areas: areaData }"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
>
|
>
|
||||||
<VisTopoJSONMap
|
<VisTopoJSONMap
|
||||||
:topojson="WorldMapTopoJSON"
|
:topojson="worldMapTopoJSON"
|
||||||
map-feature-name="states"
|
map-feature-name="countries"
|
||||||
/>
|
/>
|
||||||
<ChartSingleTooltip
|
<ChartSingleTooltip
|
||||||
index="id"
|
index="id"
|
||||||
|
|||||||
@ -1,26 +1,29 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
// https://vue3-simple-icons.wyatt-herkamp.dev/
|
// https://vue3-simple-icons.wyatt-herkamp.dev/
|
||||||
import {
|
import {
|
||||||
AppleIcon,
|
|
||||||
AndroidIcon,
|
AndroidIcon,
|
||||||
|
AppleIcon,
|
||||||
DebianIcon,
|
DebianIcon,
|
||||||
FacebookIcon,
|
FacebookIcon,
|
||||||
FirefoxBrowserIcon,
|
FirefoxBrowserIcon,
|
||||||
|
GnuIcon,
|
||||||
GoogleChromeIcon,
|
GoogleChromeIcon,
|
||||||
GoogleIcon,
|
GoogleIcon,
|
||||||
HuaweiIcon,
|
HuaweiIcon,
|
||||||
IOsIcon,
|
IOsIcon,
|
||||||
InternetExplorerIcon,
|
// InternetExplorerIcon,
|
||||||
LinuxIcon,
|
LinuxIcon,
|
||||||
MacOsIcon,
|
MacOsIcon,
|
||||||
MicrosoftEdgeIcon,
|
// MicrosoftEdgeIcon,
|
||||||
OperaIcon,
|
OperaIcon,
|
||||||
SafariIcon,
|
SafariIcon,
|
||||||
SamsungIcon,
|
SamsungIcon,
|
||||||
UbuntuIcon,
|
UbuntuIcon,
|
||||||
VivoIcon,
|
VivoIcon,
|
||||||
WeChatIcon,
|
WeChatIcon,
|
||||||
WindowsIcon,
|
WearOsIcon,
|
||||||
|
// WindowsIcon,
|
||||||
|
XIcon,
|
||||||
XiaomiIcon,
|
XiaomiIcon,
|
||||||
YandexCloudIcon,
|
YandexCloudIcon,
|
||||||
} from 'vue3-simple-icons'
|
} from 'vue3-simple-icons'
|
||||||
@ -47,6 +50,7 @@ defineProps({
|
|||||||
|
|
||||||
const iconMaps = {
|
const iconMaps = {
|
||||||
'android': AndroidIcon,
|
'android': AndroidIcon,
|
||||||
|
'android browser': AndroidIcon,
|
||||||
'browser': Globe,
|
'browser': Globe,
|
||||||
'chrome': GoogleChromeIcon,
|
'chrome': GoogleChromeIcon,
|
||||||
'chrome headless': GoogleChromeIcon,
|
'chrome headless': GoogleChromeIcon,
|
||||||
@ -55,15 +59,15 @@ const iconMaps = {
|
|||||||
'curl': Terminal,
|
'curl': Terminal,
|
||||||
'debian': DebianIcon,
|
'debian': DebianIcon,
|
||||||
'desktop': MonitorCheck,
|
'desktop': MonitorCheck,
|
||||||
'edge': MicrosoftEdgeIcon,
|
|
||||||
'facebook': FacebookIcon,
|
'facebook': FacebookIcon,
|
||||||
'facebookexternalhit': FacebookIcon,
|
'facebookexternalhit': FacebookIcon,
|
||||||
'firefox': FirefoxBrowserIcon,
|
'firefox': FirefoxBrowserIcon,
|
||||||
'googlebot': GoogleIcon,
|
'googlebot': GoogleIcon,
|
||||||
'googlebot-image': GoogleIcon,
|
'googlebot-image': GoogleIcon,
|
||||||
|
'gnu': GnuIcon,
|
||||||
'harmonyos': HuaweiIcon,
|
'harmonyos': HuaweiIcon,
|
||||||
'huawei browser': HuaweiIcon,
|
'huawei browser': HuaweiIcon,
|
||||||
'ie': InternetExplorerIcon,
|
// 'ie': InternetExplorerIcon,
|
||||||
'ios': IOsIcon,
|
'ios': IOsIcon,
|
||||||
'ipad': AppleIcon,
|
'ipad': AppleIcon,
|
||||||
'iphone': AppleIcon,
|
'iphone': AppleIcon,
|
||||||
@ -82,10 +86,11 @@ const iconMaps = {
|
|||||||
'safari': SafariIcon,
|
'safari': SafariIcon,
|
||||||
'samsung internet': SamsungIcon,
|
'samsung internet': SamsungIcon,
|
||||||
'tablet': Tablet,
|
'tablet': Tablet,
|
||||||
|
'twitterbot': XIcon,
|
||||||
'ubuntu': UbuntuIcon,
|
'ubuntu': UbuntuIcon,
|
||||||
'vivo browser': VivoIcon,
|
'vivo browser': VivoIcon,
|
||||||
'wechat': WeChatIcon,
|
'wechat': WeChatIcon,
|
||||||
'windows': WindowsIcon,
|
'wearable': WearOsIcon,
|
||||||
'yandex': YandexCloudIcon,
|
'yandex': YandexCloudIcon,
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
class="flex flex-col items-center max-w-4xl p-8 mx-auto my-12 text-center bg-black rounded-lg md:px-20 md:py-20"
|
class="flex flex-col items-center max-w-4xl p-8 mx-auto my-12 text-center bg-black rounded-lg md:px-20 md:py-20"
|
||||||
>
|
>
|
||||||
<h2 class="text-4xl tracking-tight text-white md:text-6xl">
|
<h2 class="text-4xl tracking-tight text-white md:text-6xl">
|
||||||
Deployment immediately.
|
Deployment immediately
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-4 text-lg text-slate-400 md:text-xl">
|
<p class="mt-4 text-lg text-slate-400 md:text-xl">
|
||||||
With just a few simple clicks, you can start deploying without any expenses.
|
With just a few simple clicks, you can start deploying without any expenses.
|
||||||
@ -12,6 +12,7 @@
|
|||||||
<HomeLink
|
<HomeLink
|
||||||
href="https://github.com/ccbikai/sink?tab=readme-ov-file#%EF%B8%8F-deployment"
|
href="https://github.com/ccbikai/sink?tab=readme-ov-file#%EF%B8%8F-deployment"
|
||||||
type="inverted"
|
type="inverted"
|
||||||
|
title="Start Deploy"
|
||||||
>
|
>
|
||||||
Start Deploy
|
Start Deploy
|
||||||
</HomeLink>
|
</HomeLink>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Link, AreaChart, ServerOff, Paintbrush, Sparkles, Hourglass } from 'lucide-vue-next'
|
import { AreaChart, Hourglass, Link, Paintbrush, ServerOff, Sparkles } from 'lucide-vue-next'
|
||||||
|
|
||||||
const features = ref([
|
const features = ref([
|
||||||
{
|
{
|
||||||
|
|||||||
@ -23,6 +23,7 @@ const { title, description } = useAppConfig()
|
|||||||
<HomeLink
|
<HomeLink
|
||||||
href="/dashboard"
|
href="/dashboard"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
title="Dashboard"
|
||||||
class="flex items-center justify-center gap-1"
|
class="flex items-center justify-center gap-1"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
@ -36,6 +37,7 @@ const { title, description } = useAppConfig()
|
|||||||
type="outline"
|
type="outline"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
href="https://github.com/ccbikai/sink"
|
href="https://github.com/ccbikai/sink"
|
||||||
|
title="Github"
|
||||||
class="flex items-center justify-center gap-1"
|
class="flex items-center justify-center gap-1"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -5,8 +5,9 @@ import { ArrowRight } from 'lucide-vue-next'
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a
|
<a
|
||||||
href="https://x.com/ccbikai"
|
href="https://x.com/0xKaiBi"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
title="X(Twitter)"
|
||||||
class="inline-flex items-center px-3 py-1 mx-auto my-4 space-x-1 text-sm font-medium rounded-lg bg-muted"
|
class="inline-flex items-center px-3 py-1 mx-auto my-4 space-x-1 text-sm font-medium rounded-lg bg-muted"
|
||||||
>
|
>
|
||||||
<XIcon class="w-4 h-4" />
|
<XIcon class="w-4 h-4" />
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { GmailIcon, TelegramIcon, BloggerIcon, XIcon, MastodonIcon, GitHubIcon } from 'vue3-simple-icons'
|
import { BloggerIcon, GitHubIcon, GmailIcon, MastodonIcon, TelegramIcon, XIcon } from 'vue3-simple-icons'
|
||||||
|
|
||||||
const email = ref(null)
|
const email = ref(null)
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -12,12 +12,14 @@ onMounted(() => {
|
|||||||
<div class="container flex flex-col items-center py-8 mx-auto sm:flex-row">
|
<div class="container flex flex-col items-center py-8 mx-auto sm:flex-row">
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class="text-xl font-black leading-none text-gray-900 dark:text-gray-100 select-none logo"
|
class="text-xl font-black leading-none text-gray-900 select-none dark:text-gray-100 logo"
|
||||||
|
title="Sink"
|
||||||
>Sink</a>
|
>Sink</a>
|
||||||
<a
|
<a
|
||||||
class="mt-4 text-sm text-gray-500 sm:ml-4 sm:pl-4 sm:border-l sm:border-gray-200 sm:mt-0"
|
class="mt-4 text-sm text-gray-500 sm:ml-4 sm:pl-4 sm:border-l sm:border-gray-200 sm:mt-0"
|
||||||
href="https://html.zone"
|
href="https://html.zone"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
title="HTML.ZONE"
|
||||||
>
|
>
|
||||||
© {{ new Date().getFullYear() }} Products of HTML.ZONE
|
© {{ new Date().getFullYear() }} Products of HTML.ZONE
|
||||||
</a>
|
</a>
|
||||||
@ -59,7 +61,7 @@ onMounted(() => {
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://x.com/ccbikai"
|
href="https://x.com/0xKaiBi"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="Twitter"
|
title="Twitter"
|
||||||
class="text-gray-400 hover:text-gray-500"
|
class="text-gray-400 hover:text-gray-500"
|
||||||
|
|||||||
@ -15,6 +15,7 @@ const showMenu = ref(false)
|
|||||||
<div class="flex items-center justify-start w-1/4 h-full pr-4">
|
<div class="flex items-center justify-start w-1/4 h-full pr-4">
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
|
title="Sink"
|
||||||
class="flex items-center py-4 space-x-2 text-xl font-black text-gray-900 dark:text-gray-100 md:py-0"
|
class="flex items-center py-4 space-x-2 text-xl font-black text-gray-900 dark:text-gray-100 md:py-0"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -40,7 +41,7 @@ const showMenu = ref(false)
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
target="_blank"
|
title="Sink"
|
||||||
class="inline-flex items-center w-auto h-16 px-4 text-xl font-black leading-none text-gray-900 dark:text-gray-100 md:hidden"
|
class="inline-flex items-center w-auto h-16 px-4 text-xl font-black leading-none text-gray-900 dark:text-gray-100 md:hidden"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -61,17 +62,19 @@ const showMenu = ref(false)
|
|||||||
<a
|
<a
|
||||||
class="w-full px-6 py-2 mr-0 text-gray-700 cursor-pointer dark:text-gray-300 md:px-3 md:mr-2 lg:mr-3 md:w-auto"
|
class="w-full px-6 py-2 mr-0 text-gray-700 cursor-pointer dark:text-gray-300 md:px-3 md:mr-2 lg:mr-3 md:w-auto"
|
||||||
href="/dashboard"
|
href="/dashboard"
|
||||||
|
title="Sink Dashboard"
|
||||||
>Dashboard</a>
|
>Dashboard</a>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/ccbikai/sink"
|
href="https://github.com/ccbikai/sink"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
title="Github"
|
||||||
class="inline-flex items-center w-full px-6 py-3 text-sm font-medium leading-4 text-white bg-gray-900 md:px-3 md:w-auto md:rounded-full hover:bg-gray-800 focus:outline-none md:focus:ring-2 focus:ring-0 focus:ring-offset-2 focus:ring-gray-800"
|
class="inline-flex items-center w-full px-6 py-3 text-sm font-medium leading-4 text-white bg-gray-900 md:px-3 md:w-auto md:rounded-full hover:bg-gray-800 focus:outline-none md:focus:ring-2 focus:ring-0 focus:ring-offset-2 focus:ring-gray-800"
|
||||||
>
|
>
|
||||||
<GitHubIcon
|
<GitHubIcon
|
||||||
class="w-5 h-5 mr-1"
|
class="w-5 h-5 mr-1"
|
||||||
/>
|
/>
|
||||||
GitHub</a>
|
GitHub</a>
|
||||||
<span class="px-2 py-2">
|
<span class="ml-1">
|
||||||
<SwitchTheme />
|
<SwitchTheme />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -79,7 +82,8 @@ const showMenu = ref(false)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="absolute right-0 flex flex-col items-center justify-center w-10 cursor-pointer h-10rounded-full md:hidden"
|
class="absolute right-0 flex flex-col items-center justify-center w-10 h-10 rounded-full cursor-pointer md:hidden hover:bg-muted"
|
||||||
|
:class="{ 'right-2': showMenu }"
|
||||||
@click="showMenu = !showMenu"
|
@click="showMenu = !showMenu"
|
||||||
>
|
>
|
||||||
<Ellipsis
|
<Ellipsis
|
||||||
|
|||||||
53
docs/api.md
Normal file
53
docs/api.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Sink API
|
||||||
|
|
||||||
|
Writing API documentation manually can be very laborious, and we will automatically generate documents after the official release of [Nitro's OpenAPI](https://nitro.unjs.io/config#openapi).
|
||||||
|
|
||||||
|
This place provides an example of creating a short link API. Other APIs are currently available for viewing through browser developer tools.
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Create Short Link
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/link/create
|
||||||
|
```
|
||||||
|
|
||||||
|
| Header | Description |
|
||||||
|
| :----- | :------------------------- |
|
||||||
|
| `authorization` | `Bearer SinkCool` |
|
||||||
|
| `content-type` | `application/json` |
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/link/create
|
||||||
|
HEADER authorization: Bearer SinkCool
|
||||||
|
HEADER content-type: application/json
|
||||||
|
BODY {
|
||||||
|
"url": "https://github.com/ccbikai/Sink/issues/14",
|
||||||
|
"slug": "issue14"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The BODY data must be JSON.
|
||||||
|
|
||||||
|
```http
|
||||||
|
RESPONSE 201
|
||||||
|
BODY {
|
||||||
|
"link": {
|
||||||
|
"id": "xpqhaurv5q",
|
||||||
|
"url": "https://github.com/ccbikai/Sink/issues/14",
|
||||||
|
"slug": "issue14",
|
||||||
|
"createdAt": 1718119809,
|
||||||
|
"updatedAt": 1718119809
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| :-------- | :------- | :------------------------- |
|
||||||
|
| `id` | `string` | This is automatically generated by Sink |
|
||||||
|
| `url` | `string` | This is confirmation of the submitted URL and is required. |
|
||||||
|
| `slug` | `string` | This is slug generated by the system, either automatically or from the input (if provided) |
|
||||||
|
| `createdAt` | `timestamp` | This is automatically generated with a UNIX Timestamp. |
|
||||||
|
| `updatedAt` | `timestamp` | This is automatically generated with a UNIX Timestamp. |
|
||||||
@ -14,6 +14,14 @@ Sets the default length of the generated SLUG.
|
|||||||
|
|
||||||
Redirects default to use HTTP 301 status code, you can set it to `302`/`307`/`308`.
|
Redirects default to use HTTP 301 status code, you can set it to `302`/`307`/`308`.
|
||||||
|
|
||||||
|
## `NUXT_LINK_CACHE_TTL`
|
||||||
|
|
||||||
|
Cache links can speed up access, but setting them too long may result in slow changes taking effect. The default value is 60 seconds.
|
||||||
|
|
||||||
|
## `NUXT_REDIRECT_WITH_QUERY`
|
||||||
|
|
||||||
|
URL parameters are not carried during link redirection by default and it is not recommended to enable this feature.
|
||||||
|
|
||||||
## `NUXT_HOME_URL`
|
## `NUXT_HOME_URL`
|
||||||
|
|
||||||
The default Sink homepage is the introduction page, you can replace it with your own website.
|
The default Sink homepage is the introduction page, you can replace it with your own website.
|
||||||
|
|||||||
31
docs/faqs.md
Normal file
31
docs/faqs.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# FAQs
|
||||||
|
|
||||||
|
## 1. Why can't I create a link?
|
||||||
|
|
||||||
|
Please check the Cloudflare KV bindings, the KV environment variable name should be all uppercase letters.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Screenshots</b></summary>
|
||||||
|
<img alt="KV" src="https://github.com/ccbikai/Sink/assets/21292149/3b7d584c-3afe-4d24-8c9e-e0d549c47438"/>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 2. Why can't I log in?
|
||||||
|
|
||||||
|
Please check if `NUXT_SITE_TOKEN` is set to pure numbers, Sink does not support pure number Tokens, we consider this to be unsafe.
|
||||||
|
|
||||||
|
## 3. Why can't I see the analytics data?
|
||||||
|
|
||||||
|
Analytics data needs to read Cloudflare's data, check if `NUXT_CF_ACCOUNT_ID` and `NUXT_CF_API_TOKEN` are correctly configured. Pay attention to the account id deployment zone id, whether the Worker analytics engine is turned on.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Screenshots</b></summary>
|
||||||
|
<img alt="Analytics" src="https://github.com/ccbikai/Sink/assets/21292149/aceede26-5aa1-400c-8d06-d6adc46bdcb8"/>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 4. I don't want the current homepage? Can it be redirected to my blog?
|
||||||
|
|
||||||
|
Of course. Please set the environment variable `NUXT_HOME_URL` to your blog or official website address.
|
||||||
|
|
||||||
|
## 5. Why can't I see the statistics after deploying with NuxtHub?
|
||||||
|
|
||||||
|
NuxtHub's ANALYTICS points to its dataset, you need to set the `NUXT_DATASET` environment variable to point to the same dataset.
|
||||||
@ -1,11 +1,11 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
// import antfu from '@antfu/eslint-config'
|
import antfu from '@antfu/eslint-config'
|
||||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
|
||||||
export default withNuxt(
|
export default withNuxt(
|
||||||
// antfu(),
|
antfu(),
|
||||||
{
|
{
|
||||||
ignores: ['components/ui'],
|
ignores: ['components/ui', '.data', 'public/world.json'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// 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 },
|
devtools: { enabled: true },
|
||||||
|
|
||||||
modules: [
|
modules: [
|
||||||
'@nuxthub/core',
|
'@nuxthub/core',
|
||||||
'shadcn-nuxt',
|
'shadcn-nuxt',
|
||||||
@ -8,9 +9,11 @@ export default defineNuxtConfig({
|
|||||||
'@nuxtjs/tailwindcss',
|
'@nuxtjs/tailwindcss',
|
||||||
'@nuxtjs/color-mode',
|
'@nuxtjs/color-mode',
|
||||||
],
|
],
|
||||||
|
|
||||||
colorMode: {
|
colorMode: {
|
||||||
classSuffix: '',
|
classSuffix: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
routeRules: {
|
routeRules: {
|
||||||
'/': {
|
'/': {
|
||||||
prerender: true,
|
prerender: true,
|
||||||
@ -18,38 +21,50 @@ export default defineNuxtConfig({
|
|||||||
'/dashboard/**': {
|
'/dashboard/**': {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
},
|
},
|
||||||
|
'/dashboard': {
|
||||||
|
redirect: '/dashboard/links',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
hub: {
|
hub: {
|
||||||
|
ai: true,
|
||||||
analytics: true,
|
analytics: true,
|
||||||
blob: false,
|
blob: false,
|
||||||
cache: false,
|
cache: false,
|
||||||
database: false,
|
database: false,
|
||||||
kv: true,
|
kv: true,
|
||||||
// ai: true,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
eslint: {
|
eslint: {
|
||||||
config: {
|
config: {
|
||||||
stylistic: true,
|
stylistic: true,
|
||||||
|
standalone: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
nitro: {
|
nitro: {
|
||||||
experimental: {
|
experimental: {
|
||||||
// Enable Server API documentation within NuxtHub
|
// Enable Server API documentation within NuxtHub
|
||||||
openAPI: true,
|
openAPI: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
siteToken: 'SinkCool',
|
siteToken: 'SinkCool',
|
||||||
redirectStatusCode: '301',
|
redirectStatusCode: '301',
|
||||||
|
linkCacheTtl: 60,
|
||||||
|
redirectWithQuery: false,
|
||||||
homeURL: '',
|
homeURL: '',
|
||||||
cfAccountId: '',
|
cfAccountId: '',
|
||||||
cfApiToken: '',
|
cfApiToken: '',
|
||||||
dataset: 'sink',
|
dataset: 'sink',
|
||||||
aiModel: '@cf/meta/llama-3-8b-instruct',
|
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"}`,
|
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"}`,
|
||||||
public: {
|
public: {
|
||||||
previewMode: '',
|
previewMode: '',
|
||||||
slugDefaultLength: '6',
|
slugDefaultLength: '6',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
compatibilityDate: '2024-07-08',
|
||||||
})
|
})
|
||||||
|
|||||||
54
package.json
54
package.json
@ -1,15 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "sink",
|
"name": "sink",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.1.1",
|
"version": "0.1.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@9.1.2",
|
"packageManager": "pnpm@9.7.1",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.11"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
|
"build:map": "node scripts/build-map.js",
|
||||||
"preview": "wrangler pages dev dist",
|
"preview": "wrangler pages dev dist",
|
||||||
"deploy": "wrangler pages deploy dist",
|
"deploy": "wrangler pages deploy dist",
|
||||||
"postinstall": "nuxt prepare",
|
"postinstall": "npm run build:map && nuxt prepare",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"typecheck": "nuxt typecheck",
|
"typecheck": "nuxt typecheck",
|
||||||
@ -17,42 +21,42 @@
|
|||||||
"lint-staged": "lint-staged"
|
"lint-staged": "lint-staged"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@unovis/ts": "^1.4.1",
|
"@unovis/ts": "^1.4.4",
|
||||||
"@unovis/vue": "^1.4.1",
|
"@unovis/vue": "^1.4.4",
|
||||||
"@vee-validate/zod": "^4.12.8",
|
"@vee-validate/zod": "^4.13.2",
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^11.0.0",
|
||||||
"intl-parse-accept-language": "^1.0.0",
|
"intl-parse-accept-language": "^1.0.0",
|
||||||
"lucide-vue-next": "^0.379.0",
|
"lucide-vue-next": "^0.428.0",
|
||||||
"mysql-bricks": "^1.1.2",
|
"mysql-bricks": "^1.1.2",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"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.8.1",
|
"radix-vue": "^1.9.4",
|
||||||
"ua-parser-js": "next",
|
"ua-parser-js": "next",
|
||||||
"vee-validate": "^4.12.8",
|
"vee-validate": "^4.13.2",
|
||||||
"virtua": "^0.31.0",
|
"virtua": "^0.33.7",
|
||||||
"vue-sonner": "^1.1.2",
|
"vue-sonner": "^1.1.4",
|
||||||
"vue3-simple-icons": "^11.13.0",
|
"vue3-simple-icons": "^13.2.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^2.18.1",
|
"@antfu/eslint-config": "^2.26.0",
|
||||||
"@nuxt/eslint": "^0.3.13",
|
"@nuxt/eslint": "^0.5.0",
|
||||||
"@nuxt/eslint-config": "^0.3.13",
|
"@nuxt/eslint-config": "^0.5.0",
|
||||||
"@nuxthub/core": "^0.5.17",
|
"@nuxthub/core": "^0.7.3",
|
||||||
"@nuxtjs/color-mode": "^3.4.1",
|
"@nuxtjs/color-mode": "^3.4.4",
|
||||||
"@nuxtjs/tailwindcss": "^6.12.0",
|
"@nuxtjs/tailwindcss": "^6.12.1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^9.9.0",
|
||||||
"lint-staged": "^15.2.4",
|
"lint-staged": "^15.2.9",
|
||||||
"nuxt": "^3.11.2",
|
"nuxt": "^3.12.4",
|
||||||
"shadcn-nuxt": "^0.10.4",
|
"shadcn-nuxt": "^0.10.4",
|
||||||
"simple-git-hooks": "^2.11.1",
|
"simple-git-hooks": "^2.11.1",
|
||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vue-tsc": "^2.0.19",
|
"vue-tsc": "^2.0.29",
|
||||||
"wrangler": "^3.57.1"
|
"wrangler": "^3.72.0"
|
||||||
},
|
},
|
||||||
"simple-git-hooks": {
|
"simple-git-hooks": {
|
||||||
"pre-commit": "npm run lint-staged"
|
"pre-commit": "npm run lint-staged"
|
||||||
|
|||||||
@ -17,10 +17,11 @@ async function getLink() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateLink(link, type) {
|
function updateLink(link, type) {
|
||||||
if (type === 'delete')
|
if (type === 'delete') {
|
||||||
navigateTo('/dashboard/links', {
|
navigateTo('/dashboard/links', {
|
||||||
replace: true,
|
replace: true,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
6329
pnpm-lock.yaml
6329
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
BIN
public/icon-192.png
Normal file
BIN
public/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@ -5,7 +5,7 @@ const { slugRegex } = useAppConfig()
|
|||||||
|
|
||||||
const slugDefaultLength = +useRuntimeConfig().public.slugDefaultLength
|
const slugDefaultLength = +useRuntimeConfig().public.slugDefaultLength
|
||||||
|
|
||||||
export const nanoid = (length: number = slugDefaultLength) => customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', length)
|
export const nanoid = (length: number = slugDefaultLength) => 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)),
|
||||||
|
|||||||
5
scripts/build-map.js
Normal file
5
scripts/build-map.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { writeFileSync } from 'node:fs'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { WorldMapSimplestTopoJSON } from '@unovis/ts/maps.js'
|
||||||
|
|
||||||
|
writeFileSync(join(import.meta.dirname, '../public/world.json'), JSON.stringify(WorldMapSimplestTopoJSON), 'utf8')
|
||||||
@ -31,7 +31,8 @@ export default eventHandler(async (event) => {
|
|||||||
content: url,
|
content: url,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
const { response } = await AI.run(aiModel, { messages })
|
// @ts-expect-error Workers AI is not typed
|
||||||
|
const { response } = await hubAI().run(aiModel, { messages })
|
||||||
return destr(response)
|
return destr(response)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export default eventHandler(async (event) => {
|
|||||||
statusText: 'Link already exists',
|
statusText: 'Link already exists',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
const expiration = getExpiration(event, link.expiration)
|
const expiration = getExpiration(event, link.expiration)
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import type { z } from 'zod'
|
import type { z } from 'zod'
|
||||||
import { parsePath } from 'ufo'
|
import { parsePath, withQuery } from 'ufo'
|
||||||
import type { LinkSchema } from '@/schemas/link'
|
import type { LinkSchema } from '@/schemas/link'
|
||||||
|
|
||||||
export default eventHandler(async (event) => {
|
export default eventHandler(async (event) => {
|
||||||
const { pathname: slug } = parsePath(event.path.slice(1)) // remove leading slash
|
const { pathname: slug } = parsePath(event.path.slice(1)) // remove leading slash
|
||||||
const { slugRegex, reserveSlug } = useAppConfig(event)
|
const { slugRegex, reserveSlug } = useAppConfig(event)
|
||||||
const { homeURL } = useRuntimeConfig(event)
|
const { homeURL, linkCacheTtl, redirectWithQuery } = useRuntimeConfig(event)
|
||||||
const { cloudflare } = event.context
|
const { cloudflare } = event.context
|
||||||
|
|
||||||
if (event.path === '/' && homeURL)
|
if (event.path === '/' && homeURL)
|
||||||
@ -13,7 +13,7 @@ export default eventHandler(async (event) => {
|
|||||||
|
|
||||||
if (slug && !reserveSlug.includes(slug) && slugRegex.test(slug) && cloudflare) {
|
if (slug && !reserveSlug.includes(slug) && slugRegex.test(slug) && cloudflare) {
|
||||||
const { KV } = cloudflare.env
|
const { KV } = cloudflare.env
|
||||||
const link: z.infer<typeof LinkSchema> | null = await KV.get(`link:${slug}`, { type: 'json' })
|
const link: z.infer<typeof LinkSchema> | null = await KV.get(`link:${slug}`, { type: 'json', cacheTtl: linkCacheTtl })
|
||||||
if (link) {
|
if (link) {
|
||||||
event.context.link = link
|
event.context.link = link
|
||||||
try {
|
try {
|
||||||
@ -22,7 +22,8 @@ export default eventHandler(async (event) => {
|
|||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('Failed write access log:', error)
|
console.error('Failed write access log:', error)
|
||||||
}
|
}
|
||||||
return sendRedirect(event, link.url, +useRuntimeConfig(event).redirectStatusCode)
|
const target = redirectWithQuery ? withQuery(link.url, getQuery(event)) : link.url
|
||||||
|
return sendRedirect(event, target, +useRuntimeConfig(event).redirectStatusCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,11 +2,12 @@ import type { H3Event } from 'h3'
|
|||||||
import { parseURL } from 'ufo'
|
import { parseURL } from 'ufo'
|
||||||
import { UAParser } from 'ua-parser-js'
|
import { UAParser } from 'ua-parser-js'
|
||||||
import {
|
import {
|
||||||
Apps,
|
|
||||||
Bots,
|
|
||||||
CLIs,
|
CLIs,
|
||||||
|
Crawlers,
|
||||||
Emails,
|
Emails,
|
||||||
ExtraDevices,
|
ExtraDevices,
|
||||||
|
Fetchers,
|
||||||
|
InApps,
|
||||||
MediaPlayers,
|
MediaPlayers,
|
||||||
Modules,
|
Modules,
|
||||||
} from 'ua-parser-js/extensions'
|
} from 'ua-parser-js/extensions'
|
||||||
@ -68,7 +69,7 @@ export function useAccessLog(event: H3Event) {
|
|||||||
|
|
||||||
const userAgent = getHeader(event, 'user-agent') || ''
|
const userAgent = getHeader(event, 'user-agent') || ''
|
||||||
const uaInfo = (new UAParser(userAgent, {
|
const uaInfo = (new UAParser(userAgent, {
|
||||||
browser: [Apps.browser || [], Bots.browser || [], CLIs.browser || [], Emails.browser || [], MediaPlayers.browser || [], Modules.browser || []].flat(),
|
browser: [Crawlers.browser || [], CLIs.browser || [], Emails.browser || [], Fetchers.browser || [], InApps.browser || [], MediaPlayers.browser || [], Modules.browser || []].flat(),
|
||||||
device: [ExtraDevices.device || []].flat(),
|
device: [ExtraDevices.device || []].flat(),
|
||||||
})).getResult()
|
})).getResult()
|
||||||
|
|
||||||
@ -90,7 +91,6 @@ export function useAccessLog(event: H3Event) {
|
|||||||
language,
|
language,
|
||||||
os: uaInfo?.os?.name,
|
os: uaInfo?.os?.name,
|
||||||
browser: uaInfo?.browser?.name,
|
browser: uaInfo?.browser?.name,
|
||||||
// @ts-expect-error todo
|
|
||||||
browserType: uaInfo?.browser?.type,
|
browserType: uaInfo?.browser?.type,
|
||||||
device: uaInfo?.device?.model,
|
device: uaInfo?.device?.model,
|
||||||
deviceType: uaInfo?.device?.type,
|
deviceType: uaInfo?.device?.type,
|
||||||
|
|||||||
@ -7,6 +7,10 @@ export function useAPI(api: string, options?: object): Promise<unknown> {
|
|||||||
Authorization: `Bearer ${localStorage.getItem('SinkSiteToken') || ''}`,
|
Authorization: `Bearer ${localStorage.getItem('SinkSiteToken') || ''}`,
|
||||||
},
|
},
|
||||||
})).catch((error) => {
|
})).catch((error) => {
|
||||||
|
if (error?.status === 401) {
|
||||||
|
localStorage.removeItem('SinkSiteToken')
|
||||||
|
navigateTo('/dashboard/login')
|
||||||
|
}
|
||||||
if (error?.data?.statusMessage) {
|
if (error?.data?.statusMessage) {
|
||||||
toast(error?.data?.statusMessage)
|
toast(error?.data?.statusMessage)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user