feat: pripomienky cez zdielany odkaz; predvolena poloha stitkov a pouzit na vsetky

Pripomienky:
- odkaz /review/<token> so snimkou newslettra: nic sa neda upravit, da sa
  komentovat (spendlik), pisat text do newslettra a kreslit; meno autora sa pamata
- pripomienky su ukotvene k modulu a ulozene k odkazu - vidi ich kazdy s odkazom
- v editore: tlacidlo Pripomienky (pocet otvorenych), vytvorenie a kopirovanie
  odkazu, aktualizacia nahladu v odkaze, zoznam pripomienok, Hotovo a mazanie;
  pripomienky sa zobrazuju aj priamo na platne a daju sa tam oznacit ako hotove
- supabase/migrations/002_reviews.sql (tabulky reviews, review_comments)
- pismo Poppins v layoute (nahlad vernejsi e-mailu)

Stitky predajni:
- predvolena poloha -15 vodorovne / +5 zvisle od praveho horneho rohu pozadia cenovky
- tlacidlo Polohu stitkov pouzit na vsetky produkty

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub_HostacnyandClaude Opus 5.5 committed 2026-10-21 11:03:34 +02:00
1 parent b0a050bd12
commit cfacf2474b
13 files changed
+1162 -10

No files matched your search

@@ -0,0 +1,94 @@
'use client';
// Pripomienky aktuálnej kampane v editore: odkaz, zoznam pripomienok (pravidelne obnovovaný), akcie
import { useCallback, useEffect, useState } from 'react';
import { NewsletterCampaign } from '@/types/newsletter';
import {
Review,
ReviewComment,
createReview,
deleteReviewComment,
getReviewForTemplate,
listReviewComments,
setReviewCommentDone,
updateReviewSnapshot
} from '@/lib/reviews';
const POLL_MS = 20000;
export function useCampaignReview(campaign: NewsletterCampaign) {
const [review, setReview] = useState<Review | null>(null);
const [comments, setComments] = useState<ReviewComment[]>([]);
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const templateId = campaign.id;
const refresh = useCallback(async (reviewId: string) => {
try {
setComments(await listReviewComments(reviewId));
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
}, []);
// Odkaz kampane sa načíta pri otvorení kampane; pripomienky sa obnovujú pravidelne
useEffect(() => {
let timer: ReturnType<typeof setInterval> | undefined;
let cancelled = false;
if (!templateId) return;
(async () => {
try {
const r = await getReviewForTemplate(templateId);
if (cancelled) return;
setReview(r);
setComments([]);
setError(null);
if (!r) return;
await refresh(r.id);
timer = setInterval(() => refresh(r.id), POLL_MS);
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
}
})();
return () => {
cancelled = true;
clearInterval(timer);
};
}, [templateId, refresh]);
const run = async (action: () => Promise<void>) => {
setBusy(true);
setError(null);
try {
await action();
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setBusy(false);
}
};
return {
review,
comments,
error,
busy,
openCount: comments.filter((c) => !c.done).length,
create: () => run(async () => setReview(await createReview(campaign))),
updateSnapshot: () =>
run(async () => {
if (review) setReview(await updateReviewSnapshot(review.id, campaign));
}),
toggleDone: (c: ReviewComment) =>
run(async () => {
await setReviewCommentDone(c.id, !c.done);
setComments((prev) => prev.map((x) => (x.id === c.id ? { ...x, done: !c.done } : x)));
}),
remove: (c: ReviewComment) =>
run(async () => {
await deleteReviewComment(c.id);
setComments((prev) => prev.filter((x) => x.id !== c.id));
}),
refresh: () => review && refresh(review.id)
};
}