This commit is contained in:
Cayden Schwechten 2026-10-31 20:44:29 -05:00
commit c19b910c36
80 changed files with 16134 additions and 0 deletions

View file

@ -0,0 +1,101 @@
"use client";
import { Flame, Minus, Plus } from "lucide-react";
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
import type { Recipe } from "@/app/types";
import { RecipeDetail } from "./RecipeDetail";
const noopSubscribe = () => () => {};
/** RecipeDetail with an accessible servings stepper + Screen Wake Lock ("Cook mode") toggle. */
export function RecipeServings({ recipe }: { recipe: Recipe }) {
const [servings, setServings] = useState(recipe.servings);
const [cookMode, setCookMode] = useState(false);
const wakeSupported = useSyncExternalStore(noopSubscribe, () => "wakeLock" in navigator, () => false);
const wakeLockRef = useRef<WakeLockSentinel | null>(null);
useEffect(() => {
if (!cookMode || !wakeSupported) {
wakeLockRef.current?.release().catch(() => {});
wakeLockRef.current = null;
return;
}
let cancelled = false;
async function acquire() {
try {
const sentinel = await navigator.wakeLock.request("screen");
if (cancelled) {
await sentinel.release();
return;
}
wakeLockRef.current = sentinel;
} catch {
// Battery saver or background tab can refuse wakeLock; fail gracefully.
}
}
acquire();
const onVisibility = () => {
if (document.visibilityState === "visible" && cookMode) acquire();
};
document.addEventListener("visibilitychange", onVisibility);
return () => {
cancelled = true;
document.removeEventListener("visibilitychange", onVisibility);
wakeLockRef.current?.release().catch(() => {});
wakeLockRef.current = null;
};
}, [cookMode, wakeSupported]);
return (
<div>
<div className="mb-6 flex flex-wrap items-center justify-between gap-3 border-b border-line pb-4">
<div className="flex items-center gap-3">
<span className="text-sm font-medium text-ink-muted">Scale servings</span>
<div className="inline-flex items-center rounded-lg border border-line-strong bg-raised p-0.5">
<button
type="button"
onClick={() => setServings((s) => Math.max(1, s - 1))}
className="grid size-9 place-items-center rounded-md text-ink-muted transition-colors hover:bg-surface hover:text-ink"
aria-label="Fewer servings"
>
<Minus className="size-4" aria-hidden />
</button>
<span className="min-w-9 text-center font-mono text-sm font-semibold tabular-nums" aria-live="polite">
{servings}
</span>
<button
type="button"
onClick={() => setServings((s) => Math.min(12, s + 1))}
className="grid size-9 place-items-center rounded-md text-ink-muted transition-colors hover:bg-surface hover:text-ink"
aria-label="More servings"
>
<Plus className="size-4" aria-hidden />
</button>
</div>
<span className="hidden text-xs text-ink-faint sm:inline">Tap any ingredient or step below to check it off</span>
</div>
{wakeSupported && (
<button
type="button"
aria-pressed={cookMode}
onClick={() => setCookMode((c) => !c)}
className={`trim-cap inline-flex min-h-9 items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-semibold transition-colors ${
cookMode
? "border-status-ok-line bg-status-ok-bg text-status-ok"
: "border-line-strong bg-surface text-ink-muted hover:bg-raised hover:text-ink"
}`}
>
<Flame className="size-3.5" aria-hidden />
{cookMode ? "Screen kept awake" : "Keep screen awake"}
</button>
)}
</div>
<RecipeDetail recipe={recipe} servings={servings} showMeta={false} interactive />
</div>
);
}