mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
feat: queue popups and new engage popup
This commit is contained in:
@@ -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
@@ -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,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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (clearJustUpdated) {
|
||||||
delete settingsState.justupdated;
|
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();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user