Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,857 changes: 3,165 additions & 1,692 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

30 changes: 22 additions & 8 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { ScreenCycle } from "./components/screen-cycle";
import { CalendarScreen } from "./pages/calendar-screen";
import { MessageScreen } from "./pages/message-screen";
Expand All @@ -8,9 +8,12 @@ import { isAugust, isMsgExpired, isValentinesSeason } from "./utils/date";
import WelcomeScreen from "./pages/welcome-screen";
import { AutoReload } from "./components/auto-reload";
import { useMessage } from "./hooks/use-message";
import { useVaffel } from "./hooks/use-vaffel";
import VaffelScreen from "./pages/vaffel-screen";

export default function App() {
const { data: message } = useMessage();
const { data: vaffel } = useVaffel();

useEffect(() => {
const interval = setInterval(
Expand All @@ -22,14 +25,25 @@ export default function App() {
return () => clearInterval(interval);
}, []);

const visibleScreens = [CalendarScreen, TransportScreen];
if (isAugust()) {
visibleScreens.push(WelcomeScreen);
}
const visibleScreens = useMemo(() => {
if (vaffel?.status === "open") {
return [
<VaffelScreen queue={vaffel.queue} status={vaffel.status} total={vaffel.total} />,
];
}

if (message?.title && message?.body && isMsgExpired(message._createdAt)) {
visibleScreens.push(MessageScreen);
}
const screens = [<CalendarScreen />, <TransportScreen />];

if (isAugust()) {
screens.push(<WelcomeScreen />);
}

if (message?.title && message?.body && isMsgExpired(message._createdAt)) {
screens.push(<MessageScreen />);
}

return screens;
}, [message, vaffel]);

const isValentines = isValentinesSeason();
document.body.classList.toggle("valentines", isValentines);
Expand Down
7 changes: 3 additions & 4 deletions src/components/screen-cycle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ProgressBar } from "./progress-bar";
import { TRANSITION_TIME } from "../config";
import { type ReactNode } from "react";

type ScreenCycleProps = {
screens: Array<React.FC>;
screens: Array<ReactNode>;
};

export const ScreenCycle = ({ screens }: ScreenCycleProps) => {
Expand Down Expand Up @@ -43,8 +44,6 @@ export const ScreenCycle = ({ screens }: ScreenCycleProps) => {
return () => window.removeEventListener("keydown", handleKeyDown);
}, [screens.length]);

const CurrentScreen = screens[screenIndex];

return (
<div className="flex-1">
{/* `key` is hack to force unmount the progressbar on change */}
Expand All @@ -61,7 +60,7 @@ export const ScreenCycle = ({ screens }: ScreenCycleProps) => {
transition={{ duration: 1 }}
className="w-full h-full"
>
<CurrentScreen />
{screens[screenIndex]}
</motion.div>
</AnimatePresence>
</div>
Expand Down
5 changes: 2 additions & 3 deletions src/components/utepils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ function getBarClass(score: number) {
}

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

return (
<div
className={`${getBackgroundClass(score)} rounded-2xl p-4 min-w-[240px] max-w-[320px] border border-yellow-200 font-sans`}
className={`${getBackgroundClass(score)} rounded-2xl p-4 min-w-60 max-w-[320px] border border-yellow-200 font-sans`}
>
<div className="flex justify-between items-start">
<div>
Expand Down
154 changes: 154 additions & 0 deletions src/components/vaffel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"use client";
import { useEffect, useRef } from "react";

export default function HeartsMinimal({ amount }: { amount: number }) {
const ref = useRef<HTMLCanvasElement | null>(null);

useEffect(() => {
const canvas = ref.current;
if (!canvas) return;

const ctx = canvas.getContext("2d");
if (!ctx) return;

const SPEED_Y = 0.35;
const DRIFT_X = 0.2;
const heartSpriteSize = 27;

function randomInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function rand(min: number, max: number) {
return Math.random() * (max - min) + min;
}

let w = 0,
h = 0,
DPR = 1;

const resize = () => {
w = window.innerWidth;
h = window.innerHeight;
DPR = Math.min(Math.max(window.devicePixelRatio || 1, 1), 2);
canvas.width = Math.floor(w * DPR);
canvas.height = Math.floor(h * DPR);
canvas.style.width = w + "px";
canvas.style.height = h + "px";
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
};

resize();
window.addEventListener("resize", resize);

const heartSprite = makeWaffleSprite(heartSpriteSize);

type Heart = {
x: number;
y: number;
rot: number;
vr: number;
s: number;
vy: number;
vx: number;
alpha: number;
};

const hearts: Heart[] = [];
const numHearts = amount;

for (let i = 0; i < numHearts; i += 1) {
hearts.push({
x: randomInt(0, w),
y: randomInt(0, h),
rot: Math.random() * Math.PI * 2,
vr: rand(-0.01, 0.01),
s: rand(12, 22),
vy: rand(0.6, 1.2),
vx: rand(0.6, 1.15),
alpha: rand(0.45, 0.8)
});
}

let raf = 0;

const draw = () => {
ctx.clearRect(0, 0, w, h);

for (let i = 0; i < hearts.length; i += 1) {
const ht = hearts[i];

ctx.save();
ctx.translate(ht.x, ht.y);
ht.rot += ht.vr;
ctx.rotate(ht.rot);
ctx.globalAlpha = ht.alpha;

ctx.drawImage(heartSprite, -ht.s / 2, -ht.s / 2, ht.s, ht.s);

ctx.restore();
ctx.globalAlpha = 1;

ht.y += SPEED_Y * ht.vy;
ht.x += DRIFT_X * ht.vx;

// litt “svev”
ht.x += Math.sin((ht.y + i) * 0.02) * 0.15;

// wrap sidene
if (ht.x > w) ht.x = 0;
else if (ht.x < 0) ht.x = w;

// respawn på toppen
if (ht.y > h) {
ht.x = randomInt(0, w);
ht.y = -20;
ht.rot = Math.random() * Math.PI * 2;
ht.vr = rand(-0.01, 0.01);
ht.s = rand(12, 22);
ht.vy = rand(0.6, 1.2);
ht.vx = rand(0.6, 1.15);
ht.alpha = rand(0.45, 0.8);
}
}

raf = requestAnimationFrame(draw);
};

raf = requestAnimationFrame(draw);

return () => {
cancelAnimationFrame(raf);
window.removeEventListener("resize", resize);
};
}, [amount]);

return (
<canvas
ref={ref}
aria-hidden
style={{
position: "fixed",
inset: 0,
width: "100vw",
height: "100vh",
pointerEvents: "none",
zIndex: 9999
}}
/>
);
}

function makeWaffleSprite(size: number): HTMLCanvasElement {
const spriteSize = size * 3;
const c = document.createElement("canvas");
c.width = c.height = spriteSize;

const g = c.getContext("2d")!;
g.font = `${spriteSize * 0.85}px serif`;
g.textAlign = "center";
g.textBaseline = "middle";
g.globalAlpha = 0.75;
g.fillText("🧇", spriteSize / 2, spriteSize / 2);

return c;
}
1 change: 0 additions & 1 deletion src/hooks/use-utepils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export function useUtepils() {
fetch("https://utepils-ten.vercel.app/api/utepils/bergen")
.then((res) => {
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
console.log("Fetched utepils data:", res);
return res.json();
})
.then((data) => {
Expand Down
29 changes: 29 additions & 0 deletions src/hooks/use-vaffel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useQuery } from "@tanstack/react-query";

export type VaffelQueue = {
user_id: string;
display_name: string;
total_orders: number;
}[];

const API_URL = "https://vaffel.echo-webkom.no/687650156262195217";

export function useVaffel() {
return useQuery({
queryKey: ["vaffel"],
queryFn: async () => {
const [queueRes, statusRes, totalRes] = await Promise.all([
fetch(`${API_URL}/queue`),
fetch(`${API_URL}/status`),
fetch(`${API_URL}/total`)
]);

const queue: VaffelQueue = await queueRes.json();
const status: string = await statusRes.text();
const total: number = await totalRes.json();

return { queue, status, total };
},
refetchInterval: 1000
});
}
2 changes: 1 addition & 1 deletion src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

.valentines {
--border: 338, 68%, 63%;
--primary: 338, 68%, 63%
--primary: 338, 68%, 63%;
}

@theme inline {
Expand Down
45 changes: 45 additions & 0 deletions src/pages/vaffel-screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { VaffelQueue } from "../hooks/use-vaffel";
import WaffleRain from "../components/vaffel";

type VaffelProps = {
queue: VaffelQueue;
status: string;
total: number;
};

export default function VaffelScreen({ queue, status, total }: VaffelProps) {
return (
<div className="w-full h-full flex items-center justify-center">
<WaffleRain amount={total} />
<div className="bg-background/80 border-2 shadow-lg rounded-2xl p-8 max-w-2xl w-full text-center overflow-hidden">
<h1 className="text-3xl font-semibold mb-2">Vaffelkø</h1>
<p className="text-lg font-semibold mb-6 space-x-2">
Status:{" "}
<span
className={
status === "open" ? "text-green-600 font-semibold" : "text-red-500 font-semibold"
}
>
{status === "open" ? "Åpen" : "Stengt"}
</span>
<span>
Vafler stekt: <span className="font-normal">{total} </span>
</span>
</p>

{queue.length === 0 ? (
<p className="text-gray-800">Ingen i køen</p>
) : (
<ul>
{queue.map((person, index) => (
<li key={person.user_id} className="flex items-center gap-2 rounded-xl px-6 ">
<span className=" font-semibold">{index + 1}:</span>
<span>{person.display_name}</span>
</li>
))}
</ul>
)}
</div>
</div>
);
}
Loading