diff --git a/plugins/carhop-blocks/src/company-timeline/block.json b/plugins/carhop-blocks/src/company-timeline/block.json new file mode 100644 index 0000000..7e677e3 --- /dev/null +++ b/plugins/carhop-blocks/src/company-timeline/block.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "telex/block-company-timeline", + "version": "0.1.0", + "title": "Company Timeline", + "category": "design", + "icon": "calendar-alt", + "description": "Display company milestones with a fixed sidebar navigation and scroll-based highlighting", + "keywords": [ + "timeline", + "history", + "milestones", + "chronology", + "events" + ], + "attributes": { + "entries": { + "type": "array", + "default": [ + { + "year": "2020", + "title": "Company Founded", + "description": "Our journey began with a vision to make a difference.", + "imageUrl": "", + "imageId": 0 + } + ] + } + }, + "example": { + "attributes": { + "entries": [ + { + "year": "2010", + "title": "The Beginning", + "description": "Founded with a mission to innovate.", + "imageUrl": "", + "imageId": 0 + }, + { + "year": "2015", + "title": "Major Milestone", + "description": "Reached 1 million customers worldwide.", + "imageUrl": "", + "imageId": 0 + }, + { + "year": "2020", + "title": "Global Expansion", + "description": "Opened offices in 25 countries.", + "imageUrl": "", + "imageId": 0 + } + ] + } + }, + "supports": { + "html": false, + "anchor": true, + "align": [ + "wide", + "full" + ] + }, + "textdomain": "company-timeline", + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css", + "viewScript": "file:./view.js" +} \ No newline at end of file diff --git a/plugins/carhop-blocks/src/company-timeline/edit.js b/plugins/carhop-blocks/src/company-timeline/edit.js new file mode 100644 index 0000000..73b9835 --- /dev/null +++ b/plugins/carhop-blocks/src/company-timeline/edit.js @@ -0,0 +1,219 @@ +import { __ } from "@wordpress/i18n"; + +import { + useBlockProps, + InspectorControls, + MediaUpload, + MediaUploadCheck, + RichText, +} from "@wordpress/block-editor"; + +import { + PanelBody, + Button, + TextControl, + Card, + CardBody, + CardHeader, + IconButton, +} from "@wordpress/components"; + +import { useState } from "@wordpress/element"; + +import "./editor.scss"; + +export default function Edit({ attributes, setAttributes }) { + const { entries } = attributes; + const [selectedYear, setSelectedYear] = useState(null); + + const addEntry = () => { + const newEntries = [ + ...entries, + { + year: new Date().getFullYear().toString(), + title: "", + description: "", + imageUrl: "", + imageId: 0, + }, + ]; + setAttributes({ entries: newEntries }); + }; + + const updateEntry = (index, field, value) => { + const newEntries = [...entries]; + newEntries[index] = { + ...newEntries[index], + [field]: value, + }; + setAttributes({ entries: newEntries }); + }; + + const removeEntry = (index) => { + const newEntries = entries.filter((_, i) => i !== index); + setAttributes({ entries: newEntries }); + }; + + const sortedEntries = [...entries].sort( + (a, b) => parseInt(a.year) - parseInt(b.year) + ); + + const years = [...new Set(sortedEntries.map((entry) => entry.year))]; + + return ( + <> + + + + + {sortedEntries.map((entry, index) => { + const originalIndex = entries.findIndex( + (e) => + e.year === entry.year && + e.title === entry.title && + e.description === entry.description + ); + + return ( + + + + {entry.year || __("New Entry", "company-timeline")} + + + + + updateEntry(originalIndex, "year", value) + } + type="number" + /> + + + + ); + })} + + + +
+
+
+
+

{__("Timeline", "company-timeline")}

+ {years.map((year) => ( + + ))} +
+
+ +
+ {sortedEntries.map((entry, index) => { + const originalIndex = entries.findIndex( + (e) => + e.year === entry.year && + e.title === entry.title && + e.description === entry.description + ); + + return ( +
+
+

{entry.year}

+
+ +
+ + updateEntry(originalIndex, "title", value) + } + placeholder={__( + "Enter milestone title...", + "company-timeline" + )} + className="timeline-title" + /> + + + updateEntry(originalIndex, "description", value) + } + placeholder={__( + "Enter milestone description...", + "company-timeline" + )} + className="timeline-description" + /> + + + { + updateEntry(originalIndex, "imageUrl", media.url); + updateEntry(originalIndex, "imageId", media.id); + }} + allowedTypes={["image"]} + value={entry.imageId} + render={({ open }) => ( +
+ {entry.imageUrl ? ( + <> + {entry.title} + + + ) : ( + + )} +
+ )} + /> +
+
+
+ ); + })} +
+
+
+ + ); +} diff --git a/plugins/carhop-blocks/src/company-timeline/editor.scss b/plugins/carhop-blocks/src/company-timeline/editor.scss new file mode 100644 index 0000000..fde11df --- /dev/null +++ b/plugins/carhop-blocks/src/company-timeline/editor.scss @@ -0,0 +1,43 @@ +/** + * The following styles get applied inside the editor only. + * + * Replace them with your own styles or remove the file completely. + */ + +.wp-block-telex-company-timeline { + .timeline-entry-content { + .timeline-title, + .timeline-description { + &:focus { + outline: 2px solid #3b82f6; + outline-offset: 2px; + } + } + + .timeline-media { + button { + margin-top: 12px; + } + } + } +} + +.components-panel__body { + .components-card { + margin-bottom: 12px; + + .components-card__header { + border-bottom: 1px solid #e2e8f0; + } + + .components-card__body { + .components-base-control { + margin-bottom: 16px; + + &:last-child { + margin-bottom: 0; + } + } + } + } +} diff --git a/plugins/carhop-blocks/src/company-timeline/index.js b/plugins/carhop-blocks/src/company-timeline/index.js new file mode 100644 index 0000000..ade1e47 --- /dev/null +++ b/plugins/carhop-blocks/src/company-timeline/index.js @@ -0,0 +1,39 @@ +/** + * Registers a new block provided a unique name and an object defining its behavior. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ + */ +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. + * All files containing `style` keyword are bundled together. The code used + * gets applied both to the front of your site and to the editor. + * + * @see https://www.npmjs.com/package/@wordpress/scripts#using-css + */ +import './style.scss'; + +/** + * Internal dependencies + */ +import Edit from './edit'; +import save from './save'; +import metadata from './block.json'; + +/** + * Every block starts by registering a new block type definition. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ + */ +registerBlockType( metadata.name, { + /** + * @see ./edit.js + */ + edit: Edit, + + /** + * @see ./save.js + */ + save, +} ); diff --git a/plugins/carhop-blocks/src/company-timeline/save.js b/plugins/carhop-blocks/src/company-timeline/save.js new file mode 100644 index 0000000..a7bfa9f --- /dev/null +++ b/plugins/carhop-blocks/src/company-timeline/save.js @@ -0,0 +1,88 @@ +/** + * React hook that is used to mark the block wrapper element. + * It provides all the necessary props like the class name. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops + */ +import { useBlockProps, RichText } from '@wordpress/block-editor'; + +/** + * The save function defines the way in which the different attributes should + * be combined into the final markup, which is then serialized by the block + * editor into `post_content`. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save + * + * @param {Object} props Block properties + * @return {Element} Element to render. + */ +export default function save( { attributes } ) { + const { entries } = attributes; + + const sortedEntries = [ ...entries ].sort( ( a, b ) => + parseInt( a.year ) - parseInt( b.year ) + ); + + const years = [ ...new Set( sortedEntries.map( entry => entry.year ) ) ]; + + return ( +
+
+
+
+

Timeline

+ { years.map( ( year ) => ( + + { year } + + ) ) } +
+
+ +
+ { sortedEntries.map( ( entry, index ) => ( +
+
+

{ entry.year }

+
+ +
+ { entry.title && ( + + ) } + + { entry.description && ( + + ) } + + { entry.imageUrl && ( +
+ { +
+ ) } +
+
+ ) ) } +
+
+
+ ); +} diff --git a/plugins/carhop-blocks/src/company-timeline/style.scss b/plugins/carhop-blocks/src/company-timeline/style.scss new file mode 100644 index 0000000..37f96cf --- /dev/null +++ b/plugins/carhop-blocks/src/company-timeline/style.scss @@ -0,0 +1,198 @@ +/** + * The following styles get applied both on the front of your site + * and in the editor. + * + * Replace them with your own styles or remove the file completely. + */ + +.wp-block-telex-company-timeline { + display: flex; + gap: 60px; + max-width: 1200px; + margin: 0 auto; + padding: 40px 20px; + position: relative; + + .timeline-sidebar { + flex: 0 0 200px; + position: sticky; + top: 100px; + height: fit-content; + align-self: flex-start; + + .timeline-years { + background: #f8f9fa; + border-radius: 8px; + padding: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + + h3 { + margin: 0 0 20px 0; + font-size: 18px; + font-weight: 700; + color: #1e293b; + border-bottom: 2px solid #e2e8f0; + padding-bottom: 12px; + } + + .year-link { + display: block; + padding: 12px 16px; + margin-bottom: 8px; + text-decoration: none; + color: #64748b; + font-weight: 600; + font-size: 16px; + border-radius: 6px; + transition: all 0.3s ease; + cursor: pointer; + background: transparent; + border: none; + width: 100%; + text-align: left; + + &:hover { + background: #e2e8f0; + color: #1e293b; + transform: translateX(4px); + } + + &.active { + background: #3b82f6; + color: white; + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); + } + } + } + } + + .timeline-content { + flex: 1; + min-width: 0; + + .timeline-entry { + margin-bottom: 80px; + scroll-margin-top: 100px; + + &:last-child { + margin-bottom: 0; + } + + .timeline-year-marker { + margin-bottom: 24px; + position: relative; + padding-left: 40px; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 24px; + height: 24px; + background: #3b82f6; + border-radius: 50%; + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2); + } + + h2 { + margin: 0; + font-size: 32px; + font-weight: 800; + color: #1e293b; + line-height: 1; + } + } + + .timeline-entry-content { + background: white; + border-radius: 12px; + padding: 32px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + border-left: 4px solid #3b82f6; + + .timeline-title { + margin: 0 0 16px 0; + font-size: 24px; + font-weight: 700; + color: #1e293b; + } + + .timeline-description { + margin: 0 0 24px 0; + font-size: 16px; + line-height: 1.8; + color: #475569; + } + + .timeline-media { + margin-top: 24px; + + img { + width: 100%; + height: auto; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + } + } + } + } + + @media (max-width: 768px) { + flex-direction: column; + gap: 30px; + padding: 20px 15px; + + .timeline-sidebar { + position: static; + flex: 1; + + .timeline-years { + padding: 16px; + + h3 { + font-size: 16px; + margin-bottom: 12px; + } + + .year-link { + padding: 10px 12px; + font-size: 14px; + } + } + } + + .timeline-content { + .timeline-entry { + margin-bottom: 50px; + + .timeline-year-marker { + padding-left: 30px; + + &::before { + width: 18px; + height: 18px; + } + + h2 { + font-size: 24px; + } + } + + .timeline-entry-content { + padding: 20px; + + .timeline-title { + font-size: 20px; + } + + .timeline-description { + font-size: 15px; + } + } + } + } + } +} diff --git a/plugins/carhop-blocks/src/company-timeline/view.js b/plugins/carhop-blocks/src/company-timeline/view.js new file mode 100644 index 0000000..c277630 --- /dev/null +++ b/plugins/carhop-blocks/src/company-timeline/view.js @@ -0,0 +1,67 @@ +/** + * Use this file for JavaScript code that you want to run in the front-end + * on posts/pages that contain this block. + */ + +document.addEventListener( 'DOMContentLoaded', function() { + const timeline = document.querySelector( '.wp-block-telex-company-timeline' ); + + if ( ! timeline ) { + return; + } + + const sidebar = timeline.querySelector( '.timeline-sidebar' ); + const yearLinks = timeline.querySelectorAll( '.year-link' ); + const entries = timeline.querySelectorAll( '.timeline-entry' ); + + if ( ! sidebar || yearLinks.length === 0 || entries.length === 0 ) { + return; + } + + // Smooth scroll to year when clicking sidebar link + yearLinks.forEach( link => { + link.addEventListener( 'click', function( e ) { + e.preventDefault(); + const year = this.getAttribute( 'data-year' ) || this.getAttribute( 'href' ).replace( '#year-', '' ); + const targetEntry = timeline.querySelector( `[data-year="${ year }"]` ); + + if ( targetEntry ) { + targetEntry.scrollIntoView( { + behavior: 'smooth', + block: 'start' + } ); + } + } ); + } ); + + // Update active year on scroll + const observerOptions = { + root: null, + rootMargin: '-20% 0px -70% 0px', + threshold: 0 + }; + + const observerCallback = ( entries ) => { + entries.forEach( entry => { + if ( entry.isIntersecting ) { + const year = entry.target.getAttribute( 'data-year' ); + + // Remove active class from all links + yearLinks.forEach( link => link.classList.remove( 'active' ) ); + + // Add active class to current year + const activeLink = timeline.querySelector( `.year-link[data-year="${ year }"]` ); + if ( activeLink ) { + activeLink.classList.add( 'active' ); + } + } + } ); + }; + + const observer = new IntersectionObserver( observerCallback, observerOptions ); + + // Observe all timeline entries + entries.forEach( entry => { + observer.observe( entry ); + } ); +} );