diff --git a/src/components/About.jsx b/src/components/About.jsx index fa2c045..86488b1 100644 --- a/src/components/About.jsx +++ b/src/components/About.jsx @@ -1,35 +1,35 @@ +import { useAtomValue } from 'jotai'; +import { languageState } from '../state/atoms'; +import { getTranslations } from '../translations'; import styles from '../styles/components/About.module.scss'; -const STACK = ['Azure', '.NET', 'C#', 'JavaScript', 'React', 'Node.js', 'SQL']; - export default function About() { + const language = useAtomValue(languageState); + const t = getTranslations(language); + return (
- 01. -

About me

+ {t.about.sectionNum} +

{t.about.sectionTitle}

- I'm a curious software engineer with a passion for building scalable and efficient - applications. With deep knowledge in cloud, backend, and frontend development, I enjoy - working on projects that challenge me to learn new technologies and improve my skills. + {t.about.paragraph1}

- I tend to build always something that is useful to others, and I find joy in creating tools - and applications that solve real-world problems. I believe that technology has the power to - make a positive impact on people's lives, and I strive to contribute to that impact through my work. + {t.about.paragraph2}

- Here are some technologies I've been working with recently: + {t.about.paragraph3}

    - {STACK.map((item) => ( + {t.about.stack.map((item) => (
  • ▸ {item}
  • @@ -43,15 +43,15 @@ export default function About() {

- location - Turku Archipelago, Finland + {t.about.meta.location} + {t.about.meta.locationValue}

- favorite book - The Lord of the Rings + {t.about.meta.favoriteBook} + {t.about.meta.favoriteBookValue}

- favorite songs + {t.about.meta.favoriteSongs}
Rücksturz – Agitation Free Year of Love – Jenny Hval diff --git a/src/components/Contact.jsx b/src/components/Contact.jsx index 76599c9..a6b9ba3 100644 --- a/src/components/Contact.jsx +++ b/src/components/Contact.jsx @@ -1,35 +1,42 @@ +import { useAtomValue } from 'jotai'; +import { languageState } from '../state/atoms'; +import { getTranslations } from '../translations'; import styles from '../styles/components/Contact.module.scss'; export default function Contact() { + const language = useAtomValue(languageState); + const t = getTranslations(language); + + const SOCIALS = [ + { label: t.contact.socials.gitea, href: 'https://gitea.tietokonepaja.fi/' }, + { label: t.contact.socials.linkedin, href: 'https://www.linkedin.com/in/veikko-lintuj%C3%A4rvi-5888a6175/' }, + { label: t.contact.socials.company, href: 'https://tietokonepaja.fi/' }, + ]; + return (
- 03. -

Get in touch

+ {t.contact.sectionNum} +

{t.contact.sectionTitle}

- I'm currently employed, but my inbox is always open. Whether you have a question or - just want to say hi, I'll try my best to get back to you! + {t.contact.lede}

-

I also have a small company if you are interested in collaboration or services.

+

- Say hello ↗ + {t.contact.emailBtn} ↗

    - {[ - { label: 'Gitea', href: 'https://gitea.tietokonepaja.fi/' }, - { label: 'LinkedIn', href: 'https://www.linkedin.com/in/veikko-lintuj%C3%A4rvi-5888a6175/' }, - { label: 'Company', href: 'https://www.tietokonepaja.fi/' }, - ].map(({ label, href }) => ( + {SOCIALS.map(({ label, href }) => (
  • {label} diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx index 45d7fc6..0f57807 100644 --- a/src/components/Footer.jsx +++ b/src/components/Footer.jsx @@ -1,13 +1,19 @@ +import { useAtomValue } from 'jotai'; +import { languageState } from '../state/atoms'; +import { getTranslations } from '../translations'; import styles from '../styles/components/Footer.module.scss'; export default function Footer() { + const language = useAtomValue(languageState); + const t = getTranslations(language); + return ( ); diff --git a/src/components/Hero.jsx b/src/components/Hero.jsx index eceb72c..3239903 100644 --- a/src/components/Hero.jsx +++ b/src/components/Hero.jsx @@ -1,38 +1,38 @@ import { Link } from 'react-router-dom'; +import { useAtomValue } from 'jotai'; +import { languageState } from '../state/atoms'; +import { getTranslations } from '../translations'; import styles from '../styles/components/Hero.module.scss'; export default function Hero() { + const language = useAtomValue(languageState); + const t = getTranslations(language); + return (
    ); } diff --git a/src/components/LanguagePicker.jsx b/src/components/LanguagePicker.jsx new file mode 100644 index 0000000..5ef8c24 --- /dev/null +++ b/src/components/LanguagePicker.jsx @@ -0,0 +1,23 @@ +import { useAtom } from 'jotai'; +import { languageState } from '../state/atoms'; +import { getTranslations } from '../translations'; +import styles from '../styles/components/LanguagePicker.module.scss'; + +export default function LanguagePicker() { + const [language, setLanguage] = useAtom(languageState); + const t = getTranslations(language); + + const toggleLanguage = () => { + setLanguage(language === 'en' ? 'fi' : 'en'); + }; + + return ( + + ); +} diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 418e4c6..df66367 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,19 +1,23 @@ import { Link } from 'react-router-dom'; import { useAtom, useAtomValue } from 'jotai'; -import { navOpenState, activeSectionState } from '../state/atoms'; +import { navOpenState, activeSectionState, languageState } from '../state/atoms'; +import { getTranslations } from '../translations'; import ThemeToggle from './ThemeToggle'; +import LanguagePicker from './LanguagePicker'; import styles from '../styles/components/Navbar.module.scss'; -const NAV_LINKS = [ - { 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 = useAtomValue(activeSectionState); + const language = useAtomValue(languageState); + const t = getTranslations(language); + + const NAV_LINKS = [ + { label: t.nav.home, section: 'home' }, + { label: t.nav.about, section: 'about' }, + { label: t.nav.projects, section: 'projects' }, + { label: t.nav.contact, section: 'contact' }, + ]; return (
    @@ -39,12 +43,13 @@ export default function Navbar() {
+ diff --git a/src/main.jsx b/src/main.jsx index 2c5ba73..0f92ad8 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,13 +1,33 @@ -import { StrictMode } from 'react'; +import { StrictMode, useEffect } from 'react'; import { createRoot } from 'react-dom/client'; +import { Provider } from 'jotai'; import { BrowserRouter as Router } from 'react-router-dom'; +import { useAtomValue } from 'jotai'; +import { languageState } from './state/atoms'; import './styles/main.scss'; import App from './App.jsx'; -createRoot(document.getElementById('root')).render( - - - - - , -); +export function LanguageSync() { + const language = useAtomValue(languageState); + + useEffect(() => { + document.documentElement.lang = language; + }, [language]); + + return null; +} + +export function Root() { + return ( + + + + + + + + + ); +} + +createRoot(document.getElementById('root')).render(); diff --git a/src/state/atoms.js b/src/state/atoms.js index 21f96d1..1c527ab 100644 --- a/src/state/atoms.js +++ b/src/state/atoms.js @@ -5,3 +5,5 @@ export const navOpenState = atom(false); export const activeSectionState = atom('home'); export const themeState = atom('dark'); + +export const languageState = atom('en'); diff --git a/src/styles/components/Hero.module.scss b/src/styles/components/Hero.module.scss index 8549565..6d19e02 100644 --- a/src/styles/components/Hero.module.scss +++ b/src/styles/components/Hero.module.scss @@ -127,39 +127,3 @@ transform: translateY(-2px); } } - -// ── "Open to work" badge ─────────────────────────────────────── -.badge { - position: absolute; - bottom: $space-8; - right: $space-6; - 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; - font-size: $text-xs; - font-family: $font-mono; - letter-spacing: 0.06em; - color: #4ade80; - display: flex; - align-items: center; - gap: $space-2; - - &::before { - content: ''; - width: 7px; - height: 7px; - border-radius: 50%; - background: #4ade80; - animation: pulse 2s ease infinite; - } -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.3; } -} - -@media (max-width: 640px) { - .badge { display: none; } -} diff --git a/src/styles/components/LanguagePicker.module.scss b/src/styles/components/LanguagePicker.module.scss new file mode 100644 index 0000000..1d5530f --- /dev/null +++ b/src/styles/components/LanguagePicker.module.scss @@ -0,0 +1,27 @@ +@use '../variables' as *; + +.picker { + background: transparent; + border: 1px solid var(--text-secondary); + border-radius: 4px; + padding: 0.4rem 0.8rem; + color: var(--text-primary); + cursor: pointer; + font-size: 0.85rem; + font-weight: 500; + transition: all 0.2s ease; + + &:hover { + border-color: var(--accent); + color: var(--accent); + } + + @media (max-width: 768px) { + padding: 0.35rem 0.6rem; + font-size: 0.8rem; + } +} + +.label { + display: inline-block; +} diff --git a/src/styles/components/Projects.module.scss b/src/styles/components/Projects.module.scss index 3444aee..0484ec3 100644 --- a/src/styles/components/Projects.module.scss +++ b/src/styles/components/Projects.module.scss @@ -89,19 +89,6 @@ } } -.featuredBadge { - position: absolute; - top: $space-4; - right: $space-4; - font-size: $text-xs; - font-family: $font-mono; - color: $color-accent-warm; - 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; -} - .folderIcon { font-size: $text-2xl; color: $color-accent; diff --git a/src/translations.js b/src/translations.js new file mode 100644 index 0000000..7aeaec1 --- /dev/null +++ b/src/translations.js @@ -0,0 +1,196 @@ +export const translations = { + en: { + // Navbar + nav: { + home: 'Home', + about: 'About', + projects: 'Projects', + contact: 'Contact', + }, + // Hero + hero: { + greeting: 'Hey, I\'m', + name: 'Veikko Lintujärvi', + tagline: 'Making world a better place — one line at a time.', + sub: 'I\'m a curious software engineer from Turku Archipelago, Finland. Right now I\'m excited about cloud, Linux and AI.', + ctaPrimary: 'See my work', + ctaSecondary: 'Get in touch', + }, + // About + about: { + sectionNum: '01.', + sectionTitle: 'About me', + paragraph1: 'I\'m a curious software engineer with a passion for building scalable and efficient applications. With deep knowledge in cloud, backend, and frontend development, I enjoy working on projects that challenge me to learn new technologies and improve my skills.', + paragraph2: 'I tend to build always something that is useful to others, and I find joy in creating tools and applications that solve real-world problems. I believe that technology has the power to make a positive impact on people\'s lives, and I strive to contribute to that impact through my work.', + paragraph3: 'Here are some technologies I\'ve been working with recently:', + stack: ['Azure', '.NET', 'C#', 'JavaScript', 'React', 'Node.js', 'SQL'], + meta: { + location: 'location', + locationValue: 'Turku Archipelago, Finland', + favoriteBook: 'favorite book', + favoriteBookValue: 'The Name of the Rose', + favoriteSongs: 'favorite songs', + }, + }, + // Projects + projects: { + sectionNum: '02.', + sectionTitle: 'Projects', + projects: [ + { + 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'], + }, + { + 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'], + }, + { + 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'], + }, + ], + giteaText: 'Check out here the rest of my code', + giteaBtn: 'View on Gitea', + folderIcon: '⬡', + externalLinkAria: 'Open', + sourceLinkAria: 'View source code for', + }, + // Contact + contact: { + sectionNum: '03.', + sectionTitle: 'Get in touch', + lede: "I'm currently employed, but my inbox is always open. Whether you have a question or just want to say hi, I'll try my best to get back to you!", + paragraph: 'I also have a small company if you are interested in collaboration or services.', + emailBtn: 'Say hello', + socials: { + gitea: 'Gitea', + linkedin: 'LinkedIn', + company: 'Company', + }, + }, + // Footer + footer: { + designedBy: 'Designed & built by', + separator: '·', + stack: 'React + Vite', + }, + // NotFound + notFound: { + title: 'Page Not Found', + message: "I didn't find what you were looking for :(", + }, + // ThemeToggle + theme: { + switchToLight: 'Switch to light mode', + switchToDark: 'Switch to dark mode', + }, + // Language names + languages: { + en: 'English', + fi: 'Suomi', + }, + }, + fi: { + // Navbar + nav: { + home: 'Etusivu', + about: 'Minä', + projects: 'Projektit', + contact: 'Yhteystiedot', + }, + // Hero + hero: { + greeting: 'Hei, olen', + name: 'Veikko Lintujärvi', + tagline: 'Tehdään maailmasta parempi paikka kaikille — koodirivi kerrallaan.', + sub: 'Olen utelias insinööri Turun saaristosta. Tällä hetkellä inspiroidun pilvipalveluista, Linuxista ja tekoälystä.', + ctaPrimary: 'Töitäni', + ctaSecondary: 'Ota yhteyttä', + }, + // About + about: { + sectionNum: '01.', + sectionTitle: 'Minä', + paragraph1: 'Intohimonani on rakentaa tehokasta ja skaalautuvaa internettiä. Nautin haastavista projekteista, joissa pääsen syventämään osaamistani pilvi-, fullstack- ja tekoälykehityksessä. Haluan jatkuvasti oppia uusia teknologioita ja sovelluskehityksen toimintatapoja tekemiseni ohessa.', + paragraph2: 'Pyrin aina rakentamaan jotain, mistä on hyötyä muille. Innostun kun saan rakentaa työkaluja, jotka ratkovat aitoja ongelmia. Uskon, että tekniikalla on kyky vaikuttaa positiivisesti ihmisiin ja yhteiskuntaan.', + paragraph3: 'Tässä muutamia teknologioita, joiden parissa olen viime aikoina työskennellyt:', + stack: ['Azure', '.NET', 'C#', 'Mistral AI', 'JavaScript', 'React', 'Node.js', 'SQL'], + meta: { + location: 'sijainti', + locationValue: 'Turun saaristo, Suomi', + favoriteBook: 'lempi kirja', + favoriteBookValue: 'Ruusun nimi', + favoriteSongs: 'lempi biisit', + }, + }, + // Projects + projects: { + sectionNum: '02.', + sectionTitle: 'Projektit', + projects: [ + { + name: 'Klapi - Yhtenäinen sisällönhallintapaneeli', + desc: 'Verkkokäyttöliittymä Livonsaaren Tietokonepajan datan esittämiseen ja hallintaan. Helppo mukauttaa eri asiakasratkaisuihin.', + tags: ['.NET', 'React', 'SQLite'], + }, + { + name: 'Hattara - Jaettu pilvitoimisto', + desc: 'Miksi koodata omaa, kun on olemassa toimiva avoimen lähdekoodin ratkaisu, jonka voi hostata omalla palvelimella? Hattara on itsenäinen pilvitoimistoratkaisu pienille tiimeille ja yksityishenkilöille.', + tags: ['Apache2', 'Ubuntu Server', 'NextCloud'], + }, + { + name: 'Runosaaren festivaalin verkkosivut - Pro bono -projekti', + desc: 'Paikallisten kulttuurihankkeiden tukeminen on aina ollut minulle tärkeää. Olen ollut mukana auttamassa Runosaaren festivaalia heidän verkkosivuissaan ja verkkonäkyvyydessään jo kuudennetta vuotta!', + tags: ['React', 'Vite', 'Apache2', 'Ubuntu Server'], + }, + ], + giteaText: 'Tutustu täältä muihin projekteihini', + giteaBtn: 'Katso Giteassa', + folderIcon: '⬡', + externalLinkAria: 'Avaa', + sourceLinkAria: 'Näytä lähdekoodi', + }, + // Contact + contact: { + sectionNum: '03.', + sectionTitle: 'Ota yhteyttä', + lede: 'Olen tällä hetkellä työsuhteessa, mutta luen mielenkiinnolla kaikki yhteydenotot. Olipa sinulla kysymys, ehdotus tai tervehdys, yritän parhaani mukaan vastata!', + paragraph: 'Minulla on myös yritys: Livonsaaren Tietokonepaja. Jos olet kiinnostunut yhteistyöstä tai palveluistamme, ota rohkeasti yhteyttä.', + emailBtn: 'Laita mailia', + socials: { + gitea: 'Gitea', + linkedin: 'LinkedIn', + company: 'Yritykseni', + }, + }, + // Footer + footer: { + designedBy: 'Suunnitellut & toteuttanut', + separator: '·', + stack: 'React + Vite', + }, + // NotFound + notFound: { + title: 'Sivua ei löydy', + message: 'En löytänyt etsimääsi sivua :(', + }, + // ThemeToggle + theme: { + switchToLight: 'Vaihda vaaleaan teemaan', + switchToDark: 'Vaihda tummaan teemaan', + }, + // Language names + languages: { + en: 'English', + fi: 'Suomi', + }, + }, +}; + +export const getTranslations = (lang) => { + return translations[lang] || translations.en; +};