Startpunkten för version 1.0
This commit is contained in:
8
block.json
Normal file
8
block.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"apiVersion": 2,
|
||||||
|
"name": "hv/schema-block",
|
||||||
|
"title": "HV Schema",
|
||||||
|
"category": "widgets",
|
||||||
|
"icon": "calendar",
|
||||||
|
"editorScript": "hv-schema-editor"
|
||||||
|
}
|
||||||
109
crille-schema.php
Normal file
109
crille-schema.php
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Crille Schema Viewer
|
||||||
|
* Description: Visar kommande schemahändelser från Högskolan Västs KronoX-system via server-side ICS-hämtning. Pluginet har en egen inställningssida där du kan välja vilken signatur (resurskod) schemat ska hämtas för samt ange hur många kommande schemaposter som ska visas.
|
||||||
|
* Version: 1.1
|
||||||
|
* Author: Christian Ohlsson
|
||||||
|
*/
|
||||||
|
|
||||||
|
// === SETTINGS REGISTRATION ===
|
||||||
|
add_action('admin_init', function(){
|
||||||
|
|
||||||
|
// Signatur / resurser (t.ex. s.COH)
|
||||||
|
register_setting('hv_schema_settings_group', 'hv_schema_signatur');
|
||||||
|
|
||||||
|
// Antal poster att visa
|
||||||
|
register_setting('hv_schema_settings_group', 'hv_schema_antal');
|
||||||
|
});
|
||||||
|
|
||||||
|
// === ADMIN MENU PAGE ===
|
||||||
|
add_action('admin_menu', function(){
|
||||||
|
add_options_page(
|
||||||
|
'HV Schema Inställningar',
|
||||||
|
'HV Schema',
|
||||||
|
'manage_options',
|
||||||
|
'hv-schema-settings',
|
||||||
|
'hv_schema_settings_page'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function hv_schema_register_block(){
|
||||||
|
wp_register_script('hv-schema-editor',plugins_url('src/block.js',__FILE__),['wp-blocks','wp-element'],true);
|
||||||
|
wp_register_script('hv-schema-frontend',plugins_url('src/frontend.js',__FILE__),['wp-api-fetch'],true);
|
||||||
|
wp_register_style('hv-schema-style',plugins_url('css/style.css',__FILE__));
|
||||||
|
register_block_type(__DIR__,['render_callback'=>'hv_schema_render']);
|
||||||
|
}
|
||||||
|
add_action('init','hv_schema_register_block');
|
||||||
|
add_action('wp_enqueue_scripts',function(){ wp_enqueue_script('hv-schema-frontend'); wp_enqueue_style('hv-schema-style');});
|
||||||
|
|
||||||
|
function hv_schema_render(){ return '<div id="hv-schema"></div>'; }
|
||||||
|
|
||||||
|
// server-side ICS fetch
|
||||||
|
add_action('rest_api_init', function(){
|
||||||
|
register_rest_route('hv/v1','/schema',[ 'methods'=>'GET', 'callback'=>'hv_schema_fetch' ]);
|
||||||
|
});
|
||||||
|
|
||||||
|
function hv_schema_fetch(){
|
||||||
|
|
||||||
|
// Hämta settings
|
||||||
|
$signatur = get_option('hv_schema_signatur', 's.COH');
|
||||||
|
$antal = get_option('hv_schema_antal', 7);
|
||||||
|
|
||||||
|
// Bygg ICS-URL dynamiskt
|
||||||
|
$url = 'https://schema.hv.se/setup/jsp/SchemaICAL.ics'
|
||||||
|
. '?startDatum=idag'
|
||||||
|
. '&intervallTyp=m'
|
||||||
|
. '&intervallAntal=6'
|
||||||
|
. '&sprak=SV'
|
||||||
|
. '&sokMedAND=true'
|
||||||
|
. '&forklaringar=true'
|
||||||
|
. '&resurser=' . urlencode($signatur);
|
||||||
|
|
||||||
|
$res = wp_remote_get($url);
|
||||||
|
if(is_wp_error($res)) return [];
|
||||||
|
|
||||||
|
// Skicka både ICS OCH antal
|
||||||
|
return [
|
||||||
|
'ics' => wp_remote_retrieve_body($res),
|
||||||
|
'antal' => intval($antal)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hv_schema_settings_page(){ ?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>HV Schema – Inställningar</h1>
|
||||||
|
<form method="post" action="options.php">
|
||||||
|
|
||||||
|
<?php settings_fields('hv_schema_settings_group'); ?>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr valign="top">
|
||||||
|
<th scope="row">Signatur / Resurskod</th>
|
||||||
|
<td>
|
||||||
|
<input type="text"
|
||||||
|
name="hv_schema_signatur"
|
||||||
|
value="<?php echo esc_attr(get_option('hv_schema_signatur', 's.COH')); ?>"
|
||||||
|
style="width:200px;">
|
||||||
|
<p class="description">Exempel: s.COH (din signatur)</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr valign="top">
|
||||||
|
<th scope="row">Antal poster</th>
|
||||||
|
<td>
|
||||||
|
<input type="number"
|
||||||
|
name="hv_schema_antal"
|
||||||
|
value="<?php echo esc_attr(get_option('hv_schema_antal', 7)); ?>"
|
||||||
|
min="1" max="30" style="width:80px;">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<?php submit_button(); ?>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php }
|
||||||
|
|
||||||
|
?>
|
||||||
25
css/style.css
Normal file
25
css/style.css
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#hv-schema {
|
||||||
|
padding: 10px;
|
||||||
|
font-family: Arial;
|
||||||
|
}
|
||||||
|
.hv-item {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.hv-kurs {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #003366;
|
||||||
|
}
|
||||||
|
.hv-moment {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.hv-datum,
|
||||||
|
.hv-tid {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.hv-lokal {
|
||||||
|
color: #660000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
11
src/block.js
Normal file
11
src/block.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
(function (blocks, element) {
|
||||||
|
var el = element.createElement;
|
||||||
|
blocks.registerBlockType("hv/schema-block", {
|
||||||
|
edit: function () {
|
||||||
|
return el("p", {}, "Crille Schema-block (frontend visar data).");
|
||||||
|
},
|
||||||
|
save: function () {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})(window.wp.blocks, window.wp.element);
|
||||||
169
src/frontend.js
Normal file
169
src/frontend.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const el = document.getElementById("hv-schema");
|
||||||
|
if (!el) return;
|
||||||
|
wp.apiFetch({ path: "/hv/v1/schema" }).then((raw) => {
|
||||||
|
const events = parseICS(raw.ics).slice(0, raw.antal);
|
||||||
|
el.innerHTML = events
|
||||||
|
.map(
|
||||||
|
(ev) => `
|
||||||
|
<div class='hv-item'>
|
||||||
|
<div class='hv-kurs'>${ev.kurs}</div>
|
||||||
|
<div class='hv-moment'>${ev.moment}</div>
|
||||||
|
<div class='hv-datum'>${ev.datum}</div>
|
||||||
|
<div class='hv-tid'>${ev.tid}</div>
|
||||||
|
<div class='hv-lokal'>${ev.lokal}</div>
|
||||||
|
</div>`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
function parseICS(data) {
|
||||||
|
const lines = data.split(/\r?\n/);
|
||||||
|
let ev = [],
|
||||||
|
cur = null;
|
||||||
|
|
||||||
|
for (const l of lines) {
|
||||||
|
if (l.startsWith("BEGIN:VEVENT")) cur = {};
|
||||||
|
|
||||||
|
if (l.startsWith("SUMMARY:")) {
|
||||||
|
const raw = l.replace("SUMMARY:", "").trim();
|
||||||
|
|
||||||
|
//
|
||||||
|
// ======== KURS =========
|
||||||
|
//
|
||||||
|
let kurs = "";
|
||||||
|
let kursMatch = raw.match(/Kurs\.grp:\s*([^:]+?)(?:\s+Sign:|$)/);
|
||||||
|
|
||||||
|
if (kursMatch) {
|
||||||
|
kurs = kursMatch[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Ta bort dubletter: "A A" → "A"
|
||||||
|
kurs = kurs.replace(/^(.*)\s+\1$/, "$1").trim();
|
||||||
|
|
||||||
|
cur.kurs = kurs;
|
||||||
|
|
||||||
|
//
|
||||||
|
// ======== MOMENT =========
|
||||||
|
//
|
||||||
|
let moment = "";
|
||||||
|
let momentMatch = raw.match(/Moment:\s*(.+?)(?:\s+Aktivitetstyp:|$)/);
|
||||||
|
|
||||||
|
if (momentMatch) {
|
||||||
|
moment = momentMatch[1].trim();
|
||||||
|
} else {
|
||||||
|
// fallback
|
||||||
|
const parts = raw.split(" - ");
|
||||||
|
if (parts[1]) {
|
||||||
|
moment = parts[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rensa "Moment:" om det ligger kvar
|
||||||
|
moment = moment.replace(/^Moment:\s*/i, "").trim();
|
||||||
|
|
||||||
|
cur.moment = moment;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// ======== DATUM & TID =========
|
||||||
|
//
|
||||||
|
if (l.startsWith("DTSTART:")) {
|
||||||
|
const dt = l.replace("DTSTART:", "");
|
||||||
|
cur.datum = dt.substring(0, 4) + "-" + dt.substring(4, 6) + "-" + dt.substring(6, 8);
|
||||||
|
cur.tid = dt.substring(9, 11) + ":" + dt.substring(11, 13);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// ======== LOKAL =========
|
||||||
|
//
|
||||||
|
if (l.startsWith("LOCATION:")) cur.lokal = l.replace("LOCATION:", "").trim();
|
||||||
|
|
||||||
|
if (l.startsWith("END:VEVENT")) ev.push(cur);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
function parseICS(data) {
|
||||||
|
const lines = data.split(/\r?\n/);
|
||||||
|
let ev = [],
|
||||||
|
cur = null;
|
||||||
|
|
||||||
|
for (const l of lines) {
|
||||||
|
if (l.startsWith("BEGIN:VEVENT")) cur = {};
|
||||||
|
|
||||||
|
if (l.startsWith("SUMMARY:")) {
|
||||||
|
const raw = l.replace("SUMMARY:", "").trim();
|
||||||
|
|
||||||
|
//
|
||||||
|
// ======== KURS =========
|
||||||
|
//
|
||||||
|
let kurs = "";
|
||||||
|
let kursMatch = raw.match(/Kurs\.grp:\s*([^:]+?)(?:\s+Sign:|$)/);
|
||||||
|
|
||||||
|
if (kursMatch) kurs = kursMatch[1].trim();
|
||||||
|
kurs = kurs.replace(/^(.*)\s+\1$/, "$1").trim(); // ta bort dubletter
|
||||||
|
cur.kurs = kurs;
|
||||||
|
|
||||||
|
//
|
||||||
|
// ======== MOMENT =========
|
||||||
|
//
|
||||||
|
let moment = "";
|
||||||
|
let momentMatch = raw.match(/Moment:\s*(.+?)(?:\s+Aktivitetstyp:|$)/);
|
||||||
|
|
||||||
|
if (momentMatch) {
|
||||||
|
moment = momentMatch[1].trim();
|
||||||
|
} else {
|
||||||
|
const parts = raw.split(" - ");
|
||||||
|
if (parts[1]) moment = parts[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
moment = moment.replace(/^Moment:\s*/i, "").trim();
|
||||||
|
cur.moment = moment;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// ======== DATUM & TID (MED SVERIGE-TID) ========
|
||||||
|
//
|
||||||
|
if (l.startsWith("DTSTART:")) {
|
||||||
|
const dt = l.replace("DTSTART:", "");
|
||||||
|
|
||||||
|
// Plocka ut delar ur ICS-datumet
|
||||||
|
const year = Number(dt.substring(0, 4));
|
||||||
|
const month = Number(dt.substring(4, 6));
|
||||||
|
const day = Number(dt.substring(6, 8));
|
||||||
|
const hour = Number(dt.substring(9, 11));
|
||||||
|
const min = Number(dt.substring(11, 13));
|
||||||
|
|
||||||
|
// Skapa en UTC-tid från ICS
|
||||||
|
const dateUTC = new Date(Date.UTC(year, month - 1, day, hour, min));
|
||||||
|
|
||||||
|
// Konvertera till svensk tid
|
||||||
|
const dateLocal = new Date(
|
||||||
|
dateUTC.toLocaleString("sv-SE", { timeZone: "Europe/Stockholm" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Formatera svensk datumtext, t.ex. "2 april 2026"
|
||||||
|
const formattedDate = new Intl.DateTimeFormat("sv-SE", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
}).format(dateLocal);
|
||||||
|
|
||||||
|
// Formatera tiden, t.ex. "13:15"
|
||||||
|
const hh = String(dateLocal.getHours()).padStart(2, "0");
|
||||||
|
const mi = String(dateLocal.getMinutes()).padStart(2, "0");
|
||||||
|
|
||||||
|
cur.datum = formattedDate;
|
||||||
|
cur.tid = `${hh}:${mi}`;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// ======== LOKAL =========
|
||||||
|
//
|
||||||
|
if (l.startsWith("LOCATION:")) cur.lokal = l.replace("LOCATION:", "").trim();
|
||||||
|
|
||||||
|
if (l.startsWith("END:VEVENT")) ev.push(cur);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user