feat: harden fishing plan sharing

This commit is contained in:
ik
2026-09-21 19:59:13 +07:00
parent bf0f16642a
commit be59d76791
2 changed files with 37 additions and 10 deletions
+36 -9
View File
@@ -3,7 +3,7 @@ import Layout from "../layouts/Layout.astro";
---
<Layout title="Мой план рыбалки — RF4 Spotter" description="Сохранённые точки рыбалки RF4 без аккаунта: координаты, наживки, свежесть и доверие." noindex>
<section class="plan-hero content-grid"><div><span class="eyebrow">Локально на этом устройстве</span><h1>Мой план<br/><em>рыбалки.</em></h1><p>Соберите до пяти точек, сравните их перед выездом и распечатайте список. Данные не отправляются на сервер.</p></div><div class="plan-hero__mark" aria-hidden="true">♢</div></section>
<section class="plan-toolbar content-grid" aria-label="Действия с планом"><span data-plan-count>0 из 5 точек</span><div><button data-action="secondary" type="button" data-plan-share>Поделиться планом</button><button data-action="secondary" type="button" data-plan-print>Печать / PDF</button><button data-action="quiet" type="button" data-plan-clear>Очистить план</button><small data-plan-share-status aria-live="polite"></small></div></section>
<section class="plan-toolbar content-grid" aria-label="Действия с планом"><span data-plan-count>0 из 5 точек</span><div><button data-action="secondary" type="button" data-plan-share>Поделиться планом</button><button data-action="secondary" type="button" data-plan-copy>Скопировать ссылку</button><button data-action="secondary" type="button" data-plan-print>Печать / PDF</button><button data-action="quiet" type="button" data-plan-clear>Очистить план</button><small data-plan-share-status aria-live="polite"></small></div></section>
<section class="plan-content content-grid" data-plan-page aria-live="polite"><div class="plan-grid" data-plan-list></div><div class="state" data-plan-empty><h2>План пока пуст</h2><p>Сохраните точку на её detail-странице — она появится здесь.</p><a data-action="secondary" href="/">Посмотреть горячие точки</a></div></section>
</Layout>
<script>
@@ -12,6 +12,10 @@ import Layout from "../layouts/Layout.astro";
const empty = document.querySelector<HTMLElement>("[data-plan-empty]");
const count = document.querySelector<HTMLElement>("[data-plan-count]");
const shareStatus = document.querySelector<HTMLElement>("[data-plan-share-status]");
const shareButton = document.querySelector<HTMLButtonElement>("[data-plan-share]");
const copyButton = document.querySelector<HTMLButtonElement>("[data-plan-copy]");
const printButton = document.querySelector<HTMLButtonElement>("[data-plan-print]");
const clearButton = document.querySelector<HTMLButtonElement>("[data-plan-clear]");
const planFieldLimits: Record<string, number> = { key: 512, waterbody: 160, coordinates: 80, baits: 1000, method: 120, retrieve: 160, risk: 160, freshness: 40, confidence: 8 };
const planText = (key: string, value: unknown) => typeof value === "string" ? value.slice(0, planFieldLimits[key] ?? 240) : "";
const normalisePlan = (value: unknown): Array<Record<string, string>> => {
@@ -52,6 +56,7 @@ import Layout from "../layouts/Layout.astro";
list.replaceChildren();
empty.hidden = plan.length > 0;
if (count) count.textContent = `${plan.length} из 5 точек`;
[shareButton, copyButton, printButton, clearButton].forEach(button => { if (button) button.disabled = plan.length === 0; });
plan.forEach(item => {
const card = document.createElement("article"); card.className = "plan-card";
const heading = document.createElement("h2"); heading.textContent = item.waterbody || "Водоём не указан";
@@ -65,17 +70,39 @@ import Layout from "../layouts/Layout.astro";
card.append(heading, coordinate, meta, approach, facts, actions); list.append(card);
});
};
document.querySelector<HTMLButtonElement>("[data-plan-share]")?.addEventListener("click", async () => {
const planUrl = () => {
const plan = readPlan();
if (!plan.length) { if (shareStatus) shareStatus.textContent = "Сначала добавьте точку в план"; return; }
const url = new URL(location.href); url.search = ""; url.searchParams.set("plan", JSON.stringify(plan));
return { plan, value: url.toString() };
};
const copyText = async (value: string) => {
if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(value); return; }
const field = document.createElement("textarea");
field.value = value; field.setAttribute("readonly", ""); field.style.position = "fixed"; field.style.opacity = "0";
document.body.append(field); field.select();
const copied = document.execCommand("copy");
field.remove();
if (!copied) throw new Error("Clipboard is unavailable");
};
const copyPlanLink = async () => {
const { plan, value } = planUrl();
if (!plan.length) { if (shareStatus) shareStatus.textContent = "Сначала добавьте точку в план"; return; }
await copyText(value);
if (shareStatus) shareStatus.textContent = "Ссылка скопирована";
};
shareButton?.addEventListener("click", async () => {
const { plan, value } = planUrl();
if (!plan.length) { if (shareStatus) shareStatus.textContent = "Сначала добавьте точку в план"; return; }
try {
if (navigator.share) await navigator.share({ title: "Мой план рыбалки RF4", url: url.toString() });
else await navigator.clipboard.writeText(url.toString());
if (shareStatus) shareStatus.textContent = "Ссылка готова";
} catch { if (shareStatus) shareStatus.textContent = "Скопируйте ссылку из адресной строки"; }
if (navigator.share) { await navigator.share({ title: "Мой план рыбалки RF4", url: value }); if (shareStatus) shareStatus.textContent = "План отправлен"; }
else await copyPlanLink();
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") { if (shareStatus) shareStatus.textContent = "Поделиться отменено"; return; }
if (shareStatus) shareStatus.textContent = "Не удалось скопировать ссылку";
}
});
document.querySelector<HTMLButtonElement>("[data-plan-print]")?.addEventListener("click", () => window.print());
document.querySelector<HTMLButtonElement>("[data-plan-clear]")?.addEventListener("click", () => { localStorage.removeItem(planStorageKey); render(); });
copyButton?.addEventListener("click", async () => { try { await copyPlanLink(); } catch { if (shareStatus) shareStatus.textContent = "Не удалось скопировать ссылку"; } });
printButton?.addEventListener("click", () => window.print());
clearButton?.addEventListener("click", () => { localStorage.removeItem(planStorageKey); if (shareStatus) shareStatus.textContent = "План очищен"; render(); });
render();
</script>