Compare commits

...

72 Commits

Author SHA1 Message Date
ccbikai
12e3e7c7bc 0.1.4 2024-08-20 21:25:30 +08:00
ccbikai
233d366d6e Merge branch 'dev' 2024-08-20 21:25:06 +08:00
ccbikai
05eb3bfca3 feat: enhance error handling for unauthorized API calls
Implements automatic token removal and redirection to login upon 401 error status, improving user experience and security.
2024-08-20 21:24:32 +08:00
ccbikai
d8c92aa7a1 Merge branch 'preview' into dev 2024-08-18 20:06:16 +08:00
ccbikai
f2a2e00ce6 feat: add XIcon to dashboard metrics 2024-08-18 20:05:33 +08:00
ccbikai
20eed15967 Merge branch 'dev' into preview 2024-08-18 19:43:36 +08:00
ccbikai
53a6b5d403 refactor: remove duplicate icon mappings for clarity
Eliminated redundant entries in iconMaps to streamline the mapping process and enhance readability. This consolidation reduces clutter and ensures a more efficient and maintainable codebase.
2024-08-18 19:43:19 +08:00
ccbikai
36e7962b83 chore: update dependencies 2024-08-18 19:38:04 +08:00
ccbikai
b116d4c007 feat: enable AI integration and update dependencies
- Activated AI functionality in Nuxt configuration
- Updated AI model to a newer version for enhanced performance
- Bumped @nuxthub/core to 0.7.3 for critical bug fixes and improvements
2024-08-18 19:01:14 +08:00
ccbikai
55783573ef Merge branch 'master' into dev 2024-08-18 18:40:09 +08:00
面条
ef3e704d79
Merge pull request #28 from fqd511/feature/improve-slug-readability
feat: improve slug readability
2024-07-30 20:12:47 +08:00
面条
5cf1e89ce4
Merge branch 'master' into feature/improve-slug-readability 2024-07-30 20:12:34 +08:00
ccbikai
6d0a67d9b5 feat: enhance nanoid character set for better readability
Adjusted the character set used by nanoid to exclude ambiguous characters, improving readability and reducing potential user confusion.
2024-07-28 13:40:25 +08:00
ccbikai
b2e859107d chore: Update Twitter handle to reflect new username
Update Twitter URL across multiple files to point to the new username, ensuring consistency and accuracy in social media links.
2024-07-28 13:39:32 +08:00
v
08ed8da522 feat: improve slug readability
(cherry picked from commit d950387f8cc87b166fd24bd75276bf34e1c5daa5)
2024-07-26 19:22:47 +08:00
ccbikai
2856c4013f 0.1.3 2024-07-21 20:51:29 +08:00
ccbikai
d7fce2eac0 Merge branch 'dev' 2024-07-21 20:51:17 +08:00
ccbikai
576eee43ca Merge branch 'dev' into preview 2024-07-20 20:23:37 +08:00
ccbikai
52187d1ff6 feat: disable query string redirection by default
Enhances security and performance by preventing query strings from being carried over during redirection, aligning with best practices.
2024-07-20 18:34:49 +08:00
ccbikai
2876385f20 feat: add link cache TTL for performance optimization
Improves response times by introducing a configurable link cache TTL, defaulting to 60 seconds, to ensure quick access to frequently requested links while maintaining responsiveness to updates.
2024-07-20 18:21:40 +08:00
ccbikai
3c0a7be6eb style: remove unnecessary newline for cleaner code 2024-07-20 18:13:05 +08:00
ccbikai
8f8865801a feat: redirect dashboard to analysis and update nav link
Enhances navigation by redirecting the main dashboard to the analysis page and updating the navigation link to directly access the analysis section. This change streamlines user access to the primary dashboard functionality.
2024-07-20 18:11:38 +08:00
ccbikai
7bcc5b27be Merge branch 'master' into dev 2024-07-20 18:04:24 +08:00
面条
6b3dd8d47e
Merge pull request #23 from dr-data/main
link first
2024-07-20 18:02:24 +08:00
dr-data
ca12fdd876 link first 2024-07-15 00:20:15 +08:00
dr-data
21d8352de0 link first 2024-07-15 00:16:45 +08:00
dr-data
09a97070d3 link first 2024-07-15 00:12:12 +08:00
ccbikai
0d10da9b04 Merge branch 'dev' 2024-07-11 19:55:42 +08:00
ccbikai
01be05c0fc chore: Update package dependencies and optimize imports 2024-07-08 20:28:38 +08:00
ccbikai
e16fb88c08 Merge branch 'master' into dev 2024-07-08 15:51:08 +08:00
ccbikai
bac9abb9a8 docs: enhance README with Hacker News feature badge
Added a new badge to the README to highlight that the project has been featured on Hacker News, increasing its visibility and credibility. Improved the formatting of the existing Trendshift badge.
2024-07-02 20:03:48 +08:00
ccbikai
de411396e2 chore: Update Twitter profile link 2024-07-01 13:30:08 +08:00
面条
528f5ddc48
docs: Add Badges 2024-06-27 19:52:25 +08:00
面条
503e62aa9e
chore: bump version 2024-06-14 18:56:48 +08:00
ccbikai
a98aa7c4e9 Merge branch 'master' into dev 2024-06-12 20:32:07 +08:00
ccbikai
99f1c95cf9 Merge branch 'dev' 2024-06-12 20:31:50 +08:00
ccbikai
14ff257216 chore: Update Node.js version to v20.11 2024-06-12 20:31:43 +08:00
ccbikai
08359a773b Merge branch 'master' into dev 2024-06-12 20:28:58 +08:00
ccbikai
40fa9d1581 Merge branch 'dev' 2024-06-12 20:28:41 +08:00
ccbikai
686ecd151a docs: add API Docs 2024-06-12 20:28:23 +08:00
面条
9355ff5503
Merge pull request #17 from tomcollis/issue-14
Add simple API documentation
2024-06-12 20:08:25 +08:00
ccbikai
997d58f42d Merge branch 'master' into dev 2024-06-12 16:32:52 +08:00
Tom Collis
2b4724f39c
Add simple API documentation 2024-06-11 16:39:24 +01:00
ccbikai
3f08cf0439 fix: Update SEO meta tags in app.vue 2024-06-10 11:13:06 +08:00
ccbikai
036265544f chore: a11y 2024-06-08 19:17:49 +08:00
ccbikai
426db8d149 perf: Simple map 2024-06-08 19:07:27 +08:00
ccbikai
97d6848b5e Merge branch 'master' into dev 2024-06-08 17:20:46 +08:00
ccbikai
4d35186ffe Merge branch 'dev' 2024-06-08 17:20:24 +08:00
ccbikai
07c11954c7 perf: Optimize fetching of world map JSON 2024-06-08 17:20:10 +08:00
ccbikai
c894fde09f perf: The map is not packaged into the code to reduce the server size 2024-06-08 17:15:25 +08:00
ccbikai
d2c83e43d4 Merge branch 'master' into dev 2024-06-08 17:04:08 +08:00
ccbikai
5a3f3fbeff perf: Clean up unnecessary fields 2024-06-08 14:47:57 +08:00
ccbikai
74a2309b44 Merge branch 'dev' 2024-06-08 14:43:25 +08:00
ccbikai
de730b4a04 chore: remove world-topo.json from git 2024-06-08 14:43:17 +08:00
ccbikai
3e2878c107 chore: Update Node.js version to 20.11.0 2024-06-08 14:37:43 +08:00
ccbikai
d0d0fc65e5 perf: build map 2024-06-08 14:34:34 +08:00
ccbikai
5df2adc5b0 perf: Update lazy loading for dashboard components 2024-06-08 13:48:15 +08:00
ccbikai
bdabd9d65f Merge branch 'master' into dev 2024-06-08 13:46:18 +08:00
ccbikai
d620d1f84e docs: Add explanation for NuxtHub analytics in FAQs 2024-06-08 13:17:41 +08:00
ccbikai
1127d0a4e9 fix: Update mobile menu button hover background color 2024-06-05 13:27:54 +08:00
ccbikai
95cbb64b22 feat: Update mobile menu button style and position 2024-06-05 13:14:44 +08:00
ccbikai
cd012676b4 fix: mobile menu button 2024-06-05 12:52:21 +08:00
ccbikai
3b6121e64f Merge branch 'master' into dev 2024-06-04 20:14:09 +08:00
ccbikai
9a00ed3086 Merge branch 'dev' 2024-06-04 20:13:52 +08:00
ccbikai
12f2576c0a docs: add FAQs 2024-06-04 20:13:43 +08:00
ccbikai
03ff995257 Merge branch 'master' into dev 2024-06-04 19:52:55 +08:00
ccbikai
e66c21634e Merge branch 'dev' 2024-06-04 19:52:40 +08:00
ccbikai
f2a4a5963d fix: Filter out null values 2024-06-04 19:52:22 +08:00
ccbikai
245a0ea997 Merge branch 'dev' 2024-06-04 19:38:06 +08:00
ccbikai
3ff84f61cb feat: Add Laptop icon to theme switcher and improve style 2024-06-04 18:59:18 +08:00
面条
5c61271fcf
Merge pull request #13 from QuentinHsu/fix-links-loading-on-mobile
fix: the links data loading on the mobile page did not trigger
2024-06-04 18:33:43 +08:00
QuentinHsu
dda19697d6
fix: the links data loading on the mobile page did not trigger 2024-06-04 13:18:18 +08:00
37 changed files with 3033 additions and 3686 deletions

View File

@ -2,6 +2,8 @@ NUXT_PUBLIC_PREVIEW_MODE=true
NUXT_PUBLIC_SLUG_DEFAULT_LENGTH=5
NUXT_SITE_TOKEN=SinkCool
NUXT_REDIRECT_STATUS_CODE=308
NUXT_LINK_CACHE_TTL=60
NUXT_REDIRECT_WITH_QUERY=false
NUXT_HOME_URL="https://sink.cool"
NUXT_CF_ACCOUNT_ID=123456
NUXT_CF_API_TOKEN=CloudflareAPIToken

16
.github/FUNDING.yml vendored
View File

@ -1,14 +1,2 @@
# These are supported funding model platforms
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']
github: ccbikai
buy_me_a_coffee: ccbikai

1
.gitignore vendored
View File

@ -25,3 +25,4 @@ logs
.wrangler
site
cache
public/world.json

View File

@ -1 +1 @@
v20
v20.11

View File

@ -2,6 +2,30 @@
**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>
![Cloudflare](https://img.shields.io/badge/Cloudflare-F69652?style=flat&logo=cloudflare&logoColor=white)
![Nuxt](https://img.shields.io/badge/Nuxt-00DC82?style=flat&logo=nuxtdotjs&logoColor=white)
![Tailwind CSS](https://img.shields.io/badge/Tailwind%20CSS-06B6D4?style=flat&logo=tailwindcss&logoColor=white)
![shadcn/ui](https://img.shields.io/badge/shadcn/ui-000000?style=flat&logo=shadcnui&logoColor=white)
![Hero](./public/image.png)
----
@ -73,6 +97,14 @@ We welcome your contributions and PRs.
[Configuration Docs](./docs/configuration.md)
## 🔌 API
[API Docs](./docs/api.md)
## 🙋🏻 FAQs
[FAQs](./docs/faqs.md)
## 💖 Credits
1. [**Cloudflare**](https://www.cloudflare.com/)
@ -81,5 +113,5 @@ We welcome your contributions and PRs.
## ☕ 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).

View File

@ -1,8 +1,9 @@
<script setup>
const { title, description, image } = useAppConfig()
useSeoMeta({
title,
title: `${title} - ${description}`,
description,
ogType: 'website',
ogTitle: title,
ogDescription: description,
ogImage: image,
@ -20,7 +21,7 @@ useHead({
{
rel: 'icon',
type: 'image/png',
href: '/sink.png',
href: '/icon-192.png',
},
],
})

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
<script setup>
import { Sun, Moon } from 'lucide-vue-next'
import { Laptop, Moon, Sun } from 'lucide-vue-next'
const colorMode = useColorMode()
</script>
@ -9,10 +9,10 @@ const colorMode = useColorMode()
<DropdownMenuTrigger as-child>
<Button variant="ghost">
<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
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>
</Button>
@ -21,13 +21,25 @@ const colorMode = useColorMode()
align="end"
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
</DropdownMenuItem>
<DropdownMenuItem @click="colorMode.preference = 'dark'">
<DropdownMenuItem
class="cursor-pointer"
@click="colorMode.preference = 'dark'"
>
<Moon class="w-4 h-4 mr-1" />
Dark
</DropdownMenuItem>
<DropdownMenuItem @click="colorMode.preference = 'system'">
<DropdownMenuItem
class="cursor-pointer"
@click="colorMode.preference = 'system'"
>
<Laptop class="w-4 h-4 mr-1" />
System
</DropdownMenuItem>
</DropdownMenuContent>

View File

@ -10,14 +10,14 @@ const route = useRoute()
@update:model-value="navigateTo"
>
<TabsList>
<TabsTrigger value="/dashboard">
Analysis
</TabsTrigger>
<TabsTrigger
value="/dashboard/links"
>
Links
</TabsTrigger>
<TabsTrigger value="/dashboard/analysis">
Analysis
</TabsTrigger>
</TabsList>
</Tabs>
<slot name="left" />

View File

@ -114,8 +114,13 @@ async function onSubmit(formData) {
body: link,
})
dialogOpen.value = false
emit('update:link', newLink, isEdit ? 'edit' : 'create')
isEdit ? toast('Link updated successfully') : toast('Link created successfully')
emit('update:link', newLink)
if (isEdit) {
toast('Link updated successfully')
}
else {
toast('Link created successfully')
}
}
const { previewMode } = useRuntimeConfig().public

View File

@ -14,7 +14,7 @@ async function getLinks() {
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
listComplete = data.list_complete
}
@ -22,7 +22,7 @@ async function getLinks() {
const { isLoading } = useInfiniteScroll(
document,
getLinks,
{ distance: 10, interval: 1000, canLoadMore: () => !listComplete },
{ distance: 150, interval: 1000, canLoadMore: () => !listComplete },
)
function updateLinkList(link, type) {
@ -59,5 +59,11 @@ function updateLinkList(link, type) {
>
<Loader class="animate-spin" />
</div>
<div
v-if="!isLoading && listComplete"
class="flex items-center justify-center text-sm"
>
No more
</div>
</main>
</template>

View File

@ -1,6 +1,6 @@
<template>
<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
class="lg:col-span-4"
:tabs="['country', 'region', 'city']"

View File

@ -1,26 +1,19 @@
<script setup>
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'
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 startAt = inject('startAt')
const endAt = inject('endAt')
const worldMapTopoJSON = ref({})
const areaData = ref([])
async function getWorldMapJSON() {
const data = await $fetch('/world.json')
worldMapTopoJSON.value = data
}
async function getMapData() {
areaData.value = []
const { data } = await useAPI('/api/stats/metrics', {
@ -42,6 +35,7 @@ async function getMapData() {
const stopWatchTime = watch([startAt, endAt], getMapData)
onMounted(() => {
getWorldMapJSON()
getMapData()
})
@ -72,12 +66,13 @@ const Tooltip = {
<CardContent class="flex-1 flex [&_[data-radix-aspect-ratio-wrapper]]:flex-1">
<AspectRatio :ratio="65 / 30">
<VisSingleContainer
v-if="worldMapTopoJSON.type"
:data="{ areas: areaData }"
class="h-full"
>
<VisTopoJSONMap
:topojson="WorldMapTopoJSON"
map-feature-name="states"
:topojson="worldMapTopoJSON"
map-feature-name="countries"
/>
<ChartSingleTooltip
index="id"

View File

@ -1,26 +1,29 @@
<script setup>
// https://vue3-simple-icons.wyatt-herkamp.dev/
import {
AppleIcon,
AndroidIcon,
AppleIcon,
DebianIcon,
FacebookIcon,
FirefoxBrowserIcon,
GnuIcon,
GoogleChromeIcon,
GoogleIcon,
HuaweiIcon,
IOsIcon,
InternetExplorerIcon,
// InternetExplorerIcon,
LinuxIcon,
MacOsIcon,
MicrosoftEdgeIcon,
// MicrosoftEdgeIcon,
OperaIcon,
SafariIcon,
SamsungIcon,
UbuntuIcon,
VivoIcon,
WeChatIcon,
WindowsIcon,
WearOsIcon,
// WindowsIcon,
XIcon,
XiaomiIcon,
YandexCloudIcon,
} from 'vue3-simple-icons'
@ -47,6 +50,7 @@ defineProps({
const iconMaps = {
'android': AndroidIcon,
'android browser': AndroidIcon,
'browser': Globe,
'chrome': GoogleChromeIcon,
'chrome headless': GoogleChromeIcon,
@ -55,15 +59,15 @@ const iconMaps = {
'curl': Terminal,
'debian': DebianIcon,
'desktop': MonitorCheck,
'edge': MicrosoftEdgeIcon,
'facebook': FacebookIcon,
'facebookexternalhit': FacebookIcon,
'firefox': FirefoxBrowserIcon,
'googlebot': GoogleIcon,
'googlebot-image': GoogleIcon,
'gnu': GnuIcon,
'harmonyos': HuaweiIcon,
'huawei browser': HuaweiIcon,
'ie': InternetExplorerIcon,
// 'ie': InternetExplorerIcon,
'ios': IOsIcon,
'ipad': AppleIcon,
'iphone': AppleIcon,
@ -82,10 +86,11 @@ const iconMaps = {
'safari': SafariIcon,
'samsung internet': SamsungIcon,
'tablet': Tablet,
'twitterbot': XIcon,
'ubuntu': UbuntuIcon,
'vivo browser': VivoIcon,
'wechat': WeChatIcon,
'windows': WindowsIcon,
'wearable': WearOsIcon,
'yandex': YandexCloudIcon,
}
</script>

View File

@ -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"
>
<h2 class="text-4xl tracking-tight text-white md:text-6xl">
Deployment immediately.
Deployment immediately
</h2>
<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.
@ -12,6 +12,7 @@
<HomeLink
href="https://github.com/ccbikai/sink?tab=readme-ov-file#%EF%B8%8F-deployment"
type="inverted"
title="Start Deploy"
>
Start Deploy
</HomeLink>

View File

@ -1,5 +1,5 @@
<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([
{

View File

@ -23,6 +23,7 @@ const { title, description } = useAppConfig()
<HomeLink
href="/dashboard"
target="_blank"
title="Dashboard"
class="flex items-center justify-center gap-1"
rel="noopener"
>
@ -36,6 +37,7 @@ const { title, description } = useAppConfig()
type="outline"
rel="noopener"
href="https://github.com/ccbikai/sink"
title="Github"
class="flex items-center justify-center gap-1"
target="_blank"
>

View File

@ -5,8 +5,9 @@ import { ArrowRight } from 'lucide-vue-next'
<template>
<a
href="https://x.com/ccbikai"
href="https://x.com/0xKaiBi"
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"
>
<XIcon class="w-4 h-4" />

View File

@ -1,5 +1,5 @@
<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)
onMounted(() => {
@ -12,12 +12,14 @@ onMounted(() => {
<div class="container flex flex-col items-center py-8 mx-auto sm:flex-row">
<a
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>
<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"
href="https://html.zone"
target="_blank"
title="HTML.ZONE"
>
&copy; {{ new Date().getFullYear() }} Products of HTML.ZONE
</a>
@ -59,7 +61,7 @@ onMounted(() => {
</a>
<a
href="https://x.com/ccbikai"
href="https://x.com/0xKaiBi"
target="_blank"
title="Twitter"
class="text-gray-400 hover:text-gray-500"

View File

@ -15,6 +15,7 @@ const showMenu = ref(false)
<div class="flex items-center justify-start w-1/4 h-full pr-4">
<a
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"
>
<span
@ -40,7 +41,7 @@ const showMenu = ref(false)
>
<a
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"
>
<span
@ -61,17 +62,19 @@ const showMenu = ref(false)
<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"
href="/dashboard"
title="Sink Dashboard"
>Dashboard</a>
<a
href="https://github.com/ccbikai/sink"
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"
>
<GitHubIcon
class="w-5 h-5 mr-1"
/>
GitHub</a>
<span class="px-2 py-2">
<span class="ml-1">
<SwitchTheme />
</span>
</div>
@ -79,7 +82,8 @@ const showMenu = ref(false)
</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"
>
<Ellipsis

53
docs/api.md Normal file
View 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. |

View File

@ -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`.
## `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`
The default Sink homepage is the introduction page, you can replace it with your own website.

31
docs/faqs.md Normal file
View 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.

View File

@ -1,11 +1,11 @@
// @ts-check
// import antfu from '@antfu/eslint-config'
import antfu from '@antfu/eslint-config'
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt(
// antfu(),
antfu(),
{
ignores: ['components/ui'],
ignores: ['components/ui', '.data', 'public/world.json'],
},
{
rules: {

View File

@ -1,6 +1,7 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
modules: [
'@nuxthub/core',
'shadcn-nuxt',
@ -8,9 +9,11 @@ export default defineNuxtConfig({
'@nuxtjs/tailwindcss',
'@nuxtjs/color-mode',
],
colorMode: {
classSuffix: '',
},
routeRules: {
'/': {
prerender: true,
@ -18,38 +21,50 @@ export default defineNuxtConfig({
'/dashboard/**': {
ssr: false,
},
'/dashboard': {
redirect: '/dashboard/links',
},
},
hub: {
ai: true,
analytics: true,
blob: false,
cache: false,
database: false,
kv: true,
// ai: true,
},
eslint: {
config: {
stylistic: true,
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-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"}`,
public: {
previewMode: '',
slugDefaultLength: '6',
},
},
compatibilityDate: '2024-07-08',
})

View File

@ -1,15 +1,19 @@
{
"name": "sink",
"type": "module",
"version": "0.1.1",
"version": "0.1.4",
"private": true,
"packageManager": "pnpm@9.1.2",
"packageManager": "pnpm@9.7.1",
"engines": {
"node": ">=20.11"
},
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"build:map": "node scripts/build-map.js",
"preview": "wrangler pages dev dist",
"deploy": "wrangler pages deploy dist",
"postinstall": "nuxt prepare",
"postinstall": "npm run build:map && nuxt prepare",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "nuxt typecheck",
@ -17,42 +21,42 @@
"lint-staged": "lint-staged"
},
"dependencies": {
"@unovis/ts": "^1.4.1",
"@unovis/vue": "^1.4.1",
"@vee-validate/zod": "^4.12.8",
"@vueuse/core": "^10.9.0",
"@unovis/ts": "^1.4.4",
"@unovis/vue": "^1.4.4",
"@vee-validate/zod": "^4.13.2",
"@vueuse/core": "^11.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",
"nanoid": "^5.0.7",
"pluralize": "^8.0.0",
"qr-code-styling": "1.6.0-rc.1",
"radix-vue": "^1.8.1",
"radix-vue": "^1.9.4",
"ua-parser-js": "next",
"vee-validate": "^4.12.8",
"virtua": "^0.31.0",
"vue-sonner": "^1.1.2",
"vue3-simple-icons": "^11.13.0",
"vee-validate": "^4.13.2",
"virtua": "^0.33.7",
"vue-sonner": "^1.1.4",
"vue3-simple-icons": "^13.2.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@antfu/eslint-config": "^2.18.1",
"@nuxt/eslint": "^0.3.13",
"@nuxt/eslint-config": "^0.3.13",
"@nuxthub/core": "^0.5.17",
"@nuxtjs/color-mode": "^3.4.1",
"@nuxtjs/tailwindcss": "^6.12.0",
"@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",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"eslint": "^8.57.0",
"lint-staged": "^15.2.4",
"nuxt": "^3.11.2",
"eslint": "^9.9.0",
"lint-staged": "^15.2.9",
"nuxt": "^3.12.4",
"shadcn-nuxt": "^0.10.4",
"simple-git-hooks": "^2.11.1",
"tailwind-merge": "^2.3.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"vue-tsc": "^2.0.19",
"wrangler": "^3.57.1"
"vue-tsc": "^2.0.29",
"wrangler": "^3.72.0"
},
"simple-git-hooks": {
"pre-commit": "npm run lint-staged"

View File

@ -17,10 +17,11 @@ async function getLink() {
}
function updateLink(link, type) {
if (type === 'delete')
if (type === 'delete') {
navigateTo('/dashboard/links', {
replace: true,
})
}
}
onMounted(() => {

File diff suppressed because it is too large Load Diff

BIN
public/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -5,7 +5,7 @@ const { slugRegex } = useAppConfig()
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({
id: z.string().trim().max(26).default(nanoid(10)),

5
scripts/build-map.js Normal file
View 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')

View File

@ -31,7 +31,8 @@ export default eventHandler(async (event) => {
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)
}
else {

View File

@ -12,6 +12,7 @@ export default eventHandler(async (event) => {
statusText: 'Link already exists',
})
}
else {
const expiration = getExpiration(event, link.expiration)

View File

@ -1,11 +1,11 @@
import type { z } from 'zod'
import { parsePath } from 'ufo'
import { parsePath, withQuery } from 'ufo'
import type { LinkSchema } from '@/schemas/link'
export default eventHandler(async (event) => {
const { pathname: slug } = parsePath(event.path.slice(1)) // remove leading slash
const { slugRegex, reserveSlug } = useAppConfig(event)
const { homeURL } = useRuntimeConfig(event)
const { homeURL, linkCacheTtl, redirectWithQuery } = useRuntimeConfig(event)
const { cloudflare } = event.context
if (event.path === '/' && homeURL)
@ -13,7 +13,7 @@ export default eventHandler(async (event) => {
if (slug && !reserveSlug.includes(slug) && slugRegex.test(slug) && cloudflare) {
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) {
event.context.link = link
try {
@ -22,7 +22,8 @@ export default eventHandler(async (event) => {
catch (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)
}
}
})

View File

@ -2,11 +2,12 @@ import type { H3Event } from 'h3'
import { parseURL } from 'ufo'
import { UAParser } from 'ua-parser-js'
import {
Apps,
Bots,
CLIs,
Crawlers,
Emails,
ExtraDevices,
Fetchers,
InApps,
MediaPlayers,
Modules,
} from 'ua-parser-js/extensions'
@ -68,7 +69,7 @@ export function useAccessLog(event: H3Event) {
const userAgent = getHeader(event, 'user-agent') || ''
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(),
})).getResult()
@ -90,7 +91,6 @@ export function useAccessLog(event: H3Event) {
language,
os: uaInfo?.os?.name,
browser: uaInfo?.browser?.name,
// @ts-expect-error todo
browserType: uaInfo?.browser?.type,
device: uaInfo?.device?.model,
deviceType: uaInfo?.device?.type,

View File

@ -7,6 +7,10 @@ export function useAPI(api: string, options?: object): Promise<unknown> {
Authorization: `Bearer ${localStorage.getItem('SinkSiteToken') || ''}`,
},
})).catch((error) => {
if (error?.status === 401) {
localStorage.removeItem('SinkSiteToken')
navigateTo('/dashboard/login')
}
if (error?.data?.statusMessage) {
toast(error?.data?.statusMessage)
}