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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ yarn check && yarn test
Both must pass with no errors before committing.

### After Pushing
1. Create draft PR: `gh pr create --draft`
1. Create PR: `gh pr create`
2. Check CI status: `gh pr checks`
3. Fix any CI failures on the branch
4. Note: CI environment differs (locale, timezone, Node version)
Expand Down
20 changes: 20 additions & 0 deletions src/client/features/Page/pageSlice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { createSlice } from '@reduxjs/toolkit';
import { useLayoutEffect } from 'react';
import { useDispatch } from '../../store';

export interface PageContext {
/** identifier for the sidebar heading this page appears under */
Expand Down Expand Up @@ -27,3 +29,21 @@ export const pageSlice = createSlice({

export const { set } = pageSlice.actions;
export default pageSlice.reducer;

/**
* Sets the page context (title, back button, sidebar highlight).
*
* This hook centralizes the page context setting pattern, replacing individual
* useEffect calls in each page component. It uses useLayoutEffect to ensure
* the context is set synchronously before paint, avoiding visual flicker.
*
* The hook re-runs when any context value changes.
*/
export function usePageContext(context: PageContext): void {
const dispatch = useDispatch();
const { title, back, under } = context;

useLayoutEffect(() => {
dispatch(set({ title, back, under }));
}, [dispatch, title, back, under]);
}
14 changes: 2 additions & 12 deletions src/client/pages/About.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Link } from '@mui/material';
import axios from 'axios';
import { Fragment, type ReactNode, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';

import db from '../db';
import DebugPanel from '../features/Debug/DebugPanel';
import { set as setContext } from '../features/Page/pageSlice';
import { usePageContext } from '../features/Page/pageSlice';
import SyncPanel from '../features/Sync/SyncPanel';
import UpdatePanel from '../features/Update/UpdatePanel';
import { useSelector } from '../store';
Expand All @@ -19,7 +18,7 @@ function mapProps(parent: string, info: Record<string, unknown>): InfoRow[] {
}

function About() {
const dispatch = useDispatch();
usePageContext({ title: 'About', back: true, under: 'about' });

const loggedInUser = useSelector((state) => state.user.value);
// eslint-disable-next-line no-unused-vars
Expand Down Expand Up @@ -65,15 +64,6 @@ function About() {
.catch(console.error);
}
}, []);
useEffect(() => {
dispatch(
setContext({
title: 'About',
back: true,
under: 'about',
}),
);
}, [dispatch]);

const vars: InfoRow[] = [
[<h4 key="server-header">SERVER DETAILS</h4>],
Expand Down
16 changes: 4 additions & 12 deletions src/client/pages/History.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,22 @@ import { List, Typography } from '@mui/material';
import { useEffect, useState } from 'react';
import type { TemplateDoc } from '../../shared/types';
import db from '../db';
import { set as setContext } from '../features/Page/pageSlice';
import { usePageContext } from '../features/Page/pageSlice';
import RepeatableListItem from '../features/Repeatable/RepeatableListItem';
import { useDispatch, useSelector } from '../store';
import { useSelector } from '../store';
import type { SortableRepeatableDoc } from './Home';

function History() {
usePageContext({ title: 'History', under: 'history' });

const [repeatables, setRepeatables] = useState([] as SortableRepeatableDoc[]);
const dispatch = useDispatch();

const user = useSelector((state) => state.user.value);
const handle = db(user);

// we don't actually care about this value, we just use it to trigger list reloading
const lastSynced = useSelector((state) => state.docs.lastSynced);

useEffect(() => {
dispatch(
setContext({
title: 'History',
under: 'history',
}),
);
}, [dispatch]);

// NB: this code also exists in Home.js using updated instead of completed
// biome-ignore lint/correctness/useExhaustiveDependencies: we need the timestamp to trigger
useEffect(() => {
Expand Down
15 changes: 3 additions & 12 deletions src/client/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { Divider, List, Typography } from '@mui/material';
import { Fragment, useEffect, useState } from 'react';
import type { RepeatableDoc, TemplateDoc } from '../../shared/types';
import db from '../db';
import { set as setContext } from '../features/Page/pageSlice';
import { usePageContext } from '../features/Page/pageSlice';
import RepeatableListItem from '../features/Repeatable/RepeatableListItem';
import TemplateListItem from '../features/Template/TemplateListItem';
import { useDispatch, useSelector } from '../store';
import { useSelector } from '../store';

export type SortableRepeatableDoc = Omit<RepeatableDoc, 'template'> & {
timestamp?: number;
Expand All @@ -14,7 +14,7 @@ export type SortableRepeatableDoc = Omit<RepeatableDoc, 'template'> & {
};

function Home() {
const dispatch = useDispatch();
usePageContext({ title: 'Repeatable Checklists', under: 'home' });

const user = useSelector((state) => state.user.value);
const handle = db(user);
Expand All @@ -25,15 +25,6 @@ function Home() {
const [templates, setTemplates] = useState([] as TemplateDoc[]);
const [repeatables, setRepeatables] = useState([] as SortableRepeatableDoc[]);

useEffect(() => {
dispatch(
setContext({
title: 'Repeatable Checklists',
under: 'home',
}),
);
}, [dispatch]);

// NB: this code also exists in History.js using completed instead of updated
// biome-ignore lint/correctness/useExhaustiveDependencies: TODO confirm this is OK, I think we need lastSynced as an effective "try again"
useEffect(() => {
Expand Down
16 changes: 8 additions & 8 deletions src/client/pages/Repeatable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { v4 as uuid } from 'uuid';

import type { RepeatableDoc, TemplateDoc } from '../../shared/types';
import db from '../db';
import { set as setContext } from '../features/Page/pageSlice';
import { usePageContext } from '../features/Page/pageSlice';
import RepeatableRenderer from '../features/Repeatable/RepeatableRenderer';
import { debugClient } from '../globals';
import { clearRepeatable, clearTemplate, setRepeatable, setTemplate } from '../state/docsSlice';
Expand Down Expand Up @@ -79,13 +79,6 @@ function Repeatable() {
debug('post template load');

// React 19+ automatically batches updates - no need for unstable_batchedUpdates
dispatch(
setContext({
title: template.title,
back: true,
under: 'home',
}),
);
dispatch(setRepeatable(repeatable));
dispatch(setTemplate(template));
setInitiallyOpen(!repeatable.completed);
Expand All @@ -99,6 +92,13 @@ function Repeatable() {
};
}, [dispatch, handle, repeatableId, location, navigate]);

// Set page context based on loaded template
usePageContext({
title: template?.title,
back: true,
under: 'home',
});

async function deleteRepeatable() {
const copy = Object.assign({}, repeatable);

Expand Down
16 changes: 6 additions & 10 deletions src/client/pages/Template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { v4 as uuid } from 'uuid';

import { SlugType, type TemplateDoc } from '../../shared/types';
import db from '../db';
import { set as setContext } from '../features/Page/pageSlice';
import { usePageContext } from '../features/Page/pageSlice';
import RepeatableRenderer from '../features/Repeatable/RepeatableRenderer';
import { clearTemplate, setTemplate } from '../state/docsSlice';
import { useDispatch, useSelector } from '../store';
Expand Down Expand Up @@ -60,15 +60,11 @@ function Template() {
};
}, [handle, templateId, navigate, dispatch]);

useEffect(() => {
dispatch(
setContext({
title: `${template?.title || 'New Template'} | edit`,
back: true,
under: 'home',
}),
);
}, [dispatch, template?.title]);
usePageContext({
title: `${template?.title || 'New Template'} | edit`,
back: true,
under: 'home',
});

async function handleDelete(event: MouseEvent<HTMLButtonElement>) {
event.preventDefault();
Expand Down