refactor: restructure to monorepo with backends and apiTest

- Move web/backend to backends/potter-api
- Move web/frontend to backends/potter-api/frontend
- Add root package.json with npm workspaces
- Update README.md with monorepo navigation
- Update .github/workflows/api-tests.yml for dynamic test discovery
- Update bruno_test_scenario.md paths
- Fix .gitignore to allow JSON data files
- Add three new AI tests in apiTest/PotterAPI/AI/

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
kankys committed 2026-09-15 21:08:39 +02:00
1 parent 2e185b47ef
commit d0cdabbf11
57 files changed
+292 -35

No files matched your search

+122
View File
@@ -0,0 +1,122 @@
# Created by https://www.gitignore.io/api/node,visualstudiocode
# Edit at https://www.gitignore.io/?templates=node,visualstudiocode
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# rollup.js default build output
dist/
# Uncomment the public line if your project uses Gatsby
# https://nextjs.org/blog/next-9-1#public-directory-support
# https://create-react-app.dev/docs/using-the-public-folder/#docsNav
# public
# Storybook build outputs
.out
.storybook-out
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Temporary folders
tmp/
temp/
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
# End of https://www.gitignore.io/api/node,visualstudiocode
+7
View File
@@ -0,0 +1,7 @@
FROM node:18
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD [ "node", "app.js" ]
+41
View File
@@ -0,0 +1,41 @@
# PotterAPI
### Prerequisites
1. Installed [NodeJS](https://nodejs.org/en/)
### Installation
1. Open terminal and run following commands
```
git clone https://github.com/skarbala/PotterAPI.git
cd PotterAPI
npm install
```
2. Run the API using
```
npm start
```
#### ✅ You should see following message
> Harry Potter App listening on port 3000!
## Documentation
#### Base url
All routes needs to be send to base url `http://localhost:3000`
#### Response type
Default response format is `application/json`
### Spells service
#### Authentication
No authentication needed
#### Available endpoints
``` GET /spells ``` returns list spells
``` GET /spells/{id} ``` returns specific spell.
``` DELETE /spells/{id} ``` deletes spell
```POST /spells``` adds new spell
+66
View File
@@ -0,0 +1,66 @@
var express = require('express');
var basicAuth = require('express-basic-auth');
let jwt = require('jsonwebtoken');
var boolParser = require('express-query-boolean');
var cors = require('cors');
module.exports = verifyToken;
var app = express();
const port = 3000
app.use(express.json())
app.use(boolParser());
app.use(cors());
const spellRoute = require('./routes/spells.js');
app.use('/spells', spellRoute);
const houseRoute = require('./routes/houses.js');
app.use('/houses', houseRoute);
const characterRoute = require('./routes/characters.js');
app.use('/characters', characterRoute)
const sortingHatRoute = require('./routes/sortinghat.js');
app.use('/sortingHat', sortingHatRoute);
const quote = require('./routes/quote.js');
app.use('/quote', quote);
app.get('/login', basicAuth({
users: { 'admin': 'supersecret' },
}), (req, res) => {
const user = {
username: 'furboslav',
id: 5,
}
generateToken(user, res);
});
generateToken = function (user, res) {
jwt.sign({ user: user }, 'secretkey', (err, token) => {
res.json({
token: token
});
});
}
function verifyToken(req, res, next) {
const bearerHeader = req.header('Authorization');
if (typeof bearerHeader !== 'undefined') {
const bearer = bearerHeader.split(' ');
const bearerToken = bearer[1];
req.token = bearerToken;
next();
}
else {
res.status(403)
res.json({
message: "Sorry Wizard you dont have TOKEN"
});
}
}
app.listen(port, () => console.log(`Harry Potter App listening on port ${port}!`))
+319
View File
@@ -0,0 +1,319 @@
# 🧪 PotterAPI – Testovací scénáře, tipy a rady pro Bruno API Client
Tento dokument obsahuje přehled testovacích scénářů, tipů, triků a automatizovaných testů v aplikaci **Bruno** pro testování REST API **PotterAPI**.
### 📋 Stručné shrnutí toho, co v dokumentu najdete:
1. Spouštění aplikace (Backend & Frontend):
• Spuštění backend serveru na portu 3000.
• Spuštění frontend aplikace.
2. Nastavení prostředí (Environment v Bruno):
• Nastavení proměnných baseUrl (http://localhost:3000), username ( ), password ( ).
3. Testovací scénáře pro všechny API služby:
• 🪄 Kouzla (/spells): Pozitivní testy (seznam, detail, nový záznam, úprava, smazání) a negativní testy (duplicity, chybějící Content-Type: application/json hlavička, 422 Joi
validace, 404).
• 🧙 Postavy (/characters): Testy s Basic Auth, filtrování podle koleje/role, přidávání, úprava a mazání postav, chybové stavy pro 401 Unauthorized a duplicity.
• 🏰 Koleje, Klobouk a Citáty (/houses, /sortingHat, /quote): Testy pro získání kolejí a citátů.
3. Asertace a Automatizace (Tests & Scripts):
• Příklady JavaScript testů pro záložku Tests v Bruno (kontrola stavových kódů 200, 201, 400, 401, 422, kontrola JSON struktury).
• Skript pro záložku Post Response v Bruno pro automatické uložení vygenerovaného ID z odpovídajícího POST požadavku do proměnné prostředí (bru.setEnvVar("createdSpellId", body.
spell.id)).
4. Workflow & Řetězení testů (Chaining):
• Postup, jak nastavit kompletní CRUD cyklus: POST ➡️ GET detail ➡️ PUT ➡️ DELETE ➡️ GET 404.
---
## 🚀 Spouštění aplikace
Před spuštěním testů je nutné spustit backend server a případně i frontend aplikaci.
### Backend (API Server)
Backend je Express.js aplikace běžící na portu **3000**.
**Spuštění z kořenové složky projektu:**
```bash
cd backends/potter-api
npm install # Nainstaluje závislosti (pokud ještě nejsou nainstalovány)
npm start # Spustí server pomocí nodemon (automaticky restartuje při změnách)
```
**Přímé spuštění bez nodemon:**
```bash
cd backends/potter-api
node app.js
```
Server bude dostupný na `http://localhost:3000`.
> [!TIP]
> Pokud je port 3000 obsazen, je nutné jej uvolnit nebo změnit port přímo v souboru `backends/potter-api/app.js` (řádek 9: `const port = 3000`).
### Frontend (Vue.js Aplikace)
Frontend je Vue.js aplikace, kterou lze spustit pro vývojové účely.
**Spuštění z kořenové složky projektu:**
```bash
cd backends/potter-api/frontend
npm install # Nainstaluje závislosti (pokud ještě nejsou nainstalovány)
npm run serve # Spustí vývojový server (obvykle na portu 8080)
# nebo
npm start # Stejné jako npm run serve
```
Frontend bude dostupný na `http://localhost:8080`.
> [!NOTE]
> Pro samotné testování API pomocí Bruno není spuštění frontend nutné. Stačí mít běžící backend server.
---
## ⚙️ 1. Nastavení Bruno (Environment & Kolekce)
### Vytvoření prostředí (Environment)
V aplikaci Bruno si vytvořte nové prostředí (např. `Local`) s následujícími proměnnými:
| Název proměnné | Hodnota | Popis |
| :--- | :--- | :--- |
| `baseUrl` | `http://localhost:3000` | Základní URL lokálního serveru |
| `username` | `*****` | Přihlašovací jméno pro Basic Auth |
| `password` | `*****` | Heslo pro Basic Auth |
| `createdSpellId` | *(dynamicky)* | Ukládá ID nově vytvořeného kouzla |
| `createdCharacterId` | *(dynamicky)* | Ukládá ID nově vytvořené postavy |
> [!TIP]
> V Bruno můžete v URL i v těle požadavku používat proměnné pomocí zápisu `{{baseUrl}}/spells`.
---
## 🔑 2. Autentizace v Bruno
Aplikace používá dva druhy autentizace:
1. **Basic Auth (`*****` / `*****`)**:
- Vyžadováno pro **`/characters`** a **`/login`**.
- **Nastavení v Bruno**: Na záložce **Auth** zvolte `Basic Auth` a zadejte `Username: {{username}}` a `Password: {{password}}`.
2. **Content-Type Header Check (`application/json`)**:
- Vyžadováno pro `POST` a `PUT` u **`/spells`** (middleware `checkHeader`).
- **Nastavení v Bruno**: Na záložce **Headers** přidejte `Content-Type: application/json`.
---
## 📋 3. Přehled testovacích scénářů
### 🪄 A. Kouzla (`/spells`)
#### 🟢 Pozitivní scénáře (Happy Path)
1. **Získání všech kouzel**
- **Metoda:** `GET {{baseUrl}}/spells`
- **Očekávaný stav:** `200 OK`
- **Kontrola:** Vrátí pole objektů, kde každý objekt má `id`, `spell`, `type`, `effect`, `isUnforgivable`.
2. **Filtrování kouzel podle typu**
- **Metoda:** `GET {{baseUrl}}/spells?type=Curse`
- **Očekávaný stav:** `200 OK`
- **Kontrola:** Každá položka v poli má `"type": "Curse"`.
3. **Vytvoření nového kouzla**
- **Metoda:** `POST {{baseUrl}}/spells`
- **Headers:** `Content-Type: application/json`
- **Body (JSON):**
```json
{
"spell": "Expelliarmus",
"type": "Charm",
"effect": "Disarms your opponent",
"isUnforgivable": "false"
}
```
- **Očekávaný stav:** `201 Created`
- **Kontrola:** Vrátí `{ message: "Spell created", spell: { id: "..." } }`. Hodnota `isUnforgivable` v `spells.json` bude `false` (boolean).
4. **Získání konkrétního kouzla podle ID**
- **Metoda:** `GET {{baseUrl}}/spells/{{createdSpellId}}`
- **Očekávaný stav:** `200 OK`
- **Kontrola:** Vrátí objekt kouzla s odpovídajícím ID.
5. **Úprava kouzla**
- **Metoda:** `PUT {{baseUrl}}/spells/{{createdSpellId}}`
- **Headers:** `Content-Type: application/json`
- **Body (JSON):**
```json
{
"spell": "Expelliarmus Maxima",
"type": "Charm",
"effect": "Strongly disarms opponent",
"isUnforgivable": false
}
```
- **Očekávaný stav:** `201 Created`
6. **Smazání kouzla podle ID**
- **Metoda:** `DELETE {{baseUrl}}/spells/{{createdSpellId}}`
- **Očekávaný stav:** `200 OK`
- **Kontrola:** Vrátí `{ message: "spell deleted" }`.
---
#### 🔴 Negativní a Hraniční scénáře
1. **Chybějící hlavička Content-Type (POST / PUT)**
- **Metoda:** `POST {{baseUrl}}/spells` (bez `Content-Type: application/json`)
- **Očekávaný stav:** `400 Bad Request`
- **Očekávaná zpráva:** `"Sorry Nick you dont have HEADer"`
2. **Pokus o vytvoření duplicitního kouzla**
- **Metoda:** `POST {{baseUrl}}/spells` s národem kouzla, které již existuje (např. `"spell": "Alohomora"`)
- **Očekávaný stav:** `400 Bad Request`
- **Očekávaná zpráva:** `"Spell Alohomora already exists"`
3. **Neplatná data (Joi validation failure)**
- **Metoda:** `POST {{baseUrl}}/spells`
- **Body (JSON):** (příliš krátký název, neplatný typ)
```json
{
"spell": "Ab",
"type": "SuperPower",
"isUnforgivable": true
}
```
- **Očekávaný stav:** `422 Unprocessable Entity`
4. **Vyhledání neexistujícího kouzla**
- **Metoda:** `GET {{baseUrl}}/spells/non-existing-id-999`
- **Očekávaný stav:** `404 Not Found`
---
### 🧙 B. Postavy (`/characters`)
> [!IMPORTANT]
> Všechny požadavky na `/characters` vyžadují **Basic Auth** (`****` / `*****`).
#### 🟢 Pozitivní scénáře
1. **Získání všech postav**
- **Metoda:** `GET {{baseUrl}}/characters`
- **Auth:** Basic Auth
- **Očekávaný stav:** `200 OK`
2. **Filtrování postav podle koleje a role**
- **Metoda:** `GET {{baseUrl}}/characters?house=Gryffindor&role=student`
- **Auth:** Basic Auth
- **Očekávaný stav:** `200 OK`
3. **Vytvoření nové postavy**
- **Metoda:** `POST {{baseUrl}}/characters`
- **Auth:** Basic Auth
- **Headers:** `Content-Type: application/json`
- **Body (JSON):**
```json
{
"name": "Hermione Granger",
"role": "student",
"house": "Gryffindor",
"school": "Hogwarts School of Witchcraft and Wizardry",
"dumbledoresArmy": true,
"bloodStatus": "muggle-born",
"species": "human"
}
```
- **Očekávaný stav:** `201 Created`
4. **Smazání postavy podle ID**
- **Metoda:** `DELETE {{baseUrl}}/characters/{{createdCharacterId}}`
- **Auth:** Basic Auth
- **Očekávaný stav:** `200 OK`
---
#### 🔴 Negativní scénáře
1. **Přístup bez přihlášení (Neplatné/Chybějící Basic Auth)**
- **Metoda:** `GET {{baseUrl}}/characters` (bez Auth)
- **Očekávaný stav:** `401 Unauthorized`
- **Očekávané tělo:** `{ "message": "Sorry Wizard, can't let you in." }`
2. **Duplicitní jméno postavy**
- **Metoda:** `POST {{baseUrl}}/characters` se jménem, které již v databázi je (např. `"name": "Hannah Abbott"`)
- **Očekávaný stav:** `400 Bad Request`
---
### 🏰 C. Koleje, Moudrý klobouk & Citáty (`/houses`, `/sortingHat`, `/quote`)
1. **Seznam kolejí**: `GET {{baseUrl}}/houses` -> `200 OK` (Vrátí 4 koleje)
2. **Detail koleje**: `GET {{baseUrl}}/houses/5a05e2b252f039152b96b5a7` -> `200 OK` (Gryffindor)
3. **Moudrý klobouk**: `GET {{baseUrl}}/sortingHat` -> `200 OK` (Vrátí náhodnou kolej)
4. **Náhodný citát**: `GET {{baseUrl}}/quote` -> `200 OK` (Vrátí citát)
---
## ⚡ 4. Testovací skripty & Asertace v Bruno
V Bruno můžete psát testy v záložce **Tests** (používá knihovnu Chai / JavaScript).
### Příklady asertací:
#### Test 1: Kontrola stavového kódu a struktury odpovědi při vytvoření kouzla
*(Záložka Tests u requestu `POST /spells`)*:
```javascript
test("Status code is 201 Created", function() {
expect(res.getStatus()).to.equal(201);
});
test("Response contains success message and spell ID", function() {
const data = res.getBody();
expect(data).to.have.property("message", "Spell created");
expect(data.spell).to.have.property("id");
});
```
#### Test 2: Uložení vytvořeného ID do proměnné prostředí (Post-response Script)
*(Záložka Script -> Post Response)*:
```javascript
if (res.getStatus() === 201) {
const body = res.getBody();
bru.setEnvVar("createdSpellId", body.spell.id);
}
```
---
## 🔗 5. Řetězení testů (Workflow v Bruno)
Díky skriptování můžete vytvořit automatizovaný řetězec testů:
```mermaid
flowchart LR
A["1. POST /spells<br/>(Vytvořit kouzlo)"] -->|"Uloží ID do env"| B["2. GET /spells/:id<br/>(Ověřit vytvoření)"]
B --> C["3. PUT /spells/:id<br/>(Upravit kouzlo)"]
C --> D["4. DELETE /spells/:id<br/>(Smazat kouzlo)"]
D --> E["5. GET /spells/:id<br/>(Ověřit 404)"]
```
1. **Krok 1 (`POST /spells`)**: Vytvoří kouzlo a v `Post Response` skriptu uloží `createdSpellId`.
2. **Krok 2 (`GET /spells/{{createdSpellId}}`)**: Ověří, že kouzlo existuje.
3. **Krok 3 (`PUT /spells/{{createdSpellId}}`)**: Upraví vlastnosti kouzla.
4. **Krok 4 (`DELETE /spells/{{createdSpellId}}`)**: Smaže kouzlo.
5. **Krok 5 (`GET /spells/{{createdSpellId}}`)**: Ověří navrácení kódů `404 Not Found`.
---
## 💡 Tipy a doporučení pro Bruno
1. **Používejte Bruno Runner**:
- V levém panelu můžete kliknout na menu kolekce a zvolit **Run**. Bruno spustí všechny testy v kolekci za sebou a zobrazí zelené/červené výsledky.
2. **Ukládání kolekce do Gitu**:
- Bruno ukládá kolekce jako čisté `.bru` soubory přímo ve vaší složce projektu. Můžete je verzovat v Gitu spolu s kódem backendu!
3. **Reset databáze před testy**:
- Na začátku testovací sady doporučujeme zavolat `GET {{baseUrl}}/spells/actions/reset`, aby byl stav `spells.json` vždy předvídatelný.
4. **Záloha dat**:
- Zálohuj si veškerá data před testováním i po testování dle potřeby ať o ně nepříjdeš.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,145 @@
const fs = require('fs');
const path = require('path');
const charactersFilePath = path.join(__dirname, '../characters.json');
var characters = require('../characters.json');
const joi = require('joi');
const initialCharacters = JSON.parse(JSON.stringify(require('../characters.json')));
const randomId = require('random-id');
const schema = joi.object().keys({
id: joi.any(),
name: joi.string().min(2).max(100).required(),
role: joi.string().allow('', null),
house: joi.string().allow('', null),
school: joi.string().allow('', null),
ministryOfMagic: joi.boolean().default(false),
orderOfThePhoenix: joi.boolean().default(false),
dumbledoresArmy: joi.boolean().default(false),
deathEater: joi.boolean().default(false),
bloodStatus: joi.string().allow('', null),
species: joi.string().allow('', null),
wand: joi.string().allow('', null),
boggart: joi.string().allow('', null),
patronus: joi.string().allow('', null),
animagus: joi.string().allow('', null),
alias: joi.string().allow('', null)
});
function saveCharactersToFile() {
fs.writeFileSync(charactersFilePath, JSON.stringify(characters, null, 4));
}
exports.characters_list = function (req, res) {
let result = characters;
if (typeof req.query.house !== 'undefined') {
result = result.filter(c => c.house === req.query.house);
}
if (typeof req.query.role !== 'undefined') {
result = result.filter(c => c.role === req.query.role);
}
if (typeof req.query.bloodStatus !== 'undefined') {
result = result.filter(c => c.bloodStatus === req.query.bloodStatus);
}
if (typeof req.query.species !== 'undefined') {
result = result.filter(c => c.species === req.query.species);
}
if (typeof req.query.limit !== 'undefined') {
result = result.slice(0, req.query.limit);
}
return res.json(result);
};
exports.specific_character = function (req, res) {
let result = characters.find(c => c.id == req.params.characterId);
if (typeof result !== 'undefined') {
return res.send(result);
}
return res.status(404).send({ message: "Character not found" });
};
exports.delete_all = function (req, res) {
characters = [];
saveCharactersToFile();
return res.send({ message: 'All characters deleted' });
};
exports.reset = function (req, res) {
characters = JSON.parse(JSON.stringify(initialCharacters));
saveCharactersToFile();
return res.send({ message: 'Characters reset', characters: characters });
};
exports.delete_character = function (req, res) {
let result = characters.find(c => c.id == req.params.characterId);
if (typeof result === 'undefined') {
return res.status(404).send({ message: "Character not found" });
}
characters = characters.filter(c => c.id != req.params.characterId);
saveCharactersToFile();
return res.send({ message: 'Character deleted' });
};
exports.update_character = function (req, res) {
let result = characters.find(c => c.id == req.params.characterId);
if (typeof result === 'undefined') {
return res.status(404).send({ message: "Character not found" });
}
var newChar = Object.assign({}, req.body, { id: result.id });
const validation = schema.validate(newChar);
const { value, error } = validation;
if (error) {
return res.status(422).json({
message: 'Invalid character request',
data: error
});
}
const formattedCharacter = Object.assign({ id: value.id, name: value.name }, value);
characters = characters.filter(c => c.id != req.params.characterId);
characters.push(formattedCharacter);
saveCharactersToFile();
return res.status(201).json({
message: "Character updated",
character: { id: formattedCharacter.id }
});
};
exports.new_character = function (req, res) {
var len = 24;
var pattern = '0123456789abcdef';
var newChar = Object.assign({}, req.body, { id: randomId(len, pattern) });
let existing = characters.find(c => c.name === newChar.name);
if (typeof existing !== 'undefined') {
return res.status(400).send({
message: 'Character ' + newChar.name + ' already exists'
});
}
const validation = schema.validate(newChar);
const { value, error } = validation;
if (error) {
return res.status(422).json({
message: 'Invalid request',
data: error
});
}
const formattedCharacter = Object.assign({ id: value.id, name: value.name }, value);
characters.push(formattedCharacter);
saveCharactersToFile();
return res.status(201).json({
message: "Character created",
character: { id: formattedCharacter.id }
});
};
@@ -0,0 +1,14 @@
const houses = require('../houses.json');
exports.houses_list = function (req, res) {
res.json(houses);
}
exports.specific_house = function (req, res) {
let result = houses.find(house => house.id == req.params.houseId);
if (typeof result == 'undefined') {
res.status(404)
res.send({ message: "House not found" });
}
res.send(result);
}
@@ -0,0 +1,161 @@
const fs = require('fs');
const path = require('path');
const spellsFilePath = path.join(__dirname, '../spells.json');
var spells = require('../spells.json');
const joi = require('joi');
const initialSpells = JSON.parse(JSON.stringify(require('../spells.json')));
const schema = joi.object().keys({
spell: joi.string().min(3).max(30).required().regex(/^\w+(?:\s+\w+)*$/),
type: joi.string().valid('Charm', 'Enchantment', 'Curse', 'Spell', 'Hex', 'Jinx'),
effect: joi.string().min(4),
isUnforgivable: joi.boolean().required(),
id: joi.any()
});
const randomId = require('random-id');
function saveSpellsToFile() {
fs.writeFileSync(spellsFilePath, JSON.stringify(spells, null, 2));
}
exports.spells_list = function (req, res) {
let result = spells;
if (typeof req.query.type !== 'undefined') {
result = spells.filter(spell => spell.type == req.query.type)
}
if (typeof req.query.isUnforgivable !== 'undefined') {
result = result.filter(spell => spell.isUnforgivable == req.query.isUnforgivable)
}
if (typeof req.query.limit !== 'undefined') {
result = result.slice(0, req.query.limit);
}
setTimeout(() => {
return res.json(result);
}, 100)
}
exports.delete_all = function (req, res) {
spells = [];
saveSpellsToFile();
res.send({ message: 'Mischief managed' })
}
exports.reset = function (req, res) {
spells = JSON.parse(JSON.stringify(initialSpells));
saveSpellsToFile();
res.send({ message: 'Aparecium', spells: spells })
}
exports.specific_spell = function (req, res) {
let result = spells.find(spell => spell.id == req.params.spellId);
if (typeof result !== 'undefined') {
return res.send(result);
}
return res.status(404).send({ message: "Spell not found" });
}
exports.delete_spell = function (req, res) {
let result = spells.find(spell => spell.id == req.params.spellId);
if (typeof result == 'undefined') {
return res.status(404).send({ message: "Spell not found" });
}
spells = spells.filter(spell => spell.id != req.params.spellId);
saveSpellsToFile();
return res.send({
message: 'spell deleted'
});
}
exports.update_spell = function (req, res) {
let result = spells.find(spell => spell.id == req.params.spellId);
if (typeof result == 'undefined') {
return res.status(404).send({ message: "Spell not found" });
}
var newSpell = {
spell: req.body.spell,
type: req.body.type,
effect: req.body.effect,
isUnforgivable: req.body.isUnforgivable,
id: result.id
}
const validation = schema.validate(newSpell);
const { value, error } = validation;
const valid = error == null;
if (!valid) {
return res.status(422).json({
message: 'Wizard, I\'m sory but your request is invalid',
data: error
})
}
const formattedSpell = {
id: value.id,
spell: value.spell,
type: value.type,
effect: value.effect,
isUnforgivable: value.isUnforgivable
};
spells = spells.filter(spell => spell.id != req.params.spellId);
spells.push(formattedSpell);
saveSpellsToFile();
return res.status(201).json({
message: "Spell updated",
spell: {
id: formattedSpell.id
}
});
}
exports.new_spell = function (req, res) {
var len = 30;
var pattern = 'aA0'
var newSpell = {
id: randomId(len, pattern),
spell: req.body.spell,
type: req.body.type,
effect: req.body.effect,
isUnforgivable: req.body.isUnforgivable
}
let result = spells.find((spell) => spell.spell === newSpell.spell);
if (typeof result !== 'undefined') {
return res.status(400).send({
message: 'Spell ' + newSpell.spell + ' already exists'
})
}
const validation = schema.validate(newSpell);
const { value, error } = validation;
const valid = error == null;
if (!valid) {
return res.status(422).json({
message: 'Invalid request',
data: error
})
}
const formattedSpell = {
id: value.id,
spell: value.spell,
type: value.type,
effect: value.effect,
isUnforgivable: value.isUnforgivable
};
spells.push(formattedSpell);
saveSpellsToFile();
return res.status(201).json({
message: "Spell created",
spell: {
id: formattedSpell.id
}
});
}
+21
View File
@@ -0,0 +1,21 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+12
View File
@@ -0,0 +1,12 @@
#build
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
#run
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
+24
View File
@@ -0,0 +1,24 @@
# frontend
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
File diff suppressed because it is too large. Load diff
+49
View File
@@ -0,0 +1,49 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"start": "npm run serve"
},
"dependencies": {
"axios": "^0.27.0",
"core-js": "^3.6.4",
"url": "^0.11.0",
"vue": "^2.6.14",
"vue-router": "^3.5.3"
},
"devDependencies": {
"@babel/eslint-parser": "^7.5.4",
"@vue/cli-plugin-babel": "^5.0.4",
"@vue/cli-plugin-eslint": "~5.0.4",
"@vue/cli-service": "~5.0.4",
"eslint": "^8.14.0",
"eslint-plugin-vue": "^8.7.1",
"sass": "^1.50.1",
"sass-loader": "^12.6.0",
"vue-template-compiler": "^2.6.11",
"webpack": "^5.72.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Spelleology</title>
<link href="https://fonts.googleapis.com/css?family=Kanit:100i" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Lora:700" rel="stylesheet">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/css2?family=Allura&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@400&display=swap" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
+48
View File
@@ -0,0 +1,48 @@
<template>
<div id="app">
<navigation />
<router-view></router-view>
</div>
</template>
<script>
import Navigation from "./components/Navigation";
export default {
name: "App",
components: {
Navigation
}
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
button.main {
padding: 10px 40px;
background-color: #822724;
color: white;
font-size: 1.2em;
cursor: pointer;
}
button {
border: none;
}
button:focus {
outline: none;
}
button.main:hover {
background-color: #72221f;
}
div.main {
padding-top: 50px;
min-height: 1000px;
}
</style>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

@@ -0,0 +1,121 @@
<template>
<div class="main">
<div class="container">
<div class="row">
<div class="col-2">
<img src="../assets/gringottsbanklogo.png" alt="" />
</div>
<div class="col-10">
<h1>Gringotts Bank</h1>
<h2>Investments for witches and wizards</h2>
</div>
</div>
<div class="row mt-5">
<div class="col mx-5">
<GringottsCalculator @calculation-done="handleCalculationDone" />
<GringottsForm
v-if="newInvestment"
:newInvestment="newInvestment"
@create-investment="handleCreateInvestment"
@reject-offer="handleRejectOffer"
/>
</div>
<div class="col mx-5">
<GringottsInvestmentList
:investments="investments"
@delete-investment="handleDeleteInvestment"
/>
<div v-if="investments.length === 0" class="no-investments-message">
<p>No investments yet. Make your first one!</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import GringottsCalculator from "../components/gringotts-bank/GringottsCalculator.vue";
import GringottsForm from "../components/gringotts-bank/GringottsForm.vue";
import GringottsInvestmentList from "../components/gringotts-bank/GringottsInvestmentList.vue";
export default {
components: {
GringottsCalculator,
GringottsForm,
GringottsInvestmentList,
},
created: function () {
if (localStorage.getItem("investments")) {
try {
this.investments = JSON.parse(localStorage.getItem("investments"));
} catch (e) {
localStorage.removeItem("investments");
}
}
},
data() {
return {
newInvestment: null,
investments: [],
};
},
methods: {
handleCalculationDone(newInvestmentData) {
this.newInvestment = newInvestmentData;
},
handleCreateInvestment(customerName) {
if (this.newInvestment) {
// Create a new investment object with customer name
const investment = {
...this.newInvestment,
customerName,
};
// Log the new investment to the console
console.log("New Investment:", investment);
this.investments.push(investment);
this.saveAppliedSavings();
// Optionally, reset the newInvestment data
this.newInvestment = null;
//this.$refs.calculator.resetInputFields();
}
},
handleRejectOffer() {
this.newInvestment = null;
// Handle the rejected offer, e.g., log a message or perform other actions
console.log("Offer Rejected");
},
handleDeleteInvestment(index) {
// Handle the deletion of the investment in the parent component
this.investments.splice(index, 1);
this.saveAppliedSavings();
},
saveAppliedSavings() {
const parsed = JSON.stringify(this.investments);
localStorage.setItem("investments", parsed);
},
},
};
</script>
<style scoped lang="scss">
div.main {
img {
width: 70%;
}
background-color: #eeedeb;
h1,
h2 {
color: #747264;
text-align: left;
}
p {
color: #868475;
text-align: left;
}
}
</style>
@@ -0,0 +1,113 @@
<template>
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
<h2 name="header">{{ spell.spell }}</h2>
</div>
<div class="modal-body">
<h3 name="body">{{ spell.effect }}</h3>
<h4 class="body">{{ spell.type }}</h4>
</div>
<div class="modal-footer">
<slot name="footer">
<button class="modal-default-button btn-block btn" @click="close">
Close
</button>
</slot>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "ModalComponent",
props: ["spell"],
methods: {
close() {
this.$emit("close");
},
},
};
</script>
<style scoped >
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: table;
transition: opacity 0.3s ease;
}
.modal-wrapper {
display: table-cell;
vertical-align: middle;
}
.modal-container {
text-align: center;
width: 900px;
margin: 0 auto;
padding: 20px 30px;
background-color: #822724;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.2s ease-in-out;
font-family: Helvetica, Arial, sans-serif;
}
.modal-header h2 {
margin: 0 auto;
color: white;
font-family: "Harry", serif;
font-size: 6em;
}
.modal-body {
margin: 20px 0;
color: white;
font-family: "Lora", serif;
}
.modal-body h3 {
margin-bottom: 20px;
}
.modal-body h4 {
font-family: "Kanit", serif;
font-size: 1.5em;
}
.modal-enter {
opacity: 0;
}
.modal-leave-active {
opacity: 0;
}
.modal-enter .modal-container,
.modal-leave-active .modal-container {
-webkit-transform: scale(1.5);
transform: scale(1.5);
}
.modal-default-button {
font-family: "Harry", serif;
font-size: 2em;
color: white;
background-color: transparent;
}
.modal-default-button:active,
.modal-default-button:focus {
outline: none;
border: none;
box-shadow: none;
}
</style>
@@ -0,0 +1,43 @@
<template>
<nav class="navbar navbar-expand-lg">
<ul class="navbar-nav mx-auto">
<li class="nav-item">
<router-link class="nav-link" to="/">Welcome</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" to="/spelleology"
>Spelleology</router-link
>
</li>
<li class="nav-item">
<router-link class="nav-link" to="/sortingHat">Sorting Hat</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" to="/quotes">Quote Generator</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" to="/gringottsBank"
>Gringotts Bank</router-link
>
</li>
</ul>
</nav>
</template>
<script>
export default {
name: "NavigationComponent",
};
</script>
<style lang="scss">
a {
color: #212529;
&:hover {
color: #822724;
}
&.router-link-exact-active {
border-bottom: 1px solid #822724;
color: #822724;
}
}
</style>
@@ -0,0 +1,198 @@
<template>
<div class="main">
<h1 class="title">Potter Quotes</h1>
<h2 class="subtitle">Level up by getting some wisdom</h2>
<div class="quote-controls">
<button
v-on:click="getQuote"
class="main main mx-1"
data-test="get-quote"
>
Get Quote
</button>
<button
class="main mx-1"
v-on:click="removeQuote"
:disabled="quoteList.length === 0"
id="remove-quote"
>
Remove Quote
</button>
<div class="row wisdom-level col-md-10 mx-auto">
<div class="col">
<p data-test="wisdom-points">
wisdom points +{{ this.quoteList.length }}
</p>
</div>
<div class="col">
<p data-test="wisdom-level">current level {{ wizardLevel }}</p>
</div>
</div>
</div>
<div class="col-md-6 mx-auto">
<div class="quote-container">
<div v-if="this.qouteCounter == 0" class="empty-list-message">
Click the button to get some wisdom
</div>
<transition-group v-else name="fade" tag="ul" class="quote-list">
<li v-for="quoteitem in quoteList" :key="quoteitem.quote">
<p>{{ quoteitem.quote }}</p>
<p class="author">{{ quoteitem.author }}</p>
</li>
</transition-group>
</div>
</div>
</div>
</template>
<script>
export default {
name: "QuoteGenerator",
data: function () {
return {
quote: "",
quoteList: [],
qouteCounter: "",
};
},
methods: {
getQuote: function () {
this.quote = "";
this.$http.get("/quote").then((response) => {
this.quote = response.data;
this.quoteList.unshift(this.quote);
this.qouteCounter++;
});
},
removeQuote: function () {
if (this.quoteList.length > 0) {
this.quoteList.shift(); // Remove the newest quote
this.qouteCounter--;
}
},
},
computed: {
wizardLevel: function () {
let level = "";
if (this.qouteCounter <= 5) {
level = "NOOB";
}
if (this.qouteCounter > 5) {
level = "ROOKIE wand polisher";
}
if (this.qouteCounter > 10) {
level = "Experienced broom SUMMONER";
}
if (this.qouteCounter > 15) {
level = "NIGHTMARE Alchemist";
}
if (this.qouteCounter > 20) {
level = "Spell OVERLORD";
}
if (this.qouteCounter > 25) {
level = "Death potion EVANGELIST";
}
if (this.qouteCounter > 30) {
level = "Dementor the HAPPINESS SPECIALIST";
}
if (this.qouteCounter > 35) {
level = "Part-Time DEATHEATER";
}
if (this.qouteCounter > 40) {
level = "First Impression SPOILER";
}
if (this.qouteCounter > 45) {
level = "Azcaban INFLUENCER";
}
return level;
},
},
};
</script>
<style lang="scss" scoped>
$gray-color: #9b9a9a;
$red-color: #822724;
h1.title {
font-family: "Roboto Slab", serif;
}
h2.subtitle {
font-family: "Roboto Slab", serif;
color: $gray-color;
font-size: 1.5em;
}
button {
border: none;
cursor: pointer;
padding: 10px 30px;
margin-top: 20px;
background-color: $red-color;
}
button#remove-quote {
background-color: black;
}
button#remove-quote:disabled {
background-color: #9b9a9a;
cursor: not-allowed;
&:active {
transform: scale(1);
}
}
.quote-container {
margin-top: 50px;
ul {
list-style: none;
li {
font-family: "Roboto Slab", serif;
text-align: left;
padding: 20px;
margin-bottom: 10px;
box-shadow: 0px 0px 7px 1px rgba(230, 230, 230, 1);
&:nth-child(1) {
transform: scale(1.05);
margin-bottom: 20px;
box-shadow: 0px 0px 15px 3px #c17f7d;
}
.author {
color: $gray-color;
}
}
}
}
.fade-enter-to,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
.quote-controls {
p {
display: inline-block;
color: $red-color;
font-weight: 700;
font-size: 1.5em;
text-align: justify;
}
button {
transition: all 200ms ease-in;
&:active {
transform: scale(1.1);
}
}
.wisdom-level {
margin-top: 20px;
p {
color: $gray-color;
font-weight: 300;
}
}
}
</style>
@@ -0,0 +1,73 @@
<template>
<input
type="text"
class="search"
placeholder="search for spell effect"
id="search"
data-test="search-input"
v-bind="$attrs"
v-bind:value="value"
v-on="inputListeners"
/>
</template>
<script>
export default {
name: "SearchComponent",
props: ["value"],
computed: {
inputListeners: function() {
var vm = this;
// `Object.assign` merges objects together to form a new object
return Object.assign(
{},
// We add all the listeners from the parent
this.$listeners,
// Then we can add custom listeners or override the
// behavior of some listeners.
{
// This ensures that the component works with v-model
input: function(event) {
vm.$emit("input", event.target.value);
}
}
);
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
input.search {
outline: none;
border: none;
color: #fff;
font-family: "Lora", serif;
border-bottom: 2px solid #dca39e;
background-color: #822724;
border-radius: 0;
font-size: 2em;
margin-bottom: 50px;
transition: all 200ms ease;
&:focus,
&:active {
outline: none;
box-shadow: none;
background-color: #822724;
border-radius: 0;
border-bottom: 2px solid #fff;
color: white;
}
&::placeholder {
color: #dca39e;
font-family: "Kanit", serif;
}
&:focus::placeholder {
color: #fff;
font-family: "Kanit", serif;
}
}
</style>
@@ -0,0 +1,97 @@
<template>
<div class="main">
<h1 class="title">Sorting Hat</h1>
<h2 class="subtitle">Find yourself a new home in Hogwarts</h2>
<button
class="main"
data-test="sort-button"
:disabled="disabled"
v-on:click="getQuote"
>{{sortButtonLabel}}</button>
<div class="row">
<div class="col-md-6">
<img class="hat" src="../assets/sortinghat.jpg" alt />
</div>
<div class="col-md-4 my-auto message">
<h2 v-if="quote" data-test="result-message">{{quote.sortingHatSays}}</h2>
<h1 v-if="quote" data-test="house-result">{{quote.house}}</h1>
<h3 class="message" v-else>{{loadingMessage}}</h3>
</div>
</div>
</div>
</template>
<script>
export default {
name: "SortingHat",
data: function() {
return {
disabled: false,
quote: "",
isSorted: false,
loadingMessage: "Click on sort to get yourself a new home"
};
},
computed: {
sortButtonLabel: function() {
if (this.isSorted) {
return "Sort me again!";
}
return "Sort me";
}
},
methods: {
getQuote: function() {
this.disabled = true;
this.loadingMessage = "Let me think...";
this.quote = "";
this.$http
.get("/sortingHat")
.then(response => (this.quote = response.data))
.then(() => {
(this.isSorted = true), (this.disabled = false);
});
}
}
};
</script>
<style lang="scss" scoped >
$gray-color: rgb(155, 154, 154);
div.main {
font-family: "Roboto Slab", serif;
h2.subtitle {
color: $gray-color;
font-size: 1.5em;
}
div.message {
h1 {
margin: 20px 0;
text-align: left;
color: #3f3f8c;
font-size: 3em;
}
h2 {
font-size: 1.9em;
text-align: left;
color: $gray-color;
}
.message {
text-align: center;
color: $gray-color;
}
}
.hat {
width: 60%;
}
button {
margin-top: 20px;
background-color: #3f3f8c;
&:hover {
background-color: #333373;
}
&:disabled {
background-color: $gray-color;
}
}
}
</style>
@@ -0,0 +1,56 @@
<template>
<div>
<ul class="spells row text-center">
<li
v-bind:key="spell.id"
v-for="spell in spells"
v-on:click="$emit('clickOnspell', spell)"
>
{{ spell.effect }}
</li>
</ul>
</div>
</template>
<script>
export default {
name: "SpellList",
props: ["spells"],
methods: {
clickOnspell: function () {
this.$emit("selectSpell");
},
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
.spells {
list-style: none;
padding: 20px;
li {
padding: 10px;
color: #dca39e;
font-family: "Lora", serif;
vertical-align: middle;
font-size: 2em;
cursor: pointer;
transition: all 200ms ease-in-out;
&:nth-child(1n + 2) {
font-size: 1.3em;
}
&:nth-child(2n + 3) {
font-size: 1.7em;
}
&:nth-child(3n) {
font-size: 2.2em;
}
&:hover {
color: #fffcf9;
}
}
}
</style>
@@ -0,0 +1,167 @@
<template>
<div class="main spells-main">
<h1 class="text-center title">Spelleology</h1>
<div class="col-md-12 mx-auto">
<div class="col-md-6 row mx-auto">
<search-component class="col" v-model="search" />
<div class="col-md-4 unforgivable-checkbox">
<input type="checkbox" id="unforgivableOnly" v-model="unforgivableOnly" />
<label for="unforgivableOnly">Unforgivable only</label>
</div>
</div>
<div class="type-filter">
<input type="checkbox" id="spell" value="Spell" v-model="checkedSpells" />
<label for="spell">Spell</label>
<input type="checkbox" id="charm" value="Charm" v-model="checkedSpells" />
<label for="charm">Charm</label>
<input type="checkbox" id="curse" value="Curse" v-model="checkedSpells" />
<label for="curse">Curse</label>
<input type="checkbox" id="enchantment" value="Enchantment" v-model="checkedSpells" />
<label for="enchantment">Enchantment</label>
<input type="checkbox" id="hex" value="Hex" v-model="checkedSpells" />
<label for="hex">Hex</label>
<input type="checkbox" id="jinx" value="Jinx" v-model="checkedSpells" />
<label for="jinx">Jinx</label>
</div>
<div>
<button
v-on:click="clearAll"
id="deleteAll"
data-test="delete-all"
class="spell-button"
>Delete all</button>
<button v-on:click="reset" id="resetSpells" data-test="reset" class="spell-button">Reset</button>
</div>
<div class="row col-md-12 mx-auto">
<spell-list
v-if="spells.length >=1"
:spells="filteredList"
v-on:clickOnspell="selectSpell"
/>
</div>
<h1
class="subtitle text-center"
v-if="spells.length ===0 || filteredList.length ===0"
>Mischief managed</h1>
<modal :spell="selectedSpell" v-show="showModal" @close="closeModal" />
</div>
</div>
</template>
<script>
import SpellList from "./SpellList.vue";
import SearchComponent from "./SearchComponent.vue";
import Modal from "./Modal.vue";
export default {
name: "SpelleologyComponent",
components: {
SpellList,
SearchComponent,
Modal,
},
data: function () {
return {
spells: [],
selectedSpell: "",
showModal: false,
search: "",
unforgivableOnly: false,
checkedSpells: ["Charm", "Enchantment", "Curse", "Spell", "Hex", "Jinx"],
};
},
computed: {
filteredList: function () {
let spells = this.spells;
spells = spells.filter((spell) =>
spell.effect.toLowerCase().includes(this.search.toLowerCase())
);
spells = spells.filter((spell) =>
this.checkedSpells.includes(spell.type)
);
if (this.unforgivableOnly) {
spells = spells.filter((spell) => spell.isUnforgivable === true);
}
return spells;
},
},
created: function () {
this.$http.get("/spells").then((response) => (this.spells = response.data));
},
methods: {
selectSpell: function (spell) {
this.selectedSpell = spell;
this.showModal = true;
},
closeModal() {
this.showModal = false;
},
clearAll: function () {
this.$http
.delete("spells/actions/deleteAll")
.then(() => (this.spells = []));
},
reset: function () {
this.$http.get("spells/actions/reset").then((response) => {
this.spells = response.data.spells;
});
},
},
};
</script>
<style lang="scss" scoped>
div.spells-main {
background-color: #822724;
}
h1 {
font-family: "Harry", serif;
&.title {
color: white;
font-size: 8em;
text-shadow: 7px 7px 2px #671e1c;
}
&.subtitle {
margin: 100px;
font-size: 6em;
color: #dca39e;
}
}
label {
color: white;
padding: 0 10px;
font-family: "Kanit", serif;
font-size: 1.5em;
}
div.unforgivable-checkbox {
padding: 20px 0;
}
.spell-button {
padding: 1px 20px;
background-color: #822724;
font-family: "Kanit", serif;
color: #fffcf9;
font-size: 2em;
cursor: pointer;
border-bottom: 2px solid #dca39e;
margin: 0 20px;
&:hover {
border-bottom: 2px solid white;
color: white;
}
}
@font-face {
font-family: "Harry";
src: url("../assets/HARRYP.TTF");
}
</style>
<!-- Add "scoped" attribute to limit CSS to this component only -->
@@ -0,0 +1,24 @@
<template>
<div>
<div class="welcome">
<h1>Welcome Wizard</h1><img src=../assets/hogwartslogo.png alt="">
</div>
</div>
</template>
<script>
export default {
name: "WelcomeComponent"
};
</script>
<style lang="scss" scoped>
div.welcome {
margin-top: 50px;
h1 {
font-size: 8em;
color: black;
font-family: "Roboto Slab", serif;
}
}
</style>
@@ -0,0 +1,172 @@
<template>
<div>
<h2>Enter your data muggle!</h2>
<div class="input-group mb-3">
<select class="custom-select" id="selectedFund" v-model="selectedFundId">
<option disabled value="">Please select fund</option>
<option
v-for="(product, index) in products"
:value="product.id"
:key="index"
>
{{ product.name }}
</option>
</select>
</div>
<div class="input-group mb-3">
<input
class="form-control"
aria-describedby="inputGroup-sizing-default"
type="number"
id="oneTimeInvestment"
v-model="oneTimeInvestment"
step="1000"
placeholder="one time investment"
/>
</div>
<div class="input-group mb-3">
<input
class="form-control"
aria-describedby="inputGroup-sizing-default"
type="number"
id="years"
v-model="years"
step="1"
placeholder="years"
/>
</div>
<button class="btn btn-block" @click="calculate" data-test="create-offer">
Make me an offer
</button>
</div>
</template>
<script>
export default {
data() {
return {
selectedFundId: "",
oneTimeInvestment: null,
years: null,
products: [
{ id: 1, name: "Galleon Guardian Fund", risk: "LOW", interestRate: 4 },
{
id: 2,
name: "Phoenix Feather Portfolio",
risk: "MEDIUM",
interestRate: 6,
},
{ id: 3, name: "Basilisk Bonds", risk: "HIGH", interestRate: 9 },
{
id: 4,
name: "Polyjuice Potion Fund",
risk: "MEDIUM",
interestRate: 5,
},
{
id: 5,
name: "Marauder's Map Mutuals",
risk: "HIGH",
interestRate: 8,
},
{
id: 6,
name: "Dementor Defense Dividends",
risk: "INSANE",
interestRate: 12,
},
{
id: 7,
name: "Quidditch Quest Equities",
risk: "MEDIUM",
interestRate: 6,
},
{
id: 8,
name: "Hogwarts Heritage Holdings",
risk: "HIGH",
interestRate: 9,
},
{
id: 9,
name: "Pensieve Prosperity Partners",
risk: "LOW",
interestRate: 3,
},
{ id: 10, name: "Owl Post Options", risk: "MEDIUM", interestRate: 5 },
{
id: 11,
name: "Death Eater Dominance Fund",
risk: "INSANE",
interestRate: 15,
},
],
};
},
computed: {
selectedFund() {
return this.products.find((fund) => fund.id == this.selectedFundId);
},
},
methods: {
calculate() {
if (this.selectedFund && this.oneTimeInvestment && this.years) {
const oneTimeInvestment = parseFloat(this.oneTimeInvestment);
const interestRate = this.selectedFund.interestRate / 100;
const years = parseInt(this.years);
// Simple compound interest calculation
const totalInvestment =
oneTimeInvestment * Math.pow(1 + interestRate, years);
const interestIncome = totalInvestment - oneTimeInvestment;
const taxes = interestIncome * 0.19;
const netIncome = totalInvestment - taxes;
const calculatedData = {
oneTimeInvestment,
fund: this.selectedFund,
years,
totalInvestment,
interestIncome,
taxes,
netIncome,
};
this.$emit("calculation-done", calculatedData);
this.oneTimeInvestment = null;
this.selectedFundId = "";
this.years = null;
}
},
},
};
</script>
<style scoped>
/* Add your component-specific styles here */
label {
display: block;
margin-top: 10px;
}
input,
select,
button {
margin-top: 0px;
border-radius: 0;
}
button {
background-color: #747264;
color: #fff;
font-weight: 700;
padding: 10px;
font-size: 1.2em;
}
h2 {
color: #747264;
text-align: left;
}
</style>
@@ -0,0 +1,123 @@
<template>
<div class="mt-5 offer-detail">
<h2>The offer</h2>
<hr />
<div class="row">
<div class="col your-data">
<h4>Your data</h4>
<p>
Investment: <span>{{ formatAsMoneyGBP(newInvestment.oneTimeInvestment) }}</span>
</p>
<p class="period">Period: <span>{{ newInvestment.years }} years</span></p>
<p class="fund">Fund: <span>{{ newInvestment.fund.name }}</span></p>
<p class="risk">Risk: <span>{{ newInvestment.fund.risk }}</span></p>
</div>
<div class="col our-calculations">
<h4>Our calculations</h4>
<p>Net income: <span> {{ formatAsMoneyGBP(newInvestment.netIncome) }}</span></p>
<p>
Interest income: <span>{{ formatAsMoneyGBP(newInvestment.interestIncome) }}</span>
</p>
<p>
<span>
Total income:
{{
formatAsMoneyGBP(
newInvestment.netIncome + newInvestment.interestIncome
)
}}
</span>
</p>
</div>
</div>
<div class="input-group mb-3">
<input class="form-control" aria-describedby="inputGroup-sizing-default" type="text" id="customerName"
data-test="customer-name" v-model="customerName" placeholder="enter your name" />
</div>
<button class="btn btn-success mx-3" @click="createInvestment" data-test="create-investment">
Create Investment
</button>
<button class="btn btn-danger mx-3" @click="rejectOffer" data-test="decline-investment">
Reject Offer
</button>
</div>
</template>
<script>
export default {
props: {
newInvestment: Object, // Pass the calculatedData as a prop
},
data() {
return {
customerName: "",
};
},
methods: {
formatAsMoneyGBP(number) {
// Use Intl.NumberFormat to format the number as GBP
const formatter = new Intl.NumberFormat("en-GB", {
style: "currency",
currency: "GBP",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
// Return the formatted string
return formatter.format(number);
},
createInvestment() {
if (this.customerName) {
console.log("emitting");
this.$emit("create-investment", this.customerName);
// Optionally, clear the input field after creating the investment
this.customerName = "";
}
},
rejectOffer() {
// Handle the rejection, e.g., emit an event or perform other actions
this.$emit("reject-offer");
},
},
};
</script>
<style scoped lang="scss">
div.offer-detail {
button {
font-weight: 700;
border-radius: 0;
}
input {
border-radius: 0;
}
hr {
background-color: #fff;
}
color: #fff;
background-color: #747264;
padding: 20px;
h4 {
text-align: left;
}
p {
text-align: left;
span {
font-weight: 800;
}
}
}
h2 {
text-align: left;
font-weight: 900;
}
</style>
@@ -0,0 +1,164 @@
<template>
<div>
<h2>Gringotts Investment List</h2>
<ul class="investment-list mt-3">
<li class="my-2" v-for="(investment, index) in investments" :key="index">
<h4>
Investment issued for <span>{{ investment.customerName }}</span>
<hr />
</h4>
<div class="row">
<div class="col">
<p>
Investment:
{{ formatAsMoneyGBP(investment.oneTimeInvestment) }}
</p>
<p>Period: {{ investment.years }} years</p>
<p>Fund: {{ investment.fund.name }}</p>
<p>Risk: {{ investment.fund.risk }}</p>
</div>
<div class="col">
<p>Net income: {{ formatAsMoneyGBP(investment.netIncome) }}</p>
<p>
Interest income:
{{ formatAsMoneyGBP(investment.interestIncome) }}
</p>
<p>
<span>
Total income:
{{
formatAsMoneyGBP(
investment.netIncome + investment.interestIncome
)
}}
</span>
</p>
</div>
</div>
<div class="row">
<div class="col">
<button
class="btn btn-primary mr-2 float-left"
id="showDetail"
@click="openInvestmentModal(index)"
>
View Details
</button>
<button
class="btn btn-danger float-left"
id="delete"
@click="deleteInvestment(index)"
>
Delete
</button>
</div>
</div>
</li>
</ul>
<GringottsInvestmentModal
v-if="selectedInvestment !== null"
:investment="selectedInvestment"
:is-open="isModalOpen"
@close-investment-modal="closeInvestmentModal"
/>
</div>
</template>
<script>
import GringottsInvestmentModal from "../gringotts-bank/GringottsInvestmentModal.vue";
export default {
props: {
investments: Array, // Array of investments passed as a prop
},
components: {
GringottsInvestmentModal,
},
data() {
return {
selectedInvestment: null,
isModalOpen: false,
};
},
methods: {
formatAsMoneyGBP(number) {
// Use Intl.NumberFormat to format the number as GBP
const formatter = new Intl.NumberFormat("en-GB", {
style: "currency",
currency: "GBP",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
// Return the formatted string
return formatter.format(number);
},
deleteInvestment(index) {
// Trigger the deletion of the investment at the specified index
this.$emit("delete-investment", index);
},
openInvestmentModal(index) {
this.selectedInvestment = this.investments[index];
this.isModalOpen = true;
},
closeInvestmentModal() {
this.selectedInvestment = null;
this.isModalOpen = false;
},
},
};
</script>
<style scoped lang="scss">
h2 {
color: #747264;
text-align: left;
}
ul.investment-list {
list-style: none;
padding: 0;
li {
hr {
background-color: #fff;
}
h4 {
text-align: left;
span {
font-weight: 900;
}
}
color: #fff;
background-color: #747264;
padding: 20px;
button {
border-radius: 0;
}
button#showDetail {
background-color: #504e44;
border: none;
&:hover {
background-color: #676458;
cursor: pointer;
}
}
p {
text-align: left;
span {
font-weight: 800;
}
}
h5 {
text-align: left;
}
transition: all 200ms ease-in;
&:hover {
background-color: #817f71;
cursor: pointer;
}
}
}
</style>
@@ -0,0 +1,117 @@
<!-- GringottsInvestmentModal.vue -->
<template>
<div v-if="isOpen" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h2>Investment Details</h2>
</div>
<div class="modal-body">
<div class="row">
<div class="col">
<p>Fund: {{ investment.fund.name }}</p>
<p>Risk: {{ investment.fund.risk }}</p>
<p>Name: {{ investment.customerName }}</p>
<p>
Investment: {{ formatAsMoneyGBP(investment.oneTimeInvestment) }}
</p>
<p>Years: {{ investment.years }}</p>
</div>
<div class="col">
<p>Net income: {{ formatAsMoneyGBP(investment.netIncome) }}</p>
<p>
Interest income:
{{ formatAsMoneyGBP(investment.interestIncome) }}
</p>
<p>
Total income:
{{
formatAsMoneyGBP(
investment.netIncome + investment.interestIncome
)
}}
</p>
</div>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-dismiss="modal"
@click="closeInvestmentModal"
id="close"
>
Close
</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
investment: Object, // The specific investment object to display details
isOpen: Boolean, // Whether the modal is open or not
},
methods: {
closeInvestmentModal() {
this.$emit("close-investment-modal"); // Emit an event to close the modal
},
formatAsMoneyGBP(number) {
// Use Intl.NumberFormat to format the number as GBP
const formatter = new Intl.NumberFormat("en-GB", {
style: "currency",
currency: "GBP",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
// Return the formatted string
return formatter.format(number);
},
},
};
</script>
<style scoped lang="scss">
/* Add your modal styles here */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
.modal-dialog {
margin: 0 auto;
width: 1200px;
.modal-content {
color: #fff;
padding: 20px;
border-radius: 0;
background-color: #747264;
border: none;
p {
text-align: left;
}
button#close {
background-color: #504e44;
border: none;
border-radius: 0;
&:hover {
background-color: #676458;
cursor: pointer;
}
}
}
}
}
</style>
@@ -0,0 +1,10 @@
export default {
name: "Modal",
props: ["spell"],
methods: {
close() {
this.$emit("close");
}
}
};
+31
View File
@@ -0,0 +1,31 @@
import Vue from "vue";
import App from "./App.vue";
import axios from "axios";
import VueRouter from "vue-router";
import Spelleology from "../src/components/Spelleology";
import SortingHat from "../src/components/SortingHat";
import Welcome from "../src/components/Welcome";
import QuoteGenerator from "../src/components/QuoteGenerator";
import GringottsBank from "../src/components/GringottsBank";
const axiosconfig = {
baseURL: process.env.VUE_APP_API_URL || "http://localhost:3000",
timeout: 30000,
};
Vue.prototype.$http = axios.create(axiosconfig);
Vue.use(VueRouter);
const router = new VueRouter({
routes: [
{ path: "/spelleology", component: Spelleology },
{ path: "/sortingHat", component: SortingHat },
{ path: "/quotes", component: QuoteGenerator },
{ path: "/gringottsBank", component: GringottsBank },
{ path: "/", component: Welcome },
],
});
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
@@ -0,0 +1,8 @@
module.exports = {
//...
resolve: {
fallback: {
url: require.resolve("url"),
},
},
};
+184
View File
@@ -0,0 +1,184 @@
[
{
"id": "5a05e2b252f721a3cf2ea33f",
"name": "Gryffindor",
"mascot": "lion",
"headOfHouse": "Minerva McGonagall",
"houseGhost": "Nearly Headless Nick",
"founder": "Goderic Gryffindor",
"school": "Hogwarts School of Witchcraft and Wizardry",
"members": [
"5a0fa648ae5bc100213c2332",
"5a0fa67dae5bc100213c2333",
"5a0fa7dcae5bc100213c2338",
"5a107e1ae0686c0021283b19",
"5a10944f3dc2080021cd8755",
"5a10947c3dc2080021cd8756",
"5a1096b33dc2080021cd8762",
"5a1097653dc2080021cd8763",
"5a1098fd3dc2080021cd876e",
"5a109af13dc2080021cd877a",
"5a109bfc3dc2080021cd877f",
"5a109c3d3dc2080021cd8780",
"5a109cb83dc2080021cd8784",
"5a109cd33dc2080021cd8785",
"5a109e143dc2080021cd878d",
"5a109e1e3dc2080021cd878e",
"5a109e253dc2080021cd878f",
"5a109e543dc2080021cd8790",
"5a109f053dc2080021cd8793",
"5a1226520f5ae10021650d76",
"5a1226d70f5ae10021650d77",
"5a12292a0f5ae10021650d7e",
"5a12298d0f5ae10021650d7f",
"5a1229e10f5ae10021650d80",
"5a122cbe0f5ae10021650d89",
"5a1233ff0f5ae10021650d98",
"5a1234cb0f5ae10021650d9b",
"5a1237480f5ae10021650da3",
"5a1237c00f5ae10021650da5",
"5a1238070f5ae10021650da6",
"5a1238350f5ae10021650da7",
"5a12387a0f5ae10021650da8",
"5a1238b20f5ae10021650da9",
"5a1239130f5ae10021650daa",
"5a12393d0f5ae10021650dab",
"5a12395f0f5ae10021650dac",
"5a1239c80f5ae10021650dad",
"5a1239f10f5ae10021650dae",
"5a123b450f5ae10021650db7",
"5a123f130f5ae10021650dcc"
],
"values": [
"courage",
"bravery",
"nerve",
"chivalry"
],
"colors": [
"scarlet",
"gold"
]
},
{
"id": "5a05da69d45bd0a11bd5e06f",
"name": "Ravenclaw",
"mascot": "eagle",
"headOfHouse": "Fillius Flitwick",
"houseGhost": "The Grey Lady",
"founder": "Rowena Ravenclaw",
"school": "Hogwarts School of Witchcraft and Wizardry",
"members": [
"5a0fa8a6ae5bc100213c233b",
"5a107ffee0686c0021283b21",
"5a108023e0686c0021283b22",
"5a108047e0686c0021283b23",
"5a1097d53dc2080021cd8769",
"5a1099cf3dc2080021cd8772",
"5a109a553dc2080021cd8777",
"5a109de83dc2080021cd878c",
"5a109e943dc2080021cd8791",
"5a109ebd3dc2080021cd8792",
"5a1225a20f5ae10021650d73",
"5a12260f0f5ae10021650d75",
"5a122a880f5ae10021650d82",
"5a122ac40f5ae10021650d83",
"5a122aef0f5ae10021650d84",
"5a12368a0f5ae10021650da0",
"5a123e450f5ae10021650dc8"
],
"values": [
"intelligence",
"creativity",
"learning",
"wit"
],
"colors": [
"blue",
" bronze"
]
},
{
"id": "5a05dc8cd45bd0a11bd5e071",
"name": "Slytherin",
"mascot": "serpent",
"headOfHouse": "Severus Snape",
"houseGhost": "The Bloody Baron",
"founder": "Salazar Slytherin",
"members": [
"5a0fa6bbae5bc100213c2334",
"5a0fa6e8ae5bc100213c2335",
"5a0fa74eae5bc100213c2336",
"5a0fa772ae5bc100213c2337",
"5a107e4ae0686c0021283b1a",
"5a1093eb3dc2080021cd8753",
"5a10942a3dc2080021cd8754",
"5a1099353dc2080021cd876f",
"5a109aa03dc2080021cd8779",
"5a109d893dc2080021cd8789",
"5a109d9f3dc2080021cd878a",
"5a109dab3dc2080021cd878b",
"5a109f8b3dc2080021cd8795",
"5a109fa83dc2080021cd8796",
"5a109fde3dc2080021cd8797",
"5a1225640f5ae10021650d72",
"5a1225e30f5ae10021650d74",
"5a122c200f5ae10021650d88",
"5a1232600f5ae10021650d93",
"5a12327c0f5ae10021650d94",
"5a1233bc0f5ae10021650d97",
"5a1235060f5ae10021650d9c",
"5a1236eb0f5ae10021650da1",
"5a123a710f5ae10021650db1",
"5a123af40f5ae10021650db5"
],
"values": [
"ambition",
"cunning",
"leadership",
"resourcefulness"
],
"colors": [
"green",
"silver"
]
},
{
"id": "5a05dc58d45bd0a11bd5e070",
"name": "Hufflepuff",
"mascot": "badger",
"headOfHouse": "Pomona Sprout",
"houseGhost": "The Fat Friar",
"founder": "Helga Hufflepuff",
"school": "Hogwarts School of Witchcraft and Wizardry",
"members": [
"5a0fa11a4d153d00212c47cc",
"5a0fa360ae5bc100213c232c",
"5a0fa365ae5bc100213c232d",
"5a0fa4daae5bc100213c232e",
"5a0fa842ae5bc100213c2339",
"5a0fa86dae5bc100213c233a",
"5a1096253dc2080021cd875f",
"5a1098bd3dc2080021cd876d",
"5a109c993dc2080021cd8783",
"5a1223720f5ae10021650d6f",
"5a1223ed0f5ae10021650d70",
"5a122f3d0f5ae10021650d8d",
"5a1232b10f5ae10021650d95",
"5a12333f0f5ae10021650d96",
"5a1234500f5ae10021650d99",
"5a1235790f5ae10021650d9d",
"5a123cb40f5ae10021650dbc"
],
"values": [
"hard work",
"patience",
"justice",
"loyalty"
],
"colors": [
"yellow",
"black"
]
}
]
+14
View File
@@ -0,0 +1,14 @@
let jwt = require('jsonwebtoken');
module.exports = checkToken;
function checkToken(req, res, next) {
jwt.verify(req.token, 'secretkey', (err, authData) => {
if (err) {
res.status(403);
res.send(err);
} else {
next();
}
})
}
+6
View File
@@ -0,0 +1,6 @@
exports.checkHeader = (req, res, next) => {
if (!req.is('application/json')) {
res.status(400).send({ message: "Sorry Nick you dont have HEADer" });
}
next();
}
@@ -0,0 +1,9 @@
let spells = require('../controllers/spellsController.js')
exports.spell_not_found = function (req, res, next) {
let result = spells.find(spell => spell._id == req.params.spellId);
if (typeof result == 'undefined') {
return res.status(404).send({ message: "Spell not found" });
}
next();
}
File diff suppressed because it is too large. Load diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "PotterApi",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "nodemon app.js"
},
"engines": {
"node": "18.x"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.17.1",
"express-basic-auth": "^1.2.0",
"express-query-boolean": "^2.0.0",
"joi": "^17.6.0",
"jsonwebtoken": "^8.5.1",
"nodemon": "^2.0.2",
"random-id": "^1.0.3"
}
}
+310
View File
@@ -0,0 +1,310 @@
[
{
"quote": "Mr. and Mrs. Dursley of number four, Privet Drive, were proud to say that they were perfectly normal, thank you very much.",
"author": "unknown"
},
{
"quote": "I hope you're pleased with yourselves. We could all have been killed — or worse, expelled. Now if you don't mind, I'm going to bed.",
"author": "Hermione Granger"
},
{
"quote": "It does not do well to dwell on dreams and forget to live.",
"author": "Albus Dumbledore"
},
{
"quote": "To the well-organized mind, death is but the next great adventure.",
"author": "Albus Dumbledore"
},
{
"quote": "You're a little scary sometimes, you know that? Brilliant ... but scary.",
"author": "Ron Weasley"
},
{
"quote": "There will be no foolish wand-waving or silly incantations in this class. As such, I don't expect many of you to appreciate the subtle science and exact art that is potion-making. However, for those select few who possess the predisposition, I can teach you how to bewitch the mind and ensnare the senses. I can tell you how to bottle fame, brew glory, and even put a stopper in death. Then again, maybe some of you have come to Hogwarts in possession of abilities so formidable that you feel confident enough to not pay attention!",
"author": "Severus Snape"
},
{
"quote": "It takes a great deal of bravery to stand up to our enemies, but just as much to stand up to our friends.",
"author": "Albus Dumbledore"
},
{
"quote": "Yer a wizard Harry.",
"author": "Rubeus Hagrid"
},
{
"quote": "There are all kinds of courage. It takes a great deal of bravery to stand up to our enemies, but just as much to stand up to our friends.",
"author": "Albus Dumbledore"
},
{
"quote": "The truth. It is a beautiful and terrible thing, and should therefore be treated with great caution.",
"author": "Albus Dumbledore"
},
{
"quote": "There are some things you can’t share without ending up liking each other, and knocking out a twelve-foot mountain troll is one of them.",
"author": "unknown"
},
{
"quote": "Fear of a name only increases fear of the thing itself.",
"author": "Hermione Granger"
},
{
"quote": "Dobby is free.",
"author": "Dobby"
},
{
"quote": "Training for the ballet, Potter?",
"author": "Draco Malfoy"
},
{
"quote": "I’ll be in my bedroom, making no noise and pretending I’m not there.",
"author": "Harry Potter"
},
{
"quote": "Hearing voices no one else can hear isn’t a good sign, even in the wizarding world.",
"author": "Ron Weasley"
},
{
"quote": "When in doubt, go to the library.",
"author": "Ron Weasley"
},
{
"quote": "Fawkes is a phoenix, Harry. Phoenixes burst into flame when it is time for them to die and are reborn from the ashes.",
"author": "Albus Dumbledore"
},
{
"quote": "Fame is a fickle friend, Harry. Celebrity is as celebrity does. Remember that.",
"author": "Gilderoy Lockhart"
},
{
"quote": "Honestly, if you were any slower, you’d be going backward.",
"author": "Draco Malfoy"
},
{
"quote": "I solemnly swear I am up to no good.",
"author": "Harry Potter"
},
{
"quote": "Honestly, am I the only person who's ever bothered to read 'Hogwarts: A History?",
"author": "Hermione Granger"
},
{
"quote": "But you know, happiness can be found even in the darkest of times, if one only remembers to turn on the light.",
"author": "Albus Dumbledore"
},
{
"quote": "Don’t let the muggles get you down.",
"author": "Ron Weasley"
},
{
"quote": "I want to commit the murder I was imprisoned for.",
"author": "Sirius Black"
},
{
"quote": "Why, dear boy, we don’t send wizards to Azkaban just for blowing up their aunts.",
"author": "Cornelius Fudge"
},
{
"quote": "You think the dead we loved truly ever leave us? You think that we don’t recall them more clearly in times of great trouble?",
"author": "Albus Dumbledore"
},
{
"quote": "Mischief Managed!",
"author": "Harry Potter"
},
{
"quote": "If you want to know what a man’s like, take a good look at how he treats his inferiors, not his equals.",
"author": "Sirius Black"
},
{
"quote": "I am what I am, an' I'm not ashamed. 'Never be ashamed,' my ol' dad used ter say, 'there's some who'll hold it against you, but they're not worth botherin' with.'",
"author": "Rubeus Hagrid"
},
{
"quote": "It matters not what someone is born, but what they grow to be.",
"author": "Albus Dumbledore"
},
{
"quote": "We are only as strong as we are united, as weak as we are divided.",
"author": "Albus Dumbledore"
},
{
"quote": "Your devotion is nothing more than cowardice. You would not be here if you had anywhere else to go.",
"author": "Voldemort"
},
{
"quote": "Numbing the pain for a while will make it worse when you finally feel it.",
"author": "Albus Dumbledore"
},
{
"quote": "Anyone can speak Troll. All you have to do is point and grunt.",
"author": "Fred Weasley"
},
{
"quote": "Curiosity is not a sin…. But we should exercise caution with our curiosity… yes, indeed.",
"author": "Albus Dumbledore"
},
{
"quote": "Twitchy little ferret, aren’t you, Malfoy?",
"author": "Hermione Granger"
},
{
"quote": "Just because it’s taken you three years to notice, Ron, doesn’t mean no one else has spotted I’m a girl!",
"author": "Hermione Granger"
},
{
"quote": "Differences of habit and language are nothing at all if our aims are identical and our hearts are open.",
"author": "Albus Dumbledore"
},
{
"quote": "Give her hell from us, Peeves.' And Peeves, whom Harry had never seen take an order from a student before, swept his belled hat from his head and sprang to a salute as Fred and George wheeled about to tumultuous applause from the students below and sped out of the open front doors into the glorious sunset.",
"author": "Harry Potter"
},
{
"quote": "I think we've outgrown full-time education ... Time to test our talents in the real world, d'you reckon?,",
"author": "Fred Weasley"
},
{
"quote": "I think I'll just go down and have some pudding and wait for it all to turn up — it always does in the end.",
"author": "Luna Lovegood"
},
{
"quote": "Just because you have the emotional range of a teaspoon doesn't mean we all have.",
"author": "Hermione Granger"
},
{
"quote": "You can laugh, but people used to believe there were no such things as the Blibbering Humdinger or the Crumple-Horned Snorkack!",
"author": "Luna Lovegood"
},
{
"quote": "I mean, it's sort of exciting, isn't it, breaking the rules?",
"author": "Hermione Granger"
},
{
"quote": "We’ve all got both light and dark inside us. What matters is the part we choose to act on. That’s who we really are.",
"author": "Sirius Black"
},
{
"quote": "Indifference and neglect often do much more damage than outright dislike.",
"author": "Albus Dumbledore"
},
{
"quote": "Youth can not know how age thinks and feels. But old men are guilty if they forget what it was to be young.",
"author": "Albus Dumbledore"
},
{
"quote": "Things we lose have a way of coming back to us in the end, if not always in the way we expect.",
"author": "Luna Lovegood"
},
{
"quote": "Wit beyond measure is man’s greatest treasure.",
"author": "Luna Lovegood"
},
{
"quote": "'The thing about growing up with Fred and George is that you sort of start thinking anything's possible if you've got enough nerve.'",
"author": "Ginny Weasley"
},
{
"quote": "And now, Harry, let us step out into the night and pursue that flighty temptress, adventure.",
"author": "Albus Dumbledore"
},
{
"quote": "Dumbledore says people find it far easier to forgive others for being wrong than being right.",
"author": "Hermione Granger"
},
{
"quote": "There is no need to call me ‘sir,’ Professor.",
"author": "Harry Potter"
},
{
"quote": "You’re just as sane as I am.",
"author": "Luna Lovegood"
},
{
"quote": "I am a wizard, not a baboon brandishing a stick.",
"author": "unknown"
},
{
"quote": "Age is foolish and forgetful when it underestimates youth.",
"author": "Albus Dumbledore"
},
{
"quote": "Once again, you show all the sensitivity of a blunt axe.",
"author": "Nearly Headless Nick"
},
{
"quote": "It is the unknown we fear when we look upon death and darkness, nothing more.",
"author": "Albus Dumbledore"
},
{
"quote": "It is the quality of one’s convictions that determines success, not the number of followers.",
"author": "Remus Lupin"
},
{
"quote": "Every human life is worth the same, and worth saving.",
"author": "Kingsley Shacklebolt"
},
{
"quote": "After all this time? Always.",
"author": "Severus Snape"
},
{
"quote": "Words are, in my not-so-humble opinion, our most inexhaustible source of magic. Capable of both inflicting injury, and remedying it.",
"author": "Albus Dumbledore"
},
{
"quote": "Not my daughter, you b*tch!",
"author": "Molly Weasley"
},
{
"quote": "It is a curious thing, Harry, but perhaps those who are best suited to power are those who have never sought it. Those who, like you, have leadership thrust upon them, and take up the mantle because they must, and find to their own surprise that they wear it well.",
"author": "Albus Dumbledore"
},
{
"quote": "He can run faster than Severus Snape confronted with shampoo.",
"author": "Fred Weasley"
},
{
"quote": "Of course it is happening inside your head, Harry, but why on earth should that mean that it is not real?",
"author": "Albus Dumbledore"
},
{
"quote": "I've always wanted to use that spell.",
"author": "Minerva McGonagall"
},
{
"quote": "No post on Sundays.",
"author": "Vernon Dursley"
},
{
"quote": "We could all have been killed—or worse, expelled.",
"author": "Hermione Granger"
},
{
"quote": "Never trust anything that can think for itself if you can’t see where it keeps its brain.",
"author": "Arthur Weasley"
},
{
"quote": "Why couldn’t it be ‘follow the butterflies’?",
"author": "Ron Weasley"
},
{
"quote": "Riddikulus",
"author": "Remus Lupin"
},
{
"quote": "The ones that love us never really leave us.",
"author": "Sirius Black"
},
{
"quote": "Have a biscuit, Potter.",
"author": "Minerva McGonagall"
},
{
"quote": "Just because you have the emotional range of a teaspoon doesn’t mean we all have.",
"author": "Hermione Granger"
},
{
"quote": "Život je ako rebrík do kurína - krátky a osratý",
"author": "Furbyho babka"
}
]
+21
View File
@@ -0,0 +1,21 @@
const express = require('express');
const router = express.Router();
const charactersController = require('../controllers/charactersController.js');
var basicAuth = require('express-basic-auth');
router.use(basicAuth({
users: { 'admin': 'supersecret' },
unauthorizedResponse: {
message: "Sorry Wizard, can't let you in."
}
}));
router.get('/', charactersController.characters_list);
router.post('/', charactersController.new_character);
router.delete('/actions/deleteAll', charactersController.delete_all);
router.get('/actions/reset', charactersController.reset);
router.get('/:characterId', charactersController.specific_character);
router.delete('/:characterId', charactersController.delete_character);
router.put('/:characterId', charactersController.update_character);
module.exports = router;
+13
View File
@@ -0,0 +1,13 @@
const express = require('express');
const router = express.Router();
const houseController = require('../controllers/housesController.js')
module.exports = router;
const verifyToken = require('../app.js');
const checkToken = require('../middleware/auth.js');
router.use(verifyToken, checkToken);
router.get('/', houseController.houses_list);
router.get('/:houseId', houseController.specific_house)
+12
View File
@@ -0,0 +1,12 @@
const express = require('express');
const router = express.Router();
module.exports = router;
const quotes = require('../quotes.json')
function getRandomQuote() {
return quotes[Math.floor(Math.random() * quotes.length)];
}
router.get('/', function (req, res) {
res.json(getRandomQuote())
});
+25
View File
@@ -0,0 +1,25 @@
const express = require('express');
const router = express.Router();
module.exports = router;
const houses = require('../houses.json')
const sortingHatQuotes = require('../sortinghatquotes.json')
function getRandomHouseName() {
let houseNames = houses.map(house => house.name);
return houseNames[Math.floor(Math.random() * houseNames.length)];
}
function getRandomQuote() {
return sortingHatQuotes[Math.floor(Math.random() * sortingHatQuotes.length)];
}
router.get('/', function (req, res) {
let house = getRandomHouseName();
setTimeout(() => {
return res.json({
sortingHatSays: getRandomQuote() + ' ' + house,
house: house
})
}, 500)
});
+23
View File
@@ -0,0 +1,23 @@
const express = require('express');
const router = express.Router();
const spells = require('../spells.json');
const spellController = require('../controllers/spellsController.js')
module.exports = router;
const authChecker = require('../middleware/header.js')
router.get('/', spellController.spells_list);
router.post('/', authChecker.checkHeader, spellController.new_spell);
router.get('/:spellId', spellController.specific_spell)
router.delete('/:spellId', spellController.delete_spell)
router.delete('/actions/deleteAll', spellController.delete_all)
router.get('/actions/reset', spellController.reset)
router.put('/:spellId', authChecker.checkHeader, spellController.update_spell)
@@ -0,0 +1,9 @@
[
"Move your ass to",
"You are a coward without future, your house is",
"Your broom is too magnificent, you should study at",
"Oh shit, that's right I'm a hat. And your house is",
"Stick your wand into",
"Hmm, difficult. VERY difficult. Plenty of courage, I see. But where to put you? All right it's",
"I'm just a hat. My only task is to pick random house..Like this one."
]
File diff suppressed because it is too large. Load diff