Skip to content

Commit

Permalink
tt: home screen carousels
Browse files Browse the repository at this point in the history
  • Loading branch information
eanders-ms committed Feb 23, 2024
1 parent ae559e8 commit 750e6c0
Show file tree
Hide file tree
Showing 15 changed files with 285 additions and 17 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
144 changes: 135 additions & 9 deletions teachertool/src/components/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
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 } from "../types";
import { useJsonDocRequest } from "../hooks/useJsonDocRequest";

const Welcome: React.FC = () => {
return (
Expand All @@ -27,14 +32,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 +58,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 onRubricClickedAsync = async () => {
pxt.tickEvent(Ticks.LoadRubric, { rubricUrl });
await loadRubricAsync(rubricUrl);
};
return (
<div className={css.cardContainer}>
<Button
className={classList(css.cardButton, css.rubricResource)}
title={cardTitle}
onClick={onRubricClickedAsync}
>
<div
className={classList(css.cardDiv)}
style={{
backgroundImage: `url("${window.location.origin}${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 +124,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 +156,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 +173,76 @@ 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) => {
switch (card.cardType) {
case "rubric-resource": {
const rubricCard = card as CarouselRubricResourceCard;
return (
<RubricResourceCard
key={index}
cardTitle={rubricCard.cardTitle}
imageUrl={rubricCard.imageUrl}
rubricUrl={rubricCard.rubricUrl}
/>
);
}
default:
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 @@ -22,6 +23,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.

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

export function useJsonDocRequest<T>(
url: string,
statusCb: (status: RequestStatus) => void,
jsonCb: (data: T) => void
) {
const [status, setStatus] = useState<RequestStatus | undefined>();

useEffect(() => {
if (!status) {
setStatus("loading");
statusCb("loading");
Promise.resolve().then(async () => {
const json = await fetchJsonDocAsync(url);
if (!json) {
setStatus("error");
statusCb("error");
} else {
setStatus("success");
statusCb("success");
jsonCb(json as T);
}
});
}
}, []);
}
15 changes: 15 additions & 0 deletions teachertool/src/services/backendRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ 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 {
// TODO: Prepend CDN origin if not localhost
const response = await fetch(url);
if (!response.ok) {
throw new Error("Unable to fetch the json file from CDN");
} 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 750e6c0

Please sign in to comment.