mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 10:13:47 +10:00
refactor(web): leave memoization to the React Compiler where it compiles
Removes useCallback, useMemo and memo from the 27 files the compiler compiles without bailing. Files it bails on (destructuring defaults, try/finally, refs read during render) keep their manual memoization.
This commit is contained in:
@@ -3,7 +3,6 @@ import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { hsvaToRgbaString, rgbaStringToHsva } from "@uiw/color-convert";
|
||||
import ReactColorColorful from "@uiw/react-color-colorful";
|
||||
import { useMemo } from "react";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@reactive-resume/ui/components/popover";
|
||||
import { Separator } from "@reactive-resume/ui/components/separator";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
@@ -53,7 +52,7 @@ export function ColorPicker({
|
||||
onChange,
|
||||
});
|
||||
|
||||
const color = useMemo(() => rgbaStringToHsva(currentValue), [currentValue]);
|
||||
const color = rgbaStringToHsva(currentValue);
|
||||
|
||||
function onColorChange(color: ColorResult) {
|
||||
const rgbaString = hsvaToRgbaString(color.hsva);
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { CellComponentProps } from "react-window";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { ProhibitIcon } from "@phosphor-icons/react";
|
||||
import Fuse from "fuse.js";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Grid } from "react-window";
|
||||
import { icons } from "@reactive-resume/schema/icons";
|
||||
import { Button } from "@reactive-resume/ui/components/button";
|
||||
@@ -81,7 +81,7 @@ type IconPickerProps = Omit<React.ComponentProps<typeof Button>, "value" | "onCh
|
||||
export function IconPicker({ value, onChange, popoverProps, ...props }: IconPickerProps) {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const searchedIcons = useMemo(() => searchIcons(search), [search]);
|
||||
const searchedIcons = searchIcons(search);
|
||||
const rowCount = Math.ceil(searchedIcons.length / columnCount);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Website } from "@reactive-resume/schema/resume/data";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { useCallback } from "react";
|
||||
import { Icon } from "@reactive-resume/ui/components/icon";
|
||||
import { Input } from "@reactive-resume/ui/components/input";
|
||||
import {
|
||||
@@ -35,22 +34,16 @@ type Props<TValue extends Website = Website> = Omit<React.ComponentProps<"input"
|
||||
|
||||
export function URLInput<TValue extends Website>({ value, onChange, hideLabelButton, ...props }: Props<TValue>) {
|
||||
const prefix = value.url.match(HTTP_PREFIX)?.[0] ?? DEFAULT_PREFIX;
|
||||
const handleUrlChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({
|
||||
...value,
|
||||
url: ensurePrefix(e.target.value, prefix),
|
||||
});
|
||||
},
|
||||
[onChange, value, prefix],
|
||||
);
|
||||
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({
|
||||
...value,
|
||||
url: ensurePrefix(e.target.value, prefix),
|
||||
});
|
||||
};
|
||||
|
||||
const handleLabelChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({ ...value, label: e.target.value });
|
||||
},
|
||||
[onChange, value],
|
||||
);
|
||||
const handleLabelChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({ ...value, label: e.target.value });
|
||||
};
|
||||
|
||||
const urlValue = stripPrefix(value.url);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type z from "zod";
|
||||
import type { SingleComboboxProps } from "@/components/ui/combobox";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { useMemo } from "react";
|
||||
import { match } from "ts-pattern";
|
||||
import { levelDesignSchema } from "@reactive-resume/schema/resume/data";
|
||||
import { Combobox } from "@/components/ui/combobox";
|
||||
@@ -23,12 +22,10 @@ const getLevelTypeName = (type: LevelType) => {
|
||||
};
|
||||
|
||||
export function LevelTypeCombobox({ ...props }: LevelTypeComboboxProps) {
|
||||
const options = useMemo(() => {
|
||||
return levelDesignSchema.shape.type.options.map((option) => ({
|
||||
value: option,
|
||||
label: getLevelTypeName(option),
|
||||
}));
|
||||
}, []);
|
||||
const options = levelDesignSchema.shape.type.options.map((option) => ({
|
||||
value: option,
|
||||
label: getLevelTypeName(option),
|
||||
}));
|
||||
|
||||
return <Combobox options={options} {...props} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MultiComboboxProps, SingleComboboxProps } from "@/components/ui/combobox";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { fontList, getFont, getFontDisplayName, getFontSearchKeywords, sortFontWeights } from "@reactive-resume/fonts";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
import { Combobox } from "@/components/ui/combobox";
|
||||
@@ -54,18 +54,12 @@ export function FontWeightCombobox({
|
||||
}));
|
||||
}, [fontFamily]);
|
||||
|
||||
const sortedValue = useMemo(() => (value ? sortFontWeights(value) : value), [value]);
|
||||
const sortedDefaultValue = useMemo(
|
||||
() => (defaultValue ? sortFontWeights(defaultValue) : defaultValue),
|
||||
[defaultValue],
|
||||
);
|
||||
const sortedValue = value ? sortFontWeights(value) : value;
|
||||
const sortedDefaultValue = defaultValue ? sortFontWeights(defaultValue) : defaultValue;
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
(nextValue: string[] | null) => {
|
||||
onValueChange?.(nextValue ? sortFontWeights(nextValue) : nextValue);
|
||||
},
|
||||
[onValueChange],
|
||||
);
|
||||
const handleValueChange = (nextValue: string[] | null) => {
|
||||
onValueChange?.(nextValue ? sortFontWeights(nextValue) : nextValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import Cookies from "js-cookie";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTimeout } from "usehooks-ts";
|
||||
import { toast } from "@reactive-resume/ui/components/toast";
|
||||
|
||||
@@ -15,13 +15,13 @@ export function DonationToast() {
|
||||
// ponytail: inlined from @reactive-resume/ui/hooks/use-cookie — only consumer, one read + one set-with-expiry
|
||||
const [dismissed, setDismissedState] = useState<string | null>(() => Cookies.get(DISMISSED_COOKIE_NAME) ?? null);
|
||||
|
||||
const setDismissed = useCallback((value: string, options?: { expires?: Date }) => {
|
||||
const setDismissed = (value: string, options?: { expires?: Date }) => {
|
||||
// Attributes match the former useCookie DEFAULT_COOKIE_ATTRIBUTES; options (expiry) override.
|
||||
Cookies.set(DISMISSED_COOKIE_NAME, value, { path: "/", secure: true, sameSite: "lax", ...options });
|
||||
setDismissedState(value);
|
||||
}, []);
|
||||
};
|
||||
|
||||
const showToast = useCallback(() => {
|
||||
const showToast = () => {
|
||||
if (dismissed === "true") return;
|
||||
|
||||
toast.add({
|
||||
@@ -41,7 +41,7 @@ export function DonationToast() {
|
||||
setDismissed("true", { expires: getDismissedCookieExpiresAt() });
|
||||
},
|
||||
});
|
||||
}, [dismissed, setDismissed]);
|
||||
};
|
||||
|
||||
useTimeout(showToast, SHOW_TOAST_DELAY_MS);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Application } from "../types";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { useLingui } from "@lingui/react";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { INTERVIEW_KINDS } from "@reactive-resume/schema/applications/data";
|
||||
import { Button } from "@reactive-resume/ui/components/button";
|
||||
import { Icon } from "@reactive-resume/ui/components/icon";
|
||||
@@ -44,11 +44,11 @@ export function ApplicationCalendar({ applications, allApplications, onOpen }: A
|
||||
});
|
||||
const [dialog, setDialog] = useState<DialogState>({ open: false });
|
||||
|
||||
const interviews = useMemo(() => collectInterviews(applications), [applications]);
|
||||
const byDay = useMemo(() => groupByDay(interviews), [interviews]);
|
||||
const upcoming = useMemo(() => upcomingInterviews(interviews), [interviews]);
|
||||
const upcomingByDay = useMemo(() => [...groupByDay(upcoming).values()], [upcoming]);
|
||||
const days = useMemo(() => monthGrid(month), [month]);
|
||||
const interviews = collectInterviews(applications);
|
||||
const byDay = groupByDay(interviews);
|
||||
const upcoming = upcomingInterviews(interviews);
|
||||
const upcomingByDay = [...groupByDay(upcoming).values()];
|
||||
const days = monthGrid(month);
|
||||
|
||||
const today = new Date();
|
||||
const todayKey = dayKey(today);
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { CsvField } from "../csv";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Plural, Trans } from "@lingui/react/macro";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { Button } from "@reactive-resume/ui/components/button";
|
||||
import { Icon } from "@reactive-resume/ui/components/icon";
|
||||
import { Label } from "@reactive-resume/ui/components/label";
|
||||
@@ -59,13 +59,12 @@ export function ImportApplicationsSheet({ open, onOpenChange }: ImportSheetProps
|
||||
const [overrides, setOverrides] = useState<Record<number, CsvField | null>>({});
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const table = useMemo(() => (text.trim() ? parseCsv(text) : null), [text]);
|
||||
const table = text.trim() ? parseCsv(text) : null;
|
||||
const headers = table?.[0] ?? [];
|
||||
const mapping = useMemo(
|
||||
() => autoMapHeaders(headers).map((field, index) => (index in overrides ? (overrides[index] ?? null) : field)),
|
||||
[headers, overrides],
|
||||
const mapping = autoMapHeaders(headers).map((field, index) =>
|
||||
index in overrides ? (overrides[index] ?? null) : field,
|
||||
);
|
||||
const result = useMemo(() => (table ? mapCsvToApplications(table, mapping) : null), [table, mapping]);
|
||||
const result = table ? mapCsvToApplications(table, mapping) : null;
|
||||
|
||||
// The import endpoint takes 500 at a time; the rest is reported rather than refused.
|
||||
const importable = result ? result.rows.slice(0, MAX_IMPORT) : [];
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Application } from "../types";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Plural, Trans } from "@lingui/react/macro";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMemo, useRef } from "react";
|
||||
import { useRef } from "react";
|
||||
import { Button } from "@reactive-resume/ui/components/button";
|
||||
import { Icon } from "@reactive-resume/ui/components/icon";
|
||||
import { toast } from "@reactive-resume/ui/components/toast";
|
||||
@@ -23,26 +23,16 @@ export function ApplicationInsights({ applications }: { applications: Applicatio
|
||||
const { data: documents } = useQuery(orpc.documents.list.queryOptions({ input: { trashed: false } }));
|
||||
|
||||
// A resume is tailored for an application when it was made for it (Copy for a job).
|
||||
const madeFor = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
(documents ?? []).flatMap((document) => (document.application ? [[document.id, document.application.id]] : [])),
|
||||
),
|
||||
[documents],
|
||||
const madeFor = new Map(
|
||||
(documents ?? []).flatMap((document) => (document.application ? [[document.id, document.application.id]] : [])),
|
||||
);
|
||||
const outcomes = useMemo(
|
||||
() =>
|
||||
computeOutcomes(applications, (application) =>
|
||||
Boolean(application.resumeId && madeFor.get(application.resumeId) === (application as Application).id),
|
||||
),
|
||||
[applications, madeFor],
|
||||
const outcomes = computeOutcomes(applications, (application) =>
|
||||
Boolean(application.resumeId && madeFor.get(application.resumeId) === (application as Application).id),
|
||||
);
|
||||
|
||||
// Weekly application velocity — derived from the already-loaded list, closed ones excluded.
|
||||
const timeline = useMemo(
|
||||
() =>
|
||||
computeTimeline(applications.filter((app) => app.status !== "closed").map((app) => new Date(appliedDate(app)))),
|
||||
[applications],
|
||||
const timeline = computeTimeline(
|
||||
applications.filter((app) => app.status !== "closed").map((app) => new Date(appliedDate(app))),
|
||||
);
|
||||
const maxWeek = Math.max(1, ...timeline.map((bucket) => bucket.count));
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { memo } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
/** The assistant's reply as Markdown. Memoized on the text, so finished replies stop re-rendering while one streams. */
|
||||
export const AssistantMarkdown = memo(function AssistantMarkdown({ text }: { text: string }) {
|
||||
export const AssistantMarkdown = function AssistantMarkdown({ text }: { text: string }) {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
skipHtml
|
||||
@@ -41,4 +40,4 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({ text }: { tex
|
||||
{text}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { LetterWords } from "@reactive-resume/resume/cover-letter";
|
||||
import type { CoverLetter } from "@reactive-resume/schema/cover-letter/data";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { useLingui } from "@lingui/react";
|
||||
import { useMemo } from "react";
|
||||
import { composeCoverLetter, createCoverLetterResumeData } from "@reactive-resume/resume/cover-letter";
|
||||
|
||||
/**
|
||||
@@ -12,22 +11,19 @@ import { composeCoverLetter, createCoverLetterResumeData } from "@reactive-resum
|
||||
export function useLetterWords(): LetterWords {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
greeting: (name: string) => t`Dear ${name},`,
|
||||
teamGreeting: t`Dear hiring team,`,
|
||||
hiringTeam: t`Hiring team`,
|
||||
signOff: t`Kind regards,`,
|
||||
formatDate: (date: string) =>
|
||||
new Date(`${date}T12:00:00Z`).toLocaleDateString(i18n.locale, {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
}),
|
||||
}),
|
||||
[i18n.locale],
|
||||
);
|
||||
return {
|
||||
greeting: (name: string) => t`Dear ${name},`,
|
||||
teamGreeting: t`Dear hiring team,`,
|
||||
hiringTeam: t`Hiring team`,
|
||||
signOff: t`Kind regards,`,
|
||||
formatDate: (date: string) =>
|
||||
new Date(`${date}T12:00:00Z`).toLocaleDateString(i18n.locale, {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** The letter as a one-section document the PDF renderer draws: sender header, recipient, greeting, body, sign-off. */
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { CSSProperties } from "react";
|
||||
import type { CheckIssue } from "./issues";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { useMemo } from "react";
|
||||
import { getProposalState } from "@reactive-resume/resume/proposals";
|
||||
import { Button } from "@reactive-resume/ui/components/button";
|
||||
import { Icon } from "@reactive-resume/ui/components/icon";
|
||||
@@ -95,10 +94,7 @@ export function CheckPageLayer({ pageIndex, pageMap }: CheckPageLayerProps) {
|
||||
const breakpoint = useBreakpoint();
|
||||
const page = pageMap?.pages[pageIndex];
|
||||
|
||||
const highlighted = useMemo(
|
||||
() => (check && highlightTerm && tab === "match" ? targetsMentioning(check.data, highlightTerm) : []),
|
||||
[check, highlightTerm, tab],
|
||||
);
|
||||
const highlighted = check && highlightTerm && tab === "match" ? targetsMentioning(check.data, highlightTerm) : [];
|
||||
|
||||
if (!check || !pageMap || !page || page.width <= 0 || page.height <= 0) return null;
|
||||
|
||||
|
||||
@@ -22,5 +22,5 @@ export function useCheck(): CheckResult | null {
|
||||
/** Open issues, for the badge on the Check tab. Ignored ones don't count. */
|
||||
export function useOpenIssueCount(): number {
|
||||
const data = useDeferredValue(useResumeData());
|
||||
return useMemo(() => (data ? lintResumeForAts(data).findings.length : 0), [data]);
|
||||
return data ? lintResumeForAts(data).findings.length : 0;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { t } from "@lingui/core/macro";
|
||||
import { Plural, Trans } from "@lingui/react/macro";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { buildMarkdown } from "@reactive-resume/resume/markdown";
|
||||
import { collectPassages } from "@reactive-resume/resume/proposals";
|
||||
import { Button, buttonVariants } from "@reactive-resume/ui/components/button";
|
||||
@@ -45,17 +45,13 @@ export function WritingTab({ data, issues }: WritingTabProps) {
|
||||
const setProposals = useEditorStore((state) => state.setProposals);
|
||||
|
||||
const provider = usableProviders.find((entry) => entry.id === providerOverride) ?? usableProviders[0];
|
||||
const passages = useMemo(
|
||||
() =>
|
||||
collectPassages(data, {
|
||||
summary: getSectionName(data, "summary"),
|
||||
sectionTitle: (sectionId) => getSectionName(data, sectionId),
|
||||
entryTitle: (type, entry) => describeEntry(type as never, entry as never).title,
|
||||
bullet: (n) => t`bullet ${n}`,
|
||||
paragraph: (n) => t`paragraph ${n}`,
|
||||
}).slice(0, MAX_PASSAGES),
|
||||
[data],
|
||||
);
|
||||
const passages = collectPassages(data, {
|
||||
summary: getSectionName(data, "summary"),
|
||||
sectionTitle: (sectionId) => getSectionName(data, sectionId),
|
||||
entryTitle: (type, entry) => describeEntry(type as never, entry as never).title,
|
||||
bullet: (n) => t`bullet ${n}`,
|
||||
paragraph: (n) => t`paragraph ${n}`,
|
||||
}).slice(0, MAX_PASSAGES);
|
||||
|
||||
const { mutate, isPending, error, reset } = useMutation({
|
||||
...orpc.ai.atsReview.mutationOptions(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { Template } from "@reactive-resume/schema/templates";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
|
||||
import { createPdfFirstPageImageUrl, releaseThumbnailUrls } from "@/features/resume/preview/pdf-thumbnail";
|
||||
|
||||
@@ -41,7 +41,7 @@ function fingerprint(data: ResumeData) {
|
||||
*/
|
||||
export function useTemplateThumbnail(template: Template, data: ResumeData, enabled: boolean) {
|
||||
const queryClient = useQueryClient();
|
||||
const hash = useMemo(() => fingerprint(data), [data]);
|
||||
const hash = fingerprint(data);
|
||||
|
||||
useEffect(() => {
|
||||
releaseThumbnailUrls(queryClient, QUERY_KEY);
|
||||
|
||||
@@ -2,7 +2,6 @@ import { t } from "@lingui/core/macro";
|
||||
import { Plural, Trans } from "@lingui/react/macro";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { useMemo } from "react";
|
||||
import { Alert, AlertDescription } from "@reactive-resume/ui/components/alert";
|
||||
import { Button } from "@reactive-resume/ui/components/button";
|
||||
import { Icon } from "@reactive-resume/ui/components/icon";
|
||||
@@ -31,7 +30,7 @@ export function WritePanel() {
|
||||
const locale = useCurrentBuilderResumeSelector((resume) => resume.data.metadata.page.locale);
|
||||
// The selector hook reads `undefined` as "no resume yet", so optional values need a fallback.
|
||||
const dateFormat = useCurrentBuilderResumeSelector((resume) => resume.data.metadata.page.dateFormat ?? null);
|
||||
const page = useMemo(() => ({ locale, dateFormat: dateFormat ?? undefined }), [locale, dateFormat]);
|
||||
const page = { locale, dateFormat: dateFormat ?? undefined };
|
||||
const added = useEditorStore((state) => state.addedSections);
|
||||
const isEmpty = useCurrentBuilderResumeSelector((resume) => getOutlineRows(resume.data, new Set(added)).length === 0);
|
||||
const openDialog = useDialogStore((state) => state.openDialog);
|
||||
@@ -77,7 +76,7 @@ function ImportedNote() {
|
||||
const { imported } = useSearch({ strict: false }) as { imported?: string };
|
||||
const navigate = useNavigate();
|
||||
const data = useCurrentBuilderResumeSelector((resume) => resume.data);
|
||||
const { sections, entries, toCheck } = useMemo(() => summarizeContent(data), [data]);
|
||||
const { sections, entries, toCheck } = summarizeContent(data);
|
||||
if (!imported) return null;
|
||||
|
||||
const dismiss = () =>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { IconName } from "@reactive-resume/ui/components/icon";
|
||||
import { useMemo } from "react";
|
||||
import { getResumeSectionTitle } from "@reactive-resume/pdf/section-title";
|
||||
import { Icon } from "@reactive-resume/ui/components/icon";
|
||||
import { contrastOnWhite } from "@reactive-resume/utils/color";
|
||||
@@ -108,7 +107,7 @@ type ResumeReflowProps = { data: ResumeData };
|
||||
* font. Contact details are tap targets. The PDF stays the exact page.
|
||||
*/
|
||||
export function ResumeReflow({ data }: ResumeReflowProps) {
|
||||
const order = useMemo(() => reflowOrder(data), [data]);
|
||||
const order = reflowOrder(data);
|
||||
const { basics } = data;
|
||||
// The template's colour, darkened where it would be too faint to read on white.
|
||||
const primary = data.metadata.design.colors.primary;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { startTransition, useCallback } from "react";
|
||||
import { startTransition } from "react";
|
||||
import { Button } from "@reactive-resume/ui/components/button";
|
||||
import { Icon } from "@reactive-resume/ui/components/icon";
|
||||
import { useTheme } from "./provider";
|
||||
@@ -7,11 +7,11 @@ import { useTheme } from "./provider";
|
||||
export function ThemeToggleButton(props: React.ComponentProps<typeof Button>) {
|
||||
const { resolvedTheme, toggleTheme } = useTheme();
|
||||
|
||||
const onToggleTheme = useCallback(() => {
|
||||
const onToggleTheme = () => {
|
||||
startTransition(() => {
|
||||
toggleTheme();
|
||||
});
|
||||
}, [toggleTheme]);
|
||||
};
|
||||
|
||||
const ariaLabel = resolvedTheme === "dark" ? t`Switch to light theme` : t`Switch to dark theme`;
|
||||
|
||||
|
||||
@@ -64,16 +64,4 @@ describe("useControlledState", () => {
|
||||
rerender({ value: 2 });
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns a stable setter reference when onChange is stable", () => {
|
||||
const onChange = vi.fn();
|
||||
const { result, rerender } = renderHook(
|
||||
({ value }: { value: number }) => useControlledState<number>({ value, onChange }),
|
||||
{ initialProps: { value: 1 } },
|
||||
);
|
||||
|
||||
const initialSetter = result.current[1];
|
||||
rerender({ value: 2 });
|
||||
expect(result.current[1]).toBe(initialSetter);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface CommonControlledStateProps<T> {
|
||||
value?: T;
|
||||
@@ -18,13 +18,10 @@ export function useControlledState<T, Rest extends unknown[] = []>(
|
||||
const [internalState, setInternalState] = useState<T>(value !== undefined ? value : (defaultValue as T));
|
||||
const state = isControlled ? (value as T) : internalState;
|
||||
|
||||
const setState = useCallback(
|
||||
(next: T, ...args: Rest) => {
|
||||
if (!isControlled) setInternalState(next);
|
||||
onChange?.(next, ...args);
|
||||
},
|
||||
[isControlled, onChange],
|
||||
);
|
||||
const setState = (next: T, ...args: Rest) => {
|
||||
if (!isControlled) setInternalState(next);
|
||||
onChange?.(next, ...args);
|
||||
};
|
||||
|
||||
return [state, setState] as const;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AnyFormApi } from "@tanstack/react-form";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { useStore } from "@tanstack/react-form";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useConfirm } from "@/hooks/use-confirm";
|
||||
|
||||
@@ -22,12 +22,12 @@ export function useFormBlocker(form: Pick<AnyFormApi, "store">, options?: UseFor
|
||||
shouldBlockRef.current = options?.shouldBlock;
|
||||
}, [options?.shouldBlock]);
|
||||
|
||||
const shouldBlock = useCallback(() => {
|
||||
const shouldBlock = () => {
|
||||
if (shouldBlockRef.current) return shouldBlockRef.current();
|
||||
return isDirty && !isSubmitting;
|
||||
}, [isDirty, isSubmitting]);
|
||||
};
|
||||
|
||||
const confirmClose = useCallback(() => {
|
||||
const confirmClose = () => {
|
||||
if (!shouldBlock()) return true;
|
||||
|
||||
return confirm(t`Are you sure you want to close this dialog?`, {
|
||||
@@ -35,14 +35,14 @@ export function useFormBlocker(form: Pick<AnyFormApi, "store">, options?: UseFor
|
||||
confirmText: t`Leave`,
|
||||
cancelText: t`Stay`,
|
||||
});
|
||||
}, [shouldBlock, confirm]);
|
||||
};
|
||||
|
||||
const requestClose = useCallback(async () => {
|
||||
const requestClose = async () => {
|
||||
const confirmed = await confirmClose();
|
||||
if (!confirmed) return;
|
||||
|
||||
closeDialog();
|
||||
}, [confirmClose, closeDialog]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setOnBeforeClose(confirmClose);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { t } from "@lingui/core/macro";
|
||||
import { useLingui } from "@lingui/react";
|
||||
import { Plural, Trans } from "@lingui/react/macro";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Icon } from "@reactive-resume/ui/components/icon";
|
||||
import { useBreakpoint } from "@reactive-resume/ui/hooks/use-breakpoint";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
@@ -65,25 +65,18 @@ export function PageCanvas() {
|
||||
// Check → Writing's and the assistant's proposed edits show on the page, the old text struck through and the new
|
||||
// highlighted.
|
||||
const markEdits = mode === "check" && checkTab === "writing" && pageView === "page" && proposals.length > 0;
|
||||
const marked = useMemo(
|
||||
() => [...(markEdits ? proposals : []), ...assistantProposals],
|
||||
[markEdits, proposals, assistantProposals],
|
||||
);
|
||||
const marked = [...(markEdits ? proposals : []), ...assistantProposals];
|
||||
const pendingOnPage = data ? pendingProposals(data, assistantProposals).length : 0;
|
||||
const parser = mode === "check" && pageView === "parser" && !viewing;
|
||||
|
||||
// Design: a hovered or focused template is drawn on the page until it's applied or the pointer leaves.
|
||||
const previewData = useMemo(
|
||||
() =>
|
||||
viewing
|
||||
? viewing.data
|
||||
: data && previewTemplate
|
||||
? { ...data, metadata: { ...data.metadata, template: previewTemplate } }
|
||||
: data && marked.length > 0
|
||||
? markProposals(data, marked)
|
||||
: undefined,
|
||||
[data, previewTemplate, viewing, marked],
|
||||
);
|
||||
const previewData = viewing
|
||||
? viewing.data
|
||||
: data && previewTemplate
|
||||
? { ...data, metadata: { ...data.metadata, template: previewTemplate } }
|
||||
: data && marked.length > 0
|
||||
? markProposals(data, marked)
|
||||
: undefined;
|
||||
const overflow = data && !previewTemplate && !viewing ? measureOverflow(data, rendered) : null;
|
||||
// Desktop: the page moves 120px aside so it stays visible beside the Share & export sheet.
|
||||
const shifted = sheetOpen && (breakpoint === "desktop" || breakpoint === "wide");
|
||||
|
||||
@@ -3,7 +3,7 @@ import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute, stripSearchParams, useNavigate } from "@tanstack/react-router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import z from "zod";
|
||||
import { Button } from "@reactive-resume/ui/components/button";
|
||||
import {
|
||||
@@ -92,10 +92,7 @@ function RouteComponent() {
|
||||
}, [applicationId, applications, navigate]);
|
||||
|
||||
const text = query.trim().toLowerCase();
|
||||
const filtered = useMemo(
|
||||
() => (applications ?? []).filter((application) => matches(application, text)),
|
||||
[applications, text],
|
||||
);
|
||||
const filtered = (applications ?? []).filter((application) => matches(application, text));
|
||||
const selected = applications?.find((application) => application.id === selectedId) ?? null;
|
||||
// The board needs room to drag: phones get the list instead.
|
||||
const shown: View = phone && (view === "board" || view === "calendar") ? "list" : view;
|
||||
|
||||
Reference in New Issue
Block a user