PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.7
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.7
2.0.13 2.0.12 2.0.11 2.0.10 2.0.9 trunk 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8
blockenberg / blocks / portfolio-grid / index.js

index.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.7, at blocks/portfolio-grid/index.js

406 lines 30.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 ( function () {
2 var el = wp.element.createElement;
3 var Fragment = wp.element.Fragment;
4 var useState = wp.element.useState;
5 var __ = wp.i18n.__;
6 var registerBlockType = wp.blocks.registerBlockType;
7 var InspectorControls = wp.blockEditor.InspectorControls;
8 var MediaUpload = wp.blockEditor.MediaUpload;
9 var MediaUploadCheck = wp.blockEditor.MediaUploadCheck;
10 var RichText = wp.blockEditor.RichText;
11 var useBlockProps = wp.blockEditor.useBlockProps;
12 var PanelBody = wp.components.PanelBody;
13 var Button = wp.components.Button;
14 var ToggleControl = wp.components.ToggleControl;
15 var RangeControl = wp.components.RangeControl;
16 var SelectControl = wp.components.SelectControl;
17 var TextControl = wp.components.TextControl;
18 var ColorPicker = wp.components.ColorPicker;
19 var Popover = wp.components.Popover;
20
21 var _tc; function getTypoControl() { return _tc || (_tc = window.bkbgTypographyControl); }
22 var _tv; function getTypoCssVars() { return _tv || (_tv = window.bkbgTypoCssVars); }
23
24 var HOVER_OPTIONS = [
25 { label: __('Overlay', 'blockenberg'), value: 'overlay' },
26 { label: __('Slide up', 'blockenberg'), value: 'slide-up' },
27 { label: __('Lift', 'blockenberg'), value: 'lift' },
28 ];
29 var RATIO_OPTIONS = [
30 { label: '3:2', value: '3/2' },
31 { label: '4:3', value: '4/3' },
32 { label: '1:1', value: '1/1' },
33 { label: '16:9', value: '16/9' },
34 ];
35 var SHADOW_OPTIONS = [
36 { label: __('None', 'blockenberg'), value: 'none' },
37 { label: __('Small', 'blockenberg'), value: 'sm' },
38 { label: __('Medium', 'blockenberg'), value: 'md' },
39 { label: __('Large', 'blockenberg'), value: 'lg' },
40 ];
41 var FILTER_OPTIONS = [
42 { label: __('Pills', 'blockenberg'), value: 'pills' },
43 { label: __('Tabs', 'blockenberg'), value: 'tabs' },
44 ];
45
46 function makeId() { return 'pw' + Math.random().toString(36).substr(2, 6); }
47
48 function buildWrapStyle(a) {
49 var shadow = a.cardShadow === 'sm' ? '0 1px 4px rgba(0,0,0,0.08)' :
50 a.cardShadow === 'md' ? '0 4px 16px rgba(0,0,0,0.1)' :
51 a.cardShadow === 'lg' ? '0 8px 32px rgba(0,0,0,0.14)' : 'none';
52 var s = {
53 '--bkbg-pg-cols': a.columns,
54 '--bkbg-pg-gap': a.gap + 'px',
55 '--bkbg-pg-radius': a.borderRadius + 'px',
56 '--bkbg-pg-shadow': shadow,
57 '--bkbg-pg-card-bg': a.cardBg,
58 '--bkbg-pg-title-color': a.titleColor,
59 '--bkbg-pg-desc-color': a.descColor,
60 '--bkbg-pg-tag-bg': a.tagBg,
61 '--bkbg-pg-tag-color': a.tagColor,
62 '--bkbg-pg-tag-size': a.tagSize + 'px',
63 '--bkbg-pg-accent': a.accentColor,
64 '--bkbg-pg-overlay-bg': a.overlayCoverBg,
65 '--bkbg-pg-overlay-text': a.overlayTextColor,
66 paddingTop: a.paddingTop + 'px',
67 paddingBottom: a.paddingBottom + 'px',
68 backgroundColor: a.bgColor || undefined
69 };
70 if (a.titleSize !== undefined) s['--bkbg-pg-title-size'] = a.titleSize + 'px';
71 if (a.descSize !== undefined) s['--bkbg-pg-desc-size'] = a.descSize + 'px';
72 return s;
73 }
74
75 function renderColorControl(key, label, value, onChange, openKey, setOpenKey) {
76 var isOpen = openKey === key;
77 return el('div', { key: key, style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', gap: '8px' } },
78 el('span', { style: { fontSize: '12px', color: '#1e1e1e', flex: 1, lineHeight: 1.4 } }, label),
79 el('div', { style: { position: 'relative', flexShrink: 0 } },
80 el('button', { type: 'button', title: value || 'default', onClick: function () { setOpenKey(isOpen ? null : key); }, style: { width: '28px', height: '28px', borderRadius: '4px', border: isOpen ? '2px solid #007cba' : '2px solid #ddd', cursor: 'pointer', padding: 0, background: value || '#cccccc' } }),
81 isOpen && el(Popover, { position: 'bottom left', onClose: function () { setOpenKey(null); } },
82 el('div', { style: { padding: '8px' } },
83 el(ColorPicker, { color: value, enableAlpha: true, onChange: onChange })
84 )
85 )
86 )
87 );
88 }
89
90 /* ── Get unique categories from items ─────────────────────────────────── */
91 function getCategories(items) {
92 var seen = {};
93 var cats = [];
94 (items || []).forEach(function (item) {
95 var c = (item.category || '').trim();
96 if (c && !seen[c]) { seen[c] = true; cats.push(c); }
97 });
98 return cats;
99 }
100
101 /* ── Single project card preview ──────────────────────────────────────── */
102 function ProjectCard(props) {
103 var item = props.item;
104 var a = props.a;
105 var techTags = (item.techTags || '').split(',').map(function (t) { return t.trim(); }).filter(Boolean);
106 var aspectParts = a.imageRatio.split('/');
107 var aspectPadding = ((parseInt(aspectParts[1], 10) / parseInt(aspectParts[0], 10)) * 100) + '%';
108
109 return el('div', { className: 'bkbg-pg-card bkbg-pg-hover-' + a.hoverStyle + (a.animate ? ' bkbg-pg-anim' : ''), style: { background: 'var(--bkbg-pg-card-bg)', borderRadius: 'var(--bkbg-pg-radius)', overflow: 'hidden', boxShadow: 'var(--bkbg-pg-shadow)', display: 'flex', flexDirection: 'column' } },
110 /* image */
111 el('div', { className: 'bkbg-pg-img-wrap', style: { position: 'relative', paddingBottom: aspectPadding, background: '#e5e7eb', overflow: 'hidden', flexShrink: 0 } },
112 item.imageUrl && el('img', { src: item.imageUrl, alt: item.title || '', style: { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' } }),
113 !item.imageUrl && el('div', { style: { position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#9ca3af', fontSize: '12px' } }, __('No image', 'blockenberg')),
114 /* overlay */
115 el('div', { className: 'bkbg-pg-overlay', style: { position: 'absolute', inset: 0, background: 'var(--bkbg-pg-overlay-bg)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', opacity: 0, transition: 'opacity 0.3s ease', padding: '20px' } },
116 a.showCta && el('span', { style: { color: 'var(--bkbg-pg-overlay-text)', fontWeight: 600, fontSize: a.ctaSize + 'px', border: '2px solid var(--bkbg-pg-overlay-text)', borderRadius: '8px', padding: '8px 20px' } }, item.linkLabel || __('View project', 'blockenberg'))
117 ),
118 item.category && el('span', { style: { position: 'absolute', top: '12px', left: '12px', background: 'var(--bkbg-pg-accent)', color: '#fff', fontSize: '11px', fontWeight: 700, borderRadius: '99px', padding: '3px 10px', letterSpacing: '0.04em' } }, item.category)
119 ),
120 /* body */
121 el('div', { className: 'bkbg-pg-body', style: { padding: '20px', flex: 1, display: 'flex', flexDirection: 'column', gap: '8px' } },
122 el(RichText, { tagName: 'h3', className: 'bkbg-pg-title', value: item.title, placeholder: __('Project title…', 'blockenberg'), onChange: function (v) { props.onUpdate('title', v); }, style: { color: 'var(--bkbg-pg-title-color)', margin: 0 } }),
123 a.showDescription && el(RichText, { tagName: 'p', className: 'bkbg-pg-desc', value: item.description, placeholder: __('Project description…', 'blockenberg'), onChange: function (v) { props.onUpdate('description', v); }, style: { color: 'var(--bkbg-pg-desc-color)', margin: 0 } }),
124 a.showTechTags && techTags.length > 0 && el('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '6px', marginTop: '4px' } },
125 techTags.map(function (tag, i) {
126 return el('span', { key: i, style: { background: 'var(--bkbg-pg-tag-bg)', color: 'var(--bkbg-pg-tag-color)', fontSize: 'var(--bkbg-pg-tag-size)', borderRadius: '4px', padding: '2px 8px', fontWeight: 500 } }, tag);
127 })
128 )
129 )
130 );
131 }
132
133 registerBlockType('blockenberg/portfolio-grid', {
134 title: __('Portfolio Grid', 'blockenberg'),
135 icon: 'portfolio',
136 category: 'bkbg-business',
137 description: __('Project cards with filter, hover overlay, and tech tags.', 'blockenberg'),
138
139 edit: function (props) {
140 var a = props.attributes;
141 var setAttributes = props.setAttributes;
142
143 var openColorKeyState = useState(null);
144 var openColorKey = openColorKeyState[0];
145 var setOpenColorKey = openColorKeyState[1];
146
147 var selectedItemState = useState(0);
148 var selectedItem = selectedItemState[0];
149 var setSelectedItem = selectedItemState[1];
150
151 var blockProps = useBlockProps((function () {
152 var _tv = getTypoCssVars();
153 var s = buildWrapStyle(a);
154 Object.assign(s, _tv(a.titleTypo, '--bkbg-pg-tt-'));
155 Object.assign(s, _tv(a.descTypo, '--bkbg-pg-ds-'));
156 return { style: s };
157 })());
158
159 function cc(key, label, attrKey) {
160 return renderColorControl(key, label, a[attrKey], function (val) {
161 var upd = {}; upd[attrKey] = val; setAttributes(upd);
162 }, openColorKey, setOpenColorKey);
163 }
164
165 function updateItem(idx, key, val) {
166 var items = a.items.map(function (it, i) {
167 if (i !== idx) return it;
168 var updated = Object.assign({}, it); updated[key] = val; return updated;
169 });
170 setAttributes({ items: items });
171 }
172
173 function addItem() {
174 setAttributes({ items: a.items.concat([{ id: makeId(), imageUrl: '', imageId: 0, title: 'New Project', category: 'Design', description: 'Project description goes here.', techTags: 'Tool A, Tool B', linkUrl: '#', linkLabel: 'View project', linkTarget: false, featured: false }]) });
175 }
176
177 function removeItem(idx) {
178 if (a.items.length <= 1) return;
179 setAttributes({ items: a.items.filter(function (_, i) { return i !== idx; }) });
180 }
181
182 var cats = getCategories(a.items);
183
184 return el(Fragment, null,
185 el(InspectorControls, null,
186 el(PanelBody, { title: __('Grid', 'blockenberg'), initialOpen: true },
187 el(RangeControl, { label: __('Columns', 'blockenberg'), value: a.columns, min: 1, max: 4, onChange: function (v) { setAttributes({ columns: v }); } }),
188 el(RangeControl, { label: __('Gap (px)', 'blockenberg'), value: a.gap, min: 8, max: 60, onChange: function (v) { setAttributes({ gap: v }); } }),
189 el(SelectControl, { label: __('Image ratio', 'blockenberg'), value: a.imageRatio, options: RATIO_OPTIONS, onChange: function (v) { setAttributes({ imageRatio: v }); } }),
190 el(RangeControl, { label: __('Border radius (px)', 'blockenberg'), value: a.borderRadius, min: 0, max: 40, onChange: function (v) { setAttributes({ borderRadius: v }); } }),
191 el(SelectControl, { label: __('Card shadow', 'blockenberg'), value: a.cardShadow, options: SHADOW_OPTIONS, onChange: function (v) { setAttributes({ cardShadow: v }); } }),
192 el(SelectControl, { label: __('Hover style', 'blockenberg'), value: a.hoverStyle, options: HOVER_OPTIONS, onChange: function (v) { setAttributes({ hoverStyle: v }); } }),
193 el(ToggleControl, { label: __('Animate on scroll', 'blockenberg'), checked: a.animate, onChange: function (v) { setAttributes({ animate: v }); }, __nextHasNoMarginBottom: true })
194 ),
195 el(PanelBody, { title: __('Filter Bar', 'blockenberg'), initialOpen: false },
196 el(ToggleControl, { label: __('Show filter bar', 'blockenberg'), checked: a.filterEnabled, onChange: function (v) { setAttributes({ filterEnabled: v }); }, __nextHasNoMarginBottom: true }),
197 a.filterEnabled && el(Fragment, null,
198 el(SelectControl, { label: __('Filter style', 'blockenberg'), value: a.filterStyle, options: FILTER_OPTIONS, onChange: function (v) { setAttributes({ filterStyle: v }); } }),
199 el(TextControl, { label: __('"All" label', 'blockenberg'), value: a.showAllLabel, onChange: function (v) { setAttributes({ showAllLabel: v }); } })
200 )
201 ),
202 el(PanelBody, { title: __('Display', 'blockenberg'), initialOpen: false },
203 el(ToggleControl, { label: __('Show description', 'blockenberg'), checked: a.showDescription, onChange: function (v) { setAttributes({ showDescription: v }); }, __nextHasNoMarginBottom: true }),
204 el(ToggleControl, { label: __('Show tech tags', 'blockenberg'), checked: a.showTechTags, onChange: function (v) { setAttributes({ showTechTags: v }); }, __nextHasNoMarginBottom: true }),
205 el(ToggleControl, { label: __('Show CTA link', 'blockenberg'), checked: a.showCta, onChange: function (v) { setAttributes({ showCta: v }); }, __nextHasNoMarginBottom: true })
206 ),
207 el(PanelBody, { title: __('Typography', 'blockenberg'), initialOpen: false },
208 (function () {
209 var TC = getTypoControl();
210 if (!TC) return el('p', null, 'Loading\u2026');
211 return el(Fragment, null,
212 el(TC, { label: 'Title Typography', value: a.titleTypo, onChange: function (v) { setAttributes({ titleTypo: v }); } }),
213 el(TC, { label: 'Description Typography', value: a.descTypo, onChange: function (v) { setAttributes({ descTypo: v }); } })
214 );
215 })(),
216 el(RangeControl, { label: __('Tag size (px)', 'blockenberg'), value: a.tagSize, min: 9, max: 16, onChange: function (v) { setAttributes({ tagSize: v }); } }),
217 el(RangeControl, { label: __('CTA size (px)', 'blockenberg'), value: a.ctaSize, min: 11, max: 20, onChange: function (v) { setAttributes({ ctaSize: v }); } })
218 ),
219 el(PanelBody, { title: __('Colors', 'blockenberg'), initialOpen: false },
220 cc('accent', __('Accent / filter active', 'blockenberg'), 'accentColor'),
221 cc('titleColor', __('Title', 'blockenberg'), 'titleColor'),
222 cc('descColor', __('Description', 'blockenberg'), 'descColor'),
223 cc('cardBg', __('Card background', 'blockenberg'), 'cardBg'),
224 cc('tagBg', __('Tag background', 'blockenberg'), 'tagBg'),
225 cc('tagColor', __('Tag text', 'blockenberg'), 'tagColor'),
226 cc('overlayBg', __('Hover overlay bg', 'blockenberg'), 'overlayCoverBg'),
227 cc('overlayText', __('Overlay text', 'blockenberg'), 'overlayTextColor'),
228 cc('bgColor', __('Section background', 'blockenberg'), 'bgColor')
229 ),
230 el(PanelBody, { title: __('Spacing', 'blockenberg'), initialOpen: false },
231 el(RangeControl, { label: __('Padding top (px)', 'blockenberg'), value: a.paddingTop, min: 0, max: 200, onChange: function (v) { setAttributes({ paddingTop: v }); } }),
232 el(RangeControl, { label: __('Padding bottom (px)', 'blockenberg'), value: a.paddingBottom, min: 0, max: 200, onChange: function (v) { setAttributes({ paddingBottom: v }); } })
233 ),
234 el(PanelBody, { title: __('Projects', 'blockenberg'), initialOpen: false },
235 a.items.map(function (item, idx) {
236 return el(PanelBody, { key: item.id || idx, title: item.title || (__('Project', 'blockenberg') + ' ' + (idx + 1)), initialOpen: selectedItem === idx },
237 el(MediaUploadCheck, null,
238 el(MediaUpload, {
239 onSelect: function (media) { setAttributes({ items: a.items.map(function (it, i) { return i !== idx ? it : Object.assign({}, it, { imageUrl: media.url, imageId: media.id }); }) }); },
240 allowedTypes: ['image'],
241 value: item.imageId,
242 render: function (p) {
243 return el('div', { style: { marginBottom: '12px' } },
244 item.imageUrl && el('img', { src: item.imageUrl, style: { width: '100%', height: '80px', objectFit: 'cover', borderRadius: '6px', marginBottom: '6px' } }),
245 el(Button, { onClick: p.open, variant: item.imageUrl ? 'secondary' : 'primary', size: 'compact' }, item.imageUrl ? __('Replace image', 'blockenberg') : __('Select image', 'blockenberg'))
246 );
247 }
248 })
249 ),
250 el(TextControl, { label: __('Title', 'blockenberg'), value: item.title, onChange: function (v) { updateItem(idx, 'title', v); } }),
251 el(TextControl, { label: __('Category', 'blockenberg'), value: item.category, onChange: function (v) { updateItem(idx, 'category', v); } }),
252 el(TextControl, { label: __('Description', 'blockenberg'), value: item.description, onChange: function (v) { updateItem(idx, 'description', v); } }),
253 el(TextControl, { label: __('Tech tags (comma-separated)', 'blockenberg'), value: item.techTags, onChange: function (v) { updateItem(idx, 'techTags', v); } }),
254 el(TextControl, { label: __('Link URL', 'blockenberg'), value: item.linkUrl, onChange: function (v) { updateItem(idx, 'linkUrl', v); } }),
255 el(TextControl, { label: __('Link label', 'blockenberg'), value: item.linkLabel, onChange: function (v) { updateItem(idx, 'linkLabel', v); } }),
256 el(ToggleControl, { label: __('Open in new tab', 'blockenberg'), checked: item.linkTarget, onChange: function (v) { updateItem(idx, 'linkTarget', v); }, __nextHasNoMarginBottom: true }),
257 a.items.length > 1 && el(Button, { onClick: function () { removeItem(idx); }, variant: 'tertiary', isDestructive: true, size: 'compact', style: { marginTop: '8px' } }, __('Remove project', 'blockenberg'))
258 );
259 }),
260 el(Button, { onClick: addItem, variant: 'primary', style: { marginTop: '8px' } }, __('+ Add Project', 'blockenberg'))
261 )
262 ),
263
264 el('div', blockProps,
265 /* filter bar preview */
266 a.filterEnabled && cats.length > 0 && el('div', { className: 'bkbg-pg-filters bkbg-pg-filter-' + a.filterStyle, style: { display: 'flex', flexWrap: 'wrap', gap: '8px', marginBottom: '32px' } },
267 el('button', { className: 'bkbg-pg-filter-btn bkbg-pg-filter-active', style: { padding: '7px 18px', borderRadius: '99px', border: 'none', background: a.accentColor, color: '#fff', fontWeight: 600, fontSize: '14px', cursor: 'pointer' } }, a.showAllLabel),
268 cats.map(function (cat) {
269 return el('button', { key: cat, className: 'bkbg-pg-filter-btn', style: { padding: '7px 18px', borderRadius: '99px', border: '2px solid #e5e7eb', background: 'transparent', fontWeight: 500, fontSize: '14px', cursor: 'pointer', color: a.titleColor } }, cat);
270 })
271 ),
272 /* grid */
273 el('div', { className: 'bkbg-pg-grid', style: { display: 'grid', gridTemplateColumns: 'repeat(' + a.columns + ', 1fr)', gap: a.gap + 'px' } },
274 a.items.map(function (item, idx) {
275 return el(ProjectCard, { key: item.id || idx, item: item, a: a, onUpdate: function (key, val) { updateItem(idx, key, val); } });
276 })
277 )
278 )
279 );
280 },
281
282 save: function (props) {
283 var a = props.attributes;
284 var cats = getCategories(a.items);
285
286 return el('div', wp.blockEditor.useBlockProps.save((function () {
287 var _tv = window.bkbgTypoCssVars;
288 var s = buildWrapStyle(a);
289 if (_tv) {
290 Object.assign(s, _tv(a.titleTypo, '--bkbg-pg-tt-'));
291 Object.assign(s, _tv(a.descTypo, '--bkbg-pg-ds-'));
292 }
293 return {
294 className: 'bkbg-pg-wrapper bkbg-pg-hover-' + a.hoverStyle + (a.filterEnabled ? ' bkbg-pg-filterable' : ''),
295 style: s
296 };
297 })()),
298 a.filterEnabled && cats.length > 0 && el('div', { className: 'bkbg-pg-filters bkbg-pg-filter-' + a.filterStyle },
299 el('button', { className: 'bkbg-pg-filter-btn bkbg-pg-filter-active', 'data-filter': 'all' }, a.showAllLabel),
300 cats.map(function (cat) {
301 return el('button', { key: cat, className: 'bkbg-pg-filter-btn', 'data-filter': cat }, cat);
302 })
303 ),
304 el('div', { className: 'bkbg-pg-grid' },
305 a.items.map(function (item, idx) {
306 var techTags = (item.techTags || '').split(',').map(function (t) { return t.trim(); }).filter(Boolean);
307 var aspectParts = a.imageRatio.split('/');
308 var aspectPadding = ((parseInt(aspectParts[1], 10) / parseInt(aspectParts[0], 10)) * 100) + '%';
309 return el('div', { key: item.id || idx, className: 'bkbg-pg-card' + (a.animate ? ' bkbg-pg-anim' : ''), 'data-category': item.category || '' },
310 el('div', { className: 'bkbg-pg-img-wrap', style: { position: 'relative', paddingBottom: aspectPadding } },
311 item.imageUrl && el('img', { src: item.imageUrl, alt: item.title || '', loading: 'lazy' }),
312 el('div', { className: 'bkbg-pg-overlay' },
313 a.showCta && el('a', { className: 'bkbg-pg-cta', href: item.linkUrl || '#', target: item.linkTarget ? '_blank' : undefined, rel: item.linkTarget ? 'noopener noreferrer' : undefined }, item.linkLabel || __('View project', 'blockenberg'))
314 ),
315 item.category && el('span', { className: 'bkbg-pg-cat-badge' }, item.category)
316 ),
317 el('div', { className: 'bkbg-pg-body' },
318 el(RichText.Content, { tagName: 'h3', className: 'bkbg-pg-title', value: item.title }),
319 a.showDescription && el(RichText.Content, { tagName: 'p', className: 'bkbg-pg-desc', value: item.description }),
320 a.showTechTags && techTags.length > 0 && el('div', { className: 'bkbg-pg-tags' },
321 techTags.map(function (tag, i) { return el('span', { key: i, className: 'bkbg-pg-tag' }, tag); })
322 )
323 )
324 );
325 })
326 )
327 );
328 },
329
330 deprecated: [{
331 attributes: {
332 "items": { "type": "array", "default": [{"id":"pg1","imageUrl":"","imageId":0,"title":"Brand Identity Design","category":"Design","description":"A complete visual identity system for a tech startup \u2014 logo, typography, color, and motion.","techTags":"Figma, Illustrator","linkUrl":"#","linkLabel":"View project","linkTarget":false,"featured":false},{"id":"pg2","imageUrl":"","imageId":0,"title":"E-commerce Platform","category":"Development","description":"Full-stack storefront with real-time inventory and Stripe checkout integration.","techTags":"React, Node.js, Stripe","linkUrl":"#","linkLabel":"View project","linkTarget":false,"featured":false},{"id":"pg3","imageUrl":"","imageId":0,"title":"Mobile Banking App","category":"Design","description":"UX/UI design for a neobank: onboarding flows, transaction history and dark mode.","techTags":"Figma, Principle","linkUrl":"#","linkLabel":"View project","linkTarget":false,"featured":true},{"id":"pg4","imageUrl":"","imageId":0,"title":"SaaS Dashboard","category":"Development","description":"Analytics dashboard with real-time charts, role-based access and CSV export.","techTags":"Vue, D3.js, Laravel","linkUrl":"#","linkLabel":"View project","linkTarget":false,"featured":false}], "items": { "type": "object" } },
333 "columns": { "type": "number", "default": 2 },
334 "columnsTablet": { "type": "number", "default": 2 },
335 "gap": { "type": "number", "default": 24 },
336 "filterEnabled": { "type": "boolean", "default": true },
337 "filterStyle": { "type": "string", "default": "pills" },
338 "showAllLabel": { "type": "string", "default": "All" },
339 "showDescription": { "type": "boolean", "default": true },
340 "showTechTags": { "type": "boolean", "default": true },
341 "showCta": { "type": "boolean", "default": true },
342 "hoverStyle": { "type": "string", "default": "overlay" },
343 "imageRatio": { "type": "string", "default": "3/2" },
344 "borderRadius": { "type": "number", "default": 14 },
345 "cardShadow": { "type": "string", "default": "md" },
346 "titleSize": { "type": "number", "default": 20 },
347 "descSize": { "type": "number", "default": 14 },
348 "tagSize": { "type": "number", "default": 11 },
349 "ctaSize": { "type": "number", "default": 14 },
350 "accentColor": { "type": "string", "default": "#6c3fb5" },
351 "titleColor": { "type": "string", "default": "#111827" },
352 "descColor": { "type": "string", "default": "#6b7280" },
353 "cardBg": { "type": "string", "default": "#ffffff" },
354 "tagBg": { "type": "string", "default": "#f3f4f6" },
355 "tagColor": { "type": "string", "default": "#374151" },
356 "overlayCoverBg": { "type": "string", "default": "rgba(17,24,39,0.82)" },
357 "overlayTextColor": { "type": "string", "default": "#ffffff" },
358 "animate": { "type": "boolean", "default": true },
359 "bgColor": { "type": "string", "default": "" },
360 "paddingTop": { "type": "number", "default": 40 },
361 "paddingBottom": { "type": "number", "default": 40 },
362 "titleFontWeight": { "type": "string", "default": "700" },
363 "descFontWeight": { "type": "string", "default": "400" }
364 },
365 save: function (props) {
366 var a = props.attributes;
367 var cats = getCategories(a.items);
368 return el('div', wp.blockEditor.useBlockProps.save({
369 className: 'bkbg-pg-wrapper bkbg-pg-hover-' + a.hoverStyle + (a.filterEnabled ? ' bkbg-pg-filterable' : ''),
370 style: buildWrapStyle(a)
371 }),
372 a.filterEnabled && cats.length > 0 && el('div', { className: 'bkbg-pg-filters bkbg-pg-filter-' + a.filterStyle },
373 el('button', { className: 'bkbg-pg-filter-btn bkbg-pg-filter-active', 'data-filter': 'all' }, a.showAllLabel),
374 cats.map(function (cat) {
375 return el('button', { key: cat, className: 'bkbg-pg-filter-btn', 'data-filter': cat }, cat);
376 })
377 ),
378 el('div', { className: 'bkbg-pg-grid' },
379 a.items.map(function (item, idx) {
380 var techTags = (item.techTags || '').split(',').map(function (t) { return t.trim(); }).filter(Boolean);
381 var aspectParts = a.imageRatio.split('/');
382 var aspectPadding = ((parseInt(aspectParts[1], 10) / parseInt(aspectParts[0], 10)) * 100) + '%';
383 return el('div', { key: item.id || idx, className: 'bkbg-pg-card' + (a.animate ? ' bkbg-pg-anim' : ''), 'data-category': item.category || '' },
384 el('div', { className: 'bkbg-pg-img-wrap', style: { position: 'relative', paddingBottom: aspectPadding } },
385 item.imageUrl && el('img', { src: item.imageUrl, alt: item.title || '', loading: 'lazy' }),
386 el('div', { className: 'bkbg-pg-overlay' },
387 a.showCta && el('a', { className: 'bkbg-pg-cta', href: item.linkUrl || '#', target: item.linkTarget ? '_blank' : undefined, rel: item.linkTarget ? 'noopener noreferrer' : undefined }, item.linkLabel || __('View project', 'blockenberg'))
388 ),
389 item.category && el('span', { className: 'bkbg-pg-cat-badge' }, item.category)
390 ),
391 el('div', { className: 'bkbg-pg-body' },
392 el(RichText.Content, { tagName: 'h3', className: 'bkbg-pg-title', value: item.title }),
393 a.showDescription && el(RichText.Content, { tagName: 'p', className: 'bkbg-pg-desc', value: item.description }),
394 a.showTechTags && techTags.length > 0 && el('div', { className: 'bkbg-pg-tags' },
395 techTags.map(function (tag, i) { return el('span', { key: i, className: 'bkbg-pg-tag' }, tag); })
396 )
397 )
398 );
399 })
400 )
401 );
402 }
403 }]
404 });
405 }() );
406