Skip to content

Commit

Permalink
Finalized Term Alert (#255)
Browse files Browse the repository at this point in the history
### Summary

Resolves #252 

Added functionality to alert user when they're viewing a schedule that might not be finalized

---------

Co-authored-by: mhartlage3 <[email protected]>
Co-authored-by: Yatharth Bhargava <[email protected]>
Co-authored-by: Nathan Papa <[email protected]>
Co-authored-by: Yatharth Bhargava <[email protected]>
  • Loading branch information
5 people authored Feb 26, 2024
1 parent a9de232 commit 072dd40
Show file tree
Hide file tree
Showing 12 changed files with 189 additions and 118 deletions.
4 changes: 3 additions & 1 deletion src/components/App/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from './navigation';
import { classes } from '../../utils/misc';
import { AccountContextValue } from '../../contexts/account';
import { Term } from '../../types';

/**
* Renders the actual content at the root of the app
Expand Down Expand Up @@ -85,7 +86,7 @@ export type AppSkeletonProps = {
children: React.ReactNode;
accountState?: AccountContextValue;
termsState?: {
terms: string[];
terms: Term[];
currentTerm: string;
onChangeTerm: (next: string) => void;
};
Expand Down Expand Up @@ -120,6 +121,7 @@ export function AppSkeleton({
: { type: 'loaded', ...termsState }
}
versionsState={{ type: 'loading' }}
skeleton
/>
{children}
<Attribution />
Expand Down
83 changes: 33 additions & 50 deletions src/components/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,10 @@ import useThemeFromStorage from '../../data/hooks/useThemeFromStorage';
import { DESKTOP_BREAKPOINT } from '../../constants';
import useScreenWidth from '../../hooks/useScreenWidth';
import InformationModal from '../InformationModal';
import Maintenance from './maintenance';

import 'react-virtualized/styles.css';
import './stylesheet.scss';

const date = new Date(
new Date().toLocaleString('en-US', { timeZone: 'America/New_York' })
);

const websiteDown =
date.getFullYear() === 2024 &&
date.getMonth() + 1 === 2 &&
date.getDate() === 26;

export default function App(): React.ReactElement {
// Grab the current theme (light/dark) from local storage.
// This hook returns the memoized context value.
Expand All @@ -45,52 +35,45 @@ export default function App(): React.ReactElement {
{/* To bring the website down for maintenance purposes,
insert <Maintenance /> here and disable everything below.
See https://github.com/gt-scheduler/website/pull/194 for reference. */}
{websiteDown ? (
<Maintenance />
) : (
<ErrorBoundary
fallback={(error, errorInfo): React.ReactElement => (
<AppSkeleton>
<SkeletonContent>
<ErrorHeader />
<ErrorDisplay
errorDetails={
<ReactErrorDetails
error={error}
errorInfo={errorInfo}
/>
}
>
<div>
There was en error somewhere in the core application
logic and it can&apos;t continue.
</div>
<div>
Try refreshing the page to see if it fixes the issue.
</div>
</ErrorDisplay>
</SkeletonContent>
</AppSkeleton>
)}
>
<AppNavigation>
{/* AppDataLoader is in charge of ensuring that there are valid values
<ErrorBoundary
fallback={(error, errorInfo): React.ReactElement => (
<AppSkeleton>
<SkeletonContent>
<ErrorHeader />
<ErrorDisplay
errorDetails={
<ReactErrorDetails error={error} errorInfo={errorInfo} />
}
>
<div>
There was en error somewhere in the core application logic
and it can&apos;t continue.
</div>
<div>
Try refreshing the page to see if it fixes the issue.
</div>
</ErrorDisplay>
</SkeletonContent>
</AppSkeleton>
)}
>
<AppNavigation>
{/* AppDataLoader is in charge of ensuring that there are valid values
for the Terms and Term contexts before rendering its children.
If any data is still loading,
then it displays an "app skeleton" with a spinner.
If there was an error while loading
then it displays an error screen. */}
<AppDataLoader>
<AppContent />
</AppDataLoader>
</AppNavigation>
<Feedback />
<AppDataLoader>
<AppContent />
</AppDataLoader>
</AppNavigation>
<Feedback />

{/* Display a popup when first visiting the site */}
{/* Include <InformationModal /> or <MaintenanceModal /> here */}
<InformationModal />
</ErrorBoundary>
)}
{/* Display a popup when first visiting the site */}
{/* Include <InformationModal /> or <MaintenanceModal /> here */}
<InformationModal />
</ErrorBoundary>
</TooltipProvider>
</AppCSSRoot>
</ThemeContext.Provider>
Expand Down
3 changes: 2 additions & 1 deletion src/components/AppDataLoader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
StageExtractScheduleVersion,
StageSkeletonProps,
} from './stages';
import { Term } from '../../types';

export type DataLoaderProps = {
children: React.ReactNode;
Expand Down Expand Up @@ -204,7 +205,7 @@ function GroupLoadScheduleData({
}

type ContextProviderProps = {
terms: string[];
terms: Term[];
currentTerm: string;
setTerm: (next: string) => void;
currentVersion: string;
Expand Down
6 changes: 3 additions & 3 deletions src/components/AppDataLoader/stages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Immutable, Draft, castDraft } from 'immer';
import { Oscar } from '../../data/beans';
import useDownloadOscarData from '../../data/hooks/useDownloadOscarData';
import useDownloadTerms from '../../data/hooks/useDownloadTerms';
import { NonEmptyArray } from '../../types';
import { NonEmptyArray, Term } from '../../types';
import LoadingDisplay from '../LoadingDisplay';
import { SkeletonContent, AppSkeleton, AppSkeletonProps } from '../App/content';
import {
Expand Down Expand Up @@ -78,7 +78,7 @@ export function StageLoadUIState({

export type StageEnsureValidTermProps = {
skeletonProps?: StageSkeletonProps;
terms: NonEmptyArray<string>;
terms: NonEmptyArray<Term>;
currentTermRaw: string;
setTerm: (next: string) => void;
children: (props: { currentTerm: string }) => React.ReactNode;
Expand Down Expand Up @@ -343,7 +343,7 @@ export function StageCreateScheduleDataProducer({

export type StageLoadTermsProps = {
skeletonProps?: StageSkeletonProps;
children: (props: { terms: NonEmptyArray<string> }) => React.ReactNode;
children: (props: { terms: NonEmptyArray<Term> }) => React.ReactNode;
};

/**
Expand Down
1 change: 1 addition & 0 deletions src/components/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export default function Header({
renameVersion,
cloneVersion,
}}
skeleton={false}
/>
);
}
38 changes: 33 additions & 5 deletions src/components/HeaderDisplay/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faBars,
Expand All @@ -17,6 +17,8 @@ import useScreenWidth from '../../hooks/useScreenWidth';
import HeaderActionBar from '../HeaderActionBar';
import Modal from '../Modal';
import { AccountContextValue } from '../../contexts/account';
import { Term } from '../../types';
import Toast, { notifyToast } from '../Toast';

import './stylesheet.scss';

Expand Down Expand Up @@ -49,12 +51,13 @@ export type HeaderDisplayProps = {
| { type: 'loading' }
| {
type: 'loaded';
terms: readonly string[];
terms: Term[];
currentTerm: string;
onChangeTerm: (next: string) => void;
};
versionsState: VersionState;
accountState: AccountContextValue | { type: 'loading' };
skeleton: boolean;
};

/**
Expand All @@ -79,6 +82,7 @@ export default function HeaderDisplay({
termsState,
versionsState,
accountState,
skeleton = true,
}: HeaderDisplayProps): React.ReactElement {
// Re-render when the page is re-sized to become mobile/desktop
// (desktop is >= 1024 px wide)
Expand All @@ -87,8 +91,33 @@ export default function HeaderDisplay({
// Re-render when the page is re-sized to be small mobile vs. greater
// (small mobile is < 600 px wide)
const largeMobile = useScreenWidth(LARGE_MOBILE_BREAKPOINT);

useEffect(() => {
if (termsState.type === 'loaded' && !skeleton) {
const termObject = termsState.terms.filter(
(term) => term.term === termsState.currentTerm
)[0];

if (!termObject?.finalized) {
notifyToast('finalized-term-toast');
}
}
});

return (
<div className="Header">
{!skeleton ? (
<Toast
id="finalized-term-toast"
color="orange"
message={`Note: The schedule for ${
termsState.type === 'loaded'
? getSemesterName(termsState.currentTerm)
: 'Loading'
} may not be fully finalized.`}
selfDisappearing={false}
/>
) : null}
{/* Menu button, only displayed on mobile */}
{mobile && (
<Button className="nav-menu-button" onClick={onToggleMenu}>
Expand All @@ -108,15 +137,14 @@ export default function HeaderDisplay({
onChange={termsState.onChangeTerm}
current={termsState.currentTerm}
options={termsState.terms.map((currentTerm) => ({
id: currentTerm,
label: getSemesterName(currentTerm),
id: currentTerm.term,
label: getSemesterName(currentTerm.term),
}))}
className="semester"
/>
) : (
<LoadingSelect />
)}

{/* Version selector */}
<VersionSelector state={versionsState} />

Expand Down
49 changes: 31 additions & 18 deletions src/components/Toast/index.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,62 @@
import React from 'react';

import './stylesheet.scss';
import { classes } from '../../utils/misc';
import { faWarning, faClose } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconProp } from '@fortawesome/fontawesome-svg-core';

import { classes } from '../../utils/misc';

import './stylesheet.scss';

export type ToastProps = {
id: string;
className?: string;
color?: string;
icon?: IconProp;
message?: string;
selfDisappearing?: boolean;
};

export function notifyToast(className: string): void {
const t = document.getElementsByClassName(
classes('toast', className)
)[0] as HTMLElement;
export function notifyToast(id: string): void {
const t = document.getElementById(id) as HTMLElement;

const selfDisappearing = !t.getElementsByClassName('toast-close-icon')[0];
t.style.visibility = 'visible';
t.style.animation = 'fadein 0.5s';
t.style.animation =
window.innerWidth <= 450 ? 'fadein-mobile 0.5s' : 'fadein 0.5s';
if (selfDisappearing) {
setTimeout(function () {
t.style.animation = 'fadeout 0.5s';
setTimeout(() => {
t.style.animation =
window.innerWidth <= 450 ? 'fadeout-mobile 0.5s' : 'fadeout 0.5s';
}, 5000);
setTimeout(function () {
t.style.visibility = 'hidden';
}, 5500);
}
}

export default function Toast({
id,
className,
color = 'orange',
icon = faWarning,
message = '',
selfDisappearing = false,
selfDisappearing = true,
}: ToastProps): React.ReactElement {
const handleAnimationEnd = (
event: React.AnimationEvent<HTMLDivElement>
): void => {
if (
event.animationName === 'fadeout' ||
event.animationName === 'fadeout-mobile'
) {
const t = event.target as HTMLElement;
t.style.visibility = 'hidden';
}
};

return (
<div
className={classes('toast', className)}
style={{ backgroundColor: color }}
onAnimationEnd={handleAnimationEnd}
id={id}
>
<FontAwesomeIcon fixedWidth icon={icon} className="toast-icon" />
<div className="toast-message">{message}</div>
Expand All @@ -54,10 +69,8 @@ export default function Toast({
const t = document.getElementsByClassName(
classes('toast', className)
)[0] as HTMLElement;
t.style.animation = 'fadeout 0.5s';
setTimeout(function () {
t.style.visibility = 'hidden';
}, 500);
t.style.animation =
window.innerWidth <= 450 ? 'fadeout-mobile 0.5s' : 'fadeout 0.5s';
}}
/>
)}
Expand Down
Loading

0 comments on commit 072dd40

Please sign in to comment.