feat: queue popups and new engage popup

This commit is contained in:
2026-04-12 19:54:43 +09:30
parent 2e9a643a8c
commit 1d9b8f3747
9 changed files with 188 additions and 17 deletions
+39
View File
@@ -3524,6 +3524,26 @@ div.day-empty {
font-size: 1em; font-size: 1em;
color: var(--text-primary); color: var(--text-primary);
} }
.whatsnewHeader.engageParentsAnnouncementHeader {
height: auto;
min-height: unset;
}
.whatsnewHeader.engageParentsAnnouncementHeader h1 {
line-height: 1.2;
}
.whatsnewHeader.engageParentsAnnouncementHeader .engageParentsSubheading {
margin-top: 0.35rem;
font-size: 1.05rem;
font-weight: 600;
opacity: 0.92;
}
.seqtaEngageAccent {
color: #ea580c;
font-weight: 700;
}
.dark .seqtaEngageAccent {
color: #fb923c;
}
.whatsnewBackground { .whatsnewBackground {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -3652,6 +3672,25 @@ div.day-empty {
object-fit: cover; object-fit: cover;
margin-bottom: 12px; margin-bottom: 12px;
} }
.whatsnewTextContainer .engageParentsPromoWrap {
width: 100%;
margin-bottom: 12px;
border-radius: 16px;
overflow: hidden;
aspect-ratio: 16 / 9;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.28);
background: color-mix(in srgb, var(--background-secondary) 88%, var(--text-primary) 12%);
}
.whatsnewTextContainer .engageParentsPromoWrap .engageParentsPromoImg {
display: block;
width: 100%;
height: 100%;
margin: 0;
border-radius: 0;
aspect-ratio: unset;
object-fit: contain;
object-position: center;
}
@keyframes shimmer { @keyframes shimmer {
0% { 0% {
+2 -10
View File
@@ -29,8 +29,7 @@ import {
updateEngageHomeMenuActive, updateEngageHomeMenuActive,
} from "@/seqta/utils/Loaders/LoadEngageHomePage"; } from "@/seqta/utils/Loaders/LoadEngageHomePage";
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage"; import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
import { OpenWhatsNewPopup } from "@/seqta/utils/Openers/OpenWhatsNewPopup"; import { runStartupPopupQueue } from "@/seqta/utils/Openers/StartupPopupQueue";
import { showPrivacyNotification } from "@/seqta/utils/Openers/OpenPrivacyNotification";
import { updateTimetableTimes } from "@/seqta/utils/updateTimetableTimes"; import { updateTimetableTimes } from "@/seqta/utils/updateTimetableTimes";
@@ -106,14 +105,7 @@ export async function finishLoad() {
console.error("Error during loading cleanup:", err); console.error("Error during loading cleanup:", err);
} }
// Check and show privacy statement notification (before what's new) runStartupPopupQueue();
if (!document.getElementById("privacy-notification")) {
await showPrivacyNotification();
}
if (settingsState.justupdated && !document.getElementById("whatsnewbk") && !document.getElementById("privacy-notification")) {
OpenWhatsNewPopup();
}
} }
export function GetCSSElement(file: string) { export function GetCSSElement(file: string) {
Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

@@ -0,0 +1,59 @@
import stringToHTML from "../stringToHTML";
import { settingsState } from "../listeners/SettingsState";
import { openPopup } from "./PopupManager";
/** Same hosting pattern as the privacy statement branding images (avoids page-relative extension URLs on Engage). */
const ENGAGE_PROMO_IMG_URL =
"https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/bq%2Bengage.png";
export function shouldShowEngageParentsAnnouncement(): boolean {
return !settingsState.engageParentsAnnouncementShown;
}
/**
* One-time announcement that BetterSEQTA Plus works on SEQTA Engage (parents).
*/
export function showEngageParentsAnnouncement(onDismissed?: () => void) {
if (document.getElementById("whatsnewbk")) {
onDismissed?.();
return;
}
if (!shouldShowEngageParentsAnnouncement()) {
onDismissed?.();
return;
}
const header = stringToHTML(
/* html */
`<div class="whatsnewHeader engageParentsAnnouncementHeader">
<h1>BetterSEQTA Plus now supports <span class="seqtaEngageAccent">SEQTA Engage</span></h1>
<p class="engageParentsSubheading">Buy your mom a BetterSEQTA Plus</p>
</div>`,
).firstChild as HTMLElement;
const text = stringToHTML(/* html */ `
<div class="whatsnewTextContainer privacyStatement" style="overflow-y: auto; font-size: 1.2rem; line-height: 1.6;">
<div class="engageParentsPromoWrap">
<img class="engageParentsPromoImg" src="${ENGAGE_PROMO_IMG_URL}" width="1920" height="1080" alt="BetterSEQTA Plus now supports SEQTA Engage" />
</div>
<p>
<strong class="seqtaEngageAccent">SEQTA Engage</strong> is the portal many parents use for notices, messages, and day-to-day school info.
Before anything else: BetterSEQTA Plus now supports <strong class="seqtaEngageAccent">SEQTA Engage</strong>, so parents get the same kinds of improvements you are used to on SEQTA Learn—themes, a clearer home experience, and other Plus polish while browsing Engage.
</p>
<p>
The title is a bit of fun; if the extension saves you time, you can always support development via Open Collective or Ko-fi from the What is New changelog or related links in settings.
</p>
<p>
Close this dialog when you are done. We will not show this announcement again.
</p>
</div>
`).firstChild as HTMLElement;
settingsState.engageParentsAnnouncementShown = true;
openPopup({
header,
content: [text],
afterClose: onDismissed,
});
}
@@ -2,12 +2,29 @@ import stringToHTML from "../stringToHTML";
import { settingsState } from "../listeners/SettingsState"; import { settingsState } from "../listeners/SettingsState";
import { openPopup } from "./PopupManager"; import { openPopup } from "./PopupManager";
export function showPrivacyNotification() { const PRIVACY_STATEMENT_VERSION = "2025-12-19";
const lastUpdated = "2025-12-19";
if (document.getElementById("whatsnewbk")) return; export function shouldShowPrivacyNotification(): boolean {
if (settingsState.privacyStatementShown) return; if (settingsState.privacyStatementShown) return false;
if (settingsState.privacyStatementLastUpdated && new Date(settingsState.privacyStatementLastUpdated) > new Date(lastUpdated)) return; if (
settingsState.privacyStatementLastUpdated &&
new Date(settingsState.privacyStatementLastUpdated) >
new Date(PRIVACY_STATEMENT_VERSION)
) {
return false;
}
return true;
}
export function showPrivacyNotification(onDismissed?: () => void) {
if (document.getElementById("whatsnewbk")) {
onDismissed?.();
return;
}
if (!shouldShowPrivacyNotification()) {
onDismissed?.();
return;
}
const header = stringToHTML( const header = stringToHTML(
/* html */ /* html */
@@ -48,5 +65,6 @@ export function showPrivacyNotification() {
openPopup({ openPopup({
header, header,
content: [text], content: [text],
afterClose: onDismissed,
}); });
} }
+3 -1
View File
@@ -3,7 +3,7 @@ import browser from "webextension-polyfill";
import kofi from "@/resources/kofi.png?base64"; import kofi from "@/resources/kofi.png?base64";
import { openPopup } from "./PopupManager"; import { openPopup } from "./PopupManager";
export function OpenWhatsNewPopup() { export function OpenWhatsNewPopup(onDismissed?: () => void) {
const header = stringToHTML( const header = stringToHTML(
/* html */ /* html */
`<div class="whatsnewHeader"> `<div class="whatsnewHeader">
@@ -362,5 +362,7 @@ export function OpenWhatsNewPopup() {
openPopup({ openPopup({
header, header,
content: [imageContainer, text, footer], content: [imageContainer, text, footer],
afterClose: onDismissed,
clearJustUpdated: true,
}); });
} }
+21 -1
View File
@@ -4,6 +4,13 @@ import { animate as motionAnimate, stagger } from "motion";
type AnimationTarget = string | Element | Element[] | NodeList | null; type AnimationTarget = string | Element | Element[] | NodeList | null;
let isClosing = false; let isClosing = false;
let pendingAfterClose: (() => void) | undefined;
function invokeAfterClose() {
const fn = pendingAfterClose;
pendingAfterClose = undefined;
fn?.();
}
export async function closePopup() { export async function closePopup() {
if (isClosing) return; if (isClosing) return;
@@ -16,12 +23,14 @@ export async function closePopup() {
if (!background || !popup) { if (!background || !popup) {
isClosing = false; isClosing = false;
invokeAfterClose();
return; return;
} }
if (!settingsState.animations) { if (!settingsState.animations) {
background.remove(); background.remove();
isClosing = false; isClosing = false;
invokeAfterClose();
return; return;
} }
@@ -33,19 +42,28 @@ export async function closePopup() {
background.remove(); background.remove();
isClosing = false; isClosing = false;
invokeAfterClose();
} }
interface OpenPopupOptions { interface OpenPopupOptions {
header?: Node | null; header?: Node | null;
content?: (Node | null | undefined)[]; content?: (Node | null | undefined)[];
animateSelector?: AnimationTarget; animateSelector?: AnimationTarget;
/** Called once after this popup is fully closed (including skip-animation path). */
afterClose?: () => void;
/** When true, clears the post-update flag when this popup opens (What's New only). */
clearJustUpdated?: boolean;
} }
export function openPopup({ export function openPopup({
header, header,
content = [], content = [],
animateSelector = ".whatsnewTextContainer *", animateSelector = ".whatsnewTextContainer *",
afterClose,
clearJustUpdated = false,
}: OpenPopupOptions = {}) { }: OpenPopupOptions = {}) {
pendingAfterClose = afterClose;
const background = document.createElement("div"); const background = document.createElement("div");
background.id = "whatsnewbk"; background.id = "whatsnewbk";
background.classList.add("whatsnewBackground"); background.classList.add("whatsnewBackground");
@@ -88,7 +106,9 @@ export function openPopup({
} }
} }
delete settingsState.justupdated; if (clearJustUpdated) {
delete settingsState.justupdated;
}
background.addEventListener("click", (event) => { background.addEventListener("click", (event) => {
if (event.target === background) void closePopup(); if (event.target === background) void closePopup();
@@ -0,0 +1,39 @@
import { settingsState } from "../listeners/SettingsState";
import { OpenWhatsNewPopup } from "./OpenWhatsNewPopup";
import {
shouldShowPrivacyNotification,
showPrivacyNotification,
} from "./OpenPrivacyNotification";
import {
shouldShowEngageParentsAnnouncement,
showEngageParentsAnnouncement,
} from "./OpenEngageParentsAnnouncement";
type QueueStep = (goNext: () => void) => void;
/**
* Runs startup modals in order: What's New (if the extension just updated),
* privacy statement (if required), then the SEQTA Engage announcement (once).
*/
export function runStartupPopupQueue() {
const steps: QueueStep[] = [];
if (settingsState.justupdated) {
steps.push((goNext) => OpenWhatsNewPopup(goNext));
}
if (shouldShowPrivacyNotification()) {
steps.push((goNext) => showPrivacyNotification(goNext));
}
if (shouldShowEngageParentsAnnouncement()) {
steps.push((goNext) => showEngageParentsAnnouncement(goNext));
}
function runNext() {
const step = steps.shift();
if (step) step(runNext);
}
runNext();
}
+2
View File
@@ -32,6 +32,8 @@ export interface SettingsState {
justupdated?: boolean; justupdated?: boolean;
privacyStatementShown?: boolean; privacyStatementShown?: boolean;
privacyStatementLastUpdated?: string; privacyStatementLastUpdated?: string;
/** One-time announcement: SEQTA Engage support for parents (dismissed popup queue). */
engageParentsAnnouncementShown?: boolean;
timeFormat?: string; timeFormat?: string;
animations: boolean; animations: boolean;
defaultPage: string; defaultPage: string;