Big update

This commit is contained in:
HangerThem 2024-08-11 20:09:50 +02:00
parent 709ac3929b
commit a4299d0fc4
26 changed files with 5653 additions and 4733 deletions

View file

@ -1,3 +1,3 @@
{
"extends": "next/core-web-vitals"
"extends": "next/core-web-vitals"
}

View file

@ -1,5 +1,5 @@
{
"tabWidth": 2,
"useTabs": false,
"useTabs": true,
"semi": false
}

View file

@ -1,36 +1,44 @@
import type { Metadata } from "next"
import { GlobalStyle } from "@/styles/globalStyle"
import StyledComponentsRegistry from "@/lib/registry"
import Navbar from "@/components/navbar"
import StyledComponentsRegistry from "@/lib/registry"
import { K2D } from "next/font/google"
const k2d = K2D({
weight: ["400", "500", "600"],
adjustFontFallback: true,
subsets: ["latin"],
})
export const metadata: Metadata = {
title: "HangerThem",
description: "Personal portfolio of HangerThem (Frank Borisjuk)",
authors: [
{
name: "Frank Borisjuk",
url: "https://hangerthem.com",
},
],
creator: "Frank Borisjuk",
publisher: "Frank Borisjuk",
keywords: ["Portfolio", "Personal", "Frank Borisjuk", "HangerThem"],
title: "HangerThem",
description: "Personal portfolio of HangerThem (Frank Borisjuk)",
authors: [
{
name: "Frank Borisjuk",
url: "https://github.com/HangerThem",
},
],
creator: "Frank Borisjuk",
publisher: "Frank Borisjuk",
keywords: ["Portfolio", "Personal", "Frank Borisjuk", "HangerThem"],
}
export default function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode
children: React.ReactNode
}>) {
return (
<StyledComponentsRegistry>
<html lang="en">
<GlobalStyle />
<body>
<Navbar />
{children}
</body>
</html>
</StyledComponentsRegistry>
)
return (
<StyledComponentsRegistry>
<html lang="en" className={k2d.className}>
<GlobalStyle />
<body>
<Navbar />
{children}
</body>
</html>
</StyledComponentsRegistry>
)
}

View file

@ -1,19 +1,19 @@
import HeroBanner from "@/components/heroBanner"
import About from "@/components/about"
import Divider from "@/components/divider"
import Techstack from "@/components/techstack"
import Skills from "@/components/skills"
import Projects from "@/components/projects"
export default function Home() {
return (
<>
<HeroBanner />
<About />
<Divider />
<Techstack />
<Divider />
<Projects />
<Divider />
</>
)
return (
<>
<HeroBanner />
<About />
<Divider />
<Skills />
<Divider />
<Projects />
<Divider />
</>
)
}

View file

@ -2,80 +2,166 @@
import styled, { keyframes } from "styled-components"
import Image from "next/image"
const AboutContainer = styled.section`
width: 100%;
height: 100vh;
padding: 2rem 0;
display: flex;
align-items: center;
justify-content: center;
gap: 2rem;
`
import confetti from "canvas-confetti"
import Link from "next/link"
const wave = keyframes`
0% {
transform: rotate(-15deg);
}
50% {
transform: rotate(15deg);
}
100% {
transform: rotate(-15deg);
}
0% {
transform: rotate(-15deg);
}
50% {
transform: rotate(15deg);
}
100% {
transform: rotate(-15deg);
}
`
const AboutContainer = styled.section`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
gap: 2rem;
min-height: 100vh;
@media (min-width: 768px) {
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 4rem;
}
`
const AboutContent = styled.div`
width: 50%;
display: flex;
flex-direction: column;
gap: 2rem;
display: flex;
flex-direction: column;
gap: 2rem;
h2 {
font-size: 3rem;
font-weight: 600;
position: relative;
h2 {
font-size: 1.75rem;
margin-left: 2.5rem;
font-weight: 600;
position: relative;
&::before {
content: "👋";
position: absolute;
left: -4rem;
transform-origin: 70% 70%;
animation: ${wave} 1s ease-in-out infinite;
}
}
&::before {
content: "👋";
position: absolute;
left: -3rem;
transform-origin: 70% 70%;
animation: ${wave} 1s ease-in-out infinite;
}
}
p {
font-size: 1.2rem;
font-weight: 400;
max-width: 600px;
}
p {
font-size: 1.2rem;
font-weight: 400;
a {
color: rgb(var(--primary));
font-weight: 500;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
@media (min-width: 768px) {
width: 50%;
h2 {
font-size: 2.5rem;
}
}
`
const AboutImage = styled.div`
img {
object-fit: cover;
object-position: center;
border-radius: 1rem;
}
display: flex;
align-items: center;
justify-content: center;
img {
object-fit: cover;
object-position: center;
border-radius: 1rem;
width: 100%;
height: 100%;
max-width: 500px;
}
@media (min-width: 768px) {
width: 50%;
}
`
export default function About() {
return (
<AboutContainer id="about">
<AboutContent>
<h2>Hi! My name is Frank! </h2>
<p>
I am a passionate and innovative web developer based in Czech
Republic. Ever since I laid hands on my first line of code, I&apos;ve
been captivated by the boundless possibilities of the web. With a
curious mind and a desire to stay on the cutting edge of technology, I
constantly seek new challenges that push my skills to the next level.
</p>
</AboutContent>
<AboutImage>
<Image src="/borisjuk_01.png" alt="About us" width={500} height={500} />
</AboutImage>
</AboutContainer>
)
return (
<AboutContainer id="about">
<AboutContent>
<h2>Hi! My name is Frank! </h2>
<p>
Hey there! I&apos;m Frank Borisjuk, a software developer from Czechia
who loves working with tech.
</p>
<p>
I&apos;ve built{" "}
<Link
href="https://github.com/HangerThem/linkboard"
referrerPolicy="no-referrer"
target="_blank"
>
Linkboard
</Link>
, a cool alternative to Linktree, and I&apos;m currently developing{" "}
<Link
href="https://github.com/HangerThem/ephemr"
referrerPolicy="no-referrer"
target="_blank"
>
Ephemr
</Link>
, a simple social network. I specialize in web development with skills
in JavaScript, TypeScript, React, Next.js, Node.js, and Express.js. I
also know my way around Java, C#, C, and C++.
</p>
<p>
I believe in being precise and efficient, but I also think work should
be fun and fulfilling. I keep up with the latest trends by reading
forums and browsing GitHub.
</p>
<p>
What excites me most about programming is its endless possibilities
and practical applications. Plus, let&apos;s be honest, the money
isn&apos;t bad either. While web development is my main focus,
I&apos;m always ready to dive into new challenges.
</p>
<p>
Thanks for visiting, and feel free to reach out if you want to connect
or collaborate!
</p>
</AboutContent>
<AboutImage>
<Image
src="/borisjuk_01.png"
alt="About us"
width={500}
height={500}
onClick={(e) => {
confetti({
particleCount: 100,
spread: 360,
startVelocity: 30,
origin: {
x: e.clientX / window.innerWidth,
y: e.clientY / window.innerHeight,
},
})
}}
/>
</AboutImage>
</AboutContainer>
)
}

View file

@ -2,35 +2,40 @@
import styled from "styled-components"
const DividerContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 1rem 0;
`
const DividerLine = styled.div`
width: 45%;
height: 1px;
background-color: rgba(var(--white), 0.5);
border: none;
width: 40%;
height: 1px;
background-color: rgb(var(--white));
border: none;
@media (min-width: 768px) {
width: 20%;
}
`
const DividerSquare = styled.div`
width: 1rem;
height: 1rem;
transform: rotate(45deg);
background-color: rgba(var(--white), 0.5);
`
const DividerContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
width: 1rem;
height: 1rem;
transform: rotate(45deg);
background-color: rgb(var(--white));
`
const Divider = () => {
return (
<DividerContainer>
<DividerLine />
<DividerSquare />
<DividerLine />
</DividerContainer>
)
return (
<DividerContainer>
<DividerLine />
<DividerSquare />
<DividerLine />
</DividerContainer>
)
}
export default Divider

View file

@ -1,82 +1,14 @@
"use client"
import Image from "next/image"
import styled from "styled-components"
const HeroContainer = styled.div`
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
img {
object-fit: cover;
object-position: center;
width: 100%;
height: 100%;
}
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(var(--black), 0.5);
}
`
const ScrollIndicator = styled.div`
position: absolute;
bottom: 5%;
left: 50%;
transform: translateX(-50%);
width: 2rem;
height: 4rem;
border: 2px solid rgb(var(--white));
border-radius: 2rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
z-index: 100;
cursor: pointer;
&::before {
content: "";
width: 0.5rem;
height: 0.5rem;
background-color: rgb(var(--white));
border-radius: 50%;
animation: scroll 2s infinite;
}
@keyframes scroll {
0% {
transform: translateY(1rem);
}
100% {
opacity: 0;
transform: translateY(3rem);
}
}
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
`
export default function HeroBanner() {
return (
<HeroContainer>
<Image
src="/hero-banner.jpg"
alt="Hero banner"
width={1000}
height={500}
/>
<ScrollIndicator
onClick={() =>
window.scrollTo({ top: window.innerHeight, behavior: "smooth" })
}
/>
</HeroContainer>
)
return <HeroContainer></HeroContainer>
}

View file

@ -1,93 +1,375 @@
"use client"
import styled, { keyframes } from "styled-components"
import Logo from "@/icons/logo"
import { useRef, useState, useEffect } from "react"
import links from "@/data/links"
import socialLinks from "@/data/socialLinks"
import Link from "next/link"
const NavbarContainer = styled.nav`
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background-color: rgba(var(--background), 0.5);
backdrop-filter: blur(10px);
position: absolute;
width: calc(100% - 4rem);
margin: 2rem;
border-radius: 1rem;
z-index: 100;
const ripple = keyframes`
0% {
transform: scale(0);
}
ul {
display: flex;
gap: 2rem;
li {
list-style: none;
font-size: 1.5rem;
font-weight: 600;
cursor: pointer;
a {
text-decoration: none;
color: rgba(var(--black), 0.7);
transition: color 0.2s;
font-weight: 400;
&:hover {
color: rgba(var(--black), 1);
}
&.active {
color: rgba(var(--white), 1);
font-weight: 600;
text-shadow: 0 4px 5px rgba(var(--black), 0.5);
}
}
}
}
svg {
width: 4rem;
height: 4rem;
}
100% {
transform: scale(1);
}
`
const LogoWrapper = styled.div`
display: flex;
align-items: center;
gap: 1rem;
const reverseRipple = keyframes`
0% {
transform: scale(1);
}
h1 {
font-size: 2.5rem;
font-weight: 600;
color: rgb(var(--white));
text-shadow: 0 4px 5px rgba(var(--black), 0.5);
}
100% {
transform: scale(0);
}
`
const NavbarContainer = styled.nav`
width: 100%;
padding: 2rem;
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: flex-end;
position: fixed;
top: 0;
left: 0;
z-index: 100;
background-color: rgb(var(--black));
border-bottom: 1px solid rgba(var(--white), 0.1);
@media (min-width: 768px) {
flex-direction: row;
align-items: center;
}
`
const LinksWrapper = styled.div`
display: none;
width: 100%;
height: calc(100vh - 80px);
z-index: 101;
&.visible {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
}
@media (min-width: 768px) {
flex: 1;
display: flex;
gap: 2rem;
justify-content: space-evenly;
height: auto;
}
`
const LinksContainer = styled.ul`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
li {
list-style: none;
display: flex;
a {
font-size: 1.5rem;
font-weight: 500;
color: rgba(var(--white), 0.8);
text-decoration: none;
text-align: center;
transition: all 0.3s ease;
position: relative;
&:hover {
color: rgb(var(--white));
}
&.active {
color: rgb(var(--white));
&::before {
position: absolute;
content: "<";
left: -1.25rem;
}
&::after {
position: absolute;
content: ">";
right: -1.25rem;
}
}
}
}
@media (min-width: 768px) {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 2rem;
li {
a {
word-break: keep-all;
white-space: nowrap;
font-size: 1.2rem;
width: 100%;
font-weight: 400;
color: rgb(var(--white));
transition: all 0.3s ease;
&:hover {
color: rgb(var(--primary));
}
&.active {
color: rgb(var(--primary));
&::before {
left: -1rem;
}
&::after {
right: -1rem;
}
}
}
}
}
`
const SocialLinksContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
gap: 2rem;
a {
font-size: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
color: rgba(var(--white), 0.8);
transition: all 0.3s ease;
text-decoration: none;
&:hover {
color: rgb(var(--white));
}
}
`
const Topbar = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
@media (min-width: 768px) {
flex: 1;
width: auto;
}
`
const Title = styled.h1`
font-size: 1.5rem;
font-weight: 700;
z-index: 102;
a {
text-decoration: none;
color: var(--white);
}
@media (min-width: 768px) {
font-size: 2rem;
}
`
const HamburgerWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
height: 40px;
width: 40px;
position: relative;
z-index: 101;
&.open::before,
&.closing::before {
content: "";
width: 3000px;
height: 3000px;
position: absolute;
z-index: -1;
cursor: default;
transform: translate(-50%, -50%);
border-radius: 50%;
background-color: rgb(var(--primary));
}
&.open::before {
animation: ${ripple} 0.5s ease-in forwards;
}
&.closing::before {
animation: ${reverseRipple} 0.5s ease-out forwards;
}
`
const Hamburger = styled.div`
width: 100%;
height: 2px;
background-color: var(--white);
position: relative;
z-index: 102;
&::before,
&::after {
content: "";
width: 100%;
height: 2px;
background-color: rgb(var(--white));
position: absolute;
transition: all 0.3s ease;
}
&::before {
transform: translateY(-0.5rem);
}
&::after {
transform: translateY(0.5rem);
}
&.open {
&::before {
transform: rotate(45deg) translateY(0);
}
&::after {
transform: rotate(-45deg) translateY(0);
}
}
@media (min-width: 768px) {
display: none;
}
`
const Navbar = () => {
return (
<NavbarContainer>
<LogoWrapper>
<Logo />
<h1>HangerThem</h1>
</LogoWrapper>
<ul>
<li>
<Link href="/" className="active">
Home
</Link>
</li>
<li>
<Link href="#about">About</Link>
</li>
<li>
<Link href="#projects">Projects</Link>
</li>
<li>
<Link href="#contact">Contact me</Link>
</li>
</ul>
</NavbarContainer>
)
const hanburgerRef = useRef<HTMLDivElement>(null)
const [open, setOpen] = useState(false)
const [closing, setClosing] = useState(false)
const [visible, setVisible] = useState(false)
const [activeTag, setActiveTag] = useState("home")
const handleClick = () => {
if (open) {
setOpen(false)
setClosing(true)
setTimeout(() => {
setVisible(false)
}, 200)
setTimeout(() => {
setClosing(false)
}, 500)
document.body.style.overflow = "auto"
} else {
setOpen(true)
setTimeout(() => {
setVisible(true)
}, 300)
document.body.style.overflow = "hidden"
}
}
useEffect(() => {
const handleResize = () => {
setOpen(false)
setClosing(false)
setVisible(false)
document.body.style.overflow = "auto"
}
window.addEventListener("resize", handleResize)
return () => window.removeEventListener("resize", handleResize)
}, [])
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (!open) return
if (
hanburgerRef.current &&
!hanburgerRef.current.contains(e.target as Node)
) {
setOpen(false)
setClosing(true)
setTimeout(() => {
setVisible(false)
}, 200)
setTimeout(() => {
setClosing(false)
}, 500)
document.body.style.overflow = "auto"
}
}
window.addEventListener("click", handleClickOutside)
return () => window.removeEventListener("click", handleClickOutside)
}, [open])
return (
<NavbarContainer>
<Topbar>
<Title>
<Link href="/">HangerThem</Link>
</Title>
<HamburgerWrapper
ref={hanburgerRef}
onClick={handleClick}
className={open ? "open" : closing ? "closing" : ""}
>
<Hamburger className={open ? "open" : ""} />
</HamburgerWrapper>
</Topbar>
<LinksWrapper className={visible ? "visible" : ""}>
<LinksContainer>
{links.map((link) => (
<li key={link.id}>
<Link
href={link.href}
className={activeTag === link.id ? "active" : ""}
onClick={() => setActiveTag(link.id)}
>
{link.text}
</Link>
</li>
))}
</LinksContainer>
<SocialLinksContainer>
{socialLinks.map((socialLink) => (
<Link key={socialLink.id} href={socialLink.href}>
{socialLink.icon}
{socialLink.text}
</Link>
))}
</SocialLinksContainer>
</LinksWrapper>
</NavbarContainer>
)
}
export default Navbar

View file

@ -1,45 +1,250 @@
"use client"
import styled from "styled-components"
import UnderConstruction from "@/icons/underConstruction"
import { useState } from "react"
import { CaretLeftFill, CaretRightFill, Git } from "react-bootstrap-icons"
import projects from "@/data/projects"
import Link from "next/link"
const ProjectsContainer = styled.section`
width: 100%;
height: 100vh;
padding: 2rem 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
svg {
width: 15rem;
height: 15rem;
}
width: 100%;
min-height: 100vh;
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
`
const ProjectsContent = styled.div`
width: 50%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 2rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 2rem;
h2 {
font-size: 3rem;
font-weight: 600;
}
h2 {
font-size: 3rem;
font-weight: 600;
}
p {
font-size: 1.2rem;
font-weight: 400;
}
`
export default function Projects() {
return (
<ProjectsContainer>
<ProjectsContent>
<h2>Projects</h2>
</ProjectsContent>
<UnderConstruction />
</ProjectsContainer>
)
const CarouselContainer = styled.div`
width: 100%;
position: relative;
`
const CarouselWrapper = styled.div`
display: flex;
transition: transform 0.5s ease-in-out;
gap: 2rem;
`
const ProjectCard = styled.div`
padding: 2rem;
width: 400px;
height: 600px;
display: flex;
flex-direction: column;
gap: 1rem;
border-radius: 1rem;
border: 1px solid rgba(var(--white), 0.1);
transition: opacity 0.5s ease-in-out, min-width 0.5s ease-in-out;
position: relative;
h3 {
font-size: 2rem;
font-weight: 600;
}
p {
font-size: 1.2rem;
font-weight: 400;
}
a {
font-size: 1.2rem;
font-weight: 600;
color: rgb(var(--white));
background-color: rgb(var(--blue));
padding: 0.5rem 1rem;
border-radius: 0.25rem;
text-decoration: none;
transition: background-color 0.2s;
&:hover {
background-color: rgb(var(--light-blue));
}
}
img {
width: 200px;
height: 200px;
object-fit: cover;
border-radius: 1rem;
}
&:not(.active) {
opacity: 0.5;
transform: scale(0.8);
}
`
const StateIndicator = styled.div`
position: absolute;
top: -1rem;
right: -1rem;
font-size: 1.5rem;
text-align: center;
line-height: 3rem;
background-color: rgba(var(--white));
border-radius: 50%;
width: 3rem;
height: 3rem;
`
const NavigationButton = styled.button`
position: absolute;
top: 50%;
transform: translateY(-50%);
border: none;
background-color: transparent;
padding: 0;
color: rgba(var(--white), 0.8);
cursor: pointer;
transition: color 0.3s;
z-index: 1;
&:hover {
color: rgb(var(--white));
}
`
const PrevButton = styled(NavigationButton)`
left: 0;
`
const NextButton = styled(NavigationButton)`
right: 0;
`
const CarouselIndicator = styled.div`
display: flex;
justify-content: center;
gap: 0.5rem;
margin-top: 1rem;
`
const IndicatorDot = styled.div`
width: 10px;
height: 10px;
border-radius: 50%;
cursor: pointer;
background-color: rgba(var(--white), 0.5);
&.active {
background-color: rgb(var(--white));
}
`
const TechStack = styled.div`
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
span {
font-size: 1rem;
font-weight: 400;
background-color: rgba(var(--white), 0.1);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
`
const Projects = () => {
const [currentIndex, setCurrentIndex] = useState(0)
const projectCount = projects.length
const handlePrev = () => {
setCurrentIndex(
(prevIndex) => (prevIndex - 1 + projectCount) % projectCount
)
}
const handleNext = () => {
setCurrentIndex((prevIndex) => (prevIndex + 1) % projectCount)
}
const getVisibleProjects = () => {
const prevIndex = (currentIndex - 1 + projectCount) % projectCount
const nextIndex = (currentIndex + 1) % projectCount
return [projects[prevIndex], projects[currentIndex], projects[nextIndex]]
}
return (
<ProjectsContainer>
<ProjectsContent>
<h2>📂 My Projects</h2>
<p>Here are some of the projects I&apos;ve worked on recently</p>
<CarouselContainer>
<CarouselWrapper>
{getVisibleProjects().map((project, index) => (
<ProjectCard
key={index}
className={index === 1 ? "active" : ""}
onClick={() => {
if (index === 0) {
handlePrev()
} else if (index === 2) {
handleNext()
}
}}
>
<StateIndicator>
{project.state === "wip" ? "🔄" : "✅"}
</StateIndicator>
<h3>{project.title}</h3>
{project.image && (
<img src={project.image} alt={project.title} />
)}
<p>{project.description}</p>
<TechStack>
{project.techStack.map((tech, index) => (
<span key={index}>{tech}</span>
))}
</TechStack>
{project.href && <Link href={project.href}>View Project</Link>}
<Link href={project.source}>
<Git />
</Link>
</ProjectCard>
))}
</CarouselWrapper>
<PrevButton onClick={handlePrev}>
<CaretLeftFill size={50} />
</PrevButton>
<NextButton onClick={handleNext}>
<CaretRightFill size={50} />
</NextButton>
<CarouselIndicator>
{projects.map((_, index) => (
<IndicatorDot
key={index}
onClick={() => setCurrentIndex(index)}
className={index === currentIndex ? "active" : ""}
/>
))}
</CarouselIndicator>
</CarouselContainer>
</ProjectsContent>
</ProjectsContainer>
)
}
export default Projects

187
components/skills.tsx Normal file
View file

@ -0,0 +1,187 @@
"use client"
import styled from "styled-components"
import skills from "@/data/skills"
const SkillsContainer = styled.section`
width: 100%;
min-height: 100vh;
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
h2 {
font-size: 3rem;
font-weight: 600;
}
p {
font-size: 1.2rem;
font-weight: 400;
}
@media (min-width: 768px) {
padding: 4rem;
}
`
const Categories = styled.div`
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
@media (min-width: 768px) {
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
}
`
const SkillCategory = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
h3 {
font-size: 1.75rem;
font-weight: 600;
}
ul {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
li {
width: 100%;
font-size: 1rem;
font-weight: 400;
display: flex;
flex-direction: column;
align-items: center;
}
}
@media (min-width: 768px) {
h3 {
font-size: 2rem;
}
}
`
const SkillOMeter = styled.div<{ $level: number }>`
width: 250px;
height: 1rem;
background-color: rgba(var(--white), 0.1);
border-radius: 1rem;
margin-top: 0.5rem;
position: relative;
&::before {
content: "";
display: block;
width: ${({ $level }) => $level}%;
height: 100%;
background-color: rgb(var(--primary));
border-radius: 1rem;
}
&::after {
content: ${({ $level }) => `'${$level}%'`};
font-size: 0.75rem;
color: rgb(var(--white));
text-align: center;
width: 100%;
top: 0;
position: absolute;
}
`
export default function Skills() {
skills.languages.sort((a, b) => b.persentage - a.persentage)
skills.programmingLanguages.sort((a, b) => b.persentage - a.persentage)
skills.technologies.sort((a, b) => b.persentage - a.persentage)
skills.tools.sort((a, b) => b.persentage - a.persentage)
return (
<SkillsContainer id="skills">
<h2>🎖 Skills</h2>
<p>
Here are some of the languages, technologies, and tools that I have
experience with. I am always learning new things and I am excited to
learn more!
</p>
<Categories>
{skills.languages && (
<SkillCategory>
<h3>Languages</h3>
<ul>
{skills.languages.map((language, index) => (
<li key={index}>
<span>
{language.name} - {language.level}
</span>
<SkillOMeter $level={language.persentage} />
</li>
))}
</ul>
</SkillCategory>
)}
{skills.programmingLanguages && (
<SkillCategory>
<h3>Programming Languages</h3>
<ul>
{skills.programmingLanguages.map((programmingLanguage, index) => (
<li key={index}>
<span>
{programmingLanguage.name} - {programmingLanguage.level}
</span>
<SkillOMeter $level={programmingLanguage.persentage} />
</li>
))}
</ul>
</SkillCategory>
)}
{skills.technologies && (
<SkillCategory>
<h3>Technologies</h3>
<ul>
{skills.technologies.map((technology, index) => (
<li key={index}>
<span>
{technology.name} - {technology.level}
</span>
<SkillOMeter $level={technology.persentage} />
</li>
))}
</ul>
</SkillCategory>
)}
{skills.tools && (
<SkillCategory>
<h3>Tools</h3>
<ul>
{skills.tools.map((tool, index) => (
<li key={index}>
<span>
{tool.name} - {tool.level}
</span>
<SkillOMeter $level={tool.persentage} />
</li>
))}
</ul>
</SkillCategory>
)}
</Categories>
</SkillsContainer>
)
}

View file

@ -1,45 +0,0 @@
"use client"
import styled from "styled-components"
import UnderConstruction from "@/icons/underConstruction"
const TechstackContainer = styled.section`
width: 100%;
height: 100vh;
padding: 2rem 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
svg {
width: 15rem;
height: 15rem;
}
`
const TechstackContent = styled.div`
width: 50%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 2rem;
h2 {
font-size: 3rem;
font-weight: 600;
}
`
export default function Techstack() {
return (
<TechstackContainer>
<TechstackContent>
<h2>Tech Stack</h2>
</TechstackContent>
<UnderConstruction />
</TechstackContainer>
)
}

30
data/links.ts Normal file
View file

@ -0,0 +1,30 @@
type Link = {
id: string
href: string
text: string
}
const links: Link[] = [
{
id: "home",
href: "/",
text: "Home",
},
{
id: "about",
href: "#about",
text: "About",
},
{
id: "skills",
href: "#skills",
text: "Skills",
},
{
id: "contact",
href: "#contact",
text: "Contact me",
},
]
export default links

69
data/projects.ts Normal file
View file

@ -0,0 +1,69 @@
type Project = {
title: string
description: string
image?: string
techStack: string[]
href?: string
source: string
state: "wip" | "done"
}
const projects: Project[] = [
{
title: "HangerThem Portfolio",
description: "This very website and my personal portfolio.",
// image: "/hangerthem.png",
techStack: ["Next.js", "TypeScript", "Styled Components"],
href: "https://hangerthem.com",
source: "https://github.com/HangerThem/hangerthem.com",
state: "done",
},
{
title: "Linkboard",
description:
"A cool alternative to Linktree, a simple link aggregator for your social media profiles.",
image: "/linkboard.png",
techStack: ["Next.js", "TypeScript", "Styled Components"],
href: "https://socials.hangerthem.com",
source: "https://github.com/HangerThem/linkboard",
state: "done",
},
{
title: "Ephemr",
description:
"A simple social network, a place to share your thoughts with the world.",
image: "/ephemr.png",
techStack: ["Next.js", "TypeScript", "Styled Components", "Socket.IO"],
href: "https://ephemr.net",
source: "https://github.com/HangerThem/ephemr",
state: "wip",
},
{
title: "Evolution algorithm Flappy Bird",
description:
"A Flappy Bird clone with an AI that uses an evolution algorithm to play the game.",
image: "/flappy_bird.png",
techStack: ["JavaScript"],
href: "https://hangerthem.github.io/js-flappy-bird-ea/",
source: "https://github.com/HangerThem/js-flappy-bird-ea",
state: "done",
},
{
title: "Peer to Peer Chat",
description: "A simple peer-to-peer chat application.",
techStack: ["Python"],
source: "https://github.com/HangerThem/python-p2p-real-time-chat",
state: "wip",
},
{
title: "Simple 2D physics engine",
description: "A simple 2D physics engine written in JavaScript.",
image: "/physics_engine.png",
techStack: ["JavaScript"],
href: "https://hangerthem.github.io/2d-physics-engine/",
source: "https://hangerthem.github.io/js-2D-physic-engine/",
state: "wip",
},
]
export default projects

146
data/skills.ts Normal file
View file

@ -0,0 +1,146 @@
interface Skills {
languages: Skill[]
programmingLanguages: Skill[]
technologies: Skill[]
tools: Skill[]
}
interface Skill {
name: string
level: string
persentage: number
}
const skills: Skills = {
languages: [
{
name: "Czech",
level: "Native",
persentage: 100,
},
{
name: "English",
level: "C1",
persentage: 90,
},
{
name: "Portuguese",
level: "A2",
persentage: 30,
},
{
name: "Greek",
level: "A1",
persentage: 15,
},
],
programmingLanguages: [
{
name: "JavaScript",
level: "Advanced",
persentage: 95,
},
{
name: "TypeScript",
level: "Advanced",
persentage: 93,
},
{
name: "CSS",
level: "Advanced",
persentage: 92,
},
{
name: "SQL",
level: "Advanced",
persentage: 85,
},
{
name: "Python",
level: "Intermediate",
persentage: 70,
},
{
name: "C#",
level: "Intermediate",
persentage: 52,
},
{
name: "PHP",
level: "Intermediate",
persentage: 50,
},
{
name: "Java",
level: "Beginner",
persentage: 22,
},
{
name: "C",
level: "Beginner",
persentage: 20,
},
],
technologies: [
{
name: "React",
level: "Advanced",
persentage: 95,
},
{
name: "Next.js",
level: "Advanced",
persentage: 93,
},
{
name: "Styled Components",
level: "Advanced",
persentage: 92,
},
{
name: "Node.js",
level: "Advanced",
persentage: 85,
},
{
name: "Tailwind CSS",
level: "Intermediate",
persentage: 70,
},
{
name: "Express",
level: "Intermediate",
persentage: 68,
},
{
name: "MongoDB",
level: "Intermediate",
persentage: 65,
},
{
name: "PostgreSQL",
level: "Intermediate",
persentage: 65,
},
{
name: "Docker",
level: "Beginner",
persentage: 30,
},
],
tools: [
{
name: "Git",
level: "Advanced",
persentage: 95,
},
{
name: "Figma",
level: "Intermediate",
persentage: 70,
},
],
}
export default skills

19
data/socialLinks.tsx Normal file
View file

@ -0,0 +1,19 @@
import { Github } from "react-bootstrap-icons"
type SocialLink = {
id: string
href: string
text: string
icon: React.ReactNode
}
const socialLink: SocialLink[] = [
{
id: "github",
href: "https://github.com/HangerThem",
text: "GitHub",
icon: <Github />,
},
]
export default socialLink

View file

@ -1,100 +1,65 @@
const Logo = () => (
<svg
width="98"
height="113"
viewBox="0 0 98 113"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g filter="url(#filter0_d_101_4)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4 0V75L19 90V45L49 60V105L64 90V52.5L19 30V0H4Z"
fill="#C90E0E"
/>
</g>
<g filter="url(#filter1_d_101_4)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M34 0V15H49V37.5L64 45V15H94L79 0H34Z"
fill="#C90E0E"
/>
</g>
<defs>
<filter
id="filter0_d_101_4"
x="0"
y="0"
width="68"
height="113"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_101_4"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_101_4"
result="shape"
/>
</filter>
<filter
id="filter1_d_101_4"
x="30"
y="0"
width="68"
height="53"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_101_4"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_101_4"
result="shape"
/>
</filter>
</defs>
</svg>
<svg
width="96.895882mm"
height="15.333432mm"
viewBox="0 0 96.895882 15.333432"
version="1.1"
id="svg5"
xmlns="http://www.w3.org/2000/svg"
>
<defs id="defs2" />
<g id="layer1" transform="translate(-47.269803,-109.51249)">
<g
id="SvgjsG1012"
transform="matrix(0.19914873,0,0,0.19914873,46.572783,106.82398)"
fill="#111111"
>
<g transform="translate(0,-952.36218)" id="g12" fill="#000000">
<path
direction="ltr"
baselineShift="baseline"
fill="#000000"
enableBackground="accumulate"
d="m 74.350798,965.86217 c -0.47727,0.0901 -0.84652,0.57591 -0.80789,1.06291 0.0725,1.09336 0.14561,2.18929 0.21751,3.28252 l -9.78793,7.12775 c -0.0465,-0.36954 -0.0757,-0.72191 -0.12429,-1.09417 -0.0589,-0.47056 -0.49199,-0.86419 -0.96326,-0.87534 -19.51026,-0.47963 -39.04412,-1.07245 -58.5411493,-1.53184 -0.49962,0.0766 -0.8920699,0.58851 -0.8389699,1.09417 1.5325399,16.84037 2.3138699,34.46963 1.5536399,45.86143 -0.0338,0.5443 0.45234,1.0639 0.99433,1.0629 H 26.809418 v 3.0325 h -4.16375 c -0.35006,0 -0.69501,0.1951 -0.87004,0.5001 l -6.77387,12.0047 c -0.33672,0.632 0.25322,1.5319 0.96325,1.4693 l 9.9433,-1.0004 c 0.32433,-0.034 0.62573,-0.2407 0.77682,-0.5314 l 1.74008,-3.5327 10.00544,-0.9066 3.66659,2.7824 c 0.14487,0.106 0.3187,0.1716 0.49716,0.1875 l 9.81901,1.0629 c 0.79728,0.1031 1.40396,-1.0523 0.87004,-1.6568 l -8.82468,-10.0664 c -0.19058,-0.2027 -0.46851,-0.3192 -0.74575,-0.3126 h -3.97731 v -3.0325 h 10.93762 v 3.0325 c 0.003,0.3817 0.24363,0.7514 0.59038,0.9066 l 22.40349,9.8162 c -0.0473,0.375 -0.0746,0.7506 -0.12429,1.1255 -0.0919,0.632 0.55848,1.2518 1.18077,1.1254 l 4.94057,-1.0004 c 0.2484,-0.049 0.4759,-0.198 0.62146,-0.4064 l 1.80222,-2.6573 6.02812,-0.8753 1.52257,1.5006 c 0.23547,0.2244 0.58456,0.3213 0.90111,0.2501 l 4.07054,-0.8128 c 0.42624,-0.081 0.77465,-0.4716 0.80789,-0.9066 1.54903,-19.4808 1.87298,-40.72248 -2.01973,-60.30456 -0.0678,-0.34113 -0.32546,-0.63742 -0.65253,-0.75029 -6.45553,-2.00171 -12.82815,-4.47718 -18.3951,-6.03358 z m 1.27399,2.40723 15.90927,5.34581 c 2.10404,10.82635 2.92161,22.20419 2.95192,33.51289 -4.44135,1.5015 -11.59046,0.7591 -17.21433,-0.1563 0.007,-12.92425 -0.79562,-25.83839 -1.64686,-38.7024 z m -1.70901,4.25164 c 1.32219,20.37272 2.35245,40.72086 0.031,60.99226 l -21.28487,-9.2848 v -2.3759 h 12.49126 c 0.52062,-1e-4 0.99428,-0.4766 0.99433,-1.0004 0,-11.4822 -0.003,-25.06234 -1.89544,-41.26593 z m -68.3601493,3.34504 56.3971293,1.46932 c 2.12604,16.74313 2.20478,30.6471 2.20616,42.5164 H 7.0781887 c 0.65708,-11.3478 -0.0934,-27.89075 -1.52256,-43.98572 z m 78.7695393,3.97028 c -0.55105,0.0857 -0.95376,0.70904 -0.80789,1.25049 0.94704,3.81125 0.96325,8.78464 0.96325,10.78541 -0.007,0.52851 0.46896,1.01463 0.99433,1.01463 0.52538,0 1.00176,-0.48612 0.99433,-1.01463 0,-2.00077 0.0162,-7.06251 -1.0254,-11.25434 -0.10134,-0.45796 -0.55979,-0.80739 -1.0254,-0.78156 -0.031,-10e-4 -0.0622,-10e-4 -0.0932,0 z m -7.0846,29.16754 c 5.40227,0.8805 12.15096,1.6898 17.21433,0.2188 -0.0439,7.7172 -0.41633,15.385 -0.99433,22.8213 l -2.82762,0.5627 -1.4915,-1.438 c -0.21662,-0.2167 -0.53643,-0.324 -0.83896,-0.2814 l -6.96031,1.0004 c -0.27518,0.041 -0.53011,0.2043 -0.6836,0.4377 l -1.77115,2.626 -3.26264,0.6878 c 1.10872,-8.8821 1.55148,-17.7579 1.61578,-26.6353 z m -48.44249,12.8487 h 8.94897 v 4.0328 c 5e-5,0.5238 0.47371,1.0003 0.99433,1.0004 h 4.53662 l 6.83602,7.753 -7.02245,-0.7191 -3.75981,-2.8448 c -0.19506,-0.1433 -0.44326,-0.2114 -0.6836,-0.1876 l -10.93763,1.0004 c -0.33507,0.024 -0.65127,0.2324 -0.80789,0.5315 l -1.74008,3.5326 -7.48854,0.7503 5.53096,-9.8163 h 4.59877 c 0.52061,-10e-5 0.99428,-0.4766 0.99433,-1.0004 z"
fillOpacity="1"
stroke="none"
visibility="visible"
display="inline"
overflow="visible"
id="path10"
/>
</g>
</g>
<text
xmlSpace="preserve"
fontStyle="normal"
fontVariant="normal"
fontWeight="300"
fontStretch="normal"
fontSize="14px"
fontFamily="K2D"
fill="#000000"
x="66.780556"
y="121.8967"
id="text725"
>
<tspan
id="tspan723"
fontStyle="normal"
fontVariant="normal"
fontWeight="300"
fontStretch="normal"
fontSize="14px"
fontFamily="K2D"
fill="#000000"
x="66.780556"
y="121.8967"
>
HangerThem
</tspan>
</text>
</g>
</svg>
)
export default Logo

File diff suppressed because one or more lines are too long

8465
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,26 +1,29 @@
{
"name": "hangerthem-web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "14.1.0",
"nodemailer": "^6.9.9",
"react": "^18",
"react-dom": "^18",
"styled-components": "^6.1.8"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"typescript": "^5"
}
"name": "hangerthem-web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"canvas-confetti": "^1.9.3",
"next": "14.1.0",
"nodemailer": "^6.9.9",
"react": "^18",
"react-bootstrap-icons": "^1.11.4",
"react-dom": "^18",
"styled-components": "^6.1.8"
},
"devDependencies": {
"@types/canvas-confetti": "^1.6.4",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"typescript": "^5"
}
}

BIN
public/ephemr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
public/flappy_bird.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/linkboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
public/physics_engine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -43,7 +43,6 @@ export const GlobalStyle = createGlobalStyle`
}
body {
font-family: 'K2D', sans-serif;
background-color: rgb(var(--black));
color: rgb(var(--white));
}

View file

@ -1,26 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}