This commit is contained in:
2026-01-29 05:18:41 -08:00
parent 4c13d12938
commit 12e6fadf6e
15 changed files with 355 additions and 196 deletions
+59 -1
View File
@@ -10,7 +10,8 @@
"dependencies": { "dependencies": {
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0" "react-dom": "^19.2.0",
"react-router-dom": "^7.13.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
@@ -1971,6 +1972,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2869,6 +2883,44 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-router": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz",
"integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz",
"integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==",
"license": "MIT",
"dependencies": {
"react-router": "7.13.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/resolve-from": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -2940,6 +2992,12 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+2 -1
View File
@@ -12,7 +12,8 @@
"dependencies": { "dependencies": {
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0" "react-dom": "^19.2.0",
"react-router-dom": "^7.13.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
+7 -182
View File
@@ -1,187 +1,12 @@
import { useState } from "react"; import { Routes, Route } from "react-router-dom";
import { useRef } from "react"; import HomePage from "./HomePage";
import confetti from "canvas-confetti"; import YesPage from "./yes";
import "./App.css";
export default function App() { export default function App() {
const blockbusterRef = useRef(null);
const moveButton = () => {
const button = blockbusterRef.current;
const dialog = button.closest("dialog");
const dialogRect = dialog.getBoundingClientRect();
const buttonRect = button.getBoundingClientRect();
const padding = 20;
const maxX = dialogRect.width - buttonRect.width - padding;
const maxY = dialogRect.height - buttonRect.height - padding;
const x = Math.random() * maxX;
const y = Math.random() * maxY;
button.style.left = `${x}px`;
button.style.top = `${y}px`;
};
const dialogRef = useRef(null);
const openPopup = () => {
dialogRef.current.showModal();
};
const closePopup = () => {
dialogRef.current.close();
};
const [showConfirm, setShowConfirm] = useState(false);
const [showConfirm2, setShowConfirm2] = useState(false);
const [showConfirm3, setShowConfirm3] = useState(false);
const [showConfirm4, setShowConfirm4] = useState(false);
const [showDeclineQ, setShowDeclineQ] = useState(false);
const [showDeclineOffer, setShowDeclineOffer] = useState(false);
const handleYes = () => {
const bursts = 9; // how many times it fires
for (let i = 0; i < bursts; i++) {
setTimeout(() => {
confetti({
particleCount: 80,
spread: 70,
origin: {
x: Math.random(), // 0 → 1 (left to right)
y: 0.9
// y: Math.random() * 0.6 // top half of screen
},
});
}, i * 200); // delay between bursts (ms)
}
};
const handleNo = () => {
setShowConfirm(true);
};
const handleYes2 = () => {
setShowConfirm2(true);
};
const handleYes3 = () => {
setShowConfirm3(true);
};
const handleYes4 = () => {
setShowConfirm4(true);
};
const handleNo5 = () => {
setShowDeclineQ(true);
};
return ( return (
<div className="container"> <Routes>
<h1>Will you be my Valentine? 💖</h1> <Route path="/" element={<HomePage />} />
<Route path="/yes" element={<YesPage />} />
<div className="photos"> </Routes>
<div className="photo">Photo 1</div>
<div className="photo">Photo 2</div>
<div className="photo">Photo 3</div>
</div>
<div className="buttons">
<button className="yes" onClick={handleYes}>
Yes 💕🥳
</button>
<button className="no" onClick={handleNo}>
No 😢
</button>
</div>
{showConfirm && (
<div className="overlay">
<div className="popup">
<h2>Are you sure? 😢</h2>
<button className="yes" onClick={handleYes2}>
Yes
</button>
<button className="no" onClick={handleYes}>
No
</button>
</div>
</div>
)}
{showConfirm2 && (
<div className="overlay">
<div className="popup">
<h2>Are you sure you're sure? 😭</h2>
<button className="yes" onClick={handleYes3}>
Yes
</button>
<button className="no" onClick={handleYes}>
No
</button>
</div>
</div>
)}
{showConfirm3 && (
<div className="overlay">
<div className="popup">
<h2>Are you sure that you're sure of being sure? 💔😭</h2>
<button className="yes" onClick={handleYes4}>
Yes
</button>
<button className="no" onClick={handleNo}>
No
</button>
</div>
</div>
)}
{showConfirm4 && (
<div className="overlay">
<div className="popup">
<h2>Have you tried being unsure? 🤞🙏</h2>
<button className="yes" onClick={handleYes}>
Yes
</button>
<button className="no" onClick={handleNo5}>
No
</button>
</div>
</div>
)}
{showDeclineQ && (
<div className="overlay">
<div className="popup">
<h2>Would you like to decline to be my valentine? 💔😭😢</h2>
<button className="yes" onClick={openPopup}>
Yes
</button>
<button className="no" onClick={handleNo}>
No
</button>
</div>
</div>
)}
<dialog ref={dialogRef} className="modal">
<h2>How to Decline Offer:</h2>
<p>Offer can only be declined in person at your nearest Blockbuster location*</p>
<p>*Bend, Oregon location is not a valid location to decline this offer</p>
<button onClick={closePopup}>
I understand, I'll be your valentine 💖
</button>
<button
ref={blockbusterRef}
className="runaway"
onMouseEnter={moveButton}
onClick={closePopup}
>
I understand, I will go to blockbuster
</button>
</dialog>
</div>
); );
} }
+236
View File
@@ -0,0 +1,236 @@
import { useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import confetti from "canvas-confetti";
import hozier from "./assets/hozier.jpeg"
import newberg_tree from "./assets/newberg_tree.jpeg"
import newberg_tractor from "./assets/newberg_tractor.jpeg"
import panic from "./assets/panic.jpeg"
import corvallis from "./assets/corvallis.jpeg"
import seal_rock from "./assets/seal_rock.jpeg"
import yaquina_lighthouse from "./assets/yaquina_lighthouse.jpeg"
import "./App.css";
export default function Homepage() {
const navigate = useNavigate();
const dialogRef = useRef(null);
const blockbusterButtonRef = useRef(null);
const isTouchDevice = () =>
"ontouchstart" in window || navigator.maxTouchPoints > 0;
const [showConfirm, setShowConfirm] = useState(false);
const [showConfirm2, setShowConfirm2] = useState(false);
const [showConfirm3, setShowConfirm3] = useState(false);
const [showConfirm4, setShowConfirm4] = useState(false);
const [showDeclineQ, setShowDeclineQ] = useState(false);
const openPopup = () => {
dialogRef.current.showModal();
// Wait for layout to settle
requestAnimationFrame(() => {
const button = blockbusterButtonRef.current;
const rect = button.getBoundingClientRect();
button.style.left = `${rect.left}px`;
button.style.top = `${rect.top}px`;
});
};
/* ------------------------------------------------------------------
* Runaway Blockbuster button logic
* ------------------------------------------------------------------ */
const moveButton = () => {
const button = blockbusterButtonRef.current;
const buttonRect = button.getBoundingClientRect();
const padding = 16;
const maxX = window.innerWidth - buttonRect.width - padding;
const maxY = window.innerHeight - buttonRect.height - padding;
const x = Math.random() * maxX;
const y = Math.random() * maxY;
button.style.left = `${Math.max(padding, x)}px`;
button.style.top = `${Math.max(padding, y)}px`;
};
const handleRunawayPress = (event) => {
if (isTouchDevice()) {
event.preventDefault();
event.stopPropagation();
moveButton();
}
};
/* ------------------------------------------------------------------
* Button handlers
* ------------------------------------------------------------------ */
const handleAccept = () => {
const bursts = 9;
for (let i = 0; i < bursts; i++) {
setTimeout(() => {
confetti({
particleCount: 80,
spread: 70,
origin: {
x: Math.random(),
y: 0.9,
// y: Math.random() * 0.6
},
});
}, i * 200);
}
};
const handleNo = () => {
setShowConfirm(true);
};
const handleYes2 = () => {
setShowConfirm2(true);
};
const handleYes3 = () => {
setShowConfirm3(true);
};
const handleYes4 = () => {
setShowConfirm4(true);
};
const handleNo5 = () => {
setShowDeclineQ(true);
};
/* ------------------------------------------------------------------
* Render
* ------------------------------------------------------------------ */
return (
<div className="container">
<h1>Will you be my Valentine? 💖</h1>
<div className="photos">
<div className="photo">
<img src={newberg_tree} alt="Photo 1" />
</div>
<div className="photo">
<img src={hozier} alt="Photo 1" />
</div>
<div className="photo">
<img src={panic} alt="Photo 1" />
</div>
</div>
<div className="buttons">
<button className="yes" onClick={handleAccept}>
Yes 💕🥳
</button>
<button className="no" onClick={handleNo}>
No 😢
</button>
</div>
{showConfirm && (
<div className="overlay">
<div className="popup">
<h3>Are you sure? 😢</h3>
<button className="yes" onClick={handleYes2}>
Yes
</button>
<button className="no" onClick={handleAccept}>
No
</button>
</div>
</div>
)}
{showConfirm2 && (
<div className="overlay">
<div className="popup">
<h3>Are you sure you're sure? 😭</h3>
<button className="yes" onClick={handleYes3}>
Yes
</button>
<button className="no" onClick={handleAccept}>
No
</button>
</div>
</div>
)}
{showConfirm3 && (
<div className="overlay">
<div className="popup">
<h3>Are you sure that you're sure of being sure? 💔😭</h3>
<button className="yes" onClick={handleYes4}>
Yes
</button>
<button className="no" onClick={handleAccept}>
No
</button>
</div>
</div>
)}
{showConfirm4 && (
<div className="overlay">
<div className="popup">
<h3>Have you tried being unsure? 🤞🙏</h3>
<button className="yes" onClick={handleAccept}>
Yes
</button>
<button className="no" onClick={handleNo5}>
No
</button>
</div>
</div>
)}
{showDeclineQ && (
<div className="overlay">
<div className="popup">
<h3>Would you like to decline to be my valentine? 💔😭😢</h3>
<button className="yes" onClick={openPopup}>
Yes
</button>
<button className="no" onClick={handleNo}>
No
</button>
</div>
</div>
)}
<dialog ref={dialogRef} className="modal">
<h2>How to Decline Offer:</h2>
<p>
Offer can only be declined in person at your nearest Blockbuster
location*
</p>
<p>
*Bend, Oregon location is not a valid location to decline this offer
</p>
<button onClick={handleAccept}>
I understand, I'll be your valentine 💖
</button>
<button
ref={blockbusterButtonRef}
className="runaway"
onMouseEnter={moveButton} // desktop
onTouchStart={handleRunawayPress} // mobile
onClick={handleRunawayPress} // fallback
>
I understand, I will go to blockbuster
</button>
</dialog>
</div>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

+27 -3
View File
@@ -42,7 +42,7 @@ button {
font-size: 1em; font-size: 1em;
font-weight: 500; font-weight: 500;
font-family: inherit; font-family: inherit;
background-color: #1a1a1a; background-color: #531300;
cursor: pointer; cursor: pointer;
transition: border-color 0.25s; transition: border-color 0.25s;
} }
@@ -81,9 +81,33 @@ dialog::backdrop {
.modal { .modal {
position: relative; position: relative;
overflow: hidden; /* prevent scrollbars */
} }
.runaway { .runaway {
position: absolute; position: fixed; /* escape the dialog */
transition: left 0.15s ease, top 0.15s ease; z-index: 9999; /* above everything */
transition: left 0.12s ease, top 0.12s ease;
}
.photos {
display: flex;
flex-wrap: wrap; /* allow photos to wrap to next line */
gap: 12px; /* space between photos */
justify-content: center; /* center on screen */
margin-bottom: 24px;
}
.photo img {
width: 150px;
height: 150px;
object-fit: cover;
border-radius: 12px;
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
transition: transform 0.2s;
}
.photo img:hover {
transform: scale(1.05);
} }
+11 -8
View File
@@ -1,10 +1,13 @@
import { StrictMode } from 'react' import React from "react";
import { createRoot } from 'react-dom/client' import ReactDOM from "react-dom/client";
import './index.css' import { BrowserRouter } from "react-router-dom";
import App from './App.tsx' import App from "./App";
import "./index.css"; // your global styles
createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<StrictMode> <React.StrictMode>
<BrowserRouter>
<App /> <App />
</StrictMode>, </BrowserRouter>
) </React.StrictMode>
);
+9
View File
@@ -0,0 +1,9 @@
export default function YesPage() {
return (
<div className="container">
<h1>Congratulations, you're now my valentine!!! 💖🥳</h1>
<h2>Great job using the freewill the website gave you to make the right choice!!!</h2>
<h3>No takebacks allowed, you're stuck with me 🎉</h3>
</div>
);
}
+3
View File
@@ -4,4 +4,7 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: {
host: true,
},
}) })