feat: show recipe source link, keep screen awake while viewing a recipe
- sourceUrl was already saved on URL import but never displayed — render it as a link (hostname only) under the description. - Extract cook-mode's wake lock into a reusable useWakeLock hook, add visibility-change re-acquisition (a wake lock releases when the tab goes background and won't come back on its own), and use it on the regular recipe view too, not just cook mode. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import { CommentsSection } from "@/components/social/comments-section";
|
||||
import { getPublicUrl } from "@/lib/storage";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { RecipeChatPanel } from "@/components/recipe/recipe-chat-panel";
|
||||
import { KeepScreenAwake } from "@/components/recipe/keep-screen-awake";
|
||||
import { getMessages, formatMessage } from "@/lib/i18n/server";
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
@@ -86,6 +87,7 @@ export default async function RecipePage({ params }: Params) {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-8">
|
||||
<KeepScreenAwake />
|
||||
{/* Header */}
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-3xl font-bold tracking-tight">{recipe.title}</h1>
|
||||
@@ -200,6 +202,18 @@ export default async function RecipePage({ params }: Params) {
|
||||
<p className="text-muted-foreground leading-relaxed">{recipe.description}</p>
|
||||
)}
|
||||
|
||||
{recipe.sourceUrl && (
|
||||
<a
|
||||
href={recipe.sourceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground underline underline-offset-4"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
{m.recipe.source}: {new URL(recipe.sourceUrl).hostname.replace(/^www\./, "")}
|
||||
</a>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm">
|
||||
{recipe.difficulty && (
|
||||
<Badge variant={DIFFICULTY_COLOR[recipe.difficulty]}>{m.recipe.difficulty[recipe.difficulty]}</Badge>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useLocale } from "@/lib/i18n/provider";
|
||||
import { hasQuantity } from "@/lib/fractions";
|
||||
import { useWakeLock } from "@/lib/hooks/use-wake-lock";
|
||||
|
||||
type Step = { id: string; instruction: string; timerSeconds: number | null };
|
||||
type Ingredient = { rawName: string; quantity: string | null; unit: string | null };
|
||||
@@ -85,7 +86,6 @@ export function CookingMode({
|
||||
// Parallel timers: keyed by step index
|
||||
const [timers, setTimers] = useState<Map<number, TimerState>>(() => new Map());
|
||||
|
||||
const wakeLockRef = useRef<WakeLockSentinel | null>(null);
|
||||
const recognitionRef = useRef<InstanceType<typeof window.SpeechRecognition> | null>(null);
|
||||
|
||||
const step = steps[current]!;
|
||||
@@ -95,11 +95,7 @@ export function CookingMode({
|
||||
const currentTimer = timers.get(current);
|
||||
const otherRunningTimers = [...timers.entries()].filter(([idx, t]) => idx !== current && t.running);
|
||||
|
||||
// Wake Lock
|
||||
useEffect(() => {
|
||||
navigator.wakeLock?.request("screen").then((lock) => { wakeLockRef.current = lock; }).catch(() => {});
|
||||
return () => { wakeLockRef.current?.release().catch(() => {}); };
|
||||
}, []);
|
||||
useWakeLock();
|
||||
|
||||
// Master tick: decrement all running timers once per second
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useWakeLock } from "@/lib/hooks/use-wake-lock";
|
||||
|
||||
/** Invisible — just keeps the screen awake for as long as it's mounted. */
|
||||
export function KeepScreenAwake() {
|
||||
useWakeLock();
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
/** Keeps the screen awake while the component is mounted and the tab is visible. */
|
||||
export function useWakeLock(enabled = true) {
|
||||
const wakeLockRef = useRef<WakeLockSentinel | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function acquire() {
|
||||
try {
|
||||
const lock = await navigator.wakeLock?.request("screen");
|
||||
if (cancelled) {
|
||||
void lock?.release();
|
||||
return;
|
||||
}
|
||||
wakeLockRef.current = lock ?? null;
|
||||
} catch {
|
||||
// Wake Lock unsupported or denied — silently degrade.
|
||||
}
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
if (document.visibilityState === "visible" && !wakeLockRef.current) {
|
||||
void acquire();
|
||||
}
|
||||
}
|
||||
|
||||
void acquire();
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
void wakeLockRef.current?.release().catch(() => {});
|
||||
wakeLockRef.current = null;
|
||||
};
|
||||
}, [enabled]);
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
"mention": "{name} mentioned you in a comment"
|
||||
},
|
||||
"recipe": {
|
||||
"source": "Source",
|
||||
"share": "Share",
|
||||
"shareLinkCopied": "Link copied to clipboard",
|
||||
"shareCopyFailed": "Failed to copy link",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"mention": "{name} vous a mentionné dans un commentaire"
|
||||
},
|
||||
"recipe": {
|
||||
"source": "Source",
|
||||
"share": "Partager",
|
||||
"shareLinkCopied": "Lien copié dans le presse-papiers",
|
||||
"shareCopyFailed": "Échec de la copie du lien",
|
||||
|
||||
Reference in New Issue
Block a user