added a welcome tour to the signupCallback page

This commit is contained in:
Carter 2025-07-08 14:07:01 -06:00
parent c9bf0d11ad
commit 4f8b0254bb
10 changed files with 247 additions and 32 deletions

56
package-lock.json generated
View file

@ -49,6 +49,7 @@
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-day-picker": "^9.6.5", "react-day-picker": "^9.6.5",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-emoji-render": "^2.0.1",
"react-error-boundary": "^5.0.0", "react-error-boundary": "^5.0.0",
"react-full-screen": "^1.1.1", "react-full-screen": "^1.1.1",
"react-horizontal-scrolling-menu": "^7.1.1", "react-horizontal-scrolling-menu": "^7.1.1",
@ -9188,6 +9189,15 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/is-array-buffer": { "node_modules/is-array-buffer": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@ -9971,6 +9981,18 @@
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
}, },
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.memoize": { "node_modules/lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@ -11107,6 +11129,29 @@
"react": "^18.3.1" "react": "^18.3.1"
} }
}, },
"node_modules/react-emoji-render": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/react-emoji-render/-/react-emoji-render-2.0.1.tgz",
"integrity": "sha512-SKtsdwgEf2BFNiE9y4UBFZBWjkRcyWmhMprVly52+J77/zxThcfaQ3sCA0+2LtGJIRMpm4DGWSvyLg72fd1rXQ==",
"license": "MIT",
"dependencies": {
"classnames": "^2.2.5",
"emoji-regex": "^8.0.0",
"lodash.flatten": "^4.4.0",
"prop-types": "^15.5.8",
"string-replace-to-array": "^1.0.1"
},
"peerDependencies": {
"react": ">=0.14.0",
"react-dom": ">=0.14.0"
}
},
"node_modules/react-emoji-render/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/react-error-boundary": { "node_modules/react-error-boundary": {
"version": "5.0.0", "version": "5.0.0",
"integrity": "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==", "integrity": "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==",
@ -12408,6 +12453,17 @@
"safe-buffer": "~5.2.0" "safe-buffer": "~5.2.0"
} }
}, },
"node_modules/string-replace-to-array": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string-replace-to-array/-/string-replace-to-array-1.0.3.tgz",
"integrity": "sha512-QIdKvmfXbdFvblXAAz6IIjR7A+C6SU6m2A+e7fE/0EYDC5yfeWNMJQ193fPsW7nG+9q52dv/UjnVrDaNVZXpmQ==",
"license": "MIT",
"dependencies": {
"invariant": "^2.2.1",
"lodash.flatten": "^4.2.0",
"lodash.isstring": "^4.0.1"
}
},
"node_modules/string.prototype.matchall": { "node_modules/string.prototype.matchall": {
"version": "4.0.12", "version": "4.0.12",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",

View file

@ -61,6 +61,7 @@
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-day-picker": "^9.6.5", "react-day-picker": "^9.6.5",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-emoji-render": "^2.0.1",
"react-error-boundary": "^5.0.0", "react-error-boundary": "^5.0.0",
"react-full-screen": "^1.1.1", "react-full-screen": "^1.1.1",
"react-horizontal-scrolling-menu": "^7.1.1", "react-horizontal-scrolling-menu": "^7.1.1",

View file

@ -1,4 +1,4 @@
import Joyride, { Step, CallBackProps, STATUS, TooltipRenderProps, Locale } from "react-joyride"; import Joyride, { Step, CallBackProps, STATUS, TooltipRenderProps, Locale, ACTIONS, EVENTS } from "react-joyride";
import { import {
Button, Button,
Card, Card,
@ -10,12 +10,13 @@ import {
Tooltip, Tooltip,
IconButton, IconButton,
Theme, Theme,
useTheme useTheme,
} from "@mui/material"; } from "@mui/material";
import { KeyboardArrowLeft, KeyboardArrowRight, Close as CloseIcon } from "@mui/icons-material"; import { KeyboardArrowLeft, KeyboardArrowRight, Close as CloseIcon } from "@mui/icons-material";
import classNames from "classnames"; import classNames from "classnames";
import { grey } from "@mui/material/colors"; import { grey } from "@mui/material/colors";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { CSSProperties } from "react";
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
return ({ return ({
@ -46,6 +47,10 @@ const useStyles = makeStyles((theme: Theme) => {
}) })
}); });
export interface TourStep extends Step {
onNext?: () => void; // Define onNext as an optional function
}
const StepContainer = (props: TooltipRenderProps, numSteps: number, skip: () => void) => { const StepContainer = (props: TooltipRenderProps, numSteps: number, skip: () => void) => {
const { const {
continuous, continuous,
@ -114,18 +119,30 @@ const StepContainer = (props: TooltipRenderProps, numSteps: number, skip: () =>
interface Props { interface Props {
run: boolean; run: boolean;
steps: Step[]; steps: TourStep[];
endTourCallback: () => void; endTourCallback: () => void;
spotlightStyle?: CSSProperties,
setStepIndex?: React.Dispatch<React.SetStateAction<number>>,
} }
export default function Tour(props: Props) { export default function Tour(props: Props) {
const { run, steps, endTourCallback } = props; const { run, steps, endTourCallback, setStepIndex, spotlightStyle } = props;
const theme = useTheme(); const theme = useTheme();
const handleTourCallback = (data: CallBackProps) => { const handleTourCallback = (data: CallBackProps) => {
const { status } = data; const { action, index, status, type } = data;
const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED]; const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED];
if (setStepIndex) setStepIndex(index)
if (action === ACTIONS.NEXT && type === EVENTS.STEP_AFTER) {
// Safely access the step and call onNext if it exists
const currentStep = steps[index];
if (currentStep.onNext) {
currentStep.onNext();
}
}
if (finishedStatuses.includes(status)) { if (finishedStatuses.includes(status)) {
endTourCallback(); endTourCallback();
} }
@ -150,6 +167,9 @@ export default function Tour(props: Props) {
primaryColor: theme.palette.primary.main, primaryColor: theme.palette.primary.main,
textColor: theme.palette.text.primary, textColor: theme.palette.text.primary,
zIndex: theme.zIndex.modal zIndex: theme.zIndex.modal
},
spotlight: {
...spotlightStyle
} }
}} }}
/> />

View file

@ -221,7 +221,7 @@ export default function SideNavigator(props: Props) {
{(isAg || isStreamline || user.hasFeature("admin")) && ( {(isAg || isStreamline || user.hasFeature("admin")) && (
<Tooltip title="Bins" placement="right"> <Tooltip title="Bins" placement="right">
<ListItemButton <ListItemButton
id="tour-dashboard" id="tour-bins"
onClick={() => goTo("/bins")} onClick={() => goTo("/bins")}
classes={getClasses("/bin")} classes={getClasses("/bin")}
> >
@ -277,7 +277,7 @@ export default function SideNavigator(props: Props) {
{(isMiVent || user.hasFeature("admin")) && ( {(isMiVent || user.hasFeature("admin")) && (
<Tooltip title="Mines" placement="right"> <Tooltip title="Mines" placement="right">
<ListItemButton <ListItemButton
id="tour-dashboard" id="tour-mines"
onClick={() => goTo("/mines")} onClick={() => goTo("/mines")}
classes={getClasses("/mine")} classes={getClasses("/mine")}
> >
@ -330,7 +330,7 @@ export default function SideNavigator(props: Props) {
} }
<Tooltip title="Teams" placement="right"> <Tooltip title="Teams" placement="right">
<ListItemButton <ListItemButton
id="tour-dashboard" id="tour-teams"
onClick={() => goTo("/teams")} onClick={() => goTo("/teams")}
classes={getClasses("/team")} classes={getClasses("/team")}
> >
@ -342,7 +342,7 @@ export default function SideNavigator(props: Props) {
</Tooltip> </Tooltip>
<Tooltip title="Users" placement="right"> <Tooltip title="Users" placement="right">
<ListItemButton <ListItemButton
id="tour-dashboard" id="tour-users"
onClick={() => goTo("/users")} onClick={() => goTo("/users")}
classes={getClasses("/user")} classes={getClasses("/user")}
> >

View file

@ -12,7 +12,7 @@ import {
TypeBackground TypeBackground
} from "@mui/material"; } from "@mui/material";
import { blue, green, orange, red } from "@mui/material/colors"; import { blue, green, orange, red } from "@mui/material/colors";
import { createStyles, makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
//import { TypeBackground } from "@material-ui/core/styles/createPalette"; //import { TypeBackground } from "@material-ui/core/styles/createPalette";
import { import {
ArrowUpward, ArrowUpward,
@ -267,7 +267,7 @@ export default function Firmware() {
if (selected) { if (selected) {
firmwareAPI firmwareAPI
.removeFirmware(selected.settings.platform, selected.settings.version) .removeFirmware(selected.settings.platform, selected.settings.version)
.then((response: any) => { .then(() => {
success("Successfully removed firmware"); success("Successfully removed firmware");
}) })
.catch((err: any) => { .catch((err: any) => {

View file

@ -1,26 +1,36 @@
import { useDeviceAPI, useMobile, useUserAPI } from "hooks"; import { useDeviceAPI, useMobile, useUserAPI } from "hooks";
import PageContainer from "./PageContainer"; import PageContainer from "./PageContainer";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider } from "@mui/material"; import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Typography } from "@mui/material";
import { useBinAPI, useTeamAPI } from "providers"; import { useBinAPI, useGlobalState, useTeamAPI } from "providers";
import { CheckCircleOutline } from "@mui/icons-material"; import { CheckCircleOutline } from "@mui/icons-material";
import { green } from "@mui/material/colors"; import { green } from "@mui/material/colors";
import Tour, { TourStep } from "common/Tour";
import Emoji from "react-emoji-render";
import { getWhitelabel, IsAdaptiveAgriculture } from "services/whiteLabel";
// import { color } from "framer-motion";
interface Props { // interface Props {
} // }
export default function SignupCallback (props: Props) { // export default function SignupCallback (props: Props) {
export default function SignupCallback () {
const userAPI = useUserAPI(); const userAPI = useUserAPI();
const teamAPI = useTeamAPI() const teamAPI = useTeamAPI()
const deviceAPI = useDeviceAPI(); const deviceAPI = useDeviceAPI();
const binAPI = useBinAPI(); const binAPI = useBinAPI();
const isMobile = useMobile() const isMobile = useMobile()
const [{ user }] = useGlobalState();
const whiteLabel = getWhitelabel()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [signupDialogOpen, setSignupDialogOpen] = useState(true) const [signupDialogOpen, setSignupDialogOpen] = useState(true)
const [hasTeams, setHasTeams] = useState(false) const [hasTeams, setHasTeams] = useState(false)
const [hasDevices, setHasDevices] = useState(false) const [hasDevices, setHasDevices] = useState(false)
const [hasBins, setHasBins] = useState(false) const [hasBins, setHasBins] = useState(false)
const [isTourRunning, setIsTourRunning] = useState(false);
const [stepIndex, setStepIndex] = useState(0)
const [doneBins, setDoneBins] = useState(false) const [doneBins, setDoneBins] = useState(false)
const [doneTeams, setDoneTeams] = useState(false) const [doneTeams, setDoneTeams] = useState(false)
@ -28,7 +38,7 @@ export default function SignupCallback (props: Props) {
useEffect(() => { useEffect(() => {
setLoading(true) setLoading(true)
userAPI.completeSignup().then(resp => { userAPI.completeSignup().then(() => {
teamAPI.listTeams(1, 0).then(resp => { teamAPI.listTeams(1, 0).then(resp => {
setHasTeams(resp.data.teams.length > 0) setHasTeams(resp.data.teams.length > 0)
}).finally(() => setDoneTeams(true)) }).finally(() => setDoneTeams(true))
@ -55,13 +65,131 @@ export default function SignupCallback (props: Props) {
} }
<DialogContentText sx={{marginTop: 2}}> <DialogContentText sx={{marginTop: 2}}>
{hasTeams && "Teams found!"} {hasTeams && "Teams found!"}
<br/>
{hasDevices && "Devices found!"} {hasDevices && "Devices found!"}
<br/>
{hasBins && "Bins found!"} {hasBins && "Bins found!"}
</DialogContentText> </DialogContentText>
</DialogContent> </DialogContent>
) )
} }
const getTourSteps = (): TourStep[] => {
let steps: TourStep[] = [
{
title: (
<>
Welcome to {whiteLabel.name}
<Emoji text=" 🎉" />
</>
),
content: (
<>
<Typography variant="subtitle2" gutterBottom>
{"Hello " + user.name()}
<Emoji text=" 👋" />
</Typography>
<Typography variant="body2">
Thank you for signing up! Let me give you the tour
<Emoji text=" 😊" />
</Typography>
</>
),
target: "body",
placement: "center",
disableBeacon: true,
},
{
title: "User Menu",
content: (
<>
<Typography variant="body2" >
In the user menu, you can make changes to your profile, and adjust unit preferences.
</Typography>
<br/>
<Typography variant="body2" >
If you are part of a team, you can select it here as well.
</Typography>
</>
),
target: "#tour-user-menu",
placement: "bottom",
disableBeacon: true,
// onNext: () => setDoneFirst(true)
},
// {
// title:
// }
];
if (hasTeams) steps.push({
title: "Teams",
content: (
<>
<Typography variant="body2" >
Here, you can view your team(s), access their settings, and choose your favourite.
</Typography>
</>
),
target: "#tour-teams",
placement: "right",
// spotlightPadding: 50,
})
steps.push({
title: "Devices",
content: (
<>
<Typography variant="body2" >
You can view your devices and their readings here.
</Typography>
<br/>
<Typography>
If you are viewing as a team, your team's devices will be displayed here instead.
</Typography>
</>
),
target: "#tour-dashboard",
placement: "right"
})
if (IsAdaptiveAgriculture()) steps.push({
title: "Bins",
content: (
<>
<Typography variant="body2" >
You can view your bins and their inventory here.
</Typography>
<br/>
<Typography>
If you are viewing as a team, your team's bins will be displayed here instead.
</Typography>
</>
),
target: "#tour-bins",
placement: "right",
disableBeacon: true,
})
return steps;
};
const endTour = () => {
setIsTourRunning(false);
// if (user) {
// user.status.finishedBinIntro = moment().toJSON();
// userAPI
// .updateUser(userID, user.protobuf())
// .then(() => dispatch({ key: "user", value: user }))
// .catch((err: any) => error(err));
// }
};
const closeDialog = () => {
setSignupDialogOpen(false)
setIsTourRunning(true)
}
useEffect(() => {
console.log(stepIndex)
}, [stepIndex])
return ( return (
<PageContainer spacing={1}> <PageContainer spacing={1}>
<Dialog open={signupDialogOpen} fullScreen={isMobile} > <Dialog open={signupDialogOpen} fullScreen={isMobile} >
@ -72,12 +200,18 @@ export default function SignupCallback (props: Props) {
{ content() } { content() }
<DialogActions> <DialogActions>
{loadingComplete() && {loadingComplete() &&
<Button onClick={() => setSignupDialogOpen(false)}> <Button onClick={closeDialog}>
Done Done
</Button> </Button>
} }
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<Tour
setStepIndex={setStepIndex}
spotlightStyle={{ marginTop: stepIndex!==1 ? 74 : 1 }}
run={isTourRunning} steps={getTourSteps()}
endTourCallback={endTour}
/>
</PageContainer> </PageContainer>
) )
} }

View file

@ -349,7 +349,7 @@ export default function Users() {
} }
return ( return (
<PageContainer padding={isMobile ? 0 : 2}> <PageContainer spacing={isMobile ? 0 : 2}>
<ResponsiveTable<User> <ResponsiveTable<User>
title="Users" title="Users"
rows={rows} rows={rows}

View file

@ -9,7 +9,13 @@ interface Props extends PropsWithChildren<any>{
export default function AuthWrapper(props: Props) { export default function AuthWrapper(props: Props) {
const { children, setToken } = props; const { children, setToken } = props;
const { isLoading, loginWithRedirect, isAuthenticated, getAccessTokenSilently, getIdTokenClaims } = useAuth0(); const {
isLoading,
loginWithRedirect,
isAuthenticated,
getAccessTokenSilently,
// getIdTokenClaims
} = useAuth0();
useEffect(() => { useEffect(() => {
@ -42,8 +48,8 @@ export default function AuthWrapper(props: Props) {
}, [isAuthenticated, loginWithRedirect, isLoading]); }, [isAuthenticated, loginWithRedirect, isLoading]);
useEffect(() => { useEffect(() => {
const url = new URL(window.location.href); // const url = new URL(window.location.href);
const isSignupCallback = url.pathname.includes("/signupCallback"); // const isSignupCallback = url.pathname.includes("/signupCallback");
if (isAuthenticated) { if (isAuthenticated) {
getAccessTokenSilently().then(t => { getAccessTokenSilently().then(t => {
if (t.length < 1) { if (t.length < 1) {
@ -54,14 +60,13 @@ export default function AuthWrapper(props: Props) {
// No token, go back to login // No token, go back to login
loginWithRedirect() loginWithRedirect()
}) })
if (isSignupCallback) { // if (isSignupCallback) {
console.log("IS SIGNUP") // getIdTokenClaims().then(id_claims => {
getIdTokenClaims().then(id_claims => { // console.log(id_claims)
console.log(id_claims) // console.log(id_claims?.nickname)
console.log(id_claims?.nickname) // if (id_claims) console.log(Object.keys(id_claims))
if (id_claims) console.log(Object.keys(id_claims)) // })
}) // }
}
} }
}, [setToken, isAuthenticated]) }, [setToken, isAuthenticated])

View file

@ -67,7 +67,6 @@ export default function UserProvider(props: PropsWithChildren<any>) {
request.id = user.id() request.id = user.id()
request.name = user.name() request.name = user.name()
post(pondURL("/signups"), request).then(res => { post(pondURL("/signups"), request).then(res => {
console.log(res)
resolve(pond.CompleteSignupUserResponse.create()) resolve(pond.CompleteSignupUserResponse.create())
}) })
}) })

View file

@ -228,7 +228,7 @@ export default function UserMenu() {
<ListItemText primary="Toggle Theme" /> <ListItemText primary="Toggle Theme" />
</MenuItem> */} </MenuItem> */}
{hasTeams && ( {hasTeams && (
<MenuItem onClick={openTeamDialog} aria-label="Open Team Menu" dense> <MenuItem id="tour-teams" onClick={openTeamDialog} aria-label="Open Team Menu" dense>
<ListItemIcon> <ListItemIcon>
<TeamIcon /> <TeamIcon />
</ListItemIcon> </ListItemIcon>