[Added] Most of the frontend, split up backend services

This commit is contained in:
Ninjaws
2024-10-31 23:05:22 +01:00
parent 2505fce920
commit 3188f6ce20
48 changed files with 1391 additions and 398 deletions
+16
View File
@@ -12,3 +12,19 @@ Not being able to gather blocks of 50 is a huge issue. The idea of reusing block
I can't force a big chunk, because it might just return a status 1, and then the user gets nothing.
Perhaps I can separate the questions myself and make a collection, that i can build the requests from. However, for every quest tracking if someone has already seen is a huge pain.
## Possible solution
Cache all data, 50 at a time
Store it in a database
When its done, let the db make blocks for quick cache access
Then randomize when someone asks for an item from the cache
Or return a failure if there are not enough items (a la the api itself)
Every night, create a new db in the background and start filling it (should take ~34 minutes for all 20k items the external API has)
When it's full, replace the old database with the new one, and delete the old one.
Then start replacing the cache items one at a time.
Users shouldn't feel this at all, because the active sessions are stored in their own cache items
@@ -0,0 +1,20 @@
package com.ninjaws.quiz.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // Allow all endpoints
.allowedOrigins("http://localhost:4200") // Allow specific origin
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // Allow specific methods
.allowedHeaders("*") // Allow all headers
.allowCredentials(true); // Allow credentials
}
}
@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ninjaws.quiz.models.Answers;
import com.ninjaws.quiz.models.QuizSettings;
import com.ninjaws.quiz.models.Session;
import com.ninjaws.quiz.services.QuizService;
@@ -24,12 +25,14 @@ public class QuizController {
@GetMapping("/questions")
public String startSession(
@RequestParam(required = true) String amount,
@RequestParam(required = false) Integer category,
@RequestParam(required = false) String difficulty,
@RequestParam(required = false) String type
@RequestParam(required = true) Integer amount,
@RequestParam(required = false) Integer category,
@RequestParam(required = false) String difficulty,
@RequestParam(required = false) String type
) {
return quizService.createSession(quizService.jsonToQuizSettings("todo"));
QuizSettings quizSettings = new QuizSettings(amount, category, difficulty, type);
String jsonString = String.format("{\"sessionId\":\"%s\"}", quizService.createSession(quizSettings));
return jsonString;
}
@GetMapping("/status/{sessionId}")
@@ -37,8 +40,9 @@ public class QuizController {
return quizService.checkSession(sessionId);
}
@PostMapping("/checkanswers")
@PostMapping("/score")
public String checkAnswers(@RequestBody Answers answers) {
return quizService.getScore(answers);
String jsonString = String.format("{\"score\":\"%s\"}", quizService.getScore(answers));
return jsonString;
}
}
@@ -8,9 +8,11 @@ import lombok.Setter;
@Getter
@Setter
public class Answers {
private final String sessionId;
private String sessionId;
private List<String> answers;
public Answers() {}
public Answers(String sessionId) {
this.sessionId = sessionId;
}
@@ -1,5 +1,6 @@
package com.ninjaws.quiz.models;
import com.ninjaws.quiz.models.QuizSettings.Category;
import com.ninjaws.quiz.models.QuizSettings.Difficulty;
import jakarta.validation.Valid;
@@ -54,6 +55,21 @@ public class QuizSettings {
private Category category = Category.ANY;
public QuizSettings(){}
/** Inverse of toApiQuery */
public QuizSettings(int amount, Integer category, String difficulty, String type) {
System.out.println(amount);
System.out.print(category);
System.out.print(difficulty);
System.out.print(type);
this.amount = amount;
this.category = category == null ? Category.ANY : Category.values()[category];
this.difficulty = difficulty == null ? Difficulty.ANY : Difficulty.valueOf(difficulty.toUpperCase());
this.type = type == null ? Type.ANY : type.equals("boolean") ? Type.BOOLEAN : Type.MULTIPLE;
}
/**
* Ensures the value is between 1 and 50
* @Valid doesn't seem to be enforcing it, so this ensures the value is legal
@@ -10,6 +10,7 @@ import lombok.Setter;
@Setter
public class Session {
private final String id;
private int statusCode = 0;
private List<Question> questions;
public Session() {
@@ -12,48 +12,18 @@ import jakarta.validation.Valid;
@Service
public class ApiService {
// @Autowired
// private ObjectMapper objectMapper;
private final RestTemplate restTemplate;
@Autowired
private RestTemplate restTemplate;
private final String apiUrl = "https://opentdb.com/api.php";
// private String sessionToken = "";
@Autowired
public ApiService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
// try {
// sessionToken = requestSessionToken();
// } catch (Exception e) {
// // Failed to get a session token, setting default to none
// sessionToken = "";
// }
public ApiService() {
}
private String buildRequestString(QuizSettings quizSettings) {
return apiUrl + quizSettings.toApiQuery();
}
// private int getResponseCode(String json) throws Exception {
// JsonNode jsonNode = objectMapper.readTree(json);
// return jsonNode.get("response_code").asInt();
// }
// public String requestSessionToken() throws Exception {
// String sessionTokenRequestUrl = "https://opentdb.com/api_token.php?command=request";
// ResponseEntity<String> response = restTemplate.getForEntity(sessionTokenRequestUrl, String.class);
// JsonNode jsonNode = objectMapper.readTree(response.getBody());
// if (jsonNode.get("response_code").asInt() == 0) {
// sessionToken = jsonNode.get("token").asText();
// }else {
// // Failed to get a session token, setting default to none
// sessionToken = "";
// }
// return sessionToken;
// }
public String requestQuestions(@Valid QuizSettings quizSettings) {
String url = buildRequestString(quizSettings);
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
@@ -71,4 +41,8 @@ public class ApiService {
// }
return jsonData;
}
public void handleStatusCode(int statusCode) {
}
}
@@ -0,0 +1,38 @@
package com.ninjaws.quiz.services;
import java.util.concurrent.TimeUnit;
import org.springframework.stereotype.Service;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.ninjaws.quiz.models.Session;
@Service
public class CacheService {
/**
* Stores all active sessions
* Gets added to when a user requests data
* Gets removed from when a user requests their score (by sending their answers)
*/
private final Cache<String,Session> sessions;
public CacheService() {
sessions = (Caffeine.newBuilder()
.expireAfterWrite(1,TimeUnit.HOURS)
.build());
}
public void addToCache(String key, Session session) {
sessions.put(key, session);
}
public Session getFromCache(String sessionId) {
return sessions.getIfPresent(sessionId);
}
public void removeFromCache(String sessionId) {
sessions.invalidate(sessionId);
}
}
@@ -0,0 +1,16 @@
package com.ninjaws.quiz.services;
import org.springframework.stereotype.Service;
import com.ninjaws.quiz.models.QuizSettings;
@Service
public class CleanerService {
public CleanerService() {}
public QuizSettings jsonToQuizSettings(String json){
QuizSettings quizSettings = new QuizSettings();
return quizSettings;
}
}
@@ -0,0 +1,27 @@
package com.ninjaws.quiz.services;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.springframework.stereotype.Service;
import com.ninjaws.quiz.models.Request;
@Service
public class QueueService {
/**
* Stores all user requests for data
* Executes them in order, one every 5 seconds (the limit of the external API)
* Removes them from the Queue when the request was succesful
*/
private final BlockingQueue<Request> requests = new LinkedBlockingQueue<>();
public void addToQueue(Request request) {
requests.add(request);
}
public Request popFromQueue() {
return requests.poll();
}
}
@@ -1,22 +1,11 @@
package com.ninjaws.quiz.services;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.ninjaws.quiz.models.Answers;
import com.ninjaws.quiz.models.ApiResponse;
import com.ninjaws.quiz.models.Question;
import com.ninjaws.quiz.models.QuizSettings;
import com.ninjaws.quiz.models.Request;
import com.ninjaws.quiz.models.Session;
@@ -26,40 +15,9 @@ import com.ninjaws.quiz.models.Session;
public class QuizService {
@Autowired
private ApiService apiService;
private CacheService cacheService;
@Autowired
private ObjectMapper objectMapper;
/**
* Stores all active sessions
* Gets added to when a user requests data
* Gets removed from when a user requests their score (by sending their answers)
*/
// private final Map<String,Session> sessions = new ConcurrentHashMap<>();
private final Cache<String,Session> sessions;
/**
* Stores all user requests for data
* Executes them in order, one every 5 seconds (the limit of the external API)
* Removes them from the Queue when the request was succesful
*/
private final BlockingQueue<Request> requests = new LinkedBlockingQueue<>();
public QuizService() {
sessions = (Caffeine.newBuilder()
.expireAfterWrite(1,TimeUnit.HOURS)
.build());
}
public QuizService(Cache<String, Session> cache) {
sessions = cache;
}
public QuizSettings jsonToQuizSettings(String json){
QuizSettings quizSettings = new QuizSettings();
return quizSettings;
}
private QueueService queueService;
/**
* Actions:
@@ -71,11 +29,8 @@ public class QuizService {
*/
public String createSession(QuizSettings quizSettings) {
Session session = new Session();
try {
requests.put(new Request(session.getId(), quizSettings));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
cacheService.addToCache(session.getId(), session);
queueService.addToQueue(new Request(session.getId(), quizSettings));
return session.getId();
}
@@ -88,7 +43,7 @@ public class QuizService {
* @return The data if it's available, Optional.empty() if not, and an Exception if the session is invalid
*/
public Optional<Session> checkSession(String sessionId) throws IllegalArgumentException {
Session session = sessions.getIfPresent(sessionId);
Session session = cacheService.getFromCache(sessionId);
if(session == null) {
throw new IllegalArgumentException("Invalid session ID: " + sessionId);
@@ -97,41 +52,9 @@ public class QuizService {
if (session.getQuestions() == null) {
return Optional.empty();
}
System.out.print(session.getQuestions().get(0).getIncorrectAnswers().get(0));
return Optional.of(session);
}
/**
* Every 5 seconds:
* - Take the top request from the queue (removes it)
* - Perform the request
* - Wait for the data
* - Find the matching session from the cache
* - Add the data to the session in the cache
*/
@Scheduled(fixedDelay = 5000)
protected void processQueue() {
System.out.print("Schedule!");
Request request = requests.poll();
if (request != null) {
System.out.print("----------------An item!");
String json = apiService.requestQuestions(request.getQuizSettings());
System.out.print(json);
try {
List<Question> questions = extractQuestions(json);
Session session = sessions.getIfPresent(request.getSessionId());
session.setQuestions(questions);
sessions.put(request.getSessionId(), session);
} catch (IOException e) {
//TODO: Log this
}
}
}
private List<Question> extractQuestions(String json) throws IOException {
return objectMapper.readValue(json, ApiResponse.class).getResults();
}
/**
* Receives: sessionId and answers
*
@@ -142,16 +65,25 @@ public class QuizService {
* - Calculates score
* Returns: calculated score
* */
public String getScore(Answers answers) {
Session session = sessions.getIfPresent(answers.getSessionId());
sessions.invalidate(session.getId());
public int getScore(Answers answers) {
Session session = cacheService.getFromCache(answers.getSessionId());
cacheService.removeFromCache(session.getId());
return calculateScore(session, answers);
}
private String calculateScore(Session session, Answers answers) {
System.out.print(session);
System.out.print(answers);
return "You did great!";
}
private int calculateScore(Session session, Answers answers) {
int amountCorrect = 0;
for(int i = 0; i < session.getQuestions().size(); i++) {
String correctAnswer = session.getQuestions().get(i).getCorrectAnswer();
String userAnswer = answers.getAnswers().get(i);
// for(String answer : session.getQuestions().get(i).getAnswers()) {
// System.out.print(answer);
// }
if (correctAnswer.equalsIgnoreCase(userAnswer)) {
amountCorrect++;
}
}
return amountCorrect;
}
}
@@ -0,0 +1,59 @@
package com.ninjaws.quiz.services;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ninjaws.quiz.models.ApiResponse;
import com.ninjaws.quiz.models.Question;
import com.ninjaws.quiz.models.Request;
import com.ninjaws.quiz.models.Session;
@Service
public class SchedulerService {
@Autowired
private QueueService queueService;
@Autowired
private ApiService apiService;
@Autowired
private CacheService cacheService;
@Autowired
private ObjectMapper objectMapper;
private static final System.Logger logger = System.getLogger(SchedulerService.class.getName());
public SchedulerService() {}
/**
* Every 5 seconds:
* - Take the top request from the queue (removes it)
* - Perform the request
* - Wait for the data
* - Find the matching session from the cache
* - Add the data to the session in the cache
*/
@Scheduled(fixedDelay = 5000)
protected void processQueue() {
Request request = queueService.popFromQueue();
if (request != null) {
String json = apiService.requestQuestions(request.getQuizSettings());
try {
List<Question> questions = extractQuestions(json);
Session session = cacheService.getFromCache(request.getSessionId());
session.setQuestions(questions);
cacheService.addToCache(request.getSessionId(), session);
} catch (IOException | NullPointerException e) {
logger.log(System.Logger.Level.ERROR, "Critical failure", e);
}
}
}
private List<Question> extractQuestions(String json) throws IOException {
return objectMapper.readValue(json, ApiResponse.class).getResults();
}
}
@@ -0,0 +1,5 @@
package com.ninjaws.quiz.services;
public class StorageService {
}
@@ -1,11 +1,10 @@
package com.ninjaws.quiz.services;
import org.junit.jupiter.api.TestInstance;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
// @EnableScheduling
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
// @TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class QuizServiceTest {
// @InjectMocks
+4 -1
View File
@@ -25,7 +25,10 @@
"Angular.ng-template",
"dbaeumer.vscode-eslint",
"Zignd.html-css-class-completion",
"esbenp.prettier-vscode"
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"ecmel.vscode-html-css",
"pranaygp.vscode-css-peek"
]
}
}
-21
View File
@@ -1,21 +0,0 @@
# FROM node:22-bookworm-slim
# ARG WORKDIR
# WORKDIR ${WORKDIR}
# RUN apt-get update && \
# apt-get install -y git && \
# apt-get clean && \
# rm -rf /var/lib/apt/lists/*
# # Install Angular CLI globally
# RUN npm install -g @angular/cli
# # 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"]
+3
View File
@@ -98,5 +98,8 @@
}
}
}
},
"cli": {
"analytics": false
}
}
+499 -237
View File
@@ -25,15 +25,31 @@
"@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"
}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@@ -193,6 +209,35 @@
}
}
},
"node_modules/@angular-devkit/build-angular/node_modules/postcss": {
"version": "8.4.41",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/@angular-devkit/build-angular/node_modules/tslib": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
@@ -450,6 +495,36 @@
"typescript": ">=5.4 <5.6"
}
},
"node_modules/@angular/compiler-cli/node_modules/chokidar": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/compiler-cli/node_modules/readdirp": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/core": {
"version": "18.2.9",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.9.tgz",
@@ -4962,6 +5037,13 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true,
"license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -4989,6 +5071,13 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -5474,6 +5563,16 @@
"node": ">=6"
}
},
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001673",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001673.tgz",
@@ -5520,19 +5619,41 @@
"license": "MIT"
},
"node_modules/chokidar": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 14.16.0"
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/chownr": {
@@ -6322,6 +6443,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true,
"license": "MIT"
},
"node_modules/dns-packet": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
@@ -8605,31 +8740,6 @@
"source-map-support": "^0.5.5"
}
},
"node_modules/karma/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/karma/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@@ -8649,19 +8759,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/karma/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/karma/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -8672,32 +8769,6 @@
"node": ">=8"
}
},
"node_modules/karma/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/karma/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/karma/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -8924,6 +8995,16 @@
}
}
},
"node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -9780,6 +9861,18 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
"object-assign": "^4.0.1",
"thenify-all": "^1.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@@ -10210,6 +10303,16 @@
"node": ">=0.10.0"
}
},
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/object-inspect": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
@@ -10716,6 +10819,16 @@
"node": ">=6"
}
},
"node_modules/pirates": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/piscina": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/piscina/-/piscina-4.6.1.tgz",
@@ -10743,9 +10856,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.41",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"dev": true,
"funding": [
{
@@ -10764,13 +10877,100 @@
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
"picocolors": "^1.1.0",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-import": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.0.0",
"read-cache": "^1.0.0",
"resolve": "^1.1.7"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"postcss": "^8.0.0"
}
},
"node_modules/postcss-js": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"camelcase-css": "^2.0.1"
},
"engines": {
"node": "^12 || ^14 || >= 16"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
"peerDependencies": {
"postcss": "^8.4.21"
}
},
"node_modules/postcss-load-config": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"lilconfig": "^3.0.0",
"yaml": "^2.3.4"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"postcss": ">=8.0.9",
"ts-node": ">=9.0.0"
},
"peerDependenciesMeta": {
"postcss": {
"optional": true
},
"ts-node": {
"optional": true
}
}
},
"node_modules/postcss-load-config/node_modules/lilconfig": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
"integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/postcss-loader": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz",
@@ -10873,6 +11073,32 @@
"postcss": "^8.1.0"
}
},
"node_modules/postcss-nested": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "^6.1.1"
},
"engines": {
"node": ">=12.0"
},
"peerDependencies": {
"postcss": "^8.2.14"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
@@ -11054,6 +11280,26 @@
"node": ">= 0.8"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pify": "^2.3.0"
}
},
"node_modules/read-cache/node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -11070,17 +11316,29 @@
}
},
"node_modules/readdirp": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/readdirp/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.16.0"
"node": ">=8.6"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/reflect-metadata": {
@@ -11513,70 +11771,6 @@
}
}
},
"node_modules/sass/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/sass/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/sass/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/sass/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/sax": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
@@ -12410,6 +12604,86 @@
"node": ">=6"
}
},
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
"commander": "^4.0.0",
"glob": "^10.3.10",
"lines-and-columns": "^1.1.6",
"mz": "^2.7.0",
"pirates": "^4.0.1",
"ts-interface-checker": "^0.1.9"
},
"bin": {
"sucrase": "bin/sucrase",
"sucrase-node": "bin/sucrase-node"
},
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/sucrase/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/sucrase/node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/sucrase/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sucrase/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -12446,6 +12720,44 @@
"node": ">=0.10"
}
},
"node_modules/tailwindcss": {
"version": "3.4.14",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz",
"integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.3.0",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.21.0",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
"picocolors": "^1.0.0",
"postcss": "^8.4.23",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.1",
"postcss-nested": "^6.0.1",
"postcss-selector-parser": "^6.0.11",
"resolve": "^1.22.2",
"sucrase": "^3.32.0"
},
"bin": {
"tailwind": "lib/cli.js",
"tailwindcss": "lib/cli.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -12637,6 +12949,29 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
}
},
"node_modules/thenify-all": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/thingies": {
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz",
@@ -12720,6 +13055,13 @@
"tree-kill": "cli.js"
}
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/tslib": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
@@ -13525,35 +13867,6 @@
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/vite/node_modules/postcss": {
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.0",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
@@ -13752,31 +14065,6 @@
"balanced-match": "^1.0.0"
}
},
"node_modules/webpack-dev-server/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/webpack-dev-server/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -13798,19 +14086,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/webpack-dev-server/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/webpack-dev-server/node_modules/http-proxy-middleware": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
@@ -13852,32 +14127,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/webpack-dev-server/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/webpack-dev-server/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/webpack-dev-server/node_modules/rimraf": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
@@ -14190,6 +14439,19 @@
"dev": true,
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
"integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==",
"dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+3
View File
@@ -27,12 +27,15 @@
"@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"
}
}
+4
View File
@@ -0,0 +1,4 @@
export interface Answers {
sessionId: string;
answers: 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,6 @@
export interface QuizParams {
category: string;
type: string;
amount: number;
difficulty: string;
}
+4
View File
@@ -0,0 +1,4 @@
export interface Score {
amount_questions: number;
amount_correct: number;
}
@@ -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,131 @@
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';
@Injectable({
providedIn: 'root',
})
export class QuizService {
private apiUrl = 'http://localhost:9090/quiz';
constructor(private http: HttpClient, private storageService: StorageService) {}
public async startSession(quizParams: QuizParams) {
const sessionIdObj = await firstValueFrom(this.http.get<any>(this.apiUrl + '/questions' + this.convertParamsToUrl(quizParams)));
this.storageService.setSession(sessionIdObj.sessionId);
const questionsObj = await lastValueFrom(this.pollQuestions(sessionIdObj.sessionId));
return this.cleanQuestionStrings(questionsObj.questions) as Question[];
}
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)
);
}
private 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;
}
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 = this.getCategories().indexOf(quizParams.category);
url += catIndex !== 0 ? `&category=${catIndex}` : '';
const diff = quizParams.difficulty.toLowerCase();
url += diff !== 'any difficulty' ? `&category=${diff}` : '';
const typeIndex = this.getTypes().indexOf(quizParams.type);
url +=
typeIndex !== 0
? typeIndex === 1
? '&type=multiple'
: '&type=boolean'
: '';
return url;
}
public getCategories(): string[] {
return [
'Any Category',
'General Knowledge',
'Entertainment: Books',
'Entertainment: Film',
'Entertainment: Music',
'Entertainment: Musicals & Theatres',
'Entertainment: Television',
'Entertainment: Video Games',
'Entertainment: Board Games',
'Science & Nature',
'Science: Computers',
'Science: Mathematics',
'Mythology',
'Sports',
'Geography',
'History',
'Politics',
'Art',
'Celebrities',
'Animals',
'Vehicles',
'Entertainment: Comics',
'Science: Gadgets',
'Entertainment: Japanese Anime & Manga',
'Entertainment: Cartoon & Animations',
];
}
public getDifficulties(): string[] {
return ['Any Difficulty', 'Easy', 'Medium', 'Hard'];
}
public getTypes(): string[] {
return ['Any Type', 'Multiple choice', 'True/False'];
}
}
@@ -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 quizService.getCategories()" [value]="category">{{ category }}</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,60 @@
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';
@Component({
selector: 'app-setup',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './setup.component.html',
styleUrl: './setup.component.scss',
})
export class SetupComponent {
category: string = "";
difficulty: string = "";
type: string = "";
amount: number = 10;
constructor(
private router: Router,
public quizService: QuizService,
private spinnerService: SpinnerService
) {
/** Setting default values */
this.category = this.quizService.getCategories()[0];
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[]) => {
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) { }
}
+4 -1
View File
@@ -1 +1,4 @@
<h1>Quiz App</h1>
<!-- <div class="h-screen"> -->
<router-outlet></router-outlet>
<!-- </div> -->
<app-spinner></app-spinner>
+2 -1
View File
@@ -1,10 +1,11 @@
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],
imports: [RouterOutlet, SpinnerComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
+5 -1
View File
@@ -2,7 +2,11 @@ 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)]
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient()]
};
+5 -1
View File
@@ -1,3 +1,7 @@
import { Routes } from '@angular/router';
export const routes: Routes = [];
export const routes: Routes = [
{ path: '', loadChildren: () => import('./Quiz/quiz.module').then(m => m.QuizModule) }, // Lazy load QuizModule
// { path: '**', redirectTo: '/quiz' } // Handle unmatched routes
];
+26
View File
@@ -1 +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: [],
}