Big update
This commit is contained in:
parent
709ac3929b
commit
a4299d0fc4
26 changed files with 5653 additions and 4733 deletions
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"useTabs": true,
|
||||
"semi": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
24
app/page.tsx
24
app/page.tsx
|
|
@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'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'm Frank Borisjuk, a software developer from Czechia
|
||||
who loves working with tech.
|
||||
</p>
|
||||
<p>
|
||||
I've built{" "}
|
||||
<Link
|
||||
href="https://github.com/HangerThem/linkboard"
|
||||
referrerPolicy="no-referrer"
|
||||
target="_blank"
|
||||
>
|
||||
Linkboard
|
||||
</Link>
|
||||
, a cool alternative to Linktree, and I'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's be honest, the money
|
||||
isn't bad either. While web development is my main focus,
|
||||
I'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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'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
187
components/skills.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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
30
data/links.ts
Normal 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
69
data/projects.ts
Normal 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
146
data/skills.ts
Normal 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
19
data/socialLinks.tsx
Normal 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
|
||||
157
icons/logo.tsx
157
icons/logo.tsx
|
|
@ -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
8465
package-lock.json
generated
File diff suppressed because it is too large
Load diff
51
package.json
51
package.json
|
|
@ -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
BIN
public/ephemr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
BIN
public/flappy_bird.png
Normal file
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
BIN
public/linkboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/physics_engine.png
Normal file
BIN
public/physics_engine.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -43,7 +43,6 @@ export const GlobalStyle = createGlobalStyle`
|
|||
}
|
||||
|
||||
body {
|
||||
font-family: 'K2D', sans-serif;
|
||||
background-color: rgb(var(--black));
|
||||
color: rgb(var(--white));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue