diff --git a/.codecov.yml b/.codecov.yml index f1d277d..2fb679e 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -4,7 +4,7 @@ codecov: coverage: precision: 2 round: down - range: "70...100" + range: '70...100' status: project: @@ -17,6 +17,6 @@ coverage: threshold: 1% comment: - layout: "reach, diff, flags, files" + layout: 'reach, diff, flags, files' behavior: default require_changes: no diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 74dbc06..e189799 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,26 +1,31 @@ --- name: Bug report about: Something is broken -title: "[BUG] " +title: '[BUG] ' labels: bug -assignees: "" +assignees: '' --- ## Describe the bug + ## Steps to reproduce + 1. Go to ... 2. Click on ... 3. See error ## Expected behavior + ## Screenshots + ## Environment + - OS: - Browser: - Node version: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 937df31..010eb3d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,22 +1,27 @@ --- name: Feature request about: Suggest a new feature or improvement -title: "[FEAT] " +title: '[FEAT] ' labels: enhancement -assignees: "" +assignees: '' --- ## Problem this solves + ## Proposed solution + ## Alternatives considered + ## Additional context + ## Would you like to work on this? + - [ ] Yes, I would like to implement this diff --git a/.github/ISSUE_TEMPLATE/good_first_issue.md b/.github/ISSUE_TEMPLATE/good_first_issue.md index c939123..632837a 100644 --- a/.github/ISSUE_TEMPLATE/good_first_issue.md +++ b/.github/ISSUE_TEMPLATE/good_first_issue.md @@ -1,24 +1,30 @@ --- name: Good First Issue about: A well-scoped task suitable for new contributors -title: "[GOOD FIRST ISSUE] " +title: '[GOOD FIRST ISSUE] ' labels: good first issue, help wanted -assignees: "" +assignees: '' --- ## What needs to be done + ## Files to look at + ## Acceptance criteria + + - [ ] - [ ] ## Difficulty + ## Notes for new contributors + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fb1d0b4..069483d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,10 +1,13 @@ ## Description + ## Related Issue + Closes # ## Type of Change + - [ ] Bug fix - [ ] New feature - [ ] Documentation update @@ -12,9 +15,11 @@ Closes # - [ ] Tests ## Screenshots (for UI changes) + ## Checklist + - [ ] I have read CONTRIBUTING.md - [ ] My code follows the existing style - [ ] I have run `npm test` and all tests pass diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..5af45a2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +node_modules +.next +coverage +package-lock.json diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index db5800f..f8ad9c8 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -3,6 +3,7 @@ EcoSphere is a Next.js based application utilizing a modern full-stack web architecture. ## Tech Stack + - **Framework:** Next.js (App Router) - **Database ORM:** Prisma - **Styling:** Tailwind CSS @@ -10,6 +11,7 @@ EcoSphere is a Next.js based application utilizing a modern full-stack web archi - **Linting & Formatting:** ESLint, Prettier ## Directory Structure + - `/app`: Next.js App Router pages and layouts. - `/components`: Reusable React components. - `/lib`: Utility functions and library wrappers. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index c077cd8..f791701 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -7,12 +7,14 @@ We pledge to make participation in our community a harassment-free experience fo ## Our Standards Positive behavior includes: + - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and learning from mistakes Unacceptable behavior includes: + - Sexualized language or imagery, and unwanted sexual attention - Trolling, insulting or derogatory comments, personal attacks - Public or private harassment diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d4d19d0..c48981c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,6 +70,7 @@ cp .env.example .env ``` Fill in `.env`: + - `DATABASE_URL` — from your Neon project dashboard - `NEXTAUTH_SECRET` — generate with `openssl rand -base64 32` - `NEXTAUTH_URL` — `http://localhost:3000` for local dev @@ -109,6 +110,7 @@ docs/ — Additional documentation ``` For detailed documentation, see: + - [Architecture Overview](docs/ARCHITECTURE.md) — Data flow, tech stack, and key decisions - [API Reference](docs/API.md) — All API route documentation with request/response examples - [Component Reference](docs/COMPONENTS.md) — UI component catalog and usage patterns @@ -133,14 +135,14 @@ Comment on the issue saying you'd like to work on it. A maintainer will assign i Always branch off from `dev` (the default branch). Use this naming convention: -| Type | Format | Example | -|---|---|---| -| New feature | `feat/short-description` | `feat/dark-mode-toggle` | -| Bug fix | `fix/short-description` | `fix/map-marker-overlap` | -| Documentation | `docs/short-description` | `docs/update-setup-guide` | -| Tests | `test/short-description` | `test/insights-edge-cases` | -| Refactor | `refactor/short-description` | `refactor/emissions-engine` | -| UI/styling | `ui/short-description` | `ui/mobile-nav-polish` | +| Type | Format | Example | +| ------------- | ---------------------------- | --------------------------- | +| New feature | `feat/short-description` | `feat/dark-mode-toggle` | +| Bug fix | `fix/short-description` | `fix/map-marker-overlap` | +| Documentation | `docs/short-description` | `docs/update-setup-guide` | +| Tests | `test/short-description` | `test/insights-edge-cases` | +| Refactor | `refactor/short-description` | `refactor/emissions-engine` | +| UI/styling | `ui/short-description` | `ui/mobile-nav-polish` | ```bash git checkout dev @@ -165,6 +167,7 @@ Optional longer body explaining what and why (not how). **Types:** `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` **Examples:** + ``` feat(insights): add carbon intensity per revenue metric fix(map): prevent marker overlap when coordinates are identical @@ -179,16 +182,19 @@ Keep the first line under 72 characters. Use the imperative mood ("add", not "ad ## Pull Request Process 1. **Make sure your branch is up to date** with `dev` before opening a PR: + ```bash git fetch upstream git rebase upstream/dev ``` 2. **Run the full test suite** before submitting: + ```bash npm test npm run lint ``` + Fix any failures before opening the PR — PRs that fail CI won't be reviewed. 3. **Open the PR against `dev`** on the original repo (not `main`). @@ -217,6 +223,7 @@ Every PR is automatically reviewed by **[CodeRabbit](https://coderabbit.ai)**, a - Pre-merge checks (e.g. missing tests, accessibility issues, type safety) **What to do with CodeRabbit feedback:** + - Address any `❌` (failing) checks before asking for a human review - `⚠️` warnings are suggestions — use your judgement - You can reply directly to CodeRabbit's comments to ask it to explain something or re-check after a fix diff --git a/README.md b/README.md index 69cee42..b894b30 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ cp .env.example .env ``` Fill in `.env`: + - `DATABASE_URL` — your Neon connection string (use the pooled string for Vercel) - `NEXTAUTH_SECRET` — generate with `openssl rand -base64 32` - `NEXTAUTH_URL` — `http://localhost:3000` for local dev @@ -152,18 +153,18 @@ __tests__/ All routes are scoped to the signed-in user's organization. -| Route | Methods | Purpose | -|---|---|---| -| `/api/auth/[...nextauth]` | GET/POST | NextAuth session handling | -| `/api/signup` | POST | Create user + first organization | -| `/api/suppliers` | GET/POST | List / batch-create suppliers | -| `/api/facilities` | GET/POST | List / batch-create facilities | -| `/api/routes` | GET/POST | List / batch-create transport routes | -| `/api/activities` | GET/POST | List / batch-create usage records | -| `/api/upload` | POST | Bulk CSV/Excel ingestion | -| `/api/dashboard` | GET | Scope totals, top emitters, monthly trend | -| `/api/insights` | GET | Runs the rule-based insight engine | -| `/api/reports` | GET/POST | Report export history | +| Route | Methods | Purpose | +| ------------------------- | -------- | ----------------------------------------- | +| `/api/auth/[...nextauth]` | GET/POST | NextAuth session handling | +| `/api/signup` | POST | Create user + first organization | +| `/api/suppliers` | GET/POST | List / batch-create suppliers | +| `/api/facilities` | GET/POST | List / batch-create facilities | +| `/api/routes` | GET/POST | List / batch-create transport routes | +| `/api/activities` | GET/POST | List / batch-create usage records | +| `/api/upload` | POST | Bulk CSV/Excel ingestion | +| `/api/dashboard` | GET | Scope totals, top emitters, monthly trend | +| `/api/insights` | GET | Runs the rule-based insight engine | +| `/api/reports` | GET/POST | Report export history | --- @@ -182,6 +183,7 @@ All factors live in the `EmissionFactor` table (seeded with indicative DEFRA/EPA ## Insights engine `lib/insights.ts` is fully rule-based (no ML/LLM): + - Top-emitter concentration (supplier or facility > 30% of total) - High-carbon transport mode flags (air freight, with rail/sea mode-shift suggestions) - Scope 2 share warnings (purchased electricity > 30% of total) @@ -234,4 +236,4 @@ Contributions are welcome from developers of all skill levels! Licensed under the [Apache License 2.0](LICENSE). -Copyright 2026 Arghya and EcoSphere contributors. \ No newline at end of file +Copyright 2026 Arghya and EcoSphere contributors. diff --git a/__tests__/e2e/core-flows.spec.ts b/__tests__/e2e/core-flows.spec.ts index 90ee42f..6e9b215 100644 --- a/__tests__/e2e/core-flows.spec.ts +++ b/__tests__/e2e/core-flows.spec.ts @@ -1,7 +1,9 @@ import { test, expect } from '@playwright/test'; test.describe('Landing page', () => { - test('shows hero content and the interactive demo responds to scenario clicks', async ({ page }) => { + test('shows hero content and the interactive demo responds to scenario clicks', async ({ + page, + }) => { await page.goto('/'); await expect(page.getByRole('heading', { name: /supply chain/i })).toBeVisible(); @@ -11,9 +13,19 @@ test.describe('Landing page', () => { // Toggling Scope 3 visibility should change the total readout. const scope3Toggle = page.getByLabel('Show Scope 3 (transport)'); - const totalBefore = await page.locator('text=Total estimated emissions').locator('..').locator('p').first().innerText(); + const totalBefore = await page + .locator('text=Total estimated emissions') + .locator('..') + .locator('p') + .first() + .innerText(); await scope3Toggle.uncheck(); - const totalAfter = await page.locator('text=Total estimated emissions').locator('..').locator('p').first().innerText(); + const totalAfter = await page + .locator('text=Total estimated emissions') + .locator('..') + .locator('p') + .first() + .innerText(); expect(totalAfter).not.toBe(totalBefore); }); diff --git a/__tests__/unit/activity-pagination.test.ts b/__tests__/unit/activity-pagination.test.ts index 3d3475f..c3aabe8 100644 --- a/__tests__/unit/activity-pagination.test.ts +++ b/__tests__/unit/activity-pagination.test.ts @@ -24,4 +24,3 @@ describe('Activity Pagination Calculations', () => { expect(isStartValid && isEndValid).toBe(true); }); }); - diff --git a/__tests__/unit/builder-delete.test.ts b/__tests__/unit/builder-delete.test.ts index ef2ddef..e43ad26 100644 --- a/__tests__/unit/builder-delete.test.ts +++ b/__tests__/unit/builder-delete.test.ts @@ -35,7 +35,9 @@ describe('guardNodeDeletion', () => { const facility = guardNodeDeletion('facility', 2); expect(supplier.blocked).toBe(true); expect(facility.blocked).toBe(true); - if (supplier.blocked) expect(supplier.error).toContain('This supplier is still used by 2 routes'); - if (facility.blocked) expect(facility.error).toContain('This facility is still used by 2 routes'); + if (supplier.blocked) + expect(supplier.error).toContain('This supplier is still used by 2 routes'); + if (facility.blocked) + expect(facility.error).toContain('This facility is still used by 2 routes'); }); }); diff --git a/__tests__/unit/chart-data.test.ts b/__tests__/unit/chart-data.test.ts index 844004c..4ac25b8 100644 --- a/__tests__/unit/chart-data.test.ts +++ b/__tests__/unit/chart-data.test.ts @@ -22,4 +22,3 @@ describe('Chart Data Mapping Formatting', () => { expect(filtered.find((d) => d.name.includes('Scope 2'))?.value).toBe(5000); }); }); - diff --git a/__tests__/unit/emissions.test.ts b/__tests__/unit/emissions.test.ts index 4acc2b6..40602da 100644 --- a/__tests__/unit/emissions.test.ts +++ b/__tests__/unit/emissions.test.ts @@ -1,8 +1,16 @@ -import { calculateActivityEmissions, aggregateByScope, aggregateByEntity, aggregateByMonth, deriveActivityType } from '@/lib/emissions'; +import { + calculateActivityEmissions, + aggregateByScope, + aggregateByEntity, + aggregateByMonth, + deriveActivityType, +} from '@/lib/emissions'; import type { ActivityWithFactor } from '@/lib/emissions'; function makeActivity( - overrides: Partial> & { factor?: Partial } + overrides: Partial> & { + factor?: Partial; + } ): ActivityWithFactor { const { factor: factorOverrides, ...activityOverrides } = overrides; return { @@ -54,7 +62,10 @@ describe('aggregateByScope', () => { it('sums emissions into the correct scope bucket', () => { const activities = [ makeActivity({ emissionsKg: 1340, factor: { scope: 'SCOPE_1' } }), - makeActivity({ emissionsKg: 2484, factor: { scope: 'SCOPE_2', category: 'electricity_UK-grid' } }), + makeActivity({ + emissionsKg: 2484, + factor: { scope: 'SCOPE_2', category: 'electricity_UK-grid' }, + }), makeActivity({ emissionsKg: 4816, factor: { scope: 'SCOPE_3', category: 'air_freight' } }), ]; @@ -66,7 +77,13 @@ describe('aggregateByScope', () => { }); it('falls back to amount * factor.value when emissionsKg is missing', () => { - const activities = [makeActivity({ emissionsKg: undefined as unknown as number, amount: 500, factor: { value: 2.68 } })]; + const activities = [ + makeActivity({ + emissionsKg: undefined as unknown as number, + amount: 500, + factor: { value: 2.68 }, + }), + ]; const result = aggregateByScope(activities); expect(result.scope1).toBeCloseTo(1340); }); @@ -110,7 +127,9 @@ describe('aggregateByEntity', () => { }); it('ignores activities with no matching entity id for the requested type', () => { - const activities = [makeActivity({ supplierId: null, facilityId: null, routeId: null, emissionsKg: 500 })]; + const activities = [ + makeActivity({ supplierId: null, facilityId: null, routeId: null, emissionsKg: 500 }), + ]; const result = aggregateByEntity(activities, 'supplier', []); expect(result).toEqual([]); }); diff --git a/__tests__/unit/map-filters.test.ts b/__tests__/unit/map-filters.test.ts index 4843b56..d664a12 100644 --- a/__tests__/unit/map-filters.test.ts +++ b/__tests__/unit/map-filters.test.ts @@ -10,10 +10,7 @@ describe('Map Filters Logic', () => { }); it('filters markers by search query', () => { - const suppliers = [ - { name: 'Supplier Alpha' }, - { name: 'Beta Industry' }, - ]; + const suppliers = [{ name: 'Supplier Alpha' }, { name: 'Beta Industry' }]; const query = 'alpha'; const filtered = suppliers.filter((s) => s.name.toLowerCase().includes(query)); expect(filtered.length).toBe(1); diff --git a/__tests__/unit/middleware.test.ts b/__tests__/unit/middleware.test.ts index e38003f..2f12d15 100644 --- a/__tests__/unit/middleware.test.ts +++ b/__tests__/unit/middleware.test.ts @@ -75,7 +75,7 @@ describe('Rate Limiting Middleware', () => { it('sets correct rate limit headers for first request', () => { const req = mockRequest('/api/upload'); const res = middleware(req) as any; - + expect(res.headers.get('X-RateLimit-Limit')).toBe('5'); expect(res.headers.get('X-RateLimit-Remaining')).toBe('4'); expect(res.headers.get('X-RateLimit-Reset')).toBeDefined(); diff --git a/__tests__/unit/responsive-table.test.tsx b/__tests__/unit/responsive-table.test.tsx index 1930b74..3d7b1e3 100644 --- a/__tests__/unit/responsive-table.test.tsx +++ b/__tests__/unit/responsive-table.test.tsx @@ -21,13 +21,7 @@ const data: TestItem[] = [ describe('ResponsiveTable', () => { it('renders table headers and data', () => { - render( - item.id} - /> - ); + render( item.id} />); expect(screen.getAllByText('Name').length).toBeGreaterThan(0); expect(screen.getAllByText('Value').length).toBeGreaterThan(0); diff --git a/__tests__/unit/routes-validate.test.ts b/__tests__/unit/routes-validate.test.ts index 320e16d..90faf8f 100644 --- a/__tests__/unit/routes-validate.test.ts +++ b/__tests__/unit/routes-validate.test.ts @@ -87,7 +87,11 @@ describe('normalizeAndValidateRoutes', () => { it('accepts a well-formed multi-route batch (no regression on the happy path)', () => { const result = normalizeAndValidateRoutes([ route({ destinationId: 'facility-1' }), - route({ destinationId: 'facility-2', originSupplierId: undefined, originFacilityId: 'facility-9' }), + route({ + destinationId: 'facility-2', + originSupplierId: undefined, + originFacilityId: 'facility-9', + }), ]); expect(result.ok).toBe(true); if (result.ok) { diff --git a/__tests__/unit/toast-provider.test.tsx b/__tests__/unit/toast-provider.test.tsx index ff29c75..663ea0c 100644 --- a/__tests__/unit/toast-provider.test.tsx +++ b/__tests__/unit/toast-provider.test.tsx @@ -11,12 +11,8 @@ function TestHarness() { - - + + ); } diff --git a/app/[locale]/(auth)/login/page.tsx b/app/[locale]/(auth)/login/page.tsx index e788105..008b045 100644 --- a/app/[locale]/(auth)/login/page.tsx +++ b/app/[locale]/(auth)/login/page.tsx @@ -13,14 +13,21 @@ export default function LoginPage() {
- + - Loading…
}> + Loading… + } + > diff --git a/app/[locale]/(auth)/signup/page.tsx b/app/[locale]/(auth)/signup/page.tsx index c74c59c..92fb0be 100644 --- a/app/[locale]/(auth)/signup/page.tsx +++ b/app/[locale]/(auth)/signup/page.tsx @@ -59,7 +59,10 @@ export default function SignupPage() {
- +