Compare commits

...
Sign in to create a new pull request.

8 commits

Author SHA1 Message Date
4830462f23 Update text content and adjust English proficiency rating
All checks were successful
forgejo/romanjaros/portfolio/pipeline/head This commit looks good
2025-03-14 09:39:15 +00:00
6246f0bef1 Refactor and simplify component styles and text.
All checks were successful
forgejo/romanjaros/portfolio/pipeline/head This commit looks good
2025-02-27 20:58:26 +00:00
acd06acc83 Add CV download button to Contact component
All checks were successful
forgejo/romanjaros/portfolio/pipeline/head This commit looks good
2025-02-24 13:21:22 +00:00
f056990693 Fix formatting in AboutMe component text styling
All checks were successful
forgejo/romanjaros/portfolio/pipeline/head This commit looks good
2025-01-15 19:42:37 +00:00
620bf3df8c Update skill tags to include hashtags
All checks were successful
forgejo/romanjaros/portfolio/pipeline/head This commit looks good
2025-01-15 19:39:04 +00:00
254eaaef59 Update layout styles and improve component readability
All checks were successful
forgejo/romanjaros/portfolio/pipeline/head This commit looks good
2025-01-15 19:36:28 +00:00
178afb5003 Fix grammar and formatting in jobs.json description
All checks were successful
forgejo/romanjaros/portfolio/pipeline/head This commit looks good
2025-01-15 16:16:59 +00:00
f16f36ec92 Refactor and enhance job-related components and context.
All checks were successful
forgejo/romanjaros/portfolio/pipeline/head This commit looks good
2025-01-15 16:14:26 +00:00
14 changed files with 186 additions and 107 deletions

View file

@ -3,7 +3,7 @@
"name": "Czech Quests",
"started": "July 2024",
"ended": "today",
"description": "Custom project, where I make an addon for World of Warcraft game, which show Czech translation for in-game quests. The translations are made via AI (DeepL and GPT). Text is formated for player and quest giver gender. Keep original names of other NPCs, items or places. I also create a web application where i am able to manage translations",
"description": "Custom project, where I make an addon, for World of Warcraft game, to show Czech translation for in-game quests. The translations are made via AI (DeepL and GPT). Texts are formated for player and quest giver gender. Also it keeps original names of other NPCs, items or places. I also create a web application where i am able to manage translations",
"tags": [
"Typescript",
"React",
@ -63,7 +63,7 @@
"tags": [
"Typescript",
"React",
"Graphql",
"GraphQL",
"REST",
"Mui",
"Azure DevOps"

View file

@ -1,4 +1,4 @@
import {Card} from "../../components/Card";
import {Kbd} from "../../components/Kbd";
export const AboutMe = () => {
const age = Math.floor(
@ -13,9 +13,9 @@ export const AboutMe = () => {
<>
<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.
Hello, my name is Roman Jaroš. I am {age}. <b>{work} years working as software engineer.</b>
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 programming world, I maintain servers with around 80 docker containers.
Outside of the IT world, I enjoy reading self improvement books, meditating,
alternative medicine and model painting or playing video games.
</p>
@ -24,9 +24,11 @@ export const AboutMe = () => {
<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 className="flex flex-row flex-wrap gap-2">
<Kbd>#uidesign</Kbd>
<Kbd>#webdevelopment</Kbd>
<Kbd>#automationtesting</Kbd>
<Kbd>#devops</Kbd>
</div>
</section>
</>

View file

@ -1,4 +1,4 @@
import {GithubIcon, InfoIcon, LinkedinIcon, MailIcon} from "lucide-react";
import {GithubIcon, LinkedinIcon, MailIcon, ScrollTextIcon} from "lucide-react";
export const Contact = () => {
return (
@ -8,12 +8,19 @@ export const Contact = () => {
<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
className="flex gap-2 focus:outline-none font-medium rounded-lg px-5 py-2.5 me-2 mb-2 text-yellow-600 dark:bg-transparent">
<MailIcon/> Hire me
</a>
<a
type="button"
target="_blank"
href="https://cloud.romanjaros.cz/s/HySLxskToRMxM6n"
className="flex gap-2 focus:outline-none font-medium rounded-lg px-5 py-2.5 me-2 mb-2 text-pink-600 dark:bg-transparent">
<ScrollTextIcon/> Download CV
</a>
</p>
<p className="flex justify-center gap-4 my-8">
<a href="mailto:info@romanjaros.dev"><InfoIcon/></a>
<a href="mailto:hello@romanjaros.dev"><MailIcon/></a>
<a
target="_blank"
href="http://linkedin.com/in/roman-jaroš-16a687139"

View file

@ -1,54 +1,55 @@
"use client"
import {MapPinIcon, MoveRightIcon} from "lucide-react";
import {notNil} from "../../utils";
import {Job} from "../type";
import {FC} from "react";
import {notNil} from "../../utils/notNil";
import {useJobs} from "../context/useJobs";
type JobsProps = {
jobs: Job[] | undefined
}
export const Jobs: FC<JobsProps> = ({jobs}) => {
export const Jobs = () => {
const {jobs, filter} = useJobs()
return (
<>
<section className="px-4">
<ol className="relative border-s border-gray-200 dark:border-gray-700">
{jobs?.filter(notNil).map(({name, started, ended, description, tags, link}, index) => (
<li key={index} className="mb-10 ms-6">
{jobs
?.filter(notNil)
.filter(({tags}) => filter != undefined ? tags.includes(filter) : true)
.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>
<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>
}
<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"
>
{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>
))}
))}
</p>
</li>
))}
</ol>
</section>
</>

View file

@ -1,40 +1,53 @@
import {notNil} from "../../utils";
"use client"
import {notNil} from "../../utils/notNil";
import {StarIcon} from "lucide-react";
import {FC} from "react";
import {Job} from "../type";
import {useJobs} from "../context/useJobs";
type SkillsProps = {
jobs: Job[] | undefined
}
export const Skills: FC<SkillsProps> = ({jobs}) => {
const skills = new Set(jobs?.filter(notNil).flatMap(({tags}) => tags).sort());
export const Skills: FC = () => {
const {jobs, filterJobs, filter} = useJobs()
const skills = new Set(jobs.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>
<p className="mr-2 w-24 ms-1 text-sm font-medium text-gray-500 dark:text-gray-400">Czech, native</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"/>
<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>
<p className="mr-2 w-24 ms-1 text-sm font-medium text-gray-500 dark:text-gray-400">English, B1</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"/>
<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"/>
<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>
<div className="flex flex-wrap gap-2">
{Array.from(skills)?.map((name) => (
<span
key={name}
onClick={() => filterJobs(name)}
className={[
"cursor-pointer text-xs font-medium me-2 px-2.5 py-0.5 rounded-full select-none",
filter === name
? "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"
: "bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300"
].join(" ")}>
{name}
</span>
))}
</div>
<p className="mt-5 text-xs text-gray-400">Psss, you can click on pill to filter.</p>
</section>
</>
)

View file

@ -1,12 +1,9 @@
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>
<div className="flex justify-center text-4xl sm:text-6xl my-8 text-slate-400">
<h1>
<span className="text-gray-600 dark:text-white">Just</span>
<span className="text-orange-800 dark:text-orange-400">Roman</span>
Roman Jaroš
</h1>
<span className="text-orange-800 dark:text-orange-400">&nbsp;/&gt;</span>
</div>
)
}

View file

@ -0,0 +1,25 @@
"use client"
import {createContext, ReactNode, FC, useState, Dispatch, SetStateAction} from "react";
import {Job} from "../type";
type JobsContextPayload = {
data: Job[]
filter?: string
setFilter: Dispatch<SetStateAction<string | undefined>>
} | undefined
export const JobsContext = createContext<JobsContextPayload>(undefined);
type JobsContextProviderProps = {
jobs: Job[]
children: ReactNode[]
}
export const JobsProvider: FC<JobsContextProviderProps> = ({children, jobs: data}) => {
const [filter, setFilter] = useState<string>()
return (
<JobsContext value={{data, filter, setFilter}}>{children}</JobsContext>
)
}

View file

@ -0,0 +1,25 @@
"use client"
import {useContext} from "react";
import {JobsContext} from "./JobsContext";
export const useJobs = () => {
const context = useContext(JobsContext)
if (!context) {
throw new Error('useJobs must be used within a JobsProvider!');
}
const handleFilterJob = (name: string) => {
if (name === context.filter) {
context.setFilter(undefined)
} else {
context.setFilter(name)
}
}
return {
jobs: context.data,
filter: context?.filter,
filterJobs: handleFilterJob,
}
}

6
src/app/fetch.ts Normal file
View file

@ -0,0 +1,6 @@
import {Job} from "./type";
export const fetchJobs = async (): Promise<Job[] | undefined> => {
const res = await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/jobs.json`, {cache: "no-store"})
return await res?.json();
}

View file

@ -8,13 +8,16 @@ export default function AppLayout({
}) {
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>
)
<head>
<meta charSet="utf-8"/>
<title>Roman Jaroš</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script defer src="https://wa.romanjaros.dev/script.js"
data-website-id="27410757-9ce4-4dad-a0ad-a1e2a0303495"></script>
</head>
<body className="bg-white dark:bg-slate-800">
<main>{children}</main>
</body>
</html>
)
}

View file

@ -4,21 +4,24 @@ import {Contact} from "./components/Contact";
import {Footer} from "./components/Footer";
import {Jobs} from "./components/Jobs";
import {Skills} from "./components/Skills";
import {Job} from "./type";
import {fetchJobs} from "./fetch";
import {JobsProvider} from "./context/JobsContext";
import {use} from "react";
export default async function Page() {
export default function Page() {
const res = await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/jobs.json`, {cache: "no-store"})
const data: Job[] | undefined = await res?.json();
const jobs = use(fetchJobs())
return (
<>
<div className="flex flex-col gap-y-16 md:w-3/4 lg:w-2/3 mx-auto p-4 mb-20">
<div style={{maxWidth: 1000}} className="flex flex-col gap-y-16 md:w-4/5 mx-auto p-4 mb-20">
<Title />
<AboutMe />
<Contact />
<Skills jobs={data} />
<Jobs jobs={data} />
<JobsProvider jobs={jobs ?? []}>
<Skills />
<Jobs />
</JobsProvider>
<Footer />
</div>
</>

View file

@ -1,17 +0,0 @@
import {FC} from "react";
type CardProps = {
title: string;
description: string;
}
export const Card: FC<CardProps> = ({title, description}) => {
return (
<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>
)
}

14
src/components/Kbd.tsx Normal file
View file

@ -0,0 +1,14 @@
import {FC} from "react";
type KbdProps = {
children: string
}
export const Kbd: FC<KbdProps> = ({children}) => {
return (
<kbd
className="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500">
{children}
</kbd>
)
}