Compare commits

...

25 commits
v1.0.1 ... main

Author SHA1 Message Date
cc058e1851 Delete ansico-cv-experience-blocks/style.css 2026-04-13 04:45:35 +00:00
95299d20c5 Delete ansico-cv-experience-blocks/readme.txt 2026-04-13 04:45:25 +00:00
e014ccfa59 Delete ansico-cv-experience-blocks/editor.css 2026-04-13 04:45:20 +00:00
cccf47789b Delete ansico-cv-experience-blocks/cv-experience-block-v1.php 2026-04-13 04:45:15 +00:00
47cedb088e Delete ansico-cv-experience-blocks/block.js 2026-04-13 04:45:08 +00:00
eb221d4a50 Upload files to "/" 2026-04-13 04:44:57 +00:00
7234ade371 Upload files to "ansico-cv-blocks" 2026-04-13 04:44:23 +00:00
04c9b5f536 Upload files to "ansico-cv-blocks" 2026-04-13 04:43:59 +00:00
9209a07c6e Delete ansico-cv-blocks/ansico-cv-blocks.php 2026-04-13 04:43:22 +00:00
b04171719c Update README.md 2026-04-13 04:20:15 +00:00
05c7eef874 Update README.md 2026-04-13 04:19:05 +00:00
3e4554a882 Update README.md 2026-04-13 04:18:14 +00:00
7e77fd39ad Update ansico-cv-blocks/readme.txt 2026-04-13 04:09:36 +00:00
4fcc5d9912 Update ansico-cv-blocks/ansico-cv-blocks.php 2026-04-13 04:09:20 +00:00
919747f5ce Update ansico-cv-blocks/readme.txt 2026-04-13 04:08:43 +00:00
30f139563f Update ansico-cv-blocks/readme.txt 2026-04-13 03:49:39 +00:00
ee4d78af30 Update ansico-cv-blocks/frontend.js 2026-04-13 03:49:16 +00:00
4a15b40510 Update ansico-cv-blocks/editor.css 2026-04-13 03:49:03 +00:00
1902e33b81 Update ansico-cv-blocks/block.js 2026-04-13 03:48:51 +00:00
1536ab5a43 Update ansico-cv-blocks/ansico-cv-blocks.php 2026-04-13 03:48:38 +00:00
6edf7ab1f2 Upload files to "/" 2026-04-13 03:48:00 +00:00
06334c826d Update README.md 2026-04-12 16:46:43 +00:00
123a1e2e8f Update README.md 2026-04-12 16:36:30 +00:00
ff2557d083 Update README.md 2026-04-12 16:35:08 +00:00
88da76019f Upload files to "/" 2026-04-12 16:34:30 +00:00
14 changed files with 471 additions and 235 deletions

View file

@ -1,20 +1,35 @@
# Ansico CV-Experience-Blocks # Ansico CV-Blocks
Adds Gutenberg CV Experience blocks (Company + Positions). See demo of plugin on https://www.aandersen.eu/curriculum-vitae/. 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/.
## Features ## Features
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. 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)
## Installation ## Installation
1. Upload the plugin folder to /wp-content/plugins/ 1. Upload the plugin folder to /wp-content/plugins/ or using Plugins -> Add Plugin -> Upload Plugin.
2. Activate the plugin through the WordPress admin 2. Activate the plugin through the WordPress admin.
3. Add "Company (CV)" block in the block editor 3. Add "Company (CV)" block in the block editor.
## Version ## Version
- Version 1.0.0 (12/4 2026): Initial release of CV Experience Blocks. - 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.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 ## License

BIN
Screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
ansico-cv-blocks.zip Normal file

Binary file not shown.

View file

@ -0,0 +1,83 @@
<?php
/**
* Plugin Name: Ansico CV Blocks
* Description: Adds Gutenberg CV blocks, Contact Form, TOC, Profile, and Top Navigation Block.
* Version: 1.0.2
* Author: Andreas Andersen (Ansico)
* Author URI: https://ansico.dk/Ansico
* Plugin URI: https://ansico.dk/Ansico/CV-Blocks
* License: GPL3 or later
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
*/
if (!defined('ABSPATH')) exit;
function cv_v1_register_blocks() {
wp_register_script('cv-v1-blocks', plugins_url('block.js', __FILE__), array('wp-blocks','wp-element','wp-block-editor','wp-components', 'wp-data', 'wp-plugins', 'wp-edit-post'), filemtime(plugin_dir_path(__FILE__) . 'block.js'));
wp_register_style('cv-v1-style', plugins_url('style.css', __FILE__), array(), filemtime(plugin_dir_path(__FILE__) . 'style.css'));
wp_register_style('cv-v1-editor', plugins_url('editor.css', __FILE__), array('wp-edit-blocks'), filemtime(plugin_dir_path(__FILE__) . 'editor.css'));
wp_register_script('cv-v1-frontend', plugins_url('frontend.js', __FILE__), array(), filemtime(plugin_dir_path(__FILE__) . 'frontend.js'), true);
register_block_type('cv/company', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
register_block_type('cv/position', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
register_block_type('cv/education', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
register_block_type('cv/profileheader', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
register_block_type('cv/contentblock', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
register_block_type('cv/container', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
register_block_type('cv/contactform', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor', 'view_script' => 'cv-v1-frontend']);
register_block_type('cv/toc', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor', 'view_script' => 'cv-v1-frontend']);
register_block_type('cv/profile', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
register_block_type('cv/topnav', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
}
add_action('init', 'cv_v1_register_blocks');
add_action('init', function() {
register_post_meta('page', 'ansico_cv_hide_title', array('show_in_rest' => true, 'single' => true, 'type' => 'boolean', 'default' => false));
register_post_meta('page', 'ansico_cv_frame_design', array('show_in_rest' => true, 'single' => true, 'type' => 'boolean', 'default' => false));
});
add_filter('body_class', function($classes) {
if (is_page()) {
$hide_title = get_post_meta(get_the_ID(), 'ansico_cv_hide_title', true);
$frame_design = get_post_meta(get_the_ID(), 'ansico_cv_frame_design', true);
if ($hide_title) $classes[] = 'ansico-cv-hide-title';
if ($frame_design) $classes[] = 'ansico-cv-frame-design';
}
return $classes;
});
add_filter('the_content', function($content) {
if (is_page() && in_the_loop() && is_main_query()) {
$hide_title = get_post_meta(get_the_ID(), 'ansico_cv_hide_title', true);
$frame_design = get_post_meta(get_the_ID(), 'ansico_cv_frame_design', true);
if ($frame_design) {
$title_html = '';
if (!$hide_title) {
$title_html = '<h1 class="cv-page-framed-title">' . get_the_title() . '</h1>';
}
$content = '<div class="cv-page-frame-container">' . $title_html . '<div class="cv-page-frame-inner">' . $content . '</div></div>';
}
}
return $content;
});
add_action('rest_api_init', function () {
register_rest_route('cv-blocks/v1', '/send-message', array('methods' => 'POST', 'callback' => 'cv_blocks_send_email_callback', 'permission_callback' => '__return_true'));
});
function cv_blocks_send_email_callback($request) {
$params = $request->get_params();
$receiver = sanitize_email($params['receiver']);
$name = sanitize_text_field($params['name']);
$email = sanitize_email($params['email']);
$subject = sanitize_text_field($params['subject']);
$message = sanitize_textarea_field($params['message']);
$site_name = get_bloginfo('name');
if (empty($receiver) || empty($name) || empty($email) || empty($message)) return new WP_Error('missing_data', 'Missing required fields', array('status' => 400));
$headers = array('From: ' . $name . ' <' . $email . '>', 'Reply-To: ' . $name . ' <' . $email . '>', 'Content-Type: text/plain; charset=UTF-8');
$email_subject = $subject ? "New Contact Message: " . $subject : "New Contact Message from " . $name;
$email_body = "You have received a new message from your CV Contact Form on website " . $site_name . ".\n\nName: " . $name . "\nEmail: " . $email . "\nSubject: " . $subject . "\n\nMessage:\n" . $message . "\n";
$sent = wp_mail($receiver, $email_subject, $email_body, $headers);
if ($sent) return rest_ensure_response(array('success' => true));
else return new WP_Error('mail_failed', 'Could not send email', array('status' => 500));
}

184
ansico-cv-blocks/block.js Normal file
View file

@ -0,0 +1,184 @@
const el = wp.element.createElement;
const { registerBlockType } = wp.blocks;
const { RichText, InnerBlocks, MediaUpload, InspectorControls, URLInput } = wp.blockEditor;
const { Button, PanelBody, ToggleControl, TextControl, ButtonGroup } = wp.components;
const { useSelect, useDispatch } = wp.data;
const moveItem = (list, fromIndex, toIndex) => {
const result = Array.from(list);
const [removed] = result.splice(fromIndex, 1);
result.splice(toIndex, 0, removed);
return result;
};
registerBlockType('cv/company', {
title: 'Company (CV)', icon: 'building', category: 'layout',
attributes: { name: { type: 'string', source: 'html', selector: '.cv-company-name' }, dates: { type: 'string', source: 'html', selector: '.cv-company-dates' } },
edit: ({ attributes, setAttributes }) => el('div', { className: 'cv-company' }, el(RichText, { tagName: 'div', className: 'cv-company-name', placeholder: 'Company Name', value: attributes.name, onChange: val => setAttributes({ name: val }) }), el(RichText, { tagName: 'div', className: 'cv-company-dates', placeholder: 'Years', value: attributes.dates, onChange: val => setAttributes({ dates: val }) }), el(InnerBlocks, { allowedBlocks: ['cv/position'], template: [['cv/position']] })),
save: ({ attributes }) => el('div', { className: 'cv-company' }, el(RichText.Content, { tagName: 'div', className: 'cv-company-name', value: attributes.name }), el(RichText.Content, { tagName: 'div', className: 'cv-company-dates', value: attributes.dates }), el(InnerBlocks.Content, null))
});
registerBlockType('cv/position', {
title: 'Position (CV)', icon: 'id', parent: ['cv/company'], category: 'layout',
attributes: { title: { type: 'string', source: 'html', selector: '.cv-position-title' }, department: { type: 'string', source: 'html', selector: '.cv-position-department' }, dates: { type: 'string', source: 'html', selector: '.cv-position-dates' } },
edit: ({ attributes, setAttributes }) => el('div', { className: 'cv-position' }, el(RichText, { tagName: 'div', className: 'cv-position-title', placeholder: 'Job Title', value: attributes.title, onChange: val => setAttributes({ title: val }) }), el(RichText, { tagName: 'div', className: 'cv-position-department', placeholder: 'Department / Unit', value: attributes.department, onChange: val => setAttributes({ department: val }) }), el(RichText, { tagName: 'div', className: 'cv-position-dates', placeholder: 'Period', value: attributes.dates, onChange: val => setAttributes({ dates: val }) }), el(InnerBlocks, { allowedBlocks: ['core/list'], template: [['core/list']] })),
save: ({ attributes }) => el('div', { className: 'cv-position' }, el(RichText.Content, { tagName: 'div', className: 'cv-position-title', value: attributes.title }), el(RichText.Content, { tagName: 'div', className: 'cv-position-department', value: attributes.department }), el(RichText.Content, { tagName: 'div', className: 'cv-position-dates', value: attributes.dates }), el(InnerBlocks.Content, null))
});
registerBlockType('cv/education', {
title: 'Education (CV)', icon: 'welcome-learn-more', category: 'layout',
attributes: { degree: { type: 'string', source: 'html', selector: '.cv-education-degree' }, institution: { type: 'string', source: 'html', selector: '.cv-education-institution' }, dates: { type: 'string', source: 'html', selector: '.cv-education-dates' } },
edit: ({ attributes, setAttributes }) => el('div', { className: 'cv-education' }, el(RichText, { tagName: 'div', className: 'cv-education-degree', placeholder: 'Degree / Qualification', value: attributes.degree, onChange: val => setAttributes({ degree: val }) }), el(RichText, { tagName: 'div', className: 'cv-education-institution', placeholder: 'University or School', value: attributes.institution, onChange: val => setAttributes({ institution: val }) }), el(RichText, { tagName: 'div', className: 'cv-education-dates', placeholder: 'Years', value: attributes.dates, onChange: val => setAttributes({ dates: val }) })),
save: ({ attributes }) => el('div', { className: 'cv-education' }, el(RichText.Content, { tagName: 'div', className: 'cv-education-degree', value: attributes.degree }), el(RichText.Content, { tagName: 'div', className: 'cv-education-institution', value: attributes.institution }), el(RichText.Content, { tagName: 'div', className: 'cv-education-dates', value: attributes.dates }))
});
registerBlockType('cv/profileheader', {
title: 'Profile Header (CV)', icon: 'id-alt', category: 'layout',
supports: { align: ['wide', 'full'] },
attributes: { headerImage: { type: 'string', default: '' }, profileImage: { type: 'string', default: '' }, name: { type: 'string', source: 'html', selector: '.cv-profileheader-name' }, title: { type: 'string', source: 'html', selector: '.cv-profileheader-title' }, company: { type: 'string', source: 'html', selector: '.cv-profileheader-company' }, showMenu: { type: 'boolean', default: false }, menuItems: { type: 'array', default: [] } },
edit: ({ attributes, setAttributes }) => {
const updateMenuItem = (index, key, value) => { const newItems = [...attributes.menuItems]; newItems[index] = { ...newItems[index], [key]: value }; setAttributes({ menuItems: newItems }); };
const addMenuItem = () => setAttributes({ menuItems: [...attributes.menuItems, { label: '', url: '' }] });
const removeMenuItem = (index) => { const newItems = attributes.menuItems.filter((_, i) => i !== index); setAttributes({ menuItems: newItems }); };
const moveMenuItemAt = (index, direction) => { const newPos = index + direction; if (newPos >= 0 && newPos < attributes.menuItems.length) setAttributes({ menuItems: moveItem(attributes.menuItems, index, newPos) }); };
return [
el(InspectorControls, {}, el(PanelBody, { title: 'Navigation Menu', initialOpen: true }, el(ToggleControl, { label: 'Enable Navigation Menu', checked: attributes.showMenu, onChange: (val) => setAttributes({ showMenu: val }) }))),
el('div', { className: 'cv-profileheader' },
el('div', { className: 'cv-profileheader-cover', style: { backgroundImage: attributes.headerImage ? `url(${attributes.headerImage})` : 'none' } }, el(MediaUpload, { onSelect: media => setAttributes({ headerImage: media.url }), type: 'image', render: ({ open }) => el(Button, { onClick: open, className: 'button', style: { position: 'absolute', top: '10px', right: '10px', backgroundColor: 'rgba(255,255,255,0.9)' } }, attributes.headerImage ? 'Change Header' : 'Add Header Image') })),
el('div', { className: 'cv-profileheader-avatar-container' }, el(MediaUpload, { onSelect: media => setAttributes({ profileImage: media.url }), type: 'image', render: ({ open }) => el('div', { onClick: open, style: { cursor: 'pointer' }, title: 'Click to set profile picture' }, attributes.profileImage ? el('img', { src: attributes.profileImage, className: 'cv-profileheader-avatar' }) : el('div', { className: 'cv-profileheader-avatar placeholder' }, 'Add Photo')) })),
el('div', { className: 'cv-profileheader-info' }, el(RichText, { tagName: 'div', className: 'cv-profileheader-name', placeholder: 'Full Name', value: attributes.name, onChange: val => setAttributes({ name: val }) }), el(RichText, { tagName: 'div', className: 'cv-profileheader-title', placeholder: 'Current Job Title', value: attributes.title, onChange: val => setAttributes({ title: val }) }), el(RichText, { tagName: 'div', className: 'cv-profileheader-company', placeholder: 'Company or Organization', value: attributes.company, onChange: val => setAttributes({ company: val }) })),
attributes.showMenu && el('div', { className: 'cv-profileheader-nav-editor' }, attributes.menuItems.map((item, index) => el('div', { className: 'cv-menu-item-edit', key: index }, el('div', { className: 'cv-menu-item-fields' }, el(TextControl, { placeholder: 'Label', value: item.label, onChange: val => updateMenuItem(index, 'label', val) }), el(URLInput, { className: 'cv-url-input', value: item.url, onChange: val => updateMenuItem(index, 'url', val), disableSuggestions: false, isFullWidth: true, placeholder: 'Link or Search Page...' })), el(ButtonGroup, {}, el(Button, { icon: 'arrow-up-alt2', onClick: () => moveMenuItemAt(index, -1), disabled: index === 0 }), el(Button, { icon: 'arrow-down-alt2', onClick: () => moveMenuItemAt(index, 1), disabled: index === attributes.menuItems.length - 1 }), el(Button, { isDestructive: true, onClick: () => removeMenuItem(index) }, 'Remove')))), el(Button, { isPrimary: true, onClick: addMenuItem }, 'Add Menu Item'))
)
];
},
save: ({ attributes }) => el('div', { className: 'cv-profileheader' }, el('div', { className: 'cv-profileheader-cover', style: { backgroundImage: attributes.headerImage ? `url(${attributes.headerImage})` : 'none' } }), el('div', { className: 'cv-profileheader-avatar-container' }, attributes.profileImage ? el('img', { src: attributes.profileImage, className: 'cv-profileheader-avatar', alt: '' }) : null), el('div', { className: 'cv-profileheader-info' }, el(RichText.Content, { tagName: 'div', className: 'cv-profileheader-name', value: attributes.name }), el(RichText.Content, { tagName: 'div', className: 'cv-profileheader-title', value: attributes.title }), el(RichText.Content, { tagName: 'div', className: 'cv-profileheader-company', value: attributes.company })), attributes.showMenu && attributes.menuItems.length > 0 && el('nav', { className: 'cv-profileheader-nav' }, el('ul', {}, attributes.menuItems.map((item, index) => el('li', { key: index }, el('a', { href: item.url }, item.label))))))
});
registerBlockType('cv/contentblock', {
title: 'Content Block (CV)', icon: 'layout', category: 'layout',
attributes: { heading: { type: 'string', source: 'html', selector: '.cv-contentblock-heading' }, includeInToc: { type: 'boolean', default: true } },
edit: ({ attributes, setAttributes }) => {
return [
el(InspectorControls, {}, el(PanelBody, { title: 'Table of Contents', initialOpen: true }, el(ToggleControl, { label: 'Include in Table of Contents', checked: attributes.includeInToc, onChange: (val) => setAttributes({ includeInToc: val }) }))),
el('div', { className: 'cv-contentblock' }, el(RichText, { tagName: 'h2', className: 'cv-contentblock-heading', placeholder: 'Section Heading', value: attributes.heading, onChange: val => setAttributes({ heading: val }) }), el('div', { className: 'cv-contentblock-inner' }, el(InnerBlocks, {})))
];
},
save: ({ attributes }) => el('div', { className: 'cv-contentblock', 'data-include-toc': attributes.includeInToc }, el(RichText.Content, { tagName: 'h2', className: 'cv-contentblock-heading', value: attributes.heading }), el('div', { className: 'cv-contentblock-inner' }, el(InnerBlocks.Content, null)))
});
registerBlockType('cv/container', {
title: 'Container (CV)', icon: 'editor-expand', category: 'layout',
edit: () => el('div', { className: 'cv-container' }, el('div', { className: 'cv-container-inner' }, el(InnerBlocks, {}))),
save: () => el('div', { className: 'cv-container' }, el('div', { className: 'cv-container-inner' }, el(InnerBlocks.Content, null)))
});
registerBlockType('cv/contactform', {
title: 'Contact Form (CV)', icon: 'email', category: 'layout',
attributes: { nameLabel: { type: 'string', default: 'Name' }, emailLabel: { type: 'string', default: 'Email' }, subjectLabel: { type: 'string', default: 'Subject' }, messageLabel: { type: 'string', default: 'Message' }, buttonLabel: { type: 'string', default: 'Send Message' }, receiverEmail: { type: 'string', default: '' }, successMessage: { type: 'string', default: 'The message has been sent!' }, enableControlField: { type: 'boolean', default: false }, controlQuestion: { type: 'string', default: 'What is 2+2?' }, controlAnswer: { type: 'string', default: '4' } },
edit: ({ attributes, setAttributes }) => {
return [
el(InspectorControls, {}, el(PanelBody, { title: 'Contact Form Settings', initialOpen: true }, el(TextControl, { label: 'Receiver Email Address', placeholder: 'your@email.com', value: attributes.receiverEmail, onChange: val => setAttributes({ receiverEmail: val }) }), el(TextControl, { label: 'Success Message', value: attributes.successMessage, onChange: val => setAttributes({ successMessage: val }) }), el(ToggleControl, { label: 'Enable Control Question (Anti-spam)', checked: attributes.enableControlField, onChange: val => setAttributes({ enableControlField: val }) }), attributes.enableControlField && el(TextControl, { label: 'Control Question', value: attributes.controlQuestion, onChange: val => setAttributes({ controlQuestion: val }) }), attributes.enableControlField && el(TextControl, { label: 'Expected Answer', value: attributes.controlAnswer, onChange: val => setAttributes({ controlAnswer: val }) }))),
el('div', { className: 'cv-contactform' }, el('div', { className: 'cv-form-group' }, el(RichText, { tagName: 'label', value: attributes.nameLabel, onChange: (val) => setAttributes({ nameLabel: val }) }), el('input', { type: 'text', disabled: true })), el('div', { className: 'cv-form-group' }, el(RichText, { tagName: 'label', value: attributes.emailLabel, onChange: (val) => setAttributes({ emailLabel: val }) }), el('input', { type: 'email', disabled: true })), el('div', { className: 'cv-form-group' }, el(RichText, { tagName: 'label', value: attributes.subjectLabel, onChange: (val) => setAttributes({ subjectLabel: val }) }), el('input', { type: 'text', disabled: true })), el('div', { className: 'cv-form-group' }, el(RichText, { tagName: 'label', value: attributes.messageLabel, onChange: (val) => setAttributes({ messageLabel: val }) }), el('textarea', { disabled: true, rows: 10 })), attributes.enableControlField && el('div', { className: 'cv-form-group' }, el('label', {}, attributes.controlQuestion), el('input', { type: 'text', disabled: true })), el('div', { className: 'cv-form-group' }, el(RichText, { tagName: 'button', className: 'cv-contactform-button', value: attributes.buttonLabel, onChange: (val) => setAttributes({ buttonLabel: val }) })))
];
},
save: ({ attributes }) => {
return el('div', { className: 'cv-contactform' }, el('form', { action: '#', method: 'post', 'data-receiver': attributes.receiverEmail, 'data-success': attributes.successMessage, 'data-control-answer': attributes.enableControlField ? attributes.controlAnswer : '' }, el('div', { className: 'cv-form-group' }, el('label', {}, attributes.nameLabel), el('input', { type: 'text', name: 'cv-name', required: true })), el('div', { className: 'cv-form-group' }, el('label', {}, attributes.emailLabel), el('input', { type: 'email', name: 'cv-email', required: true })), el('div', { className: 'cv-form-group' }, el('label', {}, attributes.subjectLabel), el('input', { type: 'text', name: 'cv-subject' })), el('div', { className: 'cv-form-group' }, el('label', {}, attributes.messageLabel), el('textarea', { name: 'cv-message', rows: 10, required: true })), attributes.enableControlField && el('div', { className: 'cv-form-group' }, el('label', {}, attributes.controlQuestion), el('input', { type: 'text', name: 'cv-control', required: true })), el('button', { type: 'submit', className: 'cv-contactform-button' }, attributes.buttonLabel)), el('div', { className: 'cv-contactform-feedback', style: { display: 'none' } }));
}
});
registerBlockType('cv/toc', {
title: 'Table of Contents (CV)', icon: 'list-view', category: 'layout',
attributes: {
heading: { type: 'string', source: 'html', selector: '.cv-toc-heading', default: 'Table of Content' }
},
edit: ({ attributes, setAttributes }) => {
const headings = useSelect(select => {
const blocks = select('core/block-editor').getBlocks();
const findHeadings = (blocksList) => {
let result = [];
blocksList.forEach(block => {
if (block.name === 'cv/contentblock' && block.attributes.includeInToc !== false && block.attributes.heading) result.push(block.attributes.heading.replace(/<[^>]+>/g, ''));
if (block.innerBlocks) result = result.concat(findHeadings(block.innerBlocks));
});
return result;
};
return findHeadings(blocks);
}, []);
return el('div', { className: 'cv-toc cv-contentblock' },
el(RichText, { tagName: 'h2', className: 'cv-toc-heading cv-contentblock-heading', value: attributes.heading, onChange: val => setAttributes({ heading: val }), placeholder: 'Table of Content' }),
el('div', { className: 'cv-toc-editor-preview' },
headings.length > 0 ? el('ul', { className: 'cv-toc-list' }, headings.map((h, i) => el('li', { key: i }, el('a', { href: '#' }, h)))) : el('p', { style: { color: '#666', fontStyle: 'italic', margin: 0, fontSize: '0.9rem' } }, 'Add Content Blocks to see headings.')
)
);
},
save: ({ attributes }) => el('div', { className: 'cv-toc cv-contentblock' },
el(RichText.Content, { tagName: 'h2', className: 'cv-toc-heading cv-contentblock-heading', value: attributes.heading })
)
});
registerBlockType('cv/profile', {
title: 'Profile (CV)', icon: 'admin-users', category: 'layout',
attributes: { profileImage: { type: 'string', default: '' }, name: { type: 'string', source: 'html', selector: '.cv-profile-name' }, titles: { type: 'string', source: 'html', selector: '.cv-profile-titles' }, showSocial: { type: 'boolean', default: false } },
edit: ({ attributes, setAttributes }) => {
return [
el(InspectorControls, {}, el(PanelBody, { title: 'Profile Settings', initialOpen: true }, el(ToggleControl, { label: 'Enable Social Icons', checked: attributes.showSocial, onChange: (val) => setAttributes({ showSocial: val }) }))),
el('div', { className: 'cv-profile' },
el('div', { className: 'cv-profile-avatar-container' }, el(MediaUpload, { onSelect: media => setAttributes({ profileImage: media.url }), type: 'image', render: ({ open }) => el('div', { onClick: open, style: { cursor: 'pointer' } }, attributes.profileImage ? el('img', { src: attributes.profileImage, className: 'cv-profile-avatar' }) : el('div', { className: 'cv-profile-avatar placeholder' }, 'Add Photo')) })),
el(RichText, { tagName: 'div', className: 'cv-profile-name', placeholder: 'Full Name', value: attributes.name, onChange: val => setAttributes({ name: val }) }),
attributes.showSocial && el('div', { className: 'cv-profile-social-container' }, el(InnerBlocks, { allowedBlocks: ['core/social-links'], template: [['core/social-links']] })),
el(RichText, { tagName: 'div', className: 'cv-profile-titles', placeholder: 'Titles (Press Enter for new line)', value: attributes.titles, onChange: val => setAttributes({ titles: val }) })
)
];
},
save: ({ attributes }) => el('div', { className: 'cv-profile' }, el('div', { className: 'cv-profile-avatar-container' }, attributes.profileImage ? el('img', { src: attributes.profileImage, className: 'cv-profile-avatar', alt: '' }) : null), el(RichText.Content, { tagName: 'div', className: 'cv-profile-name', value: attributes.name }), attributes.showSocial && el('div', { className: 'cv-profile-social-container' }, el(InnerBlocks.Content, null)), el(RichText.Content, { tagName: 'div', className: 'cv-profile-titles', value: attributes.titles }))
});
registerBlockType('cv/topnav', {
title: 'Top Navigation (CV)', icon: 'menu', category: 'layout',
supports: { align: ['wide', 'full'] },
attributes: {
siteName: { type: 'string', source: 'html', selector: '.cv-topnav-name', default: '' },
menuItems: { type: 'array', default: [] }
},
edit: ({ attributes, setAttributes }) => {
const updateMenuItem = (index, key, value) => { const newItems = [...attributes.menuItems]; newItems[index] = { ...newItems[index], [key]: value }; setAttributes({ menuItems: newItems }); };
const addMenuItem = () => setAttributes({ menuItems: [...attributes.menuItems, { label: '', url: '' }] });
const removeMenuItem = (index) => { const newItems = attributes.menuItems.filter((_, i) => i !== index); setAttributes({ menuItems: newItems }); };
const moveMenuItemAt = (index, direction) => { const newPos = index + direction; if (newPos >= 0 && newPos < attributes.menuItems.length) setAttributes({ menuItems: moveItem(attributes.menuItems, index, newPos) }); };
const siteTitle = useSelect(select => select('core').getSite()?.title);
wp.element.useEffect(() => { if (!attributes.siteName && siteTitle) setAttributes({ siteName: siteTitle }); }, [siteTitle]);
return el('div', { className: 'cv-topnav' },
el('div', { className: 'cv-topnav-link-wrapper' },
el(RichText, { tagName: 'h1', className: 'cv-topnav-name', placeholder: 'Site Name', value: attributes.siteName, onChange: val => setAttributes({ siteName: val }) })
),
el('div', { className: 'cv-topnav-menu-editor' },
attributes.menuItems.map((item, index) => el('div', { className: 'cv-menu-item-edit', key: index }, el('div', { className: 'cv-menu-item-fields' }, el(TextControl, { placeholder: 'Label', value: item.label, onChange: val => updateMenuItem(index, 'label', val) }), el(URLInput, { className: 'cv-url-input', value: item.url, onChange: val => updateMenuItem(index, 'url', val), disableSuggestions: false, isFullWidth: true, placeholder: 'Link or Search Page...' })), el(ButtonGroup, {}, el(Button, { icon: 'arrow-up-alt2', onClick: () => moveMenuItemAt(index, -1), disabled: index === 0 }), el(Button, { icon: 'arrow-down-alt2', onClick: () => moveMenuItemAt(index, 1), disabled: index === attributes.menuItems.length - 1 }), el(Button, { isDestructive: true, onClick: () => removeMenuItem(index) }, 'Remove')))),
el(Button, { isPrimary: true, onClick: addMenuItem, style: {marginTop: '10px'} }, 'Add Menu Link')
),
el('div', { className: 'cv-topnav-line' })
);
},
save: ({ attributes }) => el('header', { className: 'cv-topnav' },
el('a', { href: '/', className: 'cv-topnav-home-link' }, el(RichText.Content, { tagName: 'h1', className: 'cv-topnav-name', value: attributes.siteName })),
attributes.menuItems.length > 0 && el('nav', { className: 'cv-topnav-nav' }, el('ul', {}, attributes.menuItems.map((item, index) => el('li', { key: index }, el('a', { href: item.url }, item.label))))),
el('div', { className: 'cv-topnav-line' })
)
});
if (wp.plugins && wp.editPost) {
const { registerPlugin } = wp.plugins;
const { PluginDocumentSettingPanel } = wp.editPost;
const PageSettingsPanel = () => {
const postType = useSelect(select => select('core/editor').getCurrentPostType(), []);
if (postType !== 'page') return null;
const meta = useSelect(select => select('core/editor').getEditedPostAttribute('meta'), []);
const { editPost } = useDispatch('core/editor');
if (!meta) return null;
return el(PluginDocumentSettingPanel, { name: 'ansico-cv-page-settings', title: 'CV Page Settings', icon: 'layout' },
el(ToggleControl, { label: 'Hide Page Title (H1)', checked: meta.ansico_cv_hide_title || false, onChange: (val) => editPost({ meta: { ...meta, ansico_cv_hide_title: val } }) }),
el(ToggleControl, { label: 'Enable Frame Design', help: 'Wraps the page content in a CV-styled box.', checked: meta.ansico_cv_frame_design || false, onChange: (val) => editPost({ meta: { ...meta, ansico_cv_frame_design: val } }) })
);
};
registerPlugin('ansico-cv-page-settings-plugin', { render: PageSettingsPanel });
}

View file

@ -0,0 +1,19 @@
.cv-company { border-left: 4px solid #2271b1; padding-left: 10px; }
.cv-position { border-left: 2px solid #ddd; padding-left: 10px; }
.cv-education { border-left: 4px solid #46b450; padding-left: 10px; margin-bottom: 1rem; }
.cv-profileheader { border-left: 4px solid #ffba00; border-radius: 0; }
.cv-contentblock, .cv-toc { border-left: 4px solid #9b51e0; border-radius: 0; }
.cv-container { border-left: 4px solid #36c3d9; border-radius: 0; }
.cv-contactform { border-left: 4px solid #ff5722; padding-left: 15px; }
.cv-profile { border-left: 4px solid #f39c12; border-radius: 0; text-align: center; }
.cv-topnav { border-left: 4px solid #ff004f; padding-left: 15px; margin-bottom: 2rem; }
.cv-profile-avatar-container { display: flex; justify-content: center; }
.cv-profile-social-container { min-height: 20px; background: #fafafa; border: 1px dashed #ddd; }
.cv-profileheader-nav-editor, .cv-topnav-menu-editor { padding: 15px 24px; background: #f9f9f9; border-top: 1px solid #eee; }
.cv-menu-item-edit { display: flex; gap: 10px; margin-bottom: 15px; align-items: flex-end; border-bottom: 1px dashed #ccc; padding-bottom: 15px; }
.cv-menu-item-fields { flex: 1; }
.cv-url-input { border: 1px solid #ccc; padding: 2px; background: #fff; margin-top: 5px; width: 100%; }
.cv-contactform input:disabled, .cv-contactform textarea:disabled { background: #f5f5f5; cursor: not-allowed; }
.cv-toc { display: block !important; }
.cv-toc-editor-preview { padding: 20px 0 0 0; }
.cv-toc-editor-preview .cv-toc-list a { pointer-events: none; }

View file

@ -0,0 +1,51 @@
document.addEventListener('DOMContentLoaded', function() {
const forms = document.querySelectorAll('.cv-contactform form');
forms.forEach(form => {
form.addEventListener('submit', function(e) {
e.preventDefault();
const btn = form.querySelector('button[type="submit"]');
const feedback = form.parentElement.querySelector('.cv-contactform-feedback');
const originalBtnText = btn.innerText;
const receiver = form.getAttribute('data-receiver');
if (!receiver) { alert('Receiver email missing.'); return; }
const expectedAnswer = form.getAttribute('data-control-answer');
if (expectedAnswer && form.querySelector('[name="cv-control"]').value.trim().toLowerCase() !== expectedAnswer.trim().toLowerCase()) { alert('Incorrect answer.'); return; }
btn.innerText = 'Sending...'; btn.disabled = true;
const data = { receiver: receiver, name: form.querySelector('[name="cv-name"]').value, email: form.querySelector('[name="cv-email"]').value, subject: form.querySelector('[name="cv-subject"]').value, message: form.querySelector('[name="cv-message"]').value };
fetch('/wp-json/cv-blocks/v1/send-message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then(res => res.json()).then(res => {
if (res.success) { form.style.display = 'none'; feedback.innerHTML = '<div class="cv-success-msg">' + (form.getAttribute('data-success') || 'Sent!') + '</div>'; feedback.style.display = 'block'; }
else { alert('Error.'); btn.innerText = originalBtnText; btn.disabled = false; }
});
});
});
const tocBlocks = document.querySelectorAll('.cv-toc');
if (tocBlocks.length > 0) {
const contentBlocks = document.querySelectorAll('.cv-contentblock');
tocBlocks.forEach(container => {
let hasItems = false;
const ul = document.createElement('ul');
ul.className = 'cv-toc-list';
contentBlocks.forEach((b, i) => {
if (b.getAttribute('data-include-toc') === 'false') return;
const h = b.querySelector('.cv-contentblock-heading');
if (!h || !h.textContent.trim()) return;
if (b.classList.contains('cv-toc')) return;
if (!b.id) b.id = 'cv-sec-' + i;
const li = document.createElement('li');
const a = document.createElement('a');
a.href = '#' + b.id;
a.textContent = h.textContent;
a.onclick = (e) => { e.preventDefault(); document.getElementById(b.id).scrollIntoView({ behavior: 'smooth' }); };
li.appendChild(a);
ul.appendChild(li);
hasItems = true;
});
if (hasItems) {
container.appendChild(ul);
container.style.display = 'block';
} else {
container.style.display = 'none';
}
});
}
});

View file

@ -0,0 +1,31 @@
=== 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

@ -0,0 +1,82 @@
.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

@ -1,102 +0,0 @@
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

@ -1,50 +0,0 @@
<?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

@ -1,9 +0,0 @@
.cv-company {
border-left: 4px solid #2271b1;
padding-left: 10px;
}
.cv-position {
border-left: 2px solid #ddd;
padding-left: 10px;
}

View file

@ -1,28 +0,0 @@
=== 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

@ -1,40 +0,0 @@
.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;
}