Compare commits
No commits in common. "4bcc2e53cfd3b0918ed0d59837b3dcd2bf439ea4" and "c8810215324db1f59254354161ad033ae4a2d849" have entirely different histories.
4bcc2e53cf
...
c881021532
894
package-lock.json
generated
894
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,6 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-bootstrap": "^2.7.4",
|
"react-bootstrap": "^2.7.4",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-markdown": "^8.0.7",
|
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,84 +12,37 @@ export const personalData: PersonalData = {
|
|||||||
{icon: 'telephone', text: '+420 111 222 333'},
|
{icon: 'telephone', text: '+420 111 222 333'},
|
||||||
{icon: 'geo-alt', text: 'Brno, Czechia'}
|
{icon: 'geo-alt', text: 'Brno, Czechia'}
|
||||||
],
|
],
|
||||||
|
|
||||||
jobs: {
|
jobs: {
|
||||||
current: {
|
current: {
|
||||||
position: 'Senior Software Development Manager (UI Platform)',
|
position: 'Janitor',
|
||||||
company: 'Oracle|NetSuite',
|
company: 'Cleaners Limited',
|
||||||
timerange: '2022 - present',
|
timerange: '2022 - present',
|
||||||
description: `Engineering lead for multiple UI Platform teams. Development of UI frameworks, components and tools.
|
description: 'Cleanup duty 24/7.',
|
||||||
Leading a UI testing architecture group, supplemental product owner.`,
|
|
||||||
tags: ['TypeScript', 'Preact', 'NodeJs', 'Oracle JET', 'Java', 'Git']
|
|
||||||
},
|
},
|
||||||
previous: [
|
previous: [
|
||||||
{
|
{
|
||||||
position: 'Software Development Manager (UI Platform)',
|
position: 'CEO',
|
||||||
company: 'Oracle|NetSuite',
|
company: 'CryptoDancers',
|
||||||
timerange: '2020 - 2022',
|
timerange: '2019 - 2022',
|
||||||
description: 'Engineering lead for a UI Platform team. Development of UI components. Supplementing a product owner when bootstrapping a team.',
|
description: 'Revolutionizing the crypto world.',
|
||||||
tags: ['TypeScript', 'Preact', 'NodeJs', 'Oracle JET', 'Java', 'Git']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
position: 'Software Development Manager (ERP, Tax)',
|
|
||||||
company: 'Oracle|NetSuite',
|
|
||||||
timerange: '2017 - 2020',
|
|
||||||
description: 'Developer lead for ERP: Tax. Design and implementation of a pluggable Tax calculation engine. Coordination of multiple squads supporting both the legacy and the next-gen systems concurrently.',
|
|
||||||
tags: ['Java', 'Oracle SQL', 'JavaScript', 'Perforce', 'Git', 'Software Architecture']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
position: 'Senior Software Engineer (ERP, Tax)',
|
|
||||||
company: 'Oracle|NetSuite',
|
|
||||||
timerange: '2013 - 2017',
|
|
||||||
description: `Development of Tax calculation and reporting modules in a cloud ERP system.
|
|
||||||
Large-scale refactoring of legacy code. Introduction of a pluggable architecture.`,
|
|
||||||
tags: ['Java', 'Oracle SQL', 'JavaScript', 'Perforce', 'Legacy code refactoring']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
position: 'Software Engineer',
|
|
||||||
company: 'Q2 Interactive',
|
|
||||||
timerange: '2009 - 2013',
|
|
||||||
description: `Development and project leadership of multiple web applications:
|
|
||||||
- CRM system (PHP backend server, JavaScript frontend, Android client app),
|
|
||||||
- Accounting web app (PHP, JavaScript),
|
|
||||||
- e-commerce sites (Magento),
|
|
||||||
- Linux server maintenance.`,
|
|
||||||
tags: ['PHP', 'Java', 'JavaScript', 'MySQL', 'Server hosting', 'Linux', 'Android', 'Subversion (SVN)']
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
projects: {
|
|
||||||
current: {
|
|
||||||
position: `Personal projects`,
|
|
||||||
description: `Various hardware and software projects. Usually open sourced and published on [projects.dejvino.com](https://projects.dejvino.com) .
|
|
||||||
These include video games, utilities, 3D models, devices with embedded microcontrollers etc.`,
|
|
||||||
tags: ['Java', 'Python', 'C/C++', 'Embedded devices', 'OpenSCAD', 'TypeScript', 'Linux', 'Git', 'Self-hosting']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
education: {
|
education: {
|
||||||
previous: [
|
previous: [
|
||||||
{
|
{
|
||||||
position: `Master's degree, Parallel and Distributed Systems`,
|
position: 'Information Technology (unfinished)',
|
||||||
company: `Faculty of Informatics, Masaryk University Brno`,
|
company: 'University of Benimoto',
|
||||||
timerange: '2011 - 2013',
|
timerange: '2010 - 2017',
|
||||||
description: `Master's thesis: Efficient computation and visualization of correlations in medical signals.`,
|
description: '',
|
||||||
},
|
|
||||||
{
|
|
||||||
position: `Bachelor's degree, Parallel and Distributed Systems`,
|
|
||||||
company: `Faculty of Informatics, Masaryk University Brno`,
|
|
||||||
timerange: '2008 - 2011',
|
|
||||||
description: `Bachelor's thesis: Parallel implementation of force-field decompression algorithm.`,
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
skills: {
|
skills: {
|
||||||
primary: ['Java', 'TypeScript', 'JavaScript', 'Engineering leadership', 'Linux'],
|
primary: ['Java', 'TypeScript', 'JavaScript'],
|
||||||
secondary: ['SQL', 'Kotlin', 'Go', 'C/C++', 'NodeJs', 'Git', 'Preact', 'Embedded devices'],
|
secondary: ['Kotlin', 'Go'],
|
||||||
languages: ['Czech (native)', 'English (proficient)', 'German (elementary)'],
|
languages: ['Czech (native)', 'English (proficient)', 'German (elementary)'],
|
||||||
others: ['Driver\'s license (B)']
|
others: ['Driver\'s license (B)']
|
||||||
},
|
},
|
||||||
interests: ['Guitars', 'Heavy Metal', 'Mazda MX-5', 'DIY electronics', 'Linux', 'Open source'],
|
interests: ['Guitars and Heavy Metal', 'Mazda MX-5', 'DIY electronics'],
|
||||||
};
|
};
|
||||||
|
@ -5,10 +5,9 @@ export type Contact = {
|
|||||||
|
|
||||||
export type Job = {
|
export type Job = {
|
||||||
position: string,
|
position: string,
|
||||||
company?: string,
|
company: string,
|
||||||
timerange?: string,
|
timerange: string,
|
||||||
description: string,
|
description: string
|
||||||
tags?: string[],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Jobs = {
|
export type Jobs = {
|
||||||
@ -18,8 +17,6 @@ export type Jobs = {
|
|||||||
|
|
||||||
export type Education = Jobs;
|
export type Education = Jobs;
|
||||||
|
|
||||||
export type Projects = Jobs;
|
|
||||||
|
|
||||||
export type Skills = {
|
export type Skills = {
|
||||||
primary: string[],
|
primary: string[],
|
||||||
secondary?: string[],
|
secondary?: string[],
|
||||||
@ -33,7 +30,6 @@ export type PersonalData = {
|
|||||||
brief: string,
|
brief: string,
|
||||||
contacts: Contact[],
|
contacts: Contact[],
|
||||||
jobs: Jobs,
|
jobs: Jobs,
|
||||||
projects?: Projects,
|
|
||||||
education?: Education,
|
education?: Education,
|
||||||
skills: Skills,
|
skills: Skills,
|
||||||
interests: string[]
|
interests: string[]
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import Container from 'react-bootstrap/Container';
|
import Container from 'react-bootstrap/Container';
|
||||||
|
import Image from 'react-bootstrap/Image'
|
||||||
import { usePersonContext } from '../hooks/PersonContext';
|
import { usePersonContext } from '../hooks/PersonContext';
|
||||||
import md from './Markdown';
|
import { Col, Row } from 'react-bootstrap';
|
||||||
|
|
||||||
export default function AboutBrief() {
|
export default function AboutBrief() {
|
||||||
const person = usePersonContext()
|
const person = usePersonContext()
|
||||||
return (
|
return (
|
||||||
<Container className='about-brief' fluid>
|
<Container className='about-brief' fluid>
|
||||||
<h1>{person.name}</h1>
|
<h1>{person.name}</h1>
|
||||||
<div className='brief'>{md(person.brief)}</div>
|
<p className='brief'>{person.brief}</p>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { ReactNode } from 'react'
|
import React, { ReactNode } from 'react'
|
||||||
import { usePersonContext } from '../hooks/PersonContext'
|
import { usePersonContext } from '../hooks/PersonContext'
|
||||||
import Container from 'react-bootstrap/esm/Container'
|
import Container from 'react-bootstrap/esm/Container'
|
||||||
|
import { useAutoFocus } from '../hooks/FocusedElement'
|
||||||
import { Col, Row } from 'react-bootstrap'
|
import { Col, Row } from 'react-bootstrap'
|
||||||
|
|
||||||
export function Contact(props: {icon?: string, text: string}) {
|
export function Contact(props: {icon?: string, text: string}) {
|
||||||
@ -29,8 +30,9 @@ export function Contact(props: {icon?: string, text: string}) {
|
|||||||
|
|
||||||
export function Contacts() {
|
export function Contacts() {
|
||||||
const person = usePersonContext()
|
const person = usePersonContext()
|
||||||
|
const focus = useAutoFocus<HTMLDivElement>('contacts')
|
||||||
return (
|
return (
|
||||||
<Container className='contacts' fluid>
|
<Container ref={focus} className='contacts' fluid>
|
||||||
<Row>
|
<Row>
|
||||||
<h2>Contacts</h2>
|
<h2>Contacts</h2>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { usePersonContext } from '../hooks/PersonContext';
|
import { usePersonContext } from '../hooks/PersonContext';
|
||||||
import JobHistory from './job/JobsHistory';
|
import JobHistory from './JobHistory';
|
||||||
|
|
||||||
export default function Education() {
|
export default function WorkExperience() {
|
||||||
const person = usePersonContext()
|
const person = usePersonContext()
|
||||||
|
|
||||||
return person.education ? (
|
return person.education ? (
|
||||||
|
29
src/app/components/JobCard.tsx
Normal file
29
src/app/components/JobCard.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Card from 'react-bootstrap/Card';
|
||||||
|
import { useAutoFocus, useFocusedElement } from '../hooks/FocusedElement';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
heading?: string,
|
||||||
|
position: string,
|
||||||
|
timerange: string,
|
||||||
|
company: string,
|
||||||
|
description: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function JobCard(props: Props) {
|
||||||
|
const focusRef = useAutoFocus<HTMLDivElement>([props.position, props.company, props.timerange].join(' - '))
|
||||||
|
return (
|
||||||
|
<Card ref={focusRef} className='job-card'>
|
||||||
|
{props.heading && (
|
||||||
|
<Card.Header>{props.heading}</Card.Header>
|
||||||
|
)}
|
||||||
|
<Card.Body>
|
||||||
|
<Card.Title>{props.position}</Card.Title>
|
||||||
|
<Card.Subtitle>
|
||||||
|
<span className='company-name'>{props.company}</span>, <span className='timerange'>{props.timerange}</span>
|
||||||
|
</Card.Subtitle>
|
||||||
|
<Card.Text>{props.description}</Card.Text>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
94
src/app/components/JobHistory.tsx
Normal file
94
src/app/components/JobHistory.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Container from 'react-bootstrap/Container';
|
||||||
|
import Col from 'react-bootstrap/Col';
|
||||||
|
import Row from 'react-bootstrap/Row';
|
||||||
|
import JobCard from './JobCard';
|
||||||
|
import { partition } from '../utils';
|
||||||
|
import { Job, Jobs } from '@/PersonalDataTypes';
|
||||||
|
import useSize from '../hooks/Size';
|
||||||
|
import { Accordion } from 'react-bootstrap';
|
||||||
|
|
||||||
|
type JobListProps = {
|
||||||
|
jobs: Jobs,
|
||||||
|
entriesPerRow?: number,
|
||||||
|
currentHeading?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
heading: string,
|
||||||
|
} & JobListProps
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
entriesPerRow: 2,
|
||||||
|
currentHeading: 'Currently',
|
||||||
|
}
|
||||||
|
|
||||||
|
function FullList(props: JobListProps) {
|
||||||
|
const {jobs} = props
|
||||||
|
const config = {...defaultProps, ...props}
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
{jobs.current && (
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<JobCard heading={config.currentHeading} {...jobs.current} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
{partition(jobs.previous, config.entriesPerRow).map((jobs, index) => (
|
||||||
|
<Row key={index}>
|
||||||
|
{(jobs.map((job, subindex) => (
|
||||||
|
<Col key={index + '_' + subindex}>
|
||||||
|
<JobCard {...job} />
|
||||||
|
</Col>
|
||||||
|
)))}
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SmallList(props: JobListProps) {
|
||||||
|
const {jobs} = props
|
||||||
|
const config = {...defaultProps, ...props}
|
||||||
|
function jobTitle(job: Job) {
|
||||||
|
return `${job.position} at ${job.company}, ${job.timerange}`
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Accordion defaultActiveKey={jobs.current ? 'current' : 'previous-0'} alwaysOpen>
|
||||||
|
{jobs.current && (
|
||||||
|
<Accordion.Item eventKey="current">
|
||||||
|
<Accordion.Header>{config.currentHeading}:<br />{jobTitle(jobs.current)} </Accordion.Header>
|
||||||
|
<Accordion.Body>
|
||||||
|
{jobs.current.position}
|
||||||
|
</Accordion.Body>
|
||||||
|
</Accordion.Item>
|
||||||
|
)}
|
||||||
|
{jobs.previous?.map((job, index) => (
|
||||||
|
<Accordion.Item eventKey={`previous-${index}`} key={index}>
|
||||||
|
<Accordion.Header>{jobTitle(job)}</Accordion.Header>
|
||||||
|
<Accordion.Body>
|
||||||
|
{job.description}
|
||||||
|
</Accordion.Body>
|
||||||
|
</Accordion.Item>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function JobHistory(props: Props) {
|
||||||
|
const {SizeWrapper, size} = useSize()
|
||||||
|
const jobs = props.jobs
|
||||||
|
|
||||||
|
const jobsList = size.width < 600 ? <SmallList {...props} /> : <FullList {...props} />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<h2>{props.heading}</h2>
|
||||||
|
<SizeWrapper>
|
||||||
|
{jobsList}
|
||||||
|
</SizeWrapper>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
import ReactMarkdown from "react-markdown";
|
|
||||||
|
|
||||||
export default function md(text: string) {
|
|
||||||
return <ReactMarkdown>{text}</ReactMarkdown>
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { usePersonContext } from '../hooks/PersonContext';
|
|
||||||
import JobHistory from './job/JobsHistory';
|
|
||||||
|
|
||||||
export default function Projects() {
|
|
||||||
const person = usePersonContext()
|
|
||||||
|
|
||||||
return person.projects ? (
|
|
||||||
<JobHistory
|
|
||||||
jobs={person.projects}
|
|
||||||
heading='Projects'
|
|
||||||
currentHeading='Currently active'
|
|
||||||
/>
|
|
||||||
) : <></>
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
|
|
||||||
export default function Tag(props: {text: string}) {
|
|
||||||
return (
|
|
||||||
<span className='badge text-bg-light'>{props.text}</span>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import Container from 'react-bootstrap/Container';
|
import Container from 'react-bootstrap/Container';
|
||||||
import Tag from './Tag';
|
import { useAutoFocus, useFocusedElement } from '../hooks/FocusedElement';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
title: string,
|
title: string,
|
||||||
@ -9,10 +9,19 @@ export type Props = {
|
|||||||
style?: 'primary' | 'light'
|
style?: 'primary' | 'light'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Tag(props: {text: string}) {
|
||||||
|
const tagKey = 'tag ' + props.text;
|
||||||
|
const elementRef = useAutoFocus(tagKey)
|
||||||
|
return (
|
||||||
|
<span ref={elementRef} className='badge text-bg-light'>{props.text}</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function TagCloud(props: Props) {
|
export default function TagCloud(props: Props) {
|
||||||
|
const focusRef = useAutoFocus<HTMLDivElement>('tags ' + props.title)
|
||||||
const containerClasses = ['tag-cloud', 'cloud-' + (props.style || 'standard')]
|
const containerClasses = ['tag-cloud', 'cloud-' + (props.style || 'standard')]
|
||||||
return (
|
return (
|
||||||
<Container className={containerClasses.join(' ')}>
|
<Container ref={focusRef} className={containerClasses.join(' ')}>
|
||||||
<h4>{props.icon && (<i className={'bi-' + props.icon}> </i>)}{props.title}</h4>
|
<h4>{props.icon && (<i className={'bi-' + props.icon}> </i>)}{props.title}</h4>
|
||||||
<Container className='tag-badges'>
|
<Container className='tag-badges'>
|
||||||
{props.tags.map((tag: string) => (<Tag key={tag} text={tag} />) )}
|
{props.tags.map((tag: string) => (<Tag key={tag} text={tag} />) )}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { usePersonContext } from '../hooks/PersonContext';
|
import { usePersonContext } from '../hooks/PersonContext';
|
||||||
import JobHistory from './job/JobsHistory';
|
import JobHistory from './JobHistory';
|
||||||
|
|
||||||
export default function WorkExperience() {
|
export default function WorkExperience() {
|
||||||
const person = usePersonContext()
|
const person = usePersonContext()
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Card from 'react-bootstrap/Card';
|
|
||||||
import { Placeholder } from 'react-bootstrap';
|
|
||||||
import JobTags from './JobTags';
|
|
||||||
import { Job } from '@/PersonalDataTypes';
|
|
||||||
import md from '../Markdown';
|
|
||||||
|
|
||||||
export type Props = Job & {
|
|
||||||
heading?: string
|
|
||||||
};
|
|
||||||
|
|
||||||
export function JobCardPlaceholder() {
|
|
||||||
return <Card className='job-card'>
|
|
||||||
<Card.Body>
|
|
||||||
<Placeholder as={Card.Title} animation="glow">
|
|
||||||
<Placeholder xs={6} />
|
|
||||||
</Placeholder>
|
|
||||||
<Placeholder as={Card.Text} animation="glow">
|
|
||||||
<Placeholder xs={7} /> <Placeholder xs={4} /> <Placeholder xs={5} />{' '}
|
|
||||||
<Placeholder xs={4} /> <Placeholder xs={3} />
|
|
||||||
</Placeholder>
|
|
||||||
</Card.Body>
|
|
||||||
</Card>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function JobCard(props: Props) {
|
|
||||||
return (
|
|
||||||
<Card className='job-card'>
|
|
||||||
{props.heading && (
|
|
||||||
<Card.Header>{props.heading}</Card.Header>
|
|
||||||
)}
|
|
||||||
<Card.Body>
|
|
||||||
<Card.Title>{props.position}</Card.Title>
|
|
||||||
<Card.Subtitle>
|
|
||||||
{props.company && <span className='company-name'>{props.company}</span>}
|
|
||||||
{props.company && props.timerange && <span>, </span>}
|
|
||||||
{props.timerange && <span className='timerange'>{props.timerange}</span>}
|
|
||||||
</Card.Subtitle>
|
|
||||||
<Card.Text className='multiline'>{md(props.description)}</Card.Text>
|
|
||||||
{props.tags && <JobTags tags={props.tags} />}
|
|
||||||
</Card.Body>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Container } from "react-bootstrap";
|
|
||||||
import Tag from "../Tag";
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
tags: string[],
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function JobTags(props: Props) {
|
|
||||||
return (
|
|
||||||
<Container className='job-tags' fluid>
|
|
||||||
{props.tags.map((tag, index) => <Tag key={index} text={tag} />)}
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { Job } from '@/PersonalDataTypes';
|
|
||||||
import { Accordion } from 'react-bootstrap';
|
|
||||||
import { JobListProps } from './types';
|
|
||||||
import JobTags from './JobTags';
|
|
||||||
import md from '../Markdown';
|
|
||||||
|
|
||||||
function JobTitle(props: {job: Job, heading?: string}) {
|
|
||||||
const {job, heading} = props
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div><strong>{job.position}</strong></div>
|
|
||||||
<div>{[job.company, job.timerange].filter(x => x).join(', ')}</div>
|
|
||||||
{heading && (<div>({heading})</div>)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AccordionJobItem(props: {job: Job, eventKey: string, heading?: string}) {
|
|
||||||
const {job, eventKey} = props
|
|
||||||
return (
|
|
||||||
<Accordion.Item eventKey={eventKey}>
|
|
||||||
<Accordion.Header><JobTitle job={job} /></Accordion.Header>
|
|
||||||
<Accordion.Body className='multiline'>
|
|
||||||
{md(job.description)}
|
|
||||||
{job.tags && <JobTags tags={job.tags} />}
|
|
||||||
</Accordion.Body>
|
|
||||||
</Accordion.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
heading: string,
|
|
||||||
} & JobListProps
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
entriesPerRow: 2,
|
|
||||||
currentHeading: 'Currently',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function JobsAccordion(props: JobListProps) {
|
|
||||||
const {jobs} = props
|
|
||||||
const config = {...defaultProps, ...props}
|
|
||||||
return (
|
|
||||||
<Accordion defaultActiveKey={jobs.current ? 'current' : 'previous-0'}>
|
|
||||||
{jobs.current && <AccordionJobItem job={jobs.current} heading={config.currentHeading} eventKey={'current'}/>}
|
|
||||||
{jobs.previous?.map((job, index) => <AccordionJobItem key={index} job={job} eventKey={`previous-${index}`} />)}
|
|
||||||
</Accordion>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
|
|
||||||
import React from 'react';
|
|
||||||
import Container from 'react-bootstrap/Container';
|
|
||||||
import Col from 'react-bootstrap/Col';
|
|
||||||
import Row from 'react-bootstrap/Row';
|
|
||||||
import JobCard, { JobCardPlaceholder } from './JobCard';
|
|
||||||
import { JobListProps } from './types';
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
heading: string,
|
|
||||||
} & JobListProps
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
entriesPerRow: 2,
|
|
||||||
currentHeading: 'Currently',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function JobsCardsPlaceholder() {
|
|
||||||
return <Container>
|
|
||||||
<Row><Col><JobCardPlaceholder /></Col></Row>
|
|
||||||
<Row><Col><JobCardPlaceholder /></Col></Row>
|
|
||||||
</Container>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function JobsCards(props: JobListProps) {
|
|
||||||
const {jobs} = props
|
|
||||||
const config = {...defaultProps, ...props}
|
|
||||||
return (
|
|
||||||
<Container fluid>
|
|
||||||
{jobs.current && (
|
|
||||||
<Row>
|
|
||||||
<Col>
|
|
||||||
<JobCard heading={config.currentHeading} {...jobs.current} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
<Row>
|
|
||||||
{jobs.previous?.map((job, index) => (
|
|
||||||
<Col key={index} xs={12} md={12} lg={6} xl={12}>
|
|
||||||
<JobCard {...job} />
|
|
||||||
</Col>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
|
|
||||||
import React from 'react';
|
|
||||||
import Container from 'react-bootstrap/Container';
|
|
||||||
import useSize from '../../hooks/Size';
|
|
||||||
import { JobListProps } from './types';
|
|
||||||
import JobsAccordion from './JobsAccordion';
|
|
||||||
import JobsCards, { JobsCardsPlaceholder } from './JobsCards';
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
heading: string,
|
|
||||||
} & JobListProps
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
entriesPerRow: 2,
|
|
||||||
currentHeading: 'Currently',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function JobHistory(props: Props) {
|
|
||||||
const {SizeWrapper, size} = useSize()
|
|
||||||
|
|
||||||
const jobsList = size.width === 0 ? <JobsCardsPlaceholder /> : (
|
|
||||||
size.width < 600 ? <JobsAccordion {...props} /> : <JobsCards {...props} />)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<h2>{props.heading}</h2>
|
|
||||||
<SizeWrapper>
|
|
||||||
{jobsList}
|
|
||||||
</SizeWrapper>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import { Jobs } from "@/PersonalDataTypes";
|
|
||||||
|
|
||||||
export type JobListProps = {
|
|
||||||
jobs: Jobs,
|
|
||||||
entriesPerRow?: number,
|
|
||||||
currentHeading?: string,
|
|
||||||
}
|
|
@ -13,11 +13,9 @@ body {
|
|||||||
}
|
}
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
filter: opacity(75%)
|
|
||||||
}
|
}
|
||||||
.tiny {
|
.tiny {
|
||||||
font-size: 75%;
|
font-size: 75%;
|
||||||
filter: opacity(75%)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container > h2, h3, h4 {
|
.container > h2, h3, h4 {
|
||||||
@ -31,12 +29,6 @@ body {
|
|||||||
.job-card .timerange {
|
.job-card .timerange {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
.job-card .card-text {
|
|
||||||
margin-left: 0.4rem;
|
|
||||||
}
|
|
||||||
.job-card .card-subtitle {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cloud-primary .tag-badges {
|
.cloud-primary .tag-badges {
|
||||||
font-size: 180%;
|
font-size: 180%;
|
||||||
@ -64,12 +56,3 @@ body {
|
|||||||
.contacts .contact {
|
.contacts .contact {
|
||||||
margin: 0.75em;
|
margin: 0.75em;
|
||||||
}
|
}
|
||||||
.accordion-button.collapsed {
|
|
||||||
background-color: #eff2ff;
|
|
||||||
}
|
|
||||||
.accordion {
|
|
||||||
margin: 1rem;
|
|
||||||
}
|
|
||||||
.multiline {
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
|
||||||
|
69
src/app/hooks/FocusedElement.tsx
Normal file
69
src/app/hooks/FocusedElement.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { useState, useRef, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
|
const focusUrlParamName = 'focus'
|
||||||
|
const focusChangedEventName = "focus-changed"
|
||||||
|
const focusedElementClassName = 'focused-element'
|
||||||
|
|
||||||
|
function triggerElementFocused(elementKey?: string) {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
if (elementKey) {
|
||||||
|
url.searchParams.set(focusUrlParamName, elementKey)
|
||||||
|
} else {
|
||||||
|
url.searchParams.delete(focusUrlParamName)
|
||||||
|
}
|
||||||
|
const focusChangeEvent = new Event(focusChangedEventName)
|
||||||
|
history.pushState({}, "", url);
|
||||||
|
window.dispatchEvent(focusChangeEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFocusedElement<ElementType extends HTMLElement>(elementKey: string) {
|
||||||
|
const [isFocusedElement, setFocusedElement] = useState(false)
|
||||||
|
const focusedClass = isFocusedElement ? focusedElementClassName : ''
|
||||||
|
const elementRef = useRef<ElementType>(null)
|
||||||
|
|
||||||
|
const focusElement = useCallback(() => {
|
||||||
|
triggerElementFocused(isFocusedElement ? undefined : elementKey)
|
||||||
|
}, [isFocusedElement, elementKey])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function updateFocusedState() {
|
||||||
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
const focusedElement = params.get(focusUrlParamName)
|
||||||
|
const focused: boolean = focusedElement && focusedElement == elementKey || false
|
||||||
|
setFocusedElement(focused)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFocusedState()
|
||||||
|
addEventListener(focusChangedEventName, updateFocusedState)
|
||||||
|
return () => {
|
||||||
|
removeEventListener(focusChangedEventName, updateFocusedState)
|
||||||
|
}
|
||||||
|
}, [elementKey, setFocusedElement, focusElement])
|
||||||
|
|
||||||
|
isFocusedElement && elementRef.current?.scrollIntoView()
|
||||||
|
|
||||||
|
return {isFocusedElement, focusedClass, elementRef, focusElement}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAutoFocus<ElementType extends HTMLElement>(elementKey: string) {
|
||||||
|
const {elementRef, focusedClass, focusElement} = useFocusedElement<ElementType>(elementKey);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cleanup = () => {}
|
||||||
|
if (elementRef.current) {
|
||||||
|
const elem = elementRef.current
|
||||||
|
elem.onclick = (evt) => {
|
||||||
|
evt.stopPropagation()
|
||||||
|
focusElement()
|
||||||
|
}
|
||||||
|
const classNameBackup = elem.className
|
||||||
|
elem.className += ' focusable ' + focusedClass
|
||||||
|
cleanup = () => {
|
||||||
|
elem.className = classNameBackup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleanup
|
||||||
|
}, [elementRef, focusedClass, focusElement])
|
||||||
|
|
||||||
|
return elementRef
|
||||||
|
}
|
@ -9,7 +9,7 @@ import Education from './components/Education';
|
|||||||
import { Col, Row } from 'react-bootstrap';
|
import { Col, Row } from 'react-bootstrap';
|
||||||
import Footer from './components/Footer';
|
import Footer from './components/Footer';
|
||||||
import Photo from './components/Photo';
|
import Photo from './components/Photo';
|
||||||
import Projects from './components/Projects';
|
import useSize from './hooks/Size';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
@ -27,7 +27,6 @@ export default function Home() {
|
|||||||
<Row>
|
<Row>
|
||||||
<Col xs={12} xl={7}>
|
<Col xs={12} xl={7}>
|
||||||
<Row><WorkExperience /></Row>
|
<Row><WorkExperience /></Row>
|
||||||
<Row><Projects /></Row>
|
|
||||||
<Row><Education /></Row>
|
<Row><Education /></Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
|
10
src/app/utils.ts
Normal file
10
src/app/utils.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export function partition<T>(array: T[]|undefined, entriesPerRow: number): T[][] {
|
||||||
|
return array ? array.reduce((accumulator: T[][], current: T, index) => {
|
||||||
|
if (index % entriesPerRow == 0) {
|
||||||
|
accumulator[accumulator.length] = [current]
|
||||||
|
} else {
|
||||||
|
accumulator[accumulator.length - 1]
|
||||||
|
}
|
||||||
|
return accumulator
|
||||||
|
}, []) : []
|
||||||
|
}
|
71
src/index.html
Normal file
71
src/index.html
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Bootstrap w/ Webpack</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container py-4 px-3 mx-auto">
|
||||||
|
<header class="d-flex justify-content-between align-items-md-center pb-3 mb-5 border-bottom">
|
||||||
|
<h1 class="h4">
|
||||||
|
<a href="/" class="d-flex align-items-center text-dark text-decoration-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-bootstrap-fill d-inline-block me-2" viewBox="0 0 16 16">
|
||||||
|
<path d="M6.375 7.125V4.658h1.78c.973 0 1.542.457 1.542 1.237 0 .802-.604 1.23-1.764 1.23H6.375zm0 3.762h1.898c1.184 0 1.81-.48 1.81-1.377 0-.885-.65-1.348-1.886-1.348H6.375v2.725z"/>
|
||||||
|
<path d="M4.002 0a4 4 0 0 0-4 4v8a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4V4a4 4 0 0 0-4-4h-8zm1.06 12V3.545h3.399c1.587 0 2.543.809 2.543 2.11 0 .884-.65 1.675-1.483 1.816v.1c1.143.117 1.904.931 1.904 2.033 0 1.488-1.084 2.396-2.888 2.396H5.062z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Webpack</span>
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
<a href="https://github.com/twbs/examples/tree/main/webpack/" target="_blank" rel="noopener">View on GitHub</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<h1>Build Bootstrap with Webpack</h1>
|
||||||
|
<div class="col-lg-8 px-0">
|
||||||
|
<p class="fs-4">You've successfully loaded the Bootstrap + Webpack example! It's loaded up with <a href="https://getbootstrap.com/">Bootstrap 5</a> and uses Webpack to compile and bundle our Sass and JavaScript. It also includes Autoprefixer.</p>
|
||||||
|
<p>If this button appears blue and the link appears purple, you've done it!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-primary me-3" data-bs-toggle="offcanvas" data-bs-target="#offcanvasExample">Toggle offcanvas</button>
|
||||||
|
<a id="popoverButton" class="text-success" href="#" role="button" data-bs-toggle="popover" title="Custom popover" data-bs-content="This is a Bootstrap popover.">Example popover</a>
|
||||||
|
|
||||||
|
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasExample" aria-labelledby="offcanvasExampleLabel">
|
||||||
|
<div class="offcanvas-header">
|
||||||
|
<h5 class="offcanvas-title" id="offcanvasExampleLabel">Offcanvas</h5>
|
||||||
|
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="offcanvas-body">
|
||||||
|
<div>
|
||||||
|
Some text as placeholder. In real life you can have the elements you have chosen. Like, text, images, lists, etc.
|
||||||
|
</div>
|
||||||
|
<div class="dropdown mt-3">
|
||||||
|
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown">
|
||||||
|
Dropdown button
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
||||||
|
<li><a class="dropdown-item" href="#">Action</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#">Another action</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="col-1 my-5 mx-0">
|
||||||
|
|
||||||
|
<h2>Guides</h2>
|
||||||
|
<p>Read more detailed instructions and documentation on using or contributing to Bootstrap.</p>
|
||||||
|
<ul class="icon-list">
|
||||||
|
<li><a href="https://getbootstrap.com/docs/5.2/getting-started/introduction/">Bootstrap quick start guide</a></li>
|
||||||
|
<li><a href="https://getbootstrap.com/docs/5.2/getting-started/webpack/">Bootstrap Webpack guide</a></li>
|
||||||
|
<li><a href="https://getbootstrap.com/docs/5.2/getting-started/parcel/">Bootstrap Parcel guide</a></li>
|
||||||
|
<li><a href="https://getbootstrap.com/docs/5.2/getting-started/vite/">Bootstrap Vite guide</a></li>
|
||||||
|
<li><a href="https://getbootstrap.com/docs/5.2/getting-started/build-tools/">Contributing to Bootstrap</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<hr class="mt-5 mb-4">
|
||||||
|
|
||||||
|
<p class="text-muted">Created and open sourced by the Bootstrap team. Licensed MIT.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user