diff --git a/pages/devices.php b/pages/devices.php index 1883b07..5969ff1 100644 --- a/pages/devices.php +++ b/pages/devices.php @@ -36,10 +36,13 @@ return <<<'ORIBI_SYNC_CONTENT' - - - - + + + + + + + diff --git a/theme/assets/css/main.css b/theme/assets/css/main.css index ec953d7..ebeb15f 100644 --- a/theme/assets/css/main.css +++ b/theme/assets/css/main.css @@ -1656,7 +1656,80 @@ p:last-child { margin-bottom: 0; } font-size: .9rem; } -/* ── 8d. Image Card ────────────────────────────────────────── */ +/* ── 8d. Device Card ───────────────────────────────────────── */ +.device-grid { + --cols: 2; +} +.oribi-card.device-card { + display: flex; + flex-direction: column; + border-top: 3px solid var(--color-primary); + background: var(--card-bg); + padding: 0; + overflow: hidden; +} +.device-card img, +.device-card .card-image { + width: 100%; + aspect-ratio: 4 / 3; + object-fit: cover; + display: block; +} +.device-card-body { + display: flex; + flex-direction: column; + flex: 1; + padding: 1.5rem; +} +.device-best-for { + display: inline-block; + font-size: var(--wp--preset--font-size--xs, .75rem); + background: var(--color-primary-lt); + color: var(--color-primary); + padding: .25rem .75rem; + border-radius: 999px; + margin-bottom: .75rem; + font-weight: 600; +} +.device-card h3 { + color: var(--color-heading); + margin-bottom: .5rem; +} +.device-card p { + font-size: .9rem; + margin-bottom: 1rem; +} +.device-specs { + display: grid; + grid-template-columns: 1fr 1fr; + gap: .25rem .75rem; + font-size: .85rem; + margin: 0 0 1.25rem; + padding: 0; +} +.device-specs dt { + color: var(--color-muted, #6b7280); + font-weight: 500; +} +.device-specs dd { + margin: 0; + font-weight: 600; + color: var(--color-heading); +} +.device-price { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-primary); + margin-bottom: 1.25rem; +} +.device-card-btn { + display: block; + width: 100%; + text-align: center; + margin-top: auto; +} + +/* ── 8e. Image Card ────────────────────────────────────────── */ .image-card { padding: 0; overflow: hidden; diff --git a/theme/blocks/editor.js b/theme/blocks/editor.js index c49edb7..5b4c309 100644 --- a/theme/blocks/editor.js +++ b/theme/blocks/editor.js @@ -1007,6 +1007,86 @@ save: function () { return null; } }); + /* ── Device Card ──────────────────────────────────────────────────────── */ + reg('oribi/device-card', { + title: 'Device Card', + icon: 'laptop', + category: 'oribi', + parent: ['oribi/device-section'], + supports: { html: false, reusable: false }, + attributes: Object.assign({}, { + title: { type: 'string', default: '' }, + description: { type: 'string', default: '' }, + price: { type: 'string', default: '' }, + bestFor: { type: 'string', default: '' }, + specs: { type: 'array', default: [], items: { type: 'object' } }, + btnText: { type: 'string', default: 'Order Now' }, + btnUrl: { type: 'string', default: '/contact' }, + }, CARD_IMAGE_ATTRS), + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + var imgPrev = cardImagePreview(a); + + function updateSpec(i, field, val) { + var next = (a.specs || []).slice(); + next[i] = Object.assign({}, next[i], {}); + next[i][field] = val; + s({ specs: next }); + } + function addSpec() { + s({ specs: (a.specs || []).concat([{ key: '', value: '' }]) }); + } + function removeSpec(i) { + var next = (a.specs || []).slice(); + next.splice(i, 1); + s({ specs: next }); + } + + var specsRows = (a.specs || []).map(function (sp, i) { + return el('div', { key: i, style: { display: 'flex', gap: '8px', marginBottom: '6px' } }, + el(TC, { label: 'Key', value: sp.key || '', onChange: function (v) { updateSpec(i, 'key', v); }, style: { flex: 1 } }), + el(TC, { label: 'Value', value: sp.value || '', onChange: function (v) { updateSpec(i, 'value', v); }, style: { flex: 1 } }), + el('button', { onClick: function () { removeSpec(i); }, style: { alignSelf: 'flex-end', marginBottom: '8px' }, className: 'button is-small is-destructive' }, '✕') + ); + }); + + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Card Content' }, + el(TC, { label: 'Best For (pill)', value: a.bestFor || '', onChange: function (v) { s({ bestFor: v }); } }), + el(TC, { label: 'Price', value: a.price || '', onChange: function (v) { s({ price: v }); } }), + el(TC, { label: 'Button Text', value: a.btnText || '', onChange: function (v) { s({ btnText: v }); } }), + el(TC, { label: 'Button URL', value: a.btnUrl || '', onChange: function (v) { s({ btnUrl: v }); } }) + ), + el(PB, { title: 'Specs' }, + specsRows, + el('button', { onClick: addSpec, className: 'button is-secondary', style: { marginTop: '4px' } }, '+ Add Spec') + ), + cardImageControls(a, s) + ), + el('div', { className: 'oribi-card device-card' }, + imgPrev, + el('div', { className: 'device-card-body' }, + a.bestFor ? el('span', { className: 'device-best-for' }, a.bestFor) : null, + el(RT, { tagName: 'h3', value: a.title, onChange: function (v) { s({ title: v }); }, placeholder: 'Device name...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function (v) { s({ description: v }); }, placeholder: 'Short description...' }), + (a.specs || []).length ? el('dl', { className: 'device-specs' }, + (a.specs || []).map(function (sp, i) { + return [ + el('dt', { key: 'k' + i }, sp.key || ''), + el('dd', { key: 'v' + i }, sp.value || '') + ]; + }) + ) : null, + a.price ? el('div', { className: 'device-price' }, a.price) : null, + el('span', { className: 'wp-block-button__link device-card-btn' }, a.btnText || 'Order Now') + ) + ) + ); + }, + save: function () { return null; } + }); + /* ── Image Card ───────────────────────────────────────────────────────── */ reg('oribi/image-card', { title: 'Image Card', @@ -1586,6 +1666,17 @@ save: function () { return el(IB.Content); } }); + /* DEVICE SECTION ───────────────────────────────────────────────────────── */ + reg('oribi/device-section', { + title: 'Oribi Device Section', + icon: 'laptop', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: SECTION_ATTRS, + edit: createCardSectionEdit(['oribi/device-card'], [['oribi/device-card', {}]], 'Device Card'), + save: function () { return el(IB.Content); } + }); + /* IMAGE SECTION ────────────────────────────────────────────────────────── */ reg('oribi/image-section', { title: 'Oribi Image Section', diff --git a/theme/blocks/index.php b/theme/blocks/index.php index c806113..56ad765 100644 --- a/theme/blocks/index.php +++ b/theme/blocks/index.php @@ -455,6 +455,31 @@ add_action('init', function () { 'render_callback' => 'oribi_render_addon_card', ]); + /* Device Section (parent) */ + register_block_type('oribi/device-section', [ + 'attributes' => oribi_card_section_attributes(2), + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_device_section', + ]); + + /* Device Card (child) */ + register_block_type('oribi/device-card', [ + 'attributes' => array_merge( + [ + 'title' => ['type' => 'string', 'default' => ''], + 'description' => ['type' => 'string', 'default' => ''], + 'price' => ['type' => 'string', 'default' => ''], + 'bestFor' => ['type' => 'string', 'default' => ''], + 'specs' => ['type' => 'array', 'default' => [], 'items' => ['type' => 'object']], + 'btnText' => ['type' => 'string', 'default' => ''], + 'btnUrl' => ['type' => 'string', 'default' => ''], + ], + oribi_card_image_attributes() + ), + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_device_card', + ]); + /* Image Section (parent) */ register_block_type('oribi/image-section', [ 'attributes' => oribi_card_section_attributes(3), @@ -1655,6 +1680,56 @@ function oribi_render_addon_card($a) return ob_get_clean(); } +/* ── Device Section ────────────────────────────────────────────────────────── */ +function oribi_render_device_section($a, $content) +{ + return oribi_render_card_section($a, $content, 'grid device-grid', 2); +} + +/* ── Device Card ───────────────────────────────────────────────────────────── */ +function oribi_render_device_card($a) +{ + $img = oribi_card_image_html($a); + $img_cls = $img['card_class'] ? ' ' . $img['card_class'] : ''; + + $specs = !empty($a['specs']) && is_array($a['specs']) ? $a['specs'] : []; + $price = !empty($a['price']) ? $a['price'] : ''; + $bestFor = !empty($a['bestFor']) ? $a['bestFor'] : ''; + $btnText = !empty($a['btnText']) ? $a['btnText'] : 'Order Now'; + $btnUrl = !empty($a['btnUrl']) ? $a['btnUrl'] : '/contact'; + + ob_start(); ?> +
+