Upload files to "/"
This commit is contained in:
parent
06334c826d
commit
6edf7ab1f2
5 changed files with 343 additions and 0 deletions
83
ansico-cv-blocks.php
Normal file
83
ansico-cv-blocks.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
/**
|
||||
* Plugin Name: Ansico CV Blocks
|
||||
* Description: Adds Gutenberg CV blocks, Contact Form, TOC, Profile, and Top Navigation Block.
|
||||
* Version: 1.0.2
|
||||
* Author: Andreas Andersen (Ansico)
|
||||
* Author URI: https://ansico.dk/Ansico
|
||||
* Plugin URI: https://ansico.dk/Ansico/CV-Blocks
|
||||
* License: GPL3 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
function cv_v1_register_blocks() {
|
||||
wp_register_script('cv-v1-blocks', plugins_url('block.js', __FILE__), array('wp-blocks','wp-element','wp-block-editor','wp-components', 'wp-data', 'wp-plugins', 'wp-edit-post'), filemtime(plugin_dir_path(__FILE__) . 'block.js'));
|
||||
wp_register_style('cv-v1-style', plugins_url('style.css', __FILE__), array(), filemtime(plugin_dir_path(__FILE__) . 'style.css'));
|
||||
wp_register_style('cv-v1-editor', plugins_url('editor.css', __FILE__), array('wp-edit-blocks'), filemtime(plugin_dir_path(__FILE__) . 'editor.css'));
|
||||
wp_register_script('cv-v1-frontend', plugins_url('frontend.js', __FILE__), array(), filemtime(plugin_dir_path(__FILE__) . 'frontend.js'), true);
|
||||
|
||||
register_block_type('cv/company', ['editor_script' => '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 = '<h1 class="cv-page-framed-title">' . get_the_title() . '</h1>';
|
||||
}
|
||||
$content = '<div class="cv-page-frame-container">' . $title_html . '<div class="cv-page-frame-inner">' . $content . '</div></div>';
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
184
block.js
Normal file
184
block.js
Normal file
|
|
@ -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 });
|
||||
}
|
||||
19
editor.css
Normal file
19
editor.css
Normal file
|
|
@ -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; }
|
||||
51
frontend.js
Normal file
51
frontend.js
Normal file
|
|
@ -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 = '<div class="cv-success-msg">' + (form.getAttribute('data-success') || 'Sent!') + '</div>'; 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';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
6
readme.txt
Normal file
6
readme.txt
Normal file
|
|
@ -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.
|
||||
Loading…
Reference in a new issue