Compare commits

..

8 Commits

Author SHA1 Message Date
4ed7228b60 chore: update package.json to include trustedDependencies and update favicon 2025-11-12 19:10:01 +01:00
d88fd80aee feat: update nuxt configuration and add nuxt-studio module
- Refactored nuxt.config.ts to include 'nuxt-studio' module.
- Removed preview API configuration from content settings.
- Added GitHub repository configuration for Nuxt Studio.
- Introduced prerender settings for the application.

chore: update package dependencies

- Bumped versions of several @iconify-json packages.
- Updated @pinia/nuxt to version 0.11.3.
- Updated vue-router to version 4.6.3 and wrangler to 4.45.4.
- Updated vue-tsc to version 3.1.3.
- Added nuxt-studio as a dependency.

fix: update binary PDF resumes

- Updated English and French resume PDFs in the public/resumes directory.
2025-11-12 19:00:09 +01:00
8856e77ae1 feat: mettre à jour les fichiers PDF de CV en anglais et en français 2025-10-23 12:44:52 +02:00
48e6043205 chore: add wrangler dependency to package.json 2025-10-03 20:32:05 +02:00
22c93c509d chore: update dependencies to latest versions
- Updated @nuxt/ui from ^4.0.0 to 4.0.1
- Updated typescript from ^5.9.2 to 5.9.3
- Updated @types/node from ^24.6.0 to 24.6.2
2025-10-03 17:41:34 +02:00
6c5b561d49 Refactor code structure for improved readability and maintainability 2025-10-03 17:37:34 +02:00
dbebcd23a5 Merge branch 'master' of https://github.com/ArthurDanjou/artchat 2025-10-03 17:05:27 +02:00
fa0421c51d feat: add @nuxtjs/seo module for improved SEO capabilities 2025-10-03 17:05:03 +02:00
44 changed files with 912 additions and 927 deletions

2
.gitignore vendored
View File

@@ -24,3 +24,5 @@ logs
.env
.env.*
!.env.example
.wrangler

View File

@@ -1 +0,0 @@
{"configPath":"../../.output/server/wrangler.json"}

View File

@@ -28,7 +28,7 @@ const head = useLocaleHead()
<ChatCommandPalette
v-motion
:active="messages.length > 0"
:mode="route.path.includes('/projects') || route.path.includes('/writings') || route.path.includes('/canva') ? 'work' : 'chat'"
:mode="route.path.includes('/projects') || route.path.includes('/writings') ? 'work' : 'chat'"
:initial="{
opacity: 0,
y: 200,

View File

@@ -15,7 +15,7 @@ const searchTerm = ref('')
const openMessageModal = ref(false)
const openClearModal = ref(false)
const { t, locale } = useI18n()
const { t, locale } = useI18n({ useScope: 'global' })
const { messages, submitMessage } = useChat()
const { clearMessages, messages: storeMessages } = useChatStore()
@@ -72,7 +72,7 @@ const toolTipContent = {
align: 'center',
side: 'top',
sideOffset: 0,
}
} as any
const router = useRouter()
function goHome() {
@@ -134,7 +134,7 @@ function isRoute(name: string): boolean {
<div class="absolute inset-0 -m-1" />
<div class="flex items-center gap-2.5">
<UIcon :name="item.icon!" size="20" />
<span>{{ t(item.label) }}</span>
<span>{{ t(item.label || '') }}</span>
</div>
<div class="text-dimmed text-xs font-medium text-start">
{{ t(item.prompt) }}

View File

@@ -5,7 +5,7 @@ import { ChatState } from '~~/types'
const props = defineProps<{ messageId: number, fetchStates: ChatFetchState[] }>()
const currentState = ref<ChatFetchState | undefined>(props.fetchStates[0] ?? undefined)
const { setLoadingState } = useChatStore()
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
onMounted(() => {
let index = 0

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
</script>
<template>

View File

@@ -8,7 +8,7 @@ const props = defineProps<{
const isArthur = computed(() => props.message.sender === ChatSender.ARTHUR)
const { t, locale } = useI18n()
const { t, locale } = useI18n({ useScope: 'global' })
const formatDate = computed(() => useDateFormat(props.message.createdAt, 'D MMMM YYYY, HH:mm', { locales: locale.value ?? 'en' }).value)
</script>

View File

@@ -23,7 +23,7 @@ const props = defineProps<{
message: ChatMessage
}>()
const { locale, t } = useI18n()
const { locale, t } = useI18n({ useScope: 'global' })
const formatDate = computed(() => useDateFormat(props.message.createdAt, 'D MMMM YYYY, HH:mm', { locales: locale.value ?? 'en' }).value)
const componentMap: Record<ChatType, Component | undefined> = {
@@ -66,9 +66,19 @@ const dynamicComponent = computed(() => componentMap[props.message.type])
</UCard>
<UCard
v-else
v-motion
variant="soft"
class="mt-1 w-full max-w-none bg-transparent"
:ui="{ body: 'p-0 sm:p-0', header: 'p-0 sm:p-0', footer: 'p-0 sm:p-0' }"
:initial="{
opacity: 0,
y: 20,
}"
:enter="{
opacity: 1,
y: 0,
transition: { ease: 'easeInOut', duration: 300, delay: 500 },
}"
>
<component
:is="dynamicComponent"

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
const { t, locale } = useI18n()
const { t, locale } = useI18n({ useScope: 'global' })
</script>
<template>

View File

@@ -1,11 +1,11 @@
<script lang="ts" setup>
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
</script>
<template>
<UCard class="mt-8 shadow-sm bg-white dark:bg-neutral-900">
<NuxtImg
src="/arthur pro.webp"
src="/arthur-pro.webp"
alt="Arthur Danjou"
class="w-24 h-24 rounded-full float-left mr-4 mb-4"
/>

View File

@@ -3,7 +3,7 @@ import type { UseTimeAgoMessages } from '@vueuse/core'
import type { Activity } from '~~/types'
import { activityMessages, IDEs } from '~~/types'
const { locale, t } = useI18n()
const { locale, t } = useI18n({ useScope: 'global' })
const { data: activity, refresh } = await useAsyncData<Activity>('activity', () => $fetch<Activity>('/api/activity'))
useIntervalFn(async () => await refresh(), 5000)

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { socials } from '~~/types'
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
</script>
<template>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
const year = ref(useNow().value.getFullYear())
</script>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
</script>
<template>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
const { data: experiences } = await useAsyncData('experiences', async () => await queryCollection('experiences').all())
const { t, locale } = useI18n()
const { t, locale } = useI18n({ useScope: 'global' })
const formatDate = (date: string) => useDateFormat(new Date(date), 'MMM YYYY', { locales: locale.value ?? 'en' }).value
function getLanguageForText(text: { en: string, es: string, fr: string }) {
return locale.value === 'en' ? text.en : locale.value === 'es' ? text.es : text.fr

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
</script>
<template>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { en, es, fr } from '@nuxt/ui/locale'
const { locale, t } = useI18n()
const { locale, t } = useI18n({ useScope: 'global' })
const { changeLocale } = useLanguage()
</script>

View File

@@ -10,7 +10,7 @@
<div class="m-1 md:max-w-2/3 shadow-sm rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden relative z-10">
<NuxtImg class="rounded-xl" src="/location.png" />
<div class="size-12 rounded-full border-2 border-sky-500 absolute z-50 top-2/5 -translate-y-1/2 left-1/5 -translate-x-1/2 animate-bounce">
<NuxtImg src="/arthur pro.webp" class="rounded-full" alt="Location of Arthur" />
<NuxtImg src="/arthur-pro.webp" class="rounded-full" alt="Location of Arthur" />
</div>
</div>
</section>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
const { locale, t } = useI18n()
const { locale, t } = useI18n({ useScope: 'global' })
const { data: projects } = await useAsyncData('projects-index', async () => await queryCollection('projects').where('favorite', '=', true).select('title', 'description', 'id', 'publishedAt', 'tags', 'slug').all())
const date = (date: string) => useDateFormat(new Date(date), 'DD MMMM YYYY', { locales: locale.value ?? 'en' })

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
interface ResumeFile {
name: string

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
const { data: skills } = await useAsyncData('skills', async () => await queryCollection('skills').first())
const { t, locale } = useI18n()
const { t, locale } = useI18n({ useScope: 'global' })
</script>
<template>

View File

@@ -3,7 +3,7 @@ import type { Stats } from '~~/types'
const { data: stats } = await useAsyncData<Stats>('stats', () => $fetch('/api/stats'))
const { locale, t } = useI18n()
const { locale, t } = useI18n({ useScope: 'global' })
const time = useTimeAgo(new Date(stats.value!.coding.data.range.start) ?? new Date()).value.split(' ')[0]
const date = useDateFormat(new Date(stats.value!.coding.data.range.start ?? new Date()), 'DD MMMM YYYY', { locales: locale.value ?? 'en' })

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
const { dark, toggleDark } = useTheme()
</script>

View File

@@ -6,7 +6,7 @@ const props = defineProps({
},
})
const { locale } = useI18n()
const { locale } = useI18n({ useScope: 'global' })
const { data: items } = await useAsyncData(`uses-${props.category}`, async () => await queryCollection('uses').where('category', '=', props.category).all())
const { data: categoryData } = await useAsyncData(`category-${props.category}`, async () => await queryCollection('usesCategories').where('slug', '=', props.category).first())

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { Weather } from '~~/types'
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
const { data: weather } = await useAsyncData<Weather>('weather', () =>
$fetch('/api/weather'))
</script>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
const { locale, t } = useI18n()
const { locale, t } = useI18n({ useScope: 'global' })
const { data: writings } = await useAsyncData('writings-index', async () => await queryCollection('writings').order('publishedAt', 'DESC').select('title', 'description', 'id', 'publishedAt', 'tags', 'slug').limit(2).all())
const formatDate = (date: string) => useDateFormat(new Date(date), 'DD MMMM YYYY', { locales: locale.value ?? 'en' })

View File

@@ -1,5 +1,5 @@
export function useLanguage() {
const { setLocale } = useI18n()
const { setLocale } = useI18n({ useScope: 'global' })
async function changeLocale(newLocale: string) {
await setLocale(newLocale as 'en' | 'fr' | 'es')

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
</script>
<template>

View File

@@ -10,7 +10,7 @@ const { messages } = useChatStore()
const parents = useTemplateRef('parents')
const { height } = useElementBounding(parents)
const { locale } = useI18n()
const { locale } = useI18n({ useScope: 'global' })
const lastLang = ref(locale.value)
watch(
height,

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup>
import { useDateFormat } from '#imports'
const route = useRoute()
const { data: project } = await useAsyncData(`projects/${route.params.slug}`, () =>
queryCollection('projects').path(`/projects/${route.params.slug}`).first())
@@ -16,7 +18,9 @@ useSeoMeta({
author: 'Arthur Danjou',
})
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
useSeoMeta(project.value.seo || {})
</script>
<template>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
useSeoMeta({
title: 'My Projects',
description: t('projects.description'),

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup>
import { useDateFormat } from '#imports'
const route = useRoute()
const { data: writing } = await useAsyncData(`writings/${route.params.slug}`, () =>
queryCollection('writings').path(`/writings/${route.params.slug}`).first())
@@ -10,13 +12,9 @@ if (!writing.value) {
})
}
useSeoMeta({
title: writing.value?.title,
description: writing.value?.description,
author: 'Arthur Danjou',
})
const { t } = useI18n({ useScope: 'global' })
const { t } = useI18n()
useSeoMeta(writing.value.seo || {})
</script>
<template>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
useSeoMeta({
title: 'My Shelf - Arthur DANJOU',
description: t('writings.description'),

1437
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -1,104 +1,111 @@
import { defineCollection, z } from '@nuxt/content'
import { defineCollection, defineContentConfig, z } from '@nuxt/content'
import { asSeoCollection } from '@nuxtjs/seo/content'
export const collections = {
projects: defineCollection({
type: 'page',
source: 'projects/*.md',
schema: z.object({
slug: z.string(),
title: z.string(),
description: z.string(),
publishedAt: z.string(),
readingTime: z.number().optional(),
tags: z.array(z.string()),
cover: z.string(),
favorite: z.boolean().optional(),
canva: z.object({
height: z.number().default(270),
width: z.number().default(480),
}),
}),
}),
writings: defineCollection({
type: 'page',
source: 'writings/*.md',
schema: z.object({
slug: z.string(),
title: z.string(),
description: z.string(),
publishedAt: z.string(),
readingTime: z.number(),
cover: z.string().optional(),
tags: z.array(z.string()),
canva: z.object({
height: z.number().default(270),
width: z.number().default(480),
}),
}),
}),
usesCategories: defineCollection({
type: 'data',
source: 'uses/categories/*.json',
schema: z.object({
slug: z.string(),
name: z.object({
en: z.string(),
fr: z.string(),
es: z.string(),
}),
}),
}),
uses: defineCollection({
type: 'data',
source: 'uses/*.json',
schema: z.object({
name: z.string(),
description: z.object({
en: z.string(),
fr: z.string(),
es: z.string(),
}),
category: z.string(),
}),
}),
skills: defineCollection({
type: 'data',
source: 'skills.json',
schema: z.object({
body: z.array(z.object({
id: z.string(),
name: z.object({
en: z.string(),
fr: z.string(),
es: z.string(),
export default defineContentConfig({
collections: {
projects: defineCollection(
asSeoCollection({
type: 'page',
source: 'projects/*.md',
schema: z.object({
slug: z.string(),
title: z.string(),
description: z.string(),
publishedAt: z.string(),
readingTime: z.number().optional(),
tags: z.array(z.string()),
cover: z.string(),
favorite: z.boolean().optional(),
}),
items: z.array(z.object({
}),
),
writings: defineCollection(
asSeoCollection({
type: 'page',
source: 'writings/*.md',
schema: z.object({
slug: z.string(),
title: z.string(),
description: z.string(),
publishedAt: z.string(),
readingTime: z.number(),
cover: z.string().optional(),
tags: z.array(z.string()),
}),
}),
),
usesCategories: defineCollection(
asSeoCollection({
type: 'data',
source: 'uses/categories/*.json',
schema: z.object({
slug: z.string(),
name: z.object({
en: z.string(),
fr: z.string(),
es: z.string(),
}),
}),
}),
),
uses: defineCollection(
asSeoCollection({
type: 'data',
source: 'uses/*.json',
schema: z.object({
name: z.string(),
icon: z.string(),
})),
})),
}),
}),
experiences: defineCollection({
type: 'data',
source: 'experiences/*.json',
schema: z.object({
title: z.object({
en: z.string(),
fr: z.string(),
es: z.string(),
description: z.object({
en: z.string(),
fr: z.string(),
es: z.string(),
}),
category: z.string(),
}),
}),
company: z.string(),
companyUrl: z.string().url().optional(),
startDate: z.string(),
endDate: z.string().optional(),
location: z.string(),
description: z.object({
en: z.string(),
fr: z.string(),
es: z.string(),
),
skills: defineCollection(
asSeoCollection({
type: 'data',
source: 'skills.json',
schema: z.object({
body: z.array(z.object({
id: z.string(),
name: z.object({
en: z.string(),
fr: z.string(),
es: z.string(),
}),
items: z.array(z.object({
name: z.string(),
icon: z.string(),
})),
})),
}),
}),
tags: z.array(z.string()),
}),
}),
}
),
experiences: defineCollection(
asSeoCollection({
type: 'data',
source: 'experiences/*.json',
schema: z.object({
title: z.object({
en: z.string(),
fr: z.string(),
es: z.string(),
}),
company: z.string(),
companyUrl: z.string().url().optional(),
startDate: z.string(),
endDate: z.string().optional(),
location: z.string(),
description: z.object({
en: z.string(),
fr: z.string(),
es: z.string(),
}),
tags: z.array(z.string()),
}),
}),
),
},
})

View File

@@ -230,9 +230,6 @@
},
"top": "Go to top"
},
"canva": {
"title": "Loading the canva ..."
},
"writings": {
"description": "All my reflections on programming, mathematics, the conception of artificial intelligence, etc., are put in chronological order.",
"title": "Writings on math, artificial intelligence, development, and my passions.",

View File

@@ -233,9 +233,6 @@
"top": "Ir arriba"
},
"alert": "Por falta de tiempo, no tuve tiempo para traducir este contenido al francés. Gracias por su comprensión.",
"canva": {
"title": "Cargando el lienzo ..."
},
"writings": {
"description": "Todas mis reflexiones sobre programación, matemáticas, la concepción de la inteligencia artificial, etc., están organizadas en orden cronológico.",
"title": "Escritos sobre matemáticas, inteligencia artificial, desarrollo y mis pasiones.",

View File

@@ -231,9 +231,6 @@
},
"top": "Remonter en haut"
},
"canva": {
"title": "Chargement du canva..."
},
"writings": {
"description": "Toutes mes réflexions sur la programmation, les mathématiques, la conception de l'intelligence artificielle, etc., sont mises en ordre chronologique.",
"title": "Écrits sur les maths, l'intelligence artificielle, le développement et mes passions.",

View File

@@ -1,3 +1,5 @@
import { definePerson } from 'nuxt-schema-org/schema'
export default defineNuxtConfig({
compatibilityDate: '2025-07-20',
@@ -17,22 +19,45 @@ export default defineNuxtConfig({
css: ['~/assets/css/main.css'],
// Nuxt Modules
modules: [
'@nuxt/ui',
'@nuxt/content',
'@vueuse/nuxt',
'@nuxtjs/google-fonts',
'@nuxt/image',
'@vueuse/motion/nuxt',
'@pinia/nuxt',
'@nuxtjs/i18n',
],
modules: ['@nuxt/ui', '@nuxtjs/seo', '@nuxt/content', '@vueuse/nuxt', '@nuxtjs/google-fonts', '@nuxt/image', '@vueuse/motion/nuxt', '@pinia/nuxt', '@nuxtjs/i18n', 'nuxt-studio'],
ogImage: {
enabled: false,
},
linkChecker: {
enabled: false,
},
site: {
url: 'https://arthurdanjou.fr',
name: 'Developer enjoying Artificial Intelligence and Machine Learning. Mathematics Student at Paris Dauphine-PSL University specialised in Statistics and Data Science.',
},
schemaOrg: {
identity: definePerson({
// Basic Information, if applicable
name: 'Arthur Danjou',
givenName: 'Arthur',
familyName: 'Danjou',
// Profile Information, if applicable
image: '/arthur-pro.webp',
description: 'AI researcher and technical author specializing in machine learning and neural networks',
jobTitle: 'Principal AI Researcher',
// Contact & Social, if applicable
email: 'arthurdanjou@outlook.fr',
url: 'https://go.arthurdanjou.fr/website',
sameAs: [
'https://go.arthurdanjou.fr/twitter',
'https://go.arthurdanjou.fr/github',
'https://go.arthurdanjou.fr/linkedin',
],
}),
},
// Nuxt Content
content: {
preview: {
api: 'https://api.nuxt.studio',
},
build: {
markdown: {
highlight: {
@@ -69,6 +94,17 @@ export default defineNuxtConfig({
timeline: { enabled: true },
},
// Nuxt Studio
studio: {
// GitHub repository configuration (owner and repo are required)
repository: {
provider: 'github', // only GitHub is currently supported
owner: 'arthurdanjou', // your GitHub username or organization
repo: 'artchat', // your repository name
branch: 'main', // the branch to commit to (default: main)
},
},
// Nuxt I18N
i18n: {
strategy: 'no_prefix',
@@ -121,6 +157,10 @@ export default defineNuxtConfig({
deployConfig: true,
nodeCompat: true,
},
prerender: {
routes: ['/'],
crawlLinks: true,
},
},
// Nuxt Env

View File

@@ -13,36 +13,43 @@
},
"dependencies": {
"@antfu/eslint-config": "^5.4.1",
"@iconify-json/devicon": "^1.2.45",
"@iconify-json/logos": "^1.2.9",
"@iconify-json/devicon": "^1.2.46",
"@iconify-json/logos": "^1.2.10",
"@iconify-json/ph": "^1.2.2",
"@iconify-json/simple-icons": "^1.2.53",
"@iconify-json/simple-icons": "^1.2.57",
"@iconify-json/twemoji": "^1.2.4",
"@iconify-json/vscode-icons": "^1.2.30",
"@iconify-json/vscode-icons": "^1.2.33",
"@nuxt/content": "3.7.1",
"@nuxt/eslint": "1.9.0",
"@nuxt/image": "^1.11.0",
"@nuxt/ui": "^4.0.0",
"@nuxt/ui": "4.0.1",
"@nuxtjs/google-fonts": "^3.2.0",
"@nuxtjs/i18n": "10.1.0",
"@pinia/nuxt": "^0.11.2",
"@nuxtjs/seo": "^3.2.2",
"@pinia/nuxt": "^0.11.3",
"@tailwindcss/typography": "^0.5.19",
"@vueuse/math": "13.9.0",
"@vueuse/motion": "^3.0.3",
"better-sqlite3": "^12.4.1",
"eslint": "9.36.0",
"nuxt": "4.1.2",
"nuxt-studio": "1.0.0-alpha.1",
"rehype-katex": "^7.0.1",
"remark-math": "^6.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"typescript": "^5.9.2",
"typescript": "5.9.3",
"vue": "^3.5.22",
"vue-router": "^4.5.1"
"vue-router": "^4.6.3",
"wrangler": "^4.45.4"
},
"devDependencies": {
"@types/node": "^24.6.0",
"@types/node": "24.6.2",
"@vueuse/nuxt": "^13.9.0",
"vue-tsc": "^3.1.0"
}
"vue-tsc": "^3.1.3"
},
"trustedDependencies": [
"@parcel/watcher",
"unrs-resolver"
]
}

View File

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Binary file not shown.