From e654aa002b6dac4cecaed011c7b1d479d394c8a6 Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 27 Jun 2025 11:33:43 -0600 Subject: [PATCH 1/5] changed default spacing value back to 0, fixed some pageContainers using padding instead of spacing --- src/pages/Bin.tsx | 2 +- src/pages/PageContainer.tsx | 2 +- src/pages/Team.tsx | 2 +- src/pages/Teams.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index fc9463f..fcc1117 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -1107,7 +1107,7 @@ export default function Bin(props: Props) { }; return ( - + {showPlenumError()} = (props: Props) => { const { children, fullViewport, isCenterCenter, sx } = props; // let fullViewport = false // let isCenterCenter = false - const spacing = props.spacing ? props.spacing : 2; + const spacing = props.spacing ?? 0; return ( + {title()} diff --git a/src/pages/Teams.tsx b/src/pages/Teams.tsx index 2e74628..90e3401 100644 --- a/src/pages/Teams.tsx +++ b/src/pages/Teams.tsx @@ -5,7 +5,7 @@ import TeamList from "teams/TeamList"; export default function Teams() { const isMobile = useMobile() return ( - + ); From 24d27121d169b155604b01e9707fb0ff096c2ede Mon Sep 17 00:00:00 2001 From: Carter Date: Thu, 3 Jul 2025 13:41:09 -0600 Subject: [PATCH 2/5] added signup callback page that runs an api call to finish signup --- src/app/main.tsx | 4 +- src/navigation/Router.tsx | 3 ++ src/pages/SignupCallback.tsx | 87 ++++++++++++++++++++++++++++++++++ src/providers/auth.tsx | 49 ++++++++++++------- src/providers/pond/userAPI.tsx | 20 +++++++- 5 files changed, 142 insertions(+), 21 deletions(-) create mode 100644 src/pages/SignupCallback.tsx diff --git a/src/app/main.tsx b/src/app/main.tsx index e2acdd9..2d8615c 100644 --- a/src/app/main.tsx +++ b/src/app/main.tsx @@ -19,7 +19,7 @@ if ('serviceWorker' in navigator) { } createRoot(document.getElementById('root')!).render( - + // - , + // , ) diff --git a/src/navigation/Router.tsx b/src/navigation/Router.tsx index 29362ad..f6badb1 100644 --- a/src/navigation/Router.tsx +++ b/src/navigation/Router.tsx @@ -9,6 +9,7 @@ import { getWhitelabel } from "services/whiteLabel"; import Ventilation from "pages/VentEditor"; import { useGlobalState } from "providers"; import DeviceSupport from "pages/DeviceSupport"; +import SignupCallback from "pages/SignupCallback"; const DeviceHistory = lazy(() => import("pages/DeviceHistory")); const DevicePage = lazy(() => import("pages/Device")); @@ -302,6 +303,8 @@ export default function Router() { {/* Redirects */} } /> + {/* } /> */} + } /> {/* } /> */} {/* } /> */} {/* } /> */} diff --git a/src/pages/SignupCallback.tsx b/src/pages/SignupCallback.tsx new file mode 100644 index 0000000..78215f4 --- /dev/null +++ b/src/pages/SignupCallback.tsx @@ -0,0 +1,87 @@ +import { useDeviceAPI, useUserAPI } from "hooks"; +import PageContainer from "./PageContainer"; +import { useEffect, useState } from "react"; +import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider } from "@mui/material"; +import { useBinAPI, useTeamAPI } from "providers"; +import { CheckCircleOutline } from "@mui/icons-material"; +import { green } from "@mui/material/colors"; + +interface Props { + +} + +export default function SignupCallback (props: Props) { + const userAPI = useUserAPI(); + const teamAPI = useTeamAPI() + const deviceAPI = useDeviceAPI(); + const binAPI = useBinAPI(); + const [loading, setLoading] = useState(false) + const [signupDialogOpen, setSignupDialogOpen] = useState(true) + const [hasTeams, setHasTeams] = useState(false) + const [hasDevices, setHasDevices] = useState(false) + const [hasBins, setHasBins] = useState(false) + + const [doneBins, setDoneBins] = useState(false) + const [doneTeams, setDoneTeams] = useState(false) + const [doneDevices, setDoneDevices] = useState(false) + + useEffect(() => { + console.log(hasTeams) + }, [hasTeams]) + + useEffect(() => { + setLoading(true) + userAPI.completeSignup().then(resp => { + console.log(resp) + teamAPI.listTeams(1, 0).then(resp => { + setHasTeams(resp.data.teams.length > 0) + }).finally(() => setDoneTeams(true)) + deviceAPI.list(1, 0).then(resp => { + setHasDevices(resp.data.devices.length > 0) + }).finally(() => setDoneDevices(true)) + binAPI.listBins(1, 0).then(resp => { + setHasBins(resp.data.bins.length > 0) + }).finally(() => setDoneBins(true)) + }).finally(() => setLoading(false)) + }, []) + + const loadingComplete = () => { + return loading === false && doneBins && doneTeams && doneDevices + } + + const content = () => { + return ( + + { loadingComplete() ? + + : + + } + + {hasTeams && "Teams found!"} + {hasDevices && "Devices found!"} + {hasBins && "Bins found!"} + + + ) + } + + return ( + + + + Finishing Signup + + + { content() } + + {loadingComplete() && + + } + + + + ) +} \ No newline at end of file diff --git a/src/providers/auth.tsx b/src/providers/auth.tsx index 22ac76d..f7ee585 100644 --- a/src/providers/auth.tsx +++ b/src/providers/auth.tsx @@ -9,28 +9,30 @@ interface Props extends PropsWithChildren{ export default function AuthWrapper(props: Props) { const { children, setToken } = props; - const { isLoading, loginWithRedirect, isAuthenticated, getAccessTokenSilently } = useAuth0(); + const { isLoading, loginWithRedirect, isAuthenticated, getAccessTokenSilently, getIdTokenClaims } = useAuth0(); useEffect(() => { const url = new URL(window.location.href); - const isManualLoginPage = url.pathname.includes("/login"); + const options: RedirectLoginOptions = { + authorizationParams: {} // Initialize here + }; - if (isManualLoginPage) { - const options: RedirectLoginOptions = { - authorizationParams: {} // Initialize here - }; - + if (isManualLoginPage && options.authorizationParams) { + let parsed = queryString.parse(url.search); if (parsed.signup === "true") { - options.authorizationParams!.screen_hint = "signup"; - options.authorizationParams!.prompt = "login"; + options.authorizationParams.screen_hint = "signUp"; + // options.authorizationParams.m + // options.authorizationParams.prompt = "login"; } if (parsed.email && parsed.email !== "") { - options.authorizationParams!.login_hint = parsed.email.toString(); // prefill email + options.authorizationParams.login_hint = parsed.email.toString(); // prefill email } + + options.authorizationParams.redirect_uri = "http://localhost:5173/signupCallback" loginWithRedirect(options) @@ -40,15 +42,28 @@ export default function AuthWrapper(props: Props) { }, [isAuthenticated, loginWithRedirect, isLoading]); useEffect(() => { - if (isAuthenticated) getAccessTokenSilently().then(t => { - if (t.length < 1) { + const url = new URL(window.location.href); + const isSignupCallback = url.pathname.includes("/signupCallback"); + if (isAuthenticated) { + getAccessTokenSilently().then(t => { + if (t.length < 1) { + loginWithRedirect() + } + setToken(t) + }).catch(() => { + // No token, go back to login loginWithRedirect() + }) + if (isSignupCallback) { + console.log("IS SIGNUP") + getIdTokenClaims().then(id_claims => { + console.log(id_claims) + console.log(id_claims?.nickname) + if (id_claims) console.log(Object.keys(id_claims)) + }) } - setToken(t) - }).catch(() => { - // No token, go back to login - loginWithRedirect() - }) + + } }, [setToken, isAuthenticated]) return children; diff --git a/src/providers/pond/userAPI.tsx b/src/providers/pond/userAPI.tsx index 247f77c..21befb2 100644 --- a/src/providers/pond/userAPI.tsx +++ b/src/providers/pond/userAPI.tsx @@ -28,6 +28,7 @@ export interface IUserAPIContext { prefixSearch?: string, ) => Promise; listProfiles: () => Promise; + completeSignup: () => Promise; listObjectUsers: (scope: Scope) => Promise; updateObjectUsers: (scope: Scope, users: pond.IUser[]) => Promise; getUser: (id: string, scope?: Scope) => Promise; @@ -55,8 +56,22 @@ export const UserAPIContext = createContext({} as IUserAPIConte export default function UserProvider(props: PropsWithChildren) { const { children } = props; - const { get, put } = useHTTP(); - const [{ as }] = useGlobalState(); + const { get, put, post } = useHTTP(); + const [{ as, user }] = useGlobalState(); + + const completeSignup = (): Promise => { + return new Promise((resolve, reject) => { + let request = pond.CompleteSignupUserRequest.create(); + request.avatar = user.settings.avatar + request.email = user.settings.email + request.id = user.id() + request.name = user.name() + post(pondURL("/signups"), request).then(res => { + console.log(res) + resolve(pond.CompleteSignupUserResponse.create()) + }) + }) + } const listUsers = ( limit: number, @@ -234,6 +249,7 @@ export default function UserProvider(props: PropsWithChildren) { return ( Date: Mon, 7 Jul 2025 11:57:53 -0600 Subject: [PATCH 3/5] added dark streamline logo, minor styling adjustments --- public/Streamline/stream-logo-dark.png | Bin 0 -> 26259 bytes src/device/SyncDevice.tsx | 5 ++++- src/pages/SignupCallback.tsx | 10 +++------- 3 files changed, 7 insertions(+), 8 deletions(-) create mode 100644 public/Streamline/stream-logo-dark.png diff --git a/public/Streamline/stream-logo-dark.png b/public/Streamline/stream-logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a486c1e727ee6ec98c99138cca3cb6ebee18898e GIT binary patch literal 26259 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 { + close() + }) }; return ( diff --git a/src/pages/SignupCallback.tsx b/src/pages/SignupCallback.tsx index 78215f4..00b2c51 100644 --- a/src/pages/SignupCallback.tsx +++ b/src/pages/SignupCallback.tsx @@ -1,4 +1,4 @@ -import { useDeviceAPI, useUserAPI } from "hooks"; +import { useDeviceAPI, useMobile, useUserAPI } from "hooks"; import PageContainer from "./PageContainer"; import { useEffect, useState } from "react"; import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider } from "@mui/material"; @@ -15,6 +15,7 @@ export default function SignupCallback (props: Props) { const teamAPI = useTeamAPI() const deviceAPI = useDeviceAPI(); const binAPI = useBinAPI(); + const isMobile = useMobile() const [loading, setLoading] = useState(false) const [signupDialogOpen, setSignupDialogOpen] = useState(true) const [hasTeams, setHasTeams] = useState(false) @@ -25,14 +26,9 @@ export default function SignupCallback (props: Props) { const [doneTeams, setDoneTeams] = useState(false) const [doneDevices, setDoneDevices] = useState(false) - useEffect(() => { - console.log(hasTeams) - }, [hasTeams]) - useEffect(() => { setLoading(true) userAPI.completeSignup().then(resp => { - console.log(resp) teamAPI.listTeams(1, 0).then(resp => { setHasTeams(resp.data.teams.length > 0) }).finally(() => setDoneTeams(true)) @@ -68,7 +64,7 @@ export default function SignupCallback (props: Props) { return ( - + Finishing Signup From 4f8b0254bb29b02bf9908e4147d5b65217c09b07 Mon Sep 17 00:00:00 2001 From: Carter Date: Tue, 8 Jul 2025 14:07:01 -0600 Subject: [PATCH 4/5] added a welcome tour to the signupCallback page --- package-lock.json | 56 ++++++++++++ package.json | 1 + src/common/Tour.tsx | 30 +++++-- src/navigation/SideNavigator.tsx | 8 +- src/pages/Firmware.tsx | 4 +- src/pages/SignupCallback.tsx | 148 +++++++++++++++++++++++++++++-- src/pages/Users.tsx | 2 +- src/providers/auth.tsx | 27 +++--- src/providers/pond/userAPI.tsx | 1 - src/user/UserMenu.tsx | 2 +- 10 files changed, 247 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 65ae1cc..840ea14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "react-color": "^2.19.3", "react-day-picker": "^9.6.5", "react-dom": "^18.3.1", + "react-emoji-render": "^2.0.1", "react-error-boundary": "^5.0.0", "react-full-screen": "^1.1.1", "react-horizontal-scrolling-menu": "^7.1.1", @@ -9188,6 +9189,15 @@ "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": { "version": "3.0.5", "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", "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": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -11107,6 +11129,29 @@ "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": { "version": "5.0.0", "integrity": "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==", @@ -12408,6 +12453,17 @@ "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": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", diff --git a/package.json b/package.json index 67d05d8..afae078 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "react-color": "^2.19.3", "react-day-picker": "^9.6.5", "react-dom": "^18.3.1", + "react-emoji-render": "^2.0.1", "react-error-boundary": "^5.0.0", "react-full-screen": "^1.1.1", "react-horizontal-scrolling-menu": "^7.1.1", diff --git a/src/common/Tour.tsx b/src/common/Tour.tsx index 4781e21..9d328a7 100644 --- a/src/common/Tour.tsx +++ b/src/common/Tour.tsx @@ -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 { Button, Card, @@ -10,12 +10,13 @@ import { Tooltip, IconButton, Theme, - useTheme + useTheme, } from "@mui/material"; import { KeyboardArrowLeft, KeyboardArrowRight, Close as CloseIcon } from "@mui/icons-material"; import classNames from "classnames"; import { grey } from "@mui/material/colors"; import { makeStyles } from "@mui/styles"; +import { CSSProperties } from "react"; const useStyles = makeStyles((theme: Theme) => { 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 { continuous, @@ -114,18 +119,30 @@ const StepContainer = (props: TooltipRenderProps, numSteps: number, skip: () => interface Props { run: boolean; - steps: Step[]; + steps: TourStep[]; endTourCallback: () => void; + spotlightStyle?: CSSProperties, + setStepIndex?: React.Dispatch>, } export default function Tour(props: Props) { - const { run, steps, endTourCallback } = props; + const { run, steps, endTourCallback, setStepIndex, spotlightStyle } = props; const theme = useTheme(); const handleTourCallback = (data: CallBackProps) => { - const { status } = data; + const { action, index, status, type } = data; 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)) { endTourCallback(); } @@ -150,6 +167,9 @@ export default function Tour(props: Props) { primaryColor: theme.palette.primary.main, textColor: theme.palette.text.primary, zIndex: theme.zIndex.modal + }, + spotlight: { + ...spotlightStyle } }} /> diff --git a/src/navigation/SideNavigator.tsx b/src/navigation/SideNavigator.tsx index 38a10cb..f71ca9c 100644 --- a/src/navigation/SideNavigator.tsx +++ b/src/navigation/SideNavigator.tsx @@ -221,7 +221,7 @@ export default function SideNavigator(props: Props) { {(isAg || isStreamline || user.hasFeature("admin")) && ( goTo("/bins")} classes={getClasses("/bin")} > @@ -277,7 +277,7 @@ export default function SideNavigator(props: Props) { {(isMiVent || user.hasFeature("admin")) && ( goTo("/mines")} classes={getClasses("/mine")} > @@ -330,7 +330,7 @@ export default function SideNavigator(props: Props) { } goTo("/teams")} classes={getClasses("/team")} > @@ -342,7 +342,7 @@ export default function SideNavigator(props: Props) { goTo("/users")} classes={getClasses("/user")} > diff --git a/src/pages/Firmware.tsx b/src/pages/Firmware.tsx index b3a7c58..1ae97af 100644 --- a/src/pages/Firmware.tsx +++ b/src/pages/Firmware.tsx @@ -12,7 +12,7 @@ import { TypeBackground } from "@mui/material"; 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 { ArrowUpward, @@ -267,7 +267,7 @@ export default function Firmware() { if (selected) { firmwareAPI .removeFirmware(selected.settings.platform, selected.settings.version) - .then((response: any) => { + .then(() => { success("Successfully removed firmware"); }) .catch((err: any) => { diff --git a/src/pages/SignupCallback.tsx b/src/pages/SignupCallback.tsx index 00b2c51..a22dbfc 100644 --- a/src/pages/SignupCallback.tsx +++ b/src/pages/SignupCallback.tsx @@ -1,26 +1,36 @@ import { useDeviceAPI, useMobile, useUserAPI } from "hooks"; import PageContainer from "./PageContainer"; import { useEffect, useState } from "react"; -import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider } from "@mui/material"; -import { useBinAPI, useTeamAPI } from "providers"; +import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Typography } from "@mui/material"; +import { useBinAPI, useGlobalState, useTeamAPI } from "providers"; import { CheckCircleOutline } from "@mui/icons-material"; 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 teamAPI = useTeamAPI() const deviceAPI = useDeviceAPI(); const binAPI = useBinAPI(); const isMobile = useMobile() + const [{ user }] = useGlobalState(); + const whiteLabel = getWhitelabel() + const [loading, setLoading] = useState(false) const [signupDialogOpen, setSignupDialogOpen] = useState(true) const [hasTeams, setHasTeams] = useState(false) const [hasDevices, setHasDevices] = useState(false) const [hasBins, setHasBins] = useState(false) + const [isTourRunning, setIsTourRunning] = useState(false); + const [stepIndex, setStepIndex] = useState(0) const [doneBins, setDoneBins] = useState(false) const [doneTeams, setDoneTeams] = useState(false) @@ -28,7 +38,7 @@ export default function SignupCallback (props: Props) { useEffect(() => { setLoading(true) - userAPI.completeSignup().then(resp => { + userAPI.completeSignup().then(() => { teamAPI.listTeams(1, 0).then(resp => { setHasTeams(resp.data.teams.length > 0) }).finally(() => setDoneTeams(true)) @@ -55,13 +65,131 @@ export default function SignupCallback (props: Props) { } {hasTeams && "Teams found!"} +
{hasDevices && "Devices found!"} +
{hasBins && "Bins found!"}
) } + const getTourSteps = (): TourStep[] => { + let steps: TourStep[] = [ + { + title: ( + <> + Welcome to {whiteLabel.name} + + + ), + content: ( + <> + + {"Hello " + user.name()} + + + + Thank you for signing up! Let me give you the tour + + + + ), + target: "body", + placement: "center", + disableBeacon: true, + }, + { + title: "User Menu", + content: ( + <> + + In the user menu, you can make changes to your profile, and adjust unit preferences. + +
+ + If you are part of a team, you can select it here as well. + + + ), + target: "#tour-user-menu", + placement: "bottom", + disableBeacon: true, + // onNext: () => setDoneFirst(true) + }, + // { + // title: + // } + ]; + if (hasTeams) steps.push({ + title: "Teams", + content: ( + <> + + Here, you can view your team(s), access their settings, and choose your favourite. + + + ), + target: "#tour-teams", + placement: "right", + // spotlightPadding: 50, + }) + steps.push({ + title: "Devices", + content: ( + <> + + You can view your devices and their readings here. + +
+ + If you are viewing as a team, your team's devices will be displayed here instead. + + + ), + target: "#tour-dashboard", + placement: "right" + }) + if (IsAdaptiveAgriculture()) steps.push({ + title: "Bins", + content: ( + <> + + You can view your bins and their inventory here. + +
+ + If you are viewing as a team, your team's bins will be displayed here instead. + + + ), + 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 ( @@ -72,12 +200,18 @@ export default function SignupCallback (props: Props) { { content() } {loadingComplete() && - } + ) } \ No newline at end of file diff --git a/src/pages/Users.tsx b/src/pages/Users.tsx index 00ef9f7..296c60a 100644 --- a/src/pages/Users.tsx +++ b/src/pages/Users.tsx @@ -349,7 +349,7 @@ export default function Users() { } return ( - + title="Users" rows={rows} diff --git a/src/providers/auth.tsx b/src/providers/auth.tsx index f7ee585..2a8f6bb 100644 --- a/src/providers/auth.tsx +++ b/src/providers/auth.tsx @@ -9,7 +9,13 @@ interface Props extends PropsWithChildren{ export default function AuthWrapper(props: Props) { const { children, setToken } = props; - const { isLoading, loginWithRedirect, isAuthenticated, getAccessTokenSilently, getIdTokenClaims } = useAuth0(); + const { + isLoading, + loginWithRedirect, + isAuthenticated, + getAccessTokenSilently, + // getIdTokenClaims + } = useAuth0(); useEffect(() => { @@ -42,8 +48,8 @@ export default function AuthWrapper(props: Props) { }, [isAuthenticated, loginWithRedirect, isLoading]); useEffect(() => { - const url = new URL(window.location.href); - const isSignupCallback = url.pathname.includes("/signupCallback"); + // const url = new URL(window.location.href); + // const isSignupCallback = url.pathname.includes("/signupCallback"); if (isAuthenticated) { getAccessTokenSilently().then(t => { if (t.length < 1) { @@ -54,14 +60,13 @@ export default function AuthWrapper(props: Props) { // No token, go back to login loginWithRedirect() }) - if (isSignupCallback) { - console.log("IS SIGNUP") - getIdTokenClaims().then(id_claims => { - console.log(id_claims) - console.log(id_claims?.nickname) - if (id_claims) console.log(Object.keys(id_claims)) - }) - } + // if (isSignupCallback) { + // getIdTokenClaims().then(id_claims => { + // console.log(id_claims) + // console.log(id_claims?.nickname) + // if (id_claims) console.log(Object.keys(id_claims)) + // }) + // } } }, [setToken, isAuthenticated]) diff --git a/src/providers/pond/userAPI.tsx b/src/providers/pond/userAPI.tsx index 21befb2..0d33b2e 100644 --- a/src/providers/pond/userAPI.tsx +++ b/src/providers/pond/userAPI.tsx @@ -67,7 +67,6 @@ export default function UserProvider(props: PropsWithChildren) { request.id = user.id() request.name = user.name() post(pondURL("/signups"), request).then(res => { - console.log(res) resolve(pond.CompleteSignupUserResponse.create()) }) }) diff --git a/src/user/UserMenu.tsx b/src/user/UserMenu.tsx index 2b41a6f..c7b8ab3 100644 --- a/src/user/UserMenu.tsx +++ b/src/user/UserMenu.tsx @@ -228,7 +228,7 @@ export default function UserMenu() { */} {hasTeams && ( - + From 0127eeefc75d40e450eb6ab525a779f94a669191 Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 16 Jul 2025 13:32:08 -0600 Subject: [PATCH 5/5] view as team notification pops up if they don't have devices or bins but have a team --- src/pages/Bins.tsx | 76 +++++++++++++++++++++++++------------ src/pages/DeviceSupport.tsx | 5 ++- src/pages/Devices.tsx | 26 ++++++++++++- src/teams/TeamDialog.tsx | 58 +++++++++++++++++++++++++--- 4 files changed, 130 insertions(+), 35 deletions(-) diff --git a/src/pages/Bins.tsx b/src/pages/Bins.tsx index 8f77613..476b66f 100644 --- a/src/pages/Bins.tsx +++ b/src/pages/Bins.tsx @@ -18,7 +18,6 @@ import { ImageList, ImageListItem, InputLabel, - lighten, ListItemIcon, ListItemText, Menu, @@ -26,8 +25,6 @@ import { Select, Skeleton, Theme, - ToggleButton, - ToggleButtonGroup, Typography, useTheme, } from "@mui/material"; @@ -61,7 +58,8 @@ import { // useBinYardAPI, useGlobalState, useInteractionsAPI, - useSnackbar + useSnackbar, + useTeamAPI } from "providers"; import React, { SetStateAction, useCallback, useEffect, useState } from "react"; // import { getThemeType } from "theme"; @@ -72,7 +70,7 @@ import ObjectTable from "objects/ObjectTable"; import ResponsiveDialog from "common/ResponsiveDialog"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { green, yellow } from "@mui/material/colors"; -import { makeStyles, styled } from "@mui/styles"; +import { makeStyles } from "@mui/styles"; import { useNavigate } from "react-router-dom"; import BinSettings from "bin/BinSettings"; import AddBinFab from "bin/AddBinFab"; @@ -82,6 +80,7 @@ import GrainBagSettings from "grainBag/grainBagSettings"; import GrainBagList from "grainBag/grainBagList"; import BinYards from "bin/BinYards"; import ButtonGroup from "common/ButtonGroup"; +import TeamDialog from "teams/TeamDialog"; // import { useHistory } from "react-router"; const useStyles = makeStyles((theme: Theme) => { @@ -141,11 +140,13 @@ export default function Bins(props: Props) { const classes = useStyles(); const isMobile = useMobile(); const binAPI = useBinAPI(); + const teamAPI = useTeamAPI(); // const binyardAPI = useBinYardAPI(); const interactionsAPI = useInteractionsAPI(); const [{ user, as }] = useGlobalState(); const [allLoading, setAllLoading] = useState(false); const [binsLoading, setBinsLoading] = useState(false); + const [doneLoadingPage, setDoneLoadingPage] = useState(false) const [addBinOpen, setAddBinOpen] = useState(false); const [paginatedBins, setPaginatedBins] = useState({ bins: [], @@ -188,13 +189,16 @@ export default function Bins(props: Props) { const [qrKeyList, setQrKeyList] = useState([]); const [totalFanControllers, setTotalFanControllers] = useState(0); const [totalFanControllersOn, setTotalFanControllersOn] = useState(0); - const [yardsLoading, setYardsLoading] = useState(false); + // const [yardsLoading, setYardsLoading] = useState(false); const [openAddAlerts, setOpenAddAlerts] = useState(false); const [alertBins, setAlertBins] = useState([]); const { openSnack } = useSnackbar(); // const history = useHistory(); const navigate = useNavigate() const [cardValDisplay, setCardValDisplay] = useState<"high" | "low" | "average">("average"); + + const [teamsDialog, setTeamsDialog] = useState(false) + // const [scrollTranslations, setScrollTranslations] = useState({ // bins: 0, // empty: 0, @@ -252,6 +256,18 @@ export default function Bins(props: Props) { // selected: {} // })); + useEffect(() => { + if (doneLoadingPage && grainBins.length < 1 && grainBags.length < 1) { + teamAPI.listTeams(1, 0).then(resp => { + // console.log(resp.data.teams) + if (resp.data.teams.length > 0) { + // console.log("should?") + setTeamsDialog(true) + } + }) + } + }, [doneLoadingPage, grainBins, grainBags]) + useEffect(() => { let ebt = sessionStorage.getItem("expandBinTotal"); if (ebt === "true") { @@ -400,7 +416,7 @@ export default function Bins(props: Props) { const bags = resp.data.bags.map(b => GrainBag.create(b)); setGrainBags(bags); }) - .catch(err => { + .catch(() => { setPaginatedBins({ bins: [], binsOffset: 0, binsTotal: 0 }); setBinMetrics(pond.BinMetrics.create()); setYards([]); @@ -408,6 +424,7 @@ export default function Bins(props: Props) { .finally(() => { setAllLoading(false); setBinsLoading(false); + setDoneLoadingPage(true) }); }, [binAPI, yardFilter, order, orderBy, as, loadingAs, loadingFilter]); // eslint-disable-line react-hooks/exhaustive-deps @@ -454,7 +471,7 @@ export default function Bins(props: Props) { setFertilizerBins([...fert]); setQrKeyList([...newKeyList]); }) - .catch(err => { + .catch(() => { setPaginatedBins({ bins: [], binsOffset: 0, binsTotal: 0 }); }) .finally(() => { @@ -462,13 +479,13 @@ export default function Bins(props: Props) { }); }; - const searchYards = ( - search?: string, - limit?: number, - offset?: number, - order?: "asc" | "desc" - ) => { - setYardsLoading(true); + // const searchYards = ( + // search?: string, + // limit?: number, + // offset?: number, + // order?: "asc" | "desc" + // ) => { + // setYardsLoading(true); // binyardAPI.listBinYards(limit ?? binsLimit, offset ?? 0, order, "name", search).then(resp => { // let y: pond.BinYardSettings[] = []; // let p: Dictionary = {}; @@ -493,7 +510,7 @@ export default function Bins(props: Props) { // } // setYardsLoading(false); // }); - }; + // }; useEffect(() => { loadBins(); @@ -515,7 +532,7 @@ export default function Bins(props: Props) { } const duplicateBin = (bin: Bin) => { - binAPI.addBin(bin.settings, as).then(resp => { + binAPI.addBin(bin.settings, as).then(() => { loadBins(); }); }; @@ -556,7 +573,7 @@ export default function Bins(props: Props) { .then(resp => { openSnack("Successfully added " + resp.data.numAlertsSet + " alerts"); }) - .catch(err => { + .catch(() => { console.log("errors found"); }); }; @@ -1080,7 +1097,7 @@ export default function Bins(props: Props) { control={ { + onChange={(_, checked) => { setDisplayGrain(checked); }} /> @@ -1093,7 +1110,7 @@ export default function Bins(props: Props) { control={ { + onChange={(_, checked) => { setDisplayFert(checked); }} /> @@ -1106,7 +1123,7 @@ export default function Bins(props: Props) { control={ { + onChange={(_, checked) => { setDisplayEmpty(checked); }} /> @@ -1392,15 +1409,17 @@ export default function Bins(props: Props) { {addALertsDialog()} { - searchYards(search, limit, offset); - }} + // loadYards={(search, limit, offset) => { + // searchYards(search, limit, offset); + // }} + loadYards={() => {}} grainBags={grainBags} setShowing={(val: "all" | "bins" | "bags") => { setShowing(val); @@ -1473,6 +1492,13 @@ export default function Bins(props: Props) { /> {binMenu()} setAddBinOpen(true)} pulse={binsTotal < 1 && !allLoading} /> +
); }; diff --git a/src/pages/DeviceSupport.tsx b/src/pages/DeviceSupport.tsx index a6a19de..6c9a0db 100644 --- a/src/pages/DeviceSupport.tsx +++ b/src/pages/DeviceSupport.tsx @@ -3,7 +3,7 @@ import PageContainer from "./PageContainer"; import { pond } from "protobuf-ts/pond"; import LogsDisplay from "common/LogsDisplay"; import { useEffect, useState } from "react"; -import { useDeviceAPI, useSnackbar } from "hooks"; +import { useDeviceAPI, useMobile, useSnackbar } from "hooks"; import { useLocation, useParams } from "react-router-dom"; import { Component, Device, Interaction } from "models"; import { getContextKeys, getContextTypes } from "pbHelpers/Context"; @@ -43,6 +43,7 @@ export default function DeviceSupport() { const deviceAPI = useDeviceAPI() const snackbar = useSnackbar() const theme = useTheme() + const isMobile = useMobile() const { state } = useLocation(); const [{ user, as }] = useGlobalState() @@ -188,7 +189,7 @@ export default function DeviceSupport() { } return ( - + diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index fe47b3c..7721ec1 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -5,7 +5,7 @@ import { makeStyles } from "@mui/styles"; import ResponsiveTable, { Column } from "common/ResponsiveTable"; import ProvisionDevice from "device/ProvisionDevice"; import { pond } from "protobuf-ts/pond"; -import { useDeviceAPI, useGlobalState, useGroupAPI } from "providers"; +import { useDeviceAPI, useGlobalState, useGroupAPI, useTeamAPI } from "providers"; import { useCallback, useEffect, useState } from "react"; import PageContainer from "./PageContainer"; import { useMobile } from "hooks"; @@ -28,6 +28,7 @@ import StatusDust from "common/StatusDust"; import StatusGas from "common/StatusGas"; import { cloneDeep } from "lodash"; import LoadingScreen from "app/LoadingScreen"; +import TeamDialog from "teams/TeamDialog"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -98,9 +99,12 @@ export default function Devices() { const location = useLocation(); const deviceAPI = useDeviceAPI(); const groupAPI = useGroupAPI(); + const teamAPI = useTeamAPI(); const [devicesLoading, setDevicesLoading] = useState(false) const [limit, setLimit] = useState(10); const [page, setPage] = useState(0); + const [doneLoading, setDoneLoading] = useState(false) + const [teamsDialog, setTeamsDialog] = useState(false) const [{ user }] = useGlobalState() @@ -186,7 +190,7 @@ export default function Devices() { const [preferencesAnchor, setPreferencesAnchor] = useState(null) - const [{ as }] = useGlobalState() + const [{ as, team }] = useGlobalState() const updateGroups = (newGroups: Group[]) => { setGroups(newGroups); @@ -327,6 +331,7 @@ export default function Devices() { setTotal(resp.data.total) }).finally(() => { setDevicesLoading(false) + setDoneLoading(true) }) } @@ -338,6 +343,16 @@ export default function Devices() { loadGroups() }, [groupLimit, groupPage, orderGroup, orderGroupBy, searchGroup, as]) + useEffect(() => { + if (doneLoading && devices.length < 1 && groups.length < 1) { + teamAPI.listTeams(1, 0).then(resp => { + if (resp.data.teams.length > 0 && team.key().length < 1) { + setTeamsDialog(true); + } + }) + } + }, [doneLoading, devices, groups]) + const handleChange = (event: any) => { setLimit(event.target.value); }; @@ -826,6 +841,13 @@ export default function Devices() { addGroupCallback={addGroupCallback} /> {preferencesMenu()} + ) } \ No newline at end of file diff --git a/src/teams/TeamDialog.tsx b/src/teams/TeamDialog.tsx index 25ebb9c..edc2124 100644 --- a/src/teams/TeamDialog.tsx +++ b/src/teams/TeamDialog.tsx @@ -1,6 +1,9 @@ import { AppBar, Button, + Checkbox, + FormControlLabel, + FormGroup, IconButton, Theme, Toolbar, @@ -18,6 +21,9 @@ import { makeStyles } from "@mui/styles"; interface Props { open: boolean; setOpen: (value: React.SetStateAction) => void; + title?: string; + info?: string; + hideKey?: string; } const useStyles = makeStyles((theme: Theme) => { @@ -37,16 +43,22 @@ const useStyles = makeStyles((theme: Theme) => { }); export default function TeamDialog(props: Props) { - const { open, setOpen } = props; + const { open, setOpen, title, info, hideKey } = props; const teamAPI = useTeamAPI(); const userAPI = useUserAPI(); const [, dispatch] = useGlobalState(); const [teamKey, setTeamKey] = useState(""); const [{ team, user }] = useGlobalState(); + const [viewAsTeam, setViewAsTeam] = useState(user.settings.useTeam) + const [doNotShow, setDoNotShow] = useState(false) const classes = useStyles(); + const shouldHide = hideKey ? + (localStorage.getItem(hideKey) === "true" ? true : false) + : false + const setTeam = () => { - setOpen(false); + onClose() dispatch({ key: "as", value: teamKey }); if (teamKey === "" || teamKey.includes("auth")) { dispatch({ key: "team", value: Team.create() }); @@ -55,13 +67,19 @@ export default function TeamDialog(props: Props) { teamAPI.getTeam(teamKey).then(team => { dispatch({ key: "team", value: team }); user.settings.defaultTeam = team.key(); - user.settings.useTeam = true; //when selecting a team now set to view as team by default + user.settings.useTeam = viewAsTeam; //when selecting a team now set to view as team by default userAPI.updateUser(user.id(), user.protobuf()); }); }; + const onClose = () => { + console.log("close") + setOpen(false) + if (hideKey) localStorage.setItem(hideKey, ""+doNotShow) + } + return ( - setOpen(false)}> + - Select a Team to View As + { title ? title : "Select a Team to View As"} - @@ -84,12 +102,40 @@ export default function TeamDialog(props: Props) { Viewing as: {team.name()} )} + {info && + + {info} + + } + {/* setViewAsTeam(!viewAsTeam)} /> */} + + setViewAsTeam(!viewAsTeam)} + /> + } + label="View as team" + /> + + {hideKey && + setDoNotShow(!doNotShow)} + /> + } + label="Do not show again" + /> + } ); }