diff --git a/package-lock.json b/package-lock.json index 2eafe31..b5ccb74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "pyatch-vm": "^1.2.9", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-draggable": "^4.4.6", "react-firebase-hooks": "^5.1.1", "react-split-pane": "^2.0.3", "react-split-pane-next": "^1.0.5", @@ -24375,6 +24376,19 @@ "react": "^18.2.0" } }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-firebase-hooks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/react-firebase-hooks/-/react-firebase-hooks-5.1.1.tgz", diff --git a/package.json b/package.json index e1b81fb..4f99256 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "pyatch-vm": "^1.2.9", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-draggable": "^4.4.6", "react-firebase-hooks": "^5.1.1", "react-split-pane": "^2.0.3", "react-split-pane-next": "^1.0.5", diff --git a/src/components/App/component.tsx b/src/components/App/component.tsx index 8fc5a70..5264fc7 100644 --- a/src/components/App/component.tsx +++ b/src/components/App/component.tsx @@ -1,12 +1,13 @@ import React from "react"; import { useState, useEffect } from "react"; import SpritePane from "../SpritePane"; -import { Button, Grid, Tooltip } from "@mui/material"; +import { Backdrop, Button, Grid, Tooltip } from "@mui/material"; import "./style.css"; import { TopBar } from "../TopBar"; import { EditorPane, EditorTabButton } from "../EditorPane"; import { VerticalButtons } from "../PatchButton"; import { ThemeProvider } from "@emotion/react"; +import { Tutorial } from "../Tutorial" import darkTheme from "../../themes/dark"; import lightTheme from "../../themes/light"; @@ -40,6 +41,7 @@ import { LegalDialogueButton } from "./LegalDialogueButton"; import PatchFunctionJson from "../../assets/patch-api.json"; import Popover from "@mui/material/Popover"; +import { useUser } from "../../hooks/useUser"; interface Parameter { [key: string]: string; @@ -59,6 +61,7 @@ const App = () => { const patchVM = usePatchStore((state) => state.patchVM); const saveTargetThreads = usePatchStore((state) => state.saveTargetThreads); const editorTab = usePatchStore((state) => state.editorTab); + const [newUser, setNewUser] = React.useState(true); // Popover state const [anchorEl, setAnchorEl] = React.useState( @@ -98,6 +101,8 @@ const App = () => { const open = Boolean(anchorEl); const id = open ? "simple-popover" : undefined; + const userData = useUser(); + console.log(userData.userMeta); return ( @@ -229,6 +234,7 @@ const App = () => { + {(usePatchStore((state) => state.patchReady) && (userData.user == null || userData.userMeta?.newUser)) ? : <>} diff --git a/src/components/TopBar/SignUpButton/component.tsx b/src/components/TopBar/SignUpButton/component.tsx index 5a230ce..7f11ab7 100644 --- a/src/components/TopBar/SignUpButton/component.tsx +++ b/src/components/TopBar/SignUpButton/component.tsx @@ -41,6 +41,7 @@ export const SignUpButton = () => { projects: [], username: username, role: "user", + newUser: true }; const response = setDoc(reference, data, {merge: true, mergeFields: ["projects"]}); setProjectId("new"); diff --git a/src/components/Tutorial/component.tsx b/src/components/Tutorial/component.tsx new file mode 100644 index 0000000..5bb318d --- /dev/null +++ b/src/components/Tutorial/component.tsx @@ -0,0 +1,447 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { styled, useTheme } from '@mui/material/styles'; +import MobileStepper from '@mui/material/MobileStepper'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft'; +import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight'; +import CancelIcon from '@mui/icons-material/Cancel'; +import RemoveIcon from '@mui/icons-material/Remove'; +import AddIcon from '@mui/icons-material/Add'; +import Draggable from 'react-draggable'; +import { Modal } from '@mui/base'; +import { Backdrop, CardActions, CardContent, css } from '@mui/material'; +import { useUser } from '../../hooks/useUser'; +import { updateDoc } from 'firebase/firestore'; +import { EditorTab } from '../../store/patchEditorStore'; +import usePatchStore from '../../store'; +import { useEffect } from 'react'; +import { alignProperty } from '@mui/material/styles/cssUtils'; + + +//steps for self guided tour +const steps = [ + { + label: 'Welcome to Patch!', + description: `I\’m Patch the Penguin and I can walk you through getting started to code amazing projects on Patch!`, + }, + { + label: 'Stage Area', + description: + 'This is the stage area where all the action happens! Try clicking the flag to see the sample code run!', + }, + { + label: 'Stage Area', + description: `That’s some crazy color changing! Click the stop sign to stop running your code.`, + }, + { + label: 'Editor Area', + description: `The editor area allows us to add code. + Try deleting the default code and adding “move(10)”. Start typing the “m” + and you will notice autocomplete options appear.`, + }, + { + label: 'Errors', + description: + 'What happens if you delete the last parentheses on the “move(10)” command? Patch is showing you an error by underlining the incorrect code.', + }, + { + label: 'Threads', + description: `But what if you would like multiple scripts to run at once? Try threads. First save your current thread by clicking the “save thread” button.`, + }, + { + label: 'Threads', + description: + 'Now click the “new thread” button to create a new thread for Patch the Penguin.', + }, + { + label: 'Threads', + description: `At the top of the new thread change the starting condition from “When Flag Clicked” to “When Key Pressed”. Then choose the "UP ARROW" option.`, + }, + { + label: 'Threads', + description: + 'Now add code to make Patch move up whenever the up arrow is pressed. Type “changeY(10)” in the editor. Click the flag to run your code.', + }, + { + label: 'Sprites', + description: `Let’s give Patch a friend. In the sprite area click the plus button to add a sprite of your choice.`, + }, + { + label: 'Sounds', + description: `Let’s make the new sprite make a sound. Click on the sounds tab. Click on the plus button to add a sound.`, + }, + { + label: 'Sounds', + description: `Go back to the code editor to code the sound. Use the playSound function to play the sound you just added. Then run your project now to hear your sound.`, + }, + { + label: 'Save', + description: `Great! Let’s save your project to keep your progress. Press the File -> Save button to save your project.`, + }, + { + label: 'That\'s All!', + description: + 'That’s all for this tutorial. Have fun coding in Patch!', + }, +]; + + +//steps for guided tour +const stepsHandHold = [ + { + label: 'Welcome to Patch!', + description: `I\’m Patch the Penguin and I can walk you through getting started to code amazing projects on Patch!`, + tab: EditorTab.CODE, + width: 0, + height: 0, + x: 0, + y: 0, + image: 0 + }, + { + label: 'Stage Area', + description: + 'This is the stage area where all the action happens! Clicking the flag makes the code run! The stop sign makes the code stop running.', + tab: EditorTab.CODE, + width: 600, + height: 500, + x: "calc(100% - 610px)", + y: "50px", + image: 1 + }, + { + label: 'Editor Area', + description: `The editor area allows us to add code. You will notice as you start typing autocomplete options appear.`, + tab: EditorTab.CODE, + width: "calc(100% - 700px)", + height: "80%", + x: "80px", + y: "100px", + image: 3 + }, + { + label: 'Errors', + description: + 'What happens if you have incorrect code? Patch will show you an error by underlining the incorrect code.', + tab: EditorTab.CODE, + width: "calc(100% - 700px)", + height: "75px", + x: "80px", + y: "150px", + image: 4 + }, + { + label: 'Threads', + description: `But what if you would like multiple scripts to run at once? Try threads. You can save you current thread by clicking the “save thread” button.`, + tab: EditorTab.CODE, + width: "200px", + height: "60px", + x: "calc(100% - 825px)", + y: "100px", + image: 5 + }, + { + label: 'Threads', + description: + 'The “new thread” button creates a new thread for the selected sprite.', + tab: EditorTab.CODE, + width: "200px", + height: "60px", + x: "calc(100% - 825px)", + y: "100px", + image: 6 + }, + { + label: 'Threads', + description: `You can also change the starting condition. At the top of the new thread for example, you can change the dropdown to say “When Flag Clicked” or “When Key Pressed”.`, + tab: EditorTab.CODE, + width: "calc(100% - 900px)", + height: "60px", + x: "80px", + y: "100px", + image: 7 + }, + { + label: 'Sprites', + description: `In the sprite area you can click the plus button to add a sprite of your choice.`, + tab: EditorTab.CODE, + width: 600, + height: 400, + x: "calc(100% - 610px)", + y: "550px", + image: 9 + }, + { + label: 'Sounds', + description: `You can also add sounds by clicking on the sounds tab. Click on the plus button to add a sound.`, + tab: EditorTab.SOUNDS, + width: 220, + height: 60, + x: "80px", + y: "50px", + image: 10 + }, + { + label: 'Save', + description: `Press the File -> Save button to save your project.`, + tab: EditorTab.CODE, + width: 140, + height: 50, + x: "75px", + y: "5px", + image: 12 + }, + { + label: 'That\'s All!', + description: + 'That’s all for this tutorial. Have fun coding in Patch!', + tab: EditorTab.CODE, + image: 0 + }, +]; + + +//images for each step +const associatedImage = (currStep: number) => { + switch (currStep) { + case 0: + return (); + case 1: + return (); + case 2: + return (); + case 3: + return (); + case 4: + return (); + case 5: + return (); + case 6: + return (); + case 7: + return (); + case 8: + return (); + case 9: + return (); + case 10: + return (); + case 11: + return (); + case 12: + return (); + case 13: + return (); + + + } +} + + +//main function +export function Tutorial() { + + const [selfGuided, setSelfGuided] = React.useState(false); + const [handHold, setHandHold] = React.useState(false); + const [open, setOpen] = React.useState(true); + const [activeStep, setActiveStep] = React.useState(0); + const userRef = useUser().userReference; + const userData = useUser().userMeta; + const [show, setShow] = React.useState(true); + + const theme = useTheme(); + const [minimize, setMinimize] = React.useState(false); + + //Template for self guided tour + function SelfGuidedTutorial() { + const maxSteps = steps.length; + + return ( + show ? + + + + {steps[activeStep].label} +
+ {minimize ? setMinimize(false)} /> : <> setMinimize(true)} />} + { setSelfGuided(false); handleSkip(); }} /> +
+
+ {minimize ? <> : + <> + {steps[activeStep].description} + {associatedImage(activeStep)} + handleNextSelf(maxSteps)} + disabled={activeStep === maxSteps} + > + {activeStep === maxSteps - 1 ? "Finish" : "Next"} + {theme.direction === 'rtl' ? ( + + ) : ( + + )} + } + backButton={} />} +
: <> + ); + } + + //template for guided tour + function HandHoldTutorial() { + const setEditorTab = usePatchStore((state) => state.setEditorTab); + useEffect(() => { + setEditorTab(stepsHandHold[activeStep].tab); + }, [activeStep]); + return (show ? theme.zIndex.drawer + 1 }} + open={true} + onClick={() => { }}> + + +
+ + + {stepsHandHold[activeStep].label} + + + {stepsHandHold[activeStep].description} + + {associatedImage(stepsHandHold[activeStep].image)} + + + +
+
: <>); + + } + + const style = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + color: "white", + bgcolor: 'black', + display: "flex-col", + alignProperty: "center", + border: "1px solid rgba(255, 255, 255, 0.1)", + borderRadius: "5px", + padding: "5px", + + }; + + const StyledBackdrop = styled(Backdrop)` + z-index: -1; + position: fixed; + inset: 0; + background-color: rgb(0 0 0 / 0.5); + -webkit-tap-highlight-color: transparent; +`; + + // const ModalContent = styled('div')( + // ({ theme }) => css` + // text-align: start; + // position: relative; + // display: flex; + // flex-direction: column; + // gap: 8px; + // overflow: hidden; + // padding: 24px; + // `, + // ); + + + const handleNextSelf = async (maxSteps: number) => { + if (activeStep == maxSteps - 1) { + setShow(false); + if (userRef != null) { + updateDoc(userRef, { + newUser: false + }); + } + + } else { + setActiveStep((prevActiveStep) => prevActiveStep + 1); + } + }; + + const handleBackSelf = () => { + setActiveStep((prevActiveStep) => prevActiveStep - 1); + }; + + const handleSkip = () => { + setOpen(false); + if (userRef != null) { + updateDoc(userRef, { + newUser: false + }); + } + } + + + //main return + return (<> { setOpen(false) }} + slots={{ backdrop: StyledBackdrop }} + > +
+ + Welcome to Patch! How would you like to learn? + + + + +
+
+ {selfGuided ? : <>} + {handHold ? : <>} + ); +} diff --git a/src/components/Tutorial/index.ts b/src/components/Tutorial/index.ts new file mode 100644 index 0000000..7e4ff23 --- /dev/null +++ b/src/components/Tutorial/index.ts @@ -0,0 +1 @@ +export * from './component'; \ No newline at end of file diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts index 2819e0b..71bc3b8 100644 --- a/src/hooks/useUser.ts +++ b/src/hooks/useUser.ts @@ -1,7 +1,7 @@ import { useAuthState } from "react-firebase-hooks/auth"; import { useDocumentDataOnce } from "react-firebase-hooks/firestore"; import { auth, db } from "../lib/firebase"; -import { FirestoreError, doc } from "firebase/firestore"; +import { DocumentReference, FirestoreError, doc } from "firebase/firestore"; import { User } from "firebase/auth"; import { UserMeta } from "../types/userMeta"; @@ -10,6 +10,7 @@ type UseUserReturn = { userMeta: UserMeta | null; loading: boolean; error: Error | FirestoreError | null | undefined; + userReference: DocumentReference | null; } export const useUser = (): UseUserReturn => { @@ -17,5 +18,5 @@ export const useUser = (): UseUserReturn => { const userReference = user ? doc(db, 'users', user.uid) : null; const [data, loading, error] = useDocumentDataOnce(userReference); - return { user: user ?? null, userMeta: data as UserMeta, loading, error }; + return { user: user ?? null, userMeta: data as UserMeta, loading, error, userReference: userReference ?? null }; } \ No newline at end of file diff --git a/src/types/userMeta.ts b/src/types/userMeta.ts index 7f82d4f..39d0c66 100644 --- a/src/types/userMeta.ts +++ b/src/types/userMeta.ts @@ -13,4 +13,5 @@ export type UserMeta = { username: string; role?: UserRole; projects: ProjectProfile[]; + newUser: boolean; } \ No newline at end of file