Some basic improvements

This commit is contained in:
2026-05-07 23:11:45 +03:00
parent 063fe5a713
commit 75808307a4
18 changed files with 271 additions and 78 deletions

View File

@@ -1,19 +1,58 @@
import { useState, useEffect, useRef } from 'react';
import { useAtomValue } from 'jotai';
import { activeSectionState, themeState } from './state/atoms';
import Navbar from './components/Navbar/Navbar';
import Hero from './components/Hero/Hero';
import About from './components/About/About';
import Projects from './components/Projects/Projects';
import Contact from './components/Contact/Contact';
import Footer from './components/Footer/Footer';
import styles from './App.module.scss';
const SECTIONS = {
home: Hero,
about: About,
projects: Projects,
contact: Contact,
};
const EXIT_DURATION = 200;
export default function App() {
const activeSection = useAtomValue(activeSectionState);
const theme = useAtomValue(themeState);
const [displayedSection, setDisplayedSection] = useState(activeSection);
const [isExiting, setIsExiting] = useState(false);
const timerRef = useRef(null);
useEffect(() => {
if (theme === 'light') {
document.documentElement.dataset.theme = 'light';
} else {
delete document.documentElement.dataset.theme;
}
}, [theme]);
useEffect(() => {
if (activeSection === displayedSection) return;
setIsExiting(true);
timerRef.current = setTimeout(() => {
setDisplayedSection(activeSection);
setIsExiting(false);
}, EXIT_DURATION);
return () => clearTimeout(timerRef.current);
}, [activeSection]);
const Section = SECTIONS[displayedSection] ?? Hero;
return (
<>
<Navbar />
<main>
<Hero />
<About />
<Projects />
<Contact />
<main
key={displayedSection}
className={isExiting ? styles.exit : styles.enter}
>
<Section />
</main>
<Footer />
</>

32
src/App.module.scss Normal file
View File

@@ -0,0 +1,32 @@
@use './styles/variables' as *;
.enter {
animation: sectionEnter 0.25s ease both;
}
.exit {
animation: sectionExit 0.2s ease both;
pointer-events: none;
}
@keyframes sectionEnter {
from {
opacity: 0;
transform: translateY(14px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes sectionExit {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-10px);
}
}

View File

@@ -1,6 +1,6 @@
import styles from './About.module.scss';
const STACK = ['React', 'TypeScript', 'Node.js', 'PostgreSQL', 'Docker', 'Go'];
const STACK = ['Technology 1', 'Technology 2', 'Technology 3', 'Technology 4', 'Technology 5', 'Technology 6'];
export default function About() {
return (
@@ -15,14 +15,13 @@ export default function About() {
<div className={styles.body}>
<div className={styles.text}>
<p>
I'm a developer who cares deeply about the craft. Whether it's
shaving 200ms off a render or designing a data model that still
makes sense six months later the details matter.
A paragraph about yourself and your background. What kind of work
do you do and what drives you? Keep it personal and authentic.
</p>
<p>
When I'm not staring at a terminal, I'm probably hiking somewhere
with bad cell coverage, reading about distributed systems, or
losing at chess online.
A second paragraph with more details hobbies, interests, or
anything else that gives visitors a sense of who you are beyond
your job title.
</p>
<p>
Here are some technologies I've been working with recently:
@@ -39,20 +38,20 @@ export default function About() {
<div className={styles.card}>
<div className={styles.avatar}>
<span className={styles.avatarInner}>VK</span>
<span className={styles.avatarInner}>YN</span>
</div>
<div className={styles.meta}>
<p className={styles.metaLine}>
<span className={styles.metaKey}>location</span>
<span>Helsinki, Finland</span>
<span>City, Country</span>
</p>
<p className={styles.metaLine}>
<span className={styles.metaKey}>status</span>
<span className={styles.green}>Available</span>
</p>
<p className={styles.metaLine}>
<span className={styles.metaKey}>coffee</span>
<span>v.0.0</span>
<span className={styles.metaKey}>focus</span>
<span>Your focus area</span>
</p>
</div>
</div>

View File

@@ -91,7 +91,7 @@
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 30px rgba($color-primary-mid, 0.3);
box-shadow: 0 0 30px rgb(var(--color-primary-mid) / 0.3);
}
.avatarInner {

View File

@@ -12,13 +12,12 @@ export default function Contact() {
<div className={styles.body}>
<p className={styles.lede}>
Whether you have a project in mind, a question, or just want to talk
shop my inbox is open. I'm reasonably fast at responding, unless
it's a Monday.
A short message inviting visitors to reach out. Let them know
you're open to new opportunities, collaborations, or just a chat.
</p>
<a
href="mailto:hello@example.com"
href="mailto:your@email.com"
className={styles.emailBtn}
>
Say hello ↗
@@ -26,9 +25,9 @@ export default function Contact() {
<ul className={styles.socials}>
{[
{ label: 'GitHub', href: 'https://github.com' },
{ label: 'LinkedIn', href: 'https://linkedin.com' },
{ label: 'Twitter / X', href: 'https://x.com' },
{ label: 'GitHub', href: '#' },
{ label: 'LinkedIn', href: '#' },
{ label: 'Platform', href: '#' },
].map(({ label, href }) => (
<li key={label}>
<a href={href} target="_blank" rel="noreferrer">

View File

@@ -65,9 +65,9 @@
letter-spacing: 0.02em;
&:hover {
background: rgba($color-accent, 0.08);
background: rgb(var(--color-accent) / 0.08);
color: $color-accent;
box-shadow: 0 0 20px rgba($color-accent, 0.15);
box-shadow: 0 0 20px rgb(var(--color-accent) / 0.15);
transform: translateY(-2px);
}
}

View File

@@ -5,9 +5,9 @@ export default function Footer() {
<footer className={styles.footer}>
<p>
Designed &amp; built by{' '}
<a href="#home">Veikko</a>{' '}
<a href="#home">Your Name</a>{' '}
<span className={styles.separator}>·</span>{' '}
<span className={styles.stack}>React + Vite + Bun</span>
<span className={styles.stack}>React + Vite</span>
</p>
</footer>
);

View File

@@ -8,15 +8,15 @@ export default function Hero() {
<div className={styles.content}>
<p className={styles.greeting}>Hey, I'm</p>
<h1 className={styles.name}>
Veikko<span className={styles.dot}>.</span>
Your Name<span className={styles.dot}>.</span>
</h1>
<p className={styles.tagline}>
I build things for the web —{' '}
<span className={styles.highlight}>sometimes they even work.</span>
Your tagline goes here —{' '}
<span className={styles.highlight}>make it memorable.</span>
</p>
<p className={styles.sub}>
Full-stack developer with a soft spot for clean architecture,
dark mode interfaces, and coffee that's slightly too strong.
A short bio or description about yourself. What do you do, what do
you care about, and what makes you interesting?
</p>
<div className={styles.cta}>

View File

@@ -14,8 +14,8 @@
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba($color-primary, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba($color-primary, 0.08) 1px, transparent 1px);
linear-gradient(rgb(var(--color-primary) / 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgb(var(--color-primary) / 0.08) 1px, transparent 1px);
background-size: 40px 40px;
mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, black 40%, transparent 100%);
pointer-events: none;
@@ -30,7 +30,7 @@
transform: translateX(-50%);
width: 700px;
height: 500px;
background: radial-gradient(ellipse, rgba($color-primary-mid, 0.18) 0%, transparent 70%);
background: radial-gradient(ellipse, rgb(var(--color-primary-mid) / 0.18) 0%, transparent 70%);
pointer-events: none;
}
@@ -110,7 +110,7 @@
&:hover {
background: $color-primary-glow;
color: #fff;
box-shadow: 0 0 24px rgba($color-primary-glow, 0.4);
box-shadow: 0 0 24px rgb(var(--color-primary-glow) / 0.4);
transform: translateY(-2px);
}
}
@@ -133,7 +133,7 @@
position: absolute;
bottom: $space-8;
right: $space-6;
background: rgba($color-bg-card, 0.9);
background: rgb(var(--color-bg-card) / 0.9);
border: 1px solid rgba(34, 197, 94, 0.35);
border-radius: 100px;
padding: $space-2 $space-4;

View File

@@ -1,36 +1,48 @@
import { useAtom } from 'jotai';
import { navOpenState } from '../../state/atoms';
import { navOpenState, activeSectionState } from '../../state/atoms';
import ThemeToggle from '../ThemeToggle/ThemeToggle';
import styles from './Navbar.module.scss';
const NAV_LINKS = [
{ label: 'Home', href: '#home' },
{ label: 'About', href: '#about' },
{ label: 'Projects', href: '#projects' },
{ label: 'Contact', href: '#contact' },
{ label: 'Home', section: 'home' },
{ label: 'About', section: 'about' },
{ label: 'Projects', section: 'projects' },
{ label: 'Contact', section: 'contact' },
];
export default function Navbar() {
const [isOpen, setIsOpen] = useAtom(navOpenState);
const [activeSection, setActiveSection] = useAtom(activeSectionState);
function handleNav(section) {
setActiveSection(section);
setIsOpen(false);
}
return (
<header className={styles.header}>
<nav className={styles.nav}>
<a href="#home" className={styles.logo}>
<button className={styles.logo} onClick={() => handleNav('home')}>
<span className={styles.logoBracket}>&lt;</span>
vk
<span className={styles.logoBracket}>/&gt;</span>
</a>
</button>
<ul className={`${styles.links} ${isOpen ? styles.open : ''}`}>
{NAV_LINKS.map(({ label, href }) => (
<li key={href}>
<a href={href} onClick={() => setIsOpen(false)}>
{NAV_LINKS.map(({ label, section }) => (
<li key={section}>
<button
onClick={() => handleNav(section)}
className={activeSection === section ? styles.active : undefined}
>
{label}
</a>
</button>
</li>
))}
</ul>
<ThemeToggle />
<button
className={`${styles.burger} ${isOpen ? styles.burgerOpen : ''}`}
onClick={() => setIsOpen((v) => !v)}

View File

@@ -4,7 +4,7 @@
position: fixed;
inset: 0 0 auto 0;
z-index: 100;
background: rgba($color-bg, 0.85);
background: rgb(var(--color-bg) / 0.85);
backdrop-filter: blur(14px);
border-bottom: 1px solid $color-border;
transition: background $transition-base;
@@ -25,6 +25,10 @@
font-weight: 700;
color: $color-text;
letter-spacing: -0.02em;
background: none;
border: none;
cursor: pointer;
padding: 0;
&:hover {
color: $color-text;
@@ -40,13 +44,18 @@
display: flex;
gap: $space-8;
a {
button {
font-family: inherit;
font-size: $text-sm;
font-weight: 500;
color: $color-text-muted;
letter-spacing: 0.04em;
text-transform: uppercase;
position: relative;
background: none;
border: none;
cursor: pointer;
padding: 0;
transition: color $transition-fast;
&::after {
@@ -67,6 +76,14 @@
transform: scaleX(1);
}
}
&.active {
color: $color-text;
&::after {
transform: scaleX(1);
}
}
}
}

View File

@@ -2,23 +2,23 @@ import styles from './Projects.module.scss';
const PROJECTS = [
{
name: 'Lumina',
desc: 'Real-time collaborative whiteboard built with WebSockets and Canvas API. Because sometimes Figma is overkill.',
tags: ['React', 'Node.js', 'WebSockets'],
name: 'Project Alpha',
desc: 'A short description of your featured project. What problem does it solve and what technologies make it interesting?',
tags: ['Tag 1', 'Tag 2', 'Tag 3'],
href: '#',
star: true,
},
{
name: 'Hallberg',
desc: 'Personal finance tracker that actually respects your privacy — no cloud, no telemetry, just SQLite and honesty.',
tags: ['Go', 'SQLite', 'HTMX'],
name: 'Project Beta',
desc: 'A short description of your second project. Highlight the key challenge or the most exciting part of building it.',
tags: ['Tag 1', 'Tag 2', 'Tag 3'],
href: '#',
star: false,
},
{
name: 'Pikkubot',
desc: 'Tiny Telegram bot for generating daily Finnish vocabulary cards. My Finnish is still terrible, but at least it\'s automated.',
tags: ['Python', 'Telegram API'],
name: 'Project Gamma',
desc: 'A short description of your third project. What did you learn, or what are you most proud of about it?',
tags: ['Tag 1', 'Tag 2'],
href: '#',
star: false,
},

View File

@@ -57,12 +57,12 @@
&:hover {
transform: translateY(-4px);
border-color: rgba($color-primary-glow, 0.4);
box-shadow: 0 8px 32px rgba($color-primary, 0.2);
border-color: rgb(var(--color-primary-glow) / 0.4);
box-shadow: 0 8px 32px rgb(var(--color-primary) / 0.2);
}
&.featured {
border-color: rgba($color-accent, 0.25);
border-color: rgb(var(--color-accent) / 0.25);
}
}
@@ -73,8 +73,8 @@
font-size: $text-xs;
font-family: $font-mono;
color: $color-accent-warm;
background: rgba($color-accent-warm, 0.1);
border: 1px solid rgba($color-accent-warm, 0.25);
background: rgb(var(--color-accent-warm) / 0.1);
border: 1px solid rgb(var(--color-accent-warm) / 0.25);
border-radius: 100px;
padding: 2px $space-3;
}
@@ -126,7 +126,7 @@
font-family: $font-mono;
font-size: $text-xs;
color: $color-text-dim;
background: rgba($color-primary, 0.15);
background: rgb(var(--color-primary) / 0.15);
border-radius: $radius-sm;
padding: 2px $space-3;
}

View File

@@ -0,0 +1,42 @@
import { useAtom } from 'jotai';
import { themeState } from '../../state/atoms';
import styles from './ThemeToggle.module.scss';
export default function ThemeToggle() {
const [theme, setTheme] = useAtom(themeState);
const isDark = theme === 'dark';
return (
<button
className={styles.toggle}
onClick={() => setTheme(isDark ? 'light' : 'dark')}
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
{isDark ? <SunIcon /> : <MoonIcon />}
</button>
);
}
function SunIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
);
}
function MoonIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
);
}

View File

@@ -0,0 +1,19 @@
@use '../../styles/variables' as *;
.toggle {
background: none;
border: none;
cursor: pointer;
color: $color-text-muted;
padding: $space-2;
display: flex;
align-items: center;
justify-content: center;
border-radius: $radius-sm;
transition: color $transition-fast, background-color $transition-fast;
&:hover {
color: $color-text;
background-color: $color-bg-elevated;
}
}

View File

@@ -3,3 +3,5 @@ import { atom } from 'jotai';
export const navOpenState = atom(false);
export const activeSectionState = atom('home');
export const themeState = atom('dark');

View File

@@ -1,5 +1,36 @@
@use 'variables' as *;
// ── Theme tokens ───────────────────────────────────────────────
:root {
--color-bg: 10 14 26;
--color-bg-elevated: 17 24 39;
--color-bg-card: 19 29 53;
--color-border: 30 45 77;
--color-primary: 30 58 138;
--color-primary-mid: 37 99 235;
--color-primary-glow: 59 130 246;
--color-accent: 96 165 250;
--color-accent-warm: 245 158 11;
--color-text: 226 232 240;
--color-text-muted: 148 163 184;
--color-text-dim: 71 85 105;
}
[data-theme='light'] {
--color-bg: 248 250 252;
--color-bg-elevated: 241 245 249;
--color-bg-card: 255 255 255;
--color-border: 203 213 225;
--color-primary: 30 58 138;
--color-primary-mid: 37 99 235;
--color-primary-glow: 59 130 246;
--color-accent: 37 99 235;
--color-accent-warm: 217 119 6;
--color-text: 15 23 42;
--color-text-muted: 71 85 105;
--color-text-dim: 148 163 184;
}
// ── Reset & base ───────────────────────────────────────────────
*,
*::before,
@@ -21,6 +52,7 @@ body {
line-height: 1.6;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
transition: background-color $transition-base, color $transition-base;
}
a {

View File

@@ -1,18 +1,18 @@
// ── Palette ────────────────────────────────────────────────────
$color-bg: #0a0e1a;
$color-bg-elevated: #111827;
$color-bg-card: #131d35;
$color-border: #1e2d4d;
$color-bg: rgb(var(--color-bg));
$color-bg-elevated: rgb(var(--color-bg-elevated));
$color-bg-card: rgb(var(--color-bg-card));
$color-border: rgb(var(--color-border));
$color-primary: #1e3a8a; // dark blue
$color-primary-mid: #2563eb;
$color-primary-glow:#3b82f6;
$color-accent: #60a5fa; // light blue for highlights
$color-accent-warm: #f59e0b; // amber a touch of personality
$color-primary: rgb(var(--color-primary));
$color-primary-mid: rgb(var(--color-primary-mid));
$color-primary-glow:rgb(var(--color-primary-glow));
$color-accent: rgb(var(--color-accent));
$color-accent-warm: rgb(var(--color-accent-warm));
$color-text: #e2e8f0;
$color-text-muted: #94a3b8;
$color-text-dim: #475569;
$color-text: rgb(var(--color-text));
$color-text-muted: rgb(var(--color-text-muted));
$color-text-dim: rgb(var(--color-text-dim));
// ── Typography ─────────────────────────────────────────────────
$font-sans: 'Inter', 'Segoe UI', system-ui, sans-serif;