Skip to content

Commit 4d1e3f9

Browse files
committed
add vaffel queue
1 parent 762cf54 commit 4d1e3f9

8 files changed

Lines changed: 3410 additions & 1698 deletions

File tree

pnpm-lock.yaml

Lines changed: 3165 additions & 1692 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ import { isAugust, isMsgExpired, isValentinesSeason } from "./utils/date";
88
import WelcomeScreen from "./pages/welcome-screen";
99
import { AutoReload } from "./components/auto-reload";
1010
import { useMessage } from "./hooks/use-message";
11+
import { useVaffel } from "./hooks/use-vaffel";
12+
import VaffelScreen from "./pages/vaffel-screen";
1113

1214
export default function App() {
1315
const { data: message } = useMessage();
16+
const { data: vaffel } = useVaffel();
1417

1518
useEffect(() => {
1619
const interval = setInterval(
@@ -22,7 +25,7 @@ export default function App() {
2225
return () => clearInterval(interval);
2326
}, []);
2427

25-
const visibleScreens = [CalendarScreen, TransportScreen];
28+
let visibleScreens = [CalendarScreen, TransportScreen];
2629
if (isAugust()) {
2730
visibleScreens.push(WelcomeScreen);
2831
}
@@ -31,6 +34,13 @@ export default function App() {
3134
visibleScreens.push(MessageScreen);
3235
}
3336

37+
if (vaffel?.status === "open") {
38+
console.log(JSON.stringify(vaffel));
39+
visibleScreens = [
40+
() => <VaffelScreen queue={vaffel.queue} status={vaffel.status} total={vaffel.total} />
41+
];
42+
}
43+
3444
const isValentines = isValentinesSeason();
3545
document.body.classList.toggle("valentines", isValentines);
3646

src/components/utepils.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,14 @@ function getBarClass(score: number) {
1313
}
1414

1515
export default function UtepilsCard() {
16-
const { verdict, score, loading, error } = useUtepils();
17-
console.log("Utepils verdict:", verdict, "Score:", score, "Loading:", loading, "Error:", error);
16+
const { verdict, score } = useUtepils();
1817
const title = verdict?.title || "Laster...";
1918
const subtitle = verdict?.subtitle || "Henter utepils-data";
2019
const emoji = verdict?.emoji || "⏳";
2120

2221
return (
2322
<div
24-
className={`${getBackgroundClass(score)} rounded-2xl p-4 min-w-[240px] max-w-[320px] border border-yellow-200 font-sans`}
23+
className={`${getBackgroundClass(score)} rounded-2xl p-4 min-w-60 max-w-[320px] border border-yellow-200 font-sans`}
2524
>
2625
<div className="flex justify-between items-start">
2726
<div>

src/components/vaffel.tsx

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"use client";
2+
import { useEffect, useRef } from "react";
3+
4+
export default function HeartsMinimal() {
5+
const ref = useRef<HTMLCanvasElement | null>(null);
6+
7+
useEffect(() => {
8+
const canvas = ref.current;
9+
if (!canvas) return;
10+
11+
const ctx = canvas.getContext("2d");
12+
if (!ctx) return;
13+
14+
const SPEED_Y = 0.35;
15+
const DRIFT_X = 0.2;
16+
const heartSpriteSize = 27;
17+
18+
function randomInt(min: number, max: number) {
19+
return Math.floor(Math.random() * (max - min + 1)) + min;
20+
}
21+
function rand(min: number, max: number) {
22+
return Math.random() * (max - min) + min;
23+
}
24+
25+
let w = 0,
26+
h = 0,
27+
DPR = 1;
28+
29+
const resize = () => {
30+
w = window.innerWidth;
31+
h = window.innerHeight;
32+
DPR = Math.min(Math.max(window.devicePixelRatio || 1, 1), 2);
33+
canvas.width = Math.floor(w * DPR);
34+
canvas.height = Math.floor(h * DPR);
35+
canvas.style.width = w + "px";
36+
canvas.style.height = h + "px";
37+
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
38+
};
39+
40+
resize();
41+
window.addEventListener("resize", resize);
42+
43+
const heartSprite = makeWaffleSprite(heartSpriteSize);
44+
45+
type Heart = {
46+
x: number;
47+
y: number;
48+
rot: number;
49+
vr: number;
50+
s: number;
51+
vy: number;
52+
vx: number;
53+
alpha: number;
54+
};
55+
56+
const hearts: Heart[] = [];
57+
const numHearts = randomInt(150, 200);
58+
59+
for (let i = 0; i < numHearts; i += 1) {
60+
hearts.push({
61+
x: randomInt(0, w),
62+
y: randomInt(0, h),
63+
rot: Math.random() * Math.PI * 2,
64+
vr: rand(-0.01, 0.01),
65+
s: rand(12, 22),
66+
vy: rand(0.6, 1.2),
67+
vx: rand(0.6, 1.15),
68+
alpha: rand(0.45, 0.8)
69+
});
70+
}
71+
72+
let raf = 0;
73+
74+
const draw = () => {
75+
ctx.clearRect(0, 0, w, h);
76+
77+
for (let i = 0; i < hearts.length; i += 1) {
78+
const ht = hearts[i];
79+
80+
ctx.save();
81+
ctx.translate(ht.x, ht.y);
82+
ht.rot += ht.vr;
83+
ctx.rotate(ht.rot);
84+
ctx.globalAlpha = ht.alpha;
85+
86+
ctx.drawImage(heartSprite, -ht.s / 2, -ht.s / 2, ht.s, ht.s);
87+
88+
ctx.restore();
89+
ctx.globalAlpha = 1;
90+
91+
ht.y += SPEED_Y * ht.vy;
92+
ht.x += DRIFT_X * ht.vx;
93+
94+
// litt “svev”
95+
ht.x += Math.sin((ht.y + i) * 0.02) * 0.15;
96+
97+
// wrap sidene
98+
if (ht.x > w) ht.x = 0;
99+
else if (ht.x < 0) ht.x = w;
100+
101+
// respawn på toppen
102+
if (ht.y > h) {
103+
ht.x = randomInt(0, w);
104+
ht.y = -20;
105+
ht.rot = Math.random() * Math.PI * 2;
106+
ht.vr = rand(-0.01, 0.01);
107+
ht.s = rand(12, 22);
108+
ht.vy = rand(0.6, 1.2);
109+
ht.vx = rand(0.6, 1.15);
110+
ht.alpha = rand(0.45, 0.8);
111+
}
112+
}
113+
114+
raf = requestAnimationFrame(draw);
115+
};
116+
117+
raf = requestAnimationFrame(draw);
118+
119+
return () => {
120+
cancelAnimationFrame(raf);
121+
window.removeEventListener("resize", resize);
122+
};
123+
}, []);
124+
125+
return (
126+
<canvas
127+
ref={ref}
128+
aria-hidden
129+
style={{
130+
position: "fixed",
131+
inset: 0,
132+
width: "100vw",
133+
height: "100vh",
134+
pointerEvents: "none",
135+
zIndex: 9999
136+
}}
137+
/>
138+
);
139+
}
140+
141+
function makeWaffleSprite(size: number): HTMLCanvasElement {
142+
const spriteSize = size * 3;
143+
const c = document.createElement("canvas");
144+
c.width = c.height = spriteSize;
145+
146+
const g = c.getContext("2d")!;
147+
g.font = `${spriteSize * 0.85}px serif`;
148+
g.textAlign = "center";
149+
g.textBaseline = "middle";
150+
g.globalAlpha = 0.75;
151+
g.fillText("🧇", spriteSize / 2, spriteSize / 2);
152+
153+
return c;
154+
}

src/hooks/use-utepils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export function useUtepils() {
1717
fetch("https://utepils-ten.vercel.app/api/utepils/bergen")
1818
.then((res) => {
1919
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
20-
console.log("Fetched utepils data:", res);
2120
return res.json();
2221
})
2322
.then((data) => {

src/hooks/use-vaffel.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
3+
export type VaffelQueue = {
4+
user_id: string;
5+
display_name: string;
6+
total_orders: number;
7+
}[];
8+
9+
const API_URL = "https://vaffel.echo-webkom.no/687650156262195217";
10+
11+
export function useVaffel() {
12+
return useQuery({
13+
queryKey: ["vaffel"],
14+
queryFn: async () => {
15+
const [queueRes, statusRes, totalRes] = await Promise.all([
16+
fetch(`${API_URL}/queue`),
17+
fetch(`${API_URL}/status`),
18+
fetch(`${API_URL}/total`)
19+
]);
20+
21+
const queue: VaffelQueue = await queueRes.json();
22+
const status: string = await statusRes.text();
23+
const total: number = await totalRes.json();
24+
25+
return { queue, status, total };
26+
},
27+
staleTime: 1000
28+
});
29+
}

src/index.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
.valentines {
1919
--border: 338, 68%, 63%;
20-
--primary: 338, 68%, 63%
20+
--primary: 338, 68%, 63%;
2121
}
2222

2323
@theme inline {

src/pages/vaffel-screen.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { VaffelQueue } from "../hooks/use-vaffel";
2+
import WaffleRain from "../components/vaffel";
3+
4+
type VaffelProps = {
5+
queue: VaffelQueue;
6+
status: string;
7+
total: number;
8+
};
9+
10+
export default function VaffelScreen({ queue, status, total }: VaffelProps) {
11+
return (
12+
<div className="w-full h-full flex items-center justify-center">
13+
<WaffleRain />
14+
<div className="bg-background/80 border-2 shadow-lg rounded-2xl p-8 max-w-2xl w-full text-center overflow-hidden">
15+
<h1 className="text-3xl font-semibold mb-2">Vaffelkøen </h1>
16+
<p className="text-lg font-semibold mb-6 space-x-2">
17+
Status:{" "}
18+
<span
19+
className={
20+
status === "open" ? "text-green-600 font-semibold" : "text-red-500 font-semibold"
21+
}
22+
>
23+
{status === "open" ? "Åpen" : "Stengt"}
24+
</span>
25+
<span>
26+
Vafler stekt: <span className="font-normal">{total} </span>
27+
</span>
28+
</p>
29+
30+
{queue.length === 0 ? (
31+
<p className="text-2xl text-gray-400">Ingen i køen</p>
32+
) : (
33+
<ul>
34+
{queue.map((person, index) => (
35+
<li
36+
key={person.user_id}
37+
className="flex items-center gap-2 rounded-xl px-6 text-xl font-medium "
38+
>
39+
<span className=" w-6 text-right">{index} - </span>
40+
<span>{person.display_name}</span>
41+
</li>
42+
))}
43+
</ul>
44+
)}
45+
</div>
46+
</div>
47+
);
48+
}

0 commit comments

Comments
 (0)