<?xml version="1.0"?><artefact xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="artefact.xsd" name="Speaking Events" slug="speaking-events" type="code-package" schemaVersion="2">
  <file path="readme.txt">
    <content><![CDATA[=== Speaking Events ===

Contributors:      Pablo Moratinos
Tags:              block, events, speaking, timeline, collapsible
Tested up to:      6.8
Stable tag:        0.1.2
License:           GPLv2 or later
License URI:       https://www.gnu.org/licenses/gpl-2.0.html

A beautiful and interactive block for displaying speaking events organized by year with collapsible sections.

== Description ==

The Speaking Events block provides a clean and organized way to showcase speaking engagements, conferences, and presentations. Events are automatically grouped by year and displayed in collapsible sections for easy navigation.

**Key Features:**

* **Year-based Organization**: Events are automatically grouped by year with collapsible sections.
* **Comprehensive Event Data**: Capture event date, venue/location, event name, presentation title, and event URL.
* **Clean Admin Interface**: Easy-to-use form fields in the block editor for adding and managing events.
* **Smart Sorting**: Events are sorted chronologically within each year, with most recent years displayed first.
* **Interactive Frontend**: Year headers act as toggle buttons to expand/collapse event listings.
* **Clickable Links**: Event URLs are automatically converted to clickable links.
* **Responsive Design**: Optimized for both desktop and mobile viewing.
* **Smooth Animations**: Professional expand/collapse animations enhance user experience.
* **Accessible**: Proper ARIA attributes and keyboard navigation support.
* **Customizable Colors**: Choose background and text colors for year headers.
* **Translation Ready**: Fully internationalized and ready for translation to any language.
* **RTL Support**: Compatible with right-to-left languages.

**Perfect for:**

* Personal portfolios and professional websites.
* Speaker profiles and biography pages.
* Conference organizers showcasing past events.
* Academic professionals displaying speaking history.
* Corporate websites highlighting team expertise.

== Installation ==

1. Upload the plugin files to the `/wp-content/plugins/speaking-events` directory, or install the plugin through the WordPress plugins screen directly.
2. Activate the plugin through the 'Plugins' screen in WordPress.
3. Add the "Speaking Events" block to any post or page using the block editor.
4. Start adding your speaking events using the intuitive form interface.

== Frequently Asked Questions ==

= How do I add a new speaking event? =

In the block editor, click the "Add New Event" button and fill in the five fields: event date, venue/location, event name, presentation title, and event URL. The block will automatically organize the event by year.

= Can I reorder events manually? =

Events are automatically sorted chronologically within each year group, with the most recent events appearing first. This ensures a consistent and logical organization.

= What happens if I don't provide an event URL? =

The event URL field is optional. If not provided, the event will still display all other information without a clickable link.

= How can I change the format in which the date is displayed? =

Go to WordPress Admin > Settings > General and change the date format.

= Is the block responsive on mobile devices? =

Yes, the block is fully responsive and optimized for mobile viewing with touch-friendly collapsible sections.

= Can I customize the styling? =

Yes! The block includes color customization options for year headers in both collapsed and expanded states. You can also add custom CSS to match your theme's design.

= Is the plugin translation-ready? =

Absolutely! The plugin is fully internationalized with proper text domains and translation functions. All user-facing strings can be translated using standard WordPress translation tools like Poedit.

= How do I translate the plugin? =

1. Create a `/languages/` folder in the plugin directory.
2. Use translation tools like Poedit to create .po/.mo files.
3. Name them according to WordPress standards (e.g., `speaking-events-es_ES.po`).
4. WordPress will automatically load the translations.

== Screenshots ==

1. Expanded view showing all events for a selected year
2. Block editor interface without events
3. Block editor interface showing the form for adding speaking events
4. Viewing an event in the block editor
5. Viewing two events in the block editor

== Changelog ==

= 0.1.2 =
* Minor changes in documentation

= 0.1.1 =
* The date format is now displayed as defined in the general site settings.
* Minor changes in documentation.

= 0.1.0 =
* Initial release with full speaking events functionality.
* Year-based collapsible organization.
* Responsive design and smooth animations.
* Comprehensive event data capture.
* Clean admin interface.
* Color customization for year headers.
* Full internationalization support.
* Keyboard navigation and accessibility features.

== Translating This Plugin ==

The Speaking Events plugin is translation-ready. To contribute translations:

1. Visit the plugin's translation page on WordPress.org.
2. Select your language and start translating.
3. All translations are automatically included in plugin updates.

For manual translations:
1. Extract strings using `wp i18n make-pot`.
2. Create `.po` files using Poedit or similar tools.
3. Compile to `.mo` files and place in `/languages/` directory.

== Support ==

For support and feature requests, please visit Pablo Moratinos' website at https://pablomoratinos.es.]]></content>
  </file>
  <file path="speaking-events.php">
    <content><![CDATA[<?php
/**
 * Plugin Name:       Speaking Events
 * Description:       A beautiful and interactive block for displaying speaking events organized by year with collapsible sections.
 * Version:           0.1.2
 * Requires at least: 6.1
 * Requires PHP:      7.4
 * Author:            Pablo Moratinos
 * Author URI:        https://pablomoratinos.es
 * License:           GPLv2 or later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       speaking-events
 *
 * @package SpeakingEvents
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Registers the block using the metadata loaded from the `block.json` file.
 * Behind the scenes, it registers also all assets so they can be enqueued
 * through the block editor in the corresponding context.
 *
 * @see https://developer.wordpress.org/reference/functions/register_block_type/
 */
if ( ! function_exists( 'speaking_events_block_init' ) ) {
	function speaking_events_block_init() {
		register_block_type( __DIR__ . '/build/' );
	}
}
add_action( 'init', 'speaking_events_block_init' );]]></content>
  </file>
  <file path="src/block.json">
    <content><![CDATA[{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "pablo-moratinos/speaking-events",
	"version": "0.1.0",
	"title": "Speaking Events",
	"category": "widgets",
	"icon": "microphone",
	"description": "Display speaking events organized by year with collapsible sections",
	"example": {
		"attributes": {
			"events": [
				{
					"id": "1",
					"date": "2024-03-15",
					"venue": "WordCamp Europe",
					"eventName": "WordCamp Europe 2024",
					"presentationTitle": "The Future of Block Development",
					"eventUrl": "https://europe.wordcamp.org/2024/",
					"description": "A comprehensive look at the future of WordPress block development and the Gutenberg project."
				},
				{
					"id": "2",
					"date": "2023-11-20",
					"venue": "Tech Conference Center",
					"eventName": "Web Development Summit",
					"presentationTitle": "Modern WordPress Development Practices",
					"eventUrl": "https://webdevsummit.com",
					"description": "Exploring best practices and modern tools for WordPress development."
				}
			]
		}
	},
	"attributes": {
		"events": {
			"type": "array",
			"default": [],
			"items": {
				"type": "object",
				"properties": {
					"id": {
						"type": "string"
					},
					"date": {
						"type": "string"
					},
					"venue": {
						"type": "string"
					},
					"eventName": {
						"type": "string"
					},
					"presentationTitle": {
						"type": "string"
					},
					"eventUrl": {
						"type": "string"
					},
					"description": {
						"type": "string"
					}
				}
			}
		},
		"yearBackgroundColor": {
			"type": "string",
			"default": "#f8fafc"
		},
		"yearTextColor": {
			"type": "string",
			"default": "#1e293b"
		},
		"yearBackgroundColorExpanded": {
			"type": "string",
			"default": "#3b82f6"
		},
		"yearTextColorExpanded": {
			"type": "string",
			"default": "#ffffff"
		}
	},
	"supports": {
		"html": false,
		"align": true
	},
	"textdomain": "speaking-events",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"viewScript": "file:./view.js",
	"render": "file:./render.php"
}]]></content>
  </file>
  <file path="src/index.js">
    <content><![CDATA[
/**
 * 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,
} );
]]></content>
  </file>
  <file path="src/edit.js">
    <content><![CDATA[/**
 * Retrieves the translation of text.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/
 */
import { __ } from '@wordpress/i18n';

/**
 * 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, InspectorControls } from '@wordpress/block-editor';

/**
 * WordPress dependencies
 */
import { useState } from '@wordpress/element';
import { 
	Button, 
	TextControl, 
	TextareaControl,
	Card,
	CardHeader,
	CardBody,
	Flex,
	FlexBlock,
	FlexItem,
	__experimentalSpacer as Spacer,
	Icon,
	PanelBody,
	ColorPicker
} from '@wordpress/components';
import { plus, trash, calendar } from '@wordpress/icons';

/**
 * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
 * Those files can contain any CSS code that gets applied to the editor.
 *
 * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
 */
import './editor.scss';

/**
 * The edit function describes the structure of your block in the context of the
 * editor. This represents what the editor will render when the block is used.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit
 *
 * @param {Object} props               The block props.
 * @param {Object} props.attributes    The block attributes.
 * @param {Function} props.setAttributes The function to update attributes.
 *
 * @return {Element} Element to render.
 */
export default function Edit({ attributes, setAttributes }) {
	const { 
		events = [], 
		yearBackgroundColor = '#f8fafc',
		yearTextColor = '#1e293b',
		yearBackgroundColorExpanded = '#3b82f6',
		yearTextColorExpanded = '#ffffff'
	} = attributes;
	const [editingEvent, setEditingEvent] = useState(null);

	const addNewEvent = () => {
		const newEvent = {
			id: Date.now().toString(),
			date: '',
			venue: '',
			eventName: '',
			presentationTitle: '',
			eventUrl: '',
			description: ''
		};
		setEditingEvent(newEvent);
	};

	const saveEvent = (eventData) => {
		const updatedEvents = editingEvent.id && events.find(e => e.id === editingEvent.id)
			? events.map(event => event.id === eventData.id ? eventData : event)
			: [...events, eventData];
		
		setAttributes({ events: updatedEvents });
		setEditingEvent(null);
	};

	const deleteEvent = (eventId) => {
		const updatedEvents = events.filter(event => event.id !== eventId);
		setAttributes({ events: updatedEvents });
	};

	const editEvent = (event) => {
		setEditingEvent({ ...event });
	};

	const formatDate = (dateString) => {
		if (!dateString) return '';
		const date = new Date(dateString);
		return date.toLocaleDateString('en-US', { 
			year: 'numeric', 
			month: 'long', 
			day: 'numeric' 
		});
	};

	const groupEventsByYear = (events) => {
		const grouped = events.reduce((acc, event) => {
			const year = new Date(event.date).getFullYear();
			if (!acc[year]) acc[year] = [];
			acc[year].push(event);
			return acc;
		}, {});

		// Sort years descending and events within each year by date descending
		Object.keys(grouped).forEach(year => {
			grouped[year].sort((a, b) => new Date(b.date) - new Date(a.date));
		});

		return grouped;
	};

	const blockProps = useBlockProps({
		className: 'wp-block-pablo-moratinos-speaking-events-editor'
	});

	if (editingEvent) {
		return (
			<>
				<InspectorControls>
					<PanelBody title={__('Year Header Colors', 'speaking-events')} initialOpen={true}>
						<div style={{ marginBottom: '16px' }}>
							<label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
								{__('Background Color (Collapsed)', 'speaking-events')}
							</label>
							<ColorPicker
								color={yearBackgroundColor}
								onChange={(color) => setAttributes({ yearBackgroundColor: color })}
								enableAlpha
								defaultValue="#f8fafc"
							/>
						</div>
						<div style={{ marginBottom: '16px' }}>
							<label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
								{__('Text Color (Collapsed)', 'speaking-events')}
							</label>
							<ColorPicker
								color={yearTextColor}
								onChange={(color) => setAttributes({ yearTextColor: color })}
								enableAlpha
								defaultValue="#1e293b"
							/>
						</div>
						<div style={{ marginBottom: '16px' }}>
							<label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
								{__('Background Color (Expanded)', 'speaking-events')}
							</label>
							<ColorPicker
								color={yearBackgroundColorExpanded}
								onChange={(color) => setAttributes({ yearBackgroundColorExpanded: color })}
								enableAlpha
								defaultValue="#3b82f6"
							/>
						</div>
						<div>
							<label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
								{__('Text Color (Expanded)', 'speaking-events')}
							</label>
							<ColorPicker
								color={yearTextColorExpanded}
								onChange={(color) => setAttributes({ yearTextColorExpanded: color })}
								enableAlpha
								defaultValue="#ffffff"
							/>
						</div>
					</PanelBody>
				</InspectorControls>
				<div {...blockProps}>
					<Card>
						<CardHeader>
							<Flex>
								<FlexBlock>
									<h3>{editingEvent.id && events.find(e => e.id === editingEvent.id) ? 
										__('Edit Event', 'speaking-events') : 
										__('Add New Event', 'speaking-events')
									}</h3>
								</FlexBlock>
							</Flex>
						</CardHeader>
						<CardBody>
							<EventForm
								event={editingEvent}
								onSave={saveEvent}
								onCancel={() => setEditingEvent(null)}
							/>
						</CardBody>
					</Card>
				</div>
			</>
		);
	}

	const groupedEvents = groupEventsByYear(events);
	const years = Object.keys(groupedEvents).sort((a, b) => b - a);

	return (
		<>
			<InspectorControls>
				<PanelBody title={__('Year Header Colors', 'speaking-events')} initialOpen={true}>
					<div style={{ marginBottom: '16px' }}>
						<label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
							{__('Background Color (Collapsed)', 'speaking-events')}
						</label>
						<ColorPicker
							color={yearBackgroundColor}
							onChange={(color) => setAttributes({ yearBackgroundColor: color })}
							enableAlpha
							defaultValue="#f8fafc"
						/>
					</div>
					<div style={{ marginBottom: '16px' }}>
						<label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
							{__('Text Color (Collapsed)', 'speaking-events')}
						</label>
						<ColorPicker
							color={yearTextColor}
							onChange={(color) => setAttributes({ yearTextColor: color })}
							enableAlpha
							defaultValue="#1e293b"
						/>
					</div>
					<div style={{ marginBottom: '16px' }}>
						<label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
							{__('Background Color (Expanded)', 'speaking-events')}
						</label>
						<ColorPicker
							color={yearBackgroundColorExpanded}
							onChange={(color) => setAttributes({ yearBackgroundColorExpanded: color })}
							enableAlpha
							defaultValue="#3b82f6"
						/>
					</div>
					<div>
						<label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
							{__('Text Color (Expanded)', 'speaking-events')}
						</label>
						<ColorPicker
							color={yearTextColorExpanded}
							onChange={(color) => setAttributes({ yearTextColorExpanded: color })}
							enableAlpha
							defaultValue="#ffffff"
						/>
					</div>
				</PanelBody>
			</InspectorControls>
			<div {...blockProps}>
				<Card>
					<CardHeader>
						<Flex>
							<FlexBlock>
								<h3><Icon icon={calendar} /> {__('Speaking Events', 'speaking-events')}</h3>
							</FlexBlock>
							<FlexItem>
								<Button
									variant="primary"
									icon={plus}
									onClick={addNewEvent}
								>
									{__('Add Event', 'speaking-events')}
								</Button>
							</FlexItem>
						</Flex>
					</CardHeader>
					<CardBody>
						{events.length === 0 ? (
							<div className="speaking-events-empty-state">
								<Icon icon={calendar} size={48} />
								<p>{__('No speaking events yet. Add your first event to get started!', 'speaking-events')}</p>
								<Button variant="secondary" onClick={addNewEvent}>
									{__('Add Your First Event', 'speaking-events')}
								</Button>
							</div>
						) : (
							<div className="speaking-events-list">
								{years.map(year => (
									<div key={year} className="speaking-events-year-group">
										<h4 
											ClassName="year-header"
											style={{
												backgroundColor: yearBackgroundColor,
												color: yearTextColor,
												padding: '12px 16px',
												borderRadius: '6px',
												margin: '2rem 0 1rem 0'
											}}
										>
											{year}
										</h4>
										{groupedEvents[year].map(event => (
											<Card key={event.id} className="event-card">
												<CardBody>
													<Flex>
														<FlexBlock>
															<div className="event-date">
																{formatDate(event.date)}
															</div>
															<div className="event-title">
																<strong>{event.eventName}</strong>
															</div>
															<div className="event-presentation">
																{event.presentationTitle}
															</div>
															<div className="event-venue">
																{event.venue}
															</div>
															{event.description && (
																<div className="event-description">
																	{event.description}
																</div>
															)}
															{event.eventUrl && (
																<div className="event-url">
																	<a href={event.eventUrl} target="_blank" rel="noopener noreferrer">
																		{event.eventUrl}
																	</a>
																</div>
															)}
														</FlexBlock>
														<FlexItem>
															<Flex direction="column">
																<Button
																	variant="secondary"
																	size="small"
																	onClick={() => editEvent(event)}
																>
																	{__('Edit', 'speaking-events')}
																</Button>
																<Spacer marginTop={2} />
																<Button
																	variant="secondary"
																	isDestructive
																	size="small"
																	icon={trash}
																	onClick={() => deleteEvent(event.id)}
																>
																	{__('Delete', 'speaking-events')}
																</Button>
															</Flex>
														</FlexItem>
													</Flex>
												</CardBody>
											</Card>
										))}
									</div>
								))}
							</div>
						)}
					</CardBody>
				</Card>
			</div>
		</>
	);
}

function EventForm({ event, onSave, onCancel }) {
	const [formData, setFormData] = useState(event);

	const updateField = (field, value) => {
		setFormData(prev => ({ ...prev, [field]: value }));
	};

	const handleSave = () => {
		if (!formData.date || !formData.eventName || !formData.presentationTitle) {
			return;
		}
		onSave(formData);
	};

	const isValid = formData.date && formData.eventName && formData.presentationTitle;

	return (
		<div className="event-form">
			<TextControl
				label={__('Event Date', 'speaking-events')}
				type="date"
				value={formData.date}
				onChange={(value) => updateField('date', value)}
				help={__('Select the date of the speaking event', 'speaking-events')}
			/>
			
			<TextControl
				label={__('Venue/Location', 'speaking-events')}
				value={formData.venue}
				onChange={(value) => updateField('venue', value)}
				placeholder={__('e.g., WordCamp Europe, Online, San Francisco', 'speaking-events')}
			/>
			
			<TextControl
				label={__('Event Name', 'speaking-events')}
				value={formData.eventName}
				onChange={(value) => updateField('eventName', value)}
				placeholder={__('e.g., WordCamp Europe 2024', 'speaking-events')}
			/>
			
			<TextControl
				label={__('Event URL (Optional)', 'speaking-events')}
				type="url"
				value={formData.eventUrl}
				onChange={(value) => updateField('eventUrl', value)}
				placeholder={__('https://example.com/event', 'speaking-events')}
			/>
			
			<TextControl
				label={__('Presentation Title', 'speaking-events')}
				value={formData.presentationTitle}
				onChange={(value) => updateField('presentationTitle', value)}
				placeholder={__('e.g., The Future of Block Development', 'speaking-events')}
			/>

			<TextareaControl
				label={__('Description (Optional)', 'speaking-events')}
				value={formData.description || ''}
				onChange={(value) => updateField('description', value)}
				placeholder={__('A brief description of your talk or the event...', 'speaking-events')}
				help={__('Add a short description to provide more context about your presentation or the event', 'speaking-events')}
				rows={3}
			/>

			<Spacer marginTop={4} />
			
			<Flex>
				<FlexItem>
					<Button
						variant="primary"
						onClick={handleSave}
						disabled={!isValid}
					>
						{__('Save Event', 'speaking-events')}
					</Button>
				</FlexItem>
				<FlexItem>
					<Button
						variant="tertiary"
						onClick={onCancel}
					>
						{__('Cancel', 'speaking-events')}
					</Button>
				</FlexItem>
			</Flex>
		</div>
	);
}]]></content>
  </file>
  <file path="src/save.js">
    <content><![CDATA[/**
 * 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 } 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`.
 *
 * For dynamic blocks, this should return null as the content is rendered via PHP.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save
 *
 * @return {null} Null for dynamic blocks.
 */
export default function save() {
	// Dynamic block, content is rendered via render.php
	return null;
}]]></content>
  </file>
  <file path="src/style.scss">
    <content><![CDATA[/**
 * 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-pablo-moratinos-speaking-events {
	.speaking-events-container {
		max-width: 800px;
		margin: 0 auto;
	}

	.speaking-events-year-section {
		margin-bottom: 1.5rem;
		border: 1px solid #e2e8f0;
		border-radius: 8px;
		overflow: hidden;
		background: #ffffff;
		box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);

		&:last-child {
			margin-bottom: 0;
		}
	}

	.year-toggle {
		width: 100%;
		padding: 1.25rem 1.5rem;
		border: none;
		background: #f8fafc; /* Default fallback */
		color: #1e293b; /* Default fallback */
		font-size: 1.25rem;
		font-weight: 600;
		text-align: left;
		cursor: pointer;
		display: flex;
		justify-content: space-between;
		align-items: center;
		transition: all 0.3s ease;
		border-bottom: 1px solid #e2e8f0;

		&:hover {
			opacity: 0.9;
			transform: translateY(-1px);
		}

		&.expanded {
			.toggle-icon {
				transform: rotate(180deg);
			}
		}

		.year-text {
			font-size: 1.25rem;
			font-weight: 600;
		}

		.toggle-icon {
			width: 0;
			height: 0;
			border-left: 6px solid transparent;
			border-right: 6px solid transparent;
			border-top: 8px solid currentColor;
			transition: transform 0.3s ease;
			opacity: 0.7;
			display: inline-block;
		}
	}

	.events-list {
		transition: all 0.3s ease;
		overflow: hidden;

		&.expanded {
			display: block !important;
		}
	}

	.event-item {
		padding: 1.5rem;
		border-bottom: 1px solid #f1f5f9;
		display: flex;
		gap: 1.25rem;
		align-items: flex-start;

		&:last-child {
			border-bottom: none;
		}

		@media (max-width: 768px) {
			flex-direction: column;
			gap: 0.75rem;
		}
	}

	.event-date {
		flex-shrink: 0;
		width: 140px;
		color: #64748b;
		font-size: 0.875rem;
		font-weight: 500;
		text-transform: uppercase;
		letter-spacing: 0.025em;

		@media (max-width: 768px) {
			width: auto;
			font-size: 0.8rem;
		}
	}

	.event-content {
		flex: 1;
		min-width: 0;
	}

	.event-header {
		margin-bottom: 0.5rem;
	}

	.event-name {
		font-size: 1.125rem;
		font-weight: 600;
		color: #1e293b;
		margin: 0 0 0.25rem 0;
		line-height: 1.4;

		.event-name-link {
			/* Inherit all link styles from the active theme */
			color: inherit;
			text-decoration: inherit;
			font-weight: inherit;
			font-size: inherit;
			line-height: inherit;
			
			/* Reset any custom styling to let theme styles take precedence */
			all: unset;
			
			/* Restore essential link properties */
			cursor: pointer;
			display: inline;
			
			/* Allow theme to style the link completely */
			font-size: 1.125rem;
			font-weight: 600;
			line-height: 1.4;
			
			/* Add underline to indicate it's a clickable link */
			text-decoration: underline;
		}
	}

	.event-venue {
		color: #64748b;
		font-size: 0.875rem;
		font-weight: 500;
	}

	.presentation-title {
		color: #374151;
		font-size: 1rem;
		line-height: 1.5;
		margin-bottom: 0.75rem;
	}

	.event-description {
		color: #64748b;
		font-size: 0.9rem;
		line-height: 1.6;
		margin-top: 0.75rem;
		padding: 0.75rem;
		background-color: #f8fafc;
		border-radius: 6px;
		border-left: 3px solid #e2e8f0;
		font-style: italic;
	}

	// Empty state for when no events are present
	.speaking-events-empty {
		text-align: center;
		padding: 3rem 1.5rem;
		color: #64748b;

		p {
			font-size: 1.125rem;
			margin: 0;
		}
	}

	// Responsive adjustments
	@media (max-width: 640px) {
		.speaking-events-container {
			margin: 0 -1rem;
		}

		.speaking-events-year-section {
			border-radius: 0;
			border-left: none;
			border-right: none;
		}

		.year-toggle {
			padding: 1rem 1.25rem;
			font-size: 1.125rem;
		}

		.event-item {
			padding: 1.25rem;
		}

		.event-name {
			font-size: 1rem;
		}

		.presentation-title {
			font-size: 0.875rem;
		}

		.event-description {
			font-size: 0.85rem;
			padding: 0.5rem;
		}
	}
}

// Animation keyframes
@keyframes slideDown {
	from {
		max-height: 0;
		opacity: 0;
	}
	to {
		max-height: 1000px;
		opacity: 1;
	}
}

@keyframes slideUp {
	from {
		max-height: 1000px;
		opacity: 1;
	}
	to {
		max-height: 0;
		opacity: 0;
	}
}

.wp-block-pablo-moratinos-speaking-events .events-list {
	&.expanding {
		animation: slideDown 0.3s ease-out forwards;
	}

	&.collapsing {
		animation: slideUp 0.3s ease-out forwards;
	}
}]]></content>
  </file>
  <file path="src/editor.scss">
    <content><![CDATA[/**
 * The following styles get applied inside the editor only.
 *
 * Replace them with your own styles or remove the file completely.
 */

.wp-block-pablo-moratinos-speaking-events-editor {
	.speaking-events-empty-state {
		text-align: center;
		padding: 3rem 2rem;
		color: #64748b;

		svg {
			margin-bottom: 1rem;
			opacity: 0.5;
		}

		p {
			font-size: 1rem;
			margin-bottom: 1.5rem;
		}
	}

	.speaking-events-list {
		.year-header {
			font-size: 1.25rem;
			font-weight: 600;
			color: #1e293b;
			margin: 2rem 0 1rem 0;
			padding-bottom: 0.5rem;
			border-bottom: 2px solid #e2e8f0;

			&:first-child {
				margin-top: 0;
			}
		}

		.event-card {
			margin-bottom: 1rem;
			border: 1px solid #e2e8f0;

			&:hover {
				border-color: #cbd5e1;
			}
		}

		.event-date {
			color: #3b82f6;
			font-weight: 600;
			font-size: 0.875rem;
			margin-bottom: 0.25rem;
		}

		.event-title {
			margin-bottom: 0.5rem;

			strong {
				color: #1e293b;
				font-size: 1rem;
			}
		}

		.event-presentation {
			color: #374151;
			font-size: 0.875rem;
			margin-bottom: 0.25rem;
		}

		.event-venue {
			color: #64748b;
			font-size: 0.875rem;
			margin-bottom: 0.5rem;
		}

		.event-description {
			color: #64748b;
			font-size: 0.875rem;
			margin-bottom: 0.5rem;
			padding: 0.5rem;
			background-color: #f8fafc;
			border-radius: 4px;
			border-left: 3px solid #e2e8f0;
			font-style: italic;
			line-height: 1.4;
		}

		.event-url {
			font-size: 0.875rem;

			a {
				color: #3b82f6;
				text-decoration: none;

				&:hover {
					text-decoration: underline;
				}
			}
		}
	}

	.event-form {
		.components-base-control {
			margin-bottom: 1.5rem;

			&:last-of-type {
				margin-bottom: 2rem;
			}
		}

		.components-text-control__input[type="date"] {
			max-width: 200px;
		}

		.components-text-control__input[type="url"] {
			font-family: monospace;
			font-size: 0.875rem;
		}

		.components-textarea-control__input {
			resize: vertical;
			min-height: 80px;
			line-height: 1.5;
		}
	}

	// Card header styling
	.components-card-header {
		h3 {
			display: flex;
			align-items: center;
			gap: 0.5rem;
			margin: 0;
			font-size: 1.125rem;
			font-weight: 600;
		}
	}

	// Button spacing in form
	.components-flex {
		gap: 0.75rem;
	}

	// Responsive adjustments for editor
	@media (max-width: 782px) {
		.speaking-events-list .event-card .components-flex {
			flex-direction: column;
			align-items: stretch;

			.components-flex-item:last-child .components-flex {
				flex-direction: row;
				justify-content: flex-start;
			}
		}
	}
}

// Focus states for better accessibility in editor
.wp-block-pablo-moratinos-speaking-events-editor {
	.components-button:focus {
		box-shadow: 0 0 0 2px #007cba;
		outline: 2px solid transparent;
		outline-offset: -2px;
	}

	.components-text-control__input:focus,
	.components-textarea-control__input:focus {
		border-color: #007cba;
		box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2);
	}
}]]></content>
  </file>
  <file path="src/view.js">
    <content><![CDATA[/**
 * Use this file for JavaScript code that you want to run in the front-end
 * on posts/pages that contain this block.
 *
 * When this file is defined as the value of the `viewScript` property
 * in `block.json` it will be enqueued on the front end of the site.
 */

document.addEventListener('DOMContentLoaded', function() {
	// Initialize speaking events functionality
	const speakingEventsBlocks = document.querySelectorAll('.wp-block-pablo-moratinos-speaking-events');
	
	speakingEventsBlocks.forEach(function(block) {
		initializeSpeakingEvents(block);
	});
});

function initializeSpeakingEvents(block) {
	const yearToggles = block.querySelectorAll('.year-toggle');
	
	// Find the most recent year (first toggle since years are sorted descending)
	const mostRecentToggle = yearToggles[0];
	
	yearToggles.forEach(function(toggle, index) {
		// Remove any existing event listeners to prevent duplicates
		toggle.removeEventListener('click', handleToggleClick);
		toggle.addEventListener('click', handleToggleClick);
		
		// Add keyboard support
		toggle.addEventListener('keydown', function(e) {
			if (e.key === 'Enter' || e.key === ' ') {
				e.preventDefault();
				e.stopPropagation();
				handleToggleClick.call(this, e);
			}
		});
		
		// Expand the most recent year by default
		if (index === 0 && mostRecentToggle) {
			const year = toggle.getAttribute('data-year');
			const eventsList = document.getElementById('events-' + year);
			if (eventsList) {
				// Set initial expanded state without animation
				setInitialExpandedState(toggle, eventsList);
			}
		}
	});
}

function setInitialExpandedState(toggle, eventsList) {
	// Update toggle state
	toggle.setAttribute('aria-expanded', 'true');
	toggle.classList.add('expanded');
	
	// Apply expanded colors immediately
	const expandedBg = toggle.getAttribute('data-expanded-bg');
	const expandedText = toggle.getAttribute('data-expanded-text');
	if (expandedBg && expandedText) {
		toggle.style.backgroundColor = expandedBg;
		toggle.style.color = expandedText;
		toggle.style.borderBottomColor = expandedBg;
	}
	
	// Show the events list immediately (no animation)
	eventsList.style.display = 'block';
	eventsList.style.opacity = '1';
}

function handleToggleClick(e) {
	e.preventDefault();
	e.stopPropagation();
	
	// Prevent multiple rapid clicks
	if (this.dataset.animating === 'true') {
		return;
	}
	
	this.dataset.animating = 'true';
	
	const year = this.getAttribute('data-year');
	const eventsList = document.getElementById('events-' + year);
	const isExpanded = this.getAttribute('aria-expanded') === 'true';
	
	if (isExpanded) {
		collapseSection(this, eventsList);
	} else {
		expandSection(this, eventsList);
	}
	
	// Reset animation flag after animation completes
	const self = this;
	setTimeout(function() {
		self.dataset.animating = 'false';
	}, 350);
}

function expandSection(toggle, eventsList) {
	// Update toggle state
	toggle.setAttribute('aria-expanded', 'true');
	toggle.classList.add('expanded');
	
	// Apply expanded colors
	const expandedBg = toggle.getAttribute('data-expanded-bg');
	const expandedText = toggle.getAttribute('data-expanded-text');
	if (expandedBg && expandedText) {
		toggle.style.backgroundColor = expandedBg;
		toggle.style.color = expandedText;
		toggle.style.borderBottomColor = expandedBg;
	}
	
	// Show and animate the events list
	eventsList.style.display = 'block';
	eventsList.classList.remove('collapsing');
	eventsList.classList.add('expanding');
	
	// Smooth scroll to section if it's not in view
	requestAnimationFrame(function() {
		const rect = toggle.getBoundingClientRect();
		const isInView = rect.top >= 0 && rect.bottom <= window.innerHeight;
		
		if (!isInView) {
			toggle.scrollIntoView({ 
				behavior: 'smooth', 
				block: 'nearest' 
			});
		}
	});
	
	// Clean up animation class
	setTimeout(function() {
		if (eventsList) {
			eventsList.classList.remove('expanding');
		}
	}, 300);
}

function collapseSection(toggle, eventsList) {
	// Update toggle state
	toggle.setAttribute('aria-expanded', 'false');
	toggle.classList.remove('expanded');
	
	// Apply collapsed colors
	const collapsedBg = toggle.getAttribute('data-collapsed-bg');
	const collapsedText = toggle.getAttribute('data-collapsed-text');
	if (collapsedBg && collapsedText) {
		toggle.style.backgroundColor = collapsedBg;
		toggle.style.color = collapsedText;
		toggle.style.borderBottomColor = '#e2e8f0';
	}
	
	// Animate collapse
	eventsList.classList.remove('expanding');
	eventsList.classList.add('collapsing');
	
	// Hide after animation
	setTimeout(function() {
		if (eventsList) {
			eventsList.style.display = 'none';
			eventsList.classList.remove('collapsing');
		}
	}, 300);
}

// Handle window resize to ensure responsive behavior
let resizeTimer;
window.addEventListener('resize', function() {
	clearTimeout(resizeTimer);
	resizeTimer = setTimeout(function() {
		// Recalculate any necessary dimensions or states
		const expandedSections = document.querySelectorAll('.wp-block-pablo-moratinos-speaking-events .events-list[style*="block"]');
		expandedSections.forEach(function(section) {
			// Ensure expanded sections remain properly displayed
			if (section.style.display === 'block') {
				section.style.height = 'auto';
			}
		});
	}, 250);
});

// Add focus management for better keyboard navigation
document.addEventListener('keydown', function(e) {
	// Handle Escape key to collapse all sections
	if (e.key === 'Escape') {
		const expandedToggles = document.querySelectorAll('.wp-block-pablo-moratinos-speaking-events .year-toggle.expanded');
		expandedToggles.forEach(function(toggle) {
			const year = toggle.getAttribute('data-year');
			const eventsList = document.getElementById('events-' + year);
			if (eventsList) {
				collapseSection(toggle, eventsList);
			}
		});
	}
});

// Prevent event bubbling on the events list itself
document.addEventListener('click', function(e) {
	if (e.target.closest('.events-list')) {
		e.stopPropagation();
	}
});]]></content>
  </file>
  <file path="src/render.php">
    <content><![CDATA[<?php
/**
 * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
 */

$events = $attributes['events'] ?? [];
$year_background_color = $attributes['yearBackgroundColor'] ?? '#f8fafc';
$year_text_color = $attributes['yearTextColor'] ?? '#1e293b';
$year_background_color_expanded = $attributes['yearBackgroundColorExpanded'] ?? '#3b82f6';
$year_text_color_expanded = $attributes['yearTextColorExpanded'] ?? '#ffffff';

if (empty($events)) {
	return;
}

// Group events by year and sort
$grouped_events = [];
foreach ($events as $event) {
	$year = gmdate('Y', strtotime($event['date']));
	if (!isset($grouped_events[$year])) {
		$grouped_events[$year] = [];
	}
	$grouped_events[$year][] = $event;
}

// Sort years descending and events within each year by date descending
krsort($grouped_events);
foreach ($grouped_events as $year => &$year_events) {
	usort($year_events, function($a, $b) {
		return strtotime($b['date']) - strtotime($a['date']);
	});
}
unset($year_events);

$years = array_keys($grouped_events);

// Get block wrapper attributes safely
$wrapper_attributes = get_block_wrapper_attributes();

?>

<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>  
	<div class="speaking-events-container">
		<?php foreach ($years as $year_index => $year) : ?>
			<div class="speaking-events-year-section">
				<button 
					class="year-toggle"
					data-year="<?php echo esc_attr($year); ?>"
					aria-expanded="false"
					aria-controls="events-<?php echo esc_attr($year); ?>"
					data-collapsed-bg="<?php echo esc_attr($year_background_color); ?>"
					data-collapsed-text="<?php echo esc_attr($year_text_color); ?>"
					data-expanded-bg="<?php echo esc_attr($year_background_color_expanded); ?>"
					data-expanded-text="<?php echo esc_attr($year_text_color_expanded); ?>"
					style="background-color: <?php echo esc_attr($year_background_color); ?>; color: <?php echo esc_attr($year_text_color); ?>;"
				>
					<span class="year-text"><?php echo esc_html($year); ?></span>
					<span class="toggle-icon"></span>
				</button>
				<div 
					class="events-list"
					id="events-<?php echo esc_attr($year); ?>"
					style="display: none;"
				>
					<?php foreach ($grouped_events[$year] as $event_index => $event) : ?>
						<div class="event-item">
							<div class="event-date">
								<?php echo esc_html(date_i18n(get_option('date_format'), strtotime($event['date']))); ?>
							</div>
							<div class="event-content">
								<div class="event-header">
									<h4 class="event-name">
										<?php 
										$event_name = $event['eventName'] ?? '';
										$event_url = $event['eventUrl'] ?? '';
										
										if (!empty($event_url) && !empty($event_name)) {
											printf(
												'<a href="%s" target="_blank" rel="noopener noreferrer" class="event-name-link">%s</a>',
												esc_url($event_url),
												esc_html($event_name)
											);
										} else {
											echo esc_html($event_name);
										}
										?>
									</h4>
									<?php if (!empty($event['venue'])) : ?>
										<div class="event-venue"><?php echo esc_html($event['venue']); ?></div>
									<?php endif; ?>
								</div>
								<div class="presentation-title">
									<?php echo esc_html($event['presentationTitle'] ?? ''); ?>
								</div>
								<?php if (!empty($event['description'])) : ?>
									<div class="event-description">
										<?php echo esc_html($event['description']); ?>
									</div>
								<?php endif; ?>
							</div>
						</div>
					<?php endforeach; ?>
				</div>
			</div>
		<?php endforeach; ?>
	</div>
</div>]]></content>
  </file>
  <file path="package.json">
    <content><![CDATA[{
	"name": "speaking-events",
	"version": "0.1.0",
	"description": "A beautiful and interactive block for displaying speaking events organized by year with collapsible sections.",
	"author": "Pablo Moratinos <https://pablomoratinos.es>",
	"homepage": "https://pablomoratinos.es",
	"license": "GPL-2.0-or-later",
	"main": "build/index.js",
	"scripts": {
		"build": "wp-scripts build",
		"format": "wp-scripts format",
		"lint:css": "wp-scripts lint-style",
		"lint:js": "wp-scripts lint-js",
		"packages-update": "wp-scripts packages-update",
		"plugin-zip": "wp-scripts plugin-zip",
		"start": "wp-scripts start"
	},
    "devDependencies": {
        "@wordpress/scripts": "^30.15.0"
	}
}]]></content>
  </file>
</artefact>