Changeset 3373293
- Timestamp:
- 10/05/2025 10:43:11 PM (2 months ago)
- Location:
- jobbnorge-block
- Files:
-
- 38 edited
- 1 copied
-
tags/2.2.3 (copied) (copied from jobbnorge-block/trunk)
-
tags/2.2.3/CHANGELOG.md (modified) (1 diff)
-
tags/2.2.3/README.md (modified) (1 diff)
-
tags/2.2.3/build/block.json (modified) (1 diff)
-
tags/2.2.3/build/init.asset.php (modified) (1 diff)
-
tags/2.2.3/build/init.js (modified) (1 diff)
-
tags/2.2.3/build/pagination.asset.php (modified) (1 diff)
-
tags/2.2.3/build/pagination.js (modified) (1 diff)
-
tags/2.2.3/build/style-init.css (modified) (1 diff)
-
tags/2.2.3/package-lock.json (modified) (2 diffs)
-
tags/2.2.3/package.json (modified) (1 diff)
-
tags/2.2.3/readme.txt (modified) (2 diffs)
-
tags/2.2.3/src/block.json (modified) (1 diff)
-
tags/2.2.3/src/edit.js (modified) (7 diffs)
-
tags/2.2.3/src/editor.scss (modified) (5 diffs)
-
tags/2.2.3/src/index.js (modified) (3 diffs)
-
tags/2.2.3/src/pagination.js (modified) (1 diff)
-
tags/2.2.3/src/style.scss (modified) (10 diffs)
-
tags/2.2.3/webpack.config.js (modified) (1 diff)
-
tags/2.2.3/wp-jobb-norge.php (modified) (10 diffs)
-
trunk/CHANGELOG.md (modified) (1 diff)
-
trunk/README.md (modified) (1 diff)
-
trunk/build/block.json (modified) (1 diff)
-
trunk/build/init.asset.php (modified) (1 diff)
-
trunk/build/init.js (modified) (1 diff)
-
trunk/build/pagination.asset.php (modified) (1 diff)
-
trunk/build/pagination.js (modified) (1 diff)
-
trunk/build/style-init.css (modified) (1 diff)
-
trunk/package-lock.json (modified) (2 diffs)
-
trunk/package.json (modified) (1 diff)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/src/block.json (modified) (1 diff)
-
trunk/src/edit.js (modified) (7 diffs)
-
trunk/src/editor.scss (modified) (5 diffs)
-
trunk/src/index.js (modified) (3 diffs)
-
trunk/src/pagination.js (modified) (1 diff)
-
trunk/src/style.scss (modified) (10 diffs)
-
trunk/webpack.config.js (modified) (1 diff)
-
trunk/wp-jobb-norge.php (modified) (10 diffs)
Legend:
- Unmodified
- Added
- Removed
-
jobbnorge-block/tags/2.2.3/CHANGELOG.md
r3330462 r3373293 1 1 # 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. 2 7 3 8 ## 2.2.2 -
jobbnorge-block/tags/2.2.3/README.md
r3322139 r3373293 91 91 ### 2) Modify the block settings. 92 92 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. 93 96 - Set the number of jobs to display. 94 97 - Set the no jobs message. -
jobbnorge-block/tags/2.2.3/build/block.json
r3330462 r3373293 3 3 "apiVersion": 2, 4 4 "name": "dss/jobbnorge", 5 "version": "2.2. 2",5 "version": "2.2.3", 6 6 "title": "Jobbnorge", 7 7 "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)}}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: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 1 1 { 2 2 "name": "jobbnorge-block", 3 "version": "2.2. 2",3 "version": "2.2.3", 4 4 "lockfileVersion": 3, 5 5 "requires": true, … … 7 7 "": { 8 8 "name": "jobbnorge-block", 9 "version": "2.2. 2",9 "version": "2.2.3", 10 10 "license": "GPL-2.0-or-later", 11 11 "dependencies": { -
jobbnorge-block/tags/2.2.3/package.json
r3330462 r3373293 1 1 { 2 2 "name": "jobbnorge-block", 3 "version": "2.2. 2",3 "version": "2.2.3", 4 4 "description": "Jobbnorge Block for WordPress Gutenberg", 5 5 "author": "Per Søderlind <[email protected]>", -
jobbnorge-block/tags/2.2.3/readme.txt
r3330470 r3373293 5 5 Requires at least: 6.5 6 6 Requires PHP: 8.2 7 Stable tag: 2.2. 27 Stable tag: 2.2.3 8 8 License: GPL-2.0-or-later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 105 105 == Changelog == 106 106 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 107 112 = 2.2.2 = 108 113 * Update block.json to include default value and role for employerID -
jobbnorge-block/tags/2.2.3/src/block.json
r3330462 r3373293 3 3 "apiVersion": 2, 4 4 "name": "dss/jobbnorge", 5 "version": "2.2. 2",5 "version": "2.2.3", 6 6 "title": "Jobbnorge", 7 7 "category": "widgets", -
jobbnorge-block/tags/2.2.3/src/edit.js
r3322139 r3373293 2 2 * WordPress dependencies 3 3 */ 4 import { BlockControls, InspectorControls, useBlockProps } from '@wordpress/block-editor'; 4 import { 5 BlockControls, 6 InspectorControls, 7 useBlockProps, 8 } from '@wordpress/block-editor'; 5 9 import { 6 10 Button, … … 26 30 const DEFAULT_MAX_ITEMS = 100; 27 31 32 /* eslint-disable jsdoc/check-line-alignment */ 28 33 /** 29 * Description placeholder 30 * @date 17/11/2023 - 16:21:26 34 * Jobbnorge block editor component. 31 35 * 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. 37 40 */ 38 export default function JobbnorgeEdit({ attributes, setAttributes }) { 41 /* eslint-enable jsdoc/check-line-alignment */ 42 export default function JobbnorgeEdit( { attributes, setAttributes } ) { 39 43 // 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 ); 41 45 42 46 // Destructure the attributes object to get the individual attributes. … … 58 62 // Define a function to toggle an attribute. 59 63 // 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 ) { 61 65 return () => { 62 const value = attributes[ propName];63 64 setAttributes( { [propName]: !value });66 const value = attributes[ propName ]; 67 68 setAttributes( { [ propName ]: ! value } ); 65 69 }; 66 70 } … … 68 72 // Define a function to handle the form submission. 69 73 // This function will set the employerID attribute and set isEditing to false. 70 function onSubmitURL( event) {74 function onSubmitURL( event ) { 71 75 event.preventDefault(); 72 76 73 if ( employerID) {74 setAttributes( { employerID: employerID });75 setIsEditing( false);77 if ( employerID ) { 78 setAttributes( { employerID } ); 79 setIsEditing( false ); 76 80 } 77 81 } … … 79 83 const blockProps = useBlockProps(); 80 84 81 if ( isEditing) {85 if ( isEditing ) { 82 86 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 ? ( 87 95 <SelectControl 88 96 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 ) => ( { 92 106 label: o.label, 93 107 value: o.value, 94 108 disabled: o?.disabled ?? false, 95 } ))}109 } ) ) } 96 110 className="wp-block-dss-jobbnorge__placeholder-input" 97 help={ __(111 help={ __( 98 112 'Select employers to display. Ctrl-click (Windows) or Cmd-click (Mac) to select multiple employers. Shift-click to select a range of employers.', 99 113 'wp-jobbnorge-block' 100 ) }114 ) } 101 115 __nextHasNoMarginBottom 102 116 /> 103 117 ) : ( 104 118 <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 } 108 127 className="wp-block-dss-jobbnorge__placeholder-input" 109 128 /> 110 ) }129 ) } 111 130 <Button variant="primary" type="submit"> 112 { __('Save', 'wp-jobbnorge-block')}131 { __( 'Save', 'wp-jobbnorge-block' ) } 113 132 </Button> 114 133 </form> … … 121 140 { 122 141 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 ), 125 144 }, 126 145 { 127 146 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' } ), 130 149 isActive: blockLayout === 'list', 131 150 }, 132 151 { 133 152 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' } ), 136 155 isActive: blockLayout === 'grid', 137 156 }, … … 141 160 <> 142 161 <BlockControls> 143 <ToolbarGroup controls={ toolbarControls} />162 <ToolbarGroup controls={ toolbarControls } /> 144 163 </BlockControls> 145 164 <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 && ( 154 181 <RangeControl 155 182 __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 } 161 193 required 162 194 /> 163 ) }164 { enablePagination && (195 ) } 196 { enablePagination && ( 165 197 <RangeControl 166 198 __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 } 172 209 required 173 210 /> 174 ) }175 { employerID.includes(',') && (211 ) } 212 { employerID.includes( ',' ) && ( 176 213 <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 } 184 235 /> 185 ) }236 ) } 186 237 <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 } 191 256 /> 192 257 </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' ) } 213 278 /> 214 279 </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 > 217 284 <RangeControl 218 285 __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 } 224 293 required 225 294 /> 226 295 </PanelBody> 227 ) }296 ) } 228 297 </InspectorControls> 229 <div { ...blockProps}>298 <div { ...blockProps }> 230 299 <Disabled> 231 <ServerSideRender block="dss/jobbnorge" attributes={attributes} httpMethod="POST" /> 300 <ServerSideRender 301 block="dss/jobbnorge" 302 attributes={ attributes } 303 httpMethod="POST" 304 /> 232 305 </Disabled> 233 306 </div> -
jobbnorge-block/tags/2.2.3/src/editor.scss
r3322139 r3373293 5 5 6 6 @mixin break-medium() { 7 7 8 @media (min-width: #{ ($break-medium) }) { 8 9 @content; … … 11 12 12 13 @mixin break-small() { 14 13 15 @media (min-width: #{ ($break-small) }) { 14 16 @content; … … 16 18 } 17 19 18 .wp-block-dss-jobbnorge li a >div {20 .wp-block-dss-jobbnorge li a > div { 19 21 display: inline; 20 22 } … … 29 31 30 32 @include break-medium() { 33 31 34 >* { 32 35 margin-bottom: 0; … … 75 78 76 79 @include break-small { 80 77 81 @for $i from 2 through 6 { 78 82 &.columns-#{ $i } li { -
jobbnorge-block/tags/2.2.3/src/index.js
r2997962 r3373293 2 2 * WordPress dependencies 3 3 */ 4 import { people as icon } from "@wordpress/icons";5 import { registerBlockType } from "@wordpress/blocks";4 import { people as icon } from '@wordpress/icons'; 5 import { registerBlockType } from '@wordpress/blocks'; 6 6 /** 7 7 * Internal dependencies 8 8 */ 9 import "./style.scss";10 import metadata from "./block.json";11 import edit from "./edit";9 import './style.scss'; 10 import metadata from './block.json'; 11 import edit from './edit'; 12 12 13 13 const { name } = metadata; … … 19 19 example: { 20 20 attributes: { 21 employerID: "123[, 456, 789]",21 employerID: '123[, 456, 789]', 22 22 }, 23 23 }, … … 25 25 }; 26 26 27 const initBlock = (block) => { 28 // if (!block) { 29 // return; 30 // } 31 const { metadata, settings, name } = block; 32 return registerBlockType({ name, ...metadata }, settings); 27 const 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 ); 33 31 }; 34 32 35 export const init = () => initBlock( { name, metadata, settings });33 export const init = () => initBlock( { name, metadata, settings } ); -
jobbnorge-block/tags/2.2.3/src/pagination.js
r3322310 r3373293 1 1 /** 2 2 * Jobbnorge Block Pagination JavaScript 3 * 3 * 4 4 * Handles AJAX pagination for job listings. 5 5 */ 6 6 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 2 2 3 3 @mixin break-small() { 4 4 5 @media (min-width: #{ ($break-small) }) { 5 6 @content; … … 13 14 padding: 0; 14 15 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 16 18 &.wp-block-dss-jobbnorge { 17 19 box-sizing: border-box; … … 19 21 20 22 &.alignleft { 23 21 24 /*rtl:ignore*/ 22 25 margin-right: 2em; … … 24 27 25 28 &.alignright { 29 26 30 /*rtl:ignore*/ 27 31 margin-left: 2em; … … 45 49 46 50 @include break-small { 51 47 52 @for $i from 2 through 6 { 48 53 &.columns-#{ $i } li { … … 75 80 // Pagination styles 76 81 .wp-block-dss-jobbnorge { 82 77 83 &__pagination { 78 84 display: flex; … … 104 110 padding: 0.5rem 1rem; 105 111 border: 1px solid #ddd; 106 background: white;112 background: #fff; // replaced named color per stylelint 107 113 cursor: pointer; 108 114 border-radius: 4px; … … 110 116 transition: all 0.2s ease; 111 117 118 &:disabled { 119 opacity: 0.5; 120 cursor: not-allowed; 121 } 122 112 123 &:hover:not(:disabled) { 113 124 background: #f5f5f5; 114 125 border-color: #999; 115 }116 117 &:disabled {118 opacity: 0.5;119 cursor: not-allowed;120 126 } 121 127 } … … 134 140 135 141 &::after { 136 content: '';142 content: ""; 137 143 position: absolute; 138 144 top: 50%; … … 164 170 165 171 @keyframes spin { 172 166 173 to { 167 174 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');1 const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); 2 const path = require( 'path' ); 3 3 4 4 module.exports = { 5 5 ...defaultConfig, 6 6 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' ), 11 11 }, 12 12 }; -
jobbnorge-block/tags/2.2.3/wp-jobb-norge.php
r3330462 r3373293 4 4 * Plugin URI: https://wordpress.org/plugins/jobbnorge-block/ 5 5 * Description: Retrieve and display job listings from Jobbnorge.no 6 * Requires at least: 5.97 * Requires PHP: 7.08 * Version: 2.2. 26 * Requires at least: 6.5 7 * Requires PHP: 8.2 8 * Version: 2.2.3 9 9 * Author: PerS 10 10 * License: GPL-2.0-or-later 11 11 * License URI: https://www.gnu.org/licenses/gpl-2.0.html 12 12 * Text Domain: wp-jobbnorge-block 13 *14 13 * @package wp-jobbnorge-block 15 14 */ 16 15 17 16 namespace DSS\Jobbnorge; 17 18 if ( ! defined( 'ABSPATH' ) ) { 19 exit; // Safety. 20 } 21 22 if ( ! defined( 'WP_JOBBNORGE_VERSION' ) ) { 23 define( 'WP_JOBBNORGE_VERSION', '2.2.3' ); 24 } 18 25 19 26 if ( ! \class_exists( 'Jobbnorge_CacheHandler' ) ) { … … 21 28 } 22 29 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. 30 add_action( 'init', __NAMESPACE__ . '\\dss_jobbnorge_init' ); 31 32 /** 33 * Init: register block + i18n + enqueue hooks. 34 */ 35 function dss_jobbnorge_init(): void { 45 36 load_plugin_textdomain( 'wp-jobbnorge-block', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); 46 37 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. 63 47 */ 64 48 function 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 ) ) { 69 50 return; 70 51 } 71 72 // Define the path to the dependencies file.73 52 $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; 80 55 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' ] ); 85 58 $version = $file[ 'version' ]; 86 59 } 87 88 // Check if the current view is the admin dashboard.89 60 if ( is_admin() ) { 90 // If it is, register and enqueue a CSS file for the admin view.91 61 wp_register_style( 'dss-jobbnorge-admin', plugin_dir_url( __FILE__ ) . 'build/init.css', [], $version ); 92 62 wp_enqueue_style( 'dss-jobbnorge-admin' ); 93 63 } 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/' ); 103 65 $employers = apply_filters( 'jobbnorge_employers', false ); 104 105 // Proceed with localization if employers is not false.106 66 if ( false !== $employers ) { 107 // Ensure employers is an array.108 67 if ( ! is_array( $employers ) ) { 109 68 $employers = []; 110 69 } 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. 127 76 */ 128 77 function dss_jobbnorge_enqueue_frontend_styles(): void { 129 // Define the path to the dependencies file.130 78 $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; 136 80 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 139 82 $version = $file[ 'version' ]; 140 83 } 141 142 // Register and enqueue a CSS file for the public view.143 84 wp_register_style( 'dss-jobbnorge', plugin_dir_url( __FILE__ ) . 'build/style-init.css', [], $version ); 144 85 wp_enqueue_style( 'dss-jobbnorge' ); … … 146 87 147 88 /** 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 */ 91 function 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' ] ); 196 125 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 ); 204 131 $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 ); 206 133 $response_data = $cache->get( $cache_key, $expiration ); 207 208 134 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 } 213 206 } 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 } 227 213 $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 ); 243 221 } 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( [ 305 235 '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', 307 238 ] ); 308 239 309 // Generate the ul classes (including grid classes)310 240 $ul_classes = [ 'wp-block-dss-jobbnorge' ]; 311 241 if ( 'grid' === $attributes[ 'blockLayout' ] ) { 312 242 $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 317 283 $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 ); 324 289 } 325 290 … … 331 296 * @return string The formatted excerpt. 332 297 */ 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; 298 function 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 ); 353 306 } 354 307 … … 390 343 * @return string The formatted deadline date. 391 344 */ 392 function format_deadline( $deadline_date ) { 393 // If there's no deadline date, return an empty string. 345 function format_deadline( $deadline_date ): string { 394 346 if ( ! $deadline_date ) { 395 347 return ''; 396 348 } 397 398 349 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 ); 402 351 $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) { 405 353 $str_date = $deadline_date; 406 354 $date = false; 407 355 } 408 409 // If there's a formatted date, return a time element with the date.410 356 if ( $str_date ) { 411 357 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 ) 419 362 ); 420 363 } 421 422 // If there's no formatted date, return an empty string.423 364 return ''; 424 365 } … … 469 410 function parse_date_fallback( $deadline_date ) { 470 411 // Define an array of month names in Norwegian. 471 $str_months = [ 412 $str_months = [ 472 413 'januar', 473 414 'februar', … … 485 426 486 427 // Define an array of month numbers. 487 $num_months = [ 428 $num_months = [ 488 429 '01', 489 430 '02', … … 617 558 * Register AJAX endpoints for pagination. 618 559 */ 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' );560 add_action( 'wp_ajax_jobbnorge_get_jobs', __NAMESPACE__ . '\\handle_ajax_get_jobs' ); 561 add_action( 'wp_ajax_nopriv_jobbnorge_get_jobs', __NAMESPACE__ . '\\handle_ajax_get_jobs' ); 621 562 622 563 /** 623 564 * Handle AJAX request for paginated job listings. 624 565 */ 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 566 function 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 ); 647 578 wp_send_json_success( [ 'html' => $html ] ); 648 579 } … … 685 616 'jobbnorge-pagination', 686 617 'jobbnorgeAjax', 687 [ 618 [ 688 619 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 689 620 'nonce' => wp_create_nonce( 'jobbnorge_pagination_nonce' ), … … 693 624 694 625 // 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 1 1 # 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. 2 7 3 8 ## 2.2.2 -
jobbnorge-block/trunk/README.md
r3322139 r3373293 91 91 ### 2) Modify the block settings. 92 92 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. 93 96 - Set the number of jobs to display. 94 97 - Set the no jobs message. -
jobbnorge-block/trunk/build/block.json
r3330462 r3373293 3 3 "apiVersion": 2, 4 4 "name": "dss/jobbnorge", 5 "version": "2.2. 2",5 "version": "2.2.3", 6 6 "title": "Jobbnorge", 7 7 "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)}}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: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 1 1 { 2 2 "name": "jobbnorge-block", 3 "version": "2.2. 2",3 "version": "2.2.3", 4 4 "lockfileVersion": 3, 5 5 "requires": true, … … 7 7 "": { 8 8 "name": "jobbnorge-block", 9 "version": "2.2. 2",9 "version": "2.2.3", 10 10 "license": "GPL-2.0-or-later", 11 11 "dependencies": { -
jobbnorge-block/trunk/package.json
r3330462 r3373293 1 1 { 2 2 "name": "jobbnorge-block", 3 "version": "2.2. 2",3 "version": "2.2.3", 4 4 "description": "Jobbnorge Block for WordPress Gutenberg", 5 5 "author": "Per Søderlind <[email protected]>", -
jobbnorge-block/trunk/readme.txt
r3330470 r3373293 5 5 Requires at least: 6.5 6 6 Requires PHP: 8.2 7 Stable tag: 2.2. 27 Stable tag: 2.2.3 8 8 License: GPL-2.0-or-later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 105 105 == Changelog == 106 106 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 107 112 = 2.2.2 = 108 113 * Update block.json to include default value and role for employerID -
jobbnorge-block/trunk/src/block.json
r3330462 r3373293 3 3 "apiVersion": 2, 4 4 "name": "dss/jobbnorge", 5 "version": "2.2. 2",5 "version": "2.2.3", 6 6 "title": "Jobbnorge", 7 7 "category": "widgets", -
jobbnorge-block/trunk/src/edit.js
r3322139 r3373293 2 2 * WordPress dependencies 3 3 */ 4 import { BlockControls, InspectorControls, useBlockProps } from '@wordpress/block-editor'; 4 import { 5 BlockControls, 6 InspectorControls, 7 useBlockProps, 8 } from '@wordpress/block-editor'; 5 9 import { 6 10 Button, … … 26 30 const DEFAULT_MAX_ITEMS = 100; 27 31 32 /* eslint-disable jsdoc/check-line-alignment */ 28 33 /** 29 * Description placeholder 30 * @date 17/11/2023 - 16:21:26 34 * Jobbnorge block editor component. 31 35 * 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. 37 40 */ 38 export default function JobbnorgeEdit({ attributes, setAttributes }) { 41 /* eslint-enable jsdoc/check-line-alignment */ 42 export default function JobbnorgeEdit( { attributes, setAttributes } ) { 39 43 // 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 ); 41 45 42 46 // Destructure the attributes object to get the individual attributes. … … 58 62 // Define a function to toggle an attribute. 59 63 // 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 ) { 61 65 return () => { 62 const value = attributes[ propName];63 64 setAttributes( { [propName]: !value });66 const value = attributes[ propName ]; 67 68 setAttributes( { [ propName ]: ! value } ); 65 69 }; 66 70 } … … 68 72 // Define a function to handle the form submission. 69 73 // This function will set the employerID attribute and set isEditing to false. 70 function onSubmitURL( event) {74 function onSubmitURL( event ) { 71 75 event.preventDefault(); 72 76 73 if ( employerID) {74 setAttributes( { employerID: employerID });75 setIsEditing( false);77 if ( employerID ) { 78 setAttributes( { employerID } ); 79 setIsEditing( false ); 76 80 } 77 81 } … … 79 83 const blockProps = useBlockProps(); 80 84 81 if ( isEditing) {85 if ( isEditing ) { 82 86 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 ? ( 87 95 <SelectControl 88 96 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 ) => ( { 92 106 label: o.label, 93 107 value: o.value, 94 108 disabled: o?.disabled ?? false, 95 } ))}109 } ) ) } 96 110 className="wp-block-dss-jobbnorge__placeholder-input" 97 help={ __(111 help={ __( 98 112 'Select employers to display. Ctrl-click (Windows) or Cmd-click (Mac) to select multiple employers. Shift-click to select a range of employers.', 99 113 'wp-jobbnorge-block' 100 ) }114 ) } 101 115 __nextHasNoMarginBottom 102 116 /> 103 117 ) : ( 104 118 <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 } 108 127 className="wp-block-dss-jobbnorge__placeholder-input" 109 128 /> 110 ) }129 ) } 111 130 <Button variant="primary" type="submit"> 112 { __('Save', 'wp-jobbnorge-block')}131 { __( 'Save', 'wp-jobbnorge-block' ) } 113 132 </Button> 114 133 </form> … … 121 140 { 122 141 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 ), 125 144 }, 126 145 { 127 146 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' } ), 130 149 isActive: blockLayout === 'list', 131 150 }, 132 151 { 133 152 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' } ), 136 155 isActive: blockLayout === 'grid', 137 156 }, … … 141 160 <> 142 161 <BlockControls> 143 <ToolbarGroup controls={ toolbarControls} />162 <ToolbarGroup controls={ toolbarControls } /> 144 163 </BlockControls> 145 164 <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 && ( 154 181 <RangeControl 155 182 __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 } 161 193 required 162 194 /> 163 ) }164 { enablePagination && (195 ) } 196 { enablePagination && ( 165 197 <RangeControl 166 198 __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 } 172 209 required 173 210 /> 174 ) }175 { employerID.includes(',') && (211 ) } 212 { employerID.includes( ',' ) && ( 176 213 <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 } 184 235 /> 185 ) }236 ) } 186 237 <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 } 191 256 /> 192 257 </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' ) } 213 278 /> 214 279 </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 > 217 284 <RangeControl 218 285 __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 } 224 293 required 225 294 /> 226 295 </PanelBody> 227 ) }296 ) } 228 297 </InspectorControls> 229 <div { ...blockProps}>298 <div { ...blockProps }> 230 299 <Disabled> 231 <ServerSideRender block="dss/jobbnorge" attributes={attributes} httpMethod="POST" /> 300 <ServerSideRender 301 block="dss/jobbnorge" 302 attributes={ attributes } 303 httpMethod="POST" 304 /> 232 305 </Disabled> 233 306 </div> -
jobbnorge-block/trunk/src/editor.scss
r3322139 r3373293 5 5 6 6 @mixin break-medium() { 7 7 8 @media (min-width: #{ ($break-medium) }) { 8 9 @content; … … 11 12 12 13 @mixin break-small() { 14 13 15 @media (min-width: #{ ($break-small) }) { 14 16 @content; … … 16 18 } 17 19 18 .wp-block-dss-jobbnorge li a >div {20 .wp-block-dss-jobbnorge li a > div { 19 21 display: inline; 20 22 } … … 29 31 30 32 @include break-medium() { 33 31 34 >* { 32 35 margin-bottom: 0; … … 75 78 76 79 @include break-small { 80 77 81 @for $i from 2 through 6 { 78 82 &.columns-#{ $i } li { -
jobbnorge-block/trunk/src/index.js
r2997962 r3373293 2 2 * WordPress dependencies 3 3 */ 4 import { people as icon } from "@wordpress/icons";5 import { registerBlockType } from "@wordpress/blocks";4 import { people as icon } from '@wordpress/icons'; 5 import { registerBlockType } from '@wordpress/blocks'; 6 6 /** 7 7 * Internal dependencies 8 8 */ 9 import "./style.scss";10 import metadata from "./block.json";11 import edit from "./edit";9 import './style.scss'; 10 import metadata from './block.json'; 11 import edit from './edit'; 12 12 13 13 const { name } = metadata; … … 19 19 example: { 20 20 attributes: { 21 employerID: "123[, 456, 789]",21 employerID: '123[, 456, 789]', 22 22 }, 23 23 }, … … 25 25 }; 26 26 27 const initBlock = (block) => { 28 // if (!block) { 29 // return; 30 // } 31 const { metadata, settings, name } = block; 32 return registerBlockType({ name, ...metadata }, settings); 27 const 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 ); 33 31 }; 34 32 35 export const init = () => initBlock( { name, metadata, settings });33 export const init = () => initBlock( { name, metadata, settings } ); -
jobbnorge-block/trunk/src/pagination.js
r3322310 r3373293 1 1 /** 2 2 * Jobbnorge Block Pagination JavaScript 3 * 3 * 4 4 * Handles AJAX pagination for job listings. 5 5 */ 6 6 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 2 2 3 3 @mixin break-small() { 4 4 5 @media (min-width: #{ ($break-small) }) { 5 6 @content; … … 13 14 padding: 0; 14 15 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 16 18 &.wp-block-dss-jobbnorge { 17 19 box-sizing: border-box; … … 19 21 20 22 &.alignleft { 23 21 24 /*rtl:ignore*/ 22 25 margin-right: 2em; … … 24 27 25 28 &.alignright { 29 26 30 /*rtl:ignore*/ 27 31 margin-left: 2em; … … 45 49 46 50 @include break-small { 51 47 52 @for $i from 2 through 6 { 48 53 &.columns-#{ $i } li { … … 75 80 // Pagination styles 76 81 .wp-block-dss-jobbnorge { 82 77 83 &__pagination { 78 84 display: flex; … … 104 110 padding: 0.5rem 1rem; 105 111 border: 1px solid #ddd; 106 background: white;112 background: #fff; // replaced named color per stylelint 107 113 cursor: pointer; 108 114 border-radius: 4px; … … 110 116 transition: all 0.2s ease; 111 117 118 &:disabled { 119 opacity: 0.5; 120 cursor: not-allowed; 121 } 122 112 123 &:hover:not(:disabled) { 113 124 background: #f5f5f5; 114 125 border-color: #999; 115 }116 117 &:disabled {118 opacity: 0.5;119 cursor: not-allowed;120 126 } 121 127 } … … 134 140 135 141 &::after { 136 content: '';142 content: ""; 137 143 position: absolute; 138 144 top: 50%; … … 164 170 165 171 @keyframes spin { 172 166 173 to { 167 174 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');1 const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); 2 const path = require( 'path' ); 3 3 4 4 module.exports = { 5 5 ...defaultConfig, 6 6 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' ), 11 11 }, 12 12 }; -
jobbnorge-block/trunk/wp-jobb-norge.php
r3330462 r3373293 4 4 * Plugin URI: https://wordpress.org/plugins/jobbnorge-block/ 5 5 * Description: Retrieve and display job listings from Jobbnorge.no 6 * Requires at least: 5.97 * Requires PHP: 7.08 * Version: 2.2. 26 * Requires at least: 6.5 7 * Requires PHP: 8.2 8 * Version: 2.2.3 9 9 * Author: PerS 10 10 * License: GPL-2.0-or-later 11 11 * License URI: https://www.gnu.org/licenses/gpl-2.0.html 12 12 * Text Domain: wp-jobbnorge-block 13 *14 13 * @package wp-jobbnorge-block 15 14 */ 16 15 17 16 namespace DSS\Jobbnorge; 17 18 if ( ! defined( 'ABSPATH' ) ) { 19 exit; // Safety. 20 } 21 22 if ( ! defined( 'WP_JOBBNORGE_VERSION' ) ) { 23 define( 'WP_JOBBNORGE_VERSION', '2.2.3' ); 24 } 18 25 19 26 if ( ! \class_exists( 'Jobbnorge_CacheHandler' ) ) { … … 21 28 } 22 29 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. 30 add_action( 'init', __NAMESPACE__ . '\\dss_jobbnorge_init' ); 31 32 /** 33 * Init: register block + i18n + enqueue hooks. 34 */ 35 function dss_jobbnorge_init(): void { 45 36 load_plugin_textdomain( 'wp-jobbnorge-block', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); 46 37 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. 63 47 */ 64 48 function 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 ) ) { 69 50 return; 70 51 } 71 72 // Define the path to the dependencies file.73 52 $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; 80 55 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' ] ); 85 58 $version = $file[ 'version' ]; 86 59 } 87 88 // Check if the current view is the admin dashboard.89 60 if ( is_admin() ) { 90 // If it is, register and enqueue a CSS file for the admin view.91 61 wp_register_style( 'dss-jobbnorge-admin', plugin_dir_url( __FILE__ ) . 'build/init.css', [], $version ); 92 62 wp_enqueue_style( 'dss-jobbnorge-admin' ); 93 63 } 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/' ); 103 65 $employers = apply_filters( 'jobbnorge_employers', false ); 104 105 // Proceed with localization if employers is not false.106 66 if ( false !== $employers ) { 107 // Ensure employers is an array.108 67 if ( ! is_array( $employers ) ) { 109 68 $employers = []; 110 69 } 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. 127 76 */ 128 77 function dss_jobbnorge_enqueue_frontend_styles(): void { 129 // Define the path to the dependencies file.130 78 $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; 136 80 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 139 82 $version = $file[ 'version' ]; 140 83 } 141 142 // Register and enqueue a CSS file for the public view.143 84 wp_register_style( 'dss-jobbnorge', plugin_dir_url( __FILE__ ) . 'build/style-init.css', [], $version ); 144 85 wp_enqueue_style( 'dss-jobbnorge' ); … … 146 87 147 88 /** 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 */ 91 function 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' ] ); 196 125 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 ); 204 131 $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 ); 206 133 $response_data = $cache->get( $cache_key, $expiration ); 207 208 134 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 } 213 206 } 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 } 227 213 $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 ); 243 221 } 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( [ 305 235 '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', 307 238 ] ); 308 239 309 // Generate the ul classes (including grid classes)310 240 $ul_classes = [ 'wp-block-dss-jobbnorge' ]; 311 241 if ( 'grid' === $attributes[ 'blockLayout' ] ) { 312 242 $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 317 283 $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 ); 324 289 } 325 290 … … 331 296 * @return string The formatted excerpt. 332 297 */ 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; 298 function 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 ); 353 306 } 354 307 … … 390 343 * @return string The formatted deadline date. 391 344 */ 392 function format_deadline( $deadline_date ) { 393 // If there's no deadline date, return an empty string. 345 function format_deadline( $deadline_date ): string { 394 346 if ( ! $deadline_date ) { 395 347 return ''; 396 348 } 397 398 349 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 ); 402 351 $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) { 405 353 $str_date = $deadline_date; 406 354 $date = false; 407 355 } 408 409 // If there's a formatted date, return a time element with the date.410 356 if ( $str_date ) { 411 357 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 ) 419 362 ); 420 363 } 421 422 // If there's no formatted date, return an empty string.423 364 return ''; 424 365 } … … 469 410 function parse_date_fallback( $deadline_date ) { 470 411 // Define an array of month names in Norwegian. 471 $str_months = [ 412 $str_months = [ 472 413 'januar', 473 414 'februar', … … 485 426 486 427 // Define an array of month numbers. 487 $num_months = [ 428 $num_months = [ 488 429 '01', 489 430 '02', … … 617 558 * Register AJAX endpoints for pagination. 618 559 */ 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' );560 add_action( 'wp_ajax_jobbnorge_get_jobs', __NAMESPACE__ . '\\handle_ajax_get_jobs' ); 561 add_action( 'wp_ajax_nopriv_jobbnorge_get_jobs', __NAMESPACE__ . '\\handle_ajax_get_jobs' ); 621 562 622 563 /** 623 564 * Handle AJAX request for paginated job listings. 624 565 */ 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 566 function 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 ); 647 578 wp_send_json_success( [ 'html' => $html ] ); 648 579 } … … 685 616 'jobbnorge-pagination', 686 617 'jobbnorgeAjax', 687 [ 618 [ 688 619 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 689 620 'nonce' => wp_create_nonce( 'jobbnorge_pagination_nonce' ), … … 693 624 694 625 // 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.