Compare commits
2 Commits
b224c2ba65
...
897536afc9
| Author | SHA1 | Date | |
|---|---|---|---|
| 897536afc9 | |||
| 89277dc1fb |
@@ -7,7 +7,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<NuxtRouteAnnouncer />
|
<NuxtRouteAnnouncer />
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<NuxtPage></NuxtPage>
|
<NuxtPage />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -28,5 +28,4 @@ onMounted(() => {
|
|||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -19,7 +19,7 @@ body {
|
|||||||
* {
|
* {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-family: 'Hurmit';
|
font-family: 'Hurmit';
|
||||||
cursor: text;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pixelated {
|
.pixelated {
|
||||||
|
|||||||
@@ -1,55 +1,49 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const localePath = useLocalePath()
|
const localePath = useLocalePath()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const scrollTargets: Record<string, string> = {
|
const scrollTargets: Record<string, string> = {
|
||||||
'index': 'top',
|
'index': '#top',
|
||||||
'blog': '#scroll-blog',
|
'blog': '#scroll-blog',
|
||||||
'contact': '#scroll-contact',
|
'contact': '#scroll-contact',
|
||||||
'art': '#scroll-art'
|
'art': '#scroll-art'
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPath = computed(() => route.path)
|
const isHome = computed(() => route.path === localePath('index'))
|
||||||
|
|
||||||
function handleClick(targetKey: string, e: MouseEvent) {
|
function isActive(targetKey: string): boolean {
|
||||||
if (e.ctrlKey || e.metaKey || e.altKey || e.button !== 0) return
|
if (targetKey === 'index') return isHome.value
|
||||||
const isHome = currentPath.value === localePath('index')
|
const targetPath = localePath(targetKey)
|
||||||
if (!isHome) return
|
return route.path === targetPath
|
||||||
e.preventDefault()
|
}
|
||||||
|
|
||||||
|
function getToPath(targetKey: string): string {
|
||||||
|
if (isHome.value) {
|
||||||
const targetId = scrollTargets[targetKey] || 'top'
|
const targetId = scrollTargets[targetKey] || 'top'
|
||||||
if (targetId === 'top') {
|
return targetId.substring(1) !== '' ? '#' + targetId.substring(1) : '/'
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
||||||
} else {
|
|
||||||
const el = document.querySelector(targetId)
|
|
||||||
if (el) {
|
|
||||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
||||||
} else {
|
|
||||||
const hash = targetId.substring(1)
|
|
||||||
router.replace({ hash })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return localePath(targetKey === 'index' ? 'index' : targetKey)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="tui-tabs">
|
<div class="tui-tabs">
|
||||||
<a :href="localePath('index')" class="tui-tab" @click="handleClick('index', $event)">
|
<NuxtLink :to="getToPath('index')" class="tui-tab" :class="{ 'tui-tab-active': isActive('index') }">
|
||||||
<span class="tab-sep" v-if="false">│</span>
|
<span class="tab-sep" v-if="false">│</span>
|
||||||
<span class="tab-label">/</span>
|
<span class="tab-label">/</span>
|
||||||
</a>
|
</NuxtLink>
|
||||||
<a :href="localePath('blog')" class="tui-tab" @click="handleClick('blog', $event)">
|
<NuxtLink :to="getToPath('blog')" class="tui-tab" :class="{ 'tui-tab-active': isActive('blog') }">
|
||||||
<span class="tab-sep">│</span>
|
<span class="tab-sep">│</span>
|
||||||
<span class="tab-label">/{{ $t('header.links.blog') }}</span>
|
<span class="tab-label">/{{ $t('header.links.blog') }}</span>
|
||||||
</a>
|
</NuxtLink>
|
||||||
<a :href="localePath('contact')" class="tui-tab" @click="handleClick('contact', $event)">
|
<NuxtLink :to="getToPath('contact')" class="tui-tab" :class="{ 'tui-tab-active': isActive('contact') }">
|
||||||
<span class="tab-sep">│</span>
|
<span class="tab-sep">│</span>
|
||||||
<span class="tab-label">/{{ $t('header.links.contact') }}</span>
|
<span class="tab-label">/{{ $t('header.links.contact') }}</span>
|
||||||
</a>
|
</NuxtLink>
|
||||||
<a :href="localePath('art')" class="tui-tab" @click="handleClick('art', $event)">
|
<NuxtLink :to="getToPath('art')" class="tui-tab" :class="{ 'tui-tab-active': isActive('art') }">
|
||||||
<span class="tab-sep">│</span>
|
<span class="tab-sep">│</span>
|
||||||
<span class="tab-label">/{{ $t('header.links.art') }}</span>
|
<span class="tab-label">/{{ $t('header.links.art') }}</span>
|
||||||
</a>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -91,7 +85,7 @@ function handleClick(targetKey: string, e: MouseEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tui-tab.router-link-exact-active {
|
.tui-tab-active {
|
||||||
color: var(--color-background-fore);
|
color: var(--color-background-fore);
|
||||||
background-color: var(--color-link);
|
background-color: var(--color-link);
|
||||||
text-shadow: 0 0 8px rgba(255,255,255,0.3);
|
text-shadow: 0 0 8px rgba(255,255,255,0.3);
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
<script lang="js" setup>
|
|
||||||
import HeaderLinks from './HeaderLinks.vue';
|
|
||||||
import SiteOptions from './site_options/SiteOptions.vue';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="tui-minimalbar">
|
|
||||||
<div class="minimal-inner">
|
|
||||||
<div class="left">
|
|
||||||
<span class="sticky-title" aria-hidden="true">◆ ARANROIG.COM</span>
|
|
||||||
<HeaderLinks />
|
|
||||||
</div>
|
|
||||||
<SiteOptions />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="height: 80px"></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.tui-minimalbar {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
|
|
||||||
background: var(--color-sticky-header-bg);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
box-shadow: 0 4px 0px 0px var(--color-container-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.minimal-inner {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 6px 24px;
|
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
|
||||||
padding: 5px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
padding: 4px 12px;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sticky-title {
|
|
||||||
font-family: 'Hurmit', monospace;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-text);
|
|
||||||
letter-spacing: 1px;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import HeaderLinks from './HeaderLinks.vue';
|
|
||||||
import SiteOptions from './site_options/SiteOptions.vue';
|
|
||||||
import StickyHeader from './StickyHeader.vue';
|
|
||||||
|
|
||||||
const asciiLines = [
|
|
||||||
"░█▀█░█▀▄░█▀█░█▀█░█▀▄░█▀█░▀█▀░█▀▀",
|
|
||||||
"░█▀█░█▀▄░█▀█░█░█░█▀▄░█░█░░█░░█░█",
|
|
||||||
"░▀░▀░▀░▀░▀░▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀"
|
|
||||||
];
|
|
||||||
|
|
||||||
const revealedLines = ref<number>(0);
|
|
||||||
let asciiTimer: ReturnType<typeof setInterval> | null = null;
|
|
||||||
const HAS_ANIMATED_KEY = 'ascii-animated';
|
|
||||||
|
|
||||||
function startAsciiAnimation() {
|
|
||||||
if (sessionStorage.getItem(HAS_ANIMATED_KEY)) {
|
|
||||||
revealedLines.value = asciiLines.length;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const delay = 400;
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
asciiTimer = setInterval(() => {
|
|
||||||
count++;
|
|
||||||
revealedLines.value = count;
|
|
||||||
if (count >= asciiLines.length) {
|
|
||||||
clearInterval(asciiTimer!);
|
|
||||||
asciiTimer = null;
|
|
||||||
sessionStorage.setItem(HAS_ANIMATED_KEY, '1');
|
|
||||||
}
|
|
||||||
}, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
startAsciiAnimation();
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (asciiTimer) clearInterval(asciiTimer);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="header">
|
|
||||||
<div class="container">
|
|
||||||
<div class="header-container website">
|
|
||||||
<pre v-if="revealedLines > 0" class="ascii-title" aria-hidden="true">{{ asciiLines.slice(0, revealedLines).join('\n') }}</pre>
|
|
||||||
<HeaderLinks></HeaderLinks>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="header-container">
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<SiteOptions></SiteOptions>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<StickyHeader></StickyHeader>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.header {
|
|
||||||
position: relative;
|
|
||||||
margin-top: 30px;
|
|
||||||
user-select: none;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-container {
|
|
||||||
&.website {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-container {
|
|
||||||
position:relative;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ascii-title {
|
|
||||||
font-family: 'Hurmit', monospace;
|
|
||||||
color: var(--color-link);
|
|
||||||
text-shadow: 0 0 8px var(--color-link), 0 0 4px var(--color-link);
|
|
||||||
font-size: clamp(0.35rem, 1.2vw, 0.65rem);
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: -0.1ch;
|
|
||||||
margin: 0;
|
|
||||||
white-space: pre;
|
|
||||||
min-height: clamp(1.05rem, 3.6vw, 1.95rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
.ascii-title {
|
|
||||||
font-size: clamp(0.28rem, 1.8vw, 0.5rem);
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: -0.1ch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import MinimalHeader from '~/components/parts/MinimalHeader.vue';
|
import TableHeader from '~/components/parts/TableHeader.vue';
|
||||||
import PageHeader from '~/components/parts/PageHeader.vue';
|
|
||||||
|
|
||||||
const slug = useRoute().params.slug;
|
const slug = useRoute().params.slug;
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
@@ -12,7 +11,9 @@ const { data: post } = await useAsyncData(`art-${slug}`, () =>
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Render the blog post as Prose & Vue components -->
|
<!-- Render the blog post as Prose & Vue components -->
|
||||||
<MinimalHeader></MinimalHeader>
|
<div class="no-sprite">
|
||||||
|
<TableHeader></TableHeader>
|
||||||
|
</div>
|
||||||
<div class="extended-container">
|
<div class="extended-container">
|
||||||
<ContentRenderer v-if="post" :value="post" class="art" />
|
<ContentRenderer v-if="post" :value="post" class="art" />
|
||||||
</div>
|
</div>
|
||||||
@@ -40,3 +41,9 @@ const { data: post } = await useAsyncData(`art-${slug}`, () =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.no-sprite .undertable-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
||||||
import MinimalHeader from '~/components/parts/MinimalHeader.vue';
|
import TableHeader from '~/components/parts/TableHeader.vue';
|
||||||
|
|
||||||
const { locale, t } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
const localePath = useLocalePath();
|
const localePath = useLocalePath();
|
||||||
@@ -53,7 +53,9 @@ const displayedArt = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MinimalHeader></MinimalHeader>
|
<div class="no-sprite">
|
||||||
|
<TableHeader></TableHeader>
|
||||||
|
</div>
|
||||||
<FixedLayout>
|
<FixedLayout>
|
||||||
<Container>
|
<Container>
|
||||||
<h2 class="section-title">ART GALLERY</h2>
|
<h2 class="section-title">ART GALLERY</h2>
|
||||||
@@ -235,3 +237,9 @@ const displayedArt = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.no-sprite .undertable-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
||||||
import PageHeader from '~/components/parts/PageHeader.vue';
|
import TableHeader from '~/components/parts/TableHeader.vue';
|
||||||
|
|
||||||
const slug = useRoute().params.slug;
|
const slug = useRoute().params.slug;
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
@@ -12,7 +12,9 @@ const { data: post } = await useAsyncData(`blog-${slug}`, () =>
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Render the blog post as Prose & Vue components -->
|
<!-- Render the blog post as Prose & Vue components -->
|
||||||
<PageHeader></PageHeader>
|
<div class="no-sprite">
|
||||||
|
<TableHeader></TableHeader>
|
||||||
|
</div>
|
||||||
<FixedLayout>
|
<FixedLayout>
|
||||||
<Container>
|
<Container>
|
||||||
<ContentRenderer v-if="post" :value="post" class="blog" />
|
<ContentRenderer v-if="post" :value="post" class="blog" />
|
||||||
@@ -37,3 +39,9 @@ const { data: post } = await useAsyncData(`blog-${slug}`, () =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.no-sprite .undertable-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import MinimalHeader from '~/components/parts/MinimalHeader.vue';
|
import TableHeader from '~/components/parts/TableHeader.vue';
|
||||||
import { useAsyncData } from '#app';
|
|
||||||
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
@@ -26,7 +25,9 @@ const displayedPosts = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MinimalHeader></MinimalHeader>
|
<div class="no-sprite">
|
||||||
|
<TableHeader></TableHeader>
|
||||||
|
</div>
|
||||||
<FixedLayout>
|
<FixedLayout>
|
||||||
<Container>
|
<Container>
|
||||||
<section class="blog-section">
|
<section class="blog-section">
|
||||||
@@ -165,3 +166,9 @@ const displayedPosts = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.no-sprite .undertable-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ const { data: markdown } = await useAsyncData(`fixed`, async () =>
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="no-sprite">
|
||||||
<TableHeader></TableHeader>
|
<TableHeader></TableHeader>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FixedLayout>
|
<FixedLayout>
|
||||||
<Container>
|
<Container>
|
||||||
@@ -37,3 +39,9 @@ p {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.no-sprite .undertable-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -29,6 +29,19 @@ const { data: contactMarkdown } = await useAsyncData(`fixed-contact`, async () =
|
|||||||
|
|
||||||
const localePath = useLocalePath();
|
const localePath = useLocalePath();
|
||||||
|
|
||||||
|
function resolveProjectLink(project) {
|
||||||
|
if (project.link) return project.link;
|
||||||
|
const slugToProjectMap = {
|
||||||
|
'dragonroll': 'https://dragonroll.aranroig.com',
|
||||||
|
};
|
||||||
|
return slugToProjectMap[project.slug] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToProject(project) {
|
||||||
|
const link = resolveProjectLink(project);
|
||||||
|
if (link) window.open(link, '_blank', 'noopener,noreferrer');
|
||||||
|
}
|
||||||
|
|
||||||
const artPosts = useState<any[]>('art-posts', () => null as any);
|
const artPosts = useState<any[]>('art-posts', () => null as any);
|
||||||
if (!artPosts.value?.length) {
|
if (!artPosts.value?.length) {
|
||||||
const currentLocale = locale.value;
|
const currentLocale = locale.value;
|
||||||
@@ -125,33 +138,37 @@ const sectionTargets = {
|
|||||||
<section class="projects-section" id="scroll-projects" v-if="projects && projects.length > 0">
|
<section class="projects-section" id="scroll-projects" v-if="projects && projects.length > 0">
|
||||||
<Container>
|
<Container>
|
||||||
<h2 class="section-title">{{ t('pages.projects_heading') }}</h2>
|
<h2 class="section-title">{{ t('pages.projects_heading') }}</h2>
|
||||||
<!--
|
|
||||||
<div class="projects-grid">
|
<div class="projects-grid">
|
||||||
<div
|
<div
|
||||||
v-for="project in projects"
|
v-for="project in projects"
|
||||||
:key="project.slug"
|
|
||||||
class="project-card"
|
|
||||||
>
|
|
||||||
<span class="project-card-corner tl"></span>
|
|
||||||
<span class="project-card-corner tr"></span>
|
|
||||||
<span class="project-card-corner bl"></span>
|
|
||||||
<span class="project-card-corner br"></span>
|
|
||||||
|
|
||||||
<a
|
:key="project.slug"
|
||||||
:href="project.link"
|
class="project-card-wrapper"
|
||||||
target="_blank"
|
>
|
||||||
rel="noopener noreferrer"
|
<div
|
||||||
class="project-title"
|
class="project-card"
|
||||||
>{{ project.title }}</a>
|
@click="navigateToProject(project)"
|
||||||
|
>
|
||||||
|
<div class="project-layout">
|
||||||
|
<img
|
||||||
|
v-if="project.preview"
|
||||||
|
:src="project.preview"
|
||||||
|
:alt="`Preview of ${project.title}`"
|
||||||
|
class="project-preview"
|
||||||
|
/>
|
||||||
|
<div class="project-info">
|
||||||
|
<p class="project-title">{{ project.title }}</p>
|
||||||
<p class="project-description">{{ project.description }}</p>
|
<p class="project-description">{{ project.description }}</p>
|
||||||
<div class="project-tech">
|
<div class="project-tech">
|
||||||
<span v-for="tech in project.tech" :key="tech" class="tech-tag">{{ tech }}</span>
|
<span v-for="tech in project.tech" :key="tech" class="tech-tag">{{ tech }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
-->
|
|
||||||
(Under construction...)
|
|
||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -254,9 +271,9 @@ const sectionTargets = {
|
|||||||
<h2 class="section-title">{{ t('pages.blog_heading') }}</h2>
|
<h2 class="section-title">{{ t('pages.blog_heading') }}</h2>
|
||||||
<ul class="tui-list">
|
<ul class="tui-list">
|
||||||
<li v-for="post in displayedBlogPosts" :key="post.slug" class="blog-entry">
|
<li v-for="post in displayedBlogPosts" :key="post.slug" class="blog-entry">
|
||||||
<a class="entry-link" :href="localePath({ name: 'blog-slug', params: { slug: post.slug } })">
|
<NuxtLink :to="localePath({ name: 'blog-slug', params: { slug: post.slug } })" class="entry-link">
|
||||||
<span class="entry-title">{{ post.title }}</span>
|
<span class="entry-title">{{ post.title }}</span>
|
||||||
</a>
|
</NuxtLink>
|
||||||
<span class="entry-meta">[{{ post.date }}] {{ post.description }}</span>
|
<span class="entry-meta">[{{ post.date }}] {{ post.description }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -277,9 +294,9 @@ const sectionTargets = {
|
|||||||
<Container>
|
<Container>
|
||||||
<h2 class="section-title">{{ t('pages.art_heading') }}</h2>
|
<h2 class="section-title">{{ t('pages.art_heading') }}</h2>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<a v-for="art in displayedArtPosts" :key="art.slug"
|
<NuxtLink v-for="art in displayedArtPosts" :key="art.slug"
|
||||||
class="selector"
|
class="selector"
|
||||||
:href="isArtFallback(art) ? localePath(`/art/${art.slug}`) : localePath(`/art/${art.slug}`)">
|
:to="localePath(`/art/${art.slug}`)">
|
||||||
<span class="selector-border-top" aria-hidden="true">────────</span>
|
<span class="selector-border-top" aria-hidden="true">────────</span>
|
||||||
<img
|
<img
|
||||||
:src="art.thumb"
|
:src="art.thumb"
|
||||||
@@ -288,7 +305,7 @@ const sectionTargets = {
|
|||||||
/>
|
/>
|
||||||
<span class="selector-border-bottom" aria-hidden="true">────────</span>
|
<span class="selector-border-bottom" aria-hidden="true">────────</span>
|
||||||
<div class="overlay-label">{{ art.title }}</div>
|
<div class="overlay-label">{{ art.title }}</div>
|
||||||
</a>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="artPosts && artPosts.length > 0" class="show-more-link">
|
<p v-if="artPosts && artPosts.length > 0" class="show-more-link">
|
||||||
<NuxtLink :to="localePath('/art')" class="tui-link">{{ t('pages.show_more') }}</NuxtLink>
|
<NuxtLink :to="localePath('/art')" class="tui-link">{{ t('pages.show_more') }}</NuxtLink>
|
||||||
@@ -388,11 +405,6 @@ const sectionTargets = {
|
|||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
transition: all 0.1s steps(2, end);
|
transition: all 0.1s steps(2, end);
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--color-link);
|
|
||||||
box-shadow: 4px -4px 0px 0px var(--color-link);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "[ PROJECT ]";
|
content: "[ PROJECT ]";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -404,6 +416,28 @@ const sectionTargets = {
|
|||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-preview {
|
||||||
|
width: 140px;
|
||||||
|
min-width: 140px;
|
||||||
|
height: 85px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid var(--color-border-color);
|
||||||
|
image-rendering: auto;
|
||||||
|
transition: border-color 0.1s steps(2, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card-corner {
|
.project-card-corner {
|
||||||
@@ -420,18 +454,29 @@ const sectionTargets = {
|
|||||||
|
|
||||||
.project-title {
|
.project-title {
|
||||||
font-family: 'Hurmit', monospace;
|
font-family: 'Hurmit', monospace;
|
||||||
color: var(--color-link);
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
text-shadow: 0 0 4px var(--color-link);
|
text-shadow: inherit;
|
||||||
display: block;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
text-shadow: 0 0 12px var(--color-link), 0 0 4px var(--color-link);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-card {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card-wrapper:hover .project-card {
|
||||||
|
border-color: var(--color-link);
|
||||||
|
box-shadow: 4px -4px 0px 0px var(--color-link);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-info {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.project-description {
|
.project-description {
|
||||||
font-family: 'Hurmit', monospace;
|
font-family: 'Hurmit', monospace;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
@@ -763,6 +808,16 @@ const sectionTargets = {
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-layout {
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-preview {
|
||||||
|
width: 100% !important;
|
||||||
|
min-width: auto !important;
|
||||||
|
height: 160px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.project-title {
|
.project-title {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export default defineContentConfig({
|
|||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
link: z.string().optional(),
|
link: z.string().optional(),
|
||||||
|
preview: z.string().optional(),
|
||||||
tech: z.array(z.string()).default([])
|
tech: z.array(z.string()).default([])
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
title: App de Codelearn
|
|
||||||
slug: codelearn-app
|
|
||||||
date: 2026-01-01
|
|
||||||
description: Una aplicació full-stack de gestió d'aprenentatge construïda al meu temps a Codelearn. Autenticació d'usuaris, gestió de cursos i seguiment del progrés en temps real.
|
|
||||||
link: https://codelearn.cat
|
|
||||||
tech: ["Nuxt", "Express", "MongoDB", "TypeScript"]
|
|
||||||
---
|
|
||||||
7
frontend/content/projects/ca/dragonroll.md
Normal file
7
frontend/content/projects/ca/dragonroll.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Dragonroll
|
||||||
|
slug: dragonroll
|
||||||
|
date: 2026-01-01
|
||||||
|
description: Un assistent de codi obert per a jocs de rol. Fes un seguiment de personatges, comparteix notes, reprodueix música, planifica encontres i gestiona ítems i encants.
|
||||||
|
tech: ["Nuxt", "Vue", "Express", "MongoDB"]
|
||||||
|
---
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
title: Eina Pipeline ML
|
|
||||||
slug: ml-pipeline
|
|
||||||
date: 2026-01-01
|
|
||||||
description: Una eina modular de pipeline d'aprenentatge automàtic per al màster d'Aprenentatge Automàtic i Ciberseguretat de la UPC. Automatització del preprocesament, entrenament i avaluació de models.
|
|
||||||
tech: ["Python", "PyTorch", "Docker"]
|
|
||||||
---
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
title: Portfolio Website
|
|
||||||
slug: portfolio-website
|
|
||||||
date: 2026-01-01
|
|
||||||
description: Aquesta mateixa pàgina web, construïda amb Nuxt 4, Vue 3 i SCSS. Estètica pixel-art, temes fosc/clar, personalització de colors d'accent i suport multilingüe en anglès, castellà i català.
|
|
||||||
link: https://aranroig.com
|
|
||||||
tech: ["Nuxt 4", "Vue 3", "SCSS", "Node.js", "MongoDB"]
|
|
||||||
---
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
title: Codelearn Full-Stack App
|
|
||||||
slug: codelearn-app
|
|
||||||
date: 2026-01-01
|
|
||||||
description: A full-stack learning management application built during my time at Codelearn. Features user authentication, course management, and real-time progress tracking.
|
|
||||||
link: https://codelearn.cat
|
|
||||||
tech: ["Nuxt", "Express", "MongoDB", "TypeScript"]
|
|
||||||
---
|
|
||||||
7
frontend/content/projects/en/dragonroll.md
Normal file
7
frontend/content/projects/en/dragonroll.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Dragonroll
|
||||||
|
slug: dragonroll
|
||||||
|
date: 2026-01-01
|
||||||
|
description: An open-source helper for role-playing games. Track characters, share notes, play music, plan encounters, and manage items and spells.
|
||||||
|
tech: ["Nuxt", "Vue", "Express", "MongoDB"]
|
||||||
|
---
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
title: ML Pipeline Tool
|
|
||||||
slug: ml-pipeline
|
|
||||||
date: 2026-01-01
|
|
||||||
description: A modular machine learning pipeline tool for UPC's Machine Learning and Cybersecurity master's degree. Automates data preprocessing, model training, and evaluation workflows.
|
|
||||||
tech: ["Python", "PyTorch", "Docker"]
|
|
||||||
---
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
title: Portfolio Website
|
|
||||||
slug: portfolio-website
|
|
||||||
date: 2026-01-01
|
|
||||||
description: This very website, built with Nuxt 4, Vue 3, and SCSS. Features pixel-art aesthetics, dark/light themes, accent color customization, and multilingual support across English, Spanish, and Catalan.
|
|
||||||
link: https://aranroig.com
|
|
||||||
tech: ["Nuxt 4", "Vue 3", "SCSS", "Node.js", "MongoDB"]
|
|
||||||
---
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
title: App de Codelearn
|
|
||||||
slug: codelearn-app
|
|
||||||
date: 2026-01-01
|
|
||||||
description: Una aplicación full-stack de gestión de aprendizaje construida en mi tiempo en Codelearn. Autenticación de usuarios, gestión de cursos y seguimiento de progreso en tiempo real.
|
|
||||||
link: https://codelearn.cat
|
|
||||||
tech: ["Nuxt", "Express", "MongoDB", "TypeScript"]
|
|
||||||
---
|
|
||||||
7
frontend/content/projects/es/dragonroll.md
Normal file
7
frontend/content/projects/es/dragonroll.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Dragonroll
|
||||||
|
slug: dragonroll
|
||||||
|
date: 2026-01-01
|
||||||
|
description: Un asistente de código abierto para juegos de rol. Controla personajes, comparte notas, reproduce música, planea encuentros y gestiona objetos y hechizos.
|
||||||
|
tech: ["Nuxt", "Vue", "Express", "MongoDB"]
|
||||||
|
---
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
title: Herramienta Pipeline ML
|
|
||||||
slug: ml-pipeline
|
|
||||||
date: 2026-01-01
|
|
||||||
description: Una herramienta modular de pipeline de aprendizaje automático para el máster de Aprendizaje Automático y Ciberseguridad de la UPC. Automatiza preprocesamiento, entrenamiento y evaluación de modelos.
|
|
||||||
tech: ["Python", "PyTorch", "Docker"]
|
|
||||||
---
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
title: Portfolio Website
|
|
||||||
slug: portfolio-website
|
|
||||||
date: 2026-01-01
|
|
||||||
description: Esta página web, construida con Nuxt 4, Vue 3 y SCSS. Con estética pixel-art, temas oscuro/claro, personalización de colores de acento y soporte multilingue en inglés, español y catalán.
|
|
||||||
link: https://aranroig.com
|
|
||||||
tech: ["Nuxt 4", "Vue 3", "SCSS", "Node.js", "MongoDB"]
|
|
||||||
---
|
|
||||||
Reference in New Issue
Block a user