diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 052ec1a..fa4f33a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,7 +4,7 @@ on: push: branches: ["main"] pull_request: - branches: ["main"] + branches: ["**"] jobs: test: @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Bun - uses: oven-sh/setup-bun@v1 + uses: oven-sh/setup-bun@v2 with: bun-version: latest @@ -50,24 +50,23 @@ jobs: uses: actions/checkout@v4 - name: Set up Bun - uses: oven-sh/setup-bun@v1 + uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Install dependencies run: bun install --frozen-lockfile - - - name: Install Playwright browsers + + - name: Install Playwright run: bunx playwright install --with-deps - name: Run Playwright tests - run: bunx playwright test + run: bun test:e2e - name: Upload Playwright report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() with: name: playwright-report path: playwright-report/ retention-days: 30 - diff --git a/__tests__/lib/hooks/useRandomEmoji.test.ts b/__tests__/lib/hooks/useRandomEmoji.test.ts new file mode 100644 index 0000000..082048e --- /dev/null +++ b/__tests__/lib/hooks/useRandomEmoji.test.ts @@ -0,0 +1,132 @@ +import { act, cleanup, renderHook } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; +import { DEFAULT_EMOJIS, useRandomEmoji } from "lib/hooks/useRandomEmoji"; + +// Mock timers +let intervalCallback: () => void; +const mockSetInterval = mock((callback: () => void) => { + intervalCallback = callback; + return {} as NodeJS.Timeout; +}); +const mockClearInterval = mock(() => {}); + +global.setInterval = mockSetInterval as unknown as typeof setInterval; +global.clearInterval = mockClearInterval as unknown as typeof clearInterval; + +describe("useRandomEmoji", () => { + // Reset mocks before each test + beforeEach(() => { + mockSetInterval.mockClear(); + mockClearInterval.mockClear(); + }); + + // Clean up after each test + afterEach(() => { + cleanup(); + }); + + test("returns the first emoji by default", () => { + const emojis = ["😀", "😎", "🤓"]; + const { result } = renderHook(() => useRandomEmoji({ emojis })); + expect(result.current).toBe("😀"); + }); + + test("uses default emojis when none provided", () => { + const { result } = renderHook(() => useRandomEmoji()); + expect(DEFAULT_EMOJIS).toContain(result.current); + }); + + test("returns empty string when emojis array is empty", () => { + const { result } = renderHook(() => useRandomEmoji({ emojis: [] })); + expect(result.current).toBe(""); + }); + + test("returns a string from the provided emojis array", () => { + const emojis = ["😀", "😎", "🤓"]; + const { result } = renderHook(() => useRandomEmoji({ emojis })); + expect(emojis).toContain(result.current); + }); + + test("sets up interval with correct timing", () => { + const emojis = ["😀", "😎"]; + const interval = 1000; + + renderHook(() => useRandomEmoji({ emojis, interval })); + + // Should call setInterval with the correct interval + expect(mockSetInterval).toHaveBeenCalledTimes(1); + }); + + test("cleans up interval on unmount", () => { + const emojis = ["😀", "😎"]; + const { unmount } = renderHook(() => useRandomEmoji({ emojis })); + + // Get the interval ID that was returned by setInterval + const intervalId = mockSetInterval.mock.results[0].value; + + // Unmount the component + unmount(); + + // Should have called clearInterval with the correct ID + expect(mockClearInterval).toHaveBeenCalledTimes(1); + expect(mockClearInterval).toHaveBeenCalledWith(intervalId); + }); + + test("cycles through emojis correctly", () => { + const emojis = ["😀", "😎", "🤓"]; + + // Render the hook and get the initial state + const { result } = renderHook(() => useRandomEmoji({ emojis })); + + // Initial emoji should be the first one + expect(result.current).toBe(emojis[0]); + + // Call the interval callback to simulate time passing + act(() => { + intervalCallback(); + }); + + // Should now be the second emoji + expect(emojis).toContain(result.current); + + // Call it again + act(() => { + intervalCallback(); + }); + + // Should now be the third emoji + expect(emojis).toContain(result.current); + + // Call it one more time to test wrap-around + act(() => { + intervalCallback(); + }); + + // Should be one of the emojis (can't guarantee order due to random selection) + expect(emojis).toContain(result.current); + }); + + test("handles emojis change", () => { + const initialEmojis = ["😀", "😎"]; + const newEmojis = ["🚀", "🌟", "✨"]; + + const { result, rerender } = renderHook( + ({ emojis }) => useRandomEmoji({ emojis }), + { initialProps: { emojis: initialEmojis } }, + ); + + // Initial emoji from first array + expect(initialEmojis).toContain(result.current); + + // Change the emojis + rerender({ emojis: newEmojis }); + + // Call interval to force a re-render with new emojis + act(() => { + intervalCallback(); + }); + + // Should now be using the new emojis + expect(newEmojis).toContain(result.current); + }); +}); diff --git a/__tests__/lib/hooks/useUrlHash.test.ts b/__tests__/lib/hooks/useUrlHash.test.ts new file mode 100644 index 0000000..c6d2f2e --- /dev/null +++ b/__tests__/lib/hooks/useUrlHash.test.ts @@ -0,0 +1,157 @@ +import { act, renderHook } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; +import useUrlHash from "lib/hooks/useUrlHash"; + +// Mock window and location objects +interface MockLocation extends Location { + hash: string; + href: string; + assign: ReturnType; + onhashchange?: (event: HashChangeEvent) => void; +} + +const mockLocation: MockLocation = { + hash: "", + href: "http://test.com", + assign: mock(() => {}), + onhashchange: undefined, + // Add required Location properties with dummy values + ancestorOrigins: [] as unknown as DOMStringList, + host: "test.com", + hostname: "test.com", + origin: "http://test.com", + pathname: "/", + port: "", + protocol: "http:", + search: "", + replace: mock(() => {}), + reload: mock(() => {}), + toString: () => "http://test.com", +}; + +// Track event listeners +const eventListeners: Record = {}; + +const mockAddEventListener = mock((event: string, callback: EventListener) => { + eventListeners[event] = callback; +}); + +const mockRemoveEventListener = mock((event: string) => { + delete eventListeners[event]; +}); + +// Store original globals +const originalWindow = global.window; +const originalLocation = global.location; + +// Helper to trigger hash change +function triggerHashChange(newHash: string, oldHash = "") { + const oldURL = `http://test.com${oldHash ? `#${oldHash}` : ""}`; + const newURL = `http://test.com${newHash ? `#${newHash}` : ""}`; + + mockLocation.hash = newHash ? `#${newHash}` : ""; + + const hashChangeCallback = eventListeners.hashchange; + if (hashChangeCallback) { + const event = new Event("hashchange") as HashChangeEvent; + Object.defineProperty(event, "oldURL", { value: oldURL }); + Object.defineProperty(event, "newURL", { value: newURL }); + + hashChangeCallback(event); + } +} + +describe("useUrlHash", () => { + beforeEach(() => { + // Reset mocks and state + mockLocation.hash = ""; + mockLocation.href = "http://test.com"; + mockLocation.assign.mockClear(); + mockAddEventListener.mockClear(); + mockRemoveEventListener.mockClear(); + + // Apply mocks + global.window = { + ...global.window, + addEventListener: mockAddEventListener, + removeEventListener: mockRemoveEventListener, + location: mockLocation, + // biome-ignore lint/suspicious/noExplicitAny: + } as any; + + // biome-ignore lint/suspicious/noExplicitAny: + global.location = mockLocation as any; + }); + + afterEach(() => { + // Restore original globals + global.window = originalWindow; + global.location = originalLocation; + }); + + test("should return false when hash does not match", () => { + mockLocation.hash = "#other-hash"; + const { result } = renderHook(() => useUrlHash("test-hash")); + expect(result.current).toBe(false); + }); + + test("should return true when hash matches", () => { + mockLocation.hash = "#test-hash"; + const { result } = renderHook(() => useUrlHash("test-hash")); + expect(result.current).toBe(true); + }); + + test("should work with or without leading # in the hash", () => { + mockLocation.hash = "#test-hash"; + + // Test with hash that includes # + const { result: result1 } = renderHook(() => useUrlHash("#test-hash")); + expect(result1.current).toBe(true); + + // Test with hash that doesn't include # + const { result: result2 } = renderHook(() => useUrlHash("test-hash")); + expect(result2.current).toBe(true); + }); + + test("should update when hash changes", () => { + // Initial render with no hash + const { result } = renderHook(() => useUrlHash("test-hash")); + expect(result.current).toBe(false); + + // Simulate hash change + act(() => { + triggerHashChange("test-hash"); + }); + + expect(result.current).toBe(true); + + // Simulate hash change to something else + act(() => { + triggerHashChange("other-hash", "test-hash"); + }); + + expect(result.current).toBe(false); + }); + + test("should add hashchange event listener on mount", () => { + renderHook(() => useUrlHash("test-hash")); + expect(mockAddEventListener).toHaveBeenCalledWith( + "hashchange", + expect.any(Function), + ); + }); + + test("should remove hashchange event listener on unmount", () => { + const { unmount } = renderHook(() => useUrlHash("test-hash")); + + // Get the event handler that was added + const [eventName, eventHandler] = mockAddEventListener.mock.calls[0]; + + unmount(); + + expect(mockRemoveEventListener).toHaveBeenCalledWith( + eventName, + eventHandler, + ); + }); +}); diff --git a/__tests__/lib/utils/array.test.ts b/__tests__/lib/utils/array.test.ts new file mode 100644 index 0000000..5374ac3 --- /dev/null +++ b/__tests__/lib/utils/array.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from "bun:test"; +import { filterItems } from "lib/utils/array"; + +describe("Array Utilities", () => { + describe("filterItems", () => { + const testItems = ["React", "TypeScript", "JavaScript", "HTML", "CSS"]; + const excludedItems = ["css"]; // Using lowercase to test case insensitivity + + test("returns all items when search text is empty", () => { + const result = filterItems(testItems, [], ""); + expect(result).toEqual(testItems); + }); + + test("filters items by search text", () => { + const result = filterItems(testItems, [], "script"); + expect(result).toEqual(["TypeScript", "JavaScript"]); + }); + + test("excludes items in the excluded list (case insensitive)", () => { + // Test with exact case match + const result1 = filterItems(testItems, ["CSS"], ""); + expect(result1).toEqual(expect.not.arrayContaining(["CSS"])); + + // Test with different case + const result2 = filterItems(testItems, ["css"], ""); + expect(result2).toEqual(expect.not.arrayContaining(["CSS"])); + + // Test with multiple exclusions + const result3 = filterItems(testItems, ["css", "react"], ""); + expect(result3).toEqual(expect.not.arrayContaining(["CSS", "React"])); + expect(result3).toHaveLength(3); + }); + + test("is case insensitive", () => { + const result = filterItems(testItems, [], "typescript"); + expect(result).toEqual(["TypeScript"]); + }); + + test("returns empty array when no matches found", () => { + const result = filterItems(testItems, [], "nonexistent"); + expect(result).toEqual([]); + }); + }); +}); diff --git a/__tests__/lib/utils/chart-settings.test.ts b/__tests__/lib/utils/chart-settings.test.ts new file mode 100644 index 0000000..754d784 --- /dev/null +++ b/__tests__/lib/utils/chart-settings.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, test } from "bun:test"; +import { + type ChartTheme, + themedLineSettings, + themedRadarSettings, +} from "lib/utils/chart"; + +describe("Chart Settings Utilities", () => { + const mockTheme: ChartTheme = { + charts: "#123456", + text: "#ffffff", + headerText: "#cccccc", + }; + + describe("themedRadarSettings", () => { + test("returns correct radar chart settings with theme applied", () => { + const title = "Test Radar Chart"; + const settings = themedRadarSettings(title, mockTheme); + + expect(settings).toMatchObject({ + maintainAspectRatio: false, + aspectRatio: 1, + tooltips: { enabled: false }, + plugins: { + title: { + display: true, + text: title, + color: mockTheme.text, + }, + legend: { display: false }, + }, + }); + + // Check scales configuration + expect(settings.scales?.r).toBeDefined(); + expect(settings.scales?.r.ticks).toEqual({ + display: false, + maxTicksLimit: 1, + }); + expect(settings.scales?.r.pointLabels).toEqual({ + color: mockTheme.text, + }); + }); + }); + + describe("themedLineSettings", () => { + test("returns correct line chart settings with theme applied", () => { + const title = "Test Line Chart"; + const settings = themedLineSettings(title, mockTheme); + + expect(settings).toMatchObject({ + maintainAspectRatio: false, + aspectRatio: 1, + spanGaps: false, + plugins: { + title: { + display: true, + text: title, + color: mockTheme.text, + }, + legend: { + position: "top", + fullWidth: true, + labels: { + boxWidth: 5, + color: mockTheme.text, + }, + }, + }, + tooltips: { enabled: false }, + }); + + // Check scales configuration + expect(settings.scales?.xAxis).toBeDefined(); + expect(settings.scales?.xAxis.ticks).toEqual({ + color: mockTheme.text, + }); + expect(settings.scales?.xAxis.gridLines).toEqual({ + drawTicks: false, + drawOnChartArea: false, + color: mockTheme.headerText, + }); + + expect(settings.scales?.yAxis).toBeDefined(); + expect(settings.scales?.yAxis).toMatchObject({ + display: true, + ticks: { display: false }, + gridLines: { + drawTicks: false, + drawOnChartArea: false, + color: mockTheme.headerText, + }, + }); + }); + }); +}); diff --git a/__tests__/lib/utils/chart.test.ts b/__tests__/lib/utils/chart.test.ts new file mode 100644 index 0000000..304676b --- /dev/null +++ b/__tests__/lib/utils/chart.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "bun:test"; +import { randomChartData } from "lib/utils/chart"; + +describe("Chart Utilities", () => { + describe("randomChartData", () => { + test("returns an object with the correct structure", () => { + const result = randomChartData(1); + + expect(result).toHaveProperty("title"); + expect(result).toHaveProperty("labels"); + expect(result).toHaveProperty("values"); + + expect(Array.isArray(result.labels)).toBe(true); + expect(Array.isArray(result.values)).toBe(true); + }); + + test("includes the provided id in the title", () => { + const id = 42; + const result = randomChartData(id); + + expect(result.title).toBe(`Chart #${id}`); + }); + + test("returns the expected number of data points", () => { + const result = randomChartData(1); + + // Should have 5 time periods + expect(result.labels).toHaveLength(5); + + // Should have 4 datasets (Learning, Mingeling, Involvement, Contribution) + expect(result.values).toHaveLength(4); + + // Each dataset should have 5 data points + for (const dataset of result.values) { + expect(dataset.data).toHaveLength(5); + } + }); + + test("generates random data between 0 and 100", () => { + const result = randomChartData(1); + + for (const dataset of result.values) { + for (const value of dataset.data) { + expect(value).toBeGreaterThanOrEqual(0); + expect(value).toBeLessThanOrEqual(100); + } + } + }); + }); +}); diff --git a/__tests__/lib/utils/color.test.ts b/__tests__/lib/utils/color.test.ts new file mode 100644 index 0000000..86be82b --- /dev/null +++ b/__tests__/lib/utils/color.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "bun:test"; +import { hexToHSL, hexToRgb } from "lib/utils/color"; + +describe("Color Utilities", () => { + describe("hexToRgb", () => { + test("converts 3-digit hex to RGB", () => { + expect(hexToRgb("#F00")).toEqual([255, 0, 0]); + expect(hexToRgb("#0F0")).toEqual([0, 255, 0]); + expect(hexToRgb("#00F")).toEqual([0, 0, 255]); + expect(hexToRgb("#FFF")).toEqual([255, 255, 255]); + expect(hexToRgb("#000")).toEqual([0, 0, 0]); + }); + + test("converts 6-digit hex to RGB", () => { + expect(hexToRgb("#FF0000")).toEqual([255, 0, 0]); + expect(hexToRgb("#00FF00")).toEqual([0, 255, 0]); + expect(hexToRgb("#0000FF")).toEqual([0, 0, 255]); + expect(hexToRgb("#FFFFFF")).toEqual([255, 255, 255]); + expect(hexToRgb("#000000")).toEqual([0, 0, 0]); + }); + + test("throws error for invalid hex format", () => { + expect(() => hexToRgb("red")).toThrow("Invalid hex color format"); + expect(() => hexToRgb("#FF")).toThrow("Invalid hex color format"); + expect(() => hexToRgb("#FFFFF")).toThrow("Invalid hex color format"); + expect(() => hexToRgb("#GGGGGG")).toThrow("Invalid hex color format"); + }); + }); + + describe("hexToHSL", () => { + test("converts hex to HSL", () => { + // Red + expect(hexToHSL("#F00")).toEqual([0, 100, 50]); + // Green + expect(hexToHSL("#0F0")).toEqual([120, 100, 50]); + // Blue + expect(hexToHSL("#00F")).toEqual([240, 100, 50]); + // White + expect(hexToHSL("#FFF")).toEqual([0, 0, 100]); + // Black + expect(hexToHSL("#000")).toEqual([0, 0, 0]); + // Random color + const [h, s, l] = hexToHSL("#1E88E5"); + expect(h).toBeGreaterThanOrEqual(0); + expect(h).toBeLessThanOrEqual(360); + expect(s).toBeGreaterThanOrEqual(0); + expect(s).toBeLessThanOrEqual(100); + expect(l).toBeGreaterThanOrEqual(0); + expect(l).toBeLessThanOrEqual(100); + }); + + test("throws error for invalid hex format", () => { + expect(() => hexToHSL("red")).toThrow("Invalid hex color format"); + expect(() => hexToHSL("#FF")).toThrow("Invalid hex color format"); + expect(() => hexToHSL("#FFFFF")).toThrow("Invalid hex color format"); + expect(() => hexToHSL("#GGGGGG")).toThrow("Invalid hex color format"); + }); + }); +}); diff --git a/__tests__/lib/utils/job.test.ts b/__tests__/lib/utils/job.test.ts new file mode 100644 index 0000000..a3ba18e --- /dev/null +++ b/__tests__/lib/utils/job.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, test } from "bun:test"; +import type { Job } from "lib/types/jobs"; +import { filterJobsByText, sortJobsByDate } from "lib/utils/job"; + +const mockJobs: Job[] = [ + { + title: "Senior Frontend Developer", + description: "Building modern web applications with React and TypeScript", + duration: { + from: new Date("2020-01-01").getTime(), + to: new Date("2022-12-31").getTime(), + }, + company: { + logo: "https://techcorp.com/logo.png", + name: "Tech Corp", + website: "https://techcorp.com", + }, + tags: ["React", "TypeScript", "Frontend"], + }, + { + title: "Backend Engineer", + description: "Building scalable APIs with Node.js", + duration: { + from: new Date("2023-01-01").getTime(), + to: new Date("2023-12-31").getTime(), + }, + company: { + logo: "https://apimasters.com/logo.png", + name: "API Masters", + website: "https://apimasters.com", + }, + tags: ["Node.js", "API", "Backend"], + }, + { + title: "Full Stack Developer", + description: "Working on both frontend and backend with React and Node.js", + duration: { + from: new Date("2021-06-01").getTime(), + to: new Date("2023-06-30").getTime(), + }, + company: { + logo: "https://fullstack.io/logo.png", + name: "FullStack Inc", + website: "https://fullstack.io", + }, + tags: ["React", "Node.js", "Fullstack"], + }, +]; + +describe("Job Utilities", () => { + describe("filterJobsByText", () => { + test("returns all jobs when search text is empty", () => { + const result = filterJobsByText(mockJobs, ""); + expect(result).toHaveLength(mockJobs.length); + expect(result[0].title).toBe("Full Stack Developer"); // Should be reversed + }); + + test("filters jobs by title", () => { + const result = filterJobsByText(mockJobs, "Backend"); + // Should match both 'Backend Engineer' and 'Full Stack Developer' (which has 'backend' in description) + expect(result).toHaveLength(2); + const titles = result.map((job) => job.title); + expect(titles).toContain("Backend Engineer"); + expect(titles).toContain("Full Stack Developer"); + }); + + test("filters jobs by description", () => { + const result = filterJobsByText(mockJobs, "modern web"); + expect(result).toHaveLength(1); + expect(result[0].title).toBe("Senior Frontend Developer"); + }); + + test("filters jobs by tags", () => { + const result = filterJobsByText(mockJobs, "React"); + expect(result).toHaveLength(2); + expect(result[0].title).toBe("Full Stack Developer"); + expect(result[1].title).toBe("Senior Frontend Developer"); + }); + + test("handles multiple search terms", () => { + const result = filterJobsByText(mockJobs, "React, Node.js"); + expect(result).toHaveLength(3); // All jobs match at least one term + }); + + test("is case insensitive", () => { + const result = filterJobsByText(mockJobs, "react"); + expect(result).toHaveLength(2); + expect(result[0].title).toBe("Full Stack Developer"); + }); + }); + + describe("sortJobsByDate", () => { + test("sorts jobs by end date in descending order", () => { + const result = sortJobsByDate(mockJobs); + expect(result[0].title).toBe("Backend Engineer"); // 2023-12-31 + expect(result[1].title).toBe("Full Stack Developer"); // 2023-06-30 + expect(result[2].title).toBe("Senior Frontend Developer"); // 2022-12-31 + }); + }); +}); diff --git a/__tests__/lib/utils/match.test.ts b/__tests__/lib/utils/match.test.ts new file mode 100644 index 0000000..9b188f3 --- /dev/null +++ b/__tests__/lib/utils/match.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from "bun:test"; +import { calculateMatch, getQualificationText } from "lib/utils/match"; + +describe("Match Utilities", () => { + const testRankings = { + react: 10, + typescript: 8, + javascript: 7, + nodejs: 6, + python: 5, + }; + + describe("calculateMatch", () => { + test("returns 0 for empty attributes array", () => { + const result = calculateMatch([], testRankings); + expect(result).toBe(0); + }); + + test("sums up scores for matched attributes", () => { + const result = calculateMatch(["react", "typescript"], testRankings); + expect(result).toBe(18); // 10 (react) + 8 (typescript) + }); + + test("ignores attributes not found in rankings", () => { + const result = calculateMatch(["react", "unknown"], testRankings); + expect(result).toBe(10); // Only react (10) is counted + }); + + test("works with empty rankings object", () => { + const result = calculateMatch(["react", "typescript"], {}); + expect(result).toBe(0); + }); + }); + + describe("getQualificationText", () => { + test('returns "Perfect Match!" for scores >= 90', () => { + expect(getQualificationText(90)).toBe("Perfect Match!"); + expect(getQualificationText(95)).toBe("Perfect Match!"); + expect(getQualificationText(100)).toBe("Perfect Match!"); + }); + + test('returns "Great Match!" for scores between 70-89', () => { + expect(getQualificationText(70)).toBe("Great Match!"); + expect(getQualificationText(80)).toBe("Great Match!"); + expect(getQualificationText(89)).toBe("Great Match!"); + }); + + test('returns "Good Match" for scores between 50-69', () => { + expect(getQualificationText(50)).toBe("Good Match"); + expect(getQualificationText(60)).toBe("Good Match"); + expect(getQualificationText(69)).toBe("Good Match"); + }); + + test('returns "Fair Match" for scores between 30-49', () => { + expect(getQualificationText(30)).toBe("Fair Match"); + expect(getQualificationText(40)).toBe("Fair Match"); + expect(getQualificationText(49)).toBe("Fair Match"); + }); + + test('returns "Needs Improvement" for scores below 30', () => { + expect(getQualificationText(0)).toBe(""); + expect(getQualificationText(15)).toBe(""); + expect(getQualificationText(29)).toBe(""); + }); + }); +}); diff --git a/__tests__/lib/utils/quote.test.ts b/__tests__/lib/utils/quote.test.ts new file mode 100644 index 0000000..0bc4155 --- /dev/null +++ b/__tests__/lib/utils/quote.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from "bun:test"; +import { type QuotesCollection, extractQuotesByPerson } from "lib/utils/quote"; + +describe("Quote Utilities", () => { + describe("extractQuotesByPerson", () => { + test("extracts and flattens quotes from a nested collection", () => { + const mockQuotes: QuotesCollection = { + "Person 1": { + role: "Role 1", + profile: "Profile 1", + quotes: ["Quote 1", "Quote 2"], + }, + "Person 2": { + role: "Role 2", + profile: "Profile 2", + quotes: ["Quote 3"], + }, + }; + + const result = extractQuotesByPerson(mockQuotes); + + expect(result).toHaveLength(3); + expect(result).toContainEqual({ + role: "Role 1", + profile: "Profile 1", + quote: "Quote 1", + }); + expect(result).toContainEqual({ + role: "Role 1", + profile: "Profile 1", + quote: "Quote 2", + }); + expect(result).toContainEqual({ + role: "Role 2", + profile: "Profile 2", + quote: "Quote 3", + }); + }); + + test("returns an empty array for an empty collection", () => { + const result = extractQuotesByPerson({}); + expect(result).toEqual([]); + }); + + test("handles a collection with empty quotes array", () => { + const mockQuotes: QuotesCollection = { + "Person 1": { + role: "Role 1", + profile: "Profile 1", + quotes: [], + }, + }; + + const result = extractQuotesByPerson(mockQuotes); + expect(result).toEqual([]); + }); + }); +}); diff --git a/__tests__/lib/utils/routing.test.ts b/__tests__/lib/utils/routing.test.ts new file mode 100644 index 0000000..8b0fc07 --- /dev/null +++ b/__tests__/lib/utils/routing.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "bun:test"; +import { getRoute } from "lib/utils/routing"; + +describe("Routing Utilities", () => { + describe("getRoute", () => { + test("returns root path with default locale when pathname is empty", () => { + const result = getRoute(""); + expect(result).toBe("/en"); + }); + + test("switches between he and en locales", () => { + // Test switching from en to he + const enPath = "/en/about"; + const hePath = getRoute(enPath, "he"); + expect(hePath).toBe("/he/about"); + + // Test switching from he to en + const backToEnPath = getRoute(hePath, "en"); + expect(backToEnPath).toBe("/en/about"); + }); + + test("toggles locale when no target locale is provided", () => { + // From en to he + const enPath = "/en/about"; + const hePath = getRoute(enPath); + expect(hePath).toBe("/he/about"); + + // From he to en + const backToEnPath = getRoute(hePath); + expect(backToEnPath).toBe("/en/about"); + }); + + test("handles root path correctly", () => { + const rootPath = "/"; + const result = getRoute(rootPath, "he"); + expect(result).toBe("/he"); + }); + + test("handles paths with multiple segments", () => { + const path = "/en/blog/post-1"; + const result = getRoute(path, "he"); + expect(result).toBe("/he/blog/post-1"); + }); + }); +}); diff --git a/__tests__/lib/utils/tags.test.ts b/__tests__/lib/utils/tags.test.ts new file mode 100644 index 0000000..a5f336b --- /dev/null +++ b/__tests__/lib/utils/tags.test.ts @@ -0,0 +1,150 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { + addTag, + handleTagInputKeyDown, + removeLastTag, + removeTag, +} from "lib/utils/tags"; +import { vi } from "vitest"; + +describe("Tag Utilities", () => { + describe("handleTagInputKeyDown", () => { + const mockEvent = { + preventDefault: vi.fn(), + key: "", + target: { value: "test" }, + } as unknown as React.KeyboardEvent; + + const mockFilteredItems = ["apple", "banana", "cherry"]; + const mockOnEnter = vi.fn(); + const mockOnEscape = vi.fn(); + const mockOnBackspace = vi.fn(); + + afterEach(() => { + vi.clearAllMocks(); + }); + + test("handles ArrowDown key", () => { + const newIndex = handleTagInputKeyDown( + { ...mockEvent, key: "ArrowDown" }, + 0, + mockFilteredItems, + mockOnEnter, + mockOnEscape, + mockOnBackspace, + ); + expect(newIndex).toBe(1); + expect(mockOnEnter).not.toHaveBeenCalled(); + expect(mockOnEscape).not.toHaveBeenCalled(); + expect(mockOnBackspace).not.toHaveBeenCalled(); + }); + + test("handles ArrowUp key", () => { + const newIndex = handleTagInputKeyDown( + { ...mockEvent, key: "ArrowUp" }, + 1, + mockFilteredItems, + mockOnEnter, + mockOnEscape, + mockOnBackspace, + ); + expect(newIndex).toBe(0); + }); + + test("handles Enter key", () => { + const newIndex = handleTagInputKeyDown( + { ...mockEvent, key: "Enter" }, + 1, + mockFilteredItems, + mockOnEnter, + mockOnEscape, + mockOnBackspace, + ); + expect(newIndex).toBe(1); + expect(mockOnEnter).toHaveBeenCalledWith("banana"); + }); + + test("handles Escape key", () => { + const newIndex = handleTagInputKeyDown( + { ...mockEvent, key: "Escape" }, + 1, + mockFilteredItems, + mockOnEnter, + mockOnEscape, + mockOnBackspace, + ); + expect(newIndex).toBe(1); + expect(mockOnEscape).toHaveBeenCalled(); + }); + + test("handles Backspace key with empty input", () => { + const newIndex = handleTagInputKeyDown( + { + ...mockEvent, + key: "Backspace", + target: { value: "" }, + } as unknown as typeof mockEvent, + 1, + mockFilteredItems, + mockOnEnter, + mockOnEscape, + mockOnBackspace, + ); + expect(newIndex).toBe(1); + expect(mockOnBackspace).toHaveBeenCalled(); + }); + }); + + describe("removeTag", () => { + test("removes the specified tag", () => { + const tags = ["apple", "banana", "cherry"]; + const result = removeTag(tags, "banana"); + expect(result).toEqual(["apple", "cherry"]); + }); + + test("returns a new array", () => { + const tags = ["apple", "banana"]; + const result = removeTag(tags, "banana"); + expect(result).not.toBe(tags); + }); + }); + + describe("removeLastTag", () => { + test("removes the last tag", () => { + const tags = ["apple", "banana", "cherry"]; + const result = removeLastTag(tags); + expect(result).toEqual(["apple", "banana"]); + }); + + test("returns empty array for single tag", () => { + const tags = ["apple"]; + const result = removeLastTag(tags); + expect(result).toEqual([]); + }); + + test("returns empty array for empty input", () => { + const result = removeLastTag([]); + expect(result).toEqual([]); + }); + }); + + describe("addTag", () => { + test("adds a new tag", () => { + const tags = ["apple", "banana"]; + const result = addTag(tags, "cherry"); + expect(result).toEqual(["apple", "banana", "cherry"]); + }); + + test("does not add duplicate tags (case insensitive)", () => { + const tags = ["apple", "banana"]; + const result = addTag(tags, "BANANA"); + expect(result).toEqual(["apple", "banana"]); + }); + + test("returns a new array", () => { + const tags = ["apple"]; + const result = addTag(tags, "banana"); + expect(result).not.toBe(tags); + }); + }); +}); diff --git a/__tests__/lib/utils/theme.test.ts b/__tests__/lib/utils/theme.test.ts new file mode 100644 index 0000000..a7f9bc6 --- /dev/null +++ b/__tests__/lib/utils/theme.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "bun:test"; +import { getIconClassAndAction } from '../../../lib/utils/theme'; + +describe("Theme Utilities", () => { + describe("getIconClassAndAction", () => { + test('returns "darkIcon" for dark theme', () => { + const result = getIconClassAndAction(true); + expect(result).toBe("darkIcon"); + }); + + test('returns "lightIcon" for light theme', () => { + const result = getIconClassAndAction(false); + expect(result).toBe("lightIcon"); + }); + }); +}); diff --git a/app/[lang]/example-projects/proxy-state/computed-text.tsx b/app/[lang]/example-projects/proxy-state/computed-text.tsx index 3010e28..7f40211 100644 --- a/app/[lang]/example-projects/proxy-state/computed-text.tsx +++ b/app/[lang]/example-projects/proxy-state/computed-text.tsx @@ -1,8 +1,8 @@ "use client"; import { - StateProvider, - useProxyState, + StateProvider, + useProxyState, } from "app/[lang]/example-projects/proxy-state/context"; import { useTranslation } from "translations/hooks"; import styles from "./styles.module.css"; @@ -10,14 +10,14 @@ import styles from "./styles.module.css"; function Input({ name, defaultValue, -}: { - name: string; - defaultValue: number | string +}: { + name: string; + defaultValue: number | string }) { - const value = useProxyState(name, defaultValue, { + const value = useProxyState(name, defaultValue, { testId: `input-${name}` }); - + return ( ("name", translations?.placeholders.name || '', { - testId: 'name-input' + const { translations } = useTranslation("proxy-state"); + const name = useProxyState("name", translations?.placeholders.name || '', { + testId: 'name-input' }); - + if (!translations) return null; + return (
-

{translations?.title}

-
{translations.title} +
- {translations?.explanation} + {translations.explanation}
- - {translations.placeholders.name} +
- +
- +
- +
@@ -106,9 +107,9 @@ function ComputedStateExample() { } export function ComputedProxyStateExample() { - return ( - - - - ); + return ( + + + + ); } diff --git a/app/[lang]/page.tsx b/app/[lang]/page.tsx index 2da0c7b..026b368 100644 --- a/app/[lang]/page.tsx +++ b/app/[lang]/page.tsx @@ -1,7 +1,9 @@ -import HomeBackground from "layouts/home-background"; +import { HomeBackgroundAurora } from "layouts/home-background"; import HomePage from "layouts/home-page"; import { getFromKV } from "lib/get-data-methods"; -export const metadata = { +import type { Metadata } from "next"; + +export const metadata: Metadata = { title: { default: "Itamar Sharify", template: "%s - Itamar Sharify", @@ -26,7 +28,7 @@ export default async function Home() { return ( <> - + ); diff --git a/bun.lock b/bun.lock index 9a37f4a..02f0b8b 100644 --- a/bun.lock +++ b/bun.lock @@ -4,14 +4,14 @@ "": { "name": "itamar-tech", "dependencies": { - "@google/genai": "^1.3.0", + "@google/genai": "^1.10.0", "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", "@next/mdx": "15.4.1", "@vercel/analytics": "^1.5.0", "@vercel/kv": "^3.0.0", "bright": "^1.0.0", - "chart.js": "^4.4.9", + "chart.js": "^4.5.0", "eslint-config-next": "15.4.1", "groq-sdk": "^0.23.0", "lodash": "^4.17.21", @@ -31,11 +31,9 @@ "@testing-library/react-hooks": "^8.0.1", "@types/bun": "latest", "@types/jest": "^30.0.0", - "@types/node": "^24.0.13", + "@types/node": "^24.0.14", "@types/react": "19.1.8", - "@vitest/ui": "^3.2.4", "jsdom": "^26.1.0", - "playwright": "^1.53.2", "typescript": "^5.8.3", "vitest": "^3.2.4", }, @@ -164,7 +162,7 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.3", "", { "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" } }, "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag=="], - "@google/genai": ["@google/genai@1.9.0", "", { "dependencies": { "google-auth-library": "^9.14.2", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.11.0" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-w9P93OXKPMs9H1mfAx9+p3zJqQGrWBGdvK/SVc7cLZEXNHr/3+vW2eif7ZShA6wU24rNLn9z9MK2vQFUvNRI2Q=="], + "@google/genai": ["@google/genai@1.21.0", "", { "dependencies": { "google-auth-library": "^9.14.2", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.11.4" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-k47DECR8BF9z7IJxQd3reKuH2eUnOH5NlJWSe+CKM6nbXx+wH3hmtWQxUQR9M8gzWW1EvFuRVgjQssEIreNZsw=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -234,11 +232,11 @@ "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], - "@mdx-js/loader": ["@mdx-js/loader@3.1.0", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "source-map": "^0.7.0" }, "peerDependencies": { "webpack": ">=5" }, "optionalPeers": ["webpack"] }, "sha512-xU/lwKdOyfXtQGqn3VnJjlDrmKXEvMi1mgYxVmukEUtVycIz1nh7oQ40bKTd4cA7rLStqu0740pnhGYxGoqsCg=="], + "@mdx-js/loader": ["@mdx-js/loader@3.1.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "source-map": "^0.7.0" }, "peerDependencies": { "webpack": ">=5" }, "optionalPeers": ["webpack"] }, "sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ=="], "@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="], - "@mdx-js/react": ["@mdx-js/react@3.1.0", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ=="], + "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], @@ -272,49 +270,47 @@ "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], - "@playwright/test": ["@playwright/test@1.54.1", "", { "dependencies": { "playwright": "1.54.1" }, "bin": { "playwright": "cli.js" } }, "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw=="], + "@playwright/test": ["@playwright/test@1.55.1", "", { "dependencies": { "playwright": "1.55.1" }, "bin": { "playwright": "cli.js" } }, "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig=="], - "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.45.1", "", { "os": "android", "cpu": "arm" }, "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.45.0", "", { "os": "android", "cpu": "arm" }, "sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.45.1", "", { "os": "android", "cpu": "arm64" }, "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.45.0", "", { "os": "android", "cpu": "arm64" }, "sha512-PSZ0SvMOjEAxwZeTx32eI/j5xSYtDCRxGu5k9zvzoY77xUNssZM+WV6HYBLROpY5CkXsbQjvz40fBb7WPwDqtQ=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.45.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.45.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BA4yPIPssPB2aRAWzmqzQ3y2/KotkLyZukVB7j3psK/U3nVJdceo6qr9pLM2xN6iRP/wKfxEbOb1yrlZH6sYZg=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.45.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.45.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Pr2o0lvTwsiG4HCr43Zy9xXrHspyMvsvEw4FwKYqhli4FuLE5FjcZzuQ4cfPe0iUFCvSQG6lACI0xj74FDZKRA=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.45.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.45.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lYE8LkE5h4a/+6VnnLiL14zWMPnx6wNbDG23GcYFpRW1V9hYWHAw9lBZ6ZUIrOaoK7NliF1sdwYGiVmziUF4vA=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.45.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.45.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PVQWZK9sbzpvqC9Q0GlehNNSVHR+4m7+wET+7FgSnKG3ci5nAMgGmr9mGBXzAuE5SvguCKJ6mHL6vq1JaJ/gvw=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.45.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.45.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hLrmRl53prCcD+YXTfNvXd776HTxNh8wPAMllusQ+amcQmtgo3V5i/nkhPN6FakW+QVLoUUr2AsbtIRPFU3xIA=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.45.1", "", { "os": "linux", "cpu": "arm" }, "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.45.0", "", { "os": "linux", "cpu": "arm" }, "sha512-XBKGSYcrkdiRRjl+8XvrUR3AosXU0NvF7VuqMsm7s5nRy+nt58ZMB19Jdp1RdqewLcaYnpk8zeVs/4MlLZEJxw=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.45.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.45.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-fRvZZPUiBz7NztBE/2QnCS5AtqLVhXmUOPj9IHlfGEXkapgImf4W9+FSkL8cWqoAjozyUzqFmSc4zh2ooaeF6g=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.45.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.45.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Btv2WRZOcUGi8XU80XwIvzTg4U6+l6D0V6sZTrZx214nrwxw5nAi8hysaXj/mctyClWgesyuxbeLylCBNauimg=="], + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg=="], - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.45.0", "", { "os": "linux", "cpu": "none" }, "sha512-Li0emNnwtUZdLwHjQPBxn4VWztcrw/h7mgLyHiEI5Z0MhpeFGlzaiBHpSNVOMB/xucjXTTcO+dhv469Djr16KA=="], + "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.45.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg=="], - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.45.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sB8+pfkYx2kvpDCfd63d5ScYT0Fz1LO6jIb2zLZvmK9ob2D8DeVqrmBDE0iDK8KlBVmsTNzrjr3G1xV4eUZhSw=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.45.0", "", { "os": "linux", "cpu": "none" }, "sha512-5GQ6PFhh7E6jQm70p1aW05G2cap5zMOvO0se5JMecHeAdj5ZhWEHbJ4hiKpfi1nnnEdTauDXxPgXae/mqjow9w=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.45.0", "", { "os": "linux", "cpu": "none" }, "sha512-N/euLsBd1rekWcuduakTo/dJw6U6sBP3eUq+RXM9RNfPuWTvG2w/WObDkIvJ2KChy6oxZmOSC08Ak2OJA0UiAA=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.45.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.45.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-2l9sA7d7QdikL0xQwNMO3xURBUNEWyHVHfAsHsUdq+E/pgLTUcCE+gih5PCdmyHmfTDeXUWVhqL0WZzg0nua3g=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.45.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.45.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XZdD3fEEQcwG2KrJDdEQu7NrHonPxxaV0/w2HpvINBdcqebz1aL+0vM2WFJq4DeiAVT6F5SUQas65HY5JDqoPw=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.45.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.45.0", "", { "os": "linux", "cpu": "x64" }, "sha512-7ayfgvtmmWgKWBkCGg5+xTQ0r5V1owVm67zTrsEY1008L5ro7mCyGYORomARt/OquB9KY7LpxVBZes+oSniAAQ=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.45.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.45.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-B+IJgcBnE2bm93jEW5kHisqvPITs4ddLOROAcOc/diBgrEiQJJ6Qcjby75rFSmH5eMGrqJryUgJDhrfj942apQ=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.45.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.45.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+CXwwG66g0/FpWOnP/v1HnrGVSOygK/osUbu3wPRy8ECXjoYKjRAyfxYpDQOfghC5qPJYLPH0oN4MCOjwgdMug=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.45.0", "", { "os": "win32", "cpu": "x64" }, "sha512-SRf1cytG7wqcHVLrBc9VtPK4pU5wxiB/lNIkNmW2ApKXIg+RpqwHfsaEK+e7eH4A1BpI6BX/aBWXxZCIrJg3uA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.45.1", "", { "os": "win32", "cpu": "x64" }, "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], @@ -326,7 +322,7 @@ "@testing-library/dom": ["@testing-library/dom@10.4.0", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" } }, "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ=="], - "@testing-library/jest-dom": ["@testing-library/jest-dom@6.6.3", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "lodash": "^4.17.21", "redent": "^3.0.0" } }, "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA=="], + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.8.0", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ=="], "@testing-library/react": ["@testing-library/react@16.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw=="], @@ -336,7 +332,7 @@ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], - "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + "@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="], "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], @@ -368,7 +364,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.0.13", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ=="], + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], "@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="], @@ -460,8 +456,6 @@ "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - "@vitest/ui": ["@vitest/ui@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "fflate": "^0.8.2", "flatted": "^3.3.3", "pathe": "^2.0.3", "sirv": "^3.0.1", "tinyglobby": "^0.2.14", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "vitest": "3.2.4" } }, "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA=="], - "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], @@ -480,7 +474,7 @@ "ansi-sequence-parser": ["ansi-sequence-parser@1.1.1", "", {}, "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -534,7 +528,7 @@ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + "bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], @@ -552,7 +546,7 @@ "chai": ["chai@5.2.1", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A=="], - "chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], @@ -730,8 +724,6 @@ "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], - "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -1060,8 +1052,6 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -1126,9 +1116,9 @@ "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - "playwright": ["playwright@1.54.1", "", { "dependencies": { "playwright-core": "1.54.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g=="], + "playwright": ["playwright@1.55.1", "", { "dependencies": { "playwright-core": "1.55.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A=="], - "playwright-core": ["playwright-core@1.54.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA=="], + "playwright-core": ["playwright-core@1.55.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], @@ -1188,7 +1178,7 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "rollup": ["rollup@4.45.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.45.0", "@rollup/rollup-android-arm64": "4.45.0", "@rollup/rollup-darwin-arm64": "4.45.0", "@rollup/rollup-darwin-x64": "4.45.0", "@rollup/rollup-freebsd-arm64": "4.45.0", "@rollup/rollup-freebsd-x64": "4.45.0", "@rollup/rollup-linux-arm-gnueabihf": "4.45.0", "@rollup/rollup-linux-arm-musleabihf": "4.45.0", "@rollup/rollup-linux-arm64-gnu": "4.45.0", "@rollup/rollup-linux-arm64-musl": "4.45.0", "@rollup/rollup-linux-loongarch64-gnu": "4.45.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.45.0", "@rollup/rollup-linux-riscv64-gnu": "4.45.0", "@rollup/rollup-linux-riscv64-musl": "4.45.0", "@rollup/rollup-linux-s390x-gnu": "4.45.0", "@rollup/rollup-linux-x64-gnu": "4.45.0", "@rollup/rollup-linux-x64-musl": "4.45.0", "@rollup/rollup-win32-arm64-msvc": "4.45.0", "@rollup/rollup-win32-ia32-msvc": "4.45.0", "@rollup/rollup-win32-x64-msvc": "4.45.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A=="], + "rollup": ["rollup@4.45.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.45.1", "@rollup/rollup-android-arm64": "4.45.1", "@rollup/rollup-darwin-arm64": "4.45.1", "@rollup/rollup-darwin-x64": "4.45.1", "@rollup/rollup-freebsd-arm64": "4.45.1", "@rollup/rollup-freebsd-x64": "4.45.1", "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", "@rollup/rollup-linux-arm-musleabihf": "4.45.1", "@rollup/rollup-linux-arm64-gnu": "4.45.1", "@rollup/rollup-linux-arm64-musl": "4.45.1", "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", "@rollup/rollup-linux-riscv64-gnu": "4.45.1", "@rollup/rollup-linux-riscv64-musl": "4.45.1", "@rollup/rollup-linux-s390x-gnu": "4.45.1", "@rollup/rollup-linux-x64-gnu": "4.45.1", "@rollup/rollup-linux-x64-musl": "4.45.1", "@rollup/rollup-win32-arm64-msvc": "4.45.1", "@rollup/rollup-win32-ia32-msvc": "4.45.1", "@rollup/rollup-win32-x64-msvc": "4.45.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw=="], "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], @@ -1236,8 +1226,6 @@ "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], - "sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="], - "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], @@ -1310,8 +1298,6 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], - "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], @@ -1336,13 +1322,13 @@ "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], - "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], @@ -1372,7 +1358,7 @@ "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], - "vite": ["vite@7.0.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA=="], + "vite": ["vite@7.0.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw=="], "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], @@ -1420,11 +1406,11 @@ "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], - "@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "@jest/pattern/@types/node": ["@types/node@24.0.13", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ=="], - "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + "@jest/types/@types/node": ["@types/node@24.0.13", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ=="], - "@testing-library/dom/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], @@ -1434,6 +1420,8 @@ "@types/mdast/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/node-fetch/@types/node": ["@types/node@24.0.13", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -1442,7 +1430,7 @@ "@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -1462,13 +1450,9 @@ "is-bun-module/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - "jest-diff/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "jest-mock/@types/node": ["@types/node@24.0.13", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ=="], - "jest-matcher-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "jest-message-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "jest-util/@types/node": ["@types/node@24.0.13", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ=="], "mdast-util-from-markdown/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -1482,9 +1466,9 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "next-mdx-remote/@mdx-js/react": ["@mdx-js/react@3.1.0", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ=="], - "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], @@ -1518,16 +1502,24 @@ "vite/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "@testing-library/dom/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "@jest/pattern/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "@jest/types/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], "@testing-library/dom/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + "@types/node-fetch/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + "@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "groq-sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-mock/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "jest-util/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + "mdast-util-phrasing/unist-util-is/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], diff --git a/bunfig.toml b/bunfig.toml index de54e3c..15d75b8 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1,5 +1,9 @@ [test] +src="__tests__" coverage = true coverageDir = "coverage" coverageReporters = ["json", "lcov", "text"] + preload = "./test/setup.ts" +config = "./tsconfig.test.json" + diff --git a/components/match-finder/PropertiesSelect.module.css b/components/match-finder/PropertiesSelect.module.css index 37bdcad..32d3da1 100644 --- a/components/match-finder/PropertiesSelect.module.css +++ b/components/match-finder/PropertiesSelect.module.css @@ -43,16 +43,17 @@ padding: 0 0.8rem; border-radius: 0.5rem; background-color: var(--colors-decorations); - color: white; + color: var(--colors-main); white-space: nowrap; text-overflow: ellipsis; animation: fadeIn 0.2s, zoomIn 0.1s; } .removeTag { + all: unset; cursor: pointer; - margin: 0.5rem; - font-size: 1.1rem; + padding-inline-start: 0.5rem; + font-size: 1.2rem; } .resultsContainer { diff --git a/components/match-finder/fireworks.tsx b/components/match-finder/fireworks.tsx index 3b34e8f..df7160b 100644 --- a/components/match-finder/fireworks.tsx +++ b/components/match-finder/fireworks.tsx @@ -1,3 +1,5 @@ +"use client"; + import styles from "./Fireworks.module.css"; function Fireworks(props) { diff --git a/components/match-finder/properties-select.tsx b/components/match-finder/properties-select.tsx index 20f86f6..88f02a3 100644 --- a/components/match-finder/properties-select.tsx +++ b/components/match-finder/properties-select.tsx @@ -86,7 +86,7 @@ export default function PropertiesSelect({
@@ -99,12 +99,16 @@ export default function PropertiesSelect({ {tags.map((tag) => (
{tag} - removeTag(tag)} + onClick={(e) => { + e.stopPropagation(); + removeTag(tag); + }} > - x - + ✖ +
))}
diff --git a/components/theme-list.tsx b/components/theme-list.tsx index 591f03c..2aac98a 100644 --- a/components/theme-list.tsx +++ b/components/theme-list.tsx @@ -2,7 +2,6 @@ import { ThemeItem } from "components/theme-switch-client"; import { THEMES, useThemeContext } from "providers/theme"; import styles from "./ThemedIcon.module.css"; -import { getIconClassAndAction } from "@/lib/utils/theme"; export function ThemeList() { const { theme: selectedTheme, selectTheme } = useThemeContext(); @@ -14,7 +13,7 @@ export function ThemeList() { key={theme} currentTheme={theme} isSelected={selectedTheme === theme} - setTheme={selectTheme} + setThemeAction={selectTheme} /> ))} diff --git a/components/theme-switch-client.tsx b/components/theme-switch-client.tsx index a2cb871..3ab1362 100644 --- a/components/theme-switch-client.tsx +++ b/components/theme-switch-client.tsx @@ -4,22 +4,23 @@ import styles from "./ThemedIcon.module.css"; export function ThemeItem({ currentTheme, isSelected, - setTheme, + setThemeAction, }: { currentTheme: string; isSelected: boolean; - setTheme: (theme: string) => void; + setThemeAction: (theme: string) => void; }) { + const label = currentTheme === "gpt5" ? "GPT 5" : currentTheme; return (
  • ); diff --git a/e2e/smoke-test.spec.ts b/e2e/smoke-test.e2e.ts similarity index 86% rename from e2e/smoke-test.spec.ts rename to e2e/smoke-test.e2e.ts index e8f67be..28c6589 100644 --- a/e2e/smoke-test.spec.ts +++ b/e2e/smoke-test.e2e.ts @@ -52,18 +52,6 @@ test.describe("Full Site Smoke Test", () => { }); } - test("should have proper meta tags and SEO", async ({ page }) => { - await page.goto("/en"); - - // Should have proper meta tags - await expect(page.locator('meta[name="description"]')).toBeVisible(); - await expect(page.locator('meta[property="og:title"]')).toBeVisible(); - await expect(page.locator('meta[property="og:description"]')).toBeVisible(); - - // Should have proper title - await expect(page).toHaveTitle(/Itamar Sharify/); - }); - test("should be mobile responsive", async ({ page }) => { // Test common mobile viewport sizes const viewports = [ diff --git a/globals.css b/globals.css index bdf6652..8a89c06 100644 --- a/globals.css +++ b/globals.css @@ -1,4 +1,23 @@ /* Font variables are now loaded via next/font in lib/fonts.ts */ + +a, +button, +[role="button"] { + transition: transform 0.2s ease-in-out, background-color 0.2s ease-in-out; +} + +a:hover, +button:hover, +[role="button"]:hover { + transform: scale(1.05); +} + +a:active, +button:active, +[role="button"]:active { + transform: scale(0.98); +} + /* Fallback font definitions for when Google Fonts are blocked */ :root { --font-playfair: "Playfair Display", Georgia, serif; @@ -161,6 +180,32 @@ body[data-theme="cobalt2"] { --sh-comment: #a19595; } +body[data-theme="gpt5"] { + --color-lang: white; + --colors-bg: #0d1117; + --colors-modalBg: #0f141b; + --colors-main: #7c3aed; + --colors-text: #e6edf3; + --colors-subText: rgba(230, 237, 243, 0.7); + --colors-headerText: #58a6ff; + --colors-header: #7c3aed; + --colors-paragraph: #c9d1d9; + --colors-decorations: #58a6ff; + --colors-hoverDecorations: rgba(88, 166, 255, 0.12); + --colors-inputs: rgba(124, 58, 237, 0.25); + --colors-link: #58a6ff; + --colors-charts: #7c3aed; + --sh-class: var(--colors-header); + --sh-identifier: var(--colors-subText); + --sh-sign: #8996a3; + --sh-property: var(--colors-paragraph); + --sh-entity: #2dd4bf; + --sh-jsxliterals: #a78bfa; + --sh-string: #10b981; + --sh-keyword: #f47067; + --sh-comment: #8b949e; +} + body[data-theme="light"], body { --colors-bg: hsla(0, 0%, 96%, 0.8); diff --git a/layouts/HomeBackground.module.css b/layouts/HomeBackground.module.css index 0da63ef..ae02a5e 100644 --- a/layouts/HomeBackground.module.css +++ b/layouts/HomeBackground.module.css @@ -1,27 +1,96 @@ -.bg { - animation: slide 20s ease-in-out infinite alternate; - bottom: 0; - left: -50%; - opacity: 0.5; +.auroraContainer { position: fixed; - right: -50%; - top: 0; + inset: 0; + pointer-events: none; + overflow: hidden; z-index: -1; + background: radial-gradient(150% 150% at 50% 50%, color-mix(in srgb, var(--colors-hoverDecorations) 20%, transparent) 0%, transparent 70%); } -@media (prefers-reduced-motion) { - .bg { - animation: none; +.auroraLayer { + position: absolute; + width: 55vw; + height: 55vw; + border-radius: 45%; + filter: blur(140px); + opacity: 0.55; + mix-blend-mode: screen; + transform-origin: center; + animation: auroraDrift 28s linear infinite, auroraPulse 12s ease-in-out infinite alternate; +} + +.auroraLayer1 { + background: radial-gradient(circle at 20% 20%, var(--colors-main) 0%, transparent 65%); +} + +.auroraLayer2 { + background: radial-gradient(circle at 70% 40%, color-mix(in srgb, var(--colors-decorations) 70%, var(--colors-main) 30%) 0%, transparent 70%); + animation-duration: 32s, 14s; +} + +.auroraLayer3 { + background: radial-gradient(circle at 35% 80%, color-mix(in srgb, var(--colors-hoverDecorations) 65%, var(--colors-main) 35%) 0%, transparent 75%); + animation-duration: 36s, 16s; +} + +.auroraLayer4 { + background: radial-gradient(circle at 80% 75%, color-mix(in srgb, var(--colors-header) 60%, transparent) 0%, transparent 70%); + animation-duration: 40s, 18s; +} + +.auroraNoise { + position: absolute; + inset: -30%; + background-image: + repeating-linear-gradient(125deg, rgba(255, 255, 255, 0.05) 0 2px, transparent 2px 6px), + repeating-linear-gradient(305deg, rgba(0, 0, 0, 0.05) 0 1px, transparent 1px 4px); + opacity: 0.12; + mix-blend-mode: soft-light; + animation: grainShift 7s steps(6) infinite; +} + +@keyframes auroraDrift { + 0% { + transform: translate3d(-25%, -10%, 0) rotate(0deg); + } + + 50% { + transform: translate3d(20%, 15%, 0) rotate(120deg); + } + + 100% { + transform: translate3d(-20%, 5%, 0) rotate(360deg); } } -.bg2 { - animation-direction: alternate-reverse; - animation-duration: 15s; +@keyframes auroraPulse { + 0% { + opacity: 0.35; + filter: blur(120px); + } + + 100% { + opacity: 0.7; + filter: blur(160px); + } } -.bg3 { - animation-duration: 12s; +@keyframes grainShift { + 0% { + transform: translate3d(0, 0, 0); + } + + 100% { + transform: translate3d(10%, -10%, 0); + } +} + +@media (prefers-reduced-motion) { + + .auroraLayer, + .auroraNoise { + animation: none; + } } @keyframes slide { @@ -32,4 +101,4 @@ to { transform: translateX(25%); } -} +} \ No newline at end of file diff --git a/layouts/home-background.tsx b/layouts/home-background.tsx index 76ff1e3..7b0b411 100644 --- a/layouts/home-background.tsx +++ b/layouts/home-background.tsx @@ -1,37 +1,85 @@ import styles from "./HomeBackground.module.css"; -function getNarrowAngle(baseDegree: number) { - const randomDegree = Math.random() * 360; - let degree = baseDegree + randomDegree; - if (Math.random() > 0.8) degree = baseDegree - randomDegree; - if (degree % 60 > 30) return degree + 30; - return degree; +type AuroraConfig = { + className: string; + translateX: [number, number]; + translateY: [number, number]; + scale: [number, number]; + rotate: [number, number]; + delay: [number, number]; + secondaryDelay?: [number, number]; +}; + +const AURORA_CONFIGS: AuroraConfig[] = [ + { + className: styles.auroraLayer1, + translateX: [-20, 20], + translateY: [-15, 10], + scale: [0.8, 1.15], + rotate: [-90, 90], + delay: [-6, 4], + secondaryDelay: [-2, 2], + }, + { + className: styles.auroraLayer2, + translateX: [-35, 25], + translateY: [-20, 20], + scale: [0.7, 1.1], + rotate: [-120, 120], + delay: [-10, 6], + secondaryDelay: [-3, 3], + }, + { + className: styles.auroraLayer3, + translateX: [-15, 30], + translateY: [-10, 25], + scale: [0.85, 1.25], + rotate: [-60, 60], + delay: [-8, 5], + secondaryDelay: [-5, 4], + }, + { + className: styles.auroraLayer4, + translateX: [-25, 35], + translateY: [-25, 25], + scale: [0.9, 1.35], + rotate: [-180, 180], + delay: [-12, 8], + secondaryDelay: [-6, 6], + }, +]; + +function randomBetween([min, max]: [number, number]) { + return min + Math.random() * (max - min); } -function HomeBackground() { - const baseDegree = Math.random() * 360; + +export function HomeBackgroundAurora() { + const baseAngle = Math.random() * 360; return ( - <> -
    -
    -
    - +
    + {AURORA_CONFIGS.map((config, index) => { + const translateX = randomBetween(config.translateX); + const translateY = randomBetween(config.translateY); + const scale = randomBetween(config.scale); + const rotate = baseAngle + randomBetween(config.rotate); + const primaryDelay = randomBetween(config.delay); + const secondaryDelay = config.secondaryDelay + ? randomBetween(config.secondaryDelay) + : randomBetween(config.delay); + const animationDelay = `${primaryDelay.toFixed(2)}s, ${secondaryDelay.toFixed(2)}s`; + return ( +
    + ); + })} +
    +
    ); -} - -export default HomeBackground; +} \ No newline at end of file diff --git a/lib/headers.ts b/lib/headers.ts index d6fb415..9d79e4f 100644 --- a/lib/headers.ts +++ b/lib/headers.ts @@ -2,6 +2,7 @@ import { revalidatePath } from "next/cache"; import { cookies, headers } from "next/headers"; import type { Theme } from "providers/theme"; + export async function getCurrentLang() { const headersList = await headers(); const pathname = headersList.get("x-current-path") || ""; @@ -21,7 +22,7 @@ export async function setCurrentTheme(theme: string) { const cookieCache = await cookies(); cookieCache.set("current-theme", theme); revalidatePath("/", "layout"); - return theme; + return { success: true, message: theme }; } export async function toggleDarkTheme() { diff --git a/lib/hooks/useRandomEmoji.test.ts b/lib/hooks/useRandomEmoji.test.ts deleted file mode 100644 index 0806e09..0000000 --- a/lib/hooks/useRandomEmoji.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { renderHook, cleanup, act } from '@testing-library/react'; -import { describe, expect, test, afterEach, beforeEach, mock, jest } from 'bun:test'; -import { useRandomEmoji, DEFAULT_EMOJIS, DEFAULT_INTERVAL } from './useRandomEmoji'; - -// Mock timers -let intervalCallback: () => void; -const mockSetInterval = mock((callback: () => void) => { - intervalCallback = callback; - return {} as NodeJS.Timeout; -}); -const mockClearInterval = mock(() => {}); - -global.setInterval = mockSetInterval as any; -global.clearInterval = mockClearInterval as any; - -describe('useRandomEmoji', () => { - // Reset mocks before each test - beforeEach(() => { - mockSetInterval.mockClear(); - mockClearInterval.mockClear(); - }); - - // Clean up after each test - afterEach(() => { - cleanup(); - }); - - test('returns the first emoji by default', () => { - const emojis = ['😀', '😎', '🤓']; - const { result } = renderHook(() => useRandomEmoji({ emojis })); - expect(result.current).toBe('😀'); - }); - - test('uses default emojis when none provided', () => { - const { result } = renderHook(() => useRandomEmoji()); - expect(DEFAULT_EMOJIS).toContain(result.current); - }); - - test('returns empty string when emojis array is empty', () => { - const { result } = renderHook(() => useRandomEmoji({ emojis: [] })); - expect(result.current).toBe(''); - }); - - test('returns a string from the provided emojis array', () => { - const emojis = ['😀', '😎', '🤓']; - const { result } = renderHook(() => useRandomEmoji({ emojis })); - expect(emojis).toContain(result.current); - }); - - test('sets up interval with correct timing', () => { - const emojis = ['😀', '😎']; - const interval = 1000; - - renderHook(() => useRandomEmoji({ emojis, interval })); - - // Should call setInterval with the correct interval - expect(mockSetInterval).toHaveBeenCalledTimes(1); - expect(mockSetInterval.mock.calls[0][1]).toBe(interval); - }); - - test('cleans up interval on unmount', () => { - const emojis = ['😀', '😎']; - const { unmount } = renderHook(() => useRandomEmoji({ emojis })); - - // Get the interval ID that was returned by setInterval - const intervalId = mockSetInterval.mock.results[0].value; - - // Unmount the component - unmount(); - - // Should have called clearInterval with the correct ID - expect(mockClearInterval).toHaveBeenCalledTimes(1); - expect(mockClearInterval).toHaveBeenCalledWith(intervalId); - }); - - test('cycles through emojis correctly', () => { - const emojis = ['😀', '😎', '🤓']; - - // Render the hook and get the initial state - const { result } = renderHook(() => useRandomEmoji({ emojis })); - - // Initial emoji should be the first one - expect(result.current).toBe(emojis[0]); - - // Call the interval callback to simulate time passing - act(() => { - intervalCallback(); - }); - - // Should now be the second emoji - expect(emojis).toContain(result.current); - - // Call it again - act(() => { - intervalCallback(); - }); - - // Should now be the third emoji - expect(emojis).toContain(result.current); - - // Call it one more time to test wrap-around - act(() => { - intervalCallback(); - }); - - // Should be one of the emojis (can't guarantee order due to random selection) - expect(emojis).toContain(result.current); - }); - - test('handles emojis change', () => { - const initialEmojis = ['😀', '😎']; - const newEmojis = ['🚀', '🌟', '✨']; - - const { result, rerender } = renderHook( - ({ emojis }) => useRandomEmoji({ emojis }), - { initialProps: { emojis: initialEmojis } } - ); - - // Initial emoji from first array - expect(initialEmojis).toContain(result.current); - - // Change the emojis - rerender({ emojis: newEmojis }); - - // Call interval to force a re-render with new emojis - act(() => { - intervalCallback(); - }); - - // Should now be using the new emojis - expect(newEmojis).toContain(result.current); - }); -}); diff --git a/lib/hooks/useUrlHash.test.ts b/lib/hooks/useUrlHash.test.ts deleted file mode 100644 index ea48275..0000000 --- a/lib/hooks/useUrlHash.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { renderHook, act } from '@testing-library/react'; -import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'; -import useUrlHash from './useUrlHash'; - -// Mock window and location objects -interface MockLocation extends Location { - hash: string; - href: string; - assign: ReturnType; - onhashchange?: (event: HashChangeEvent) => void; -} - -const mockLocation: MockLocation = { - hash: '', - href: 'http://test.com', - assign: mock(() => {}), - onhashchange: undefined, - // Add required Location properties with dummy values - ancestorOrigins: [] as unknown as DOMStringList, - host: 'test.com', - hostname: 'test.com', - origin: 'http://test.com', - pathname: '/', - port: '', - protocol: 'http:', - search: '', - replace: mock(() => {}), - reload: mock(() => {}), - toString: () => 'http://test.com', -}; - -// Track event listeners -const eventListeners: Record = {}; - -const mockAddEventListener = mock((event: string, callback: EventListener) => { - eventListeners[event] = callback; -}); - -const mockRemoveEventListener = mock((event: string) => { - delete eventListeners[event]; -}); - -// Store original globals -const originalWindow = global.window; -const originalLocation = global.location; - -// Helper to trigger hash change -function triggerHashChange(newHash: string, oldHash: string = '') { - const oldURL = `http://test.com${oldHash ? `#${oldHash}` : ''}`; - const newURL = `http://test.com${newHash ? `#${newHash}` : ''}`; - - mockLocation.hash = newHash ? `#${newHash}` : ''; - - const hashChangeCallback = eventListeners['hashchange']; - if (hashChangeCallback) { - const event = new Event('hashchange') as HashChangeEvent; - Object.defineProperty(event, 'oldURL', { value: oldURL }); - Object.defineProperty(event, 'newURL', { value: newURL }); - - hashChangeCallback(event); - } -} - -describe('useUrlHash', () => { - beforeEach(() => { - // Reset mocks and state - mockLocation.hash = ''; - mockLocation.href = 'http://test.com'; - mockLocation.assign.mockClear(); - mockAddEventListener.mockClear(); - mockRemoveEventListener.mockClear(); - - // Apply mocks - global.window = { - ...global.window, - addEventListener: mockAddEventListener, - removeEventListener: mockRemoveEventListener, - location: mockLocation, - } as any; - - global.location = mockLocation as any; - }); - - afterEach(() => { - // Restore original globals - global.window = originalWindow; - global.location = originalLocation; - }); - - test('should return false when hash does not match', () => { - mockLocation.hash = '#other-hash'; - const { result } = renderHook(() => useUrlHash('test-hash')); - expect(result.current).toBe(false); - }); - - test('should return true when hash matches', () => { - mockLocation.hash = '#test-hash'; - const { result } = renderHook(() => useUrlHash('test-hash')); - expect(result.current).toBe(true); - }); - - test('should work with or without leading # in the hash', () => { - mockLocation.hash = '#test-hash'; - - // Test with hash that includes # - const { result: result1 } = renderHook(() => useUrlHash('#test-hash')); - expect(result1.current).toBe(true); - - // Test with hash that doesn't include # - const { result: result2 } = renderHook(() => useUrlHash('test-hash')); - expect(result2.current).toBe(true); - }); - - test('should update when hash changes', () => { - // Initial render with no hash - const { result } = renderHook(() => useUrlHash('test-hash')); - expect(result.current).toBe(false); - - // Simulate hash change - act(() => { - triggerHashChange('test-hash'); - }); - - expect(result.current).toBe(true); - - // Simulate hash change to something else - act(() => { - triggerHashChange('other-hash', 'test-hash'); - }); - - expect(result.current).toBe(false); - }); - - test('should add hashchange event listener on mount', () => { - renderHook(() => useUrlHash('test-hash')); - expect(mockAddEventListener).toHaveBeenCalledWith('hashchange', expect.any(Function)); - }); - - test('should remove hashchange event listener on unmount', () => { - const { unmount } = renderHook(() => useUrlHash('test-hash')); - - // Get the event handler that was added - const [eventName, eventHandler] = mockAddEventListener.mock.calls[0]; - - unmount(); - - expect(mockRemoveEventListener).toHaveBeenCalledWith(eventName, eventHandler); - }); -}); diff --git a/lib/utils/array.test.ts b/lib/utils/array.test.ts deleted file mode 100644 index 33ed3ab..0000000 --- a/lib/utils/array.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, test } from 'bun:test'; -import { filterItems } from './array'; - -describe('Array Utilities', () => { - describe('filterItems', () => { - const testItems = ['React', 'TypeScript', 'JavaScript', 'HTML', 'CSS']; - const excludedItems = ['css']; // Using lowercase to test case insensitivity - - test('returns all items when search text is empty', () => { - const result = filterItems(testItems, [], ''); - expect(result).toEqual(testItems); - }); - - test('filters items by search text', () => { - const result = filterItems(testItems, [], 'script'); - expect(result).toEqual(['TypeScript', 'JavaScript']); - }); - - test('excludes items in the excluded list (case insensitive)', () => { - // Test with exact case match - const result1 = filterItems(testItems, ['CSS'], ''); - expect(result1).toEqual(expect.not.arrayContaining(['CSS'])); - - // Test with different case - const result2 = filterItems(testItems, ['css'], ''); - expect(result2).toEqual(expect.not.arrayContaining(['CSS'])); - - // Test with multiple exclusions - const result3 = filterItems(testItems, ['css', 'react'], ''); - expect(result3).toEqual(expect.not.arrayContaining(['CSS', 'React'])); - expect(result3).toHaveLength(3); - }); - - test('is case insensitive', () => { - const result = filterItems(testItems, [], 'typescript'); - expect(result).toEqual(['TypeScript']); - }); - - test('returns empty array when no matches found', () => { - const result = filterItems(testItems, [], 'nonexistent'); - expect(result).toEqual([]); - }); - }); -}); diff --git a/lib/utils/chart-settings.test.ts b/lib/utils/chart-settings.test.ts deleted file mode 100644 index c647948..0000000 --- a/lib/utils/chart-settings.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { describe, expect, test } from 'bun:test'; -import { - themedRadarSettings, - themedLineSettings, - type ChartTheme -} from './chart'; - -describe('Chart Settings Utilities', () => { - const mockTheme: ChartTheme = { - charts: '#123456', - text: '#ffffff', - headerText: '#cccccc', - }; - - describe('themedRadarSettings', () => { - test('returns correct radar chart settings with theme applied', () => { - const title = 'Test Radar Chart'; - const settings = themedRadarSettings(title, mockTheme); - - expect(settings).toMatchObject({ - maintainAspectRatio: false, - aspectRatio: 1, - tooltips: { enabled: false }, - plugins: { - title: { - display: true, - text: title, - color: mockTheme.text, - }, - legend: { display: false }, - }, - }); - - // Check scales configuration - expect(settings.scales?.r).toBeDefined(); - expect(settings.scales?.r.ticks).toEqual({ - display: false, - maxTicksLimit: 1, - }); - expect(settings.scales?.r.pointLabels).toEqual({ - color: mockTheme.text, - }); - }); - }); - - describe('themedLineSettings', () => { - test('returns correct line chart settings with theme applied', () => { - const title = 'Test Line Chart'; - const settings = themedLineSettings(title, mockTheme); - - expect(settings).toMatchObject({ - maintainAspectRatio: false, - aspectRatio: 1, - spanGaps: false, - plugins: { - title: { - display: true, - text: title, - color: mockTheme.text, - }, - legend: { - position: 'top', - fullWidth: true, - labels: { - boxWidth: 5, - color: mockTheme.text, - }, - }, - }, - tooltips: { enabled: false }, - }); - - // Check scales configuration - expect(settings.scales?.xAxis).toBeDefined(); - expect(settings.scales?.xAxis.ticks).toEqual({ - color: mockTheme.text, - }); - expect(settings.scales?.xAxis.gridLines).toEqual({ - drawTicks: false, - drawOnChartArea: false, - color: mockTheme.headerText, - }); - - expect(settings.scales?.yAxis).toBeDefined(); - expect(settings.scales?.yAxis).toMatchObject({ - display: true, - ticks: { display: false }, - gridLines: { - drawTicks: false, - drawOnChartArea: false, - color: mockTheme.headerText, - }, - }); - }); - }); -}); diff --git a/lib/utils/chart.test.ts b/lib/utils/chart.test.ts deleted file mode 100644 index 2f9af65..0000000 --- a/lib/utils/chart.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, expect, test } from 'bun:test'; -import { randomChartData, themedDatasets } from './chart'; -import type { ChartTheme } from './chart'; - -describe('Chart Utilities', () => { - describe('randomChartData', () => { - test('returns an object with the correct structure', () => { - const result = randomChartData(1); - - expect(result).toHaveProperty('title'); - expect(result).toHaveProperty('labels'); - expect(result).toHaveProperty('values'); - - expect(Array.isArray(result.labels)).toBe(true); - expect(Array.isArray(result.values)).toBe(true); - }); - - test('includes the provided id in the title', () => { - const id = 42; - const result = randomChartData(id); - - expect(result.title).toBe(`Chart #${id}`); - }); - - test('returns the expected number of data points', () => { - const result = randomChartData(1); - - // Should have 5 time periods - expect(result.labels).toHaveLength(5); - - // Should have 4 datasets (Learning, Mingeling, Involvement, Contribution) - expect(result.values).toHaveLength(4); - - // Each dataset should have 5 data points - result.values.forEach(dataset => { - expect(dataset.data).toHaveLength(5); - }); - }); - - test('generates random data between 0 and 100', () => { - const result = randomChartData(1); - - result.values.forEach(dataset => { - dataset.data.forEach(value => { - expect(value).toBeGreaterThanOrEqual(0); - expect(value).toBeLessThanOrEqual(100); - }); - }); - }); - }); -}); diff --git a/lib/utils/color.test.ts b/lib/utils/color.test.ts deleted file mode 100644 index 992dc16..0000000 --- a/lib/utils/color.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, expect, test } from 'bun:test'; -import { hexToRgb, hexToHSL } from './color'; - -describe('Color Utilities', () => { - describe('hexToRgb', () => { - test('converts 3-digit hex to RGB', () => { - expect(hexToRgb('#F00')).toEqual([255, 0, 0]); - expect(hexToRgb('#0F0')).toEqual([0, 255, 0]); - expect(hexToRgb('#00F')).toEqual([0, 0, 255]); - expect(hexToRgb('#FFF')).toEqual([255, 255, 255]); - expect(hexToRgb('#000')).toEqual([0, 0, 0]); - }); - - test('converts 6-digit hex to RGB', () => { - expect(hexToRgb('#FF0000')).toEqual([255, 0, 0]); - expect(hexToRgb('#00FF00')).toEqual([0, 255, 0]); - expect(hexToRgb('#0000FF')).toEqual([0, 0, 255]); - expect(hexToRgb('#FFFFFF')).toEqual([255, 255, 255]); - expect(hexToRgb('#000000')).toEqual([0, 0, 0]); - }); - - test('throws error for invalid hex format', () => { - expect(() => hexToRgb('red')).toThrow('Invalid hex color format'); - expect(() => hexToRgb('#FF')).toThrow('Invalid hex color format'); - expect(() => hexToRgb('#FFFFF')).toThrow('Invalid hex color format'); - expect(() => hexToRgb('#GGGGGG')).toThrow('Invalid hex color format'); - }); - }); - - describe('hexToHSL', () => { - test('converts hex to HSL', () => { - // Red - expect(hexToHSL('#F00')).toEqual([0, 100, 50]); - // Green - expect(hexToHSL('#0F0')).toEqual([120, 100, 50]); - // Blue - expect(hexToHSL('#00F')).toEqual([240, 100, 50]); - // White - expect(hexToHSL('#FFF')).toEqual([0, 0, 100]); - // Black - expect(hexToHSL('#000')).toEqual([0, 0, 0]); - // Random color - const [h, s, l] = hexToHSL('#1E88E5'); - expect(h).toBeGreaterThanOrEqual(0); - expect(h).toBeLessThanOrEqual(360); - expect(s).toBeGreaterThanOrEqual(0); - expect(s).toBeLessThanOrEqual(100); - expect(l).toBeGreaterThanOrEqual(0); - expect(l).toBeLessThanOrEqual(100); - }); - - test('throws error for invalid hex format', () => { - expect(() => hexToHSL('red')).toThrow('Invalid hex color format'); - expect(() => hexToHSL('#FF')).toThrow('Invalid hex color format'); - expect(() => hexToHSL('#FFFFF')).toThrow('Invalid hex color format'); - expect(() => hexToHSL('#GGGGGG')).toThrow('Invalid hex color format'); - }); - }); -}); diff --git a/lib/utils/job.test.ts b/lib/utils/job.test.ts deleted file mode 100644 index 77d98d4..0000000 --- a/lib/utils/job.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { describe, expect, test } from 'bun:test'; -import { filterJobsByText, sortJobsByDate } from './job'; -import type { Job } from 'lib/types/jobs'; - -const mockJobs: Job[] = [ - { - id: '1', - title: 'Senior Frontend Developer', - description: 'Building modern web applications with React and TypeScript', - duration: { - from: '2020-01-01', - to: '2022-12-31', - }, - company: { - name: 'Tech Corp', - website: 'https://techcorp.com', - }, - tags: ['React', 'TypeScript', 'Frontend'], - }, - { - id: '2', - title: 'Backend Engineer', - description: 'Building scalable APIs with Node.js', - duration: { - from: '2023-01-01', - to: '2023-12-31', - }, - company: { - name: 'API Masters', - website: 'https://apimasters.com', - }, - tags: ['Node.js', 'API', 'Backend'], - }, - { - id: '3', - title: 'Full Stack Developer', - description: 'Working on both frontend and backend with React and Node.js', - duration: { - from: '2021-06-01', - to: '2023-06-30', - }, - company: { - name: 'FullStack Inc', - website: 'https://fullstack.io', - }, - tags: ['React', 'Node.js', 'Fullstack'], - }, -]; - -describe('Job Utilities', () => { - describe('filterJobsByText', () => { - test('returns all jobs when search text is empty', () => { - const result = filterJobsByText(mockJobs, ''); - expect(result).toHaveLength(mockJobs.length); - expect(result[0].id).toBe('3'); // Should be reversed - }); - - test('filters jobs by title', () => { - const result = filterJobsByText(mockJobs, 'Backend'); - // Should match both 'Backend Engineer' and 'Full Stack Developer' (which has 'backend' in description) - expect(result).toHaveLength(2); - const titles = result.map(job => job.title); - expect(titles).toContain('Backend Engineer'); - expect(titles).toContain('Full Stack Developer'); - }); - - test('filters jobs by description', () => { - const result = filterJobsByText(mockJobs, 'modern web'); - expect(result).toHaveLength(1); - expect(result[0].title).toBe('Senior Frontend Developer'); - }); - - test('filters jobs by tags', () => { - const result = filterJobsByText(mockJobs, 'React'); - expect(result).toHaveLength(2); - expect(result[0].title).toBe('Full Stack Developer'); - expect(result[1].title).toBe('Senior Frontend Developer'); - }); - - test('handles multiple search terms', () => { - const result = filterJobsByText(mockJobs, 'React, Node.js'); - expect(result).toHaveLength(3); // All jobs match at least one term - }); - - test('is case insensitive', () => { - const result = filterJobsByText(mockJobs, 'react'); - expect(result).toHaveLength(2); - expect(result[0].title).toBe('Full Stack Developer'); - }); - }); - - describe('sortJobsByDate', () => { - test('sorts jobs by end date in descending order', () => { - const result = sortJobsByDate(mockJobs); - expect(result[0].id).toBe('2'); // 2023-12-31 - expect(result[1].id).toBe('3'); // 2023-06-30 - expect(result[2].id).toBe('1'); // 2022-12-31 - }); - }); -}); diff --git a/lib/utils/match.test.ts b/lib/utils/match.test.ts deleted file mode 100644 index 12cb0d1..0000000 --- a/lib/utils/match.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, expect, test } from 'bun:test'; -import { calculateMatch, getQualificationText } from './match'; - -describe('Match Utilities', () => { - const testRankings = { - 'react': 10, - 'typescript': 8, - 'javascript': 7, - 'nodejs': 6, - 'python': 5 - }; - - describe('calculateMatch', () => { - test('returns 0 for empty attributes array', () => { - const result = calculateMatch([], testRankings); - expect(result).toBe(0); - }); - - test('sums up scores for matched attributes', () => { - const result = calculateMatch(['react', 'typescript'], testRankings); - expect(result).toBe(18); // 10 (react) + 8 (typescript) - }); - - test('ignores attributes not found in rankings', () => { - const result = calculateMatch(['react', 'unknown'], testRankings); - expect(result).toBe(10); // Only react (10) is counted - }); - - test('works with empty rankings object', () => { - const result = calculateMatch(['react', 'typescript'], {}); - expect(result).toBe(0); - }); - }); - - describe('getQualificationText', () => { - test('returns "Perfect Match!" for scores >= 90', () => { - expect(getQualificationText(90)).toBe('Perfect Match!'); - expect(getQualificationText(95)).toBe('Perfect Match!'); - expect(getQualificationText(100)).toBe('Perfect Match!'); - }); - - test('returns "Great Match!" for scores between 70-89', () => { - expect(getQualificationText(70)).toBe('Great Match!'); - expect(getQualificationText(80)).toBe('Great Match!'); - expect(getQualificationText(89)).toBe('Great Match!'); - }); - - test('returns "Good Match" for scores between 50-69', () => { - expect(getQualificationText(50)).toBe('Good Match'); - expect(getQualificationText(60)).toBe('Good Match'); - expect(getQualificationText(69)).toBe('Good Match'); - }); - - test('returns "Fair Match" for scores between 30-49', () => { - expect(getQualificationText(30)).toBe('Fair Match'); - expect(getQualificationText(40)).toBe('Fair Match'); - expect(getQualificationText(49)).toBe('Fair Match'); - }); - - test('returns "Needs Improvement" for scores below 30', () => { - expect(getQualificationText(0)).toBe('Needs Improvement'); - expect(getQualificationText(15)).toBe('Needs Improvement'); - expect(getQualificationText(29)).toBe('Needs Improvement'); - }); - }); -}); diff --git a/lib/utils/match.ts b/lib/utils/match.ts index f0a8843..6734290 100644 --- a/lib/utils/match.ts +++ b/lib/utils/match.ts @@ -1,4 +1,3 @@ - /** * Calculates a match score based on the provided attributes and their rankings * @param attributes - Array of technology/skill names to calculate score for @@ -6,13 +5,13 @@ * @returns The total match score */ export function calculateMatch( - attributes: string[], - rankings: Record = {} + attributes: string[], + rankings: Record = {}, ): number { - return attributes.reduce((total, item) => { - const itemRank = rankings[item]; - return itemRank !== undefined ? total + itemRank : total; - }, 0); + return attributes.reduce((total, item) => { + const itemRank = rankings[item]; + return itemRank !== undefined ? total + itemRank : total; + }, 0); } /** @@ -21,16 +20,16 @@ export function calculateMatch( * @returns A string representing the qualification level */ export function getQualificationText(matchPercentage: number): string { - switch (true) { - case matchPercentage >= 90: - return "Perfect Match!"; - case matchPercentage >= 70: - return "Great Match!"; - case matchPercentage >= 50: - return "Good Match"; - case matchPercentage >= 30: - return "Fair Match"; - default: - return "Needs Improvement"; - } + switch (true) { + case matchPercentage >= 90: + return "Perfect Match!"; + case matchPercentage >= 70: + return "Great Match!"; + case matchPercentage >= 50: + return "Good Match"; + case matchPercentage >= 30: + return "Fair Match"; + default: + return ""; + } } diff --git a/lib/utils/quote.test.ts b/lib/utils/quote.test.ts deleted file mode 100644 index 6bc0d5a..0000000 --- a/lib/utils/quote.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, expect, test } from 'bun:test'; -import { extractQuotesByPerson, type QuotesCollection } from './quote'; - -describe('Quote Utilities', () => { - describe('extractQuotesByPerson', () => { - test('extracts and flattens quotes from a nested collection', () => { - const mockQuotes: QuotesCollection = { - 'Person 1': { - role: 'Role 1', - profile: 'Profile 1', - quotes: ['Quote 1', 'Quote 2'] - }, - 'Person 2': { - role: 'Role 2', - profile: 'Profile 2', - quotes: ['Quote 3'] - } - }; - - const result = extractQuotesByPerson(mockQuotes); - - expect(result).toHaveLength(3); - expect(result).toContainEqual({ - role: 'Role 1', - profile: 'Profile 1', - quote: 'Quote 1' - }); - expect(result).toContainEqual({ - role: 'Role 1', - profile: 'Profile 1', - quote: 'Quote 2' - }); - expect(result).toContainEqual({ - role: 'Role 2', - profile: 'Profile 2', - quote: 'Quote 3' - }); - }); - - test('returns an empty array for an empty collection', () => { - const result = extractQuotesByPerson({}); - expect(result).toEqual([]); - }); - - test('handles a collection with empty quotes array', () => { - const mockQuotes: QuotesCollection = { - 'Person 1': { - role: 'Role 1', - profile: 'Profile 1', - quotes: [] - } - }; - - const result = extractQuotesByPerson(mockQuotes); - expect(result).toEqual([]); - }); - }); -}); diff --git a/lib/utils/routing.test.ts b/lib/utils/routing.test.ts deleted file mode 100644 index e2e5184..0000000 --- a/lib/utils/routing.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { describe, expect, test } from 'bun:test'; -import { getRoute } from './routing'; - -describe('Routing Utilities', () => { - describe('getRoute', () => { - test('returns root path with default locale when pathname is empty', () => { - const result = getRoute(''); - expect(result).toBe('/en'); - }); - - test('switches between he and en locales', () => { - // Test switching from en to he - const enPath = '/en/about'; - const hePath = getRoute(enPath, 'he'); - expect(hePath).toBe('/he/about'); - - // Test switching from he to en - const backToEnPath = getRoute(hePath, 'en'); - expect(backToEnPath).toBe('/en/about'); - }); - - test('toggles locale when no target locale is provided', () => { - // From en to he - const enPath = '/en/about'; - const hePath = getRoute(enPath); - expect(hePath).toBe('/he/about'); - - // From he to en - const backToEnPath = getRoute(hePath); - expect(backToEnPath).toBe('/en/about'); - }); - - test('handles root path correctly', () => { - const rootPath = '/'; - const result = getRoute(rootPath, 'he'); - expect(result).toBe('/he'); - }); - - test('handles paths with multiple segments', () => { - const path = '/en/blog/post-1'; - const result = getRoute(path, 'he'); - expect(result).toBe('/he/blog/post-1'); - }); - }); -}); diff --git a/lib/utils/tags.test.ts b/lib/utils/tags.test.ts deleted file mode 100644 index 8619d7b..0000000 --- a/lib/utils/tags.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { describe, expect, test, vi, afterEach } from 'bun:test'; -import { - handleTagInputKeyDown, - removeTag, - removeLastTag, - addTag -} from './tags'; - -describe('Tag Utilities', () => { - describe('handleTagInputKeyDown', () => { - const mockEvent = { - preventDefault: vi.fn(), - key: '', - target: { value: 'test' } - } as unknown as React.KeyboardEvent; - - const mockFilteredItems = ['apple', 'banana', 'cherry']; - const mockOnEnter = vi.fn(); - const mockOnEscape = vi.fn(); - const mockOnBackspace = vi.fn(); - - afterEach(() => { - vi.clearAllMocks(); - }); - - test('handles ArrowDown key', () => { - const newIndex = handleTagInputKeyDown( - { ...mockEvent, key: 'ArrowDown' }, - 0, - mockFilteredItems, - mockOnEnter, - mockOnEscape, - mockOnBackspace - ); - expect(newIndex).toBe(1); - expect(mockOnEnter).not.toHaveBeenCalled(); - expect(mockOnEscape).not.toHaveBeenCalled(); - expect(mockOnBackspace).not.toHaveBeenCalled(); - }); - - test('handles ArrowUp key', () => { - const newIndex = handleTagInputKeyDown( - { ...mockEvent, key: 'ArrowUp' }, - 1, - mockFilteredItems, - mockOnEnter, - mockOnEscape, - mockOnBackspace - ); - expect(newIndex).toBe(0); - }); - - test('handles Enter key', () => { - const newIndex = handleTagInputKeyDown( - { ...mockEvent, key: 'Enter' }, - 1, - mockFilteredItems, - mockOnEnter, - mockOnEscape, - mockOnBackspace - ); - expect(newIndex).toBe(1); - expect(mockOnEnter).toHaveBeenCalledWith('banana'); - }); - - test('handles Escape key', () => { - const newIndex = handleTagInputKeyDown( - { ...mockEvent, key: 'Escape' }, - 1, - mockFilteredItems, - mockOnEnter, - mockOnEscape, - mockOnBackspace - ); - expect(newIndex).toBe(1); - expect(mockOnEscape).toHaveBeenCalled(); - }); - - test('handles Backspace key with empty input', () => { - const newIndex = handleTagInputKeyDown( - { ...mockEvent, key: 'Backspace', target: { value: '' } } as typeof mockEvent, - 1, - mockFilteredItems, - mockOnEnter, - mockOnEscape, - mockOnBackspace - ); - expect(newIndex).toBe(1); - expect(mockOnBackspace).toHaveBeenCalled(); - }); - }); - - describe('removeTag', () => { - test('removes the specified tag', () => { - const tags = ['apple', 'banana', 'cherry']; - const result = removeTag(tags, 'banana'); - expect(result).toEqual(['apple', 'cherry']); - }); - - test('returns a new array', () => { - const tags = ['apple', 'banana']; - const result = removeTag(tags, 'banana'); - expect(result).not.toBe(tags); - }); - }); - - describe('removeLastTag', () => { - test('removes the last tag', () => { - const tags = ['apple', 'banana', 'cherry']; - const result = removeLastTag(tags); - expect(result).toEqual(['apple', 'banana']); - }); - - test('returns empty array for single tag', () => { - const tags = ['apple']; - const result = removeLastTag(tags); - expect(result).toEqual([]); - }); - - test('returns empty array for empty input', () => { - const result = removeLastTag([]); - expect(result).toEqual([]); - }); - }); - - describe('addTag', () => { - test('adds a new tag', () => { - const tags = ['apple', 'banana']; - const result = addTag(tags, 'cherry'); - expect(result).toEqual(['apple', 'banana', 'cherry']); - }); - - test('does not add duplicate tags (case insensitive)', () => { - const tags = ['apple', 'banana']; - const result = addTag(tags, 'BANANA'); - expect(result).toEqual(['apple', 'banana']); - }); - - test('returns a new array', () => { - const tags = ['apple']; - const result = addTag(tags, 'banana'); - expect(result).not.toBe(tags); - }); - }); -}); diff --git a/lib/utils/theme.test.ts b/lib/utils/theme.test.ts deleted file mode 100644 index a2fd3b4..0000000 --- a/lib/utils/theme.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { describe, expect, test } from 'bun:test'; -import { getIconClassAndAction } from './theme'; - -describe('Theme Utilities', () => { - describe('getIconClassAndAction', () => { - test('returns "darkIcon" for dark theme', () => { - const result = getIconClassAndAction(true); - expect(result).toBe('darkIcon'); - }); - - test('returns "lightIcon" for light theme', () => { - const result = getIconClassAndAction(false); - expect(result).toBe('lightIcon'); - }); - }); -}); diff --git a/package.json b/package.json index d2ebc2c..047df73 100644 --- a/package.json +++ b/package.json @@ -1,60 +1,54 @@ { - "name": "itamar-tech", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --turbopack", - "build": "next build", - "start": "next start", - "lint": "next lint", - "format": "biome format", - "ci": "biome ci", - "fix": "biome check --write . --diagnostic-level=error", - "test": "vitest", - "test:ui": "vitest --ui", - "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --ui" - }, - "dependencies": { - "@google/genai": "^1.3.0", - "@mdx-js/loader": "^3.1.0", - "@mdx-js/react": "^3.1.0", - "@next/mdx": "15.4.1", - "@vercel/analytics": "^1.5.0", - "@vercel/kv": "^3.0.0", - "bright": "^1.0.0", - "chart.js": "^4.4.9", - "eslint-config-next": "15.4.1", - "groq-sdk": "^0.23.0", - "lodash": "^4.17.21", - "next": "15.4.1", - "next-mdx-remote": "^5.0.0", - "nodemailer": "^6.10.1", - "react": "19.1.0", - "react-chartjs-2": "^5.3.0", - "react-dom": "19.1.0", - "react-markdown": "^10.1.0" - }, - "devDependencies": { - "@biomejs/biome": "^1.9.4", - "@playwright/test": "^1.54.1", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.3.0", - "@testing-library/react-hooks": "^8.0.1", - "@types/bun": "latest", - "@types/jest": "^30.0.0", - "@types/node": "^24.0.13", - "@types/react": "19.1.8", - "@vitest/ui": "^3.2.4", - "jsdom": "^26.1.0", - "playwright": "^1.53.2", - "typescript": "^5.8.3", - "vitest": "^3.2.4" - }, - "overrides": { - "@types/react": "19.1.8" - }, - "trustedDependencies": [ - "@biomejs/biome" - ] + "name": "itamar-tech", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint", + "format": "biome format", + "ci": "biome ci", + "fix": "biome check --write . --diagnostic-level=error", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui" + }, + "dependencies": { + "@google/genai": "^1.21.0", + "@mdx-js/loader": "^3.1.1", + "@mdx-js/react": "^3.1.1", + "@next/mdx": "15.4.1", + "@vercel/analytics": "^1.5.0", + "@vercel/kv": "^3.0.0", + "bright": "^1.0.0", + "chart.js": "^4.5.0", + "eslint-config-next": "15.4.1", + "groq-sdk": "^0.23.0", + "lodash": "^4.17.21", + "next": "15.4.1", + "next-mdx-remote": "^5.0.0", + "nodemailer": "^6.10.1", + "react": "19.1.0", + "react-chartjs-2": "^5.3.0", + "react-dom": "19.1.0", + "react-markdown": "^10.1.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@playwright/test": "^1.55.1", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@testing-library/react-hooks": "^8.0.1", + "@types/bun": "latest", + "@types/jest": "^30.0.0", + "@types/node": "^24.5.2", + "@types/react": "19.1.8", + "jsdom": "^26.1.0", + "typescript": "^5.9.2", + "vitest": "^3.2.4" + }, + "overrides": { + "@types/react": "19.1.8" + }, + "trustedDependencies": ["@biomejs/biome"] } diff --git a/playwright.config.ts b/playwright.config.ts index 7b18149..c1bbd91 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,78 +1,59 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; /** * @see https://playwright.dev/docs/test-configuration */ export default defineConfig({ - testDir: './e2e', - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:3000', - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, + testMatch: ["**/*.e2e.ts"], + testDir: "./e2e", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:3000", + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + ], - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, - ], - - /* Run your local dev server before starting the tests */ - webServer: { - command: 'bun run dev', - url: 'http://localhost:3000', - reuseExistingServer: !process.env.CI, - timeout: 120 * 1000, // Increase timeout to 2 minutes - stdout: 'pipe', - stderr: 'pipe', - env: { - NODE_ENV: 'test', - PORT: '3000', - // Add any other environment variables needed for testing - }, - }, -}); \ No newline at end of file + /* Run your local dev server before starting the tests */ + webServer: { + command: "bun run dev", + url: "http://localhost:3000", + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, // Increase timeout to 2 minutes + stdout: "pipe", + stderr: "pipe", + env: { + NODE_ENV: "test", + PORT: "3000", + // Add any other environment variables needed for testing + }, + }, +}); diff --git a/providers/theme.tsx b/providers/theme.tsx index 4f07cf5..854ff73 100644 --- a/providers/theme.tsx +++ b/providers/theme.tsx @@ -3,7 +3,8 @@ import { setCurrentTheme, toggleDarkTheme } from "lib/headers"; import { useRouter } from "next/navigation"; import { createContext, useContext, useState } from "react"; -export const THEMES = ["light", "dark", "monokai", "cobalt2"] as const; + +export const THEMES = ["light", "dark", "monokai", "cobalt2", "gpt5"] as const; export type Theme = (typeof THEMES)[number]; type ThemeContextType = { @@ -14,8 +15,8 @@ type ThemeContextType = { const ThemeContext = createContext({ theme: "dark", - selectTheme: () => {}, - toggleMode: () => {}, + selectTheme: () => { }, + toggleMode: () => { }, }); export function useThemeContext() { @@ -28,9 +29,9 @@ function useTheme(currentThemeName: Theme) { const selectTheme = async (theme: Theme) => { document.body.setAttribute("data-theme", theme); + setTheme(theme); await setCurrentTheme(theme); router.refresh(); - setTheme(theme); }; const toggleMode = async () => { diff --git a/test/infrastructure.test.ts b/test/infrastructure.test.ts deleted file mode 100644 index 93992db..0000000 --- a/test/infrastructure.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, it, expect } from 'vitest' - -describe('Test Infrastructure', () => { - it('should have vitest working correctly', () => { - expect(1 + 1).toBe(2) - }) - - it('should validate environment setup', () => { - expect(process.env.NODE_ENV).toBeDefined() - // In jsdom environment, window is available - expect(typeof window).toBe('object') - }) - - it('should test string operations', () => { - const testString = 'Itamar Sharify' - expect(testString).toContain('Itamar') - expect(testString.length).toBeGreaterThan(0) - }) - - it('should test array operations', () => { - const pages = ['home', 'blog', 'resume', 'example-projects'] - expect(pages).toHaveLength(4) - expect(pages).toContain('blog') - expect(pages).not.toContain('nonexistent') - }) -}) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 447150a..3624c90 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,7 +23,7 @@ ], "strictNullChecks": true }, - "exclude": ["node_modules", "**/node_modules/*"], + "exclude": ["node_modules", "**/node_modules/*", "e2e"], "include": [ "next-env.d.ts", "**/*.ts", diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..fafb891 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx" + }, + "exclude": [ + "node_modules", + "e2e" + ] +} \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index e30cb8d..0000000 --- a/vitest.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { defineConfig } from 'vitest/config' -import { resolve } from 'path' - -export default defineConfig({ - test: { - globals: true, - environment: 'jsdom', - setupFiles: ['./test/setup.ts'], - exclude: ['**/node_modules/**', '**/e2e/**', '**/dist/**'], - }, - resolve: { - alias: { - '@': resolve(__dirname, './'), - 'components': resolve(__dirname, './components'), - 'lib': resolve(__dirname, './lib'), - 'layouts': resolve(__dirname, './layouts'), - 'app': resolve(__dirname, './app'), - }, - }, -}) \ No newline at end of file