feat: save local fishing plans
This commit is contained in:
@@ -35,7 +35,7 @@ const breadcrumbs = spot ? { "@context": "https://schema.org", "@type": "Breadcr
|
||||
<Layout title={spot ? `Точка ${spot.x}:${spot.y}, ${spot.waterbody} — RF4 Spotter` : "Точка не найдена — RF4 Spotter"} description={spotDescription} noindex={!spot} structuredData={breadcrumbs} errorPage={!spot || unavailable}>
|
||||
<AtlasBreadcrumbs items={[{ label: "Сейчас клюёт", href: "/" }, ...(spot ? [{ label: spot.waterbody, href: `/waterbodies/${spot.waterbody_slug}` }, { label: `Точка ${spot.x}:${spot.y}` }] : [{ label: "Точка недоступна" }])]} />
|
||||
{unavailable || !spot ? <div class="state"><h1>Точка недоступна</h1><p>API не ответил или такой точки нет.</p></div> : <>
|
||||
<section class="spot-hero"><div><span class="eyebrow">{spot.waterbody}</span><h1>Точка {spot.x}:{spot.y}</h1><p>{spot.description}</p><p class="coordinate-precision">Точность координат: <strong>{coordinatePrecision[spot.coordinate_precision as keyof typeof coordinatePrecision] ?? "не указаны"}</strong></p><button class="coordinate-copy" data-action="inverse" type="button" data-copy-coordinates={`${spot.x}:${spot.y}`}>Скопировать координаты</button><small class="copy-status" aria-live="polite"></small></div><CoordinateRadar x={spot.x} y={spot.y}/></section>
|
||||
<section class="spot-hero"><div><span class="eyebrow">{spot.waterbody}</span><h1>Точка {spot.x}:{spot.y}</h1><p>{spot.description}</p><p class="coordinate-precision">Точность координат: <strong>{coordinatePrecision[spot.coordinate_precision as keyof typeof coordinatePrecision] ?? "не указаны"}</strong></p><div class="spot-hero__actions"><button class="coordinate-copy" data-action="inverse" type="button" data-copy-coordinates={`${spot.x}:${spot.y}`}>Скопировать координаты</button><button class="plan-save" data-action="inverse" type="button" data-plan-save data-plan-key={Astro.url.pathname} data-plan-waterbody={spot.waterbody} data-plan-coordinates={`${spot.x}:${spot.y}`} data-plan-baits={spot.top_baits.join("|")} data-plan-freshness={activity?.last_confirmed_at ?? ""} data-plan-confidence={activity?.confidence_score ?? ""} aria-pressed="false">Сохранить в план</button><small class="copy-status" aria-live="polite"></small><small class="plan-status" aria-live="polite"></small></div></div><CoordinateRadar x={spot.x} y={spot.y}/></section>
|
||||
<div class="periods"><div><strong>{spot.catches_24h}</strong><span>за 24 часа</span></div><div><strong>{spot.catches_3d}</strong><span>за 3 дня</span></div><div><strong>{spot.catches_7d}</strong><span>за 7 дней</span></div></div>
|
||||
<ActivityTimeline buckets={timeline}/>
|
||||
<div class="activity-legend" aria-label="Уровни активности"><span>Тихо</span><span>Есть сигналы</span><span>Горячо</span></div>
|
||||
@@ -57,4 +57,28 @@ const breadcrumbs = spot ? { "@context": "https://schema.org", "@type": "Breadcr
|
||||
catch { if (status) status.textContent = value; }
|
||||
});
|
||||
});
|
||||
const planStorageKey = "rf4spotter:fishing-plan";
|
||||
const readPlan = (): Array<Record<string, string>> => {
|
||||
try { const value = JSON.parse(localStorage.getItem(planStorageKey) || "[]"); return Array.isArray(value) ? value : []; }
|
||||
catch { return []; }
|
||||
};
|
||||
document.querySelectorAll<HTMLButtonElement>("[data-plan-save]").forEach((button) => {
|
||||
const key = button.dataset.planKey || "";
|
||||
const status = button.parentElement?.querySelector<HTMLElement>(".plan-status");
|
||||
const sync = () => {
|
||||
const saved = readPlan().some(item => item.key === key);
|
||||
button.textContent = saved ? "В плане" : "Сохранить в план";
|
||||
button.setAttribute("aria-pressed", String(saved));
|
||||
button.dataset.saved = String(saved);
|
||||
};
|
||||
sync();
|
||||
button.addEventListener("click", () => {
|
||||
const plan = readPlan();
|
||||
const index = plan.findIndex(item => item.key === key);
|
||||
if (index >= 0) { plan.splice(index, 1); if (status) status.textContent = "Удалено из плана"; }
|
||||
else { plan.unshift({ key, waterbody: button.dataset.planWaterbody || "", coordinates: button.dataset.planCoordinates || "", baits: button.dataset.planBaits || "", freshness: button.dataset.planFreshness || "", confidence: button.dataset.planConfidence || "" }); if (status) status.textContent = "Добавлено в план"; }
|
||||
localStorage.setItem(planStorageKey, JSON.stringify(plan.slice(0, 5)));
|
||||
sync();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -6,4 +6,6 @@
|
||||
.query-summary__filters a{margin-left:4px;color:#4d625e;font-size:13px;text-underline-offset:3px}
|
||||
.detail-action-heading{margin:27px 0 8px;font:400 24px Georgia,serif;color:var(--lime)}
|
||||
.tackle-card-passport{grid-column:1/-1;min-width:0}.tackle-card-passport .data-passport{margin-top:14px}
|
||||
.spot-hero__actions{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.spot-hero__actions small{color:#a9b8b5;font-size:11px}.plan-save[data-saved="true"]{background:var(--lime);color:var(--deep)}
|
||||
@media(max-width:720px){.spot-hero__actions{align-items:flex-start;flex-direction:column}.spot-hero__actions small{min-height:15px}}
|
||||
@media(max-width:720px){.query-summary{display:block;padding:16px 14px 0;width:100%}.query-summary h2{font-size:23px}.query-summary__filters{justify-content:flex-start;flex-wrap:nowrap;overflow-x:auto;padding-top:11px;scroll-snap-type:x proximity;scrollbar-width:none}.query-summary__filters::-webkit-scrollbar{display:none}.query-summary__filters span{flex:0 0 auto;scroll-snap-align:start}.query-summary__filters a{position:sticky;right:0;flex:0 0 auto;padding:6px 10px;background:var(--paper)}}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("spot can be saved to a five-item local fishing plan and survives reload", async ({ page }) => {
|
||||
await page.goto("/spots/vyunok-6331x6332");
|
||||
await page.evaluate(() => localStorage.removeItem("rf4spotter:fishing-plan"));
|
||||
await page.reload();
|
||||
|
||||
const save = page.getByRole("button", { name: "Сохранить в план" });
|
||||
await expect(save).toHaveAttribute("aria-pressed", "false");
|
||||
await save.click();
|
||||
await expect(page.getByRole("button", { name: "В плане" })).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(page.getByText("Добавлено в план")).toBeVisible();
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByRole("button", { name: "В плане" })).toHaveAttribute("aria-pressed", "true");
|
||||
await page.getByRole("button", { name: "В плане" }).click();
|
||||
await expect(page.getByRole("button", { name: "Сохранить в план" })).toHaveAttribute("aria-pressed", "false");
|
||||
});
|
||||
Reference in New Issue
Block a user