diff --git a/package-lock.json b/package-lock.json index ea63335..924fe43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "canvas-confetti": "^1.9.4", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -1971,6 +1972,19 @@ "dev": true, "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": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2869,6 +2883,44 @@ "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2940,6 +2992,12 @@ "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index d900bd4..5800c32 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "dependencies": { "canvas-confetti": "^1.9.4", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/src/App.tsx b/src/App.tsx index 9e20659..32806d2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,187 +1,12 @@ -import { useState } from "react"; -import { useRef } from "react"; -import confetti from "canvas-confetti"; -import "./App.css"; +import { Routes, Route } from "react-router-dom"; +import HomePage from "./HomePage"; +import YesPage from "./yes"; 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 ( -
-

Will you be my Valentine? 💖

- -
-
Photo 1
-
Photo 2
-
Photo 3
-
- -
- - -
- - {showConfirm && ( -
-
-

Are you sure? 😢

- - -
-
- )} - {showConfirm2 && ( -
-
-

Are you sure you're sure? 😭

- - -
-
- )} - {showConfirm3 && ( -
-
-

Are you sure that you're sure of being sure? 💔😭

- - -
-
- )} - {showConfirm4 && ( -
-
-

Have you tried being unsure? 🤞🙏

- - -
-
- )} - {showDeclineQ && ( -
-
-

Would you like to decline to be my valentine? 💔😭😢

- - -
-
- )} - -

How to Decline Offer:

-

Offer can only be declined in person at your nearest Blockbuster location*

-

*Bend, Oregon location is not a valid location to decline this offer

- - - - -
- -
+ + } /> + } /> + ); } diff --git a/src/HomePage.tsx b/src/HomePage.tsx new file mode 100644 index 0000000..5c9fbee --- /dev/null +++ b/src/HomePage.tsx @@ -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 ( +
+

Will you be my Valentine? 💖

+ +
+
+ Photo 1 +
+
+ Photo 1 +
+
+ Photo 1 +
+
+ +
+ + +
+ + {showConfirm && ( +
+
+

Are you sure? 😢

+ + +
+
+ )} + + {showConfirm2 && ( +
+
+

Are you sure you're sure? 😭

+ + +
+
+ )} + + {showConfirm3 && ( +
+
+

Are you sure that you're sure of being sure? 💔😭

+ + +
+
+ )} + + {showConfirm4 && ( +
+
+

Have you tried being unsure? 🤞🙏

+ + +
+
+ )} + + {showDeclineQ && ( +
+
+

Would you like to decline to be my valentine? 💔😭😢

+ + +
+
+ )} + + +

How to Decline Offer:

+

+ Offer can only be declined in person at your nearest Blockbuster + location* +

+

+ *Bend, Oregon location is not a valid location to decline this offer +

+ + + + + +
+
+ ); +} diff --git a/src/assets/corvallis.jpeg b/src/assets/corvallis.jpeg new file mode 100644 index 0000000..7374abb Binary files /dev/null and b/src/assets/corvallis.jpeg differ diff --git a/src/assets/hozier.jpeg b/src/assets/hozier.jpeg new file mode 100644 index 0000000..5558e7e Binary files /dev/null and b/src/assets/hozier.jpeg differ diff --git a/src/assets/newberg_tractor.jpeg b/src/assets/newberg_tractor.jpeg new file mode 100644 index 0000000..f7f6312 Binary files /dev/null and b/src/assets/newberg_tractor.jpeg differ diff --git a/src/assets/newberg_tree.jpeg b/src/assets/newberg_tree.jpeg new file mode 100644 index 0000000..9ef535c Binary files /dev/null and b/src/assets/newberg_tree.jpeg differ diff --git a/src/assets/panic.jpeg b/src/assets/panic.jpeg new file mode 100644 index 0000000..7251792 Binary files /dev/null and b/src/assets/panic.jpeg differ diff --git a/src/assets/seal_rock.jpeg b/src/assets/seal_rock.jpeg new file mode 100644 index 0000000..1f79fa5 Binary files /dev/null and b/src/assets/seal_rock.jpeg differ diff --git a/src/assets/yaquina_lighthouse.jpeg b/src/assets/yaquina_lighthouse.jpeg new file mode 100644 index 0000000..b028c69 Binary files /dev/null and b/src/assets/yaquina_lighthouse.jpeg differ diff --git a/src/index.css b/src/index.css index 1b95ec4..6070478 100644 --- a/src/index.css +++ b/src/index.css @@ -42,7 +42,7 @@ button { font-size: 1em; font-weight: 500; font-family: inherit; - background-color: #1a1a1a; + background-color: #531300; cursor: pointer; transition: border-color 0.25s; } @@ -81,9 +81,33 @@ dialog::backdrop { .modal { position: relative; + overflow: hidden; /* prevent scrollbars */ } .runaway { - position: absolute; - transition: left 0.15s ease, top 0.15s ease; + position: fixed; /* escape the dialog */ + 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); } diff --git a/src/main.tsx b/src/main.tsx index bef5202..d81c2dc 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,13 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import React from "react"; +import ReactDOM from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import App from "./App"; +import "./index.css"; // your global styles -createRoot(document.getElementById('root')!).render( - - - , -) +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + + + +); diff --git a/src/yes.tsx b/src/yes.tsx new file mode 100644 index 0000000..858d001 --- /dev/null +++ b/src/yes.tsx @@ -0,0 +1,9 @@ +export default function YesPage() { + return ( +
+

Congratulations, you're now my valentine!!! 💖🥳

+

Great job using the freewill the website gave you to make the right choice!!!

+

No takebacks allowed, you're stuck with me 🎉

+
+ ); +} diff --git a/vite.config.ts b/vite.config.ts index 8b0f57b..259be20 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,4 +4,7 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + server: { + host: true, + }, })