Compare commits
2 Commits
7d378cb2d5
...
666a53592d
| Author | SHA1 | Date | |
|---|---|---|---|
| 666a53592d | |||
| ae10c8f049 |
@@ -1,3 +1,4 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
import styles from '../styles/components/Hero.module.scss';
|
import styles from '../styles/components/Hero.module.scss';
|
||||||
|
|
||||||
export default function Hero() {
|
export default function Hero() {
|
||||||
@@ -20,12 +21,12 @@ export default function Hero() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className={styles.cta}>
|
<div className={styles.cta}>
|
||||||
<a href="/projects" className={styles.btnPrimary}>
|
<Link to="/projects" className={styles.btnPrimary}>
|
||||||
See my work
|
See my work
|
||||||
</a>
|
</Link>
|
||||||
<a href="/contact" className={styles.btnGhost}>
|
<Link to="/contact" className={styles.btnGhost}>
|
||||||
Get in touch
|
Get in touch
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,28 +2,41 @@ import styles from '../styles/components/Projects.module.scss';
|
|||||||
|
|
||||||
const PROJECTS = [
|
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.',
|
desc: 'Web interface to view and manage data held by Livonsaaren Tietokonepaja. Easy to customize serving various customer solutions.',
|
||||||
tags: ['.NET', 'React', 'SQLite'],
|
tags: ['.NET', 'React', 'SQLite'],
|
||||||
href: 'https://klapi.tietokonepaja.fi/',
|
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.',
|
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'],
|
tags: ['Apache2', 'Ubuntu Server', 'NextCloud'],
|
||||||
href: 'https://hattara.tietokonepaja.fi/',
|
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!",
|
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'],
|
tags: ['React', 'Vite', 'Apache2', 'Ubuntu Server'],
|
||||||
href: 'https://www.runosaari.fi/',
|
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() {
|
export default function Projects() {
|
||||||
return (
|
return (
|
||||||
<section id="projects" className={styles.projects}>
|
<section id="projects" className={styles.projects}>
|
||||||
@@ -36,19 +49,25 @@ export default function Projects() {
|
|||||||
|
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
{PROJECTS.map((p) => (
|
{PROJECTS.map((p) => (
|
||||||
<article key={p.name} className={`${styles.card} ${p.star ? styles.featured : ''}`}>
|
<article key={p.name} className={styles.card}>
|
||||||
{p.star && <span className={styles.featuredBadge}>Featured</span>}
|
|
||||||
<div className={styles.cardTop}>
|
<div className={styles.cardTop}>
|
||||||
<span className={styles.folderIcon}>⬡</span>
|
<span className={styles.folderIcon}>⬡</span>
|
||||||
|
<div className={styles.cardLinks}>
|
||||||
<a href={p.href} className={styles.externalLink} aria-label={`Open ${p.name}`}>
|
<a href={p.href} className={styles.externalLink} aria-label={`Open ${p.name}`}>
|
||||||
↗
|
↗
|
||||||
</a>
|
</a>
|
||||||
|
<a href={p.sourceHref} className={styles.sourceLink} aria-label={`View source code for ${p.name}`}>
|
||||||
|
📦
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className={styles.cardName}>{p.name}</h3>
|
<h3 className={styles.cardName}>{p.name}</h3>
|
||||||
<p className={styles.cardDesc}>{p.desc}</p>
|
<p className={styles.cardDesc}>{p.desc}</p>
|
||||||
<ul className={styles.tags}>
|
<ul className={styles.tags}>
|
||||||
{p.tags.map((t) => (
|
{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>
|
</ul>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -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 {
|
.featuredBadge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: $space-4;
|
top: $space-4;
|
||||||
@@ -79,13 +102,6 @@
|
|||||||
padding: 2px $space-3;
|
padding: 2px $space-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardTop {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: $space-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folderIcon {
|
.folderIcon {
|
||||||
font-size: $text-2xl;
|
font-size: $text-2xl;
|
||||||
color: $color-accent;
|
color: $color-accent;
|
||||||
@@ -121,13 +137,58 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: $space-2;
|
gap: $space-2;
|
||||||
margin-top: $space-2;
|
margin-top: $space-2;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
li {
|
.tagItem {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagPill {
|
||||||
font-family: $font-mono;
|
font-family: $font-mono;
|
||||||
font-size: $text-xs;
|
font-size: $text-xs;
|
||||||
color: $color-text-dim;
|
|
||||||
background: rgb(var(--color-primary) / 0.15);
|
|
||||||
border-radius: $radius-sm;
|
border-radius: $radius-sm;
|
||||||
padding: 2px $space-3;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user