Skip to content

Commit 3bd0438

Browse files
tyler-daneCopilot
andauthored
feat(onboarding): add interactive guide for someday tasks (#891)
* feat(web): refactor onboarding components to use new layouts and remove boilerplate * feat(web): enhance OnboardingTwoRowLayout with keyboard navigation support and refactor content structure * feat(web): enhance SomedaySandbox with task management features and animated text * refactor(web): rename taskColors to colors and update related functions in SomedaySandbox * feat(web): update event creation API to accept multiple event formats * feat(web): integrate SomedaySandbox into OnboardingDemo component * feat(web): enhance SomedaySandbox with event creation and keyboard navigation improvements * feat(web): add SomedaySandbox step to OnboardingDemo component * feat(web): improve task input handling in SomedaySandbox with dynamic limits and keyboard navigation * refactor(web): improve keyboard navigation handling in OnboardingTwoRowLayout and clean up SomedaySandbox event logic * refactor(web): implement useOnboardingShortcuts hook for improved navigation control in OnboardingTwoRowLayout and SomedaySandbox * refactor(web): update state management and variable naming in SomedaySandbox for clarity and consistency * refactor(web): remove unused keyboard event handling in Onboarding component * refactor(web): simplify navigation control and keyboard event handling in Onboarding components * refactor(web): streamline task management and remove unused editing functionality in SomedaySandbox * refactor(web): enhance OnboardingNextButton with pulsing animation and focus trapping * refactor(web): enhance focus management in SomedaySandbox for improved task input navigation * test(web): add unit tests for useOnboardingShortcuts hook to validate keyboard navigation behavior * refactor(web): enhance conditional rendering in SomedaySandbox for improved user guidance * test(web): add unit tests for SomedaySandbox component to validate task input functionality * refactor(web): enhance onboarding navigation control and keyboard shortcuts for improved user experience * refactor(web): improve onboarding step navigation control and state management for enhanced user experience * fix(web): update ResultText styling in Welcome component for improved visual consistency * fix(web): ensure next step triggers on key press after animation completion in Welcome component * refactor(web): improve onboarding button functionality and user guidance * docs(agents): add naming conventions for boolean variables * refactor(web): simplify onboarding navigation logic and update tests for clarity * feat(web): add keyboard event handling to onboarding steps for improved navigation * refactor(web): remove SetSomedayEventsSuccess component from onboarding steps * feat(web): implement disableLeftArrow functionality in onboarding steps for enhanced navigation control * feat(web): enhance keyboard navigation in SomedaySandbox with task validation * feat(web): implement submission prevention in SomedaySandbox to enhance user experience * refactor(web): remove keyboard event handling from SomedaySandbox to streamline navigation * refactor(web): add aria-labels to onboarding footer buttons for improved accessibility * feat(web): introduce SomedaySandbox component with task management and keyboard shortcuts for enhanced onboarding experience * chore(onboarding): fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore(onboarding): remove console.log Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 50ba7cb commit 3bd0438

42 files changed

Lines changed: 3609 additions & 263 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ TZ=Etc/UTC
187187

188188
Always prioritize frontend development with `yarn dev:web` when backend services are unavailable.
189189

190+
## Naming Conventions
191+
192+
- Use `is` prefix for boolean variables. For example, `isLoading`, `isError`, `isSuccess`
193+
190194
## Branch Naming & Commit Message Conventions
191195

192196
### Semantic Branch Naming

packages/web/src/ducks/events/event.api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import { CompassApi } from "@web/common/apis/compass.api";
99

1010
const EventApi = {
11-
create: (event: Schema_Event) => {
11+
create: (event: Schema_Event | Schema_Event[]) => {
1212
return CompassApi.post(`/event`, event);
1313
},
1414
delete: (_id: string) => {

packages/web/src/routers/index.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@ const router = createBrowserRouter([
2525
},
2626
{
2727
path: ROOT_ROUTES.LOGIN,
28-
element: (
29-
// <LoginView />
30-
<OnboardingDemo />
31-
),
28+
element: <OnboardingDemo />,
3229
},
3330
{ path: ROOT_ROUTES.LOGOUT, element: <LogoutView /> },
3431
{ path: "*", element: <NotFoundView /> },

packages/web/src/views/Onboarding/OnboardingDemo.tsx

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ import {
1212
SetReminder,
1313
SetReminderSuccess,
1414
SetSomedayEventTwo,
15-
SetSomedayEvents,
1615
SetSomedayEventsOne,
17-
SetSomedayEventsSuccess,
1816
SignInWithGoogle,
1917
SignInWithGooglePrelude,
2018
WaitlistCheck,
@@ -23,6 +21,7 @@ import {
2321
WelcomeScreen,
2422
WelcomeStep,
2523
} from "./steps";
24+
import { SomedaySandbox } from "./steps/events/SomedaySandbox/SomedaySandbox";
2625
import { ReminderIntroOne } from "./steps/reminder/ReminderIntroOne";
2726
import { ReminderIntroTwo } from "./steps/reminder/ReminderIntroTwo";
2827

@@ -55,6 +54,7 @@ const OnboardingDemo_: React.FC = () => {
5554
component: (props: OnboardingStepProps) => (
5655
<WelcomeScreen firstName="hello" {...props} />
5756
),
57+
disableLeftArrow: true,
5858
},
5959
{
6060
id: "welcome-note-one",
@@ -76,6 +76,8 @@ const OnboardingDemo_: React.FC = () => {
7676
<SignInWithGoogle {...props} />
7777
),
7878
disableRightArrow: true,
79+
preventNavigation: true,
80+
handlesKeyboardEvents: true,
7981
},
8082
{
8183
id: "reminder-intro-one",
@@ -112,20 +114,15 @@ const OnboardingDemo_: React.FC = () => {
112114
),
113115
},
114116
{
115-
id: "set-someday-events",
116-
component: (props: OnboardingStepProps) => (
117-
<SetSomedayEvents {...props} />
118-
),
119-
},
120-
{
121-
id: "set-someday-events-success",
122-
component: (props: OnboardingStepProps) => (
123-
<SetSomedayEventsSuccess {...props} />
124-
),
117+
id: "someday-sandbox",
118+
component: (props: OnboardingStepProps) => <SomedaySandbox {...props} />,
119+
preventNavigation: true,
125120
},
121+
126122
{
127123
id: "outro-two",
128124
component: (props: OnboardingStepProps) => <OutroTwo {...props} />,
125+
disableLeftArrow: true,
129126
},
130127
{
131128
id: "outro-quote",
@@ -135,7 +132,7 @@ const OnboardingDemo_: React.FC = () => {
135132
},
136133
];
137134

138-
// Initially hide the steps til the user logs in
135+
// Initially hide the steps until the user logs in
139136
useEffect(() => {
140137
setHideSteps(true);
141138
}, [setHideSteps]);

packages/web/src/views/Onboarding/components/IconButtons.tsx

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,65 @@
1-
import React from "react";
2-
import styled from "styled-components";
1+
import React, { useEffect, useRef } from "react";
2+
import styled, { css, keyframes } from "styled-components";
33
import { OnboardingButton } from "./styled";
44

5-
const IconButton = styled(OnboardingButton)`
5+
// Keyframes for pulsing animation
6+
const pulse = keyframes`
7+
0% {
8+
transform: scale(1);
9+
opacity: 1;
10+
}
11+
50% {
12+
transform: scale(1.05);
13+
opacity: 0.8;
14+
}
15+
100% {
16+
transform: scale(1);
17+
opacity: 1;
18+
}
19+
`;
20+
21+
const IconButton = styled(OnboardingButton)<{ $shouldPulse?: boolean }>`
622
padding: 0;
723
min-width: 0;
824
width: 24px;
925
height: 24px;
26+
transition: all 0.2s ease-in-out;
27+
28+
${({ $shouldPulse }) =>
29+
$shouldPulse &&
30+
css`
31+
animation: ${pulse} 2s ease-in-out infinite;
32+
`}
33+
34+
&:disabled {
35+
opacity: 0.5;
36+
cursor: not-allowed;
37+
pointer-events: none;
38+
}
1039
`;
1140

12-
export const OnboardingNextButton = (
13-
props: React.ButtonHTMLAttributes<HTMLButtonElement>,
14-
) => {
41+
interface OnboardingNextButtonProps
42+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
43+
shouldTrapFocus?: boolean;
44+
shouldPulse?: boolean;
45+
}
46+
47+
export const OnboardingNextButton: React.FC<OnboardingNextButtonProps> = ({
48+
shouldTrapFocus = false,
49+
shouldPulse = false,
50+
...props
51+
}) => {
52+
const buttonRef = useRef<HTMLButtonElement>(null);
53+
54+
useEffect(() => {
55+
if (shouldTrapFocus && buttonRef.current) {
56+
// Focus the button when focus trapping is enabled
57+
buttonRef.current.focus();
58+
}
59+
}, [shouldTrapFocus]);
60+
1561
return (
16-
<IconButton {...props}>
62+
<IconButton ref={buttonRef} $shouldPulse={shouldPulse} {...props}>
1763
<svg
1864
width="10"
1965
height="9"
@@ -30,9 +76,9 @@ export const OnboardingNextButton = (
3076
);
3177
};
3278

33-
export const OnboardingPreviousButton = (
34-
props: React.ButtonHTMLAttributes<HTMLButtonElement>,
35-
) => {
79+
export const OnboardingPreviousButton: React.FC<
80+
React.ButtonHTMLAttributes<HTMLButtonElement>
81+
> = (props) => {
3682
return (
3783
<IconButton {...props}>
3884
<svg

packages/web/src/views/Onboarding/components/Onboarding.tsx

Lines changed: 64 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React, { useEffect, useState } from "react";
1+
import React, { useEffect, useRef, useState } from "react";
22
import styled from "styled-components";
3-
import { Key } from "ts-key-enum";
3+
import { useOnboardingShortcuts } from "../hooks/useOnboardingShortcuts";
44

55
export const OnboardingRoot = styled.div`
66
// background: rgba(0, 0, 0, 0.8);
@@ -9,19 +9,25 @@ export const OnboardingRoot = styled.div`
99
width: 100vw;
1010
`;
1111

12-
export const OnboardingContainer = styled.div`
12+
interface OnboardingContainerProps {
13+
fullWidth?: boolean;
14+
}
15+
16+
export const OnboardingContainer = styled.div<OnboardingContainerProps>`
1317
background-color: #12151b;
1418
position: absolute;
15-
width: 900px;
16-
height: 800px;
17-
border-radius: 44px;
18-
top: 50%;
19-
left: 50%;
20-
transform: translate(-50%, -50%);
19+
width: ${({ fullWidth }) => (fullWidth ? "100vw" : "900px")};
20+
height: ${({ fullWidth }) => (fullWidth ? "100vh" : "800px")};
21+
border-radius: ${({ fullWidth }) => (fullWidth ? "0" : "44px")};
22+
top: ${({ fullWidth }) => (fullWidth ? "0" : "50%")};
23+
left: ${({ fullWidth }) => (fullWidth ? "0" : "50%")};
24+
transform: ${({ fullWidth }) =>
25+
fullWidth ? "none" : "translate(-50%, -50%)"};
2126
display: flex;
2227
align-items: center;
2328
justify-content: center;
24-
box-shadow: rgb(255 255 255 / 15%) 0px 9px 20px 1px;
29+
box-shadow: ${({ fullWidth }) =>
30+
fullWidth ? "none" : "rgb(255 255 255 / 15%) 0px 9px 20px 1px"};
2531
2632
@keyframes flicker {
2733
0%,
@@ -60,7 +66,8 @@ export const OnboardingContainer = styled.div`
6066
}
6167
}
6268
63-
animation: flicker 0.5s infinite;
69+
animation: ${({ fullWidth }) =>
70+
fullWidth ? "none" : "flicker 0.5s infinite"};
6471
6572
&::before {
6673
content: "";
@@ -73,7 +80,7 @@ export const OnboardingContainer = styled.div`
7380
background-size: 100% 4px;
7481
pointer-events: none;
7582
z-index: 1;
76-
border-radius: 44px;
83+
border-radius: ${({ fullWidth }) => (fullWidth ? "0" : "44px")};
7784
}
7885
`;
7986

@@ -84,6 +91,12 @@ export interface OnboardingStepProps {
8491
onPrevious: () => void;
8592
onComplete: () => void;
8693
onSkip: () => void;
94+
// New props for keyboard control
95+
canNavigateNext?: boolean;
96+
nextButtonDisabled?: boolean;
97+
onNavigationControlChange?: (shouldPrevent: boolean) => void;
98+
isNavPrevented?: boolean;
99+
handlesKeyboardEvents?: boolean;
87100
}
88101

89102
export interface OnboardingStep {
@@ -92,20 +105,21 @@ export interface OnboardingStep {
92105
onNext?: (data?: Record<string, unknown>) => void;
93106
disableLeftArrow?: boolean;
94107
disableRightArrow?: boolean;
108+
// Navigation control properties
109+
preventNavigation?: boolean;
110+
nextButtonDisabled?: boolean;
111+
canNavigateNext?: boolean;
112+
handlesKeyboardEvents?: boolean;
95113
}
96114

97115
interface Props {
98116
steps: OnboardingStep[];
99117
onComplete: (reason: "skip" | "complete") => void;
100-
className?: string;
101118
}
102119

103-
export const Onboarding: React.FC<Props> = ({
104-
steps,
105-
onComplete,
106-
className,
107-
}) => {
120+
export const Onboarding: React.FC<Props> = ({ steps, onComplete }) => {
108121
const [currentStepIndex, setCurrentStepIndex] = useState(0);
122+
const [isNavPrevented, setIsNavPrevented] = useState(false);
109123

110124
const handleNext = (data?: Record<string, unknown>) => {
111125
// Call `onNext` if provided
@@ -119,6 +133,11 @@ export const Onboarding: React.FC<Props> = ({
119133
};
120134

121135
const handlePrevious = () => {
136+
// Check if the current step disables left arrow navigation
137+
if (currentStep.disableLeftArrow) {
138+
return;
139+
}
140+
122141
if (currentStepIndex > 0) {
123142
setCurrentStepIndex(currentStepIndex - 1);
124143
}
@@ -135,44 +154,48 @@ export const Onboarding: React.FC<Props> = ({
135154
const currentStep = steps[currentStepIndex];
136155
const StepComponent = currentStep?.component;
137156

138-
useEffect(() => {
139-
const handleKeyDown = (event: KeyboardEvent) => {
140-
const currentStep = steps[currentStepIndex];
141-
142-
if (
143-
(event.key === Key.ArrowRight || event.key === Key.Enter) &&
144-
!currentStep?.disableRightArrow
145-
) {
146-
event.preventDefault();
147-
handleNext();
148-
} else if (event.key === "ArrowLeft" && !currentStep?.disableLeftArrow) {
149-
event.preventDefault();
150-
handlePrevious();
151-
}
152-
};
153-
154-
document.addEventListener("keydown", handleKeyDown);
155-
return () => document.removeEventListener("keydown", handleKeyDown);
156-
}, [currentStepIndex, steps.length, handleNext, handlePrevious]);
157-
158157
if (!StepComponent) {
159158
return null;
160159
}
161160

161+
// Get navigation control from step configuration
162+
const preventNavigation = currentStep.preventNavigation || false;
163+
const nextButtonDisabled = currentStep.nextButtonDisabled || false;
164+
const canNavigateNext = currentStep.canNavigateNext !== false; // Default to true
165+
const handlesKeyboardEvents = currentStep.handlesKeyboardEvents || false;
166+
167+
// Use the keyboard shortcuts hook
168+
useOnboardingShortcuts({
169+
onNext: handleNext,
170+
onPrevious: handlePrevious,
171+
canNavigateNext,
172+
shouldPreventNavigation: preventNavigation ? isNavPrevented : false,
173+
handlesKeyboardEvents,
174+
disableLeftArrow: currentStep.disableLeftArrow || false,
175+
});
176+
177+
// Handle navigation control changes from steps
178+
const handleNavigationControlChange = (shouldPrevent: boolean) => {
179+
setIsNavPrevented(shouldPrevent);
180+
};
181+
162182
const stepProps: OnboardingStepProps = {
163183
currentStep: currentStepIndex + 1,
164184
totalSteps: steps.length,
165185
onNext: handleNext,
166186
onPrevious: handlePrevious,
167187
onComplete: handleComplete,
168188
onSkip: handleSkip,
189+
canNavigateNext,
190+
nextButtonDisabled,
191+
onNavigationControlChange: handleNavigationControlChange,
192+
isNavPrevented: preventNavigation ? isNavPrevented : false,
193+
handlesKeyboardEvents,
169194
};
170195

171196
return (
172197
<OnboardingRoot>
173-
<OnboardingContainer className={className} id="onboarding-container">
174-
<StepComponent {...stepProps} />
175-
</OnboardingContainer>
198+
<StepComponent {...stepProps} />
176199
</OnboardingRoot>
177200
);
178201
};

0 commit comments

Comments
 (0)