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
32 changes: 32 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Normalize line endings to LF in the repository and in every working tree.
#
# Without this, `core.autocrlf=true` on Windows (the Git for Windows default)
# rewrites the checked-out files to CRLF. Prettier then reports spurious
# formatting failures on a clean checkout even though the files are correct in
# the repository. Setting `eol=lf` here means all four of us see the same bytes
# regardless of platform, so `pnpm format:check` behaves identically everywhere.
#
# Found in review of PR #32.
* text=auto eol=lf

# Shell and batch scripts. Shebangs need LF to be honoured on Linux; the
# Windows shell driver below explicitly wants CRLF.
*.sh text eol=lf
*.bash text eol=lf
*.cmd text eol=crlf
*.bat text eol=crlf

# Never normalize binary assets.
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.webp binary
*.ico binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.pdf binary
*.zip binary
*.gz binary
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
node_modules
dist
coverage
.env
.env.local
*.log
.DS_Store
.vscode
.idea
tsconfig.tsbuildinfo
*.tsbuildinfo
5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dist
coverage
node_modules
pnpm-lock.yaml
*.min.js
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
}
82 changes: 82 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# AGENTS.md

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

## Project

**Crate** - a searchable catalog of albums. Users search by word, filter and
sort the results, open an album for detail, and persist ratings, reviews and a
personal wantlist ("crate").

IT2810 (NTNU) Project 2. Group T19: `erikhfj`, `robertky`, `evenkkl`, `rachelks`.

**Status: M1 - spec and mockup.** `client/` renders mock data. `server/`
(PostgreSQL + GraphQL) arrives in M2, `e2e/` (Playwright) in M4.

## Commands

| Command | Purpose |
| ----------------------------------- | -------------------------------------- |
| `pnpm install` | Install workspace dependencies |
| `pnpm dev` | Vite dev server |
| `pnpm build` | Type-check + production build |
| `pnpm lint` | ESLint - must pass with **0 warnings** |
| `pnpm format` / `pnpm format:check` | Prettier |
| `pnpm typecheck` | `tsc --noEmit` in every workspace |
| `pnpm test` | Vitest, single run |

Run `pnpm format:check && pnpm lint && pnpm typecheck && pnpm test` before
opening a PR. CI enforces the same gates.

## Architecture rules

These are the rules we agreed on. They are the ones that make the graded
requirements work - do not casually change them.

1. **Sorting and filtering will happen in SQL**, never by rearranging a loaded
page in the client. The brief is explicit that they must apply to the whole
result set. (From M2 onward.)
2. **Pagination will be keyset/cursor-based** (`first` + `after`), not
`OFFSET`. It has to stay fast on page 1 and page 5000 of an undefined data
volume.
3. **Every relation field will go through per-request DataLoaders.** GraphQL's
weak spot is server-side N+1; do not call the database one row at a time
from a field resolver.
4. **User input is never interpolated into SQL.** Use parameterised queries.
5. **Users are simulated** - a UUID in `localStorage`, sent with every
mutation. No authentication. This is allowed by the course and is enough to
persist reviews server-side.
6. **Plain CSS.** No CSS framework, no component library. Smaller bundle for
bærekraft, full control over focus rings and contrast for tilgjengelighet.
7. **Accessibility is not optional.** Every input needs a label or
`aria-label`. New async content needs `role="status"` +
`aria-live="polite"`. Interactive star ratings must be a radio group.
Visible focus rings everywhere.
8. **Search state lives in the URL** so reload, back/forward and shared links
all work.

## Conventions

- TypeScript `strict` with `noUncheckedIndexedAccess`.
- One component per file; props-driven; derived logic in hooks.
- Conventional commits referencing the issue (`feat: #9 mock-data search list`).
- Branch names: `feat/<issue>-short-name`, `fix/<issue>-short-name`.
- PRs are small and describe **what was done and what was tested**. The
reviewer is not the author.

## Layout

```
client/ React + Vite + TypeScript (mock data at M1)
docs/ workflow, AI usage log (from #4)
```

`server/` (GraphQL Yoga + Drizzle + PostgreSQL) and `e2e/` (Playwright) join
as they land.

## Infrastructure

The finished app runs on the group VM: client under
`http://it2810-19.idi.ntnu.no/project2` behind Apache, GraphQL API on port
3001, PostgreSQL reachable only from the VM. No Docker. See `docs/VM-setup.md`
(from M2).
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Repository for team IT2810-H26-T19 in IT2810-H26.

## Working in this repository

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

```bash
Expand All @@ -15,10 +15,10 @@ git push -u origin my-feature
Before a pull request can be merged:

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
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*.
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.
approved is what gets merged.
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>
30 changes: 30 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@t19/client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"typecheck": "tsc -b --noEmit",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.1.0",
"jsdom": "^29.1.1",
"typescript": "~5.9.3",
"vite": "^8.2.2",
"vitest": "^4.1.11"
}
}
84 changes: 84 additions & 0 deletions client/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/* Plain CSS, no framework — smaller bundle, and full control over focus
styles and contrast for tilgjengelighet. */

:root {
color-scheme: light dark;
--bg: #fafaf8;
--surface: #ffffff;
--text: #1a1a1a;
--muted: #5c5c5c;
--accent: #2f5d50;
--focus: #1a73e8;
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
line-height: 1.5;
}

@media (prefers-color-scheme: dark) {
:root {
--bg: #141414;
--surface: #1e1e1e;
--text: #f2f2f0;
--muted: #a8a8a4;
--accent: #6fae9c;
}
}

@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

* {
box-sizing: border-box;
}

body {
margin: 0;
background: var(--bg);
color: var(--text);
}

.skip-link {
position: absolute;
left: -9999px;
top: 0;
background: var(--accent);
color: var(--surface);
padding: 0.5rem 1rem;
}
.skip-link:focus {
left: 0;
}

:where(a, button, input, select, textarea, [tabindex]):focus-visible {
outline: 3px solid var(--focus);
outline-offset: 2px;
}

header {
padding: 1.5rem;
border-bottom: 1px solid color-mix(in srgb, var(--text) 15%, transparent);
}
header h1 {
margin: 0;
}
.tagline {
margin: 0.25rem 0 0;
color: var(--muted);
}

main {
padding: 1.5rem;
max-width: 60rem;
margin: 0 auto;
}
22 changes: 22 additions & 0 deletions client/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import App from './App.js';

describe('App', () => {
it('renders the product name and tagline', () => {
render(<App />);
expect(screen.getByRole('heading', { level: 1, name: 'Crate' })).toBeInTheDocument();
expect(screen.getByText('Search, review and collect albums.')).toBeInTheDocument();
});

it('exposes a skip link as the first focusable element', () => {
render(<App />);
const skip = screen.getByRole('link', { name: 'Skip to content' });
expect(skip).toHaveAttribute('href', '#main');
});

it('renders a main landmark', () => {
render(<App />);
expect(screen.getByRole('main')).toBeInTheDocument();
});
});
21 changes: 21 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* M1 placeholder. The point of this file is to prove the tooling works:
* typecheck, lint, tests and build are all green before #9 replaces it with
* the mock-data search interface.
*/
export default function App() {
return (
<>
<a href="#main" className="skip-link">
Skip to content
</a>
<header>
<h1>Crate</h1>
<p className="tagline">Search, review and collect albums.</p>
</header>
<main id="main" tabIndex={-1}>
<p>Skeleton only — the mock-data search interface arrives in issue #9.</p>
</main>
</>
);
}
13 changes: 13 additions & 0 deletions client/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.js';
import './App.css';

const container = document.getElementById('root');
if (!container) throw new Error('Missing #root element in index.html');

createRoot(container).render(
<StrictMode>
<App />
</StrictMode>,
);
7 changes: 7 additions & 0 deletions client/src/test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';

afterEach(() => {
cleanup();
});
10 changes: 10 additions & 0 deletions client/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"noEmit": true,
"types": ["vite/client", "vitest/globals"]
},
"include": ["src/**/*", "vite.config.ts", "vitest.config.ts"]
}
11 changes: 11 additions & 0 deletions client/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
plugins: [react()],
// The course VM serves the built app from /project2.
base: '/project2/',
server: {
port: 5173,
},
});
12 changes: 12 additions & 0 deletions client/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
include: ['src/**/*.test.{ts,tsx}'],
},
});
Loading