From fe990b4004995c0c74b9d93384bcbbdef59ed0c6 Mon Sep 17 00:00:00 2001 From: Mathea Liv Date: Fri, 30 Oct 2026 15:15:27 +0100 Subject: [PATCH 1/2] [docs] oppdaterte readme og la til kommentarer i andre filer ved behov --- README.md | 83 +++++++------------------- backend/src/checkDb.ts | 1 + backend/src/db.ts | 1 + backend/src/schema.graphql | 4 ++ backend/src/seed.ts | 1 + backend/src/server.ts | 7 +++ src/App.css | 1 + src/App.tsx | 7 +++ src/components/CountryCard.css | 1 + src/components/CountryCard.tsx | 3 + src/components/CountryList.css | 1 + src/components/CountryList.tsx | 3 + src/components/CountryStatusPicker.css | 1 + src/components/CountryStatusPicker.tsx | 3 + src/components/LoadMore.css | 1 + src/components/LoadMore.tsx | 5 ++ src/components/QuizHistory.css | 1 + src/components/QuizHistory.tsx | 17 ++++++ src/components/SearchBar.tsx | 4 ++ src/graphql/countries.ts | 7 +++ src/hooks/useDebounce.ts | 1 + src/hooks/useLoadMore.ts | 4 +- src/index.css | 1 + src/lib/answer.ts | 4 +- src/lib/apollo.ts | 8 ++- src/lib/continents.ts | 1 + src/lib/quizHistory.ts | 3 +- src/lib/userId.ts | 1 + src/main.tsx | 1 + src/pages/CountryDetailPage.css | 1 + src/pages/CountryDetailPage.tsx | 3 + src/pages/QuizPage.css | 1 + src/pages/QuizPage.tsx | 7 +++ src/pages/QuizResultsPage.css | 1 + src/pages/QuizResultsPage.tsx | 2 + src/types/city.ts | 1 + src/types/country.ts | 1 + 37 files changed, 126 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 76f82bc..9047584 100644 --- a/README.md +++ b/README.md @@ -18,34 +18,24 @@ Users can mark countries as visited or want to visit. The data is stored in the ## Implemented so far: -- Country list using data fetched from the GraphQL backend. -- Country detail page using data fetched from the GraphQL backend. +- Country list and country detail page using data fetched from the GraphQL backend. - Responsive CountryQuest landing page with separate search, quiz, and results views, plus navigation to visited, wish-list, and unexplored countries. -- Search functionality for countries and cities, with case-insensitive searching performed in the PostgreSQL database. +- Search for countries and cities, with case-insensitive searching performed in the PostgreSQL database. - Debounced search input (300 ms) to reduce unnecessary GraphQL requests. -- Loading, error, and empty-result states for country searches. -- Apollo Client integration for GraphQL queries and client-side caching. -- GraphQL resolvers for countries, individual countries, cities, and individual cities. -- Apollo Server with resolvers that retrieve data from PostgreSQL. -- Backend support for country search, continent filtering, and pagination. -- PostgreSQL database hosted on the group's NTNU VM, populated with country and city data. -- Backend deployed on the NTNU VM as a systemd service, allowing it to continue running after SSH logout and restart automatically if the process fails. -- Pagination of country search results using a reusable "Load more" component, fetching results in batches of 20. -- Apollo Client cache policies that merge paginated results and reuse previously fetched pages for the same search and filters. - -## Planned Functionality - -- **Result list**: Further improvements to result loading and pagination as needed when working with larger datasets. - -- **Sorting and filtering**: Sorting by name/population and filtering by continent/language and Visited/Want to visit. These operations will be performed on the entire dataset in the backend, rather than only on the data loaded into the client. - -- **User-generated data**: The ability to mark countries as Visited/Want to visit, stored persistently in the database. - +- Backend filtering by continent, language, visited status, and unexplored status, with sorting by name, population, or area. +- Country and city pagination handled by the backend; results are loaded in batches of 20. +- Users can mark countries as visited or want to visit. Statuses are stored persistently and can be changed or removed. +- Quiz attempts are saved in the browser's IndexedDB and can be viewed on the quiz results page. +- Loading, error, and empty-result states for searches and data updates. +- Apollo Client integration and cache policies for GraphQL queries and paginated results. +- PostgreSQL database and GraphQL backend hosted on the group's NTNU VM; the backend runs as a systemd service. - **Accessibility and sustainable web development**: Semantic HTML, ARIA where necessary, and keyboard navigation. Conscious choices regarding data usage, caching, and performance to reduce unnecessary resource consumption. See our [Accessibility and Sustainability Plan](docs/planAccessibilityAndSustainability.md). -- **Responsive design**: Continued improvements for both mobile and desktop. +## User data and anonymous browser ID + +The app creates a random ID for each browser and stores it in that browser's local storage. The ID is sent with requests so the backend can associate visited and want-to-visit statuses with the same browser without requiring account creation, passwords, or collecting account details. This keeps the feature simple and avoids introducing a login system for a project that does not need cross-device accounts. -- See our [design mockups](docs/design.md). +The data is tied to the browser that created it. It is not shared across browsers or devices, and clearing that browser's site data removes the ID needed to look up its saved statuses. ## Technology @@ -54,7 +44,7 @@ Users can mark countries as visited or want to visit. The data is stored in the - **Backend**: GraphQL (Node.js/TypeScript) - **Database**: PostgreSQL, installed directly on the group's VM - **Data**: countries and cities from open datasets (see [Data](#data)) -- **Testing**: Vitest (coming) +- **Testing**: Vitest - **Linting/formatting**: ESLint and Prettier ## Architecture and Scalability @@ -69,13 +59,13 @@ The backend and database are hosted on the group's NTNU VM. The backend runs as To support larger datasets, searching, filtering, and pagination are handled by the backend and database rather than requiring the frontend to load all records. The GraphQL API allows the frontend to request only the data needed for each view. -The dataset currently contains approximately 250 countries and 34,155 cities. Including city data provides a larger dataset for evaluating database queries and scalability. +The city dataset provides a larger set of records for evaluating database queries and scalability. For deployment details, see [Backend Deployment](docs/backendDeployment.md). ## Pagination and Caching -Country results are fetched in batches of 20 using offset-based pagination. Apollo Client merges the fetched pages into a cached list for each search and continent filter combination. +Country and city results are fetched in batches of 20 using offset-based pagination. Apollo Client merges the fetched pages into a cached list for each search and filter combination. Previously fetched results are reused when returning to a search with the same parameters. For example, if 60 countries have been loaded for a search and the user switches to another search before returning, the previously cached results may be displayed immediately. @@ -90,20 +80,16 @@ This is expected Apollo Client cache behavior and helps avoid unnecessary networ To run the frontend using the deployed backend, no direct database access or SSH access to the VM is required. -To run the backend locally, access to the PostgreSQL database and SSH access to the group's VM may be required. See [Database Setup](docs/databaseSetup.md) for instructions. - -### 1. Start the Frontend - -Open a terminal in the repository root and install the dependencies: +### Start the frontend ```bash -npm install +npm ci ``` -Connect to the NTNU network or VPN and create a `.env.local` file in the repository root with the following content: +Connect to the NTNU network or VPN and create a `.env.local` file in the repository root: ```env -VITE_GRAPHQL_URL=http://it2810-28.idi.ntnu.no:3001/ +VITE_GRAPHQL_URL=http://it2810-28.idi.ntnu.no:3001/graphql ``` Start the frontend: @@ -112,32 +98,9 @@ Start the frontend: npm run dev ``` -Open [http://localhost:5173/](http://localhost:5173/) in your browser. - -The frontend communicates with the GraphQL backend hosted on the group's NTNU VM. You do not need to start the backend locally. - -### 2. Run the Backend Locally (Optional) - -If you need to develop or test the backend locally, first follow the instructions in [Database Setup](docs/databaseSetup.md) to configure `backend/.env` and establish an SSH tunnel if necessary. - -Open a terminal in the repository root and run: - -```bash -cd backend -npm install -npm run db:check -npm run dev -``` - -The GraphQL server runs at [http://localhost:3001/](http://localhost:3001/). - -To connect the frontend to your local backend instead of the deployed backend, update `.env.local` in the repository root: - -```env -VITE_GRAPHQL_URL=http://localhost:3001/ -``` +Open [http://localhost:5173/](http://localhost:5173/) in your browser. The frontend uses the deployed backend, so you do not need database credentials or SSH access to the VM. -Keep the backend running in a separate terminal while developing. +To develop the backend locally, see [Database Setup](docs/databaseSetup.md) for the required database configuration and SSH tunnel instructions. ### Backend Deployment (NTNU VM) @@ -204,7 +167,7 @@ Examples include working with React and TypeScript, understanding GraphQL querie AI has also been used to help troubleshoot backend deployment on the NTNU VM, configure the systemd service, resolve Git-related issues, and improve technical documentation, including the README. -Using AI to explain concepts and provide feedback on our own code has helped us better understand the technologies used in the project. It has also made debugging and troubleshooting more efficient, particularly when working with unfamiliar tools and configurations. +Using AI to explain concepts and provide feedback on our own code has helped us better understand the technologies used in this project. It has also made debugging and troubleshooting more efficient, particularly when working with unfamiliar tools and configurations. All AI-generated suggestions have been reviewed and evaluated before being applied, and the group remains responsible for the final implementation. diff --git a/backend/src/checkDb.ts b/backend/src/checkDb.ts index f05c545..6be1637 100644 --- a/backend/src/checkDb.ts +++ b/backend/src/checkDb.ts @@ -1,3 +1,4 @@ +/** Standalone connectivity check for the PostgreSQL settings in the backend environment. */ import pg from 'pg'; const pool = new pg.Pool({ diff --git a/backend/src/db.ts b/backend/src/db.ts index c6ac3de..dc5de47 100644 --- a/backend/src/db.ts +++ b/backend/src/db.ts @@ -1,3 +1,4 @@ +/** Shared PostgreSQL connection pool used by the GraphQL server. */ import pg from 'pg'; export const pool = new pg.Pool({ diff --git a/backend/src/schema.graphql b/backend/src/schema.graphql index 8696da6..7467a97 100644 --- a/backend/src/schema.graphql +++ b/backend/src/schema.graphql @@ -1,3 +1,4 @@ +# GraphQL API contract for country and city data, user statuses, and quiz selection. enum CountryStatus { VISITED WANT_TO_VISIT @@ -28,6 +29,7 @@ type Country { population: Int area: Float flagUrl: String! + # Null for anonymous users with no saved status for this country. status: CountryStatus } @@ -40,6 +42,7 @@ type City { population: Int! } +# Page types expose a slice in items and a totalCount across active filters. type CountryPage { items: [Country!]! totalCount: Int! @@ -52,6 +55,7 @@ type CityPage { type Query { country(code: ID!): Country + # limit/offset select a page; country status filters require the anonymous browser ID. countries( search: String continent: String diff --git a/backend/src/seed.ts b/backend/src/seed.ts index c2e9fd6..5e02e2d 100644 --- a/backend/src/seed.ts +++ b/backend/src/seed.ts @@ -8,6 +8,7 @@ * - user_country is never touched, so user data survives a re-seed * * Run from backend/: npm run db:seed + * Country and city imports share one transaction so partial data refreshes are not exposed. */ import { readFile } from 'node:fs/promises'; import path from 'node:path'; diff --git a/backend/src/server.ts b/backend/src/server.ts index c2f7b35..b4b21e9 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -1,3 +1,4 @@ +/** Starts the GraphQL API and attaches the validated anonymous browser ID to each request. */ import { readFile } from 'node:fs/promises'; import { ApolloServer } from '@apollo/server'; import { startStandaloneServer } from '@apollo/server/standalone'; @@ -16,6 +17,7 @@ const CITY_COLUMNS = ` id, name, latitude, longitude, population, country_code `; +// SQL identifiers cannot be parameterized, so enum inputs map only to fixed column names. const COUNTRY_SORT_COLUMNS = new Map([ ['NAME', 'name'], ['POPULATION', 'population'], @@ -58,6 +60,7 @@ const resolvers = { const sortExpression = args.sortBy === 'NAME' ? 'name COLLATE "C"' : sortColumn; const sortOrder = SORT_ORDERS.get(args.sortOrder) ?? 'ASC'; + // Bind search text as a value; ILIKE provides case-insensitive substring matching. const filters = ` WHERE ($1::text IS NULL OR name ILIKE '%' || $1 || '%') AND ($2::text IS NULL OR continent = $2) @@ -110,6 +113,7 @@ const resolvers = { [...filterValues, args.limit ?? 20, args.offset ?? 0] ); + // Count all filtered matches separately so totalCount does not shrink with each page. const countResult = await pool.query(`SELECT count(*) FROM country ${filters}`, filterValues); return { @@ -141,6 +145,7 @@ const resolvers = { const sortColumn = CITY_SORT_COLUMNS.get(args.sortBy) ?? 'population'; const sortOrder = SORT_ORDERS.get(args.sortOrder) ?? 'DESC'; + // ILIKE makes city-name matching case-insensitive; values remain bound parameters. const filters = ` WHERE ($1::text IS NULL OR name ILIKE '%' || $1 || '%') AND ($2::text IS NULL OR country_code = $2) @@ -148,6 +153,7 @@ const resolvers = { const filterValues = [args.search ?? null, args.countryCode ?? null]; + // Apply pagination after filtering/sorting, and keep the total independent of the page. const itemsResult = await pool.query( `SELECT ${CITY_COLUMNS} FROM city @@ -157,6 +163,7 @@ const resolvers = { [...filterValues, args.limit ?? 20, args.offset ?? 0] ); + // Return the filtered total, not just the current LIMIT/OFFSET page. const countResult = await pool.query(`SELECT count(*) FROM city ${filters}`, filterValues); return { diff --git a/src/App.css b/src/App.css index b827ab2..4b8cc7b 100644 --- a/src/App.css +++ b/src/App.css @@ -1,3 +1,4 @@ +/* Global application shell, navigation, hero, filters, and result layout. */ #root { min-height: 100vh; } diff --git a/src/App.tsx b/src/App.tsx index 83ceee7..672d360 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,4 @@ +/** Coordinates app-level views and shared search, filter, and navigation state. */ import { useState } from 'react'; import { useQuery } from '@apollo/client/react'; import languages from './data/languages.json'; @@ -19,6 +20,7 @@ const PAGE_SIZE = 20; type Mode = 'search' | 'quiz'; type Section = 'home' | 'visited' | 'wishlist' | 'unexplored' | 'quizResults'; +/** Inline home glyph used by the persistent top-level navigation. */ function HomeIcon() { return (

Could not load countries: {countriesError.message}

)} {citiesError &&

Could not load cities: {citiesError.message}

} + {/* Keep already displayed results visible during refetches; show loading only before the first page arrives. */} {countriesLoading && !(isUnexplored ? unexploredData : data) && (

Loading countries…

)} diff --git a/src/components/CountryCard.css b/src/components/CountryCard.css index ef19b78..71ddb53 100644 --- a/src/components/CountryCard.css +++ b/src/components/CountryCard.css @@ -1,3 +1,4 @@ +/* Country card layout, status badges, and its quick-action row. */ .country-card { display: flex; flex-direction: column; diff --git a/src/components/CountryCard.tsx b/src/components/CountryCard.tsx index 5285c68..74b4307 100644 --- a/src/components/CountryCard.tsx +++ b/src/components/CountryCard.tsx @@ -9,10 +9,13 @@ const STATUS_LABELS: Record = { }; interface CountryCardProps { + /** Country fields displayed by the card, including its current saved status. */ country: CountrySummary; + /** Opens the selected country's details view. */ onSelect: () => void; } +/** Displays country facts and lets the user toggle visited or want-to-visit status. */ export function CountryCard({ country, onSelect }: CountryCardProps) { const formattedCapital = country.capital.length > 0 ? country.capital.join(', ') : 'None'; const [setStatus, { loading, error }] = useMutation(SET_COUNTRY_STATUS, { diff --git a/src/components/CountryList.css b/src/components/CountryList.css index eb160fb..f25a131 100644 --- a/src/components/CountryList.css +++ b/src/components/CountryList.css @@ -1,3 +1,4 @@ +/* Responsive grid for the semantic list of country cards. */ .country-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); diff --git a/src/components/CountryList.tsx b/src/components/CountryList.tsx index ea05107..e18b052 100644 --- a/src/components/CountryList.tsx +++ b/src/components/CountryList.tsx @@ -3,10 +3,13 @@ import { CountryCard } from './CountryCard'; import './CountryList.css'; interface CountryListProps { + /** Countries to display in the list. */ countries: CountrySummary[]; + /** Called with the country whose details the user selected. */ onSelect: (country: CountrySummary) => void; } +/** Maps country results to cards while keeping list semantics and selection centralized. */ export function CountryList({ countries, onSelect }: CountryListProps) { return (