mirror of
https://github.com/NotXia/notxia.github.io.git
synced 2025-12-14 19:01:51 +01:00
Migration to Nuxt3
This commit is contained in:
19
components/ContactLink.vue
Normal file
19
components/ContactLink.vue
Normal 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
139
components/Cookie.vue
Normal 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
53
components/Goodreads.vue
Normal 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>
|
||||
33
components/LanguageSelector.vue
Normal file
33
components/LanguageSelector.vue
Normal 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>
|
||||
79
components/ProfilePicture.vue
Normal file
79
components/ProfilePicture.vue
Normal 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>
|
||||
44
components/RandomSomething.vue
Normal file
44
components/RandomSomething.vue
Normal 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>
|
||||
7
components/ScreenCenter.vue
Normal file
7
components/ScreenCenter.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-1 items-center py-0">
|
||||
<div class="w-full">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
29
components/ThemeSwitch.vue
Normal file
29
components/ThemeSwitch.vue
Normal 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
127
components/Timeline.vue
Normal 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>
|
||||
64
components/easteregg-banner/EastereggBanner.vue
Normal file
64
components/easteregg-banner/EastereggBanner.vue
Normal 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>
|
||||
16
components/easteregg-banner/eggs/CookieEgg.vue
Normal file
16
components/easteregg-banner/eggs/CookieEgg.vue
Normal 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>
|
||||
16
components/easteregg-banner/eggs/FutureEgg.vue
Normal file
16
components/easteregg-banner/eggs/FutureEgg.vue
Normal 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>
|
||||
25
components/easteregg-banner/eggs/PictureBrightEgg.vue
Normal file
25
components/easteregg-banner/eggs/PictureBrightEgg.vue
Normal 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>
|
||||
25
components/easteregg-banner/eggs/PictureNoLightEgg.vue
Normal file
25
components/easteregg-banner/eggs/PictureNoLightEgg.vue
Normal 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>
|
||||
16
components/easteregg-banner/eggs/SomethingEgg.vue
Normal file
16
components/easteregg-banner/eggs/SomethingEgg.vue
Normal 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>
|
||||
25
components/navbar/NavLink.vue
Normal file
25
components/navbar/NavLink.vue
Normal 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>
|
||||
46
components/navbar/Navbar.vue
Normal file
46
components/navbar/Navbar.vue
Normal 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>
|
||||
23
components/projects/ProjectCard.vue
Normal file
23
components/projects/ProjectCard.vue
Normal 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>
|
||||
21
components/projects/cards/AnimalHouse.vue
Normal file
21
components/projects/cards/AnimalHouse.vue
Normal 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>
|
||||
16
components/projects/cards/Imaging.vue
Normal file
16
components/projects/cards/Imaging.vue
Normal 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>
|
||||
16
components/projects/cards/MNKGame.vue
Normal file
16
components/projects/cards/MNKGame.vue
Normal 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>
|
||||
40
components/projects/cards/NotXiaGithubio.vue
Normal file
40
components/projects/cards/NotXiaGithubio.vue
Normal 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>
|
||||
11
components/projects/cards/PandOSplus.vue
Normal file
11
components/projects/cards/PandOSplus.vue
Normal 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>
|
||||
16
components/projects/cards/PathfindingVisualizer.vue
Normal file
16
components/projects/cards/PathfindingVisualizer.vue
Normal 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>
|
||||
16
components/projects/cards/Platform.vue
Normal file
16
components/projects/cards/Platform.vue
Normal 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>
|
||||
16
components/projects/cards/SortingVisualizer.vue
Normal file
16
components/projects/cards/SortingVisualizer.vue
Normal 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>
|
||||
16
components/projects/cards/TweetAnalysis.vue
Normal file
16
components/projects/cards/TweetAnalysis.vue
Normal 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>
|
||||
12
components/projects/cards/Wirefilter.vue
Normal file
12
components/projects/cards/Wirefilter.vue
Normal 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>
|
||||
22
components/resume/ActivityParagraph.vue
Normal file
22
components/resume/ActivityParagraph.vue
Normal 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>
|
||||
100
components/resume/ExperienceTimeline.vue
Normal file
100
components/resume/ExperienceTimeline.vue
Normal 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: 'CS50’s 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>
|
||||
19
components/resume/ProgrammingLogo.vue
Normal file
19
components/resume/ProgrammingLogo.vue
Normal 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>
|
||||
14
components/resume/sections/Certificates.vue
Normal file
14
components/resume/sections/Certificates.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold tracking-wide">{{ $t("certificates") }}</h2>
|
||||
|
||||
<ActivityParagraph title="CS50’s 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>
|
||||
12
components/resume/sections/Education.vue
Normal file
12
components/resume/sections/Education.vue
Normal 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>
|
||||
13
components/resume/sections/Other.vue
Normal file
13
components/resume/sections/Other.vue
Normal 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>
|
||||
108
components/resume/sections/Skills.vue
Normal file
108
components/resume/sections/Skills.vue
Normal 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>
|
||||
10
components/resume/sections/Work.vue
Normal file
10
components/resume/sections/Work.vue
Normal 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>
|
||||
Reference in New Issue
Block a user