Skip to content

Commit

Permalink
teacher tool: add home screen carousels (#9885)
Browse files Browse the repository at this point in the history
* tt: home screen carousels

* card type

* move confirmation

* rename handler, remove window origin

* callback naming

* remove comment

* better card typing
  • Loading branch information
eanders-ms authored Feb 26, 2024
1 parent cd8edad commit 9440155
Show file tree
Hide file tree
Showing 16 changed files with 284 additions and 18 deletions.
10 changes: 10 additions & 0 deletions localtypings/pxtarget.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ declare namespace pxt {
electronManifest?: pxt.electron.ElectronManifest;
profileNotification?: ProfileNotification;
kiosk?: KioskConfig;
teachertool?: TeacherToolConfig;
}

interface PackagesConfig {
Expand Down Expand Up @@ -88,6 +89,15 @@ declare namespace pxt {
highScoreMode: string;
}

interface TeacherToolConfig {
carousels?: TeacherToolCarouselConfig[];
}

interface TeacherToolCarouselConfig {
title: string;
cardsUrl: string;
}

interface AppTarget {
id: string; // has to match ^[a-z]+$; used in URLs and domain names
platformid?: string; // eg "codal"; used when search for gh packages ("for PXT/codal"); defaults to id
Expand Down
2 changes: 0 additions & 2 deletions teachertool/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@
<script type="text/javascript" src="/blb/target.js"></script>
<script type="text/javascript" src="/blb/pxtlib.js"></script>
<script type="text/javascript" src="/blb/pxtsim.js"></script>
<script type="text/javascript" src="/blb/pxtblockly.js"></script>
<script type="text/javascript" src="/blb/pxtblocks.js"></script>

<div id="root"></div>
</body>
Expand Down
2 changes: 1 addition & 1 deletion teachertool/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useContext, useState } from "react";
import { AppStateContext, AppStateReady } from "./state/appStateContext";
import { usePromise } from "./hooks";
import { usePromise } from "./hooks/usePromise";
import { makeToast } from "./utils";
import * as Actions from "./state/actions";
import { downloadTargetConfigAsync } from "./services/backendRequests";
Expand Down
135 changes: 126 additions & 9 deletions teachertool/src/components/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import "swiper/scss";
import "swiper/scss/navigation";
import "swiper/scss/mousewheel";

import * as React from "react";
import { useContext, useState } from "react";
import css from "./styling/HomeScreen.module.scss";
import { Link } from "react-common/components/controls/Link";
import { Button } from "react-common/components/controls/Button";
import { classList } from "react-common/components/util";
import { showModal } from "../transforms/showModal";
import { resetRubricAsync } from "../transforms/resetRubricAsync";
import { loadRubricAsync } from "../transforms/loadRubricAsync";
import { Constants, Strings, Ticks } from "../constants";
import { Swiper, SwiperSlide } from "swiper/react";
import { Mousewheel, Navigation } from "swiper";

import "swiper/scss";
import "swiper/scss/navigation";
import "swiper/scss/mousewheel";
import { AppStateContext } from "../state/appStateContext";
import { CarouselCardSet, RequestStatus, CarouselRubricResourceCard, CardType } from "../types";
import { useJsonDocRequest } from "../hooks/useJsonDocRequest";
import { isRubricResourceCard } from "../utils";

const Welcome: React.FC = () => {
return (
Expand All @@ -27,14 +33,14 @@ const Welcome: React.FC = () => {
);
};

interface CardProps {
interface IconCardProps {
title: string;
className?: string;
icon?: string;
onClick: () => void;
}

const Card: React.FC<CardProps> = ({ title, className, icon, onClick }) => {
const IconCard: React.FC<IconCardProps> = ({ title, className, icon, onClick }) => {
return (
<div className={css.cardContainer}>
<Button className={classList(css.cardButton, className)} title={title} onClick={onClick}>
Expand All @@ -53,6 +59,60 @@ const Card: React.FC<CardProps> = ({ title, className, icon, onClick }) => {
);
};

interface LoadingCardProps {
delay?: boolean;
}

const LoadingCard: React.FC<LoadingCardProps> = ({ delay }) => {
return (
<div className={css.cardContainer}>
<Button
className={classList(css.cardButton, css.loadingGradient, delay ? css.loadingGradientDelay : undefined)}
title={""}
onClick={() => {}}
>
<div className={css.cardDiv}>
<div className={css.loadingGradient}></div>
</div>
</Button>
</div>
);
};

interface RubricResourceCardProps {
cardTitle: string;
imageUrl: string;
rubricUrl: string;
}

const RubricResourceCard: React.FC<RubricResourceCardProps> = ({ cardTitle, imageUrl, rubricUrl }) => {
const onCardClickedAsync = async () => {
pxt.tickEvent(Ticks.LoadRubric, { rubricUrl });
await loadRubricAsync(rubricUrl);
};
return (
<div className={css.cardContainer}>
<Button
className={classList(css.cardButton, css.rubricResource)}
title={cardTitle}
onClick={onCardClickedAsync}
>
<div
className={classList(css.cardDiv)}
style={{
backgroundImage: `url("${imageUrl}")`,
backgroundSize: "cover",
}}
>
<div className={classList(css.cardTitle, css.rubricResourceCardTitle)}>
<h3>{cardTitle}</h3>
</div>
</div>
</Button>
</div>
);
};

interface CarouselProps extends React.PropsWithChildren<{}> {}

const Carousel: React.FC<CarouselProps> = ({ children }) => {
Expand All @@ -65,7 +125,9 @@ const Carousel: React.FC<CarouselProps> = ({ children }) => {
allowTouchMove={true}
slidesOffsetBefore={32}
navigation={true}
mousewheel={true}
mousewheel={{
forceToAxis: true,
}}
modules={[Navigation, Mousewheel]}
className={css.swiperCarousel}
>
Expand Down Expand Up @@ -95,13 +157,13 @@ const GetStarted: React.FC = () => {
<h2>{lf("Get Started")}</h2>
</div>
<Carousel>
<Card
<IconCard
title={Strings.NewRubric}
icon={"fas fa-plus-circle"}
className={css.newRubric}
onClick={onNewRubricClickedAsync}
/>
<Card
<IconCard
title={Strings.ImportRubric}
icon={"fas fa-file-upload"}
className={css.importRubric}
Expand All @@ -112,11 +174,66 @@ const GetStarted: React.FC = () => {
);
};

interface DataCarouselProps {
title: string;
cardsUrl: string;
}

const CardCarousel: React.FC<DataCarouselProps> = ({ title, cardsUrl }) => {
const [cardSet, setCardSet] = useState<CarouselCardSet | undefined>();
const [fetchStatus, setFetchStatus] = useState<RequestStatus | undefined>();

useJsonDocRequest(cardsUrl, setFetchStatus, setCardSet);

return (
<>
<div className={css.carouselRow}>
<div className={css.rowTitle}>
<h2>{title}</h2>
</div>
{(fetchStatus === "loading" || fetchStatus === "error") && (
<Carousel>
<LoadingCard />
<LoadingCard delay={true} />
</Carousel>
)}
{fetchStatus === "success" && (
<Carousel>
{cardSet?.cards.map((card, index) => {
if (isRubricResourceCard(card)) {
return <RubricResourceCard key={index} {...card} />;
} else {
return <LoadingCard />;
}
})}
</Carousel>
)}
</div>
</>
);
};

const CardCarousels: React.FC = () => {
const { state } = useContext(AppStateContext);
const { targetConfig } = state;
const teachertool = targetConfig?.teachertool;
const carousels = teachertool?.carousels;

return (
<>
{carousels?.map((carousel, index) => (
<CardCarousel key={index} title={carousel.title} cardsUrl={carousel.cardsUrl} />
))}
</>
);
};

export const HomeScreen: React.FC = () => {
return (
<div className={css.page}>
<Welcome />
<GetStarted />
<CardCarousels />
</div>
);
};
35 changes: 34 additions & 1 deletion teachertool/src/components/styling/HomeScreen.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ div.page {
padding: 0;
min-width: 17.5rem;
min-height: 12rem;
overflow: hidden;

div.cardDiv {
display: flex;
Expand Down Expand Up @@ -104,6 +105,38 @@ div.page {
outline-color: var(--pxt-button-primary-foreground);
}
}
&.loadingGradient {
background: linear-gradient(45deg, var(--pxt-content-background), var(--pxt-content-foreground));
opacity: 0.2;
background-size: 400% 200%;
animation: loading 3s ease infinite;

@keyframes loading {
0% {
background-position: 0 0;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0 0;
}
}
&.loadingGradientDelay {
animation-delay: 0.3s;
}
}
&.rubricResource {
background-size: 100% 100%;
background-repeat: no-repeat;
grid-area: 1 / 1 / 2 / 5;
border: 1px solid var(--pxt-content-background);

.rubricResourceCardTitle {
background-color: rgba(228, 231, 241, 0.9);
color: var(--pxt-page-foreground);
}
}
}

.swiperCarousel {
Expand Down Expand Up @@ -146,7 +179,7 @@ div.page {
transform: scale(1.1);
}
}

&::after {
transform: scale(0.9);
color: var(--pxt-headerbar-background);
Expand Down
1 change: 1 addition & 0 deletions teachertool/src/components/styling/SplitPane.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
width: 5px;
height: 100%;
cursor: ew-resize;
z-index: 1;
}

.splitter-vertical-inner:hover {
Expand Down
4 changes: 3 additions & 1 deletion teachertool/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export namespace Strings {
export const ConfirmReplaceRubric = lf("This will replace your current rubric. Continue?");
export const ErrorLoadingRubricMsg = lf("That wasn't a valid rubric.");
export const ConfirmReplaceRubricMsg = lf("This will replace your current rubric. Continue?");
export const UntitledProject = lf("Untitled Project");
export const UntitledRubric = lf("Untitled Rubric");
export const NewRubric = lf("New Rubric");
Expand All @@ -24,6 +25,7 @@ export namespace Ticks {
export const NewRubric = "teachertool.newrubric";
export const ImportRubric = "teachertool.importrubric";
export const ExportRubric = "teachertool.exportrubric";
export const LoadRubric = "teachertool.loadrubric";
export const Evaluate = "teachertool.evaluate";
export const Autorun = "teachertool.autorun";
export const AddCriteria = "teachertool.addcriteria";
Expand Down
1 change: 0 additions & 1 deletion teachertool/src/hooks/index.ts

This file was deleted.

31 changes: 31 additions & 0 deletions teachertool/src/hooks/useJsonDocRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useEffect, useState } from "react";
import { RequestStatus } from "../types";
import { fetchJsonDocAsync } from "../services/backendRequests";

export function useJsonDocRequest<T>(
url: string,
setStatus: (status: RequestStatus) => void,
setJson: (data: T) => void
) {
const [fetching, setFetching] = useState<boolean>();

useEffect(() => {
if (!fetching) {
setFetching(true);
setStatus("loading");
Promise.resolve()
.then(async () => {
const json = await fetchJsonDocAsync(url);
if (!json) {
setStatus("error");
} else {
setStatus("success");
setJson(json as T);
}
})
.catch(() => {
setStatus("error");
});
}
}, [fetching]);
}
14 changes: 14 additions & 0 deletions teachertool/src/services/backendRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ import { stateAndDispatch } from "../state";
import { ErrorCode } from "../types/errorCode";
import { logError } from "./loggingService";

export async function fetchJsonDocAsync<T = any>(url: string): Promise<T | undefined> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("Unable to fetch the json file");
} else {
const json = await response.json();
return json;
}
} catch (e) {
logError(ErrorCode.fetchJsonDocAsync, e);
}
}

export async function getProjectTextAsync(projectId: string): Promise<pxt.Cloud.JsonText | undefined> {
try {
const projectTextUrl = `${pxt.Cloud.apiRoot}/${projectId}/text`;
Expand Down
Loading

0 comments on commit 9440155

Please sign in to comment.