portfolio overhaul

This commit is contained in:
SowinskiBraeden committed 2026-03-14 15:41:08 -07:00
1 parent 2b0b41f773
commit 7c26e115ee
20 files changed
+1874 -683

No files matched your search

+147
View File
@@ -0,0 +1,147 @@
{
"projects": [
{
"title": "Schedule Generator App",
"slug": "schedule-generator-app",
"type": "projects",
"summary": "A desktop application to interact with my schedule generator algorithm. The algorithm is written in Python and generates master and student timetables.",
"label": "Project",
"date": "2023-08-01",
"status": "Personal Project",
"github": "https://github.com/SowinskiBraeden/ScheduleGeneratorApp",
"demo": null,
"featured": true,
"order": 1,
"tags": [
"Python",
"Desktop App",
"Scheduling",
"Data Processing"
],
"path": "./public/content/projects/schedule-generator-app.md"
},
{
"title": "School Management API",
"slug": "school-management-api",
"type": "projects",
"summary": "I developed an API to manage an entire school, keeping track of students and staff details while expanding my understanding of Go, API development, and documentation.",
"label": "Project",
"date": "2023-09-01",
"status": "Personal Project",
"github": "https://github.com/SowinskiBraeden/school-management-api",
"demo": null,
"featured": true,
"order": 2,
"tags": [
"Go",
"API",
"Documentation",
"Education Tech"
],
"path": "./public/content/projects/school-management-api.md"
},
{
"title": "Lines Police CAD",
"slug": "lines-police-cad",
"type": "projects",
"summary": "A web service built with Node.js, Express.js, and MongoDB. I contributed features, bug fixes, and general technical support to a product serving a large user base.",
"label": "Project",
"date": "2022-06-01",
"status": "Collaboration",
"github": "https://github.com/LinesMerrill/police-cad",
"demo": "https://www.linespolice-cad.com/",
"featured": false,
"order": 3,
"tags": [
"Node.js",
"Express",
"MongoDB",
"Web App"
],
"path": "./public/content/projects/lines-police-cad.md"
},
{
"title": "DayZ Reforger Bot",
"slug": "dayz-reforger-bot",
"type": "projects",
"summary": "A large-scale Discord bot focused on parsing game server data, storing it, and presenting valuable information back to users through analytics-driven features.",
"label": "Project",
"date": "2024-11-08",
"status": "Active",
"github": "https://github.com/SowinskiBraeden/dayz-reforger",
"demo": null,
"featured": true,
"order": 4,
"tags": [
"Node.js",
"Discord.js",
"MongoDB",
"Analytics",
"Nitrado"
],
"path": "./public/content/projects/dayz-reforger-bot.md"
},
{
"title": "XP-System",
"slug": "xp-system",
"type": "projects",
"summary": "An early web application project where teachers could assign work and students could earn XP and levels, making the learning process more interactive.",
"label": "Project",
"date": "2021-05-01",
"status": "Early Project",
"github": "https://github.com/SowinskiBraeden/XP-System",
"demo": null,
"featured": false,
"order": 5,
"tags": [
"Web App",
"Education Tech",
"Gamification"
],
"path": "./public/content/projects/xp-system.md"
},
{
"title": "Portfolio",
"slug": "portfolio",
"type": "projects",
"summary": "A minimal personal portfolio focused on clarity, clean presentation, and an easy way to showcase projects, updates, and professional growth.",
"label": "Project",
"date": "2026-03-14",
"status": "Active",
"github": "https://github.com/SowinskiBraeden/SowinskiBraeden",
"demo": null,
"featured": false,
"order": 6,
"tags": [
"HTML",
"CSS",
"JavaScript",
"Portfolio"
],
"path": "./public/content/projects/portfolio.md"
}
],
"updates": [
{
"title": "3rd Place at the inaugural Red Team Hackathon",
"slug": "red-team-hackathon-2026",
"type": "updates",
"summary": "Team Cerberus Gate earned a third-place finish while building a prototype focused on RF signal classification, modulation analysis, and emitter geolocation.",
"label": "Update",
"date": "2026-03-12",
"status": "Achievement",
"github": null,
"demo": null,
"featured": true,
"order": 1,
"tags": [
"Hackathon",
"RF",
"Machine Learning",
"Geolocation",
"BCIT"
],
"path": "./public/content/updates/red-team-hackathon-2026.md"
}
]
}
@@ -0,0 +1,28 @@
---
title: DayZ Reforger Bot
slug: dayz-reforger-bot
summary: A large-scale Discord bot focused on parsing game server data, storing it, and presenting valuable information back to users through analytics-driven features.
label: Project
date: 2024-11-08
status: Active
featured: true
order: 4
github: https://github.com/SowinskiBraeden/dayz-reforger
tags: Node.js, Discord.js, MongoDB, Analytics, Nitrado
---
## Overview
This project highlights my interest in scalable backend systems, data parsing, and user-focused tooling.
It started as a smaller utility but expanded into a much larger system handling killfeeds, alarms, faction armbands, server data, and other game-related features.
## Focus Areas
- Parsing and structuring raw game server data
- Storing and querying useful analytics data
- Designing for modular growth and future expansion
## Notes
This is one of the projects that best reflects how I like to build software: practical, data-driven, and designed with extensibility in mind.
@@ -0,0 +1,29 @@
---
title: Lines Police CAD
slug: lines-police-cad
summary: A web service built with Node.js, Express.js, and MongoDB. I contributed features, bug fixes, and general technical support to a product serving a large user base.
label: Project
date: 2022-06-01
status: Collaboration
featured: false
order: 3
github: https://github.com/LinesMerrill/police-cad
demo: https://www.linespolice-cad.com/
tags: Node.js, Express, MongoDB, Web App
---
## Overview
Lines Police CAD is a web-based service built with Node.js and MongoDB.
I contributed new features, bug fixes, and general technical support while working within an already established product and codebase.
## Focus Areas
- Contributing effectively inside a collaborative project
- Working on production-facing features and fixes
- Supporting a platform used by a large community
## Notes
This project strengthened my ability to work inside an existing system rather than only building from scratch.
+26
View File
@@ -0,0 +1,26 @@
---
title: Portfolio
slug: portfolio
summary: A minimal personal portfolio focused on clarity, clean presentation, and an easy way to showcase projects, updates, and professional growth.
label: Project
date: 2026-03-14
status: Active
featured: false
order: 6
github: https://github.com/SowinskiBraeden/SowinskiBraeden
tags: HTML, CSS, JavaScript, Portfolio
---
## Overview
This portfolio is intentionally minimal.
The goal is to keep the presentation clean and professional while still giving enough room to show technical work, current interests, and recent accomplishments.
## What Changed
This version introduces a markdown-based content system so projects and updates can be added more cleanly over time.
## Notes
The structure stays simple, but the content model is much easier to maintain and extend.
@@ -0,0 +1,28 @@
---
title: Schedule Generator App
slug: schedule-generator-app
summary: A desktop application to interact with my schedule generator algorithm. The algorithm is written in Python and generates master and student timetables.
label: Project
date: 2023-08-01
status: Personal Project
featured: true
order: 1
github: https://github.com/SowinskiBraeden/ScheduleGeneratorApp
tags: Python, Desktop App, Scheduling, Data Processing
---
## Overview
This desktop application was built to make my schedule generation algorithm easier to use and easier to test in a real workflow.
The project takes a difficult real-world scheduling problem and turns it into a usable tool for producing both student schedules and master timetables.
## Focus Areas
- Desktop tooling around a custom scheduling algorithm
- Parsing and transforming structured school data
- Exporting useful outputs for administrators and students
## Notes
This project is one of the clearest examples of my interest in backend logic, data processing, and software that solves practical operational problems.
@@ -0,0 +1,28 @@
---
title: School Management API
slug: school-management-api
summary: I developed an API to manage an entire school, keeping track of students and staff details while expanding my understanding of Go, API development, and documentation.
label: Project
date: 2023-09-01
status: Personal Project
featured: true
order: 2
github: https://github.com/SowinskiBraeden/school-management-api
tags: Go, API, Documentation, Education Tech
---
## Overview
This API was designed around managing student and staff information in a school environment.
It gave me a strong opportunity to deepen my understanding of Go, REST-style API design, and clear technical documentation.
## Focus Areas
- Building maintainable backend services in Go
- Managing structured school data and future expansion paths
- Designing endpoints that are easy to reason about and document
## Notes
The broader vision around this work included course management, schedule generation, and other administration tools that could grow over time.
+24
View File
@@ -0,0 +1,24 @@
---
title: XP-System
slug: xp-system
summary: An early web application project where teachers could assign work and students could earn XP and levels, making the learning process more interactive.
label: Project
date: 2021-05-01
status: Early Project
featured: false
order: 5
github: https://github.com/SowinskiBraeden/XP-System
tags: Web App, Education Tech, Gamification
---
## Overview
XP-System was one of the projects that introduced me to the value of web applications and product thinking.
The core idea was to make school work feel more engaging by layering XP, levels, and a clearer sense of progress on top of classroom assignments.
## Focus Areas
- Building an interactive web application
- Thinking about user motivation and engagement
- Learning early lessons around application structure and workflows
@@ -0,0 +1,27 @@
---
title: 3rd Place at the inaugural Red Team Hackathon
slug: red-team-hackathon-2026
summary: Team Cerberus Gate earned a third-place finish while building a prototype focused on RF signal classification, modulation analysis, and emitter geolocation.
label: Update
date: 2026-03-12
status: Achievement
featured: true
order: 1
tags: Hackathon, RF, Machine Learning, Geolocation, BCIT
---
## Team Cerberus Gate
At the inaugural Red Team Hackathon, I competed with **Team Cerberus Gate** on the **Find My Force** challenge.
Our team built a prototype aimed at classifying RF signals, identifying modulation schemes, and estimating emitter locations. It was a fast-paced systems problem that combined software, signal understanding, and rapid iteration under pressure.
## Result
Our team finished **3rd place** and also received the **Best Dressed Award**.
## Why it mattered to me
This was the kind of challenge I enjoy most: ambiguous requirements, technical constraints, short timelines, and a working prototype that had to come together quickly.
It also gave me a chance to build alongside a strong team and work on a problem that blended software engineering, signal analysis, and practical decision-making.
+96 -80
View File
@@ -1,17 +1,21 @@
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Jersey+15&family=Sixtyfour+Convergence&family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap');
.container {
display: grid;
grid-template-columns: minmax(280px, 320px) repeat(3, 1fr);
grid-template-areas:
"pro about about about about about"
"pro other other other other other"
"footer footer footer footer footer footer";
gap: 24pt;
padding: 24pt;
"pro about about about"
"pro other other other"
"footer footer footer footer";
grid-template-rows: auto 1fr auto;
gap: 24px;
padding-top: 18px;
min-height: calc(100vh - 132px);
align-items: stretch;
}
.left-content {
grid-area: pro;
display: flex;
flex-direction: column;
}
.about {
@@ -20,103 +24,115 @@
.honors {
grid-area: other;
display: flex;
flex-direction: column;
height: 100%;
}
.footer {
grid-area: footer;
}
.about {
display: flex;
flex-direction: column;
}
section {
transition: .8s;
padding: 24pt;
transition: 0.8s;
padding: 24px;
background-color: var(--menu-text-bg);
border-radius: 12pt;
border-radius: 18px;
border: 1px solid var(--border);
}
section h3 {
margin-top: -5pt;
}
.social-icons {
margin-top: 6pt;
margin-right: 24pt;
margin-top: 0;
margin-bottom: 14px;
font-size: 1.2rem;
}
#profile {
background-image: url("../images/pic.png");
background-image: linear-gradient(rgba(0, 0, 0, 0.24), rgba(0, 0, 0, 0.24)), url("../images/pic.png");
background-size: cover;
background-position: center;
color: #d8d8d9;
}
#profile img {
border: 6pt solid var(--primary);
transition: .8s;
}
.profile-sub {
background-color: rgba(0, 0, 0, .7);
padding: 12pt;
border-radius: 12pt;
}
dt {
margin-top: 12pt;
}
.text {
margin-right: 24pt;
font-family: "Ubuntu", serif;
font-weight: 400;
font-style: normal;
transition: .8s;
}
footer {
display: flex;
justify-content: space-between;
transition: .8s;
padding: 8pt;
background-color: var(--menu-text-bg);
border-radius: 12pt;
}
footer p {
margin-left: 12pt;
font-family: "Ubuntu", serif;
font-weight: 400;
font-style: normal;
margin-top: 20pt;
}
#profile {
border-bottom-right-radius: 0pt;
border-bottom-left-radius: 0pt;
margin-bottom: 0pt;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
margin-bottom: 0;
}
#skills {
border-top-left-radius: 0pt;
border-top-right-radius: 0pt;
border-top-left-radius: 0;
border-top-right-radius: 0;
flex: 1;
}
@media only screen and (max-width: 740pt) {
#profile img {
border: 4px solid rgba(235, 82, 52, 0.18);
box-shadow: var(--shadow);
}
.profile-sub {
background-color: rgba(0, 0, 0, 0.62);
padding: 16px;
border-radius: 14px;
margin-top: 16px;
}
.skills-list {
padding-left: 18px;
}
.skills-list li {
margin-bottom: 8px;
}
.text {
font-weight: 400;
transition: 0.8s;
margin: 0 0 14px;
}
dt {
margin-top: 16px;
}
dd {
margin-left: 0;
color: var(--muted);
}
.honors dl,
.skills-list {
margin-bottom: 0;
}
@media only screen and (max-width: 920px) {
.container {
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: auto;
}
#skills {
margin-bottom: 0pt;
}
.footer {
display: block;
text-align: center;
}
.footer p {
margin: 0pt;
margin-top: 12pt;
}
.social-icons {
margin: 0pt;
margin-top: 6pt;
margin-bottom: 0;
}
}
.profile-name {
margin: 0 0 6px;
font-size: clamp(1.45rem, 2vw, 1.9rem);
line-height: 1.1;
}
.profile-role,
.profile-focus {
margin: 0;
}
.profile-focus {
margin-top: 6px;
color: rgba(255, 255, 255, 0.76);
}
+234 -120
View File
@@ -1,165 +1,279 @@
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Jersey+15&family=Sixtyfour+Convergence&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Jersey+15&family=Sixtyfour+Convergence&family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap');
.container {
margin-top: 2%;
display: flex;
flex-direction: row;
justify-content: space-around;
flex-wrap: wrap;
.projects-page,
.entry-page {
padding-top: 18px;
}
.card {
transition: 0.8s;
width: 54vh;
height: auto;
margin: 30pt;
padding: 20pt;
background-color: var(--menu-text-bg);
border-radius: 24pt;
display: flex;
.projects-hero,
.entry-shell,
.content-section {
background: var(--menu-text-bg);
border: 1px solid var(--border);
border-radius: 22px;
padding: 28px;
box-shadow: var(--shadow);
}
.box {
transition: 0.8s;
background-color: var(--theme);
border-radius: 12pt;
padding-top: 12pt;
padding-left: 12pt;
padding-right: 12pt;
.projects-hero {
margin-bottom: 24px;
}
.content {
transition: 0.8s;
margin-left: 18pt;
margin-right: 18pt;
align-items: stretch;
.projects-hero h1,
.entry-title {
font-size: clamp(2rem, 4vw, 3rem);
line-height: 1.1;
margin: 6px 0 14px;
}
.content h2 {
transition: 0.8s;
font-size: 275%;
margin-top: 5pt;
font-family: "Sixtyfour Convergence", serif;
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
font-variation-settings:
"BLED" 0,
"SCAN" 0,
"XELA" 0,
"YELA" 0;
margin-bottom: 0pt;
text-shadow: 1pt 1pt 2pt red, 0 0 1em var(--accent), 0 0 0.2em var(--secondary);
min-width: 2.75cm;
.projects-intro,
.entry-summary {
margin: 0;
max-width: 760px;
color: var(--muted);
}
.content h3 {
font-size: 150%;
font-family: "Jersey 15", serif;
font-weight: 400;
font-style: normal;
.eyebrow {
text-transform: uppercase;
margin-top: 20pt;
margin-left: 0.75cm;
letter-spacing: 0.14em;
font-size: 0.78rem;
color: var(--secondary);
margin: 0;
font-weight: 700;
}
.content p {
transition: 0.8s;
font-size: 18px;
line-height: 1.6;
word-break: break-word;
background: var(--theme);
font-family: -apple-system, BlinkMacSystemFont, segoe ui, Roboto, Oxygen, Ubuntu, Cantarell, open sans, helvetica neue, sans-serif;
.content-section {
margin-bottom: 24px;
}
.content a {
background-color: var(--secondary);
padding: 8pt;
border-radius: 8pt;
position: relative;
box-shadow: 1pt 1pt 2pt red, 0 0 1em var(--accent), 0 0 0.2em var(--secondary);
margin-bottom: 15pt;
}
.content-head {
.section-heading {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: end;
gap: 12px;
margin-bottom: 18px;
}
.content-body {
.section-heading h2 {
margin: 6px 0 0;
font-size: 1.8rem;
}
.section-link,
.card-link,
.entry-back,
.inline-button {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 12px;
border: 1px solid var(--border);
padding: 10px 14px;
font-weight: 600;
background: transparent;
}
.section-link:hover,
.card-link:hover,
.entry-back:hover,
.inline-button:hover {
border-color: rgba(235, 82, 52, 0.32);
}
.content-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 18px;
}
.content-card {
border-radius: 18px;
padding: 22px;
background: var(--panel);
border: 1px solid var(--border);
display: flex;
flex-direction: column;
min-height: 100%;
}
.content-card.featured {
border-color: rgba(235, 82, 52, 0.34);
}
.card-meta,
.entry-meta,
.entry-links,
.card-links,
.tag-list,
.entry-tag-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.card-meta {
margin-bottom: 14px;
}
.card-kicker,
.card-date,
.entry-pill {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 6px 12px;
font-size: 0.84rem;
font-weight: 600;
}
.card-kicker,
.entry-pill-primary {
background: rgba(235, 82, 52, 0.16);
color: var(--secondary);
}
.card-date,
.entry-pill-secondary {
background: rgba(216, 216, 217, 0.08);
color: var(--muted);
}
.content-card h3 {
margin: 0 0 10px;
font-size: 1.35rem;
line-height: 1.2;
}
.card-summary {
color: var(--muted);
margin: 0 0 18px;
flex: 1;
}
.content-body p {
min-height: 5cm;
.tag,
.entry-tag {
display: inline-flex;
border-radius: 999px;
padding: 6px 10px;
font-size: 0.82rem;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border);
}
.content-body a {
margin-bottom: 15pt;
text-align: center;
max-width: 100pt;
.card-links,
.entry-links {
margin-top: 18px;
}
.content-body #links {
display: flex;
justify-content: space-around;
.card-link-primary,
.inline-button-primary {
background: var(--secondary);
color: #fff;
border-color: transparent;
}
#links a {
margin-bottom: 15pt;
text-align: center;
width: 100pt;
.card-link-primary:hover,
.inline-button-primary:hover {
color: #fff;
filter: brightness(1.04);
}
/* prevents it changing color */
.content-body a:hover {
.updates-grid .content-card {
background: linear-gradient(180deg, rgba(235, 82, 52, 0.08), rgba(255, 255, 255, 0.02));
}
.site-footer {
margin-bottom: 24px;
}
.entry-shell {
padding: 24px 28px 30px;
}
.entry-header {
margin-bottom: 24px;
}
.entry-back {
margin-bottom: 20px;
}
.entry-meta {
margin-bottom: 18px;
}
.entry-links {
margin-top: 18px;
}
.entry-content {
margin-top: 28px;
padding-top: 24px;
border-top: 1px solid var(--border);
}
.entry-content h1,
.entry-content h2,
.entry-content h3,
.entry-content h4 {
margin-top: 28px;
line-height: 1.2;
}
.entry-content p,
.entry-content li,
.entry-content blockquote {
color: var(--primary);
}
.footer {
display: flex;
justify-content: space-between;
transition: .8s;
padding: 8pt;
background-color: var(--menu-text-bg);
border-radius: 12pt;
margin-bottom: 24pt;
width: 80vw;
.entry-content ul,
.entry-content ol {
padding-left: 22px;
}
.footer p {
font-family: "Ubuntu", serif;
font-weight: 400;
font-style: normal;
margin-left: 12pt;
.entry-content a {
color: var(--secondary);
}
.social-icons {
margin-top: 6pt;
margin-right: 24pt;
.entry-content blockquote {
margin: 18px 0;
padding: 12px 16px;
border-left: 4px solid var(--secondary);
background: rgba(235, 82, 52, 0.08);
border-radius: 0 12px 12px 0;
}
.content-body a:hover {
background-color: #e4a295;
.entry-content code {
background: rgba(0, 0, 0, 0.24);
padding: 2px 6px;
border-radius: 6px;
font-size: 0.92rem;
}
@media only screen and (max-width: 544pt) {
.content-head h2 {
margin-top: 24pt;
.entry-content pre {
padding: 16px;
overflow-x: auto;
background: rgba(0, 0, 0, 0.24);
border-radius: 16px;
}
.entry-content pre code {
background: transparent;
padding: 0;
}
.entry-empty {
text-align: center;
padding: 40px 18px;
}
@media only screen and (max-width: 740pt) {
.projects-hero,
.entry-shell,
.content-section {
padding: 22px;
}
.footer {
display: block;
text-align: center;
}
.footer p {
margin: 0pt;
margin-top: 12pt;
}
.social-icons {
margin: 0pt;
margin-top: 6pt;
.section-heading {
flex-direction: column;
align-items: start;
}
}
+526 -72
View File
@@ -1,15 +1,24 @@
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;700;800&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800&display=swap");
/* root is dark theme */
:root {
--theme: #1d1e20;
--primary: #d8d8d9;
--secondary: #eb5234;
--accent: #ff005a;
--panel: rgba(216, 216, 217);
--panel: rgba(216, 216, 217, 0.08);
--panel-text: #303030;
--menu-text-bg: #303030;
--not-found: #030005;
--border: rgba(216, 216, 217, 0.12);
--muted: rgba(216, 216, 217, 0.72);
--shadow: 0 16px 44px rgba(0, 0, 0, 0.18);
--hero-glow-x: 50%;
--hero-glow-y: 20%;
--hero-glow-alt-x: 50%;
--hero-glow-alt-y: 28%;
--hero-glow-rotate: 0deg;
--hero-glow-shift-x: 0px;
--hero-glow-shift-y: 0px;
}
.light {
@@ -17,10 +26,28 @@
--primary: rgba(0, 0, 0, 0.88);
--secondary: #eb5234;
--accent: #ff005a;
--panel: #303030;
--panel: rgba(0, 0, 0, 0.06);
--panel-text: rgba(216, 216, 217);
--menu-text-bg: #9e9e9e;
--menu-text-bg: #b7b7b7;
--not-found: #303030;
--border: rgba(0, 0, 0, 0.08);
--muted: rgba(0, 0, 0, 0.68);
--shadow: 0 16px 40px rgba(0, 0, 0, 0.08);
--hero-glow-x: 50%;
--hero-glow-y: 20%;
--hero-glow-alt-x: 50%;
--hero-glow-alt-y: 28%;
--hero-glow-rotate: 0deg;
--hero-glow-shift-x: 0px;
--hero-glow-shift-y: 0px;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
@@ -29,7 +56,40 @@ body {
line-height: 1.6;
word-break: break-word;
background: var(--theme);
font-family: -apple-system, BlinkMacSystemFont, segoe ui, Roboto, Oxygen, Ubuntu, Cantarell, open sans, helvetica neue, sans-serif;
font-family: "Poppins", -apple-system, BlinkMacSystemFont, segoe ui, Roboto, Oxygen, Ubuntu, Cantarell, open sans,
helvetica neue, sans-serif;
min-height: 100vh;
position: relative;
overflow-x: hidden;
}
body::before,
body::after {
content: "";
position: fixed;
inset: -20%;
pointer-events: none;
z-index: 0;
transition: transform 0.18s ease-out, opacity 0.25s ease-out;
}
body::before {
background:
radial-gradient(circle at var(--hero-glow-x) var(--hero-glow-y), rgba(235, 82, 52, 0.24), transparent 18%),
radial-gradient(circle at var(--hero-glow-alt-x) var(--hero-glow-alt-y), rgba(255, 0, 90, 0.14), transparent 20%);
filter: blur(48px);
opacity: 0.9;
transform: translate3d(var(--hero-glow-shift-x), var(--hero-glow-shift-y), 0) rotate(var(--hero-glow-rotate));
}
body::after {
background:
radial-gradient(circle at 20% 20%, rgba(235, 82, 52, 0.08), transparent 20%),
radial-gradient(circle at 80% 18%, rgba(255, 0, 90, 0.05), transparent 18%);
filter: blur(90px);
opacity: 0.55;
transform: translate3d(calc(var(--hero-glow-shift-x) * -0.35), calc(var(--hero-glow-shift-y) * -0.35), 0)
rotate(calc(var(--hero-glow-rotate) * -0.45));
}
body.js-stop-transition * {
@@ -42,17 +102,36 @@ a:hover {
a,
body,
h1 {
h1,
h2,
h3,
h4,
p,
li,
dd,
dt {
color: var(--primary);
}
header,
nav {
display: block
display: block;
}
header,
main,
.page-shell {
position: relative;
z-index: 1;
}
.header {
position: relative;
z-index: 1000;
}
ul {
padding: 0
padding: 0;
}
ul li::before {
@@ -77,19 +156,16 @@ ul li#projects-link:hover::before {
a {
transition: 0.8s;
text-decoration: none;
}
a:hover {
color: var(--secondary);
}
a {
text-decoration: none
}
body,
ul {
margin: 0
margin: 0;
}
.nav {
@@ -104,7 +180,7 @@ ul {
}
.nav a {
display: block
display: block;
}
.logo,
@@ -114,12 +190,16 @@ ul {
}
.logo {
flex-wrap: inherit
flex-wrap: inherit;
align-items: center;
gap: 4px;
}
.logo a {
font-size: 24px;
font-weight: 700
font-weight: 700;
display: inline-flex;
align-items: center;
}
.logo a img {
@@ -128,17 +208,17 @@ ul {
pointer-events: none;
transform: translate(0, -10%);
border-radius: 6pt;
margin-inline-end: 8pt
margin-inline-end: 8pt;
}
#menu {
list-style: none;
word-break: keep-all;
overflow-x: auto;
white-space: nowrap
white-space: nowrap;
}
#menu li+li {
#menu li + li {
margin-inline-start: 24pt;
}
@@ -150,18 +230,27 @@ ul {
font-weight: 500;
}
.page-shell {
width: min(1120px, calc(100% - 32px));
margin: 0 auto;
padding-bottom: 32px;
}
.main {
zoom: 110%;
text-align: center;
position: absolute;
top: 50%;
top: 52%;
left: 50%;
transform: translate(-50%, -50%);
width: 50%;
padding: 10pt;
width: min(760px, calc(100% - 32px));
padding: 24px 12px 12px;
color: var(--primary);
}
.hero-main {
position: absolute;
}
.svg-wrapper {
margin: 0 auto;
width: 320pt;
@@ -194,23 +283,26 @@ ul {
transform: translate(0, -64pt);
}
.name:hover {
.name:hover,
.des:hover,
.mobile-name:hover {
color: var(--secondary);
cursor: default;
}
.des {
transition: 0.8s;
font-size: larger;
font-size: 1.06rem;
display: block;
}
.des:hover {
color: var(--secondary);
cursor: default;
.hero-description {
margin: 0;
}
.des:focus {
color: var(--secondary);
.hero-description-secondary {
margin-top: 6px;
color: var(--muted);
}
.profile {
@@ -218,6 +310,70 @@ ul {
height: 190pt;
border-radius: 50%;
transition: 0.8s;
object-fit: cover;
border: 4px solid rgba(235, 82, 52, 0.18);
box-shadow: var(--shadow);
}
.hero-main .profile,
.hero-main .svg-wrapper,
.hero-main .hero-description,
.hero-main .hero-badges,
.hero-main .hero-actions,
.hero-main .social-icons {
will-change: transform;
transition: transform 0.18s ease-out;
}
.hero-badges {
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
margin-top: 18px;
}
.badge {
border: 1px solid var(--border);
background: var(--panel);
color: var(--primary);
border-radius: 999px;
padding: 8px 14px;
font-size: 0.88rem;
}
.hero-actions {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
margin-top: 20px;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 148px;
border-radius: 12px;
padding: 12px 18px;
font-weight: 600;
border: 1px solid transparent;
}
.button-primary {
background: var(--secondary);
color: #fff;
}
.button-primary:hover {
color: #fff;
filter: brightness(1.04);
}
.button-secondary {
background: transparent;
border-color: var(--border);
}
.social-icons {
@@ -235,7 +391,6 @@ ul {
.social-icons a {
color: var(--primary);
text-decoration: none;
}
.icon {
@@ -247,7 +402,9 @@ ul {
}
.logo-switches {
flex-wrap: inherit
flex-wrap: inherit;
display: inline-flex;
align-items: center;
}
#theme-toggle svg {
@@ -255,12 +412,18 @@ ul {
}
button#theme-toggle {
background-color: var(--theme);
background-color: transparent;
border: none;
color: var(--primary);
font-size: 26px;
margin: auto 4pt;
margin: 0 0 0 4pt;
transition: 0.8s;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
padding: 0;
height: 32px;
}
button#theme-toggle:hover {
@@ -277,7 +440,6 @@ body:not(.light) #sun {
display: none;
}
/* Mobile Nav */
.cta {
cursor: pointer;
font-weight: 600;
@@ -293,50 +455,80 @@ body:not(.light) #sun {
}
.overlay {
height: 100%;
width: 0;
position: fixed;
z-index: 1;
left: 0;
top: 0;
background-color: #24252a;
overflow-x: hidden;
transition: all 0.5s ease 0s;
inset: 0;
z-index: 2147483647;
isolation: isolate;
background: rgba(20, 20, 24, 0.86);
backdrop-filter: blur(14px) saturate(135%);
-webkit-backdrop-filter: blur(14px) saturate(135%);
overflow: hidden;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.28s ease, visibility 0.28s ease;
}
.overlay--active {
width: 100%;
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.overlay__content {
position: relative;
z-index: 2;
display: flex;
height: 100%;
min-height: 100%;
width: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateY(12px) scale(0.985);
transition: opacity 0.24s ease, transform 0.24s ease;
}
.overlay--active .overlay__content {
opacity: 1;
transform: translateY(0) scale(1);
}
.overlay a {
color: white;
text-decoration: none;
padding: 15pt;
font-size: 36px;
font-size: clamp(1.9rem, 5vw, 2.35rem);
line-height: 1.1;
display: block;
transition: all 0.3s ease 0s;
text-align: center;
transition: color 0.24s ease, transform 0.24s ease;
}
.overlay__content a {
width: 100%;
max-width: 320px;
}
.overlay a:hover,
.overlay a:focus {
color: var(--secondary);
transform: translateY(-1px);
}
.overlay .close {
position: absolute;
z-index: 3;
top: 20pt;
right: 45pt;
font-size: 60px;
line-height: 1;
color: #edf0f1;
cursor: pointer;
border: 0;
background: transparent;
padding: 0;
appearance: none;
}
.mobile-name {
@@ -346,40 +538,56 @@ body:not(.light) #sun {
font-weight: 900;
}
.mobile-name:hover {
color: var(--secondary);
cursor: default;
body.menu-open {
overflow: hidden;
}
@media screen and (max-width:768pt) {
.profile img {
transform: scale(.85);
}
body.menu-open main,
body.menu-open .page-shell,
body.menu-open .container,
body.menu-open .projects-page,
body.menu-open .entry-page,
body.menu-open .footer {
pointer-events: none;
}
body.menu-open .header,
body.menu-open .overlay,
body.menu-open .overlay * {
pointer-events: auto;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
transition: 0.8s;
padding: 12pt 18pt;
background-color: var(--menu-text-bg);
border-radius: 18px;
}
.footer p {
margin: 0;
}
.loading-state {
color: var(--muted);
}
@media screen and (max-width: 768pt) {
.profile {
width: 128pt;
height: auto;
}
.first-entry {
min-height: 260pt;
}
.archive-month {
flex-direction: column;
}
.archive-year {
margin-top: 20pt;
height: 128pt;
}
.footer {
padding: calc((60 - 24pt - 10pt)/2)24pt;
padding: 16px;
}
}
@media only screen and (max-width: 740pt) {
.nav__links li a,
.svg-wrapper#main-wrapper {
display: none;
@@ -393,10 +601,256 @@ body:not(.light) #sun {
.logo {
margin-top: 12pt;
}
.logo-switches {
margin-top: 4pt;
}
.main {
width: 100%;
position: static;
transform: none;
width: min(760px, calc(100% - 24px));
margin: 44px auto 0;
padding-bottom: 40px;
}
}
.hero-main {
position: static;
}
.page-shell {
width: min(1120px, calc(100% - 20px));
}
.footer {
display: block;
text-align: center;
}
.social-icons {
margin-top: 6pt;
}
.overlay .close {
right: 20px;
top: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
body::before,
body::after {
transition: none;
transform: none !important;
}
.hero-main .profile,
.hero-main .svg-wrapper,
.hero-main .hero-description,
.hero-main .hero-badges,
.hero-main .hero-actions,
.hero-main .social-icons {
transition: none;
transform: none !important;
}
}
/* Glassy premium hero refinements */
:root {
--hero-shell-spot-x: 50%;
--hero-shell-spot-y: 28%;
--hero-shell-tilt-x: 0deg;
--hero-shell-tilt-y: 0deg;
}
body {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.015), transparent 28%),
linear-gradient(140deg, rgba(235, 82, 52, 0.045), transparent 34%),
linear-gradient(220deg, rgba(255, 0, 90, 0.03), transparent 30%),
var(--theme);
}
body::before {
background:
radial-gradient(circle at var(--hero-glow-x) var(--hero-glow-y), rgba(255, 255, 255, 0.08), transparent 12%),
radial-gradient(circle at var(--hero-glow-x) var(--hero-glow-y), rgba(235, 82, 52, 0.3), transparent 24%),
radial-gradient(circle at var(--hero-glow-alt-x) var(--hero-glow-alt-y), rgba(255, 0, 90, 0.18), transparent 24%);
filter: blur(78px);
opacity: 1;
}
body::after {
background:
radial-gradient(circle at 18% 18%, rgba(255, 255, 255, 0.055), transparent 15%),
radial-gradient(circle at 82% 14%, rgba(235, 82, 52, 0.08), transparent 18%),
radial-gradient(circle at 70% 76%, rgba(255, 255, 255, 0.03), transparent 20%);
filter: blur(110px);
opacity: 0.82;
}
.logo {
align-items: center;
}
.logo-switches {
align-items: center;
min-height: 32px;
}
button#theme-toggle {
margin-top: 1px;
}
.hero-main {
width: min(820px, calc(100% - 32px));
}
.hero-shell {
position: relative;
overflow: hidden;
border-radius: 30px;
padding: 34px 28px 18px;
border: 1px solid rgba(255, 255, 255, 0.13);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.02)),
linear-gradient(135deg, rgba(235, 82, 52, 0.09), transparent 42%),
linear-gradient(225deg, rgba(255, 0, 90, 0.05), transparent 34%),
rgba(20, 21, 24, 0.44);
box-shadow:
0 24px 80px rgba(0, 0, 0, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.18),
inset 0 -1px 0 rgba(255, 255, 255, 0.04);
backdrop-filter: blur(22px) saturate(135%);
-webkit-backdrop-filter: blur(22px) saturate(135%);
isolation: isolate;
transform: perspective(1200px) rotateX(var(--hero-shell-tilt-x)) rotateY(var(--hero-shell-tilt-y));
transition: transform 0.18s ease-out, border-color 0.25s ease-out, box-shadow 0.25s ease-out;
}
body.light .hero-shell {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(255, 255, 255, 0.4)),
linear-gradient(135deg, rgba(235, 82, 52, 0.08), transparent 42%),
linear-gradient(225deg, rgba(255, 0, 90, 0.04), transparent 34%),
rgba(255, 255, 255, 0.42);
border-color: rgba(255, 255, 255, 0.55);
box-shadow:
0 24px 60px rgba(0, 0, 0, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.75),
inset 0 -1px 0 rgba(255, 255, 255, 0.35);
}
.hero-shell::before,
.hero-shell::after,
.hero-shell__shine {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
}
.hero-shell::before {
background: radial-gradient(circle at var(--hero-shell-spot-x) var(--hero-shell-spot-y), rgba(255, 255, 255, 0.14), transparent 26%);
opacity: 0.85;
z-index: 0;
}
.hero-shell::after {
inset: 1px;
border-radius: 29px;
border: 1px solid rgba(255, 255, 255, 0.07);
z-index: 1;
}
.hero-shell__shine {
background: linear-gradient(115deg, transparent 18%, rgba(255, 255, 255, 0.085) 42%, transparent 68%);
transform: translate3d(calc(var(--hero-glow-shift-x) * 0.18), calc(var(--hero-glow-shift-y) * 0.16), 0) rotate(0deg);
opacity: 0.9;
z-index: 0;
}
.hero-shell > * {
position: relative;
z-index: 2;
}
.profile {
border: 4px solid rgba(255, 255, 255, 0.14);
box-shadow:
0 16px 42px rgba(0, 0, 0, 0.22),
0 0 0 1px rgba(255, 255, 255, 0.05);
}
.shape {
stroke: rgba(235, 82, 52, 0.95);
filter: drop-shadow(0 6px 18px rgba(235, 82, 52, 0.18));
}
.hero-description-secondary {
max-width: 640px;
margin-left: auto;
margin-right: auto;
}
.badge {
background: rgba(255, 255, 255, 0.07);
border-color: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
body.light .badge {
background: rgba(255, 255, 255, 0.55);
border-color: rgba(255, 255, 255, 0.6);
}
.button {
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.button-primary {
background: linear-gradient(135deg, rgba(255, 108, 74, 0.96), rgba(235, 82, 52, 0.9));
box-shadow:
0 10px 24px rgba(235, 82, 52, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.14);
}
.button-secondary {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.12);
}
body.light .button-secondary {
background: rgba(255, 255, 255, 0.55);
border-color: rgba(255, 255, 255, 0.65);
}
.social-icons {
padding-top: 16px;
}
@media only screen and (max-width: 740pt) {
.hero-main {
width: min(760px, calc(100% - 24px));
}
.hero-shell {
padding: 26px 18px 12px;
border-radius: 24px;
}
}
@media (prefers-reduced-motion: reduce) {
.hero-shell {
transform: none !important;
}
.hero-shell__shine {
transform: none !important;
}
}
+194
View File
@@ -0,0 +1,194 @@
(function() {
const CONTENT_INDEX_PATH = "./public/content/index.json";
const escapeHtml = (value) => String(value)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\"/g, "&quot;")
.replace(/'/g, "&#039;");
const formatDate = (value) => {
if (!value) {
return "";
}
const date = new Date(`${value}T00:00:00`);
if (Number.isNaN(date.getTime())) {
return value;
}
return new Intl.DateTimeFormat("en-CA", {
year: "numeric",
month: "short",
day: "numeric"
}).format(date);
};
const getIndex = async () => {
if (window.location.protocol === "file:") {
throw new Error("This content page needs a local web server. Run python3 -m http.server and open http://localhost:8000.");
}
const response = await fetch(CONTENT_INDEX_PATH);
if (!response.ok) {
throw new Error("Unable to load content index.");
}
return response.json();
};
const stripFrontMatter = (markdown) => {
if (!markdown.startsWith("---")) {
return markdown;
}
const parts = markdown.split("---");
if (parts.length < 3) {
return markdown;
}
return parts.slice(2).join("---").trim();
};
const renderTags = (tags, className) => {
if (!tags || !tags.length) {
return "";
}
return `<div class="${className}">${tags.map((tag) => `<span class="${className === "tag-list" ? "tag" : "entry-tag"}">${escapeHtml(tag)}</span>`).join("")}</div>`;
};
const renderLinks = (entry, className) => {
const links = [];
links.push(`<a class="${className} ${className}-primary" href="./entry.html?type=${encodeURIComponent(entry.type)}&slug=${encodeURIComponent(entry.slug)}">Read More</a>`);
if (entry.github) {
links.push(`<a class="${className}" href="${escapeHtml(entry.github)}" target="_blank" rel="noopener noreferrer">Github</a>`);
}
if (entry.demo) {
links.push(`<a class="${className}" href="${escapeHtml(entry.demo)}" target="_blank" rel="noopener noreferrer">Live</a>`);
}
return `<div class="${className.replace("card-link", "card-links")}">${links.join("")}</div>`;
};
const createCard = (entry) => {
const formattedDate = formatDate(entry.date);
const featuredClass = entry.featured ? " featured" : "";
return `
<article class="content-card${featuredClass}">
<div class="card-meta">
<span class="card-kicker">${escapeHtml(entry.label || entry.type)}</span>
${formattedDate ? `<span class="card-date">${escapeHtml(formattedDate)}</span>` : ""}
</div>
<h3>${escapeHtml(entry.title)}</h3>
<p class="card-summary">${escapeHtml(entry.summary || "")}</p>
${renderTags(entry.tags || [], "tag-list")}
${renderLinks(entry, "card-link")}
</article>
`;
};
const renderSection = (containerId, entries, emptyMessage) => {
const container = document.getElementById(containerId);
if (!container) {
return;
}
if (!entries.length) {
container.innerHTML = `<p class="loading-state">${escapeHtml(emptyMessage)}</p>`;
return;
}
container.innerHTML = entries.map(createCard).join("");
};
const initListPage = async () => {
try {
const index = await getIndex();
renderSection("projects-grid", index.projects || [], "No projects added yet.");
renderSection("updates-grid", index.updates || [], "No updates added yet.");
} catch (error) {
renderSection("projects-grid", [], error.message);
renderSection("updates-grid", [], error.message);
}
};
const initEntryPage = async () => {
const shell = document.getElementById("entry-shell");
if (!shell) {
return;
}
try {
const params = new URLSearchParams(window.location.search);
const type = params.get("type");
const slug = params.get("slug");
if (!type || !slug) {
shell.innerHTML = `
<div class="entry-empty">
<p class="eyebrow">Not found</p>
<h1 class="entry-title">This entry could not be loaded.</h1>
<a class="entry-back" href="./projects.html">Back to projects</a>
</div>
`;
return;
}
const index = await getIndex();
const entries = [...(index.projects || []), ...(index.updates || [])];
const entry = entries.find((item) => item.type === type && item.slug === slug);
if (!entry) {
throw new Error("Entry not found.");
}
const response = await fetch(entry.path);
if (!response.ok) {
throw new Error("Unable to load markdown entry.");
}
const rawMarkdown = await response.text();
const markdown = stripFrontMatter(rawMarkdown);
shell.innerHTML = `
<div class="entry-header">
<a class="entry-back" href="./projects.html">Back to projects</a>
<div class="entry-meta">
<span class="entry-pill entry-pill-primary">${escapeHtml(entry.label || entry.type)}</span>
${entry.date ? `<span class="entry-pill entry-pill-secondary">${escapeHtml(formatDate(entry.date))}</span>` : ""}
${entry.status ? `<span class="entry-pill entry-pill-secondary">${escapeHtml(entry.status)}</span>` : ""}
</div>
<h1 class="entry-title">${escapeHtml(entry.title)}</h1>
<p class="entry-summary">${escapeHtml(entry.summary || "")}</p>
${renderTags(entry.tags || [], "entry-tag-list")}
<div class="entry-links">
${entry.github ? `<a class="inline-button" href="${escapeHtml(entry.github)}" target="_blank" rel="noopener noreferrer">Github</a>` : ""}
${entry.demo ? `<a class="inline-button inline-button-primary" href="${escapeHtml(entry.demo)}" target="_blank" rel="noopener noreferrer">Live</a>` : ""}
</div>
</div>
<article class="entry-content">
${window.marked.parse(markdown)}
</article>
`;
} catch (error) {
shell.innerHTML = `
<div class="entry-empty">
<p class="eyebrow">Not found</p>
<h1 class="entry-title">${escapeHtml(error.message)}</h1>
<a class="entry-back" href="./projects.html">Back to projects</a>
</div>
`;
}
};
window.PORTFOLIO_CONTENT = {
initListPage,
initEntryPage
};
})();
+82
View File
@@ -0,0 +1,82 @@
(function() {
const hero = document.querySelector(".hero-main");
const heroShell = document.querySelector(".hero-main .hero-shell");
const profile = document.querySelector(".hero-main .profile");
const title = document.querySelector(".hero-main .svg-wrapper");
const descriptions = document.querySelectorAll(
".hero-main .hero-description, .hero-main .hero-badges, .hero-main .hero-actions, .hero-main .social-icons"
);
if (!hero || !heroShell || !profile || !title) {
return;
}
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const coarsePointer = window.matchMedia("(pointer: coarse)").matches;
if (prefersReducedMotion || coarsePointer) {
return;
}
let targetX = 0;
let targetY = 0;
let currentX = 0;
let currentY = 0;
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
const setTransform = (element, x, y) => {
element.style.transform = `translate3d(${x}px, ${y}px, 0)`;
};
const animate = () => {
currentX += (targetX - currentX) * 0.065;
currentY += (targetY - currentY) * 0.065;
const bgX = 50 + currentX * 6.8;
const bgY = 18 + currentY * 5.6;
const bgXAlt = 50 - currentX * 5.2;
const bgYAlt = 30 - currentY * 4.6;
const glowRotate = currentX * 16 + currentY * 10;
const glowShiftX = currentX * 54;
const glowShiftY = currentY * 42;
document.body.style.setProperty("--hero-glow-x", `${bgX}%`);
document.body.style.setProperty("--hero-glow-y", `${bgY}%`);
document.body.style.setProperty("--hero-glow-alt-x", `${bgXAlt}%`);
document.body.style.setProperty("--hero-glow-alt-y", `${bgYAlt}%`);
document.body.style.setProperty("--hero-glow-rotate", `${glowRotate}deg`);
document.body.style.setProperty("--hero-glow-shift-x", `${glowShiftX}px`);
document.body.style.setProperty("--hero-glow-shift-y", `${glowShiftY}px`);
heroShell.style.setProperty("--hero-shell-spot-x", `${50 + currentX * 14}%`);
heroShell.style.setProperty("--hero-shell-spot-y", `${28 + currentY * 10}%`);
heroShell.style.setProperty("--hero-shell-tilt-x", `${currentY * -2.2}deg`);
heroShell.style.setProperty("--hero-shell-tilt-y", `${currentX * 2.6}deg`);
setTransform(profile, currentX * -4.5, currentY * -4.5);
setTransform(title, currentX * 2.2, currentY * 2.2);
descriptions.forEach((element, index) => {
const strength = 1.3 + index * 0.35;
setTransform(element, currentX * strength, currentY * strength);
});
requestAnimationFrame(animate);
};
window.addEventListener("mousemove", (event) => {
const x = clamp((event.clientX / window.innerWidth - 0.5) * 2, -1, 1);
const y = clamp((event.clientY / window.innerHeight - 0.5) * 2, -1, 1);
targetX = x;
targetY = y;
});
window.addEventListener("mouseleave", () => {
targetX = 0;
targetY = 0;
});
requestAnimationFrame(animate);
})();
+26 -6
View File
@@ -1,12 +1,32 @@
const doc = document;
const body = doc.body;
const menuOpen = doc.querySelector(".menu");
const menuClose = doc.querySelector(".close");
const overlay = doc.querySelector(".overlay");
menuOpen.addEventListener("click", () => {
overlay.classList.add("overlay--active");
});
if (menuOpen && menuClose && overlay) {
const openMenu = () => {
overlay.classList.add("overlay--active");
body.classList.add("menu-open");
};
menuClose.addEventListener("click", () => {
overlay.classList.remove("overlay--active");
});
const closeMenu = () => {
overlay.classList.remove("overlay--active");
body.classList.remove("menu-open");
};
menuOpen.addEventListener("click", openMenu);
menuClose.addEventListener("click", closeMenu);
overlay.addEventListener("click", (event) => {
if (event.target === overlay) {
closeMenu();
}
});
window.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeMenu();
}
});
}
+31
View File
@@ -0,0 +1,31 @@
(function() {
const body = document.body;
const themeToggle = document.getElementById("theme-toggle");
const applyStoredTheme = () => {
const storedTheme = localStorage.getItem("pref-theme") || sessionStorage.getItem("theme") || "dark";
if (storedTheme === "light") {
body.classList.add("light");
sessionStorage.setItem("theme", "light");
localStorage.setItem("pref-theme", "light");
return;
}
body.classList.remove("light");
sessionStorage.setItem("theme", "dark");
localStorage.setItem("pref-theme", "dark");
};
applyStoredTheme();
if (themeToggle) {
themeToggle.addEventListener("click", () => {
const isLight = body.classList.toggle("light");
const nextTheme = isLight ? "light" : "dark";
sessionStorage.setItem("theme", nextTheme);
localStorage.setItem("pref-theme", nextTheme);
});
}
body.classList.remove("js-stop-transition");
})();