Skip to content

Commit 7bc107d

Browse files
authored
feat(web-planner): add routing shortcuts and navigation (#1141)
* feat(web): refactor shortcuts overlay and add new components for improved keyboard shortcut management * feat(web): enhance keyboard shortcut management with type-safe handlers and improved configuration * refactor(web): streamline shortcut types and imports for better organization * feat(web): add NowView component and routing for real-time updates * refactor(web): update shortcuts overlay tests and enhance keyboard shortcut handling * feat(web): implement NowView component with keyboard shortcuts and overlay * refactor(web): rename TodayView to DayView * feat(web): implement useWeekShortcuts for keyboard navigation in week view * feat(web): integrate feature flags into useWeekShortcuts for conditional navigation * refactor(web): rename Today shortcut to Day in shortcuts and tests * refactor(web): simplify CSS properties import in NowView component
1 parent 1c94527 commit 7bc107d

28 files changed

+1147
-350
lines changed

packages/web/src/common/constants/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export const ROOT_ROUTES = {
44
LOGOUT: "/logout",
55
ROOT: "/",
66
DAY: "/day",
7+
NOW: "/now",
78
};

packages/web/src/routers/index.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
1-
import React from "react";
21
import { RouterProvider, createBrowserRouter } from "react-router-dom";
32
import { ProtectedRoute } from "@web/auth/ProtectedRoute";
43
import { UserProvider } from "@web/auth/UserContext";
54
import { ROOT_ROUTES } from "@web/common/constants/routes";
65
import SocketProvider from "@web/socket/SocketProvider";
76
import { LogoutView } from "@web/views/Logout";
87
import { NotFoundView } from "@web/views/NotFound";
8+
import { NowView } from "@web/views/Now/NowView";
99
import OnboardingFlow from "@web/views/Onboarding/OnboardingFlow";
1010
import { RootView } from "@web/views/Root";
11-
import { TodayView } from "@web/views/Today/view/TodayView";
11+
import { DayView } from "@web/views/Today/view/DayView";
1212

1313
const router = createBrowserRouter(
1414
[
15+
{
16+
path: ROOT_ROUTES.NOW,
17+
element: (
18+
<ProtectedRoute>
19+
<UserProvider>
20+
<SocketProvider>
21+
<NowView />
22+
</SocketProvider>
23+
</UserProvider>
24+
</ProtectedRoute>
25+
),
26+
},
1527
{
1628
path: ROOT_ROUTES.ROOT,
1729
element: (
@@ -30,7 +42,7 @@ const router = createBrowserRouter(
3042
<ProtectedRoute>
3143
<UserProvider>
3244
<SocketProvider>
33-
<TodayView />
45+
<DayView />
3446
</SocketProvider>
3547
</UserProvider>
3648
</ProtectedRoute>

packages/web/src/views/Calendar/Calendar.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import React from "react";
21
import { ID_MAIN } from "@web/common/constants/web.constants";
32
import { ContextMenuWrapper } from "@web/components/ContextMenu/GridContextMenuWrapper";
43
import { FlexDirections } from "@web/components/Flex/styled";

packages/web/src/views/Calendar/components/Shortcuts.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
ShortcutProps,
33
useShortcuts,
44
} from "@web/views/Calendar/hooks/shortcuts/useShortcuts";
5+
import { useWeekShortcuts } from "@web/views/Week/useWeekShortcuts";
56

67
export function Shortcuts({
78
children,
@@ -11,6 +12,7 @@ export function Shortcuts({
1112
shortcutsProps: ShortcutProps;
1213
}) {
1314
useShortcuts(shortcutsProps);
15+
useWeekShortcuts();
1416

1517
return children;
1618
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import "@testing-library/jest-dom";
2+
import { render, screen } from "@testing-library/react";
3+
import { NowView } from "./NowView";
4+
5+
// Mock the useNowShortcuts hook
6+
jest.mock("./useNowShortcuts", () => ({
7+
useNowShortcuts: jest.fn(),
8+
}));
9+
10+
describe("NowView", () => {
11+
it("renders the shortcuts overlay", () => {
12+
render(<NowView />);
13+
14+
expect(
15+
screen.getByRole("complementary", { name: "Shortcut overlay" }),
16+
).toBeInTheDocument();
17+
expect(screen.getByText("Shortcuts")).toBeInTheDocument();
18+
});
19+
20+
it("renders global shortcuts", () => {
21+
render(<NowView />);
22+
23+
expect(screen.getByText("Global")).toBeInTheDocument();
24+
expect(screen.getByText("Now")).toBeInTheDocument();
25+
expect(screen.getByText("Day")).toBeInTheDocument();
26+
expect(screen.getByText("Week")).toBeInTheDocument();
27+
});
28+
29+
it("renders shortcut keys correctly", () => {
30+
render(<NowView />);
31+
32+
// Check that shortcut keys are rendered
33+
expect(screen.getByText("1")).toBeInTheDocument();
34+
expect(screen.getByText("2")).toBeInTheDocument();
35+
expect(screen.getByText("3")).toBeInTheDocument();
36+
});
37+
38+
it("renders the main content", () => {
39+
render(<NowView />);
40+
41+
expect(screen.getByText("Coming Soon")).toBeInTheDocument();
42+
});
43+
});
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { CSSProperties, useMemo } from "react";
2+
import { ShortcutSection } from "../Today/components/Shortcuts/components/ShortcutSection";
3+
import { getShortcuts } from "../Today/components/Shortcuts/data/shortcuts.data";
4+
import { useNowShortcuts } from "./useNowShortcuts";
5+
6+
export const NowView = () => {
7+
// Initialize keyboard shortcuts
8+
useNowShortcuts();
9+
10+
// Get shortcuts for the Now view
11+
const { global } = getShortcuts({ isNow: true });
12+
13+
// Generate particles with random properties
14+
const particles = useMemo(() => {
15+
const particleCount = 120; // Increased from 60 to 120
16+
return Array.from({ length: particleCount }, (_, i) => ({
17+
id: i,
18+
size: Math.random() * 8 + 1, // 1-9px (increased range)
19+
left: Math.random() * 100, // 0-100%
20+
animationDelay: Math.random() * 25, // 0-25s delay (increased)
21+
duration: Math.random() * 15 + 10, // 10-25s duration (varied more)
22+
}));
23+
}, []);
24+
25+
return (
26+
<div className="fixed inset-0 overflow-hidden bg-gradient-to-b from-slate-900 via-blue-900 to-slate-900">
27+
{/* Custom CSS animations */}
28+
<style>
29+
{`
30+
@keyframes wave1 {
31+
0%, 100% {
32+
transform: translateX(-50%) translateY(0px) rotate(0deg);
33+
border-radius: 0% 0% 50% 50% / 0% 0% 100% 100%;
34+
}
35+
25% {
36+
transform: translateX(-30%) translateY(-15px) rotate(0.5deg);
37+
border-radius: 0% 0% 60% 40% / 0% 0% 80% 100%;
38+
}
39+
50% {
40+
transform: translateX(-20%) translateY(-8px) rotate(0deg);
41+
border-radius: 0% 0% 40% 60% / 0% 0% 100% 80%;
42+
}
43+
75% {
44+
transform: translateX(-40%) translateY(-12px) rotate(-0.5deg);
45+
border-radius: 0% 0% 70% 30% / 0% 0% 90% 100%;
46+
}
47+
}
48+
49+
@keyframes wave2 {
50+
0%, 100% {
51+
transform: translateX(-50%) translateY(0px) rotate(0deg);
52+
border-radius: 0% 0% 60% 40% / 0% 0% 100% 100%;
53+
}
54+
33% {
55+
transform: translateX(-20%) translateY(-20px) rotate(-0.8deg);
56+
border-radius: 0% 0% 50% 50% / 0% 0% 80% 100%;
57+
}
58+
66% {
59+
transform: translateX(-40%) translateY(-5px) rotate(0.8deg);
60+
border-radius: 0% 0% 70% 30% / 0% 0% 90% 100%;
61+
}
62+
}
63+
64+
@keyframes wave3 {
65+
0%, 100% {
66+
transform: translateX(-50%) translateY(0px) rotate(0deg);
67+
border-radius: 0% 0% 70% 30% / 0% 0% 100% 100%;
68+
}
69+
20% {
70+
transform: translateX(-10%) translateY(-25px) rotate(1.2deg);
71+
border-radius: 0% 0% 80% 20% / 0% 0% 70% 100%;
72+
}
73+
40% {
74+
transform: translateX(-30%) translateY(-15px) rotate(-0.8deg);
75+
border-radius: 0% 0% 60% 40% / 0% 0% 90% 100%;
76+
}
77+
60% {
78+
transform: translateX(-50%) translateY(-8px) rotate(0.5deg);
79+
border-radius: 0% 0% 50% 50% / 0% 0% 100% 80%;
80+
}
81+
80% {
82+
transform: translateX(-70%) translateY(-20px) rotate(-1.2deg);
83+
border-radius: 0% 0% 90% 10% / 0% 0% 80% 100%;
84+
}
85+
}
86+
87+
@keyframes particleFloat {
88+
0% {
89+
transform: translateY(100vh) translateX(0px) rotate(0deg) scale(0.5);
90+
opacity: 0;
91+
}
92+
5% {
93+
opacity: 0.3;
94+
transform: translateY(95vh) translateX(var(--drift, 0px)) rotate(45deg) scale(0.7);
95+
}
96+
15% {
97+
opacity: 0.8;
98+
transform: translateY(85vh) translateX(calc(var(--drift, 0px) * 1.2)) rotate(90deg) scale(1);
99+
}
100+
85% {
101+
opacity: 0.6;
102+
transform: translateY(15vh) translateX(calc(var(--drift, 0px) * 1.5)) rotate(315deg) scale(0.9);
103+
}
104+
100% {
105+
transform: translateY(-100px) translateX(calc(var(--drift, 0px) * 2)) rotate(360deg) scale(0.3);
106+
opacity: 0;
107+
}
108+
}
109+
110+
.wave-1 {
111+
animation: wave1 8s ease-in-out infinite;
112+
}
113+
114+
.wave-2 {
115+
animation: wave2 12s ease-in-out infinite;
116+
}
117+
118+
.wave-3 {
119+
animation: wave3 16s ease-in-out infinite;
120+
}
121+
122+
.particle {
123+
animation: particleFloat var(--duration) linear infinite;
124+
animation-delay: var(--delay);
125+
}
126+
`}
127+
</style>
128+
129+
{/* Ocean Wave Layers */}
130+
<div className="absolute inset-0 z-0">
131+
{/* Wave Layer 1 - Deepest */}
132+
<div
133+
className="wave-1 absolute bottom-0 left-0 h-40 w-[200%] bg-gradient-to-r from-blue-600/20 via-cyan-500/30 to-blue-800/20"
134+
style={{
135+
background:
136+
"linear-gradient(90deg, rgba(37, 99, 235, 0.2) 0%, rgba(6, 182, 212, 0.3) 25%, rgba(30, 64, 175, 0.2) 50%, rgba(6, 182, 212, 0.3) 75%, rgba(37, 99, 235, 0.2) 100%)",
137+
clipPath: "polygon(0 100%, 100% 100%, 100% 60%, 0 80%)",
138+
}}
139+
/>
140+
141+
{/* Wave Layer 2 - Middle */}
142+
<div
143+
className="wave-2 absolute bottom-0 left-0 h-32 w-[200%] bg-gradient-to-r from-cyan-400/25 via-blue-500/35 to-teal-600/25"
144+
style={{
145+
background:
146+
"linear-gradient(90deg, rgba(34, 211, 238, 0.25) 0%, rgba(59, 130, 246, 0.35) 25%, rgba(13, 148, 136, 0.25) 50%, rgba(59, 130, 246, 0.35) 75%, rgba(34, 211, 238, 0.25) 100%)",
147+
clipPath: "polygon(0 100%, 100% 100%, 100% 70%, 0 85%)",
148+
}}
149+
/>
150+
151+
{/* Wave Layer 3 - Surface */}
152+
<div
153+
className="wave-3 absolute bottom-0 left-0 h-24 w-[200%] bg-gradient-to-r from-cyan-300/30 via-blue-400/40 to-teal-500/30"
154+
style={{
155+
background:
156+
"linear-gradient(90deg, rgba(103, 232, 249, 0.3) 0%, rgba(96, 165, 250, 0.4) 25%, rgba(20, 184, 166, 0.3) 50%, rgba(96, 165, 250, 0.4) 75%, rgba(103, 232, 249, 0.3) 100%)",
157+
clipPath: "polygon(0 100%, 100% 100%, 100% 80%, 0 90%)",
158+
}}
159+
/>
160+
</div>
161+
162+
{/* Particle System */}
163+
<div className="absolute inset-0 z-10">
164+
{particles.map((particle) => (
165+
<div
166+
key={particle.id}
167+
className="particle absolute rounded-full bg-white/60"
168+
style={
169+
{
170+
width: `${particle.size}px`,
171+
height: `${particle.size}px`,
172+
left: `${particle.left}%`,
173+
bottom: "0px",
174+
"--delay": `${particle.animationDelay}s`,
175+
"--duration": `${particle.duration}s`,
176+
"--drift": `${(Math.random() - 0.5) * 100}px`,
177+
} as CSSProperties
178+
}
179+
/>
180+
))}
181+
</div>
182+
183+
{/* Shortcuts Overlay */}
184+
<aside
185+
aria-label="Shortcut overlay"
186+
className="fixed top-24 left-3 z-30 hidden w-[240px] rounded-lg border border-white/10 bg-[#1e1e1e]/90 p-3 shadow-lg backdrop-blur-sm md:block"
187+
>
188+
<div className="mb-2 text-xs font-medium text-white">Shortcuts</div>
189+
<ShortcutSection title="Global" shortcuts={global} />
190+
</aside>
191+
192+
{/* Coming Soon Text */}
193+
<div className="absolute inset-0 z-20 flex items-center justify-center">
194+
<h1 className="text-center text-7xl font-bold text-white drop-shadow-2xl">
195+
<span className="bg-gradient-to-r from-cyan-300 via-blue-400 to-teal-300 bg-clip-text text-transparent">
196+
Coming Soon
197+
</span>
198+
</h1>
199+
</div>
200+
</div>
201+
);
202+
};

0 commit comments

Comments
 (0)