26 Commits

Author SHA1 Message Date
da5a859d0f fix: mettre à jour l'année de copyright dans le fichier LICENSE 2026-01-06 16:23:03 +01:00
e33ef53329 fix: supprimer la route de prévisualisation dans la configuration de Wrangler 2026-01-06 15:00:24 +01:00
de2e3cc0a5 fix: améliorer le déploiement avec Cloudflare Wrangler en capturant l'URL de déploiement 2026-01-06 14:56:53 +01:00
2197c23062 fix: supprimer les fichiers de configuration inutiles de .gitignore 2026-01-06 14:52:44 +01:00
830ea37cb0 fix: ajouter les secrets API pour Cloudflare Wrangler 2026-01-06 14:15:48 +01:00
f3d3a507dc fix: mettre à jour la commande de déploiement pour Cloudflare Wrangler 2026-01-06 14:14:52 +01:00
33bb54ec5f fix: ajouter la route de pré-rendu pour la page d'accueil 2026-01-06 13:57:18 +01:00
e0bb04d9b8 fix: mettre à jour les dépendances nuxt-studio et vue-tsc vers leurs versions stables 2026-01-06 13:50:45 +01:00
b30f2eb523 fix: étendre la condition de déploiement pour inclure la branche master 2026-01-05 15:42:35 +01:00
e32e7bf7bb fix: supprimer l'affichage des heures brutes et du total dans le composant Stats 2026-01-04 20:07:31 +01:00
4dc74c0011 fix: corriger l'utilisation de usePrecision pour totalHours dans le composant Stats 2026-01-04 20:04:38 +01:00
f95a417b37 fix: mettre à jour le statut du projet de 'En cours' à 'Terminé' dans data-visualisation.md 2026-01-04 20:01:00 +01:00
055c16e198 Refactor code structure for improved readability and maintainability 2026-01-04 20:00:43 +01:00
669eec60a1 fix: refactor le calcul de totalHours dans le composant Stats 2026-01-04 19:57:42 +01:00
4cda11a6a3 fix: ajouter des conditions d'affichage pour les cartes dans le composant Stats 2026-01-04 19:54:18 +01:00
df33efc731 fix: corriger la déclaration de totalHours dans le composant Stats 2026-01-04 19:53:15 +01:00
305c91199a fix: corriger la récupération de la date de début dans le composant Stats 2026-01-04 19:46:58 +01:00
ed3019d1b9 fix: corriger l'utilisation des valeurs retournées pour yearsCollected et formattedDate dans le composant Stats 2026-01-04 19:43:10 +01:00
988e18b2f7 fix: gérer les valeurs nulles pour les statistiques dans le composant Stats 2026-01-04 19:41:22 +01:00
5d4f6ee9a4 fix: uniformiser le nom de cache pour les fonctions Wakatime dans le composant Stats 2026-01-04 19:40:10 +01:00
d13594e5d1 fix: uniformiser le nom de cache pour les données Wakatime dans le composant Stats 2026-01-04 19:34:43 +01:00
9aabe422b3 fix: supprimer le chargement paresseux des données dans le composant Stats et améliorer la gestion des requêtes API 2026-01-04 19:30:48 +01:00
e30d956f58 fix: activer le chargement paresseux des données dans le composant Stats 2026-01-04 19:23:08 +01:00
56c63d8db7 fix: mettre à jour les couleurs des icônes et des barres de progression dans le composant Stats 2026-01-04 19:07:47 +01:00
c8116e10b8 fix: supprimer l'affichage des langues, éditeurs et systèmes d'exploitation dans le composant Stats et ajuster les couleurs 2026-01-04 19:03:32 +01:00
9dc21f4287 fix: afficher les langues, éditeurs et systèmes d'exploitation dans le composant Stats 2026-01-04 18:59:57 +01:00
12 changed files with 100 additions and 66 deletions

View File

@@ -31,6 +31,8 @@ jobs:
uses: cloudflare/wrangler-action@v3
with:
command: types
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: Build
run: bun run build
@@ -44,25 +46,32 @@ jobs:
NUXT_STATUS_PAGE: ${{ secrets.NUXT_STATUS_PAGE }}
STUDIO_GITHUB_CLIENT_ID: ${{ secrets.STUDIO_GITHUB_CLIENT_ID }}
STUDIO_GITHUB_CLIENT_SECRET: ${{ secrets.STUDIO_GITHUB_CLIENT_SECRET }}
STUDIO_GITHUB_MODERATORS: ${{ secrets.STUDIO_GITHUB_MODERATORS }}
- name: Determine Deployment Target
- name: Determine Deployment Command
id: target
run: |
if [ "${{ github.ref_name }}" = "main" ]; then
echo "env_flag=" >> $GITHUB_OUTPUT
if [ "${{ github.ref_name }}" = "main" ] || [ "${{ github.ref_name }}" = "master" ]; then
echo "wrangler_command=deploy" >> $GITHUB_OUTPUT
echo "env_name=Production" >> $GITHUB_OUTPUT
else
echo "env_flag=--env preview" >> $GITHUB_OUTPUT
echo "wrangler_command=versions upload" >> $GITHUB_OUTPUT
echo "env_name=Preview" >> $GITHUB_OUTPUT
fi
- name: Publish to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy ${{ steps.target.outputs.env_flag }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
- name: Run Cloudflare Wrangler & Capture URL
id: wrangler
run: |
# Exécuter wrangler et rediriger la sortie vers un fichier tout en l'affichant (tee)
bunx wrangler ${{ steps.target.outputs.wrangler_command }} | tee wrangler.log
# Extraction de l'URL
if [ "${{ steps.target.outputs.env_name }}" = "Preview" ]; then
PREVIEW_URL=$(grep -o 'https://[^ ]*\.workers\.dev' wrangler.log | head -n 1)
echo "DEPLOY_URL=$PREVIEW_URL" >> $GITHUB_OUTPUT
else
echo "DEPLOY_URL=https://arthurdanjou.fr" >> $GITHUB_OUTPUT
fi
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
@@ -77,7 +86,8 @@ jobs:
title: "Déploiement Portfolio (${{ steps.target.outputs.env_name }})"
description: |
Build terminé sur la branche **${{ github.ref_name }}**.
Environnement cible : **${{ steps.target.outputs.env_name }}**.
Environnement : **${{ steps.target.outputs.env_name }}**
URL : **${{ steps.wrangler.outputs.DEPLOY_URL }}**
Commit: `${{ github.sha }}` par ${{ github.actor }}.
nofail: false
nodetail: false

2
.gitignore vendored
View File

@@ -16,8 +16,6 @@ logs
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Arthur Danjou
Copyright (c) 2026 Arthur Danjou
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -4,15 +4,19 @@ import { usePrecision } from '@vueuse/math'
const { data: stats } = await useAsyncData<Stats>('stats', () => $fetch('/api/stats'))
const startDate = computed(() => new Date(stats.value!.coding.range.start))
const yearsCollected = useTimeAgo(startDate).value
const formattedDate = useDateFormat(startDate, 'MMM DD, YYYY').value
const startDate = computed(() => new Date(stats.value?.coding?.range?.start ?? new Date()))
const rawHours = computed(() => {
const seconds = stats.value?.coding?.grand_total?.total_seconds_including_other_language ?? 0
return seconds / 3600
})
const totalHours = usePrecision((stats.value!.coding.grand_total.total_seconds_including_other_language ?? 0) / 3600, 0)
const totalHours = usePrecision(rawHours, 0)
const yearsCollected = useTimeAgo(startDate)
const formattedDate = useDateFormat(startDate, 'MMM DD, YYYY')
const topLanguages = computed(() => stats.value!.languages.slice(0, 4))
const topEditors = computed(() => stats.value!.editors.slice(0, 3))
const topOS = computed(() => stats.value!.os.slice(0, 2))
const topLanguages = computed(() => stats.value?.languages.slice(0, 4) ?? [])
const topEditors = computed(() => stats.value?.editors.slice(0, 3) ?? [])
const topOS = computed(() => stats.value?.os.slice(0, 2) ?? [])
</script>
<template>
@@ -22,7 +26,7 @@ const topOS = computed(() => stats.value!.os.slice(0, 2))
class="space-y-6"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<UCard>
<UCard v-if="totalHours">
<div class="flex items-center gap-4">
<div class="p-3 bg-primary-200 dark:bg-primary-900 rounded-lg text-primary-500 flex items-center justify-center">
<UIcon
@@ -41,7 +45,7 @@ const topOS = computed(() => stats.value!.os.slice(0, 2))
</div>
</UCard>
<UCard>
<UCard v-if="formattedDate && yearsCollected">
<div class="flex items-center gap-4">
<div class="p-3 bg-emerald-100 dark:bg-emerald-900/20 rounded-lg text-emerald-500 flex items-center justify-center">
<UIcon
@@ -71,11 +75,14 @@ const topOS = computed(() => stats.value!.os.slice(0, 2))
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="col-span-1 lg:col-span-1 space-y-4">
<div
v-if="topLanguages.length"
class="col-span-1 lg:col-span-1 space-y-4"
>
<h4 class="text-sm font-semibold text-neutral-900 dark:text-white flex items-center gap-2">
<UIcon
name="i-ph-code-block-duotone"
class="text-primary-500 w-5 h-5"
class="text-red-500 w-5 h-5"
/>
Top Languages
</h4>
@@ -91,18 +98,21 @@ const topOS = computed(() => stats.value!.os.slice(0, 2))
</div>
<UProgress
v-model="lang.percent"
color="primary"
color="red"
size="sm"
/>
</div>
</div>
</div>
<div class="col-span-1 lg:col-span-1 space-y-4">
<div
v-if="topEditors.length"
class="col-span-1 lg:col-span-1 space-y-4"
>
<h4 class="text-sm font-semibold text-neutral-900 dark:text-white flex items-center gap-2">
<UIcon
name="i-ph-terminal-window-duotone"
class="text-orange-500 w-5 h-5"
class="text-green-500 w-5 h-5"
/>
Preferred Editors
</h4>
@@ -118,14 +128,17 @@ const topOS = computed(() => stats.value!.os.slice(0, 2))
</div>
<UProgress
v-model="editor.percent"
color="orange"
color="green"
size="sm"
/>
</div>
</div>
</div>
<div class="col-span-1 lg:col-span-1 space-y-4">
<div
v-if="topOS.length"
class="col-span-1 lg:col-span-1 space-y-4"
>
<h4 class="text-sm font-semibold text-neutral-900 dark:text-white flex items-center gap-2">
<UIcon
name="i-ph-desktop-duotone"

View File

@@ -18,10 +18,10 @@
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",
"nuxt": "4.2.2",
"nuxt-studio": "1.0.0-beta.3",
"nuxt-studio": "1.0.0",
"vue": "3.5.26",
"vue-router": "4.6.4",
"zod": "^4.3.4",
"zod": "^4.3.5",
},
"devDependencies": {
"@iconify-json/devicon": "1.2.56",
@@ -34,7 +34,7 @@
"@vueuse/nuxt": "14.1.0",
"eslint": "9.39.2",
"typescript": "^5.9.3",
"vue-tsc": "3.2.1",
"vue-tsc": "3.2.2",
"wrangler": "4.54.0",
},
},
@@ -970,7 +970,7 @@
"@vue/devtools-shared": ["@vue/devtools-shared@8.0.5", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg=="],
"@vue/language-core": ["@vue/language-core@3.2.1", "", { "dependencies": { "@volar/language-core": "2.4.27", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" } }, "sha512-g6oSenpnGMtpxHGAwKuu7HJJkNZpemK/zg3vZzZbJ6cnnXq1ssxuNrXSsAHYM3NvH8p4IkTw+NLmuxyeYz4r8A=="],
"@vue/language-core": ["@vue/language-core@3.2.2", "", { "dependencies": { "@volar/language-core": "2.4.27", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" } }, "sha512-5DAuhxsxBN9kbriklh3Q5AMaJhyOCNiQJvCskN9/30XOpdLiqZU9Q+WvjArP17ubdGEyZtBzlIeG5nIjEbNOrQ=="],
"@vue/reactivity": ["@vue/reactivity@3.5.26", "", { "dependencies": { "@vue/shared": "3.5.26" } }, "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ=="],
@@ -2014,7 +2014,7 @@
"nuxt-site-config-kit": ["nuxt-site-config-kit@3.2.14", "", { "dependencies": { "@nuxt/kit": "^4.2.2", "pkg-types": "^2.3.0", "site-config-stack": "3.2.14", "std-env": "^3.10.0", "ufo": "^1.6.1" } }, "sha512-HOdYOLWL8f25aNEPdf72OuvL4Z7bMCNDm+XXDQ9bykNA1X3wJ8/vcMfU3tS4nPccoyaW100RTgTFSitAWp+iuA=="],
"nuxt-studio": ["nuxt-studio@1.0.0-beta.3", "", { "dependencies": { "@iconify-json/lucide": "^1.2.82", "@nuxtjs/mdc": "^0.19.1", "@vueuse/core": "^14.1.0", "defu": "^6.1.4", "destr": "^2.0.5", "js-yaml": "^4.1.1", "minimatch": "^10.1.1", "nuxt-component-meta": "^0.16.0", "remark-mdc": "^3.9.0", "shiki": "^3.20.0", "unstorage": "1.17.3" } }, "sha512-Rcx7sfsQ0CeHY0rtqLKSgm2pRPF0mknJ9UVXyKWHkcnAFnhouAM8r6SoPLVHRdd/cmxdM9uTrNTfz6CNbk7vtQ=="],
"nuxt-studio": ["nuxt-studio@1.0.0", "", { "dependencies": { "@iconify-json/lucide": "^1.2.82", "@nuxtjs/mdc": "^0.19.2", "@vueuse/core": "^14.1.0", "defu": "^6.1.4", "destr": "^2.0.5", "js-yaml": "^4.1.1", "minimatch": "^10.1.1", "nuxt-component-meta": "^0.16.0", "remark-mdc": "^3.10.0", "shiki": "^3.20.0", "unstorage": "1.17.3" } }, "sha512-6tlO2jzZZ5xvSari4155lh1blV+jUC/JxQGY+437vUrVdFZPH1bALfwtHzWosEY1vbMpb2chr3I+OTUdnkTKxw=="],
"nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="],
@@ -2658,7 +2658,7 @@
"vue-router": ["vue-router@4.6.4", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg=="],
"vue-tsc": ["vue-tsc@3.2.1", "", { "dependencies": { "@volar/typescript": "2.4.27", "@vue/language-core": "3.2.1" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-I23Rk8dkQfmcSbxDO0dmg9ioMLjKA1pjlU3Lz6Jfk2pMGu3Uryu9810XkcZH24IzPbhzPCnkKo2rEMRX0skSrw=="],
"vue-tsc": ["vue-tsc@3.2.2", "", { "dependencies": { "@volar/typescript": "2.4.27", "@vue/language-core": "3.2.2" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-r9YSia/VgGwmbbfC06hDdAatH634XJ9nVl6Zrnz1iK4ucp8Wu78kawplXnIDa3MSu1XdQQePTHLXYwPDWn+nyQ=="],
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],

View File

@@ -5,7 +5,7 @@ type: Academic Project
description: An interactive data visualization project built with R, R Shiny, and ggplot2 for creating dynamic, explorable visualizations.
publishedAt: 2026-01-05
readingTime: 1
status: In progress
status: Completed
tags:
- R
- R Shiny
@@ -44,9 +44,11 @@ This project involves creating an interactive data visualization application usi
## 📚 Resources
You can find the code here: [Data Visualisation](https://go.arthurdanjou.fr/dataviz)
You can find the code here: [Data Visualisation Code](https://go.arthurdanjou.fr/datavis-code)
And the online application here: [Data Visualisation App](https://go.arthurdanjou.fr/datavis-app)
## 📄 Detailed Report
<iframe src="/projects/dataviz.pdf" width="100%" height="1000px">
<iframe src="/projects/datavis.pdf" width="100%" height="1000px">
</iframe>

View File

@@ -122,6 +122,7 @@ export default defineNuxtConfig({
},
prerender: {
routes: ['/'],
crawlLinks: true
}
},

View File

@@ -24,7 +24,7 @@
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",
"nuxt": "4.2.2",
"nuxt-studio": "1.0.0-beta.3",
"nuxt-studio": "1.0.0",
"vue": "3.5.26",
"vue-router": "4.6.4",
"zod": "^4.3.5"
@@ -40,7 +40,7 @@
"@vueuse/nuxt": "14.1.0",
"eslint": "9.39.2",
"typescript": "^5.9.3",
"vue-tsc": "3.2.1",
"vue-tsc": "3.2.2",
"wrangler": "4.54.0"
}
}

BIN
public/projects/datavis.pdf Normal file

Binary file not shown.

View File

@@ -1,42 +1,56 @@
import type { H3Event } from 'h3'
const WAKATIME_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'application/json'
}
const fetchWakatime = async (url: string) => {
try {
return await $fetch<{ data: unknown[] }>(url, {
headers: WAKATIME_HEADERS,
timeout: 5000
})
}
catch (err) {
console.error(`[Wakatime Error] Failed to fetch ${url}`, err)
return { data: [] }
}
}
const cachedWakatimeCoding = defineCachedFunction(async (event: H3Event) => {
const config = useRuntimeConfig(event)
return await $fetch<{ data: unknown[] }>(`https://wakatime.com/share/${config.wakatime.userId}/${config.wakatime.coding}.json`)
return await fetchWakatime(`https://wakatime.com/share/${config.wakatime.userId}/${config.wakatime.coding}.json`)
}, {
maxAge: 60,
name: 'wakatime',
maxAge: 60 * 50,
name: 'stats',
getKey: () => 'coding'
})
const cachedWakatimeEditors = defineCachedFunction(async (event: H3Event) => {
const config = useRuntimeConfig(event)
return await $fetch<{ data: unknown[] }>(`https://wakatime.com/share/${config.wakatime.userId}/${config.wakatime.editors}.json`)
return await fetchWakatime(`https://wakatime.com/share/${config.wakatime.userId}/${config.wakatime.editors}.json`)
}, {
maxAge: 60,
name: 'wakatime',
maxAge: 60 * 60,
name: 'stats',
getKey: () => 'editors'
})
const cachedWakatimeOs = defineCachedFunction(async (event: H3Event) => {
const config = useRuntimeConfig(event)
return await $fetch<{ data: unknown[] }>(`https://wakatime.com/share/${config.wakatime.userId}/${config.wakatime.os}.json`)
return await fetchWakatime(`https://wakatime.com/share/${config.wakatime.userId}/${config.wakatime.os}.json`)
}, {
maxAge: 60,
name: 'wakatime',
maxAge: 60 * 60,
name: 'stats',
getKey: () => 'os'
})
const cachedWakatimeLanguages = defineCachedFunction(async (event: H3Event) => {
const config = useRuntimeConfig(event)
return await $fetch<{ data: unknown[] }>(`https://wakatime.com/share/${config.wakatime.userId}/${config.wakatime.languages}.json`)
return await fetchWakatime(`https://wakatime.com/share/${config.wakatime.userId}/${config.wakatime.languages}.json`)
}, {
maxAge: 60,
name: 'wakatime',
maxAge: 60 * 60,
name: 'stats',
getKey: () => 'languages'
})

View File

@@ -1,7 +1,10 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types` (hash: 373e9a05bf207b93549ab53665d07e4b)
// Generated by Wrangler by running `wrangler types` (hash: 8c48032b4b2801cdbac6e8dbc9d26203)
// Runtime types generated with workerd@1.20251210.0 2025-12-13 nodejs_compat
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./.output/server/index");
}
interface Env {
CACHE: KVNamespace;
STUDIO_GITHUB_CLIENT_ID: string;

View File

@@ -57,13 +57,6 @@
},
"env": {
"preview": {
"routes": [
{
"pattern": "preview.arthurdanjou.fr",
"zone_name": "arthurdanjou.fr",
"custom_domain": true
}
],
"d1_databases": [
{
"binding": "DB",