Renamed, reformatted and expanded project readmes in anticipation of project handover.

This commit is contained in:
LosEvons
2025-12-01 00:44:14 +00:00
parent 7e90658e12
commit be312ba140
11 changed files with 955 additions and 203 deletions
+48 -11
View File
@@ -2,6 +2,12 @@
GRADESA is a freely accessible website where language learners at language centres as well as university students of German studies can autonomously work on their German grammar skills and knowledge.
## Prerequisites
- **Node.js**: v20 or higher (included in the dev container)
- **npm**: v10.2.4 or higher (included in the dev container)
- **Container runtime**: Docker or Podman with compose support
## Install
To be able to use the container-based development environment, you need to be able to run **rootless** containers.
@@ -13,7 +19,12 @@ To be able to use the container-based development environment, you need to be ab
### For Windows & MacOS
Since none of us has a MacOS or Windows machine available, we are not able to test this. But most likely using podman would be suitable. If not, see the explanation section below to make debugging easier.
**Note**: These instructions have not been tested on Windows or MacOS.
- **Windows**: Use [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/) or [Podman Desktop](https://podman-desktop.io/)
- **MacOS**: Use [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/) or [Podman Desktop](https://podman-desktop.io/)
If you encounter permission issues, see the explanation section below about rootless containers.
### Explanation
@@ -22,17 +33,43 @@ The development environment includes a container named node-dev-env which can be
## Running locally
1. Clone the repo to your machine: `$ git clone git@github.com:OHTU-German-learning-website/OHTU-German-learning-website.git`
2. Cd into cloned repository: `$ cd OHTU-German-learning-website`
2. cd into cloned repository: `$ cd OHTU-German-learning-website`
3. For docker: `$ docker-compose up -d`, or for Podman: `$ podman-compose up -d`
4. Steps 4-8 for VScode only
5. Install dev-containers extension
6. If you are using podman, change the value of "Dev › Containers: Docker Path" to "podman" in settings
7. In the bottom left corner, press "open a remote window" and select "attach to running container" and select "node-dev-env"
8. Open folder /host in VScode in the remote window
9. The next actions should be run inside the container at /host
10. Run inside the container: `$ npm install && npm run db:migrate`, `$ cd gradesa && npm install`
11. Start the application: `$ npm run dev`
12. Navigate to localhost:3000 in your browser
### VSCode Setup (Optional)
4. Install the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
5. If using Podman, change "Dev › Containers: Docker Path" to "podman" in VSCode settings
6. Click the remote window icon (bottom left corner) → "Attach to Running Container" → select "node-dev-env"
7. In the container window, open folder `/host`
### Alternative: Terminal Access
Without VSCode, access the container with:
```bash
# Docker
docker exec -it node-dev-env bash
# Podman
podman exec -it node-dev-env bash
```
### Start Development
8. Inside the container at `/host`, install dependencies and set up the database:
```bash
npm install && npm run db:migrate
cd gradesa && npm install
```
9. Start the application: run `npm run dev` from the `gradesa/` folder (or run `npm run dev` at the repository root which forwards to the `gradesa` app).
10. Navigate to `http://localhost:3000` in your browser
### Environment Variables
Required environment variables are documented in `gradesa/src/backend/config.js`. The dev environment uses `.env` and `.env.development` files. See [Database Migrations](gradesa/data/DATABASE_MIGRATIONS.md) for database configuration.
### Explanation
+89 -9
View File
@@ -1,6 +1,10 @@
# Middleware Documentation
# Global Middleware Documentation
This document explains the purpose and functionality of the middleware located in `src/middleware.js`.
_Last Updated: November 30, 2025_
**File Location**: `src/middleware.js`
This document explains the purpose and functionality of the global Next.js middleware that runs on every request (except API routes and static assets).
## Table of Contents
@@ -8,7 +12,8 @@ This document explains the purpose and functionality of the middleware located i
2. [How It Works](#how-it-works)
3. [Configuration](#configuration)
4. [Examples](#examples)
5. [Notes](#notes)
5. [API Route Helpers](#api-route-helpers)
6. [Notes](#notes)
## Purpose
@@ -27,7 +32,7 @@ The middleware is used for:
3. The middleware performs the following checks:
- If the user is **not logged in** and tries to access a route in `authRequired`, they are redirected to the login page (`/auth/login`).
- If the user is **not logged in** and tries to access a route in `authRequired`, they are redirected to the login page (`/auth/login`) with the original path saved as a `redirect` query parameter. After successful login, they will be redirected back to the originally requested page.
- If the user is **logged in** and tries to access a route in `unauthRequired`, they are redirected to the home page (`/`).
- If the route is not in either list, the request is allowed to proceed.
@@ -48,17 +53,92 @@ Paths are enclosed in quotes and separated by commas.
To protect a route so that only logged-in users can access it, add the route to `authRequired`:
```
const authRequired = ["/dashboard", "/profile", "/lessons/exercises"];
```javascript
const authRequired = ["/grammar/exercises", "/edit_info", "/dashboard"];
```
To restrict a route so that logged-in users cannot access it, add the route to `unauthRequired`:
```
```javascript
const unauthRequired = ["/auth/login", "/auth/register"];
```
**Current Configuration** (as of November 2025):
- Protected routes: `/grammar/exercises`, `/edit_info`
- Unauth-only routes: `/auth/login`, `/auth/register`
## API Route Helpers
While this global middleware handles page-level redirects, **API routes** use middleware wrappers for authentication and validation:
### `withAuth` - Authentication & Authorization
Use `withAuth` from `src/backend/middleware/withAuth.js` to enforce auth in API route handlers:
```js
import { withAuth } from "@/backend/middleware/withAuth";
// Require any authenticated user
export const GET = withAuth(async (request, { params }) => {
const user = request.user; // populated by withAuth
return new Response("ok");
});
// Require admin
export const POST = withAuth(
async (request) => {
// request.user.is_admin is true here
return new Response("admin ok");
},
{ requireAuth: true, requireAdmin: true }
);
```
**Options**:
- `requireAuth` (default `true`): When `true`, unauthenticated requests receive `401`.
- `requireAdmin` (default `false`): When `true`, non-admin users receive `401`.
### `withInputValidation` - Request Body Validation
Use `withInputValidation` from `src/backend/middleware/withInputValidation.js` to validate request bodies with Zod schemas:
```js
import { withInputValidation } from "@/backend/middleware/withInputValidation";
import { z } from "zod";
const schema = z.object({
email: z.string().email(),
username: z.string().min(3),
});
export const POST = withInputValidation(schema, async (req) => {
const body = await req.json(); // validated data
return new Response(JSON.stringify({ success: true }));
});
```
### Composing Middleware
Combine both for authenticated routes with validation:
```js
export const POST = withAuth(
withInputValidation(schema, async (req) => {
const user = req.user; // from withAuth
const data = await req.json(); // validated by withInputValidation
// ...
}),
{ requireAdmin: true }
);
```
For detailed API middleware documentation, see [BACKEND_MIDDLEWARE.md](src/backend/middleware/BACKEND_MIDDLEWARE.md).
## Notes
- If an unauthenticated user tries to access a protected page, they will be redirected to the login page. After logging in successfully, the user will be redirected back to the protected page.
- Middleware cannot access client-side state or hooks.
- If an unauthenticated user tries to access a protected page, they will be redirected to the login page. After logging in successfully, the user will be redirected back to the protected page (via the `redirect` query parameter).
- This middleware runs on the **edge runtime** and cannot access client-side state or React hooks.
- For API routes, use the `withAuth` and `withInputValidation` wrappers instead (see [API Route Helpers](#api-route-helpers) above).
- Session verification uses the `AUTH_COOKIE_NAME` cookie (defined in `src/shared/const.js`).
+66 -15
View File
@@ -1,37 +1,72 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# Next.js Application Guide
_Last Updated: November 30, 2025_
This is a [Next.js](https://nextjs.org) v15 project using the App Router architecture. This guide covers development workflow specific to the Next.js application in the `gradesa/` directory.
> **Note**: For overall project setup, see the [root README](../README.md).
## Getting Started
First, run the development server:
Run the development server from inside the container at `/host/gradesa`:
```bash
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
You can start editing the page by modifying `src/app/page.js`. The page auto-updates as you edit the file (hot module replacement).
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Project Structure
## Learn More
- `src/app/` - Pages, layouts, and API routes (Next.js App Router)
- `src/backend/` - Server-side utilities (database, auth, middleware)
- `src/components/` - Reusable UI components
- `src/shared/` - Code shared between client and server
- See [SOURCE_OVERVIEW.md](src/SOURCE_OVERVIEW.md) for detailed structure
To learn more about Next.js, take a look at the following resources:
## Available Scripts
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
### Development
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
- `npm run dev` - Start development server with Turbopack
- `npm run build` - Create production build
- `npm start` - Run production build (applies migrations first)
- `npm run lint` - Run ESLint
## Deploy on Vercel
### Testing
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
- `npm test` or `npm run test` - Run unit tests with Vitest
- `npm run test:coverage` - Run tests with coverage report
- `npm run test:coverage:ui` - Open Vitest UI with coverage
- `npm run test:coverage:ui:localhost` - Vitest UI bound to localhost
- `npm run test:e2e` - Run Playwright E2E tests
- `npm run test:e2e:ui` - Run E2E tests with Playwright UI
- `npm run test:e2e:debug` - Debug E2E tests
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
```
### Database
## Testing & Vitest UI
- `npm run db:migrate` - Apply database migrations (run from root)
- `npm run db:reset` - Reset database and reapply migrations (run from root)
This project uses Vitest for unit tests and V8 for coverage. Two helper scripts are provided:
## Deployment
This project deploys to **University of Helsinki OpenShift** (not Vercel). See the [root README](../README.md#openshift) for deployment details.
## Testing
### Unit Tests (Vitest)
This project uses [Vitest](https://vitest.dev/) for unit tests and V8 for coverage.
**Test Location**: Place test files adjacent to the code being tested with `.test.js` extension:
- Component tests: `src/components/**/*.test.js`
- Backend tests: `src/backend/**/*.test.js`
- Hook tests: `src/shared/hooks/**/*.test.js`
**Scripts**:
- `npm run test:coverage` — run tests with coverage in the terminal (no UI).
- `npm run test:coverage:ui` — start the Vitest UI bound to all interfaces so you can open the browser on your host:
@@ -53,3 +88,19 @@ Notes & Troubleshooting
- When running inside Docker/devcontainer:
- Ensure port `51204` is forwarded (the provided `docker-compose.yml` already maps `51204:51204`).
- If Vitest attempts to open a browser inside the container, use `--open=false` (already set in scripts).
### End-to-End Tests (Playwright)
E2E tests use [Playwright](https://playwright.dev/) to test the application in real browsers.
**Test Location**: `playwright/e2e/*.spec.js`
See [E2E_TESTS.md](playwright/e2e/E2E_TESTS.md) for detailed E2E testing documentation.
## Learn More
To learn more about Next.js and related tooling:
- [Next.js Documentation](https://nextjs.org/docs)
- [Learn Next.js](https://nextjs.org/learn)
- E2E tests: see `playwright/e2e/E2E_TESTS.md` (Playwright)
+43 -12
View File
@@ -1,22 +1,53 @@
# Database Migration with pgmigrate
# Database Migrations
This project uses `pgmigrate` to manage database migrations. `pgmigrate` is a tool that helps apply, and manage database schema changes in a consistent and reliable manner. Ran migrations are tracked in the `pg_migrate_migrations` table.
_Last Updated: November 30, 2025_
Having a migration system that's based on sql files is good because it keeps track of schema changes in git and guarantees that everyone is using the same schema. Pgmigrate is automatically installed in the node-dev-env container.
This project uses `pgmigrate` to manage schema changes. Applied migrations are tracked in the `pg_migrate_migrations` table.
## Migration Script
Location and scripts
The migration script is located at `data/migrate.sh`. This script uses `pgmigrate` to apply migrations to the database.
- Migration files: `gradesa/data/migrations/`
- Helper scripts: `gradesa/data/migrate.sh`, `gradesa/data/reset-db.sh`
### Running Migrations
Connection configuration is read from environment variables (see `gradesa/src/backend/config.js`): `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME`.
To run the migrations, execute the following command (in root): `npm run db:migrate`
Note: the development `docker-compose.yml` and the migration scripts use port `7742` for the main database by default.
### Resetting the database
Quick commands (run from project root `/host`):
To reset the database, you can use the following command: `npm run db:reset`
This will clear all tables, and rerun migrations.
```bash
# Apply pending migrations
npm run db:migrate
### Adding migrations
# Reset DB (drops data) and reapply all migrations
npm run db:reset
```
To add a migration, create a new SQL file with the migration you want to run and give it a name with the next number. (ex. `00042_create_user.sql`)
Naming and workflow
- File pattern: `<5-digit-number>_<descriptive_name>.sql` (e.g. `00002_add_users_table.sql`). Numbers must be sequential and zero-padded.
- Never edit a migration that has already been applied in any environment—create a new migration to make changes.
Adding a migration (summary):
1. Add `00027_your_description.sql` to `gradesa/data/migrations/`.
2. Test locally: `npm run db:migrate`.
3. Commit the new file and push.
Best practices
- Prefer idempotent statements (e.g. `IF NOT EXISTS`).
- Use transactions where appropriate.
- For destructive fixes in production, prefer new compensating migrations over editing old files.
- Document complex migrations with comments.
Rollback strategy
- `pgmigrate` does not provide automatic rollbacks. To reverse a change, author a new migration that undoes the previous change (e.g. `00028_rollback_xyz.sql`).
- For local development, `npm run db:reset` recreates the schema from scratch.
Troubleshooting pointers
- If a migration fails partway, inspect `pg_migrate_migrations` to see what applied and author a corrective migration.
- Check environment variables and ensure the DB container is running (`docker ps` / `podman ps`).
- Pull latest migrations before creating new ones to avoid numbering conflicts.
+152 -20
View File
@@ -1,14 +1,35 @@
# End-to-End Testing with Playwright
## Setup
_Last Updated: November 30, 2025_
This directory contains E2E tests using [Playwright](https://playwright.dev/) to test the application in real browsers.
## Prerequisites
- **Node.js**: v20+ (included in dev container)
- **Environment**: Run commands inside the dev container at `/host/gradesa`
- **Database**: Tests run against the test database (configured via `.env.test`)
## Initial Setup
Playwright is already configured in this project. If setting up from scratch:
```
npm install --save-dev @playwright/test
npx playwright install
```
## Quick E2E checklist
- Ensure the app is running (dev server at `http://localhost:3000`).
- Ensure the test database is migrated/available (`npm run db:migrate` with `.env.test` set if needed).
- Install Playwright browsers: `npx playwright install`.
- Run tests: `npm run test:e2e` (or `npx playwright test <file>`).
## Commands
Run these commands from `/host/gradesa` inside the container:
```bash
npm run test:e2e # Run all tests
npm run test:e2e:ui # Run with UI mode
@@ -16,18 +37,75 @@ npm run test:e2e:debug # Run in debug mode
npx playwright test [file] # Run specific test file
```
## Test Structure
## CI/CD Integration
Playwright tests run automatically in the GitHub Actions pipeline:
- Triggered on pull requests to `master`
- Tests run in headless mode
- Screenshots and videos captured on failure
- Test results available in GitHub Actions artifacts
## Writing New Tests
### File Naming
Follow the existing pattern: `feature-name.spec.js`
### Test Structure
```javascript
import { test, expect } from "@playwright/test";
test.describe("Feature Name", () => {
test.beforeEach(async ({ page }) => {
// Setup: navigate to page, login if needed
await page.goto("/your-route");
});
test("should do something specific", async ({ page }) => {
// Arrange: Set up test data
// Act: Perform user actions
// Assert: Verify results
await expect(page.locator("selector")).toBeVisible();
});
});
```
### Naming Conventions
- Use descriptive test names: `should display error message when form is invalid`
- Group related tests in `test.describe` blocks
- Use `test.beforeEach` for common setup
### Finding Elements
Prefer semantic selectors (in order of preference):
1. `getByRole()` - Most robust, uses accessibility roles
2. `getByLabel()` - For form fields
3. `getByText()` - For unique text content
4. `getByTestId()` - Last resort, requires adding `data-testid`
## Test Files Structure
Current test files in this directory:
- `homepage.spec.js` - Home page tests
- `navigation.spec.js` - Navigation tests
- `auth.spec.js` - Authentication tests
- `components.spec.js` - Component interaction tests
- `forms.spec.js` - Form tests
- `grammar.spec.js` - Grammar feature tests
- `page-analyzer.spec.js` - Page analysis utilities
- `advanced.spec.js` - Advanced interaction examples
- `utils/helpers.js` - Helper functions
## Key Findings
## Developer Notes
The following sections contain observations and findings from E2E test development. These are helpful for understanding the application structure and writing effective tests.
### Key Findings
1. **Navigation** - `page.goto()` is more reliable than clicking links
2. **Selectors** - `getByRole()` is more reliable than `locator()` with CSS selectors
@@ -39,30 +117,84 @@ npx playwright test [file] # Run specific test file
- Resources: Chapter navigation and content
- No hamburger menu or mobile-specific UI elements
### Screenshots
Playwright automatically captures screenshots on test failures. These are saved in:
- `test-results/` directory (gitignored)
- Available as artifacts in CI/CD runs
To manually capture screenshots during development:
```javascript
await page.screenshot({ path: "debug-screenshot.png" });
```
## Best Practices
1. Use `page-analyzer.spec.js` to understand page structure
2. Review screenshots in `screenshots/` to understand the UI
3. Use role-based selectors: `getByRole('link', { name: 'Kapitel 1' })`
4. For navigation, use direct `page.goto()` rather than clicking links
5. Add appropriate assertions to verify page content
1. **Understand page structure first**: Use `page-analyzer.spec.js` to explore pages
2. **Review screenshots**: Check `test-results/` and `screenshots/` to understand the UI
3. **Use semantic selectors**: Prefer `getByRole('link', { name: 'Kapitel 1' })`
4. **Direct navigation**: Use `page.goto()` for navigation rather than clicking links when appropriate
5. **Wait for stability**: Use `waitForLoadState('networkidle')` after navigation
6. **Isolate tests**: Each test should be independent and not rely on others
7. **Clean up**: Reset state after tests (e.g., delete created data)
## Common Issues
## Common Issues & Debugging
- **Element not found**: Use more robust selectors
- **Strict mode violations**: Use more specific selectors when multiple elements match
- **Navigation timeouts**: Use direct navigation
- **Timing issues**: Use `waitForLoadState('networkidle')`
### Element Not Found
## Next Steps
- **Solution**: Use more robust selectors (role-based)
- **Debug**: Use `await page.pause()` to inspect the page
- **Debug**: Print available elements: `console.log(await page.content())`
1. Form submission tests after analyzing form behavior
2. Content interaction tests
3. User journey tests
### Strict Mode Violations
- **Error**: "locator resolved to multiple elements"
- **Solution**: Make selector more specific or use `.first()`, `.last()`, `.nth()`
### Navigation Timeouts
- **Solution**: Use direct navigation with `page.goto()`
- **Solution**: Increase timeout: `await page.goto(url, { timeout: 60000 })`
### Timing Issues
- **Solution**: Use Playwright's auto-waiting (preferred)
- **Solution**: Explicit waits: `await page.waitForSelector('selector')`
- **Avoid**: `page.waitForTimeout()` - this is an anti-pattern
### Debugging Tips
```javascript
// Pause test execution
await page.pause();
// Take screenshot at specific point
await page.screenshot({ path: "debug.png" });
// Print page HTML
console.log(await page.content());
// Check what's visible
const visible = await page.locator("selector").isVisible();
console.log("Element visible:", visible);
```
## Further Development
Planned improvements:
1. Form submission tests with various input scenarios
2. Content interaction tests (exercise completion flows)
3. User journey tests (end-to-end user scenarios)
4. Visual regression tests
5. Accessibility tests using Playwright's accessibility testing
## Resources
- [Playwright Documentation](https://playwright.dev/docs/intro)
- [API Reference](https://playwright.dev/docs/api/class-playwright)
- [Best Practices](https://playwright.dev/docs/best-practices)
- [Playwright Documentation](https://playwright.dev/docs/intro) - Getting started guide
- [Playwright API Reference](https://playwright.dev/docs/api/class-playwright) - Complete API docs
- [Playwright Best Practices](https://playwright.dev/docs/best-practices) - Official best practices
- [Playwright Test Generator](https://playwright.dev/docs/codegen) - Generate tests by recording actions
- [Debugging Tests](https://playwright.dev/docs/debug) - Debugging guide
+30 -5
View File
@@ -1,7 +1,25 @@
# src Directory Overview
# Source Code Overview
_Last Updated: November 30, 2025_
This directory contains the application source code organized by responsibility. Below is a quick guide to the main folders and notable files.
## Import Path Aliases
The project uses path aliases configured in `jsconfig.json`:
```javascript
// Instead of relative imports:
import { useUser } from "../../../context/user.context";
// Use @/ alias for cleaner imports:
import { useUser } from "@/context/user.context";
import Button from "@/components/ui/button/button";
import { withAuth } from "@/backend/middleware/withAuth";
```
The `@/*` alias maps to `./src/*`, making imports consistent regardless of file depth.
## Top-Level Structure
- `app/` – Next.js App Router pages, layouts, and API route handlers. Subfolders group feature areas (e.g. `auth/`, `grammar/`, `learning/`, `admin/`, `pages/` for HTML content pages, and `api/` for server endpoints). Global styles (`globals.css`) and the root `layout.js` live here.
@@ -11,7 +29,7 @@ This directory contains the application source code organized by responsibility.
- `middleware/` (`withAuth`, `withInputValidation` and their tests, middleware README)
- `test/` backend-specific helpers/tests
- `components/` – Reusable UI components. Currently organized under `components/ui/`.
- `context/` – React context providers and related logic (e.g. user and glossary contexts). Contains its own README.
- `context/` – React context providers and related logic (e.g. user and glossary contexts). See [APP_CONTEXTS.md](./context/APP_CONTEXTS.md) for details.
- `shared/` – Common code shared across client and server: constants, hooks, schemas (Zod), styles, small utilities.
- `styles/` – Global or feature-scoped standalone CSS not colocated with components (e.g. `anchoredExercises.css`).
- `middleware.js` – Global Next.js middleware controlling auth-based route access and redirects.
@@ -32,8 +50,15 @@ This directory contains the application source code organized by responsibility.
## Testing Notes
- Middleware and validation have dedicated tests under `backend/middleware/`.
- Backend helpers use test flags (`isTest`) allowing session and DB swapping.
### Testing
- Unit tests: Vitest (V8 coverage). Run with `npm test` from the repository root or use the scripts documented in `gradesa/NEXTJS_APP_GUIDE.md`.
- E2E tests: Playwright — see `gradesa/playwright/e2e/E2E_TESTS.md` for how to run and author tests.
Test helpers and conventions (summary):
- Tests should be colocated with source where practical; see existing patterns in `components/`, `backend/`, and `shared/`.
- Backend helpers expose test flags (e.g. `isTest`) to make session/DB swapping easier in tests.
## Conventions
@@ -43,4 +68,4 @@ This directory contains the application source code organized by responsibility.
---
For deeper details on middleware wrappers, see `backend/middleware/README.md`. For auth/session details, consult JSDoc in `backend/auth/session.js` and `backend/auth/hash.js`.
For deeper details on middleware wrappers, see [BACKEND_MIDDLEWARE.md](./backend/middleware/BACKEND_MIDDLEWARE.md). For auth/session details, consult JSDoc in `backend/auth/session.js` and `backend/auth/hash.js`.
@@ -1,58 +1,49 @@
# Middleware Documentation
# Backend middleware (API helpers)
This document provides an overview and usage examples for the middleware functions in the application.
Last updated: November 30, 2025
## Table of Contents
This document summarizes the small set of middleware wrappers used by Next.js API route handlers. For implementation, see the source files in `gradesa/src/backend/middleware/`.
1. [`withAuth`](#withAuth)
- [Features](#features)
- [Parameters](#parameters)
- [Usage](#usage)
- [Example Use-Cases](#example-use-cases)
Key wrappers
## `withAuth`
- `withAuth` — enforces authentication (optionally admin-only) and attaches `request.user`.
- `withInputValidation` — validates request bodies using Zod schemas and attaches the parsed data to `request.body`.
The `withAuth` middleware provides authentication and authorization for API route handlers. It wraps route handlers to check if the user has an active session and optionally verifies admin privileges.
Usage highlights
### Features
- Verifies user authentication status
- Optionally enforces admin-only access
- Attaches the user object to the request for use in the handler
- Returns appropriate error responses for unauthorized requests
### Parameters
- `callback`: The route handler function to be wrapped
- `options`: Configuration object with the following properties:
- `requireAuth`: Boolean (default: `true`) - Whether authentication is required
- `requireAdmin`: Boolean (default: `false`) - Whether admin privileges are required
### Usage
import { NextResponse } from "next/server";
import { withAuth } from "@/backend/middleware/withAuth";
- Compose wrappers for common patterns. Example (authentication outside validation):
```js
// Protected GET endpoint example
export const GET = withAuth(async (request, { params }) => {
// Retrieve the authenticated user added to the request by withAuth
const user = request.user;
return NextResponse.json({
message: "Protected GET route accessed successfully",
user,
});
});
// Admin-only POST endpoint example
export const POST = withAuth(
async (request) => {
// Implement admin-specific logic here.
return NextResponse.json({
message: "Admin-only route accessed successfully",
});
},
{ requireAdmin: true }
// PUT endpoint: auth + validation
export const PUT = withAuth(
withInputValidation(async (req) => {
const user = req.user;
const data = req.body;
// ...
}, updateProfileSchema)
);
```
- `withAuth` options: pass `{ isAdmin: true }` to require admin privileges.
Error responses
- `withAuth` returns `401` for missing/invalid session and `403` for insufficient privileges.
- `withInputValidation` returns `400` with a `details` array (Zod-style errors) for invalid input.
Testing
- Middleware tests live with other backend tests under `gradesa/src/backend/test/` (search for `withAuth` and `withInputValidation` tests).
- To run the app's unit tests from the repo root:
```bash
cd /host/gradesa
npm test
```
Notes
- Keep middleware small and focused; prefer composition over duplication.
- For handler-specific behavior (optional auth), inspect `request.user` directly in the handler instead of using `withAuth`.
See also: `gradesa/README.md` (root onboarding), and `gradesa/src/SOURCE_OVERVIEW.md` for a high-level map of source folders.
+1 -1
View File
@@ -4,7 +4,7 @@ import { checkSession } from "@/backend/auth/session";
/**
* Wraps a Next.js Route Handler with authentication/authorization checks.
*
* Usage examples appear in `src/backend/middleware/README.md` and various
* Usage examples appear in `src/backend/middleware/BACKEND_MIDDLEWARE.md` and various
* API routes (e.g., `src/app/api/.../route.js`).
*
* Behavior:
@@ -1,53 +1,100 @@
# Layout Components Documentation
# Layout Components
This document provides an overview and usage examples for the layout components: Container, Row, and Column.
_Last Updated: November 30, 2025_
This document provides an overview and usage examples for the layout components: `Container`, `Row`, and `Column`.
## Overview
These components provide a flexible, responsive layout system built on CSS Flexbox/Grid with shorthand props and breakpoint utilities.
**Import from:**
```javascript
import { Container, Row, Column } from "@/components/ui/layout/container";
```
## Container Component
The `Container` component is a flexible container that accepts a range of layout properties. It converts prop aliases (such as `p` for padding, `w` for width, etc.) into corresponding CSS styles applied inline. It also supports responsive styling using both array and object syntaxes for breakpoints.
The `Container` component is a flexible container that accepts a range of layout properties. It converts prop aliases (such as `p` for padding, `w` for width, etc.) into corresponding CSS styles applied inline.
### Key Features
- Maps shorthand layout props to standard CSS properties.
- Supports responsive design via:
- **Array Syntax:** The first element is applied as the base style, and additional elements correspond to increasing breakpoints.
- **Object Syntax:** Requires a `base` key, with other keys representing designated breakpoints (e.g., `sm`, `md`, `lg`, etc.).
- Aggregates media query styles dynamically and merges them into the inline style object.
- **Shorthand Props**: Maps aliases to CSS properties (e.g., `p` → `padding`, `w` → `width`).
- **Responsive Design**: Supports array and object syntax for breakpoint-based styling.
- **CSS Variable Integration**: Predefined size tokens (`xs`, `sm`, `md`, etc.) map to CSS variables.
- **Polymorphic**: Renders as different HTML elements via `as` prop (defaults to `div`).
- **Dynamic Styles**: Generates inline styles and media queries at runtime.
### Example Usage (Container)
### Responsive Breakpoints
Basic usage:
| Breakpoint | Min Width | Typical Device |
| ---------- | --------- | ------------------------ |
| `xs` | 480px | Small phones (landscape) |
| `sm` | 576px | Large phones |
| `md` | 768px | Tablets |
| `lg` | 1080px | Laptops |
| `xl` | 1280px | Desktops |
| `2xl` | 1536px | Large desktops |
| `3xl` | 1920px | Ultra-wide displays |
### Responsive Syntax
#### Array Syntax
Values map to breakpoints in order: base, then `xs`, `sm`, `md`, etc.
```javascript
// First value = base (mobile-first), subsequent = breakpoints
<Container p={["8px", "12px", "16px", "20px"]}>
{/* Base: 8px, xs: 12px, sm: 16px, md: 20px */}
</Container>
```
#### Object Syntax
Explicit breakpoint keys. Requires `base` key.
```javascript
<Container p={{ base: "8px", md: "16px", xl: "24px" }}>
{/* Base: 8px, md: 16px, xl: 24px */}
</Container>
```
### Example Usage
#### Basic Container
```jsx
import { Container } from "./container";
import { Container } from "@/components/ui/layout/container";
function BasicExample() {
return (
<Container p="10px" w="200px" h="100px">
<Container p="10px" w="200px" h="100px" bg="#eef2ff">
Basic Container
</Container>
);
}
```
Responsive usage with Array syntax:
#### Responsive Array Syntax
```jsx
import { Container } from "./container";
import { Container } from "@/components/ui/layout/container";
function ResponsiveArrayExample() {
return (
<Container p={["8px", "12px", "16px"]}>
<Container p={["8px", "12px", "16px"]} w={["100%", "80%", "60%"]}>
Responsive Container (Array Syntax)
</Container>
);
}
```
Responsive usage with Object syntax:
#### Responsive Object Syntax
```jsx
import { Container } from "./container";
import { Container } from "@/components/ui/layout/container";
function ResponsiveObjectExample() {
return (
@@ -58,23 +105,46 @@ function ResponsiveObjectExample() {
}
```
#### Using CSS Variable Sizes
```jsx
import { Container } from "@/components/ui/layout/container";
function CSSVariableExample() {
return (
<Container fontSize="md" p="lg" m="sm" br="md">
Using CSS variable sizes for consistent theming
</Container>
);
}
```
### Polymorphic `as` Prop
Render different semantic elements:
```jsx
<Container as="section" p="20px">Renders as section</Container>
<Container as="article" p="10px">Renders as article</Container>
```
## Row and Column Components
The `Row` and `Column` components extend the `Container` to simplify common flexbox layout patterns.
`Row` and `Column` extend `Container` with preset flex direction and helpful defaults.
### Row Component
The `Row` component renders a flex container with `flexDirection` set to `row`. It accepts additional layout props like padding, margin, and alignment.
Example:
Horizontal flex container (`flexDirection: row`).
```jsx
import { Row } from "./container";
import { Row } from "@/components/ui/layout/container";
function RowExample() {
return (
<Row p="20px" justify="center">
Row Content
<Row p="20px" justify="space-between" align="center" gap="10px">
<div>Left</div>
<div>Center</div>
<div>Right</div>
</Row>
);
}
@@ -82,46 +152,98 @@ function RowExample() {
### Column Component
The `Column` component renders a flex container with `flexDirection` set to `column`. It supports the same props as `Container` for flexible layout management.
Example:
Vertical flex container (`flexDirection: column`).
```jsx
import { Column } from "./container";
import { Column } from "@/components/ui/layout/container";
function ColumnExample() {
return (
<Column p="15px" align="center">
Column Content
<Column p="15px" align="center" gap="10px">
<div>Top</div>
<div>Middle</div>
<div>Bottom</div>
</Column>
);
}
```
### CSS Variable Sizes and Responsive Values
## Prop Reference
The Container component supports both raw CSS values (e.g., "10px", "1rem") and predefined CSS variable sizes for consistent styling. When you supply a size keyword (one of: xs, sm, md, lg, xl, 2xl, 3xl), it is converted into a corresponding CSS variable according to the mappings defined in container.jsx and your global CSS.
Complete list of commonly used props and their CSS mappings.
CSS variable resolution works as follows:
### Layout Props
• For the fontSize prop:
| Prop | CSS Property | Example |
| ---------- | ------------ | --------------------- |
| `position` | `position` | `position="relative"` |
| `display` | `display` | `display="flex"` |
| `overflow` | `overflow` | `overflow="hidden"` |
- A size such as "sm" is transformed to "var(--font-sm)".
### Flexbox Props
• For spacing and sizing props (e.g., padding (p), width (w), height (h), gap, etc.):
| Prop | CSS Property | Example |
| ----------- | ---------------- | -------------------- |
| `direction` | `flexDirection` | `direction="column"` |
| `justify` | `justifyContent` | `justify="center"` |
| `align` | `alignItems` | `align="center"` |
| `wrap` | `flexWrap` | `wrap="wrap"` |
| `gap` | `gap` | `gap="10px"` |
- Sizes are mapped to "var(--u-[size])". For example, p="lg" becomes "var(--u-lg)".
### Spacing & Sizing
• For margin (m) and borderRadius (br or r):
| Prop | CSS Property | Example |
| ------------------------------ | ------------ | ---------------------- |
| `p`, `pt`, `pb`, `pl`, `pr` | padding\* | `p="10px"` or `p="lg"` |
| `m`, `mt`, `mb`, `ml`, `mr` | margin\* | `m="1rem"` |
| `w` | width | `w="100%"` |
| `h` | height | `h="50px"` |
| `minW`, `maxW`, `minH`, `maxH` | sizing | `minW="200px"` |
- Sizes are resolved to "var(--radius-[size])".
(\* accepts raw values or CSS variable tokens like `sm`, `md`, `lg`)
The component also supports responsive values using two syntaxes:
### Border & Appearance
Using CSS variable sizes ensures consistency in spacing, sizing, and typography across your UI. For example:
| Prop | CSS Property | Example |
| ---------- | -------------- | -------------------- |
| `b` | `border` | `b="1px solid #000"` |
| `br` / `r` | `borderRadius` | `br="md"` |
| `bg` | `background` | `bg="#fff"` |
| `color` | `color` | `color="#333"` |
| `fontSize` | `fontSize` | `fontSize="md"` |
<Container fontSize="md" p="lg">
Using CSS variable sizes for consistent theming
</Container>
## CSS Variable Sizes
Refer to container.jsx and globals.css for the complete details on how size keywords are mapped to CSS variables.
Predefined tokens map to CSS variables declared in `globals.css`.
Examples:
```javascript
fontSize = "md"; // → var(--font-md)
p = "lg"; // → var(--u-lg)
br = "sm"; // → var(--radius-sm)
```
## Accessibility
Use semantic `as` prop and proper ARIA attributes for interactive containers.
```jsx
<Container as="nav">Navigation</Container>
<Container as="button" role="button" aria-label="Close">X</Container>
```
## Best Practices
1. Use semantic HTML via the `as` prop where appropriate.
2. Prefer mobile-first responsive styles (use base then breakpoints).
3. Use CSS variable tokens for consistent spacing and typography.
4. Avoid over-nesting; flatten layout when possible.
5. Use object syntax for explicit breakpoint rules in complex layouts.
## Performance Notes
- The `Container` implementation uses `memo()` and `useMemo()` to avoid unnecessary recalculation of inline styles.
- Avoid passing new object/array references on every render for responsive props.
Refer to the implementation in the layout source (e.g. `components/ui/layout/container.*`) for the exact prop registry and breakpoint values.
+201 -22
View File
@@ -1,36 +1,76 @@
# Context
# Application Contexts
This project uses React Context to manage global state across the application. Below is an overview of the available contexts and their usage.
_Last Updated: November 30, 2025_
This project uses React Context to manage global state across the application. Two main contexts are provided:
- **`UserProvider`** - Authentication state and user information
- **`GlossaryProvider`** - Glossary data with word lookup functionality
Both providers are configured in the root layout and available throughout the application.
## Table of Contents
1. [`userContext`](#userContext)
1. [`userContext`](#usercontext)
- [Features](#features)
- [Usage](#usage)
- [Example Page](#example-page)
- [Example Use-Cases](#example-use-cases)
- [Available Hooks](#available-hooks)
- [User Object Structure](#user-object-structure)
- [Usage Examples](#usage-examples)
- [Notes](#notes)
2. [`glossaryContext`](#glossarycontext)
- [Features](#features-1)
- [Available Hook](#available-hook)
- [Usage Examples](#usage-examples-1)
- [Notes](#notes-1)
3. [Testing with Contexts](#testing-with-contexts)
## `userContext`
The `userContext` manages the user's authentication state. It determines whether the user has an active session and allows conditional rendering of content based on the user's login status.
The `userContext` manages the user's authentication state and user information. It provides authentication status, user details, and logout functionality.
### Features
- Tracks whether the user is logged in with `isLoggedIn`.
- Provides the utility function `logout` for logging out the user.
- Tracks authentication state with `isLoggedIn` boolean
- Provides complete user object with profile information
- Offers `logout` function for ending sessions
- Automatically fetches session on mount
- Handles loading states during session checks
### Usage
### Available Hooks
To add authentication-dependent content to a page, follow these steps:
The `userContext` provides multiple hooks for accessing different parts of the state:
1. Import useUser on the page with `import { useIsLoggedIn } from "@/context/user.context";`
2. Define isLoggedIn constant in the page export function: `const isLoggedIn = useIsLoggedIn()`
3. Insert `{isLoggedIn && ()}` in the return statement with protected content inside the parantheses.
#### Example Page:
```javascript
import {
useUser, // Full user object and logout function
useIsLoggedIn, // Only authentication status boolean
} from "@/context/user.context";
// In component:
const { user, logout } = useUser(); // { user: {...}, logout: fn }
const isLoggedIn = useIsLoggedIn(); // boolean
```
### User Object Structure
When authenticated, the `user` object contains (example):
```javascript
{
id: 1,
username: "student123",
isAdmin: false
// Additional fields from the users table
}
```
When not authenticated, `user` is `null`.
### Usage Examples
#### Conditional Rendering Based on Authentication
```javascript
import { useIsLoggedIn } from "@/context/user.context";
export default function Page() {
@@ -39,23 +79,162 @@ export default function Page() {
return (
<div>
{/* Public content */}
<h1>Welcome</h1>
{/* Content for authenticated users */}
{isLoggedIn && (
<div>
<p>Content visible to logged-in users only</p>
</div>
)}
{/* Public content */}
{/* Content for non-authenticated users */}
{!isLoggedIn && (
<div>
<p>Please log in to access exercises</p>
</div>
)}
</div>
);
}
```
### Example Use-cases
#### Using User Information and Logout
- **Logged-In Users**: Display protected content to logged-in users, such as exercises.
- **Logged-Out Users**: Show public content, or use `{isLoggedIn === false && ()}` to show exclusive content to logged-out users, such as a prompt to log in.
```javascript
import { useUser } from "@/context/user.context";
export default function ProfilePage() {
const { user, logout } = useUser();
if (!user) {
return <p>Please log in</p>;
}
const handleLogout = async () => {
await logout();
// User is logged out, context updates automatically
};
return (
<div>
<h1>Profile</h1>
<p>Username: {user.username}</p>
{user.isAdmin && <p>Admin privileges active</p>}
<button onClick={handleLogout}>Log out</button>
</div>
);
}
```
#### Use Cases
- **Logged-In Users**: Display protected content like exercises, profile, admin panels
- **Logged-Out Users**: Show login prompts, public content, registration links
- **Admin Users**: Conditionally render admin-only features using `user.isAdmin`
### Notes
- The `UserProvider` is already configured in the `RootLayout` in `app/layout.js`, so `userContext` is available throughout the application. You can use the `useUser` hook in any component or page without additional setup.
- The `userContext` interacts with the `/api/auth/session` endpoint to determine the user's session status and the `/api/auth/logout` endpoint to end the active session.
- The `UserProvider` is configured in the root layout (`app/layout.js`), so the context is available app-wide.
- The context fetches session data from `/api/auth/session` and calls `/api/auth/logout` to end sessions.
- Session checks run on mount; prefer hooks (`useUser`, `useIsLoggedIn`) to access the state.
## `glossaryContext`
The `glossaryContext` provides access to the application's glossary data, including word definitions and lookup functionality.
### Features
- Fetches and caches glossary entries on mount
- Provides word lookup by entry or inflected form
- Handles loading and error states
- Exposes raw entries array and optimized word map
### Available Hook
```javascript
import { useGlossary } from "@/context/glossary.context";
// In component:
const { entries, wordMap, isLoading, error } = useGlossary();
```
**Returns:**
- `entries` - Array of all glossary entries
- `wordMap` - Map for fast lookup: `{ word: entryId }`
- `isLoading` - Boolean indicating if data is being fetched
- `error` - Error object if fetch failed, null otherwise
### Usage Examples
#### Basic Glossary Display
```javascript
import { useGlossary } from "@/context/glossary.context";
export default function GlossaryPage() {
const { entries, isLoading, error } = useGlossary();
if (isLoading) return <p>Loading glossary...</p>;
if (error) return <p>Failed to load glossary</p>;
return (
<ul>
{entries.map((entry) => (
<li key={entry.id}>
<strong>{entry.word}</strong>: {entry.definition}
</li>
))}
</ul>
);
}
```
### Notes
- The `GlossaryProvider` is configured in the root layout and available throughout the application.
- Glossary data is fetched from `/api/glossary` on mount.
- The `wordMap` enables O(1) lookup for both base forms and inflected forms of words.
- Use `isLoading` to show loading states and prevent rendering before data is available.
## Testing with Contexts
When testing components that use contexts, wrap them in the appropriate provider:
```javascript
import { render } from "@testing-library/react";
import { UserProvider } from "@/context/user.context";
import { GlossaryProvider } from "@/context/glossary.context";
import MyComponent from "./MyComponent";
test("renders with context", () => {
render(
<UserProvider>
<GlossaryProvider>
<MyComponent />
</GlossaryProvider>
</UserProvider>
);
// Test assertions...
});
```
### Mocking Context Values
For unit tests, mock the context hooks:
```javascript
import { vi } from "vitest";
import * as userContext from "@/context/user.context";
vi.spyOn(userContext, "useUser").mockReturnValue({
user: { id: 1, username: "testuser", isAdmin: false },
logout: vi.fn(),
});
vi.spyOn(userContext, "useIsLoggedIn").mockReturnValue(true);
```
This approach isolates component logic from context implementation.
+119 -15
View File
@@ -1,8 +1,17 @@
# Hooks Documentation
# Data Fetching Hooks
These are some simple request hooks that help with making network requests to API routes.
_Last Updated: November 30, 2025_
They are very simple, no caching etc. I highly recommend you refactor gradesa to use a proper fetching library like react-query. The first team was pretty inexperienced with JS so we used this to limit the amount of new stuff we had to learn.
## Recommendation: Consider React Query for new work
The hooks in this folder provide compact, easy-to-read utilities for data fetching, but they lack advanced features (robust caching, cache invalidation, complex retries). For new features or larger refactors, consider [TanStack Query (React Query)](https://tanstack.com/query) to get better caching, devtools, and mutation handling.
## Overview
This directory contains two custom hooks for making network requests to API routes:
- **`useQuery`** - GET requests with loading states and retries
- **`useRequest`** - POST/PUT/DELETE requests with flexible configuration
## useQuery
@@ -12,10 +21,10 @@ They are very simple, no caching etc. I highly recommend you refactor gradesa to
- Automatic GET requests with query string parameter handling
- Loading state management
- Basic error handling
- Automatic retries with exponential backoff
- Request cancellation on component unmount
- very simplistic Session ID tracking
- Basic error handling with structured error objects
- Automatic retries with exponential backoff on failure
- Request cancellation on component unmount (prevents memory leaks)
- Session ID tracking via custom header (`X-Session-Id`) for request tracing
### Usage
@@ -50,6 +59,14 @@ function MyComponent() {
}
```
### Error handling
The `error` object is a structured object (e.g. `{ message, status, data }`). Use `error.message` for user-friendly messages and inspect `error.status`/`error.data` for debugging.
### Session ID tracking
Both hooks attach an `X-Session-Id` header (generated once per page load and stored in `sessionStorage`) to help correlate frontend requests with backend logs. This is added in the shared Axios instance via an interceptor.
### Parameters
- `url` (string): The API endpoint to fetch data from
@@ -70,14 +87,15 @@ function MyComponent() {
## useRequest
`useRequest` is a flexible hook for making non-GET HTTP requests (POST, PUT, DELETE, etc.) to the API.
`useRequest` is a flexible hook for making non-GET HTTP requests (POST, PUT, DELETE, etc.) to the API. Returns a function that can be called multiple times.
### Features
- Support for all HTTP methods
- Customizable request configuration
- Error handling
- Session ID tracking
- Support for all HTTP methods (POST, PUT, DELETE, PATCH)
- Customizable request configuration (headers, auth, etc.)
- Structured error handling with status codes
- Session ID tracking (same as `useQuery`)
- Promise-based API for async/await usage
### Usage
@@ -116,13 +134,99 @@ function MyComponent() {
### Parameters
- The hook returns a `makeRequest` function that accepts:
- `url` (string): The API endpoint
- `body` (object, optional): The request body/payload
- `url` (string): The API endpoint (relative or absolute)
- `body` (object, optional): The request body/payload (will be JSON stringified)
- `config` (object, optional): Axios request configuration
- `method` (string): HTTP method (default: 'POST')
- `headers` (object): Custom request headers
- `timeout` (number): Request timeout in milliseconds
- Any other valid Axios request options
### Return Values
- The function returns a Promise that resolves to the Axios response object
- Returns a Promise that resolves to the Axios response object:
- `response.data` - Response body
- `response.status` - HTTP status code
- `response.headers` - Response headers
### Abort Controller Example
For long-running requests, you can implement cancellation:
```jsx
import { useRequest } from "@/shared/hooks/useRequest";
import { useEffect, useRef } from "react";
function MyComponent() {
const makeRequest = useRequest();
const abortControllerRef = useRef(null);
const handleSubmit = async (formData) => {
// Cancel previous request if still running
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
try {
const response = await makeRequest("/api/process", formData, {
method: "POST",
signal: abortControllerRef.current.signal,
});
console.log("Success:", response.data);
} catch (error) {
if (error.name === "AbortError") {
console.log("Request cancelled");
} else {
console.error("Error:", error.message);
}
}
};
useEffect(() => {
// Cleanup: cancel request on unmount
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
return <button onClick={handleSubmit}>Submit</button>;
}
```
## Implementation Details
### Axios Configuration
Both hooks use a shared Axios instance configured in `axios-config.js`:
- **Base URL**: Automatically uses the current domain (no hardcoded URLs)
- **Content-Type**: Set to `application/json` by default
- **Interceptors**: Request interceptor adds `X-Session-Id` header
- **Error handling**: Response interceptor structures error objects
### Base URL Behavior
```javascript
// Relative URLs resolve to current domain:
await makeRequest("/api/users", data); // → https://current-domain.com/api/users
// Absolute URLs work as expected:
await makeRequest("https://external-api.com/data", data); // → https://external-api.com/data
```
## Migration note
If you decide to migrate to React Query, focus on replacing `useQuery` call-sites with `useQuery`/`useMutation` from `@tanstack/react-query` and centralizing side-effects through query keys and invalidations. The hooks here show the minimal surface you'll need to map over.
## Best Practices
1. **Use `useQuery` for GET requests**: It handles loading states and retries automatically
2. **Use `useRequest` for mutations**: POST/PUT/DELETE operations that change data
3. **Handle errors explicitly**: Always check for `error` before rendering data
4. **Set `enabled: false` for conditional queries**: Prevents automatic fetching
5. **Cancel requests on unmount**: Already handled by `useQuery`, implement manually for `useRequest` if needed
6. **Consider React Query for new features**: Better caching, invalidation, and state management