mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-23 23:02:17 +10:00
refactor: ponytail audit
This commit is contained in:
@@ -13,28 +13,24 @@ import {
|
||||
} from "better-auth/client/plugins";
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
|
||||
const getAuthClient = () => {
|
||||
return createAuthClient({
|
||||
plugins: [
|
||||
dashClient(),
|
||||
adminClient(),
|
||||
apiKeyClient(),
|
||||
passkeyClient(),
|
||||
usernameClient(),
|
||||
twoFactorClient({
|
||||
onTwoFactorRedirect() {
|
||||
// Redirect to 2FA verification page
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.href = "/auth/verify-2fa";
|
||||
}
|
||||
},
|
||||
}),
|
||||
genericOAuthClient(),
|
||||
oauthProviderClient(),
|
||||
oauthProviderResourceClient(),
|
||||
inferAdditionalFields<typeof auth>(),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export const authClient = getAuthClient();
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [
|
||||
dashClient(),
|
||||
adminClient(),
|
||||
apiKeyClient(),
|
||||
passkeyClient(),
|
||||
usernameClient(),
|
||||
twoFactorClient({
|
||||
onTwoFactorRedirect() {
|
||||
// Redirect to 2FA verification page
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.href = "/auth/verify-2fa";
|
||||
}
|
||||
},
|
||||
}),
|
||||
genericOAuthClient(),
|
||||
oauthProviderClient(),
|
||||
oauthProviderResourceClient(),
|
||||
inferAdditionalFields<typeof auth>(),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isLocale, resolveLocale } from "./locale";
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import Cookies from "js-cookie";
|
||||
import { changeLocale, formatRelativeTime, isLocale, resolveLocale } from "./locale";
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
Cookies.remove("locale");
|
||||
});
|
||||
|
||||
describe("isLocale", () => {
|
||||
it("returns true for known locale en-US", () => {
|
||||
@@ -44,3 +53,31 @@ describe("resolveLocale", () => {
|
||||
expect(resolveLocale("")).toBe("en-US");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatRelativeTime", () => {
|
||||
it("selects the largest matching unit", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-02T12:00:00Z"));
|
||||
const formatter = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
||||
|
||||
expect(formatRelativeTime("2026-01-02T10:00:00Z", formatter)).toBe("2 hours ago");
|
||||
expect(formatRelativeTime("2026-01-02T11:59:45Z", formatter)).toBe("now");
|
||||
});
|
||||
|
||||
it("uses the requested fallback for an invalid date", () => {
|
||||
const formatter = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
||||
|
||||
expect(formatRelativeTime("invalid", formatter, "")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("changeLocale", () => {
|
||||
it("persists a valid locale and reloads", () => {
|
||||
const reload = vi.spyOn(window.location, "reload").mockImplementation(() => undefined);
|
||||
|
||||
changeLocale("de-DE");
|
||||
|
||||
expect(Cookies.get("locale")).toBe("de-DE");
|
||||
expect(reload).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,14 @@ export { isRTL };
|
||||
const storageKey = "locale";
|
||||
const defaultLocale: Locale = "en-US";
|
||||
const messageLoaders = import.meta.glob<{ messages: Messages }>("../../locales/*.po");
|
||||
const relativeTimeDivisions: Array<{ amount: number; unit: Intl.RelativeTimeFormatUnit }> = [
|
||||
{ amount: 31_536_000_000, unit: "year" },
|
||||
{ amount: 2_592_000_000, unit: "month" },
|
||||
{ amount: 604_800_000, unit: "week" },
|
||||
{ amount: 86_400_000, unit: "day" },
|
||||
{ amount: 3_600_000, unit: "hour" },
|
||||
{ amount: 60_000, unit: "minute" },
|
||||
];
|
||||
|
||||
export const localeMap = {
|
||||
"af-ZA": msg`Afrikaans`,
|
||||
@@ -77,16 +85,24 @@ export const resolveLocale = (locale: string): Locale => {
|
||||
return isLocale(locale) ? locale : defaultLocale;
|
||||
};
|
||||
|
||||
export function formatRelativeTime(value: Date | string, formatter: Intl.RelativeTimeFormat, invalidFallback?: string) {
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
const diffMs = date.getTime() - Date.now();
|
||||
if (Number.isNaN(diffMs)) return invalidFallback ?? formatter.format(0, "second");
|
||||
|
||||
const division = relativeTimeDivisions.find((candidate) => Math.abs(diffMs) >= candidate.amount);
|
||||
|
||||
return division
|
||||
? formatter.format(Math.round(diffMs / division.amount), division.unit)
|
||||
: formatter.format(0, "second");
|
||||
}
|
||||
|
||||
export const getLocale = () => {
|
||||
const locale = Cookies.get(storageKey);
|
||||
if (!locale || !isLocale(locale)) return defaultLocale;
|
||||
return locale;
|
||||
};
|
||||
|
||||
export const setLocaleCookie = (locale: Locale) => {
|
||||
Cookies.set(storageKey, locale);
|
||||
};
|
||||
|
||||
const loadMessages = async (locale: Locale) => {
|
||||
const load = messageLoaders[`../../locales/${locale}.po`];
|
||||
|
||||
@@ -113,3 +129,9 @@ export const loadLocale = async (locale: string) => {
|
||||
const { locale: resolvedLocale, messages } = await getLocaleMessages(locale);
|
||||
i18n.loadAndActivate({ locale: resolvedLocale, messages });
|
||||
};
|
||||
|
||||
export const changeLocale = (value: string | null) => {
|
||||
if (!value || !isLocale(value)) return;
|
||||
Cookies.set(storageKey, value);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
@@ -10,8 +10,8 @@ const getRpcUrl = () => {
|
||||
return `${window.location.origin}/api/rpc`;
|
||||
};
|
||||
|
||||
const createRpcClient = (): RouterClient<typeof router> => {
|
||||
const link = new RPCLink({
|
||||
export const client: RouterClient<typeof router> = createORPCClient(
|
||||
new RPCLink({
|
||||
url: getRpcUrl(),
|
||||
fetch: (request, init) => fetch(request, { ...init, credentials: "include" }),
|
||||
plugins: [
|
||||
@@ -26,15 +26,11 @@ const createRpcClient = (): RouterClient<typeof router> => {
|
||||
console.warn("[oRPC client]", error);
|
||||
}),
|
||||
],
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return createORPCClient(link);
|
||||
};
|
||||
|
||||
export const client = createRpcClient();
|
||||
|
||||
const createStreamClient = (): RouterClient<typeof router> => {
|
||||
const link = new RPCLink({
|
||||
export const streamClient: RouterClient<typeof router> = createORPCClient(
|
||||
new RPCLink({
|
||||
url: getRpcUrl(),
|
||||
fetch: (request, init) => fetch(request, { ...init, credentials: "include" }),
|
||||
interceptors: [
|
||||
@@ -43,12 +39,8 @@ const createStreamClient = (): RouterClient<typeof router> => {
|
||||
console.warn("[oRPC stream client]", error);
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
return createORPCClient(link);
|
||||
};
|
||||
|
||||
export const streamClient = createStreamClient();
|
||||
}),
|
||||
);
|
||||
|
||||
export const orpc = createTanstackQueryUtils(client);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createSectionTitleResolver } from "./section-title";
|
||||
|
||||
const resolverCache = new Map<string, Promise<SectionTitleResolver>>();
|
||||
|
||||
export const createSectionTitleResolverForLocale = async (localeParam: string) => {
|
||||
export const createSectionTitleResolverForLocale = (localeParam: string) => {
|
||||
const requestedLocale = resolveLocale(localeParam);
|
||||
const cachedResolver = resolverCache.get(requestedLocale);
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import type { Website } from "@reactive-resume/schema/resume/data";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import { useAppForm } from "./tanstack-form";
|
||||
|
||||
vi.mock("@/components/input/url-input", () => ({
|
||||
URLInput: ({
|
||||
value,
|
||||
onChange,
|
||||
hideLabelButton,
|
||||
}: {
|
||||
value: Website;
|
||||
onChange: (value: Website) => void;
|
||||
hideLabelButton?: boolean;
|
||||
}) => (
|
||||
<input
|
||||
aria-label="Website value"
|
||||
data-hide-label-button={hideLabelButton}
|
||||
value={value.url}
|
||||
onChange={(event) => onChange({ ...value, url: event.target.value })}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/input/rich-input", () => ({
|
||||
RichInput: ({ value, onChange }: { value: string; onChange: (value: string) => void }) => (
|
||||
<textarea aria-label="Description value" value={value} onChange={(event) => onChange(event.target.value)} />
|
||||
),
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
i18n.loadAndActivate({ locale: "en", messages: {} });
|
||||
});
|
||||
|
||||
function TestForm() {
|
||||
const form = useAppForm({
|
||||
defaultValues: {
|
||||
description: "Initial description",
|
||||
website: { url: "https://example.com", label: "Example", inlineLink: false },
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<I18nProvider i18n={i18n}>
|
||||
<form.AppField
|
||||
name="website"
|
||||
validators={{ onChange: ({ value }) => (value.url ? undefined : "Website is required") }}
|
||||
>
|
||||
{(field) => <field.WebsiteField label="Website" hideLabelButton formItemClassName="website-field" />}
|
||||
</form.AppField>
|
||||
|
||||
<form.AppField
|
||||
name="description"
|
||||
validators={{ onChange: ({ value }) => (value ? undefined : "Description is required") }}
|
||||
>
|
||||
{(field) => <field.RichTextField label="Description" formItemClassName="description-field" />}
|
||||
</form.AppField>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe("registered resume fields", () => {
|
||||
it("preserves labels, layout classes, attributes, values, and errors", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(<TestForm />);
|
||||
|
||||
expect(screen.getByText("Website")).toBeInTheDocument();
|
||||
expect(screen.getByText("Description")).toBeInTheDocument();
|
||||
expect(container.querySelector(".website-field")).toBeInTheDocument();
|
||||
expect(container.querySelector(".description-field")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Website value")).toHaveAttribute("data-hide-label-button", "true");
|
||||
|
||||
await user.clear(screen.getByLabelText("Website value"));
|
||||
await user.clear(screen.getByLabelText("Description value"));
|
||||
|
||||
expect(screen.getByText("Website is required")).toBeInTheDocument();
|
||||
expect(screen.getByText("Description is required")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { Website } from "@reactive-resume/schema/resume/data";
|
||||
import type * as React from "react";
|
||||
import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
|
||||
import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
||||
import { Input } from "@reactive-resume/ui/components/input";
|
||||
import { InputGroupInput } from "@reactive-resume/ui/components/input-group";
|
||||
import { RichInput } from "@/components/input/rich-input";
|
||||
import { URLInput } from "@/components/input/url-input";
|
||||
|
||||
type FieldFrameProps = {
|
||||
label?: React.ReactNode;
|
||||
@@ -25,6 +28,17 @@ type NumberFieldProps = FieldFrameProps &
|
||||
"children" | "defaultValue" | "name" | "onBlur" | "onChange" | "type" | "value"
|
||||
>;
|
||||
|
||||
type WebsiteFieldProps = {
|
||||
label: React.ReactNode;
|
||||
formItemClassName?: string;
|
||||
hideLabelButton: boolean;
|
||||
};
|
||||
|
||||
type RichTextFieldProps = {
|
||||
label: React.ReactNode;
|
||||
formItemClassName?: string;
|
||||
};
|
||||
|
||||
const { fieldContext, formContext, useFieldContext } = createFormHookContexts();
|
||||
|
||||
function TextField({ label, description, formItemClassName, ...props }: TextFieldProps) {
|
||||
@@ -103,8 +117,38 @@ function NumberField({ label, description, formItemClassName, ...props }: Number
|
||||
);
|
||||
}
|
||||
|
||||
function WebsiteField({ label, formItemClassName, hideLabelButton }: WebsiteFieldProps) {
|
||||
const field = useFieldContext<Website>();
|
||||
const hasError = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
||||
|
||||
return (
|
||||
<FormItem hasError={hasError} className={formItemClassName}>
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<URLInput
|
||||
value={field.state.value}
|
||||
onChange={(value) => field.handleChange(value)}
|
||||
hideLabelButton={hideLabelButton}
|
||||
/>
|
||||
<FormMessage errors={field.state.meta.errors} />
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
function RichTextField({ label, formItemClassName }: RichTextFieldProps) {
|
||||
const field = useFieldContext<string>();
|
||||
const hasError = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
||||
|
||||
return (
|
||||
<FormItem hasError={hasError} className={formItemClassName}>
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<FormControl render={<RichInput value={field.state.value} onChange={(value) => field.handleChange(value)} />} />
|
||||
<FormMessage errors={field.state.meta.errors} />
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
export const { useAppForm, withForm } = createFormHook({
|
||||
fieldComponents: { InputGroupTextField, NumberField, TextField },
|
||||
fieldComponents: { InputGroupTextField, NumberField, RichTextField, TextField, WebsiteField },
|
||||
fieldContext,
|
||||
formComponents: {},
|
||||
formContext,
|
||||
|
||||
Reference in New Issue
Block a user