Compare commits

...

8 Commits

Author SHA1 Message Date
cabbbyy
3c6654ba47 updated pipeline - correct dir
All checks were successful
Deploy on push / deploy (push) Has been skipped
2025-08-11 22:19:34 +08:00
cabbbyy
12f585642d added pipeline 2025-08-11 22:18:14 +08:00
cabbbyy
05f6603fc4 added js 2025-08-11 22:18:02 +08:00
cabbbyy
098d0f5b41 added css: 2025-08-11 22:17:45 +08:00
cabbbyy
5c4c2c3b24 added resume 2025-08-11 22:17:34 +08:00
cabbbyy
658be88bcc added favicon 2025-08-11 22:17:18 +08:00
cabbbyy
e1b512d8cd added images 2025-08-11 22:17:08 +08:00
cabbbyy
1cdaaf5dff added index 2025-08-11 22:17:01 +08:00
26 changed files with 677 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
name: Deploy on push
on:
push:
branches: [ "main" ]
jobs:
deploy:
runs-on: ubuntu-latest
if: contains(gitea.event.head_commit.message, '[deploy]')
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install ssh + rsync
run: |
sudo apt-get update
sudo apt-get install -y openssh-client rsync
- name: Setup SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -p 22 "rchou.org" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: Rsync to server
env:
SSH_TARGET_DIR: /home/cab/docker/portfolio/src
RSYNC_SOURCE: .
run: |
rsync -rz --delete \
--no-times --no-perms --no-owner --no-group \
--omit-dir-times --mkpath \
-e "ssh -i ~/.ssh/id_ed25519 -p 22" \
"$RSYNC_SOURCE"/ "deploy@rchou.org:${SSH_TARGET_DIR}/"

BIN
favicon/.DS_Store vendored Normal file

Binary file not shown.

6
favicon/about.txt Normal file
View File

@@ -0,0 +1,6 @@
This favicon was generated using the following font:
- Font Title: Fira Sans
- Font Author: undefined
- Font Source: https://fonts.gstatic.com/s/firasans/v17/va9B4kDNxMZdWfMOD5VnFK_uQR37fF3Wlg.ttf
- Font License: undefined)

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
favicon/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

BIN
favicon/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
favicon/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
favicon/site.webmanifest Normal file
View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

BIN
images/cabby.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
images/tancha.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
images/wip.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
images/work.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

111
index.html Normal file
View File

@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Meta Info -->
<title>Ryan Chou's Portfolio</title>
<meta charset="UTF-8">
<meta name="description" content="Ryan Chou's Portfolio">
<meta property="og:title" content="Ryan Chou's Portfolio">
<meta property="og:description" content="Ryan Chou's Portfolio">
<meta property="og:url" content="https://rchou.org">
<meta property="og:type" content="website">
<meta property="og:image" content="https://rchou.org/images/cabby.jpg">
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="./favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="./favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="./favicon/favicon-16x16.png">
<link rel="manifest" href="./favicon/site.webmanifest">
<!-- CSS -->
<link href="https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.css" rel="stylesheet">
<link rel="stylesheet" href="style/style.css">
<link rel="stylesheet" href="style/intro.css">
<link rel="stylesheet" href="style/mapbox.css">
<link rel="stylesheet" href="style/projects.css">
<link rel="stylesheet" href="style/hobbies.css">
<!-- Font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<!-- intro.css -->
<section id="intro" class="scroll-section">
<div id="intro" class="intro">
<h1><b>Hi,</b>
I'm Ryan Chou</h1>
<p>I'm a Computer Science Major at UCSC.
Keep scrolling to learn more about me!</p>
</div>
</section>
<!-- mapbox.css -->
<section id="intro" class="scroll-section">
<h2>Experience</h2>
<div class="map-wrapper">
<div id="map" class="map"></div>
<!-- buttons -->
<nav class="company-list" id="company-list"></nav>
<div id="popup" class="popup"></div>
</div>
<!-- blurb -->
<div class="blurb" id="blurb">
<p>Select a company to learn more about what I did there.</p>
</div>
</section>
<!-- projects.css -->
<section id="intro" class="scroll-section">
<div id="projects" class="projects">
<h2>Projects</h2>
<p>test</p>
</div>
</section>
<!-- hobbies.css -->
<section id="hobbies" class="scroll-section">
<h2>Hobbies</h2>
<div class="horizontal-section">
<div class="horizontal-section__inner">
<!-- Card #1 -->
<div class="card">
<img src="images/wip.jpg" alt="Hobby 1">
<div class="caption">Caption for hobby 1</div>
</div>
<!-- Card #2 -->
<div class="card">
<img src="images/wip.jpg" alt="Hobby 2">
<div class="caption">Caption for hobby 2</div>
</div>
<!-- Card #2 -->
<div class="card">
<img src="images/wip.jpg" alt="Hobby 2">
<div class="caption">Caption for hobby 2</div>
</div>
<!-- Card #2 -->
<div class="card">
<img src="images/wip.jpg" alt="Hobby 2">
<div class="caption">Caption for hobby 2</div>
</div>
<!-- Card #2 -->
<div class="card">
<img src="images/wip.jpg" alt="Hobby 2">
<div class="caption">
<p> test <br> test <br> test <br> test <br> test <br> test <br> test </p>
</div>
</div>
<!-- …add as many cards as you like… -->
</div>
</div>
</section>
<!-- thanks.css -->
<section id="intro" class="scroll-section">
<div id="thanks" class="thanks">
<h2>Thanks</h2>
<p>Thanks for stopping by. If you like what you saw, please leave a like and subscribe</p>
</div>
</section>
</div>
<!-- JS -->
<script src="https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.js"></script>
<script src="script/mapbox.js"></script>
<script src="script/scroll.js"></script>
</body>
</html>

6
resume.html Normal file
View File

@@ -0,0 +1,6 @@
<iframe
src="https://mozilla.github.io/pdf.js/web/viewer.html?file=https://rchou.org/resume.pdf"
width="100%"
height="800px"
style="border: none;">
</iframe>

BIN
resume.pdf Normal file

Binary file not shown.

160
script/mapbox.js Normal file
View File

@@ -0,0 +1,160 @@
// script.js
// 1⃣ Your Mapbox access token
mapboxgl.accessToken = 'pk.eyJ1IjoiY2FiYmJ5eSIsImEiOiJjbWRpYzY1MGgwYzA5Mm1xM25udDUzbGtpIn0.RX6zv4VI6r2vpj7IWidz0w';
// 2⃣ Initialize the map zoomed fully out (globelike)
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [0, 0],
zoom: 1,
attributionControl: false
});
// 3⃣ Define your companies ([lat, lng], zoom, imageUrl, description)
const companies = [
{
id: 'overview',
name: 'Overview',
coords: [40.14010599415405, -107.69483342077913],
zoom: 3,
imageUrl: 'images/wip.jpg',
description: `
<h2>Overview</h2>
<p>Use the controls in the top left corner to jump to a spot!</p>
`
},
{
id: 'tancha',
name: 'TanCha',
coords: [37.325382569107695, -122.01149036275054],
zoom: 9,
imageUrl: 'images/tancha.jpg',
description: `
<h2>TanCha</h2>
<p>Wip.</p>
`
},
{
id: 'pokehouse',
name: 'Poke House',
coords: [37.301113384436285, -121.9493462690826],
zoom: 9,
imageUrl: 'images/wip.jpg',
description: `
<h2>Poke House</h2>
<p>Wip.</p>
`
},
{
id: 'nanobase',
name: 'NanoBase',
coords: [34.68104506798117, 135.49167196036512],
zoom: 9,
imageUrl: 'images/work.jpg',
description: `
<h2>NanoBase</h2>
<p>Developed automated payroll processing scripts in Python, reducing errors by 85%.</p>
<a href="https://www.nanobase.co.jp/blogs/30">NanoBase Blog</a>
`
},
{
id: 'lanner',
name: 'Lanner Electronics',
coords: [43.69824981965482, -79.62570806485385],
zoom: 9,
imageUrl: 'images/wip.jpg',
description: `
<h2>Lanner Electronics</h2>
<p>Wip.</p>
`
}
];
// 4⃣ Populate the company list
const listEl = document.getElementById('company-list');
companies.forEach((company, idx) => {
const btn = document.createElement('button');
btn.textContent = company.name;
btn.id = company.id;
btn.addEventListener('click', () => selectCompany(idx));
listEl.appendChild(btn);
});
// Holder for mapboxgl.Marker instances
const markers = [];
// Reference to the .map-wrapper for our fixed popups
const mapWrapper = document.querySelector('.map-wrapper');
/**
* Renders a fixed-position popup (does not move with the map)
*/
function showFixedPopup(company, [lng, lat]) {
// Remove any existing fixed popups
document.querySelectorAll('.fixed-popup').forEach(el => el.remove());
// Build the popup element
const popupEl = document.createElement('div');
popupEl.className = 'fixed-popup';
popupEl.innerHTML = `
<img
loading="lazy"
src="${company.imageUrl}"
alt="Me at ${company.name}" />
<strong>${company.name}</strong>
`;
// Attach into the map wrapper (above the map canvas)
mapWrapper.appendChild(popupEl);
}
/**
* Handles company selection:
* - cleans up old markers/popups
* - flies the map
* - updates the blurb
* - adds marker + fixed popup (if not overview)
*/
function selectCompany(index) {
const { coords, zoom, description, id } = companies[index];
const [lat, lng] = coords;
// Remove existing markers
markers.forEach(m => m.remove());
markers.length = 0;
// Remove any existing popups
document.querySelectorAll('.mapboxgl-popup, .fixed-popup').forEach(el => el.remove());
// Fly to the new location
map.flyTo({
center: [lng, lat],
zoom,
speed: 1.5,
essential: true
});
// Update the description blurb
document.getElementById('blurb').innerHTML = description;
// Highlight the active button
document.querySelectorAll('.company-list button')
.forEach(b => b.classList.toggle('active', b.id === id));
// If it's the overview, skip marker + popup
if (index === 0) return;
// Add a marker
const marker = new mapboxgl.Marker()
.setLngLat([lng, lat])
.addTo(map);
markers.push(marker);
// Show our custom fixed popup
showFixedPopup(companies[index], [lng, lat]);
}
// Initialize on load with the overview
selectCompany(0);

59
script/scroll.js Normal file
View File

@@ -0,0 +1,59 @@
// scroll.js
document.addEventListener('DOMContentLoaded', () => {
// ─────────────────────────────────────────────────────────────────────────────
// 1) Vertical “zoomin/out” on .scrollsection using IntersectionObserver
// ─────────────────────────────────────────────────────────────────────────────
const sections = document.querySelectorAll('.scroll-section');
const io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
entry.target.classList.toggle('in-view', entry.isIntersecting);
});
}, { threshold: 0.15 });
sections.forEach(sec => io.observe(sec));
// ─────────────────────────────────────────────────────────────────────────────
// 2) Horizontalscroll “pin & pan” for the Hobbies section
// ─────────────────────────────────────────────────────────────────────────────
const wrapper = document.getElementById('hobbies');
if (!wrapper) return; // no Hobbies section → exit
// The element that will stay stuck in the viewport:
const stickySection = wrapper.querySelector('.horizontal-section');
// The inner flex row we translate left/right:
const inner = stickySection.querySelector('.horizontal-section__inner');
// A) Make the section itself “sticky” at the top of the viewport
stickySection.style.position = 'sticky';
stickySection.style.top = '3rem';
stickySection.style.height = '100vh';
stickySection.style.overflow = 'hidden';
// B) Compute how far we need to scroll to reveal all cards
let containerW = wrapper.clientWidth; // e.g. 70vw in px
let scrollableDist = inner.scrollWidth - containerW; // total horizontal span minus one screen
// C) Stretch the wrappers vertical height so that
// scrolling wrapperHeight(px) → moving inner by scrollableDist(px)
wrapper.style.height = `${window.innerHeight + scrollableDist}px`;
// D) On scroll, when within the wrappers “pin zone,”
// translate the inner row instead of moving vertically
window.addEventListener('scroll', () => {
const y = window.scrollY;
const top = wrapper.offsetTop;
const end = top + scrollableDist;
if (y >= top && y <= end) {
// map vertical scroll → horizontal translate
inner.style.transform = `translateX(-${y - top}px)`;
}
});
// E) Recompute on resize
window.addEventListener('resize', () => {
containerW = wrapper.clientWidth;
scrollableDist = inner.scrollWidth - containerW;
wrapper.style.height = `${window.innerHeight + scrollableDist}px`;
});
});

48
style/hobbies.css Normal file
View File

@@ -0,0 +1,48 @@
.hobbies {
width: 100%;
height: 100vh; /* adjust as you like */
border-radius: 10px !important; /* ← rounded corners */
margin-top: 1rem;
padding: 1rem;
}
/* Horizontalscroll wrapper */
.horizontal-section {
position: relative;
overflow: hidden;
}
/* Inner scroller — flex row, full viewport height */
.horizontal-section__inner {
position: absolute;
top: 0; left: 0;
display: flex;
height: 100vh;
will-change: transform;
}
/* Each card: same width as the container (70vw), stacked vertically inside */
.card {
flex: none;
width: 100%; /* same as .container width (70vw) */
margin-right: 1rem; /* gap between cards */
display: flex;
flex-direction: column;
}
/* Image on top */
.card img {
width: 100%;
height: auto;
border-radius: 10px;
object-fit: cover;
}
/* Caption below */
.card .caption {
margin-top: 0.5rem;
text-align: center;
color: var(--fg-one);
font-size: 0.95rem;
}

22
style/intro.css Normal file
View File

@@ -0,0 +1,22 @@
.intro {
width: 100%;
height: 100vh; /* adjust as you like */
border-radius: 10px !important; /* ← rounded corners */
margin-top: 1rem;
padding: 1rem;
}
.intro h1 {
font-size: 7rem;
font-weight: 700;
}
.intro b {
font-size: 10rem;
font-weight: 800;
}
.intro p {
font-size: 3rem;
}

155
style/mapbox.css Normal file
View File

@@ -0,0 +1,155 @@
/* map + overlay wrapper */
.map-wrapper {
position: relative;
}
/* Map */
.map {
width: 100%;
height: 70vh; /* adjust as you like */
border-radius: 10px !important; /* ← rounded corners */
margin-top: 1rem;
padding: 1rem;
}
/* removing watermark */
.mapboxgl-ctrl-logo { display: none !important; }
/* Company list */
.company-list {
position: absolute;
top: 1rem;
left: 1rem;
padding: 0.5rem;
max-height: 50vh;
overflow-y: auto;
z-index: 1;
color: var(--fg-one);
background: rgba(0, 0, 0, 0.1) !important;
backdrop-filter: blur(4px);
border-radius: 10px !important; /* ← rounded corners */
filter: drop-shadow(0 0 1px gray) !important; /* <- gives a border */
}
/* company list button*/
.company-list button {
display: block;
width: 100%;
padding: 0.5rem;
text-align: left;
cursor: pointer;
background: none;
border-radius: 10px; /* ← rounded corners */
border-width: 0px;
}
.company-list button:hover {
background: rgb(255, 255, 255, 0.2);
}
.company-list button + button {
margin-top: 0.5rem; /* Only applies if there's a previous button */
}
/* pressed company list button */
.company-list button.active {
background: rgb(255, 255, 255, 0.2);
filter: drop-shadow(0 0 2px gray) !important; /* <- gives a border */
color: var(--fg-one);
}
/* popup */
.mapboxgl-popup {
position: absolute !important;
transform: none !important; /* cancel out Mapboxs translate() */
overflow-y: auto;
z-index: 1;
top: 1rem !important;
left: auto !important;
right: 1rem !important;
}
/* popup bg */
.mapboxgl-popup-content {
background: rgba(0, 0, 0, 0.1) !important;
-webkit-backdrop-filter: blur(4px) !important;
backdrop-filter: blur(4px) !important;
border-radius: 10px !important; /* ← rounded corners */
filter: drop-shadow(0 0 1px gray) !important; /* <- gives a border */
color: var(--bg-one);
padding-bottom: 5px; /* or else its too big */
}
/* popup image */
.mapboxgl-popup-content img {
border-radius: 5px !important; /* ← rounded corners */
filter: drop-shadow(0 0 1px gray) !important; /* <- gives a border */
width: 100%; /* fill the popups width */
max-width: none; /* ignore any mapbox defaults */
height: auto; /* maintain aspect ratio */
}
/* hide the popup tip/arrow */
.mapboxgl-popup-tip {
display: none;
}
/* Caption */
.blurb {
width: 100%;
margin-top: 1rem;
padding: 1rem;
background: var(--bg-two);
color: var(--fg-one);
border-radius: 10px;
min-height: 8rem; /* optional */
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
font-size: 1rem;
}
/* blurb title */
.blurb h2 {
font-size: 1.5rem;
}
/* fixed popup that wont move with the map */
.fixed-popup {
position: absolute !important;
transform: none !important; /* cancel out Mapboxs translate() */
z-index: 1;
top: 1rem !important;
right: -1rem !important;
background: rgba(0, 0, 0, 0.1) !important;
-webkit-backdrop-filter: blur(4px) !important;
backdrop-filter: blur(4px) !important;
border-radius: 10px !important; /* ← rounded corners */
filter: drop-shadow(0 0 1px gray) !important; /* <- gives a border */
color: var(--bg-one);
padding: 0.75rem;
border-radius: 6px;
max-width: 200px;
pointer-events: none;
}
.fixed-popup img {
width: 100%;
border-radius: 4px;
margin-bottom: 0.5rem;
}

8
style/projects.css Normal file
View File

@@ -0,0 +1,8 @@
.projects {
width: 100%;
height: 50vh; /* adjust as you like */
border-radius: 10px !important; /* ← rounded corners */
margin-top: 1rem;
padding: 1rem;
}

52
style/style.css Normal file
View File

@@ -0,0 +1,52 @@
/* styles.css
* Global Color Theme
* ex: ` color: var(--red);`
*/
:root {
--bg-one: #111111; /* black */
--bg-two: #303030; /* gray */
--fg-one: #ffffff; /* white */
--fg-two: #f3f3f3; /* light gray */
--blue: #1e90ff;
--red: #e63946;
--green: #2a9d8f;
}
/* General Site */
html, body {
margin: 0;
padding: 0;
height: 100%;
background-color: var(--bg-one);
color: var(--fg-one);
}
/* container centered at 85% width */
.container {
width: 70vw;
margin: 0 auto;
}
h2 {
font-size: 3rem;
}
/* scroll.js */
/* base state: slightly shrunken + faded out */
.scroll-section {
transform: scale(0.85);
opacity: 0.5;
transition:
transform 0.6s cubic-bezier(.22,.61,.36,1),
opacity 0.6s cubic-bezier(.22,.61,.36,1);
transform-origin: center center;
}
/* when in view: full size + fully opaque */
.scroll-section.in-view {
transform: scale(1);
opacity: 1;
}

8
style/thanks.css Normal file
View File

@@ -0,0 +1,8 @@
.thanks {
width: 100%;
height: 50vh; /* adjust as you like */
border-radius: 10px !important; /* ← rounded corners */
margin-top: 1rem;
padding: 1rem;
}