mirror of
https://github.com/ArthurDanjou/artsite.git
synced 2026-02-01 22:29:34 +01:00
Test visitors
This commit is contained in:
@@ -44,7 +44,6 @@ const head = useLocaleHead({
|
|||||||
<AppBackground />
|
<AppBackground />
|
||||||
<UContainer class="z-50 relative">
|
<UContainer class="z-50 relative">
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
<AppVisitors />
|
|
||||||
<NuxtPage class="mt-12" />
|
<NuxtPage class="mt-12" />
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
</UContainer>
|
</UContainer>
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const { isLoading, visitors } = useVisitors()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-if="!isLoading" class="fixed bottom-4 right-4">
|
|
||||||
<UTooltip text="Visitors currently on my portfolio" placement="left">
|
|
||||||
<nav class="text-xs flex space-x-1 items-center border border-green-400 dark:border-green-600 px-2 rounded-xl bg-white dark:bg-black">
|
|
||||||
<p class="text-neutral-700 dark:text-neutral-300">
|
|
||||||
{{ visitors }}
|
|
||||||
</p>
|
|
||||||
<div class="h-3 w-3 bg-green-200 dark:bg-green-600 rounded-full border-2 border-green-400 dark:border-green-800" />
|
|
||||||
</nav>
|
|
||||||
</UTooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,83 +1,141 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
|
import type { COBEOptions } from 'cobe'
|
||||||
import createGlobe from 'cobe'
|
import createGlobe from 'cobe'
|
||||||
|
import { useSpring } from 'vue-use-spring'
|
||||||
|
|
||||||
const { t } = useI18n({
|
interface GlobeProps {
|
||||||
useScope: 'local',
|
class?: string
|
||||||
|
config?: Partial<COBEOptions>
|
||||||
|
mass?: number
|
||||||
|
tension?: number
|
||||||
|
friction?: number
|
||||||
|
precision?: number
|
||||||
|
locations?: Array<{ latitude: number, longitude: number }>
|
||||||
|
myLocation?: { latitude: number, longitude: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<GlobeProps>(), {
|
||||||
|
mass: 1,
|
||||||
|
tension: 280,
|
||||||
|
friction: 100,
|
||||||
|
precision: 0.001,
|
||||||
})
|
})
|
||||||
|
|
||||||
const myLocation = useState<{ longitude: number, latitude: number }>('location')
|
const DEFAULT_CONFIG: COBEOptions = {
|
||||||
const globe = ref<HTMLCanvasElement | null>(null)
|
width: 400,
|
||||||
|
height: 400,
|
||||||
|
onRender: () => {},
|
||||||
|
devicePixelRatio: 2,
|
||||||
|
phi: 0,
|
||||||
|
theta: 0.3,
|
||||||
|
dark: 0,
|
||||||
|
diffuse: 0.4,
|
||||||
|
mapSamples: 20000,
|
||||||
|
mapBrightness: 1,
|
||||||
|
baseColor: [0.8, 0.8, 0.8],
|
||||||
|
opacity: 0.7,
|
||||||
|
markerColor: [251 / 255, 100 / 255, 21 / 255],
|
||||||
|
glowColor: [1, 1, 1],
|
||||||
|
markers: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const globeCanvasRef = ref<HTMLCanvasElement>()
|
||||||
const phi = ref(0)
|
const phi = ref(0)
|
||||||
const locations = ref<Array<{ latitude: number, longitude: number }>>([])
|
const width = ref(0)
|
||||||
|
const pointerInteracting = ref()
|
||||||
|
const pointerInteractionMovement = ref()
|
||||||
|
|
||||||
const { data, open } = useWebSocket(`/visitors?latitude=${myLocation.value.latitude}&longitude=${myLocation.value.longitude}`, { immediate: true })
|
let globe: ReturnType<typeof createGlobe> | null = null
|
||||||
watch(data, async (newData) => {
|
|
||||||
locations.value = JSON.parse(typeof newData === 'string' ? newData : await newData.text())
|
const spring = useSpring(
|
||||||
})
|
{
|
||||||
|
r: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mass: props.mass,
|
||||||
|
tension: props.tension,
|
||||||
|
friction: props.friction,
|
||||||
|
precision: props.precision,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function updatePointerInteraction(clientX: number | null) {
|
||||||
|
if (clientX !== null) {
|
||||||
|
pointerInteracting.value = clientX - (pointerInteractionMovement.value ?? clientX)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pointerInteracting.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globeCanvasRef.value) {
|
||||||
|
globeCanvasRef.value.style.cursor = clientX ? 'grabbing' : 'grab'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMovement(clientX: number) {
|
||||||
|
if (pointerInteracting.value !== null) {
|
||||||
|
const delta = clientX - (pointerInteracting.value ?? clientX)
|
||||||
|
pointerInteractionMovement.value = delta
|
||||||
|
spring.r = delta / 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRender(state: Record<string, unknown>) {
|
||||||
|
if (!pointerInteracting.value) {
|
||||||
|
phi.value += 0.005
|
||||||
|
}
|
||||||
|
|
||||||
|
state.phi = phi.value + spring.r
|
||||||
|
state.width = width.value * 2
|
||||||
|
state.height = width.value * 2
|
||||||
|
state.markers = props.locations?.map(location => ({
|
||||||
|
location: [location.latitude, location.longitude],
|
||||||
|
// Set the size of the marker to 0.1 if it's the user's location, otherwise 0.05
|
||||||
|
size: props.myLocation?.latitude === location.latitude && props.myLocation?.longitude === location.longitude ? 0.1 : 0.05,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResize() {
|
||||||
|
if (globeCanvasRef.value) {
|
||||||
|
width.value = globeCanvasRef.value.offsetWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGlobeOnMounted() {
|
||||||
|
const config = { ...DEFAULT_CONFIG, ...props.config }
|
||||||
|
|
||||||
|
globe = createGlobe(globeCanvasRef.value!, {
|
||||||
|
...config,
|
||||||
|
width: width.value,
|
||||||
|
height: width.value,
|
||||||
|
onRender,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
open()
|
window.addEventListener('resize', onResize)
|
||||||
createGlobe(globe.value!, {
|
onResize()
|
||||||
devicePixelRatio: 2,
|
createGlobeOnMounted()
|
||||||
width: 400 * 2,
|
|
||||||
height: 400 * 2,
|
setTimeout(() => (globeCanvasRef.value!.style.opacity = '1'))
|
||||||
phi: 0,
|
})
|
||||||
theta: 0,
|
|
||||||
dark: 1,
|
onBeforeUnmount(() => {
|
||||||
diffuse: 1.2,
|
globe?.destroy()
|
||||||
scale: 1,
|
window.removeEventListener('resize', onResize)
|
||||||
mapSamples: 20000,
|
|
||||||
mapBrightness: 6,
|
|
||||||
baseColor: [0.3, 0.3, 0.3],
|
|
||||||
markerColor: [0.1, 0.8, 0.1],
|
|
||||||
glowColor: [0.2, 0.2, 0.2],
|
|
||||||
markers: [],
|
|
||||||
opacity: 0.3,
|
|
||||||
onRender(state) {
|
|
||||||
state.markers = locations.value.map(location => ({
|
|
||||||
location: [location.latitude, location.longitude],
|
|
||||||
size: myLocation.value.latitude === location.latitude && myLocation.value.longitude === location.longitude ? 0.1 : 0.05,
|
|
||||||
}))
|
|
||||||
state.phi = phi.value
|
|
||||||
phi.value += 0.01
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="mt-12 not-prose w-full flex flex-col items-center justify-center">
|
<div :class="props.class">
|
||||||
<canvas ref="globe" />
|
<canvas
|
||||||
<!-- <ClientOnly>
|
ref="globeCanvasRef"
|
||||||
<div class="group text-[12px] flex items-center gap-1">
|
class="size-full opacity-0 transition-opacity duration-1000 ease-in-out [contain:layout_paint_size]"
|
||||||
<UIcon
|
@pointerdown="(e) => updatePointerInteraction(e.clientX)"
|
||||||
name="i-ph-map-pin-duotone"
|
@pointerup="updatePointerInteraction(null)"
|
||||||
/>
|
@pointerout="updatePointerInteraction(null)"
|
||||||
<p>{{ t('globe') }}</p>
|
@mousemove="(e) => updateMovement(e.clientX)"
|
||||||
</div>
|
@touchmove="(e) => e.touches[0] && updateMovement(e.touches[0].clientX)"
|
||||||
</ClientOnly> -->
|
/>
|
||||||
</section>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
canvas {
|
|
||||||
width: 400px;
|
|
||||||
height: 400px;
|
|
||||||
max-width: 100%;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<i18n>
|
|
||||||
{
|
|
||||||
"en": {
|
|
||||||
"globe": "Each marker represents a visitor connected to my site."
|
|
||||||
},
|
|
||||||
"fr": {
|
|
||||||
"globe": "Chaque point représente un visiteur connecté sur mon site."
|
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"globe": "Cada marcador representa un visitante conectado a mi sitio."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</i18n>
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ const { data: page } = await useAsyncData(`/home/${locale.value}`, () => {
|
|||||||
}, {
|
}, {
|
||||||
watch: [locale],
|
watch: [locale],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { myLocation, locations } = useVisitors()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -15,6 +17,12 @@ const { data: page } = await useAsyncData(`/home/${locale.value}`, () => {
|
|||||||
<HomeActivity />
|
<HomeActivity />
|
||||||
<HomeQuote />
|
<HomeQuote />
|
||||||
<HomeCatchPhrase />
|
<HomeCatchPhrase />
|
||||||
<HomeGlobe />
|
{{ locations }}
|
||||||
|
{{ myLocation }}
|
||||||
|
<HomeGlobe
|
||||||
|
:my-location
|
||||||
|
:locations
|
||||||
|
class="mt-8 mx-auto aspect-[1/1] duration-500 md:w-1/2"
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
@@ -66,6 +66,11 @@ export default defineNuxtConfig({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Nuxt Visitors
|
||||||
|
visitors: {
|
||||||
|
locations: true,
|
||||||
|
},
|
||||||
|
|
||||||
// Nuxt Color Mode
|
// Nuxt Color Mode
|
||||||
colorMode: {
|
colorMode: {
|
||||||
preference: 'system',
|
preference: 'system',
|
||||||
@@ -104,6 +109,9 @@ export default defineNuxtConfig({
|
|||||||
// Nuxt Icon
|
// Nuxt Icon
|
||||||
icon: {
|
icon: {
|
||||||
serverBundle: 'remote',
|
serverBundle: 'remote',
|
||||||
|
clientBundle: {
|
||||||
|
scan: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Nuxt Google Fonts
|
// Nuxt Google Fonts
|
||||||
@@ -119,9 +127,9 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
// Nitro
|
// Nitro
|
||||||
nitro: {
|
nitro: {
|
||||||
preset: 'cloudflare_durable',
|
|
||||||
experimental: {
|
experimental: {
|
||||||
websocket: true,
|
websocket: true,
|
||||||
|
openAPI: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -19,16 +19,17 @@
|
|||||||
"@nuxt/ui": "^2.21.0",
|
"@nuxt/ui": "^2.21.0",
|
||||||
"@nuxthub/core": "^0.8.15",
|
"@nuxthub/core": "^0.8.15",
|
||||||
"@nuxtjs/google-fonts": "^3.2.0",
|
"@nuxtjs/google-fonts": "^3.2.0",
|
||||||
"@nuxtjs/i18n": "^8.5.3",
|
"@nuxtjs/i18n": "^9.1.5",
|
||||||
"cobe": "^0.6.3",
|
"cobe": "^0.6.3",
|
||||||
"drizzle-orm": "^0.33.0",
|
"drizzle-orm": "^0.33.0",
|
||||||
"h3-zod": "^0.5.3",
|
"h3-zod": "^0.5.3",
|
||||||
"nuxt": "^3.15.3",
|
"nuxt": "^3.15.4",
|
||||||
"nuxt-visitors": "^1.1.2",
|
"nuxt-visitors": "^1.1.2",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-rehype": "^11.1.1",
|
"remark-rehype": "^11.1.1",
|
||||||
|
"vue-use-spring": "^0.3.3",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
780
pnpm-lock.yaml
generated
780
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,17 +0,0 @@
|
|||||||
import { consola } from 'consola'
|
|
||||||
import { migrate } from 'drizzle-orm/d1/migrator'
|
|
||||||
|
|
||||||
export default defineNitroPlugin(async () => {
|
|
||||||
if (!import.meta.dev)
|
|
||||||
return
|
|
||||||
|
|
||||||
onHubReady(async () => {
|
|
||||||
await migrate(useDB(), { migrationsFolder: 'server/database/migrations' })
|
|
||||||
.then(() => {
|
|
||||||
consola.success('Database migrations done')
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
consola.error('Database migrations failed', err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user