Skip to content
Merged
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
8 changes: 4 additions & 4 deletions .github/workflows/pr-quality-gates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ jobs:
with:
script: |
const body = context.payload.pull_request?.body || '';
// Accept common keywords: close/closes/closed/fix/fixes/fixed/resolve/resolves/resolved
// Require a github issue reference like: "Closes #123"
const re = /(close[sd]?|fix(e[sd])?|resolve[sd]?)\s+#\d+/i;
// Accept common keywords: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved
// Require a GitHub issue reference like: "Closes #123"
const re = /(close[sd]?|fix(ed|es)?|resolve[sd]?)\s+#\d+/i;
if (!re.test(body)) {
core.setFailed('PR description must reference an issue using e.g. "Closes #123".');
}
}
15 changes: 11 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# Contributing to TeachLink Frontend

<!-- This document describes the Circuit Breaker pattern implementation for Toast Notifications in the TeachLink frontend. The Circuit Breaker prevents cascading failures and provides fallback behavior when the toast notification system is overwhelmed. -->

<!-- Your Material Design Breadcrumbs implementation is now properly on a feature branch! -->

Thanks for contributing to TeachLink.
<!--

<!--
This guide provides comprehensive instructions for implementing WCAG 2.1 AA compliant accessibility features across the learning platform. -->

## Branching & workflow

<!-- This guide provides comprehensive instructions for implementing WCAG 2.1 AA compliant accessibility features across the learning platform. -->

- **Do not push directly** to protected branches (`main`, `develop`).
Expand Down Expand Up @@ -38,9 +43,11 @@ Your PR will be blocked from merging unless it meets the following:

- All review conversations must be resolved before merge.

5. **Issue must be referenced**
- PR description must reference a GitHub issue and include one of:
- `Close #<issue-number>` / `Closes #<issue-number>` / `Fixes #<issue-number>`
5. 5. **Issue must be referenced**
- PR description must reference a GitHub issue and include one of the standard closing keywords:
- **Close variants:** `Close #<issue-number>`, `Closes #<issue-number>`, `Closed #<issue-number>`
- **Fix variants:** `Fix #<issue-number>`, `Fixes #<issue-number>`, `Fixed #<issue-number>`
- **Resolve variants:** `Resolve #<issue-number>`, `Resolves #<issue-number>`, `Resolved #<issue-number>`

## Local checks (run before pushing)

Expand Down
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 20 additions & 9 deletions src/hooks/useKeyboardShortcuts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

const STORAGE_KEY = 'teachlink-keyboard-shortcuts-v1';

Expand Down Expand Up @@ -176,19 +176,29 @@ export function useKeyboardShortcuts(
return new Map(commands.map((command) => [command.id, command.run]));
}, [commands]);

// Keep a stable ref of shortcuts and command handlers to prevent listener re-subscription
const shortcutsRef = useRef<ShortcutDefinition[]>([]);
const commandMapRef = useRef(commandMap);

const shortcuts = useMemo<ShortcutDefinition[]>(() => {
return DEFAULT_SHORTCUTS.map((item) => ({
...item,
binding: customBindings[item.id] ?? item.defaultBinding,
}));
}, [customBindings]);

// Keep refs synchronized on every render
useEffect(() => {
shortcutsRef.current = shortcuts;
commandMapRef.current = commandMap;
}, [shortcuts, commandMap]);

const runShortcutAction = useCallback(
(id: ShortcutActionId) => {
const handler = commandMap.get(id);
const handler = commandMapRef.current.get(id);
if (handler) handler();
},
[commandMap],
[],
);

useEffect(() => {
Expand All @@ -198,25 +208,26 @@ export function useKeyboardShortcuts(
if (isInputLike(event.target)) return;
const pressed = eventToBinding(event);

const targetShortcut = shortcuts.find((shortcut) => {
const targetShortcut = shortcutsRef.current.find((shortcut: ShortcutDefinition) => {
const candidates = resolveModBinding(shortcut.binding);
return candidates.includes(pressed);
});

if (!targetShortcut) return;

event.preventDefault();
runShortcutAction(targetShortcut.id);
const handler = commandMapRef.current.get(targetShortcut.id);
if (handler) handler();
};

document.addEventListener('keydown', listener);
return () => document.removeEventListener('keydown', listener);
}, [enabled, runShortcutAction, shortcuts]);
}, [enabled]);

const setShortcutBinding = useCallback((id: ShortcutActionId, binding: string) => {
const normalized = normalizeBinding(binding);
if (!normalized) return;
setCustomBindings((prev) => {
setCustomBindings((prev: Partial<Record<ShortcutActionId, string>>) => {
const next = { ...prev, [id]: normalized };
if (typeof window !== 'undefined') {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
Expand All @@ -226,7 +237,7 @@ export function useKeyboardShortcuts(
}, []);

const resetShortcutBinding = useCallback((id: ShortcutActionId) => {
setCustomBindings((prev) => {
setCustomBindings((prev: Partial<Record<ShortcutActionId, string>>) => {
const next = { ...prev };
delete next[id];
if (typeof window !== 'undefined') {
Expand All @@ -250,4 +261,4 @@ export function useKeyboardShortcuts(
resetAllShortcutBindings,
runShortcutAction,
};
}
}
10 changes: 7 additions & 3 deletions src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"types": ["vitest/globals", "@testing-library/jest-dom"],
"types": [
"react",
"react-dom",
"vitest/globals",
"@testing-library/jest-dom"
],
"plugins": [
{
"name": "next"
Expand All @@ -35,4 +39,4 @@
"**/*.spec.tsx"
],
"exclude": ["node_modules"]
}
}
Loading