This commit is contained in:
275
frontend/app/pages/projects/index.vue
Normal file
275
frontend/app/pages/projects/index.vue
Normal file
@@ -0,0 +1,275 @@
|
||||
<script setup lang="ts">
|
||||
import FixedLayout from '~/components/layouts/FixedLayout.vue';
|
||||
import TableHeader from '~/components/parts/TableHeader.vue';
|
||||
import { useSeo } from '~/composables/seo';
|
||||
|
||||
const { locale, t } = useI18n();
|
||||
|
||||
useSeo({
|
||||
title: 'Projects',
|
||||
description: 'Explore the projects of Aran Roig. Software engineering, tools, games and creative experiments.',
|
||||
canonicalUrl: '/projects'
|
||||
});
|
||||
|
||||
useHead({
|
||||
script: [{
|
||||
type: 'application/ld+json',
|
||||
innerHTML: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'CollectionPage',
|
||||
headline: 'Projects by Aran Roig',
|
||||
description: 'Explore the projects of Aran Roig. Software engineering, tools, games and creative experiments.',
|
||||
url: 'https://aranroig.com/projects',
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
name: 'Aran Roig'
|
||||
},
|
||||
inLanguage: ['en', 'es', 'ca', 'zh']
|
||||
})
|
||||
}]
|
||||
});
|
||||
|
||||
const { data: projects } = useAsyncData('projects-list', async () => {
|
||||
const currentLocale = locale.value;
|
||||
|
||||
const enProjects = await queryCollection('projects')
|
||||
.where('path', 'LIKE', `/projects/en/%`)
|
||||
.all();
|
||||
|
||||
if (currentLocale === 'en') return enProjects;
|
||||
|
||||
const localeProjects = await queryCollection('projects')
|
||||
.where('path', 'LIKE', `/projects/${currentLocale}/%`)
|
||||
.all();
|
||||
|
||||
const translatedSlugs = new Set(
|
||||
localeProjects.map((p) => p.path.replace(`/projects/${currentLocale}/`, ''))
|
||||
);
|
||||
|
||||
const enFallbacks = enProjects.filter(
|
||||
(p) => !translatedSlugs.has(p.path.replace('/projects/en/', ''))
|
||||
);
|
||||
|
||||
return [...localeProjects, ...enFallbacks];
|
||||
}, { watch: [locale, () => useRoute().path] });
|
||||
|
||||
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');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="no-sprite">
|
||||
<TableHeader></TableHeader>
|
||||
</div>
|
||||
<FixedLayout>
|
||||
<Container class="top-margin">
|
||||
<h1 class="section-title">{{ t('pages.projects_heading') }}</h1>
|
||||
<div class="projects-grid">
|
||||
<div
|
||||
v-for="project in projects"
|
||||
:key="project.slug"
|
||||
class="project-card-wrapper"
|
||||
>
|
||||
<div class="project-card" @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>
|
||||
<div class="project-tech">
|
||||
<span v-for="tech in project.tech" :key="tech" class="tech-tag">{{ tech }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</FixedLayout>
|
||||
<Footer></Footer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.section-title {
|
||||
font-family: 'Hurmit', monospace;
|
||||
color: var(--color-text);
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 28px 0 16px 0;
|
||||
padding-left: 12px;
|
||||
line-height: 1.3;
|
||||
|
||||
&::before {
|
||||
content: "├";
|
||||
color: var(--color-link);
|
||||
margin-right: 8px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: var(--color-link);
|
||||
margin-left: 10px;
|
||||
vertical-align: middle;
|
||||
box-shadow: 0 0 4px var(--color-link);
|
||||
animation: blink-cursor 1s steps(1) infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink-cursor {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.projects-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
position: relative;
|
||||
background: var(--color-background-fore);
|
||||
border: 1px solid var(--color-border-color);
|
||||
box-shadow: 4px -4px 0px 0px var(--color-container-shadow);
|
||||
padding: 12px 16px;
|
||||
transition: all 0.1s steps(2, end);
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
content: "[ PROJECT ]";
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
left: 12px;
|
||||
background-color: var(--color-background-fore);
|
||||
padding: 0 6px;
|
||||
font-size: 0.65rem;
|
||||
color: var(--color-link);
|
||||
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;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.project-card-wrapper:hover .project-card {
|
||||
border-color: var(--color-link);
|
||||
box-shadow: 4px -4px 0px 0px var(--color-link);
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-family: 'Hurmit', monospace;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: 1.05rem;
|
||||
text-shadow: inherit;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-link);
|
||||
}
|
||||
}
|
||||
|
||||
.project-description {
|
||||
font-family: 'Hurmit', monospace;
|
||||
color: var(--color-text);
|
||||
margin: 8px 0 12px 0;
|
||||
line-height: 1.5;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.project-tech {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tech-tag {
|
||||
font-family: 'Hurmit', monospace;
|
||||
font-size: 0.7rem;
|
||||
color: var(--color-background-fore);
|
||||
background-color: var(--color-link);
|
||||
padding: 1px 6px;
|
||||
box-shadow: 2px -2px 0px 0px rgba(0, 0, 0, 0.3);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.section-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.project-layout {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.project-preview {
|
||||
width: 100% !important;
|
||||
min-width: auto !important;
|
||||
height: 160px !important;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.project-description {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.section-title {
|
||||
font-size: 1rem;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.no-sprite .undertable-wrapper {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user