Portfolio page and language picker

This commit is contained in:
2026-05-14 23:40:04 +03:00
parent 000eb3367a
commit 136dc8b892
8 changed files with 657 additions and 54 deletions

View File

@@ -1,30 +1,33 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import ThemeToggle from './components/ThemeToggle.vue'
import LanguageToggle from './components/LanguageToggle.vue'
import LogoSvg from './components/icons/IconLogo.vue'
import { useLanguage } from './composables/useLanguage'
const { t } = useLanguage()
</script>
<template>
<ThemeToggle />
<LanguageToggle />
<header>
<div class="wrapper">
<div class="title-section">
<div class="greetings">
<div class="title-with-logo">
<LogoSvg alt="Tietokonepajan logo" class="logo" />
<h1>Livonsaaren Tietokonepaja</h1>
<h1>{{ t.siteTitle }}</h1>
</div>
<h3>
Tervetuloa Livonsaaren Tietokonepajan verkkosivuille! Tarjoamme kattavia IT-palveluita,
laitehuoltoja, kotisivuratkaisuja ja Linux-tukea. Tutustu palveluihimme ja ota yhteyttä!
</h3>
<h3>{{ t.siteSubtitle }}</h3>
</div>
</div>
<nav>
<RouterLink to="/">Etusivu</RouterLink>
<RouterLink to="/about">Tietoa</RouterLink>
<RouterLink to="/contact">Yhteystiedot</RouterLink>
<RouterLink to="/">{{ t.navHome }}</RouterLink>
<RouterLink to="/about">{{ t.navAbout }}</RouterLink>
<RouterLink to="/portfolio">{{ t.navPortfolio }}</RouterLink>
<RouterLink to="/contact">{{ t.navContact }}</RouterLink>
</nav>
</div>
</header>

View File

@@ -4,6 +4,9 @@ import IconHomesite from './icons/IconHomesite.vue'
import IconItSupport from './icons/IconItSupport.vue'
import IconLinux from './icons/IconLinux.vue'
import IconRepair from './icons/IconRepair.vue'
import { useLanguage } from '@/composables/useLanguage'
const { t } = useLanguage()
</script>
<template>
@@ -11,42 +14,31 @@ import IconRepair from './icons/IconRepair.vue'
<template #icon>
<IconHomesite />
</template>
<template #heading>Kotisivut</template>
Tarvitsetko kotisivut yrityksellesi tai yhdistyksellesi? Suunnittelemme ja toteutamme
responsiiviset ja käyttäjäystävälliset kotisivut, jotka vastaavat tarpeitasi ja edistävät
näkyvyyttäsi verkossa.
<template #heading>{{ t.service1Heading }}</template>
{{ t.service1Text }}
</ServiceItem>
<ServiceItem>
<template #icon>
<IconItSupport />
</template>
<template #heading>IT Tukea</template>
Apua arjen haasteisiin tietokoneiden, ohjelmistojen ja verkkojen kanssa. Mikään pulma ei ole
liian pieni tai suuri! Kysymisestä emme laskuta mitään, joten ota rohkeasti yhteyttä!
<template #heading>{{ t.service2Heading }}</template>
{{ t.service2Text }}
</ServiceItem>
<ServiceItem>
<template #icon>
<IconRepair />
</template>
<template #heading>Laitehuollot</template>
Tarjoamme luotettavaa ja nopeaa huoltopalvelua tietokoneille, älypuhelimille ja tableteille.
Vianmääritys ja korjaukset suoritetaan ammattitaidolla, jotta laitteesi toimii taas
moitteettomasti.
<template #heading>{{ t.service3Heading }}</template>
{{ t.service3Text }}
</ServiceItem>
<ServiceItem>
<template #icon>
<IconLinux />
</template>
<template #heading>Linux</template>
Vapaat ohjelmistot ja avoin lähdekoodi on lähellä sydäntämme. Tarjoamme Linux-käyttöjärjestelmä
tukea ja asennuksia huokeasti. Mikäli vaihdat pois Windowsista, saat Linux asennuksen
ilmaiseksi!
<template #heading>{{ t.service4Heading }}</template>
{{ t.service4Text }}
</ServiceItem>
</template>

View File

@@ -0,0 +1,73 @@
<template>
<div class="language-picker">
<button
@click="setLanguage('fi')"
class="lang-btn"
:class="{ active: language === 'fi' }"
aria-label="Vaihda suomeksi"
>
FI
</button>
<button
@click="setLanguage('en')"
class="lang-btn"
:class="{ active: language === 'en' }"
aria-label="Switch to English"
>
EN
</button>
</div>
</template>
<script setup lang="ts">
import { useLanguage } from '@/composables/useLanguage'
const { language, setLanguage } = useLanguage()
</script>
<style scoped>
.language-picker {
position: fixed;
top: 20px;
right: 78px;
z-index: 1000;
display: flex;
gap: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: 24px;
background: var(--color-background-soft);
border: 1px solid var(--color-border);
padding: 3px;
}
.lang-btn {
background: transparent;
border: none;
border-radius: 20px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.8rem;
font-weight: 600;
color: var(--color-text);
opacity: 0.5;
}
.lang-btn:hover {
opacity: 0.8;
}
.lang-btn.active {
background: var(--color-background-mute);
opacity: 1;
border: 1px solid var(--color-border);
}
.lang-btn:active {
transform: scale(0.95);
}
</style>

View File

@@ -0,0 +1,185 @@
import { ref, computed } from 'vue'
type Lang = 'fi' | 'en'
const language = ref<Lang>((localStorage.getItem('language') as Lang) ?? 'fi')
const setLanguage = (lang: Lang) => {
language.value = lang
localStorage.setItem('language', lang)
}
const toggleLanguage = () => {
setLanguage(language.value === 'fi' ? 'en' : 'fi')
}
const translations = {
fi: {
siteTitle: 'Livonsaaren Tietokonepaja',
siteSubtitle:
'Tervetuloa Livonsaaren Tietokonepajan verkkosivuille! Tarjoamme kattavia IT-palveluita, laitehuoltoja, kotisivuratkaisuja ja Linux-tukea. Tutustu palveluihimme ja ota yhteyttä!',
navHome: 'Etusivu',
navAbout: 'Tietoa',
navContact: 'Yhteystiedot',
// HomeSection
service1Heading: 'Kotisivut',
service1Text:
'Tarvitsetko kotisivut yrityksellesi tai yhdistyksellesi? Suunnittelemme ja toteutamme responsiiviset ja käyttäjäystävälliset kotisivut, jotka vastaavat tarpeitasi ja edistävät näkyvyyttäsi verkossa.',
service2Heading: 'IT Tukea',
service2Text:
'Apua arjen haasteisiin tietokoneiden, ohjelmistojen ja verkkojen kanssa. Mikään pulma ei ole liian pieni tai suuri! Kysymisestä emme laskuta mitään, joten ota rohkeasti yhteyttä!',
service3Heading: 'Laitehuollot',
service3Text:
'Tarjoamme luotettavaa ja nopeaa huoltopalvelua tietokoneille, älypuhelimille ja tableteille. Vianmääritys ja korjaukset suoritetaan ammattitaidolla, jotta laitteesi toimii taas moitteettomasti.',
service4Heading: 'Linux',
service4Text:
'Vapaat ohjelmistot ja avoin lähdekoodi on lähellä sydäntämme. Tarjoamme Linux-käyttöjärjestelmä tukea ja asennuksia huokeasti. Mikäli vaihdat pois Windowsista, saat Linux asennuksen ilmaiseksi!',
// InfoView
infoHeading: 'Tietoa',
infoText:
'Livonsaaren Tietokonepaja vuonna 2024 perustettu kahden miehen projekti, jonka tehtävänä on tarjota matalankynnyksen IT-tukea ja -palveluita Livonsaaren ja lähialueiden asukkaille sekä yrityksille. Yrityksemme erikoistuu tietokoneiden huoltoon, ohjelmistojen asennukseen, kotisivuratkaisuihin ja Linux-käyttöjärjestelmän tukeen.',
valuesHeading: 'Arvomme',
value1Label: 'Järkevyys:',
value1Text:
'Laitteiden ja palveluiden tulee aina helpottaa käyttäjiensä elämää. Ei vaikeuttaa sitä.',
value2Label: 'Hallinta:',
value2Text:
'Käyttäjän tulee voida itse määritellä mitä laite tai ohjelmisto tekee. Mikäli tämä ei ole mahdollista, se pitää olla vaihdettavissa toiseen.',
value3Label: 'Korjattavuus:',
value3Text:
'Laitteiden tulee olla ylläpidettävissä ja korjattavissa ilman kalliita työkaluja tai lisenssejä.',
visionHeading: 'Visiomme',
visionText:
'Nykypäivänä erilaiset vempeleet ja palvelut vievät valtavasti rahaa ja aikaamme huonolla hyötysuhteella. Tämä johdosta tietokoneista onkin tullut monille kirosana. Me Tietokonepajalla haluamme olla rakentamassa uudenlaista tulevaisuutta, jossa teknologia palvelee käyttäjiään eikä päinvastoin. Vapaat ohjelmistot ja laitteiden kiertotalous tarjoaakin halvan, hallittavan ja mielekkään vaihtoehdon nykymenolle. Tule siis rohkeasti mukaan tekemään tietotekniikasta taas hauskaa ja hyödyllistä!',
// ContactView
whoWeAreHeading: 'Keitä me olemme',
veikkoAlt: 'Veikon kuva',
janiAlt: 'Janin kuva',
contactHeading: 'Yhteydenotot',
// PortfolioView
navPortfolio: 'Portfolio',
portfolioHeading: 'Portfolio',
portfolioCustomerHeading: 'Asiakasprojektit',
portfolioMaintenanceHeading: 'Paikallinen IT-infrastruktuuri',
portfolioSourceCode: 'Lähdekoodi',
portfolioVisitSite: 'Sivustolle',
portfolioProject1Name: 'Pro Sinervo Ry',
portfolioProject1Desc:
'Kotisivu Pro Sinervo ry:lle Velkualaiselle paikallisyhdistykselle. Sivusto tarjoaa tapahtumakalenterin, uutisia ja tietoa talon toiminnasta sekä tilavarausmahdollisuuden.',
portfolioProject2Name: 'Runosaari',
portfolioProject2Desc:
'Verkkosivusto Runosaari-runofestivaalille, joka järjestetään vuosittain Naantalin saaristossa. Sivusto esittelee ohjelman, esiintyjät ja käytännön tiedot festivaalivieraille.',
portfolioProject3Name: 'Livonsaaren Osuuskauppa',
portfolioProject3Desc:
'Verkkosivusto Livonsaaren osuuskaupalle paikalliselle kyläkaupalle Livonsaaressa. Sivusto näyttää ajantasaiset aukioloajat (haetaan Klapi-rajapinnasta), yhteystiedot sekä sijainnin karttanäkymän.',
portfolioMaint1Name: 'Klapi',
portfolioMaint1Desc:
'Klapi on Tietokonepajan kehittämä monikäyttöinen API-rajapinta ja tietovarasto. Se mahdollistaa datan tallentamisen ja hakemisen muihin palveluihin esimerkiksi osuuskaupan aukioloajat haetaan Klapin kautta. Käyttäjillä on selainpohjainen hallintapaneeli omien tietojensa muokkaamiseen.',
portfolioMaint2Name: 'Sähköpostipalvelin',
portfolioMaint2Desc:
'Tietokonepajan itse isännöimä sähköpostipalvelin, joka pyörittää organisaation sähköpostiviestintää. Palvelin hallinnoi useita sähköpostitilejä, roskapostisuodatuksen sekä turvallisen viestinlähetyksen ja -vastaanoton.',
portfolioMaint3Name: 'Gitea',
portfolioMaint3Desc:
'Tietokonepajan oma Git-palvelin, jossa säilytetään kaikkien projektien lähdekoodit. Gitea tarjoaa GitHubin kaltaisen käyttöliittymän repositorioiden hallintaan, pull requesteihin ja issue-seurantaan.',
portfolioMaint4Name: 'Hattara',
portfolioMaint4Desc:
'Tietokonepajan ylläpitämä Nextcloud-instanssi paikallisille asukkaille. Hattara tarjoaa yksityisyydensuojaa arvostavan vaihtoehdon kaupallisille pilvipalveluille tiedostot, kalenterit ja yhteystiedot pysyvät omalla palvelimella.',
},
en: {
siteTitle: 'Livonsaaren Tietokonepaja',
siteSubtitle:
"Welcome to Livonsaaren Tietokonepaja's website! We offer comprehensive IT services, device repairs, website solutions, and Linux support. Browse our services and get in touch!",
navHome: 'Home',
navAbout: 'About',
navContact: 'Contact',
// HomeSection
service1Heading: 'Websites',
service1Text:
'Need a website for your business or association? We design and build responsive, user-friendly websites that meet your needs and boost your online visibility.',
service2Heading: 'IT Support',
service2Text:
"Help with everyday challenges involving computers, software and networks. No problem is too small or too large! We don't charge for asking questions, so feel free to contact us!",
service3Heading: 'Device Repairs',
service3Text:
'We offer reliable and fast repair services for computers, smartphones and tablets. Diagnostics and repairs are carried out professionally so your device works flawlessly again.',
service4Heading: 'Linux',
service4Text:
'Free software and open source is close to our hearts. We offer Linux operating system support and installations at affordable prices. If you switch away from Windows, you get a Linux installation for free!',
// InfoView
infoHeading: 'About',
infoText:
'Livonsaaren Tietokonepaja is a two-person project founded in 2024, with the goal of providing accessible IT support and services to residents and businesses in Livonsaari and the surrounding area. Our company specializes in computer repairs, software installation, website solutions, and Linux operating system support.',
valuesHeading: 'Our Values',
value1Label: 'Practicality:',
value1Text: "Devices and services should always make users' lives easier. Not harder.",
value2Label: 'Control:',
value2Text:
'Users should be able to define what their device or software does. If this is not possible, it should be replaceable with something that allows it.',
value3Label: 'Repairability:',
value3Text:
'Devices should be maintainable and repairable without expensive tools or licenses.',
visionHeading: 'Our Vision',
visionText:
'Nowadays, various gadgets and services consume vast amounts of our money and time with poor value. As a result, computers have become a dirty word for many people. At Livonsaaren Tietokonepaja, we want to help build a new kind of future where technology serves its users, not the other way around. Free software and the circular economy of devices offer a cheap, controllable, and meaningful alternative to the current trend. So come join us in making technology fun and useful again!',
// ContactView
whoWeAreHeading: 'Who We Are',
veikkoAlt: 'Photo of Veikko',
janiAlt: 'Photo of Jani',
contactHeading: 'Contact Us',
// PortfolioView
navPortfolio: 'Portfolio',
portfolioHeading: 'Portfolio',
portfolioCustomerHeading: 'Customer Projects',
portfolioMaintenanceHeading: 'Local IT Infrastructure',
portfolioSourceCode: 'Source Code',
portfolioVisitSite: 'Visit Site',
portfolioProject1Name: 'Pro Sinervo Ry',
portfolioProject1Desc:
'Website for Pro Sinervo ry — a local association in Velkua. The site features an event calendar, news posts, and information about booking the venue.',
portfolioProject2Name: 'Runosaari',
portfolioProject2Desc:
'Website for the Runosaari poetry festival held annually in the Naantali archipelago. The site showcases the programme, performers, and practical information for festival visitors.',
portfolioProject3Name: 'Livonsaaren Osuuskauppa',
portfolioProject3Desc:
'Website for Livonsaaren Osuuskauppa — a local cooperative grocery store on Livonsaari island. The site displays up-to-date opening hours (fetched from the Klapi API), contact details, and an embedded map.',
portfolioMaint1Name: 'Klapi',
portfolioMaint1Desc:
"Klapi is a versatile API platform and data store developed by Tietokonepaja. It enables storing and retrieving data for other services — for example, the cooperative store's opening hours are served through Klapi. Users manage their own data through a browser-based admin panel.",
portfolioMaint2Name: 'Mail Server',
portfolioMaint2Desc:
"Tietokonepaja's self-hosted mail server managing the organisation's email communication. The server handles multiple email accounts, spam filtering, and secure message delivery and receipt.",
portfolioMaint3Name: 'Gitea',
portfolioMaint3Desc:
"Tietokonepaja's self-hosted Git server storing source code for all projects. Gitea provides a GitHub-like interface for repository management, pull requests, and issue tracking.",
portfolioMaint4Name: 'Hattara',
portfolioMaint4Desc:
'A Nextcloud instance hosted by Tietokonepaja for local residents. Hattara provides a privacy-driven alternative to commercial cloud services — files, calendars, and contacts stay on our own server.',
},
}
export function useLanguage() {
const t = computed(() => translations[language.value])
return { language, t, setLanguage, toggleLanguage }
}

View File

@@ -22,6 +22,11 @@ const router = createRouter({
name: 'contact',
component: () => import('../views/ContactView.vue'),
},
{
path: '/portfolio',
name: 'portfolio',
component: () => import('../views/PortfolioView.vue'),
},
],
})

View File

@@ -1,19 +1,22 @@
<script setup lang="ts">
import IconEmail from '@/components/icons/IconEmail.vue'
import IconPhone from '@/components/icons/IconPhone.vue'
import { useLanguage } from '@/composables/useLanguage'
const { t } = useLanguage()
</script>
<template>
<div class="contact">
<div class="contact-item">
<div class="details">
<h3>Keitä me olemme</h3>
<h3>{{ t.whoWeAreHeading }}</h3>
<div class="contact-person-container">
<div class="contact-person">
<strong>Veikko</strong>
<img
src="@/assets/veikko.png"
alt="Veikon kuva"
:alt="t.veikkoAlt"
style="
width: 100px;
height: auto;
@@ -27,7 +30,7 @@ import IconPhone from '@/components/icons/IconPhone.vue'
<strong>Jani</strong>
<img
src="@/assets/jani.png"
alt="Janin kuva"
:alt="t.janiAlt"
style="
width: 100px;
height: auto;
@@ -38,7 +41,7 @@ import IconPhone from '@/components/icons/IconPhone.vue'
/>
</div>
</div>
<h4>Yhteydenotot</h4>
<h4>{{ t.contactHeading }}</h4>
<div class="contact-info">
<IconEmail />

View File

@@ -1,32 +1,30 @@
<script setup lang="ts">
import { useLanguage } from '@/composables/useLanguage'
const { t } = useLanguage()
</script>
<template>
<div class="about">
<div class="about-item">
<div class="details">
<h3>Tietoa</h3>
<p>
Livonsaaren Tietokonepaja vuonna 2024 perustettu kahden miehen projekti, jonka tehtävänä
on tarjota matalankynnyksen IT-tukea ja -palveluita Livonsaaren ja lähialueiden asukkaille
sekä yrityksille. Yrityksemme erikoistuu tietokoneiden huoltoon, ohjelmistojen
asennukseen, kotisivuratkaisuihin ja Linux-käyttöjärjestelmän tukeen.
</p>
<h3>{{ t.infoHeading }}</h3>
<p>{{ t.infoText }}</p>
</div>
</div>
<div class="about-item">
<div class="details">
<h3>Arvomme</h3>
<h3>{{ t.valuesHeading }}</h3>
<ul>
<li>
<strong>Järkevyys:</strong> Laitteiden ja palveluiden tulee aina helpottaa käyttäjiensä
elämää. Ei vaikeuttaa sitä.
<strong>{{ t.value1Label }}</strong> {{ t.value1Text }}
</li>
<li>
<strong>Hallinta:</strong> Käyttäjän tulee voida itse määritellä mitä laite tai
ohjelmisto tekee. Mikäli tämä ei ole mahdollista, se pitää olla vaihdettavissa toiseen.
<strong>{{ t.value2Label }}</strong> {{ t.value2Text }}
</li>
<li>
<strong>Korjattavuus:</strong> Laitteiden tulee olla ylläpidettävissä ja korjattavissa
ilman kalliita työkaluja tai lisenssejä.
<strong>{{ t.value3Label }}</strong> {{ t.value3Text }}
</li>
</ul>
</div>
@@ -34,15 +32,8 @@
<div class="about-item">
<div class="details">
<h3>Visiomme</h3>
<p>
Nykypäivänä erilaiset vempeleet ja palvelut vievät valtavasti rahaa ja aikaamme huonolla
hyötysuhteella. Tämä johdosta tietokoneista onkin tullut monille kirosana. Me
Tietokonepajalla haluamme olla rakentamassa uudenlaista tulevaisuutta, jossa teknologia
palvelee käyttäjiään eikä päinvastoin. Vapaat ohjelmistot ja laitteiden kiertotalous
tarjoaakin halvan, hallittavan ja mielekkään vaihtoehdon nykymenolle. Tule siis rohkeasti
mukaan tekemään tietotekniikasta taas hauskaa ja hyödyllistä!
</p>
<h3>{{ t.visionHeading }}</h3>
<p>{{ t.visionText }}</p>
</div>
</div>
</div>

351
src/views/PortfolioView.vue Normal file
View File

@@ -0,0 +1,351 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useLanguage } from '@/composables/useLanguage'
const { t } = useLanguage()
interface Tag {
label: string
color: string
}
interface Project {
nameKey: string
descKey: string
url: string
sourceUrl?: string
tags: Tag[]
}
const customerProjects: Project[] = [
{
nameKey: 'portfolioProject1Name',
descKey: 'portfolioProject1Desc',
url: 'https://prosinervo.com/',
tags: [
{ label: 'WordPress', color: '#21759b' },
{ label: 'PHP', color: '#7a86b8' },
{ label: 'MySQL', color: '#4479a1' },
],
},
{
nameKey: 'portfolioProject2Name',
descKey: 'portfolioProject2Desc',
url: 'https://www.runosaari.fi/',
sourceUrl: 'https://gitea.tietokonepaja.fi/tietokonepaja/runosaari',
tags: [
{ label: 'Next.js', color: '#3d3d3d' },
{ label: 'React', color: '#087ea4' },
{ label: 'TypeScript', color: '#3178c6' },
{ label: 'SCSS', color: '#c6538c' },
],
},
{
nameKey: 'portfolioProject3Name',
descKey: 'portfolioProject3Desc',
url: 'https://www.livonsaarenosuuskauppa.fi/',
sourceUrl: 'https://gitea.tietokonepaja.fi/tietokonepaja/osuuskauppa',
tags: [
{ label: 'Next.js', color: '#3d3d3d' },
{ label: 'TypeScript', color: '#3178c6' },
{ label: 'SCSS', color: '#c6538c' },
{ label: 'Klapi API', color: '#2e7d32' },
],
},
]
const maintenanceProjects: Project[] = [
{
nameKey: 'portfolioMaint1Name',
descKey: 'portfolioMaint1Desc',
url: 'https://klapi.tietokonepaja.fi/',
sourceUrl: 'https://gitea.tietokonepaja.fi/tietokonepaja/klapi',
tags: [
{ label: 'ASP.NET Core', color: '#512bd4' },
{ label: 'C#', color: '#7b4fa6' },
{ label: 'TypeScript', color: '#3178c6' },
{ label: 'React', color: '#087ea4' },
{ label: 'SQLite', color: '#0f7b6c' },
],
},
{
nameKey: 'portfolioMaint2Name',
descKey: 'portfolioMaint2Desc',
url: 'https://mail.tietokonepaja.fi/',
tags: [
{ label: 'Mailcow', color: '#e65100' },
{ label: 'Docker', color: '#2496ed' },
{ label: 'Postfix', color: '#b71c1c' },
{ label: 'Dovecot', color: '#1565c0' },
{ label: 'Nginx', color: '#009900' },
],
},
{
nameKey: 'portfolioMaint3Name',
descKey: 'portfolioMaint3Desc',
url: 'https://gitea.tietokonepaja.fi/',
tags: [
{ label: 'Gitea', color: '#609926' },
{ label: 'Go', color: '#00add8' },
{ label: 'Docker', color: '#2496ed' },
],
},
{
nameKey: 'portfolioMaint4Name',
descKey: 'portfolioMaint4Desc',
url: 'https://hattara.tietokonepaja.fi/',
tags: [
{ label: 'Nextcloud', color: '#0082c9' },
{ label: 'PHP', color: '#7a86b8' },
{ label: 'Docker', color: '#2496ed' },
],
},
]
const openItems = ref<Set<string>>(new Set())
function toggle(url: string) {
if (openItems.value.has(url)) {
openItems.value.delete(url)
} else {
openItems.value.add(url)
}
}
function isOpen(url: string) {
return openItems.value.has(url)
}
</script>
<template>
<div class="portfolio">
<section>
<h3>{{ t.portfolioCustomerHeading }}</h3>
<div class="project-list">
<div v-for="project in customerProjects" :key="project.url" class="project-card">
<button
class="project-header"
@click="toggle(project.url)"
:aria-expanded="isOpen(project.url)"
>
<h4>{{ t[project.nameKey] }}</h4>
<div class="header-right">
<div class="tag-list">
<span
v-for="tag in project.tags"
:key="tag.label"
class="tag"
:style="{ backgroundColor: tag.color }"
>
{{ tag.label }}
</span>
</div>
<span class="chevron" :class="{ open: isOpen(project.url) }"></span>
</div>
</button>
<div class="expand-wrapper" :class="{ expanded: isOpen(project.url) }">
<div class="expand-content">
<p>{{ t[project.descKey] }}</p>
<div class="project-links">
<a :href="project.url" target="_blank" rel="noopener noreferrer">
{{ t.portfolioVisitSite }}
</a>
<a
v-if="project.sourceUrl"
:href="project.sourceUrl"
target="_blank"
rel="noopener noreferrer"
>
{{ t.portfolioSourceCode }}
</a>
</div>
</div>
</div>
</div>
</div>
</section>
<section>
<h3>{{ t.portfolioMaintenanceHeading }}</h3>
<div class="project-list">
<div v-for="project in maintenanceProjects" :key="project.url" class="project-card">
<button
class="project-header"
@click="toggle(project.url)"
:aria-expanded="isOpen(project.url)"
>
<h4>{{ t[project.nameKey] }}</h4>
<div class="header-right">
<div class="tag-list">
<span
v-for="tag in project.tags"
:key="tag.label"
class="tag"
:style="{ backgroundColor: tag.color }"
>
{{ tag.label }}
</span>
</div>
<span class="chevron" :class="{ open: isOpen(project.url) }"></span>
</div>
</button>
<div class="expand-wrapper" :class="{ expanded: isOpen(project.url) }">
<div class="expand-content">
<p>{{ t[project.descKey] }}</p>
<div class="project-links">
<a :href="project.url" target="_blank" rel="noopener noreferrer">
{{ t.portfolioVisitSite }}
</a>
<a
v-if="project.sourceUrl"
:href="project.sourceUrl"
target="_blank"
rel="noopener noreferrer"
>
{{ t.portfolioSourceCode }}
</a>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<style scoped>
.portfolio {
display: flex;
flex-direction: column;
gap: 2.5rem;
}
section {
display: flex;
flex-direction: column;
gap: 1rem;
}
h3 {
font-size: 1.2rem;
font-weight: 500;
color: var(--color-heading);
border-bottom: 1px solid var(--color-border);
padding-bottom: 0.4rem;
margin-bottom: 0.25rem;
}
.project-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.project-card {
background-color: var(--color-background-soft);
border: 1px solid var(--color-border);
border-radius: 8px;
overflow: hidden;
}
.project-header {
width: 100%;
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
align-items: center;
gap: 0.35rem 0.75rem;
padding: 0.75rem 1.25rem;
background: none;
border: none;
cursor: pointer;
color: inherit;
text-align: left;
}
.project-header:hover {
background-color: var(--color-background-mute);
}
.header-right {
display: contents;
}
h4 {
font-size: 1rem;
font-weight: 600;
color: var(--color-heading);
margin: 0;
grid-column: 1;
grid-row: 1;
}
.chevron {
font-size: 1.1rem;
color: var(--color-text);
transition: transform 0.25s ease;
line-height: 1;
grid-column: 2;
grid-row: 1;
align-self: center;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
grid-column: 1 / -1;
grid-row: 2;
}
.chevron.open {
transform: rotate(180deg);
}
/* Expand/collapse animation using max-height */
.expand-wrapper {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.28s ease;
}
.expand-wrapper.expanded {
grid-template-rows: 1fr;
}
.expand-content {
overflow: hidden;
padding: 0 1.25rem;
transition: padding 0.28s ease;
}
.expand-wrapper.expanded .expand-content {
padding: 0 1.25rem 0.9rem;
}
p {
color: var(--color-text);
line-height: 1.6;
margin: 0 0 0.6rem;
font-size: 0.95rem;
}
.project-links {
display: flex;
gap: 0.75rem;
}
.project-links a {
font-size: 0.85rem;
}
.tag {
display: inline-block;
padding: 0.2rem 0.55rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
color: #ffffff;
letter-spacing: 0.01em;
}
</style>