Upgrade to latest nodejs and completely rework website

This commit is contained in:
Roman Jaroš 2025-01-11 19:11:20 +00:00
parent 07270c3aa6
commit 0372c4cd77
38 changed files with 4135 additions and 6933 deletions

View file

@ -0,0 +1,34 @@
import {Card} from "../../components/Skills";
export const AboutMe = () => {
const age = Math.floor(
((new Date() as any) - (new Date("1993-07-11") as any)) / 31557600000
);
const work = Math.floor(
((new Date() as any) - (new Date("2012-12-01") as any)) / 31557600000
);
return (
<>
<section className="leading-snug text-slate-500 dark:text-slate-400">
<p>
👋, my name is Roman Jaroš. I am {age}. {work} years working as software engineer.
I have been programming since I was 15 years old and still love to learn new technologies.
Outside of the programming world, I maintain servers with about 120 docker containers.
Outside of the IT world, I enjoy reading self improvement books, meditating,
alternative medicine and model painting or playing video games.
</p>
<br/>
<p>
<b>I am contractor and currently only for full remote jobs.</b>
</p>
<br />
<div className="flex justify-center flex-col md:flex-row gap-2">
<Card title="Web development" description="I offer my experience in web development." />
<Card title="UI/UX Design" description="I offer design web application in figma or penpot." />
</div>
</section>
</>
);
};

View file

@ -0,0 +1,31 @@
import {GithubIcon, InfoIcon, LinkedinIcon, MailIcon} from "lucide-react";
export const Contact = () => {
return (
<>
<section className="leading-relaxed">
<p className="flex justify-center">
<a
type="button"
href="mailto:sales@romanjaros.dev"
className="flex gap-2 focus:outline-none text-white bg-yellow-400 hover:bg-yellow-500 focus:ring-4 focus:ring-yellow-300 font-medium rounded-lg px-5 py-2.5 me-2 mb-2 dark:text-slate-600 dark:focus:ring-yellow-900">
<MailIcon/> Send e-mail
</a>
</p>
<p className="flex justify-center gap-4 my-8">
<a href="mailto:info@romanjaros.dev"><InfoIcon/></a>
<a
target="_blank"
href="http://linkedin.com/in/roman-jaroš-16a687139"
rel="noreferrer"
>
<LinkedinIcon/>
</a>
<a target="_blank" href="https://forgejo.romanjaros.dev" rel="noreferrer">
<GithubIcon/>
</a>
</p>
</section>
</>
);
};

View file

@ -0,0 +1,20 @@
export const Footer = () => {
return (
<>
<footer className="text-slate-800 p-2 my-2 leading-relaxed text-sm dark:text-slate-400">
<p>
IČO 08738734, DIČ CZ9307111946 |
Bražec 50, Bochov 36471, Česká Republika
</p>
<p>
<b>Jsem plátce DPH.</b> Fyzická osoba zapsaná v živnostenském
rejstříku v Karlových Varech (CZ0412) od 02.12.2019.
</p>
<p className="mt-2">
This website does not use cookies.
The measurement of site traffic is completely anonymous.
</p>
</footer>
</>
);
};

View file

@ -0,0 +1,62 @@
import {ArrowLeftIcon, ExternalLinkIcon, MapPinIcon, MoveRightIcon, SquareArrowOutUpRightIcon} from "lucide-react";
import {notNil} from "../../utils";
type Job = {
name: string,
started: string,
ended: string,
description: string,
tags: string[],
link?: string
}
export const Jobs = async () => {
const res = await fetch("http://localhost:3000/jobs.json")
const data: Job[] | undefined = await res?.json();
return (
<>
<section className="px-4">
<ol className="relative border-s border-gray-200 dark:border-gray-700">
{data?.filter(notNil).map(({name, started, ended, description, tags, link}, index) => (
<li key={index} className="mb-10 ms-6">
<span
className="absolute flex items-center justify-center w-6 h-6 bg-white rounded-full -start-3 ring-8 ring-white dark:ring-slate-800 dark:bg-slate-800">
<MapPinIcon className="text-black dark:text-white"/>
</span>
<h3 className="flex items-center gap-2 mb-1 text-lg font-semibold text-gray-900 dark:text-white">
{name}
</h3>
<time className="block mb-2 text-sm font-normal leading-none text-gray-400 dark:text-gray-500">
{started} - {ended}
</time>
<p className="mb-4 text-base font-normal text-gray-500 dark:text-gray-400">
{description}
</p>
{link != undefined &&
<p>
<a href={link}
target="_blank"
className="inline-flex gap-2 mb-4 items-center px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
Visit website
<MoveRightIcon />
</a>
</p>
}
<p className="flex flex-wrap gap-2">
{tags.sort().map((name) => (
<span
key={name}
className="bg-purple-100 text-purple-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-full dark:bg-purple-900 dark:text-purple-300"
>
{name}
</span>
))}
</p>
</li>
))}
</ol>
</section>
</>
)
}

View file

@ -0,0 +1,41 @@
import {notNil} from "../../utils";
import {StarIcon} from "lucide-react";
type Job = {
tags: string[]
}
export const Skills = async () => {
const res = await fetch("http://localhost:3000/jobs.json")
const data: Job[] | undefined = await res?.json();
const skills = new Set(data?.filter(notNil).flatMap(({tags}) => tags).sort());
return (
<>
<section>
<div className="flex items-center my-2">
<p className="mr-2 w-12 ms-1 text-sm font-medium text-gray-500 dark:text-gray-400">Czech</p>
<StarIcon fill="#fde047" className="w-4 h-4 ms-1 text-yellow-300"/>
<StarIcon fill="#fde047" className="w-4 h-4 ms-1 text-yellow-300"/>
<StarIcon fill="#fde047" className="w-4 h-4 ms-1 text-yellow-300"/>
</div>
<div className="flex items-center my-2">
<p className="mr-2 w-12 ms-1 text-sm font-medium text-gray-500 dark:text-gray-400">English&emsp;</p>
<StarIcon fill="#fde047" className="w-4 h-4 ms-1 text-yellow-300"/>
<StarIcon className="w-4 h-4 ms-1 text-gray-300 dark:text-gray-500"/>
<StarIcon className="w-4 h-4 ms-1 text-gray-300 dark:text-gray-500"/>
</div>
</section>
<section className="flex flex-wrap gap-2">
{Array.from(skills)?.map((name) => (
<span
key={name}
className="bg-indigo-100 text-indigo-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-full dark:bg-indigo-900 dark:text-indigo-300">
{name}
</span>
))}
</section>
</>
)
}

View file

@ -0,0 +1,12 @@
export const Title = () => {
return (
<div className="flex justify-center text-4xl sm:text-6xl my-8">
<span className="text-gray-600 dark:text-white">&lt;</span>
<h1>
<span className="text-gray-600 dark:text-white">Just</span>
<span className="text-orange-800 dark:text-orange-400">Roman</span>
</h1>
<span className="text-orange-800 dark:text-orange-400">&nbsp;/&gt;</span>
</div>
)
}

View file

@ -1,7 +1,9 @@
@import '@treejs/styles/global.css';
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
@apply bg-white text-black text-sm;

20
src/app/layout.tsx Normal file
View file

@ -0,0 +1,20 @@
import {ReactNode} from "react";
import "./globals.css"
export default function AppLayout({
children,
}: {
children: ReactNode
}) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body className="bg-white dark:bg-slate-800">
<main>{children}</main>
</body>
</html>
)
}

21
src/app/page.tsx Normal file
View file

@ -0,0 +1,21 @@
import {Title} from "./components/Title";
import {AboutMe} from "./components/AboutMe";
import {Contact} from "./components/Contact";
import {Footer} from "./components/Footer";
import {Jobs} from "./components/Jobs";
import {Skills} from "./components/Skills";
export default function Page() {
return (
<>
<div className="flex flex-col gap-y-16 md:w-3/4 lg:w-2/3 mx-auto p-4 mb-20">
<Title />
<AboutMe />
<Contact />
<Skills />
<Jobs />
<Footer />
</div>
</>
)
}

View file

@ -1,51 +0,0 @@
import { FC } from "react";
import MyTitle from "../components/MyTitle";
import MySection from "./MySection";
const AboutMe: FC = () => {
const age = Math.floor(
((new Date() as any) - (new Date("1993-07-11") as any)) / 31557600000
);
const work = Math.floor(
((new Date() as any) - (new Date("2012-12-01") as any)) / 31557600000
);
return (
<MySection iconName="me">
<MyTitle left="Dobrý" right="Den" />
<div className="mt-10">
Jmenuji se <b>Roman Jaroš</b>.
</div>
<div className="mt-5">
<p>
Je mi <b>{age} let</b> a již <b>{work} let</b> se profesionálně věnuji
vývoji a správě webových aplikací.
</p>
<p className="mt-3">
Programování se věnuji od svých 15 let. dosažené vzdělání je
Střední lesnická škola zakončené s maturitou. Fakt, že mám vystudováný
zcela jiný obor, jen podtrhuje nabité znalosti a zkušenosti. Osobně
si i myslím, že na vzdělání tolik nezáleží, pokud je práce i zábavou.
Stále se rád učím novým technologiím a snažím se je aplikovat ve svém
FW nebo na nových projektech.
</p>
<p className="mt-3">
Rád se vzdělávám i mimo svět programování. Doma mám dva Intel NUC
počítače, na kterých mi běží plno aplikací. Na těchto serverech se
učím pracovat s aplikacemi pro CI/CD, monitorování různých aktivit a
zapracování bezpečnostním prvků.
</p>
<p className="mt-3">
Mimo svět IT se rád zabívám čtením knih o osobním rozvoji, meditacím a
alternativní medicíně.
</p>
<p className="mt-3">
<b>Aktuálně spolupracuji pouze na IČO.</b>
</p>
</div>
</MySection>
);
};
export default AboutMe;

View file

@ -1,47 +0,0 @@
import { FC } from "react";
import MySection from "./MySection";
import MyTitle from "./MyTitle";
const Contact: FC = () => {
return (
<MySection iconName="contact">
<MyTitle left="Můj" right="Kontakt" />
<div className="mt-10">
<p className="mb-2">
Obchodní nabídky prosím zasílejte&nbsp;
<a href="mailto:sales@romanjaros.dev">sem</a>.
</p>
<p className="mb-2">
Jiné dotazy prosím <a href="mailto:info@romanjaros.dev">sem</a>.
</p>
<p className="mt-4">
<a
target="_blank"
href="http://linkedin.com/in/roman-jaroš-16a687139"
rel="noreferrer"
>
LinkedIn
</a>
</p>
<p className="mt-2">
<a target="_blank" href="https://git.romanjaros.dev" rel="noreferrer">
Gitea (Git)
</a>
</p>
<p className="mt-10">
Pokud potřebujete zadat tiket na podporu, použijte prosím&nbsp;
<a
target="_blank"
href="https://helpdesk.romanjaros.dev/index.php?a=add"
rel="noreferrer"
>
tento formulář
</a>
.
</p>
</div>
</MySection>
);
};
export default Contact;

View file

@ -1,36 +0,0 @@
import { Grid, GridCol } from "@treejs/components/Grid";
import { FC } from "react";
const Footer: FC = () => {
return (
<footer className="bg-slate-300 p-2">
<Grid cols={2} className="mx-auto w-full lg:w-3/5 md:w-4/5">
<GridCol>
<p>IČO 08738734</p>
<p>DIČ CZ9307111946</p>
</GridCol>
<GridCol>
<p>
Bražec 50
<br />
Bochov 36471
<br />
Česká Republika
</p>
</GridCol>
<GridCol colSpan={2}>
<p>
<b>Jsem plátce DPH.</b> Fyzická osoba zapsaná v živnostenském
rejstříku v Karlových Varech (CZ0412) od 02.12.2019.
</p>
<p className="mt-2">
Tento web nepoužívá cookies. Měření návšťěvnosti webu je zcela
anonymní a probíhá pouze na službě vlastníka webu.
</p>
</GridCol>
</Grid>
</footer>
);
};
export default Footer;

View file

@ -1,54 +0,0 @@
import { isNil, map } from "ramda";
import { FC, Fragment, useMemo } from "react";
import { projects } from "../constants/projects";
import MySection from "./MySection";
import MyTitle from "./MyTitle";
const FullStory: FC = () => {
const projectsRender = useMemo(() => {
let i = 0;
return map((project) => {
i++;
return (
<Fragment key={i}>
<div className="border-timeline border-dotted border-l-4 pl-4 my-2">
<p className="mb-2 font-bold">{project.name}</p>
<p className="mb-2 font-thin">
{project.dateFrom} - {project.dateTo}
</p>
<p>
{project.desciption}
{!isNil(project.link) ? (
<>
<a
href={project.link}
target="_blank"
rel="noreferrer"
className="ml-1"
>
Odkaz na projekt
</a>
.
</>
) : null}
</p>
</div>
{i === projects.length ? null : (
<svg height="20" width="20" className="-ml-2">
<circle cx="10" cy="10" r="9" fill="#B25068" />
</svg>
)}
</Fragment>
);
}, projects);
}, []);
return (
<MySection iconName="story">
<MyTitle left="Na" right="Projektech" />
<div className="mt-10">{projectsRender}</div>
</MySection>
);
};
export default FullStory;

View file

@ -1,38 +0,0 @@
import React, { FC, ReactElement } from "react";
import { Grid, GridCol } from "@treejs/components/Grid";
import Image from "next/image";
import { GRID_SIZE } from "@treejs/components/Grid/types";
type IProps = {
iconName: string;
children: ReactElement | ReactElement[];
};
const MySection: FC<IProps> = ({ iconName, children }) => {
return (
<section>
<Grid
cols={{ [GRID_SIZE.SM]: 1, [GRID_SIZE.LG]: 6 }}
className="mb-28 mx-4"
>
<GridCol colSpan={1} className="text-center">
<Image
src={`/${iconName}.svg`}
alt={iconName}
width={200}
height={200}
objectFit="contain"
/>
</GridCol>
<GridCol
colSpan={{ [GRID_SIZE.SM]: 1, [GRID_SIZE.LG]: 5 }}
className="lg:ml-10"
>
{children}
</GridCol>
</Grid>
</section>
);
};
export default MySection;

View file

@ -1,23 +0,0 @@
import React, { FC } from "react";
import cx from "classnames";
type IProps = {
left: string;
right: string;
className?: string;
};
const MyTitle: FC<IProps> = ({ left, right, className }) => {
return (
<div className={cx("mytitle", className)}>
<span className="mytitle-left">&lt;</span>
<h2>
<span className="mytitle-left">{left}</span>
<span className="mytitle-right">{right}</span>
</h2>
<span className="mytitle-left">&nbsp;/&gt;</span>
</div>
);
};
export default MyTitle;

View file

@ -1,81 +1,17 @@
import { Grid, GridCol } from "@treejs/components/Grid";
import { FC, useMemo } from "react";
import { skills } from "../constants/skills";
import MyTitle from "./MyTitle";
import { map } from "ramda";
import { ISkill } from "../types/skills";
import MySection from "./MySection";
import { GRID_SIZE } from "@treejs/components/Grid/types";
import {FC} from "react";
const Skills: FC = () => {
const renderSkills = useMemo(() => {
let i = 0;
return map((skillList: ISkill[]) => {
let z = 0;
const skills = map((skill: ISkill) => {
z++;
return (
<li className="flex mb-1" key={z}>
<svg height="10" width="10" className="mt-1.5 mr-2">
<circle
cx="5"
cy="5"
r="4"
fill={skill.level === "full" ? "#90C8AC" : "#FFE7BF"}
/>
</svg>
{skill.name}
</li>
);
})(skillList);
i++;
return (
<ol className="mt-2" key={i}>
{skills}
</ol>
);
})(skills);
}, []);
type CardProps = {
title: string;
description: string;
}
export const Card: FC<CardProps> = ({title, description}) => {
return (
<MySection iconName="skills">
<MyTitle left="Umím" right="Ovládat" />
<Grid cols={{ [GRID_SIZE.SM]: 4 }} className="mt-10 grid-cols-2">
<GridCol>
<b>Vývoj FE</b>
{renderSkills[0]}
</GridCol>
<GridCol>
<b>Vývoj BE</b>
{renderSkills[1]}
</GridCol>
<GridCol>
<b>Další</b>
{renderSkills[2]}
</GridCol>
<GridCol>
<b>Správa serveru</b>
{renderSkills[3]}
</GridCol>
</Grid>
<div className="flex mt-6 text-sm">
<div className="flex">
<svg height="10" width="10" className="mt-1.5 mr-2">
<circle cx="5" cy="5" r="4" fill="#90C8AC" />
</svg>
používám na projektech
</div>
<div className="flex ml-5">
<svg height="10" width="10" className="mt-1.5 mr-2">
<circle cx="5" cy="5" r="4" fill="#FFE7BF" />
</svg>
hraju si a učím se
</div>
</div>
</MySection>
);
};
export default Skills;
<div
className="w-full md:w-80 block p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700"
>
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">{title}</h5>
<p className="font-normal text-gray-700 dark:text-gray-400">{description}</p>
</div>
)
}

View file

@ -1,33 +0,0 @@
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import Skeleton from "@treejs/components/Skeleton";
import store from "../redux/store";
import { Provider } from "react-redux";
import Footer from "../components/Footer";
function PortolioApp({ Component, pageProps }: AppProps) {
const router = useRouter();
return (
<Provider store={store}>
<Skeleton
history={router}
menuItems={{}}
enabledMenu={{
sidebar: false,
user: false,
}}
components={{
footer: <Footer />,
}}
>
<Component {...pageProps} />
</Skeleton>
</Provider>
);
}
export default PortolioApp;

View file

@ -1,83 +0,0 @@
import type { NextPage } from "next";
import AboutMe from "../components/AboutMe";
import Skills from "../components/Skills";
import Contact from "../components/Contact";
import FullStory from "../components/FullStory";
import { NextSeo, WebPageJsonLd } from "next-seo";
import Head from "next/head";
import Script from "next/script";
const date = new Date().toISOString();
const Home: NextPage = () => {
return (
<>
<Script
defer
type="text/javascript"
data-website-id="27410757-9ce4-4dad-a0ad-a1e2a0303495"
src="https://wa.romanjaros.dev/script.js"
/>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
<WebPageJsonLd
description="Roman Developer"
id="https://www.romanjaros.cz"
lastReviewed={date}
reviewedBy={{
type: "Person",
name: "Roman",
}}
/>
<NextSeo
title="Roman - Frontend developer"
description="Primárně se zaměřuji na vývoj frontendů webových aplikací. Jsem schopný dělat vývoj backedů webových aplikací a učím se udržovat vlastní server pro dev stack."
canonical="https://www.romanjaros.cz"
openGraph={{
url: "https://www.romanjaros.cz",
type: "websites",
title: "Roman Jaroš, Developer",
description:
"Portfolio web frontent and backend vývojáře Roman Jaroš",
site_name: "Roman Developer",
locale: "Czech Republic",
images: [
{
url: "https://www.romanjaros.cz/icon.svg",
alt: "Roman Developer",
},
],
}}
additionalLinkTags={[
{
rel: "icon",
href: "https://www.romanjaros.cz/favicon.ico",
},
{
rel: "apple-touch-icon",
href: "https://www.romanjaros.cz/icon.svg",
sizes: "76x76",
},
]}
/>
<div className="mx-auto w-full md:w-4/5">
<header className="mytitle text-3xl lg:text-6xl justify-center text-center mb-20">
<span className="mytitle-left">&lt;</span>
<h1>
<span className="mytitle-left">Roman</span>
<span className="mytitle-right">Developer</span>
</h1>
<span className="mytitle-left">&nbsp;/&gt;</span>
</header>
<AboutMe />
<Skills />
<Contact />
<FullStory />
</div>
</>
);
};
export default Home;

View file

@ -1,25 +0,0 @@
import { combineReducers } from "redux";
import { MODAL_REDUCER_NAME } from "@treejs/components/Modal/constants";
import modalReducer from "@treejs/components/Modal/reducer";
import { TOASTER_REDUCER_NAME } from "@treejs/components/Toaster/constants";
import toasterReducer from "@treejs/components/Toaster/reducer";
import { ROOT_REDUCER_NAME } from "@treejs/constants/redux";
import { configureStore } from "@reduxjs/toolkit";
type ITreejsReducer = {
[MODAL_REDUCER_NAME]: typeof modalReducer;
[TOASTER_REDUCER_NAME]: typeof toasterReducer;
};
const store = configureStore({
reducer: combineReducers({
[ROOT_REDUCER_NAME]: combineReducers<ITreejsReducer>({
[MODAL_REDUCER_NAME]: modalReducer,
[TOASTER_REDUCER_NAME]: toasterReducer,
}),
}),
devTools: process.env.NEXT_PUBLIC__IS_DEV === "true",
});
export default store;

View file

@ -1,22 +0,0 @@
const title = (theme) => ({
".mytitle": {
"font-size": "30px",
"font-weight": "bold",
"padding-top": "40px",
display: "flex",
"&-left": {
color: theme("colors.black"),
},
"&-right": {
color: theme("colors.primary.DEFAULT"),
},
},
});
module.exports = ({ addComponents, theme }) => {
addComponents(title(theme));
};
module.exports.title = title;

View file

@ -1,14 +0,0 @@
const title = () => ({
".page-main": {
".content": {
padding: 0,
margin: 0,
},
},
});
module.exports = ({ addComponents, theme }) => {
addComponents(title(theme));
};
module.exports.title = title;

View file

@ -1,27 +0,0 @@
/** @type {import('tailwindcss').Config} */
const path = require("path");
const plugin = require("tailwindcss/plugin");
module.exports = {
presets: [require("@treejs/styles/tailwind.config")],
content: [
...require("@treejs/styles/tailwind.config").content,
path.resolve(""), // path to place, where are used tailwind styles
],
safelist: require("@treejs/styles/tailwind.config").safelist,
theme: {
extend: {
colors: {
timeline: "var(--color-timeline)",
},
},
},
variants: {
extend: {},
},
plugins: [
plugin(require("./plugins/mytitle")),
plugin(require("./plugins/sketeton")),
],
};

5
src/utils.ts Normal file
View file

@ -0,0 +1,5 @@
export function notNil<TValue>(
value: TValue | null | undefined,
): value is TValue {
return value !== null && value !== undefined;
}