Skip to content

Commit 92dc429

Browse files
authored
Merge pull request #55 from deco-finter/feat/hifi-tutorial-canvas
[FAB-133] HiFi Canvas Tutorial
2 parents f087edd + 8e77537 commit 92dc429

16 files changed

+544
-57
lines changed

fableous-fe/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"react": "^17.0.2",
2525
"react-canvas-confetti": "^1.2.1",
2626
"react-dom": "^17.0.2",
27+
"react-joyride": "^2.3.1",
2728
"react-resize-detector": "^6.7.6",
2829
"react-router-dom": "^5.2.0",
2930
"react-scripts": "4.0.3",

fableous-fe/src/App.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Navbar from "./components/Navbar";
1212
import Routes from "./Routes";
1313
import InjectAxiosRespInterceptor from "./components/InjectAxiosRespInterceptor";
1414
import { colors } from "./colors";
15+
import CustomNavProvider from "./components/CustomNavProvider";
1516

1617
// generated background from https://www.svgbackgrounds.com/
1718
const useStyles = makeStyles({
@@ -136,13 +137,15 @@ export default function App() {
136137
<React.StrictMode>
137138
<AuthProvider>
138139
<Router>
139-
<InjectAxiosRespInterceptor />
140-
<ThemeProvider theme={theme}>
141-
<Navbar />
142-
<Container className="flex flex-col flex-1 pt-5 pb-12">
143-
<Routes />
144-
</Container>
145-
</ThemeProvider>
140+
<CustomNavProvider>
141+
<InjectAxiosRespInterceptor />
142+
<ThemeProvider theme={theme}>
143+
<Navbar />
144+
<Container className="flex flex-col flex-1 pt-5 pb-12">
145+
<Routes />
146+
</Container>
147+
</ThemeProvider>
148+
</CustomNavProvider>
146149
</Router>
147150
</AuthProvider>
148151
</React.StrictMode>

fableous-fe/src/api.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
22
import { configure } from "axios-hooks";
33
import { TOKEN_KEY } from "./components/AuthProvider";
4+
import { getLocalStorage } from "./storage";
45
import { proto as pb } from "./proto/message_pb";
56

67
const baseAPI =
@@ -16,7 +17,7 @@ const baseWS =
1617
const apiClient = axios.create();
1718
apiClient.defaults.baseURL = baseAPI;
1819
apiClient.interceptors.request.use((req: AxiosRequestConfig) => {
19-
const token = localStorage.getItem(TOKEN_KEY);
20+
const token = getLocalStorage(TOKEN_KEY);
2021
if (token) {
2122
req.headers = {
2223
authorization: `Bearer ${token}`,
@@ -137,13 +138,12 @@ export const restAPI = {
137138
method: "delete",
138139
}),
139140
},
140-
// @TODO: create POST for story saving
141141
} as ApiEndpoints;
142142

143143
export const wsAPI = {
144144
hub: {
145145
main: (classroomId: string) => {
146-
const token = localStorage.getItem(TOKEN_KEY);
146+
const token = getLocalStorage(TOKEN_KEY);
147147
return `${baseWS}/ws/hub?token=${token}&classroom_id=${classroomId}`;
148148
},
149149
},

fableous-fe/src/components/AuthProvider.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createContext, ReactNode, useState } from "react";
2+
import { getLocalStorage, setLocalStorage } from "../storage";
23

34
export const TOKEN_KEY = "token";
45

@@ -17,7 +18,7 @@ export const AuthContext = createContext<AuthContextType>([
1718
]);
1819

1920
export default function AuthProvider(props: { children: ReactNode }) {
20-
const [token, setToken] = useState(localStorage.getItem(TOKEN_KEY) || "");
21+
const [token, setToken] = useState(getLocalStorage(TOKEN_KEY) || "");
2122
const { children } = props;
2223

2324
return (
@@ -26,11 +27,11 @@ export default function AuthProvider(props: { children: ReactNode }) {
2627
token,
2728
token !== "",
2829
(t: string) => {
29-
localStorage.setItem(TOKEN_KEY, t);
30+
setLocalStorage(TOKEN_KEY, t);
3031
setToken(t);
3132
},
3233
() => {
33-
localStorage.setItem(TOKEN_KEY, "");
34+
setLocalStorage(TOKEN_KEY, "");
3435
setToken("");
3536
},
3637
]}

fableous-fe/src/components/ChipRow.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,20 @@ import React from "react";
55
interface ChipRowProps {
66
chips: (React.ReactNode | ChipProps | string)[];
77
primary?: boolean;
8+
rootProps?: React.DetailedHTMLProps<
9+
React.HTMLAttributes<HTMLDivElement>,
10+
HTMLDivElement
11+
>;
812
}
913

1014
export default function ChipRow(props: ChipRowProps) {
11-
const { chips, primary } = props;
15+
const { chips, primary, rootProps } = props;
1216

1317
return (
14-
<div className="flex justify-evenly flex-grow flex-wrap gap-y-4">
18+
<div
19+
className="flex justify-evenly flex-grow flex-wrap gap-y-4"
20+
{...rootProps}
21+
>
1522
{chips.map((chip) =>
1623
React.isValidElement(chip) ? (
1724
chip
@@ -30,4 +37,5 @@ export default function ChipRow(props: ChipRowProps) {
3037

3138
ChipRow.defaultProps = {
3239
primary: false,
40+
rootProps: {},
3341
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { ButtonProps } from "@material-ui/core";
2+
import { createContext, ReactNode, useContext, useState } from "react";
3+
4+
export interface Nav {
5+
icon: string;
6+
label: string;
7+
buttonProps: ButtonProps;
8+
}
9+
10+
export type CustomNavContextType = [
11+
additionalNavs: Nav[],
12+
setAdditionalNavs: React.Dispatch<React.SetStateAction<Nav[]>>,
13+
isLogoClickable: boolean,
14+
setIsLogoClickable: React.Dispatch<React.SetStateAction<boolean>>
15+
];
16+
17+
export const CustomNavContext = createContext<CustomNavContextType>([
18+
[],
19+
(_) => {},
20+
true,
21+
(_) => {},
22+
]);
23+
24+
export const useCustomNav = () => useContext(CustomNavContext);
25+
26+
export default function CustomNavProvider(props: { children: ReactNode }) {
27+
const [additionalNavs, setAdditionalNavs] = useState<Nav[]>([]);
28+
const [isLogoClickable, setIsLogoClickable] = useState(true);
29+
const { children } = props;
30+
31+
return (
32+
<CustomNavContext.Provider
33+
value={[
34+
additionalNavs,
35+
setAdditionalNavs,
36+
isLogoClickable,
37+
setIsLogoClickable,
38+
]}
39+
>
40+
{children}
41+
</CustomNavContext.Provider>
42+
);
43+
}

fableous-fe/src/components/Navbar.tsx

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
import { ReactNode, useContext } from "react";
12
import { Icon, makeStyles } from "@material-ui/core";
23
import AppBar from "@material-ui/core/AppBar";
34
import Toolbar from "@material-ui/core/Toolbar";
45
import Typography from "@material-ui/core/Typography";
56
import Button from "@material-ui/core/Button";
6-
import { Link, useHistory } from "react-router-dom";
7-
import { useContext } from "react";
7+
import { Link, useHistory, useLocation } from "react-router-dom";
88

99
import { AuthContext } from "./AuthProvider";
10+
import { useCustomNav } from "./CustomNavProvider";
1011

1112
const useStyles = makeStyles(() => ({
1213
home: {
@@ -16,23 +17,44 @@ const useStyles = makeStyles(() => ({
1617

1718
export default function Navbar() {
1819
const history = useHistory();
20+
const location = useLocation();
1921
const [, isAuthenticated, , clearToken] = useContext(AuthContext);
2022
const onLogout = () => {
2123
clearToken();
2224
history.push("/");
2325
};
26+
const [additionalNavs, , isLogoClickable] = useCustomNav();
2427
const classes = useStyles();
2528

29+
// navbar will be simplified for students
30+
const isOnStudentPages = location.pathname === "/join";
31+
32+
const logoLinkWrapper = (children: ReactNode) => (
33+
<Link to="/">{children}</Link>
34+
);
35+
const logoElement = (
36+
<Typography variant="h1" className={classes.home}>
37+
Fableous
38+
</Typography>
39+
);
40+
2641
return (
2742
<AppBar position="static">
2843
<Toolbar>
29-
<Link to="/">
30-
<Typography variant="h1" className={classes.home}>
31-
Fableous
32-
</Typography>
33-
</Link>
44+
{isLogoClickable ? logoLinkWrapper(logoElement) : logoElement}
3445
<div className="flex-grow" /> {/* spacer */}
35-
{isAuthenticated ? (
46+
{additionalNavs.map(({ icon, label, buttonProps }) => (
47+
<Button
48+
variant="outlined"
49+
className="text-white"
50+
startIcon={<Icon fontSize="small">{icon}</Icon>}
51+
// eslint-disable-next-line react/jsx-props-no-spreading
52+
{...buttonProps}
53+
>
54+
{label}
55+
</Button>
56+
))}
57+
{isAuthenticated && !isOnStudentPages && (
3658
<>
3759
<Button
3860
variant="outlined"
@@ -43,7 +65,6 @@ export default function Navbar() {
4365
>
4466
Profile
4567
</Button>
46-
4768
<Button
4869
variant="outlined"
4970
className="ml-4 text-white"
@@ -53,7 +74,8 @@ export default function Navbar() {
5374
Logout
5475
</Button>
5576
</>
56-
) : (
77+
)}
78+
{!isAuthenticated && !isOnStudentPages && (
5779
<>
5880
<Button
5981
variant="outlined"

fableous-fe/src/components/canvas/Canvas.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ interface CanvasProps {
5454
setToolMode?: React.Dispatch<React.SetStateAction<ToolMode>>;
5555
toolColor?: string;
5656
toolWidth?: number;
57+
rootId?: string | undefined;
5758
}
5859

5960
const defaultProps = {
@@ -65,6 +66,7 @@ const defaultProps = {
6566
toolMode: ToolMode.None,
6667
setToolMode: () => {},
6768
toolWidth: 8 * SCALE,
69+
rootId: undefined,
6870
};
6971

7072
interface SimplePointerEventData {
@@ -74,8 +76,6 @@ interface SimplePointerEventData {
7476
onLeave: boolean;
7577
}
7678

77-
// TODO after width of canvas DOM element is dynamic, attempt to make canvas drawing scaling dynamic
78-
// that is, resizing screen allows drawing without issue (no translation error)
7979
const Canvas = forwardRef<ImperativeCanvasRef, CanvasProps>(
8080
(props: CanvasProps, ref) => {
8181
let FRAME_COUNTER = 0;
@@ -98,6 +98,7 @@ const Canvas = forwardRef<ImperativeCanvasRef, CanvasProps>(
9898
toolColor = defaultProps.toolColor,
9999
toolWidth = defaultProps.toolWidth,
100100
wsConn,
101+
rootId,
101102
} = props;
102103
// useImperativeHandle of type ImperativeCanvasRef defined at bottom
103104
const canvasRef = useRef<HTMLCanvasElement>(
@@ -911,6 +912,7 @@ const Canvas = forwardRef<ImperativeCanvasRef, CanvasProps>(
911912

912913
return (
913914
<div
915+
id={rootId}
914916
className="relative place-self-center"
915917
style={{
916918
width: offsetWidth,

fableous-fe/src/components/canvas/CanvasToolbar.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { ImperativeCanvasRef } from "./data";
1414
import { proto as pb } from "../../proto/message_pb";
1515
import BrushWidthIcon from "./BrushWidthIcon";
1616
import CanvasToolbarTooltip from "./CanvasToolbarTooltip";
17+
import { TutorialTargetId } from "../../tutorialTargetIds";
1718

1819
interface CanvasToolbarProps {
1920
role: pb.ControllerRole;
@@ -140,6 +141,7 @@ const CanvasToolbar = forwardRef<ImperativeCanvasRef, CanvasToolbarProps>(
140141
}
141142
>
142143
<IconButton
144+
id={TutorialTargetId.BrushTool}
143145
className="relative"
144146
onClick={() => {
145147
if (toolColor === ERASE_COLOR) {
@@ -168,6 +170,7 @@ const CanvasToolbar = forwardRef<ImperativeCanvasRef, CanvasToolbarProps>(
168170
</IconButton>
169171
</CanvasToolbarTooltip>
170172
<IconButton
173+
id={TutorialTargetId.EraseTool}
171174
onClick={() => {
172175
setToolColorRememberPrev(ERASE_COLOR);
173176
setToolMode(ToolMode.Paint);
@@ -178,9 +181,10 @@ const CanvasToolbar = forwardRef<ImperativeCanvasRef, CanvasToolbarProps>(
178181
: "primary"
179182
}
180183
>
181-
<EraserIcon fontSize="medium" />
184+
<EraserIcon fontSize="large" />
182185
</IconButton>
183186
<IconButton
187+
id={TutorialTargetId.FillTool}
184188
onClick={() => {
185189
if (toolColor === ERASE_COLOR) {
186190
setToolColor(prevColor);
@@ -224,6 +228,7 @@ const CanvasToolbar = forwardRef<ImperativeCanvasRef, CanvasToolbarProps>(
224228
}
225229
>
226230
<IconButton
231+
id={TutorialTargetId.PaletteTool}
227232
onClick={() => setIsColorPickerOpen((prev) => !prev)}
228233
color="primary"
229234
className="relative"
@@ -239,17 +244,12 @@ const CanvasToolbar = forwardRef<ImperativeCanvasRef, CanvasToolbarProps>(
239244
/>
240245
</IconButton>
241246
</CanvasToolbarTooltip>
242-
<IconButton
243-
onClick={imperativeCanvasRef.current.runUndo}
244-
color="primary"
245-
>
246-
<UndoIcon fontSize="large" />
247-
</IconButton>
248247
</>
249248
)}
250249
{role === pb.ControllerRole.STORY && (
251250
<>
252251
<IconButton
252+
id={TutorialTargetId.TextTool}
253253
onClick={() => setToolMode(ToolMode.Text)}
254254
color={toolMode === ToolMode.Text ? "secondary" : "primary"}
255255
>
@@ -265,6 +265,7 @@ const CanvasToolbar = forwardRef<ImperativeCanvasRef, CanvasToolbarProps>(
265265
}
266266
>
267267
<IconButton
268+
id={TutorialTargetId.AudioTool}
268269
onClick={() => {
269270
setToolMode(ToolMode.Audio);
270271
setIsRecordingAudio((prev) => {
@@ -285,14 +286,15 @@ const CanvasToolbar = forwardRef<ImperativeCanvasRef, CanvasToolbarProps>(
285286
)}
286287
</IconButton>
287288
</CanvasToolbarTooltip>
288-
<IconButton
289-
onClick={imperativeCanvasRef.current.runUndo}
290-
color="primary"
291-
>
292-
<UndoIcon fontSize="large" />
293-
</IconButton>
294289
</>
295290
)}
291+
<IconButton
292+
id={TutorialTargetId.UndoTool}
293+
onClick={imperativeCanvasRef.current.runUndo}
294+
color="primary"
295+
>
296+
<UndoIcon fontSize="large" />
297+
</IconButton>
296298
</Paper>
297299
</div>
298300
</div>

0 commit comments

Comments
 (0)