mirror of
https://github.com/ArthurDanjou/artsite.git
synced 2026-01-25 02:52:17 +01:00
Add nuxt-visitors
This commit is contained in:
@@ -8,6 +8,7 @@ useHead({
|
||||
<UApp>
|
||||
<NuxtLoadingIndicator color="#808080" />
|
||||
<AppBackground />
|
||||
<AppVisitors />
|
||||
<UContainer class="z-50 relative">
|
||||
<AppHeader />
|
||||
<NuxtPage class="mt-12" />
|
||||
|
||||
18
app/components/AppVisitors.vue
Normal file
18
app/components/AppVisitors.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
const { visitors } = useVisitors()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UBadge
|
||||
color="green"
|
||||
variant="outline"
|
||||
class="shadow-xl fixed bottom-4 right-4 rounded-full px-1.5 py-0.5"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<p class="text-neutral-500">
|
||||
{{ visitors }}
|
||||
</p>
|
||||
<div class="w-3 h-3 bg-green-200/70 dark:bg-green-800/70 rounded-full border-2 border-green-400 dark:border-green-600" />
|
||||
</div>
|
||||
</UBadge>
|
||||
</template>
|
||||
@@ -1,136 +0,0 @@
|
||||
import { useState } from '#imports'
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
export function useVisitors() {
|
||||
const visitors = useState<number>('visitors', () => 0) // Added default value
|
||||
const locations = ref<Array<{ latitude: number, longitude: number }>>([])
|
||||
const myLocation = useState('location', () => ({
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
}))
|
||||
const isLoading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
const wsRef = ref<WebSocket | null>(null)
|
||||
const isConnected = ref(false)
|
||||
const isMounted = ref(true)
|
||||
|
||||
const RECONNECTION_DELAY = 5000 // 5 seconds delay for reconnection
|
||||
const WS_NORMAL_CLOSURE = 1000
|
||||
|
||||
const getWebSocketUrl = (): string => {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const baseUrl = window.location.host.replace(/^(http|https):\/\//, '')
|
||||
return `${protocol}//${baseUrl}/ws?latitude=${myLocation.value.latitude}&longitude=${myLocation.value.longitude}`
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
if (wsRef.value) {
|
||||
wsRef.value.close()
|
||||
wsRef.value = null
|
||||
}
|
||||
isConnected.value = false
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const handleMessage = async (event: MessageEvent) => {
|
||||
if (!isMounted.value)
|
||||
return
|
||||
|
||||
try {
|
||||
const data = typeof event.data === 'string' ? event.data : await event.data.text()
|
||||
locations.value = JSON.parse(data) as { latitude: number, longitude: number }[]
|
||||
const visitorCount = locations.value.length
|
||||
if (!Number.isNaN(visitorCount) && visitorCount >= 0) {
|
||||
visitors.value = visitorCount
|
||||
}
|
||||
else {
|
||||
throw new Error('Invalid visitor count received')
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Failed to parse visitors WebSocket data:', err)
|
||||
error.value = 'Invalid data received'
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = (event: CloseEvent) => {
|
||||
console.warn('Visitors WebSocket closed:', event.code, event.reason)
|
||||
isConnected.value = false
|
||||
wsRef.value = null
|
||||
|
||||
if (isMounted.value && event.code !== WS_NORMAL_CLOSURE) {
|
||||
error.value = 'Connection lost'
|
||||
// eslint-disable-next-line ts/no-use-before-define
|
||||
setTimeout(reconnect, RECONNECTION_DELAY)
|
||||
}
|
||||
}
|
||||
|
||||
const initWebSocket = () => {
|
||||
if (!isMounted.value)
|
||||
return
|
||||
|
||||
cleanup()
|
||||
|
||||
try {
|
||||
const ws = new WebSocket(getWebSocketUrl())
|
||||
wsRef.value = ws
|
||||
|
||||
ws.onopen = () => {
|
||||
if (!isMounted.value) {
|
||||
ws.close()
|
||||
return
|
||||
}
|
||||
console.warn('Stats WebSocket connected')
|
||||
isConnected.value = true
|
||||
isLoading.value = false
|
||||
error.value = null
|
||||
}
|
||||
|
||||
ws.onmessage = handleMessage
|
||||
ws.onclose = handleClose
|
||||
ws.onerror = (event: Event) => {
|
||||
if (!isMounted.value)
|
||||
return
|
||||
console.error('Visitors WebSocket error:', event)
|
||||
error.value = 'Connection error'
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
if (!isMounted.value)
|
||||
return
|
||||
console.error('Failed to initialize Visitors WebSocket:', err)
|
||||
error.value = 'Failed to initialize connection'
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const reconnect = () => {
|
||||
if (!isMounted.value)
|
||||
return
|
||||
error.value = null
|
||||
isLoading.value = true
|
||||
initWebSocket()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (import.meta.client) {
|
||||
isMounted.value = true
|
||||
initWebSocket()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isMounted.value = false
|
||||
cleanup()
|
||||
})
|
||||
|
||||
return {
|
||||
visitors,
|
||||
locations,
|
||||
myLocation,
|
||||
isLoading,
|
||||
error,
|
||||
isConnected,
|
||||
reconnect,
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export default defineNuxtPlugin(() => {
|
||||
const event = useRequestEvent()
|
||||
|
||||
useState('location', () => ({
|
||||
latitude: event?.context.cf?.latitude || Math.random() * 180 - 90, // default to random latitude (only in dev)
|
||||
longitude: event?.context.cf?.longitude || Math.random() * 360 - 180, // default to random longitude (only in dev)
|
||||
}))
|
||||
})
|
||||
@@ -10,8 +10,8 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
rootAttrs: {
|
||||
'vaul-drawer-wrapper': '',
|
||||
'class': 'bg-[var(--ui-bg)]',
|
||||
// 'vaul-drawer-wrapper': '',
|
||||
class: 'bg-[var(--ui-bg)]',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ export default defineNuxtConfig({
|
||||
'@nuxtjs/google-fonts',
|
||||
'@nuxt/image',
|
||||
'@nuxtjs/i18n',
|
||||
'nuxt-visitors',
|
||||
],
|
||||
|
||||
// Nuxt Hub
|
||||
@@ -83,8 +84,6 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
|
||||
plugins: ['~/plugins/location.server'],
|
||||
|
||||
// Nuxt Color Mode
|
||||
colorMode: {
|
||||
preference: 'system',
|
||||
@@ -170,5 +169,11 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
|
||||
// Nuxt Visitors
|
||||
visitors: {
|
||||
// Set to true to enable tracking of visitor locations
|
||||
locations: true,
|
||||
},
|
||||
|
||||
compatibilityDate: '2025-01-28',
|
||||
})
|
||||
|
||||
27
package.json
27
package.json
@@ -14,38 +14,39 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/logos": "^1.2.4",
|
||||
"@iconify-json/lucide": "^1.2.35",
|
||||
"@iconify-json/lucide": "^1.2.37",
|
||||
"@iconify-json/ph": "^1.2.2",
|
||||
"@iconify-json/twemoji": "^1.2.2",
|
||||
"@iconify-json/vscode-icons": "^1.2.18",
|
||||
"@iconify-json/vscode-icons": "^1.2.19",
|
||||
"@intlify/message-compiler": "^11.1.3",
|
||||
"@nuxt/content": "3.4.0",
|
||||
"@nuxt/image": "^1.10.0",
|
||||
"@nuxt/ui": "3.0.2",
|
||||
"@nuxthub/core": "^0.8.23",
|
||||
"@nuxthub/core": "^0.8.24",
|
||||
"@nuxtjs/google-fonts": "^3.2.0",
|
||||
"@nuxtjs/i18n": "9.5.2",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"drizzle-orm": "^0.41.0",
|
||||
"@nuxtjs/i18n": "9.5.3",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"drizzle-orm": "^0.42.0",
|
||||
"h3-zod": "^0.5.3",
|
||||
"nuxt": "^3.16.2",
|
||||
"nuxt-visitors": "1.2.1",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"vue-use-spring": "^0.3.3",
|
||||
"zod": "^3.24.2"
|
||||
"zod": "^3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^4.11.0",
|
||||
"@antfu/eslint-config": "^4.12.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vueuse/math": "^13.0.0",
|
||||
"@vueuse/nuxt": "^13.0.0",
|
||||
"drizzle-kit": "^0.30.6",
|
||||
"@types/node": "^22.14.1",
|
||||
"@vueuse/math": "^13.1.0",
|
||||
"@vueuse/nuxt": "^13.1.0",
|
||||
"drizzle-kit": "^0.31.0",
|
||||
"eslint": "^9.24.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vue-tsc": "^2.2.8",
|
||||
"wrangler": "^4.7.2"
|
||||
"wrangler": "^4.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
2168
pnpm-lock.yaml
generated
2168
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
import type { Peer } from 'crossws'
|
||||
import { defineWebSocketHandler } from 'h3'
|
||||
import { getQuery } from 'ufo'
|
||||
|
||||
export default defineWebSocketHandler({
|
||||
open(peer: Peer) {
|
||||
const locations = Array.from(peer.peers.values()).map(peer => getQuery(peer.websocket.url!))
|
||||
peer.subscribe('nuxt-visitors')
|
||||
peer.publish('nuxt-visitors', JSON.stringify(locations))
|
||||
peer.send(JSON.stringify(locations))
|
||||
},
|
||||
|
||||
close(peer: Peer) {
|
||||
peer.unsubscribe('nuxt-visitors')
|
||||
setTimeout(() => {
|
||||
const locations = Array.from(peer.peers.values()).map(peer => getQuery(peer.websocket.url!))
|
||||
peer.publish('nuxt-visitors', JSON.stringify(locations))
|
||||
}, 500)
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user