Skip to content
Closed
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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Server
PORT=3001
DATABASE_URL=postgres://t19:t19@localhost:5432/crate

# Client (Vite)
VITE_GRAPHQL_URL=http://localhost:3001/graphql
111 changes: 111 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
name: CI/CD

on:
pull_request:
branches: [main]
push:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
quality:
name: Format, lint, typecheck, test, build
runs-on: [self-hosted, linux, x64]
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.33.0

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Check formatting
run: pnpm format:check

- name: Lint
run: pnpm lint

- name: Typecheck
run: pnpm typecheck

- name: Unit tests (server + client)
run: pnpm test

- name: Build (server + client)
run: pnpm build

# Bundle budget: fail the build if the main JS chunk creeps past 500 kB.
# This is one of our documented bærekraft measures — see docs/architecture.md.
- name: Enforce client bundle budget
run: |
set -euo pipefail
MAX_BYTES=512000
for f in client/dist/assets/*.js; do
size=$(stat -c%s "$f")
echo "$f = ${size} bytes"
if [ "$size" -gt "$MAX_BYTES" ]; then
echo "::error::Bundle budget exceeded: $f is ${size} bytes (max ${MAX_BYTES})."
exit 1
fi
done
deploy:
name: Deploy to the group VM
needs: quality
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: [self-hosted, linux, x64]
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.33.0

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build server and client
run: pnpm build
env:
VITE_GRAPHQL_URL: /graphql

- name: Deploy client to Apache
run: |
set -euo pipefail
find /var/www/html/project2 -mindepth 1 -delete
cp -r client/dist/. /var/www/html/project2/
chmod -R a+rX /var/www/html/project2
- name: Restart the GraphQL API
run: sudo systemctl restart crate-api

- name: Smoke check
run: |
set -euo pipefail
sleep 2
curl -fsS -o /dev/null -w 'GET /project2/ : %{http_code}\n' http://localhost/project2/
curl -fsS -o /dev/null -w 'POST /graphql : %{http_code}\n' \
-X POST http://localhost/graphql \
-H 'content-type: application/json' \
-d '{"query":"{ genres { name } }"}'
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
node_modules
dist
coverage
.env
.env.local
*.log
.DS_Store
e2e/test-results
e2e/playwright-report
e2e/.cache
server/src/generated
client/src/gql/graphql.ts
.vscode
.idea
9 changes: 9 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
dist
coverage
node_modules
pnpm-lock.yaml
*.min.js
client/src/gql
server/src/generated
e2e/test-results
e2e/playwright-report
6 changes: 6 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 90,
"semi": true
}
86 changes: 86 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# AGENTS.md

Guidance for AI coding agents and human contributors working in this repository.

## Project

**Crate** — community album catalog for IT2810 (NTNU) Project 2. Search, filter
and sort albums; open a detail view; persist ratings, reviews and a personal
crate.

- pnpm workspace: `client/` (React 19 + Vite + TS + Apollo Client), `server/`
(GraphQL Yoga + Drizzle + PostgreSQL), `e2e/` (Playwright + axe-core).
- Node.js 24.x, pnpm 10.x.
- TypeScript strict mode with `noUnusedLocals` and `noUncheckedIndexedAccess`.

## Commands

| Command | Purpose |
| --------------------------------------------------------- | ----------------------------------------------- |
| `pnpm install` | Install all workspaces |
| `pnpm dev` | API (:3001) + client (:5173) in watch mode |
| `pnpm build` | Production build of server + client |
| `pnpm lint` | ESLint, must pass with 0 warnings |
| `pnpm format` / `pnpm format:check` | Prettier |
| `pnpm typecheck` | `tsc --noEmit` in every workspace |
| `pnpm test` | Vitest for server + client |
| `pnpm test:e2e` | Playwright (needs API + client preview running) |
| `pnpm db:migrate` / `pnpm db:seed` / `pnpm db:seed:large` | Schema / data |

Run `pnpm format:check && pnpm lint && pnpm typecheck && pnpm test` before a PR.

## Architecture rules (do not casually change these)

1. **Sorting and filtering happen in SQL**, never by rearranging a loaded page
in the client. `searchAlbums` arguments map to `WHERE` / `ORDER BY`.
2. **Pagination is keyset/cursor-based** (`first` + `after`), not `OFFSET`. See
`server/src/search/search.ts`.
3. **Every relation field goes through the per-request DataLoaders** in
`server/src/loaders/index.ts`. Do not call `db.select` from a field resolver.
4. **User input is never interpolated into SQL.** Use drizzle `sql` templates so
values are bound parameters (there is a unit test for this).
5. **Users are simulated**: a UUID in `localStorage` (`t19.crate.user`). No
authentication. Mutations take `userId` + `displayName` and upsert into
`"user"`.
6. **Plain CSS only** (one file, `client/src/App.css`). No CSS framework, no
inline style objects for static styling. Respect `prefers-color-scheme` and
`prefers-reduced-motion`. Visible focus rings are required.
7. **GraphQL schema lives in `server/src/schema.graphql`** and is the contract.
Resolvers must not return shapes that deviate from it.
8. **Accessibility is not optional**: form controls need labels or
`aria-label`, new async content needs `role="status"` + `aria-live="polite"`,
interactive star ratings must be a radio group.

## Layout

```
server/src/
schema.graphql GraphQL SDL
resolvers/ Query, Mutation and per-type field resolvers
search/ pure WHERE / ORDER BY / cursor builders (unit-tested)
loaders/ DataLoader batchers (N+1 defence)
db/ Drizzle schema, migrations (drizzle/*.sql), seed scripts
client/src/
api/ Apollo Client instance + GraphQL documents
components/ presentational + container components
hooks/ useDebounce, useSearchState (URL), useUserId
test/ Vitest setup + MSW handlers
e2e/tests/ Playwright journeys (search → filter → sort → review)
docs/ architecture.md, VM-setup.md, AI-USAGE.md
```

## Conventions

- One component per file; props-driven; derived logic in hooks.
- Conventional commits referencing the issue (`feat: #12 add decade facet`).
- Branch names: `feat/<issue>-short-name`, `fix/<issue>-short-name`.
- Every AI-assisted change is logged in `docs/AI-USAGE.md` and goes through
normal peer review (see `CODEOWNERS`).

## Infrastructure

- Backend listens on port **3001**; database is only reachable from the VM.
- Client is built with `base: '/project2/'` and served by Apache at
`http://it2810-19.idi.ntnu.no/project2`.
- CI runs on the self-hosted runner on the group VM. Full VM runbook:
`docs/VM-setup.md`.
130 changes: 114 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,122 @@
# T19-Project-2
# Crate — community album catalog

Repository for team IT2810-H26-T19 in IT2810-H26.
**IT2810 Project 2 · Team T19** · live at `http://it2810-19.idi.ntnu.no/project2`

## Working in this repository
Crate is a searchable catalog of albums. You search by word (case-insensitive),
filter by genre and decade, sort the **whole** result set, page through large
results, open an album to see its tracklist and community reviews, and add
your own rating, review and wantlist ("crate").

`main` is protected — you cannot push to it directly. Work on a branch and
open a pull request:
---

## Quick start

Requirements: **Node.js 24+** and **pnpm 10+** (see [nvm](https://github.com/nvm-sh/nvm)),
and a local **PostgreSQL 16+** (only needed for the API).

```bash
git checkout -b my-feature
git push -u origin my-feature
# 1. Install workspace dependencies
pnpm install

# 2. Create a database and user (once)
sudo -u postgres psql <<'SQL'
CREATE USER t19 WITH PASSWORD 't19';
CREATE DATABASE crate OWNER t19;
SQL

# 3. Configure the server
cp .env.example .env # adjust DATABASE_URL if needed

# 4. Create tables and seed a sample catalog
pnpm db:migrate
pnpm db:seed # ~20 curated albums
pnpm db:seed:large # optional: +100k synthetic albums (ALBUM_COUNT=…)

# 5. Run both apps (API on :3001, client on :5173)
pnpm dev
```

Open <http://localhost:5173/project2/>.

| Command | Purpose |
| ---------------------------------- | --------------------------------- |
| `pnpm dev` | API + client in watch mode |
| `pnpm build` | Production build of both packages |
| `pnpm lint` / `pnpm format` | ESLint (0 warnings) / Prettier |
| `pnpm typecheck` | TypeScript, no emit |
| `pnpm test` | Vitest for server + client |
| `pnpm test:e2e` | Playwright end-to-end tests |
| `pnpm db:migrate` / `pnpm db:seed` | Schema / sample data |

Run `pnpm format:check && pnpm lint && pnpm typecheck && pnpm test` before opening a PR.

---

## What the application does

| Requirement | How it is implemented |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Search on single terms, case-insensitive, no wasted requests | Postgres full-text search (`websearch_to_tsquery('simple', …)` on a GIN-indexed `search_vector`), 300 ms debounce + explicit submit |
| List presentation of large result sets | Infinite scroll with a keyboard-accessible **Load more** button. Cursor (keyset) pagination — see [docs/architecture.md](docs/architecture.md) |
| Detail view | `/album/:id` with tracklist, artist, genres, community reviews |
| Sort and filter on the **whole** result set | Both are SQL `ORDER BY` / `WHERE` clauses. Changing either re-queries the server; nothing is rearranged client-side |
| Persistent user-generated data | Ratings (1–5) + written reviews + a personal crate, all in PostgreSQL. Users are simulated (UUID in `localStorage`) as the lecture allows |
| Universal utforming | Skip link, landmarks, `aria-live` result count, radio-group star rating, visible focus, keyboard-operable everything. `axe-core` runs in the E2E suite |
| Bærekraft | No UI framework, debounced queries, lazy images, `prefers-color-scheme` + `prefers-reduced-motion`, GraphQL fetches only the requested fields |
| GraphQL backend | Hand-written schema + resolvers on GraphQL Yoga. DataLoaders remove the N+1 pattern |
| State management | Apollo Client cache with a `typePolicies` merge for paginated results, plus URL state for query/filters/sort |
| Testing | Vitest (server SQL builders + client components, MSW), Playwright E2E for the full journey |

---

## Architecture

```
client/ React 19 + Vite + TypeScript + Apollo Client (SPA)
server/ GraphQL Yoga + Drizzle + PostgreSQL 16
e2e/ Playwright + axe-core
```

Before a pull request can be merged:
The database is only reachable from the VM itself. The GraphQL API on port
**3001** is the only thing exposed to the browser — matching the course
requirement that the database and backend run _directly on the VM_ (no Docker,
no external BaaS).

Decisions and trade-offs (why PostgreSQL and not MongoDB/Neo4j, why cursor
pagination, how N+1 is handled) are written up in
[docs/architecture.md](docs/architecture.md). VM setup is in
[docs/VM-setup.md](docs/VM-setup.md). AI usage is logged in
[docs/AI-USAGE.md](docs/AI-USAGE.md).

---

## Project layout

```
server/src/
schema.graphql GraphQL SDL — the contract
resolvers/ Query/Mutation/field resolvers
search/ WHERE/ORDER BY/cursor builders (pure, unit-tested)
loaders/ per-request DataLoaders (N+1 defence)
db/ Drizzle schema, migrations, seed scripts
client/src/
api/ Apollo Client + GraphQL documents
components/ SearchBar, FilterPanel, AlbumList, AlbumCard, …
hooks/ useDebounce, useSearchState (URL), useUserId
e2e/tests/ Playwright journeys
docs/ architecture, VM setup, AI usage
```

---

## Workflow

1. Pick an issue from the current milestone.
2. Branch from `main` as `feat/<issue>-short-name`.
3. Conventional commits referencing the issue.
4. Open a PR. A teammate must approve (see `CODEOWNERS`).
5. Resolve every review conversation, then merge.

1. **A member of IT2810-H26-T19 must approve it.** You cannot approve your own pull
request, and approvals from staff do not count — you review each other's
code.
2. **Every review comment must be marked resolved.** Reply to each one, fix it
or explain why not, then click *Resolve conversation*.
3. **Pushing new commits dismisses the approval**, so if you change something
after being approved, ask for another review. This is deliberate: what was
approved is what gets merged.
CI runs on the self-hosted runner on the group VM: lint, typecheck, unit tests
and a production build must pass before merge. Pushes to `main` deploy the
client build and restart the API.
13 changes: 13 additions & 0 deletions client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Crate — search, review and collect albums." />
<title>Crate — album catalog</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Loading
Loading