From 6edf7ab1f24b7338a3136c71ecb3dbf0328c4119 Mon Sep 17 00:00:00 2001 From: aphandersen Date: Mon, 13 Apr 2026 03:48:00 +0000 Subject: [PATCH] Upload files to "/" --- ansico-cv-blocks.php | 83 +++++++++++++++++++ block.js | 184 +++++++++++++++++++++++++++++++++++++++++++ editor.css | 19 +++++ frontend.js | 51 ++++++++++++ readme.txt | 6 ++ 5 files changed, 343 insertions(+) create mode 100644 ansico-cv-blocks.php create mode 100644 block.js create mode 100644 editor.css create mode 100644 frontend.js create mode 100644 readme.txt diff --git a/ansico-cv-blocks.php b/ansico-cv-blocks.php new file mode 100644 index 0000000..2de3b98 --- /dev/null +++ b/ansico-cv-blocks.php @@ -0,0 +1,83 @@ + 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']); + register_block_type('cv/position', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']); + register_block_type('cv/education', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']); + register_block_type('cv/profileheader', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']); + register_block_type('cv/contentblock', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']); + register_block_type('cv/container', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']); + register_block_type('cv/contactform', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor', 'view_script' => 'cv-v1-frontend']); + register_block_type('cv/toc', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor', 'view_script' => 'cv-v1-frontend']); + register_block_type('cv/profile', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']); + register_block_type('cv/topnav', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']); +} +add_action('init', 'cv_v1_register_blocks'); + +add_action('init', function() { + register_post_meta('page', 'ansico_cv_hide_title', array('show_in_rest' => true, 'single' => true, 'type' => 'boolean', 'default' => false)); + register_post_meta('page', 'ansico_cv_frame_design', array('show_in_rest' => true, 'single' => true, 'type' => 'boolean', 'default' => false)); +}); + +add_filter('body_class', function($classes) { + if (is_page()) { + $hide_title = get_post_meta(get_the_ID(), 'ansico_cv_hide_title', true); + $frame_design = get_post_meta(get_the_ID(), 'ansico_cv_frame_design', true); + if ($hide_title) $classes[] = 'ansico-cv-hide-title'; + if ($frame_design) $classes[] = 'ansico-cv-frame-design'; + } + return $classes; +}); + +add_filter('the_content', function($content) { + if (is_page() && in_the_loop() && is_main_query()) { + $hide_title = get_post_meta(get_the_ID(), 'ansico_cv_hide_title', true); + $frame_design = get_post_meta(get_the_ID(), 'ansico_cv_frame_design', true); + if ($frame_design) { + $title_html = ''; + if (!$hide_title) { + $title_html = '

' . get_the_title() . '

'; + } + $content = '
' . $title_html . '
' . $content . '
'; + } + } + return $content; +}); + +add_action('rest_api_init', function () { + register_rest_route('cv-blocks/v1', '/send-message', array('methods' => 'POST', 'callback' => 'cv_blocks_send_email_callback', 'permission_callback' => '__return_true')); +}); + +function cv_blocks_send_email_callback($request) { + $params = $request->get_params(); + $receiver = sanitize_email($params['receiver']); + $name = sanitize_text_field($params['name']); + $email = sanitize_email($params['email']); + $subject = sanitize_text_field($params['subject']); + $message = sanitize_textarea_field($params['message']); + $site_name = get_bloginfo('name'); + if (empty($receiver) || empty($name) || empty($email) || empty($message)) return new WP_Error('missing_data', 'Missing required fields', array('status' => 400)); + $headers = array('From: ' . $name . ' <' . $email . '>', 'Reply-To: ' . $name . ' <' . $email . '>', 'Content-Type: text/plain; charset=UTF-8'); + $email_subject = $subject ? "New Contact Message: " . $subject : "New Contact Message from " . $name; + $email_body = "You have received a new message from your CV Contact Form on website " . $site_name . ".\n\nName: " . $name . "\nEmail: " . $email . "\nSubject: " . $subject . "\n\nMessage:\n" . $message . "\n"; + $sent = wp_mail($receiver, $email_subject, $email_body, $headers); + if ($sent) return rest_ensure_response(array('success' => true)); + else return new WP_Error('mail_failed', 'Could not send email', array('status' => 500)); +} diff --git a/block.js b/block.js new file mode 100644 index 0000000..d348324 --- /dev/null +++ b/block.js @@ -0,0 +1,184 @@ +const el = wp.element.createElement; +const { registerBlockType } = wp.blocks; +const { RichText, InnerBlocks, MediaUpload, InspectorControls, URLInput } = wp.blockEditor; +const { Button, PanelBody, ToggleControl, TextControl, ButtonGroup } = wp.components; +const { useSelect, useDispatch } = wp.data; + +const moveItem = (list, fromIndex, toIndex) => { + const result = Array.from(list); + const [removed] = result.splice(fromIndex, 1); + result.splice(toIndex, 0, removed); + return result; +}; + +registerBlockType('cv/company', { + title: 'Company (CV)', icon: 'building', category: 'layout', + attributes: { name: { type: 'string', source: 'html', selector: '.cv-company-name' }, dates: { type: 'string', source: 'html', selector: '.cv-company-dates' } }, + edit: ({ attributes, setAttributes }) => el('div', { className: 'cv-company' }, el(RichText, { tagName: 'div', className: 'cv-company-name', placeholder: 'Company Name', value: attributes.name, onChange: val => setAttributes({ name: val }) }), el(RichText, { tagName: 'div', className: 'cv-company-dates', placeholder: 'Years', value: attributes.dates, onChange: val => setAttributes({ dates: val }) }), el(InnerBlocks, { allowedBlocks: ['cv/position'], template: [['cv/position']] })), + save: ({ attributes }) => el('div', { className: 'cv-company' }, el(RichText.Content, { tagName: 'div', className: 'cv-company-name', value: attributes.name }), el(RichText.Content, { tagName: 'div', className: 'cv-company-dates', value: attributes.dates }), el(InnerBlocks.Content, null)) +}); + +registerBlockType('cv/position', { + title: 'Position (CV)', icon: 'id', parent: ['cv/company'], category: 'layout', + attributes: { title: { type: 'string', source: 'html', selector: '.cv-position-title' }, department: { type: 'string', source: 'html', selector: '.cv-position-department' }, dates: { type: 'string', source: 'html', selector: '.cv-position-dates' } }, + edit: ({ attributes, setAttributes }) => el('div', { className: 'cv-position' }, el(RichText, { tagName: 'div', className: 'cv-position-title', placeholder: 'Job Title', value: attributes.title, onChange: val => setAttributes({ title: val }) }), el(RichText, { tagName: 'div', className: 'cv-position-department', placeholder: 'Department / Unit', value: attributes.department, onChange: val => setAttributes({ department: val }) }), el(RichText, { tagName: 'div', className: 'cv-position-dates', placeholder: 'Period', value: attributes.dates, onChange: val => setAttributes({ dates: val }) }), el(InnerBlocks, { allowedBlocks: ['core/list'], template: [['core/list']] })), + save: ({ attributes }) => el('div', { className: 'cv-position' }, el(RichText.Content, { tagName: 'div', className: 'cv-position-title', value: attributes.title }), el(RichText.Content, { tagName: 'div', className: 'cv-position-department', value: attributes.department }), el(RichText.Content, { tagName: 'div', className: 'cv-position-dates', value: attributes.dates }), el(InnerBlocks.Content, null)) +}); + +registerBlockType('cv/education', { + title: 'Education (CV)', icon: 'welcome-learn-more', category: 'layout', + attributes: { degree: { type: 'string', source: 'html', selector: '.cv-education-degree' }, institution: { type: 'string', source: 'html', selector: '.cv-education-institution' }, dates: { type: 'string', source: 'html', selector: '.cv-education-dates' } }, + edit: ({ attributes, setAttributes }) => el('div', { className: 'cv-education' }, el(RichText, { tagName: 'div', className: 'cv-education-degree', placeholder: 'Degree / Qualification', value: attributes.degree, onChange: val => setAttributes({ degree: val }) }), el(RichText, { tagName: 'div', className: 'cv-education-institution', placeholder: 'University or School', value: attributes.institution, onChange: val => setAttributes({ institution: val }) }), el(RichText, { tagName: 'div', className: 'cv-education-dates', placeholder: 'Years', value: attributes.dates, onChange: val => setAttributes({ dates: val }) })), + save: ({ attributes }) => el('div', { className: 'cv-education' }, el(RichText.Content, { tagName: 'div', className: 'cv-education-degree', value: attributes.degree }), el(RichText.Content, { tagName: 'div', className: 'cv-education-institution', value: attributes.institution }), el(RichText.Content, { tagName: 'div', className: 'cv-education-dates', value: attributes.dates })) +}); + +registerBlockType('cv/profileheader', { + title: 'Profile Header (CV)', icon: 'id-alt', category: 'layout', + supports: { align: ['wide', 'full'] }, + attributes: { headerImage: { type: 'string', default: '' }, profileImage: { type: 'string', default: '' }, name: { type: 'string', source: 'html', selector: '.cv-profileheader-name' }, title: { type: 'string', source: 'html', selector: '.cv-profileheader-title' }, company: { type: 'string', source: 'html', selector: '.cv-profileheader-company' }, showMenu: { type: 'boolean', default: false }, menuItems: { type: 'array', default: [] } }, + edit: ({ attributes, setAttributes }) => { + const updateMenuItem = (index, key, value) => { const newItems = [...attributes.menuItems]; newItems[index] = { ...newItems[index], [key]: value }; setAttributes({ menuItems: newItems }); }; + const addMenuItem = () => setAttributes({ menuItems: [...attributes.menuItems, { label: '', url: '' }] }); + const removeMenuItem = (index) => { const newItems = attributes.menuItems.filter((_, i) => i !== index); setAttributes({ menuItems: newItems }); }; + const moveMenuItemAt = (index, direction) => { const newPos = index + direction; if (newPos >= 0 && newPos < attributes.menuItems.length) setAttributes({ menuItems: moveItem(attributes.menuItems, index, newPos) }); }; + return [ + el(InspectorControls, {}, el(PanelBody, { title: 'Navigation Menu', initialOpen: true }, el(ToggleControl, { label: 'Enable Navigation Menu', checked: attributes.showMenu, onChange: (val) => setAttributes({ showMenu: val }) }))), + el('div', { className: 'cv-profileheader' }, + el('div', { className: 'cv-profileheader-cover', style: { backgroundImage: attributes.headerImage ? `url(${attributes.headerImage})` : 'none' } }, el(MediaUpload, { onSelect: media => setAttributes({ headerImage: media.url }), type: 'image', render: ({ open }) => el(Button, { onClick: open, className: 'button', style: { position: 'absolute', top: '10px', right: '10px', backgroundColor: 'rgba(255,255,255,0.9)' } }, attributes.headerImage ? 'Change Header' : 'Add Header Image') })), + el('div', { className: 'cv-profileheader-avatar-container' }, el(MediaUpload, { onSelect: media => setAttributes({ profileImage: media.url }), type: 'image', render: ({ open }) => el('div', { onClick: open, style: { cursor: 'pointer' }, title: 'Click to set profile picture' }, attributes.profileImage ? el('img', { src: attributes.profileImage, className: 'cv-profileheader-avatar' }) : el('div', { className: 'cv-profileheader-avatar placeholder' }, 'Add Photo')) })), + el('div', { className: 'cv-profileheader-info' }, el(RichText, { tagName: 'div', className: 'cv-profileheader-name', placeholder: 'Full Name', value: attributes.name, onChange: val => setAttributes({ name: val }) }), el(RichText, { tagName: 'div', className: 'cv-profileheader-title', placeholder: 'Current Job Title', value: attributes.title, onChange: val => setAttributes({ title: val }) }), el(RichText, { tagName: 'div', className: 'cv-profileheader-company', placeholder: 'Company or Organization', value: attributes.company, onChange: val => setAttributes({ company: val }) })), + attributes.showMenu && el('div', { className: 'cv-profileheader-nav-editor' }, attributes.menuItems.map((item, index) => el('div', { className: 'cv-menu-item-edit', key: index }, el('div', { className: 'cv-menu-item-fields' }, el(TextControl, { placeholder: 'Label', value: item.label, onChange: val => updateMenuItem(index, 'label', val) }), el(URLInput, { className: 'cv-url-input', value: item.url, onChange: val => updateMenuItem(index, 'url', val), disableSuggestions: false, isFullWidth: true, placeholder: 'Link or Search Page...' })), el(ButtonGroup, {}, el(Button, { icon: 'arrow-up-alt2', onClick: () => moveMenuItemAt(index, -1), disabled: index === 0 }), el(Button, { icon: 'arrow-down-alt2', onClick: () => moveMenuItemAt(index, 1), disabled: index === attributes.menuItems.length - 1 }), el(Button, { isDestructive: true, onClick: () => removeMenuItem(index) }, 'Remove')))), el(Button, { isPrimary: true, onClick: addMenuItem }, 'Add Menu Item')) + ) + ]; + }, + save: ({ attributes }) => el('div', { className: 'cv-profileheader' }, el('div', { className: 'cv-profileheader-cover', style: { backgroundImage: attributes.headerImage ? `url(${attributes.headerImage})` : 'none' } }), el('div', { className: 'cv-profileheader-avatar-container' }, attributes.profileImage ? el('img', { src: attributes.profileImage, className: 'cv-profileheader-avatar', alt: '' }) : null), el('div', { className: 'cv-profileheader-info' }, el(RichText.Content, { tagName: 'div', className: 'cv-profileheader-name', value: attributes.name }), el(RichText.Content, { tagName: 'div', className: 'cv-profileheader-title', value: attributes.title }), el(RichText.Content, { tagName: 'div', className: 'cv-profileheader-company', value: attributes.company })), attributes.showMenu && attributes.menuItems.length > 0 && el('nav', { className: 'cv-profileheader-nav' }, el('ul', {}, attributes.menuItems.map((item, index) => el('li', { key: index }, el('a', { href: item.url }, item.label)))))) +}); + +registerBlockType('cv/contentblock', { + title: 'Content Block (CV)', icon: 'layout', category: 'layout', + attributes: { heading: { type: 'string', source: 'html', selector: '.cv-contentblock-heading' }, includeInToc: { type: 'boolean', default: true } }, + edit: ({ attributes, setAttributes }) => { + return [ + el(InspectorControls, {}, el(PanelBody, { title: 'Table of Contents', initialOpen: true }, el(ToggleControl, { label: 'Include in Table of Contents', checked: attributes.includeInToc, onChange: (val) => setAttributes({ includeInToc: val }) }))), + el('div', { className: 'cv-contentblock' }, el(RichText, { tagName: 'h2', className: 'cv-contentblock-heading', placeholder: 'Section Heading', value: attributes.heading, onChange: val => setAttributes({ heading: val }) }), el('div', { className: 'cv-contentblock-inner' }, el(InnerBlocks, {}))) + ]; + }, + save: ({ attributes }) => el('div', { className: 'cv-contentblock', 'data-include-toc': attributes.includeInToc }, el(RichText.Content, { tagName: 'h2', className: 'cv-contentblock-heading', value: attributes.heading }), el('div', { className: 'cv-contentblock-inner' }, el(InnerBlocks.Content, null))) +}); + +registerBlockType('cv/container', { + title: 'Container (CV)', icon: 'editor-expand', category: 'layout', + edit: () => el('div', { className: 'cv-container' }, el('div', { className: 'cv-container-inner' }, el(InnerBlocks, {}))), + save: () => el('div', { className: 'cv-container' }, el('div', { className: 'cv-container-inner' }, el(InnerBlocks.Content, null))) +}); + +registerBlockType('cv/contactform', { + title: 'Contact Form (CV)', icon: 'email', category: 'layout', + attributes: { nameLabel: { type: 'string', default: 'Name' }, emailLabel: { type: 'string', default: 'Email' }, subjectLabel: { type: 'string', default: 'Subject' }, messageLabel: { type: 'string', default: 'Message' }, buttonLabel: { type: 'string', default: 'Send Message' }, receiverEmail: { type: 'string', default: '' }, successMessage: { type: 'string', default: 'The message has been sent!' }, enableControlField: { type: 'boolean', default: false }, controlQuestion: { type: 'string', default: 'What is 2+2?' }, controlAnswer: { type: 'string', default: '4' } }, + edit: ({ attributes, setAttributes }) => { + return [ + el(InspectorControls, {}, el(PanelBody, { title: 'Contact Form Settings', initialOpen: true }, el(TextControl, { label: 'Receiver Email Address', placeholder: 'your@email.com', value: attributes.receiverEmail, onChange: val => setAttributes({ receiverEmail: val }) }), el(TextControl, { label: 'Success Message', value: attributes.successMessage, onChange: val => setAttributes({ successMessage: val }) }), el(ToggleControl, { label: 'Enable Control Question (Anti-spam)', checked: attributes.enableControlField, onChange: val => setAttributes({ enableControlField: val }) }), attributes.enableControlField && el(TextControl, { label: 'Control Question', value: attributes.controlQuestion, onChange: val => setAttributes({ controlQuestion: val }) }), attributes.enableControlField && el(TextControl, { label: 'Expected Answer', value: attributes.controlAnswer, onChange: val => setAttributes({ controlAnswer: val }) }))), + el('div', { className: 'cv-contactform' }, el('div', { className: 'cv-form-group' }, el(RichText, { tagName: 'label', value: attributes.nameLabel, onChange: (val) => setAttributes({ nameLabel: val }) }), el('input', { type: 'text', disabled: true })), el('div', { className: 'cv-form-group' }, el(RichText, { tagName: 'label', value: attributes.emailLabel, onChange: (val) => setAttributes({ emailLabel: val }) }), el('input', { type: 'email', disabled: true })), el('div', { className: 'cv-form-group' }, el(RichText, { tagName: 'label', value: attributes.subjectLabel, onChange: (val) => setAttributes({ subjectLabel: val }) }), el('input', { type: 'text', disabled: true })), el('div', { className: 'cv-form-group' }, el(RichText, { tagName: 'label', value: attributes.messageLabel, onChange: (val) => setAttributes({ messageLabel: val }) }), el('textarea', { disabled: true, rows: 10 })), attributes.enableControlField && el('div', { className: 'cv-form-group' }, el('label', {}, attributes.controlQuestion), el('input', { type: 'text', disabled: true })), el('div', { className: 'cv-form-group' }, el(RichText, { tagName: 'button', className: 'cv-contactform-button', value: attributes.buttonLabel, onChange: (val) => setAttributes({ buttonLabel: val }) }))) + ]; + }, + save: ({ attributes }) => { + return el('div', { className: 'cv-contactform' }, el('form', { action: '#', method: 'post', 'data-receiver': attributes.receiverEmail, 'data-success': attributes.successMessage, 'data-control-answer': attributes.enableControlField ? attributes.controlAnswer : '' }, el('div', { className: 'cv-form-group' }, el('label', {}, attributes.nameLabel), el('input', { type: 'text', name: 'cv-name', required: true })), el('div', { className: 'cv-form-group' }, el('label', {}, attributes.emailLabel), el('input', { type: 'email', name: 'cv-email', required: true })), el('div', { className: 'cv-form-group' }, el('label', {}, attributes.subjectLabel), el('input', { type: 'text', name: 'cv-subject' })), el('div', { className: 'cv-form-group' }, el('label', {}, attributes.messageLabel), el('textarea', { name: 'cv-message', rows: 10, required: true })), attributes.enableControlField && el('div', { className: 'cv-form-group' }, el('label', {}, attributes.controlQuestion), el('input', { type: 'text', name: 'cv-control', required: true })), el('button', { type: 'submit', className: 'cv-contactform-button' }, attributes.buttonLabel)), el('div', { className: 'cv-contactform-feedback', style: { display: 'none' } })); + } +}); + +registerBlockType('cv/toc', { + title: 'Table of Contents (CV)', icon: 'list-view', category: 'layout', + attributes: { + heading: { type: 'string', source: 'html', selector: '.cv-toc-heading', default: 'Table of Content' } + }, + edit: ({ attributes, setAttributes }) => { + const headings = useSelect(select => { + const blocks = select('core/block-editor').getBlocks(); + const findHeadings = (blocksList) => { + let result = []; + blocksList.forEach(block => { + if (block.name === 'cv/contentblock' && block.attributes.includeInToc !== false && block.attributes.heading) result.push(block.attributes.heading.replace(/<[^>]+>/g, '')); + if (block.innerBlocks) result = result.concat(findHeadings(block.innerBlocks)); + }); + return result; + }; + return findHeadings(blocks); + }, []); + return el('div', { className: 'cv-toc cv-contentblock' }, + el(RichText, { tagName: 'h2', className: 'cv-toc-heading cv-contentblock-heading', value: attributes.heading, onChange: val => setAttributes({ heading: val }), placeholder: 'Table of Content' }), + el('div', { className: 'cv-toc-editor-preview' }, + headings.length > 0 ? el('ul', { className: 'cv-toc-list' }, headings.map((h, i) => el('li', { key: i }, el('a', { href: '#' }, h)))) : el('p', { style: { color: '#666', fontStyle: 'italic', margin: 0, fontSize: '0.9rem' } }, 'Add Content Blocks to see headings.') + ) + ); + }, + save: ({ attributes }) => el('div', { className: 'cv-toc cv-contentblock' }, + el(RichText.Content, { tagName: 'h2', className: 'cv-toc-heading cv-contentblock-heading', value: attributes.heading }) + ) +}); + +registerBlockType('cv/profile', { + title: 'Profile (CV)', icon: 'admin-users', category: 'layout', + attributes: { profileImage: { type: 'string', default: '' }, name: { type: 'string', source: 'html', selector: '.cv-profile-name' }, titles: { type: 'string', source: 'html', selector: '.cv-profile-titles' }, showSocial: { type: 'boolean', default: false } }, + edit: ({ attributes, setAttributes }) => { + return [ + el(InspectorControls, {}, el(PanelBody, { title: 'Profile Settings', initialOpen: true }, el(ToggleControl, { label: 'Enable Social Icons', checked: attributes.showSocial, onChange: (val) => setAttributes({ showSocial: val }) }))), + el('div', { className: 'cv-profile' }, + el('div', { className: 'cv-profile-avatar-container' }, el(MediaUpload, { onSelect: media => setAttributes({ profileImage: media.url }), type: 'image', render: ({ open }) => el('div', { onClick: open, style: { cursor: 'pointer' } }, attributes.profileImage ? el('img', { src: attributes.profileImage, className: 'cv-profile-avatar' }) : el('div', { className: 'cv-profile-avatar placeholder' }, 'Add Photo')) })), + el(RichText, { tagName: 'div', className: 'cv-profile-name', placeholder: 'Full Name', value: attributes.name, onChange: val => setAttributes({ name: val }) }), + attributes.showSocial && el('div', { className: 'cv-profile-social-container' }, el(InnerBlocks, { allowedBlocks: ['core/social-links'], template: [['core/social-links']] })), + el(RichText, { tagName: 'div', className: 'cv-profile-titles', placeholder: 'Titles (Press Enter for new line)', value: attributes.titles, onChange: val => setAttributes({ titles: val }) }) + ) + ]; + }, + save: ({ attributes }) => el('div', { className: 'cv-profile' }, el('div', { className: 'cv-profile-avatar-container' }, attributes.profileImage ? el('img', { src: attributes.profileImage, className: 'cv-profile-avatar', alt: '' }) : null), el(RichText.Content, { tagName: 'div', className: 'cv-profile-name', value: attributes.name }), attributes.showSocial && el('div', { className: 'cv-profile-social-container' }, el(InnerBlocks.Content, null)), el(RichText.Content, { tagName: 'div', className: 'cv-profile-titles', value: attributes.titles })) +}); + +registerBlockType('cv/topnav', { + title: 'Top Navigation (CV)', icon: 'menu', category: 'layout', + supports: { align: ['wide', 'full'] }, + attributes: { + siteName: { type: 'string', source: 'html', selector: '.cv-topnav-name', default: '' }, + menuItems: { type: 'array', default: [] } + }, + edit: ({ attributes, setAttributes }) => { + const updateMenuItem = (index, key, value) => { const newItems = [...attributes.menuItems]; newItems[index] = { ...newItems[index], [key]: value }; setAttributes({ menuItems: newItems }); }; + const addMenuItem = () => setAttributes({ menuItems: [...attributes.menuItems, { label: '', url: '' }] }); + const removeMenuItem = (index) => { const newItems = attributes.menuItems.filter((_, i) => i !== index); setAttributes({ menuItems: newItems }); }; + const moveMenuItemAt = (index, direction) => { const newPos = index + direction; if (newPos >= 0 && newPos < attributes.menuItems.length) setAttributes({ menuItems: moveItem(attributes.menuItems, index, newPos) }); }; + const siteTitle = useSelect(select => select('core').getSite()?.title); + wp.element.useEffect(() => { if (!attributes.siteName && siteTitle) setAttributes({ siteName: siteTitle }); }, [siteTitle]); + + return el('div', { className: 'cv-topnav' }, + el('div', { className: 'cv-topnav-link-wrapper' }, + el(RichText, { tagName: 'h1', className: 'cv-topnav-name', placeholder: 'Site Name', value: attributes.siteName, onChange: val => setAttributes({ siteName: val }) }) + ), + el('div', { className: 'cv-topnav-menu-editor' }, + attributes.menuItems.map((item, index) => el('div', { className: 'cv-menu-item-edit', key: index }, el('div', { className: 'cv-menu-item-fields' }, el(TextControl, { placeholder: 'Label', value: item.label, onChange: val => updateMenuItem(index, 'label', val) }), el(URLInput, { className: 'cv-url-input', value: item.url, onChange: val => updateMenuItem(index, 'url', val), disableSuggestions: false, isFullWidth: true, placeholder: 'Link or Search Page...' })), el(ButtonGroup, {}, el(Button, { icon: 'arrow-up-alt2', onClick: () => moveMenuItemAt(index, -1), disabled: index === 0 }), el(Button, { icon: 'arrow-down-alt2', onClick: () => moveMenuItemAt(index, 1), disabled: index === attributes.menuItems.length - 1 }), el(Button, { isDestructive: true, onClick: () => removeMenuItem(index) }, 'Remove')))), + el(Button, { isPrimary: true, onClick: addMenuItem, style: {marginTop: '10px'} }, 'Add Menu Link') + ), + el('div', { className: 'cv-topnav-line' }) + ); + }, + save: ({ attributes }) => el('header', { className: 'cv-topnav' }, + el('a', { href: '/', className: 'cv-topnav-home-link' }, el(RichText.Content, { tagName: 'h1', className: 'cv-topnav-name', value: attributes.siteName })), + attributes.menuItems.length > 0 && el('nav', { className: 'cv-topnav-nav' }, el('ul', {}, attributes.menuItems.map((item, index) => el('li', { key: index }, el('a', { href: item.url }, item.label))))), + el('div', { className: 'cv-topnav-line' }) + ) +}); + +if (wp.plugins && wp.editPost) { + const { registerPlugin } = wp.plugins; + const { PluginDocumentSettingPanel } = wp.editPost; + const PageSettingsPanel = () => { + const postType = useSelect(select => select('core/editor').getCurrentPostType(), []); + if (postType !== 'page') return null; + const meta = useSelect(select => select('core/editor').getEditedPostAttribute('meta'), []); + const { editPost } = useDispatch('core/editor'); + if (!meta) return null; + return el(PluginDocumentSettingPanel, { name: 'ansico-cv-page-settings', title: 'CV Page Settings', icon: 'layout' }, + el(ToggleControl, { label: 'Hide Page Title (H1)', checked: meta.ansico_cv_hide_title || false, onChange: (val) => editPost({ meta: { ...meta, ansico_cv_hide_title: val } }) }), + el(ToggleControl, { label: 'Enable Frame Design', help: 'Wraps the page content in a CV-styled box.', checked: meta.ansico_cv_frame_design || false, onChange: (val) => editPost({ meta: { ...meta, ansico_cv_frame_design: val } }) }) + ); + }; + registerPlugin('ansico-cv-page-settings-plugin', { render: PageSettingsPanel }); +} diff --git a/editor.css b/editor.css new file mode 100644 index 0000000..15cdf4c --- /dev/null +++ b/editor.css @@ -0,0 +1,19 @@ +.cv-company { border-left: 4px solid #2271b1; padding-left: 10px; } +.cv-position { border-left: 2px solid #ddd; padding-left: 10px; } +.cv-education { border-left: 4px solid #46b450; padding-left: 10px; margin-bottom: 1rem; } +.cv-profileheader { border-left: 4px solid #ffba00; border-radius: 0; } +.cv-contentblock, .cv-toc { border-left: 4px solid #9b51e0; border-radius: 0; } +.cv-container { border-left: 4px solid #36c3d9; border-radius: 0; } +.cv-contactform { border-left: 4px solid #ff5722; padding-left: 15px; } +.cv-profile { border-left: 4px solid #f39c12; border-radius: 0; text-align: center; } +.cv-topnav { border-left: 4px solid #ff004f; padding-left: 15px; margin-bottom: 2rem; } +.cv-profile-avatar-container { display: flex; justify-content: center; } +.cv-profile-social-container { min-height: 20px; background: #fafafa; border: 1px dashed #ddd; } +.cv-profileheader-nav-editor, .cv-topnav-menu-editor { padding: 15px 24px; background: #f9f9f9; border-top: 1px solid #eee; } +.cv-menu-item-edit { display: flex; gap: 10px; margin-bottom: 15px; align-items: flex-end; border-bottom: 1px dashed #ccc; padding-bottom: 15px; } +.cv-menu-item-fields { flex: 1; } +.cv-url-input { border: 1px solid #ccc; padding: 2px; background: #fff; margin-top: 5px; width: 100%; } +.cv-contactform input:disabled, .cv-contactform textarea:disabled { background: #f5f5f5; cursor: not-allowed; } +.cv-toc { display: block !important; } +.cv-toc-editor-preview { padding: 20px 0 0 0; } +.cv-toc-editor-preview .cv-toc-list a { pointer-events: none; } diff --git a/frontend.js b/frontend.js new file mode 100644 index 0000000..ba23c15 --- /dev/null +++ b/frontend.js @@ -0,0 +1,51 @@ +document.addEventListener('DOMContentLoaded', function() { + const forms = document.querySelectorAll('.cv-contactform form'); + forms.forEach(form => { + form.addEventListener('submit', function(e) { + e.preventDefault(); + const btn = form.querySelector('button[type="submit"]'); + const feedback = form.parentElement.querySelector('.cv-contactform-feedback'); + const originalBtnText = btn.innerText; + const receiver = form.getAttribute('data-receiver'); + if (!receiver) { alert('Receiver email missing.'); return; } + const expectedAnswer = form.getAttribute('data-control-answer'); + if (expectedAnswer && form.querySelector('[name="cv-control"]').value.trim().toLowerCase() !== expectedAnswer.trim().toLowerCase()) { alert('Incorrect answer.'); return; } + btn.innerText = 'Sending...'; btn.disabled = true; + const data = { receiver: receiver, name: form.querySelector('[name="cv-name"]').value, email: form.querySelector('[name="cv-email"]').value, subject: form.querySelector('[name="cv-subject"]').value, message: form.querySelector('[name="cv-message"]').value }; + fetch('/wp-json/cv-blocks/v1/send-message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then(res => res.json()).then(res => { + if (res.success) { form.style.display = 'none'; feedback.innerHTML = '
' + (form.getAttribute('data-success') || 'Sent!') + '
'; feedback.style.display = 'block'; } + else { alert('Error.'); btn.innerText = originalBtnText; btn.disabled = false; } + }); + }); + }); + const tocBlocks = document.querySelectorAll('.cv-toc'); + if (tocBlocks.length > 0) { + const contentBlocks = document.querySelectorAll('.cv-contentblock'); + tocBlocks.forEach(container => { + let hasItems = false; + const ul = document.createElement('ul'); + ul.className = 'cv-toc-list'; + contentBlocks.forEach((b, i) => { + if (b.getAttribute('data-include-toc') === 'false') return; + const h = b.querySelector('.cv-contentblock-heading'); + if (!h || !h.textContent.trim()) return; + if (b.classList.contains('cv-toc')) return; + if (!b.id) b.id = 'cv-sec-' + i; + const li = document.createElement('li'); + const a = document.createElement('a'); + a.href = '#' + b.id; + a.textContent = h.textContent; + a.onclick = (e) => { e.preventDefault(); document.getElementById(b.id).scrollIntoView({ behavior: 'smooth' }); }; + li.appendChild(a); + ul.appendChild(li); + hasItems = true; + }); + if (hasItems) { + container.appendChild(ul); + container.style.display = 'block'; + } else { + container.style.display = 'none'; + } + }); + } +}); diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..6e82481 --- /dev/null +++ b/readme.txt @@ -0,0 +1,6 @@ +=== Ansico CV Blocks === +Stable tag: 1.0.2 +== Changelog == += 1.0.2 = +* Top Nav: Changed hover color to LinkedIn blue (#0A66C2) with white text. +* Added supports align to Profile Header so it can be used natively in Site Editor FSE parts.