mirror of
https://github.com/ArthurDanjou/artsite.git
synced 2026-01-14 15:54:13 +01:00
Replace Mapbox integration with custom Globe component
Switched from Mapbox to a custom globe visualization using 'cobe' library. Removed all Mapbox-related code, dependencies, and environmental variables. Introduced a WebSocket-based system to display live visitor data on the globe.
This commit is contained in:
@@ -17,9 +17,4 @@ NUXT_DISCORD_TOKEN=
|
|||||||
NUXT_DISCORD_USER_ID=
|
NUXT_DISCORD_USER_ID=
|
||||||
|
|
||||||
# Nuxt I18N
|
# Nuxt I18N
|
||||||
NUXT_PUBLIC_I18N_BASE_URL=
|
NUXT_PUBLIC_I18N_BASE_URL=
|
||||||
|
|
||||||
# Nuxt Mapbox
|
|
||||||
NUXT_PUBLIC_MAPBOX_LIGHT=
|
|
||||||
NUXT_PUBLIC_MAPBOX_STYLE_DARK=
|
|
||||||
NUXT_PUBLIC_MAPBOX_ACCESS_TOKEN=
|
|
||||||
@@ -66,11 +66,6 @@ NUXT_PUBLIC_CLOUD_RESUME=...
|
|||||||
|
|
||||||
# Nuxt I18N
|
# Nuxt I18N
|
||||||
NUXT_PUBLIC_I18N_BASE_URL=...
|
NUXT_PUBLIC_I18N_BASE_URL=...
|
||||||
|
|
||||||
# Nuxt Mapbox
|
|
||||||
NUXT_PUBLIC_MAPBOX_STYLE_LIGHT=...
|
|
||||||
NUXT_PUBLIC_MAPBOX_STYLE_DARK=...
|
|
||||||
NUXT_PUBLIC_MAPBOX_ACCESS_TOKEN=...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ const { isLoading, visitors } = useVisitors()
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="fixed bottom-4 right-4">
|
<div v-if="!isLoading" class="fixed bottom-4 right-4">
|
||||||
<UTooltip text="Visitors currently on my portfolio" placement="left">
|
<UTooltip text="Visitors currently on my portfolio" placement="left">
|
||||||
<nav v-if="!isLoading" 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">
|
<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">
|
<p class="text-neutral-700 dark:text-neutral-300">
|
||||||
{{ visitors }}
|
{{ visitors }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -30,6 +30,6 @@ const { t } = useI18n({
|
|||||||
},
|
},
|
||||||
"es": {
|
"es": {
|
||||||
"quote": "Pase el cursor sobre los textos en negrita para obtener más información sobre mí."
|
"quote": "Pase el cursor sobre los textos en negrita para obtener más información sobre mí."
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
83
app/components/home/Globe.vue
Normal file
83
app/components/home/Globe.vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import createGlobe from 'cobe'
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
useScope: 'local',
|
||||||
|
})
|
||||||
|
|
||||||
|
const myLocation = useState<{ longitude: number, latitude: number }>('location')
|
||||||
|
const globe = ref<HTMLCanvasElement | null>(null)
|
||||||
|
const phi = ref(0)
|
||||||
|
const locations = ref<Array<{ latitude: number, longitude: number }>>([])
|
||||||
|
|
||||||
|
const { data, open } = useWebSocket(`/visitors?latitude=${myLocation.value.latitude}&longitude=${myLocation.value.longitude}`, { immediate: true })
|
||||||
|
watch(data, async (newData) => {
|
||||||
|
locations.value = JSON.parse(typeof newData === 'string' ? newData : await newData.text())
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
open()
|
||||||
|
createGlobe(globe.value!, {
|
||||||
|
devicePixelRatio: 2,
|
||||||
|
width: 400 * 2,
|
||||||
|
height: 400 * 2,
|
||||||
|
phi: 0,
|
||||||
|
theta: 0,
|
||||||
|
dark: 1,
|
||||||
|
diffuse: 1.2,
|
||||||
|
scale: 1,
|
||||||
|
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>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="mt-12 not-prose w-full flex flex-col items-center justify-center">
|
||||||
|
<canvas ref="globe" />
|
||||||
|
<!-- <ClientOnly>
|
||||||
|
<div class="group text-[12px] flex items-center gap-1">
|
||||||
|
<UIcon
|
||||||
|
name="i-ph-map-pin-duotone"
|
||||||
|
/>
|
||||||
|
<p>{{ t('globe') }}</p>
|
||||||
|
</div>
|
||||||
|
</ClientOnly> -->
|
||||||
|
</section>
|
||||||
|
</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>
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
const colorMode = useColorMode()
|
|
||||||
const isDark = computed(() => colorMode.value === 'dark')
|
|
||||||
|
|
||||||
const zoom = ref(11)
|
|
||||||
const coordinates = ref<[number, number]>([2.179040, 48.877419])
|
|
||||||
|
|
||||||
function adjustZoom(amount: number) {
|
|
||||||
const targetZoom = zoom.value + amount
|
|
||||||
const frameRate = 1000 / 60
|
|
||||||
const step = amount / 20
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
if ((amount > 0 && zoom.value < targetZoom) || (amount < 0 && zoom.value > targetZoom)) {
|
|
||||||
zoom.value += step
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
clearInterval(interval)
|
|
||||||
}
|
|
||||||
}, frameRate)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
useScope: 'local',
|
|
||||||
})
|
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex items-center justify-center mt-4 flex-col space-y-1">
|
|
||||||
<div class="relative h-80 md:h-96 w-full">
|
|
||||||
<MapboxMap
|
|
||||||
:options="{
|
|
||||||
accessToken: config.public.mapbox.accessToken,
|
|
||||||
style: isDark ? config.public.mapbox.style.dark : config.public.mapbox.style.light,
|
|
||||||
center: coordinates,
|
|
||||||
zoom,
|
|
||||||
projection: 'globe',
|
|
||||||
}"
|
|
||||||
class="relative z-10"
|
|
||||||
map-id="map"
|
|
||||||
>
|
|
||||||
<MapboxDefaultMarker
|
|
||||||
:lnglat="coordinates"
|
|
||||||
:options="{
|
|
||||||
color: '#808080',
|
|
||||||
size: 1.5,
|
|
||||||
}"
|
|
||||||
marker-id="marker"
|
|
||||||
/>
|
|
||||||
</MapboxMap>
|
|
||||||
<div
|
|
||||||
v-show="zoom < 15"
|
|
||||||
class="map-button left-2"
|
|
||||||
@click.prevent="adjustZoom(1)"
|
|
||||||
>
|
|
||||||
<UIcon
|
|
||||||
name="i-ph-plus-bold"
|
|
||||||
size="24"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-show="zoom > 0"
|
|
||||||
class="map-button right-2"
|
|
||||||
@click.prevent="adjustZoom(-1)"
|
|
||||||
>
|
|
||||||
<UIcon
|
|
||||||
name="i-ph-minus-bold"
|
|
||||||
size="24"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-3 items-center group">
|
|
||||||
<div class="flex items-center justify-center group-hover:animate-slide duration-300">
|
|
||||||
<UIcon
|
|
||||||
name="i-ph-hand-grabbing-duotone"
|
|
||||||
size="16"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p class="text-[12px] italic">
|
|
||||||
{{ t('caption') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.map-button {
|
|
||||||
@apply z-30 absolute bottom-2 dark:bg-gray-900 dark:hover:bg-gray-900 bg-gray-200 hover:bg-gray-100 duration-300 border border-neutral-300 dark:border-neutral-700 cursor-pointer flex items-center justify-center rounded-full p-2
|
|
||||||
}
|
|
||||||
|
|
||||||
.mapboxgl-control-container {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mapboxgl-canvas {
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<i18n lang="json">
|
|
||||||
{
|
|
||||||
"en": {
|
|
||||||
"caption": "Where I live"
|
|
||||||
},
|
|
||||||
"fr": {
|
|
||||||
"caption": "Où j'habite"
|
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"caption": "Donde vivo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</i18n>
|
|
||||||
@@ -15,6 +15,6 @@ const { data: page } = await useAsyncData(`/home/${locale.value}`, () => {
|
|||||||
<HomeActivity />
|
<HomeActivity />
|
||||||
<HomeQuote />
|
<HomeQuote />
|
||||||
<HomeCatchPhrase />
|
<HomeCatchPhrase />
|
||||||
<HomeMap />
|
<HomeGlobe />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
8
app/plugins/location.server.ts
Normal file
8
app/plugins/location.server.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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)
|
||||||
|
}))
|
||||||
|
})
|
||||||
@@ -20,7 +20,6 @@ export default defineNuxtConfig({
|
|||||||
'@nuxtjs/google-fonts',
|
'@nuxtjs/google-fonts',
|
||||||
'@nuxt/image',
|
'@nuxt/image',
|
||||||
'@nuxtjs/i18n',
|
'@nuxtjs/i18n',
|
||||||
'nuxt-mapbox',
|
|
||||||
'nuxt-visitors',
|
'nuxt-visitors',
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -120,8 +119,8 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
// Nitro
|
// Nitro
|
||||||
nitro: {
|
nitro: {
|
||||||
|
preset: 'cloudflare_durable',
|
||||||
experimental: {
|
experimental: {
|
||||||
openAPI: true,
|
|
||||||
websocket: true,
|
websocket: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -144,15 +143,8 @@ export default defineNuxtConfig({
|
|||||||
i18n: {
|
i18n: {
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
},
|
},
|
||||||
mapbox: {
|
|
||||||
accessToken: '',
|
|
||||||
style: {
|
|
||||||
light: '',
|
|
||||||
dark: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
compatibilityDate: '2024-08-19',
|
compatibilityDate: '2025-01-28',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "artsite",
|
"name": "artsite",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"packageManager": "pnpm@9.5.0",
|
"packageManager": "pnpm@9.5.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
"@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": "^8.5.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.3",
|
||||||
@@ -31,14 +33,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^4.1.0",
|
"@antfu/eslint-config": "^4.1.0",
|
||||||
"@nuxt/devtools": "^2.0.0-beta.3",
|
|
||||||
"@types/node": "^22.12.0",
|
"@types/node": "^22.12.0",
|
||||||
"@vueuse/core": "^12.5.0",
|
"@vueuse/core": "^12.5.0",
|
||||||
"@vueuse/nuxt": "^12.5.0",
|
"@vueuse/nuxt": "^12.5.0",
|
||||||
"drizzle-kit": "^0.30.3",
|
"drizzle-kit": "^0.30.3",
|
||||||
"eslint": "^9.19.0",
|
"eslint": "^9.19.0",
|
||||||
"mapbox-gl": "^3.9.4",
|
|
||||||
"nuxt-mapbox": "^1.6.2",
|
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vue-tsc": "^2.2.0",
|
"vue-tsc": "^2.2.0",
|
||||||
"wrangler": "^3.106.0"
|
"wrangler": "^3.106.0"
|
||||||
|
|||||||
614
pnpm-lock.yaml
generated
614
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user