feat: klik na produkt v nahlade otvori jeho editor (harmonika)
- v paneli je otvoreny vzdy len jeden produkt, ostatne su zbalene - klik na kartu produktu v nahlade vyberie sekciu aj produkt, panel sa posunie na jeho editor a karta sa v nahlade zvyrazni - odkazy v nahlade sa klikom neotvaraju Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
109d65a690
commit
c426145c48
4 files changed
+49
-12
No files matched your search
+23
-2
@@ -20,6 +20,8 @@ import { ChevronUp, ChevronDown, Copy, Trash2, Anchor, Lock } from 'lucide-react
|
||||
export default function Home() {
|
||||
const [campaign, setCampaign] = useState<NewsletterCampaign>(DEFAULT_CAMPAIGN);
|
||||
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);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isExportingBrevo, setIsExportingBrevo] = useState(false);
|
||||
const [brevoUrl, setBrevoUrl] = useState<string | null>(null);
|
||||
@@ -667,6 +669,10 @@ export default function Home() {
|
||||
backgroundImage: 'linear-gradient(135deg, #005bb5 0%, #000d1a 100%)'
|
||||
}}
|
||||
>
|
||||
{/* Zvýraznenie produktu otvoreného v paneli */}
|
||||
{selectedProductId && (
|
||||
<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">
|
||||
{campaign.modules.map((mod, idx) => {
|
||||
@@ -683,7 +689,16 @@ export default function Home() {
|
||||
return (
|
||||
<div
|
||||
key={mod.id}
|
||||
onClick={() => setSelectedModuleId(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]');
|
||||
if (card) {
|
||||
setSelectedProductId(card.getAttribute('data-product-id'));
|
||||
} else if (mod.id !== selectedModuleId) {
|
||||
setSelectedProductId(null);
|
||||
}
|
||||
setSelectedModuleId(mod.id);
|
||||
}}
|
||||
className={`relative cursor-pointer transition-all duration-150 rounded-2xl ${
|
||||
isSelected
|
||||
? 'ring-2 ring-offset-4 ring-offset-[#001a33] ring-red-500 shadow-2xl'
|
||||
@@ -866,7 +881,11 @@ export default function Home() {
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: renderSingleModuleHtml(mod, campaign)
|
||||
}}
|
||||
className="pointer-events-none"
|
||||
onClick={(e) => {
|
||||
// odkazy v náhľade neotvárame, klik slúži na výber modulu / produktu
|
||||
if ((e.target as HTMLElement).closest('a')) e.preventDefault();
|
||||
}}
|
||||
className="[&_[data-product-id]]:cursor-pointer"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -879,6 +898,8 @@ export default function Home() {
|
||||
<aside className="w-96 flex-shrink-0 h-full bg-white shadow-2xl z-20">
|
||||
<ConfigPanel
|
||||
module={selectedModule}
|
||||
selectedProductId={selectedProductId}
|
||||
onSelectProduct={setSelectedProductId}
|
||||
onUpdateModule={handleUpdateModule}
|
||||
onApplyBadgeToAll={handleApplyBadgeToAll}
|
||||
onUpdateAllProductSections={handleUpdateAllProductSections}
|
||||
|
||||
@@ -21,6 +21,9 @@ interface ConfigPanelProps {
|
||||
onOpenBadgeManager?: (productId: string) => void;
|
||||
onOpenImport?: () => void;
|
||||
availableBadges?: BadgeTemplate[];
|
||||
// Otvorený produkt v harmonike (klik na produkt v náhľade ho otvorí)
|
||||
selectedProductId?: string | null;
|
||||
onSelectProduct?: (productId: string | null) => void;
|
||||
}
|
||||
|
||||
// Jednoduchý HTML editor: vlastné HTML nahradí modul vygenerovaný z formulára
|
||||
@@ -94,7 +97,9 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||
campaignValidity = '',
|
||||
onOpenBadgeManager,
|
||||
onOpenImport,
|
||||
availableBadges = []
|
||||
availableBadges = [],
|
||||
selectedProductId = null,
|
||||
onSelectProduct
|
||||
}) => {
|
||||
if (!module) {
|
||||
return (
|
||||
@@ -660,7 +665,8 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||
product={p}
|
||||
index={pIdx}
|
||||
variant="standard"
|
||||
defaultOpen={pIdx === 0}
|
||||
open={selectedProductId === p.id}
|
||||
onToggle={() => onSelectProduct?.(selectedProductId === p.id ? null : p.id)}
|
||||
availableBadges={availableBadges || []}
|
||||
onChange={(patch) => updateProduct(pIdx, patch)}
|
||||
onDelete={() => updateData({ products: (module.data.products || []).filter((_: ProductItem, i: number) => i !== pIdx) })}
|
||||
@@ -845,7 +851,8 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
||||
product={p}
|
||||
index={pIdx}
|
||||
variant="loyalty"
|
||||
defaultOpen={pIdx === 0}
|
||||
open={selectedProductId === p.id}
|
||||
onToggle={() => onSelectProduct?.(selectedProductId === p.id ? null : p.id)}
|
||||
availableBadges={availableBadges || []}
|
||||
onChange={(patch) => updateProduct(pIdx, patch)}
|
||||
onOpenBadgeEditor={onOpenBadgeManager ? () => onOpenBadgeManager(p.id) : undefined}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// Editor jedného produktu (bežné aj vernostné produkty) – zbalený po produktoch, len to, čo treba:
|
||||
// texty, fotka (zoom/posun), cenovka (šablóna, veľkosť, ceny), dostupnosť a ďalšie možnosti.
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { BadgeTemplate, CardProduct } from '@/types/newsletter';
|
||||
import { ChevronDown, Trash2, Pencil, Globe, Layers } from 'lucide-react';
|
||||
import { ImageUploadInput } from './ImageUploadInput';
|
||||
@@ -31,7 +31,8 @@ interface ProductEditorProps {
|
||||
index: number;
|
||||
variant: Variant;
|
||||
availableBadges: BadgeTemplate[];
|
||||
defaultOpen?: boolean;
|
||||
open: boolean; // harmonika: otvorený je vždy len jeden produkt
|
||||
onToggle: () => void;
|
||||
onChange: (patch: Patch) => void;
|
||||
onDelete?: () => void;
|
||||
onOpenBadgeEditor?: () => void;
|
||||
@@ -111,14 +112,19 @@ export const ProductEditor: React.FC<ProductEditorProps> = ({
|
||||
index,
|
||||
variant,
|
||||
availableBadges,
|
||||
defaultOpen = false,
|
||||
open,
|
||||
onToggle,
|
||||
onChange,
|
||||
onDelete,
|
||||
onOpenBadgeEditor,
|
||||
onApplyBadgeToSection,
|
||||
onApplyBadgeToAll
|
||||
}) => {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
// Po otvorení (aj klikom na produkt v náhľade) posunieme panel na tento produkt
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (open) rootRef.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||
}, [open]);
|
||||
const isLoyalty = variant === 'loyalty';
|
||||
const set = (key: string) => (v: unknown) => onChange({ [key]: v });
|
||||
|
||||
@@ -166,10 +172,13 @@ export const ProductEditor: React.FC<ProductEditorProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50 overflow-hidden">
|
||||
<div
|
||||
ref={rootRef}
|
||||
className={`rounded-xl border bg-gray-50 overflow-hidden scroll-mt-2 ${open ? 'border-red-400 ring-1 ring-red-200' : 'border-gray-200'}`}
|
||||
>
|
||||
{/* Hlavička produktu – klik rozbalí / zbalí */}
|
||||
<div className="flex items-center gap-2 px-2.5 py-2 bg-white">
|
||||
<button type="button" onClick={() => setOpen(!open)} className="flex-1 flex items-center gap-2 min-w-0 text-left">
|
||||
<button type="button" onClick={onToggle} className="flex-1 flex items-center gap-2 min-w-0 text-left">
|
||||
<span className="w-8 h-8 rounded-md bg-gray-100 border border-gray-200 shrink-0 overflow-hidden flex items-center justify-center">
|
||||
{p.imageUrl && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
|
||||
@@ -302,7 +302,7 @@ export function renderProductCard(p: CardProduct, ctx: CardContext): string {
|
||||
const key = cardVisualKey(p, ctx);
|
||||
const visual = ctx.mode === 'preview' ? renderVisualPreview(p, ctx, key) : renderVisualExport(p, ctx, key);
|
||||
return `
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="100%" class="glass-product ${colorClass}" bgcolor="${bg}" style="background-color: ${bg}; border-radius: 16px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); overflow: hidden; height: 100%;">
|
||||
<table ${ctx.mode === 'preview' ? `data-product-id="${escapeHtml(p.id)}" ` : ''}role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" height="100%" class="glass-product ${colorClass}" bgcolor="${bg}" style="background-color: ${bg}; border-radius: 16px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); overflow: hidden; height: 100%;">
|
||||
<tr>
|
||||
<td style="padding: 0; line-height: 0; font-size: 0; border-top-left-radius: 16px; border-top-right-radius: 16px;">
|
||||
${visual}
|
||||
|
||||
Reference in new issue
Block a user