diff --git a/apps/web/src/components/ui/donation-toast.test.tsx b/apps/web/src/components/ui/donation-toast.test.tsx
index 0589e21d3..7e8547b27 100644
--- a/apps/web/src/components/ui/donation-toast.test.tsx
+++ b/apps/web/src/components/ui/donation-toast.test.tsx
@@ -14,9 +14,9 @@ type ToastOptions = {
unstyled: boolean;
};
-const useCookieMock = vi.hoisted(() => ({
- setDismissed: vi.fn(),
+const cookieMock = vi.hoisted(() => ({
value: null as string | null,
+ set: vi.fn(),
}));
const toastMock = vi.hoisted(() => ({
@@ -26,8 +26,11 @@ const toastMock = vi.hoisted(() => ({
},
}));
-vi.mock("@reactive-resume/ui/hooks/use-cookie", () => ({
- useCookie: vi.fn(() => [useCookieMock.value, useCookieMock.setDismissed, vi.fn()] as const),
+vi.mock("js-cookie", () => ({
+ default: {
+ get: vi.fn(() => cookieMock.value ?? undefined),
+ set: cookieMock.set,
+ },
}));
vi.mock("sonner", () => ({
@@ -51,8 +54,8 @@ describe("DonationToast", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-11T12:00:00.000Z"));
i18n.loadAndActivate({ locale: "en-US", messages: {} });
- useCookieMock.value = null;
- useCookieMock.setDismissed.mockClear();
+ cookieMock.value = null;
+ cookieMock.set.mockClear();
toastMock.toast.custom.mockClear();
toastMock.toast.dismiss.mockClear();
vi.spyOn(window, "open").mockReturnValue(null);
@@ -89,7 +92,7 @@ describe("DonationToast", () => {
});
it("does not show the toast after it has been dismissed", () => {
- useCookieMock.value = "true";
+ cookieMock.value = "true";
render();
@@ -110,7 +113,7 @@ describe("DonationToast", () => {
fireEvent.click(screen.getByRole("button", { name: "Dismiss" }));
- expect(useCookieMock.setDismissed).toHaveBeenCalledWith("true", {
+ expect(cookieMock.set).toHaveBeenCalledWith("donation-toast-dismissed", "true", {
expires: new Date("2026-06-10T12:05:00.000Z"),
});
expect(toastMock.toast.dismiss).toHaveBeenCalledWith("donation-toast");
@@ -126,7 +129,7 @@ describe("DonationToast", () => {
fireEvent.click(screen.getByRole("button", { name: "Donate" }));
- expect(useCookieMock.setDismissed).toHaveBeenCalledWith("true", {
+ expect(cookieMock.set).toHaveBeenCalledWith("donation-toast-dismissed", "true", {
expires: new Date("2026-06-10T12:05:00.000Z"),
});
expect(window.open).toHaveBeenCalledWith(
diff --git a/apps/web/src/components/ui/donation-toast.tsx b/apps/web/src/components/ui/donation-toast.tsx
index debef1de4..8b5a4a98b 100644
--- a/apps/web/src/components/ui/donation-toast.tsx
+++ b/apps/web/src/components/ui/donation-toast.tsx
@@ -1,10 +1,10 @@
import { Trans } from "@lingui/react/macro";
import { HandHeartIcon } from "@phosphor-icons/react";
-import { useCallback } from "react";
+import Cookies from "js-cookie";
+import { useCallback, useState } from "react";
import { toast } from "sonner";
import { useTimeout } from "usehooks-ts";
import { Button } from "@reactive-resume/ui/components/button";
-import { useCookie } from "@reactive-resume/ui/hooks/use-cookie";
const TOAST_ID = "donation-toast";
const SHOW_TOAST_DELAY_MS = 5 * 60 * 1000; // 5 minutes
@@ -14,7 +14,13 @@ const DISMISSED_COOKIE_EXPIRES_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
const getDismissedCookieExpiresAt = () => new Date(Date.now() + DISMISSED_COOKIE_EXPIRES_MS);
export function DonationToast() {
- const [dismissed, setDismissed] = useCookie(DISMISSED_COOKIE_NAME);
+ // ponytail: inlined from @reactive-resume/ui/hooks/use-cookie — only consumer, one read + one set-with-expiry
+ const [dismissed, setDismissedState] = useState(() => Cookies.get(DISMISSED_COOKIE_NAME) ?? null);
+
+ const setDismissed = useCallback((value: string, options?: { expires?: Date }) => {
+ Cookies.set(DISMISSED_COOKIE_NAME, value, options);
+ setDismissedState(value);
+ }, []);
const showToast = useCallback(() => {
if (dismissed === "true") return;
diff --git a/packages/ui/package.json b/packages/ui/package.json
index e03ba3d67..35dd2c6e4 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -26,7 +26,6 @@
"@shadcn/react": "^0.2.0",
"class-variance-authority": "^0.7.1",
"cmdk": "^1.1.1",
- "js-cookie": "^3.0.8",
"next-themes": "^0.4.6",
"react": "^19.2.7",
"react-dom": "^19.2.7",
@@ -39,7 +38,6 @@
"@reactive-resume/config": "workspace:*",
"@tailwindcss/postcss": "^4.3.2",
"@tailwindcss/typography": "^0.5.20",
- "@types/js-cookie": "^3.0.6",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "7.0.0-dev.20260704.1",
diff --git a/packages/ui/src/hooks/use-cookie.test.tsx b/packages/ui/src/hooks/use-cookie.test.tsx
deleted file mode 100644
index aa8f0a608..000000000
--- a/packages/ui/src/hooks/use-cookie.test.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import { act, renderHook } from "@testing-library/react";
-import { beforeEach, describe, expect, it, vi } from "vitest";
-import { DEFAULT_COOKIE_ATTRIBUTES, useCookie } from "./use-cookie";
-
-type CookieAttributes = {
- expires?: Date | number;
- path?: string;
- sameSite?: string;
- secure?: boolean;
-};
-
-const cookieMock = vi.hoisted(() => {
- const values = new Map();
-
- return {
- get: vi.fn((name: string) => values.get(name)),
- remove: vi.fn((name: string) => {
- values.delete(name);
- }),
- reset: () => values.clear(),
- set: vi.fn((name: string, value: string, _attributes?: CookieAttributes) => {
- values.set(name, value);
- }),
- values,
- };
-});
-
-vi.mock("js-cookie", () => ({
- default: {
- get: cookieMock.get,
- remove: cookieMock.remove,
- set: cookieMock.set,
- },
-}));
-
-describe("useCookie", () => {
- beforeEach(() => {
- cookieMock.reset();
- cookieMock.get.mockClear();
- cookieMock.set.mockClear();
- cookieMock.remove.mockClear();
- });
-
- it("reads an existing cookie value", () => {
- cookieMock.values.set("theme", "dark");
-
- const { result } = renderHook(() => useCookie("theme", "light"));
-
- expect(result.current[0]).toBe("dark");
- expect(cookieMock.set).not.toHaveBeenCalled();
- });
-
- it("sets the default value with secure defaults when the cookie is missing", () => {
- const { result } = renderHook(() => useCookie("theme", "dark"));
-
- expect(result.current[0]).toBe("dark");
- expect(cookieMock.set).toHaveBeenCalledWith("theme", "dark", DEFAULT_COOKIE_ATTRIBUTES);
- });
-
- it("does not write a cookie when no default value is provided", () => {
- const { result } = renderHook(() => useCookie("theme"));
-
- expect(result.current[0]).toBeNull();
- expect(cookieMock.set).not.toHaveBeenCalled();
- });
-
- it("updates the cookie value and state with secure defaults", () => {
- const { result } = renderHook(() => useCookie("theme"));
-
- act(() => {
- result.current[1]("dark");
- });
-
- expect(result.current[0]).toBe("dark");
- expect(cookieMock.set).toHaveBeenCalledWith("theme", "dark", DEFAULT_COOKIE_ATTRIBUTES);
- });
-
- it("passes through native expires cookie attributes", () => {
- const { result } = renderHook(() => useCookie("session"));
- const expires = new Date("2026-05-11T12:01:00.000Z");
-
- act(() => {
- result.current[1]("active", { expires });
- });
-
- const attributes = cookieMock.set.mock.calls.at(-1)?.[2] as CookieAttributes;
- expect(attributes).toMatchObject({ path: "/", sameSite: "lax", secure: true });
- expect(attributes.expires).toBe(expires);
- });
-
- it("does not expose a custom expiresMs option", () => {
- const { result } = renderHook(() => useCookie("session"));
-
- act(() => {
- // @ts-expect-error use the native js-cookie expires attribute instead
- result.current[1]("active", { expiresMs: 60_000 });
- });
-
- const attributes = cookieMock.set.mock.calls.at(-1)?.[2] as CookieAttributes & { expiresMs?: number };
- expect(attributes).not.toHaveProperty("expiresMs");
- });
-
- it("allows explicit cookie attributes to override defaults", () => {
- const { result } = renderHook(() => useCookie("theme"));
-
- act(() => {
- result.current[1]("dark", { sameSite: "strict", secure: false });
- });
-
- expect(cookieMock.set).toHaveBeenCalledWith("theme", "dark", {
- ...DEFAULT_COOKIE_ATTRIBUTES,
- sameSite: "strict",
- secure: false,
- });
- });
-
- it("removes the cookie with secure defaults and clears state", () => {
- cookieMock.values.set("theme", "dark");
- const { result } = renderHook(() => useCookie("theme"));
-
- act(() => {
- result.current[2]();
- });
-
- expect(result.current[0]).toBeNull();
- expect(cookieMock.remove).toHaveBeenCalledWith("theme", DEFAULT_COOKIE_ATTRIBUTES);
- });
-});
diff --git a/packages/ui/src/hooks/use-cookie.tsx b/packages/ui/src/hooks/use-cookie.tsx
deleted file mode 100644
index 1c3442db9..000000000
--- a/packages/ui/src/hooks/use-cookie.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import Cookie from "js-cookie";
-import { useCallback, useEffect, useMemo, useState } from "react";
-
-type CookieAttributes = NonNullable[2]>;
-
-type SharedCookieOptions = {
- domain?: CookieAttributes["domain"];
- partitioned?: boolean;
- path?: CookieAttributes["path"];
- sameSite?: CookieAttributes["sameSite"];
- secure?: CookieAttributes["secure"];
-};
-
-type UseCookieOptions = SharedCookieOptions & {
- expires?: CookieAttributes["expires"];
-};
-type UseCookieRemoveOptions = SharedCookieOptions;
-
-export const DEFAULT_COOKIE_ATTRIBUTES = {
- path: "/",
- secure: true,
- sameSite: "lax",
-} as const satisfies UseCookieRemoveOptions;
-
-const canUseCookies = () => typeof document !== "undefined";
-
-const getCookie = (name: string) => {
- if (!canUseCookies()) return null;
- return Cookie.get(name) ?? null;
-};
-
-const compactCookieOptions = ({
- domain,
- expires,
- partitioned,
- path,
- sameSite,
- secure,
-}: UseCookieOptions): CookieAttributes => ({
- ...(domain !== undefined && { domain }),
- ...(expires !== undefined && { expires }),
- ...(partitioned !== undefined && { partitioned }),
- ...(path !== undefined && { path }),
- ...(sameSite !== undefined && { sameSite }),
- ...(secure !== undefined && { secure }),
-});
-
-const resolveCookieOptions = (options?: UseCookieOptions): CookieAttributes => {
- return {
- ...DEFAULT_COOKIE_ATTRIBUTES,
- ...(options && compactCookieOptions(options)),
- };
-};
-
-const resolveRemoveOptions = (options?: UseCookieRemoveOptions): CookieAttributes => {
- return {
- ...DEFAULT_COOKIE_ATTRIBUTES,
- ...(options && compactCookieOptions(options)),
- };
-};
-
-export function useCookie(
- name: string,
- defaultValue?: string,
- defaultOptions?: UseCookieOptions,
-): readonly [
- string | null,
- (value: string, options?: UseCookieOptions) => void,
- (options?: UseCookieRemoveOptions) => void,
-] {
- const initialValue = useMemo(() => getCookie(name) ?? defaultValue ?? null, [name, defaultValue]);
- const [value, setValue] = useState(initialValue);
-
- useEffect(() => {
- const cookie = getCookie(name);
- let nextValue: string | null;
-
- if (cookie !== null) {
- nextValue = cookie;
- } else if (defaultValue === undefined) {
- nextValue = null;
- } else {
- if (canUseCookies()) Cookie.set(name, defaultValue, resolveCookieOptions(defaultOptions));
- nextValue = defaultValue;
- }
-
- setValue(nextValue);
- }, [name, defaultValue, defaultOptions]);
-
- const updateCookie = useCallback(
- (nextValue: string, options?: UseCookieOptions) => {
- if (canUseCookies()) Cookie.set(name, nextValue, resolveCookieOptions(options));
- setValue(nextValue);
- },
- [name],
- );
-
- const deleteCookie = useCallback(
- (options?: UseCookieRemoveOptions) => {
- if (canUseCookies()) Cookie.remove(name, resolveRemoveOptions(options));
- setValue(null);
- },
- [name],
- );
-
- return [value, updateCookie, deleteCookie] as const;
-}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d53d4cc2e..c311db387 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -63,9 +63,6 @@ importers:
lefthook:
specifier: ^2.1.9
version: 2.1.9
- npm-check-updates:
- specifier: ^22.2.9
- version: 22.2.9
pg:
specifier: ^8.22.0
version: 8.22.0
@@ -1028,9 +1025,6 @@ importers:
cmdk:
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
- js-cookie:
- specifier: ^3.0.8
- version: 3.0.8
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -1062,9 +1056,6 @@ importers:
'@tailwindcss/typography':
specifier: ^0.5.20
version: 0.5.20(tailwindcss@4.3.2)
- '@types/js-cookie':
- specifier: ^3.0.6
- version: 3.0.6
'@types/react':
specifier: ^19.2.17
version: 19.2.17
@@ -6242,11 +6233,6 @@ packages:
normalize-wheel@1.0.1:
resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==}
- npm-check-updates@22.2.9:
- resolution: {integrity: sha512-DVeZ0KirHfliSsHuR2o7cHE+tW439sVHfJjF6cGWeDiY0Wyl3BI/jS4zV0eixtcMOquFbcF1Su/FsxOvk5MoYA==}
- engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10.0.0'}
- hasBin: true
-
npm-run-path@4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
@@ -12792,8 +12778,6 @@ snapshots:
normalize-wheel@1.0.1: {}
- npm-check-updates@22.2.9: {}
-
npm-run-path@4.0.1:
dependencies:
path-key: 3.1.1