FEATURE temporarely keeping this demonsrtration block

This commit is contained in:
Antoine M 2025-12-04 16:57:48 +01:00
parent d99a296280
commit bf417b4e59
7 changed files with 725 additions and 0 deletions

View File

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

View File

@ -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 (
<>
<InspectorControls>
<PanelBody title={__("Timeline Entries", "company-timeline")}>
<Button
variant="primary"
onClick={addEntry}
style={{ marginBottom: "16px" }}
>
{__("Add Timeline Entry", "company-timeline")}
</Button>
{sortedEntries.map((entry, index) => {
const originalIndex = entries.findIndex(
(e) =>
e.year === entry.year &&
e.title === entry.title &&
e.description === entry.description
);
return (
<Card key={index} style={{ marginBottom: "12px" }}>
<CardHeader>
<strong>
{entry.year || __("New Entry", "company-timeline")}
</strong>
</CardHeader>
<CardBody>
<TextControl
label={__("Year", "company-timeline")}
value={entry.year}
onChange={(value) =>
updateEntry(originalIndex, "year", value)
}
type="number"
/>
<Button
isDestructive
onClick={() => removeEntry(originalIndex)}
style={{ marginTop: "8px" }}
>
{__("Remove Entry", "company-timeline")}
</Button>
</CardBody>
</Card>
);
})}
</PanelBody>
</InspectorControls>
<div {...useBlockProps()}>
<div className="wp-block-telex-company-timeline">
<div className="timeline-sidebar">
<div className="timeline-years">
<h3>{__("Timeline", "company-timeline")}</h3>
{years.map((year) => (
<button
key={year}
className={`year-link ${
selectedYear === year ? "active" : ""
}`}
onClick={() => setSelectedYear(year)}
>
{year}
</button>
))}
</div>
</div>
<div className="timeline-content">
{sortedEntries.map((entry, index) => {
const originalIndex = entries.findIndex(
(e) =>
e.year === entry.year &&
e.title === entry.title &&
e.description === entry.description
);
return (
<div
key={index}
className="timeline-entry"
data-year={entry.year}
>
<div className="timeline-year-marker">
<h2>{entry.year}</h2>
</div>
<div className="timeline-entry-content">
<RichText
tagName="h3"
value={entry.title}
onChange={(value) =>
updateEntry(originalIndex, "title", value)
}
placeholder={__(
"Enter milestone title...",
"company-timeline"
)}
className="timeline-title"
/>
<RichText
tagName="p"
value={entry.description}
onChange={(value) =>
updateEntry(originalIndex, "description", value)
}
placeholder={__(
"Enter milestone description...",
"company-timeline"
)}
className="timeline-description"
/>
<MediaUploadCheck>
<MediaUpload
onSelect={(media) => {
updateEntry(originalIndex, "imageUrl", media.url);
updateEntry(originalIndex, "imageId", media.id);
}}
allowedTypes={["image"]}
value={entry.imageId}
render={({ open }) => (
<div className="timeline-media">
{entry.imageUrl ? (
<>
<img src={entry.imageUrl} alt={entry.title} />
<Button
isDestructive
onClick={() => {
updateEntry(originalIndex, "imageUrl", "");
updateEntry(originalIndex, "imageId", 0);
}}
>
{__("Remove Image", "company-timeline")}
</Button>
</>
) : (
<Button onClick={open} variant="secondary">
{__("Add Image", "company-timeline")}
</Button>
)}
</div>
)}
/>
</MediaUploadCheck>
</div>
</div>
);
})}
</div>
</div>
</div>
</>
);
}

View File

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

View File

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

View File

@ -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 (
<div { ...useBlockProps.save() }>
<div className="wp-block-telex-company-timeline">
<div className="timeline-sidebar">
<div className="timeline-years">
<h3>Timeline</h3>
{ years.map( ( year ) => (
<a
key={ year }
href={ `#year-${ year }` }
className="year-link"
data-year={ year }
>
{ year }
</a>
) ) }
</div>
</div>
<div className="timeline-content">
{ sortedEntries.map( ( entry, index ) => (
<div
key={ index }
className="timeline-entry"
id={ `year-${ entry.year }` }
data-year={ entry.year }
>
<div className="timeline-year-marker">
<h2>{ entry.year }</h2>
</div>
<div className="timeline-entry-content">
{ entry.title && (
<RichText.Content
tagName="h3"
value={ entry.title }
className="timeline-title"
/>
) }
{ entry.description && (
<RichText.Content
tagName="p"
value={ entry.description }
className="timeline-description"
/>
) }
{ entry.imageUrl && (
<div className="timeline-media">
<img src={ entry.imageUrl } alt={ entry.title || '' } />
</div>
) }
</div>
</div>
) ) }
</div>
</div>
</div>
);
}

View File

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

View File

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