Countdown

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque nec laoreet metus. Proin et consequat nulla, nec viverra dui. Nam maximus fermentum magna in ornare. Interdum et malesuada fames ac ante ipsum primis in faucibus.

block.json

{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 3,
    "name": "wlc/countdown",
    "title": " Countdown",
    "category": "theme",
    "icon": "dashboard",
    "description": "Countdown",
    "keywords": [ "countdown" ],
    "attributes":{
        "timer": {
            "type": "string"
        },
        "timerSeparator": {
            "type": "string"
        },
        "displayLabels": {
            "type": "boolean",
            "default": true
        },
        "displayDays": {
            "type": "boolean",
            "default": true
        },
        "align": {
            "type": "string",
            "default": "center"
        },
        "style": {
            "type": "object",
            "default": {
                "typography": {
                    "fontSize": "6rem",
                    "fontWeight": "400"
                }
            }
        },
        "labelsStyle": {
            "type": "object",
            "default": {
                "fontSize": "2rem",
                "fontWeight": "400",
                "color": ""
            }
        },
        "separatorStyle": {
            "type": "object",
            "default": {
                "fontSize": "6rem",
                "fontWeight": "400",
                "color": "",
                "transform": "translateY(0)"
            }
        }
    },
    "supports": {
        "anchor": true,
        "align": true,
        "color": true,
        "spacing": {
            "margin": [ "vertical" ],
            "padding": true
        },
        "typography": {
            "fontSize": true,
            "lineHeight": true
        },
        "renaming": true
    },
    "version": "1.0.0",
    "textdomain": "WLC",
    "editorScript": "file:./index.js",
    "script": "file:./view.js",
    "style": "file:./style-index.css"
  }

edit.js

import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import {
  PanelBody,
  DateTimePicker,
  TextControl,
  ToggleControl,
  RangeControl,
  FontSizePicker,
  ColorPalette,
  Dropdown,
  Button,
  ColorIndicator,
  BaseControl
} from '@wordpress/components';
import { __, _n } from '@wordpress/i18n';
import { dateI18n } from '@wordpress/date';
import { select } from '@wordpress/data';

const edit = ( { attributes, setAttributes } ) => {

  const { timer, timerSeparator, displayLabels, displayDays, labelsStyle, separatorStyle } = attributes;
  const dataProps = JSON.stringify( { timer, displayLabels, displayDays } );
  const blockProps = useBlockProps({ className: 'wlc-countdown', 'data-props': dataProps });

  const globalSettings = select( "core/editor" ).getEditorSettings();

  const labelsStyleProps = {
      style: {...labelsStyle }
  }

  const separatorStyleProps = {
      style: { ...separatorStyle }
  }

  const isPastDate = () => {
      return Date.parse( timer ) < Date.parse( new Date() );
  }

  return (
    <>
      <InspectorControls>
        <PanelBody title={ __( 'Countdown Block Settings', 'WLC' ) }>
          <BaseControl>
            <BaseControl.VisualLabel>{ __( 'time and date', 'WLC' ) }</BaseControl.VisualLabel>
            <p>
              { __( 'Selected date and time:', 'WLC' ) }<br/>
              <strong>{ dateI18n( 'j F Y H:i', timer ) }</strong>
            </p>
            { !!isPastDate() && (
              <p><strong>{ __( 'WARNING! Selected Date is in past', 'WLC' ) }</strong></p>
            ) }
            <Dropdown
              popoverProps={ { placement: 'left-start' } }
              renderToggle={ ( { isOpen, onToggle } ) => (
                <Button
                  variant="primary"
                  onClick={ onToggle }
                  aria-expanded={ isOpen }
                >
                  { __( 'Set time and date', 'WLC' ) }
                </Button>
              ) }
              renderContent={ () =>
                <DateTimePicker
                  currentDate={ timer }
                  onChange={ value => setAttributes( { timer: value } ) }
                />
              }
            />
          </BaseControl>
          <TextControl
            label={ __( 'Separator', 'WLC' ) }
            value={ timerSeparator }
            onChange={ value => setAttributes( { timerSeparator: value } ) }
          />
          <BaseControl>
            <BaseControl.VisualLabel>{ __( 'Labels', 'WLC' ) }</BaseControl.VisualLabel>
            <ToggleControl
              label={ __( 'Display Labels', 'WLC' ) }
              checked={ displayLabels }
              onChange={ value => setAttributes( { displayLabels: value } ) }
            />
          </BaseControl>
          <BaseControl>
            <BaseControl.VisualLabel>{ __( 'Days', 'WLC' ) }</BaseControl.VisualLabel>
            <ToggleControl
              label={ __( 'Display Days if none left (at 0)', 'WLC' ) }
              checked={ displayDays }
              onChange={ value => setAttributes( { displayDays: value } ) }
            />
          </BaseControl>
        </PanelBody>
      </InspectorControls>
      <InspectorControls group="styles">
        <PanelBody title={ __( 'Labels Styles', 'WLC' ) }>
          <FontSizePicker
            value={ labelsStyle.fontSize }
            fontSizes={ globalSettings.fontSizes }
            withSlider={ true }
            withReset={ false }
            units={ [ 'px', 'em', 'rem' ] }
            onChange={ value => setAttributes( { labelsStyle: { ...labelsStyle, fontSize: value } } ) }
          />
          <Dropdown
            popoverProps={ { placement: 'left-start' } }
            renderToggle={ ( { isOpen, onToggle } ) => (
              <Button
                  onClick={ onToggle }
                  aria-expanded={ isOpen }
              >
                <ColorIndicator colorValue={labelsStyle.color} />&nbsp;{ __( 'Select color', 'WLC' ) }
              </Button>
            ) }
            renderContent={ () =>
              <ColorPalette
                value={labelsStyle.color}
                colors= { globalSettings.colors }
                onChange={ value => setAttributes( { labelsStyle: { ...labelsStyle, color: value } } ) }
                enableAlpha
                clearable
                defaultValue="#000"
              />
            }
          />
        </PanelBody>
        <PanelBody title={ __( 'Separator Styles', 'WLC' ) }>
          <FontSizePicker
            value={ separatorStyle.fontSize }
            fontSizes={ globalSettings.fontSizes }
            withSlider={ true }
            withReset={ false }
            units={ [ 'px', 'em', 'rem' ] }
            onChange={ value => setAttributes( { separatorStyle: { ...separatorStyle, fontSize: value } } ) }
          />
          <RangeControl
            label={ __( 'Vertical position', 'WLC' ) }
            value={ parseInt( separatorStyle.transform.slice(12,-1) ) * (-1) }
            onChange={ value => setAttributes( { separatorStyle: { ...separatorStyle, transform: ` translateY(${value * (-1) }%)` } } ) }
            min={ 0 }
            max={ 100 }
            marks={ [
                {
                    value: 0,
                    label: '0'
                },
                {
                    value: 100,
                    label: '-100%'
                }
            ] }
          />
          <Dropdown
            popoverProps={ { placement: 'left-start' } }
            renderToggle={ ( { isOpen, onToggle } ) => (
                <Button
                    onClick={ onToggle }
                    aria-expanded={ isOpen }
                >
                    <ColorIndicator colorValue={separatorStyle.color} />&nbsp;{ __( 'Select color', 'WLC' ) }
                </Button>
            ) }
            renderContent={ () =>
                <ColorPalette
                    value={separatorStyle.color}
                    colors={ globalSettings.colors }
                    onChange={ value => setAttributes( { separatorStyle: { ...separatorStyle, color: value } } ) }
                    enableAlpha
                    clearable
                    defaultValue="#000"
                />
            }
          />
        </PanelBody>
      </InspectorControls>
      <div { ...blockProps }>
        <div className="wlc-countdown__timer">
          <span className="time days">
            <span className="timer">00</span>
            { displayLabels && (
                <span className="label" { ...labelsStyleProps}>{ __( 'days', 'WLC' ) }</span>
            ) }
          </span>
          <span className="time hours">
            <span className="timer">00</span>
            { displayLabels && (
                <span className="label" { ...labelsStyleProps}>{ __( 'hours', 'WLC' ) }</span>
            ) }
          </span>
          { !!timerSeparator && (
            <span className="separator" { ...separatorStyleProps}>
              { timerSeparator }
            </span>
          ) }
          <span className="time minutes">
            <span className="timer">00</span>
            { displayLabels && (
              <span className="label" { ...labelsStyleProps}>{ __( 'minutes', 'WLC' ) }</span>
            ) }
          </span>
          { !!timerSeparator && (
            <span className="separator" { ...separatorStyleProps}>
              { timerSeparator }
            </span>
          ) }
          <span className="time seconds">
            <span className="timer">00</span>
            { displayLabels && (
              <span className="label" { ...labelsStyleProps}>{ __( 'seconds', 'WLC' ) }</span>
            ) }
          </span>
        </div>
      </div>
    </>
 );
}

export default edit;

index.js

import { registerBlockType } from "@wordpress/blocks";
import metadata from './block.json';
import save from "./save";
import edit from './edit';
import './style.css';

registerBlockType( metadata, {
    edit: edit,
    save: save,
});

save.js

import { useBlockProps } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
const save = ( { attributes } ) => {
    const { timer, timerSeparator, displayLabels, displayDays, labelsStyle, separatorStyle } = attributes;
    const dataProps = JSON.stringify( { timer, displayLabels, displayDays } );
    const blockProps = useBlockProps.save({ className: 'wlc-countdown', 'data-props': dataProps });

    const labelsStyleProps = {
        style: {...labelsStyle }
    }

    const separatorStyleProps = {
        style: { ...separatorStyle }
    }
    return (
        <div { ...blockProps }>
            <div className="wlc-countdown__timer">
                <span className="time days">
                    <span className="timer">00</span>
                    { displayLabels && (
                        <span className="label" { ...labelsStyleProps}>{ __( 'days', 'WLC' ) }</span>
                    ) }
                </span>
                <span className="time hours">
                    <span className="timer">00</span>
                    { displayLabels && (
                        <span className="label" { ...labelsStyleProps}>{ __( 'hours', 'WLC' ) }</span>
                    ) }
                </span>
                { !!timerSeparator && (
                    <span className="separator" { ...separatorStyleProps}>{ timerSeparator }</span>
                ) }
                <span className="time minutes">
                    <span className="timer">00</span>
                    { displayLabels && (
                        <span className="label" { ...labelsStyleProps}>{ __( 'minutes', 'WLC' ) }</span>
                    ) }
                </span>
                { !!timerSeparator && (
                    <span className="separator" { ...separatorStyleProps}>{ timerSeparator }</span>
                ) }
                <span className="time seconds">
                    <span className="timer">00</span>
                    { displayLabels && (
                        <span className="label" { ...labelsStyleProps}>{ __( 'seconds', 'WLC' ) }</span>
                    ) }
                </span>
            </div>
        </div>
    );
}

export default save;

style.css

.wlc-countdown {

    & .wlc-countdown__timer {
        @apply flex flex-wrap gap-2 justify-center items-baseline;
        
        & > .time {
            @apply block text-center max-lg:min-w-[0.5em] lg:min-w-[1.5em];

            &.days {
                @apply max-lg:mr-4 lg:mr-8;
            }

            & > .timer {
                @apply block max-lg:!text-30;
                
            }
            & > .label {
                @apply block max-lg:!text-16;
            }
        }

        & .separator {
            @apply max-lg:!text-30 max-lg:!translate-y-0;
        }
    }
}

view.js

import { _n } from '@wordpress/i18n';

const wlcCountdown = () => {
    document.querySelectorAll('.wlc-countdown').forEach(countdownTimer => {

        const data = countdownTimer.dataset?.props;
        if ( !data ) return;

        const props = JSON.parse( data );
        const timer = props.timer || Date.now();
        const labels = props.displayLabels;
        const zeroDays = props.displayDays;
        const deadline = new Date( timer );

        const timers = countdownTimer.querySelector('.wlc-countdown__timer');

        const daysTimer = timers.querySelector('.days');
        const hoursTimer = timers.querySelector('.hours');
        const minutesTimer = timers.querySelector('.minutes');
        const secondsTimer = timers.querySelector('.seconds');
 
        function count() {

            const total = Date.parse( deadline ) - Date.parse( new Date() );
            const days = Math.max( 0, Math.floor( total/(24*60*60*1000) ) );
            const hours = Math.max( 0, Math.floor( ( total/(60*60*1000) ) % 24 ) );
            const minutes = Math.max( 0, Math.floor( ( total/1000/60 ) % 60 ) ) ;
            const seconds = Math.max( 0, Math.floor( ( total/1000 ) % 60 ) );

            daysTimer.style.display = ( days > 0 || ( days == 0 && zeroDays ) ) ? "block" : "none";

            daysTimer.querySelector('.timer').textContent = days;
            hoursTimer.querySelector('.timer').textContent = hours.toString().padStart(2, '0');
            minutesTimer.querySelector('.timer').textContent = minutes.toString().padStart(2, '0');
            secondsTimer.querySelector('.timer').textContent = seconds.toString().padStart(2, '0');
            
            if ( labels ) {
                if (days > 0 || ( days == 0 && zeroDays ) ) {
                    daysTimer.querySelector('.label').textContent = _n( 'day', 'days', days, 'WLC' );
                }
                hoursTimer.querySelector('.label').textContent = _n( 'hour', 'hours', hours, 'WLC' );
                minutesTimer.querySelector('.label').textContent = _n( 'minute', 'minutes', minutes, 'WLC' );
                secondsTimer.querySelector('.label').textContent = _n( 'second', 'seconds', seconds, 'WLC' );
            }

            if ( days >= 0 && hours >= 0 && minutes >= 0 && seconds >= 0 ) {
                window.requestAnimationFrame( count );
            }

        }
        window.requestAnimationFrame( count );
    } );


}

// delay execution of carousel script in admin after block is fully loaded
function delayedWlcCountdown() {
	let blockLoaded = false;
	let blockLoadedInterval = setInterval(() => {
		if (document.querySelector('.wlc-countdown')) {
			wlcCountdown();
			blockLoaded = true;
		}
		if ( blockLoaded ) {
			clearInterval( blockLoadedInterval );
		}
	}, 500);
}

document.addEventListener( 'DOMContentLoaded', function() {
	// in editor execute funtion after server-side-rendered block is fully loaded
	if ( document.querySelector('.wp-admin') ) {
		delayedWlcCountdown();
		
		// add reloading carousel script after updates
		const observer = new MutationObserver( mutations => {
			for (const mutation of mutations) { 
				if (mutation.type == "attributes"
					&& mutation.target.classList.contains( 'wlc-countdown') ) {
					wlcCountdown();
				}
			}
		});
		observer.observe( document.getElementById('editor'), { subtree: true, attributes: true, attributeFilter: [ 'data-props' ] } );
	} else  {
		// else call instantly
		wlcCountdown();
	}
} );