Compare commits
No commits in common. "master" and "v0.1.2" have entirely different histories.
@ -2,8 +2,6 @@ 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
16
.github/FUNDING.yml
vendored
@ -1,2 +1,14 @@
|
||||
github: ccbikai
|
||||
buy_me_a_coffee: ccbikai
|
||||
# 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']
|
||||
|
||||
26
README.md
26
README.md
@ -2,30 +2,6 @@
|
||||
|
||||
**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>
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
----
|
||||
@ -113,5 +89,5 @@ We welcome your contributions and PRs.
|
||||
|
||||
## ☕ Sponsor
|
||||
|
||||
1. [Follow Me on X(Twitter)](https://x.com/0xKaiBi).
|
||||
1. [Follow Me on X(Twitter)](https://x.com/ccbikai).
|
||||
2. [Become a sponsor to on GitHub](https://github.com/sponsors/ccbikai).
|
||||
|
||||
2
app.vue
2
app.vue
@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
const { title, description, image } = useAppConfig()
|
||||
useSeoMeta({
|
||||
title: `${title} - ${description}`,
|
||||
title: title + ' - ' + description,
|
||||
description,
|
||||
ogType: 'website',
|
||||
ogTitle: title,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { Laptop, Moon, Sun } from 'lucide-vue-next'
|
||||
import { Sun, Moon, Laptop } from 'lucide-vue-next'
|
||||
|
||||
const colorMode = useColorMode()
|
||||
</script>
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -114,13 +114,8 @@ async function onSubmit(formData) {
|
||||
body: link,
|
||||
})
|
||||
dialogOpen.value = false
|
||||
emit('update:link', newLink)
|
||||
if (isEdit) {
|
||||
toast('Link updated successfully')
|
||||
}
|
||||
else {
|
||||
toast('Link created successfully')
|
||||
}
|
||||
emit('update:link', newLink, isEdit ? 'edit' : 'create')
|
||||
isEdit ? toast('Link updated successfully') : toast('Link created successfully')
|
||||
}
|
||||
|
||||
const { previewMode } = useRuntimeConfig().public
|
||||
|
||||
@ -1,29 +1,26 @@
|
||||
<script setup>
|
||||
// https://vue3-simple-icons.wyatt-herkamp.dev/
|
||||
import {
|
||||
AndroidIcon,
|
||||
AppleIcon,
|
||||
AndroidIcon,
|
||||
DebianIcon,
|
||||
FacebookIcon,
|
||||
FirefoxBrowserIcon,
|
||||
GnuIcon,
|
||||
GoogleChromeIcon,
|
||||
GoogleIcon,
|
||||
HuaweiIcon,
|
||||
IOsIcon,
|
||||
// InternetExplorerIcon,
|
||||
InternetExplorerIcon,
|
||||
LinuxIcon,
|
||||
MacOsIcon,
|
||||
// MicrosoftEdgeIcon,
|
||||
MicrosoftEdgeIcon,
|
||||
OperaIcon,
|
||||
SafariIcon,
|
||||
SamsungIcon,
|
||||
UbuntuIcon,
|
||||
VivoIcon,
|
||||
WeChatIcon,
|
||||
WearOsIcon,
|
||||
// WindowsIcon,
|
||||
XIcon,
|
||||
WindowsIcon,
|
||||
XiaomiIcon,
|
||||
YandexCloudIcon,
|
||||
} from 'vue3-simple-icons'
|
||||
@ -50,7 +47,6 @@ defineProps({
|
||||
|
||||
const iconMaps = {
|
||||
'android': AndroidIcon,
|
||||
'android browser': AndroidIcon,
|
||||
'browser': Globe,
|
||||
'chrome': GoogleChromeIcon,
|
||||
'chrome headless': GoogleChromeIcon,
|
||||
@ -59,15 +55,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,
|
||||
@ -86,11 +82,10 @@ const iconMaps = {
|
||||
'safari': SafariIcon,
|
||||
'samsung internet': SamsungIcon,
|
||||
'tablet': Tablet,
|
||||
'twitterbot': XIcon,
|
||||
'ubuntu': UbuntuIcon,
|
||||
'vivo browser': VivoIcon,
|
||||
'wechat': WeChatIcon,
|
||||
'wearable': WearOsIcon,
|
||||
'windows': WindowsIcon,
|
||||
'yandex': YandexCloudIcon,
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { AreaChart, Hourglass, Link, Paintbrush, ServerOff, Sparkles } from 'lucide-vue-next'
|
||||
import { Link, AreaChart, ServerOff, Paintbrush, Sparkles, Hourglass } from 'lucide-vue-next'
|
||||
|
||||
const features = ref([
|
||||
{
|
||||
|
||||
@ -5,7 +5,7 @@ import { ArrowRight } from 'lucide-vue-next'
|
||||
|
||||
<template>
|
||||
<a
|
||||
href="https://x.com/0xKaiBi"
|
||||
href="https://x.com/ccbikai"
|
||||
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"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { BloggerIcon, GitHubIcon, GmailIcon, MastodonIcon, TelegramIcon, XIcon } from 'vue3-simple-icons'
|
||||
import { GmailIcon, TelegramIcon, BloggerIcon, XIcon, MastodonIcon, GitHubIcon } from 'vue3-simple-icons'
|
||||
|
||||
const email = ref(null)
|
||||
onMounted(() => {
|
||||
@ -61,7 +61,7 @@ onMounted(() => {
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://x.com/0xKaiBi"
|
||||
href="https://x.com/ccbikai"
|
||||
target="_blank"
|
||||
title="Twitter"
|
||||
class="text-gray-400 hover:text-gray-500"
|
||||
|
||||
@ -14,14 +14,6 @@ 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.
|
||||
|
||||
@ -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', '.data', 'public/world.json'],
|
||||
ignores: ['components/ui'],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
devtools: { enabled: true },
|
||||
|
||||
modules: [
|
||||
'@nuxthub/core',
|
||||
'shadcn-nuxt',
|
||||
@ -9,11 +8,9 @@ export default defineNuxtConfig({
|
||||
'@nuxtjs/tailwindcss',
|
||||
'@nuxtjs/color-mode',
|
||||
],
|
||||
|
||||
colorMode: {
|
||||
classSuffix: '',
|
||||
},
|
||||
|
||||
routeRules: {
|
||||
'/': {
|
||||
prerender: true,
|
||||
@ -21,50 +18,38 @@ 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.1-8b-instruct',
|
||||
aiModel: '@cf/meta/llama-3-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',
|
||||
})
|
||||
|
||||
54
package.json
54
package.json
@ -1,12 +1,9 @@
|
||||
{
|
||||
"name": "sink",
|
||||
"type": "module",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.7.1",
|
||||
"engines": {
|
||||
"node": ">=20.11"
|
||||
},
|
||||
"packageManager": "pnpm@9.1.2",
|
||||
"scripts": {
|
||||
"dev": "nuxt dev",
|
||||
"build": "nuxt build",
|
||||
@ -20,43 +17,46 @@
|
||||
"wrangler": "wrangler",
|
||||
"lint-staged": "lint-staged"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@unovis/ts": "^1.4.4",
|
||||
"@unovis/vue": "^1.4.4",
|
||||
"@vee-validate/zod": "^4.13.2",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@unovis/ts": "^1.4.1",
|
||||
"@unovis/vue": "^1.4.1",
|
||||
"@vee-validate/zod": "^4.12.8",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"intl-parse-accept-language": "^1.0.0",
|
||||
"lucide-vue-next": "^0.428.0",
|
||||
"lucide-vue-next": "^0.379.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.9.4",
|
||||
"radix-vue": "^1.8.1",
|
||||
"ua-parser-js": "next",
|
||||
"vee-validate": "^4.13.2",
|
||||
"virtua": "^0.33.7",
|
||||
"vue-sonner": "^1.1.4",
|
||||
"vue3-simple-icons": "^13.2.0",
|
||||
"vee-validate": "^4.12.8",
|
||||
"virtua": "^0.31.0",
|
||||
"vue-sonner": "^1.1.2",
|
||||
"vue3-simple-icons": "^11.13.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"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": "^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",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.9.0",
|
||||
"lint-staged": "^15.2.9",
|
||||
"nuxt": "^3.12.4",
|
||||
"eslint": "^8.57.0",
|
||||
"lint-staged": "^15.2.4",
|
||||
"nuxt": "^3.11.2",
|
||||
"shadcn-nuxt": "^0.10.4",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vue-tsc": "^2.0.29",
|
||||
"wrangler": "^3.72.0"
|
||||
"vue-tsc": "^2.0.19",
|
||||
"wrangler": "^3.57.1"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "npm run lint-staged"
|
||||
|
||||
@ -17,11 +17,10 @@ async function getLink() {
|
||||
}
|
||||
|
||||
function updateLink(link, type) {
|
||||
if (type === 'delete') {
|
||||
if (type === 'delete')
|
||||
navigateTo('/dashboard/links', {
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
6315
pnpm-lock.yaml
6315
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@ const { slugRegex } = useAppConfig()
|
||||
|
||||
const slugDefaultLength = +useRuntimeConfig().public.slugDefaultLength
|
||||
|
||||
export const nanoid = (length: number = slugDefaultLength) => customAlphabet('23456789abcdefghjkmnpqrstuvwxyz', length)
|
||||
export const nanoid = (length: number = slugDefaultLength) => customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', length)
|
||||
|
||||
export const LinkSchema = z.object({
|
||||
id: z.string().trim().max(26).default(nanoid(10)),
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { writeFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { WorldMapSimplestTopoJSON } from '@unovis/ts/maps.js'
|
||||
|
||||
writeFileSync(join(import.meta.dirname, '../public/world.json'), JSON.stringify(WorldMapSimplestTopoJSON), 'utf8')
|
||||
|
||||
@ -31,8 +31,7 @@ export default eventHandler(async (event) => {
|
||||
content: url,
|
||||
},
|
||||
]
|
||||
// @ts-expect-error Workers AI is not typed
|
||||
const { response } = await hubAI().run(aiModel, { messages })
|
||||
const { response } = await AI.run(aiModel, { messages })
|
||||
return destr(response)
|
||||
}
|
||||
else {
|
||||
|
||||
@ -12,7 +12,6 @@ export default eventHandler(async (event) => {
|
||||
statusText: 'Link already exists',
|
||||
})
|
||||
}
|
||||
|
||||
else {
|
||||
const expiration = getExpiration(event, link.expiration)
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import type { z } from 'zod'
|
||||
import { parsePath, withQuery } from 'ufo'
|
||||
import { parsePath } 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, linkCacheTtl, redirectWithQuery } = useRuntimeConfig(event)
|
||||
const { homeURL } = 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', cacheTtl: linkCacheTtl })
|
||||
const link: z.infer<typeof LinkSchema> | null = await KV.get(`link:${slug}`, { type: 'json' })
|
||||
if (link) {
|
||||
event.context.link = link
|
||||
try {
|
||||
@ -22,8 +22,7 @@ export default eventHandler(async (event) => {
|
||||
catch (error) {
|
||||
console.error('Failed write access log:', error)
|
||||
}
|
||||
const target = redirectWithQuery ? withQuery(link.url, getQuery(event)) : link.url
|
||||
return sendRedirect(event, target, +useRuntimeConfig(event).redirectStatusCode)
|
||||
return sendRedirect(event, link.url, +useRuntimeConfig(event).redirectStatusCode)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -2,12 +2,11 @@ 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'
|
||||
@ -69,7 +68,7 @@ export function useAccessLog(event: H3Event) {
|
||||
|
||||
const userAgent = getHeader(event, 'user-agent') || ''
|
||||
const uaInfo = (new UAParser(userAgent, {
|
||||
browser: [Crawlers.browser || [], CLIs.browser || [], Emails.browser || [], Fetchers.browser || [], InApps.browser || [], MediaPlayers.browser || [], Modules.browser || []].flat(),
|
||||
browser: [Apps.browser || [], Bots.browser || [], CLIs.browser || [], Emails.browser || [], MediaPlayers.browser || [], Modules.browser || []].flat(),
|
||||
device: [ExtraDevices.device || []].flat(),
|
||||
})).getResult()
|
||||
|
||||
@ -91,6 +90,7 @@ 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,
|
||||
|
||||
@ -7,10 +7,6 @@ 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)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user