Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
662 changes: 619 additions & 43 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 10 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
"name": "veritix-web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint",
"test": "vitest run --coverage",
"test:watch": "vitest"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint",
"test": "vitest run --coverage",
"test:watch": "vitest"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-slot": "^1.2.4",
Expand All @@ -31,13 +31,15 @@
"devDependencies": {
"@playwright/test": "^1.60.0",
"@tailwindcss/postcss": "^4.1.18",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^4.5.2",
"@vitest/coverage-v8": "^3.2.4",
"autoprefixer": "^10.4.23",
"eslint": "^9",
"eslint-config-next": "16.1.4",
Expand Down
11 changes: 6 additions & 5 deletions src/__tests__/auth-forms.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for the login and sign-up flows (issues #105 / FE-014).
*/
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

Check warning on line 6 in src/__tests__/auth-forms.test.tsx

View workflow job for this annotation

GitHub Actions / build-and-check (18.x)

'userEvent' is defined but never used
import { describe, it, expect, vi, beforeEach } from "vitest";

// ── mocks ──────────────────────────────────────────────────────────────────
Expand All @@ -21,11 +21,12 @@
vi.mock("react-toastify", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));

vi.mock("framer-motion", () => {
const React = require("react");
const motion: Record<string, React.FC<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>> = {};
["div", "form", "h2", "p"].forEach((tag) => {
motion[tag] = ({ children, ...rest }) => React.createElement(tag, rest, children);
});
// eslint-disable-next-line @typescript-eslint/no-require-imports
const React = require("react") as typeof import("react");
const tags = ["div", "form", "h2", "p"] as const;
const motion = Object.fromEntries(
tags.map((tag) => [tag, ({ children, ...rest }: React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }) => React.createElement(tag, rest, children)])
);
return { motion, AnimatePresence: ({ children }: { children: React.ReactNode }) => children };
});

Expand Down
65 changes: 65 additions & 0 deletions src/__tests__/dashboard-revenue-trend.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, it, expect } from 'vitest'

/**
* Unit tests for the week-over-week revenue trend computation used in
* dashboard/page.tsx (issue #364).
*
* The logic is extracted here for isolation:
* - Split data.revenue into current-week (last 7) and prior-week (prev 7)
* - trend = ((currentWeekTotal - lastWeekTotal) / lastWeekTotal) * 100
* - Returns null when fewer than 14 data points or lastWeek total is 0
*/
function computeRevenueTrend(
revenue: { day: string; revenue: number }[]
): number | null {
if (revenue.length < 14) return null
const currentWeek = revenue.slice(-7).reduce((sum, d) => sum + d.revenue, 0)
const lastWeek = revenue.slice(-14, -7).reduce((sum, d) => sum + d.revenue, 0)
if (lastWeek === 0) return null
return ((currentWeek - lastWeek) / lastWeek) * 100
}

function makeRevenue(values: number[]): { day: string; revenue: number }[] {
return values.map((revenue, i) => ({ day: `day-${i}`, revenue }))
}

describe('computeRevenueTrend', () => {
it('returns null when revenue array has fewer than 14 entries', () => {
expect(computeRevenueTrend(makeRevenue([100, 200]))).toBeNull()
expect(computeRevenueTrend(makeRevenue(Array(13).fill(100)))).toBeNull()
})

it('returns null when last-week total is 0 (avoid division by zero)', () => {
const revenue = makeRevenue([...Array(7).fill(0), ...Array(7).fill(200)])
expect(computeRevenueTrend(revenue)).toBeNull()
})

it('returns positive trend when current week outperforms last week', () => {
// last week: 7 × 100 = 700, current week: 7 × 200 = 1400 → +100%
const revenue = makeRevenue([...Array(7).fill(100), ...Array(7).fill(200)])
const trend = computeRevenueTrend(revenue)
expect(trend).toBeCloseTo(100)
})

it('returns negative trend when current week underperforms last week', () => {
// last week: 7 × 200 = 1400, current week: 7 × 100 = 700 → -50%
const revenue = makeRevenue([...Array(7).fill(200), ...Array(7).fill(100)])
const trend = computeRevenueTrend(revenue)
expect(trend).toBeCloseTo(-50)
})

it('returns 0 when both weeks are identical', () => {
const revenue = makeRevenue(Array(14).fill(500))
expect(computeRevenueTrend(revenue)).toBeCloseTo(0)
})

it('uses only the last 14 entries when the array is longer', () => {
// Prepend noise; only the last 14 matter
const noise = Array(20).fill(9999)
const signal = [...Array(7).fill(100), ...Array(7).fill(150)]
const revenue = makeRevenue([...noise, ...signal])
const trend = computeRevenueTrend(revenue)
// 150/100 - 1 = +50%
expect(trend).toBeCloseTo(50)
})
})
3 changes: 2 additions & 1 deletion src/__tests__/event-creation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ vi.mock("next/link", () => ({
<a href={href}>{children}</a>,
}));
vi.mock("framer-motion", () => {
const React = require("react");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const React = require("react") as typeof import("react");
const proxy = new Proxy({}, {
get: (_t, tag: string) =>
({ children, ...rest }: React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }) =>
Expand Down
3 changes: 2 additions & 1 deletion src/__tests__/event-discovery.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

Check warning on line 3 in src/__tests__/event-discovery.test.tsx

View workflow job for this annotation

GitHub Actions / build-and-check (18.x)

'userEvent' is defined but never used
import EventsPage from "@/app/(public)/events/page";
import { mockEvents } from "@/mocks/events";

Expand All @@ -10,14 +10,15 @@
<a href={href} {...rest}>{children}</a>,
}));
vi.mock("next/image", () => ({
default: ({ src, alt }: { src: string; alt: string }) => <img src={src} alt={alt} />,

Check warning on line 13 in src/__tests__/event-discovery.test.tsx

View workflow job for this annotation

GitHub Actions / build-and-check (18.x)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
}));
vi.mock("@/lib/eventsApi", () => ({
fetchEvents: () => Promise.resolve(mockEvents),
fetchEventById: (id: string) => Promise.resolve(mockEvents.find((e) => e.id === id) ?? null),
}));
vi.mock("framer-motion", () => {
const React = require("react");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const React = require("react") as typeof import("react");
const proxy = new Proxy({}, {
get: (_t, tag: string) =>
({ children, ...rest }: React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }) =>
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/profile-edit.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ vi.mock("react-toastify", () => ({
}));

vi.mock("framer-motion", () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const React = require("react");
const motion: Record<string, React.FC<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>> = {};
["div", "form", "h2", "p"].forEach((tag) => {
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/ticket-verification.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import VerifyPage from "@/app/(protected)/verify/page";

vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));
vi.mock("framer-motion", () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const React = require("react");
const proxy = new Proxy({}, {
get: (_t, tag: string) =>
Expand Down
21 changes: 19 additions & 2 deletions src/app/(protected)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ export default function DashboardPage() {
const payoutsQueued = data?.payoutsQueued ?? 0
const nextSettlementDays = data?.nextSettlementDays ?? 0

// Compute week-over-week revenue trend from data.revenue
const revenueTrend = (() => {
const rev = data?.revenue ?? []
if (rev.length < 14) return null
const currentWeek = rev.slice(-7).reduce((sum, d) => sum + d.revenue, 0)
const lastWeek = rev.slice(-14, -7).reduce((sum, d) => sum + d.revenue, 0)
if (lastWeek === 0) return null
return ((currentWeek - lastWeek) / lastWeek) * 100
})()

const eventImages = data?.events?.slice(0, 4).map((e) => ({
const eventImgs = data?.events?.slice(0, 4).map((e) => ({
src: e.coverImage ?? null,
alt: e.name,
Expand Down Expand Up @@ -122,7 +133,13 @@ export default function DashboardPage() {
<div className="h-48 w-full min-h-[192px]">
<RevenueChart data={revenueData} />
</div>
<p className="mt-4 text-xs text-[#21D4FF]">Trending by 18.6% in the past week ↗️</p>
{revenueTrend === null ? (
<p className="mt-4 text-xs text-gray-500">Insufficient data for trend</p>
) : (
<p className={`mt-4 text-xs ${revenueTrend >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
Trending by {Math.abs(revenueTrend).toFixed(1)}% {revenueTrend >= 0 ? '↗️' : '↘️'} this week
</p>
)}
</Card>

<Card>
Expand Down Expand Up @@ -154,7 +171,7 @@ export default function DashboardPage() {
<p className="text-xs text-[#21D4FF]">1.5k from last week</p>
</div>
<div className="grid grid-cols-2 gap-3">
{eventImgs.map((image, index) => (
{eventImages.map((image, index) => (
<EventImage key={index} src={image.src} alt={image.alt} />
))}
</div>
Expand Down
Loading
Loading