Complete english version of site

This commit is contained in:
Wouter van Veelen
2025-12-20 02:02:11 +01:00
parent 23cd5a9e53
commit 87932f1934
19 changed files with 355 additions and 139 deletions

View File

@@ -1,75 +1,2 @@
# React + TypeScript + Vite # Hi
Nice of you to check out the source of the website. Hopefully you enjoy what you see!
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information.
Note: This will impact Vite dev & build performances.
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(["dist"]),
{
files: ["**/*.{ts,tsx}"],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
]);
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.ts
import reactX from "eslint-plugin-react-x";
import reactDom from "eslint-plugin-react-dom";
export default defineConfig([
globalIgnores(["dist"]),
{
files: ["**/*.{ts,tsx}"],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs["recommended-typescript"],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
]);
```

BIN
assets/Squagon.pdf Normal file

Binary file not shown.

View File

@@ -1,18 +1,18 @@
import js from "@eslint/js"; import js from '@eslint/js';
import globals from "globals"; import globals from 'globals';
import reactHooks from "eslint-plugin-react-hooks"; import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from "eslint-plugin-react-refresh"; import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from "typescript-eslint"; import tseslint from 'typescript-eslint';
import { defineConfig, globalIgnores } from "eslint/config"; import { defineConfig, globalIgnores } from 'eslint/config';
export default defineConfig([ export default defineConfig([
globalIgnores(["dist"]), globalIgnores(['dist', 'node_modules']),
{ {
files: ["**/*.{ts,tsx}"], files: ['**/*.{ts,tsx}'],
extends: [ extends: [
js.configs.recommended, js.configs.recommended,
tseslint.configs.recommended, tseslint.configs.recommended,
reactHooks.configs["recommended-latest"], reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite, reactRefresh.configs.vite,
], ],
languageOptions: { languageOptions: {

View File

@@ -2,10 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="stylesheet" href="/src/App.css" /> <link rel="stylesheet" href="/src/App.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>cblt-website</title> <title>Wouter van Veelen</title>
</head> </head>
<body> <body>
<div id="background"></div> <div id="background"></div>

4
package-lock.json generated
View File

@@ -2412,7 +2412,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@@ -1,5 +1,5 @@
{ {
"name": "cblt-website", "name": "webhome",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",

View File

@@ -81,3 +81,10 @@ body {
min-width: 100%; min-width: 100%;
z-index: -100; z-index: -100;
} }
p {
margin: 0;
+ p {
margin-top: 1.5em;
}
}

View File

@@ -5,6 +5,8 @@ import { Content } from './layout/Content.tsx';
import { Home } from './pages/Home.tsx'; import { Home } from './pages/Home.tsx';
import { About } from './pages/About.tsx'; import { About } from './pages/About.tsx';
import { Career } from './pages/Career.tsx'; import { Career } from './pages/Career.tsx';
import { Storytelling } from './pages/Storytelling.tsx';
import { Puzzles } from './pages/Puzzles.tsx';
function App() { function App() {
return ( return (
@@ -15,6 +17,8 @@ function App() {
<Routes> <Routes>
<Route index path='home' element={<Home />} /> <Route index path='home' element={<Home />} />
<Route path='career' element={<Career />} /> <Route path='career' element={<Career />} />
<Route path='storytelling' element={<Storytelling />} />
<Route path='puzzles' element={<Puzzles />} />
<Route path='about' element={<About />} /> <Route path='about' element={<About />} />
</Routes> </Routes>
</Content> </Content>

View File

@@ -0,0 +1,35 @@
import { type ReactElement, type ReactNode } from 'react';
import { CollapsableCard, type CollapsableCardProps } from './CollapsableCard.tsx';
import {
TimelineConnector,
TimelineContent,
TimelineDot,
TimelineItem,
TimelineOppositeContent,
TimelineSeparator,
} from '@mui/lab';
export type CareerEntryProps = CollapsableCardProps & {
start: ReactNode;
finalEntry?: boolean;
};
export function CareerEntry({
finalEntry,
startCollapsed = true,
variant = 'outlined',
start,
...props
}: CareerEntryProps): ReactElement {
return (
<TimelineItem>
<TimelineOppositeContent color='textPrimary'>{start}</TimelineOppositeContent>
<TimelineSeparator>
<TimelineDot />
{!finalEntry && <TimelineConnector />}
</TimelineSeparator>
<TimelineContent color='textPrimary'>
<CollapsableCard startCollapsed={startCollapsed} variant={variant} {...props} />
</TimelineContent>
</TimelineItem>
);
}

View File

@@ -15,6 +15,7 @@ export function CollapsableCard({
startCollapsed, startCollapsed,
...props ...props
}: CollapsableCardProps): ReactElement { }: CollapsableCardProps): ReactElement {
const hasContent = children != null;
const [collapsed, setCollapsed] = useState(startCollapsed ?? false); const [collapsed, setCollapsed] = useState(startCollapsed ?? false);
const toggleCollapse = () => setCollapsed((oldState) => !oldState); const toggleCollapse = () => setCollapsed((oldState) => !oldState);
@@ -26,16 +27,22 @@ export function CollapsableCard({
title={header} title={header}
subheader={subheader} subheader={subheader}
action={ action={
hasContent ?
<IconButton onClick={toggleCollapse}> <IconButton onClick={toggleCollapse}>
{collapsed ? {collapsed ?
<ArrowDownward /> <ArrowDownward />
: <ArrowUpward />} : <ArrowUpward />}
</IconButton> </IconButton>
: undefined
} }
/> />
)} )}
<Collapse in={!collapsed}> <Collapse in={!collapsed}>
<CardContent>{children}</CardContent> {hasContent && (
<CardContent sx={{ fontSize: '14px', textAlign: 'left' }}>
{children}
</CardContent>
)}
</Collapse> </Collapse>
</Card> </Card>
); );

View File

@@ -0,0 +1,12 @@
import { OpenInNewOutlined } from '@mui/icons-material';
import { Link, type LinkProps } from '@mui/material';
import type { ReactElement } from 'react';
export function OutsideLink({ children, target = '_blank', ...props }: LinkProps): ReactElement {
return (
<Link {...props} target={target}>
{children}
<OpenInNewOutlined fontSize='inherit' />
</Link>
);
}

View File

@@ -6,7 +6,14 @@ export type ContentProps = PropsWithChildren;
export function Content({ children }: ContentProps): ReactElement { export function Content({ children }: ContentProps): ReactElement {
return ( return (
<Box sx={{ width: { xs: '100%', md: '80%' }, margin: 'auto' }}> <Box
sx={{
width: { xs: '100%', md: '80%' },
margin: 'auto',
height: 'calc(100vh - 25px)',
color: (theme) => theme.palette.text.primary,
}}
>
<TopBar /> <TopBar />
{children} {children}
</Box> </Box>

View File

@@ -2,7 +2,13 @@ import React, { type ReactElement } from 'react';
import { Box, Tab, Tabs } from '@mui/material'; import { Box, Tab, Tabs } from '@mui/material';
import { useLocation, useNavigate } from 'react-router'; import { useLocation, useNavigate } from 'react-router';
const ValidTabs = ['home', 'career', 'about'] as const satisfies string[]; const ValidTabs = [
'home',
'career',
'storytelling',
'puzzles',
'about',
] as const satisfies string[];
export function TopBar(): ReactElement { export function TopBar(): ReactElement {
const { pathname } = useLocation(); const { pathname } = useLocation();

View File

@@ -1,5 +1,22 @@
import { Fragment, type ReactElement } from 'react'; import { Fragment, type ReactElement } from 'react';
export function About(): ReactElement { export function About(): ReactElement {
return <Fragment>Hello, About.</Fragment>; return (
<Fragment>
<p />
<p>
This website is by and about me, Wouter van Veelen. The reason for having this
website is three-fold. First, I want to have a place where I can experiment with web
techniques and frameworks. Secondly, I want a place where I can publicly share
creative works. Third, I wanted to delete my LinkedIn, without becoming completely
unfindable for potential employers or collaborators.
</p>
<p>
About the technical specs, this website built using Vite + React (compiler) and
served by an Apache2 web server running on a self-built Arch-linux server. The
storage on the server runs on zfs in a 3+1 HDD configuration with an NVME boot
drive.
</p>
</Fragment>
);
} }

View File

@@ -1,43 +1,153 @@
import { import { Timeline } from '@mui/lab';
Timeline, import { Fragment, type ReactElement } from 'react';
TimelineConnector, import { Switch, Typography } from '@mui/material';
TimelineContent, import { useToggle } from '../utils.ts';
TimelineDot, import { OutsideLink } from '../components/OutsideLink.tsx';
TimelineItem, import { CareerEntry } from '../components/CareerEntry.tsx';
TimelineOppositeContent,
TimelineSeparator,
} from '@mui/lab';
import type { ReactElement } from 'react';
import { CollapsableCard } from '../components/CollapsableCard.tsx';
export function Career(): ReactElement { export function Career(): ReactElement {
const [full, toggleFull] = useToggle(true);
return ( return (
<Timeline position='alternate'> <Fragment>
<TimelineItem> <div
<TimelineOppositeContent color='textPrimary'>2014</TimelineOppositeContent> style={{
<TimelineSeparator> display: 'flex',
<TimelineDot /> flexDirection: 'row',
<TimelineConnector /> justifyContent: 'center',
</TimelineSeparator> alignItems: 'center',
<TimelineContent color='textPrimary'> }}
<CollapsableCard
startCollapsed
variant='outlined'
header='BSc. Computer Science'
subheader='University of Twente.'
> >
Test <div style={{ width: '100%' }} />
</CollapsableCard> <Switch checked={full} onChange={toggleFull} />
</TimelineContent> <div style={{ width: '100%' }}>
</TimelineItem> <Typography color='textPrimary'>Include activism and volunteer work</Typography>
<TimelineItem> </div>
<TimelineOppositeContent color='textPrimary'>10:00 am</TimelineOppositeContent> </div>
<TimelineSeparator> <Timeline position='alternate'>
<TimelineDot /> <CareerEntry
<TimelineConnector /> key='bsc'
</TimelineSeparator> start='Sep. 2014'
<TimelineContent color='textPrimary'>Code</TimelineContent> header='BSc. Computer Science'
</TimelineItem> subheader='Until 2018, University of Twente.'
/>
{full && (
<CareerEntry
key='iapc'
start='Oct. 2014'
header='Volunteer and Board Member'
subheader={
<Fragment>
Until 2018, <OutsideLink href='iapc.nl'>IAPC</OutsideLink>
</Fragment>
}
>
<p>
<OutsideLink href='iapc.nl'>IAPC</OutsideLink> is a computer parts and
service store completely run by volunteers on the campus of the
Universtiy of Twente.
</p>
<p>
During my active years I helped in the committees for logistics, RMA and
PR, as well as being board member for logistics in 2015-2016.
</p>
</CareerEntry>
)}
{full && (
<CareerEntry
key='stretchers'
start='Oct. 2015'
header='Trainer, Volunteer and Board Member'
subheader={
<Fragment>
Until 2023,{' '}
<OutsideLink href='https://stretchers.nl'>
D.B.V. de Stretchers
</OutsideLink>
</Fragment>
}
>
<p>
<OutsideLink href='https://stretchers.nl'>
D.B.V. de Stretchers
</OutsideLink>{' '}
is a sports association on the University of Twente where all sports are
practiced and the focus is on fun, learning and activities
</p>
<p>
During my active years I was a trainer and assisted in the events
committee as well as PR and the website. During the academic year
2020-2021 I was the chairman of the association
</p>
</CareerEntry>
)}
<CareerEntry
key='msc'
start='Sep. 2018'
header='Master Data Science'
subheader='Unfinished, Until 2021, University of Twente.'
>
<p>
With courses focused around algorithms, machine learning and data
processing.
</p>
<p>
Completed all required courses, but did not complete the final thesis
because of personal reasons.
</p>
</CareerEntry>
{full && (
<CareerEntry
key='muziekbank'
start='Feb. 2022'
header='Volunteer Muziekbank'
subheader='Until 2023, Muziekbank Enschede'
>
<p>
The{' '}
<OutsideLink href='http://muziekbank.nl' target='_blank'>
muziekbank
</OutsideLink>{' '}
is a library of vinyls and cds where customers can go to discover new
and old music, experience event and rent items like a normal library.
</p>
<p>
While being active I manned the counter to assist customers and was
involved in processing newly bought cds to be added to the collection
</p>
</CareerEntry>
)}
<CareerEntry
key='cofano'
start='Nov. 2022'
header='Full Stack Developer'
subheader='Current, Cofano'
finalEntry={!full}
>
<p>
Full stack developer on a SaaS project based on Java, Spring Boot, GraphQL,
Typescript and React.
</p>
<p>
Personally dove deep in understanding the fundamentals of frameworks and
languages and applying them for major version upgrades and developing on
internal tools and libraries.
</p>
</CareerEntry>
{full && (
<CareerEntry
start='Apr. 2025'
header='Trainer and Technical Committee'
subheader={
<Fragment>
Current, Volleyball association{' '}
<OutsideLink href='https://twente05.nl'>Twente '05</OutsideLink>
</Fragment>
}
finalEntry={full}
/>
)}
</Timeline> </Timeline>
</Fragment>
); );
} }

View File

@@ -1,11 +1,42 @@
import { type ReactElement } from 'react'; import { type ReactElement } from 'react';
import { Card, CardContent, CardHeader } from '@mui/material'; import { Link, Stack, Tooltip } from '@mui/material';
import { GitHub, Source } from '@mui/icons-material';
export function Home(): ReactElement { export function Home(): ReactElement {
return ( return (
<Card> <Stack width='100%' justifyContent='center' textAlign='center' direction='column'>
<CardHeader title='Home'></CardHeader> <h1>Welcome</h1>
<CardContent>Hello, others</CardContent> <h2>To My Website</h2>
</Card> <p />
<p>
On this website you can find bits about me and bits by me. I am a software developer
and the career tab can tell you some of my history in the world. As proof of being a
software developer, this website is entirely self-hosted, including a git repository
(and except the DNS servers, I suppose), on a server that I have physically built
and maintain.
</p>
<p>
Besides being a software developer, I also like stories and puzzles a whole lot, so
you can also find tabs dedicated to those. Hope you enjoy! I hope to add more
puzzle-focused elements to the site in the future.
</p>
<p>
Use the top bar to find what you are looking for or contact me via email. No direct
mailto link here to avoid scraper bots, but my first name and this domain name
should do the trick.
</p>
<Stack marginTop='20px' direction='row' spacing={2} justifyContent='center'>
<Link href='https://github.com/WouterLVV' target='_blank'>
<Tooltip title='GitHub'>
<GitHub />
</Tooltip>
</Link>
<Link href='https://git.cblt.xyz/wouter/WebHome' target='_blank'>
<Tooltip title='source'>
<Source />
</Tooltip>
</Link>
</Stack>
</Stack>
); );
} }

View File

@@ -0,0 +1,17 @@
import { Fragment, type ReactElement } from 'react';
import { OutsideLink } from '../components/OutsideLink.tsx';
export function Puzzles(): ReactElement {
return (
<Fragment>
<p />
<p>
I have a love for puzzles and puzzle events. In the past I have particapated in and
organized the{' '}
<OutsideLink href='https://iapandora.nl'>Pandora puzzle event</OutsideLink> at the
University of Twente.
</p>
<p>As such I want to create and share more puzzles here, in time.</p>
</Fragment>
);
}

View File

@@ -0,0 +1,28 @@
import { Fragment, type ReactElement } from 'react';
import { Card, CardContent } from '@mui/material';
import { OutsideLink } from '../components/OutsideLink.tsx';
export function Storytelling(): ReactElement {
return (
<Fragment>
<p />
<p>
I like to consume and tell stories. Besides reading and watching tv and movies, I
also like to create my own stories.
</p>
<p>
I've played my fair share of Dungeons and Dragons and have also DM'ed for a while,
and for the{' '}
<OutsideLink href='https://iapandora.nl'>Pandora puzzle event</OutsideLink> I have
also been responsible for or involved with the story aspect during the two times I
was part of the event organisation
</p>
<p>
I've also participated in a small short story writing competition for{' '}
<OutsideLink href='https://bellettrie.utwente.nl'>Bellettrie</OutsideLink>, the
student library of the University of Twente. The theme was 'Fairytale' and you can
read my entry <OutsideLink href='assets/Squagon.pdf'>here</OutsideLink>.
</p>
</Fragment>
);
}

7
src/utils.ts Normal file
View File

@@ -0,0 +1,7 @@
import { useCallback, useState } from 'react';
export function useToggle(initialValue?: boolean): [boolean, () => void] {
const [state, setState] = useState<boolean>(initialValue ?? false);
const toggle = useCallback(() => setState((oldState) => !oldState), []);
return [state, toggle];
}