Compare commits
No commits in common. "main" and "v1.0.0" have entirely different histories.
13 changed files with 207 additions and 473 deletions
29
README.md
29
README.md
|
|
@ -1,35 +1,18 @@
|
|||
# Ansico CV-Blocks
|
||||
# CV-Experience-Blocks
|
||||
|
||||
Adds more Gutenberg CV blocks that let you show your Curriculum Vitae (CV) on a WordPress site. See demo of plugin on https://www.aandersen.eu/curriculum-vitae/.
|
||||
Adds Gutenberg CV Experience blocks (Company + Positions). See demo of plugin on https://www.aandersen.eu/curriculum-vitae/.
|
||||
|
||||
## Features
|
||||
|
||||
Adds the following Gutenberg Block called "Company (CV)" where you can enter a company and employeed from and to. You can add a child block called "Position (CV)" for each position in that company. For each position you can enter department, employeed from and to and a list of skills you have achieved. The style is LinkedIn-liked style.
|
||||
Adds a new Gutenberg Block called "Company (CV)" where you can enter a company and employeed from and to. You can add a child block called "Position (CV)" for each position in that company. For each position you can enter department, employeed from and to and a list of skills you have achieved. The style is LinkedIn-liked style.
|
||||
|
||||
- Company (CV) Block - Enter company and the positions you have had in this company.
|
||||
- Education (CV) Block - Enter educations for your CV.
|
||||
- Profile Header (CV) Block - LinkedIn-like profile header with profile photo, cover photo, name, title, company or location and navigation menu.
|
||||
- Content (CV) Block - Content block with a header and frame around.
|
||||
- Container (CV) Block - Content block with frame around, but without header.
|
||||
- Table of Contents (CV) Block - TOC block that lists and links to headers of Content (CV) Blocks.
|
||||
- Contact Form (CV) Block - Simple contact form that lets your visitors contact you.
|
||||
- Top Navigation (CV) Block - Top navigation of website with website title (your name).
|
||||
- Profile (CV) Block - Profile block for a sidebar with photo and titles.
|
||||
- Hide titles of single pages (setting on pages).
|
||||
- Enable frame design on whole content area on pages (setting on pages).
|
||||
## How to use
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
1. Upload the plugin folder to /wp-content/plugins/ or using Plugins -> Add Plugin -> Upload Plugin.
|
||||
2. Activate the plugin through the WordPress admin.
|
||||
3. Add "Company (CV)" block in the block editor.
|
||||
Go to your WordPress site and upload the cv-experience-blocks.zip under Plugins -> Add Plugin -> Upload Plugin. Activate it and go to a post or page and add the new Gutenberg plugin Company (CV).
|
||||
|
||||
## Version
|
||||
|
||||
- Version 1.0.0 (12/4 2026): Initial release of CV Experience Blocks.
|
||||
- Version 1.0.1 (12/4 2026): Adjusted to WordPress Plugin Directory.
|
||||
- Version 1.1.0 (13/4 2026): Changed name from Ansico CV Experience Blocks to Ansico CV Blocks. Added Education (CV) block, Profile header (CV) block, Content (CV) block, Container (CV) block, TOC (CV) block, Contact Form (CV) block, Top Navigation (CV) block, Profile (CV) block. Adds page settings to hide header and add frame design.
|
||||
- Version 1.0 (12/4 2026): First version published.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
BIN
Screenshot.png
BIN
Screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 113 KiB |
Binary file not shown.
|
|
@ -1,83 +0,0 @@
|
|||
<?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));
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
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 });
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
.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; }
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
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';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
=== Ansico CV Blocks ===
|
||||
Contributors: Ansico
|
||||
Donate link: https://ansico.dk/ansico
|
||||
Tags: cv, resume, gutenberg, blocks
|
||||
Requires at least: 6.0
|
||||
Tested up to: 6.9
|
||||
Requires PHP: 7.4
|
||||
Stable tag: 1.1.0
|
||||
License: GPLv3 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
CV Blocks adds more Gutenberg blocks for building professional CVs with companies and positions, education, profile etc.
|
||||
|
||||
== Description ==
|
||||
CV Blocks adds more Gutenberg blocks for building professional CVs with companies and positions, education, profile etc.
|
||||
|
||||
== Installation ==
|
||||
1. Upload the plugin folder to /wp-content/plugins/ or install using Plugins -> Add Plugin -> Upload Plugin.
|
||||
2. Activate the plugin through the WordPress admin.
|
||||
3. Add "Company (CV)" block in the block editor.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 1.0.0 =
|
||||
* Initial release of CV Experience Blocks
|
||||
|
||||
= 1.0.1 =
|
||||
* Adjusted to WordPress Plugin Directory
|
||||
|
||||
= 1.1.0 =
|
||||
* Lots of new blocks.
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
.cv-company { margin-bottom: 1.25rem; }
|
||||
.cv-company-name { font-size: 1.25rem; font-weight: 700; }
|
||||
.cv-company-dates { color: #666; margin-bottom: 0.5rem; }
|
||||
.cv-position-dates { color: #777; margin-bottom: 4px; }
|
||||
.cv-position .wp-block-list { margin-top: 4px; margin-bottom: 0; }
|
||||
.cv-position p { margin-bottom: 4px; }
|
||||
.cv-position { margin-left: 24px; margin-bottom: 0.5rem; }
|
||||
.cv-position-title { font-weight: 700; }
|
||||
.cv-position-department { font-style: italic; }
|
||||
.cv-education { margin-bottom: 1.25rem; }
|
||||
.cv-education-degree { font-size: 1.1rem; font-weight: 700; }
|
||||
.cv-education-institution { font-style: italic; margin-bottom: 2px; }
|
||||
.cv-education-dates { color: #666; margin-bottom: 0.25rem; }
|
||||
|
||||
.cv-profileheader, .cv-contentblock, .cv-container, .cv-profile { border: 1px solid #ddd; border-radius: 8px; background-color: #fff; margin-bottom: 2rem; box-shadow: 0 0 0 1px rgba(0,0,0,0.05); }
|
||||
.cv-profileheader { overflow: hidden; position: relative; }
|
||||
.cv-profileheader-cover { width: 100%; height: 160px; background-color: #a0b4b7; background-size: cover; background-position: center; position: relative; }
|
||||
.cv-profileheader-avatar-container { margin-top: -75px; margin-left: 24px; position: relative; display: inline-block; }
|
||||
.cv-profileheader-avatar { width: 150px; height: 150px; border-radius: 50%; border: 4px solid #fff; background-color: #fff; object-fit: cover; display: block; margin: 0 auto; }
|
||||
.cv-profileheader-avatar.placeholder { display: flex; align-items: center; justify-content: center; background-color: #e0e0e0; color: #666; font-weight: bold; width: 150px; height: 150px; border-radius: 50%; margin: 0 auto; }
|
||||
.cv-profileheader-info { padding: 12px 24px 16px 24px; }
|
||||
.cv-profileheader-name { font-size: 1.5rem; font-weight: 700; margin-bottom: 4px; }
|
||||
.cv-profileheader-title { font-size: 1.1rem; margin-bottom: 4px; }
|
||||
.cv-profileheader-company { color: #666; font-size: 0.95rem; }
|
||||
.cv-profileheader-nav { border-top: 1px solid #eee; padding: 10px 24px; }
|
||||
.cv-profileheader-nav ul { list-style: none; padding: 0; margin: 0; display: flex; gap: 20px; flex-wrap: wrap; }
|
||||
.cv-profileheader-nav a { text-decoration: none; color: #0a66c2; font-weight: 600; font-size: 0.9rem; }
|
||||
.cv-profileheader-nav a:hover { text-decoration: underline; }
|
||||
|
||||
.cv-contentblock, .cv-toc { padding: 32px 32px; }
|
||||
.cv-contentblock-heading { margin-top: 0; margin-bottom: 1.5rem; font-size: 1.75rem; font-weight: 700; border-bottom: 1px solid #eee; padding-bottom: 12px; }
|
||||
.cv-contentblock-inner > * { margin-bottom: 1rem; }
|
||||
.cv-contentblock-inner > *:last-child { margin-bottom: 0; }
|
||||
.cv-container { padding: 16px 24px; }
|
||||
.cv-container-inner > * { margin-bottom: 1rem; }
|
||||
.cv-container-inner > *:first-child { margin-top: 0; }
|
||||
.cv-container-inner > *:last-child { margin-bottom: 0; }
|
||||
|
||||
.cv-contactform .cv-form-group { margin-bottom: 1.25rem; }
|
||||
.cv-contactform label { display: block; font-weight: 600; margin-bottom: 6px; font-size: 0.95rem; }
|
||||
.cv-contactform input, .cv-contactform textarea { width: 100%; padding: 12px; border: 1px solid #ccc; border-radius: 4px; font-family: inherit; font-size: 1rem; box-sizing: border-box; }
|
||||
.cv-contactform input:focus, .cv-contactform textarea:focus { border-color: #0a66c2; outline: none; box-shadow: 0 0 0 1px #0a66c2; }
|
||||
.cv-contactform textarea { resize: vertical; }
|
||||
.cv-contactform-button { background-color: #0a66c2; color: #fff; padding: 12px 30px; border: none; border-radius: 24px; font-weight: 600; font-size: 1rem; cursor: pointer; font-family: inherit; transition: background-color 0.2s; }
|
||||
.cv-contactform-button:hover { background-color: #004182; }
|
||||
.cv-contactform-button:disabled { background-color: #7b9cb8; cursor: not-allowed; }
|
||||
.cv-success-msg { padding: 15px; background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; border-radius: 4px; text-align: center; font-weight: 600; }
|
||||
|
||||
.cv-toc { margin-bottom: 2rem; display: none; }
|
||||
.cv-toc-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 12px; }
|
||||
.cv-toc-list a { text-decoration: none; color: #0a66c2; font-weight: 600; font-size: 1.05rem; display: inline-block; transition: color 0.2s; }
|
||||
.cv-toc-list a:hover { color: #004182; text-decoration: underline; }
|
||||
|
||||
.cv-profile { text-align: center; padding: 32px 24px; }
|
||||
.cv-profile-avatar-container { margin-bottom: 15px; }
|
||||
.cv-profile-avatar { width: 150px; height: 150px; border-radius: 50%; border: 4px solid #fff; box-shadow: 0 2px 10px rgba(0,0,0,0.1); object-fit: cover; display: block; margin: 0 auto; }
|
||||
.cv-profile-avatar.placeholder { background: #eee; display: flex; align-items: center; justify-content: center; color: #999; font-weight: bold; width: 150px; height: 150px; border-radius: 50%; margin: 0 auto; }
|
||||
.cv-profile-name { font-size: 1.5rem; font-weight: 700; margin-bottom: 10px; }
|
||||
.cv-profile-social-container { margin-bottom: 15px; display: flex; justify-content: center; }
|
||||
.cv-profile-social-container .wp-block-social-links { justify-content: center; }
|
||||
.cv-profile-titles { font-size: 1.05rem; color: #555; line-height: 1.4; }
|
||||
.cv-profile-titles p, .cv-profile-titles div { margin: 0 0 10px 0 !important; padding: 0; }
|
||||
.cv-profile-titles p:last-child, .cv-profile-titles div:last-child { margin-bottom: 0 !important; }
|
||||
.cv-profile-titles br { display: block; content: ""; margin-bottom: 10px; }
|
||||
|
||||
/* TOP NAV BLOCK UPDATES */
|
||||
.cv-topnav { text-align: center; margin-bottom: 3rem; padding-top: 40px; }
|
||||
.cv-topnav-home-link { text-decoration: none; display: inline-block; }
|
||||
.cv-topnav-name { font-size: 3.5rem; font-weight: 700; margin: 0 0 1.5rem 0; font-family: "Open Sans", sans-serif; text-transform: none; letter-spacing: 1px; color: #000; }
|
||||
.cv-topnav-nav { margin-bottom: 0; }
|
||||
.cv-topnav-nav ul { list-style: none; padding: 0; margin: 0; display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; }
|
||||
.cv-topnav-nav a { text-decoration: none; color: #000; font-weight: 600; font-size: 1.35rem; padding: 12px 18px; transition: background-color 0.2s, color 0.2s; display: block; border-radius: 4px 4px 0 0; }
|
||||
.cv-topnav-nav a:hover { text-decoration: none; background-color: #0A66C2; color: #fff; }
|
||||
.cv-topnav-line { height: 4px; background-color: #ff004f; width: 100%; border-radius: 2px; margin-top: 0; }
|
||||
|
||||
/* PAGE SETTINGS STYLES */
|
||||
.ansico-cv-hide-title h1.entry-title, .ansico-cv-hide-title .wp-block-post-title, .ansico-cv-hide-title header.entry-header h1, .ansico-cv-hide-title .page-title,
|
||||
.ansico-cv-frame-design h1.entry-title, .ansico-cv-frame-design .wp-block-post-title, .ansico-cv-frame-design header.entry-header h1, .ansico-cv-frame-design .page-title {
|
||||
display: none !important;
|
||||
}
|
||||
.cv-page-frame-container { border: 1px solid #ddd; border-radius: 8px; background-color: #fff; margin-top: -25px; margin-bottom: 2rem; padding: 32px 32px; box-shadow: 0 0 0 1px rgba(0,0,0,0.05); }
|
||||
.cv-page-framed-title { margin-top: 0; margin-bottom: 1.5rem; font-size: 2.25rem; font-weight: 700; border-bottom: 1px solid #eee; padding-bottom: 12px; }
|
||||
102
cv-experience-blocks/block.js
Normal file
102
cv-experience-blocks/block.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
const el = wp.element.createElement;
|
||||
const { registerBlockType } = wp.blocks;
|
||||
const { RichText, InnerBlocks } = wp.blockEditor;
|
||||
|
||||
// COMPANY
|
||||
registerBlockType('cv/company', {
|
||||
title: 'Company (CV)',
|
||||
icon: 'building',
|
||||
category: 'layout',
|
||||
|
||||
attributes: {
|
||||
name: { type: 'string', default: '' },
|
||||
dates: { type: 'string', default: '' }
|
||||
},
|
||||
|
||||
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 (e.g. 2020 - 2024)',
|
||||
value: attributes.dates,
|
||||
onChange: val => setAttributes({ dates: val })
|
||||
}),
|
||||
|
||||
el(InnerBlocks, {
|
||||
allowedBlocks: ['cv/position'],
|
||||
template: [['cv/position']]
|
||||
})
|
||||
),
|
||||
|
||||
save: ({ attributes }) =>
|
||||
el('div', { className: 'cv-company' },
|
||||
el('div', { className: 'cv-company-name' }, el('strong', null, attributes.name)),
|
||||
el('div', { className: 'cv-company-dates' }, attributes.dates),
|
||||
el(InnerBlocks.Content, null)
|
||||
)
|
||||
});
|
||||
|
||||
// POSITION
|
||||
registerBlockType('cv/position', {
|
||||
title: 'Position (CV)',
|
||||
icon: 'id',
|
||||
parent: ['cv/company'],
|
||||
category: 'layout',
|
||||
|
||||
attributes: {
|
||||
title: { type: 'string', default: '' },
|
||||
department: { type: 'string', default: '' },
|
||||
dates: { type: 'string', default: '' }
|
||||
},
|
||||
|
||||
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',
|
||||
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('div', { className: 'cv-position-title' }, el('strong', null, attributes.title)),
|
||||
el('div', { className: 'cv-position-department' }, attributes.department),
|
||||
el('div', { className: 'cv-position-dates' }, attributes.dates),
|
||||
el(InnerBlocks.Content, null)
|
||||
)
|
||||
});
|
||||
50
cv-experience-blocks/cv-experience-block-v1.php
Normal file
50
cv-experience-blocks/cv-experience-block-v1.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
/**
|
||||
* Plugin Name: CV Experience Blocks
|
||||
* Description: Adds Gutenberg CV Experience blocks (Company + Positions)
|
||||
* Version: 1.0.0
|
||||
* Author: Andreas Andersen (Ansico)
|
||||
* Author URI: https://ansico.dk/Ansico
|
||||
* Plugin URI: https://ansico.dk/Ansico/CV-Experience-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'),
|
||||
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')
|
||||
);
|
||||
|
||||
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',
|
||||
]);
|
||||
}
|
||||
add_action('init', 'cv_v1_register_blocks');
|
||||
9
cv-experience-blocks/editor.css
Normal file
9
cv-experience-blocks/editor.css
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.cv-company {
|
||||
border-left: 4px solid #2271b1;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.cv-position {
|
||||
border-left: 2px solid #ddd;
|
||||
padding-left: 10px;
|
||||
}
|
||||
40
cv-experience-blocks/style.css
Normal file
40
cv-experience-blocks/style.css
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
.cv-company {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.cv-company-name {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.cv-company-dates {
|
||||
color: #666;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* tighter spacing fix */
|
||||
.cv-position-dates {
|
||||
color: #777;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.cv-position .wp-block-list {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.cv-position p {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.cv-position {
|
||||
margin-left: 24px;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.cv-position-title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cv-position-department {
|
||||
font-style: italic;
|
||||
}
|
||||
Loading…
Reference in a new issue