Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
300 changes: 294 additions & 6 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.7",
"next": "^14.2.35",
"next": "^9.3.3",
"next": "^14.2.35",
"next-auth": "^4.24.14",
"react": "^18",
"react-dom": "^18",
Expand Down
2 changes: 2 additions & 0 deletions src/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SessionProvider } from "next-auth/react";
import { AccountProvider } from "@/components/AccountContext";
import { ThemeProvider } from "@/components/ThemeContext";
import BackToTopButton from "@/components/BackToTopButton";
import GlobalKeyboardShortcuts from "@/components/GlobalKeyboardShortcuts";

export default function Providers({ children }: { children: ReactNode }) {
return (
Expand All @@ -13,6 +14,7 @@ export default function Providers({ children }: { children: ReactNode }) {
<ThemeProvider>
{children}
<BackToTopButton />
<GlobalKeyboardShortcuts />
</ThemeProvider>
</AccountProvider>
</SessionProvider>
Expand Down
85 changes: 85 additions & 0 deletions src/components/GlobalKeyboardShortcuts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"use client";

import { useEffect, useState, useRef } from "react";
import { useTheme } from "@/components/ThemeContext";
import ShortcutsModal from "./ShortcutsModal";

export default function GlobalKeyboardShortcuts() {
const [isOpen, setIsOpen] = useState(false);
const [announcement, setAnnouncement] = useState("");
const { theme, toggleTheme } = useTheme();
const keyboardToggleRef = useRef(false);

useEffect(() => {
if (keyboardToggleRef.current && theme !== undefined) {
setAnnouncement(theme === "dark" ? "Dark mode enabled" : "Light mode enabled");
}
keyboardToggleRef.current = false;
}, [theme]);

useEffect(() => {
const handleOpenShortcuts = () => {
setIsOpen(true);
};

window.addEventListener("openShortcuts", handleOpenShortcuts);
return () => {
window.removeEventListener("openShortcuts", handleOpenShortcuts);
};
}, []);

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const activeElement = document.activeElement;
if (activeElement) {
const tagName = activeElement.tagName.toLowerCase();
if (tagName === "input" || tagName === "textarea" || tagName === "select") return;
if (activeElement.getAttribute("contenteditable") === "true") return;
}

// Show shortcuts modal
if (e.key === "?") {
setIsOpen(true);
e.preventDefault();
return;
}

// Alt+T / Option+T to toggle theme
if (e.altKey && e.key.toLowerCase() === "t") {
keyboardToggleRef.current = true;
toggleTheme();
e.preventDefault();
return;
}

// Toggle chart
if (e.key.toLowerCase() === "b") {
window.dispatchEvent(new Event("toggleChart"));
e.preventDefault();
return;
}

// Reload page
if (e.key.toLowerCase() === "r") {
window.location.reload();
e.preventDefault();
return;
}
};

document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [toggleTheme]);

return (
<>
<div aria-live="polite" className="sr-only">
{announcement}
</div>

<ShortcutsModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
</>
);
}
6 changes: 1 addition & 5 deletions src/components/KeyboardShortcuts.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
"use client";

import { useEffect, useState, useRef } from "react";
import { useTheme } from "@/components/ThemeContext";
import ShortcutsModal from "./ShortcutsModal";

export default function KeyboardShortcuts() {
const [isOpen, setIsOpen] = useState(false);

Expand Down Expand Up @@ -96,4 +92,4 @@ export default function KeyboardShortcuts() {
<ShortcutsModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
</div>
);
}
}
11 changes: 9 additions & 2 deletions src/components/ShortcutsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";

interface ShortcutsModalProps {
isOpen: boolean;
Expand All @@ -25,6 +25,13 @@ export default function ShortcutsModal({
}: ShortcutsModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const closeBtnRef = useRef<HTMLButtonElement>(null);
const [isMac, setIsMac] = useState(false);

useEffect(() => {
if (typeof window !== "undefined" && typeof navigator !== "undefined") {
setIsMac(/Mac|iPod|iPhone|iPad/.test(navigator.userAgent));
}
}, []);

useEffect(() => {
if (!isOpen) return;
Expand Down Expand Up @@ -102,7 +109,7 @@ export default function ShortcutsModal({
{item.action}
</span>
<kbd className="min-w-[28px] rounded-md border border-[var(--border)] bg-[var(--control)] px-2 py-1 text-center text-xs font-semibold text-[var(--card-foreground)] shadow-sm">
{item.key}
{item.key === "T" ? (isMac ? "Option + T" : "Alt + T") : item.key}
</kbd>
</div>
))}
Expand Down
Loading