mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 09:11:06 +00:00
feat: minor fixes before update
This commit is contained in:
+42
-6
@@ -561,16 +561,32 @@ ul.magicDelete > li.deleting {
|
|||||||
display: flex !important;
|
display: flex !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
width: calc(100% - 12px) !important;
|
||||||
|
max-width: none !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu.bsplus-sidebar-edit-mode .item.draggable > label {
|
#menu.bsplus-sidebar-edit-mode .item.draggable > label:not(.toggle) {
|
||||||
flex: 1;
|
flex: 1 1 0 !important;
|
||||||
min-width: 0;
|
width: 0 !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
max-width: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#menu.bsplus-sidebar-edit-mode .item.draggable .label {
|
||||||
|
white-space: nowrap !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
|
overflow-wrap: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu.bsplus-sidebar-edit-mode .item.draggable > .toggle,
|
||||||
#menu.bsplus-sidebar-edit-mode .onoffswitch {
|
#menu.bsplus-sidebar-edit-mode .onoffswitch {
|
||||||
pointer-events: auto !important;
|
pointer-events: auto !important;
|
||||||
flex-shrink: 0;
|
flex: 0 0 auto !important;
|
||||||
|
width: auto !important;
|
||||||
|
margin: 0 10px 0 0 !important;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
@@ -655,6 +671,17 @@ html.bsplus-custom-sidebar-pending #menu > .icon-cover,
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extend the logo spacer for the same gap as between items.
|
||||||
|
* SEQTA's #menu>ul::before border-bottom is the hairline under the logo.
|
||||||
|
*/
|
||||||
|
&::before {
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-bottom: none !important;
|
||||||
|
height: calc(69.5px + 8px) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
li.hasChildren {
|
li.hasChildren {
|
||||||
@@ -667,7 +694,7 @@ html.bsplus-custom-sidebar-pending #menu > .icon-cover,
|
|||||||
pointer-events: auto !important;
|
pointer-events: auto !important;
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
max-width: none !important;
|
max-width: none !important;
|
||||||
margin: 2px 6px !important;
|
margin: 0 6px 8px !important;
|
||||||
box-sizing: border-box !important;
|
box-sizing: border-box !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1044,6 +1071,12 @@ ol:has([class*="MessageList__avatar___"] svg) {
|
|||||||
-webkit-box-shadow: 0px 5px 16px 6px rgba(0, 0, 0, 0.2) !important;
|
-webkit-box-shadow: 0px 5px 16px 6px rgba(0, 0, 0, 0.2) !important;
|
||||||
box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, 0.2) !important;
|
box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, 0.2) !important;
|
||||||
}
|
}
|
||||||
|
/* Custom sidebar: keep title depth to the right, not a hairline under the logo into the menu. */
|
||||||
|
body:has(#menu.bsplus-custom-sidebar) #container #content #title,
|
||||||
|
body:has(#menu.bsplus-custom-sidebar).menuShown #container #content #title {
|
||||||
|
-webkit-box-shadow: 6px 0 8px -4px rgba(0, 0, 0, 0.25) !important;
|
||||||
|
box-shadow: 6px 0 8px -4px rgba(0, 0, 0, 0.25) !important;
|
||||||
|
}
|
||||||
#main .timetablepage .quickbar {
|
#main .timetablepage .quickbar {
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
|
||||||
@@ -1279,9 +1312,12 @@ html.transparencyEffects
|
|||||||
.assessmentsWrapper .message {
|
.assessmentsWrapper .message {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#menu li:hover {
|
#menu li:hover:not(.active) {
|
||||||
background: rgba(0, 0, 0, 0.15) !important;
|
background: rgba(0, 0, 0, 0.15) !important;
|
||||||
}
|
}
|
||||||
|
#menu li.active:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.35) !important;
|
||||||
|
}
|
||||||
|
|
||||||
#menu li:focus-visible,
|
#menu li:focus-visible,
|
||||||
#menu section:focus-visible {
|
#menu section:focus-visible {
|
||||||
|
|||||||
@@ -63,9 +63,12 @@
|
|||||||
{#if activeTab === index}
|
{#if activeTab === index}
|
||||||
<div
|
<div
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
class="focus:outline-none w-full h-full min-h-0 pt-2 overflow-y-auto no-scrollbar pb-6 tab active"
|
class="focus:outline-none w-full h-full min-h-0 pt-2 overflow-y-auto no-scrollbar pb-6 tab active relative"
|
||||||
>
|
>
|
||||||
<div class="sticky top-0 w-full h-8 bg-gradient-to-b to-transparent pointer-events-none z-[100] from-white dark:from-zinc-800 dark:to-transparent"></div>
|
<div
|
||||||
|
class="sticky top-0 w-full h-3 -mb-3 bg-gradient-to-b from-white/80 dark:from-zinc-800/80 to-transparent pointer-events-none z-[1]"
|
||||||
|
aria-hidden="true"
|
||||||
|
></div>
|
||||||
<Content {...props} />
|
<Content {...props} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -327,12 +327,6 @@
|
|||||||
if (menuEl) syncCalendarSyncTheme(menuEl);
|
if (menuEl) syncCalendarSyncTheme(menuEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
const host = rootEl?.closest(".timetable-calendar-controls");
|
|
||||||
host?.classList.toggle("bsplus-cal-menu-open", menuOpen);
|
|
||||||
return () => host?.classList.remove("bsplus-cal-menu-open");
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (menuOpen && menuEl) syncHostTheme();
|
if (menuOpen && menuEl) syncHostTheme();
|
||||||
});
|
});
|
||||||
@@ -496,7 +490,7 @@
|
|||||||
>
|
>
|
||||||
<div class="bsplus-cal-menu-header">
|
<div class="bsplus-cal-menu-header">
|
||||||
<span class="bsplus-cal-menu-title">Calendar sync</span>
|
<span class="bsplus-cal-menu-title">Calendar sync</span>
|
||||||
<span class="bsplus-cal-menu-sub">Copy your SEQTA timetable classes to Google or Outlook</span>
|
<span class="bsplus-cal-menu-sub">Copy your SEQTA classes and appointments to Google or Outlook</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{@render providerPanel(
|
{@render providerPanel(
|
||||||
@@ -697,10 +691,14 @@
|
|||||||
min-width: auto;
|
min-width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
margin-left: 4px;
|
margin: 0;
|
||||||
border-radius: 16px !important;
|
border-radius: 16px !important;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
background: var(--bsplus-cal-surface, var(--background-primary, #232323)) !important;
|
||||||
|
transition:
|
||||||
|
transform 0.2s ease,
|
||||||
|
opacity 0.2s ease,
|
||||||
|
background-color 0.2s ease;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
@@ -751,8 +749,13 @@
|
|||||||
transform: scale(0.97);
|
transform: scale(0.97);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-cal-trigger--open {
|
.bsplus-cal-trigger--open,
|
||||||
background: color-mix(in srgb, var(--bsplus-cal-accent, var(--better-main, #3b82f6)) 14%, transparent) !important;
|
.bsplus-cal-trigger--open:hover,
|
||||||
|
.bsplus-cal-trigger--open:active {
|
||||||
|
background: var(
|
||||||
|
--bsplus-cal-surface-muted,
|
||||||
|
color-mix(in srgb, var(--bsplus-cal-text, #fff) 12%, var(--bsplus-cal-surface, #232323))
|
||||||
|
) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-cal-trigger--busy {
|
.bsplus-cal-trigger--busy {
|
||||||
@@ -829,7 +832,7 @@
|
|||||||
|
|
||||||
.bsplus-cal-menu {
|
.bsplus-cal-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: var(--bsplus-cal-z-menu, 2147483646);
|
z-index: var(--bsplus-cal-z-menu, 1200);
|
||||||
width: min(320px, calc(100vw - 24px));
|
width: min(320px, calc(100vw - 24px));
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
@@ -951,7 +954,7 @@
|
|||||||
.bsplus-cal-modal-backdrop {
|
.bsplus-cal-modal-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: var(--bsplus-cal-z-modal, 2147483647);
|
z-index: var(--bsplus-cal-z-modal, 1300);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -1027,7 +1030,7 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
bottom: 16px;
|
bottom: 16px;
|
||||||
z-index: 100000;
|
z-index: var(--bsplus-cal-z-modal, 1300);
|
||||||
max-width: min(360px, calc(100vw - 32px));
|
max-width: min(360px, calc(100vw - 32px));
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|||||||
@@ -1,28 +1,16 @@
|
|||||||
.timetablepage #toolbar {
|
|
||||||
position: relative;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timetable-calendar-controls {
|
.timetable-calendar-controls {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 100001;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
vertical-align: middle;
|
||||||
|
|
||||||
.timetable-calendar-controls.bsplus-cal-menu-open {
|
|
||||||
z-index: 2147483645;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timetablepage #toolbar:has(.bsplus-cal-menu-open) {
|
|
||||||
z-index: 2147483645 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bsplus-cal-z-menu: 2147483646;
|
--bsplus-cal-z-menu: 1200;
|
||||||
--bsplus-cal-z-modal: 2147483647;
|
--bsplus-cal-z-modal: 1300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-calendar-sync-mount {
|
.bsplus-calendar-sync-mount {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,8 +155,18 @@ export async function mountGoogleCalendarButton(): Promise<void> {
|
|||||||
registerCalendarContentHandlers();
|
registerCalendarContentHandlers();
|
||||||
|
|
||||||
const controls = document.createElement("div");
|
const controls = document.createElement("div");
|
||||||
controls.className = `${CONTROLS_CLASS} bsplus-timetable-control`;
|
controls.className = CONTROLS_CLASS;
|
||||||
toolbar.appendChild(controls);
|
|
||||||
|
// Sit between the date selector and zoom controls (same spacing as neighbours).
|
||||||
|
const zoomControls = toolbar.querySelector(".timetable-zoom-controls");
|
||||||
|
const dateButton = toolbar.querySelector(".buttonCalendar, button.calendar");
|
||||||
|
if (zoomControls) {
|
||||||
|
toolbar.insertBefore(controls, zoomControls);
|
||||||
|
} else if (dateButton?.nextSibling) {
|
||||||
|
toolbar.insertBefore(controls, dateButton.nextSibling);
|
||||||
|
} else {
|
||||||
|
toolbar.appendChild(controls);
|
||||||
|
}
|
||||||
|
|
||||||
const mountRoot = document.createElement("div");
|
const mountRoot = document.createElement("div");
|
||||||
mountRoot.className = "bsplus-calendar-sync-mount";
|
mountRoot.className = "bsplus-calendar-sync-mount";
|
||||||
|
|||||||
@@ -530,7 +530,30 @@ function setupSidebarAccessibility() {
|
|||||||
if (!menu) return;
|
if (!menu) return;
|
||||||
|
|
||||||
sidebarAccessibilityObserver?.disconnect();
|
sidebarAccessibilityObserver?.disconnect();
|
||||||
sidebarAccessibilityObserver = new MutationObserver(() => {
|
sidebarAccessibilityObserver = new MutationObserver((mutations) => {
|
||||||
|
// Custom Svelte sidebar owns drill a11y — ignore its DOM (opening Goals/Folios
|
||||||
|
// mutates a lot; re-running here used to help freeze the tab).
|
||||||
|
if (menu.classList.contains("bsplus-custom-sidebar")) {
|
||||||
|
const root = document.getElementById("bsplus-sidebar-root");
|
||||||
|
if (
|
||||||
|
root &&
|
||||||
|
mutations.every(
|
||||||
|
(m) => root === m.target || root.contains(m.target as Node),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Still ignore class/style-only native noise while custom sidebar is on.
|
||||||
|
if (
|
||||||
|
mutations.every(
|
||||||
|
(m) =>
|
||||||
|
m.type === "attributes" &&
|
||||||
|
(m.attributeName === "class" || m.attributeName === "style"),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
scheduleSidebarAccessibilityUpdate();
|
scheduleSidebarAccessibilityUpdate();
|
||||||
});
|
});
|
||||||
sidebarAccessibilityObserver.observe(menu, {
|
sidebarAccessibilityObserver.observe(menu, {
|
||||||
|
|||||||
@@ -97,6 +97,20 @@
|
|||||||
void sidebarState.isDrilling;
|
void sidebarState.isDrilling;
|
||||||
restoreCustomMenuActive();
|
restoreCustomMenuActive();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Drill `.sub` is position:absolute inside this scrollport — if the list was
|
||||||
|
// scrolled down (e.g. Folios/Goals near the bottom), the panel sits under the
|
||||||
|
// logo until we reset. Also reset when going back up the stack.
|
||||||
|
$effect(() => {
|
||||||
|
void sidebarState.drillStack.length;
|
||||||
|
void sidebarState.enterFrameKey;
|
||||||
|
const root = document.getElementById("bsplus-sidebar-root");
|
||||||
|
if (!root) return;
|
||||||
|
root.scrollTop = 0;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
root.scrollTop = 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -212,10 +226,15 @@
|
|||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-label="Back"
|
aria-label="Back"
|
||||||
onclick={() => sidebarState.goBack()}
|
onclick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
sidebarState.goBack();
|
||||||
|
}}
|
||||||
onkeydown={(e) => {
|
onkeydown={(e) => {
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
sidebarState.goBack();
|
sidebarState.goBack();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -303,22 +322,27 @@
|
|||||||
.bsplus-sidebar-edit-actions {
|
.bsplus-sidebar-edit-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 85%;
|
width: calc(100% - 12px);
|
||||||
margin: 12px auto 16px;
|
margin: 12px 6px 16px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-btn {
|
.edit-btn {
|
||||||
flex: 1;
|
flex: 1 1 0;
|
||||||
padding: 10px 12px;
|
min-width: 0;
|
||||||
|
padding: 10px 8px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font: inherit;
|
font-family: inherit;
|
||||||
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
background-color 0.2s ease,
|
background-color 0.2s ease,
|
||||||
|
|||||||
@@ -56,13 +56,18 @@
|
|||||||
ondragstart={() => onDragStart?.(item.key)}
|
ondragstart={() => onDragStart?.(item.key)}
|
||||||
ondragover={(e) => e.preventDefault()}
|
ondragover={(e) => e.preventDefault()}
|
||||||
ondrop={() => onDrop?.(item.key)}
|
ondrop={() => onDrop?.(item.key)}
|
||||||
onclick={() => {
|
onclick={(e) => {
|
||||||
|
// Keep SEQTA's #menu handlers from seeing custom-list clicks — that fights
|
||||||
|
// our drill UI and can freeze the tab (Goals / Folios / etc.).
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
if (!editMode) onActivate(item);
|
if (!editMode) onActivate(item);
|
||||||
}}
|
}}
|
||||||
onkeydown={(e) => {
|
onkeydown={(e) => {
|
||||||
if (editMode) return;
|
if (editMode) return;
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
onActivate(item);
|
onActivate(item);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -118,10 +123,11 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-sidebar-item > label {
|
.bsplus-sidebar-item > label:not(.toggle) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
flex: 1 1 auto;
|
||||||
|
width: auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@@ -129,7 +135,7 @@
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-sidebar-item:hover {
|
.bsplus-sidebar-item:hover:not(.active) {
|
||||||
background: rgba(0, 0, 0, 0.15);
|
background: rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +144,8 @@
|
|||||||
box-shadow: 0 0 0 2px var(--theme-primary, #fff);
|
box-shadow: 0 0 0 2px var(--theme-primary, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-sidebar-item.active:not(.hasChildren) {
|
.bsplus-sidebar-item.active:not(.hasChildren),
|
||||||
|
.bsplus-sidebar-item.active:not(.hasChildren):hover {
|
||||||
background: rgba(0, 0, 0, 0.35);
|
background: rgba(0, 0, 0, 0.35);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
@@ -147,7 +154,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-sidebar-item.compact > label {
|
.bsplus-sidebar-item.compact > label:not(.toggle) {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
@@ -158,6 +165,18 @@
|
|||||||
|
|
||||||
.bsplus-sidebar-item.edit-mode {
|
.bsplus-sidebar-item.edit-mode {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
gap: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsplus-sidebar-item.edit-mode > label:not(.toggle) {
|
||||||
|
flex: 1 1 0;
|
||||||
|
width: 0;
|
||||||
|
min-width: 0;
|
||||||
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-sidebar-item :global(label > svg) {
|
.bsplus-sidebar-item :global(label > svg) {
|
||||||
@@ -171,8 +190,9 @@
|
|||||||
.label {
|
.label {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
white-space: normal;
|
white-space: nowrap;
|
||||||
overflow-wrap: anywhere;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,17 +214,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toggle {
|
.toggle {
|
||||||
margin-right: 12px;
|
margin: 0 10px 0 0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-shrink: 0;
|
flex: 0 0 auto;
|
||||||
|
width: auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle input {
|
.toggle input {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
margin: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
accent-color: var(--theme-primary, #fff);
|
accent-color: var(--theme-primary, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ let menuEl: HTMLElement | null = null;
|
|||||||
let menuObserver: MutationObserver | null = null;
|
let menuObserver: MutationObserver | null = null;
|
||||||
let syncTimer: ReturnType<typeof setTimeout> | null = null;
|
let syncTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let hashListenerAttached = false;
|
let hashListenerAttached = false;
|
||||||
|
let sidebarCaptureAttached = false;
|
||||||
let earlyPrepareStarted = false;
|
let earlyPrepareStarted = false;
|
||||||
let catchupTimer: ReturnType<typeof setInterval> | null = null;
|
let catchupTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
let nativeMenuListenerAttached = false;
|
let nativeMenuListenerAttached = false;
|
||||||
@@ -56,6 +57,44 @@ function scheduleSync() {
|
|||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture-phase: own all clicks inside the custom list so SEQTA's #menu handlers
|
||||||
|
* never see them. Opening Goals/Folios via SEQTA + our drill UI freezes the tab.
|
||||||
|
*/
|
||||||
|
function onCustomSidebarCaptureClick(event: MouseEvent) {
|
||||||
|
if (!menuEl || sidebarState.editMode) return;
|
||||||
|
|
||||||
|
const root = document.getElementById(ROOT_ID);
|
||||||
|
const target = event.target;
|
||||||
|
if (!(target instanceof Element) || !root?.contains(target)) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
|
const back = target.closest(".back");
|
||||||
|
if (back instanceof HTMLElement && root.contains(back)) {
|
||||||
|
sidebarState.goBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const li = target.closest("li.item[data-key]");
|
||||||
|
if (!(li instanceof HTMLElement) || !root.contains(li)) return;
|
||||||
|
|
||||||
|
// Already-open folder chrome (renders its own `.sub`) — ignore; use Back.
|
||||||
|
if (
|
||||||
|
li.classList.contains("hasChildren") &&
|
||||||
|
li.querySelector(":scope > .sub")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = li.dataset.key;
|
||||||
|
if (!key) return;
|
||||||
|
const item = sidebarState.findByKey(key);
|
||||||
|
if (item) sidebarState.activateItem(item, menuEl);
|
||||||
|
}
|
||||||
|
|
||||||
function onHashChange() {
|
function onHashChange() {
|
||||||
sidebarState.syncActiveFromLocation();
|
sidebarState.syncActiveFromLocation();
|
||||||
if (menuEl) clearNativeDrillActive(menuEl);
|
if (menuEl) clearNativeDrillActive(menuEl);
|
||||||
@@ -189,11 +228,14 @@ export async function mountCustomSidebar(): Promise<boolean> {
|
|||||||
menuObserver?.disconnect();
|
menuObserver?.disconnect();
|
||||||
menuObserver = new MutationObserver((mutations) => {
|
menuObserver = new MutationObserver((mutations) => {
|
||||||
const ours = document.getElementById(ROOT_ID);
|
const ours = document.getElementById(ROOT_ID);
|
||||||
// Ignore our list entirely. Also ignore native `class` toggles — SEQTA
|
// Ignore our list entirely. Ignore native class/style churn — SEQTA and
|
||||||
// re-adds drill `.active` after we clear it; syncing on that freezes the tab.
|
// theme transitions rewrite those constantly; syncing on them freezes the tab.
|
||||||
const relevant = mutations.some((m) => {
|
const relevant = mutations.some((m) => {
|
||||||
if (ours?.contains(m.target as Node)) return false;
|
if (ours?.contains(m.target as Node)) return false;
|
||||||
if (m.type === "attributes" && m.attributeName === "class") return false;
|
if (m.type === "attributes") {
|
||||||
|
const attr = m.attributeName;
|
||||||
|
if (attr === "class" || attr === "style") return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
if (!relevant) return;
|
if (!relevant) return;
|
||||||
@@ -203,9 +245,14 @@ export async function mountCustomSidebar(): Promise<boolean> {
|
|||||||
subtree: true,
|
subtree: true,
|
||||||
childList: true,
|
childList: true,
|
||||||
attributes: true,
|
attributes: true,
|
||||||
attributeFilter: ["style", "data-key", "data-path", "data-colour"],
|
attributeFilter: ["data-key", "data-path", "data-colour"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!sidebarCaptureAttached) {
|
||||||
|
document.addEventListener("click", onCustomSidebarCaptureClick, true);
|
||||||
|
sidebarCaptureAttached = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!hashListenerAttached) {
|
if (!hashListenerAttached) {
|
||||||
window.addEventListener("hashchange", onHashChange);
|
window.addEventListener("hashchange", onHashChange);
|
||||||
hashListenerAttached = true;
|
hashListenerAttached = true;
|
||||||
@@ -242,6 +289,11 @@ export function unmountCustomSidebar() {
|
|||||||
hashListenerAttached = false;
|
hashListenerAttached = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sidebarCaptureAttached) {
|
||||||
|
document.removeEventListener("click", onCustomSidebarCaptureClick, true);
|
||||||
|
sidebarCaptureAttached = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (nativeMenuListenerAttached) {
|
if (nativeMenuListenerAttached) {
|
||||||
window.removeEventListener("bsplus-native-menu-updated", syncFromMenu);
|
window.removeEventListener("bsplus-native-menu-updated", syncFromMenu);
|
||||||
nativeMenuListenerAttached = false;
|
nativeMenuListenerAttached = false;
|
||||||
|
|||||||
@@ -43,6 +43,15 @@ function ensureActive(el: Element | null | undefined) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetSidebarScroll() {
|
||||||
|
const root = document.getElementById("bsplus-sidebar-root");
|
||||||
|
if (!(root instanceof HTMLElement)) return;
|
||||||
|
root.scrollTop = 0;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
root.scrollTop = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SEQTA (and some themes) strip `.active` from `#menu li` after navigation.
|
* SEQTA (and some themes) strip `.active` from `#menu li` after navigation.
|
||||||
* Theme decorations and drill `.sub` chrome depend on that class on our list.
|
* Theme decorations and drill `.sub` chrome depend on that class on our list.
|
||||||
@@ -247,17 +256,29 @@ class SidebarState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openFolder(item: SidebarItem) {
|
openFolder(item: SidebarItem, menu?: HTMLElement) {
|
||||||
if (!item.hasChildren) return;
|
if (!item.hasChildren) return;
|
||||||
|
// Ignore duplicate opens (double-firing click / label + li).
|
||||||
|
if (this.drillStack.at(-1)?.key === item.key) return;
|
||||||
|
|
||||||
|
const frame: SidebarDrillFrame = {
|
||||||
|
key: item.key,
|
||||||
|
label: item.label,
|
||||||
|
items: filterVisible(item.children),
|
||||||
|
};
|
||||||
|
const isRoot = this.visibleRootItems.some((entry) => entry.key === item.key);
|
||||||
|
|
||||||
this.enterFrameKey = item.key;
|
this.enterFrameKey = item.key;
|
||||||
this.drillStack = [
|
// Root folders replace the stack; nested folders append.
|
||||||
...this.drillStack,
|
this.drillStack = isRoot ? [frame] : [...this.drillStack, frame];
|
||||||
{
|
|
||||||
key: item.key,
|
// Keep native drill closed so SEQTA CSS :has(> ul > li.hasChildren.active)
|
||||||
label: item.label,
|
// does not lock pointer-events on the custom list.
|
||||||
items: filterVisible(item.children),
|
if (menu) clearNativeDrillActive(menu);
|
||||||
},
|
|
||||||
];
|
// Absolute `.sub` panels live inside the scrollport — jump to top so the
|
||||||
|
// drilled page isn't left under the logo when the list was scrolled down.
|
||||||
|
resetSidebarScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearEnterFrame(key?: string) {
|
clearEnterFrame(key?: string) {
|
||||||
@@ -270,11 +291,13 @@ class SidebarState {
|
|||||||
if (!this.drillStack.length) return;
|
if (!this.drillStack.length) return;
|
||||||
this.enterFrameKey = null;
|
this.enterFrameKey = null;
|
||||||
this.drillStack = this.drillStack.slice(0, -1);
|
this.drillStack = this.drillStack.slice(0, -1);
|
||||||
|
resetSidebarScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
resetDrill() {
|
resetDrill() {
|
||||||
this.enterFrameKey = null;
|
this.enterFrameKey = null;
|
||||||
this.drillStack = [];
|
this.drillStack = [];
|
||||||
|
resetSidebarScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
setEditMode(enabled: boolean) {
|
setEditMode(enabled: boolean) {
|
||||||
@@ -320,7 +343,7 @@ class SidebarState {
|
|||||||
if (this.editMode) return;
|
if (this.editMode) return;
|
||||||
|
|
||||||
if (item.hasChildren) {
|
if (item.hasChildren) {
|
||||||
this.openFolder(item);
|
this.openFolder(item, menu);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const WHATS_NEW_CHANGELOG: WhatsNewRelease[] = [
|
|||||||
"title": "3.7.3 – Timetable sync to Calendar & Bugfix Bundle",
|
"title": "3.7.3 – Timetable sync to Calendar & Bugfix Bundle",
|
||||||
"items": [
|
"items": [
|
||||||
"Added an option in the Timetable to sync to Google Calendar and Outlook Calendar",
|
"Added an option in the Timetable to sync to Google Calendar and Outlook Calendar",
|
||||||
|
"Improved the sidebar to be more stable and performant.",
|
||||||
"Fixed dropdown contrast and readability in settings and across SEQTA pages.",
|
"Fixed dropdown contrast and readability in settings and across SEQTA pages.",
|
||||||
"Fixed Analytics sidebar item not hiding when toggled off in Edit Sidebar.",
|
"Fixed Analytics sidebar item not hiding when toggled off in Edit Sidebar.",
|
||||||
"Fixed timetable subject colour picker not reopening after closing (#221).",
|
"Fixed timetable subject colour picker not reopening after closing (#221).",
|
||||||
@@ -17,7 +18,6 @@ export const WHATS_NEW_CHANGELOG: WhatsNewRelease[] = [
|
|||||||
"Fixed assessment overview showing <code>Undefined%</code> for letter grades (#430).",
|
"Fixed assessment overview showing <code>Undefined%</code> for letter grades (#430).",
|
||||||
"Fixed notifications older than a year being removed; added local per-account archive (#443).",
|
"Fixed notifications older than a year being removed; added local per-account archive (#443).",
|
||||||
"Fixed notices on the home screen sometimes failing to load (#388).",
|
"Fixed notices on the home screen sometimes failing to load (#388).",
|
||||||
"Fixed nightly/CI release builds (Windows runners, zip packaging, layerchart compile).",
|
|
||||||
"Fixed multi-target builds exiting early and masking Vite errors.",
|
"Fixed multi-target builds exiting early and masking Vite errors.",
|
||||||
"Improved background music autoplay with a tap-to-start hint when blocked.",
|
"Improved background music autoplay with a tap-to-start hint when blocked.",
|
||||||
"Added verbose logging toggle under Developer settings.",
|
"Added verbose logging toggle under Developer settings.",
|
||||||
|
|||||||
@@ -61,6 +61,31 @@ describe("lessonToGoogleEvent", () => {
|
|||||||
);
|
);
|
||||||
expect(event?.colorId).toBe("11");
|
expect(event?.colorId).toBe("11");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps appointments with notes", () => {
|
||||||
|
const event = lessonToGoogleEvent(
|
||||||
|
ORIGIN,
|
||||||
|
{
|
||||||
|
date: "2026-07-20",
|
||||||
|
from: "09:30",
|
||||||
|
until: "13:10",
|
||||||
|
description: "Advisor meeting",
|
||||||
|
type: "appointment",
|
||||||
|
calendarid: "event:5",
|
||||||
|
colour: "#ffc107",
|
||||||
|
notes: "Bring forms",
|
||||||
|
},
|
||||||
|
"Australia/Perth",
|
||||||
|
);
|
||||||
|
expect(event).toMatchObject({
|
||||||
|
summary: "Advisor meeting",
|
||||||
|
startDateTime: "2026-07-20T09:30:00",
|
||||||
|
endDateTime: "2026-07-20T13:10:00",
|
||||||
|
seqtaKey: `${ORIGIN}:cal:event:5`,
|
||||||
|
});
|
||||||
|
expect(event?.description).toContain("Type: Appointment");
|
||||||
|
expect(event?.description).toContain("Bring forms");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("googleApiEventBody", () => {
|
describe("googleApiEventBody", () => {
|
||||||
|
|||||||
@@ -50,10 +50,18 @@ export function lessonToGoogleEvent(
|
|||||||
const staff = lesson.staff?.trim();
|
const staff = lesson.staff?.trim();
|
||||||
const room = lesson.room?.trim();
|
const room = lesson.room?.trim();
|
||||||
|
|
||||||
|
const isAppointment = (lesson.type ?? "").toLowerCase() === "appointment";
|
||||||
|
const notes = lesson.notes?.trim();
|
||||||
const descriptionLines = ["Synced by BetterSEQTA+"];
|
const descriptionLines = ["Synced by BetterSEQTA+"];
|
||||||
if (staff) descriptionLines.push(`Teacher: ${staff}`);
|
if (isAppointment) {
|
||||||
if (lesson.code) descriptionLines.push(`Code: ${lesson.code}`);
|
descriptionLines.push("Type: Appointment");
|
||||||
if (lesson.period) descriptionLines.push(`Period: ${lesson.period}`);
|
if (notes) descriptionLines.push(`Notes: ${notes}`);
|
||||||
|
} else {
|
||||||
|
if (staff) descriptionLines.push(`Teacher: ${staff}`);
|
||||||
|
if (lesson.code) descriptionLines.push(`Code: ${lesson.code}`);
|
||||||
|
if (lesson.period) descriptionLines.push(`Period: ${lesson.period}`);
|
||||||
|
if (notes) descriptionLines.push(`Notes: ${notes}`);
|
||||||
|
}
|
||||||
|
|
||||||
const colorId = nearestGoogleEventColorId(lesson.colour);
|
const colorId = nearestGoogleEventColorId(lesson.colour);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { describe, expect, it } from "@jest/globals";
|
||||||
|
import { appointmentToLesson, parseSeqtaDateTime } from "./fetchTimetable";
|
||||||
|
|
||||||
|
describe("parseSeqtaDateTime", () => {
|
||||||
|
it("parses SEQTA event timestamps", () => {
|
||||||
|
expect(parseSeqtaDateTime("2026-07-20 09:30:00.0")).toEqual({
|
||||||
|
date: "2026-07-20",
|
||||||
|
time: "09:30",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects empty values", () => {
|
||||||
|
expect(parseSeqtaDateTime(undefined)).toBeNull();
|
||||||
|
expect(parseSeqtaDateTime("")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("appointmentToLesson", () => {
|
||||||
|
it("maps appointment payload rows into timetable lessons", () => {
|
||||||
|
expect(
|
||||||
|
appointmentToLesson({
|
||||||
|
id: 5,
|
||||||
|
from: "2026-07-20 09:30:00.0",
|
||||||
|
until: "2026-07-20 13:10:00.0",
|
||||||
|
event: {
|
||||||
|
id: 5,
|
||||||
|
title: "fsdfsdsdfdsfdsf",
|
||||||
|
notes: "sdfsfddfsdsfdsfdfssdcfdsfsdfdsfds",
|
||||||
|
colour: "#ffc107",
|
||||||
|
event_type: "appointment",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
date: "2026-07-20",
|
||||||
|
from: "09:30",
|
||||||
|
until: "13:10",
|
||||||
|
description: "fsdfsdsdfdsfdsf",
|
||||||
|
type: "appointment",
|
||||||
|
calendarid: "event:5",
|
||||||
|
colour: "#ffc107",
|
||||||
|
notes: "sdfsfddfsdsfdsfdfssdcfdsfsdfdsfds",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips incomplete appointment rows", () => {
|
||||||
|
expect(
|
||||||
|
appointmentToLesson({
|
||||||
|
from: "2026-07-20 09:30:00.0",
|
||||||
|
until: "2026-07-20 13:10:00.0",
|
||||||
|
event: { title: "" },
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -69,19 +69,42 @@ function withSubjectColours(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveStudentId(): Promise<number | undefined> {
|
type SeqtaLoginPayload = {
|
||||||
|
id?: number;
|
||||||
|
student?: number;
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let cachedLogin: SeqtaLoginPayload | null = null;
|
||||||
|
|
||||||
|
async function resolveLoginPayload(): Promise<SeqtaLoginPayload | undefined> {
|
||||||
|
if (cachedLogin?.id != null) return cachedLogin;
|
||||||
try {
|
try {
|
||||||
const json = await postSeqtaJson<{ payload?: { id?: number; student?: number } }>(
|
const json = await postSeqtaJson<{ payload?: SeqtaLoginPayload }>(
|
||||||
"/seqta/student/login",
|
"/seqta/student/login",
|
||||||
{ mode: "normal", query: null, redirect_url: location.origin },
|
{ mode: "normal", query: null, redirect_url: location.href },
|
||||||
);
|
);
|
||||||
const id = json?.payload?.id ?? json?.payload?.student;
|
const payload = json?.payload;
|
||||||
return typeof id === "number" && Number.isFinite(id) ? id : undefined;
|
if (!payload) return undefined;
|
||||||
|
cachedLogin = payload;
|
||||||
|
return payload;
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resolveStudentId(): Promise<number | undefined> {
|
||||||
|
const payload = await resolveLoginPayload();
|
||||||
|
const id = payload?.id ?? payload?.student;
|
||||||
|
return typeof id === "number" && Number.isFinite(id) ? id : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolvePersonType(): Promise<string> {
|
||||||
|
const payload = await resolveLoginPayload();
|
||||||
|
const type = payload?.type?.trim().toLowerCase();
|
||||||
|
return type || "student";
|
||||||
|
}
|
||||||
|
|
||||||
function isEngageParentContext(): boolean {
|
function isEngageParentContext(): boolean {
|
||||||
return (
|
return (
|
||||||
location.pathname.includes("/parent/") ||
|
location.pathname.includes("/parent/") ||
|
||||||
@@ -90,11 +113,83 @@ function isEngageParentContext(): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SeqtaAppointmentPayload = {
|
||||||
|
id?: number;
|
||||||
|
from?: string;
|
||||||
|
until?: string;
|
||||||
|
event?: {
|
||||||
|
id?: number;
|
||||||
|
title?: string;
|
||||||
|
notes?: string;
|
||||||
|
colour?: string;
|
||||||
|
event_type?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Parse SEQTA datetimes like `2026-07-20 09:30:00.0` into lesson date/time fields. */
|
||||||
|
export function parseSeqtaDateTime(value: string | undefined): { date: string; time: string } | null {
|
||||||
|
if (!value) return null;
|
||||||
|
const match = value.trim().match(/^(\d{4}-\d{2}-\d{2})[ T](\d{1,2}:\d{2})/);
|
||||||
|
if (!match) return null;
|
||||||
|
return { date: match[1], time: match[2] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function appointmentToLesson(item: SeqtaAppointmentPayload): SeqtaTimetableLesson | null {
|
||||||
|
const start = parseSeqtaDateTime(item.from);
|
||||||
|
const end = parseSeqtaDateTime(item.until);
|
||||||
|
const title = item.event?.title?.trim();
|
||||||
|
const eventId = item.event?.id ?? item.id;
|
||||||
|
if (!start || !end || !title || eventId == null) return null;
|
||||||
|
|
||||||
|
const notes = item.event?.notes?.trim();
|
||||||
|
return {
|
||||||
|
date: start.date,
|
||||||
|
from: start.time,
|
||||||
|
until: end.time,
|
||||||
|
description: title,
|
||||||
|
type: item.event?.event_type?.trim() || "appointment",
|
||||||
|
calendarid: `event:${eventId}`,
|
||||||
|
colour: item.event?.colour?.trim() || undefined,
|
||||||
|
...(notes ? { notes } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAppointments(range: SyncDateRange): Promise<SeqtaTimetableLesson[]> {
|
||||||
|
if (isEngageParentContext()) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const person = await resolveStudentId();
|
||||||
|
if (person == null) return [];
|
||||||
|
|
||||||
|
const personType = await resolvePersonType();
|
||||||
|
const data = await postSeqtaJson<{ payload?: SeqtaAppointmentPayload[] }>(
|
||||||
|
"/seqta/student/events/load",
|
||||||
|
{
|
||||||
|
dateFrom: range.from,
|
||||||
|
dateTo: range.until,
|
||||||
|
person,
|
||||||
|
personType,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = Array.isArray(data?.payload) ? data.payload : [];
|
||||||
|
const lessons: SeqtaTimetableLesson[] = [];
|
||||||
|
for (const item of payload) {
|
||||||
|
const lesson = appointmentToLesson(item);
|
||||||
|
if (lesson) lessons.push(lesson);
|
||||||
|
}
|
||||||
|
return lessons;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchTimetableLessons(
|
export async function fetchTimetableLessons(
|
||||||
range: SyncDateRange,
|
range: SyncDateRange,
|
||||||
): Promise<SeqtaTimetableLesson[]> {
|
): Promise<SeqtaTimetableLesson[]> {
|
||||||
const { from, until } = range;
|
const { from, until } = range;
|
||||||
const coloursPromise = fetchSubjectColours();
|
const coloursPromise = fetchSubjectColours();
|
||||||
|
const appointmentsPromise = fetchAppointments(range);
|
||||||
|
|
||||||
let lessons: SeqtaTimetableLesson[] = [];
|
let lessons: SeqtaTimetableLesson[] = [];
|
||||||
|
|
||||||
@@ -125,7 +220,9 @@ export async function fetchTimetableLessons(
|
|||||||
lessons = Array.isArray(data?.payload?.items) ? data.payload.items : [];
|
lessons = Array.isArray(data?.payload?.items) ? data.payload.items : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return withSubjectColours(lessons, await coloursPromise);
|
const coloured = withSubjectColours(lessons, await coloursPromise);
|
||||||
|
const appointments = await appointmentsPromise;
|
||||||
|
return [...coloured, ...appointments];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTimetableForSync(weeksAhead?: number): Promise<SeqtaTimetableLesson[]> {
|
export async function fetchTimetableForSync(weeksAhead?: number): Promise<SeqtaTimetableLesson[]> {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface SeqtaTimetableLesson {
|
|||||||
code?: string;
|
code?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
period?: string;
|
period?: string;
|
||||||
|
notes?: string;
|
||||||
calendarid?: string | number;
|
calendarid?: string | number;
|
||||||
ci?: number;
|
ci?: number;
|
||||||
/** SEQTA subject colour (hex/rgb) used for Google Calendar event colour. */
|
/** SEQTA subject colour (hex/rgb) used for Google Calendar event colour. */
|
||||||
|
|||||||
Reference in New Issue
Block a user