Plugin Directory

Changeset 3373293


Ignore:
Timestamp:
10/05/2025 10:43:11 PM (2 months ago)
Author:
PerS
Message:

Update to version 2.2.3 from GitHub

Location:
jobbnorge-block
Files:
38 edited
1 copied

Legend:

Unmodified
Added
Removed
  • jobbnorge-block/tags/2.2.3/CHANGELOG.md

    r3330462 r3373293  
    11# Changelog
     2
     3## 2.2.3
     4* Version bump: synchronize plugin header, constant, readme Stable tag and package.json.
     5* Enhancement: Add resilient API failure handling (HTTP status differentiation, stale cache fallback, logging hook `jobbnorge_api_request_failed`).
     6* Enhancement: Display stale cache notice when serving cached results after API failure.
    27
    38## 2.2.2
  • jobbnorge-block/tags/2.2.3/README.md

    r3322139 r3373293  
    9191### 2) Modify the block settings.
    9292
     93-   In pagination mode (default), set the number of jobs to display per page (10 is default), else set the number of jobs to display.
     94-   Sort jobs bye deadline, closest first.
     95-   Does not show jobs that are past the deadline.
    9396-   Set the number of jobs to display.
    9497-   Set the no jobs message.
  • jobbnorge-block/tags/2.2.3/build/block.json

    r3330462 r3373293  
    33  "apiVersion": 2,
    44  "name": "dss/jobbnorge",
    5   "version": "2.2.2",
     5  "version": "2.2.3",
    66  "title": "Jobbnorge",
    77  "category": "widgets",
  • jobbnorge-block/tags/2.2.3/build/init.asset.php

    r3330462 r3373293  
    1 <?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-server-side-render'), 'version' => 'd713101f8e701317709b');
     1<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-server-side-render'), 'version' => '7ed0b4ca7d9ee76915e1');
  • jobbnorge-block/tags/2.2.3/build/init.js

    r3330462 r3373293  
    1 !function(){"use strict";var e,o={938:function(e,o,t){var n=window.React,l=window.wp.primitives,r=(0,n.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,n.createElement)(l.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})),a=window.wp.blocks,i=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","apiVersion":2,"name":"dss/jobbnorge","version":"2.2.2","title":"Jobbnorge","category":"widgets","icon":"people","description":"Retrieve and display job listings from Jobbnorge.no","keywords":["jobbnorge","jobbnorge.no"],"supports":{"html":false},"attributes":{"columns":{"type":"number","default":3},"blockLayout":{"type":"string","default":"list"},"employerID":{"type":"string","default":"","role":"content"},"noJobsMessage":{"type":"string","default":""},"orderBy":{"type":"string","default":"Deadline"},"itemsToShow":{"type":"number","default":5},"displayEmployer":{"type":"boolean","default":false},"displayExcerpt":{"type":"boolean","default":true},"displayDeadline":{"type":"boolean","default":false},"displayScope":{"type":"boolean","default":false},"displayDate":{"type":"boolean","default":true},"excerptLength":{"type":"number","default":55},"enablePagination":{"type":"boolean","default":true},"jobsPerPage":{"type":"number","default":10}},"textdomain":"wp-jobbnorge-block","editorScript":"file:init.js","editorStyle":"file:editor.css","style":"file:style-init.css"}'),c=window.wp.blockEditor,b=window.wp.components,s=window.wp.element,p=(0,n.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,n.createElement)(l.Path,{d:"m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z"})),d=(0,n.createElement)(l.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},(0,n.createElement)(l.Path,{d:"M4 4v1.5h16V4H4zm8 8.5h8V11h-8v1.5zM4 20h16v-1.5H4V20zm4-8c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2z"})),m=(0,n.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,n.createElement)(l.Path,{d:"m3 5c0-1.10457.89543-2 2-2h13.5c1.1046 0 2 .89543 2 2v13.5c0 1.1046-.8954 2-2 2h-13.5c-1.10457 0-2-.8954-2-2zm2-.5h6v6.5h-6.5v-6c0-.27614.22386-.5.5-.5zm-.5 8v6c0 .2761.22386.5.5.5h6v-6.5zm8 0v6.5h6c.2761 0 .5-.2239.5-.5v-6zm0-8v6.5h6.5v-6c0-.27614-.2239-.5-.5-.5z",fillRule:"evenodd",clipRule:"evenodd"})),u=window.wp.i18n,g=window.wp.serverSideRender,w=t.n(g);const{name:h}=i;(e=>{const{metadata:o,settings:t,name:n}=e;(0,a.registerBlockType)({name:n,...o},t)})({name:h,metadata:i,settings:{icon:r,example:{attributes:{employerID:"123[, 456, 789]"}},edit:function({attributes:e,setAttributes:o}){const[t,l]=(0,s.useState)(!e.employerID),{blockLayout:a,columns:i,displayScope:g,displayDate:h,displayEmployer:y,displayExcerpt:v,employerID:f,itemsToShow:_,noJobsMessage:k,orderBy:j,enablePagination:E,jobsPerPage:C}=e;function x(t){return()=>{const n=e[t];o({[t]:!n})}}const S=(0,c.useBlockProps)();var B;if(t)return(0,n.createElement)("div",{...S},(0,n.createElement)(b.Placeholder,{icon:r,label:"Jobbnorge"},(0,n.createElement)("form",{onSubmit:function(e){e.preventDefault(),f&&(o({employerID:f}),l(!1))},className:"wp-block-dss-jobbnorge__placeholder-form"},window.wpJobbnorgeBlock&&window.wpJobbnorgeBlock.employers?(0,n.createElement)(b.SelectControl,{multiple:!0,value:f.split(","),onChange:e=>o({employerID:e.toString()}),options:(null!==(B=wpJobbnorgeBlock.employers)&&void 0!==B?B:[]).map((e=>{var o;return{label:e.label,value:e.value,disabled:null!==(o=e?.disabled)&&void 0!==o&&o}})),className:"wp-block-dss-jobbnorge__placeholder-input",help:(0,u.__)("Select employers to display. Ctrl-click (Windows) or Cmd-click (Mac) to select multiple employers. Shift-click to select a range of employers.","wp-jobbnorge-block"),__nextHasNoMarginBottom:!0}):(0,n.createElement)(b.TextControl,{placeholder:(0,u.__)("Employer ID [,id2, id3, ..]","wp-jobbnorge-block"),value:f,onChange:e=>o({employerID:e}),className:"wp-block-dss-jobbnorge__placeholder-input"}),(0,n.createElement)(b.Button,{variant:"primary",type:"submit"},(0,u.__)("Save","wp-jobbnorge-block")))));const D=[{icon:p,title:(0,u.__)("Edit Jobbnorge URL","wp-jobbnorge-block"),onClick:()=>l(!0)},{icon:d,title:(0,u.__)("List view","wp-jobbnorge-block"),onClick:()=>o({blockLayout:"list"}),isActive:"list"===a},{icon:m,title:(0,u.__)("Grid view","wp-jobbnorge-block"),onClick:()=>o({blockLayout:"grid"}),isActive:"grid"===a}];return(0,n.createElement)(n.Fragment,null,(0,n.createElement)(c.BlockControls,null,(0,n.createElement)(b.ToolbarGroup,{controls:D})),(0,n.createElement)(c.InspectorControls,null,(0,n.createElement)(b.PanelBody,{title:(0,u.__)("Settings","wp-jobbnorge-block")},(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Enable pagination","wp-jobbnorge-block"),help:(0,u.__)("When enabled, all jobs will be displayed with pagination controls. When disabled, only the specified number of jobs will be shown.","wp-jobbnorge-block"),checked:E,onChange:e=>o({enablePagination:e})}),!E&&(0,n.createElement)(b.RangeControl,{__nextHasNoMarginBottom:!0,label:(0,u.__)("Number of items","wp-jobbnorge-block"),value:_,onChange:e=>o({itemsToShow:e}),min:1,max:100,required:!0}),E&&(0,n.createElement)(b.RangeControl,{__nextHasNoMarginBottom:!0,label:(0,u.__)("Jobs per page","wp-jobbnorge-block"),value:C,onChange:e=>o({jobsPerPage:e}),min:1,max:50,required:!0}),f.includes(",")&&(0,n.createElement)(b.RadioControl,{label:(0,u.__)("Order by","wp-jobbnorge-block"),selected:j,options:[{label:(0,u.__)("Deadline","wp-jobbnorge-block"),value:"Deadline"},{label:(0,u.__)("Employer","wp-jobbnorge-block"),value:"Employer"}],onChange:e=>o({orderBy:e})}),(0,n.createElement)(b.TextareaControl,{label:(0,u.__)("No jobs found message","wp-jobbnorge-block"),help:(0,u.__)("Message to display if no jobs are found","wp-jobbnorge-block"),value:k||(0,u.__)("There are no jobs at this time.","wp-jobbnorge-block"),onChange:e=>o({noJobsMessage:e})})),(0,n.createElement)(b.PanelBody,{title:(0,u.__)("Item","wp-jobbnorge-block")},(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display employer","wp-jobbnorge-block"),checked:y,onChange:x("displayEmployer")}),(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display excerpt","wp-jobbnorge-block"),checked:v,onChange:x("displayExcerpt")}),(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display deadline","wp-jobbnorge-block"),checked:h,onChange:x("displayDate")}),(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display scope","wp-jobbnorge-block"),checked:g,onChange:x("displayScope")})),"grid"===a&&(0,n.createElement)(b.PanelBody,{title:(0,u.__)("Grid view","wp-jobbnorge-block")},(0,n.createElement)(b.RangeControl,{__nextHasNoMarginBottom:!0,label:(0,u.__)("Columns","wp-jobbnorge-block"),value:i,onChange:e=>o({columns:e}),min:2,max:6,required:!0}))),(0,n.createElement)("div",{...S},(0,n.createElement)(b.Disabled,null,(0,n.createElement)(w(),{block:"dss/jobbnorge",attributes:e,httpMethod:"POST"}))))}}})}},t={};function n(e){var l=t[e];if(void 0!==l)return l.exports;var r=t[e]={exports:{}};return o[e](r,r.exports,n),r.exports}n.m=o,e=[],n.O=function(o,t,l,r){if(!t){var a=1/0;for(s=0;s<e.length;s++){t=e[s][0],l=e[s][1],r=e[s][2];for(var i=!0,c=0;c<t.length;c++)(!1&r||a>=r)&&Object.keys(n.O).every((function(e){return n.O[e](t[c])}))?t.splice(c--,1):(i=!1,r<a&&(a=r));if(i){e.splice(s--,1);var b=l();void 0!==b&&(o=b)}}return o}r=r||0;for(var s=e.length;s>0&&e[s-1][2]>r;s--)e[s]=e[s-1];e[s]=[t,l,r]},n.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(o,{a:o}),o},n.d=function(e,o){for(var t in o)n.o(o,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:o[t]})},n.o=function(e,o){return Object.prototype.hasOwnProperty.call(e,o)},function(){var e={410:0,308:0};n.O.j=function(o){return 0===e[o]};var o=function(o,t){var l,r,a=t[0],i=t[1],c=t[2],b=0;if(a.some((function(o){return 0!==e[o]}))){for(l in i)n.o(i,l)&&(n.m[l]=i[l]);if(c)var s=c(n)}for(o&&o(t);b<a.length;b++)r=a[b],n.o(e,r)&&e[r]&&e[r][0](),e[r]=0;return n.O(s)},t=self.webpackChunkjobbnorge_block=self.webpackChunkjobbnorge_block||[];t.forEach(o.bind(null,0)),t.push=o.bind(null,t.push.bind(t))}();var l=n.O(void 0,[308],(function(){return n(938)}));l=n.O(l)}();
     1!function(){"use strict";var e,o={938:function(e,o,t){var n=window.React,l=window.wp.primitives,r=(0,n.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,n.createElement)(l.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})),a=window.wp.blocks,i=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","apiVersion":2,"name":"dss/jobbnorge","version":"2.2.3","title":"Jobbnorge","category":"widgets","icon":"people","description":"Retrieve and display job listings from Jobbnorge.no","keywords":["jobbnorge","jobbnorge.no"],"supports":{"html":false},"attributes":{"columns":{"type":"number","default":3},"blockLayout":{"type":"string","default":"list"},"employerID":{"type":"string","default":"","role":"content"},"noJobsMessage":{"type":"string","default":""},"orderBy":{"type":"string","default":"Deadline"},"itemsToShow":{"type":"number","default":5},"displayEmployer":{"type":"boolean","default":false},"displayExcerpt":{"type":"boolean","default":true},"displayDeadline":{"type":"boolean","default":false},"displayScope":{"type":"boolean","default":false},"displayDate":{"type":"boolean","default":true},"excerptLength":{"type":"number","default":55},"enablePagination":{"type":"boolean","default":true},"jobsPerPage":{"type":"number","default":10}},"textdomain":"wp-jobbnorge-block","editorScript":"file:init.js","editorStyle":"file:editor.css","style":"file:style-init.css"}'),c=window.wp.blockEditor,b=window.wp.components,s=window.wp.element,p=(0,n.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,n.createElement)(l.Path,{d:"m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z"})),d=(0,n.createElement)(l.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},(0,n.createElement)(l.Path,{d:"M4 4v1.5h16V4H4zm8 8.5h8V11h-8v1.5zM4 20h16v-1.5H4V20zm4-8c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2z"})),m=(0,n.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,n.createElement)(l.Path,{d:"m3 5c0-1.10457.89543-2 2-2h13.5c1.1046 0 2 .89543 2 2v13.5c0 1.1046-.8954 2-2 2h-13.5c-1.10457 0-2-.8954-2-2zm2-.5h6v6.5h-6.5v-6c0-.27614.22386-.5.5-.5zm-.5 8v6c0 .2761.22386.5.5.5h6v-6.5zm8 0v6.5h6c.2761 0 .5-.2239.5-.5v-6zm0-8v6.5h6.5v-6c0-.27614-.2239-.5-.5-.5z",fillRule:"evenodd",clipRule:"evenodd"})),u=window.wp.i18n,g=window.wp.serverSideRender,w=t.n(g);const{name:h}=i;(e=>{const{metadata:o,settings:t,name:n}=e;(0,a.registerBlockType)({name:n,...o},t)})({name:h,metadata:i,settings:{icon:r,example:{attributes:{employerID:"123[, 456, 789]"}},edit:function({attributes:e,setAttributes:o}){const[t,l]=(0,s.useState)(!e.employerID),{blockLayout:a,columns:i,displayScope:g,displayDate:h,displayEmployer:y,displayExcerpt:v,employerID:f,itemsToShow:_,noJobsMessage:k,orderBy:j,enablePagination:E,jobsPerPage:C}=e;function x(t){return()=>{const n=e[t];o({[t]:!n})}}const S=(0,c.useBlockProps)();var B;if(t)return(0,n.createElement)("div",{...S},(0,n.createElement)(b.Placeholder,{icon:r,label:"Jobbnorge"},(0,n.createElement)("form",{onSubmit:function(e){e.preventDefault(),f&&(o({employerID:f}),l(!1))},className:"wp-block-dss-jobbnorge__placeholder-form"},window.wpJobbnorgeBlock&&window.wpJobbnorgeBlock.employers?(0,n.createElement)(b.SelectControl,{multiple:!0,value:f.split(","),onChange:e=>o({employerID:e.toString()}),options:(null!==(B=window.wpJobbnorgeBlock?.employers)&&void 0!==B?B:[]).map((e=>{var o;return{label:e.label,value:e.value,disabled:null!==(o=e?.disabled)&&void 0!==o&&o}})),className:"wp-block-dss-jobbnorge__placeholder-input",help:(0,u.__)("Select employers to display. Ctrl-click (Windows) or Cmd-click (Mac) to select multiple employers. Shift-click to select a range of employers.","wp-jobbnorge-block"),__nextHasNoMarginBottom:!0}):(0,n.createElement)(b.TextControl,{placeholder:(0,u.__)("Employer ID [,id2, id3, ..]","wp-jobbnorge-block"),value:f,onChange:e=>o({employerID:e}),className:"wp-block-dss-jobbnorge__placeholder-input"}),(0,n.createElement)(b.Button,{variant:"primary",type:"submit"},(0,u.__)("Save","wp-jobbnorge-block")))));const D=[{icon:p,title:(0,u.__)("Edit Jobbnorge URL","wp-jobbnorge-block"),onClick:()=>l(!0)},{icon:d,title:(0,u.__)("List view","wp-jobbnorge-block"),onClick:()=>o({blockLayout:"list"}),isActive:"list"===a},{icon:m,title:(0,u.__)("Grid view","wp-jobbnorge-block"),onClick:()=>o({blockLayout:"grid"}),isActive:"grid"===a}];return(0,n.createElement)(n.Fragment,null,(0,n.createElement)(c.BlockControls,null,(0,n.createElement)(b.ToolbarGroup,{controls:D})),(0,n.createElement)(c.InspectorControls,null,(0,n.createElement)(b.PanelBody,{title:(0,u.__)("Settings","wp-jobbnorge-block")},(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Enable pagination","wp-jobbnorge-block"),help:(0,u.__)("When enabled, all jobs will be displayed with pagination controls. When disabled, only the specified number of jobs will be shown.","wp-jobbnorge-block"),checked:E,onChange:e=>o({enablePagination:e})}),!E&&(0,n.createElement)(b.RangeControl,{__nextHasNoMarginBottom:!0,label:(0,u.__)("Number of items","wp-jobbnorge-block"),value:_,onChange:e=>o({itemsToShow:e}),min:1,max:100,required:!0}),E&&(0,n.createElement)(b.RangeControl,{__nextHasNoMarginBottom:!0,label:(0,u.__)("Jobs per page","wp-jobbnorge-block"),value:C,onChange:e=>o({jobsPerPage:e}),min:1,max:50,required:!0}),f.includes(",")&&(0,n.createElement)(b.RadioControl,{label:(0,u.__)("Order by","wp-jobbnorge-block"),selected:j,options:[{label:(0,u.__)("Deadline","wp-jobbnorge-block"),value:"Deadline"},{label:(0,u.__)("Employer","wp-jobbnorge-block"),value:"Employer"}],onChange:e=>o({orderBy:e})}),(0,n.createElement)(b.TextareaControl,{label:(0,u.__)("No jobs found message","wp-jobbnorge-block"),help:(0,u.__)("Message to display if no jobs are found","wp-jobbnorge-block"),value:k||(0,u.__)("There are no jobs at this time.","wp-jobbnorge-block"),onChange:e=>o({noJobsMessage:e})})),(0,n.createElement)(b.PanelBody,{title:(0,u.__)("Item","wp-jobbnorge-block")},(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display employer","wp-jobbnorge-block"),checked:y,onChange:x("displayEmployer")}),(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display excerpt","wp-jobbnorge-block"),checked:v,onChange:x("displayExcerpt")}),(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display deadline","wp-jobbnorge-block"),checked:h,onChange:x("displayDate")}),(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display scope","wp-jobbnorge-block"),checked:g,onChange:x("displayScope")})),"grid"===a&&(0,n.createElement)(b.PanelBody,{title:(0,u.__)("Grid view","wp-jobbnorge-block")},(0,n.createElement)(b.RangeControl,{__nextHasNoMarginBottom:!0,label:(0,u.__)("Columns","wp-jobbnorge-block"),value:i,onChange:e=>o({columns:e}),min:2,max:6,required:!0}))),(0,n.createElement)("div",{...S},(0,n.createElement)(b.Disabled,null,(0,n.createElement)(w(),{block:"dss/jobbnorge",attributes:e,httpMethod:"POST"}))))}}})}},t={};function n(e){var l=t[e];if(void 0!==l)return l.exports;var r=t[e]={exports:{}};return o[e](r,r.exports,n),r.exports}n.m=o,e=[],n.O=function(o,t,l,r){if(!t){var a=1/0;for(s=0;s<e.length;s++){t=e[s][0],l=e[s][1],r=e[s][2];for(var i=!0,c=0;c<t.length;c++)(!1&r||a>=r)&&Object.keys(n.O).every((function(e){return n.O[e](t[c])}))?t.splice(c--,1):(i=!1,r<a&&(a=r));if(i){e.splice(s--,1);var b=l();void 0!==b&&(o=b)}}return o}r=r||0;for(var s=e.length;s>0&&e[s-1][2]>r;s--)e[s]=e[s-1];e[s]=[t,l,r]},n.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(o,{a:o}),o},n.d=function(e,o){for(var t in o)n.o(o,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:o[t]})},n.o=function(e,o){return Object.prototype.hasOwnProperty.call(e,o)},function(){var e={410:0,308:0};n.O.j=function(o){return 0===e[o]};var o=function(o,t){var l,r,a=t[0],i=t[1],c=t[2],b=0;if(a.some((function(o){return 0!==e[o]}))){for(l in i)n.o(i,l)&&(n.m[l]=i[l]);if(c)var s=c(n)}for(o&&o(t);b<a.length;b++)r=a[b],n.o(e,r)&&e[r]&&e[r][0](),e[r]=0;return n.O(s)},t=self.webpackChunkjobbnorge_block=self.webpackChunkjobbnorge_block||[];t.forEach(o.bind(null,0)),t.push=o.bind(null,t.push.bind(t))}();var l=n.O(void 0,[308],(function(){return n(938)}));l=n.O(l)}();
  • jobbnorge-block/tags/2.2.3/build/pagination.asset.php

    r3322310 r3373293  
    1 <?php return array('dependencies' => array(), 'version' => '09b126faf8a7661ac618');
     1<?php return array('dependencies' => array(), 'version' => 'c74730f64eb48d06bafd');
  • jobbnorge-block/tags/2.2.3/build/pagination.js

    r3322310 r3373293  
    1 !function(){"use strict";function e(){document.querySelectorAll(".wp-block-dss-jobbnorge__pagination").forEach((function(e){const o=e.closest(".wp-block-dss-jobbnorge");if(!o)return;const n=o.getAttribute("data-attributes");if(!n)return;let r;try{r=JSON.parse(n)}catch(e){return void console.error("Failed to parse block attributes:",e)}const a=e.querySelector(".wp-block-dss-jobbnorge__pagination-prev"),i=e.querySelector(".wp-block-dss-jobbnorge__pagination-next");a&&!a.disabled&&a.addEventListener("click",(function(e){e.preventDefault(),t(parseInt(this.getAttribute("data-page")),r,o)})),i&&!i.disabled&&i.addEventListener("click",(function(e){e.preventDefault(),t(parseInt(this.getAttribute("data-page")),r,o)}))}))}function t(t,n,r){r.classList.add("wp-block-dss-jobbnorge__loading"),r.querySelectorAll(".wp-block-dss-jobbnorge__pagination button").forEach((function(e){e.disabled=!0}));const a=new FormData;a.append("action","jobbnorge_get_jobs"),a.append("page",t),a.append("attributes",JSON.stringify(n)),a.append("nonce",jobbnorgeAjax.nonce),fetch(jobbnorgeAjax.ajaxUrl,{method:"POST",body:a}).then((function(e){return e.json()})).then((function(n){if(n.success){r.innerHTML=n.data.html,e();const o=r.getBoundingClientRect(),a=2*parseFloat(getComputedStyle(document.documentElement).fontSize),i=window.pageYOffset+o.top-a;window.scrollTo({top:i,behavior:"smooth"}),function(e){if(history.pushState){const t=new URL(window.location);e>1?t.searchParams.set("jobbnorge_page",e):t.searchParams.delete("jobbnorge_page"),history.pushState({page:e},"",t)}}(t)}else console.error("AJAX request failed:",n.data),o(r,"Failed to load page. Please try again.")})).catch((function(e){console.error("AJAX request error:",e),o(r,"An error occurred while loading the page.")})).finally((function(){r.classList.remove("wp-block-dss-jobbnorge__loading")}))}function o(e,t){const o=document.createElement("div");o.className="wp-block-dss-jobbnorge__error notice notice-error",o.innerHTML="<p>"+t+"</p>",e.insertBefore(o,e.firstChild),setTimeout((function(){o.parentNode&&o.parentNode.removeChild(o)}),5e3)}document.addEventListener("DOMContentLoaded",(function(){e()})),window.addEventListener("popstate",(function(e){e.state&&e.state.page&&window.location.reload()}))}();
     1!function(){"use strict";function e(){document.querySelectorAll(".wp-block-dss-jobbnorge__pagination").forEach((function(e){const o=e.closest(".wp-block-dss-jobbnorge");if(!o)return;const n=o.getAttribute("data-attributes");if(!n)return;let r;try{r=JSON.parse(n)}catch(e){return void console.error("Failed to parse block attributes:",e)}const a=e.querySelector(".wp-block-dss-jobbnorge__pagination-prev"),i=e.querySelector(".wp-block-dss-jobbnorge__pagination-next");a&&!a.disabled&&a.addEventListener("click",(function(e){e.preventDefault(),t(parseInt(this.getAttribute("data-page")),r,o)})),i&&!i.disabled&&i.addEventListener("click",(function(e){e.preventDefault(),t(parseInt(this.getAttribute("data-page")),r,o)}))}))}function t(t,n,r){r.classList.add("wp-block-dss-jobbnorge__loading"),r.querySelectorAll(".wp-block-dss-jobbnorge__pagination button").forEach((function(e){e.disabled=!0}));const a=new FormData;a.append("action","jobbnorge_get_jobs"),a.append("page",t),a.append("attributes",JSON.stringify(n)),a.append("nonce",window.jobbnorgeAjax?.nonce||""),fetch(window.jobbnorgeAjax?.ajaxUrl||window.ajaxurl||"",{method:"POST",body:a}).then((function(e){return e.json()})).then((function(n){if(n.success){r.innerHTML=n.data.html,e();const o=r.getBoundingClientRect(),a=2*parseFloat(window.getComputedStyle(document.documentElement).fontSize),i=window.pageYOffset+o.top-a;window.scrollTo({top:i,behavior:"smooth"}),function(e){if(window.history.pushState){const t=new URL(window.location);e>1?t.searchParams.set("jobbnorge_page",e):t.searchParams.delete("jobbnorge_page"),window.history.pushState({page:e},"",t)}}(t)}else console.error("AJAX request failed:",n.data),o(r,"Failed to load page. Please try again.")})).catch((function(e){console.error("AJAX request error:",e),o(r,"An error occurred while loading the page.")})).finally((function(){r.classList.remove("wp-block-dss-jobbnorge__loading")}))}function o(e,t){const o=document.createElement("div");o.className="wp-block-dss-jobbnorge__error notice notice-error",o.innerHTML="<p>"+t+"</p>",e.insertBefore(o,e.firstChild),setTimeout((function(){o.parentNode&&o.parentNode.removeChild(o)}),5e3)}document.addEventListener("DOMContentLoaded",(function(){e()})),window.addEventListener("popstate",(function(e){e.state&&e.state.page&&window.location.reload()}))}();
  • jobbnorge-block/tags/2.2.3/build/style-init.css

    r3322139 r3373293  
    1 ul.wp-block-dss-jobbnorge{list-style:none;padding:0}ul.wp-block-dss-jobbnorge.wp-block-dss-jobbnorge{box-sizing:border-box}ul.wp-block-dss-jobbnorge.alignleft{margin-right:2em}ul.wp-block-dss-jobbnorge.alignright{margin-left:2em}ul.wp-block-dss-jobbnorge li{margin:0 0 1em}ul.wp-block-dss-jobbnorge.is-grid{display:flex;flex-wrap:wrap;list-style:none;padding:0}ul.wp-block-dss-jobbnorge.is-grid li{margin:0 1em 1em 0;width:100%}@media(min-width:600px){ul.wp-block-dss-jobbnorge.columns-2 li{width:calc(50% - 1em)}ul.wp-block-dss-jobbnorge.columns-3 li{width:calc(33.33333% - 1em)}ul.wp-block-dss-jobbnorge.columns-4 li{width:calc(25% - 1em)}ul.wp-block-dss-jobbnorge.columns-5 li{width:calc(20% - 1em)}ul.wp-block-dss-jobbnorge.columns-6 li{width:calc(16.66667% - 1em)}}.wp-block-dss-jobbnorge__item-title{font-size:1.125em;font-weight:600;margin:0 0 .25em}.wp-block-dss-jobbnorge__item-meta{margin:0 0 .25em;padding:0}.wp-block-dss-jobbnorge__item-deadline,.wp-block-dss-jobbnorge__item-employer,.wp-block-dss-jobbnorge__item-scope{display:block;font-size:.8125em;font-weight:600}.wp-block-dss-jobbnorge__pagination{border-top:1px solid #e0e0e0;display:flex;flex-direction:column;gap:1rem;margin-top:2rem;padding:1rem 0}@media(min-width:600px){.wp-block-dss-jobbnorge__pagination{align-items:center;flex-direction:row;justify-content:space-between}}.wp-block-dss-jobbnorge__pagination-info{color:#666;font-size:.875rem;margin:0}.wp-block-dss-jobbnorge__pagination-controls{align-items:center;display:flex;gap:.5rem}.wp-block-dss-jobbnorge__pagination-controls button{background:#fff;border:1px solid #ddd;border-radius:4px;cursor:pointer;font-size:.875rem;padding:.5rem 1rem;transition:all .2s ease}.wp-block-dss-jobbnorge__pagination-controls button:hover:not(:disabled){background:#f5f5f5;border-color:#999}.wp-block-dss-jobbnorge__pagination-controls button:disabled{cursor:not-allowed;opacity:.5}.wp-block-dss-jobbnorge__pagination-controls .wp-block-dss-jobbnorge__pagination-info{color:#333;font-size:.875rem;margin:0 .5rem}.wp-block-dss-jobbnorge__loading{opacity:.6;pointer-events:none}.wp-block-dss-jobbnorge__loading:after{animation:spin 1s linear infinite;border:2px solid #ccc;border-radius:50%;border-top-color:#333;content:"";height:20px;left:50%;margin:-10px 0 0 -10px;position:absolute;top:50%;width:20px}.wp-block-dss-jobbnorge__error{background:#ffebe8;border:1px solid #d63638;border-radius:4px;color:#d63638;margin:1rem 0;padding:.75rem}.wp-block-dss-jobbnorge__error p{margin:0}@keyframes spin{to{transform:rotate(1turn)}}
     1ul.wp-block-dss-jobbnorge{list-style:none;padding:0}ul.wp-block-dss-jobbnorge.wp-block-dss-jobbnorge{box-sizing:border-box}ul.wp-block-dss-jobbnorge.alignleft{margin-right:2em}ul.wp-block-dss-jobbnorge.alignright{margin-left:2em}ul.wp-block-dss-jobbnorge li{margin:0 0 1em}ul.wp-block-dss-jobbnorge.is-grid{display:flex;flex-wrap:wrap;list-style:none;padding:0}ul.wp-block-dss-jobbnorge.is-grid li{margin:0 1em 1em 0;width:100%}@media(min-width:600px){ul.wp-block-dss-jobbnorge.columns-2 li{width:calc(50% - 1em)}ul.wp-block-dss-jobbnorge.columns-3 li{width:calc(33.33333% - 1em)}ul.wp-block-dss-jobbnorge.columns-4 li{width:calc(25% - 1em)}ul.wp-block-dss-jobbnorge.columns-5 li{width:calc(20% - 1em)}ul.wp-block-dss-jobbnorge.columns-6 li{width:calc(16.66667% - 1em)}}.wp-block-dss-jobbnorge__item-title{font-size:1.125em;font-weight:600;margin:0 0 .25em}.wp-block-dss-jobbnorge__item-meta{margin:0 0 .25em;padding:0}.wp-block-dss-jobbnorge__item-deadline,.wp-block-dss-jobbnorge__item-employer,.wp-block-dss-jobbnorge__item-scope{display:block;font-size:.8125em;font-weight:600}.wp-block-dss-jobbnorge__pagination{border-top:1px solid #e0e0e0;display:flex;flex-direction:column;gap:1rem;margin-top:2rem;padding:1rem 0}@media(min-width:600px){.wp-block-dss-jobbnorge__pagination{align-items:center;flex-direction:row;justify-content:space-between}}.wp-block-dss-jobbnorge__pagination-info{color:#666;font-size:.875rem;margin:0}.wp-block-dss-jobbnorge__pagination-controls{align-items:center;display:flex;gap:.5rem}.wp-block-dss-jobbnorge__pagination-controls button{background:#fff;border:1px solid #ddd;border-radius:4px;cursor:pointer;font-size:.875rem;padding:.5rem 1rem;transition:all .2s ease}.wp-block-dss-jobbnorge__pagination-controls button:disabled{cursor:not-allowed;opacity:.5}.wp-block-dss-jobbnorge__pagination-controls button:hover:not(:disabled){background:#f5f5f5;border-color:#999}.wp-block-dss-jobbnorge__pagination-controls .wp-block-dss-jobbnorge__pagination-info{color:#333;font-size:.875rem;margin:0 .5rem}.wp-block-dss-jobbnorge__loading{opacity:.6;pointer-events:none}.wp-block-dss-jobbnorge__loading:after{animation:spin 1s linear infinite;border:2px solid #ccc;border-radius:50%;border-top-color:#333;content:"";height:20px;left:50%;margin:-10px 0 0 -10px;position:absolute;top:50%;width:20px}.wp-block-dss-jobbnorge__error{background:#ffebe8;border:1px solid #d63638;border-radius:4px;color:#d63638;margin:1rem 0;padding:.75rem}.wp-block-dss-jobbnorge__error p{margin:0}@keyframes spin{to{transform:rotate(1turn)}}
  • jobbnorge-block/tags/2.2.3/package-lock.json

    r3330462 r3373293  
    11{
    22    "name": "jobbnorge-block",
    3     "version": "2.2.2",
     3    "version": "2.2.3",
    44    "lockfileVersion": 3,
    55    "requires": true,
     
    77        "": {
    88            "name": "jobbnorge-block",
    9             "version": "2.2.2",
     9            "version": "2.2.3",
    1010            "license": "GPL-2.0-or-later",
    1111            "dependencies": {
  • jobbnorge-block/tags/2.2.3/package.json

    r3330462 r3373293  
    11{
    22    "name": "jobbnorge-block",
    3     "version": "2.2.2",
     3    "version": "2.2.3",
    44    "description": "Jobbnorge Block for WordPress Gutenberg",
    55    "author": "Per Søderlind <[email protected]>",
  • jobbnorge-block/tags/2.2.3/readme.txt

    r3330470 r3373293  
    55Requires at least: 6.5
    66Requires PHP:      8.2
    7 Stable tag:        2.2.2
     7Stable tag:        2.2.3
    88License:           GPL-2.0-or-later
    99License URI:       https://www.gnu.org/licenses/gpl-2.0.html
     
    105105== Changelog ==
    106106
     107= 2.2.3 =
     108* Version bump: synchronize plugin header, constant, readme Stable tag and package.json.
     109* Enhancement: Add resilient API failure handling (HTTP status differentiation, stale cache fallback, logging hook `jobbnorge_api_request_failed`).
     110* Enhancement: Display stale cache notice when serving cached results after API failure.
     111
    107112= 2.2.2 =
    108113* Update block.json to include default value and role for employerID
  • jobbnorge-block/tags/2.2.3/src/block.json

    r3330462 r3373293  
    33    "apiVersion": 2,
    44    "name": "dss/jobbnorge",
    5     "version": "2.2.2",
     5    "version": "2.2.3",
    66    "title": "Jobbnorge",
    77    "category": "widgets",
  • jobbnorge-block/tags/2.2.3/src/edit.js

    r3322139 r3373293  
    22 * WordPress dependencies
    33 */
    4 import { BlockControls, InspectorControls, useBlockProps } from '@wordpress/block-editor';
     4import {
     5    BlockControls,
     6    InspectorControls,
     7    useBlockProps,
     8} from '@wordpress/block-editor';
    59import {
    610    Button,
     
    2630const DEFAULT_MAX_ITEMS = 100;
    2731
     32/* eslint-disable jsdoc/check-line-alignment */
    2833/**
    29  * Description placeholder
    30  * @date 17/11/2023 - 16:21:26
     34 * Jobbnorge block editor component.
    3135 *
    32  * @export
    33  * @param {{ attributes: any; setAttributes: any; }} param0
    34  * @param {*} param0.attributes
    35  * @param {*} param0.setAttributes
    36  * @returns {*}
     36 * @param {Object}      props              Component props.
     37 * @param {Object}      props.attributes   Block attributes.
     38 * @param {Function}    props.setAttributes Setter for block attributes.
     39 * @return {JSX.Element} Editor element.
    3740 */
    38 export default function JobbnorgeEdit({ attributes, setAttributes }) {
     41/* eslint-enable jsdoc/check-line-alignment */
     42export default function JobbnorgeEdit( { attributes, setAttributes } ) {
    3943    // Initialize the isEditing state variable. If the employerID attribute is not set, isEditing will be true.
    40     const [isEditing, setIsEditing] = useState(!attributes.employerID);
     44    const [ isEditing, setIsEditing ] = useState( ! attributes.employerID );
    4145
    4246    // Destructure the attributes object to get the individual attributes.
     
    5862    // Define a function to toggle an attribute.
    5963    // This function returns another function that, when called, will toggle the value of the attribute specified by propName.
    60     function toggleAttribute(propName) {
     64    function toggleAttribute( propName ) {
    6165        return () => {
    62             const value = attributes[propName];
    63 
    64             setAttributes({ [propName]: !value });
     66            const value = attributes[ propName ];
     67
     68            setAttributes( { [ propName ]: ! value } );
    6569        };
    6670    }
     
    6872    // Define a function to handle the form submission.
    6973    // This function will set the employerID attribute and set isEditing to false.
    70     function onSubmitURL(event) {
     74    function onSubmitURL( event ) {
    7175        event.preventDefault();
    7276
    73         if (employerID) {
    74             setAttributes({ employerID: employerID });
    75             setIsEditing(false);
     77        if ( employerID ) {
     78            setAttributes( { employerID } );
     79            setIsEditing( false );
    7680        }
    7781    }
     
    7983    const blockProps = useBlockProps();
    8084
    81     if (isEditing) {
     85    if ( isEditing ) {
    8286        return (
    83             <div {...blockProps}>
    84                 <Placeholder icon={people} label="Jobbnorge">
    85                     <form onSubmit={onSubmitURL} className="wp-block-dss-jobbnorge__placeholder-form">
    86                         {window.wpJobbnorgeBlock && window.wpJobbnorgeBlock.employers ? (
     87            <div { ...blockProps }>
     88                <Placeholder icon={ people } label="Jobbnorge">
     89                    <form
     90                        onSubmit={ onSubmitURL }
     91                        className="wp-block-dss-jobbnorge__placeholder-form"
     92                    >
     93                        { window.wpJobbnorgeBlock &&
     94                        window.wpJobbnorgeBlock.employers ? (
    8795                            <SelectControl
    8896                                multiple
    89                                 value={employerID.split(',')}
    90                                 onChange={(value) => setAttributes({ employerID: value.toString() })}
    91                                 options={(wpJobbnorgeBlock.employers ?? []).map((o) => ({
     97                                value={ employerID.split( ',' ) }
     98                                onChange={ ( value ) =>
     99                                    setAttributes( {
     100                                        employerID: value.toString(),
     101                                    } )
     102                                }
     103                                options={ (
     104                                    window.wpJobbnorgeBlock?.employers ?? []
     105                                ).map( ( o ) => ( {
    92106                                    label: o.label,
    93107                                    value: o.value,
    94108                                    disabled: o?.disabled ?? false,
    95                                 }))}
     109                                } ) ) }
    96110                                className="wp-block-dss-jobbnorge__placeholder-input"
    97                                 help={__(
     111                                help={ __(
    98112                                    'Select employers to display. Ctrl-click (Windows) or Cmd-click (Mac) to select multiple employers. Shift-click to select a range of employers.',
    99113                                    'wp-jobbnorge-block'
    100                                 )}
     114                                ) }
    101115                                __nextHasNoMarginBottom
    102116                            />
    103117                        ) : (
    104118                            <TextControl
    105                                 placeholder={__('Employer ID [,id2, id3, ..]', 'wp-jobbnorge-block')}
    106                                 value={employerID}
    107                                 onChange={(value) => setAttributes({ employerID: value })}
     119                                placeholder={ __(
     120                                    'Employer ID [,id2, id3, ..]',
     121                                    'wp-jobbnorge-block'
     122                                ) }
     123                                value={ employerID }
     124                                onChange={ ( value ) =>
     125                                    setAttributes( { employerID: value } )
     126                                }
    108127                                className="wp-block-dss-jobbnorge__placeholder-input"
    109128                            />
    110                         )}
     129                        ) }
    111130                        <Button variant="primary" type="submit">
    112                             {__('Save', 'wp-jobbnorge-block')}
     131                            { __( 'Save', 'wp-jobbnorge-block' ) }
    113132                        </Button>
    114133                    </form>
     
    121140        {
    122141            icon: edit,
    123             title: __('Edit Jobbnorge URL', 'wp-jobbnorge-block'),
    124             onClick: () => setIsEditing(true),
     142            title: __( 'Edit Jobbnorge URL', 'wp-jobbnorge-block' ),
     143            onClick: () => setIsEditing( true ),
    125144        },
    126145        {
    127146            icon: list,
    128             title: __('List view', 'wp-jobbnorge-block'),
    129             onClick: () => setAttributes({ blockLayout: 'list' }),
     147            title: __( 'List view', 'wp-jobbnorge-block' ),
     148            onClick: () => setAttributes( { blockLayout: 'list' } ),
    130149            isActive: blockLayout === 'list',
    131150        },
    132151        {
    133152            icon: grid,
    134             title: __('Grid view', 'wp-jobbnorge-block'),
    135             onClick: () => setAttributes({ blockLayout: 'grid' }),
     153            title: __( 'Grid view', 'wp-jobbnorge-block' ),
     154            onClick: () => setAttributes( { blockLayout: 'grid' } ),
    136155            isActive: blockLayout === 'grid',
    137156        },
     
    141160        <>
    142161            <BlockControls>
    143                 <ToolbarGroup controls={toolbarControls} />
     162                <ToolbarGroup controls={ toolbarControls } />
    144163            </BlockControls>
    145164            <InspectorControls>
    146                 <PanelBody title={__('Settings', 'wp-jobbnorge-block')}>
    147                     <ToggleControl
    148                         label={__('Enable pagination', 'wp-jobbnorge-block')}
    149                         help={__('When enabled, all jobs will be displayed with pagination controls. When disabled, only the specified number of jobs will be shown.', 'wp-jobbnorge-block')}
    150                         checked={enablePagination}
    151                         onChange={(value) => setAttributes({ enablePagination: value })}
    152                     />
    153                     {!enablePagination && (
     165                <PanelBody title={ __( 'Settings', 'wp-jobbnorge-block' ) }>
     166                    <ToggleControl
     167                        label={ __(
     168                            'Enable pagination',
     169                            'wp-jobbnorge-block'
     170                        ) }
     171                        help={ __(
     172                            'When enabled, all jobs will be displayed with pagination controls. When disabled, only the specified number of jobs will be shown.',
     173                            'wp-jobbnorge-block'
     174                        ) }
     175                        checked={ enablePagination }
     176                        onChange={ ( value ) =>
     177                            setAttributes( { enablePagination: value } )
     178                        }
     179                    />
     180                    { ! enablePagination && (
    154181                        <RangeControl
    155182                            __nextHasNoMarginBottom
    156                             label={__('Number of items', 'wp-jobbnorge-block')}
    157                             value={itemsToShow}
    158                             onChange={(value) => setAttributes({ itemsToShow: value })}
    159                             min={DEFAULT_MIN_ITEMS}
    160                             max={DEFAULT_MAX_ITEMS}
     183                            label={ __(
     184                                'Number of items',
     185                                'wp-jobbnorge-block'
     186                            ) }
     187                            value={ itemsToShow }
     188                            onChange={ ( value ) =>
     189                                setAttributes( { itemsToShow: value } )
     190                            }
     191                            min={ DEFAULT_MIN_ITEMS }
     192                            max={ DEFAULT_MAX_ITEMS }
    161193                            required
    162194                        />
    163                     )}
    164                     {enablePagination && (
     195                    ) }
     196                    { enablePagination && (
    165197                        <RangeControl
    166198                            __nextHasNoMarginBottom
    167                             label={__('Jobs per page', 'wp-jobbnorge-block')}
    168                             value={jobsPerPage}
    169                             onChange={(value) => setAttributes({ jobsPerPage: value })}
    170                             min={1}
    171                             max={50}
     199                            label={ __(
     200                                'Jobs per page',
     201                                'wp-jobbnorge-block'
     202                            ) }
     203                            value={ jobsPerPage }
     204                            onChange={ ( value ) =>
     205                                setAttributes( { jobsPerPage: value } )
     206                            }
     207                            min={ 1 }
     208                            max={ 50 }
    172209                            required
    173210                        />
    174                     )}
    175                     {employerID.includes(',') && (
     211                    ) }
     212                    { employerID.includes( ',' ) && (
    176213                        <RadioControl
    177                             label={__('Order by', 'wp-jobbnorge-block')}
    178                             selected={orderBy}
    179                             options={[
    180                                 { label: __('Deadline', 'wp-jobbnorge-block'), value: 'Deadline' },
    181                                 { label: __('Employer', 'wp-jobbnorge-block'), value: 'Employer' },
    182                             ]}
    183                             onChange={(value) => setAttributes({ orderBy: value })}
     214                            label={ __( 'Order by', 'wp-jobbnorge-block' ) }
     215                            selected={ orderBy }
     216                            options={ [
     217                                {
     218                                    label: __(
     219                                        'Deadline',
     220                                        'wp-jobbnorge-block'
     221                                    ),
     222                                    value: 'Deadline',
     223                                },
     224                                {
     225                                    label: __(
     226                                        'Employer',
     227                                        'wp-jobbnorge-block'
     228                                    ),
     229                                    value: 'Employer',
     230                                },
     231                            ] }
     232                            onChange={ ( value ) =>
     233                                setAttributes( { orderBy: value } )
     234                            }
    184235                        />
    185                     )}
     236                    ) }
    186237                    <TextareaControl
    187                         label={__('No jobs found message', 'wp-jobbnorge-block')}
    188                         help={__('Message to display if no jobs are found', 'wp-jobbnorge-block')}
    189                         value={noJobsMessage || __('There are no jobs at this time.', 'wp-jobbnorge-block')}
    190                         onChange={(value) => setAttributes({ noJobsMessage: value })}
     238                        label={ __(
     239                            'No jobs found message',
     240                            'wp-jobbnorge-block'
     241                        ) }
     242                        help={ __(
     243                            'Message to display if no jobs are found',
     244                            'wp-jobbnorge-block'
     245                        ) }
     246                        value={
     247                            noJobsMessage ||
     248                            __(
     249                                'There are no jobs at this time.',
     250                                'wp-jobbnorge-block'
     251                            )
     252                        }
     253                        onChange={ ( value ) =>
     254                            setAttributes( { noJobsMessage: value } )
     255                        }
    191256                    />
    192257                </PanelBody>
    193                 <PanelBody title={__('Item', 'wp-jobbnorge-block')}>
    194                     <ToggleControl
    195                         label={__('Display employer', 'wp-jobbnorge-block')}
    196                         checked={displayEmployer}
    197                         onChange={toggleAttribute('displayEmployer')}
    198                     />
    199                     <ToggleControl
    200                         label={__('Display excerpt', 'wp-jobbnorge-block')}
    201                         checked={displayExcerpt}
    202                         onChange={toggleAttribute('displayExcerpt')}
    203                     />
    204                     <ToggleControl
    205                         label={__('Display deadline', 'wp-jobbnorge-block')}
    206                         checked={displayDate}
    207                         onChange={toggleAttribute('displayDate')}
    208                     />
    209                     <ToggleControl
    210                         label={__('Display scope', 'wp-jobbnorge-block')}
    211                         checked={displayScope}
    212                         onChange={toggleAttribute('displayScope')}
     258                <PanelBody title={ __( 'Item', 'wp-jobbnorge-block' ) }>
     259                    <ToggleControl
     260                        label={ __( 'Display employer', 'wp-jobbnorge-block' ) }
     261                        checked={ displayEmployer }
     262                        onChange={ toggleAttribute( 'displayEmployer' ) }
     263                    />
     264                    <ToggleControl
     265                        label={ __( 'Display excerpt', 'wp-jobbnorge-block' ) }
     266                        checked={ displayExcerpt }
     267                        onChange={ toggleAttribute( 'displayExcerpt' ) }
     268                    />
     269                    <ToggleControl
     270                        label={ __( 'Display deadline', 'wp-jobbnorge-block' ) }
     271                        checked={ displayDate }
     272                        onChange={ toggleAttribute( 'displayDate' ) }
     273                    />
     274                    <ToggleControl
     275                        label={ __( 'Display scope', 'wp-jobbnorge-block' ) }
     276                        checked={ displayScope }
     277                        onChange={ toggleAttribute( 'displayScope' ) }
    213278                    />
    214279                </PanelBody>
    215                 {blockLayout === 'grid' && (
    216                     <PanelBody title={__('Grid view', 'wp-jobbnorge-block')}>
     280                { blockLayout === 'grid' && (
     281                    <PanelBody
     282                        title={ __( 'Grid view', 'wp-jobbnorge-block' ) }
     283                    >
    217284                        <RangeControl
    218285                            __nextHasNoMarginBottom
    219                             label={__('Columns', 'wp-jobbnorge-block')}
    220                             value={columns}
    221                             onChange={(value) => setAttributes({ columns: value })}
    222                             min={2}
    223                             max={6}
     286                            label={ __( 'Columns', 'wp-jobbnorge-block' ) }
     287                            value={ columns }
     288                            onChange={ ( value ) =>
     289                                setAttributes( { columns: value } )
     290                            }
     291                            min={ 2 }
     292                            max={ 6 }
    224293                            required
    225294                        />
    226295                    </PanelBody>
    227                 )}
     296                ) }
    228297            </InspectorControls>
    229             <div {...blockProps}>
     298            <div { ...blockProps }>
    230299                <Disabled>
    231                     <ServerSideRender block="dss/jobbnorge" attributes={attributes} httpMethod="POST" />
     300                    <ServerSideRender
     301                        block="dss/jobbnorge"
     302                        attributes={ attributes }
     303                        httpMethod="POST"
     304                    />
    232305                </Disabled>
    233306            </div>
  • jobbnorge-block/tags/2.2.3/src/editor.scss

    r3322139 r3373293  
    55
    66@mixin break-medium() {
     7
    78    @media (min-width: #{ ($break-medium) }) {
    89        @content;
     
    1112
    1213@mixin break-small() {
     14
    1315    @media (min-width: #{ ($break-small) }) {
    1416        @content;
     
    1618}
    1719
    18 .wp-block-dss-jobbnorge li a>div {
     20.wp-block-dss-jobbnorge li a > div {
    1921    display: inline;
    2022}
     
    2931
    3032    @include break-medium() {
     33
    3134        >* {
    3235            margin-bottom: 0;
     
    7578
    7679    @include break-small {
     80
    7781        @for $i from 2 through 6 {
    7882            &.columns-#{ $i } li {
  • jobbnorge-block/tags/2.2.3/src/index.js

    r2997962 r3373293  
    22 * WordPress dependencies
    33 */
    4 import { people as icon } from "@wordpress/icons";
    5 import { registerBlockType } from "@wordpress/blocks";
     4import { people as icon } from '@wordpress/icons';
     5import { registerBlockType } from '@wordpress/blocks';
    66/**
    77 * Internal dependencies
    88 */
    9 import "./style.scss";
    10 import metadata from "./block.json";
    11 import edit from "./edit";
     9import './style.scss';
     10import metadata from './block.json';
     11import edit from './edit';
    1212
    1313const { name } = metadata;
     
    1919    example: {
    2020        attributes: {
    21             employerID: "123[, 456, 789]",
     21            employerID: '123[, 456, 789]',
    2222        },
    2323    },
     
    2525};
    2626
    27 const initBlock = (block) => {
    28     // if (!block) {
    29     //  return;
    30     // }
    31     const { metadata, settings, name } = block;
    32     return registerBlockType({ name, ...metadata }, settings);
     27const initBlock = ( block ) => {
     28    // Accept an object with keys name, metadata, settings without shadowing outer scope.
     29    const { metadata: md, settings: st, name: blockName } = block;
     30    return registerBlockType( { name: blockName, ...md }, st );
    3331};
    3432
    35 export const init = () => initBlock({ name, metadata, settings });
     33export const init = () => initBlock( { name, metadata, settings } );
  • jobbnorge-block/tags/2.2.3/src/pagination.js

    r3322310 r3373293  
    11/**
    22 * Jobbnorge Block Pagination JavaScript
    3  * 
     3 *
    44 * Handles AJAX pagination for job listings.
    55 */
    66
    7 (function() {
    8     'use strict';
    9    
    10     // Initialize pagination when DOM is ready
    11     document.addEventListener('DOMContentLoaded', function() {
    12         initializePagination();
    13     });
    14    
    15     /**
    16      * Initialize pagination functionality
    17      */
    18     function initializePagination() {
    19         const paginationContainers = document.querySelectorAll('.wp-block-dss-jobbnorge__pagination');
    20        
    21         paginationContainers.forEach(function(container) {
    22             const blockContainer = container.closest('.wp-block-dss-jobbnorge');
    23            
    24             if (!blockContainer) return;
    25            
    26             // Get block attributes from data attribute
    27             const attributesData = blockContainer.getAttribute('data-attributes');
    28             if (!attributesData) return;
    29            
    30             let attributes;
    31             try {
    32                 attributes = JSON.parse(attributesData);
    33             } catch (e) {
    34                 console.error('Failed to parse block attributes:', e);
    35                 return;
    36             }
    37            
    38             // Add event listeners to pagination buttons
    39             const prevButton = container.querySelector('.wp-block-dss-jobbnorge__pagination-prev');
    40             const nextButton = container.querySelector('.wp-block-dss-jobbnorge__pagination-next');
    41            
    42             if (prevButton && !prevButton.disabled) {
    43                 prevButton.addEventListener('click', function(e) {
    44                     e.preventDefault();
    45                     const page = parseInt(this.getAttribute('data-page'));
    46                     loadPage(page, attributes, blockContainer);
    47                 });
    48             }
    49            
    50             if (nextButton && !nextButton.disabled) {
    51                 nextButton.addEventListener('click', function(e) {
    52                     e.preventDefault();
    53                     const page = parseInt(this.getAttribute('data-page'));
    54                     loadPage(page, attributes, blockContainer);
    55                 });
    56             }
    57         });
    58     }
    59    
    60     /**
    61      * Load a specific page via AJAX
    62      *
    63      * @param {number} page - Page number to load
    64      * @param {Object} attributes - Block attributes
    65      * @param {Element} container - Block container element
    66      */
    67     function loadPage(page, attributes, container) {
    68         // Show loading state
    69         container.classList.add('wp-block-dss-jobbnorge__loading');
    70        
    71         // Disable pagination buttons during loading
    72         const buttons = container.querySelectorAll('.wp-block-dss-jobbnorge__pagination button');
    73         buttons.forEach(function(button) {
    74             button.disabled = true;
    75         });
    76        
    77         // Prepare AJAX data
    78         const formData = new FormData();
    79         formData.append('action', 'jobbnorge_get_jobs');
    80         formData.append('page', page);
    81         formData.append('attributes', JSON.stringify(attributes));
    82         formData.append('nonce', jobbnorgeAjax.nonce);
    83        
    84         // Make AJAX request
    85         fetch(jobbnorgeAjax.ajaxUrl, {
    86             method: 'POST',
    87             body: formData
    88         })
    89         .then(function(response) {
    90             return response.json();
    91         })
    92         .then(function(data) {
    93             if (data.success) {
    94                 // Update the container with new content
    95                 container.innerHTML = data.data.html;
    96                
    97                 // Reinitialize pagination for the new content
    98                 initializePagination();
    99                
    100                 // Scroll to 2em above the top of the block
    101                 const containerRect = container.getBoundingClientRect();
    102                 const twoEm = parseFloat(getComputedStyle(document.documentElement).fontSize) * 2;
    103                 const targetPosition = window.pageYOffset + containerRect.top - twoEm;
    104                
    105                 window.scrollTo({
    106                     top: targetPosition,
    107                     behavior: 'smooth'
    108                 });
    109                
    110                 // Update URL with page parameter (optional)
    111                 updateURL(page);
    112                
    113             } else {
    114                 console.error('AJAX request failed:', data.data);
    115                 showError(container, 'Failed to load page. Please try again.');
    116             }
    117         })
    118         .catch(function(error) {
    119             console.error('AJAX request error:', error);
    120             showError(container, 'An error occurred while loading the page.');
    121         })
    122         .finally(function() {
    123             // Remove loading state
    124             container.classList.remove('wp-block-dss-jobbnorge__loading');
    125         });
    126     }
    127    
    128     /**
    129      * Update URL with page parameter
    130      *
    131      * @param {number} page - Current page number
    132      */
    133     function updateURL(page) {
    134         if (history.pushState) {
    135             const url = new URL(window.location);
    136             if (page > 1) {
    137                 url.searchParams.set('jobbnorge_page', page);
    138             } else {
    139                 url.searchParams.delete('jobbnorge_page');
    140             }
    141             history.pushState({ page: page }, '', url);
    142         }
    143     }
    144    
    145     /**
    146      * Show error message
    147      *
    148      * @param {Element} container - Block container
    149      * @param {string} message - Error message
    150      */
    151     function showError(container, message) {
    152         const errorDiv = document.createElement('div');
    153         errorDiv.className = 'wp-block-dss-jobbnorge__error notice notice-error';
    154         errorDiv.innerHTML = '<p>' + message + '</p>';
    155        
    156         // Insert error message at the top of the container
    157         container.insertBefore(errorDiv, container.firstChild);
    158        
    159         // Remove error message after 5 seconds
    160         setTimeout(function() {
    161             if (errorDiv.parentNode) {
    162                 errorDiv.parentNode.removeChild(errorDiv);
    163             }
    164         }, 5000);
    165     }
    166    
    167     // Handle browser back/forward buttons
    168     window.addEventListener('popstate', function(event) {
    169         if (event.state && event.state.page) {
    170             // Reload the page with the correct page number
    171             window.location.reload();
    172         }
    173     });
    174    
    175 })();
     7/* eslint-env browser */
     8( function () {
     9    'use strict';
     10
     11    // Initialize pagination when DOM is ready
     12    document.addEventListener( 'DOMContentLoaded', function () {
     13        initializePagination();
     14    } );
     15
     16    /**
     17     * Initialize pagination functionality
     18     */
     19    function initializePagination() {
     20        const paginationContainers = document.querySelectorAll(
     21            '.wp-block-dss-jobbnorge__pagination'
     22        );
     23
     24        paginationContainers.forEach( function ( container ) {
     25            const blockContainer = container.closest(
     26                '.wp-block-dss-jobbnorge'
     27            );
     28
     29            if ( ! blockContainer ) return;
     30
     31            // Get block attributes from data attribute
     32            const attributesData =
     33                blockContainer.getAttribute( 'data-attributes' );
     34            if ( ! attributesData ) return;
     35
     36            let attributes;
     37            try {
     38                attributes = JSON.parse( attributesData );
     39            } catch ( e ) {
     40                // eslint-disable-next-line no-console
     41                console.error( 'Failed to parse block attributes:', e );
     42                return;
     43            }
     44
     45            // Add event listeners to pagination buttons
     46            const prevButton = container.querySelector(
     47                '.wp-block-dss-jobbnorge__pagination-prev'
     48            );
     49            const nextButton = container.querySelector(
     50                '.wp-block-dss-jobbnorge__pagination-next'
     51            );
     52
     53            if ( prevButton && ! prevButton.disabled ) {
     54                prevButton.addEventListener( 'click', function ( e ) {
     55                    e.preventDefault();
     56                    const page = parseInt( this.getAttribute( 'data-page' ) );
     57                    loadPage( page, attributes, blockContainer );
     58                } );
     59            }
     60
     61            if ( nextButton && ! nextButton.disabled ) {
     62                nextButton.addEventListener( 'click', function ( e ) {
     63                    e.preventDefault();
     64                    const page = parseInt( this.getAttribute( 'data-page' ) );
     65                    loadPage( page, attributes, blockContainer );
     66                } );
     67            }
     68        } );
     69    }
     70
     71    /**
     72     * Load a specific page via AJAX
     73     *
     74     * @param {number}  page       - Page number to load
     75     * @param {Object}  attributes - Block attributes
     76     * @param {Element} container  - Block container element
     77     */
     78    function loadPage( page, attributes, container ) {
     79        // Show loading state
     80        container.classList.add( 'wp-block-dss-jobbnorge__loading' );
     81
     82        // Disable pagination buttons during loading
     83        const buttons = container.querySelectorAll(
     84            '.wp-block-dss-jobbnorge__pagination button'
     85        );
     86        buttons.forEach( function ( button ) {
     87            button.disabled = true;
     88        } );
     89
     90        // Prepare AJAX data
     91        const formData = new FormData();
     92        formData.append( 'action', 'jobbnorge_get_jobs' );
     93        formData.append( 'page', page );
     94        formData.append( 'attributes', JSON.stringify( attributes ) );
     95        formData.append( 'nonce', window.jobbnorgeAjax?.nonce || '' );
     96
     97        // Make AJAX request
     98        fetch( window.jobbnorgeAjax?.ajaxUrl || window.ajaxurl || '', {
     99            method: 'POST',
     100            body: formData,
     101        } )
     102            .then( function ( response ) {
     103                return response.json();
     104            } )
     105            .then( function ( data ) {
     106                if ( data.success ) {
     107                    // Update the container with new content
     108                    container.innerHTML = data.data.html;
     109
     110                    // Reinitialize pagination for the new content
     111                    initializePagination();
     112
     113                    // Scroll to 2em above the top of the block
     114                    const containerRect = container.getBoundingClientRect();
     115                    const twoEm =
     116                        parseFloat(
     117                            window.getComputedStyle( document.documentElement )
     118                                .fontSize
     119                            ) * 2;
     120                    const targetPosition =
     121                        window.pageYOffset + containerRect.top - twoEm;
     122
     123                    window.scrollTo( {
     124                        top: targetPosition,
     125                        behavior: 'smooth',
     126                    } );
     127
     128                    // Update URL with page parameter (optional)
     129                    updateURL( page );
     130                } else {
     131                    // eslint-disable-next-line no-console
     132                    console.error( 'AJAX request failed:', data.data );
     133                    showError(
     134                        container,
     135                        'Failed to load page. Please try again.'
     136                    );
     137                }
     138            } )
     139            .catch( function ( error ) {
     140                // eslint-disable-next-line no-console
     141                console.error( 'AJAX request error:', error );
     142                showError( container, 'An error occurred while loading the page.' );
     143            } )
     144            .finally( function () {
     145                // Remove loading state
     146                container.classList.remove( 'wp-block-dss-jobbnorge__loading' );
     147            } );
     148    }
     149
     150    /**
     151     * Update URL with page parameter
     152     *
     153     * @param {number} page - Current page number
     154     */
     155    function updateURL( page ) {
     156        if ( window.history.pushState ) {
     157            const url = new URL( window.location );
     158            if ( page > 1 ) {
     159                url.searchParams.set( 'jobbnorge_page', page );
     160            } else {
     161                url.searchParams.delete( 'jobbnorge_page' );
     162            }
     163            window.history.pushState( { page }, '', url );
     164        }
     165    }
     166
     167    /**
     168     * Show error message
     169     *
     170     * @param {Element} container - Block container
     171     * @param {string}  message   - Error message
     172     */
     173    function showError( container, message ) {
     174        const errorDiv = document.createElement( 'div' );
     175        errorDiv.className =
     176            'wp-block-dss-jobbnorge__error notice notice-error';
     177        errorDiv.innerHTML = '<p>' + message + '</p>';
     178
     179        // Insert error message at the top of the container
     180        container.insertBefore( errorDiv, container.firstChild );
     181
     182        // Remove error message after 5 seconds
     183        setTimeout( function () {
     184            if ( errorDiv.parentNode ) {
     185                errorDiv.parentNode.removeChild( errorDiv );
     186            }
     187        }, 5000 );
     188    }
     189
     190    // Handle browser back/forward buttons
     191    window.addEventListener( 'popstate', function ( event ) {
     192        if ( event.state && event.state.page ) {
     193            // Reload the page with the correct page number
     194            window.location.reload();
     195        }
     196    } );
     197} )();
  • jobbnorge-block/tags/2.2.3/src/style.scss

    r3322139 r3373293  
    22
    33@mixin break-small() {
     4
    45    @media (min-width: #{ ($break-small) }) {
    56        @content;
     
    1314    padding: 0;
    1415
    15     // This needs extra specificity due to the reset mixin on the parent: https://github.com/WordPress/gutenberg/blob/a250e9e5fe00dd5195624f96a3d924e7078951c3/packages/edit-post/src/style.scss#L54
     16    // This needs extra specificity due to the reset mixin on the parent:
     17    // See: https://github.com/WordPress/gutenberg/blob/a250e9e5fe00dd5195624f96a3d924e7078951c3/packages/edit-post/src/style.scss#L54
    1618    &.wp-block-dss-jobbnorge {
    1719        box-sizing: border-box;
     
    1921
    2022    &.alignleft {
     23
    2124        /*rtl:ignore*/
    2225        margin-right: 2em;
     
    2427
    2528    &.alignright {
     29
    2630        /*rtl:ignore*/
    2731        margin-left: 2em;
     
    4549
    4650    @include break-small {
     51
    4752        @for $i from 2 through 6 {
    4853            &.columns-#{ $i } li {
     
    7580// Pagination styles
    7681.wp-block-dss-jobbnorge {
     82
    7783    &__pagination {
    7884        display: flex;
     
    104110            padding: 0.5rem 1rem;
    105111            border: 1px solid #ddd;
    106             background: white;
     112            background: #fff; // replaced named color per stylelint
    107113            cursor: pointer;
    108114            border-radius: 4px;
     
    110116            transition: all 0.2s ease;
    111117
     118            &:disabled {
     119                opacity: 0.5;
     120                cursor: not-allowed;
     121            }
     122
    112123            &:hover:not(:disabled) {
    113124                background: #f5f5f5;
    114125                border-color: #999;
    115             }
    116 
    117             &:disabled {
    118                 opacity: 0.5;
    119                 cursor: not-allowed;
    120126            }
    121127        }
     
    134140
    135141        &::after {
    136             content: '';
     142            content: "";
    137143            position: absolute;
    138144            top: 50%;
     
    164170
    165171@keyframes spin {
     172
    166173    to {
    167174        transform: rotate(360deg);
  • jobbnorge-block/tags/2.2.3/webpack.config.js

    r3322139 r3373293  
    1 const defaultConfig = require('@wordpress/scripts/config/webpack.config');
    2 const path = require('path');
     1const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
     2const path = require( 'path' );
    33
    44module.exports = {
    55    ...defaultConfig,
    66    entry: {
    7         init: path.resolve(__dirname, 'src/init.js'),
    8         editor: path.resolve(__dirname, 'src/editor.scss'),
    9         style: path.resolve(__dirname, 'src/style.scss'),
    10         pagination: path.resolve(__dirname, 'src/pagination.js'),
     7        init: path.resolve( __dirname, 'src/init.js' ),
     8        editor: path.resolve( __dirname, 'src/editor.scss' ),
     9        style: path.resolve( __dirname, 'src/style.scss' ),
     10        pagination: path.resolve( __dirname, 'src/pagination.js' ),
    1111    },
    1212};
  • jobbnorge-block/tags/2.2.3/wp-jobb-norge.php

    r3330462 r3373293  
    44 * Plugin URI:        https://wordpress.org/plugins/jobbnorge-block/
    55 * Description:       Retrieve and display job listings from Jobbnorge.no
    6  * Requires at least: 5.9
    7  * Requires PHP:      7.0
    8  * Version:           2.2.2
     6 * Requires at least: 6.5
     7 * Requires PHP:      8.2
     8 * Version:           2.2.3
    99 * Author:            PerS
    1010 * License:           GPL-2.0-or-later
    1111 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
    1212 * Text Domain:       wp-jobbnorge-block
    13  *
    1413 * @package           wp-jobbnorge-block
    1514 */
    1615
    1716namespace DSS\Jobbnorge;
     17
     18if ( ! defined( 'ABSPATH' ) ) {
     19    exit; // Safety.
     20}
     21
     22if ( ! defined( 'WP_JOBBNORGE_VERSION' ) ) {
     23    define( 'WP_JOBBNORGE_VERSION', '2.2.3' );
     24}
    1825
    1926if ( ! \class_exists( 'Jobbnorge_CacheHandler' ) ) {
     
    2128}
    2229
    23 
    24 add_action( 'init', __NAMESPACE__ . '\dss_jobbnorge_init' );
    25 
    26 /**
    27  * Registers the block using the metadata loaded from the `block.json` file.
    28  * Behind the scenes, it registers also all assets so they can be enqueued
    29  * through the block editor in the corresponding context.
    30  *
    31  * @see https://developer.wordpress.org/reference/functions/register_block_type/
    32  */
    33 function dss_jobbnorge_init() {
    34     // Add the 'dss_jobbnorge_enqueue_scripts' function to the 'admin_enqueue_scripts' action hook.
    35     // This function will be called when scripts and styles are enqueued for the admin panel.
    36     add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\dss_jobbnorge_enqueue_scripts' );
    37 
    38     // Add the 'dss_jobbnorge_enqueue_frontend_styles' function to the 'wp_enqueue_scripts' action hook.
    39     // This function will be called when scripts and styles are enqueued for the front end of the site.
    40     add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\dss_jobbnorge_enqueue_frontend_styles' );
    41 
    42     // Load the plugin's text domain for internationalization.
    43     // The second argument is set to false to not override the global locale.
    44     // The third argument is the path to the plugin's languages directory.
     30add_action( 'init', __NAMESPACE__ . '\\dss_jobbnorge_init' );
     31
     32/**
     33 * Init: register block + i18n + enqueue hooks.
     34 */
     35function dss_jobbnorge_init(): void {
    4536    load_plugin_textdomain( 'wp-jobbnorge-block', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
    4637
    47     // Register the block type.
    48     // The first argument is the path to the block's build directory.
    49     // The second argument is an array of options for the block, including a render callback function.
    50     register_block_type(
    51         __DIR__ . '/build',
    52         [
    53             'render_callback' => __NAMESPACE__ . '\render_block_dss_jobbnorge',
    54         ]
    55     );
    56 }
    57 
    58 /**
    59  * Enqueue block editor only JavaScript and CSS
    60  *
    61  * @param string $hook_suffix The current admin page.
    62  * @return void
     38    register_block_type( __DIR__ . '/build', [ 'render_callback' => __NAMESPACE__ . '\\render_block_dss_jobbnorge' ] );
     39
     40    add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\\dss_jobbnorge_enqueue_scripts' );
     41    add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\\dss_jobbnorge_enqueue_frontend_styles' );
     42    add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\\enqueue_pagination_script' );
     43}
     44
     45/**
     46 * Editor assets.
    6347 */
    6448function dss_jobbnorge_enqueue_scripts( string $hook_suffix ): void {
    65 
    66     // Check if the current page is a post editing page.
    67     if ( 'post.php' !== $hook_suffix && 'post-new.php' !== $hook_suffix && 'edit.php' !== $hook_suffix ) {
    68         // If not, exit early.
     49    if ( ! in_array( $hook_suffix, [ 'post.php', 'post-new.php', 'edit.php' ], true ) ) {
    6950        return;
    7051    }
    71 
    72     // Define the path to the dependencies file.
    7352    $deps_file = plugin_dir_path( __FILE__ ) . 'build/init.asset.php';
    74 
    75     // Initialize an array for JavaScript dependencies and a random version number.
    76     $jsdeps  = [];
    77     $version = wp_rand();
    78 
    79     // Check if the dependencies file exists.
     53    $jsdeps    = [];
     54    $version   = WP_JOBBNORGE_VERSION;
    8055    if ( file_exists( $deps_file ) ) {
    81         // If it does, require it and merge its dependencies with the existing ones.
    82         $file   = require $deps_file;
    83         $jsdeps = array_merge( $jsdeps, $file[ 'dependencies' ] );
    84         // Also, set the version to the one from the file.
     56        $file    = require $deps_file; // phpcs:ignore
     57        $jsdeps  = array_merge( $jsdeps, $file[ 'dependencies' ] );
    8558        $version = $file[ 'version' ];
    8659    }
    87 
    88     // Check if the current view is the admin dashboard.
    8960    if ( is_admin() ) {
    90         // If it is, register and enqueue a CSS file for the admin view.
    9161        wp_register_style( 'dss-jobbnorge-admin', plugin_dir_url( __FILE__ ) . 'build/init.css', [], $version );
    9262        wp_enqueue_style( 'dss-jobbnorge-admin' );
    9363    }
    94 
    95     // Set translations for the script.
    96     wp_set_script_translations(
    97         'dss-jobbnorge-editor-script', // Handle = block.json "name" (replace / with -) + "-editor-script".
    98         'wp-jobbnorge-block',
    99         plugin_dir_path( __FILE__ ) . 'languages/'
    100     );
    101 
    102     // Apply filter to modify the employers list.
     64    wp_set_script_translations( 'dss-jobbnorge-editor-script', 'wp-jobbnorge-block', plugin_dir_path( __FILE__ ) . 'languages/' );
    10365    $employers = apply_filters( 'jobbnorge_employers', false );
    104 
    105     // Proceed with localization if employers is not false.
    10666    if ( false !== $employers ) {
    107         // Ensure employers is an array.
    10867        if ( ! is_array( $employers ) ) {
    10968            $employers = [];
    11069        }
    111 
    112         // Localize the script to make employers data available.
    113         wp_localize_script(
    114             'dss-jobbnorge-editor-script',
    115             'wpJobbnorgeBlock',
    116             [
    117                 'employers' => $employers,
    118             ]
    119         );
    120     }
    121 }
    122 
    123 /**
    124  * Enqueue frontend styles for the block
    125  *
    126  * @return void
     70        wp_localize_script( 'dss-jobbnorge-editor-script', 'wpJobbnorgeBlock', [ 'employers' => $employers ] );
     71    }
     72}
     73
     74/**
     75 * Frontend styles.
    12776 */
    12877function dss_jobbnorge_enqueue_frontend_styles(): void {
    129     // Define the path to the dependencies file.
    13078    $deps_file = plugin_dir_path( __FILE__ ) . 'build/init.asset.php';
    131 
    132     // Initialize version number.
    133     $version = wp_rand();
    134 
    135     // Check if the dependencies file exists.
     79    $version   = WP_JOBBNORGE_VERSION;
    13680    if ( file_exists( $deps_file ) ) {
    137         // If it does, require it and get the version.
    138         $file    = require $deps_file;
     81        $file    = require $deps_file; // phpcs:ignore
    13982        $version = $file[ 'version' ];
    14083    }
    141 
    142     // Register and enqueue a CSS file for the public view.
    14384    wp_register_style( 'dss-jobbnorge', plugin_dir_url( __FILE__ ) . 'build/style-init.css', [], $version );
    14485    wp_enqueue_style( 'dss-jobbnorge' );
     
    14687
    14788/**
    148  * Renders the `jobbnorge` block on server.
    149  *
    150  * @param array $attributes The block attributes.
    151  *
    152  * @return string Returns the block content with received rss items.
    153  */
    154 function render_block_dss_jobbnorge( $attributes ) {
    155 
    156     // Set default values for attributes.
    157     $attributes = wp_parse_args(
    158         $attributes,
    159         [
    160             'employerID'       => '',
    161             'displayEmployer'  => false,
    162             'displayDate'      => true,
    163             'displayDeadline'  => false,
    164             'displayScope'     => false,
    165             'displayExcerpt'   => true,
    166             'excerptLength'    => 55,
    167             'blockLayout'      => 'list',
    168             'orderBy'          => 'Deadline',
    169             'columns'          => 3,
    170             'itemsToShow'      => 5,
    171             'enablePagination' => true,
    172             'jobsPerPage'      => 10,
    173         ]
    174     );
    175 
    176     // Convert employer IDs to an array and trim whitespace.
    177     $arr_ids = array_map( 'trim', explode( ',', $attributes[ 'employerID' ] ) );
    178 
    179     // Check if all IDs are numeric. If not, return an error message.
    180     if ( ! array_filter( $arr_ids, 'is_numeric' ) ) {
    181         return '<div class="components-placeholder"><div class="notice notice-error">' . __( 'Invalid ID', 'wp-jobbnorge-block' ) . '</div></div>';
    182     }
    183 
    184     // Get current page for pagination
    185     $current_page = isset( $_GET[ 'jobbnorge_page' ] ) ? max( 1, intval( $_GET[ 'jobbnorge_page' ] ) ) : 1;
    186 
    187     // Determine items per page based on pagination setting
    188     $items_per_page = $attributes[ 'enablePagination' ] ? $attributes[ 'jobsPerPage' ] : $attributes[ 'itemsToShow' ];
    189 
    190     // Construct the API URL for v3
    191     // NOTE: API v3 pagination doesn't work correctly with employer filtering
    192     // So we fetch all jobs for the employers and paginate in PHP
    193     $jobbnorge_api_url = 'https://publicapi.jobbnorge.no/v3/Jobs?abroad=false&orderBy=' . $attributes[ 'orderBy' ];
    194 
    195     // Add each employer ID to the API URL.
     89 * Render block frontend.
     90 */
     91function render_block_dss_jobbnorge( $attributes ): string {
     92    $attributes = wp_parse_args( $attributes, [
     93        'employerID'       => '',
     94        'displayEmployer'  => false,
     95        'displayDate'      => true, // currently unused but kept for backward compat.
     96        'displayDeadline'  => false,
     97        'displayScope'     => false,
     98        'displayExcerpt'   => true,
     99        'excerptLength'    => 55,
     100        'blockLayout'      => 'list',
     101        'orderBy'          => 'Deadline',
     102        'columns'          => 3,
     103        'itemsToShow'      => 5,
     104        'enablePagination' => true,
     105        'jobsPerPage'      => 10,
     106    ] );
     107
     108    // Sanitize employer IDs.
     109    $arr_ids_raw = array_filter( array_map( 'trim', explode( ',', (string) $attributes[ 'employerID' ] ) ) );
     110    $arr_ids     = [];
     111    foreach ( $arr_ids_raw as $maybe ) {
     112        if ( ctype_digit( $maybe ) ) {
     113            $arr_ids[] = (string) absint( $maybe );
     114        }
     115    }
     116    if ( empty( $arr_ids ) && ! empty( $attributes[ 'employerID' ] ) ) {
     117        return '<div class="components-placeholder"><div class="notice notice-error">' . esc_html__( 'Invalid ID', 'wp-jobbnorge-block' ) . '</div></div>';
     118    }
     119
     120    $current_page   = isset( $_GET[ 'jobbnorge_page' ] ) ? max( 1, absint( $_GET[ 'jobbnorge_page' ] ) ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only.
     121    $items_per_page = $attributes[ 'enablePagination' ] ? (int) $attributes[ 'jobsPerPage' ] : (int) $attributes[ 'itemsToShow' ];
     122
     123    // Build API URL.
     124    $jobbnorge_api_url = 'https://publicapi.jobbnorge.no/v3/Jobs?abroad=false&orderBy=' . rawurlencode( $attributes[ 'orderBy' ] );
    196125    foreach ( $arr_ids as $id ) {
    197         $jobbnorge_api_url .= '&employer=' . $id;
    198     }
    199 
    200     $cache_path = apply_filters( 'jobbnorge_cache_path', WP_CONTENT_DIR . '/cache/jobbnorge' );
    201     $cache      = new \Jobbnorge_CacheHandler( $cache_path );
    202 
    203     // Cache key based on employer IDs and settings, not pagination
     126        $jobbnorge_api_url .= '&employer=' . absint( $id );
     127    }
     128
     129    $cache_path    = apply_filters( 'jobbnorge_cache_path', WP_CONTENT_DIR . '/cache/jobbnorge' );
     130    $cache         = new \Jobbnorge_CacheHandler( $cache_path );
    204131    $cache_key     = md5( $jobbnorge_api_url );
    205     $expiration    = apply_filters( 'jobbnorge_cache_time', 30 * MINUTE_IN_SECONDS );
     132    $expiration    = (int) apply_filters( 'jobbnorge_cache_time', 30 * MINUTE_IN_SECONDS );
    206133    $response_data = $cache->get( $cache_key, $expiration );
    207 
    208134    if ( false === $response_data ) {
    209         $response = wp_remote_get( $jobbnorge_api_url );
    210 
    211         if ( is_wp_error( $response ) ) {
    212             return '<div class="components-placeholder"><div class="notice notice-error">' . __( 'Error connecting to Jobbnorge.no', 'wp-jobbnorge-block' ) . '</div></div>';
     135        // Perform remote request when no *fresh* cache. We'll attempt a stale cache fallback below if available.
     136        $response = wp_remote_get( $jobbnorge_api_url, [
     137            'timeout' => 10,
     138            'headers' => [
     139                'Accept'     => 'application/json',
     140                'User-Agent' => 'JobbnorgeBlock/' . WP_JOBBNORGE_VERSION . ' ' . home_url( '/' ),
     141            ],
     142        ] );
     143
     144        $http_status = ! is_wp_error( $response ) ? wp_remote_retrieve_response_code( $response ) : 0;
     145        $body        = ! is_wp_error( $response ) ? wp_remote_retrieve_body( $response ) : '';
     146        $tmp         = $body ? json_decode( $body, true ) : null;
     147        $json_ok     = is_array( $tmp ) && json_last_error() === JSON_ERROR_NONE;
     148
     149        if ( ! is_wp_error( $response ) && $http_status >= 200 && $http_status < 300 && $json_ok ) {
     150            // Happy path: cache and proceed.
     151            $response_data = $tmp;
     152            $cache->set( $cache_key, $response_data );
     153        } else {
     154            // Attempt stale cache fallback: read cache file ignoring expiration.
     155            $stale_data = null;
     156            $cache_file = apply_filters( 'jobbnorge_cache_path', WP_CONTENT_DIR . '/cache/jobbnorge' ) . '/' . $cache_key . '.php';
     157            if ( file_exists( $cache_file ) ) {
     158                // Suppress errors; include returns data array.
     159                $maybe_stale = @include $cache_file; // phpcs:ignore
     160                if ( is_array( $maybe_stale ) ) {
     161                    $stale_data = $maybe_stale;
     162                }
     163            }
     164
     165            $error_type = 'unknown';
     166            if ( is_wp_error( $response ) ) {
     167                $error_type = 'network';
     168            } elseif ( $http_status >= 500 ) {
     169                $error_type = 'server';
     170            } elseif ( $http_status === 404 ) {
     171                $error_type = 'not_found';
     172            } elseif ( $http_status >= 400 ) {
     173                $error_type = 'client';
     174            } elseif ( ! $json_ok ) {
     175                $error_type = 'invalid_json';
     176            }
     177
     178            /**
     179             * Fires when the Jobbnorge API request fails.
     180             *
     181             * @param string $error_type One of network|server|client|not_found|invalid_json|unknown.
     182             * @param int    $http_status HTTP status code (0 if network error).
     183             * @param string $api_url     Requested API URL.
     184             */
     185            do_action( 'jobbnorge_api_request_failed', $error_type, $http_status, $jobbnorge_api_url );
     186
     187            if ( $stale_data ) {
     188                // Provide gentle notice while still rendering stale data.
     189                $response_data = $stale_data;
     190                // Prepend a warning message that content may be outdated.
     191                $stale_notice = '<div class="notice notice-warning jobbnorge-stale" role="alert">' . esc_html__( 'Showing cached results due to a temporary connection issue.', 'wp-jobbnorge-block' ) . '</div>';
     192            } else {
     193                // User-facing message depending on error type.
     194                $human_msg = __( 'Error connecting to Jobbnorge.no', 'wp-jobbnorge-block' );
     195                if ( 'not_found' === $error_type ) {
     196                    $human_msg = __( 'Job listings not found (404).', 'wp-jobbnorge-block' );
     197                } elseif ( 'server' === $error_type ) {
     198                    $human_msg = __( 'Jobbnorge service temporarily unavailable.', 'wp-jobbnorge-block' );
     199                } elseif ( 'client' === $error_type ) {
     200                    $human_msg = __( 'Request error retrieving jobs.', 'wp-jobbnorge-block' );
     201                } elseif ( 'invalid_json' === $error_type ) {
     202                    $human_msg = __( 'Received invalid data from Jobbnorge.', 'wp-jobbnorge-block' );
     203                }
     204                return '<div class="components-placeholder"><div class="notice notice-error">' . esc_html( $human_msg ) . '</div></div>';
     205            }
    213206        }
    214 
    215         $body          = wp_remote_retrieve_body( $response );
    216         $response_data = json_decode( $body, true );
    217         $cache->set( $cache_key, $response_data );
    218     }
    219 
    220     // Debug: Log the API response structure
    221     if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    222         error_log( 'Jobbnorge API URL: ' . $jobbnorge_api_url );
    223         error_log( 'Jobbnorge API Response: ' . print_r( $response_data, true ) );
    224     }
    225     // Handle v3 API response structure
    226     $all_items  = isset( $response_data[ 'jobs' ] ) ? $response_data[ 'jobs' ] : $response_data;
     207    }
     208
     209    $all_items = isset( $response_data[ 'jobs' ] ) && is_array( $response_data[ 'jobs' ] ) ? $response_data[ 'jobs' ] : ( is_array( $response_data ) ? $response_data : [] );
     210    if ( ! is_array( $all_items ) ) {
     211        $all_items = [];
     212    }
    227213    $total_jobs = count( $all_items );
    228 
    229     // Debug: Log the items array
    230     if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    231         error_log( 'Items count: ' . count( $all_items ) );
    232         error_log( 'Total jobs: ' . $total_jobs );
    233         if ( ! empty( $all_items ) ) {
    234             error_log( 'First item: ' . print_r( $all_items[ 0 ], true ) );
    235         }
    236     }
    237 
    238     // Implement pagination in PHP since API pagination doesn't work with employer filtering
    239     if ( $attributes[ 'enablePagination' ] && $total_jobs > 0 ) {
    240         // Calculate pagination
    241         $start_index = ( $current_page - 1 ) * $attributes[ 'jobsPerPage' ];
    242         $items       = array_slice( $all_items, $start_index, $attributes[ 'jobsPerPage' ] );
     214    if ( 0 === $total_jobs ) {
     215        return '<div class="components-placeholder"><div class="notice notice-error">' . esc_html__( 'No jobs found', 'wp-jobbnorge-block' ) . '</div></div>';
     216    }
     217
     218    if ( $attributes[ 'enablePagination' ] ) {
     219        $start_index = ( $current_page - 1 ) * $items_per_page;
     220        $items       = array_slice( $all_items, $start_index, $items_per_page );
    243221    } else {
    244         // For non-paginated, limit to itemsToShow
    245         $items      = array_slice( $all_items, 0, $attributes[ 'itemsToShow' ] );
    246         $total_jobs = count( $items ); // Update total for non-paginated display
    247     }
    248 
    249     // If there are no items, return an error message.
    250     if ( ! $items ) {
    251         return '<div class="components-placeholder"><div class="notice notice-error">' . __( 'No jobs found', 'wp-jobbnorge-block' ) . '</div></div>';
    252     }
    253 
    254     // Initialize an empty string for the list items.
    255     $list_items = '';
    256 
    257     // Loop through each item.
    258     foreach ( $items as $item ) {
    259         // Sanitize and format the title.
    260         $title = esc_html( trim( wp_strip_all_tags( $item[ 'title' ] ) ) );
    261         $title = empty( $title ) ? __( '(no title)', 'wp-jobbnorge-block' ) : $title;
    262 
    263         // Sanitize the link.
    264         $link = esc_url( $item[ 'link' ] );
    265         // If there's a link, wrap the title in an anchor tag.
    266         $title = $link ? "<a href='{$link}'>{$title}</a>" : $title;
    267 
    268         // Wrap the title in a div.
    269         $title = "<div class='wp-block-dss-jobbnorge__item-title'>{$title}</div>";
    270 
    271         // Initialize an empty string for the deadline.
    272         $deadline = '';
    273         // If the displayDate attribute is true and the item has a deadline, format the deadline.
    274         if ( $attributes[ 'displayDate' ] && isset( $item[ 'deadline' ] ) ) {
    275             $deadline = format_deadline( $item[ 'deadline' ] );
    276         }
    277 
    278         // Format the excerpt.
    279         $excerpt = format_excerpt( $attributes, $item );
    280 
    281         // Format the employer and scope attributes.
    282         $employer = format_attribute( $attributes, $item, 'employer', 'displayEmployer', 'wp-block-dss-jobbnorge__item-employer', __( 'Employer', 'wp-jobbnorge-block' ) );
    283         $scope    = format_attribute( $attributes, $item, 'jobScope', 'displayScope', 'wp-block-dss-jobbnorge__item-scope', __( 'Scope', 'wp-jobbnorge-block' ) );
    284 
    285         // Initialize an empty string for the meta.
    286         $meta = '';
    287         // If there's an employer, deadline, or scope, wrap them in a div.
    288         if ( $employer || $deadline || $scope ) {
    289             $meta = '<div class="wp-block-dss-jobbnorge__item-meta">' . $employer . $deadline . $scope . '</div>';
    290         }
    291 
    292         // Add the item to the list items string.
    293         $list_items .= "<li class='wp-block-dss-jobbnorge__item'>{$title}{$meta}{$excerpt}</li>";
    294     }
    295 
    296     // Get the block wrapper attributes (without grid classes)
    297     $wrapper_classes = [];
    298     add_classname( $wrapper_classes, $attributes, 'displayEmployer', 'has-employer' );
    299     add_classname( $wrapper_classes, $attributes, 'displayDate', 'has-dates' );
    300     add_classname( $wrapper_classes, $attributes, 'displayDeadline', 'has-deadline' );
    301     add_classname( $wrapper_classes, $attributes, 'displayScope', 'has-scope' );
    302     add_classname( $wrapper_classes, $attributes, 'displayExcerpt', 'has-excerpts' );
    303 
    304     $wrapper_attributes = get_block_wrapper_attributes( [
     222        $items      = array_slice( $all_items, 0, $items_per_page );
     223        $total_jobs = count( $items );
     224    }
     225    if ( empty( $items ) ) {
     226        return '<div class="components-placeholder"><div class="notice notice-error">' . esc_html__( 'No jobs found', 'wp-jobbnorge-block' ) . '</div></div>';
     227    }
     228
     229    $wrapper_classes = [ 'wp-block-dss-jobbnorge__wrapper' ];
     230    if ( $attributes[ 'enablePagination' ] ) {
     231        $wrapper_classes[] = 'has-pagination';
     232    }
     233
     234    $wrapper_attributes = get_block_wrapper_attributes( [
    305235        'class'           => implode( ' ', $wrapper_classes ),
    306         'data-attributes' => esc_attr( json_encode( $attributes ) ),
     236        'data-attributes' => esc_attr( wp_json_encode( $attributes ) ),
     237        'aria-live'       => 'polite',
    307238    ] );
    308239
    309     // Generate the ul classes (including grid classes)
    310240    $ul_classes = [ 'wp-block-dss-jobbnorge' ];
    311241    if ( 'grid' === $attributes[ 'blockLayout' ] ) {
    312242        $ul_classes[] = 'is-grid';
    313         $ul_classes[] = 'columns-' . $attributes[ 'columns' ];
    314     }
    315 
    316     // Generate pagination controls if enabled
     243        $ul_classes[] = 'columns-' . (int) $attributes[ 'columns' ];
     244    }
     245
     246    $list_items = '';
     247    foreach ( $items as $item ) {
     248        // Deadline filtering: hide past deadlines if ordering by deadline.
     249        if ( isset( $item[ 'deadlineDate' ] ) && 'Deadline' === $attributes[ 'orderBy' ] ) {
     250            $deadline_ts = false;
     251            try {
     252                $deadline_ts = parse_date( $item[ 'deadlineDate' ] );
     253            } catch (\Throwable $t) {
     254                $deadline_ts = false;
     255            }
     256            if ( $deadline_ts && $deadline_ts < time() ) {
     257                continue; // Skip expired.
     258            }
     259        }
     260        $title     = isset( $item[ 'title' ] ) ? $item[ 'title' ] : '';
     261        $link      = isset( $item[ 'link' ] ) ? $item[ 'link' ] : '#';
     262        $deadline  = $attributes[ 'displayDeadline' ] && ! empty( $item[ 'deadlineDate' ] ) ? format_deadline( $item[ 'deadlineDate' ] ) : '';
     263        $excerpt   = format_excerpt( $attributes, $item );
     264        $employer  = format_attribute( $attributes, $item, 'employer', 'displayEmployer', 'wp-block-dss-jobbnorge__item-employer', __( 'Employer', 'wp-jobbnorge-block' ) );
     265        $scope     = format_attribute( $attributes, $item, 'jobScope', 'displayScope', 'wp-block-dss-jobbnorge__item-scope', __( 'Scope', 'wp-jobbnorge-block' ) );
     266        $meta_html = '';
     267        if ( $employer || $deadline || $scope ) {
     268            $meta_html = sprintf( '<div class="wp-block-dss-jobbnorge__item-meta">%s%s%s</div>', $employer, $deadline, $scope );
     269        }
     270        $list_items .= sprintf(
     271            '<li class="wp-block-dss-jobbnorge__item"><div class="wp-block-dss-jobbnorge__item-title"><a href="%s">%s</a></div>%s%s</li>',
     272            esc_url( $link ),
     273            esc_html( $title ),
     274            $meta_html,
     275            $excerpt
     276        );
     277    }
     278
     279    if ( '' === $list_items ) {
     280        return '<div class="components-placeholder"><div class="notice notice-error">' . esc_html__( 'No jobs found', 'wp-jobbnorge-block' ) . '</div></div>';
     281    }
     282
    317283    $pagination_html = '';
    318     if ( $attributes[ 'enablePagination' ] && count( $all_items ) > $attributes[ 'jobsPerPage' ] ) {
    319         $pagination_html = generate_pagination_controls( $current_page, count( $all_items ), $attributes[ 'jobsPerPage' ], $attributes );
    320     }
    321 
    322     // Return the final HTML string, wrapping the list items in an unordered list.
    323     return sprintf( '<div %s><ul class="%s">%s</ul>%s</div>', $wrapper_attributes, esc_attr( implode( ' ', $ul_classes ) ), $list_items, $pagination_html );
     284    if ( $attributes[ 'enablePagination' ] && $total_jobs > $items_per_page ) {
     285        $pagination_html = generate_pagination_controls( $current_page, $total_jobs, $items_per_page, $attributes );
     286    }
     287
     288    return sprintf( '<div %1$s><ul class="%2$s">%3$s</ul>%4$s</div>', $wrapper_attributes, esc_attr( implode( ' ', $ul_classes ) ), $list_items, $pagination_html );
    324289}
    325290
     
    331296 * @return string The formatted excerpt.
    332297 */
    333 function format_excerpt( $attributes, $item ) {
    334     // Initialize an empty string for the result.
    335     $result = '';
    336 
    337     // If the displayExcerpt attribute is true and the item has a summary, format the excerpt.
    338     if ( $attributes[ 'displayExcerpt' ] && isset( $item[ 'summary' ] ) ) {
    339         // Decode the HTML entities in the summary.
    340         $excerpt = html_entity_decode( $item[ 'summary' ], ENT_QUOTES, get_option( 'blog_charset' ) );
    341         // Trim the excerpt to the excerptLength and escape it for safe use in HTML output.
    342         $excerpt = esc_attr( wp_trim_words( $excerpt, $attributes[ 'excerptLength' ], '' ) );
    343 
    344         // Format the read more link.
    345         $read_more = sprintf( ' ... <a href="%s">%s</a>', esc_url( $item[ 'link' ] ), __( 'Read more', 'wp-jobbnorge-block' ) );
    346 
    347         // Add the excerpt and read more link to the result string, wrapped in a div.
    348         $result = sprintf( '<div class="wp-block-dss-jobbnorge__item-excerpt">%s%s</div>', esc_html( $excerpt ), $read_more );
    349     }
    350 
    351     // Return the result.
    352     return $result;
     298function format_excerpt( $attributes, $item ): string {
     299    if ( empty( $attributes[ 'displayExcerpt' ] ) || empty( $item[ 'summary' ] ) ) {
     300        return '';
     301    }
     302    $excerpt_raw = html_entity_decode( wp_strip_all_tags( (string) $item[ 'summary' ] ), ENT_QUOTES, get_option( 'blog_charset' ) );
     303    $excerpt     = wp_trim_words( $excerpt_raw, (int) $attributes[ 'excerptLength' ], '' );
     304    $read_more   = sprintf( ' <a href="%s">%s</a>', esc_url( $item[ 'link' ] ?? '#' ), esc_html__( 'Read more', 'wp-jobbnorge-block' ) );
     305    return sprintf( '<div class="wp-block-dss-jobbnorge__item-excerpt">%s%s</div>', esc_html( $excerpt ), $read_more );
    353306}
    354307
     
    390343 * @return string The formatted deadline date.
    391344 */
    392 function format_deadline( $deadline_date ) {
    393     // If there's no deadline date, return an empty string.
     345function format_deadline( $deadline_date ): string {
    394346    if ( ! $deadline_date ) {
    395347        return '';
    396348    }
    397 
    398349    try {
    399         // Try to parse the deadline date.
    400         $date = parse_date( $deadline_date );
    401         // Format the date according to the site's date format.
     350        $date     = parse_date( $deadline_date );
    402351        $str_date = date_i18n( get_option( 'date_format' ), $date );
    403     } catch (\Exception $e) {
    404         // If there's an exception, fallback to the original date.
     352    } catch (\Throwable $t) {
    405353        $str_date = $deadline_date;
    406354        $date     = false;
    407355    }
    408 
    409     // If there's a formatted date, return a time element with the date.
    410356    if ( $str_date ) {
    411357        return sprintf(
    412             '<time datetime="%1$s" class="wp-block-dss-jobbnorge__item-deadline">%2$s %3$s</time> ',
    413                 // If there's a parsed date, use it for the datetime attribute. Otherwise, leave it empty.
    414             ( $date ) ? esc_attr( wp_date( 'c', $date ) ) : '',
    415             // Translate the 'Deadline:' string.
    416             __( 'Deadline:', 'wp-jobbnorge-block' ),
    417             // Escape the formatted date for safe use in HTML output.
    418             esc_attr( $str_date )
     358            '<time datetime="%1$s" class="wp-block-dss-jobbnorge__item-deadline">%2$s %3$s</time>',
     359            $date ? esc_attr( wp_date( 'c', $date ) ) : '',
     360            esc_html__( 'Deadline:', 'wp-jobbnorge-block' ),
     361            esc_html( $str_date )
    419362        );
    420363    }
    421 
    422     // If there's no formatted date, return an empty string.
    423364    return '';
    424365}
     
    469410function parse_date_fallback( $deadline_date ) {
    470411    // Define an array of month names in Norwegian.
    471     $str_months = [ 
     412    $str_months = [
    472413        'januar',
    473414        'februar',
     
    485426
    486427    // Define an array of month numbers.
    487     $num_months = [ 
     428    $num_months = [
    488429        '01',
    489430        '02',
     
    617558 * Register AJAX endpoints for pagination.
    618559 */
    619 add_action( 'wp_ajax_jobbnorge_get_jobs', __NAMESPACE__ . '\handle_ajax_get_jobs' );
    620 add_action( 'wp_ajax_nopriv_jobbnorge_get_jobs', __NAMESPACE__ . '\handle_ajax_get_jobs' );
     560add_action( 'wp_ajax_jobbnorge_get_jobs', __NAMESPACE__ . '\\handle_ajax_get_jobs' );
     561add_action( 'wp_ajax_nopriv_jobbnorge_get_jobs', __NAMESPACE__ . '\\handle_ajax_get_jobs' );
    621562
    622563/**
    623564 * Handle AJAX request for paginated job listings.
    624565 */
    625 function handle_ajax_get_jobs() {
    626     // Verify nonce
    627     if ( ! wp_verify_nonce( $_POST[ 'nonce' ], 'jobbnorge_pagination_nonce' ) ) {
    628         wp_die( 'Security check failed' );
    629     }
    630 
    631     // Get and sanitize parameters
    632     $page       = isset( $_POST[ 'page' ] ) ? max( 1, intval( $_POST[ 'page' ] ) ) : 1;
    633     $attributes = isset( $_POST[ 'attributes' ] ) ? json_decode( stripslashes( $_POST[ 'attributes' ] ), true ) : [];
    634 
    635     // Validate attributes
    636     if ( empty( $attributes ) || ! is_array( $attributes ) ) {
    637         wp_send_json_error( 'Invalid attributes' );
    638     }
    639 
    640     // Set current page in GET superglobal for compatibility
    641     $_GET[ 'jobbnorge_page' ] = $page;
    642 
    643     // Generate the job listings HTML
    644     $html = render_block_dss_jobbnorge( $attributes );
    645 
    646     // Return JSON response
     566function handle_ajax_get_jobs(): void {
     567    if ( empty( $_POST[ 'nonce' ] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ 'nonce' ] ) ), 'jobbnorge_pagination_nonce' ) ) {
     568        wp_send_json_error( [ 'message' => __( 'Security check failed', 'wp-jobbnorge-block' ) ], 403 );
     569    }
     570    $page       = isset( $_POST[ 'page' ] ) ? max( 1, absint( wp_unslash( $_POST[ 'page' ] ) ) ) : 1;
     571    $raw_attr   = isset( $_POST[ 'attributes' ] ) ? wp_unslash( $_POST[ 'attributes' ] ) : '';
     572    $attributes = json_decode( $raw_attr, true );
     573    if ( json_last_error() !== JSON_ERROR_NONE || empty( $attributes ) || ! is_array( $attributes ) ) {
     574        wp_send_json_error( [ 'message' => __( 'Invalid attributes', 'wp-jobbnorge-block' ) ], 400 );
     575    }
     576    $_GET[ 'jobbnorge_page' ] = $page; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
     577    $html                     = render_block_dss_jobbnorge( $attributes );
    647578    wp_send_json_success( [ 'html' => $html ] );
    648579}
     
    685616        'jobbnorge-pagination',
    686617        'jobbnorgeAjax',
    687         [ 
     618        [
    688619            'ajaxUrl' => admin_url( 'admin-ajax.php' ),
    689620            'nonce'   => wp_create_nonce( 'jobbnorge_pagination_nonce' ),
     
    693624
    694625// Hook into wp_enqueue_scripts to add pagination script
    695 add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\enqueue_pagination_script' );
     626// Enqueue pagination script already hooked in init via dss_jobbnorge_init.
  • jobbnorge-block/trunk/CHANGELOG.md

    r3330462 r3373293  
    11# Changelog
     2
     3## 2.2.3
     4* Version bump: synchronize plugin header, constant, readme Stable tag and package.json.
     5* Enhancement: Add resilient API failure handling (HTTP status differentiation, stale cache fallback, logging hook `jobbnorge_api_request_failed`).
     6* Enhancement: Display stale cache notice when serving cached results after API failure.
    27
    38## 2.2.2
  • jobbnorge-block/trunk/README.md

    r3322139 r3373293  
    9191### 2) Modify the block settings.
    9292
     93-   In pagination mode (default), set the number of jobs to display per page (10 is default), else set the number of jobs to display.
     94-   Sort jobs bye deadline, closest first.
     95-   Does not show jobs that are past the deadline.
    9396-   Set the number of jobs to display.
    9497-   Set the no jobs message.
  • jobbnorge-block/trunk/build/block.json

    r3330462 r3373293  
    33  "apiVersion": 2,
    44  "name": "dss/jobbnorge",
    5   "version": "2.2.2",
     5  "version": "2.2.3",
    66  "title": "Jobbnorge",
    77  "category": "widgets",
  • jobbnorge-block/trunk/build/init.asset.php

    r3330462 r3373293  
    1 <?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-server-side-render'), 'version' => 'd713101f8e701317709b');
     1<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-server-side-render'), 'version' => '7ed0b4ca7d9ee76915e1');
  • jobbnorge-block/trunk/build/init.js

    r3330462 r3373293  
    1 !function(){"use strict";var e,o={938:function(e,o,t){var n=window.React,l=window.wp.primitives,r=(0,n.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,n.createElement)(l.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})),a=window.wp.blocks,i=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","apiVersion":2,"name":"dss/jobbnorge","version":"2.2.2","title":"Jobbnorge","category":"widgets","icon":"people","description":"Retrieve and display job listings from Jobbnorge.no","keywords":["jobbnorge","jobbnorge.no"],"supports":{"html":false},"attributes":{"columns":{"type":"number","default":3},"blockLayout":{"type":"string","default":"list"},"employerID":{"type":"string","default":"","role":"content"},"noJobsMessage":{"type":"string","default":""},"orderBy":{"type":"string","default":"Deadline"},"itemsToShow":{"type":"number","default":5},"displayEmployer":{"type":"boolean","default":false},"displayExcerpt":{"type":"boolean","default":true},"displayDeadline":{"type":"boolean","default":false},"displayScope":{"type":"boolean","default":false},"displayDate":{"type":"boolean","default":true},"excerptLength":{"type":"number","default":55},"enablePagination":{"type":"boolean","default":true},"jobsPerPage":{"type":"number","default":10}},"textdomain":"wp-jobbnorge-block","editorScript":"file:init.js","editorStyle":"file:editor.css","style":"file:style-init.css"}'),c=window.wp.blockEditor,b=window.wp.components,s=window.wp.element,p=(0,n.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,n.createElement)(l.Path,{d:"m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z"})),d=(0,n.createElement)(l.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},(0,n.createElement)(l.Path,{d:"M4 4v1.5h16V4H4zm8 8.5h8V11h-8v1.5zM4 20h16v-1.5H4V20zm4-8c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2z"})),m=(0,n.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,n.createElement)(l.Path,{d:"m3 5c0-1.10457.89543-2 2-2h13.5c1.1046 0 2 .89543 2 2v13.5c0 1.1046-.8954 2-2 2h-13.5c-1.10457 0-2-.8954-2-2zm2-.5h6v6.5h-6.5v-6c0-.27614.22386-.5.5-.5zm-.5 8v6c0 .2761.22386.5.5.5h6v-6.5zm8 0v6.5h6c.2761 0 .5-.2239.5-.5v-6zm0-8v6.5h6.5v-6c0-.27614-.2239-.5-.5-.5z",fillRule:"evenodd",clipRule:"evenodd"})),u=window.wp.i18n,g=window.wp.serverSideRender,w=t.n(g);const{name:h}=i;(e=>{const{metadata:o,settings:t,name:n}=e;(0,a.registerBlockType)({name:n,...o},t)})({name:h,metadata:i,settings:{icon:r,example:{attributes:{employerID:"123[, 456, 789]"}},edit:function({attributes:e,setAttributes:o}){const[t,l]=(0,s.useState)(!e.employerID),{blockLayout:a,columns:i,displayScope:g,displayDate:h,displayEmployer:y,displayExcerpt:v,employerID:f,itemsToShow:_,noJobsMessage:k,orderBy:j,enablePagination:E,jobsPerPage:C}=e;function x(t){return()=>{const n=e[t];o({[t]:!n})}}const S=(0,c.useBlockProps)();var B;if(t)return(0,n.createElement)("div",{...S},(0,n.createElement)(b.Placeholder,{icon:r,label:"Jobbnorge"},(0,n.createElement)("form",{onSubmit:function(e){e.preventDefault(),f&&(o({employerID:f}),l(!1))},className:"wp-block-dss-jobbnorge__placeholder-form"},window.wpJobbnorgeBlock&&window.wpJobbnorgeBlock.employers?(0,n.createElement)(b.SelectControl,{multiple:!0,value:f.split(","),onChange:e=>o({employerID:e.toString()}),options:(null!==(B=wpJobbnorgeBlock.employers)&&void 0!==B?B:[]).map((e=>{var o;return{label:e.label,value:e.value,disabled:null!==(o=e?.disabled)&&void 0!==o&&o}})),className:"wp-block-dss-jobbnorge__placeholder-input",help:(0,u.__)("Select employers to display. Ctrl-click (Windows) or Cmd-click (Mac) to select multiple employers. Shift-click to select a range of employers.","wp-jobbnorge-block"),__nextHasNoMarginBottom:!0}):(0,n.createElement)(b.TextControl,{placeholder:(0,u.__)("Employer ID [,id2, id3, ..]","wp-jobbnorge-block"),value:f,onChange:e=>o({employerID:e}),className:"wp-block-dss-jobbnorge__placeholder-input"}),(0,n.createElement)(b.Button,{variant:"primary",type:"submit"},(0,u.__)("Save","wp-jobbnorge-block")))));const D=[{icon:p,title:(0,u.__)("Edit Jobbnorge URL","wp-jobbnorge-block"),onClick:()=>l(!0)},{icon:d,title:(0,u.__)("List view","wp-jobbnorge-block"),onClick:()=>o({blockLayout:"list"}),isActive:"list"===a},{icon:m,title:(0,u.__)("Grid view","wp-jobbnorge-block"),onClick:()=>o({blockLayout:"grid"}),isActive:"grid"===a}];return(0,n.createElement)(n.Fragment,null,(0,n.createElement)(c.BlockControls,null,(0,n.createElement)(b.ToolbarGroup,{controls:D})),(0,n.createElement)(c.InspectorControls,null,(0,n.createElement)(b.PanelBody,{title:(0,u.__)("Settings","wp-jobbnorge-block")},(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Enable pagination","wp-jobbnorge-block"),help:(0,u.__)("When enabled, all jobs will be displayed with pagination controls. When disabled, only the specified number of jobs will be shown.","wp-jobbnorge-block"),checked:E,onChange:e=>o({enablePagination:e})}),!E&&(0,n.createElement)(b.RangeControl,{__nextHasNoMarginBottom:!0,label:(0,u.__)("Number of items","wp-jobbnorge-block"),value:_,onChange:e=>o({itemsToShow:e}),min:1,max:100,required:!0}),E&&(0,n.createElement)(b.RangeControl,{__nextHasNoMarginBottom:!0,label:(0,u.__)("Jobs per page","wp-jobbnorge-block"),value:C,onChange:e=>o({jobsPerPage:e}),min:1,max:50,required:!0}),f.includes(",")&&(0,n.createElement)(b.RadioControl,{label:(0,u.__)("Order by","wp-jobbnorge-block"),selected:j,options:[{label:(0,u.__)("Deadline","wp-jobbnorge-block"),value:"Deadline"},{label:(0,u.__)("Employer","wp-jobbnorge-block"),value:"Employer"}],onChange:e=>o({orderBy:e})}),(0,n.createElement)(b.TextareaControl,{label:(0,u.__)("No jobs found message","wp-jobbnorge-block"),help:(0,u.__)("Message to display if no jobs are found","wp-jobbnorge-block"),value:k||(0,u.__)("There are no jobs at this time.","wp-jobbnorge-block"),onChange:e=>o({noJobsMessage:e})})),(0,n.createElement)(b.PanelBody,{title:(0,u.__)("Item","wp-jobbnorge-block")},(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display employer","wp-jobbnorge-block"),checked:y,onChange:x("displayEmployer")}),(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display excerpt","wp-jobbnorge-block"),checked:v,onChange:x("displayExcerpt")}),(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display deadline","wp-jobbnorge-block"),checked:h,onChange:x("displayDate")}),(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display scope","wp-jobbnorge-block"),checked:g,onChange:x("displayScope")})),"grid"===a&&(0,n.createElement)(b.PanelBody,{title:(0,u.__)("Grid view","wp-jobbnorge-block")},(0,n.createElement)(b.RangeControl,{__nextHasNoMarginBottom:!0,label:(0,u.__)("Columns","wp-jobbnorge-block"),value:i,onChange:e=>o({columns:e}),min:2,max:6,required:!0}))),(0,n.createElement)("div",{...S},(0,n.createElement)(b.Disabled,null,(0,n.createElement)(w(),{block:"dss/jobbnorge",attributes:e,httpMethod:"POST"}))))}}})}},t={};function n(e){var l=t[e];if(void 0!==l)return l.exports;var r=t[e]={exports:{}};return o[e](r,r.exports,n),r.exports}n.m=o,e=[],n.O=function(o,t,l,r){if(!t){var a=1/0;for(s=0;s<e.length;s++){t=e[s][0],l=e[s][1],r=e[s][2];for(var i=!0,c=0;c<t.length;c++)(!1&r||a>=r)&&Object.keys(n.O).every((function(e){return n.O[e](t[c])}))?t.splice(c--,1):(i=!1,r<a&&(a=r));if(i){e.splice(s--,1);var b=l();void 0!==b&&(o=b)}}return o}r=r||0;for(var s=e.length;s>0&&e[s-1][2]>r;s--)e[s]=e[s-1];e[s]=[t,l,r]},n.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(o,{a:o}),o},n.d=function(e,o){for(var t in o)n.o(o,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:o[t]})},n.o=function(e,o){return Object.prototype.hasOwnProperty.call(e,o)},function(){var e={410:0,308:0};n.O.j=function(o){return 0===e[o]};var o=function(o,t){var l,r,a=t[0],i=t[1],c=t[2],b=0;if(a.some((function(o){return 0!==e[o]}))){for(l in i)n.o(i,l)&&(n.m[l]=i[l]);if(c)var s=c(n)}for(o&&o(t);b<a.length;b++)r=a[b],n.o(e,r)&&e[r]&&e[r][0](),e[r]=0;return n.O(s)},t=self.webpackChunkjobbnorge_block=self.webpackChunkjobbnorge_block||[];t.forEach(o.bind(null,0)),t.push=o.bind(null,t.push.bind(t))}();var l=n.O(void 0,[308],(function(){return n(938)}));l=n.O(l)}();
     1!function(){"use strict";var e,o={938:function(e,o,t){var n=window.React,l=window.wp.primitives,r=(0,n.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,n.createElement)(l.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})),a=window.wp.blocks,i=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","apiVersion":2,"name":"dss/jobbnorge","version":"2.2.3","title":"Jobbnorge","category":"widgets","icon":"people","description":"Retrieve and display job listings from Jobbnorge.no","keywords":["jobbnorge","jobbnorge.no"],"supports":{"html":false},"attributes":{"columns":{"type":"number","default":3},"blockLayout":{"type":"string","default":"list"},"employerID":{"type":"string","default":"","role":"content"},"noJobsMessage":{"type":"string","default":""},"orderBy":{"type":"string","default":"Deadline"},"itemsToShow":{"type":"number","default":5},"displayEmployer":{"type":"boolean","default":false},"displayExcerpt":{"type":"boolean","default":true},"displayDeadline":{"type":"boolean","default":false},"displayScope":{"type":"boolean","default":false},"displayDate":{"type":"boolean","default":true},"excerptLength":{"type":"number","default":55},"enablePagination":{"type":"boolean","default":true},"jobsPerPage":{"type":"number","default":10}},"textdomain":"wp-jobbnorge-block","editorScript":"file:init.js","editorStyle":"file:editor.css","style":"file:style-init.css"}'),c=window.wp.blockEditor,b=window.wp.components,s=window.wp.element,p=(0,n.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,n.createElement)(l.Path,{d:"m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z"})),d=(0,n.createElement)(l.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},(0,n.createElement)(l.Path,{d:"M4 4v1.5h16V4H4zm8 8.5h8V11h-8v1.5zM4 20h16v-1.5H4V20zm4-8c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2z"})),m=(0,n.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,n.createElement)(l.Path,{d:"m3 5c0-1.10457.89543-2 2-2h13.5c1.1046 0 2 .89543 2 2v13.5c0 1.1046-.8954 2-2 2h-13.5c-1.10457 0-2-.8954-2-2zm2-.5h6v6.5h-6.5v-6c0-.27614.22386-.5.5-.5zm-.5 8v6c0 .2761.22386.5.5.5h6v-6.5zm8 0v6.5h6c.2761 0 .5-.2239.5-.5v-6zm0-8v6.5h6.5v-6c0-.27614-.2239-.5-.5-.5z",fillRule:"evenodd",clipRule:"evenodd"})),u=window.wp.i18n,g=window.wp.serverSideRender,w=t.n(g);const{name:h}=i;(e=>{const{metadata:o,settings:t,name:n}=e;(0,a.registerBlockType)({name:n,...o},t)})({name:h,metadata:i,settings:{icon:r,example:{attributes:{employerID:"123[, 456, 789]"}},edit:function({attributes:e,setAttributes:o}){const[t,l]=(0,s.useState)(!e.employerID),{blockLayout:a,columns:i,displayScope:g,displayDate:h,displayEmployer:y,displayExcerpt:v,employerID:f,itemsToShow:_,noJobsMessage:k,orderBy:j,enablePagination:E,jobsPerPage:C}=e;function x(t){return()=>{const n=e[t];o({[t]:!n})}}const S=(0,c.useBlockProps)();var B;if(t)return(0,n.createElement)("div",{...S},(0,n.createElement)(b.Placeholder,{icon:r,label:"Jobbnorge"},(0,n.createElement)("form",{onSubmit:function(e){e.preventDefault(),f&&(o({employerID:f}),l(!1))},className:"wp-block-dss-jobbnorge__placeholder-form"},window.wpJobbnorgeBlock&&window.wpJobbnorgeBlock.employers?(0,n.createElement)(b.SelectControl,{multiple:!0,value:f.split(","),onChange:e=>o({employerID:e.toString()}),options:(null!==(B=window.wpJobbnorgeBlock?.employers)&&void 0!==B?B:[]).map((e=>{var o;return{label:e.label,value:e.value,disabled:null!==(o=e?.disabled)&&void 0!==o&&o}})),className:"wp-block-dss-jobbnorge__placeholder-input",help:(0,u.__)("Select employers to display. Ctrl-click (Windows) or Cmd-click (Mac) to select multiple employers. Shift-click to select a range of employers.","wp-jobbnorge-block"),__nextHasNoMarginBottom:!0}):(0,n.createElement)(b.TextControl,{placeholder:(0,u.__)("Employer ID [,id2, id3, ..]","wp-jobbnorge-block"),value:f,onChange:e=>o({employerID:e}),className:"wp-block-dss-jobbnorge__placeholder-input"}),(0,n.createElement)(b.Button,{variant:"primary",type:"submit"},(0,u.__)("Save","wp-jobbnorge-block")))));const D=[{icon:p,title:(0,u.__)("Edit Jobbnorge URL","wp-jobbnorge-block"),onClick:()=>l(!0)},{icon:d,title:(0,u.__)("List view","wp-jobbnorge-block"),onClick:()=>o({blockLayout:"list"}),isActive:"list"===a},{icon:m,title:(0,u.__)("Grid view","wp-jobbnorge-block"),onClick:()=>o({blockLayout:"grid"}),isActive:"grid"===a}];return(0,n.createElement)(n.Fragment,null,(0,n.createElement)(c.BlockControls,null,(0,n.createElement)(b.ToolbarGroup,{controls:D})),(0,n.createElement)(c.InspectorControls,null,(0,n.createElement)(b.PanelBody,{title:(0,u.__)("Settings","wp-jobbnorge-block")},(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Enable pagination","wp-jobbnorge-block"),help:(0,u.__)("When enabled, all jobs will be displayed with pagination controls. When disabled, only the specified number of jobs will be shown.","wp-jobbnorge-block"),checked:E,onChange:e=>o({enablePagination:e})}),!E&&(0,n.createElement)(b.RangeControl,{__nextHasNoMarginBottom:!0,label:(0,u.__)("Number of items","wp-jobbnorge-block"),value:_,onChange:e=>o({itemsToShow:e}),min:1,max:100,required:!0}),E&&(0,n.createElement)(b.RangeControl,{__nextHasNoMarginBottom:!0,label:(0,u.__)("Jobs per page","wp-jobbnorge-block"),value:C,onChange:e=>o({jobsPerPage:e}),min:1,max:50,required:!0}),f.includes(",")&&(0,n.createElement)(b.RadioControl,{label:(0,u.__)("Order by","wp-jobbnorge-block"),selected:j,options:[{label:(0,u.__)("Deadline","wp-jobbnorge-block"),value:"Deadline"},{label:(0,u.__)("Employer","wp-jobbnorge-block"),value:"Employer"}],onChange:e=>o({orderBy:e})}),(0,n.createElement)(b.TextareaControl,{label:(0,u.__)("No jobs found message","wp-jobbnorge-block"),help:(0,u.__)("Message to display if no jobs are found","wp-jobbnorge-block"),value:k||(0,u.__)("There are no jobs at this time.","wp-jobbnorge-block"),onChange:e=>o({noJobsMessage:e})})),(0,n.createElement)(b.PanelBody,{title:(0,u.__)("Item","wp-jobbnorge-block")},(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display employer","wp-jobbnorge-block"),checked:y,onChange:x("displayEmployer")}),(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display excerpt","wp-jobbnorge-block"),checked:v,onChange:x("displayExcerpt")}),(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display deadline","wp-jobbnorge-block"),checked:h,onChange:x("displayDate")}),(0,n.createElement)(b.ToggleControl,{label:(0,u.__)("Display scope","wp-jobbnorge-block"),checked:g,onChange:x("displayScope")})),"grid"===a&&(0,n.createElement)(b.PanelBody,{title:(0,u.__)("Grid view","wp-jobbnorge-block")},(0,n.createElement)(b.RangeControl,{__nextHasNoMarginBottom:!0,label:(0,u.__)("Columns","wp-jobbnorge-block"),value:i,onChange:e=>o({columns:e}),min:2,max:6,required:!0}))),(0,n.createElement)("div",{...S},(0,n.createElement)(b.Disabled,null,(0,n.createElement)(w(),{block:"dss/jobbnorge",attributes:e,httpMethod:"POST"}))))}}})}},t={};function n(e){var l=t[e];if(void 0!==l)return l.exports;var r=t[e]={exports:{}};return o[e](r,r.exports,n),r.exports}n.m=o,e=[],n.O=function(o,t,l,r){if(!t){var a=1/0;for(s=0;s<e.length;s++){t=e[s][0],l=e[s][1],r=e[s][2];for(var i=!0,c=0;c<t.length;c++)(!1&r||a>=r)&&Object.keys(n.O).every((function(e){return n.O[e](t[c])}))?t.splice(c--,1):(i=!1,r<a&&(a=r));if(i){e.splice(s--,1);var b=l();void 0!==b&&(o=b)}}return o}r=r||0;for(var s=e.length;s>0&&e[s-1][2]>r;s--)e[s]=e[s-1];e[s]=[t,l,r]},n.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(o,{a:o}),o},n.d=function(e,o){for(var t in o)n.o(o,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:o[t]})},n.o=function(e,o){return Object.prototype.hasOwnProperty.call(e,o)},function(){var e={410:0,308:0};n.O.j=function(o){return 0===e[o]};var o=function(o,t){var l,r,a=t[0],i=t[1],c=t[2],b=0;if(a.some((function(o){return 0!==e[o]}))){for(l in i)n.o(i,l)&&(n.m[l]=i[l]);if(c)var s=c(n)}for(o&&o(t);b<a.length;b++)r=a[b],n.o(e,r)&&e[r]&&e[r][0](),e[r]=0;return n.O(s)},t=self.webpackChunkjobbnorge_block=self.webpackChunkjobbnorge_block||[];t.forEach(o.bind(null,0)),t.push=o.bind(null,t.push.bind(t))}();var l=n.O(void 0,[308],(function(){return n(938)}));l=n.O(l)}();
  • jobbnorge-block/trunk/build/pagination.asset.php

    r3322310 r3373293  
    1 <?php return array('dependencies' => array(), 'version' => '09b126faf8a7661ac618');
     1<?php return array('dependencies' => array(), 'version' => 'c74730f64eb48d06bafd');
  • jobbnorge-block/trunk/build/pagination.js

    r3322310 r3373293  
    1 !function(){"use strict";function e(){document.querySelectorAll(".wp-block-dss-jobbnorge__pagination").forEach((function(e){const o=e.closest(".wp-block-dss-jobbnorge");if(!o)return;const n=o.getAttribute("data-attributes");if(!n)return;let r;try{r=JSON.parse(n)}catch(e){return void console.error("Failed to parse block attributes:",e)}const a=e.querySelector(".wp-block-dss-jobbnorge__pagination-prev"),i=e.querySelector(".wp-block-dss-jobbnorge__pagination-next");a&&!a.disabled&&a.addEventListener("click",(function(e){e.preventDefault(),t(parseInt(this.getAttribute("data-page")),r,o)})),i&&!i.disabled&&i.addEventListener("click",(function(e){e.preventDefault(),t(parseInt(this.getAttribute("data-page")),r,o)}))}))}function t(t,n,r){r.classList.add("wp-block-dss-jobbnorge__loading"),r.querySelectorAll(".wp-block-dss-jobbnorge__pagination button").forEach((function(e){e.disabled=!0}));const a=new FormData;a.append("action","jobbnorge_get_jobs"),a.append("page",t),a.append("attributes",JSON.stringify(n)),a.append("nonce",jobbnorgeAjax.nonce),fetch(jobbnorgeAjax.ajaxUrl,{method:"POST",body:a}).then((function(e){return e.json()})).then((function(n){if(n.success){r.innerHTML=n.data.html,e();const o=r.getBoundingClientRect(),a=2*parseFloat(getComputedStyle(document.documentElement).fontSize),i=window.pageYOffset+o.top-a;window.scrollTo({top:i,behavior:"smooth"}),function(e){if(history.pushState){const t=new URL(window.location);e>1?t.searchParams.set("jobbnorge_page",e):t.searchParams.delete("jobbnorge_page"),history.pushState({page:e},"",t)}}(t)}else console.error("AJAX request failed:",n.data),o(r,"Failed to load page. Please try again.")})).catch((function(e){console.error("AJAX request error:",e),o(r,"An error occurred while loading the page.")})).finally((function(){r.classList.remove("wp-block-dss-jobbnorge__loading")}))}function o(e,t){const o=document.createElement("div");o.className="wp-block-dss-jobbnorge__error notice notice-error",o.innerHTML="<p>"+t+"</p>",e.insertBefore(o,e.firstChild),setTimeout((function(){o.parentNode&&o.parentNode.removeChild(o)}),5e3)}document.addEventListener("DOMContentLoaded",(function(){e()})),window.addEventListener("popstate",(function(e){e.state&&e.state.page&&window.location.reload()}))}();
     1!function(){"use strict";function e(){document.querySelectorAll(".wp-block-dss-jobbnorge__pagination").forEach((function(e){const o=e.closest(".wp-block-dss-jobbnorge");if(!o)return;const n=o.getAttribute("data-attributes");if(!n)return;let r;try{r=JSON.parse(n)}catch(e){return void console.error("Failed to parse block attributes:",e)}const a=e.querySelector(".wp-block-dss-jobbnorge__pagination-prev"),i=e.querySelector(".wp-block-dss-jobbnorge__pagination-next");a&&!a.disabled&&a.addEventListener("click",(function(e){e.preventDefault(),t(parseInt(this.getAttribute("data-page")),r,o)})),i&&!i.disabled&&i.addEventListener("click",(function(e){e.preventDefault(),t(parseInt(this.getAttribute("data-page")),r,o)}))}))}function t(t,n,r){r.classList.add("wp-block-dss-jobbnorge__loading"),r.querySelectorAll(".wp-block-dss-jobbnorge__pagination button").forEach((function(e){e.disabled=!0}));const a=new FormData;a.append("action","jobbnorge_get_jobs"),a.append("page",t),a.append("attributes",JSON.stringify(n)),a.append("nonce",window.jobbnorgeAjax?.nonce||""),fetch(window.jobbnorgeAjax?.ajaxUrl||window.ajaxurl||"",{method:"POST",body:a}).then((function(e){return e.json()})).then((function(n){if(n.success){r.innerHTML=n.data.html,e();const o=r.getBoundingClientRect(),a=2*parseFloat(window.getComputedStyle(document.documentElement).fontSize),i=window.pageYOffset+o.top-a;window.scrollTo({top:i,behavior:"smooth"}),function(e){if(window.history.pushState){const t=new URL(window.location);e>1?t.searchParams.set("jobbnorge_page",e):t.searchParams.delete("jobbnorge_page"),window.history.pushState({page:e},"",t)}}(t)}else console.error("AJAX request failed:",n.data),o(r,"Failed to load page. Please try again.")})).catch((function(e){console.error("AJAX request error:",e),o(r,"An error occurred while loading the page.")})).finally((function(){r.classList.remove("wp-block-dss-jobbnorge__loading")}))}function o(e,t){const o=document.createElement("div");o.className="wp-block-dss-jobbnorge__error notice notice-error",o.innerHTML="<p>"+t+"</p>",e.insertBefore(o,e.firstChild),setTimeout((function(){o.parentNode&&o.parentNode.removeChild(o)}),5e3)}document.addEventListener("DOMContentLoaded",(function(){e()})),window.addEventListener("popstate",(function(e){e.state&&e.state.page&&window.location.reload()}))}();
  • jobbnorge-block/trunk/build/style-init.css

    r3322139 r3373293  
    1 ul.wp-block-dss-jobbnorge{list-style:none;padding:0}ul.wp-block-dss-jobbnorge.wp-block-dss-jobbnorge{box-sizing:border-box}ul.wp-block-dss-jobbnorge.alignleft{margin-right:2em}ul.wp-block-dss-jobbnorge.alignright{margin-left:2em}ul.wp-block-dss-jobbnorge li{margin:0 0 1em}ul.wp-block-dss-jobbnorge.is-grid{display:flex;flex-wrap:wrap;list-style:none;padding:0}ul.wp-block-dss-jobbnorge.is-grid li{margin:0 1em 1em 0;width:100%}@media(min-width:600px){ul.wp-block-dss-jobbnorge.columns-2 li{width:calc(50% - 1em)}ul.wp-block-dss-jobbnorge.columns-3 li{width:calc(33.33333% - 1em)}ul.wp-block-dss-jobbnorge.columns-4 li{width:calc(25% - 1em)}ul.wp-block-dss-jobbnorge.columns-5 li{width:calc(20% - 1em)}ul.wp-block-dss-jobbnorge.columns-6 li{width:calc(16.66667% - 1em)}}.wp-block-dss-jobbnorge__item-title{font-size:1.125em;font-weight:600;margin:0 0 .25em}.wp-block-dss-jobbnorge__item-meta{margin:0 0 .25em;padding:0}.wp-block-dss-jobbnorge__item-deadline,.wp-block-dss-jobbnorge__item-employer,.wp-block-dss-jobbnorge__item-scope{display:block;font-size:.8125em;font-weight:600}.wp-block-dss-jobbnorge__pagination{border-top:1px solid #e0e0e0;display:flex;flex-direction:column;gap:1rem;margin-top:2rem;padding:1rem 0}@media(min-width:600px){.wp-block-dss-jobbnorge__pagination{align-items:center;flex-direction:row;justify-content:space-between}}.wp-block-dss-jobbnorge__pagination-info{color:#666;font-size:.875rem;margin:0}.wp-block-dss-jobbnorge__pagination-controls{align-items:center;display:flex;gap:.5rem}.wp-block-dss-jobbnorge__pagination-controls button{background:#fff;border:1px solid #ddd;border-radius:4px;cursor:pointer;font-size:.875rem;padding:.5rem 1rem;transition:all .2s ease}.wp-block-dss-jobbnorge__pagination-controls button:hover:not(:disabled){background:#f5f5f5;border-color:#999}.wp-block-dss-jobbnorge__pagination-controls button:disabled{cursor:not-allowed;opacity:.5}.wp-block-dss-jobbnorge__pagination-controls .wp-block-dss-jobbnorge__pagination-info{color:#333;font-size:.875rem;margin:0 .5rem}.wp-block-dss-jobbnorge__loading{opacity:.6;pointer-events:none}.wp-block-dss-jobbnorge__loading:after{animation:spin 1s linear infinite;border:2px solid #ccc;border-radius:50%;border-top-color:#333;content:"";height:20px;left:50%;margin:-10px 0 0 -10px;position:absolute;top:50%;width:20px}.wp-block-dss-jobbnorge__error{background:#ffebe8;border:1px solid #d63638;border-radius:4px;color:#d63638;margin:1rem 0;padding:.75rem}.wp-block-dss-jobbnorge__error p{margin:0}@keyframes spin{to{transform:rotate(1turn)}}
     1ul.wp-block-dss-jobbnorge{list-style:none;padding:0}ul.wp-block-dss-jobbnorge.wp-block-dss-jobbnorge{box-sizing:border-box}ul.wp-block-dss-jobbnorge.alignleft{margin-right:2em}ul.wp-block-dss-jobbnorge.alignright{margin-left:2em}ul.wp-block-dss-jobbnorge li{margin:0 0 1em}ul.wp-block-dss-jobbnorge.is-grid{display:flex;flex-wrap:wrap;list-style:none;padding:0}ul.wp-block-dss-jobbnorge.is-grid li{margin:0 1em 1em 0;width:100%}@media(min-width:600px){ul.wp-block-dss-jobbnorge.columns-2 li{width:calc(50% - 1em)}ul.wp-block-dss-jobbnorge.columns-3 li{width:calc(33.33333% - 1em)}ul.wp-block-dss-jobbnorge.columns-4 li{width:calc(25% - 1em)}ul.wp-block-dss-jobbnorge.columns-5 li{width:calc(20% - 1em)}ul.wp-block-dss-jobbnorge.columns-6 li{width:calc(16.66667% - 1em)}}.wp-block-dss-jobbnorge__item-title{font-size:1.125em;font-weight:600;margin:0 0 .25em}.wp-block-dss-jobbnorge__item-meta{margin:0 0 .25em;padding:0}.wp-block-dss-jobbnorge__item-deadline,.wp-block-dss-jobbnorge__item-employer,.wp-block-dss-jobbnorge__item-scope{display:block;font-size:.8125em;font-weight:600}.wp-block-dss-jobbnorge__pagination{border-top:1px solid #e0e0e0;display:flex;flex-direction:column;gap:1rem;margin-top:2rem;padding:1rem 0}@media(min-width:600px){.wp-block-dss-jobbnorge__pagination{align-items:center;flex-direction:row;justify-content:space-between}}.wp-block-dss-jobbnorge__pagination-info{color:#666;font-size:.875rem;margin:0}.wp-block-dss-jobbnorge__pagination-controls{align-items:center;display:flex;gap:.5rem}.wp-block-dss-jobbnorge__pagination-controls button{background:#fff;border:1px solid #ddd;border-radius:4px;cursor:pointer;font-size:.875rem;padding:.5rem 1rem;transition:all .2s ease}.wp-block-dss-jobbnorge__pagination-controls button:disabled{cursor:not-allowed;opacity:.5}.wp-block-dss-jobbnorge__pagination-controls button:hover:not(:disabled){background:#f5f5f5;border-color:#999}.wp-block-dss-jobbnorge__pagination-controls .wp-block-dss-jobbnorge__pagination-info{color:#333;font-size:.875rem;margin:0 .5rem}.wp-block-dss-jobbnorge__loading{opacity:.6;pointer-events:none}.wp-block-dss-jobbnorge__loading:after{animation:spin 1s linear infinite;border:2px solid #ccc;border-radius:50%;border-top-color:#333;content:"";height:20px;left:50%;margin:-10px 0 0 -10px;position:absolute;top:50%;width:20px}.wp-block-dss-jobbnorge__error{background:#ffebe8;border:1px solid #d63638;border-radius:4px;color:#d63638;margin:1rem 0;padding:.75rem}.wp-block-dss-jobbnorge__error p{margin:0}@keyframes spin{to{transform:rotate(1turn)}}
  • jobbnorge-block/trunk/package-lock.json

    r3330462 r3373293  
    11{
    22    "name": "jobbnorge-block",
    3     "version": "2.2.2",
     3    "version": "2.2.3",
    44    "lockfileVersion": 3,
    55    "requires": true,
     
    77        "": {
    88            "name": "jobbnorge-block",
    9             "version": "2.2.2",
     9            "version": "2.2.3",
    1010            "license": "GPL-2.0-or-later",
    1111            "dependencies": {
  • jobbnorge-block/trunk/package.json

    r3330462 r3373293  
    11{
    22    "name": "jobbnorge-block",
    3     "version": "2.2.2",
     3    "version": "2.2.3",
    44    "description": "Jobbnorge Block for WordPress Gutenberg",
    55    "author": "Per Søderlind <[email protected]>",
  • jobbnorge-block/trunk/readme.txt

    r3330470 r3373293  
    55Requires at least: 6.5
    66Requires PHP:      8.2
    7 Stable tag:        2.2.2
     7Stable tag:        2.2.3
    88License:           GPL-2.0-or-later
    99License URI:       https://www.gnu.org/licenses/gpl-2.0.html
     
    105105== Changelog ==
    106106
     107= 2.2.3 =
     108* Version bump: synchronize plugin header, constant, readme Stable tag and package.json.
     109* Enhancement: Add resilient API failure handling (HTTP status differentiation, stale cache fallback, logging hook `jobbnorge_api_request_failed`).
     110* Enhancement: Display stale cache notice when serving cached results after API failure.
     111
    107112= 2.2.2 =
    108113* Update block.json to include default value and role for employerID
  • jobbnorge-block/trunk/src/block.json

    r3330462 r3373293  
    33    "apiVersion": 2,
    44    "name": "dss/jobbnorge",
    5     "version": "2.2.2",
     5    "version": "2.2.3",
    66    "title": "Jobbnorge",
    77    "category": "widgets",
  • jobbnorge-block/trunk/src/edit.js

    r3322139 r3373293  
    22 * WordPress dependencies
    33 */
    4 import { BlockControls, InspectorControls, useBlockProps } from '@wordpress/block-editor';
     4import {
     5    BlockControls,
     6    InspectorControls,
     7    useBlockProps,
     8} from '@wordpress/block-editor';
    59import {
    610    Button,
     
    2630const DEFAULT_MAX_ITEMS = 100;
    2731
     32/* eslint-disable jsdoc/check-line-alignment */
    2833/**
    29  * Description placeholder
    30  * @date 17/11/2023 - 16:21:26
     34 * Jobbnorge block editor component.
    3135 *
    32  * @export
    33  * @param {{ attributes: any; setAttributes: any; }} param0
    34  * @param {*} param0.attributes
    35  * @param {*} param0.setAttributes
    36  * @returns {*}
     36 * @param {Object}      props              Component props.
     37 * @param {Object}      props.attributes   Block attributes.
     38 * @param {Function}    props.setAttributes Setter for block attributes.
     39 * @return {JSX.Element} Editor element.
    3740 */
    38 export default function JobbnorgeEdit({ attributes, setAttributes }) {
     41/* eslint-enable jsdoc/check-line-alignment */
     42export default function JobbnorgeEdit( { attributes, setAttributes } ) {
    3943    // Initialize the isEditing state variable. If the employerID attribute is not set, isEditing will be true.
    40     const [isEditing, setIsEditing] = useState(!attributes.employerID);
     44    const [ isEditing, setIsEditing ] = useState( ! attributes.employerID );
    4145
    4246    // Destructure the attributes object to get the individual attributes.
     
    5862    // Define a function to toggle an attribute.
    5963    // This function returns another function that, when called, will toggle the value of the attribute specified by propName.
    60     function toggleAttribute(propName) {
     64    function toggleAttribute( propName ) {
    6165        return () => {
    62             const value = attributes[propName];
    63 
    64             setAttributes({ [propName]: !value });
     66            const value = attributes[ propName ];
     67
     68            setAttributes( { [ propName ]: ! value } );
    6569        };
    6670    }
     
    6872    // Define a function to handle the form submission.
    6973    // This function will set the employerID attribute and set isEditing to false.
    70     function onSubmitURL(event) {
     74    function onSubmitURL( event ) {
    7175        event.preventDefault();
    7276
    73         if (employerID) {
    74             setAttributes({ employerID: employerID });
    75             setIsEditing(false);
     77        if ( employerID ) {
     78            setAttributes( { employerID } );
     79            setIsEditing( false );
    7680        }
    7781    }
     
    7983    const blockProps = useBlockProps();
    8084
    81     if (isEditing) {
     85    if ( isEditing ) {
    8286        return (
    83             <div {...blockProps}>
    84                 <Placeholder icon={people} label="Jobbnorge">
    85                     <form onSubmit={onSubmitURL} className="wp-block-dss-jobbnorge__placeholder-form">
    86                         {window.wpJobbnorgeBlock && window.wpJobbnorgeBlock.employers ? (
     87            <div { ...blockProps }>
     88                <Placeholder icon={ people } label="Jobbnorge">
     89                    <form
     90                        onSubmit={ onSubmitURL }
     91                        className="wp-block-dss-jobbnorge__placeholder-form"
     92                    >
     93                        { window.wpJobbnorgeBlock &&
     94                        window.wpJobbnorgeBlock.employers ? (
    8795                            <SelectControl
    8896                                multiple
    89                                 value={employerID.split(',')}
    90                                 onChange={(value) => setAttributes({ employerID: value.toString() })}
    91                                 options={(wpJobbnorgeBlock.employers ?? []).map((o) => ({
     97                                value={ employerID.split( ',' ) }
     98                                onChange={ ( value ) =>
     99                                    setAttributes( {
     100                                        employerID: value.toString(),
     101                                    } )
     102                                }
     103                                options={ (
     104                                    window.wpJobbnorgeBlock?.employers ?? []
     105                                ).map( ( o ) => ( {
    92106                                    label: o.label,
    93107                                    value: o.value,
    94108                                    disabled: o?.disabled ?? false,
    95                                 }))}
     109                                } ) ) }
    96110                                className="wp-block-dss-jobbnorge__placeholder-input"
    97                                 help={__(
     111                                help={ __(
    98112                                    'Select employers to display. Ctrl-click (Windows) or Cmd-click (Mac) to select multiple employers. Shift-click to select a range of employers.',
    99113                                    'wp-jobbnorge-block'
    100                                 )}
     114                                ) }
    101115                                __nextHasNoMarginBottom
    102116                            />
    103117                        ) : (
    104118                            <TextControl
    105                                 placeholder={__('Employer ID [,id2, id3, ..]', 'wp-jobbnorge-block')}
    106                                 value={employerID}
    107                                 onChange={(value) => setAttributes({ employerID: value })}
     119                                placeholder={ __(
     120                                    'Employer ID [,id2, id3, ..]',
     121                                    'wp-jobbnorge-block'
     122                                ) }
     123                                value={ employerID }
     124                                onChange={ ( value ) =>
     125                                    setAttributes( { employerID: value } )
     126                                }
    108127                                className="wp-block-dss-jobbnorge__placeholder-input"
    109128                            />
    110                         )}
     129                        ) }
    111130                        <Button variant="primary" type="submit">
    112                             {__('Save', 'wp-jobbnorge-block')}
     131                            { __( 'Save', 'wp-jobbnorge-block' ) }
    113132                        </Button>
    114133                    </form>
     
    121140        {
    122141            icon: edit,
    123             title: __('Edit Jobbnorge URL', 'wp-jobbnorge-block'),
    124             onClick: () => setIsEditing(true),
     142            title: __( 'Edit Jobbnorge URL', 'wp-jobbnorge-block' ),
     143            onClick: () => setIsEditing( true ),
    125144        },
    126145        {
    127146            icon: list,
    128             title: __('List view', 'wp-jobbnorge-block'),
    129             onClick: () => setAttributes({ blockLayout: 'list' }),
     147            title: __( 'List view', 'wp-jobbnorge-block' ),
     148            onClick: () => setAttributes( { blockLayout: 'list' } ),
    130149            isActive: blockLayout === 'list',
    131150        },
    132151        {
    133152            icon: grid,
    134             title: __('Grid view', 'wp-jobbnorge-block'),
    135             onClick: () => setAttributes({ blockLayout: 'grid' }),
     153            title: __( 'Grid view', 'wp-jobbnorge-block' ),
     154            onClick: () => setAttributes( { blockLayout: 'grid' } ),
    136155            isActive: blockLayout === 'grid',
    137156        },
     
    141160        <>
    142161            <BlockControls>
    143                 <ToolbarGroup controls={toolbarControls} />
     162                <ToolbarGroup controls={ toolbarControls } />
    144163            </BlockControls>
    145164            <InspectorControls>
    146                 <PanelBody title={__('Settings', 'wp-jobbnorge-block')}>
    147                     <ToggleControl
    148                         label={__('Enable pagination', 'wp-jobbnorge-block')}
    149                         help={__('When enabled, all jobs will be displayed with pagination controls. When disabled, only the specified number of jobs will be shown.', 'wp-jobbnorge-block')}
    150                         checked={enablePagination}
    151                         onChange={(value) => setAttributes({ enablePagination: value })}
    152                     />
    153                     {!enablePagination && (
     165                <PanelBody title={ __( 'Settings', 'wp-jobbnorge-block' ) }>
     166                    <ToggleControl
     167                        label={ __(
     168                            'Enable pagination',
     169                            'wp-jobbnorge-block'
     170                        ) }
     171                        help={ __(
     172                            'When enabled, all jobs will be displayed with pagination controls. When disabled, only the specified number of jobs will be shown.',
     173                            'wp-jobbnorge-block'
     174                        ) }
     175                        checked={ enablePagination }
     176                        onChange={ ( value ) =>
     177                            setAttributes( { enablePagination: value } )
     178                        }
     179                    />
     180                    { ! enablePagination && (
    154181                        <RangeControl
    155182                            __nextHasNoMarginBottom
    156                             label={__('Number of items', 'wp-jobbnorge-block')}
    157                             value={itemsToShow}
    158                             onChange={(value) => setAttributes({ itemsToShow: value })}
    159                             min={DEFAULT_MIN_ITEMS}
    160                             max={DEFAULT_MAX_ITEMS}
     183                            label={ __(
     184                                'Number of items',
     185                                'wp-jobbnorge-block'
     186                            ) }
     187                            value={ itemsToShow }
     188                            onChange={ ( value ) =>
     189                                setAttributes( { itemsToShow: value } )
     190                            }
     191                            min={ DEFAULT_MIN_ITEMS }
     192                            max={ DEFAULT_MAX_ITEMS }
    161193                            required
    162194                        />
    163                     )}
    164                     {enablePagination && (
     195                    ) }
     196                    { enablePagination && (
    165197                        <RangeControl
    166198                            __nextHasNoMarginBottom
    167                             label={__('Jobs per page', 'wp-jobbnorge-block')}
    168                             value={jobsPerPage}
    169                             onChange={(value) => setAttributes({ jobsPerPage: value })}
    170                             min={1}
    171                             max={50}
     199                            label={ __(
     200                                'Jobs per page',
     201                                'wp-jobbnorge-block'
     202                            ) }
     203                            value={ jobsPerPage }
     204                            onChange={ ( value ) =>
     205                                setAttributes( { jobsPerPage: value } )
     206                            }
     207                            min={ 1 }
     208                            max={ 50 }
    172209                            required
    173210                        />
    174                     )}
    175                     {employerID.includes(',') && (
     211                    ) }
     212                    { employerID.includes( ',' ) && (
    176213                        <RadioControl
    177                             label={__('Order by', 'wp-jobbnorge-block')}
    178                             selected={orderBy}
    179                             options={[
    180                                 { label: __('Deadline', 'wp-jobbnorge-block'), value: 'Deadline' },
    181                                 { label: __('Employer', 'wp-jobbnorge-block'), value: 'Employer' },
    182                             ]}
    183                             onChange={(value) => setAttributes({ orderBy: value })}
     214                            label={ __( 'Order by', 'wp-jobbnorge-block' ) }
     215                            selected={ orderBy }
     216                            options={ [
     217                                {
     218                                    label: __(
     219                                        'Deadline',
     220                                        'wp-jobbnorge-block'
     221                                    ),
     222                                    value: 'Deadline',
     223                                },
     224                                {
     225                                    label: __(
     226                                        'Employer',
     227                                        'wp-jobbnorge-block'
     228                                    ),
     229                                    value: 'Employer',
     230                                },
     231                            ] }
     232                            onChange={ ( value ) =>
     233                                setAttributes( { orderBy: value } )
     234                            }
    184235                        />
    185                     )}
     236                    ) }
    186237                    <TextareaControl
    187                         label={__('No jobs found message', 'wp-jobbnorge-block')}
    188                         help={__('Message to display if no jobs are found', 'wp-jobbnorge-block')}
    189                         value={noJobsMessage || __('There are no jobs at this time.', 'wp-jobbnorge-block')}
    190                         onChange={(value) => setAttributes({ noJobsMessage: value })}
     238                        label={ __(
     239                            'No jobs found message',
     240                            'wp-jobbnorge-block'
     241                        ) }
     242                        help={ __(
     243                            'Message to display if no jobs are found',
     244                            'wp-jobbnorge-block'
     245                        ) }
     246                        value={
     247                            noJobsMessage ||
     248                            __(
     249                                'There are no jobs at this time.',
     250                                'wp-jobbnorge-block'
     251                            )
     252                        }
     253                        onChange={ ( value ) =>
     254                            setAttributes( { noJobsMessage: value } )
     255                        }
    191256                    />
    192257                </PanelBody>
    193                 <PanelBody title={__('Item', 'wp-jobbnorge-block')}>
    194                     <ToggleControl
    195                         label={__('Display employer', 'wp-jobbnorge-block')}
    196                         checked={displayEmployer}
    197                         onChange={toggleAttribute('displayEmployer')}
    198                     />
    199                     <ToggleControl
    200                         label={__('Display excerpt', 'wp-jobbnorge-block')}
    201                         checked={displayExcerpt}
    202                         onChange={toggleAttribute('displayExcerpt')}
    203                     />
    204                     <ToggleControl
    205                         label={__('Display deadline', 'wp-jobbnorge-block')}
    206                         checked={displayDate}
    207                         onChange={toggleAttribute('displayDate')}
    208                     />
    209                     <ToggleControl
    210                         label={__('Display scope', 'wp-jobbnorge-block')}
    211                         checked={displayScope}
    212                         onChange={toggleAttribute('displayScope')}
     258                <PanelBody title={ __( 'Item', 'wp-jobbnorge-block' ) }>
     259                    <ToggleControl
     260                        label={ __( 'Display employer', 'wp-jobbnorge-block' ) }
     261                        checked={ displayEmployer }
     262                        onChange={ toggleAttribute( 'displayEmployer' ) }
     263                    />
     264                    <ToggleControl
     265                        label={ __( 'Display excerpt', 'wp-jobbnorge-block' ) }
     266                        checked={ displayExcerpt }
     267                        onChange={ toggleAttribute( 'displayExcerpt' ) }
     268                    />
     269                    <ToggleControl
     270                        label={ __( 'Display deadline', 'wp-jobbnorge-block' ) }
     271                        checked={ displayDate }
     272                        onChange={ toggleAttribute( 'displayDate' ) }
     273                    />
     274                    <ToggleControl
     275                        label={ __( 'Display scope', 'wp-jobbnorge-block' ) }
     276                        checked={ displayScope }
     277                        onChange={ toggleAttribute( 'displayScope' ) }
    213278                    />
    214279                </PanelBody>
    215                 {blockLayout === 'grid' && (
    216                     <PanelBody title={__('Grid view', 'wp-jobbnorge-block')}>
     280                { blockLayout === 'grid' && (
     281                    <PanelBody
     282                        title={ __( 'Grid view', 'wp-jobbnorge-block' ) }
     283                    >
    217284                        <RangeControl
    218285                            __nextHasNoMarginBottom
    219                             label={__('Columns', 'wp-jobbnorge-block')}
    220                             value={columns}
    221                             onChange={(value) => setAttributes({ columns: value })}
    222                             min={2}
    223                             max={6}
     286                            label={ __( 'Columns', 'wp-jobbnorge-block' ) }
     287                            value={ columns }
     288                            onChange={ ( value ) =>
     289                                setAttributes( { columns: value } )
     290                            }
     291                            min={ 2 }
     292                            max={ 6 }
    224293                            required
    225294                        />
    226295                    </PanelBody>
    227                 )}
     296                ) }
    228297            </InspectorControls>
    229             <div {...blockProps}>
     298            <div { ...blockProps }>
    230299                <Disabled>
    231                     <ServerSideRender block="dss/jobbnorge" attributes={attributes} httpMethod="POST" />
     300                    <ServerSideRender
     301                        block="dss/jobbnorge"
     302                        attributes={ attributes }
     303                        httpMethod="POST"
     304                    />
    232305                </Disabled>
    233306            </div>
  • jobbnorge-block/trunk/src/editor.scss

    r3322139 r3373293  
    55
    66@mixin break-medium() {
     7
    78    @media (min-width: #{ ($break-medium) }) {
    89        @content;
     
    1112
    1213@mixin break-small() {
     14
    1315    @media (min-width: #{ ($break-small) }) {
    1416        @content;
     
    1618}
    1719
    18 .wp-block-dss-jobbnorge li a>div {
     20.wp-block-dss-jobbnorge li a > div {
    1921    display: inline;
    2022}
     
    2931
    3032    @include break-medium() {
     33
    3134        >* {
    3235            margin-bottom: 0;
     
    7578
    7679    @include break-small {
     80
    7781        @for $i from 2 through 6 {
    7882            &.columns-#{ $i } li {
  • jobbnorge-block/trunk/src/index.js

    r2997962 r3373293  
    22 * WordPress dependencies
    33 */
    4 import { people as icon } from "@wordpress/icons";
    5 import { registerBlockType } from "@wordpress/blocks";
     4import { people as icon } from '@wordpress/icons';
     5import { registerBlockType } from '@wordpress/blocks';
    66/**
    77 * Internal dependencies
    88 */
    9 import "./style.scss";
    10 import metadata from "./block.json";
    11 import edit from "./edit";
     9import './style.scss';
     10import metadata from './block.json';
     11import edit from './edit';
    1212
    1313const { name } = metadata;
     
    1919    example: {
    2020        attributes: {
    21             employerID: "123[, 456, 789]",
     21            employerID: '123[, 456, 789]',
    2222        },
    2323    },
     
    2525};
    2626
    27 const initBlock = (block) => {
    28     // if (!block) {
    29     //  return;
    30     // }
    31     const { metadata, settings, name } = block;
    32     return registerBlockType({ name, ...metadata }, settings);
     27const initBlock = ( block ) => {
     28    // Accept an object with keys name, metadata, settings without shadowing outer scope.
     29    const { metadata: md, settings: st, name: blockName } = block;
     30    return registerBlockType( { name: blockName, ...md }, st );
    3331};
    3432
    35 export const init = () => initBlock({ name, metadata, settings });
     33export const init = () => initBlock( { name, metadata, settings } );
  • jobbnorge-block/trunk/src/pagination.js

    r3322310 r3373293  
    11/**
    22 * Jobbnorge Block Pagination JavaScript
    3  * 
     3 *
    44 * Handles AJAX pagination for job listings.
    55 */
    66
    7 (function() {
    8     'use strict';
    9    
    10     // Initialize pagination when DOM is ready
    11     document.addEventListener('DOMContentLoaded', function() {
    12         initializePagination();
    13     });
    14    
    15     /**
    16      * Initialize pagination functionality
    17      */
    18     function initializePagination() {
    19         const paginationContainers = document.querySelectorAll('.wp-block-dss-jobbnorge__pagination');
    20        
    21         paginationContainers.forEach(function(container) {
    22             const blockContainer = container.closest('.wp-block-dss-jobbnorge');
    23            
    24             if (!blockContainer) return;
    25            
    26             // Get block attributes from data attribute
    27             const attributesData = blockContainer.getAttribute('data-attributes');
    28             if (!attributesData) return;
    29            
    30             let attributes;
    31             try {
    32                 attributes = JSON.parse(attributesData);
    33             } catch (e) {
    34                 console.error('Failed to parse block attributes:', e);
    35                 return;
    36             }
    37            
    38             // Add event listeners to pagination buttons
    39             const prevButton = container.querySelector('.wp-block-dss-jobbnorge__pagination-prev');
    40             const nextButton = container.querySelector('.wp-block-dss-jobbnorge__pagination-next');
    41            
    42             if (prevButton && !prevButton.disabled) {
    43                 prevButton.addEventListener('click', function(e) {
    44                     e.preventDefault();
    45                     const page = parseInt(this.getAttribute('data-page'));
    46                     loadPage(page, attributes, blockContainer);
    47                 });
    48             }
    49            
    50             if (nextButton && !nextButton.disabled) {
    51                 nextButton.addEventListener('click', function(e) {
    52                     e.preventDefault();
    53                     const page = parseInt(this.getAttribute('data-page'));
    54                     loadPage(page, attributes, blockContainer);
    55                 });
    56             }
    57         });
    58     }
    59    
    60     /**
    61      * Load a specific page via AJAX
    62      *
    63      * @param {number} page - Page number to load
    64      * @param {Object} attributes - Block attributes
    65      * @param {Element} container - Block container element
    66      */
    67     function loadPage(page, attributes, container) {
    68         // Show loading state
    69         container.classList.add('wp-block-dss-jobbnorge__loading');
    70        
    71         // Disable pagination buttons during loading
    72         const buttons = container.querySelectorAll('.wp-block-dss-jobbnorge__pagination button');
    73         buttons.forEach(function(button) {
    74             button.disabled = true;
    75         });
    76        
    77         // Prepare AJAX data
    78         const formData = new FormData();
    79         formData.append('action', 'jobbnorge_get_jobs');
    80         formData.append('page', page);
    81         formData.append('attributes', JSON.stringify(attributes));
    82         formData.append('nonce', jobbnorgeAjax.nonce);
    83        
    84         // Make AJAX request
    85         fetch(jobbnorgeAjax.ajaxUrl, {
    86             method: 'POST',
    87             body: formData
    88         })
    89         .then(function(response) {
    90             return response.json();
    91         })
    92         .then(function(data) {
    93             if (data.success) {
    94                 // Update the container with new content
    95                 container.innerHTML = data.data.html;
    96                
    97                 // Reinitialize pagination for the new content
    98                 initializePagination();
    99                
    100                 // Scroll to 2em above the top of the block
    101                 const containerRect = container.getBoundingClientRect();
    102                 const twoEm = parseFloat(getComputedStyle(document.documentElement).fontSize) * 2;
    103                 const targetPosition = window.pageYOffset + containerRect.top - twoEm;
    104                
    105                 window.scrollTo({
    106                     top: targetPosition,
    107                     behavior: 'smooth'
    108                 });
    109                
    110                 // Update URL with page parameter (optional)
    111                 updateURL(page);
    112                
    113             } else {
    114                 console.error('AJAX request failed:', data.data);
    115                 showError(container, 'Failed to load page. Please try again.');
    116             }
    117         })
    118         .catch(function(error) {
    119             console.error('AJAX request error:', error);
    120             showError(container, 'An error occurred while loading the page.');
    121         })
    122         .finally(function() {
    123             // Remove loading state
    124             container.classList.remove('wp-block-dss-jobbnorge__loading');
    125         });
    126     }
    127    
    128     /**
    129      * Update URL with page parameter
    130      *
    131      * @param {number} page - Current page number
    132      */
    133     function updateURL(page) {
    134         if (history.pushState) {
    135             const url = new URL(window.location);
    136             if (page > 1) {
    137                 url.searchParams.set('jobbnorge_page', page);
    138             } else {
    139                 url.searchParams.delete('jobbnorge_page');
    140             }
    141             history.pushState({ page: page }, '', url);
    142         }
    143     }
    144    
    145     /**
    146      * Show error message
    147      *
    148      * @param {Element} container - Block container
    149      * @param {string} message - Error message
    150      */
    151     function showError(container, message) {
    152         const errorDiv = document.createElement('div');
    153         errorDiv.className = 'wp-block-dss-jobbnorge__error notice notice-error';
    154         errorDiv.innerHTML = '<p>' + message + '</p>';
    155        
    156         // Insert error message at the top of the container
    157         container.insertBefore(errorDiv, container.firstChild);
    158        
    159         // Remove error message after 5 seconds
    160         setTimeout(function() {
    161             if (errorDiv.parentNode) {
    162                 errorDiv.parentNode.removeChild(errorDiv);
    163             }
    164         }, 5000);
    165     }
    166    
    167     // Handle browser back/forward buttons
    168     window.addEventListener('popstate', function(event) {
    169         if (event.state && event.state.page) {
    170             // Reload the page with the correct page number
    171             window.location.reload();
    172         }
    173     });
    174    
    175 })();
     7/* eslint-env browser */
     8( function () {
     9    'use strict';
     10
     11    // Initialize pagination when DOM is ready
     12    document.addEventListener( 'DOMContentLoaded', function () {
     13        initializePagination();
     14    } );
     15
     16    /**
     17     * Initialize pagination functionality
     18     */
     19    function initializePagination() {
     20        const paginationContainers = document.querySelectorAll(
     21            '.wp-block-dss-jobbnorge__pagination'
     22        );
     23
     24        paginationContainers.forEach( function ( container ) {
     25            const blockContainer = container.closest(
     26                '.wp-block-dss-jobbnorge'
     27            );
     28
     29            if ( ! blockContainer ) return;
     30
     31            // Get block attributes from data attribute
     32            const attributesData =
     33                blockContainer.getAttribute( 'data-attributes' );
     34            if ( ! attributesData ) return;
     35
     36            let attributes;
     37            try {
     38                attributes = JSON.parse( attributesData );
     39            } catch ( e ) {
     40                // eslint-disable-next-line no-console
     41                console.error( 'Failed to parse block attributes:', e );
     42                return;
     43            }
     44
     45            // Add event listeners to pagination buttons
     46            const prevButton = container.querySelector(
     47                '.wp-block-dss-jobbnorge__pagination-prev'
     48            );
     49            const nextButton = container.querySelector(
     50                '.wp-block-dss-jobbnorge__pagination-next'
     51            );
     52
     53            if ( prevButton && ! prevButton.disabled ) {
     54                prevButton.addEventListener( 'click', function ( e ) {
     55                    e.preventDefault();
     56                    const page = parseInt( this.getAttribute( 'data-page' ) );
     57                    loadPage( page, attributes, blockContainer );
     58                } );
     59            }
     60
     61            if ( nextButton && ! nextButton.disabled ) {
     62                nextButton.addEventListener( 'click', function ( e ) {
     63                    e.preventDefault();
     64                    const page = parseInt( this.getAttribute( 'data-page' ) );
     65                    loadPage( page, attributes, blockContainer );
     66                } );
     67            }
     68        } );
     69    }
     70
     71    /**
     72     * Load a specific page via AJAX
     73     *
     74     * @param {number}  page       - Page number to load
     75     * @param {Object}  attributes - Block attributes
     76     * @param {Element} container  - Block container element
     77     */
     78    function loadPage( page, attributes, container ) {
     79        // Show loading state
     80        container.classList.add( 'wp-block-dss-jobbnorge__loading' );
     81
     82        // Disable pagination buttons during loading
     83        const buttons = container.querySelectorAll(
     84            '.wp-block-dss-jobbnorge__pagination button'
     85        );
     86        buttons.forEach( function ( button ) {
     87            button.disabled = true;
     88        } );
     89
     90        // Prepare AJAX data
     91        const formData = new FormData();
     92        formData.append( 'action', 'jobbnorge_get_jobs' );
     93        formData.append( 'page', page );
     94        formData.append( 'attributes', JSON.stringify( attributes ) );
     95        formData.append( 'nonce', window.jobbnorgeAjax?.nonce || '' );
     96
     97        // Make AJAX request
     98        fetch( window.jobbnorgeAjax?.ajaxUrl || window.ajaxurl || '', {
     99            method: 'POST',
     100            body: formData,
     101        } )
     102            .then( function ( response ) {
     103                return response.json();
     104            } )
     105            .then( function ( data ) {
     106                if ( data.success ) {
     107                    // Update the container with new content
     108                    container.innerHTML = data.data.html;
     109
     110                    // Reinitialize pagination for the new content
     111                    initializePagination();
     112
     113                    // Scroll to 2em above the top of the block
     114                    const containerRect = container.getBoundingClientRect();
     115                    const twoEm =
     116                        parseFloat(
     117                            window.getComputedStyle( document.documentElement )
     118                                .fontSize
     119                            ) * 2;
     120                    const targetPosition =
     121                        window.pageYOffset + containerRect.top - twoEm;
     122
     123                    window.scrollTo( {
     124                        top: targetPosition,
     125                        behavior: 'smooth',
     126                    } );
     127
     128                    // Update URL with page parameter (optional)
     129                    updateURL( page );
     130                } else {
     131                    // eslint-disable-next-line no-console
     132                    console.error( 'AJAX request failed:', data.data );
     133                    showError(
     134                        container,
     135                        'Failed to load page. Please try again.'
     136                    );
     137                }
     138            } )
     139            .catch( function ( error ) {
     140                // eslint-disable-next-line no-console
     141                console.error( 'AJAX request error:', error );
     142                showError( container, 'An error occurred while loading the page.' );
     143            } )
     144            .finally( function () {
     145                // Remove loading state
     146                container.classList.remove( 'wp-block-dss-jobbnorge__loading' );
     147            } );
     148    }
     149
     150    /**
     151     * Update URL with page parameter
     152     *
     153     * @param {number} page - Current page number
     154     */
     155    function updateURL( page ) {
     156        if ( window.history.pushState ) {
     157            const url = new URL( window.location );
     158            if ( page > 1 ) {
     159                url.searchParams.set( 'jobbnorge_page', page );
     160            } else {
     161                url.searchParams.delete( 'jobbnorge_page' );
     162            }
     163            window.history.pushState( { page }, '', url );
     164        }
     165    }
     166
     167    /**
     168     * Show error message
     169     *
     170     * @param {Element} container - Block container
     171     * @param {string}  message   - Error message
     172     */
     173    function showError( container, message ) {
     174        const errorDiv = document.createElement( 'div' );
     175        errorDiv.className =
     176            'wp-block-dss-jobbnorge__error notice notice-error';
     177        errorDiv.innerHTML = '<p>' + message + '</p>';
     178
     179        // Insert error message at the top of the container
     180        container.insertBefore( errorDiv, container.firstChild );
     181
     182        // Remove error message after 5 seconds
     183        setTimeout( function () {
     184            if ( errorDiv.parentNode ) {
     185                errorDiv.parentNode.removeChild( errorDiv );
     186            }
     187        }, 5000 );
     188    }
     189
     190    // Handle browser back/forward buttons
     191    window.addEventListener( 'popstate', function ( event ) {
     192        if ( event.state && event.state.page ) {
     193            // Reload the page with the correct page number
     194            window.location.reload();
     195        }
     196    } );
     197} )();
  • jobbnorge-block/trunk/src/style.scss

    r3322139 r3373293  
    22
    33@mixin break-small() {
     4
    45    @media (min-width: #{ ($break-small) }) {
    56        @content;
     
    1314    padding: 0;
    1415
    15     // This needs extra specificity due to the reset mixin on the parent: https://github.com/WordPress/gutenberg/blob/a250e9e5fe00dd5195624f96a3d924e7078951c3/packages/edit-post/src/style.scss#L54
     16    // This needs extra specificity due to the reset mixin on the parent:
     17    // See: https://github.com/WordPress/gutenberg/blob/a250e9e5fe00dd5195624f96a3d924e7078951c3/packages/edit-post/src/style.scss#L54
    1618    &.wp-block-dss-jobbnorge {
    1719        box-sizing: border-box;
     
    1921
    2022    &.alignleft {
     23
    2124        /*rtl:ignore*/
    2225        margin-right: 2em;
     
    2427
    2528    &.alignright {
     29
    2630        /*rtl:ignore*/
    2731        margin-left: 2em;
     
    4549
    4650    @include break-small {
     51
    4752        @for $i from 2 through 6 {
    4853            &.columns-#{ $i } li {
     
    7580// Pagination styles
    7681.wp-block-dss-jobbnorge {
     82
    7783    &__pagination {
    7884        display: flex;
     
    104110            padding: 0.5rem 1rem;
    105111            border: 1px solid #ddd;
    106             background: white;
     112            background: #fff; // replaced named color per stylelint
    107113            cursor: pointer;
    108114            border-radius: 4px;
     
    110116            transition: all 0.2s ease;
    111117
     118            &:disabled {
     119                opacity: 0.5;
     120                cursor: not-allowed;
     121            }
     122
    112123            &:hover:not(:disabled) {
    113124                background: #f5f5f5;
    114125                border-color: #999;
    115             }
    116 
    117             &:disabled {
    118                 opacity: 0.5;
    119                 cursor: not-allowed;
    120126            }
    121127        }
     
    134140
    135141        &::after {
    136             content: '';
     142            content: "";
    137143            position: absolute;
    138144            top: 50%;
     
    164170
    165171@keyframes spin {
     172
    166173    to {
    167174        transform: rotate(360deg);
  • jobbnorge-block/trunk/webpack.config.js

    r3322139 r3373293  
    1 const defaultConfig = require('@wordpress/scripts/config/webpack.config');
    2 const path = require('path');
     1const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
     2const path = require( 'path' );
    33
    44module.exports = {
    55    ...defaultConfig,
    66    entry: {
    7         init: path.resolve(__dirname, 'src/init.js'),
    8         editor: path.resolve(__dirname, 'src/editor.scss'),
    9         style: path.resolve(__dirname, 'src/style.scss'),
    10         pagination: path.resolve(__dirname, 'src/pagination.js'),
     7        init: path.resolve( __dirname, 'src/init.js' ),
     8        editor: path.resolve( __dirname, 'src/editor.scss' ),
     9        style: path.resolve( __dirname, 'src/style.scss' ),
     10        pagination: path.resolve( __dirname, 'src/pagination.js' ),
    1111    },
    1212};
  • jobbnorge-block/trunk/wp-jobb-norge.php

    r3330462 r3373293  
    44 * Plugin URI:        https://wordpress.org/plugins/jobbnorge-block/
    55 * Description:       Retrieve and display job listings from Jobbnorge.no
    6  * Requires at least: 5.9
    7  * Requires PHP:      7.0
    8  * Version:           2.2.2
     6 * Requires at least: 6.5
     7 * Requires PHP:      8.2
     8 * Version:           2.2.3
    99 * Author:            PerS
    1010 * License:           GPL-2.0-or-later
    1111 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
    1212 * Text Domain:       wp-jobbnorge-block
    13  *
    1413 * @package           wp-jobbnorge-block
    1514 */
    1615
    1716namespace DSS\Jobbnorge;
     17
     18if ( ! defined( 'ABSPATH' ) ) {
     19    exit; // Safety.
     20}
     21
     22if ( ! defined( 'WP_JOBBNORGE_VERSION' ) ) {
     23    define( 'WP_JOBBNORGE_VERSION', '2.2.3' );
     24}
    1825
    1926if ( ! \class_exists( 'Jobbnorge_CacheHandler' ) ) {
     
    2128}
    2229
    23 
    24 add_action( 'init', __NAMESPACE__ . '\dss_jobbnorge_init' );
    25 
    26 /**
    27  * Registers the block using the metadata loaded from the `block.json` file.
    28  * Behind the scenes, it registers also all assets so they can be enqueued
    29  * through the block editor in the corresponding context.
    30  *
    31  * @see https://developer.wordpress.org/reference/functions/register_block_type/
    32  */
    33 function dss_jobbnorge_init() {
    34     // Add the 'dss_jobbnorge_enqueue_scripts' function to the 'admin_enqueue_scripts' action hook.
    35     // This function will be called when scripts and styles are enqueued for the admin panel.
    36     add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\dss_jobbnorge_enqueue_scripts' );
    37 
    38     // Add the 'dss_jobbnorge_enqueue_frontend_styles' function to the 'wp_enqueue_scripts' action hook.
    39     // This function will be called when scripts and styles are enqueued for the front end of the site.
    40     add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\dss_jobbnorge_enqueue_frontend_styles' );
    41 
    42     // Load the plugin's text domain for internationalization.
    43     // The second argument is set to false to not override the global locale.
    44     // The third argument is the path to the plugin's languages directory.
     30add_action( 'init', __NAMESPACE__ . '\\dss_jobbnorge_init' );
     31
     32/**
     33 * Init: register block + i18n + enqueue hooks.
     34 */
     35function dss_jobbnorge_init(): void {
    4536    load_plugin_textdomain( 'wp-jobbnorge-block', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
    4637
    47     // Register the block type.
    48     // The first argument is the path to the block's build directory.
    49     // The second argument is an array of options for the block, including a render callback function.
    50     register_block_type(
    51         __DIR__ . '/build',
    52         [
    53             'render_callback' => __NAMESPACE__ . '\render_block_dss_jobbnorge',
    54         ]
    55     );
    56 }
    57 
    58 /**
    59  * Enqueue block editor only JavaScript and CSS
    60  *
    61  * @param string $hook_suffix The current admin page.
    62  * @return void
     38    register_block_type( __DIR__ . '/build', [ 'render_callback' => __NAMESPACE__ . '\\render_block_dss_jobbnorge' ] );
     39
     40    add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\\dss_jobbnorge_enqueue_scripts' );
     41    add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\\dss_jobbnorge_enqueue_frontend_styles' );
     42    add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\\enqueue_pagination_script' );
     43}
     44
     45/**
     46 * Editor assets.
    6347 */
    6448function dss_jobbnorge_enqueue_scripts( string $hook_suffix ): void {
    65 
    66     // Check if the current page is a post editing page.
    67     if ( 'post.php' !== $hook_suffix && 'post-new.php' !== $hook_suffix && 'edit.php' !== $hook_suffix ) {
    68         // If not, exit early.
     49    if ( ! in_array( $hook_suffix, [ 'post.php', 'post-new.php', 'edit.php' ], true ) ) {
    6950        return;
    7051    }
    71 
    72     // Define the path to the dependencies file.
    7352    $deps_file = plugin_dir_path( __FILE__ ) . 'build/init.asset.php';
    74 
    75     // Initialize an array for JavaScript dependencies and a random version number.
    76     $jsdeps  = [];
    77     $version = wp_rand();
    78 
    79     // Check if the dependencies file exists.
     53    $jsdeps    = [];
     54    $version   = WP_JOBBNORGE_VERSION;
    8055    if ( file_exists( $deps_file ) ) {
    81         // If it does, require it and merge its dependencies with the existing ones.
    82         $file   = require $deps_file;
    83         $jsdeps = array_merge( $jsdeps, $file[ 'dependencies' ] );
    84         // Also, set the version to the one from the file.
     56        $file    = require $deps_file; // phpcs:ignore
     57        $jsdeps  = array_merge( $jsdeps, $file[ 'dependencies' ] );
    8558        $version = $file[ 'version' ];
    8659    }
    87 
    88     // Check if the current view is the admin dashboard.
    8960    if ( is_admin() ) {
    90         // If it is, register and enqueue a CSS file for the admin view.
    9161        wp_register_style( 'dss-jobbnorge-admin', plugin_dir_url( __FILE__ ) . 'build/init.css', [], $version );
    9262        wp_enqueue_style( 'dss-jobbnorge-admin' );
    9363    }
    94 
    95     // Set translations for the script.
    96     wp_set_script_translations(
    97         'dss-jobbnorge-editor-script', // Handle = block.json "name" (replace / with -) + "-editor-script".
    98         'wp-jobbnorge-block',
    99         plugin_dir_path( __FILE__ ) . 'languages/'
    100     );
    101 
    102     // Apply filter to modify the employers list.
     64    wp_set_script_translations( 'dss-jobbnorge-editor-script', 'wp-jobbnorge-block', plugin_dir_path( __FILE__ ) . 'languages/' );
    10365    $employers = apply_filters( 'jobbnorge_employers', false );
    104 
    105     // Proceed with localization if employers is not false.
    10666    if ( false !== $employers ) {
    107         // Ensure employers is an array.
    10867        if ( ! is_array( $employers ) ) {
    10968            $employers = [];
    11069        }
    111 
    112         // Localize the script to make employers data available.
    113         wp_localize_script(
    114             'dss-jobbnorge-editor-script',
    115             'wpJobbnorgeBlock',
    116             [
    117                 'employers' => $employers,
    118             ]
    119         );
    120     }
    121 }
    122 
    123 /**
    124  * Enqueue frontend styles for the block
    125  *
    126  * @return void
     70        wp_localize_script( 'dss-jobbnorge-editor-script', 'wpJobbnorgeBlock', [ 'employers' => $employers ] );
     71    }
     72}
     73
     74/**
     75 * Frontend styles.
    12776 */
    12877function dss_jobbnorge_enqueue_frontend_styles(): void {
    129     // Define the path to the dependencies file.
    13078    $deps_file = plugin_dir_path( __FILE__ ) . 'build/init.asset.php';
    131 
    132     // Initialize version number.
    133     $version = wp_rand();
    134 
    135     // Check if the dependencies file exists.
     79    $version   = WP_JOBBNORGE_VERSION;
    13680    if ( file_exists( $deps_file ) ) {
    137         // If it does, require it and get the version.
    138         $file    = require $deps_file;
     81        $file    = require $deps_file; // phpcs:ignore
    13982        $version = $file[ 'version' ];
    14083    }
    141 
    142     // Register and enqueue a CSS file for the public view.
    14384    wp_register_style( 'dss-jobbnorge', plugin_dir_url( __FILE__ ) . 'build/style-init.css', [], $version );
    14485    wp_enqueue_style( 'dss-jobbnorge' );
     
    14687
    14788/**
    148  * Renders the `jobbnorge` block on server.
    149  *
    150  * @param array $attributes The block attributes.
    151  *
    152  * @return string Returns the block content with received rss items.
    153  */
    154 function render_block_dss_jobbnorge( $attributes ) {
    155 
    156     // Set default values for attributes.
    157     $attributes = wp_parse_args(
    158         $attributes,
    159         [
    160             'employerID'       => '',
    161             'displayEmployer'  => false,
    162             'displayDate'      => true,
    163             'displayDeadline'  => false,
    164             'displayScope'     => false,
    165             'displayExcerpt'   => true,
    166             'excerptLength'    => 55,
    167             'blockLayout'      => 'list',
    168             'orderBy'          => 'Deadline',
    169             'columns'          => 3,
    170             'itemsToShow'      => 5,
    171             'enablePagination' => true,
    172             'jobsPerPage'      => 10,
    173         ]
    174     );
    175 
    176     // Convert employer IDs to an array and trim whitespace.
    177     $arr_ids = array_map( 'trim', explode( ',', $attributes[ 'employerID' ] ) );
    178 
    179     // Check if all IDs are numeric. If not, return an error message.
    180     if ( ! array_filter( $arr_ids, 'is_numeric' ) ) {
    181         return '<div class="components-placeholder"><div class="notice notice-error">' . __( 'Invalid ID', 'wp-jobbnorge-block' ) . '</div></div>';
    182     }
    183 
    184     // Get current page for pagination
    185     $current_page = isset( $_GET[ 'jobbnorge_page' ] ) ? max( 1, intval( $_GET[ 'jobbnorge_page' ] ) ) : 1;
    186 
    187     // Determine items per page based on pagination setting
    188     $items_per_page = $attributes[ 'enablePagination' ] ? $attributes[ 'jobsPerPage' ] : $attributes[ 'itemsToShow' ];
    189 
    190     // Construct the API URL for v3
    191     // NOTE: API v3 pagination doesn't work correctly with employer filtering
    192     // So we fetch all jobs for the employers and paginate in PHP
    193     $jobbnorge_api_url = 'https://publicapi.jobbnorge.no/v3/Jobs?abroad=false&orderBy=' . $attributes[ 'orderBy' ];
    194 
    195     // Add each employer ID to the API URL.
     89 * Render block frontend.
     90 */
     91function render_block_dss_jobbnorge( $attributes ): string {
     92    $attributes = wp_parse_args( $attributes, [
     93        'employerID'       => '',
     94        'displayEmployer'  => false,
     95        'displayDate'      => true, // currently unused but kept for backward compat.
     96        'displayDeadline'  => false,
     97        'displayScope'     => false,
     98        'displayExcerpt'   => true,
     99        'excerptLength'    => 55,
     100        'blockLayout'      => 'list',
     101        'orderBy'          => 'Deadline',
     102        'columns'          => 3,
     103        'itemsToShow'      => 5,
     104        'enablePagination' => true,
     105        'jobsPerPage'      => 10,
     106    ] );
     107
     108    // Sanitize employer IDs.
     109    $arr_ids_raw = array_filter( array_map( 'trim', explode( ',', (string) $attributes[ 'employerID' ] ) ) );
     110    $arr_ids     = [];
     111    foreach ( $arr_ids_raw as $maybe ) {
     112        if ( ctype_digit( $maybe ) ) {
     113            $arr_ids[] = (string) absint( $maybe );
     114        }
     115    }
     116    if ( empty( $arr_ids ) && ! empty( $attributes[ 'employerID' ] ) ) {
     117        return '<div class="components-placeholder"><div class="notice notice-error">' . esc_html__( 'Invalid ID', 'wp-jobbnorge-block' ) . '</div></div>';
     118    }
     119
     120    $current_page   = isset( $_GET[ 'jobbnorge_page' ] ) ? max( 1, absint( $_GET[ 'jobbnorge_page' ] ) ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only.
     121    $items_per_page = $attributes[ 'enablePagination' ] ? (int) $attributes[ 'jobsPerPage' ] : (int) $attributes[ 'itemsToShow' ];
     122
     123    // Build API URL.
     124    $jobbnorge_api_url = 'https://publicapi.jobbnorge.no/v3/Jobs?abroad=false&orderBy=' . rawurlencode( $attributes[ 'orderBy' ] );
    196125    foreach ( $arr_ids as $id ) {
    197         $jobbnorge_api_url .= '&employer=' . $id;
    198     }
    199 
    200     $cache_path = apply_filters( 'jobbnorge_cache_path', WP_CONTENT_DIR . '/cache/jobbnorge' );
    201     $cache      = new \Jobbnorge_CacheHandler( $cache_path );
    202 
    203     // Cache key based on employer IDs and settings, not pagination
     126        $jobbnorge_api_url .= '&employer=' . absint( $id );
     127    }
     128
     129    $cache_path    = apply_filters( 'jobbnorge_cache_path', WP_CONTENT_DIR . '/cache/jobbnorge' );
     130    $cache         = new \Jobbnorge_CacheHandler( $cache_path );
    204131    $cache_key     = md5( $jobbnorge_api_url );
    205     $expiration    = apply_filters( 'jobbnorge_cache_time', 30 * MINUTE_IN_SECONDS );
     132    $expiration    = (int) apply_filters( 'jobbnorge_cache_time', 30 * MINUTE_IN_SECONDS );
    206133    $response_data = $cache->get( $cache_key, $expiration );
    207 
    208134    if ( false === $response_data ) {
    209         $response = wp_remote_get( $jobbnorge_api_url );
    210 
    211         if ( is_wp_error( $response ) ) {
    212             return '<div class="components-placeholder"><div class="notice notice-error">' . __( 'Error connecting to Jobbnorge.no', 'wp-jobbnorge-block' ) . '</div></div>';
     135        // Perform remote request when no *fresh* cache. We'll attempt a stale cache fallback below if available.
     136        $response = wp_remote_get( $jobbnorge_api_url, [
     137            'timeout' => 10,
     138            'headers' => [
     139                'Accept'     => 'application/json',
     140                'User-Agent' => 'JobbnorgeBlock/' . WP_JOBBNORGE_VERSION . ' ' . home_url( '/' ),
     141            ],
     142        ] );
     143
     144        $http_status = ! is_wp_error( $response ) ? wp_remote_retrieve_response_code( $response ) : 0;
     145        $body        = ! is_wp_error( $response ) ? wp_remote_retrieve_body( $response ) : '';
     146        $tmp         = $body ? json_decode( $body, true ) : null;
     147        $json_ok     = is_array( $tmp ) && json_last_error() === JSON_ERROR_NONE;
     148
     149        if ( ! is_wp_error( $response ) && $http_status >= 200 && $http_status < 300 && $json_ok ) {
     150            // Happy path: cache and proceed.
     151            $response_data = $tmp;
     152            $cache->set( $cache_key, $response_data );
     153        } else {
     154            // Attempt stale cache fallback: read cache file ignoring expiration.
     155            $stale_data = null;
     156            $cache_file = apply_filters( 'jobbnorge_cache_path', WP_CONTENT_DIR . '/cache/jobbnorge' ) . '/' . $cache_key . '.php';
     157            if ( file_exists( $cache_file ) ) {
     158                // Suppress errors; include returns data array.
     159                $maybe_stale = @include $cache_file; // phpcs:ignore
     160                if ( is_array( $maybe_stale ) ) {
     161                    $stale_data = $maybe_stale;
     162                }
     163            }
     164
     165            $error_type = 'unknown';
     166            if ( is_wp_error( $response ) ) {
     167                $error_type = 'network';
     168            } elseif ( $http_status >= 500 ) {
     169                $error_type = 'server';
     170            } elseif ( $http_status === 404 ) {
     171                $error_type = 'not_found';
     172            } elseif ( $http_status >= 400 ) {
     173                $error_type = 'client';
     174            } elseif ( ! $json_ok ) {
     175                $error_type = 'invalid_json';
     176            }
     177
     178            /**
     179             * Fires when the Jobbnorge API request fails.
     180             *
     181             * @param string $error_type One of network|server|client|not_found|invalid_json|unknown.
     182             * @param int    $http_status HTTP status code (0 if network error).
     183             * @param string $api_url     Requested API URL.
     184             */
     185            do_action( 'jobbnorge_api_request_failed', $error_type, $http_status, $jobbnorge_api_url );
     186
     187            if ( $stale_data ) {
     188                // Provide gentle notice while still rendering stale data.
     189                $response_data = $stale_data;
     190                // Prepend a warning message that content may be outdated.
     191                $stale_notice = '<div class="notice notice-warning jobbnorge-stale" role="alert">' . esc_html__( 'Showing cached results due to a temporary connection issue.', 'wp-jobbnorge-block' ) . '</div>';
     192            } else {
     193                // User-facing message depending on error type.
     194                $human_msg = __( 'Error connecting to Jobbnorge.no', 'wp-jobbnorge-block' );
     195                if ( 'not_found' === $error_type ) {
     196                    $human_msg = __( 'Job listings not found (404).', 'wp-jobbnorge-block' );
     197                } elseif ( 'server' === $error_type ) {
     198                    $human_msg = __( 'Jobbnorge service temporarily unavailable.', 'wp-jobbnorge-block' );
     199                } elseif ( 'client' === $error_type ) {
     200                    $human_msg = __( 'Request error retrieving jobs.', 'wp-jobbnorge-block' );
     201                } elseif ( 'invalid_json' === $error_type ) {
     202                    $human_msg = __( 'Received invalid data from Jobbnorge.', 'wp-jobbnorge-block' );
     203                }
     204                return '<div class="components-placeholder"><div class="notice notice-error">' . esc_html( $human_msg ) . '</div></div>';
     205            }
    213206        }
    214 
    215         $body          = wp_remote_retrieve_body( $response );
    216         $response_data = json_decode( $body, true );
    217         $cache->set( $cache_key, $response_data );
    218     }
    219 
    220     // Debug: Log the API response structure
    221     if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    222         error_log( 'Jobbnorge API URL: ' . $jobbnorge_api_url );
    223         error_log( 'Jobbnorge API Response: ' . print_r( $response_data, true ) );
    224     }
    225     // Handle v3 API response structure
    226     $all_items  = isset( $response_data[ 'jobs' ] ) ? $response_data[ 'jobs' ] : $response_data;
     207    }
     208
     209    $all_items = isset( $response_data[ 'jobs' ] ) && is_array( $response_data[ 'jobs' ] ) ? $response_data[ 'jobs' ] : ( is_array( $response_data ) ? $response_data : [] );
     210    if ( ! is_array( $all_items ) ) {
     211        $all_items = [];
     212    }
    227213    $total_jobs = count( $all_items );
    228 
    229     // Debug: Log the items array
    230     if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    231         error_log( 'Items count: ' . count( $all_items ) );
    232         error_log( 'Total jobs: ' . $total_jobs );
    233         if ( ! empty( $all_items ) ) {
    234             error_log( 'First item: ' . print_r( $all_items[ 0 ], true ) );
    235         }
    236     }
    237 
    238     // Implement pagination in PHP since API pagination doesn't work with employer filtering
    239     if ( $attributes[ 'enablePagination' ] && $total_jobs > 0 ) {
    240         // Calculate pagination
    241         $start_index = ( $current_page - 1 ) * $attributes[ 'jobsPerPage' ];
    242         $items       = array_slice( $all_items, $start_index, $attributes[ 'jobsPerPage' ] );
     214    if ( 0 === $total_jobs ) {
     215        return '<div class="components-placeholder"><div class="notice notice-error">' . esc_html__( 'No jobs found', 'wp-jobbnorge-block' ) . '</div></div>';
     216    }
     217
     218    if ( $attributes[ 'enablePagination' ] ) {
     219        $start_index = ( $current_page - 1 ) * $items_per_page;
     220        $items       = array_slice( $all_items, $start_index, $items_per_page );
    243221    } else {
    244         // For non-paginated, limit to itemsToShow
    245         $items      = array_slice( $all_items, 0, $attributes[ 'itemsToShow' ] );
    246         $total_jobs = count( $items ); // Update total for non-paginated display
    247     }
    248 
    249     // If there are no items, return an error message.
    250     if ( ! $items ) {
    251         return '<div class="components-placeholder"><div class="notice notice-error">' . __( 'No jobs found', 'wp-jobbnorge-block' ) . '</div></div>';
    252     }
    253 
    254     // Initialize an empty string for the list items.
    255     $list_items = '';
    256 
    257     // Loop through each item.
    258     foreach ( $items as $item ) {
    259         // Sanitize and format the title.
    260         $title = esc_html( trim( wp_strip_all_tags( $item[ 'title' ] ) ) );
    261         $title = empty( $title ) ? __( '(no title)', 'wp-jobbnorge-block' ) : $title;
    262 
    263         // Sanitize the link.
    264         $link = esc_url( $item[ 'link' ] );
    265         // If there's a link, wrap the title in an anchor tag.
    266         $title = $link ? "<a href='{$link}'>{$title}</a>" : $title;
    267 
    268         // Wrap the title in a div.
    269         $title = "<div class='wp-block-dss-jobbnorge__item-title'>{$title}</div>";
    270 
    271         // Initialize an empty string for the deadline.
    272         $deadline = '';
    273         // If the displayDate attribute is true and the item has a deadline, format the deadline.
    274         if ( $attributes[ 'displayDate' ] && isset( $item[ 'deadline' ] ) ) {
    275             $deadline = format_deadline( $item[ 'deadline' ] );
    276         }
    277 
    278         // Format the excerpt.
    279         $excerpt = format_excerpt( $attributes, $item );
    280 
    281         // Format the employer and scope attributes.
    282         $employer = format_attribute( $attributes, $item, 'employer', 'displayEmployer', 'wp-block-dss-jobbnorge__item-employer', __( 'Employer', 'wp-jobbnorge-block' ) );
    283         $scope    = format_attribute( $attributes, $item, 'jobScope', 'displayScope', 'wp-block-dss-jobbnorge__item-scope', __( 'Scope', 'wp-jobbnorge-block' ) );
    284 
    285         // Initialize an empty string for the meta.
    286         $meta = '';
    287         // If there's an employer, deadline, or scope, wrap them in a div.
    288         if ( $employer || $deadline || $scope ) {
    289             $meta = '<div class="wp-block-dss-jobbnorge__item-meta">' . $employer . $deadline . $scope . '</div>';
    290         }
    291 
    292         // Add the item to the list items string.
    293         $list_items .= "<li class='wp-block-dss-jobbnorge__item'>{$title}{$meta}{$excerpt}</li>";
    294     }
    295 
    296     // Get the block wrapper attributes (without grid classes)
    297     $wrapper_classes = [];
    298     add_classname( $wrapper_classes, $attributes, 'displayEmployer', 'has-employer' );
    299     add_classname( $wrapper_classes, $attributes, 'displayDate', 'has-dates' );
    300     add_classname( $wrapper_classes, $attributes, 'displayDeadline', 'has-deadline' );
    301     add_classname( $wrapper_classes, $attributes, 'displayScope', 'has-scope' );
    302     add_classname( $wrapper_classes, $attributes, 'displayExcerpt', 'has-excerpts' );
    303 
    304     $wrapper_attributes = get_block_wrapper_attributes( [
     222        $items      = array_slice( $all_items, 0, $items_per_page );
     223        $total_jobs = count( $items );
     224    }
     225    if ( empty( $items ) ) {
     226        return '<div class="components-placeholder"><div class="notice notice-error">' . esc_html__( 'No jobs found', 'wp-jobbnorge-block' ) . '</div></div>';
     227    }
     228
     229    $wrapper_classes = [ 'wp-block-dss-jobbnorge__wrapper' ];
     230    if ( $attributes[ 'enablePagination' ] ) {
     231        $wrapper_classes[] = 'has-pagination';
     232    }
     233
     234    $wrapper_attributes = get_block_wrapper_attributes( [
    305235        'class'           => implode( ' ', $wrapper_classes ),
    306         'data-attributes' => esc_attr( json_encode( $attributes ) ),
     236        'data-attributes' => esc_attr( wp_json_encode( $attributes ) ),
     237        'aria-live'       => 'polite',
    307238    ] );
    308239
    309     // Generate the ul classes (including grid classes)
    310240    $ul_classes = [ 'wp-block-dss-jobbnorge' ];
    311241    if ( 'grid' === $attributes[ 'blockLayout' ] ) {
    312242        $ul_classes[] = 'is-grid';
    313         $ul_classes[] = 'columns-' . $attributes[ 'columns' ];
    314     }
    315 
    316     // Generate pagination controls if enabled
     243        $ul_classes[] = 'columns-' . (int) $attributes[ 'columns' ];
     244    }
     245
     246    $list_items = '';
     247    foreach ( $items as $item ) {
     248        // Deadline filtering: hide past deadlines if ordering by deadline.
     249        if ( isset( $item[ 'deadlineDate' ] ) && 'Deadline' === $attributes[ 'orderBy' ] ) {
     250            $deadline_ts = false;
     251            try {
     252                $deadline_ts = parse_date( $item[ 'deadlineDate' ] );
     253            } catch (\Throwable $t) {
     254                $deadline_ts = false;
     255            }
     256            if ( $deadline_ts && $deadline_ts < time() ) {
     257                continue; // Skip expired.
     258            }
     259        }
     260        $title     = isset( $item[ 'title' ] ) ? $item[ 'title' ] : '';
     261        $link      = isset( $item[ 'link' ] ) ? $item[ 'link' ] : '#';
     262        $deadline  = $attributes[ 'displayDeadline' ] && ! empty( $item[ 'deadlineDate' ] ) ? format_deadline( $item[ 'deadlineDate' ] ) : '';
     263        $excerpt   = format_excerpt( $attributes, $item );
     264        $employer  = format_attribute( $attributes, $item, 'employer', 'displayEmployer', 'wp-block-dss-jobbnorge__item-employer', __( 'Employer', 'wp-jobbnorge-block' ) );
     265        $scope     = format_attribute( $attributes, $item, 'jobScope', 'displayScope', 'wp-block-dss-jobbnorge__item-scope', __( 'Scope', 'wp-jobbnorge-block' ) );
     266        $meta_html = '';
     267        if ( $employer || $deadline || $scope ) {
     268            $meta_html = sprintf( '<div class="wp-block-dss-jobbnorge__item-meta">%s%s%s</div>', $employer, $deadline, $scope );
     269        }
     270        $list_items .= sprintf(
     271            '<li class="wp-block-dss-jobbnorge__item"><div class="wp-block-dss-jobbnorge__item-title"><a href="%s">%s</a></div>%s%s</li>',
     272            esc_url( $link ),
     273            esc_html( $title ),
     274            $meta_html,
     275            $excerpt
     276        );
     277    }
     278
     279    if ( '' === $list_items ) {
     280        return '<div class="components-placeholder"><div class="notice notice-error">' . esc_html__( 'No jobs found', 'wp-jobbnorge-block' ) . '</div></div>';
     281    }
     282
    317283    $pagination_html = '';
    318     if ( $attributes[ 'enablePagination' ] && count( $all_items ) > $attributes[ 'jobsPerPage' ] ) {
    319         $pagination_html = generate_pagination_controls( $current_page, count( $all_items ), $attributes[ 'jobsPerPage' ], $attributes );
    320     }
    321 
    322     // Return the final HTML string, wrapping the list items in an unordered list.
    323     return sprintf( '<div %s><ul class="%s">%s</ul>%s</div>', $wrapper_attributes, esc_attr( implode( ' ', $ul_classes ) ), $list_items, $pagination_html );
     284    if ( $attributes[ 'enablePagination' ] && $total_jobs > $items_per_page ) {
     285        $pagination_html = generate_pagination_controls( $current_page, $total_jobs, $items_per_page, $attributes );
     286    }
     287
     288    return sprintf( '<div %1$s><ul class="%2$s">%3$s</ul>%4$s</div>', $wrapper_attributes, esc_attr( implode( ' ', $ul_classes ) ), $list_items, $pagination_html );
    324289}
    325290
     
    331296 * @return string The formatted excerpt.
    332297 */
    333 function format_excerpt( $attributes, $item ) {
    334     // Initialize an empty string for the result.
    335     $result = '';
    336 
    337     // If the displayExcerpt attribute is true and the item has a summary, format the excerpt.
    338     if ( $attributes[ 'displayExcerpt' ] && isset( $item[ 'summary' ] ) ) {
    339         // Decode the HTML entities in the summary.
    340         $excerpt = html_entity_decode( $item[ 'summary' ], ENT_QUOTES, get_option( 'blog_charset' ) );
    341         // Trim the excerpt to the excerptLength and escape it for safe use in HTML output.
    342         $excerpt = esc_attr( wp_trim_words( $excerpt, $attributes[ 'excerptLength' ], '' ) );
    343 
    344         // Format the read more link.
    345         $read_more = sprintf( ' ... <a href="%s">%s</a>', esc_url( $item[ 'link' ] ), __( 'Read more', 'wp-jobbnorge-block' ) );
    346 
    347         // Add the excerpt and read more link to the result string, wrapped in a div.
    348         $result = sprintf( '<div class="wp-block-dss-jobbnorge__item-excerpt">%s%s</div>', esc_html( $excerpt ), $read_more );
    349     }
    350 
    351     // Return the result.
    352     return $result;
     298function format_excerpt( $attributes, $item ): string {
     299    if ( empty( $attributes[ 'displayExcerpt' ] ) || empty( $item[ 'summary' ] ) ) {
     300        return '';
     301    }
     302    $excerpt_raw = html_entity_decode( wp_strip_all_tags( (string) $item[ 'summary' ] ), ENT_QUOTES, get_option( 'blog_charset' ) );
     303    $excerpt     = wp_trim_words( $excerpt_raw, (int) $attributes[ 'excerptLength' ], '' );
     304    $read_more   = sprintf( ' <a href="%s">%s</a>', esc_url( $item[ 'link' ] ?? '#' ), esc_html__( 'Read more', 'wp-jobbnorge-block' ) );
     305    return sprintf( '<div class="wp-block-dss-jobbnorge__item-excerpt">%s%s</div>', esc_html( $excerpt ), $read_more );
    353306}
    354307
     
    390343 * @return string The formatted deadline date.
    391344 */
    392 function format_deadline( $deadline_date ) {
    393     // If there's no deadline date, return an empty string.
     345function format_deadline( $deadline_date ): string {
    394346    if ( ! $deadline_date ) {
    395347        return '';
    396348    }
    397 
    398349    try {
    399         // Try to parse the deadline date.
    400         $date = parse_date( $deadline_date );
    401         // Format the date according to the site's date format.
     350        $date     = parse_date( $deadline_date );
    402351        $str_date = date_i18n( get_option( 'date_format' ), $date );
    403     } catch (\Exception $e) {
    404         // If there's an exception, fallback to the original date.
     352    } catch (\Throwable $t) {
    405353        $str_date = $deadline_date;
    406354        $date     = false;
    407355    }
    408 
    409     // If there's a formatted date, return a time element with the date.
    410356    if ( $str_date ) {
    411357        return sprintf(
    412             '<time datetime="%1$s" class="wp-block-dss-jobbnorge__item-deadline">%2$s %3$s</time> ',
    413                 // If there's a parsed date, use it for the datetime attribute. Otherwise, leave it empty.
    414             ( $date ) ? esc_attr( wp_date( 'c', $date ) ) : '',
    415             // Translate the 'Deadline:' string.
    416             __( 'Deadline:', 'wp-jobbnorge-block' ),
    417             // Escape the formatted date for safe use in HTML output.
    418             esc_attr( $str_date )
     358            '<time datetime="%1$s" class="wp-block-dss-jobbnorge__item-deadline">%2$s %3$s</time>',
     359            $date ? esc_attr( wp_date( 'c', $date ) ) : '',
     360            esc_html__( 'Deadline:', 'wp-jobbnorge-block' ),
     361            esc_html( $str_date )
    419362        );
    420363    }
    421 
    422     // If there's no formatted date, return an empty string.
    423364    return '';
    424365}
     
    469410function parse_date_fallback( $deadline_date ) {
    470411    // Define an array of month names in Norwegian.
    471     $str_months = [ 
     412    $str_months = [
    472413        'januar',
    473414        'februar',
     
    485426
    486427    // Define an array of month numbers.
    487     $num_months = [ 
     428    $num_months = [
    488429        '01',
    489430        '02',
     
    617558 * Register AJAX endpoints for pagination.
    618559 */
    619 add_action( 'wp_ajax_jobbnorge_get_jobs', __NAMESPACE__ . '\handle_ajax_get_jobs' );
    620 add_action( 'wp_ajax_nopriv_jobbnorge_get_jobs', __NAMESPACE__ . '\handle_ajax_get_jobs' );
     560add_action( 'wp_ajax_jobbnorge_get_jobs', __NAMESPACE__ . '\\handle_ajax_get_jobs' );
     561add_action( 'wp_ajax_nopriv_jobbnorge_get_jobs', __NAMESPACE__ . '\\handle_ajax_get_jobs' );
    621562
    622563/**
    623564 * Handle AJAX request for paginated job listings.
    624565 */
    625 function handle_ajax_get_jobs() {
    626     // Verify nonce
    627     if ( ! wp_verify_nonce( $_POST[ 'nonce' ], 'jobbnorge_pagination_nonce' ) ) {
    628         wp_die( 'Security check failed' );
    629     }
    630 
    631     // Get and sanitize parameters
    632     $page       = isset( $_POST[ 'page' ] ) ? max( 1, intval( $_POST[ 'page' ] ) ) : 1;
    633     $attributes = isset( $_POST[ 'attributes' ] ) ? json_decode( stripslashes( $_POST[ 'attributes' ] ), true ) : [];
    634 
    635     // Validate attributes
    636     if ( empty( $attributes ) || ! is_array( $attributes ) ) {
    637         wp_send_json_error( 'Invalid attributes' );
    638     }
    639 
    640     // Set current page in GET superglobal for compatibility
    641     $_GET[ 'jobbnorge_page' ] = $page;
    642 
    643     // Generate the job listings HTML
    644     $html = render_block_dss_jobbnorge( $attributes );
    645 
    646     // Return JSON response
     566function handle_ajax_get_jobs(): void {
     567    if ( empty( $_POST[ 'nonce' ] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ 'nonce' ] ) ), 'jobbnorge_pagination_nonce' ) ) {
     568        wp_send_json_error( [ 'message' => __( 'Security check failed', 'wp-jobbnorge-block' ) ], 403 );
     569    }
     570    $page       = isset( $_POST[ 'page' ] ) ? max( 1, absint( wp_unslash( $_POST[ 'page' ] ) ) ) : 1;
     571    $raw_attr   = isset( $_POST[ 'attributes' ] ) ? wp_unslash( $_POST[ 'attributes' ] ) : '';
     572    $attributes = json_decode( $raw_attr, true );
     573    if ( json_last_error() !== JSON_ERROR_NONE || empty( $attributes ) || ! is_array( $attributes ) ) {
     574        wp_send_json_error( [ 'message' => __( 'Invalid attributes', 'wp-jobbnorge-block' ) ], 400 );
     575    }
     576    $_GET[ 'jobbnorge_page' ] = $page; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
     577    $html                     = render_block_dss_jobbnorge( $attributes );
    647578    wp_send_json_success( [ 'html' => $html ] );
    648579}
     
    685616        'jobbnorge-pagination',
    686617        'jobbnorgeAjax',
    687         [ 
     618        [
    688619            'ajaxUrl' => admin_url( 'admin-ajax.php' ),
    689620            'nonce'   => wp_create_nonce( 'jobbnorge_pagination_nonce' ),
     
    693624
    694625// Hook into wp_enqueue_scripts to add pagination script
    695 add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\enqueue_pagination_script' );
     626// Enqueue pagination script already hooked in init via dss_jobbnorge_init.
Note: See TracChangeset for help on using the changeset viewer.