Migration to Nuxt3

This commit is contained in:
2023-04-30 18:36:39 +02:00
parent ed2e1824c5
commit 0480b99865
170 changed files with 13890 additions and 3699 deletions

View File

@ -0,0 +1,19 @@
<template>
<div class="my-4 text-left md:text-center">
<a :href="props.url">
<div class="inline-flex items-center">
<img :src="props.icon" alt="Github" class="h-7 mr-2 select-none dark:invert" />
<span class="text-xl text-left">{{ props.label }}</span>
</div>
</a>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
url: String,
label: String,
icon: String, alt: String
});
</script>

139
components/Cookie.vue Normal file
View File

@ -0,0 +1,139 @@
<template>
<ClientOnly>
<div v-if="show_banner" class="w-full h-full">
<div ref="container_cookie" class="absolute top-0 left-0 h-full w-full pointer-events-none z-40">
<canvas ref="canvas_cookie" :width="canvas_width" :height="canvas_height"></canvas>
</div>
<div ref="banner" class="fixed bottom-0 left-0 z-50 bg-slate-200/90 border-slate-700 dark:bg-slate-800/90 dark:border-slate-400
border rounded w-fit lg:w-1/2 p-3 px-5 m-5">
<p class="text-sm">{{ $t("cookie policy title") }}</p>
<div class="text-xs">
<span>{{ $t("cookie policy") }} <button @click="throwCookie" class="underline">{{ $t("cookie policy link") }}</button></span>
</div>
<div class="flex justify-end mt-2 text-sm">
<button @click="refuseCookieBanner" class="mx-1 hover:text-slate-500 dark:hover:text-slate-300">{{ $t("reject") }}</button>
<button @click="acceptCookieBanner" class="rounded p-2 mx-1 bg-slate-300 hover:bg-slate-400 dark:bg-slate-700 dark:hover:bg-slate-600">{{ $t("accept") }}</button>
</div>
</div>
</div>
</ClientOnly>
</template>
<script setup lang="ts">
// @ts-ignore
import { Engine, Render, Bodies, Composite, Body, Runner } from "matter-js";
import cookie_image from "@/assets/images/cookie.png";
const container_cookie = ref(null);
const canvas_cookie = ref(null);
const show_banner = ref(true);
const canvas_width = ref(0);
const canvas_height = ref(0);
let engine:any = null;
onMounted(async () => {
show_banner.value = shouldShowCookie()
canvas_width.value = getWidth();
canvas_height.value = getHeight();
if (shouldShowCookie()) {
await nextTick()
initCanvas();
new ResizeObserver(() => {
resizeCanvas();
}).observe(document.body);
}
});
function acceptCookieBanner() {
acceptCookie();
show_banner.value = false;
}
function refuseCookieBanner() {
refuseCookie();
show_banner.value = false;
}
function getWidth() { return document.body.clientWidth; }
function getHeight() { return document.body.scrollHeight; }
function resizeCanvas() {
canvas_width.value = getWidth();
canvas_height.value = getHeight();
}
/* Inits cookie throwing area */
function initCanvas() {
if (!engine) {
engine = Engine.create();
let renderer = Render.create({
element: container_cookie.value,
canvas: canvas_cookie.value,
engine: engine,
options: {
width: getWidth(),
height: getHeight(),
wireframes: false,
background: "#00000000"
}
});
Render.run(renderer);
Runner.run(Runner.create(), engine);
}
resizeCanvas();
}
function throwCookie() {
addFoundEasterEgg("cookie");
if (!engine) { return; }
// Creates a cookie at the bottom of visible screen
let cookie = Bodies.circle(randomInt(0, getWidth()), (document.documentElement.scrollTop + window.screen.height), 30, {
render: {
sprite: {
texture: cookie_image,
xScale: 0.08, yScale: 0.08
}
}
});
// Adds cookie to world
Composite.add(engine.world, [cookie]);
// Launches cookie
const force = 0.02 * cookie.mass;
Body.applyForce(cookie, cookie.position,
{
x: force * (cookie.position.x > getWidth()/2 ? -1 : 1),
y: -force * random(3, 6)
});
clearCookie(cookie);
}
/* Clears a cookie when out of the screen */
function clearCookie(cookie:any) {
const tollerance_offset = 1000;
if (cookie.position.y > document.body.scrollHeight + tollerance_offset) { // If out of the screen
Composite.remove(engine.world, cookie);
}
else {
setTimeout(() => {
clearCookie(cookie);
}, 5000);
}
}
</script>

53
components/Goodreads.vue Normal file
View File

@ -0,0 +1,53 @@
<template>
<div class="h-full w-fit">
<div v-if="is_loading" class="flex h-full w-full items-center justify-center">
<span class="animate-ping absolute inline-flex h-5 w-5 rounded-full bg-slate-800 dark:bg-slate-200 opacity-75"></span>
</div>
<div v-show="!is_loading" id="gr_grid_widget_1673812364" class="h-full"></div>
</div>
</template>
<script setup lang="ts">
const is_loading = ref(true);
onMounted(() => {
if (document.querySelector("#script-goodreads")) { document.querySelector("#script-goodreads")?.remove() };
let scriptTag = document.createElement("script");
scriptTag.src = "https://www.goodreads.com/review/grid_widget/158866642?cover_size=medium&hide_link=true&hide_title=true&num_books=20&order=d&shelf=currently-reading&sort=date_updated&widget_id=1673812364";
scriptTag.id = "script-goodreads";
scriptTag.type = "text/javascript";
document.getElementsByTagName("head")[0].appendChild(scriptTag);
let observer = new MutationObserver(function(mutations) {
// @ts-ignore
// Replaces the book covers with a higher resolution image
document.querySelectorAll("#gr_grid_widget_1673812364 > * img").forEach((image) => image.src = image.src.replace("_SX98_", "_SY475_"));
observer.disconnect();
is_loading.value = false;
});
observer.observe((document.querySelector("#gr_grid_widget_1673812364") as Node), { childList: true });
})
</script>
<style>
.gr_grid_container {
height: 100%;
}
.gr_grid_book_container {
float: left;
height: 100%;
margin-left: 0.5rem;
margin-right: 0.5rem;
overflow: hidden;
border-radius: 0.2rem;
border: 1px solid #424242;
}
.gr_grid_book_container > * img {
height: 100%;
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<div>
<button id="button-dropdown-locales" data-dropdown-toggle="dropdown-locales" type="button"
class="rounded-full p-1 hover:bg-slate-200 dark:hover:bg-slate-700">
<div class="w-5 h-5 flex items-center justify-center">
<img src="~/assets/images/icons/globe.svg" alt="Language" class="h-full w-full dark:invert" />
</div>
</button>
<div id="dropdown-locales" class="z-10 hidden bg-white divide-y divide-gray-100 rounded shadow dark:bg-gray-700 dark:divide-gray-600">
<ul class="p-3 space-y-1 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="button-dropdown-locales">
<li v-for="locale in $i18n.locales" :key="`locale-${locale.code}`">
<NuxtLink :to="switchLocalePath(locale.code)" class="text-sm font-medium uppercase text-gray-900 rounded dark:text-gray-300">
<span class="flex items-center p-2 px-5 rounded hover:bg-gray-100 dark:hover:bg-gray-600">
{{ locale.code }}
</span>
</NuxtLink>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { initDropdowns } from "flowbite";
const switchLocalePath = useSwitchLocalePath()
onMounted(() => {
initDropdowns();
})
</script>

View File

@ -0,0 +1,79 @@
<template>
<div class="relative">
<div class="flex items-center h-60 w-60">
<img v-show="picture === 'dark'" src="~/assets/images/profile/picture-dark.png" alt="" class="max-h-full max-w-full">
<img v-show="picture === 'light'" src="~/assets/images/profile/picture-light.png" alt="" class="max-h-full max-w-full">
<img v-show="picture === 'bright'" src="~/assets/images/profile/picture-bright.png" alt="" class="max-h-full max-w-full">
<img v-show="picture === 'no light'" src="~/assets/images/profile/picture-nolight.png" alt="" class="max-h-full max-w-full">
</div>
<div v-if="message" class="absolute bottom-0 left-0 w-full">
<p class="w-fit mx-auto px-2 pt-1 mb-1 bg-gray-200 dark:bg-gray-700">{{ message }}</p>
</div>
</div>
</template>
<script setup lang="ts">
const { t } = useI18n();
const picture = ref(getTheme());
const message = ref("");
// Finite-state automata to handle theme changes
interface State {
image: string; // Image to display
message: string; // Message to display (string to parse with i18n)
expect: string; // Expected active theme when this state is applied
next: string; // Next state
}
onMounted(() => {
let current_state = document.querySelector("html")?.classList.contains("dark") ? "dark1" : "light1";
let states:Record<string, State> = {
"dark1": { image: "dark", message: "", expect: "dark", next: "bright" },
"bright": { image: "bright", message: "that's bright", expect: "light", next: "dark2" },
"dark2": { image: "dark", message: "better", expect: "dark", next: "light_final" },
"light1": { image: "light", message: "", expect: "light", next: "nolights" },
"nolights": { image: "no light", message: "where lights", expect: "dark", next: "light2" },
"light2": { image: "light", message: "here lights", expect: "light", next: "dark_final" },
"dark_final": { image: "dark", message: "", expect: "dark", next: "light_final" },
"light_final": { image: "light", message: "", expect: "light", next: "dark_final" },
}
function applyState() {
picture.value = states[current_state]?.image;
message.value = states[current_state]?.message !== "" ? t(states[current_state]?.message) : "";
}
function nextState() {
current_state = states[current_state]?.next;
}
applyState();
let observer = new MutationObserver(function(mutations) {
if (!useRoute().name?.toString().startsWith("about")) { observer.disconnect(); return; }
const is_dark_theme = document.querySelector("html")?.classList.contains("dark");
if ( states[states[current_state].next].expect === (is_dark_theme ? "dark" : "light") ) {
nextState();
applyState();
if (current_state === "bright") { addFoundEasterEgg("picture-bright"); }
else if (current_state === "nolights") { addFoundEasterEgg("picture-nolights"); }
}
});
// Observes for theme changes
observer.observe((document.querySelector("html") as Node), { attributes: true, attributeFilter: ['class'] });
});
</script>

View File

@ -0,0 +1,44 @@
<template>
<div v-if="current_name !== ''" class="w-52">
<img :src="current_image" alt="" class="h-40 max-w-xs max-w- mx-auto" @click="userChangeThing">
<p class="text-center text-sm mt-2 select-none">{{ $t(current_name) }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import penguin_image from "@/assets/images/penguin.png";
import llama_image from "@/assets/images/llama.png";
import rock_image from "@/assets/images/rock.png";
import coconut_image from "@/assets/images/coconut.png";
import red_panda_image from "@/assets/images/red-panda.png";
const things = [
{ name: "penguin", image: penguin_image },
{ name: "llama", image: llama_image },
{ name: "rock", image: rock_image },
{ name: "coconut", image: coconut_image },
{ name: "red panda", image: red_panda_image }
];
const current_name = ref("");
const current_image = ref("");
function changeThing() {
const to_show_thing = randomOfArray(things.filter((thing) => thing.name !== current_name.value));
current_name.value = to_show_thing.name;
current_image.value = to_show_thing.image;
}
function userChangeThing() {
addFoundEasterEgg("change-something");
changeThing();
}
onMounted(() => {
changeThing();
})
</script>

View File

@ -0,0 +1,7 @@
<template>
<div class="flex flex-1 items-center py-0">
<div class="w-full">
<slot></slot>
</div>
</div>
</template>

View File

@ -0,0 +1,29 @@
<template>
<button class="rounded-full p-1 hover:bg-slate-200 dark:hover:bg-slate-700" @click="changeTheme">
<div class="w-5 h-5 flex items-center justify-center">
<div v-if="current_theme === 'light'">
<img src="~/assets/images/icons/moon.svg" alt="Dark theme" class="h-full w-full" />
</div>
<div v-if="current_theme === 'dark'">
<img src="~/assets/images/icons/sun.svg" alt="Light theme" class="invert h-full w-full" />
</div>
</div>
</button>
</template>
<script setup lang="ts">
import { ref } from "vue";
const current_theme = ref("");
onMounted(() => {
current_theme.value = getTheme();
})
function changeTheme() {
flipTheme();
current_theme.value = getTheme();
applyTheme(current_theme.value);
}
</script>

127
components/Timeline.vue Normal file
View File

@ -0,0 +1,127 @@
<template>
<div v-if="loading" class="flex w-full justify-center">
<span class="animate-ping absolute inline-flex h-5 w-5 rounded-full bg-slate-800 dark:bg-slate-200 opacity-75"></span>
</div>
<div class="w-full h-full" ref="container_timeline">
<div class="flex justify-center w-full h-full" v-if="month_offset > 0 && min_date && max_date">
<!-- Left side -->
<ol class="relative border-r text-right w-1/2 border-zinc-300 dark:border-zinc-700">
<li class="mr-4 absolute right-0" v-for="event in left_events" :key="props.left[event.index].title" :style="`top: ${event.offset*month_offset}px`">
<div class="relative">
<!-- Start point -->
<div class="absolute w-3 h-3 z-10 bg-gray-400 rounded-full border border-white dark:border-gray-900 dark:bg-gray-500" style="right: -1.43rem"></div>
<div v-if="monthDifference(props.left[event.index].start, props.left[event.index].end) > 0" >
<!-- End point -->
<div v-if="!props.left[event.index].current" class="absolute w-3 h-3 z-10 bg-gray-400 rounded-full border border-white dark:border-gray-900 dark:bg-gray-500" style="right: -1.43rem; bottom: 0"></div>
<!-- Interval line -->
<div class="absolute rounded-full border-l-2 border-gray-400 dark:border-gray-500" style="height: 100%; top: 0; right: -1.12rem"></div>
</div>
<div class="flex items-center" :style="`height: ${monthDifference(props.left[event.index].start, props.left[event.index].end)*month_offset}px`">
<div>
<span class="text-xs mb-0 font-normal leading-5 whitespace-pre-wrap text-gray-500 dark:text-gray-400">{{ props.left[event.index].time_label }}</span>
<h3 class="text-base mb-0 font-semibold leading-5 whitespace-pre-wrap text-gray-900 dark:text-white">{{ props.left[event.index].title }}</h3>
<p class="text-sm mb-0 font-normal leading-5 whitespace-pre-wrap text-gray-500 dark:text-gray-400">{{ props.left[event.index].description }}</p>
</div>
</div>
</div>
</li>
</ol>
<div class="w-4"></div>
<!-- Right side -->
<ol class="relative w-1/2 border-l border-zinc-300 dark:border-zinc-700">
<li class="ml-4 absolute left-0" v-for="event in right_events" :key="props.right[event.index].title" :style="`top: ${event.offset*month_offset}px`">
<div class="relative">
<!-- Start point -->
<div class="absolute w-3 h-3 z-10 bg-gray-400 rounded-full border border-white dark:border-gray-900 dark:bg-gray-500" style="left: -1.43rem; bottom: 0"></div>
<div v-if="monthDifference(props.right[event.index].start, props.right[event.index].end) > 0">
<!-- End point -->
<div v-if="!props.right[event.index].current" class="absolute w-3 h-3 z-10 bg-gray-400 rounded-full border border-white dark:border-gray-900 dark:bg-gray-500" style="left: -1.43rem"></div>
<!-- Interval line -->
<div class="absolute rounded-full border-l-2 border-we border-gray-400 dark:border-gray-500" style="height: 100%; top: 0; left: -1.12rem"></div>
</div>
<div class="flex items-center" :style="`height: ${monthDifference(props.right[event.index].start, props.right[event.index].end)*month_offset}px`">
<div>
<span class="mb-0 text-xs font-normal leading-5 whitespace-pre-wrap text-gray-500 dark:text-gray-400">{{ props.right[event.index].time_label }}</span>
<h3 class="mb-0 text-base font-semibold leading-5 whitespace-pre-wrap text-gray-900 dark:text-white">{{ props.right[event.index].title }}</h3>
<p class="mb-0 text-sm font-normal leading-5 whitespace-pre-wrap text-gray-500 dark:text-gray-400">{{ props.right[event.index].description }}</p>
</div>
</div>
</div>
</li>
</ol>
</div>
</div>
</template>
<script setup lang="ts">
interface Event {
title: string, description: string, time_label: string,
start: Date, end: Date, current?: boolean
};
const props = defineProps({
right: {
type: Object as PropType<Event[]>,
default: []
},
left: {
type: Object as PropType<Event[]>,
default: []
},
});
const loading = ref(true);
const container_timeline = ref();
const month_offset = ref(-1);
const right_events:Ref<{ index: number, offset: number }[]> = ref([]);
const left_events:Ref<{ index: number, offset: number }[]> = ref([]);
const min_date:Ref<Date> = ref(new Date());
const max_date:Ref<Date> = ref(new Date());
// Return the number of months between the start and the end of the interval
function monthDifference(start_date:Date, end_date:Date):number {
return end_date.getMonth() - start_date.getMonth() + (12 * (end_date.getFullYear() - start_date.getFullYear()));
}
function updateTimelineSize():void {
const available_space:number = container_timeline.value?.clientHeight ?? 0;
let events_min_date:Date = new Date(),
events_max_date:Date = new Date(0);
// Searches for the start and the end of the time interval
[...props.right, ...props.left]?.forEach((event) => {
if (!events_min_date || event.start < events_min_date) { events_min_date = event.start; }
if (!events_max_date || event.end > events_max_date) { events_max_date = event.end; }
});
min_date.value = events_min_date;
max_date.value = events_max_date;
month_offset.value = Math.floor( available_space / (monthDifference(events_min_date, events_max_date)) );
}
onMounted(() => {
loading.value = false;
updateTimelineSize();
// Computes the offset w.r.t. the end of the time interval
right_events.value = props.right.map((event, index) => ({
offset: monthDifference(event.end, max_date.value),
index: index
}));
left_events.value = props.left.map((event, index) => ({
offset: monthDifference(event.end, max_date.value),
index: index
}));
new ResizeObserver(updateTimelineSize).observe(document.querySelector("html") as Element)
})
</script>

View File

@ -0,0 +1,64 @@
<template>
<ClientOnly>
<div v-if="show_banner" class="fixed top-0 left-0 w-full pointer-events-none z-50">
<div :class="`border rounded-sm mx-auto w-fit max-w-xs md:max-w-md pointer-events-auto
bg-slate-200/90 border-slate-700 dark:bg-slate-800/90 dark:border-slate-400
transition-opacity ${show_banner ? 'opacity-100 duration-300 p-3 px-5 my-2' : 'opacity-0 duration-200'}`"
@click="dismiss">
<div class="flex text-sm">
<div class="flex-1">
<CookieEgg v-if="easteregg === 'cookie'" />
<FutureEgg v-if="easteregg === 'future'" />
<SomethingEgg v-if="easteregg === 'change-something'" />
<PictureBrightEgg v-if="easteregg === 'picture-bright'" />
<PictureNoLightEgg v-if="easteregg === 'picture-nolights'" />
<div class="mt-1 text-center">
<p v-if="found_eastereggs != total_eastereggs">{{ found_eastereggs }}/{{ total_eastereggs }} {{ $t("easter eggs found") }}</p>
<p v-if="found_eastereggs === total_eastereggs">{{ $t("all easter eggs found") }}</p>
</div>
</div>
</div>
</div>
</div>
</ClientOnly>
</template>
<script setup lang="ts">
const show_banner = ref(false);
const easteregg = ref("");
const total_eastereggs = ref(getTotalEasterEggsCount());
const found_eastereggs = ref(0);
onMounted(() => {
found_eastereggs.value = getFoundEasterEggsCount();
})
let current_dismiss_timeout:NodeJS.Timeout|null = null;
function show(easteregg_name:string) {
easteregg.value = easteregg_name;
found_eastereggs.value = getFoundEasterEggsCount();
show_banner.value = true;
if (current_dismiss_timeout) { clearTimeout(current_dismiss_timeout) }
current_dismiss_timeout = setTimeout(() => {
dismiss();
}, 7000);
}
function dismiss() {
show_banner.value = false;
}
defineExpose({
show
})
</script>

View File

@ -0,0 +1,16 @@
<template>
<div class="flex justify-center text-sm">
<div class="flex items-center justify-center">
<div class="w-10 h-10 flex items-center justify-center overflow-hidden">
<img src="~/assets/images/cookie.png" alt="" class="h-full w-full" />
</div>
</div>
<div class="flex-1 ml-2">
<p class="font-bold text-base">{{ $t("cookie.title") }}</p>
<p>{{ $t("cookie.description") }}</p>
</div>
</div>
</template>

View File

@ -0,0 +1,16 @@
<template>
<div class="flex text-sm">
<div class="flex items-center justify-center">
<div class="w-10 h-10 flex items-center justify-center overflow-hidden">
<img src="~/assets/images/future.png" alt="" class="h-full w-full" />
</div>
</div>
<div class="flex-1 ml-2">
<p class="font-bold text-base">{{ $t("future.title") }}</p>
<p>{{ $t("future.description") }}</p>
</div>
</div>
</template>

View File

@ -0,0 +1,25 @@
<template>
<ClientOnly>
<div class="flex text-sm">
<div class="flex items-center justify-center">
<div class="w-10 h-10 flex items-center justify-center overflow-hidden">
<img src="~/assets/images/sun.png" alt="" class="h-full w-full" />
</div>
</div>
<div class="flex-1 ml-2">
<p class="font-bold text-base">{{ $t("bright.title") }}</p>
<p v-if="!dark_unlocked">{{ $t("bright.description") }}</p>
<p v-if="dark_unlocked">{{ $t("bright_either.description") }}</p>
</div>
</div>
</ClientOnly>
</template>
<script setup lang="ts">
const dark_unlocked = ref(false);
onMounted(() => {
dark_unlocked.value = getFoundEasterEggs().includes("picture-nolights");
});
</script>

View File

@ -0,0 +1,25 @@
<template>
<ClientOnly>
<div class="flex text-sm">
<div class="flex items-center justify-center">
<div class="w-10 h-10 flex items-center justify-center overflow-hidden">
<img src="~/assets/images/moon.png" alt="" class="h-full w-full" />
</div>
</div>
<div class="flex-1 ml-2">
<p class="font-bold text-base">{{ $t("dark.title") }}</p>
<p v-if="!light_unlocked">{{ $t("dark.description") }}</p>
<p v-if="light_unlocked">{{ $t("dark_either.description") }}</p>
</div>
</div>
</ClientOnly>
</template>
<script setup lang="ts">
const light_unlocked = ref(false);
onMounted(() => {
light_unlocked.value = getFoundEasterEggs().includes("picture-bright");
});
</script>

View File

@ -0,0 +1,16 @@
<template>
<div class="flex text-sm">
<div class="flex items-center justify-center">
<div class="w-10 h-10 flex items-center justify-center overflow-hidden">
<img src="~/assets/images/sad.svg" alt="" class="h-full w-full dark:invert" />
</div>
</div>
<div class="flex-1 ml-2">
<p class="font-bold text-base">{{ $t("something.title") }}</p>
<p>{{ $t("something.description") }}</p>
</div>
</div>
</template>

View File

@ -0,0 +1,25 @@
<template>
<li>
<NuxtLink :to="localePath(props.to)" :aria-current="is_active_page ? 'page' : null"
:class="`block py-2 md:p-0 text-right md:text-center
${is_active_page ? 'font-bold text-zinc-900 dark:text-zinc-400' : 'font-normal hover:underline text-gray-700 dark:text-slate-50'}`">
{{ props.label }}
</NuxtLink>
</li>
</template>
<script setup lang="ts">
import { ref } from "vue";
const localePath = useLocalePath()
const router = useRouter();
const props = defineProps({
to: { type: String, required: true },
label: String
})
let current_path = router.currentRoute.value.fullPath.replace(/\/$/, "");
if (current_path === "") { current_path = "/"; }
const is_active_page = ref(current_path === localePath(props.to) || current_path === props.to);
</script>

View File

@ -0,0 +1,46 @@
<template>
<nav class="bg-transparent border-gray-200 py-2.5">
<div class="container flex flex-wrap items-center justify-end md:justify-start mx-auto">
<div class="block md:flex w-full">
<div class="flex justify-end items-center order-2 md:w-1/2">
<a href="https://github.com/NotXia" class="rounded-full p-1 mx-1 hover:bg-slate-200 dark:hover:bg-slate-700">
<img src="~/assets/images/icons/github.svg" alt="Github" class="h-5 dark:invert" />
</a>
<ThemeSwitch class="mx-1" />
<LanguageSelector class="mx-1" />
<button class="inline-flex items-center mx-3 text-sm text-gray-500 md:hidden dark:text-gray-400"
data-collapse-toggle="navbar-main" type="button" aria-controls="navbar-main" aria-expanded="false">
<span class="sr-only">{{ $t("open nav") }}</span>
<svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path>
</svg>
</button>
</div>
<div class="flex items-center order-1 md:w-full">
<div class="hidden w-full md:block md:w-auto" id="navbar-main">
<ul class="flex flex-col py-4 pr-4 mt-0 md:flex-row md:space-x-8 md:text-sm md:font-medium bg-transparent">
<NavLink to="/" :label="$t('home')"/>
<NavLink to="/about" :label="$t('about')"/>
<NavLink to="/projects" :label="$t('projects')"/>
<NavLink to="/resume" :label="$t('resume')"/>
<NavLink to="/contacts" :label="$t('contacts')"/>
</ul>
</div>
</div>
</div>
</div>
</nav>
</template>
<script setup lang="ts">
import NavLink from "./NavLink.vue"
import { initCollapses } from "flowbite";
import { onMounted } from "vue";
onMounted(() => {
initCollapses();
})
</script>

View File

@ -0,0 +1,23 @@
<template>
<div class="border border-gray-500 dark:border-gray-300 rounded-md p-3 mx-auto w-full lg:w-2/3 xl:w-1/2">
<h3 class="text-2xl font-semibold text-center text-gray-900 dark:text-white">{{ props.title }}</h3>
<div class="text-center mb-2">
<a v-for="link in props.links" :href="link.url" class="font-mono inline-block hover:underline mx-2">{{ link.label }}</a>
</div>
<p class="text-lg whitespace-pre-wrap mb-2 text-gray-500 dark:text-gray-400">
<slot></slot>
</p>
<img :src="props.image" alt="" class="max-w-full max-h-96 mx-auto">
</div>
</template>
<script setup lang="ts">
import type { PropType } from "vue";
const props = defineProps({
title: String,
links: Object as PropType<{ label: string, url: string }[]>,
image: String
});
</script>

View File

@ -0,0 +1,21 @@
<template>
<ProjectCard
title="Animal House" :image="image"
:links="[
{ label: 'Repository', url: 'https://github.com/NotXia/animal-house' }
]">
<p class="text-center">{{ $t("unibo_21-22") }}</p>
<p>{{ $t('animalhouse.description') }}</p>
<ul class="list-inside list-['-_']">
<li>{{ $t('animalhouse.description.game') }}</li>
<li>{{ $t('animalhouse.description.frontoffice') }}</li>
<li>{{ $t('animalhouse.description.backoffice') }}</li>
</ul>
</ProjectCard>
</template>
<script setup lang="ts">
import image from "@/assets/images/projects/animal-house.png";
</script>

View File

@ -0,0 +1,16 @@
<template>
<ProjectCard
title="Image deblur" :image="image"
:links="[
{ label: 'Repository', url: 'https://github.com/NotXia/imaging' }
]">
<p class="text-center">{{ $t("unibo_21-22") }}</p>
<p>{{ $t('imaging.description') }}</p>
</ProjectCard>
</template>
<script setup lang="ts">
import image from "@/assets/images/projects/imaging.png";
</script>

View File

@ -0,0 +1,16 @@
<template>
<ProjectCard
title="MNK Game" :image="image"
:links="[
{ label: 'Repository', url: 'https://github.com/NotXia/MNKGame' }
]">
<p class="text-center">{{ $t("unibo_20-21") }}</p>
<p>{{ $t('mnk.description') }}</p>
</ProjectCard>
</template>
<script setup lang="ts">
import image from "@/assets/images/projects/mnkgame.png";
</script>

View File

@ -0,0 +1,40 @@
<template>
<ProjectCard
title="notxia.github.io"
:links="[
{ label: 'Repository', url: 'https://github.com/NotXia/notxia.github.io' },
]">
<p v-if="!show_recursive_message">{{ $t('notxia.github.io.description') }}</p>
<div class="w-full h-72 relative">
<div role="status" v-if="!iframe_loaded && !show_recursive_message" class="absolute top-0 left-0 w-full h-full flex justify-center items-center">
<svg aria-hidden="true" class="w-8 h-8 animate-spin text-gray-200 fill-gray-600 dark:text-gray-600 dark:fill-gray-300" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
</svg>
</div>
<div v-if="show_recursive_message" class="flex justify-center items-center w-full h-full">
<p>{{ $t("no recursion") }}</p>
</div>
<iframe v-if="!show_recursive_message" src="/" frameborder="0" width="100%" height="100%" @load="iframe_loaded=true"></iframe>
</div>
</ProjectCard>
</template>
<script setup lang="ts">
const iframe_loaded = ref(false);
const show_recursive_message = ref(false);
onMounted(() => {
try {
if (window.frameElement) { // The component is loaded inside an iframe
show_recursive_message.value = true;
}
}
catch (err) { show_recursive_message.value = false; }
});
</script>

View File

@ -0,0 +1,11 @@
<template>
<ProjectCard
title="PandOS+"
:links="[
{ label: 'Repository', url: 'https://github.com/NotXia/pandos-plus' }
]">
<p class="text-center">{{ $t("unibo_21-22") }}</p>
<p>{{ $t('pandos+.description') }}</p>
</ProjectCard>
</template>

View File

@ -0,0 +1,16 @@
<template>
<ProjectCard
title="Pathfinding visualizer" :image="image"
:links="[
{ label: 'Repository', url: 'https://github.com/NotXia/pathfinding-visualizer' },
{ label: 'Demo', url: 'https://notxia.github.io/pathfinding-visualizer/' }
]">
{{ $t('pathfinding_visualizer.description') }}
</ProjectCard>
</template>
<script setup lang="ts">
import image from "@/assets/images/projects/pathfinding-visualizer.png";
</script>

View File

@ -0,0 +1,16 @@
<template>
<ProjectCard
title="Platform game" :image="image"
:links="[
{ label: 'Repository', url: 'https://github.com/NotXia/platform-game' }
]">
<p class="text-center">{{ $t("unibo_20-21") }}</p>
<p>{{ $t('platform.description') }}</p>
</ProjectCard>
</template>
<script setup lang="ts">
import image from "@/assets/images/projects/platform.png";
</script>

View File

@ -0,0 +1,16 @@
<template>
<ProjectCard
title="Sorting visualizer" :image="image"
:links="[
{ label: 'Repository', url: 'https://github.com/NotXia/sorting-visualizer' },
{ label: 'Demo', url: 'https://notxia.github.io/sorting-visualizer/' }
]">
{{ $t('sort_visualizer.description') }}
</ProjectCard>
</template>
<script setup lang="ts">
import image from "@/assets/images/projects/sorting-visualizer.png";
</script>

View File

@ -0,0 +1,16 @@
<template>
<ProjectCard
title="Tweet Analysis" :image="image"
:links="[
{ label: 'Repository', url: 'https://github.com/NotXia/tweet-analysis' }
]">
<p class="text-center">{{ $t("unibo_22-23") }}</p>
<p>{{ $t('tweet_analysis.description') }}</p>
</ProjectCard>
</template>
<script setup lang="ts">
import image from "@/assets/images/projects/tweet-analysis.png";
</script>

View File

@ -0,0 +1,12 @@
<template>
<ProjectCard
title="Wirefilter"
:links="[
{ label: 'Repository', url: 'https://github.com/NotXia/vdeplug_wirefilter' },
{ label: 'VirtualSquare', url: 'http://wiki.virtualsquare.org/#!index.md' }
]">
<p class="text-center">{{ $t("unibo_22-23") }}</p>
<p>{{ $t("wirefilter.description") }}</p>
</ProjectCard>
</template>

View File

@ -0,0 +1,22 @@
<template>
<div class="md:ml-2 mt-4">
<div class="flex justify-between">
<h3 class="text-xl font-semibold tracking-wide">{{ props.title }}</h3>
<div class="text-right text-gray-500 dark:text-gray-400">{{ props.right_text }}</div>
</div>
<div class="leading-5 text-gray-500 dark:text-gray-400">{{ props.subtitle }}</div>
<div class="mt-1 whitespace-pre-wrap">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
title: String,
subtitle: String,
right_text: String
});
</script>

View File

@ -0,0 +1,100 @@
<template>
<div class="flex h-full justify-center relative">
<div class="absolute top-0 left-0 w-full">
<div data-tooltip-target="tooltip-future" class="relative w-6 h-2 mx-auto z-50" @mouseover="startArchievementTimer" @mouseleave="stopAchievementTimer">
</div>
<div id="tooltip-future" role="tooltip"
class="absolute z-10 invisible inline-block px-2 py-1 text-xs font-medium transition-opacity duration-1000 rounded-lg opacity-0 tooltip">
{{ $t("future") }}
</div>
</div>
<div class="w-full timeline-height">
<Timeline
:right="[
{
title: $t('diploma'), time_label: '2015 - 2020',
description: $t('aldini'),
start: new Date(2015, september, 1), end: new Date(2020, june, 1)
},
{
title: $t('bs in cs'), time_label: '2020 - 2023',
description: $t('unibo'),
start: new Date(2020, september, 1), end: new Date(), current: true
}
]"
:left="[
{
title: $t('pcto toyota'), time_label: `${$t('m_12')} 2019 | ${$t('m_7')} 2019 | ${$t('m_2')} 2019`,
description: 'Toyota Material Handling Manufacturing Italy',
start: new Date(2019, february, 1), end: new Date(2019, december, 1)
},
{
title: 'CS50s Introduction to AI with Python', time_label: '2022',
description: 'HarvardX',
start: new Date(2022, september, 1), end: new Date(2022, september, 1)
},
{
title: 'CISCO: IT Essentials', time_label: '2018',
description: 'CISCO Networking Academy',
start: new Date(2018, september, 1), end: new Date(2018, september, 1)
}
]" />
</div>
</div>
<p class="text-center text-xs text-gray-400 dark:text-slate-600">{{ $t("like timelines") }}</p>
</template>
<script setup lang="ts">
import { initTooltips } from "flowbite";
const january = 0, february = 1, march = 2, april = 3, may = 4, june = 5, july = 6, august = 7, september = 8, october = 9, november = 10, december = 11;
onMounted(() => {
initTooltips();
});
let achievement_timer:NodeJS.Timeout|null = null;
function startArchievementTimer() {
achievement_timer = setTimeout(() => {
addFoundEasterEgg('future');
achievement_timer = null;
}, 500);
}
function stopAchievementTimer() {
if (achievement_timer) {
clearTimeout(achievement_timer);
}
}
</script>
<style scoped>
/* Extra small devices */
@media only screen and (max-width: 600px) {
.timeline-height { min-height: 90rem; }
}
/* Small devices */
@media only screen and (min-width: 600px) {
.timeline-height { min-height: 80rem; }
}
/* Medium devices */
@media only screen and (min-width: 768px) {
.timeline-height { min-height: 70rem; }
}
/* Large devices */
@media only screen and (min-width: 992px) {
.timeline-height { min-height: 60rem; }
}
/* Extra large devices */
@media only screen and (min-width: 1200px) {
.timeline-height { min-height: 60rem; }
}
</style>

View File

@ -0,0 +1,19 @@
<template>
<div class="inline-block">
<div class="flex items-center">
<img :src="props.logo" alt="" :class="`h-5 mr-1 ${props.needInvert ? 'dark:invert' : ''}`">
{{ props.language }}
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
language: String,
logo: String,
needInvert: Boolean
});
</script>

View File

@ -0,0 +1,14 @@
<template>
<div>
<h2 class="text-4xl font-bold tracking-wide">{{ $t("certificates") }}</h2>
<ActivityParagraph title="CS50s Introduction to AI with Python" subtitle="HarvardX" right_text="2022">
<a class="font-mono hover:underline" href="https://certificates.cs50.io/bb09e788-f9da-4055-8645-aba7ef163683.pdf?size=a4">{{ $t("link to certificate") }}</a>
<p>{{ $t("cs50 ai description") }}</p>
</ActivityParagraph>
<ActivityParagraph title="CISCO: IT Essentials" subtitle="CISCO Networking Academy" right_text="2018">
{{ $t("cisco it essentials description") }}
</ActivityParagraph>
</div>
</template>

View File

@ -0,0 +1,12 @@
<template>
<div>
<h2 class="text-4xl font-bold tracking-wide">{{ $t("education") }}</h2>
<ActivityParagraph :title="$t('bs in cs')" :subtitle="$t('unibo')" :right_text="`2020 - ${$t('present')}`">
</ActivityParagraph>
<ActivityParagraph :title="$t('diploma')" :subtitle="$t('aldini')" right_text="2015 - 2020">
{{ $t("final degree") }}: 100/100 {{ $t("with honors") }}
</ActivityParagraph>
</div>
</template>

View File

@ -0,0 +1,13 @@
<template>
<div>
<h2 class="text-4xl font-bold tracking-wide">{{ $t("other") }}</h2>
<ActivityParagraph :title="$t('ois')" :subtitle="$t('aldini')" right_text="2017-18 | 2018-19 | 2019-20">
{{ $t("ois description") }}
</ActivityParagraph>
<ActivityParagraph title="MAST Academy: Expeditions" :subtitle="$t('MAST foundation')" right_text="2018">
{{ $t("MAST expeditions description") }}
</ActivityParagraph>
</div>
</template>

View File

@ -0,0 +1,108 @@
<template>
<div class="[&_li]:mr-4">
<h2 class="text-4xl font-bold tracking-wide">{{ $t("skills") }}</h2>
<ActivityParagraph :title="$t('data analysis')">
<ul class="flex flex-wrap items-center">
<li><ProgrammingLogo :logo="python_logo" language="Python" /></li>
<li><ProgrammingLogo :logo="database_logo" language="SQL" needInvert /></li>
<li><ProgrammingLogo :logo="mongo_logo" language="MongoDB" /></li>
</ul>
<ul class="flex flex-wrap items-center">
<li><ProgrammingLogo :logo="numpy_logo" language="Numpy" /></li>
<li><ProgrammingLogo :logo="pandas_logo" language="Pandas" /></li>
</ul>
<ul class="flex flex-wrap items-center">
<li><ProgrammingLogo :logo="matplotlib_logo" language="Matplotlib" /></li>
<li><ProgrammingLogo :logo="seaborn_logo" language="Seaborn" /></li>
</ul>
<ul class="flex flex-wrap items-center">
<li><ProgrammingLogo :logo="scikitlearn_logo" language="Scikit-learn" /></li>
<li><ProgrammingLogo :logo="tensorflow_logo" language="Tensorflow" /></li>
<li><ProgrammingLogo :logo="keras_logo" language="Keras" /></li>
</ul>
<!-- <ul class="flex flex-wrap items-center">
<li><ProgrammingLogo :logo="knime_logo" language="KNIME" /></li>
</ul> -->
</ActivityParagraph>
<ActivityParagraph :title="$t('devops')">
<ul class="flex flex-wrap items-center">
<li><ProgrammingLogo :logo="docker_logo" language="Docker" /></li>
</ul>
<ul class="flex flex-wrap items-center">
<li><ProgrammingLogo :logo="ansible_logo" language="Ansible" /></li>
</ul>
<ul class="flex flex-wrap items-center">
<li><ProgrammingLogo :logo="jenkins_logo" language="Jenkins" /></li>
<li><ProgrammingLogo :logo="gitlab_runners_logo" language="Gitlab Runner" /></li>
<li><ProgrammingLogo :logo="github_actions_logo" language="Github Actions" /></li>
</ul>
</ActivityParagraph>
<ActivityParagraph :title="$t('web development')">
<ul class="flex flex-wrap items-center">
<li><ProgrammingLogo :logo="nodejs_logo" language="NodeJS" /></li>
<li><ProgrammingLogo :logo="php_logo" language="PHP" /></li>
<!-- <li><ProgrammingLogo :logo="nginx_logo" language="Nginx" /></li> -->
</ul>
<ul class="flex flex-wrap items-center">
<li><ProgrammingLogo :logo="react_logo" language="React" /></li>
<li><ProgrammingLogo :logo="vue_logo" language="Vue" /></li>
<li><ProgrammingLogo :logo="nuxt_logo" language="Nuxt" /></li>
</ul>
<!-- <ul class="flex flex-wrap items-center">
<li><ProgrammingLogo :logo="html_logo" language="HTML" /></li>
<li><ProgrammingLogo :logo="css_logo" language="CSS" /></li>
<li><ProgrammingLogo :logo="js_logo" language="Javascript" /></li>
</ul> -->
<!-- <p>Bootstrap Tailwind</p> -->
</ActivityParagraph>
<ActivityParagraph :title="$t('other programming languages')">
<ul class="flex flex-wrap items-center">
<li><ProgrammingLogo :logo="c_logo" language="C" /></li>
<li><ProgrammingLogo :logo="cpp_logo" language="C++" /></li>
<li><ProgrammingLogo :logo="java_logo" language="Java" /></li>
<li><ProgrammingLogo :logo="cpu_logo" language="Assembly x86" needInvert /></li>
</ul>
</ActivityParagraph>
</div>
</template>
<script setup lang="ts">
import nodejs_logo from "@/assets/images/icons/nodejs.svg";
import php_logo from "@/assets/images/icons/php.svg";
import nginx_logo from "@/assets/images/icons/nginx.svg";
import html_logo from "@/assets/images/icons/html.svg";
import css_logo from "@/assets/images/icons/css.svg";
import js_logo from "@/assets/images/icons/js.svg";
import react_logo from "@/assets/images/icons/react.svg";
import vue_logo from "@/assets/images/icons/vue.svg";
import nuxt_logo from "@/assets/images/icons/nuxt.svg";
import docker_logo from "@/assets/images/icons/docker.svg";
import ansible_logo from "@/assets/images/icons/ansible.svg";
import jenkins_logo from "@/assets/images/icons/jenkins.svg";
import gitlab_runners_logo from "@/assets/images/icons/gitlab.svg";
import github_actions_logo from "@/assets/images/icons/github-actions.svg";
import numpy_logo from "@/assets/images/icons/numpy.svg";
import pandas_logo from "@/assets/images/icons/pandas.svg";
import matplotlib_logo from "@/assets/images/icons/matplotlib.svg";
import seaborn_logo from "@/assets/images/icons/seaborn.svg";
import scikitlearn_logo from "@/assets/images/icons/scikitlearn.svg";
import tensorflow_logo from "@/assets/images/icons/tensorflow.svg";
import keras_logo from "@/assets/images/icons/keras.svg";
import knime_logo from "@/assets/images/icons/knime.svg";
import database_logo from "@/assets/images/icons/database.svg";
import mongo_logo from "@/assets/images/icons/mongo.svg";
import c_logo from "@/assets/images/icons/c.svg";
import cpp_logo from "@/assets/images/icons/cpp.svg";
import java_logo from "@/assets/images/icons/java.svg";
import python_logo from "@/assets/images/icons/python.svg";
import cpu_logo from "@/assets/images/icons/cpu.svg";
</script>

View File

@ -0,0 +1,10 @@
<template>
<div>
<h2 class="text-4xl font-bold tracking-wide">{{ $t("working experience") }}</h2>
<ActivityParagraph :title="$t('pcto toyota')" subtitle="Toyota Material Handling Manufacturing Italy"
:right_text="`${$t('december')} 2019 | ${$t('july')} 2019 | ${$t('february')} 2019`">
{{ $t("pcto toyota description") }}
</ActivityParagraph>
</div>
</template>