init
This commit is contained in:
commit
c19b910c36
80 changed files with 16134 additions and 0 deletions
101
app/components/RecipeServings.tsx
Normal file
101
app/components/RecipeServings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue