Added Project

This commit is contained in:
Ninjaws
2024-11-06 22:13:00 +01:00
commit 10c4aa8831
107 changed files with 18082 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
{
"name": "Quiz Frontend",
"build": {
"context": "..",
"dockerfile": "../Dockerfile",
"args": {
// "WORKDIR": "/app"
},
"target": "development"
},
"runArgs": [
"--name=quiz-frontend-dev-cont",
"--net=host" // Allows it to be opened on the host network
],
"workspaceFolder": "/app",
"mounts": [
"source=.,target=/app,type=bind"
],
"customizations": {
"vscode": {
"settings": {},
"extensions": [
"christian-kohler.npm-intellisense",
"Angular.ng-template",
"dbaeumer.vscode-eslint",
"Zignd.html-css-class-completion",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"ecmel.vscode-html-css",
"pranaygp.vscode-css-peek"
]
}
}
}
+9
View File
@@ -0,0 +1,9 @@
node_modules/
.git/
.gitignore
README.md
LICENSE
.vscode/
Dockerfile*
docker-compose*
npm-debug.log
+17
View File
@@ -0,0 +1,17 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false
+42
View File
@@ -0,0 +1,42 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db
+32
View File
@@ -0,0 +1,32 @@
FROM node:22-bookworm-slim AS base
WORKDIR /app
ENV NG_CLI_ANALYTICS=ci
RUN npm install -g @angular/cli
#--- Development
FROM base AS development
RUN apt-get update && \
apt-get install -y git && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Switching user so it doesn't use root
# If root adds files, it takes ownership, making them unremovable on the host except by using sudo
# node tranfers ownership to the host user
USER node
EXPOSE 4200
CMD ["tail","-f","/dev/null"]
#--- Building
FROM base AS build
COPY package*.json ./
RUN npm install
# Copied here, since the the Development setup uses a bind volume instead
COPY . .
RUN ng build --configuration production
#--- Production
FROM nginx:alpine AS production
COPY --from=build /app/dist/quiz/browser /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+111
View File
@@ -0,0 +1,111 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"quiz": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/quiz",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "quiz:build:production"
},
"development": {
"buildTarget": "quiz:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
}
}
}
},
"cli": {
"analytics": false
}
}
+61
View File
@@ -0,0 +1,61 @@
server {
listen 80;
server_name quiz.ianvink.nl;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_set_header Connection '';
proxy_http_version 1.1;
proxy_read_timeout 360s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# add_header 'Access-Control-Allow-Origin' '*'; # Adjust this to your frontend domain
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; # Include POST
# add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization'; # Include any other headers
proxy_pass http://backend:9090/;
}
}
# server {
# listen 80;
# server_name quiz.ianvink.nl;
# location /.well-known/acme-challenge/ {
# root /usr/share/nginx/html; # Adjust this if needed
# }
# location / {
# return 301 https://$host$request_uri;
# }
# }
# server {
# listen 443 ssl;
# server_name quiz.example.com;
# ssl_certificate /etc/letsencrypt/live/quiz.example.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/quiz.example.com/privkey.pem;
# location / {
# root /usr/share/nginx/html;
# index index.html;
# try_files $uri $uri/ /index.html;
# }
# location /api/ {
# proxy_pass http://backend:9090/; # Adjust this if needed
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
# }
+14549
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
{
"name": "quiz",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^18.2.0",
"@angular/common": "^18.2.0",
"@angular/compiler": "^18.2.0",
"@angular/core": "^18.2.0",
"@angular/forms": "^18.2.0",
"@angular/platform-browser": "^18.2.0",
"@angular/platform-browser-dynamic": "^18.2.0",
"@angular/router": "^18.2.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.10"
},
"devDependencies": {
"@angular-devkit/build-angular": "^18.2.10",
"@angular/cli": "^18.2.10",
"@angular/compiler-cli": "^18.2.0",
"@types/jasmine": "~5.1.0",
"autoprefixer": "^10.4.20",
"jasmine-core": "~5.2.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14",
"typescript": "~5.5.2"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+4
View File
@@ -0,0 +1,4 @@
export interface Answers {
sessionId: string;
answers: string[];
}
+4
View File
@@ -0,0 +1,4 @@
export interface Category {
id: number;
name: string;
}
+11
View File
@@ -0,0 +1,11 @@
export interface Question {
question: string;
answers: string[];
category: string;
type: string;
difficulty: string;
/** To make special characters visible */
question_decoded?: string;
answers_decoded?: string[];
}
@@ -0,0 +1,8 @@
import { Category } from "./category";
export interface QuizParams {
category: number;
type: string;
amount: number;
difficulty: string;
}
+4
View File
@@ -0,0 +1,4 @@
export interface Score {
amount_questions: number;
amount_correct: number;
}
+7
View File
@@ -0,0 +1,7 @@
import { Question } from "./question";
export interface Session {
id: string;
statusCode: number;
questions: Question[];
}
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SetupComponent } from './setup/setup.component';
import { QuizComponent } from './quiz/quiz.component';
import { ScoreComponent } from './score/score.component';
const routes: Routes = [
{ path: '', component: SetupComponent },
{ path: 'quiz', component: QuizComponent },
{ path: 'score', component: ScoreComponent },
{ path: '**', redirectTo: '' }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class QuizRoutingModule { }
+15
View File
@@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { QuizRoutingModule } from './quiz-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
QuizRoutingModule
],
providers: []
})
export class QuizModule { }
@@ -0,0 +1,19 @@
<div class="flex items-center justify-center min-h-screen bg-gray-100">
<div class="bg-white p-8 rounded-lg shadow-md w-80 overflow-y-scroll max-h-3/4 no-scrollbar">
<div *ngFor="let question of questions; let i = index" class="mb-6">
<h2 class="text-lg font-semibold mb-2">{{ question.question_decoded }}</h2>
<div *ngFor="let answer of question.answers_decoded; let j = index">
<label class="block mb-1">
<input type="radio" name="question{{ i }}" [value]="answer" (change)="selectAnswer(i, question.answers[j])"
[checked]="selectedAnswers[i] === answer" class="mr-2" />
{{ answer }}
</label>
</div>
</div>
<button (click)="getScore()" [disabled]="!allQuestionsAnswered()"
class="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600 transition disabled:opacity-50 ">
Get Score
</button>
</div>
</div>
@@ -0,0 +1,3 @@
.no-scrollbar {
scrollbar-width: none;
}
@@ -0,0 +1,61 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { Score } from '../models/score';
import { Question } from '../models/question';
import { QuizService } from '../services/quiz.service';
@Component({
selector: 'app-quiz',
standalone: true,
imports: [CommonModule],
templateUrl: './quiz.component.html',
styleUrl: './quiz.component.scss'
})
export class QuizComponent {
questions: Question[] = [];
selectedAnswers: { [key: number]: string } = {};
constructor(private router: Router, private quizService: QuizService) {
const navigation = this.router.getCurrentNavigation();
if (navigation?.extras?.state) {
this.questions = navigation.extras.state['questions'];
}else {
// You didn't come here through the proper channels, byebye
this.router.navigate(['']);
}
}
selectAnswer(questionIndex: number, answer: string) {
this.selectedAnswers[questionIndex] = answer;
}
allQuestionsAnswered(): boolean {
return Object.keys(this.selectedAnswers).length === this.questions.length;
}
getScore() {
const answers: string[] = []
for(let i = 0; i < Object.keys(this.selectedAnswers).length; i++) {
answers.push(this.selectedAnswers[i]);
}
this.quizService.getScore(answers)
.then((result:number) => {
const score: Score = {
amount_correct:result,
amount_questions: Object.keys(this.selectedAnswers).length
}
this.router.navigate(['/score'], { state: { score: score },skipLocationChange:true });
})
.catch((e) => {
alert("Something went wrong");
});
// const sampleScore: Score = {
// amount_questions: 10,
// amount_correct: 7
// }
}
}
@@ -0,0 +1,11 @@
<div class="flex flex-col items-center justify-center h-screen bg-gray-100">
<h1 class="text-4xl font-bold mb-4">Your Score</h1>
<p class="text-2xl">
{{ score.amount_correct }} / {{ score.amount_questions }}
</p>
<button
(click)="playAgain()"
class="mt-6 bg-blue-500 text-white p-2 rounded hover:bg-blue-600">
Play Again
</button>
</div>
@@ -0,0 +1,29 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Score } from '../models/score';
@Component({
selector: 'app-score',
standalone: true,
imports: [],
templateUrl: './score.component.html',
styleUrl: './score.component.scss'
})
export class ScoreComponent {
score !: Score;
constructor(private router: Router) {
const navigation = this.router.getCurrentNavigation();
if (navigation?.extras?.state) {
this.score = navigation.extras.state['score'];
}else {
// You didn't come here through the proper channels, byebye
this.router.navigate(['']);
}
}
playAgain() {
this.router.navigate(['']);
}
}
@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { Question } from '../models/question';
@Injectable({
providedIn: 'root'
})
export class CleanerService {
constructor() { }
/**
* Sets decoded strings for the questions and the answers, so the special characters will be shown instead of their codes
*/
public cleanQuestionStrings(questions: Question[]): Question[] {
for(let i = 0; i < questions.length; i++) {
questions[i].question_decoded = this.decodeString(questions[i].question);
questions[i].answers_decoded = [];
for(let j = 0; j < questions[i].answers.length; j++){
questions[i].answers_decoded!.push(this.decodeString(questions[i].answers[j]));
}
}
return questions;
}
/** Puts special characters back into the string, like ' and " */
private decodeString(text: string) {
const txt = document.createElement("textarea");
txt.innerHTML = text;
const decodedValue = txt.value;
txt.remove(); // Clean up the temporary element
return decodedValue;
}
}
@@ -0,0 +1,100 @@
import { Injectable } from '@angular/core';
import { QuizParams } from '../models/quiz-params';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import {
catchError,
filter,
firstValueFrom,
interval,
lastValueFrom,
of,
startWith,
switchMap,
take,
takeWhile,
} from 'rxjs';
import { Question } from '../models/question';
import { Answers } from '../models/answers';
import { StorageService } from './storage.service';
import { CleanerService } from './cleaner.service';
import { ResponseHandlerService } from './response-handler.service';
import { environment } from '../../../environments/environment';
import { Category } from '../models/category';
@Injectable({
providedIn: 'root',
})
export class QuizService {
private apiUrl = environment.apiUrl + '/quiz';
constructor(
private http: HttpClient,
private storageService: StorageService,
private cleanerService: CleanerService,
private reponseHandler: ResponseHandlerService) {}
public async startSession(quizParams: QuizParams): Promise<Question[] | null> {
const sessionIdObj = await firstValueFrom(this.http.get<any>(this.apiUrl + '/questions' + this.convertParamsToUrl(quizParams)));
/** No need to poll if the session is not created. Likely means the queue is full */
if(sessionIdObj.statusCode !== 0) {
return this.reponseHandler.handleResponse(sessionIdObj.statusCode);
}
this.storageService.setSession(sessionIdObj.sessionId);
const sessionObj = await lastValueFrom(this.pollQuestions(sessionIdObj.sessionId));
return this.reponseHandler.handleResponse(sessionObj.statusCode, sessionObj);
}
public pollQuestions(sessionId: string) {
return interval(2000).pipe(
startWith(0),
switchMap(() => this.http.get<any>(this.apiUrl + `/status/${sessionId}`)),
// takeWhile((response) => response === null),
filter((response) => response !== null),
take(1)
);
}
public async getScore(answers: string[]) {
const answersJson: Answers = {
sessionId: this.storageService.getSession()!,
answers: answers
}
const headers = new HttpHeaders({'Content-Type': 'application/json'});
const scoreObj = await firstValueFrom(this.http.post<any>(this.apiUrl+'/score', answersJson, { headers }));
return scoreObj.score;
}
private convertParamsToUrl(quizParams: QuizParams) {
let url = `?amount=${quizParams.amount}`;
const catIndex = quizParams.category;
url += catIndex !== 0 ? `&category=${catIndex}` : '';
const diff = quizParams.difficulty.toLowerCase();
url += diff !== 'any difficulty' ? `&difficulty=${diff}` : '';
const typeIndex = this.getTypes().indexOf(quizParams.type);
url +=
typeIndex !== 0
? typeIndex === 1
? '&type=multiple'
: '&type=boolean'
: '';
return url;
}
public async getCategories(): Promise<Category[]> {
let categories: Category[] = await firstValueFrom(this.http.get<any>(this.apiUrl + '/categories'));
categories.push({id:0,name:"Any Category"});
categories.sort((a,b) => a.id - b.id);
return categories;
}
public getDifficulties(): string[] {
return ['Any Difficulty', 'Easy', 'Medium', 'Hard'];
}
public getTypes(): string[] {
return ['Any Type', 'Multiple choice', 'True/False'];
}
}
@@ -0,0 +1,36 @@
import { Injectable } from '@angular/core';
import { Session } from '../models/session';
import { CleanerService } from './cleaner.service';
import { Question } from '../models/question';
@Injectable({
providedIn: 'root',
})
export class ResponseHandlerService {
constructor(private cleanerService: CleanerService) {}
/**
* Session is optional, except for the success state (0)
*/
public handleResponse(statusCode: number, session?: Session): Question[] | null {
switch (statusCode) {
case 0:
// Success
session!.questions = this.cleanerService.cleanQuestionStrings(session!.questions);
return session!.questions;
case 1:
// No Results (requests too much data for this combination of parameters)
alert('Not enough data for this request, please try a different combination');
return null;
case 2:
// Invalid parameter (should be handled by the backend)
alert('Invalid input');
return null;
case 5:
// Queue is full, try again later
alert('The server is experiencing a lot of traffic, please try again later');
return null;
}
return null;
}
}
@@ -0,0 +1,17 @@
import { Injectable } from "@angular/core";
@Injectable({
providedIn: 'root',
})
export class StorageService {
constructor() {
}
public setSession(sessionId: string): void {
localStorage.setItem("sessionId", sessionId);
}
public getSession(): string | null {
return localStorage.getItem("sessionId");
}
}
@@ -0,0 +1,35 @@
<div class="flex items-center justify-center min-h-screen bg-gray-100">
<div class="bg-white p-8 rounded-lg shadow-md w-80">
<h1 class="text-2xl font-bold mb-6 text-center bg-red">Quiz App</h1>
<div class="mb-4">
<label for="category" class="block mb-1">Category</label>
<select id="category" class="w-full p-2 border rounded" [(ngModel)]="category">
<option *ngFor="let category of categories" [value]="category.id">{{ category.name }}</option>
</select>
</div>
<div class="mb-4">
<label for="difficulty" class="block mb-1">Difficulty</label>
<select id="difficulty" class="w-full p-2 border rounded" [(ngModel)]="difficulty">
<option *ngFor="let difficulty of quizService.getDifficulties()" class="focus:outline-none" [value]="difficulty">{{ difficulty }}</option>
</select>
</div>
<div class="mb-4">
<label for="type" class="block mb-1">Type</label>
<select id="type" class="w-full p-2 border rounded" [(ngModel)]="type">
<option *ngFor="let type of quizService.getTypes()" [value]="type">{{ type }}</option>
</select>
</div>
<div class="mb-4">
<label for="amount" class="block mb-1">Amount</label>
<input type="number" id="amount" [(ngModel)]="amount" min="1" max="50" class="w-full p-2 border rounded" />
</div>
<button class="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600 transition" (click)="startQuiz()">
Start Quiz
</button>
</div>
</div>
@@ -0,0 +1,66 @@
import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { QuizService } from '../services/quiz.service';
import { QuizParams } from '../models/quiz-params';
import { SpinnerService } from '../../Spinner/services/spinner.service';
import { CommonModule } from '@angular/common';
import { Question } from '../models/question';
import { Category } from '../models/category';
@Component({
selector: 'app-setup',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './setup.component.html',
styleUrl: './setup.component.scss',
})
export class SetupComponent {
categories!: Category[];
category!: number;
difficulty: string = "";
type: string = "";
amount: number = 10;
constructor(
private router: Router,
public quizService: QuizService,
private spinnerService: SpinnerService
) {
quizService.getCategories().then((categories: Category[]) => {
/** Setting default values */
this.categories = categories;
this.category = this.categories.find(item => item.id == 0)!.id;
this.difficulty = this.quizService.getDifficulties()[0];
this.type = this.quizService.getTypes()[0];
});
}
startQuiz() {
const params: QuizParams = {
category: this.category,
type: this.type,
amount: this.amount,
difficulty: this.difficulty,
};
this.spinnerService.setActive(true);
this.quizService.startSession(params).then((questions: Question[] | null) => {
if(questions !== null) {
this.gotoQuiz(questions);
}
})
.catch((e) => {alert("Something went wrong, please try again");})
.finally(() => {this.spinnerService.setActive(false);});
}
gotoQuiz(questions: Question[]) {
this.router.navigate(['/quiz'], {
state: { questions: questions },
skipLocationChange: true,
});
}
}
@@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class SpinnerService {
private isActive = false;
constructor() { }
toggleSpinner() {
this.isActive = !this.isActive;
}
setActive(active: boolean) {
new Promise(
(resolve) => setTimeout(resolve, 0)).then(
_ =>this.isActive = active
)
}
isSpinnerActive() {
return this.isActive;
}
}
@@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
declarations: [],
imports: []
})
export class SpinnerModule { }
@@ -0,0 +1,5 @@
<div class="spinner" *ngIf="spinnerService.isSpinnerActive()">
<div class="spinner-content">
<div class="lds-spinner"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
</div>
</div>
@@ -0,0 +1,98 @@
.lds-spinner {
color: green;
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-spinner div {
transform-origin: 40px 40px;
animation: lds-spinner 1.2s linear infinite;
}
.lds-spinner div:after {
content: " ";
display: block;
position: absolute;
top: 3px;
left: 37px;
width: 6px;
height: 18px;
border-radius: 20%;
background: #fff;
}
.lds-spinner div:nth-child(1) {
transform: rotate(0deg);
animation-delay: -1.1s;
}
.lds-spinner div:nth-child(2) {
transform: rotate(30deg);
animation-delay: -1s;
}
.lds-spinner div:nth-child(3) {
transform: rotate(60deg);
animation-delay: -0.9s;
}
.lds-spinner div:nth-child(4) {
transform: rotate(90deg);
animation-delay: -0.8s;
}
.lds-spinner div:nth-child(5) {
transform: rotate(120deg);
animation-delay: -0.7s;
}
.lds-spinner div:nth-child(6) {
transform: rotate(150deg);
animation-delay: -0.6s;
}
.lds-spinner div:nth-child(7) {
transform: rotate(180deg);
animation-delay: -0.5s;
}
.lds-spinner div:nth-child(8) {
transform: rotate(210deg);
animation-delay: -0.4s;
}
.lds-spinner div:nth-child(9) {
transform: rotate(240deg);
animation-delay: -0.3s;
}
.lds-spinner div:nth-child(10) {
transform: rotate(270deg);
animation-delay: -0.2s;
}
.lds-spinner div:nth-child(11) {
transform: rotate(300deg);
animation-delay: -0.1s;
}
.lds-spinner div:nth-child(12) {
transform: rotate(330deg);
animation-delay: 0s;
}
@keyframes lds-spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.spinner {
position: fixed; /* Stay in place */
z-index: var(--z-spinner);
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
.spinner-content {
width:100%;
height:100%;
display:flex;
align-items: center;
justify-content: center;
}
@@ -0,0 +1,22 @@
import { animate, style, transition, trigger } from '@angular/animations';
import { Component } from '@angular/core';
import { SpinnerService } from '../services/spinner.service';
import { CommonModule } from '@angular/common';
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
@Component({
selector: 'app-spinner',
standalone: true,
imports: [CommonModule],
templateUrl: './spinner.component.html',
styleUrl: './spinner.component.scss',
styles: [],
animations: [
trigger('fadeInOut', [
transition(':enter', [style({ opacity:0}), animate("300ms ease-in-out")]),
transition(':leave', [animate("300ms ease-in-out", style({ opacity: 0}))]),
])]
})
export class SpinnerComponent {
constructor(public spinnerService: SpinnerService) { }
}
+2
View File
@@ -0,0 +1,2 @@
<router-outlet></router-outlet>
<app-spinner></app-spinner>
View File
+29
View File
@@ -0,0 +1,29 @@
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have the 'quiz' title`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('quiz');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, quiz');
});
});
+14
View File
@@ -0,0 +1,14 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { SpinnerComponent } from './Spinner/spinner/spinner.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, SpinnerComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'quiz';
}
+12
View File
@@ -0,0 +1,12 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient()]
};
+7
View File
@@ -0,0 +1,7 @@
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', loadChildren: () => import('./Quiz/quiz.module').then(m => m.QuizModule) }, // Lazy load QuizModule
// { path: '**', redirectTo: '/quiz' } // Handle unmatched routes
];
@@ -0,0 +1,7 @@
export const environment = {
production: true,
/** Connecting through the docker compose network is not working for now, so I'll have to use the localhost */
// apiUrl: 'http://backend:9090',
apiUrl: '/api' //'https://api.nosotros.ianvink.nl',
// apiUrl: 'http://localhost:9090',
};
+4
View File
@@ -0,0 +1,4 @@
export const environment = {
production: false,
apiUrl: 'http://localhost:9090'
};
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Quiz</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="question.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
+27
View File
@@ -0,0 +1,27 @@
/* You can add global styles to this file, and also import other style files */
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
html, body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
display: flex;
}
app-root {
flex: 1; /* Makes the AppComponent fill the available space */
// display:flex;
// justify-content: center;
// align-items: center;
}
:root {
// --z-base: 0;
// --z-dropdown: 100;
// --z-modal: 200;
// --z-tooltip: 300;
--z-spinner: 1800;
}
+11
View File
@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{html,ts}"
],
theme: {
extend: {},
},
plugins: [],
}
+15
View File
@@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}
+33
View File
@@ -0,0 +1,33 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"isolatedModules": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"lib": [
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
+15
View File
@@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}