Compare commits

..

No commits in common. "0ff94c61cadf97525a5991b3463e3bcb2ebe885d" and "e45d92b3336acacc187d90619094d10580f84d84" have entirely different histories.

5 changed files with 26 additions and 120 deletions

View file

@ -105,23 +105,4 @@ describe("Leftovers basket", () => {
cy.contains(".leftovers-basket__empty", "Aucun reste cette semaine.").should("be.visible");
cy.get(".leftovers-basket__count").should("not.exist");
});
it("opens inside a bottom sheet on a phone instead of the anchored popover", () => {
// Literal 390×844 rather than `cy.setViewport("phone")` (ticket #83) —
// that command doesn't exist on this ticket's own base yet; matches its
// PHONE_VIEWPORT value on purpose, worth switching to the real import
// once both have landed on `main`.
cy.viewport(390, 844);
cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: planningWithLeftovers });
cy.visit("/");
cy.get(".leftovers-basket__toggle").click();
cy.get(".leftovers-basket__popover").should("not.exist");
cy.get(".bottom-sheet").should("be.visible");
cy.contains(".bottom-sheet", "Restes disponibles").should("be.visible");
cy.contains(".bottom-sheet .leftovers-basket__line", "Lasagnes").should("be.visible");
cy.get(".bottom-sheet__close").click();
cy.get(".bottom-sheet").should("not.exist");
});
});

View file

@ -197,25 +197,4 @@ describe("Planning grid", () => {
cy.contains("Semaine du 24 au 30 août 2026").should("be.visible");
cy.get(".calendar-popover").should("not.exist");
});
it("opens the calendar inside a bottom sheet on a phone instead of the anchored popover", () => {
// Literal 390×844 rather than `cy.setViewport("phone")` (ticket #83) —
// that command doesn't exist on this ticket's own base yet; matches its
// PHONE_VIEWPORT value on purpose, worth switching to the real import
// once both have landed on `main`.
cy.viewport(390, 844);
cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }).as("getPlanning");
cy.visit("/");
cy.wait("@getPlanning");
cy.contains("button", "Semaine du").click();
cy.get(".calendar-popover").should("not.exist");
cy.get(".bottom-sheet").should("be.visible");
cy.contains(".bottom-sheet", "Choisir une semaine").should("be.visible");
cy.get(".bottom-sheet .calendar-grid__day").contains(/^25$/).click();
cy.contains("Semaine du 24 au 30 août 2026").should("be.visible");
cy.get(".bottom-sheet").should("not.exist");
});
});

View file

@ -3,8 +3,6 @@ import { useDraggable } from "@dnd-kit/core";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { apiClient } from "../../api/client";
import { BottomSheet } from "../../components/ui/BottomSheet";
import { useIsMobileViewport } from "../../lib/useIsMobileViewport";
import { collectLeftoverLines, dragIdForLeftover } from "./leftovers-basket";
import "./leftovers-basket.scss";
@ -21,13 +19,6 @@ import "./leftovers-basket.scss";
* The list is derived on every render from `planning` (see
* {@link collectLeftoverLines}) rather than held in its own state, so a
* discard or assignment elsewhere on the page is reflected here for free.
*
* On a phone this renders inside a {@link BottomSheet} instead of the
* plain anchored `div` below — the fixed-width popover (`right: 0`, no
* edge handling) used to render identically at every width, which could
* push it partly or fully off a narrow screen (see the mobile/tablet UI
* chantier's own audit — very plausibly *the* concrete cause of reported
* "can't reach a button" complaints on mobile).
*/
export function LeftoversBasket({
planning,
@ -38,7 +29,6 @@ export function LeftoversBasket({
onLeftoverDiscarded: (item: PlanningItemView) => void;
}) {
const { t } = useTranslation();
const isMobile = useIsMobileViewport();
const [isOpen, setIsOpen] = useState(false);
const [discardingId, setDiscardingId] = useState<number | null>(null);
const popoverRef = useRef<HTMLDivElement>(null);
@ -46,12 +36,7 @@ export function LeftoversBasket({
const lines = collectLeftoverLines(planning);
useEffect(() => {
// On mobile the BottomSheet branch below never attaches this ref (it's
// a native <dialog> with its own tap-outside handling instead) —
// `popoverRef.current` would just stay `null` and this would silently
// no-op every time, harmless but pointless, so skip attaching the
// listener at all in that case.
if (!isOpen || isMobile) return;
if (!isOpen) return;
function handleClickOutside(e: MouseEvent) {
if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
setIsOpen(false);
@ -59,7 +44,7 @@ export function LeftoversBasket({
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [isOpen, isMobile]);
}, [isOpen]);
async function discard(item: PlanningItemView) {
setDiscardingId(item.id);
@ -74,23 +59,6 @@ export function LeftoversBasket({
}
}
const listContent =
lines.length === 0 ? (
<p className="leftovers-basket__empty">{t("planning.leftovers.empty")}</p>
) : (
<ul className="leftovers-basket__list">
{lines.map(({ item, available }) => (
<LeftoverLineCard
key={item.id}
item={item}
available={available}
isDiscarding={discardingId === item.id}
onDiscard={() => void discard(item)}
/>
))}
</ul>
);
return (
<div className="leftovers-basket">
<button
@ -105,17 +73,26 @@ export function LeftoversBasket({
{lines.length > 0 && <span className="leftovers-basket__count">{lines.length}</span>}
</button>
{isOpen &&
(isMobile ? (
<BottomSheet onClose={() => setIsOpen(false)} title={t("planning.leftovers.title")}>
{listContent}
</BottomSheet>
) : (
{isOpen && (
<div className="leftovers-basket__popover" ref={popoverRef}>
<h2 className="leftovers-basket__heading">{t("planning.leftovers.title")}</h2>
{listContent}
</div>
{lines.length === 0 ? (
<p className="leftovers-basket__empty">{t("planning.leftovers.empty")}</p>
) : (
<ul className="leftovers-basket__list">
{lines.map(({ item, available }) => (
<LeftoverLineCard
key={item.id}
item={item}
available={available}
isDiscarding={discardingId === item.id}
onDiscard={() => void discard(item)}
/>
))}
</ul>
)}
</div>
)}
</div>
);
}

View file

@ -8,8 +8,6 @@ import {
import { WEEK_DAYS } from "@batch-cooking/shared";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { BottomSheet } from "../../components/ui/BottomSheet";
import { useIsMobileViewport } from "../../lib/useIsMobileViewport";
import "./week-navigator.scss";
/** "17 au 23 août 2026" — collapses the month/year to just the end date when both ends of the week share it, spells it out on both ends otherwise (e.g. a week straddling two months). */
@ -91,15 +89,7 @@ export function WeekNavigator({
);
}
/**
* Month calendar letting the visitor jump to any week at once — selecting a
* day selects its whole (Monday-first) week. Closes itself on an outside
* click on desktop; on a phone it renders inside a {@link BottomSheet}
* instead of the plain anchored `div` below, same reasoning as
* `LeftoversBasket`'s identical split — a fixed-width popover with no
* viewport-edge handling can otherwise render partly or fully off a narrow
* screen.
*/
/** Month calendar letting the visitor jump to any week at once — selecting a day selects its whole (Monday-first) week. Closes itself on an outside click. */
function CalendarPopover({
selectedWeekStart,
onSelectDay,
@ -110,7 +100,6 @@ function CalendarPopover({
onClose: () => void;
}) {
const { t } = useTranslation();
const isMobile = useIsMobileViewport();
// Its own state: browsing to a different month to pick a week there
// shouldn't jump back every render — only re-anchors when the popover is
// first opened (`selectedWeekStart` at that point), not while it's open.
@ -118,11 +107,6 @@ function CalendarPopover({
const popoverRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// See LeftoversBasket's identical guard: on mobile this ref never
// attaches to anything (the BottomSheet branch below handles its own
// tap-outside-to-close), so the listener would just be a permanent
// no-op — skip attaching it at all in that case.
if (isMobile) return;
function handleClickOutside(e: MouseEvent) {
if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
onClose();
@ -130,14 +114,14 @@ function CalendarPopover({
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [onClose, isMobile]);
}, [onClose]);
const today = toDateOnly(DateTime.utc());
const selectedWeekEnd = selectedWeekStart.plus({ days: 6 });
const weeks = buildCalendarMonth(visibleMonth);
const calendarContent = (
<>
return (
<div className="calendar-popover" ref={popoverRef}>
<div className="calendar-popover__header">
<button
type="button"
@ -185,20 +169,6 @@ function CalendarPopover({
);
})}
</div>
</>
);
if (isMobile) {
return (
<BottomSheet onClose={onClose} title={t("common.calendar.title")}>
{calendarContent}
</BottomSheet>
);
}
return (
<div className="calendar-popover" ref={popoverRef}>
{calendarContent}
</div>
);
}

View file

@ -10,7 +10,6 @@
"label": "Semaine du {{range}}"
},
"calendar": {
"title": "Choisir une semaine",
"prevMonth": "Mois précédent",
"nextMonth": "Mois suivant"
},