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:
1 parent
b0a050bd12
commit
cfacf2474b
13 files changed
+1162
-10
No files matched your search
@@ -23,6 +23,10 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
>
|
||||
<head>
|
||||
{/* Písmo e-mailu (Poppins) – náhľad v editore aj na stránke s pripomienkami */}
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700;800;900&display=swap" />
|
||||
</head>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
+74
-1
@@ -14,6 +14,10 @@ import { BadgeEditorModal } from '@/components/editor/BadgeEditorModal';
|
||||
import { SavedCampaignsModal } from '@/components/editor/SavedCampaignsModal';
|
||||
import JSZip from 'jszip';
|
||||
import { prepareCardImages } from '@/lib/cardImages';
|
||||
import { useCampaignReview } from '@/components/review/useCampaignReview';
|
||||
import { ReviewModal } from '@/components/review/ReviewModal';
|
||||
import { AnnotationLayer, useModuleLayout } from '@/components/review/AnnotationLayer';
|
||||
import { ReviewComment } from '@/lib/reviews';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { ChevronUp, ChevronDown, Copy, Trash2, Anchor, Lock } from 'lucide-react';
|
||||
|
||||
@@ -22,6 +26,14 @@ export default function Home() {
|
||||
const [selectedModuleId, setSelectedModuleId] = useState<string | null>('mod-products-fantasticky');
|
||||
// Produkt otvorený v paneli (harmonika); nastaví sa aj klikom na produkt v náhľade
|
||||
const [selectedProductId, setSelectedProductId] = useState<string | null>(null);
|
||||
|
||||
// Pripomienky zo zdieľaného odkazu (zobrazujú sa aj na plátne, dajú sa označiť ako hotové)
|
||||
const reviews = useCampaignReview(campaign);
|
||||
const [isReviewOpen, setIsReviewOpen] = useState(false);
|
||||
const [showReviewOverlay, setShowReviewOverlay] = useState(true);
|
||||
const [selectedCommentId, setSelectedCommentId] = useState<string | null>(null);
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
const canvasLayout = useModuleLayout(canvasRef, [campaign.modules, reviews.comments.length, showReviewOverlay]);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isExportingBrevo, setIsExportingBrevo] = useState(false);
|
||||
const [brevoUrl, setBrevoUrl] = useState<string | null>(null);
|
||||
@@ -594,6 +606,19 @@ export default function Home() {
|
||||
});
|
||||
};
|
||||
|
||||
// Poloha štítkov predajní pre všetky produkty v celej kampani (všetky sekcie + vernostné zľavy)
|
||||
const handleApplyStoresOffsetToAll = (x: number, y: number) => {
|
||||
if (!confirm(`Použiť polohu štítkov (${x} / ${y}) pre všetky produkty v newsletteri?`)) return;
|
||||
setCampaign((prev) => ({
|
||||
...prev,
|
||||
modules: prev.modules.map((m) =>
|
||||
m.type === 'products_grid' || m.type === 'loyalty_club'
|
||||
? { ...m, data: { ...m.data, products: (m.data.products || []).map((p: ProductItem) => ({ ...p, storesOffsetX: x, storesOffsetY: y })) } }
|
||||
: m
|
||||
)
|
||||
}));
|
||||
};
|
||||
|
||||
// Hromadná aplikácia cenovky na všetky produkty v celej kampani (všetky sekcie + vernostné zľavy)
|
||||
const handleApplyBadgeToAll = (badge: BadgeTemplate, badgeLayout?: 'below' | 'side') => {
|
||||
setCampaign((prev) => ({
|
||||
@@ -657,6 +682,8 @@ export default function Home() {
|
||||
brevoListingUrl="https://app.brevo.com/campaigns/listing"
|
||||
onClearBrevoId={handleClearBrevoId}
|
||||
onAddModule={handleAddModule}
|
||||
onOpenReviews={() => setIsReviewOpen(true)}
|
||||
reviewOpenCount={reviews.openCount}
|
||||
/>
|
||||
|
||||
{/* 2. Hlavný pracovný priestor */}
|
||||
@@ -674,7 +701,7 @@ export default function Home() {
|
||||
<style>{`main [data-product-id="${selectedProductId.replace(/["\\]/g, '')}"] { outline: 3px solid #ef4444; outline-offset: 3px; }`}</style>
|
||||
)}
|
||||
{/* E-mail Container 1:1 k originálu */}
|
||||
<div className="w-full max-w-[600px] my-4">
|
||||
<div ref={canvasRef} className="relative w-full max-w-[600px] h-fit my-4">
|
||||
{campaign.modules.map((mod, idx) => {
|
||||
const isSelected = mod.id === selectedModuleId;
|
||||
const isAnchored = isAnchoredModule(mod);
|
||||
@@ -689,6 +716,7 @@ export default function Home() {
|
||||
return (
|
||||
<div
|
||||
key={mod.id}
|
||||
data-module-id={mod.id}
|
||||
onClick={(e) => {
|
||||
// Klik na kartu produktu otvorí v paneli práve tento produkt
|
||||
const card = (e.target as HTMLElement).closest('[data-product-id]');
|
||||
@@ -891,6 +919,27 @@ export default function Home() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Pripomienky zo zdieľaného odkazu */}
|
||||
{showReviewOverlay && reviews.comments.length > 0 && (
|
||||
<AnnotationLayer
|
||||
layout={canvasLayout}
|
||||
comments={reviews.comments}
|
||||
selectedId={selectedCommentId}
|
||||
onSelect={setSelectedCommentId}
|
||||
renderActions={(c: ReviewComment) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => reviews.toggleDone(c)}
|
||||
className={`text-[11px] font-bold px-2.5 py-1 rounded-lg ${
|
||||
c.done ? 'bg-gray-100 text-gray-700' : 'bg-emerald-600 text-white hover:bg-emerald-700'
|
||||
}`}
|
||||
>
|
||||
{c.done ? 'Vrátiť ako neurobené' : '✓ Označiť ako hotové'}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -902,6 +951,7 @@ export default function Home() {
|
||||
onSelectProduct={setSelectedProductId}
|
||||
onUpdateModule={handleUpdateModule}
|
||||
onApplyBadgeToAll={handleApplyBadgeToAll}
|
||||
onApplyStoresOffsetToAll={handleApplyStoresOffsetToAll}
|
||||
onUpdateAllProductSections={handleUpdateAllProductSections}
|
||||
campaignValidity={getCampaignValidity(campaign)}
|
||||
availableBadges={availableBadges}
|
||||
@@ -914,6 +964,29 @@ export default function Home() {
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* Pripomienky: odkaz a zoznam */}
|
||||
<ReviewModal
|
||||
isOpen={isReviewOpen}
|
||||
onClose={() => setIsReviewOpen(false)}
|
||||
hasCampaignId={!!campaign.id}
|
||||
review={reviews.review}
|
||||
comments={reviews.comments}
|
||||
error={reviews.error}
|
||||
busy={reviews.busy}
|
||||
showOverlay={showReviewOverlay}
|
||||
onToggleOverlay={() => setShowReviewOverlay(!showReviewOverlay)}
|
||||
onCreate={reviews.create}
|
||||
onUpdateSnapshot={reviews.updateSnapshot}
|
||||
onToggleDone={reviews.toggleDone}
|
||||
onDelete={reviews.remove}
|
||||
onShowComment={(c) => {
|
||||
setShowReviewOverlay(true);
|
||||
setSelectedCommentId(c.id);
|
||||
setIsReviewOpen(false);
|
||||
document.querySelector(`[data-module-id="${c.module_id}"]`)?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Modál Hromadného importu */}
|
||||
<ProductImportModal
|
||||
isOpen={isImportOpen}
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
'use client';
|
||||
|
||||
// Náhľad newslettra na pripomienky (zdieľaný odkaz). Nič sa tu nedá upravovať – len komentovať,
|
||||
// písať text priamo do newslettra a kresliť. Pripomienky sa ukladajú k odkazu, vidí ich každý s odkazom.
|
||||
import React, { use, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { MessageCircle, Type, Pencil, MousePointer2, CheckCircle2, Loader2 } from 'lucide-react';
|
||||
import { renderSingleModuleHtml } from '@/lib/emailRenderer';
|
||||
import {
|
||||
Review,
|
||||
ReviewComment,
|
||||
ReviewKind,
|
||||
REVIEW_COLORS,
|
||||
getReviewByToken,
|
||||
listReviewComments,
|
||||
addReviewComment
|
||||
} from '@/lib/reviews';
|
||||
import { AnnotationLayer, pointToModuleCoords, useModuleLayout } from '@/components/review/AnnotationLayer';
|
||||
import { NewsletterCampaign } from '@/types/newsletter';
|
||||
|
||||
type Tool = 'view' | ReviewKind;
|
||||
|
||||
interface Draft {
|
||||
kind: 'comment' | 'text';
|
||||
moduleId: string;
|
||||
x: number;
|
||||
y: number;
|
||||
left: number;
|
||||
top: number;
|
||||
}
|
||||
|
||||
const AUTHOR_KEY = 'coop_review_author';
|
||||
const POLL_MS = 10000;
|
||||
|
||||
const TOOLS: Array<{ id: Tool; label: string; icon: React.ElementType; hint: string }> = [
|
||||
{ id: 'view', label: 'Prezerať', icon: MousePointer2, hint: 'Prezeranie a výber pripomienok' },
|
||||
{ id: 'comment', label: 'Komentár', icon: MessageCircle, hint: 'Kliknite do newslettra a napíšte komentár' },
|
||||
{ id: 'text', label: 'Text', icon: Type, hint: 'Kliknite do newslettra a napíšte text priamo do neho' },
|
||||
{ id: 'drawing', label: 'Kresliť', icon: Pencil, hint: 'Ťahaním myšou kreslite do newslettra' }
|
||||
];
|
||||
|
||||
export default function ReviewPage({ params }: { params: Promise<{ token: string }> }) {
|
||||
const { token } = use(params);
|
||||
const [review, setReview] = useState<Review | null>(null);
|
||||
const [comments, setComments] = useState<ReviewComment[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [tool, setTool] = useState<Tool>('comment');
|
||||
const [color, setColor] = useState(REVIEW_COLORS[0]);
|
||||
// meno sa pamätá v prehliadači (stránka sa vykreslí až po načítaní odkazu, takže bez rozdielu so serverom)
|
||||
const [author, setAuthor] = useState(() => {
|
||||
try {
|
||||
return typeof window === 'undefined' ? '' : localStorage.getItem(AUTHOR_KEY) || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [draft, setDraft] = useState<Draft | null>(null);
|
||||
const [draftText, setDraftText] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [livePath, setLivePath] = useState<{ moduleId: string; points: Array<[number, number]> } | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const layout = useModuleLayout(containerRef, [review, comments.length]);
|
||||
|
||||
const refreshComments = useCallback(async (reviewId: string) => {
|
||||
try {
|
||||
setComments(await listReviewComments(reviewId));
|
||||
} catch (e) {
|
||||
console.warn('Pripomienky sa nepodarilo načítať:', e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: ReturnType<typeof setInterval> | undefined;
|
||||
(async () => {
|
||||
try {
|
||||
const r = await getReviewByToken(token);
|
||||
if (!r) {
|
||||
setError('Odkaz neexistuje alebo bol zrušený.');
|
||||
return;
|
||||
}
|
||||
setReview(r);
|
||||
await refreshComments(r.id);
|
||||
timer = setInterval(() => refreshComments(r.id), POLL_MS);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Náhľad sa nepodarilo načítať.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => clearInterval(timer);
|
||||
}, [token, refreshComments]);
|
||||
|
||||
const saveAuthor = (value: string) => {
|
||||
setAuthor(value);
|
||||
try {
|
||||
localStorage.setItem(AUTHOR_KEY, value);
|
||||
} catch {
|
||||
// localStorage nemusí byť dostupný
|
||||
}
|
||||
};
|
||||
|
||||
const save = async (data: { kind: ReviewKind; moduleId: string; x: number; y: number; text?: string; path?: Array<[number, number]> }) => {
|
||||
if (!review) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const created = await addReviewComment({
|
||||
review_id: review.id,
|
||||
kind: data.kind,
|
||||
module_id: data.moduleId,
|
||||
x: data.x,
|
||||
y: data.y,
|
||||
path: data.path || null,
|
||||
text: data.text || '',
|
||||
color,
|
||||
author: author.trim() || 'Anonym'
|
||||
});
|
||||
setComments((prev) => [...prev, created]);
|
||||
if (data.kind !== 'drawing') setSelectedId(created.id);
|
||||
} catch (e) {
|
||||
alert(e instanceof Error ? e.message : 'Pripomienku sa nepodarilo uložiť.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Klik do newslettra: komentár / text → okienko na napísanie
|
||||
const onCanvasClick = (e: React.MouseEvent) => {
|
||||
if (tool !== 'comment' && tool !== 'text') return;
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
const coords = pointToModuleCoords(container, e.clientX, e.clientY);
|
||||
if (!coords) return;
|
||||
const rect = container.getBoundingClientRect();
|
||||
setDraft({ kind: tool, ...coords, left: e.clientX - rect.left, top: e.clientY - rect.top });
|
||||
setDraftText('');
|
||||
setSelectedId(null);
|
||||
};
|
||||
|
||||
const submitDraft = async () => {
|
||||
if (!draft || !draftText.trim()) return;
|
||||
await save({ kind: draft.kind, moduleId: draft.moduleId, x: draft.x, y: draft.y, text: draftText.trim() });
|
||||
setDraft(null);
|
||||
setDraftText('');
|
||||
};
|
||||
|
||||
// Kreslenie: body sa ukotvia k modulu, v ktorom ťah začal
|
||||
const onPointerDown = (e: React.PointerEvent) => {
|
||||
if (tool !== 'drawing' || !containerRef.current) return;
|
||||
const start = pointToModuleCoords(containerRef.current, e.clientX, e.clientY);
|
||||
if (!start) return;
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
setLivePath({ moduleId: start.moduleId, points: [[start.x, start.y]] });
|
||||
};
|
||||
const onPointerMove = (e: React.PointerEvent) => {
|
||||
if (!livePath || !containerRef.current) return;
|
||||
const p = pointToModuleCoords(containerRef.current, e.clientX, e.clientY, livePath.moduleId);
|
||||
if (p) setLivePath({ ...livePath, points: [...livePath.points, [p.x, p.y]] });
|
||||
};
|
||||
const onPointerUp = async () => {
|
||||
if (!livePath) return;
|
||||
const path = livePath;
|
||||
setLivePath(null);
|
||||
if (path.points.length < 2) return;
|
||||
await save({ kind: 'drawing', moduleId: path.moduleId, x: path.points[0][0], y: path.points[0][1], path: path.points });
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[#001a33] text-white gap-2">
|
||||
<Loader2 className="w-5 h-5 animate-spin" /> Načítavam náhľad…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (error || !review) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[#001a33] p-6">
|
||||
<p className="max-w-md text-center text-sm text-white/90 bg-white/10 rounded-xl p-5">{error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const campaign: NewsletterCampaign = { id: '', status: 'draft', ...review.snapshot };
|
||||
const openCount = comments.filter((c) => !c.done).length;
|
||||
const livePreview = livePath && layout
|
||||
? livePath.points.map(([x, y]) => `${x * layout.width},${(layout.tops[livePath.moduleId] ?? 0) + y}`).join(' ')
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col overflow-hidden bg-slate-950 font-sans">
|
||||
{/* Horná lišta: nástroje */}
|
||||
<header className="h-14 shrink-0 bg-white border-b border-gray-200 px-4 flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-[#e2001a] flex items-center justify-center text-white font-black text-[11px]">COOP</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-bold text-gray-900 truncate">{review.title || 'Newsletter'}</p>
|
||||
<p className="text-[10px] text-gray-500">Náhľad na pripomienky · aktualizovaný {new Date(review.updated_at).toLocaleString('sk-SK')}</p>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
<div className="flex items-center gap-1 bg-gray-100 p-1 rounded-xl">
|
||||
{TOOLS.map(({ id, label, icon: Icon, hint }) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
title={hint}
|
||||
onClick={() => {
|
||||
setTool(id);
|
||||
setDraft(null);
|
||||
}}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-bold transition-colors ${
|
||||
tool === id ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-500 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
<span className="hidden md:inline">{label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{REVIEW_COLORS.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
onClick={() => setColor(c)}
|
||||
className={`w-5 h-5 rounded-full border-2 ${color === c ? 'border-gray-900 scale-110' : 'border-white shadow'}`}
|
||||
style={{ backgroundColor: c }}
|
||||
title="Farba"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Newsletter */}
|
||||
<main
|
||||
className="flex-1 overflow-y-auto p-4 md:p-8 flex justify-center"
|
||||
style={{ backgroundColor: '#001a33', backgroundImage: 'linear-gradient(135deg, #005bb5 0%, #000d1a 100%)' }}
|
||||
onClick={() => setSelectedId(null)}
|
||||
>
|
||||
<div ref={containerRef} className="relative w-full max-w-[600px] h-fit my-4 select-none">
|
||||
{review.snapshot.modules.map((m) => (
|
||||
<div
|
||||
key={m.id}
|
||||
data-module-id={m.id}
|
||||
className="pointer-events-none"
|
||||
dangerouslySetInnerHTML={{ __html: renderSingleModuleHtml(m, campaign) }}
|
||||
/>
|
||||
))}
|
||||
|
||||
<AnnotationLayer layout={layout} comments={comments} selectedId={selectedId} onSelect={setSelectedId} />
|
||||
|
||||
{/* Plocha na pridávanie pripomienok */}
|
||||
{tool !== 'view' && (
|
||||
<div
|
||||
className={`absolute inset-0 z-20 ${tool === 'drawing' ? 'cursor-crosshair touch-none' : 'cursor-copy'}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCanvasClick(e);
|
||||
}}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Rozkreslená čiara */}
|
||||
{livePath && (
|
||||
<svg className="absolute inset-0 w-full h-full pointer-events-none z-40 overflow-visible">
|
||||
<polyline points={livePreview} fill="none" stroke={color} strokeWidth={3} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)}
|
||||
|
||||
{/* Okienko na napísanie komentára / textu */}
|
||||
{draft && (
|
||||
<div
|
||||
className="absolute z-50 w-64 bg-white rounded-xl shadow-2xl border border-gray-200 p-2.5"
|
||||
style={{ left: Math.min(draft.left, (layout?.width || 600) - 256), top: draft.top + 6 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<textarea
|
||||
autoFocus
|
||||
rows={3}
|
||||
value={draftText}
|
||||
onChange={(e) => setDraftText(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
submitDraft();
|
||||
}
|
||||
if (e.key === 'Escape') setDraft(null);
|
||||
}}
|
||||
placeholder={draft.kind === 'comment' ? 'Napíšte komentár…' : 'Text, ktorý sa zobrazí v newslettri…'}
|
||||
className="w-full text-xs border border-gray-300 rounded-lg p-2 resize-none"
|
||||
/>
|
||||
<div className="flex items-center justify-between mt-1.5">
|
||||
<span className="text-[10px] text-gray-400">Enter = uložiť, Shift+Enter = nový riadok</span>
|
||||
<div className="flex gap-1">
|
||||
<button type="button" onClick={() => setDraft(null)} className="text-[11px] font-bold text-gray-500 px-2 py-1">
|
||||
Zrušiť
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving || !draftText.trim()}
|
||||
onClick={submitDraft}
|
||||
className="text-[11px] font-bold text-white bg-[#e2001a] disabled:opacity-40 px-2.5 py-1 rounded-lg"
|
||||
>
|
||||
Uložiť
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Zoznam pripomienok */}
|
||||
<aside className="w-80 shrink-0 bg-white border-l border-gray-200 flex flex-col">
|
||||
<div className="p-4 border-b border-gray-100 space-y-2">
|
||||
<label className="block">
|
||||
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-wider">Vaše meno</span>
|
||||
<input
|
||||
type="text"
|
||||
value={author}
|
||||
onChange={(e) => saveAuthor(e.target.value)}
|
||||
placeholder="Meno, aby bolo jasné, kto pripomienku napísal"
|
||||
className={`mt-1 w-full text-xs border rounded-lg p-2 ${author.trim() ? 'border-gray-300' : 'border-amber-400 bg-amber-50'}`}
|
||||
/>
|
||||
</label>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
{TOOLS.find((t) => t.id === tool)?.hint}. Pripomienky uvidí každý, kto má tento odkaz.
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-4 py-2 text-[11px] font-bold text-gray-700 flex items-center justify-between">
|
||||
<span>Pripomienky ({comments.length})</span>
|
||||
<span className="text-gray-400 font-medium">{openCount} otvorených</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-3 pb-4 space-y-1.5">
|
||||
{comments.length === 0 && <p className="text-xs text-gray-400 px-1 py-6 text-center">Zatiaľ žiadne pripomienky.</p>}
|
||||
{comments.map((c) => {
|
||||
const number = comments.filter((x) => x.kind === 'comment').indexOf(c) + 1;
|
||||
return (
|
||||
<button
|
||||
key={c.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedId(c.id === selectedId ? null : c.id)}
|
||||
className={`w-full text-left rounded-lg border p-2 transition-colors ${
|
||||
c.id === selectedId ? 'border-gray-900 bg-gray-50' : 'border-gray-200 hover:border-gray-300'
|
||||
} ${c.done ? 'opacity-60' : ''}`}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-4 h-4 rounded-full text-[9px] font-black text-white flex items-center justify-center shrink-0" style={{ backgroundColor: c.color }}>
|
||||
{c.kind === 'comment' ? number : c.kind === 'text' ? 'T' : '✎'}
|
||||
</span>
|
||||
<span className="text-[11px] font-bold text-gray-900 truncate">{c.author || 'Anonym'}</span>
|
||||
{c.done && <CheckCircle2 className="w-3.5 h-3.5 text-emerald-600 shrink-0" />}
|
||||
<span className="ml-auto text-[10px] text-gray-400 shrink-0">{new Date(c.created_at).toLocaleDateString('sk-SK')}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-700 whitespace-pre-wrap line-clamp-3">
|
||||
{c.text || (c.kind === 'drawing' ? 'Kresba' : '')}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user