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.