Compare commits
25 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc058e1851 | |||
| 95299d20c5 | |||
| e014ccfa59 | |||
| cccf47789b | |||
| 47cedb088e | |||
| eb221d4a50 | |||
| 7234ade371 | |||
| 04c9b5f536 | |||
| 9209a07c6e | |||
| b04171719c | |||
| 05c7eef874 | |||
| 3e4554a882 | |||
| 7e77fd39ad | |||
| 4fcc5d9912 | |||
| 919747f5ce | |||
| 30f139563f | |||
| ee4d78af30 | |||
| 4a15b40510 | |||
| 1902e33b81 | |||
| 1536ab5a43 | |||
| 6edf7ab1f2 | |||
| 06334c826d | |||
| 123a1e2e8f | |||
| ff2557d083 | |||
| 88da76019f |
14 changed files with 471 additions and 235 deletions
27
README.md
27
README.md
|
|
@ -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
|
||||
|
||||
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).
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
|
|
|
|||
BIN
Screenshot.png
Normal file
BIN
Screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
BIN
ansico-cv-blocks.zip
Normal file
BIN
ansico-cv-blocks.zip
Normal file
Binary file not shown.
83
ansico-cv-blocks/ansico-cv-blocks.php
Normal file
83
ansico-cv-blocks/ansico-cv-blocks.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
/**
|
||||
* Plugin Name: Ansico CV Blocks
|
||||
* Description: Adds Gutenberg CV blocks, Contact Form, TOC, Profile, and Top Navigation Block.
|
||||
* Version: 1.0.2
|
||||
* Author: Andreas Andersen (Ansico)
|
||||
* Author URI: https://ansico.dk/Ansico
|
||||
* Plugin URI: https://ansico.dk/Ansico/CV-Blocks
|
||||
* License: GPL3 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
function cv_v1_register_blocks() {
|
||||
wp_register_script('cv-v1-blocks', plugins_url('block.js', __FILE__), array('wp-blocks','wp-element','wp-block-editor','wp-components', 'wp-data', 'wp-plugins', 'wp-edit-post'), filemtime(plugin_dir_path(__FILE__) . 'block.js'));
|
||||
wp_register_style('cv-v1-style', plugins_url('style.css', __FILE__), array(), filemtime(plugin_dir_path(__FILE__) . 'style.css'));
|
||||
wp_register_style('cv-v1-editor', plugins_url('editor.css', __FILE__), array('wp-edit-blocks'), filemtime(plugin_dir_path(__FILE__) . 'editor.css'));
|
||||
wp_register_script('cv-v1-frontend', plugins_url('frontend.js', __FILE__), array(), filemtime(plugin_dir_path(__FILE__) . 'frontend.js'), true);
|
||||
|
||||
register_block_type('cv/company', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
|
||||
register_block_type('cv/position', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
|
||||
register_block_type('cv/education', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
|
||||
register_block_type('cv/profileheader', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
|
||||
register_block_type('cv/contentblock', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
|
||||
register_block_type('cv/container', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
|
||||
register_block_type('cv/contactform', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor', 'view_script' => 'cv-v1-frontend']);
|
||||
register_block_type('cv/toc', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor', 'view_script' => 'cv-v1-frontend']);
|
||||
register_block_type('cv/profile', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
|
||||
register_block_type('cv/topnav', ['editor_script' => 'cv-v1-blocks', 'style' => 'cv-v1-style', 'editor_style' => 'cv-v1-editor']);
|
||||
}
|
||||
add_action('init', 'cv_v1_register_blocks');
|
||||
|
||||
add_action('init', function() {
|
||||
register_post_meta('page', 'ansico_cv_hide_title', array('show_in_rest' => true, 'single' => true, 'type' => 'boolean', 'default' => false));
|
||||
register_post_meta('page', 'ansico_cv_frame_design', array('show_in_rest' => true, 'single' => true, 'type' => 'boolean', 'default' => false));
|
||||
});
|
||||
|
||||
add_filter('body_class', function($classes) {
|
||||
if (is_page()) {
|
||||
$hide_title = get_post_meta(get_the_ID(), 'ansico_cv_hide_title', true);
|
||||
$frame_design = get_post_meta(get_the_ID(), 'ansico_cv_frame_design', true);
|
||||
if ($hide_title) $classes[] = 'ansico-cv-hide-title';
|
||||
if ($frame_design) $classes[] = 'ansico-cv-frame-design';
|
||||
}
|
||||
return $classes;
|
||||
});
|
||||
|
||||
add_filter('the_content', function($content) {
|
||||
if (is_page() && in_the_loop() && is_main_query()) {
|
||||
$hide_title = get_post_meta(get_the_ID(), 'ansico_cv_hide_title', true);
|
||||
$frame_design = get_post_meta(get_the_ID(), 'ansico_cv_frame_design', true);
|
||||
if ($frame_design) {
|
||||
$title_html = '';
|
||||
if (!$hide_title) {
|
||||
$title_html = '<h1 class="cv-page-framed-title">' . get_the_title() . '</h1>';
|
||||
}
|
||||
$content = '<div class="cv-page-frame-container">' . $title_html . '<div class="cv-page-frame-inner">' . $content . '</div></div>';
|
||||
}
|
||||
}
|
||||
return $content;
|
||||
});
|
||||
|
||||
add_action('rest_api_init', function () {
|
||||
register_rest_route('cv-blocks/v1', '/send-message', array('methods' => 'POST', 'callback' => 'cv_blocks_send_email_callback', 'permission_callback' => '__return_true'));
|
||||
});
|
||||
|
||||
function cv_blocks_send_email_callback($request) {
|
||||
$params = $request->get_params();
|
||||
$receiver = sanitize_email($params['receiver']);
|
||||
$name = sanitize_text_field($params['name']);
|
||||
$email = sanitize_email($params['email']);
|
||||
$subject = sanitize_text_field($params['subject']);
|
||||
$message = sanitize_textarea_field($params['message']);
|
||||
$site_name = get_bloginfo('name');
|
||||
if (empty($receiver) || empty($name) || empty($email) || empty($message)) return new WP_Error('missing_data', 'Missing required fields', array('status' => 400));
|
||||
$headers = array('From: ' . $name . ' <' . $email . '>', 'Reply-To: ' . $name . ' <' . $email . '>', 'Content-Type: text/plain; charset=UTF-8');
|
||||
$email_subject = $subject ? "New Contact Message: " . $subject : "New Contact Message from " . $name;
|
||||
$email_body = "You have received a new message from your CV Contact Form on website " . $site_name . ".\n\nName: " . $name . "\nEmail: " . $email . "\nSubject: " . $subject . "\n\nMessage:\n" . $message . "\n";
|
||||
$sent = wp_mail($receiver, $email_subject, $email_body, $headers);
|
||||
if ($sent) return rest_ensure_response(array('success' => true));
|
||||
else return new WP_Error('mail_failed', 'Could not send email', array('status' => 500));
|
||||
}
|
||||
184
ansico-cv-blocks/block.js
Normal file
184
ansico-cv-blocks/block.js
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
const el = wp.element.createElement;
|
||||
const { registerBlockType } = wp.blocks;
|
||||
const { RichText, InnerBlocks, MediaUpload, InspectorControls, URLInput } = wp.blockEditor;
|
||||
const { Button, PanelBody, ToggleControl, TextControl, ButtonGroup } = wp.components;
|
||||
const { useSelect, useDispatch } = wp.data;
|
||||
|
||||
const moveItem = (list, fromIndex, toIndex) => {
|
||||
const result = Array.from(list);
|
||||
const [removed] = result.splice(fromIndex, 1);
|
||||
result.splice(toIndex, 0, removed);
|
||||
return result;
|
||||
};
|
||||
|
||||
registerBlockType('cv/company', {
|
||||
title: 'Company (CV)', icon: 'building', category: 'layout',
|
||||
attributes: { name: { type: 'string', source: 'html', selector: '.cv-company-name' }, dates: { type: 'string', source: 'html', selector: '.cv-company-dates' } },
|
||||
edit: ({ attributes, setAttributes }) => el('div', { className: 'cv-company' }, el(RichText, { tagName: 'div', className: 'cv-company-name', placeholder: 'Company Name', value: attributes.name, onChange: val => setAttributes({ name: val }) }), el(RichText, { tagName: 'div', className: 'cv-company-dates', placeholder: 'Years', value: attributes.dates, onChange: val => setAttributes({ dates: val }) }), el(InnerBlocks, { allowedBlocks: ['cv/position'], template: [['cv/position']] })),
|
||||
save: ({ attributes }) => el('div', { className: 'cv-company' }, el(RichText.Content, { tagName: 'div', className: 'cv-company-name', value: attributes.name }), el(RichText.Content, { tagName: 'div', className: 'cv-company-dates', value: attributes.dates }), el(InnerBlocks.Content, null))
|
||||
});
|
||||
|
||||
registerBlockType('cv/position', {
|
||||
title: 'Position (CV)', icon: 'id', parent: ['cv/company'], category: 'layout',
|
||||
attributes: { title: { type: 'string', source: 'html', selector: '.cv-position-title' }, department: { type: 'string', source: 'html', selector: '.cv-position-department' }, dates: { type: 'string', source: 'html', selector: '.cv-position-dates' } },
|
||||
edit: ({ attributes, setAttributes }) => el('div', { className: 'cv-position' }, el(RichText, { tagName: 'div', className: 'cv-position-title', placeholder: 'Job Title', value: attributes.title, onChange: val => setAttributes({ title: val }) }), el(RichText, { tagName: 'div', className: 'cv-position-department', placeholder: 'Department / Unit', value: attributes.department, onChange: val => setAttributes({ department: val }) }), el(RichText, { tagName: 'div', className: 'cv-position-dates', placeholder: 'Period', value: attributes.dates, onChange: val => setAttributes({ dates: val }) }), el(InnerBlocks, { allowedBlocks: ['core/list'], template: [['core/list']] })),
|
||||
save: ({ attributes }) => el('div', { className: 'cv-position' }, el(RichText.Content, { tagName: 'div', className: 'cv-position-title', value: attributes.title }), el(RichText.Content, { tagName: 'div', className: 'cv-position-department', value: attributes.department }), el(RichText.Content, { tagName: 'div', className: 'cv-position-dates', value: attributes.dates }), el(InnerBlocks.Content, null))
|
||||
});
|
||||
|
||||
registerBlockType('cv/education', {
|
||||
title: 'Education (CV)', icon: 'welcome-learn-more', category: 'layout',
|
||||
attributes: { degree: { type: 'string', source: 'html', selector: '.cv-education-degree' }, institution: { type: 'string', source: 'html', selector: '.cv-education-institution' }, dates: { type: 'string', source: 'html', selector: '.cv-education-dates' } },
|
||||
edit: ({ attributes, setAttributes }) => el('div', { className: 'cv-education' }, el(RichText, { tagName: 'div', className: 'cv-education-degree', placeholder: 'Degree / Qualification', value: attributes.degree, onChange: val => setAttributes({ degree: val }) }), el(RichText, { tagName: 'div', className: 'cv-education-institution', placeholder: 'University or School', value: attributes.institution, onChange: val => setAttributes({ institution: val }) }), el(RichText, { tagName: 'div', className: 'cv-education-dates', placeholder: 'Years', value: attributes.dates, onChange: val => setAttributes({ dates: val }) })),
|
||||
save: ({ attributes }) => el('div', { className: 'cv-education' }, el(RichText.Content, { tagName: 'div', className: 'cv-education-degree', value: attributes.degree }), el(RichText.Content, { tagName: 'div', className: 'cv-education-institution', value: attributes.institution }), el(RichText.Content, { tagName: 'div', className: 'cv-education-dates', value: attributes.dates }))
|
||||
});
|
||||
|
||||
registerBlockType('cv/profileheader', {
|
||||
title: 'Profile Header (CV)', icon: 'id-alt', category: 'layout',
|
||||
supports: { align: ['wide', 'full'] },
|
||||
attributes: { headerImage: { type: 'string', default: '' }, profileImage: { type: 'string', default: '' }, name: { type: 'string', source: 'html', selector: '.cv-profileheader-name' }, title: { type: 'string', source: 'html', selector: '.cv-profileheader-title' }, company: { type: 'string', source: 'html', selector: '.cv-profileheader-company' }, showMenu: { type: 'boolean', default: false }, menuItems: { type: 'array', default: [] } },
|
||||
edit: ({ attributes, setAttributes }) => {
|
||||
const updateMenuItem = (index, key, value) => { const newItems = [...attributes.menuItems]; newItems[index] = { ...newItems[index], [key]: value }; setAttributes({ menuItems: newItems }); };
|
||||
const addMenuItem = () => setAttributes({ menuItems: [...attributes.menuItems, { label: '', url: '' }] });
|
||||
const removeMenuItem = (index) => { const newItems = attributes.menuItems.filter((_, i) => i !== index); setAttributes({ menuItems: newItems }); };
|
||||
const moveMenuItemAt = (index, direction) => { const newPos = index + direction; if (newPos >= 0 && newPos < attributes.menuItems.length) setAttributes({ menuItems: moveItem(attributes.menuItems, index, newPos) }); };
|
||||
return [
|
||||
el(InspectorControls, {}, el(PanelBody, { title: 'Navigation Menu', initialOpen: true }, el(ToggleControl, { label: 'Enable Navigation Menu', checked: attributes.showMenu, onChange: (val) => setAttributes({ showMenu: val }) }))),
|
||||
el('div', { className: 'cv-profileheader' },
|
||||
el('div', { className: 'cv-profileheader-cover', style: { backgroundImage: attributes.headerImage ? `url(${attributes.headerImage})` : 'none' } }, el(MediaUpload, { onSelect: media => setAttributes({ headerImage: media.url }), type: 'image', render: ({ open }) => el(Button, { onClick: open, className: 'button', style: { position: 'absolute', top: '10px', right: '10px', backgroundColor: 'rgba(255,255,255,0.9)' } }, attributes.headerImage ? 'Change Header' : 'Add Header Image') })),
|
||||
el('div', { className: 'cv-profileheader-avatar-container' }, el(MediaUpload, { onSelect: media => setAttributes({ profileImage: media.url }), type: 'image', render: ({ open }) => el('div', { onClick: open, style: { cursor: 'pointer' }, title: 'Click to set profile picture' }, attributes.profileImage ? el('img', { src: attributes.profileImage, className: 'cv-profileheader-avatar' }) : el('div', { className: 'cv-profileheader-avatar placeholder' }, 'Add Photo')) })),
|
||||
el('div', { className: 'cv-profileheader-info' }, el(RichText, { tagName: 'div', className: 'cv-profileheader-name', placeholder: 'Full Name', value: attributes.name, onChange: val => setAttributes({ name: val }) }), el(RichText, { tagName: 'div', className: 'cv-profileheader-title', placeholder: 'Current Job Title', value: attributes.title, onChange: val => setAttributes({ title: val }) }), el(RichText, { tagName: 'div', className: 'cv-profileheader-company', placeholder: 'Company or Organization', value: attributes.company, onChange: val => setAttributes({ company: val }) })),
|
||||
attributes.showMenu && el('div', { className: 'cv-profileheader-nav-editor' }, attributes.menuItems.map((item, index) => el('div', { className: 'cv-menu-item-edit', key: index }, el('div', { className: 'cv-menu-item-fields' }, el(TextControl, { placeholder: 'Label', value: item.label, onChange: val => updateMenuItem(index, 'label', val) }), el(URLInput, { className: 'cv-url-input', value: item.url, onChange: val => updateMenuItem(index, 'url', val), disableSuggestions: false, isFullWidth: true, placeholder: 'Link or Search Page...' })), el(ButtonGroup, {}, el(Button, { icon: 'arrow-up-alt2', onClick: () => moveMenuItemAt(index, -1), disabled: index === 0 }), el(Button, { icon: 'arrow-down-alt2', onClick: () => moveMenuItemAt(index, 1), disabled: index === attributes.menuItems.length - 1 }), el(Button, { isDestructive: true, onClick: () => removeMenuItem(index) }, 'Remove')))), el(Button, { isPrimary: true, onClick: addMenuItem }, 'Add Menu Item'))
|
||||
)
|
||||
];
|
||||
},
|
||||
save: ({ attributes }) => el('div', { className: 'cv-profileheader' }, el('div', { className: 'cv-profileheader-cover', style: { backgroundImage: attributes.headerImage ? `url(${attributes.headerImage})` : 'none' } }), el('div', { className: 'cv-profileheader-avatar-container' }, attributes.profileImage ? el('img', { src: attributes.profileImage, className: 'cv-profileheader-avatar', alt: '' }) : null), el('div', { className: 'cv-profileheader-info' }, el(RichText.Content, { tagName: 'div', className: 'cv-profileheader-name', value: attributes.name }), el(RichText.Content, { tagName: 'div', className: 'cv-profileheader-title', value: attributes.title }), el(RichText.Content, { tagName: 'div', className: 'cv-profileheader-company', value: attributes.company })), attributes.showMenu && attributes.menuItems.length > 0 && el('nav', { className: 'cv-profileheader-nav' }, el('ul', {}, attributes.menuItems.map((item, index) => el('li', { key: index }, el('a', { href: item.url }, item.label))))))
|
||||
});
|
||||
|
||||
registerBlockType('cv/contentblock', {
|
||||
title: 'Content Block (CV)', icon: 'layout', category: 'layout',
|
||||
attributes: { heading: { type: 'string', source: 'html', selector: '.cv-contentblock-heading' }, includeInToc: { type: 'boolean', default: true } },
|
||||
edit: ({ attributes, setAttributes }) => {
|
||||
return [
|
||||
el(InspectorControls, {}, el(PanelBody, { title: 'Table of Contents', initialOpen: true }, el(ToggleControl, { label: 'Include in Table of Contents', checked: attributes.includeInToc, onChange: (val) => setAttributes({ includeInToc: val }) }))),
|
||||
el('div', { className: 'cv-contentblock' }, el(RichText, { tagName: 'h2', className: 'cv-contentblock-heading', placeholder: 'Section Heading', value: attributes.heading, onChange: val => setAttributes({ heading: val }) }), el('div', { className: 'cv-contentblock-inner' }, el(InnerBlocks, {})))
|
||||
];
|
||||
},
|
||||
save: ({ attributes }) => el('div', { className: 'cv-contentblock', 'data-include-toc': attributes.includeInToc }, el(RichText.Content, { tagName: 'h2', className: 'cv-contentblock-heading', value: attributes.heading }), el('div', { className: 'cv-contentblock-inner' }, el(InnerBlocks.Content, null)))
|
||||
});
|
||||
|
||||
registerBlockType('cv/container', {
|
||||
title: 'Container (CV)', icon: 'editor-expand', category: 'layout',
|
||||
edit: () => el('div', { className: 'cv-container' }, el('div', { className: 'cv-container-inner' }, el(InnerBlocks, {}))),
|
||||
save: () => el('div', { className: 'cv-container' }, el('div', { className: 'cv-container-inner' }, el(InnerBlocks.Content, null)))
|
||||
});
|
||||
|
||||
registerBlockType('cv/contactform', {
|
||||
title: 'Contact Form (CV)', icon: 'email', category: 'layout',
|
||||
attributes: { nameLabel: { type: 'string', default: 'Name' }, emailLabel: { type: 'string', default: 'Email' }, subjectLabel: { type: 'string', default: 'Subject' }, messageLabel: { type: 'string', default: 'Message' }, buttonLabel: { type: 'string', default: 'Send Message' }, receiverEmail: { type: 'string', default: '' }, successMessage: { type: 'string', default: 'The message has been sent!' }, enableControlField: { type: 'boolean', default: false }, controlQuestion: { type: 'string', default: 'What is 2+2?' }, controlAnswer: { type: 'string', default: '4' } },
|
||||
edit: ({ attributes, setAttributes }) => {
|
||||
return [
|
||||
el(InspectorControls, {}, el(PanelBody, { title: 'Contact Form Settings', initialOpen: true }, el(TextControl, { label: 'Receiver Email Address', placeholder: 'your@email.com', value: attributes.receiverEmail, onChange: val => setAttributes({ receiverEmail: val }) }), el(TextControl, { label: 'Success Message', value: attributes.successMessage, onChange: val => setAttributes({ successMessage: val }) }), el(ToggleControl, { label: 'Enable Control Question (Anti-spam)', checked: attributes.enableControlField, onChange: val => setAttributes({ enableControlField: val }) }), attributes.enableControlField && el(TextControl, { label: 'Control Question', value: attributes.controlQuestion, onChange: val => setAttributes({ controlQuestion: val }) }), attributes.enableControlField && el(TextControl, { label: 'Expected Answer', value: attributes.controlAnswer, onChange: val => setAttributes({ controlAnswer: val }) }))),
|
||||
el('div', { className: 'cv-contactform' }, el('div', { className: 'cv-form-group' }, el(RichText, { tagName: 'label', value: attributes.nameLabel, onChange: (val) => setAttributes({ nameLabel: val }) }), el('input', { type: 'text', disabled: true })), el('div', { className: 'cv-form-group' }, el(RichText, { tagName: 'label', value: attributes.emailLabel, onChange: (val) => setAttributes({ emailLabel: val }) }), el('input', { type: 'email', disabled: true })), el('div', { className: 'cv-form-group' }, el(RichText, { tagName: 'label', value: attributes.subjectLabel, onChange: (val) => setAttributes({ subjectLabel: val }) }), el('input', { type: 'text', disabled: true })), el('div', { className: 'cv-form-group' }, el(RichText, { tagName: 'label', value: attributes.messageLabel, onChange: (val) => setAttributes({ messageLabel: val }) }), el('textarea', { disabled: true, rows: 10 })), attributes.enableControlField && el('div', { className: 'cv-form-group' }, el('label', {}, attributes.controlQuestion), el('input', { type: 'text', disabled: true })), el('div', { className: 'cv-form-group' }, el(RichText, { tagName: 'button', className: 'cv-contactform-button', value: attributes.buttonLabel, onChange: (val) => setAttributes({ buttonLabel: val }) })))
|
||||
];
|
||||
},
|
||||
save: ({ attributes }) => {
|
||||
return el('div', { className: 'cv-contactform' }, el('form', { action: '#', method: 'post', 'data-receiver': attributes.receiverEmail, 'data-success': attributes.successMessage, 'data-control-answer': attributes.enableControlField ? attributes.controlAnswer : '' }, el('div', { className: 'cv-form-group' }, el('label', {}, attributes.nameLabel), el('input', { type: 'text', name: 'cv-name', required: true })), el('div', { className: 'cv-form-group' }, el('label', {}, attributes.emailLabel), el('input', { type: 'email', name: 'cv-email', required: true })), el('div', { className: 'cv-form-group' }, el('label', {}, attributes.subjectLabel), el('input', { type: 'text', name: 'cv-subject' })), el('div', { className: 'cv-form-group' }, el('label', {}, attributes.messageLabel), el('textarea', { name: 'cv-message', rows: 10, required: true })), attributes.enableControlField && el('div', { className: 'cv-form-group' }, el('label', {}, attributes.controlQuestion), el('input', { type: 'text', name: 'cv-control', required: true })), el('button', { type: 'submit', className: 'cv-contactform-button' }, attributes.buttonLabel)), el('div', { className: 'cv-contactform-feedback', style: { display: 'none' } }));
|
||||
}
|
||||
});
|
||||
|
||||
registerBlockType('cv/toc', {
|
||||
title: 'Table of Contents (CV)', icon: 'list-view', category: 'layout',
|
||||
attributes: {
|
||||
heading: { type: 'string', source: 'html', selector: '.cv-toc-heading', default: 'Table of Content' }
|
||||
},
|
||||
edit: ({ attributes, setAttributes }) => {
|
||||
const headings = useSelect(select => {
|
||||
const blocks = select('core/block-editor').getBlocks();
|
||||
const findHeadings = (blocksList) => {
|
||||
let result = [];
|
||||
blocksList.forEach(block => {
|
||||
if (block.name === 'cv/contentblock' && block.attributes.includeInToc !== false && block.attributes.heading) result.push(block.attributes.heading.replace(/<[^>]+>/g, ''));
|
||||
if (block.innerBlocks) result = result.concat(findHeadings(block.innerBlocks));
|
||||
});
|
||||
return result;
|
||||
};
|
||||
return findHeadings(blocks);
|
||||
}, []);
|
||||
return el('div', { className: 'cv-toc cv-contentblock' },
|
||||
el(RichText, { tagName: 'h2', className: 'cv-toc-heading cv-contentblock-heading', value: attributes.heading, onChange: val => setAttributes({ heading: val }), placeholder: 'Table of Content' }),
|
||||
el('div', { className: 'cv-toc-editor-preview' },
|
||||
headings.length > 0 ? el('ul', { className: 'cv-toc-list' }, headings.map((h, i) => el('li', { key: i }, el('a', { href: '#' }, h)))) : el('p', { style: { color: '#666', fontStyle: 'italic', margin: 0, fontSize: '0.9rem' } }, 'Add Content Blocks to see headings.')
|
||||
)
|
||||
);
|
||||
},
|
||||
save: ({ attributes }) => el('div', { className: 'cv-toc cv-contentblock' },
|
||||
el(RichText.Content, { tagName: 'h2', className: 'cv-toc-heading cv-contentblock-heading', value: attributes.heading })
|
||||
)
|
||||
});
|
||||
|
||||
registerBlockType('cv/profile', {
|
||||
title: 'Profile (CV)', icon: 'admin-users', category: 'layout',
|
||||
attributes: { profileImage: { type: 'string', default: '' }, name: { type: 'string', source: 'html', selector: '.cv-profile-name' }, titles: { type: 'string', source: 'html', selector: '.cv-profile-titles' }, showSocial: { type: 'boolean', default: false } },
|
||||
edit: ({ attributes, setAttributes }) => {
|
||||
return [
|
||||
el(InspectorControls, {}, el(PanelBody, { title: 'Profile Settings', initialOpen: true }, el(ToggleControl, { label: 'Enable Social Icons', checked: attributes.showSocial, onChange: (val) => setAttributes({ showSocial: val }) }))),
|
||||
el('div', { className: 'cv-profile' },
|
||||
el('div', { className: 'cv-profile-avatar-container' }, el(MediaUpload, { onSelect: media => setAttributes({ profileImage: media.url }), type: 'image', render: ({ open }) => el('div', { onClick: open, style: { cursor: 'pointer' } }, attributes.profileImage ? el('img', { src: attributes.profileImage, className: 'cv-profile-avatar' }) : el('div', { className: 'cv-profile-avatar placeholder' }, 'Add Photo')) })),
|
||||
el(RichText, { tagName: 'div', className: 'cv-profile-name', placeholder: 'Full Name', value: attributes.name, onChange: val => setAttributes({ name: val }) }),
|
||||
attributes.showSocial && el('div', { className: 'cv-profile-social-container' }, el(InnerBlocks, { allowedBlocks: ['core/social-links'], template: [['core/social-links']] })),
|
||||
el(RichText, { tagName: 'div', className: 'cv-profile-titles', placeholder: 'Titles (Press Enter for new line)', value: attributes.titles, onChange: val => setAttributes({ titles: val }) })
|
||||
)
|
||||
];
|
||||
},
|
||||
save: ({ attributes }) => el('div', { className: 'cv-profile' }, el('div', { className: 'cv-profile-avatar-container' }, attributes.profileImage ? el('img', { src: attributes.profileImage, className: 'cv-profile-avatar', alt: '' }) : null), el(RichText.Content, { tagName: 'div', className: 'cv-profile-name', value: attributes.name }), attributes.showSocial && el('div', { className: 'cv-profile-social-container' }, el(InnerBlocks.Content, null)), el(RichText.Content, { tagName: 'div', className: 'cv-profile-titles', value: attributes.titles }))
|
||||
});
|
||||
|
||||
registerBlockType('cv/topnav', {
|
||||
title: 'Top Navigation (CV)', icon: 'menu', category: 'layout',
|
||||
supports: { align: ['wide', 'full'] },
|
||||
attributes: {
|
||||
siteName: { type: 'string', source: 'html', selector: '.cv-topnav-name', default: '' },
|
||||
menuItems: { type: 'array', default: [] }
|
||||
},
|
||||
edit: ({ attributes, setAttributes }) => {
|
||||
const updateMenuItem = (index, key, value) => { const newItems = [...attributes.menuItems]; newItems[index] = { ...newItems[index], [key]: value }; setAttributes({ menuItems: newItems }); };
|
||||
const addMenuItem = () => setAttributes({ menuItems: [...attributes.menuItems, { label: '', url: '' }] });
|
||||
const removeMenuItem = (index) => { const newItems = attributes.menuItems.filter((_, i) => i !== index); setAttributes({ menuItems: newItems }); };
|
||||
const moveMenuItemAt = (index, direction) => { const newPos = index + direction; if (newPos >= 0 && newPos < attributes.menuItems.length) setAttributes({ menuItems: moveItem(attributes.menuItems, index, newPos) }); };
|
||||
const siteTitle = useSelect(select => select('core').getSite()?.title);
|
||||
wp.element.useEffect(() => { if (!attributes.siteName && siteTitle) setAttributes({ siteName: siteTitle }); }, [siteTitle]);
|
||||
|
||||
return el('div', { className: 'cv-topnav' },
|
||||
el('div', { className: 'cv-topnav-link-wrapper' },
|
||||
el(RichText, { tagName: 'h1', className: 'cv-topnav-name', placeholder: 'Site Name', value: attributes.siteName, onChange: val => setAttributes({ siteName: val }) })
|
||||
),
|
||||
el('div', { className: 'cv-topnav-menu-editor' },
|
||||
attributes.menuItems.map((item, index) => el('div', { className: 'cv-menu-item-edit', key: index }, el('div', { className: 'cv-menu-item-fields' }, el(TextControl, { placeholder: 'Label', value: item.label, onChange: val => updateMenuItem(index, 'label', val) }), el(URLInput, { className: 'cv-url-input', value: item.url, onChange: val => updateMenuItem(index, 'url', val), disableSuggestions: false, isFullWidth: true, placeholder: 'Link or Search Page...' })), el(ButtonGroup, {}, el(Button, { icon: 'arrow-up-alt2', onClick: () => moveMenuItemAt(index, -1), disabled: index === 0 }), el(Button, { icon: 'arrow-down-alt2', onClick: () => moveMenuItemAt(index, 1), disabled: index === attributes.menuItems.length - 1 }), el(Button, { isDestructive: true, onClick: () => removeMenuItem(index) }, 'Remove')))),
|
||||
el(Button, { isPrimary: true, onClick: addMenuItem, style: {marginTop: '10px'} }, 'Add Menu Link')
|
||||
),
|
||||
el('div', { className: 'cv-topnav-line' })
|
||||
);
|
||||
},
|
||||
save: ({ attributes }) => el('header', { className: 'cv-topnav' },
|
||||
el('a', { href: '/', className: 'cv-topnav-home-link' }, el(RichText.Content, { tagName: 'h1', className: 'cv-topnav-name', value: attributes.siteName })),
|
||||
attributes.menuItems.length > 0 && el('nav', { className: 'cv-topnav-nav' }, el('ul', {}, attributes.menuItems.map((item, index) => el('li', { key: index }, el('a', { href: item.url }, item.label))))),
|
||||
el('div', { className: 'cv-topnav-line' })
|
||||
)
|
||||
});
|
||||
|
||||
if (wp.plugins && wp.editPost) {
|
||||
const { registerPlugin } = wp.plugins;
|
||||
const { PluginDocumentSettingPanel } = wp.editPost;
|
||||
const PageSettingsPanel = () => {
|
||||
const postType = useSelect(select => select('core/editor').getCurrentPostType(), []);
|
||||
if (postType !== 'page') return null;
|
||||
const meta = useSelect(select => select('core/editor').getEditedPostAttribute('meta'), []);
|
||||
const { editPost } = useDispatch('core/editor');
|
||||
if (!meta) return null;
|
||||
return el(PluginDocumentSettingPanel, { name: 'ansico-cv-page-settings', title: 'CV Page Settings', icon: 'layout' },
|
||||
el(ToggleControl, { label: 'Hide Page Title (H1)', checked: meta.ansico_cv_hide_title || false, onChange: (val) => editPost({ meta: { ...meta, ansico_cv_hide_title: val } }) }),
|
||||
el(ToggleControl, { label: 'Enable Frame Design', help: 'Wraps the page content in a CV-styled box.', checked: meta.ansico_cv_frame_design || false, onChange: (val) => editPost({ meta: { ...meta, ansico_cv_frame_design: val } }) })
|
||||
);
|
||||
};
|
||||
registerPlugin('ansico-cv-page-settings-plugin', { render: PageSettingsPanel });
|
||||
}
|
||||
19
ansico-cv-blocks/editor.css
Normal file
19
ansico-cv-blocks/editor.css
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
.cv-company { border-left: 4px solid #2271b1; padding-left: 10px; }
|
||||
.cv-position { border-left: 2px solid #ddd; padding-left: 10px; }
|
||||
.cv-education { border-left: 4px solid #46b450; padding-left: 10px; margin-bottom: 1rem; }
|
||||
.cv-profileheader { border-left: 4px solid #ffba00; border-radius: 0; }
|
||||
.cv-contentblock, .cv-toc { border-left: 4px solid #9b51e0; border-radius: 0; }
|
||||
.cv-container { border-left: 4px solid #36c3d9; border-radius: 0; }
|
||||
.cv-contactform { border-left: 4px solid #ff5722; padding-left: 15px; }
|
||||
.cv-profile { border-left: 4px solid #f39c12; border-radius: 0; text-align: center; }
|
||||
.cv-topnav { border-left: 4px solid #ff004f; padding-left: 15px; margin-bottom: 2rem; }
|
||||
.cv-profile-avatar-container { display: flex; justify-content: center; }
|
||||
.cv-profile-social-container { min-height: 20px; background: #fafafa; border: 1px dashed #ddd; }
|
||||
.cv-profileheader-nav-editor, .cv-topnav-menu-editor { padding: 15px 24px; background: #f9f9f9; border-top: 1px solid #eee; }
|
||||
.cv-menu-item-edit { display: flex; gap: 10px; margin-bottom: 15px; align-items: flex-end; border-bottom: 1px dashed #ccc; padding-bottom: 15px; }
|
||||
.cv-menu-item-fields { flex: 1; }
|
||||
.cv-url-input { border: 1px solid #ccc; padding: 2px; background: #fff; margin-top: 5px; width: 100%; }
|
||||
.cv-contactform input:disabled, .cv-contactform textarea:disabled { background: #f5f5f5; cursor: not-allowed; }
|
||||
.cv-toc { display: block !important; }
|
||||
.cv-toc-editor-preview { padding: 20px 0 0 0; }
|
||||
.cv-toc-editor-preview .cv-toc-list a { pointer-events: none; }
|
||||
51
ansico-cv-blocks/frontend.js
Normal file
51
ansico-cv-blocks/frontend.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const forms = document.querySelectorAll('.cv-contactform form');
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const btn = form.querySelector('button[type="submit"]');
|
||||
const feedback = form.parentElement.querySelector('.cv-contactform-feedback');
|
||||
const originalBtnText = btn.innerText;
|
||||
const receiver = form.getAttribute('data-receiver');
|
||||
if (!receiver) { alert('Receiver email missing.'); return; }
|
||||
const expectedAnswer = form.getAttribute('data-control-answer');
|
||||
if (expectedAnswer && form.querySelector('[name="cv-control"]').value.trim().toLowerCase() !== expectedAnswer.trim().toLowerCase()) { alert('Incorrect answer.'); return; }
|
||||
btn.innerText = 'Sending...'; btn.disabled = true;
|
||||
const data = { receiver: receiver, name: form.querySelector('[name="cv-name"]').value, email: form.querySelector('[name="cv-email"]').value, subject: form.querySelector('[name="cv-subject"]').value, message: form.querySelector('[name="cv-message"]').value };
|
||||
fetch('/wp-json/cv-blocks/v1/send-message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then(res => res.json()).then(res => {
|
||||
if (res.success) { form.style.display = 'none'; feedback.innerHTML = '<div class="cv-success-msg">' + (form.getAttribute('data-success') || 'Sent!') + '</div>'; feedback.style.display = 'block'; }
|
||||
else { alert('Error.'); btn.innerText = originalBtnText; btn.disabled = false; }
|
||||
});
|
||||
});
|
||||
});
|
||||
const tocBlocks = document.querySelectorAll('.cv-toc');
|
||||
if (tocBlocks.length > 0) {
|
||||
const contentBlocks = document.querySelectorAll('.cv-contentblock');
|
||||
tocBlocks.forEach(container => {
|
||||
let hasItems = false;
|
||||
const ul = document.createElement('ul');
|
||||
ul.className = 'cv-toc-list';
|
||||
contentBlocks.forEach((b, i) => {
|
||||
if (b.getAttribute('data-include-toc') === 'false') return;
|
||||
const h = b.querySelector('.cv-contentblock-heading');
|
||||
if (!h || !h.textContent.trim()) return;
|
||||
if (b.classList.contains('cv-toc')) return;
|
||||
if (!b.id) b.id = 'cv-sec-' + i;
|
||||
const li = document.createElement('li');
|
||||
const a = document.createElement('a');
|
||||
a.href = '#' + b.id;
|
||||
a.textContent = h.textContent;
|
||||
a.onclick = (e) => { e.preventDefault(); document.getElementById(b.id).scrollIntoView({ behavior: 'smooth' }); };
|
||||
li.appendChild(a);
|
||||
ul.appendChild(li);
|
||||
hasItems = true;
|
||||
});
|
||||
if (hasItems) {
|
||||
container.appendChild(ul);
|
||||
container.style.display = 'block';
|
||||
} else {
|
||||
container.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
31
ansico-cv-blocks/readme.txt
Normal file
31
ansico-cv-blocks/readme.txt
Normal 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.
|
||||
82
ansico-cv-blocks/style.css
Normal file
82
ansico-cv-blocks/style.css
Normal 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; }
|
||||
|
|
@ -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)
|
||||
)
|
||||
});
|
||||
|
|
@ -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');
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
.cv-company {
|
||||
border-left: 4px solid #2271b1;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.cv-position {
|
||||
border-left: 2px solid #ddd;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in a new issue