Rewrite in bun and vite away from NextJs

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-29 22:07:58 +03:00
parent af7f23b9b5
commit 0d60dbbb5f
119 changed files with 714 additions and 302 deletions

34
src/App.tsx Normal file
View File

@@ -0,0 +1,34 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Footer from './components/Footer';
import Logo from './components/Logo';
import Index from './pages/Index';
import Performers from './pages/Performers';
import PerformerPage from './pages/PerformerPage';
import Program from './pages/Program';
import Info from './pages/Info';
import Safety from './pages/Safety';
import Workshops from './pages/Workshops';
import Archive from './pages/Archive';
function App() {
return (
<BrowserRouter>
<Logo />
<main>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/performers" element={<Performers />} />
<Route path="/performers/:id" element={<PerformerPage />} />
<Route path="/program" element={<Program />} />
<Route path="/info" element={<Info />} />
<Route path="/safety" element={<Safety />} />
<Route path="/workshops" element={<Workshops />} />
<Route path="/archive" element={<Archive />} />
</Routes>
</main>
<Footer />
</BrowserRouter>
);
}
export default App;

View File

@@ -0,0 +1,28 @@
import React from 'react';
import styles from '../../styles/Collaboration.module.scss';
const Collaboration = () => {
return (
<>
<h1>Yhteistyössä</h1>
<div className={styles.collabContainer}>
<span>Runoviikko Ry</span>
<div className={styles.collabLogoRow}>
<img
className={styles.collabLogo}
src='/collaborators/kirjan_talo.png'
alt='kirjan talo logo'
/>
</div>
<span>Velkuan saaristolaisyhdistys ry</span>
<span>Cafe Laituri</span>
<span>Saaristohotelli Vaihela</span>
<span>Aviador Kustannus</span>
<span>Enostone Kustannus</span>
<span>Keski-Suomen kirjailijat</span>
</div>
</>
);
};
export default Collaboration;

42
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,42 @@
import { Link } from 'react-router-dom';
import React from 'react';
import styles from '../../styles/Footer.module.scss';
const Footer = () => {
return (
<footer className={styles.footer}>
<div className={styles.container}>
<div className={styles.left}>
Livonsaari & Palva & Velkuanmaa
<br />
Naantalin saaristo
<br />
</div>
<div className={styles.middle}>
<Link to='/#nav-bar'>
<img
className={styles.logo}
src='/small-logo.png'
alt='small logo'
/>
</Link>
<div>
<a
className={styles.sourceLink}
href='https://github.com/codevictory/runosaari.net'
>
lähdekoodi
</a>
{' '}by codevictory
</div>
</div>
<div className={styles.right}>
Katariina Vuorinen <br />
<a href='mailto:runosaari@gmail.com'>runosaari@gmail.com</a>
</div>
</div>
</footer>
);
};
export default Footer;

17
src/components/Lead.tsx Normal file
View File

@@ -0,0 +1,17 @@
import React from 'react';
import styles from '../../styles/Lead.module.scss';
const Lead = () => {
return (
<div className={styles.leadContainer}>
<h2 className={styles.leadTitle}>
Runofestivaali saariston sylissä!
<span className={styles.dateAndPlace}>
11. - 13.6.2026
</span>
</h2>
</div>
);
};
export default Lead;

31
src/components/Logo.tsx Normal file
View File

@@ -0,0 +1,31 @@
import React from 'react';
import { Link } from 'react-router-dom';
import styles from '../../styles/Logo.module.scss';
const Logo = () => {
return (
<Link to="/">
<section className={styles.logoContainer}>
<div className={styles.logo} id='logo-start'>
<picture>
<source
srcSet='/runosaari-logo_small.jpg'
media='(max-width: 600px)'
/>
<source srcSet='/runosaari-logo.jpg' />
<img
src='/runosaari-logo.jpg'
alt='Runosaari logo'
className={styles.logoImage}
/>
</picture>
<div className={styles.logoCredits}>
<div>@Sanna Hukkanen</div>
</div>
</div>
</section>
</Link>
);
};
export default Logo;

22
src/components/NavBar.tsx Normal file
View File

@@ -0,0 +1,22 @@
import { Link } from 'react-router-dom';
import styles from '../../styles/NavBar.module.scss';
const NavBar = () => {
return (
<>
<div className={styles.navBarContainer}>
<nav className={styles.navBar}>
<Link to='/' className={styles.navBarLink}>Etusivu</Link>
<Link to='/program' className={styles.navBarLink}>Ohjelma</Link>
<Link to='/performers' className={styles.navBarLink}>Esiintyjät</Link>
{/* <Link to='/workshops' className={styles.navBarLink}>Työpajat</Link> */}
<Link to='/info' className={styles.navBarLink}>Info</Link>
<Link to='/safety' className={styles.navBarLink}>Turvallisuus</Link>
<Link to='/archive' className={styles.navBarLink}>Arkisto</Link>
</nav>
</div>
</>
);
};
export default NavBar;

11
src/main.tsx Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import '../styles/globals.scss';
import '../styles/transitions.scss';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

102
src/pages/Archive.tsx Normal file
View File

@@ -0,0 +1,102 @@
import React, { useEffect, useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import shared from '../../styles/Shared.module.scss';
import styles from '../../styles/Archive.module.scss';
import PerformersData2021 from '../../data/performers/2021';
import PerformersData2022 from '../../data/performers/2022';
import PerformersData2023 from '../../data/performers/2023';
import PerformersData2024 from '../../data/performers/2024';
import PerformersData2025 from '../../data/performers/2025';
import Performer from '../../types/Performer';
import { BiChevronDown, BiChevronLeft } from 'react-icons/bi';
interface PerformerCard extends Performer {
showDesc: boolean;
}
const dataByYear: Record<number, Performer[]> = {
2025: PerformersData2025,
2024: PerformersData2024,
2023: PerformersData2023,
2022: PerformersData2022,
2021: PerformersData2021,
};
const sortedYears = Object.keys(dataByYear).map(Number).sort((a, b) => b - a);
const Archive = () => {
const [performersByYear, setPerformersByYear] = useState<Record<number, PerformerCard[]>>({});
useEffect(() => {
const initial: Record<number, PerformerCard[]> = {};
for (const year of sortedYears) {
initial[year] = dataByYear[year].map((p) => ({ ...p, showDesc: false }));
}
setPerformersByYear(initial);
}, []);
const togglePerformerDesc = (year: number, id: string) => {
setPerformersByYear((prev) => ({
...prev,
[year]: prev[year].map((p) =>
p.id === id ? { ...p, showDesc: !p.showDesc } : p
),
}));
};
return (
<section className={shared.page + ' ' + styles.archivePage}>
<h1>Arkisto</h1>
{sortedYears.map((year) => (
<React.Fragment key={year}>
<h2 className={styles.year}>{year}</h2>
{(performersByYear[year] ?? []).map((p) => (
<div className={styles.performerContainer} key={p.id}>
<img
className={styles.performerImage}
src={'/performers/' + year + '/' + p.id + '.jpg'}
width={100}
height={100}
loading="lazy"
alt={p.name}
/>
<div className={styles.performerTextContainer}>
<div
className={styles.performerTitle}
onClick={() => togglePerformerDesc(year, p.id)}
>
<h2>{p.name}</h2>
<button className={shared.openingChevron}>
{p.showDesc ? (
<BiChevronDown size='3rem' />
) : (
<BiChevronLeft size='3rem' />
)}
</button>
</div>
<CSSTransition
in={p.showDesc}
timeout={1000}
classNames='fadeTransition'
>
{p.showDesc ? (
<div>
{p.paragraphs.map((parag, index) => (
<p key={index}>{parag.toString()}</p>
))}
</div>
) : (
<span></span>
)}
</CSSTransition>
<hr />
</div>
</div>
))}
</React.Fragment>
))}
</section>
);
};
export default Archive;

21
src/pages/Index.tsx Normal file
View File

@@ -0,0 +1,21 @@
import Performers from './Performers';
import Program from './Program';
import styles from '../../styles/Index.module.scss';
import Lead from '../components/Lead';
const Index = () => {
return (
<div className={styles.indexContainer}>
<h1>Runosaari</h1>
<Lead />
<Program />
<Performers />
{/* <Workshops /> */}
{/* <Info /> */}
{/* <Collaboration /> */}
{/* <Archive /> */}
</div>
);
};
export default Index;

205
src/pages/Info.tsx Normal file
View File

@@ -0,0 +1,205 @@
import React, { useState } from 'react';
import shared from '../../styles/Shared.module.scss';
import styles from '../../styles/Info.module.scss';
import { BiChevronDown, BiChevronLeft } from 'react-icons/bi';
import { CSSTransition } from 'react-transition-group';
interface InfoToggles {
shop: boolean;
bus: boolean;
accom: boolean;
ferry: boolean;
fb: boolean;
}
const Info = () => {
const [infoToggles, setInfoToggles] = useState<InfoToggles>({
shop: false,
bus: false,
accom: false,
ferry: false,
fb: false,
});
const toggleInfo = (info: keyof InfoToggles) => {
let updated = { ...infoToggles };
updated[info] = !updated[info];
setInfoToggles(updated);
};
return (
<section className={shared.page}>
<h1>Info</h1>
<div className={styles.infoContainer}>
<div className={styles.infoTitle} onClick={() => toggleInfo('shop')}>
<h2>Livonsaaren Osuuskauppa</h2>
<button className={shared.openingChevron}>
{infoToggles.shop ? (
<BiChevronDown size='3rem' />
) : (
<BiChevronLeft size='3rem' />
)}
</button>
</div>
<CSSTransition
in={infoToggles.shop}
timeout={1000}
classNames='fadeTransition'
>
{infoToggles.shop ? (
<div>
<p>
Livonsaaren osuuskauppa palvelee klo 9-19 joka päivä. Keittiö ja
baari 12-19 to-pe. Ostosten yhteydessä mahdollista nostaa
käteistä, mutta suosittelemme tuomaan käteistä rahaa kaupungista
esim. mahdollisia kirjaostoksia varten.
</p>
<a href='https://livonsaarenosuuskauppa.fi/'>Kaupan kotisivut</a>
</div>
) : (
<></>
)}
</CSSTransition>
<hr />
</div>
<div className={styles.infoContainer}>
<div className={styles.infoTitle} onClick={() => toggleInfo('bus')}>
<h2>Bussiaikataulut</h2>
<button className={shared.openingChevron}>
{infoToggles.bus ? (
<BiChevronDown size='3rem' />
) : (
<BiChevronLeft size='3rem' />
)}
</button>
</div>
<CSSTransition
in={infoToggles.bus}
timeout={1000}
classNames='fadeTransition'
>
{infoToggles.bus ? (
<div>
<p>
Bussi 203 lähtee Turusta ma-pe klo 16:10 ja Naantalista klo 17,
ja saapuu Velkuan Teersaloon n. klo 17:45. Tämä on ainoa suora
yhteys festivaalille. Palvan saareen on Teersalosta yksi lossi,
ja Velkuanmaahan kaksi. Lossit lähtevät puolen tunnin välein
(tasatunnein ja puolelta) Teersalosta ja Velkuanmaasta, ja Palvasta
aina varttia vaille ja yli tasatunnin. (Aikataulut:{' '}
<a href="https://www.finferries.fi/">finnferries.fi</a>)
Takaisin päin busseja ei kulje viikonloppuisin.
</p>
<a href='https://cms.foli.fi/sites/default/files/documents-2024-04/Linja%20200%2C203.pdf'>
Paikallisliikenteen bussiaikataulut
</a>
</div>
) : (
<></>
)}
</CSSTransition>
<hr />
</div>
<div className={styles.infoContainer}>
<div className={styles.infoTitle} onClick={() => toggleInfo('accom')}>
<h2>Majoitus</h2>
<button className={shared.openingChevron}>
{infoToggles.accom ? (
<BiChevronDown size='3rem' />
) : (
<BiChevronLeft size='3rem' />
)}
</button>
</div>
<CSSTransition
in={infoToggles.accom}
timeout={1000}
classNames='fadeTransition'
>
{infoToggles.accom ? (
<div className={styles.infoContent}>
<p>
Majoituspalveluita Livonsaari-Velkua: Wanha Salakuljettaja
(Teersalo), Livonsaari Caravan, Saaristohotelli Vaihela.
</p>
<div className={styles.linkList}>
<a href='https://oldsmuggler.fi/'>Vanha Salakuljettaja</a>
<a href='https://www.livonsaarencaravan.fi/'>
Livonsaari Caravan
</a>
<a href='https://vaihela.fi/'>Saaristohotelli Vaihela</a>
</div>
</div>
) : (
<></>
)}
</CSSTransition>
<hr />
</div>
<div className={styles.infoContainer}>
<div className={styles.infoTitle} onClick={() => toggleInfo('ferry')}>
<h2>Lossiyhteydet</h2>
<button className={shared.openingChevron}>
{infoToggles.ferry ? (
<BiChevronDown size='3rem' />
) : (
<BiChevronLeft size='3rem' />
)}
</button>
</div>
<CSSTransition
in={infoToggles.ferry}
timeout={1000}
classNames='fadeTransition'
>
{infoToggles.ferry ? (
<div>
<p>
Lossiyhteydet Palvaan ja Velkuanmaahan Finferries-sivustolla
(huom. yövuoro edellyttää tilauksen etukäteen. Lossi on osa
julkista tieverkostoa eli maksuton.)
</p>
<a href='https://www.finferries.fi/lauttaliikenne/lauttapaikat-ja-aikataulut/velkuanmaa.html'>
Aikataulut Palvaan ja Velkuanmaahan
</a>
</div>
) : (
<></>
)}
</CSSTransition>
<hr />
</div>
<div className={styles.infoContainer}>
<div className={styles.infoTitle} onClick={() => toggleInfo('fb')}>
<h2>Facebook</h2>
<button className={shared.openingChevron}>
{infoToggles.fb ? (
<BiChevronDown size='3rem' />
) : (
<BiChevronLeft size='3rem' />
)}
</button>
</div>
<CSSTransition
in={infoToggles.fb}
timeout={1000}
classNames='fadeTransition'
>
{infoToggles.fb ? (
<div>
<a href='https://www.facebook.com/Runosaari-festivaali-110533364561933'>
Tapahtuman facebook-sivut
</a>
</div>
) : (
<></>
)}
</CSSTransition>
<hr />
</div>
</section>
);
};
export default Info;

View File

@@ -0,0 +1,49 @@
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import shared from '../../styles/Shared.module.scss';
import styles from '../../styles/Performer.module.scss';
import PerformersData from '../../data/performers/2025';
import Performer from '../../types/Performer';
const PerformerPage = () => {
const { id } = useParams<{ id: string }>();
const [performer, setPerformer] = useState<Performer>({
name: '',
paragraphs: [],
id: '',
});
useEffect(() => {
setPerformer(
PerformersData.find((p) => p.id === id) ?? {
name: '',
paragraphs: [],
id: '',
}
);
}, [id]);
return performer.name === '' ? (
<div>Esiintyjää tunnisteella {id} ei löydy.</div>
) : (
<div className={styles.performerContainer}>
<img
className={styles.performerImage}
src={'/performers/2025/' + performer.id + '.jpg'}
width={300}
height={300}
loading="lazy"
alt={performer.name}
/>
<h2>{performer.name}</h2>
<div>
{performer.paragraphs.map((parag, index) => (
<p key={index}>{parag.toString()}</p>
))}
</div>
</div>
);
};
export default PerformerPage;

99
src/pages/Performers.tsx Normal file
View File

@@ -0,0 +1,99 @@
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import styles from '../../styles/Performers.module.scss';
import shared from '../../styles/Shared.module.scss';
import Performer from '../../types/Performer';
import { BiChevronDown, BiChevronLeft } from 'react-icons/bi';
import PerformersData from '../../data/performers/2026';
import { CSSTransition } from 'react-transition-group';
import { FiExternalLink } from 'react-icons/fi';
interface PerformerCard extends Performer {
showDesc: boolean;
}
const Performers = () => {
const [performers, setPerformers] = useState<PerformerCard[]>([]);
useEffect(() => {
let cards: PerformerCard[] = [];
PerformersData.map((p) => {
let newCard = { ...p, showDesc: false };
cards.push(newCard);
});
setPerformers(cards);
}, []);
const togglePerformerDesc = (id: string) => {
let updated: PerformerCard[];
updated = performers.map((p) => {
if (p.id === id) {
p.showDesc = !p.showDesc;
}
return p;
});
setPerformers(updated);
};
return (
<section className={shared.page}>
<h1>Esiintyjät</h1>
{performers.length !== 0 ? (
performers.map((p) => (
<div className={styles.performerContainer} key={p.id}>
<img
className={styles.performerImage}
src={'/performers/2025/' + p.id + '.jpg'}
width={100}
height={100}
loading="lazy"
alt={p.name + ' image'}
/>
<div className={styles.performerTextContainer}>
<div
className={styles.performerTitle}
onClick={() => togglePerformerDesc(p.id)}
>
<h2>{p.name}</h2>
<button className={shared.openingChevron}>
{p.showDesc ? (
<BiChevronDown size='3rem' />
) : (
<BiChevronLeft size='3rem' />
)}
</button>
</div>
<CSSTransition
in={p.showDesc}
timeout={1000}
classNames='fadeTransition'
>
{p.showDesc ? (
<div>
{p.paragraphs.map((parag, index) => (
<p key={index}>{parag.toString()}</p>
))}
</div>
) : (
<span></span>
)}
</CSSTransition>
<hr />
</div>
</div>
))
) : (
<i className={shared.moreInfoLaterText}>Lisätietoja tulossa myöhemmin...</i>
)}
<Link to="/archive">
<span className={styles.archiveLinkText}>Aiempien vuosien esiintyjiä</span>
<FiExternalLink fontSize={20} />
</Link>
</section>
);
};
export default Performers;

70
src/pages/Program.tsx Normal file
View File

@@ -0,0 +1,70 @@
import React from 'react';
import { FiExternalLink } from 'react-icons/fi';
import shared from '../../styles/Shared.module.scss';
import styles from '../../styles/Program.module.scss';
import { BiChevronDown, BiChevronLeft } from 'react-icons/bi';
const Program = () => {
return (
<section className={shared.page}>
<h1 id='program-start'>Ohjelma</h1>
<i className={shared.moreInfoLaterText}>Lisätietoja tulossa myöhemmin...</i>
{/* <p className={styles.programTimeAndPlace}>
Torstai 12.6. klo 18-21 <span className={styles.locationName}>Palva</span>
</p>
<h2 className={styles.placeTitle}>
<a href='https://www.cafelaituri.fi'>
Cafe Laituri
</a>
</h2>
<ul className={styles.performerList}>
<li>Pegasos</li>
<li>Henriikka Tavi ja Jaakko Martikainen</li>
<li>Peter Mickwitz</li>
<li>Milagros Corcuera</li>
</ul>
<hr className={styles.programHr} />
<p className={styles.programTimeAndPlace}>
Perjantai 13.6. klo 18-21 <span className={styles.locationName}>Velkuanmaa</span>
</p>
<h2 className={styles.placeTitle}>
<a href='https://www.vaihela.fi/'>
<span className={styles.placeName}>Saaristohotelli Vaihela</span>
</a>
</h2>
<ul className={styles.performerList}>
<li>Kauko Röyhkä & Severi Pyysalo</li>
<li>Juha Kulmala + Positroninen Runo-orkesteri!</li>
<li>Katariina Vuorinen & Pekka Tolonen</li>
</ul>
<hr className={styles.programHr} />
<p className={styles.programTimeAndPlace}>
Lauantai 14.6. klo 15-20 <span className={styles.locationName}>Teersalo</span>
</p>
<h2 className={styles.placeTitle}>
<a href='https://prosinervo.com/'>
<span className={styles.placeName}>Sinervon talo</span>
</a>
</h2>
<ul className={styles.performerList}>
<li>Santtu Puukka</li>
<li>Maaria Päivinen</li>
<li>Silja Järventausta</li>
<li>Tiina Lehikoinen</li>
<li>Katariina Vuorinen</li>
<li>Panu Hämeenaho</li>
<li>Uhrijuhla</li>
<li>Djangomania</li>
<li>Milagros Corcuera</li>
</ul>
<hr className={styles.programHr} /> */}
</section>
);
};
export default Program;

54
src/pages/Safety.tsx Normal file
View File

@@ -0,0 +1,54 @@
import React from 'react';
import shared from '../../styles/Shared.module.scss';
import styles from '../../styles/Safety.module.scss';
const Safety = () => {
return (
<section className={shared.page + ' ' + styles.safetyPage}>
<h1>Turvallisemman tilan periaatteet</h1>
<p>
Runosaaren tapahtumissa noudatetaan turvallisemman tilan periaatteita.
Pyrimme tapahtumissamme turvaamaan näiden periaatteiden toteutumisen
omalla toiminnallamme, tilojen suunnittelulla sekä henkilökunnan,
esiintyjien ja yleisön informoinnilla.
</p>
<ul className={styles.safetyList}>
<li>
<b>Kunnioitus. </b>Jokaisella ihmisellä on oikeus tulla kunnioitetuksi
omana itsenään. Kunnioitathan muiden ihmisten fyysistä ja psyykkistä
koskemattomuutta, mielipiteitä sekä ihmisarvoa. Jokaisella on oikeus
poistua epämukavaksi kokemastaan tilanteesta tai keskustelusta. Ethän
myöskään ota ihmisistä kuvia kysymättä ensin.{' '}
</li>
<li>
<b>Olettaminen. </b>Ethän oleta kenenkään sukupuolta, kansallisuutta,
seksuaalista suuntautumista, kulttuuria, kieltä, uskontoa, arvoja,
terveydentilaa tai toimintakykyä. Ethän tee johtopäätelmiä kenenkään
ulkonäön, käytöksen tai oletetun sosioekonomisen aseman perusteella.
</li>
<li>
<b>Kommunikointi. </b>
Pyrithän luomaan ympärillesi ystävällistä ja turvallista ilmapiiriä.
Ole avoin muita ihmisiä kohtaan, kuuntele ja käytä kunnioittavaa
kieltä. Ethän oleta puheessasi kenenkään sukupuolta tai muita
ominaisuuksia. Ethän käytä stereotypisoivaa, toiseuttavaa tai
halventaa kieltä. Jos kuitenkin vahingossa loukkaat sanoillasi
jotakuta, pyydäthän anteeksi.
</li>
<li>
<b>Toimiminen. </b>
Mikäli havaitset epäasiallista käytöstä tai koet olosi uhatuksi, ä
epäröi pyytää apua Runosaaren ja tapahtumapaikkojen työntekijöiltä.
Jos koet, että olet kohdannut häirintää tapahtumissamme ja siihen ei
ole onnistuttu puuttumaan, ole yhteydessä Runosaari-työryhmään
sähköpostilla{' '}
<a href='mailto:runosaari@gmail.com'>runosaari@gmail.com</a> .
</li>
</ul>
</section>
);
};
export default Safety;

14
src/pages/Workshops.tsx Normal file
View File

@@ -0,0 +1,14 @@
import React from 'react';
import shared from '../../styles/Shared.module.scss';
import styles from '../../styles/Workshops.module.scss';
const Workshops = () => {
return (
<section className={shared.page + ' ' + styles.workshopPage}>
<h1>Työpajat</h1>
<i>Lisätietoja tulossa myöhemmin...</i>
</section>
);
};
export default Workshops;