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() {
|
export default function Home() {
|
||||||
const [campaign, setCampaign] = useState<NewsletterCampaign>(DEFAULT_CAMPAIGN);
|
const [campaign, setCampaign] = useState<NewsletterCampaign>(DEFAULT_CAMPAIGN);
|
||||||
const [selectedModuleId, setSelectedModuleId] = useState<string | null>('mod-products-fantasticky');
|
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 [isSaving, setIsSaving] = useState(false);
|
||||||
const [isExportingBrevo, setIsExportingBrevo] = useState(false);
|
const [isExportingBrevo, setIsExportingBrevo] = useState(false);
|
||||||
const [brevoUrl, setBrevoUrl] = useState<string | null>(null);
|
const [brevoUrl, setBrevoUrl] = useState<string | null>(null);
|
||||||
@@ -667,6 +669,10 @@ export default function Home() {
|
|||||||
backgroundImage: 'linear-gradient(135deg, #005bb5 0%, #000d1a 100%)'
|
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 */}
|
{/* E-mail Container 1:1 k originálu */}
|
||||||
<div className="w-full max-w-[600px] my-4">
|
<div className="w-full max-w-[600px] my-4">
|
||||||
{campaign.modules.map((mod, idx) => {
|
{campaign.modules.map((mod, idx) => {
|
||||||
@@ -683,7 +689,16 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={mod.id}
|
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 ${
|
className={`relative cursor-pointer transition-all duration-150 rounded-2xl ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'ring-2 ring-offset-4 ring-offset-[#001a33] ring-red-500 shadow-2xl'
|
? 'ring-2 ring-offset-4 ring-offset-[#001a33] ring-red-500 shadow-2xl'
|
||||||
@@ -866,7 +881,11 @@ export default function Home() {
|
|||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: renderSingleModuleHtml(mod, campaign)
|
__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>
|
</div>
|
||||||
@@ -879,6 +898,8 @@ export default function Home() {
|
|||||||
<aside className="w-96 flex-shrink-0 h-full bg-white shadow-2xl z-20">
|
<aside className="w-96 flex-shrink-0 h-full bg-white shadow-2xl z-20">
|
||||||
<ConfigPanel
|
<ConfigPanel
|
||||||
module={selectedModule}
|
module={selectedModule}
|
||||||
|
selectedProductId={selectedProductId}
|
||||||
|
onSelectProduct={setSelectedProductId}
|
||||||
onUpdateModule={handleUpdateModule}
|
onUpdateModule={handleUpdateModule}
|
||||||
onApplyBadgeToAll={handleApplyBadgeToAll}
|
onApplyBadgeToAll={handleApplyBadgeToAll}
|
||||||
onUpdateAllProductSections={handleUpdateAllProductSections}
|
onUpdateAllProductSections={handleUpdateAllProductSections}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ interface ConfigPanelProps {
|
|||||||
onOpenBadgeManager?: (productId: string) => void;
|
onOpenBadgeManager?: (productId: string) => void;
|
||||||
onOpenImport?: () => void;
|
onOpenImport?: () => void;
|
||||||
availableBadges?: BadgeTemplate[];
|
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
|
// Jednoduchý HTML editor: vlastné HTML nahradí modul vygenerovaný z formulára
|
||||||
@@ -94,7 +97,9 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
|||||||
campaignValidity = '',
|
campaignValidity = '',
|
||||||
onOpenBadgeManager,
|
onOpenBadgeManager,
|
||||||
onOpenImport,
|
onOpenImport,
|
||||||
availableBadges = []
|
availableBadges = [],
|
||||||
|
selectedProductId = null,
|
||||||
|
onSelectProduct
|
||||||
}) => {
|
}) => {
|
||||||
if (!module) {
|
if (!module) {
|
||||||
return (
|
return (
|
||||||
@@ -660,7 +665,8 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
|||||||
product={p}
|
product={p}
|
||||||
index={pIdx}
|
index={pIdx}
|
||||||
variant="standard"
|
variant="standard"
|
||||||
defaultOpen={pIdx === 0}
|
open={selectedProductId === p.id}
|
||||||
|
onToggle={() => onSelectProduct?.(selectedProductId === p.id ? null : p.id)}
|
||||||
availableBadges={availableBadges || []}
|
availableBadges={availableBadges || []}
|
||||||
onChange={(patch) => updateProduct(pIdx, patch)}
|
onChange={(patch) => updateProduct(pIdx, patch)}
|
||||||
onDelete={() => updateData({ products: (module.data.products || []).filter((_: ProductItem, i: number) => i !== pIdx) })}
|
onDelete={() => updateData({ products: (module.data.products || []).filter((_: ProductItem, i: number) => i !== pIdx) })}
|
||||||
@@ -845,7 +851,8 @@ export const ConfigPanel: React.FC<ConfigPanelProps> = ({
|
|||||||
product={p}
|
product={p}
|
||||||
index={pIdx}
|
index={pIdx}
|
||||||
variant="loyalty"
|
variant="loyalty"
|
||||||
defaultOpen={pIdx === 0}
|
open={selectedProductId === p.id}
|
||||||
|
onToggle={() => onSelectProduct?.(selectedProductId === p.id ? null : p.id)}
|
||||||
availableBadges={availableBadges || []}
|
availableBadges={availableBadges || []}
|
||||||
onChange={(patch) => updateProduct(pIdx, patch)}
|
onChange={(patch) => updateProduct(pIdx, patch)}
|
||||||
onOpenBadgeEditor={onOpenBadgeManager ? () => onOpenBadgeManager(p.id) : undefined}
|
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:
|
// 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.
|
// 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 { BadgeTemplate, CardProduct } from '@/types/newsletter';
|
||||||
import { ChevronDown, Trash2, Pencil, Globe, Layers } from 'lucide-react';
|
import { ChevronDown, Trash2, Pencil, Globe, Layers } from 'lucide-react';
|
||||||
import { ImageUploadInput } from './ImageUploadInput';
|
import { ImageUploadInput } from './ImageUploadInput';
|
||||||
@@ -31,7 +31,8 @@ interface ProductEditorProps {
|
|||||||
index: number;
|
index: number;
|
||||||
variant: Variant;
|
variant: Variant;
|
||||||
availableBadges: BadgeTemplate[];
|
availableBadges: BadgeTemplate[];
|
||||||
defaultOpen?: boolean;
|
open: boolean; // harmonika: otvorený je vždy len jeden produkt
|
||||||
|
onToggle: () => void;
|
||||||
onChange: (patch: Patch) => void;
|
onChange: (patch: Patch) => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
onOpenBadgeEditor?: () => void;
|
onOpenBadgeEditor?: () => void;
|
||||||
@@ -111,14 +112,19 @@ export const ProductEditor: React.FC<ProductEditorProps> = ({
|
|||||||
index,
|
index,
|
||||||
variant,
|
variant,
|
||||||
availableBadges,
|
availableBadges,
|
||||||
defaultOpen = false,
|
open,
|
||||||
|
onToggle,
|
||||||
onChange,
|
onChange,
|
||||||
onDelete,
|
onDelete,
|
||||||
onOpenBadgeEditor,
|
onOpenBadgeEditor,
|
||||||
onApplyBadgeToSection,
|
onApplyBadgeToSection,
|
||||||
onApplyBadgeToAll
|
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 isLoyalty = variant === 'loyalty';
|
||||||
const set = (key: string) => (v: unknown) => onChange({ [key]: v });
|
const set = (key: string) => (v: unknown) => onChange({ [key]: v });
|
||||||
|
|
||||||
@@ -166,10 +172,13 @@ export const ProductEditor: React.FC<ProductEditorProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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í */}
|
{/* Hlavička produktu – klik rozbalí / zbalí */}
|
||||||
<div className="flex items-center gap-2 px-2.5 py-2 bg-white">
|
<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">
|
<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 && (
|
{p.imageUrl && (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// 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 key = cardVisualKey(p, ctx);
|
||||||
const visual = ctx.mode === 'preview' ? renderVisualPreview(p, ctx, key) : renderVisualExport(p, ctx, key);
|
const visual = ctx.mode === 'preview' ? renderVisualPreview(p, ctx, key) : renderVisualExport(p, ctx, key);
|
||||||
return `
|
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>
|
<tr>
|
||||||
<td style="padding: 0; line-height: 0; font-size: 0; border-top-left-radius: 16px; border-top-right-radius: 16px;">
|
<td style="padding: 0; line-height: 0; font-size: 0; border-top-left-radius: 16px; border-top-right-radius: 16px;">
|
||||||
${visual}
|
${visual}
|
||||||
|
|||||||
Reference in new issue
Block a user