Compare commits

..

2 Commits

Author SHA1 Message Date
666a53592d Fix landing page links 2026-08-07 21:39:46 +03:00
ae10c8f049 Project page touch up 2026-08-07 20:06:34 +03:00
3 changed files with 113 additions and 32 deletions

View File

@@ -1,3 +1,4 @@
import { Link } from 'react-router-dom';
import styles from '../styles/components/Hero.module.scss';
export default function Hero() {
@@ -20,12 +21,12 @@ export default function Hero() {
</p>
<div className={styles.cta}>
<a href="/projects" className={styles.btnPrimary}>
<Link to="/projects" className={styles.btnPrimary}>
See my work
</a>
<a href="/contact" className={styles.btnGhost}>
</Link>
<Link to="/contact" className={styles.btnGhost}>
Get in touch
</a>
</Link>
</div>
</div>

View File

@@ -2,28 +2,41 @@ import styles from '../styles/components/Projects.module.scss';
const PROJECTS = [
{
name: 'Klapi - Unified Control Panel',
name: 'Klapi - Unified content management panel',
desc: 'Web interface to view and manage data held by Livonsaaren Tietokonepaja. Easy to customize serving various customer solutions.',
tags: ['.NET', 'React', 'SQLite'],
href: 'https://klapi.tietokonepaja.fi/',
star: false,
sourceHref: 'https://github.com',
},
{
name: 'Hattara - Online office',
name: 'Hattara - Shared online office',
desc: 'Why to code your own when there is working open source solution to host on your own server? Hattara is a self-hosted cloud office solution for small teams and individuals.',
tags: ['Apache2', 'Ubuntu Server', 'NextCloud'],
href: 'https://hattara.tietokonepaja.fi/',
star: false,
sourceHref: 'https://github.com',
},
{
name: 'Runosaari festival website - Pro bono',
name: 'Runosaari festival website - Pro bono project',
desc: "Supporting local culture initiatives have always been close to my heart. I'm proudly helping Runosaari festival with their website and online presence already for the sixth year!",
tags: ['React', 'Vite', 'Apache2', 'Ubuntu Server'],
href: 'https://www.runosaari.fi/',
star: false,
sourceHref: 'https://github.com',
},
];
const getTagColor = (tag) => {
const colorMap = {
'.NET': 'tagDotnet',
React: 'tagReact',
SQLite: 'tagSqlite',
Apache2: 'tagApache',
'Ubuntu Server': 'tagUbuntu',
NextCloud: 'tagNextcloud',
Vite: 'tagVite',
};
return colorMap[tag] || 'tagDefault';
};
export default function Projects() {
return (
<section id="projects" className={styles.projects}>
@@ -36,19 +49,25 @@ export default function Projects() {
<div className={styles.grid}>
{PROJECTS.map((p) => (
<article key={p.name} className={`${styles.card} ${p.star ? styles.featured : ''}`}>
{p.star && <span className={styles.featuredBadge}>Featured</span>}
<article key={p.name} className={styles.card}>
<div className={styles.cardTop}>
<span className={styles.folderIcon}>⬡</span>
<a href={p.href} className={styles.externalLink} aria-label={`Open ${p.name}`}>
↗
</a>
<div className={styles.cardLinks}>
<a href={p.href} className={styles.externalLink} aria-label={`Open ${p.name}`}>
↗
</a>
<a href={p.sourceHref} className={styles.sourceLink} aria-label={`View source code for ${p.name}`}>
📦
</a>
</div>
</div>
<h3 className={styles.cardName}>{p.name}</h3>
<p className={styles.cardDesc}>{p.desc}</p>
<ul className={styles.tags}>
{p.tags.map((t) => (
<li key={t}>{t}</li>
<li key={t} className={styles.tagItem}>
<span className={`${styles.tagPill} ${styles[getTagColor(t)]}`}>{t}</span>
</li>
))}
</ul>
</article>

View File

@@ -66,6 +66,29 @@
}
}
.cardTop {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $space-2;
}
.cardLinks {
display: flex;
gap: $space-2;
}
.sourceLink {
font-size: $text-xl;
color: $color-text-muted;
transition: color $transition-fast, transform $transition-fast;
&:hover {
color: $color-accent;
transform: translate(2px, -2px);
}
}
.featuredBadge {
position: absolute;
top: $space-4;
@@ -79,13 +102,6 @@
padding: 2px $space-3;
}
.cardTop {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $space-2;
}
.folderIcon {
font-size: $text-2xl;
color: $color-accent;
@@ -121,13 +137,58 @@
flex-wrap: wrap;
gap: $space-2;
margin-top: $space-2;
li {
font-family: $font-mono;
font-size: $text-xs;
color: $color-text-dim;
background: rgb(var(--color-primary) / 0.15);
border-radius: $radius-sm;
padding: 2px $space-3;
}
padding: 0;
}
.tagItem {
list-style: none;
}
.tagPill {
font-family: $font-mono;
font-size: $text-xs;
border-radius: $radius-sm;
padding: 2px $space-3;
display: inline-block;
}
// Tag color variants
.tagReact {
background: rgb(97, 218, 251);
color: rgb(0, 0, 0);
}
.tagDotnet {
background: rgb(102, 51, 153);
color: rgb(255, 255, 255);
}
.tagSqlite {
background: rgb(70, 130, 180);
color: rgb(255, 255, 255);
}
.tagApache {
background: rgb(204, 102, 51);
color: rgb(255, 255, 255);
}
.tagUbuntu {
background: rgb(220, 100, 40);
color: rgb(255, 255, 255);
}
.tagNextcloud {
background: rgb(0, 150, 200);
color: rgb(255, 255, 255);
}
.tagVite {
background: rgb(150, 50, 200);
color: rgb(255, 255, 255);
}
.tagDefault {
background: rgb(var(--color-primary) / 0.15);
color: $color-text-dim;
}