{data.message}
} + DB TIME: {data &&{data.now}
}diff --git a/README.md b/README.md index 66a27ff..a5d84f4 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,42 @@ -# GRADESA German Grammar Interactive Learning and Exercising Website +# GRADESA German Grammar Interactive Learning and Exercising Website 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. +## Install + +This should be all that's needed: + +- `node` +- `npm` +- https://github.com/peterldowns/pgmigrate + - MAC: Install the newest version with `brew install peterldowns/tap/pgmigrate` + - Linux: # Install pgmigrate + - `wget https://github.com/peterldowns/pgmigrate/releases/download/v0.0.6%2Bcommit.4f90829/pgmigrate-linux-amd64 -O /usr/local/bin/pgmigrate` + - `chmod +x /usr/local/bin/pgmigrate` +- Docker, either of these installation options work (if you're new to docker, I recommend Docker Desktop): + - `brew install docker` + `brew install colima` (https://github.com/abiosoft/colima) + - [Docker Desktop](https://www.docker.com/products/docker-desktop/) +- psql: `brew install postgresql@15` + +## Running locally + +- Run PostgreSQL: + - Init .env files `./init-env.sh` — This will create the required .env files for environment variables. The local db credentials are + - DB_HOST=localhost + - DB_PORT=6543 + - DB_USER=ohtu + - DB_PASSWORD=password + - DB_NAME=gradesa + - `docker-compose up -d` — This command starts the services defined in the docker-compose.yml file in detached mode, meaning the services run in the background and the terminal is freed up for other tasks. For more information, checkout [Docker Compose docs](https://docs.docker.com/compose/) + - `npm run dev` — Starts the dev server. + - You can now find the NextJS app on localhost:3000 + ## Documentation + - [Product and Sprint Backlogs](https://github.com/orgs/OHTU-German-learning-website/projects/3) +- Migrations: check [the migration README](data/README.md) + ## Definition of Done -"*The requirement has been analyzed, designed, programmed, tested, automated testing implemented, documented, integrated into other software, and deployed to the production environment.*" + +"_The requirement has been analyzed, designed, programmed, tested, automated testing implemented, documented, integrated into other software, and deployed to the production environment._" diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..e305f55 --- /dev/null +++ b/data/README.md @@ -0,0 +1,25 @@ +# Database Migration with pgmigrate + +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. + +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. + +## Prerequisites + +Ensure you have the following installed on your system: + +- PostgreSQL +- `pgmigrate` (You can find more information and installation instructions on the [pgmigrate GitHub page](https://github.com/peterldowns/pgmigrate)) + +## Migration Script + +The migration script is located at `data/migrate.sh`. This script uses `pgmigrate` to apply migrations to the database. + +### Running Migrations + +To run the migrations, execute the following command (in root): `npm run db:migrate` + +### Resetting the database + +To reset the database, you can use the following command: `npm run db:reset` +This will clear all tables, and rerun migrations. diff --git a/data/migrate.sh b/data/migrate.sh new file mode 100755 index 0000000..92760f8 --- /dev/null +++ b/data/migrate.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -euo pipefail + +cd "$(dirname "$0")" +cd .. + +pgmigrate migrate -d postgres://ohtu:password@localhost:6543/gradesa -m data/migrations diff --git a/data/reset-db.sh b/data/reset-db.sh new file mode 100755 index 0000000..ec1c75a --- /dev/null +++ b/data/reset-db.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(dirname "$0")" + +psql "postgres://ohtu:password@localhost:6543/gradesa" -c "DROP SCHEMA public CASCADE;CREATE SCHEMA public;" +cd ../ +pnpm run db:migrate +echo "Migration DONE" diff --git a/docker-compose.yml b/docker-compose.yml index 157ae04..d076c1a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,8 +5,6 @@ services: container_name: gradesa-db environment: POSTGRES_PASSWORD: password - POSTGRES_USER: tsoha - POSTGRES_DB: gradesa volumes: - pg-data:/var/lib/postgresql/data/ - ./provision_db.sql:/docker-entrypoint-initdb.d/1_provision_db.sql @@ -14,7 +12,7 @@ services: ports: - "6543:5432" healthcheck: - test: ["CMD", "pg_isready", "-U", "tsoha"] + test: ["CMD", "pg_isready", "-U", "postgres"] interval: 10s retries: 20 volumes: diff --git a/gradesa/.env.template b/gradesa/.env.template new file mode 100644 index 0000000..25b41db --- /dev/null +++ b/gradesa/.env.template @@ -0,0 +1,5 @@ +DB_HOST= +DB_PORT= +DB_USER= +DB_PASSWORD= +DB_NAME= diff --git a/gradesa/package-lock.json b/gradesa/package-lock.json index e61dbd6..c77c3e3 100644 --- a/gradesa/package-lock.json +++ b/gradesa/package-lock.json @@ -8,7 +8,9 @@ "name": "gradesa", "version": "0.1.0", "dependencies": { + "dotenv": "^16.4.7", "next": "15.1.5", + "pg": "^8.13.1", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -1580,6 +1582,17 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3527,6 +3540,87 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/pg": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", + "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3580,6 +3674,41 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4031,6 +4160,14 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stable-hash": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", @@ -4504,6 +4641,14 @@ "node": ">=0.10.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/gradesa/package.json b/gradesa/package.json index fdf47c6..eb928a5 100644 --- a/gradesa/package.json +++ b/gradesa/package.json @@ -3,19 +3,21 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { + "dotenv": "^16.4.7", + "next": "15.1.5", + "pg": "^8.13.1", "react": "^19.0.0", - "react-dom": "^19.0.0", - "next": "15.1.5" + "react-dom": "^19.0.0" }, "devDependencies": { + "@eslint/eslintrc": "^3", "eslint": "^9", - "eslint-config-next": "15.1.5", - "@eslint/eslintrc": "^3" + "eslint-config-next": "15.1.5" } -} +} \ No newline at end of file diff --git a/gradesa/src/app/api/hello/route.js b/gradesa/src/app/api/hello/route.js new file mode 100644 index 0000000..27e6472 --- /dev/null +++ b/gradesa/src/app/api/hello/route.js @@ -0,0 +1,7 @@ +import { query } from '../../../backend/db'; + +export async function GET(request) { + const result = await query('SELECT now()'); + const now = result.rows[0].now; + return Response.json({ message: 'Hello, world!', now }); +} diff --git a/gradesa/src/app/page.js b/gradesa/src/app/page.js index 03f4b46..e1d53ac 100644 --- a/gradesa/src/app/page.js +++ b/gradesa/src/app/page.js @@ -1,7 +1,14 @@ +'use client'; +import { useEffect, useState } from "react"; import Image from "next/image"; import styles from "./page.module.css"; - export default function Home() { + const [data, setData] = useState(null); + useEffect(() => { + fetch('/api/hello') + .then(res => res.json()) + .then(data => setData(data)); + }, []); return (