Compare commits

..

No commits in common. "main" and "v1.0.1" have entirely different histories.
main ... v1.0.1

14 changed files with 235 additions and 471 deletions

View file

@ -1,35 +1,20 @@
# Ansico CV-Blocks
# Ansico 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.
- 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).
![Screenshot](Screenshot.png)
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.
## 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.
1. Upload the plugin folder to /wp-content/plugins/
2. Activate the plugin through the WordPress admin
3. Add "Company (CV)" block in the block editor
## 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.
## License

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

View file

@ -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));
}

View file

@ -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 });
}

View file

@ -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; }

View file

@ -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';
}
});
}
});

View file

@ -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.

View file

@ -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; }

View 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)
)
});

View file

@ -0,0 +1,50 @@
<?php
/**
* Plugin Name: Ansico CV Experience Blocks
* Description: Adds Gutenberg CV Experience blocks (Company + Positions)
* Version: 1.0.1
* 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');

View 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;
}

View file

@ -0,0 +1,28 @@
=== Ansico CV Experience 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.0.1
License: GPLv3 or later
License URI: https://www.gnu.org/licenses/gpl-3.0.html
CV Experience Blocks adds structured Gutenberg blocks for building professional CVs with companies and positions.
== Description ==
CV Experience Blocks adds structured Gutenberg blocks for building professional CVs with companies and positions.
== Installation ==
1. Upload the plugin folder to /wp-content/plugins/
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

View 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;
}