fix(layout): masque l'onglet « Paramètres d'application » hors de l'app native (#107)
All checks were successful
CI / lint (push) Successful in 4m25s
CI / build (push) Successful in 2m18s
CI / intent-service-test (push) Successful in 5m41s
CI / e2e (push) Successful in 8m25s
CI / test (push) Successful in 30m0s
CI / build-android (push) Has been skipped

Ses sections n'ont pas de sens sur le web (pas de « version installée », la
vérif de mise à jour y est un no-op) et son seul réglage web-pertinent — la
case « toujours rester sur le site web » — vit mieux dans le flux qu'il
pilote.

- `AppLayout.tsx` : `SETTINGS_ITEMS` gagne un `nativeOnly`, filtré une fois
  au chargement du module (`settingsNavItems`) via `Capacitor.isNativePlatform()`
  — l'entrée disparaît du menu Paramètres (sidebar + feuille « Plus »).
- `App.tsx` : la route `/parametres/application` redirige vers
  `/parametres/compte` sur le web, comme le précédent `/foyer`, pour qu'un
  bookmark ne tombe pas sur une page orpheline.
- `AppSettingsPage.tsx` : devient natif-only — la branche `webHint` et la
  section « rester sur le site web » sont retirées.
- La case « toujours rester sur le site web » déménage sur `/telecharger`
  (`DownloadPage`) : page publique qu'un visiteur non connecté sur
  navigateur téléphone peut atteindre, et c'est exactement le flux que la
  préférence concerne. `localStorage` sur l'origine web publique, là où
  `attemptOpenNativeApp` la lit. i18n `appSettings.redirect.*` →
  `download.stayOnWeb.*` ; `appSettings.version.webHint` supprimée.

Tests : `sidebar.cy.ts` / `app-settings-page.cy.ts` — l'entrée et la page
sont absentes sur le web, la route redirige. `download-page.cy.ts` — la
case « rester sur le site web » par défaut décochée, persistée au reload.
Local OK (sidebar 5/5, app-settings 2/2, download 3/3).

Closes #107

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Nicolas 2026-09-08 02:07:23 +02:00
parent 1b96b891db
commit 8bcea74b56
9 changed files with 174 additions and 137 deletions

View file

@ -1,16 +1,14 @@
// Mocks the API via cy.intercept — this job doesn't run a live backend (see // Mocks the API via cy.intercept — this job doesn't run a live backend (see
// .github/workflows/ci.yml); apps/api's own Mocha suite covers real API // .github/workflows/ci.yml).
// behavior against a real database.
// //
// Cypress always runs as a plain web browser — `Capacitor.isNativePlatform()` // Issue #107 — "Paramètres d'application" is packaged-app only now. Its
// is never true here, so only this page's web-side content (the build // content (installed version + update check) is inert on the web, and the
// version + the "stay on web" preference) is reachable from this spec. The // one web-relevant control it carried (the "stay on web" redirect toggle)
// native-only half (App.getInfo()/checkNow's actual version-comparison // moved to `/telecharger` (see download-page.cy.ts). Cypress always runs as
// network call) has no seam to fake native from a web run — same // a plain web browser (`Capacitor.isNativePlatform()` is never true here),
// untested-on-purpose posture already accepted for `useAppUpdateCheck`'s // so the page itself has no seam to reach — same untested-on-purpose
// background check and `UpdateBanner` (neither has e2e coverage either); // posture already accepted for `useAppUpdateCheck` / `UpdateBanner`. This
// the underlying comparison logic itself is unit-tested in // spec now only covers the web-side gating.
// version-compare.cy.tsx, unchanged by this ticket.
const authenticatedProfile = { const authenticatedProfile = {
id: 1, id: 1,
@ -22,47 +20,21 @@ const authenticatedProfile = {
dietId: null, dietId: null,
}; };
const STAY_ON_WEB_KEY = "batchcooking:stayOnWeb"; describe("App settings — hidden on the web (#107)", () => {
describe("App settings (/parametres/application)", () => {
beforeEach(() => { beforeEach(() => {
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile });
cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null });
}); });
it("shows the web build version, and the update-check section only on native", () => { it("is absent from the Paramètres menu", () => {
cy.visit("/");
cy.contains("button", "Paramètres").click();
cy.contains("a", "Paramètres d'application").should("not.exist");
});
it("redirects /parametres/application to /parametres/compte instead of an orphaned page", () => {
cy.visit("/parametres/application"); cy.visit("/parametres/application");
cy.url().should("include", "/parametres/compte");
cy.contains("h1", "Paramètres d'application").should("be.visible"); cy.contains("h1", "Paramètres d'application").should("not.exist");
// The exact build number isn't asserted (would couple this test to
// package.json's version, bumped independently of this page) — just
// that a "Version X" line rendered at all, proving `useInstalledVersion`
// resolved something rather than being stuck empty.
cy.contains(".app-settings__version", "Version").should("be.visible");
cy.contains("button", "Vérifier les mises à jour").should("not.exist");
cy.contains(
"La vérification des mises à jour n'est disponible que dans l'application mobile",
).should("be.visible");
});
it("defaults 'stay on web' to unchecked, and persists a toggle across a reload", () => {
cy.visit("/parametres/application");
cy.contains("label", "Toujours rester sur le site web")
.find('input[type="checkbox"]')
.should("not.be.checked");
cy.contains("label", "Toujours rester sur le site web").click();
cy.contains("label", "Toujours rester sur le site web")
.find('input[type="checkbox"]')
.should("be.checked");
cy.window().then((win) => {
expect(win.localStorage.getItem(STAY_ON_WEB_KEY)).to.equal("true");
});
cy.reload();
cy.contains("label", "Toujours rester sur le site web")
.find('input[type="checkbox"]')
.should("be.checked");
}); });
}); });

View file

@ -23,4 +23,29 @@ describe("DownloadPage — the mobile-app-redirect fallback", () => {
cy.visit("/telecharger"); cy.visit("/telecharger");
cy.url().should("include", "/telecharger"); cy.url().should("include", "/telecharger");
}); });
// Issue #107 — the "toujours rester sur le site web" preference moved
// here from "Paramètres d'application" (packaged-app only now). Written to
// `localStorage` on the public web origin, which is where
// `attemptOpenNativeApp` reads it.
it("defaults 'stay on web' to unchecked, and persists a toggle across a reload", () => {
cy.visit("/telecharger");
cy.contains("label", "Toujours rester sur le site web")
.find('input[type="checkbox"]')
.should("not.be.checked");
cy.contains("label", "Toujours rester sur le site web").click();
cy.contains("label", "Toujours rester sur le site web")
.find('input[type="checkbox"]')
.should("be.checked");
cy.window().then((win) => {
expect(win.localStorage.getItem("batchcooking:stayOnWeb")).to.equal("true");
});
cy.reload();
cy.contains("label", "Toujours rester sur le site web")
.find('input[type="checkbox"]')
.should("be.checked");
});
}); });

View file

@ -25,7 +25,7 @@ describe("Sidebar — settings menu and account menu", () => {
cy.get(".app-sidebar__nav a").should("not.contain", "Foyer"); cy.get(".app-sidebar__nav a").should("not.contain", "Foyer");
}); });
it("reveals the four settings pages behind the Paramètres toggle", () => { it("reveals the settings pages behind the Paramètres toggle", () => {
cy.visit("/"); cy.visit("/");
cy.contains("a", "Compte").should("not.exist"); cy.contains("a", "Compte").should("not.exist");
@ -43,11 +43,9 @@ describe("Sidebar — settings menu and account menu", () => {
"href", "href",
"/parametres/preferences-utilisateur", "/parametres/preferences-utilisateur",
); );
cy.contains("a", "Paramètres d'application").should( // "Paramètres d'application" is packaged-app only (issue #107) — Cypress
"have.attr", // runs as a plain web browser, so it's filtered out here.
"href", cy.contains("a", "Paramètres d'application").should("not.exist");
"/parametres/application",
);
}); });
it("opens the account menu from the greeting and links to Mon compte", () => { it("opens the account menu from the greeting and links to Mon compte", () => {

View file

@ -1,3 +1,4 @@
import { Capacitor } from "@capacitor/core";
import { Navigate, Outlet, Route, Routes } from "react-router-dom"; import { Navigate, Outlet, Route, Routes } from "react-router-dom";
import { AdminAuthProvider } from "./features/admin/AdminAuthContext"; import { AdminAuthProvider } from "./features/admin/AdminAuthContext";
import { RequireAdmin } from "./features/admin/RequireAdmin"; import { RequireAdmin } from "./features/admin/RequireAdmin";
@ -108,7 +109,21 @@ export function App() {
<Route path="/parametres/preferences" element={<PreferencesPage />} /> <Route path="/parametres/preferences" element={<PreferencesPage />} />
<Route path="/parametres/foyer" element={<HouseholdSettingsPage />} /> <Route path="/parametres/foyer" element={<HouseholdSettingsPage />} />
<Route path="/parametres/preferences-utilisateur" element={<UserPreferencesPage />} /> <Route path="/parametres/preferences-utilisateur" element={<UserPreferencesPage />} />
<Route path="/parametres/application" element={<AppSettingsPage />} /> {/* Native app only (issue #107): on the web its sections are inert
(no "installed version", the update check is a no-op) or have
moved to `/telecharger` (the stay-on-web toggle), and the nav
entry is hidden — so a stray bookmark redirects rather than
landing on an orphaned page (same pattern as `/foyer` below). */}
<Route
path="/parametres/application"
element={
Capacitor.isNativePlatform() ? (
<AppSettingsPage />
) : (
<Navigate to="/parametres/compte" replace />
)
}
/>
<Route path="/parametres/credits" element={<CreditsPage />} /> <Route path="/parametres/credits" element={<CreditsPage />} />
<Route path="/foyer" element={<Navigate to="/parametres/foyer" replace />} /> <Route path="/foyer" element={<Navigate to="/parametres/foyer" replace />} />
</Route> </Route>

View file

@ -1,3 +1,5 @@
import { Capacitor } from "@capacitor/core";
import type { LucideIcon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
@ -41,19 +43,44 @@ const NAV_ITEMS = [
}, },
] as const; ] as const;
/** One entry in the "Paramètres" nav — see {@link SETTINGS_ITEMS}. */
interface SettingsNavItem {
to: string;
/** Maps to `layout.settings.nav.<key>` in `locales/fr/translation.json`. */
key: string;
Icon: LucideIcon;
/** /**
* One entry in the "Paramètres" nav — revealed by {@link SettingsMenu} on * Hidden entirely outside the packaged app (issue #107): its page's
* desktop, listed inside {@link MobileMoreSheet} on mobile. `key` maps to * options — the version/update check, and the "stay on the website"
* `layout.settings.nav.<key>`. * redirect toggle — are either inert on the web or have moved to
* `/telecharger`. The route itself redirects away on the web too (see
* `App.tsx`).
*/ */
const SETTINGS_ITEMS = [ nativeOnly?: boolean;
}
/**
* The "Paramètres" nav entries — revealed by {@link SettingsMenu} on
* desktop, listed inside {@link MobileMoreSheet} on mobile. Filtered to
* {@link settingsNavItems} for actual rendering.
*/
const SETTINGS_ITEMS: SettingsNavItem[] = [
{ to: "/parametres/compte", key: "account", Icon: AccountIcon }, { to: "/parametres/compte", key: "account", Icon: AccountIcon },
{ to: "/parametres/preferences", key: "preferences", Icon: DietPreferencesIcon }, { to: "/parametres/preferences", key: "preferences", Icon: DietPreferencesIcon },
{ to: "/parametres/foyer", key: "household", Icon: HouseholdIcon }, { to: "/parametres/foyer", key: "household", Icon: HouseholdIcon },
{ to: "/parametres/preferences-utilisateur", key: "userPreferences", Icon: UserPreferencesIcon }, { to: "/parametres/preferences-utilisateur", key: "userPreferences", Icon: UserPreferencesIcon },
{ to: "/parametres/application", key: "application", Icon: AppSettingsIcon }, { to: "/parametres/application", key: "application", Icon: AppSettingsIcon, nativeOnly: true },
{ to: "/parametres/credits", key: "credits", Icon: CreditsIcon }, { to: "/parametres/credits", key: "credits", Icon: CreditsIcon },
] as const; ];
/**
* What's actually rendered — {@link SETTINGS_ITEMS} minus the `nativeOnly`
* ones on the web. `Capacitor.isNativePlatform()` is fixed for the life of
* the page, so this is settled once at module load rather than per render.
*/
const settingsNavItems: SettingsNavItem[] = SETTINGS_ITEMS.filter(
(item) => Capacitor.isNativePlatform() || !item.nativeOnly,
);
/** /**
* Shell for every authenticated page: on desktop/tablet, a sidebar (brand, * Shell for every authenticated page: on desktop/tablet, a sidebar (brand,
@ -220,7 +247,7 @@ function SettingsMenu() {
{isOpen && ( {isOpen && (
<nav className="app-sidebar__nav app-sidebar__settings-nav"> <nav className="app-sidebar__nav app-sidebar__settings-nav">
{SETTINGS_ITEMS.map(({ to, key, Icon }) => ( {settingsNavItems.map(({ to, key, Icon }) => (
<NavLink <NavLink
key={to} key={to}
to={to} to={to}
@ -379,7 +406,7 @@ function MobileMoreSheet({ onClose }: { onClose: () => void }) {
</div> </div>
<nav className="mobile-more-sheet__nav"> <nav className="mobile-more-sheet__nav">
{SETTINGS_ITEMS.map(({ to, key, Icon }) => ( {settingsNavItems.map(({ to, key, Icon }) => (
<NavLink <NavLink
key={to} key={to}
to={to} to={to}

View file

@ -1378,7 +1378,11 @@
"lede": "Téléchargez le fichier .apk, ou réessayez d'ouvrir l'application si elle est déjà installée sur cet appareil.", "lede": "Téléchargez le fichier .apk, ou réessayez d'ouvrir l'application si elle est déjà installée sur cet appareil.",
"downloadButton": "Télécharger l'APK", "downloadButton": "Télécharger l'APK",
"retryButton": "Réessayer d'ouvrir l'application", "retryButton": "Réessayer d'ouvrir l'application",
"backLink": "Retour à l'accueil" "backLink": "Retour à l'accueil",
"stayOnWeb": {
"label": "Toujours rester sur le site web",
"hint": "Par défaut, ouvrir batchCooking sur votre téléphone propose de basculer vers l'application si elle est installée. Activez cette option pour rester sur le site web à chaque fois."
}
}, },
"tour": { "tour": {
"controls": { "controls": {
@ -1406,13 +1410,7 @@
"checkButton": "Vérifier les mises à jour", "checkButton": "Vérifier les mises à jour",
"checking": "Vérification…", "checking": "Vérification…",
"upToDate": "L'application est à jour.", "upToDate": "L'application est à jour.",
"checkError": "Impossible de vérifier les mises à jour pour le moment.", "checkError": "Impossible de vérifier les mises à jour pour le moment."
"webHint": "La vérification des mises à jour n'est disponible que dans l'application mobile — sur le web, actualiser la page charge toujours la dernière version."
},
"redirect": {
"title": "Ouverture de l'application",
"stayOnWebLabel": "Toujours rester sur le site web",
"stayOnWebHint": "Par défaut, ouvrir batchCooking sur votre téléphone propose de basculer vers l'application si elle est installée. Activez cette option pour rester sur le site web à chaque fois."
} }
} }
} }

View file

@ -1,7 +1,9 @@
import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { API_BASE_URL } from "../../api/client"; import { API_BASE_URL } from "../../api/client";
import { attemptOpenNativeApp } from "../../lib/mobile-app-redirect"; import { CheckboxOption } from "../../components/ui/Checkbox";
import { attemptOpenNativeApp, STAY_ON_WEB_PREFERENCE_KEY } from "../../lib/mobile-app-redirect";
import "./download-page.scss"; import "./download-page.scss";
/** /**
@ -18,9 +20,25 @@ import "./download-page.scss";
* Forgejo's release asset (see that module's own doc comment for why a * Forgejo's release asset (see that module's own doc comment for why a
* direct Forgejo link doesn't work: no CORS headers, no GitHub-style * direct Forgejo link doesn't work: no CORS headers, no GitHub-style
* `/releases/latest/download/<file>` shortcut). Nothing new needed there. * `/releases/latest/download/<file>` shortcut). Nothing new needed there.
*
* Also home to the "toujours rester sur le site web" toggle (moved here
* from "Paramètres d'application" in issue #107 — that page is now packaged-
* app only): this is exactly the flow the preference is about, and it's a
* public page a logged-out phone-browser visitor can reach. It's written to
* `localStorage` on the public web origin, which is where
* {@link attemptOpenNativeApp} reads it from (never inside the packaged
* app — a different origin, a different `localStorage`).
*/ */
export function DownloadPage() { export function DownloadPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const [stayOnWeb, setStayOnWeb] = useState(
() => localStorage.getItem(STAY_ON_WEB_PREFERENCE_KEY) === "true",
);
function handleStayOnWebChange(checked: boolean) {
setStayOnWeb(checked);
localStorage.setItem(STAY_ON_WEB_PREFERENCE_KEY, String(checked));
}
return ( return (
<main className="download-page"> <main className="download-page">
@ -45,6 +63,13 @@ export function DownloadPage() {
<p className="download-page__hint">{t("landing.hero.mobileHint")}</p> <p className="download-page__hint">{t("landing.hero.mobileHint")}</p>
<div className="download-page__stay-on-web">
<CheckboxOption checked={stayOnWeb} onChange={handleStayOnWebChange}>
{t("download.stayOnWeb.label")}
</CheckboxOption>
<p className="download-page__hint">{t("download.stayOnWeb.hint")}</p>
</div>
<Link className="download-page__back" to="/"> <Link className="download-page__back" to="/">
{t("download.backLink")} {t("download.backLink")}
</Link> </Link>

View file

@ -77,6 +77,20 @@
color: var(--color-text-muted); color: var(--color-text-muted);
} }
// The "toujours rester sur le site web" preference (moved here from
// "Paramètres d'application", issue #107) — set off from the download
// actions above by a rule + its own spacing, since it's a lasting choice
// rather than a one-shot action.
.download-page__stay-on-web {
align-self: stretch;
display: flex;
flex-direction: column;
gap: var(--space-xs);
margin-top: var(--space-sm);
padding-top: var(--space-md);
border-top: 1px solid var(--color-border);
}
.download-page__back { .download-page__back {
margin-top: var(--space-sm); margin-top: var(--space-sm);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);

View file

@ -1,14 +1,11 @@
import { Capacitor } from "@capacitor/core";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { API_BASE_URL } from "../../api/client"; import { API_BASE_URL } from "../../api/client";
import { CheckboxOption } from "../../components/ui/Checkbox";
import { import {
type AppUpdateCheckResult, type AppUpdateCheckResult,
useAppUpdateCheck, useAppUpdateCheck,
} from "../../features/app-update/useAppUpdateCheck"; } from "../../features/app-update/useAppUpdateCheck";
import { useInstalledVersion } from "../../features/app-update/useInstalledVersion"; import { useInstalledVersion } from "../../features/app-update/useInstalledVersion";
import { STAY_ON_WEB_PREFERENCE_KEY } from "../../lib/mobile-app-redirect";
import "./settings-pages.scss"; import "./settings-pages.scss";
/** Status of the manual "Vérifier les mises à jour" click — `"idle"` before it's ever been pressed, `"checking"` while in flight, then whatever {@link AppUpdateCheckResult} it resolved to. */ /** Status of the manual "Vérifier les mises à jour" click — `"idle"` before it's ever been pressed, `"checking"` while in flight, then whatever {@link AppUpdateCheckResult} it resolved to. */
@ -21,34 +18,21 @@ type CheckState = "idle" | "checking" | AppUpdateCheckResult["status"];
* where they are, this page is new content only — the installed app's own * where they are, this page is new content only — the installed app's own
* identity/behavior, not how it looks. * identity/behavior, not how it looks.
* *
* Shows a genuinely different section depending on platform, not just a * **Packaged app only.** Its content — the installed version and a "check
* cosmetic tweak — each half of this page is *inert* on the other * for updates" button — has no meaning on the web (there is no "installed
* platform, not merely irrelevant: * version", and `useAppUpdateCheck`'s underlying check is a no-op there),
* - **Version + "Vérifier les mises à jour"** (native only): there is no * so on the web the nav entry is hidden and the route redirects to
* "installed version" concept on the web (see {@link useInstalledVersion}), * `/parametres/compte` (see `AppLayout` / `App.tsx`, issue #107). The
* and `useAppUpdateCheck`'s underlying check itself is a no-op there. * "toujours rester sur le site web" toggle that used to live here — the one
* - **"Toujours rester sur le site web"** (web only, any device — not * thing that was web-only — moved to `/telecharger` (`DownloadPage`), the
* viewport-gated to phone, so it can be set ahead of time): drives * page that whole redirect flow already lands on.
* `attemptOpenNativeApp` (`lib/mobile-app-redirect.ts`), which only ever
* runs from a real mobile *browser* tab. Reachable from *inside* the
* native app too (it's a normal in-app route), but toggling it there
* would be silently pointless: Capacitor's Android WebView runs on its
* own fixed `https://localhost` origin, a completely different
* `localStorage` than the public website a browser visitor actually
* lands on — so writing the preference from inside the app can never
* reach the code that reads it. Gating this section to non-native avoids
* ever presenting a control that visibly does nothing when toggled.
*/ */
export function AppSettingsPage() { export function AppSettingsPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const isNative = Capacitor.isNativePlatform();
const installedVersion = useInstalledVersion(); const installedVersion = useInstalledVersion();
const { checkNow } = useAppUpdateCheck(); const { checkNow } = useAppUpdateCheck();
const [checkState, setCheckState] = useState<CheckState>("idle"); const [checkState, setCheckState] = useState<CheckState>("idle");
const [foundVersion, setFoundVersion] = useState<string | null>(null); const [foundVersion, setFoundVersion] = useState<string | null>(null);
const [stayOnWeb, setStayOnWeb] = useState(
() => localStorage.getItem(STAY_ON_WEB_PREFERENCE_KEY) === "true",
);
async function handleCheckNow() { async function handleCheckNow() {
setCheckState("checking"); setCheckState("checking");
@ -67,11 +51,6 @@ export function AppSettingsPage() {
} }
} }
function handleStayOnWebChange(checked: boolean) {
setStayOnWeb(checked);
localStorage.setItem(STAY_ON_WEB_PREFERENCE_KEY, String(checked));
}
return ( return (
<div className="settings-page"> <div className="settings-page">
<h1>{t("appSettings.title")}</h1> <h1>{t("appSettings.title")}</h1>
@ -82,8 +61,6 @@ export function AppSettingsPage() {
{t("appSettings.version.installed", { version: installedVersion })} {t("appSettings.version.installed", { version: installedVersion })}
</p> </p>
{isNative ? (
<>
<button <button
type="button" type="button"
onClick={() => void handleCheckNow()} onClick={() => void handleCheckNow()}
@ -108,21 +85,7 @@ export function AppSettingsPage() {
{checkState === "error" && ( {checkState === "error" && (
<p className="field-error">{t("appSettings.version.checkError")}</p> <p className="field-error">{t("appSettings.version.checkError")}</p>
)} )}
</>
) : (
<p className="settings-page__hint">{t("appSettings.version.webHint")}</p>
)}
</div> </div>
{!isNative && (
<div className="settings-page__section">
<h2>{t("appSettings.redirect.title")}</h2>
<CheckboxOption checked={stayOnWeb} onChange={handleStayOnWebChange}>
{t("appSettings.redirect.stayOnWebLabel")}
</CheckboxOption>
<p className="settings-page__hint">{t("appSettings.redirect.stayOnWebHint")}</p>
</div>
)}
</div> </div>
); );
} }