Skip to content

Commit

Permalink
Teacher Tool: Update Notifications (#9857)
Browse files Browse the repository at this point in the history
This swaps out our Teacher Tool notification system for one based on the multiplayer toast system. Since multiplayer uses tailwind, it would've been a pain to move everything into shared components. Instead, I've more-or-less copied everything over, but the functionality is the same.
  • Loading branch information
thsparks authored Feb 8, 2024
1 parent 305bf5f commit 249d5fa
Show file tree
Hide file tree
Showing 19 changed files with 611 additions and 105 deletions.
288 changes: 288 additions & 0 deletions teachertool/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions teachertool/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@types/node": "^16.11.33",
"framer-motion": "^6.5.1",
"idb": "^7.1.1",
"nanoid": "^4.0.2",
"react-scripts": "5.0.1",
Expand Down
13 changes: 5 additions & 8 deletions teachertool/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { useEffect, useContext, useState } from "react";
import { AppStateContext, AppStateReady } from "./state/appStateContext";
import { usePromise } from "./hooks";
import { makeNotification } from "./utils";
import { makeToast } from "./utils";
import * as Actions from "./state/actions";
import * as NotificationService from "./services/notificationService";
import { downloadTargetConfigAsync } from "./services/backendRequests";
import { logDebug } from "./services/loggingService";
import { HeaderBar } from "./components/HeaderBar";
import { MainPanel } from "./components/MainPanel";
import { Notifications } from "./components/Notifications";
import { Toasts } from "./components/Toasts";
import { CatalogModal } from "./components/CatalogModal";
import { postNotification } from "./transforms/postNotification";
import { showToast } from "./transforms/showToast";
import { loadCatalogAsync } from "./transforms/loadCatalogAsync";
import { loadValidatorPlansAsync } from "./transforms/loadValidatorPlansAsync";
import { tryLoadLastActiveRubricAsync } from "./transforms/tryLoadLastActiveRubricAsync";
Expand All @@ -24,8 +23,6 @@ export const App = () => {

useEffect(() => {
if (ready && !inited) {
NotificationService.initialize();

Promise.resolve().then(async () => {
const cfg = await downloadTargetConfigAsync();
dispatch(Actions.setTargetConfig(cfg || {}));
Expand All @@ -37,7 +34,7 @@ export const App = () => {
await tryLoadLastActiveRubricAsync();

// Test notification
postNotification(makeNotification("🎓", 2000));
showToast(makeToast("success", "🎓", 2000));

setInited(true);
logDebug("App initialized");
Expand All @@ -55,7 +52,7 @@ export const App = () => {
<MainPanel />
<CatalogModal />
<ImportRubricModal />
<Notifications />
<Toasts />
</>
);
};
4 changes: 3 additions & 1 deletion teachertool/src/components/AddCriteriaButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ export const AddCriteriaButton: React.FC<IProps> = ({}) => {
() => getSelectableCatalogCriteria(teacherTool).length > 0,
[teacherTool.catalog, teacherTool.rubric]
);
return <Button
return (
<Button
className="inline"
label={lf("Add Criteria")}
onClick={() => showModal("catalog-display")}
title={lf("Add Criteria")}
leftIcon="fas fa-plus-circle"
disabled={!hasAvailableCriteria}
/>
);
};
18 changes: 0 additions & 18 deletions teachertool/src/components/Notifications.tsx

This file was deleted.

105 changes: 105 additions & 0 deletions teachertool/src/components/Toasts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { useCallback, useContext, useEffect, useState, useRef } from "react";
import { AppStateContext } from "../state/appStateContext";
import { ToastType, ToastWithId } from "../types";
import { AnimatePresence, motion } from "framer-motion";
import { dismissToast } from "../state/actions";
import { classList } from "react-common/components/util";
// eslint-disable-next-line import/no-internal-modules
import css from "./styling/Toasts.module.scss";

const icons: { [type in ToastType]: string } = {
success: "😊",
info: "🔔",
warning: "😮",
error: "😢",
};

const SLIDER_DELAY_MS = 300;

interface IToastNotificationProps {
toast: ToastWithId;
}
const ToastNotification: React.FC<IToastNotificationProps> = ({ toast }) => {
const { dispatch } = useContext(AppStateContext);
const [sliderActive, setSliderActive] = useState(false);
const sliderRef = useRef<HTMLDivElement>(null);

useEffect(() => {
let t1: NodeJS.Timeout, t2: NodeJS.Timeout;
if (toast.timeoutMs) {
t1 = setTimeout(() => dispatch(dismissToast(toast.id)), SLIDER_DELAY_MS + toast.timeoutMs);
t2 = setTimeout(() => setSliderActive(true), SLIDER_DELAY_MS);
}
return () => {
clearTimeout(t1);
clearTimeout(t2);
};
}, [dispatch, toast.id, toast.timeoutMs]);

const handleDismissClicked = () => {
dispatch(dismissToast(toast.id));
};

const sliderWidth = useCallback(() => {
return sliderActive ? "0%" : "100%";
}, [sliderActive]);

return (
<div className={classList(css["toast"], css[toast.type])}>
<div className={css["toast-content"]}>
{!toast.hideIcon && (
<div className={classList(css["icon-container"], css[toast.type])}>
{toast.icon ?? icons[toast.type]}
</div>
)}
<div className={css["text-container"]}>
{toast.text && <div className={css["text"]}>{toast.text}</div>}
{toast.detail && <div className={css["detail"]}>{toast.detail}</div>}
{toast.jsx && <div>{toast.jsx}</div>}
</div>
{!toast.hideDismissBtn && !toast.showSpinner && (
<div className={css["dismiss-btn"]} onClick={handleDismissClicked}>
<i className={classList("far fa-times-circle", css["icon"])} />
</div>
)}
{toast.showSpinner && (
<div className={css["spinner"]}>
<i className="fas fa-circle-notch fa-spin" />
</div>
)}
</div>
{toast.timeoutMs && (
<div>
<div
ref={sliderRef}
className={classList(css["slider"], css[toast.type])}
style={{ width: sliderWidth(), transitionDuration: `${toast.timeoutMs}ms` }}
></div>
</div>
)}
</div>
);
};

interface IProps {}
export const Toasts: React.FC<IProps> = ({}) => {
const { state: teacherTool } = useContext(AppStateContext);

return (
<div className={classList(css["toast-container"])}>
<AnimatePresence>
{teacherTool.toasts.map(item => (
<motion.div
key={item.id}
layout
initial={{ opacity: 0, y: 50, scale: 0.3 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, transition: { duration: 0.2 } }}
>
<ToastNotification toast={item} />
</motion.div>
))}
</AnimatePresence>
</div>
);
};
133 changes: 133 additions & 0 deletions teachertool/src/components/styling/Toasts.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
.toast-container {
display: flex;
gap: 0.5rem;
flex-direction: column-reverse;
align-items: flex-end;
position: fixed;
bottom: 0;
right: 0;
margin-bottom: 2rem;
margin-right: 1rem;
z-index: 50;
pointer-events: none;
}

.toast {
display: flex;
flex-direction: column;
margin-right: 0;
border: none;
border-radius: 0.25rem;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
overflow: hidden;
pointer-events: none;

&.success {
background-color: var(--pxt-toast-background-success);
}

&.info {
background-color: var(--pxt-toast-background-info);
}

&.warning {
background-color: var(--pxt-toast-background-warning);
}

&.error {
background-color: var(--pxt-toast-background-error);
}

.toast-content {
display: flex;
gap: 0.5rem;
padding: 0.75rem;
font-size: 1.125rem;

.icon-container {
display: flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 50%;
width: 1.75rem;
height: 1.75rem;

&.success {
background-color: var(--pxt-toast-accent-success);;
}

&.info {
background-color: var(--pxt-toast-accent-info);;
}

&.warning {
background-color: var(--pxt-toast-accent-warning);;
}

&.error {
background-color: var(--pxt-toast-accent-error);
}
}

.text-container {
display: flex;
flex-direction: column;
text-align: left;

.text {
white-space: nowrap;
font-size: 1rem;
overflow: hidden;
text-overflow: ellipsis;
}

.detail {
font-size: 0.875rem;
}
}

.dismiss-btn {
display: flex;
flex-grow: 1;
justify-content: flex-end;
pointer-events: auto;

.icon {
cursor: pointer;

&:hover {
transform: scale(1.25);
transition: all 0.15s ease-in-out;
}
}
}

.spinner {
display: flex;
flex-grow: 1;
justify-content: flex-end;
}
}

.slider {
height: 0.25rem;
transition: all 0.2s linear;

&.success {
background-color: var(--pxt-toast-accent-success);;
}

&.info {
background-color: var(--pxt-toast-accent-info);;
}

&.warning {
background-color: var(--pxt-toast-accent-warning);;
}

&.error {
background-color: var(--pxt-toast-accent-error);
}
}
}
21 changes: 0 additions & 21 deletions teachertool/src/services/notificationService.ts

This file was deleted.

Loading

0 comments on commit 249d5fa

Please sign in to comment.