Oviya
commited on
Commit
·
679d3a0
1
Parent(s):
7a7b728
deploy app
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .editorconfig +16 -0
- .gitattributes +10 -0
- .gitignore +42 -0
- .vscode/extensions.json +4 -0
- .vscode/launch.json +19 -0
- .vscode/tasks.json +42 -0
- README.md +37 -6
- angular.json +103 -0
- karma.conf.js +44 -0
- nuget.config +10 -0
- obj/Debug/py-match.esproj.CoreCompileInputs.cache +0 -0
- obj/Debug/py-match.esproj.FileListAbsolute.txt +7 -0
- package-lock.json +0 -0
- package.json +40 -0
- py-match.esproj +10 -0
- py-match.esproj.user +8 -0
- src/app/api.service.ts +18 -0
- src/app/app-routing.module.ts +37 -0
- src/app/app.component.css +0 -0
- src/app/app.component.html +1 -0
- src/app/app.component.spec.ts +27 -0
- src/app/app.component.ts +79 -0
- src/app/auth/sign-in/sign-in.component.css +134 -0
- src/app/auth/sign-in/sign-in.component.html +52 -0
- src/app/auth/sign-in/sign-in.component.spec.ts +21 -0
- src/app/auth/sign-in/sign-in.component.ts +63 -0
- src/app/auth/sign-up/sign-up.component.css +147 -0
- src/app/auth/sign-up/sign-up.component.html +76 -0
- src/app/auth/sign-up/sign-up.component.spec.ts +21 -0
- src/app/auth/sign-up/sign-up.component.ts +100 -0
- src/app/auth/sign-up/sign-up.service.ts +37 -0
- src/app/intro-page/intro-page.component.css +380 -0
- src/app/intro-page/intro-page.component.html +100 -0
- src/app/intro-page/intro-page.component.spec.ts +21 -0
- src/app/intro-page/intro-page.component.ts +92 -0
- src/app/llm-quiz/llm-quiz.component.css +274 -0
- src/app/llm-quiz/llm-quiz.component.html +142 -0
- src/app/llm-quiz/llm-quiz.component.spec.ts +21 -0
- src/app/llm-quiz/llm-quiz.component.ts +244 -0
- src/app/question-answer/question-answer-service.service.ts +50 -0
- src/app/question-answer/question-answer.component.css +317 -0
- src/app/question-answer/question-answer.component.html +67 -0
- src/app/question-answer/question-answer.component.spec.ts +21 -0
- src/app/question-answer/question-answer.component.ts +157 -0
- src/app/quiz/quiz.component.css +126 -0
- src/app/quiz/quiz.component.html +35 -0
- src/app/quiz/quiz.component.spec.ts +21 -0
- src/app/quiz/quiz.component.ts +107 -0
- src/app/quiz/quiz.service.spec.ts +16 -0
- src/app/quiz/quiz.service.ts +30 -0
.editorconfig
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Editor configuration, see https://editorconfig.org
|
| 2 |
+
root = true
|
| 3 |
+
|
| 4 |
+
[*]
|
| 5 |
+
charset = utf-8
|
| 6 |
+
indent_style = space
|
| 7 |
+
indent_size = 2
|
| 8 |
+
insert_final_newline = true
|
| 9 |
+
trim_trailing_whitespace = true
|
| 10 |
+
|
| 11 |
+
[*.ts]
|
| 12 |
+
quote_type = single
|
| 13 |
+
|
| 14 |
+
[*.md]
|
| 15 |
+
max_line_length = off
|
| 16 |
+
trim_trailing_whitespace = false
|
.gitattributes
CHANGED
|
@@ -33,3 +33,13 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.gif filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
*.webp filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
*.svg filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
*.otf filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
*.ttf filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
*.woff filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
*.woff2 filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# Compiled output
|
| 4 |
+
/dist
|
| 5 |
+
/tmp
|
| 6 |
+
/out-tsc
|
| 7 |
+
/bazel-out
|
| 8 |
+
|
| 9 |
+
# Node
|
| 10 |
+
/node_modules
|
| 11 |
+
npm-debug.log
|
| 12 |
+
yarn-error.log
|
| 13 |
+
|
| 14 |
+
# IDEs and editors
|
| 15 |
+
.idea/
|
| 16 |
+
.project
|
| 17 |
+
.classpath
|
| 18 |
+
.c9/
|
| 19 |
+
*.launch
|
| 20 |
+
.settings/
|
| 21 |
+
*.sublime-workspace
|
| 22 |
+
|
| 23 |
+
# Visual Studio Code
|
| 24 |
+
.vscode/*
|
| 25 |
+
!.vscode/settings.json
|
| 26 |
+
!.vscode/tasks.json
|
| 27 |
+
!.vscode/launch.json
|
| 28 |
+
!.vscode/extensions.json
|
| 29 |
+
.history/*
|
| 30 |
+
|
| 31 |
+
# Miscellaneous
|
| 32 |
+
/.angular/cache
|
| 33 |
+
.sass-cache/
|
| 34 |
+
/connect.lock
|
| 35 |
+
/coverage
|
| 36 |
+
/libpeerconnection.log
|
| 37 |
+
testem.log
|
| 38 |
+
/typings
|
| 39 |
+
|
| 40 |
+
# System files
|
| 41 |
+
.DS_Store
|
| 42 |
+
Thumbs.db
|
.vscode/extensions.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
| 3 |
+
"recommendations": ["angular.ng-template"]
|
| 4 |
+
}
|
.vscode/launch.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "0.2.0",
|
| 3 |
+
"configurations": [
|
| 4 |
+
{
|
| 5 |
+
"type": "edge",
|
| 6 |
+
"request": "launch",
|
| 7 |
+
"name": "localhost (Edge)",
|
| 8 |
+
"url": "http://localhost:4200",
|
| 9 |
+
"webRoot": "${workspaceFolder}"
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"type": "chrome",
|
| 13 |
+
"request": "launch",
|
| 14 |
+
"name": "localhost (Chrome)",
|
| 15 |
+
"url": "http://localhost:4200",
|
| 16 |
+
"webRoot": "${workspaceFolder}"
|
| 17 |
+
}
|
| 18 |
+
]
|
| 19 |
+
}
|
.vscode/tasks.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
| 3 |
+
"version": "2.0.0",
|
| 4 |
+
"tasks": [
|
| 5 |
+
{
|
| 6 |
+
"type": "npm",
|
| 7 |
+
"script": "start",
|
| 8 |
+
"isBackground": true,
|
| 9 |
+
"problemMatcher": {
|
| 10 |
+
"owner": "typescript",
|
| 11 |
+
"pattern": "$tsc",
|
| 12 |
+
"background": {
|
| 13 |
+
"activeOnStart": true,
|
| 14 |
+
"beginsPattern": {
|
| 15 |
+
"regexp": "(.*?)"
|
| 16 |
+
},
|
| 17 |
+
"endsPattern": {
|
| 18 |
+
"regexp": "bundle generation complete"
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"type": "npm",
|
| 25 |
+
"script": "test",
|
| 26 |
+
"isBackground": true,
|
| 27 |
+
"problemMatcher": {
|
| 28 |
+
"owner": "typescript",
|
| 29 |
+
"pattern": "$tsc",
|
| 30 |
+
"background": {
|
| 31 |
+
"activeOnStart": true,
|
| 32 |
+
"beginsPattern": {
|
| 33 |
+
"regexp": "(.*?)"
|
| 34 |
+
},
|
| 35 |
+
"endsPattern": {
|
| 36 |
+
"regexp": "bundle generation complete"
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
]
|
| 42 |
+
}
|
README.md
CHANGED
|
@@ -1,10 +1,41 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: PY-Match
|
| 3 |
+
emoji: 🎯
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: static
|
| 7 |
+
app_build_command: npm ci && npm run build -- --configuration production
|
| 8 |
+
app_file: dist/py-match/index.html
|
| 9 |
+
fullWidth: true
|
| 10 |
---
|
| 11 |
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# PyMatch
|
| 16 |
+
|
| 17 |
+
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.1.0.
|
| 18 |
+
|
| 19 |
+
## Development server
|
| 20 |
+
|
| 21 |
+
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
|
| 22 |
+
|
| 23 |
+
## Code scaffolding
|
| 24 |
+
|
| 25 |
+
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
| 26 |
+
|
| 27 |
+
## Build
|
| 28 |
+
|
| 29 |
+
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
| 30 |
+
|
| 31 |
+
## Running unit tests
|
| 32 |
+
|
| 33 |
+
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
| 34 |
+
|
| 35 |
+
## Running end-to-end tests
|
| 36 |
+
|
| 37 |
+
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
| 38 |
+
|
| 39 |
+
## Further help
|
| 40 |
+
|
| 41 |
+
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
angular.json
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
| 3 |
+
"version": 1,
|
| 4 |
+
"newProjectRoot": "projects",
|
| 5 |
+
"projects": {
|
| 6 |
+
"py-match": {
|
| 7 |
+
"projectType": "application",
|
| 8 |
+
"schematics": {},
|
| 9 |
+
"root": "",
|
| 10 |
+
"sourceRoot": "src",
|
| 11 |
+
"prefix": "app",
|
| 12 |
+
"architect": {
|
| 13 |
+
"build": {
|
| 14 |
+
"builder": "@angular-devkit/build-angular:browser",
|
| 15 |
+
"options": {
|
| 16 |
+
"outputPath": "dist/py-match",
|
| 17 |
+
"index": "src/index.html",
|
| 18 |
+
"main": "src/main.ts",
|
| 19 |
+
"polyfills": [
|
| 20 |
+
"zone.js"
|
| 21 |
+
],
|
| 22 |
+
"tsConfig": "tsconfig.app.json",
|
| 23 |
+
"assets": [
|
| 24 |
+
"src/favicon.ico",
|
| 25 |
+
"src/assets"
|
| 26 |
+
],
|
| 27 |
+
"styles": [
|
| 28 |
+
"src/styles.css"
|
| 29 |
+
],
|
| 30 |
+
"scripts": []
|
| 31 |
+
},
|
| 32 |
+
"configurations": {
|
| 33 |
+
"production": {
|
| 34 |
+
"budgets": [
|
| 35 |
+
{
|
| 36 |
+
"type": "initial",
|
| 37 |
+
"maximumWarning": "500kb",
|
| 38 |
+
"maximumError": "1mb"
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"type": "anyComponentStyle",
|
| 42 |
+
"maximumWarning": "4.5kb",
|
| 43 |
+
"maximumError": "6kb"
|
| 44 |
+
}
|
| 45 |
+
],
|
| 46 |
+
"outputHashing": "all"
|
| 47 |
+
},
|
| 48 |
+
"development": {
|
| 49 |
+
"buildOptimizer": false,
|
| 50 |
+
"optimization": false,
|
| 51 |
+
"vendorChunk": true,
|
| 52 |
+
"extractLicenses": false,
|
| 53 |
+
"sourceMap": true,
|
| 54 |
+
"namedChunks": true
|
| 55 |
+
}
|
| 56 |
+
},
|
| 57 |
+
"defaultConfiguration": "production"
|
| 58 |
+
},
|
| 59 |
+
"serve": {
|
| 60 |
+
"builder": "@angular-devkit/build-angular:dev-server",
|
| 61 |
+
"configurations": {
|
| 62 |
+
"production": {
|
| 63 |
+
"browserTarget": "py-match:build:production"
|
| 64 |
+
},
|
| 65 |
+
"development": {
|
| 66 |
+
"browserTarget": "py-match:build:development"
|
| 67 |
+
}
|
| 68 |
+
},
|
| 69 |
+
"defaultConfiguration": "development"
|
| 70 |
+
},
|
| 71 |
+
"extract-i18n": {
|
| 72 |
+
"builder": "@angular-devkit/build-angular:extract-i18n",
|
| 73 |
+
"options": {
|
| 74 |
+
"browserTarget": "py-match:build"
|
| 75 |
+
}
|
| 76 |
+
},
|
| 77 |
+
"test": {
|
| 78 |
+
"builder": "@angular-devkit/build-angular:karma",
|
| 79 |
+
"options": {
|
| 80 |
+
"polyfills": [
|
| 81 |
+
"zone.js",
|
| 82 |
+
"zone.js/testing"
|
| 83 |
+
],
|
| 84 |
+
"tsConfig": "tsconfig.spec.json",
|
| 85 |
+
"assets": [
|
| 86 |
+
"src/favicon.ico",
|
| 87 |
+
"src/assets"
|
| 88 |
+
],
|
| 89 |
+
"styles": [
|
| 90 |
+
"src/styles.css",
|
| 91 |
+
"node_modules/@fortawesome/fontawesome-free/css/all.css"
|
| 92 |
+
],
|
| 93 |
+
"scripts": [],
|
| 94 |
+
"karmaConfig": "karma.conf.js"
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
},
|
| 100 |
+
"cli": {
|
| 101 |
+
"analytics": "c88a5f95-78ff-4d1a-be52-0edf479dc3e1"
|
| 102 |
+
}
|
| 103 |
+
}
|
karma.conf.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = function (config) {
|
| 2 |
+
config.set({
|
| 3 |
+
basePath: '',
|
| 4 |
+
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
| 5 |
+
plugins: [
|
| 6 |
+
require('karma-jasmine'),
|
| 7 |
+
require('karma-chrome-launcher'),
|
| 8 |
+
require('karma-jasmine-html-reporter'),
|
| 9 |
+
require('karma-coverage'),
|
| 10 |
+
require('@angular-devkit/build-angular/plugins/karma')
|
| 11 |
+
],
|
| 12 |
+
client: {
|
| 13 |
+
jasmine: {
|
| 14 |
+
// you can add configuration options for Jasmine here
|
| 15 |
+
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
| 16 |
+
// for example, you can disable the random execution with `random: false`
|
| 17 |
+
// or set a specific seed with `seed: 4321`
|
| 18 |
+
},
|
| 19 |
+
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
| 20 |
+
},
|
| 21 |
+
jasmineHtmlReporter: {
|
| 22 |
+
suppressAll: true // removes the duplicated traces
|
| 23 |
+
},
|
| 24 |
+
coverageReporter: {
|
| 25 |
+
dir: require('path').join(__dirname, './coverage/'),
|
| 26 |
+
subdir: '.',
|
| 27 |
+
reporters: [
|
| 28 |
+
{ type: 'html' },
|
| 29 |
+
{ type: 'text-summary' }
|
| 30 |
+
]
|
| 31 |
+
},
|
| 32 |
+
reporters: ['progress', 'kjhtml'],
|
| 33 |
+
port: 9876,
|
| 34 |
+
colors: true,
|
| 35 |
+
logLevel: config.LOG_INFO,
|
| 36 |
+
autoWatch: true,
|
| 37 |
+
browsers: ['Chrome'],
|
| 38 |
+
singleRun: false,
|
| 39 |
+
restartOnFileChange: true,
|
| 40 |
+
listenAddress: 'localhost',
|
| 41 |
+
hostname: 'localhost'
|
| 42 |
+
});
|
| 43 |
+
};
|
| 44 |
+
|
nuget.config
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<configuration>
|
| 3 |
+
<packageSources>
|
| 4 |
+
<clear />
|
| 5 |
+
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
| 6 |
+
</packageSources>
|
| 7 |
+
<disabledPackageSources>
|
| 8 |
+
<clear />
|
| 9 |
+
</disabledPackageSources>
|
| 10 |
+
</configuration>
|
obj/Debug/py-match.esproj.CoreCompileInputs.cache
ADDED
|
File without changes
|
obj/Debug/py-match.esproj.FileListAbsolute.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
D:\py-match\py-match\py-match\obj\Debug\py-match.esproj.CoreCompileInputs.cache
|
| 2 |
+
D:\py-match\nw\New folder\py-match\py-match\py-match\obj\Debug\py-match.esproj.CoreCompileInputs.cache
|
| 3 |
+
C:\Users\Admin\Desktop\py-match\py-match\py-match\obj\Debug\py-match.esproj.CoreCompileInputs.cache
|
| 4 |
+
C:\Users\Admin\Desktop\Learning\py-match\py-match\py-match\py-match\obj\Debug\py-match.esproj.CoreCompileInputs.cache
|
| 5 |
+
D:\py-match\py-match\py-match\py-match\py-match\py-match\obj\Debug\py-match.esproj.CoreCompileInputs.cache
|
| 6 |
+
D:\py-match\py-match\py-match\py-match\obj\Debug\py-match.esproj.CoreCompileInputs.cache
|
| 7 |
+
D:\py-match\Frontend\py-match\py-match\py-match\obj\Debug\py-match.esproj.CoreCompileInputs.cache
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "py-match",
|
| 3 |
+
"version": "0.0.0",
|
| 4 |
+
"scripts": {
|
| 5 |
+
"ng": "ng",
|
| 6 |
+
"start": "ng serve --host=127.0.0.1",
|
| 7 |
+
"build": "ng build",
|
| 8 |
+
"watch": "ng build --watch --configuration development",
|
| 9 |
+
"test": "ng test"
|
| 10 |
+
},
|
| 11 |
+
"private": true,
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"@angular/animations": "^16.1.0",
|
| 14 |
+
"@angular/common": "^16.1.0",
|
| 15 |
+
"@angular/compiler": "^16.1.0",
|
| 16 |
+
"@angular/core": "^16.1.0",
|
| 17 |
+
"@angular/forms": "^16.1.0",
|
| 18 |
+
"@angular/platform-browser": "^16.1.0",
|
| 19 |
+
"@angular/platform-browser-dynamic": "^16.1.0",
|
| 20 |
+
"@angular/router": "^16.1.0",
|
| 21 |
+
"@fortawesome/fontawesome-free": "^7.0.0",
|
| 22 |
+
"jest-editor-support": "*",
|
| 23 |
+
"rxjs": "~7.8.0",
|
| 24 |
+
"tslib": "^2.3.0",
|
| 25 |
+
"zone.js": "~0.13.0"
|
| 26 |
+
},
|
| 27 |
+
"devDependencies": {
|
| 28 |
+
"@angular-devkit/build-angular": "^16.1.0",
|
| 29 |
+
"@angular/cli": "~16.1.0",
|
| 30 |
+
"@angular/compiler-cli": "^16.1.0",
|
| 31 |
+
"@types/jasmine": "~4.3.0",
|
| 32 |
+
"jasmine-core": "~4.6.0",
|
| 33 |
+
"karma": "~6.4.0",
|
| 34 |
+
"karma-chrome-launcher": "~3.2.0",
|
| 35 |
+
"karma-coverage": "~2.2.0",
|
| 36 |
+
"karma-jasmine": "~5.1.0",
|
| 37 |
+
"karma-jasmine-html-reporter": "~2.1.0",
|
| 38 |
+
"typescript": "~5.1.3"
|
| 39 |
+
}
|
| 40 |
+
}
|
py-match.esproj
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/0.5.271090-alpha">
|
| 2 |
+
<PropertyGroup>
|
| 3 |
+
<StartupCommand>npm start</StartupCommand>
|
| 4 |
+
<JavaScriptTestFramework>Jasmine</JavaScriptTestFramework>
|
| 5 |
+
<!-- Allows the build (or compile) script located on package.json to run on Build -->
|
| 6 |
+
<ShouldRunBuildScript>false</ShouldRunBuildScript>
|
| 7 |
+
<!-- Folder where production build objects will be placed -->
|
| 8 |
+
<BuildOutputFolder>$(MSBuildProjectDirectory)\dist\py-match\</BuildOutputFolder>
|
| 9 |
+
</PropertyGroup>
|
| 10 |
+
</Project>
|
py-match.esproj.user
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
| 3 |
+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
| 4 |
+
<DebuggerFlavor>LaunchJsonDebugger</DebuggerFlavor>
|
| 5 |
+
<LaunchJsonTarget>
|
| 6 |
+
</LaunchJsonTarget>
|
| 7 |
+
</PropertyGroup>
|
| 8 |
+
</Project>
|
src/app/api.service.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@angular/core';
|
| 2 |
+
import { HttpClient } from '@angular/common/http';
|
| 3 |
+
import { environment } from '../environments/environment';
|
| 4 |
+
|
| 5 |
+
@Injectable({ providedIn: 'root' })
|
| 6 |
+
export class ApiService {
|
| 7 |
+
private base = environment.apiBase;
|
| 8 |
+
|
| 9 |
+
constructor(private http: HttpClient) { }
|
| 10 |
+
|
| 11 |
+
start(body = { n_questions: 10, batch_size: 5, domain: 'marriage' }) {
|
| 12 |
+
return this.http.post<any>(`${this.base}/q/start`, body);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
next(session_id: string, selected_color: 'blue' | 'green' | 'red' | 'yellow') {
|
| 16 |
+
return this.http.post<any>(`${this.base}/q/next`, { session_id, selected_color });
|
| 17 |
+
}
|
| 18 |
+
}
|
src/app/app-routing.module.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NgModule } from '@angular/core';
|
| 2 |
+
import { RouterModule, Routes } from '@angular/router';
|
| 3 |
+
import { IntroPageComponent } from './intro-page/intro-page.component';
|
| 4 |
+
import { QuestionAnswerComponent } from './question-answer/question-answer.component';
|
| 5 |
+
import { QuizComponent } from './quiz/quiz.component';
|
| 6 |
+
|
| 7 |
+
const routes: Routes = [
|
| 8 |
+
{
|
| 9 |
+
path: 'auth',
|
| 10 |
+
children: [
|
| 11 |
+
{
|
| 12 |
+
path: 'signin',
|
| 13 |
+
loadComponent: () =>
|
| 14 |
+
import('./auth/sign-in/sign-in.component').then(m => m.SignInComponent),
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
path: 'signup',
|
| 18 |
+
loadComponent: () =>
|
| 19 |
+
import('./auth/sign-up/sign-up.component').then(m => m.SignUpComponent),
|
| 20 |
+
},
|
| 21 |
+
],
|
| 22 |
+
},
|
| 23 |
+
|
| 24 |
+
{ path: '', component: IntroPageComponent, pathMatch: 'full' },
|
| 25 |
+
|
| 26 |
+
{ path: 'sign-in', redirectTo: 'auth/signin', pathMatch: 'full' },
|
| 27 |
+
{ path: 'sign-up', redirectTo: 'auth/signup', pathMatch: 'full' },
|
| 28 |
+
{ path: 'question-answer', component: QuestionAnswerComponent },
|
| 29 |
+
{ path: 'quiz', component: QuizComponent },
|
| 30 |
+
{ path: '**', redirectTo: '' },
|
| 31 |
+
];
|
| 32 |
+
|
| 33 |
+
@NgModule({
|
| 34 |
+
imports: [RouterModule.forRoot(routes)],
|
| 35 |
+
exports: [RouterModule],
|
| 36 |
+
})
|
| 37 |
+
export class AppRoutingModule { }
|
src/app/app.component.css
ADDED
|
File without changes
|
src/app/app.component.html
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
<router-outlet></router-outlet>
|
src/app/app.component.spec.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { TestBed } from '@angular/core/testing';
|
| 2 |
+
import { AppComponent } from './app.component';
|
| 3 |
+
|
| 4 |
+
describe('AppComponent', () => {
|
| 5 |
+
beforeEach(() => TestBed.configureTestingModule({
|
| 6 |
+
declarations: [AppComponent]
|
| 7 |
+
}));
|
| 8 |
+
|
| 9 |
+
it('should create the app', () => {
|
| 10 |
+
const fixture = TestBed.createComponent(AppComponent);
|
| 11 |
+
const app = fixture.componentInstance;
|
| 12 |
+
expect(app).toBeTruthy();
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it(`should have as title 'py-match'`, () => {
|
| 16 |
+
const fixture = TestBed.createComponent(AppComponent);
|
| 17 |
+
const app = fixture.componentInstance;
|
| 18 |
+
expect(app.title).toEqual('py-match');
|
| 19 |
+
});
|
| 20 |
+
|
| 21 |
+
it('should render title', () => {
|
| 22 |
+
const fixture = TestBed.createComponent(AppComponent);
|
| 23 |
+
fixture.detectChanges();
|
| 24 |
+
const compiled = fixture.nativeElement as HTMLElement;
|
| 25 |
+
expect(compiled.querySelector('.content span')?.textContent).toContain('py-match app is running!');
|
| 26 |
+
});
|
| 27 |
+
});
|
src/app/app.component.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
//import { Component } from '@angular/core';
|
| 3 |
+
//import { FormsModule } from '@angular/forms'; // Import FormsModule
|
| 4 |
+
//import { CommonModule } from '@angular/common'; // Import CommonModule
|
| 5 |
+
|
| 6 |
+
//import { IntroPageComponent } from './intro-page/intro-page.component';
|
| 7 |
+
//import { SignInComponent } from './auth/sign-in/sign-in.component';
|
| 8 |
+
//import { SignUpComponent } from './auth/sign-up/sign-up.component';
|
| 9 |
+
//import { QuestionAnswerComponent } from './question-answer/question-answer.component';
|
| 10 |
+
//import { QuizComponent } from './quiz/quiz.component';
|
| 11 |
+
//import { ApiService } from './api.service';
|
| 12 |
+
//import { RouterOutlet } from '@angular/router';
|
| 13 |
+
//type Color = 'blue' | 'green' | 'red' | 'yellow';
|
| 14 |
+
|
| 15 |
+
//@Component({
|
| 16 |
+
// selector: 'app-root',
|
| 17 |
+
// standalone: true,
|
| 18 |
+
// imports: [RouterOutlet, QuestionAnswerComponent, IntroPageComponent, QuizComponent,
|
| 19 |
+
// SignInComponent,
|
| 20 |
+
// SignUpComponent,
|
| 21 |
+
// FormsModule, // Add FormsModule here
|
| 22 |
+
// CommonModule] , // Add CommonModule here],
|
| 23 |
+
// templateUrl: './app.component.html',
|
| 24 |
+
// styleUrls: ['./app.component.css']
|
| 25 |
+
//})
|
| 26 |
+
//export class AppComponent {
|
| 27 |
+
// title = 'py-match';
|
| 28 |
+
// sessionId: string | null = null;
|
| 29 |
+
// index = 0;
|
| 30 |
+
// total = 0;
|
| 31 |
+
// question = '';
|
| 32 |
+
// options: { text: string, color: Color }[] = [];
|
| 33 |
+
// progress?: Record<Color, number>; // percentages
|
| 34 |
+
|
| 35 |
+
// constructor(private api: ApiService) { }
|
| 36 |
+
|
| 37 |
+
// start() {
|
| 38 |
+
// this.api.start({ n_questions: 10, batch_size: 5, domain: 'marriage' })
|
| 39 |
+
// .subscribe(res => {
|
| 40 |
+
// this.sessionId = res.session_id;
|
| 41 |
+
// this.index = res.index;
|
| 42 |
+
// this.total = res.total;
|
| 43 |
+
// this.question = res.question;
|
| 44 |
+
// this.options = res.options;
|
| 45 |
+
// this.progress = undefined; // first question has no progress yet
|
| 46 |
+
// });
|
| 47 |
+
// }
|
| 48 |
+
|
| 49 |
+
// answer(color: Color) {
|
| 50 |
+
// if (!this.sessionId) { return; }
|
| 51 |
+
// this.api.next(this.sessionId, color).subscribe(res => {
|
| 52 |
+
// if (res.done) {
|
| 53 |
+
// alert('Finished! Check console for final mix.');
|
| 54 |
+
// console.log('Final mix (percent):', res.mix);
|
| 55 |
+
// return;
|
| 56 |
+
// }
|
| 57 |
+
// this.index = res.index;
|
| 58 |
+
// this.total = res.total;
|
| 59 |
+
// this.question = res.question;
|
| 60 |
+
// this.options = res.options;
|
| 61 |
+
// this.progress = res.progress; // already in percentages (from your backend change)
|
| 62 |
+
// });
|
| 63 |
+
// }
|
| 64 |
+
//}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
import { Component } from '@angular/core';
|
| 68 |
+
import { RouterOutlet } from '@angular/router';
|
| 69 |
+
|
| 70 |
+
@Component({
|
| 71 |
+
selector: 'app-root',
|
| 72 |
+
standalone: true,
|
| 73 |
+
imports: [RouterOutlet],
|
| 74 |
+
templateUrl: './app.component.html',
|
| 75 |
+
styleUrls: ['./app.component.css']
|
| 76 |
+
})
|
| 77 |
+
export class AppComponent {
|
| 78 |
+
title = 'py-match';
|
| 79 |
+
}
|
src/app/auth/sign-in/sign-in.component.css
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:host {
|
| 2 |
+
display: block
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
.auth-box {
|
| 7 |
+
width: 49vw;
|
| 8 |
+
display: grid;
|
| 9 |
+
grid-template-columns: 1fr;
|
| 10 |
+
background: #2b1b6b;
|
| 11 |
+
border-radius: 14px;
|
| 12 |
+
overflow: hidden;
|
| 13 |
+
box-shadow: 0 20px 60px rgba(0,0,0,.35)
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.panel-right {
|
| 17 |
+
position: relative;
|
| 18 |
+
background: radial-gradient(120% 120% at 20% 50%,rgba(0,0,0,.25) 0%,rgba(0,0,0,0) 60%)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.panel-right::before {
|
| 22 |
+
content: "";
|
| 23 |
+
position: absolute;
|
| 24 |
+
inset: 0;
|
| 25 |
+
background: linear-gradient(90deg,rgba(0,0,0,.45) 0%,rgba(0,0,0,0) 26%);
|
| 26 |
+
pointer-events: none
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.right-image img {
|
| 30 |
+
max-width: 100%;
|
| 31 |
+
height: auto;
|
| 32 |
+
display: block
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.panel-left {
|
| 36 |
+
padding: clamp(22px,3.5vw,36px);
|
| 37 |
+
background: white;
|
| 38 |
+
color: black;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.brand-mark {
|
| 42 |
+
width: 4vw;
|
| 43 |
+
margin-bottom: 14px;
|
| 44 |
+
border: 2px solid #b1b1b17d;
|
| 45 |
+
box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.title {
|
| 49 |
+
margin: 0 0 6px;
|
| 50 |
+
font-size: clamp(22px,3vw,26px);
|
| 51 |
+
font-weight: 700
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.form {
|
| 55 |
+
display: grid;
|
| 56 |
+
gap: 12px
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.field {
|
| 60 |
+
display: grid;
|
| 61 |
+
gap: 6px
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
label {
|
| 65 |
+
font-weight: 600;
|
| 66 |
+
font-size: 13px;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
input[type="email"], input[type="password"] {
|
| 70 |
+
color: black;
|
| 71 |
+
border: 1px solid rgb(0 0 0 / 57%);
|
| 72 |
+
border-radius: 10px;
|
| 73 |
+
padding: 11px 12px;
|
| 74 |
+
outline: none
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
input::placeholder {
|
| 78 |
+
color: #808080;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
input:focus {
|
| 82 |
+
border-color: #a78bfa;
|
| 83 |
+
box-shadow: 0 0 0 3px rgba(167,139,250,.25)
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.error {
|
| 87 |
+
color: red;
|
| 88 |
+
font-size: 12px
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.btn {
|
| 92 |
+
width: 100%;
|
| 93 |
+
border-radius: 999px;
|
| 94 |
+
padding: 12px 18px;
|
| 95 |
+
cursor: pointer
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.btn-primary {
|
| 99 |
+
background: #0b0f1a;
|
| 100 |
+
color: #fff;
|
| 101 |
+
border: none;
|
| 102 |
+
font-weight: 700
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.btn[aria-busy="true"] {
|
| 106 |
+
opacity: .75;
|
| 107 |
+
cursor: progress
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.footnote {
|
| 111 |
+
margin: 14px 0 0;
|
| 112 |
+
font-size: 13px;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.footnote a {
|
| 116 |
+
color: red;
|
| 117 |
+
text-decoration: underline
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
@media(min-width:900px) {
|
| 121 |
+
.auth-box {
|
| 122 |
+
grid-template-columns: 520px 1fr
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.topTitle {
|
| 127 |
+
display: flex;
|
| 128 |
+
align-items: center;
|
| 129 |
+
gap: 21px;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.topHeader {
|
| 133 |
+
font-size: 1vw;
|
| 134 |
+
}
|
src/app/auth/sign-in/sign-in.component.html
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<section class="auth-page">
|
| 2 |
+
<div class="auth-box" role="dialog" aria-labelledby="siTitle">
|
| 3 |
+
<!-- Left (form) -->
|
| 4 |
+
<div class="panel-left">
|
| 5 |
+
<div class="topTitle">
|
| 6 |
+
<img class="brand-mark" src="/assets/pykara-logo.png" alt="Brand" />
|
| 7 |
+
<span class="topHeader"><b>Welcome back!</b></span>
|
| 8 |
+
</div>
|
| 9 |
+
<h2 id="siTitle" class="title">Log in</h2>
|
| 10 |
+
|
| 11 |
+
<form class="form" [formGroup]="form" (ngSubmit)="submit()" novalidate>
|
| 12 |
+
<div class="field">
|
| 13 |
+
<label for="email">
|
| 14 |
+
Email
|
| 15 |
+
<small *ngIf="controlHasError('email','required')" class="error">*</small>
|
| 16 |
+
</label>
|
| 17 |
+
<input id="email" type="email" formControlName="email" placeholder="you@example.com"
|
| 18 |
+
[attr.aria-invalid]="controlHasError('email')" />
|
| 19 |
+
<small *ngIf="controlHasError('email','email')" class="error">Enter a valid email.</small>
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<div class="field">
|
| 23 |
+
<label for="password">
|
| 24 |
+
Password
|
| 25 |
+
<small *ngIf="controlHasError('password','required')" class="error">*</small>
|
| 26 |
+
</label>
|
| 27 |
+
<input id="password" type="password" formControlName="password" placeholder="••••••••"
|
| 28 |
+
[attr.aria-invalid]="controlHasError('password')" />
|
| 29 |
+
<small *ngIf="controlHasError('password','minlength')" class="error">Use at least 6 characters.</small>
|
| 30 |
+
</div>
|
| 31 |
+
|
| 32 |
+
<button class="btn btn-primary" type="submit"
|
| 33 |
+
[disabled]="form.invalid || submitting()" [attr.aria-busy]="submitting()">
|
| 34 |
+
Sign in
|
| 35 |
+
</button>
|
| 36 |
+
|
| 37 |
+
<p class="footnote">
|
| 38 |
+
New here?
|
| 39 |
+
<!-- Use click handler instead of routerLink -->
|
| 40 |
+
<a href="#" (click)="goToSignUp(); $event.preventDefault()">Create an account</a>
|
| 41 |
+
</p>
|
| 42 |
+
</form>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<!-- Right (image) -->
|
| 46 |
+
<div class="panel-right">
|
| 47 |
+
<div class="right-image">
|
| 48 |
+
<img src="/assets/user.png" alt="Decorative" />
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
</section>
|
src/app/auth/sign-in/sign-in.component.spec.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
| 2 |
+
|
| 3 |
+
import { SignInComponent } from './sign-in.component';
|
| 4 |
+
|
| 5 |
+
describe('SignInComponent', () => {
|
| 6 |
+
let component: SignInComponent;
|
| 7 |
+
let fixture: ComponentFixture<SignInComponent>;
|
| 8 |
+
|
| 9 |
+
beforeEach(() => {
|
| 10 |
+
TestBed.configureTestingModule({
|
| 11 |
+
declarations: [SignInComponent]
|
| 12 |
+
});
|
| 13 |
+
fixture = TestBed.createComponent(SignInComponent);
|
| 14 |
+
component = fixture.componentInstance;
|
| 15 |
+
fixture.detectChanges();
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
it('should create', () => {
|
| 19 |
+
expect(component).toBeTruthy();
|
| 20 |
+
});
|
| 21 |
+
});
|
src/app/auth/sign-in/sign-in.component.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Component, ChangeDetectionStrategy, signal, Output, EventEmitter } from '@angular/core';
|
| 2 |
+
import { CommonModule } from '@angular/common';
|
| 3 |
+
import { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from '@angular/forms';
|
| 4 |
+
import { Router, RouterLink } from '@angular/router';
|
| 5 |
+
|
| 6 |
+
@Component({
|
| 7 |
+
selector: 'app-sign-in',
|
| 8 |
+
standalone: true,
|
| 9 |
+
imports: [CommonModule, ReactiveFormsModule, RouterLink],
|
| 10 |
+
templateUrl: './sign-in.component.html',
|
| 11 |
+
styleUrls: ['./sign-in.component.css'],
|
| 12 |
+
changeDetection: ChangeDetectionStrategy.OnPush
|
| 13 |
+
})
|
| 14 |
+
export class SignInComponent {
|
| 15 |
+
submitting = signal(false);
|
| 16 |
+
form: FormGroup;
|
| 17 |
+
|
| 18 |
+
@Output() switchToSignUp = new EventEmitter<void>();
|
| 19 |
+
@Output() signInSuccess = new EventEmitter<void>();
|
| 20 |
+
|
| 21 |
+
constructor(private fb: FormBuilder, private router: Router) {
|
| 22 |
+
this.form = this.fb.group({
|
| 23 |
+
email: ['', [Validators.required, Validators.email]],
|
| 24 |
+
password: ['', [Validators.required, Validators.minLength(8)]],
|
| 25 |
+
remember: [true]
|
| 26 |
+
});
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
navigateHome() {
|
| 30 |
+
this.router.navigate(['/']);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
goToSignUp() {
|
| 34 |
+
this.switchToSignUp.emit();
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
controlHasError(name: string, key?: string) {
|
| 38 |
+
const c = this.form.get(name);
|
| 39 |
+
if (!c) return false;
|
| 40 |
+
if (!key) return c.invalid && (c.dirty || c.touched);
|
| 41 |
+
return c.hasError(key) && (c.dirty || c.touched);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
async submit() {
|
| 45 |
+
if (this.form.invalid) {
|
| 46 |
+
this.form.markAllAsTouched();
|
| 47 |
+
return;
|
| 48 |
+
}
|
| 49 |
+
this.submitting.set(true);
|
| 50 |
+
try {
|
| 51 |
+
const payload = this.form.value;
|
| 52 |
+
console.log('SIGN IN payload', payload);
|
| 53 |
+
|
| 54 |
+
// Emit success event instead of alert
|
| 55 |
+
this.signInSuccess.emit();
|
| 56 |
+
|
| 57 |
+
// Optional: You can also navigate or do other actions here
|
| 58 |
+
// this.router.navigate(['/question-answer']);
|
| 59 |
+
} finally {
|
| 60 |
+
this.submitting.set(false);
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
}
|
src/app/auth/sign-up/sign-up.component.css
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:host {
|
| 2 |
+
display: block
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
/*.auth-page {
|
| 6 |
+
min-height: 100svh;
|
| 7 |
+
display: grid;
|
| 8 |
+
place-items: center;
|
| 9 |
+
padding: clamp(16px,3vw,24px);
|
| 10 |
+
background: linear-gradient(135deg,#5b21b6 0%,#7c3aed 50%,#4c1d95 100%);
|
| 11 |
+
}*/
|
| 12 |
+
|
| 13 |
+
.auth-box {
|
| 14 |
+
width: 49vw;
|
| 15 |
+
display: grid;
|
| 16 |
+
grid-template-columns: 1fr;
|
| 17 |
+
background: #2b1b6b;
|
| 18 |
+
border-radius: 14px;
|
| 19 |
+
overflow: hidden;
|
| 20 |
+
box-shadow: 0 20px 60px rgba(0,0,0,.35);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.panel-right {
|
| 24 |
+
position: relative;
|
| 25 |
+
background: radial-gradient(120% 120% at 20% 50%, rgba(0,0,0,.25) 0%, rgba(0,0,0,0) 60%);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.panel-right::before {
|
| 29 |
+
content: "";
|
| 30 |
+
position: absolute;
|
| 31 |
+
inset: 0;
|
| 32 |
+
background: linear-gradient(90deg, rgba(0,0,0,.45) 0%, rgba(0,0,0,0) 26%);
|
| 33 |
+
pointer-events: none;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.right-image {
|
| 37 |
+
display: flex;
|
| 38 |
+
align-items: center;
|
| 39 |
+
justify-content: center;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.right-image img {
|
| 43 |
+
max-width: 100%;
|
| 44 |
+
height: auto;
|
| 45 |
+
display: block
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.panel-left {
|
| 49 |
+
padding: clamp(22px,3.5vw,36px);
|
| 50 |
+
background: white;
|
| 51 |
+
color: black;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.brand-mark {
|
| 55 |
+
width: 4vw;
|
| 56 |
+
margin-bottom: 14px;
|
| 57 |
+
border: 2px solid #b1b1b17d;
|
| 58 |
+
box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.title {
|
| 62 |
+
margin: 0 0 6px;
|
| 63 |
+
font-size: clamp(22px,3vw,26px);
|
| 64 |
+
font-weight: 700
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.form {
|
| 68 |
+
display: grid;
|
| 69 |
+
gap: 12px
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.field {
|
| 73 |
+
display: grid;
|
| 74 |
+
gap: 6px
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
label {
|
| 78 |
+
font-weight: 600;
|
| 79 |
+
font-size: 13px;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
input[type="text"], input[type="password"] {
|
| 83 |
+
color: #000000;
|
| 84 |
+
border: 1px solid rgb(0 0 0 / 57%);
|
| 85 |
+
border-radius: 10px;
|
| 86 |
+
padding: 11px 12px;
|
| 87 |
+
outline: none;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
input::placeholder {
|
| 91 |
+
color: #808080
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
input:focus {
|
| 95 |
+
border-color: #a78bfa;
|
| 96 |
+
box-shadow: 0 0 0 3px rgba(167,139,250,.25)
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.error {
|
| 100 |
+
color: red;
|
| 101 |
+
font-size: 12px
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.btn {
|
| 105 |
+
width: 100%;
|
| 106 |
+
border-radius: 999px;
|
| 107 |
+
padding: 12px 18px;
|
| 108 |
+
cursor: pointer
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.btn-primary {
|
| 112 |
+
background: #0b0f1a;
|
| 113 |
+
color: #fff;
|
| 114 |
+
border: none;
|
| 115 |
+
font-weight: 700
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.btn[aria-busy="true"] {
|
| 119 |
+
opacity: .75;
|
| 120 |
+
cursor: progress
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.footnote {
|
| 124 |
+
margin: 14px 0 0;
|
| 125 |
+
font-size: 13px;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.footnote a {
|
| 129 |
+
color: red;
|
| 130 |
+
text-decoration: underline
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
@media(min-width:900px) {
|
| 134 |
+
.auth-box {
|
| 135 |
+
grid-template-columns: 520px 1fr
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.topTitle {
|
| 140 |
+
display: flex;
|
| 141 |
+
align-items: center;
|
| 142 |
+
gap: 21px;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.topHeader {
|
| 146 |
+
font-size: 1vw;
|
| 147 |
+
}
|
src/app/auth/sign-up/sign-up.component.html
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<section class="auth-page">
|
| 2 |
+
<div class="auth-box" role="dialog" aria-labelledby="suTitle">
|
| 3 |
+
<!-- Left (form) -->
|
| 4 |
+
<div class="panel-left">
|
| 5 |
+
<div class="topTitle">
|
| 6 |
+
<img class="brand-mark" src="/assets/pykara-logo.png" alt="Brand" />
|
| 7 |
+
<p class="topHeader"><b>Let's get started!</b></p>
|
| 8 |
+
</div>
|
| 9 |
+
<h2 id="suTitle" class="title">Create an account</h2>
|
| 10 |
+
|
| 11 |
+
<form class="form" [formGroup]="form" (ngSubmit)="submit()" novalidate>
|
| 12 |
+
<!-- Name -->
|
| 13 |
+
<div class="field">
|
| 14 |
+
<label for="name">
|
| 15 |
+
Name
|
| 16 |
+
<small *ngIf="controlHasError('name','required')" class="error">*</small>
|
| 17 |
+
</label>
|
| 18 |
+
<input id="name" type="text" placeholder="John"
|
| 19 |
+
formControlName="name" [attr.aria-invalid]="controlHasError('name')" />
|
| 20 |
+
<small *ngIf="controlHasError('name','minlength')" class="error">Enter at least 2 characters.</small>
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<!-- Email/Phone (contact) -->
|
| 24 |
+
<div class="field">
|
| 25 |
+
<label for="contact">
|
| 26 |
+
Email
|
| 27 |
+
<small *ngIf="controlHasError('contact','required')" class="error">*</small>
|
| 28 |
+
</label>
|
| 29 |
+
<input id="contact" type="text" placeholder="hello@gmail.com"
|
| 30 |
+
formControlName="contact" [attr.aria-invalid]="controlHasError('contact')" />
|
| 31 |
+
<small *ngIf="controlHasError('contact','pattern')" class="error">Enter a valid email/phone.</small>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<!-- Password -->
|
| 35 |
+
<div class="field">
|
| 36 |
+
<label for="password">
|
| 37 |
+
Password
|
| 38 |
+
<small *ngIf="controlHasError('password','required')" class="error">*</small>
|
| 39 |
+
</label>
|
| 40 |
+
<input id="password" type="password" placeholder="••••••••"
|
| 41 |
+
formControlName="password" [attr.aria-invalid]="controlHasError('password')" />
|
| 42 |
+
<small *ngIf="controlHasError('password','minlength')" class="error">Use at least 6 characters.</small>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<!-- Confirm password -->
|
| 46 |
+
<div class="field">
|
| 47 |
+
<label for="confirmPassword">
|
| 48 |
+
Confirm password
|
| 49 |
+
<small *ngIf="controlHasError('confirmPassword','required')" class="error">*</small>
|
| 50 |
+
</label>
|
| 51 |
+
<input id="confirmPassword" type="password" placeholder="••••••••"
|
| 52 |
+
formControlName="confirmPassword" [attr.aria-invalid]="showPwdMismatch()" />
|
| 53 |
+
<small *ngIf="showPwdMismatch()" class="error">Passwords do not match.</small>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
<button class="btn btn-primary" type="submit"
|
| 57 |
+
[disabled]="form.invalid || submitting()" [attr.aria-busy]="submitting()">
|
| 58 |
+
Sign up
|
| 59 |
+
</button>
|
| 60 |
+
|
| 61 |
+
<p class="footnote">
|
| 62 |
+
Already have an account?
|
| 63 |
+
<!-- Use click handler instead of routerLink -->
|
| 64 |
+
<a href="#" (click)="goToLogin(); $event.preventDefault()">Log in</a>
|
| 65 |
+
</p>
|
| 66 |
+
</form>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
<!-- Right (image) -->
|
| 70 |
+
<div class="panel-right">
|
| 71 |
+
<div class="right-image">
|
| 72 |
+
<img src="/assets/user.png" alt="Decorative" />
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
</section>
|
src/app/auth/sign-up/sign-up.component.spec.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
| 2 |
+
|
| 3 |
+
import { SignUpComponent } from './sign-up.component';
|
| 4 |
+
|
| 5 |
+
describe('SignUpComponent', () => {
|
| 6 |
+
let component: SignUpComponent;
|
| 7 |
+
let fixture: ComponentFixture<SignUpComponent>;
|
| 8 |
+
|
| 9 |
+
beforeEach(() => {
|
| 10 |
+
TestBed.configureTestingModule({
|
| 11 |
+
declarations: [SignUpComponent]
|
| 12 |
+
});
|
| 13 |
+
fixture = TestBed.createComponent(SignUpComponent);
|
| 14 |
+
component = fixture.componentInstance;
|
| 15 |
+
fixture.detectChanges();
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
it('should create', () => {
|
| 19 |
+
expect(component).toBeTruthy();
|
| 20 |
+
});
|
| 21 |
+
});
|
src/app/auth/sign-up/sign-up.component.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Component, Output, EventEmitter } from '@angular/core';
|
| 2 |
+
import { CommonModule } from '@angular/common';
|
| 3 |
+
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, AbstractControl } from '@angular/forms';
|
| 4 |
+
import { Router, RouterLink } from '@angular/router';
|
| 5 |
+
import { SignupService, SignupPayload } from './sign-up.service';
|
| 6 |
+
|
| 7 |
+
@Component({
|
| 8 |
+
selector: 'app-sign-up',
|
| 9 |
+
standalone: true,
|
| 10 |
+
imports: [CommonModule, ReactiveFormsModule, RouterLink],
|
| 11 |
+
templateUrl: './sign-up.component.html',
|
| 12 |
+
styleUrls: ['./sign-up.component.css']
|
| 13 |
+
})
|
| 14 |
+
export class SignUpComponent {
|
| 15 |
+
form: FormGroup;
|
| 16 |
+
private isSubmitting = false;
|
| 17 |
+
|
| 18 |
+
@Output() switchToSignIn = new EventEmitter<void>();
|
| 19 |
+
@Output() signUpSuccess = new EventEmitter<void>();
|
| 20 |
+
|
| 21 |
+
constructor(private fb: FormBuilder, private router: Router, private signupService: SignupService) {
|
| 22 |
+
this.form = this.fb.group({
|
| 23 |
+
name: ['', [Validators.required, Validators.minLength(2)]],
|
| 24 |
+
contact: ['', [Validators.required, Validators.pattern(/(^[^\s@]+@[^\s@]+\.[^\s@]+$)|(^\+?\d[\d\-\s]{8,14}\d$)/)]],
|
| 25 |
+
password: ['', [Validators.required, Validators.minLength(6)]],
|
| 26 |
+
confirmPassword: ['', [Validators.required]],
|
| 27 |
+
}, { validators: [this.passwordsMatchValidator] });
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
control(path: string): AbstractControl | null {
|
| 31 |
+
return this.form.get(path);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
showPwdMismatch(): boolean {
|
| 35 |
+
const pw = this.control('password');
|
| 36 |
+
const cpw = this.control('confirmPassword');
|
| 37 |
+
const groupMismatch = this.form.errors?.['passwordMismatch'];
|
| 38 |
+
return !!(pw && cpw && (cpw.touched || pw.touched) && groupMismatch);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
passwordsMatchValidator(group: AbstractControl) {
|
| 42 |
+
const pw = group.get('password')?.value;
|
| 43 |
+
const cpw = group.get('confirmPassword')?.value;
|
| 44 |
+
return pw && cpw && pw === cpw ? null : { passwordMismatch: true };
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
submitting(): boolean {
|
| 48 |
+
return this.isSubmitting;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
async submit() {
|
| 52 |
+
this.form.markAllAsTouched();
|
| 53 |
+
|
| 54 |
+
if (this.form.invalid) {
|
| 55 |
+
return;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
const payload: SignupPayload = {
|
| 59 |
+
name: this.control('name')?.value,
|
| 60 |
+
email: this.control('contact')?.value,
|
| 61 |
+
password: this.control('password')?.value
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
this.isSubmitting = true;
|
| 65 |
+
|
| 66 |
+
try {
|
| 67 |
+
const res = await this.signupService.signup(payload).toPromise();
|
| 68 |
+
if (res && res.message) {
|
| 69 |
+
alert('Success: ' + res.message);
|
| 70 |
+
this.signUpSuccess.emit(); // Emit success instead of navigating
|
| 71 |
+
} else if (res && res.error) {
|
| 72 |
+
alert('Signup failed: ' + res.error);
|
| 73 |
+
} else {
|
| 74 |
+
alert('Unexpected response');
|
| 75 |
+
}
|
| 76 |
+
} catch (err: any) {
|
| 77 |
+
console.error('Signup error:', err);
|
| 78 |
+
alert('Signup failed due to server or network error.');
|
| 79 |
+
} finally {
|
| 80 |
+
this.isSubmitting = false;
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
controlHasError(path: string, error?: string): boolean {
|
| 85 |
+
const c = this.control(path);
|
| 86 |
+
if (!c) return false;
|
| 87 |
+
return error ? !!(c.touched && c.errors?.[error]) : !!(c.touched && c.invalid);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
navigateHome() { this.router.navigateByUrl('/'); }
|
| 91 |
+
|
| 92 |
+
goToLogin() {
|
| 93 |
+
this.switchToSignIn.emit();
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
tr(key: string): string {
|
| 97 |
+
const map: Record<string, string> = { title: 'Create your account', subtitle: 'Join to continue' };
|
| 98 |
+
return map[key] || '';
|
| 99 |
+
}
|
| 100 |
+
}
|
src/app/auth/sign-up/sign-up.service.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@angular/core';
|
| 2 |
+
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
| 3 |
+
import { Observable, catchError, throwError } from 'rxjs';
|
| 4 |
+
|
| 5 |
+
import { environment } from '../../../environments/environment';
|
| 6 |
+
export interface SignupPayload {
|
| 7 |
+
name: string;
|
| 8 |
+
email: string; // email or phone
|
| 9 |
+
|
| 10 |
+
password: string;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export interface SignupResponse {
|
| 14 |
+
message?: string; // success message from backend
|
| 15 |
+
error?: string; // error message from backend
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
@Injectable({
|
| 19 |
+
providedIn: 'root'
|
| 20 |
+
})
|
| 21 |
+
export class SignupService {
|
| 22 |
+
|
| 23 |
+
// private apiUrl = 'http://localhost:5000/api/signup'; // Replace with your backend URL
|
| 24 |
+
private apiUrl = 'http://localhost:5000/api'; // Replace with your backend URL
|
| 25 |
+
//private base = environment.apiBase; // e.g., http://192.168.29.27:5000
|
| 26 |
+
constructor(private http: HttpClient) { }
|
| 27 |
+
|
| 28 |
+
signup(payload: SignupPayload): Observable<SignupResponse> {
|
| 29 |
+
return this.http.post<SignupResponse>(`${this.apiUrl}/signup`, payload).pipe(
|
| 30 |
+
catchError(this.handleError)
|
| 31 |
+
);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
private handleError(error: HttpErrorResponse) {
|
| 35 |
+
return throwError(() => new Error(error.message || 'Something went wrong'));
|
| 36 |
+
}
|
| 37 |
+
}
|
src/app/intro-page/intro-page.component.css
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:host {
|
| 2 |
+
display: block;
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
* {
|
| 6 |
+
box-sizing: border-box;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
.sr-only {
|
| 10 |
+
position: absolute;
|
| 11 |
+
width: 1px;
|
| 12 |
+
height: 1px;
|
| 13 |
+
margin: -1px;
|
| 14 |
+
padding: 0;
|
| 15 |
+
overflow: hidden;
|
| 16 |
+
clip: rect(0,0,0,0);
|
| 17 |
+
border: 0;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.skip-link {
|
| 21 |
+
position: absolute;
|
| 22 |
+
left: -9999px;
|
| 23 |
+
top: auto;
|
| 24 |
+
width: 1px;
|
| 25 |
+
height: 1px;
|
| 26 |
+
overflow: hidden;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.skip-link:focus {
|
| 30 |
+
left: 16px;
|
| 31 |
+
top: 12px;
|
| 32 |
+
width: auto;
|
| 33 |
+
height: auto;
|
| 34 |
+
padding: 8px 10px;
|
| 35 |
+
background: #fff;
|
| 36 |
+
color: #000;
|
| 37 |
+
border-radius: 6px;
|
| 38 |
+
z-index: 10;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.page {
|
| 42 |
+
height: 100vh;
|
| 43 |
+
display: flex;
|
| 44 |
+
flex-direction: column;
|
| 45 |
+
background: url(/assets/4.png) no-repeat center center fixed;
|
| 46 |
+
background-size: cover;
|
| 47 |
+
color: #e5e7eb;
|
| 48 |
+
justify-content: space-around;
|
| 49 |
+
gap: 6vw;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/* Topbar */
|
| 53 |
+
.topbar {
|
| 54 |
+
display: flex;
|
| 55 |
+
align-items: center;
|
| 56 |
+
justify-content: space-around;
|
| 57 |
+
gap: 54vw;
|
| 58 |
+
width: 100%;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.brand {
|
| 62 |
+
display: flex;
|
| 63 |
+
align-items: center;
|
| 64 |
+
gap: 10px;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.brand__logo {
|
| 68 |
+
width: 34px;
|
| 69 |
+
height: 34px;
|
| 70 |
+
border-radius: 8px;
|
| 71 |
+
background: #00ff88;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.brand__name {
|
| 75 |
+
letter-spacing: 3.2px;
|
| 76 |
+
font-size: 2.5vw;
|
| 77 |
+
font-family: Roliana;
|
| 78 |
+
font-weight: lighter;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.actions {
|
| 82 |
+
display: flex;
|
| 83 |
+
align-items: center;
|
| 84 |
+
gap: 8px;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/* Controls / buttons */
|
| 88 |
+
.select {
|
| 89 |
+
background: #0b0f1a;
|
| 90 |
+
color: #e5e7eb;
|
| 91 |
+
border: 1px solid #2b3246;
|
| 92 |
+
border-radius: 10px;
|
| 93 |
+
padding: 8px 10px;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.btn {
|
| 97 |
+
display: inline-flex;
|
| 98 |
+
align-items: center;
|
| 99 |
+
justify-content: center;
|
| 100 |
+
gap: 8px;
|
| 101 |
+
background: #0b0f1a;
|
| 102 |
+
color: #e5e7eb;
|
| 103 |
+
border: 1px solid #2b3246;
|
| 104 |
+
border-radius: 10px;
|
| 105 |
+
padding: 10px 14px;
|
| 106 |
+
cursor: pointer;
|
| 107 |
+
text-decoration: none;
|
| 108 |
+
border: 2px solid #f3a54c;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.btn:hover {
|
| 112 |
+
border-color: #00ff88;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.btn-primary {
|
| 116 |
+
background: #f3a54c;
|
| 117 |
+
color: #0b0f1a;
|
| 118 |
+
border: none;
|
| 119 |
+
font-weight: 700;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.btn-ghost {
|
| 123 |
+
background: #0b0f1a;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
/*.btn-disabled {
|
| 127 |
+
opacity: 0.6;
|
| 128 |
+
cursor: not-allowed;
|
| 129 |
+
pointer-events: none;
|
| 130 |
+
}*/
|
| 131 |
+
|
| 132 |
+
.btn-disabled:hover {
|
| 133 |
+
border-color: #f3a54c !important;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
/* Hero */
|
| 137 |
+
.hero {
|
| 138 |
+
margin-left: 5vw;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.hero__content {
|
| 142 |
+
max-width: 620px;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.hero__title {
|
| 146 |
+
margin: 0 0 10px;
|
| 147 |
+
font-size: clamp(26px, 4.2vw, 44px);
|
| 148 |
+
line-height: 1.12;
|
| 149 |
+
font-weight: 800;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.hero__text {
|
| 153 |
+
color: #b9c2d0;
|
| 154 |
+
margin: 0 0 18px;
|
| 155 |
+
font-size: clamp(14px, 2vw, 16px);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.cta {
|
| 159 |
+
display: flex;
|
| 160 |
+
gap: 10px;
|
| 161 |
+
flex-wrap: wrap;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
/* Footer */
|
| 165 |
+
.footer {
|
| 166 |
+
display: flex;
|
| 167 |
+
align-items: center;
|
| 168 |
+
gap: 16px;
|
| 169 |
+
margin-left: 5vw;
|
| 170 |
+
font-size: 0.7vw;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.footer a {
|
| 174 |
+
text-decoration: unset;
|
| 175 |
+
align-items: center;
|
| 176 |
+
border: 2px solid #f3a54c;
|
| 177 |
+
color: white;
|
| 178 |
+
border-radius: 2vw;
|
| 179 |
+
display: inline-flex;
|
| 180 |
+
align-items: center;
|
| 181 |
+
border-radius: 8px;
|
| 182 |
+
border: 1px solid rgba(255, 255, 255, .25);
|
| 183 |
+
background: rgba(11, 15, 26, 0.6);
|
| 184 |
+
color: #cfe8ff;
|
| 185 |
+
cursor: pointer;
|
| 186 |
+
padding: 0 4px;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.footer a i {
|
| 190 |
+
margin-right: 2px;
|
| 191 |
+
line-height: 1;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.about-btn {
|
| 195 |
+
display: inline-flex;
|
| 196 |
+
align-items: center;
|
| 197 |
+
gap: 8px;
|
| 198 |
+
padding: 6px 8px;
|
| 199 |
+
border-radius: 8px;
|
| 200 |
+
border: 1px solid rgba(255,255,255,.25);
|
| 201 |
+
background: rgba(11, 15, 26, 0.6);
|
| 202 |
+
color: #cfe8ff;
|
| 203 |
+
cursor: pointer;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.about-btn:hover, .footer a:hover {
|
| 207 |
+
border-color: #00ff88;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
/* High contrast mode */
|
| 211 |
+
.page.high-contrast {
|
| 212 |
+
background: #000;
|
| 213 |
+
color: #fff;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.page.high-contrast a {
|
| 217 |
+
color: #fff;
|
| 218 |
+
text-decoration: underline;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.page.high-contrast .card, .page.high-contrast .banner {
|
| 222 |
+
background: #000;
|
| 223 |
+
border: 2px solid #fff;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.page.high-contrast .btn, .page.high-contrast .select {
|
| 227 |
+
border-color: #fff !important;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.brand__logo-img {
|
| 231 |
+
background: white;
|
| 232 |
+
max-width: 5vw;
|
| 233 |
+
height: auto;
|
| 234 |
+
border-radius: 1vw;
|
| 235 |
+
margin: 0.5vw;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.brand__link {
|
| 239 |
+
display: inline-flex;
|
| 240 |
+
align-items: center;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
/* AUTH Modal overlay */
|
| 244 |
+
.modal-backdrop {
|
| 245 |
+
position: fixed;
|
| 246 |
+
inset: 0;
|
| 247 |
+
background: rgb(255 255 255 / 34%);
|
| 248 |
+
display: grid;
|
| 249 |
+
place-items: center;
|
| 250 |
+
z-index: 1000;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
button.modal-close {
|
| 254 |
+
border: 3px solid #ff0000;
|
| 255 |
+
background: transparent;
|
| 256 |
+
color: white;
|
| 257 |
+
position: relative;
|
| 258 |
+
left: 47vw;
|
| 259 |
+
top: 1.8vw;
|
| 260 |
+
z-index: 11;
|
| 261 |
+
border-radius: 10px;
|
| 262 |
+
cursor: pointer;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
/* Ensure the auth components fit properly in the modal */
|
| 268 |
+
.modal-panel app-sign-in,
|
| 269 |
+
.modal-panel app-sign-up {
|
| 270 |
+
display: block;
|
| 271 |
+
width: 100%;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
/* ABOUT Modal (new) */
|
| 276 |
+
.about-backdrop {
|
| 277 |
+
position: fixed;
|
| 278 |
+
inset: 0;
|
| 279 |
+
background: rgb(255 255 255 / 34%);
|
| 280 |
+
z-index: 1100; /* above auth backdrop */
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.about-modal {
|
| 284 |
+
position: fixed;
|
| 285 |
+
z-index: 1101; /* above auth modal */
|
| 286 |
+
inset: 50% auto auto 50%;
|
| 287 |
+
transform: translate(-50%, -50%);
|
| 288 |
+
width: min(720px, 92vw);
|
| 289 |
+
max-height: 80vh;
|
| 290 |
+
overflow: auto;
|
| 291 |
+
background: #ffffff;
|
| 292 |
+
color: #111;
|
| 293 |
+
border-radius: 12px;
|
| 294 |
+
box-shadow: 0 20px 50px rgba(0,0,0,.25);
|
| 295 |
+
outline: none;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.about-modal__header,
|
| 299 |
+
.about-modal__footer {
|
| 300 |
+
padding: 16px 20px;
|
| 301 |
+
border-bottom: 1px solid rgba(0,0,0,.08);
|
| 302 |
+
display: flex;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.about-modal__footer {
|
| 306 |
+
border-top: 1px solid rgba(0,0,0,.08);
|
| 307 |
+
border-bottom: none;
|
| 308 |
+
display: flex;
|
| 309 |
+
justify-content: flex-end;
|
| 310 |
+
gap: 8px;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.about-modal__body {
|
| 314 |
+
padding: 16px 20px;
|
| 315 |
+
line-height: 1.6;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.about-close {
|
| 319 |
+
margin-left: auto;
|
| 320 |
+
border: none;
|
| 321 |
+
background: transparent;
|
| 322 |
+
font-size: 20px;
|
| 323 |
+
cursor: pointer;
|
| 324 |
+
color: #111;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
/* Make the embedded auth components look like panels */
|
| 328 |
+
/*:host ::ng-deep app-sign-in .auth-page,
|
| 329 |
+
:host ::ng-deep app-sign-up .auth-page {
|
| 330 |
+
min-height: auto !important;
|
| 331 |
+
padding: 0 !important;
|
| 332 |
+
background: transparent !important;
|
| 333 |
+
}*/
|
| 334 |
+
|
| 335 |
+
/*:host ::ng-deep app-sign-in .auth-box,
|
| 336 |
+
:host ::ng-deep app-sign-up .auth-box {
|
| 337 |
+
width: min(920px, 92vw) !important;
|
| 338 |
+
margin: 0 auto !important;
|
| 339 |
+
}*/
|
| 340 |
+
|
| 341 |
+
/* Optional: if you want a tighter width for Sign-In
|
| 342 |
+
:host ::ng-deep app-sign-in .auth-box {
|
| 343 |
+
width: min(720px, 92vw) !important;
|
| 344 |
+
}
|
| 345 |
+
*/
|
| 346 |
+
.modal-backdrop {
|
| 347 |
+
position: fixed;
|
| 348 |
+
inset: 0;
|
| 349 |
+
background: rgb(255 255 255 / 34%);
|
| 350 |
+
display: grid;
|
| 351 |
+
place-items: center;
|
| 352 |
+
z-index: 1000;
|
| 353 |
+
animation: fadeIn 0.2s ease-out;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.modal-panel {
|
| 357 |
+
animation: slideIn 0.3s ease-out;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
@keyframes fadeIn {
|
| 361 |
+
from {
|
| 362 |
+
opacity: 0;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
to {
|
| 366 |
+
opacity: 1;
|
| 367 |
+
}
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
@keyframes slideIn {
|
| 371 |
+
from {
|
| 372 |
+
opacity: 0;
|
| 373 |
+
transform: translateY(-20px);
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
to {
|
| 377 |
+
opacity: 1;
|
| 378 |
+
transform: translateY(0);
|
| 379 |
+
}
|
| 380 |
+
}
|
src/app/intro-page/intro-page.component.html
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<a class="skip-link" href="#main">Skip to content</a>
|
| 2 |
+
|
| 3 |
+
<section class="page">
|
| 4 |
+
|
| 5 |
+
<!-- Top bar -->
|
| 6 |
+
<header class="topbar" role="banner" aria-label="Top navigation">
|
| 7 |
+
<div class="brand" aria-label="Application name">
|
| 8 |
+
<img src="assets/pykara-logo.png" alt="Pykara Technologies logo" class="brand__logo-img" />
|
| 9 |
+
<span class="brand__name">{{ tr('brand') }}</span>
|
| 10 |
+
</div>
|
| 11 |
+
|
| 12 |
+
<nav class="actions" aria-label="Actions">
|
| 13 |
+
<!-- CHANGED: open modal instead of routerLink -->
|
| 14 |
+
<button type="button" class="btn btn-primary" (click)="openSignUp()">{{ tr('signup') }}</button>
|
| 15 |
+
<button type="button" class="btn btn-ghost" (click)="openSignIn()">{{ tr('signin') }}</button>
|
| 16 |
+
</nav>
|
| 17 |
+
</header>
|
| 18 |
+
|
| 19 |
+
<!-- Hero (unchanged) -->
|
| 20 |
+
<main id="main" class="hero" role="main">
|
| 21 |
+
<div class="hero__content">
|
| 22 |
+
<h1 class="hero__title">{{ tr('tagline') }}</h1>
|
| 23 |
+
<p class="hero__text">{{ tr('subtext') }}</p>
|
| 24 |
+
<div class="cta">
|
| 25 |
+
<!-- Updated: Add disabled attribute and tooltip -->
|
| 26 |
+
<a routerLink="/question-answer"
|
| 27 |
+
class="btn btn-primary"
|
| 28 |
+
[class.btn-disabled]="!isSignedIn"
|
| 29 |
+
[attr.aria-disabled]="!isSignedIn"
|
| 30 |
+
[title]="!isSignedIn ? 'Please sign in first' : ''">
|
| 31 |
+
{{ tr('getStarted') }}
|
| 32 |
+
</a>
|
| 33 |
+
<small *ngIf="!isSignedIn" style="display: block; margin-top: 8px; color: #ffa500;">
|
| 34 |
+
Please sign in to get started
|
| 35 |
+
</small>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
</main>
|
| 39 |
+
|
| 40 |
+
<!-- Footer (updated) -->
|
| 41 |
+
<footer class="footer" aria-label="Footer">
|
| 42 |
+
<a href="https://pykara.net/"
|
| 43 |
+
target="_blank"
|
| 44 |
+
rel="noopener noreferrer"
|
| 45 |
+
aria-label="Visit Pykara website">
|
| 46 |
+
<i class="fa-solid fa-globe" aria-hidden="true"></i>
|
| 47 |
+
<span style="font-family: sans-serif">Visit Pykara</span>
|
| 48 |
+
</a>
|
| 49 |
+
|
| 50 |
+
<!-- About us button -->
|
| 51 |
+
<button type="button"
|
| 52 |
+
class="about-btn"
|
| 53 |
+
(click)="openAbout()"
|
| 54 |
+
aria-haspopup="dialog"
|
| 55 |
+
aria-controls="aboutDialog"
|
| 56 |
+
[attr.aria-expanded]="isAboutOpen ? 'true' : 'false'">
|
| 57 |
+
<i class="fa-solid fa-circle-info" aria-hidden="true"></i>
|
| 58 |
+
<span>About us</span>
|
| 59 |
+
</button>
|
| 60 |
+
</footer>
|
| 61 |
+
|
| 62 |
+
<!-- AUTH MODAL OVERLAY (unchanged) -->
|
| 63 |
+
<div class="modal-backdrop" *ngIf="modal" (click)="backdropClick($event)">
|
| 64 |
+
<div class="modal-panel modal-panel--white"
|
| 65 |
+
role="dialog"
|
| 66 |
+
aria-modal="true"
|
| 67 |
+
[attr.aria-label]="modal === 'signin' ? 'Sign in' : 'Sign up'">
|
| 68 |
+
<button type="button" class="modal-close" (click)="closeModal()" aria-label="Close">×</button>
|
| 69 |
+
|
| 70 |
+
<app-sign-in *ngIf="modal === 'signin'"
|
| 71 |
+
(switchToSignUp)="openSignUp()"
|
| 72 |
+
(signInSuccess)="handleSignInSuccess()"></app-sign-in>
|
| 73 |
+
<app-sign-up *ngIf="modal === 'signup'"
|
| 74 |
+
(switchToSignIn)="openSignIn()"
|
| 75 |
+
(signUpSuccess)="handleSignUpSuccess()"></app-sign-up> <!-- Add this line -->
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<!-- ABOUT MODAL OVERLAY (new) -->
|
| 80 |
+
<div class="about-backdrop" *ngIf="isAboutOpen" (click)="closeAbout()" tabindex="-1"></div>
|
| 81 |
+
|
| 82 |
+
<div class="about-modal"
|
| 83 |
+
*ngIf="isAboutOpen"
|
| 84 |
+
role="dialog"
|
| 85 |
+
aria-modal="true"
|
| 86 |
+
aria-labelledby="aboutTitle"
|
| 87 |
+
id="aboutDialog"
|
| 88 |
+
tabindex="-1">
|
| 89 |
+
<div class="about-modal__header">
|
| 90 |
+
<h2 id="aboutTitle">About Py-Match</h2>
|
| 91 |
+
<button class="about-close" (click)="closeAbout()" aria-label="Close dialog">×</button>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<div class="about-modal__body">
|
| 95 |
+
<p><strong>Py-Match</strong> is an AI-driven personality profiling and matching system that models human traits using four colors — Red, Blue, Green, and Yellow. It conducts an adaptive Q&A session where each response secretly maps to a color; the engine then computes a percentage distribution across the four colors to describe the user’s dominant and supporting traits.</p>
|
| 96 |
+
<p>The platform applies these profiles to practical scenarios — such as marriage compatibility, recruitment, leadership assessment, and personal development — by comparing user profiles to role templates with weighted color mixes. The system architecture includes a web/mobile UI, an AI question generator, a scoring and profile calculator, a matching engine, and secure data storage. The design emphasizes simplicity, scalability, localization, and privacy.</p>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
</section>
|
src/app/intro-page/intro-page.component.spec.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
| 2 |
+
|
| 3 |
+
import { IntroPageComponent } from './intro-page.component';
|
| 4 |
+
|
| 5 |
+
describe('IntroPageComponent', () => {
|
| 6 |
+
let component: IntroPageComponent;
|
| 7 |
+
let fixture: ComponentFixture<IntroPageComponent>;
|
| 8 |
+
|
| 9 |
+
beforeEach(() => {
|
| 10 |
+
TestBed.configureTestingModule({
|
| 11 |
+
declarations: [IntroPageComponent]
|
| 12 |
+
});
|
| 13 |
+
fixture = TestBed.createComponent(IntroPageComponent);
|
| 14 |
+
component = fixture.componentInstance;
|
| 15 |
+
fixture.detectChanges();
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
it('should create', () => {
|
| 19 |
+
expect(component).toBeTruthy();
|
| 20 |
+
});
|
| 21 |
+
});
|
src/app/intro-page/intro-page.component.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Component, ChangeDetectionStrategy, HostListener } from '@angular/core';
|
| 2 |
+
import { CommonModule } from '@angular/common';
|
| 3 |
+
import { RouterLink } from '@angular/router';
|
| 4 |
+
import { SignInComponent } from '../auth/sign-in/sign-in.component';
|
| 5 |
+
import { SignUpComponent } from '../auth/sign-up/sign-up.component';
|
| 6 |
+
|
| 7 |
+
type AuthModal = 'signin' | 'signup' | null;
|
| 8 |
+
|
| 9 |
+
@Component({
|
| 10 |
+
selector: 'app-intro-page',
|
| 11 |
+
standalone: true,
|
| 12 |
+
imports: [CommonModule, RouterLink, SignInComponent, SignUpComponent],
|
| 13 |
+
templateUrl: './intro-page.component.html',
|
| 14 |
+
styleUrls: ['./intro-page.component.css'],
|
| 15 |
+
changeDetection: ChangeDetectionStrategy.OnPush
|
| 16 |
+
})
|
| 17 |
+
export class IntroPageComponent {
|
| 18 |
+
modal: AuthModal = null;
|
| 19 |
+
isSignedIn = false;
|
| 20 |
+
isAboutOpen = false;
|
| 21 |
+
|
| 22 |
+
// --- Auth modal handlers ---
|
| 23 |
+
openSignIn(): void {
|
| 24 |
+
this.modal = 'signin';
|
| 25 |
+
document.body.style.overflow = 'hidden';
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
openSignUp(): void {
|
| 29 |
+
this.modal = 'signup';
|
| 30 |
+
document.body.style.overflow = 'hidden';
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
closeModal(): void {
|
| 34 |
+
this.modal = null;
|
| 35 |
+
if (!this.isAboutOpen) document.body.style.overflow = '';
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
// Called by child components on success
|
| 40 |
+
handleSignInSuccess(): void {
|
| 41 |
+
this.isSignedIn = true;
|
| 42 |
+
this.closeModal();
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Handle sign-up success - switch to sign-in modal
|
| 46 |
+
handleSignUpSuccess(): void {
|
| 47 |
+
// Close sign-up and open sign-in
|
| 48 |
+
this.modal = 'signin';
|
| 49 |
+
// You can optionally show a success message here
|
| 50 |
+
alert('Sign up successful! Please sign in to continue.');
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
backdropClick(e: MouseEvent): void {
|
| 54 |
+
if ((e.target as HTMLElement).classList.contains('modal-backdrop')) {
|
| 55 |
+
this.closeModal();
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// --- About modal handlers ---
|
| 60 |
+
openAbout(): void {
|
| 61 |
+
this.isAboutOpen = true;
|
| 62 |
+
document.body.style.overflow = 'hidden';
|
| 63 |
+
// Move focus into the dialog for accessibility
|
| 64 |
+
setTimeout(() => document.getElementById('aboutDialog')?.focus(), 0);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
closeAbout(): void {
|
| 68 |
+
this.isAboutOpen = false;
|
| 69 |
+
// If Auth is also open, keep scroll locked. Otherwise restore.
|
| 70 |
+
if (!this.modal) document.body.style.overflow = '';
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// --- Global ESC handler closes whichever is open ---
|
| 74 |
+
@HostListener('document:keydown.escape')
|
| 75 |
+
onEsc(): void {
|
| 76 |
+
if (this.modal) this.closeModal();
|
| 77 |
+
else if (this.isAboutOpen) this.closeAbout();
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Your existing i18n helper, keep as-is if already present:
|
| 81 |
+
tr(key: string): string {
|
| 82 |
+
const dict = {
|
| 83 |
+
brand: 'Py-Match',
|
| 84 |
+
signup: 'Sign up',
|
| 85 |
+
signin: 'Sign in',
|
| 86 |
+
tagline: 'Better match starts here',
|
| 87 |
+
subtext: 'Designed for trust, focused on fit.',
|
| 88 |
+
getStarted: 'Get started'
|
| 89 |
+
};
|
| 90 |
+
return dict[key as keyof typeof dict] ?? key;
|
| 91 |
+
}
|
| 92 |
+
}
|
src/app/llm-quiz/llm-quiz.component.css
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*.quiz-wrap {
|
| 2 |
+
max-width: 880px;
|
| 3 |
+
margin: 20px auto;
|
| 4 |
+
padding: 12px;
|
| 5 |
+
font-family: system-ui, Arial, sans-serif;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
h2 {
|
| 9 |
+
margin: 0 0 12px;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.panel, .qcard, .result {
|
| 13 |
+
border: 1px solid #ddd;
|
| 14 |
+
border-radius: 8px;
|
| 15 |
+
padding: 12px;
|
| 16 |
+
background: #fff;
|
| 17 |
+
margin-bottom: 14px;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.row {
|
| 21 |
+
display: flex;
|
| 22 |
+
align-items: center;
|
| 23 |
+
gap: 12px;
|
| 24 |
+
margin-bottom: 10px;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.row.column {
|
| 28 |
+
flex-direction: column;
|
| 29 |
+
align-items: stretch;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.row label {
|
| 33 |
+
width: 180px;
|
| 34 |
+
font-weight: 600;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.row input[type="number"], .row select, textarea {
|
| 38 |
+
flex: 1;
|
| 39 |
+
padding: 8px;
|
| 40 |
+
border: 1px solid #ccc;
|
| 41 |
+
border-radius: 6px;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.actions {
|
| 45 |
+
display: flex;
|
| 46 |
+
gap: 10px;
|
| 47 |
+
margin-top: 8px;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.actions button {
|
| 51 |
+
padding: 8px 14px;
|
| 52 |
+
border: 0;
|
| 53 |
+
border-radius: 6px;
|
| 54 |
+
background: #111827;
|
| 55 |
+
color: #fff;
|
| 56 |
+
cursor: pointer;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.actions button[disabled] {
|
| 60 |
+
opacity: 0.5;
|
| 61 |
+
cursor: not-allowed;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.error {
|
| 65 |
+
color: #b91c1c;
|
| 66 |
+
margin-top: 8px;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.loading {
|
| 70 |
+
margin-top: 10px;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.meta {
|
| 74 |
+
display: flex;
|
| 75 |
+
justify-content: space-between;
|
| 76 |
+
margin-bottom: 8px;
|
| 77 |
+
color: #374151;
|
| 78 |
+
font-size: 13px;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.question {
|
| 82 |
+
font-size: 18px;
|
| 83 |
+
font-weight: 700;
|
| 84 |
+
margin-bottom: 10px;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.options {
|
| 88 |
+
display: grid;
|
| 89 |
+
gap: 8px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.opt {
|
| 93 |
+
display: flex;
|
| 94 |
+
align-items: center;
|
| 95 |
+
gap: 10px;
|
| 96 |
+
padding: 8px;
|
| 97 |
+
border: 1px solid #eee;
|
| 98 |
+
border-radius: 6px;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.opt input {
|
| 102 |
+
transform: scale(1.1);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.badge {
|
| 106 |
+
display: inline-block;
|
| 107 |
+
padding: 3px 8px;
|
| 108 |
+
border-radius: 999px;
|
| 109 |
+
font-size: 12px;
|
| 110 |
+
color: #fff;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.badge.blue {
|
| 114 |
+
background: #2563eb;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.badge.green {
|
| 118 |
+
background: #059669;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.badge.red {
|
| 122 |
+
background: #dc2626;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.badge.yellow {
|
| 126 |
+
background: #d97706;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.mix, .result {
|
| 130 |
+
margin-top: 14px;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.bar {
|
| 134 |
+
display: grid;
|
| 135 |
+
grid-template-columns: 90px 1fr 48px;
|
| 136 |
+
align-items: center;
|
| 137 |
+
gap: 8px;
|
| 138 |
+
margin: 6px 0;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.bar-label {
|
| 142 |
+
font-weight: 600;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.bar-track {
|
| 146 |
+
height: 10px;
|
| 147 |
+
background: #f3f4f6;
|
| 148 |
+
border-radius: 999px;
|
| 149 |
+
overflow: hidden;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.bar-fill {
|
| 153 |
+
height: 100%;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.bar-fill.blue {
|
| 157 |
+
background: #93c5fd;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.bar-fill.green {
|
| 161 |
+
background: #86efac;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.bar-fill.red {
|
| 165 |
+
background: #fca5a5;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.bar-fill.yellow {
|
| 169 |
+
background: #fde68a;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.bar-val {
|
| 173 |
+
text-align: right;
|
| 174 |
+
font-variant-numeric: tabular-nums;
|
| 175 |
+
}
|
| 176 |
+
*/
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
.panel {
|
| 180 |
+
padding: 12px;
|
| 181 |
+
border: 1px solid #ddd;
|
| 182 |
+
border-radius: 8px;
|
| 183 |
+
margin-bottom: 16px;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.row {
|
| 187 |
+
display: flex;
|
| 188 |
+
gap: 8px;
|
| 189 |
+
align-items: center;
|
| 190 |
+
margin: 8px 0;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.row label {
|
| 194 |
+
width: 160px;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.actions {
|
| 198 |
+
margin-top: 12px;
|
| 199 |
+
display: flex;
|
| 200 |
+
gap: 8px;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.loader-overlay {
|
| 204 |
+
position: fixed;
|
| 205 |
+
inset: 0;
|
| 206 |
+
display: grid;
|
| 207 |
+
place-items: center;
|
| 208 |
+
background: rgba(0,0,0,0.08);
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.question-card {
|
| 212 |
+
padding: 16px;
|
| 213 |
+
border: 1px solid #eee;
|
| 214 |
+
border-radius: 8px;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.q-meta {
|
| 218 |
+
display: flex;
|
| 219 |
+
gap: 16px;
|
| 220 |
+
color: #666;
|
| 221 |
+
margin-bottom: 8px;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.q-text {
|
| 225 |
+
margin: 8px 0 16px;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.options {
|
| 229 |
+
display: grid;
|
| 230 |
+
gap: 8px;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.option {
|
| 234 |
+
display: flex;
|
| 235 |
+
align-items: center;
|
| 236 |
+
gap: 8px;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.opt-color {
|
| 240 |
+
color: #888;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.progress, .result {
|
| 244 |
+
margin-top: 16px;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.bar {
|
| 248 |
+
display: grid;
|
| 249 |
+
grid-template-columns: 80px 1fr 60px;
|
| 250 |
+
align-items: center;
|
| 251 |
+
gap: 8px;
|
| 252 |
+
margin: 6px 0;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.bar-track {
|
| 256 |
+
height: 10px;
|
| 257 |
+
background: #eee;
|
| 258 |
+
border-radius: 8px;
|
| 259 |
+
overflow: hidden;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.bar-fill {
|
| 263 |
+
height: 100%;
|
| 264 |
+
background: #7dafff;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.bar-val {
|
| 268 |
+
text-align: right;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.empty {
|
| 272 |
+
color: #777;
|
| 273 |
+
margin-top: 16px;
|
| 274 |
+
}
|
src/app/llm-quiz/llm-quiz.component.html
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- Control panel -->
|
| 2 |
+
<!--<section class="panel">
|
| 3 |
+
<div class="row">
|
| 4 |
+
<label>User ID</label>
|
| 5 |
+
<input type="text" [(ngModel)]="userId" placeholder="e.g. 1" />
|
| 6 |
+
</div>
|
| 7 |
+
|
| 8 |
+
<div class="row">
|
| 9 |
+
<label>Role</label>
|
| 10 |
+
<select [(ngModel)]="role">
|
| 11 |
+
<option value="interview">interview</option>
|
| 12 |
+
<option value="marriage">marriage</option>
|
| 13 |
+
<option value="partnership">partnership</option>
|
| 14 |
+
<option value="team">team</option>
|
| 15 |
+
<option value="general">general</option>
|
| 16 |
+
<option value="assistant">assistant</option>
|
| 17 |
+
<option value="ceo">ceo</option>
|
| 18 |
+
</select>
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<div class="row">
|
| 22 |
+
<label>No. of questions</label>
|
| 23 |
+
<input type="number" [(ngModel)]="nQuestions" min="1" max="50" />
|
| 24 |
+
</div>
|
| 25 |
+
|
| 26 |
+
<div class="row">
|
| 27 |
+
<label>Batch size</label>
|
| 28 |
+
<input type="number" [(ngModel)]="batchSize" min="1" max="20" />
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
<div class="actions">
|
| 32 |
+
<button type="button" (click)="start()" [disabled]="loading">Start</button>
|
| 33 |
+
<button type="button" (click)="restart()" [disabled]="loading">Reset</button>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<p class="error" *ngIf="errorMsg">{{ errorMsg }}</p>
|
| 37 |
+
</section>-->
|
| 38 |
+
|
| 39 |
+
<!-- Loader -->
|
| 40 |
+
<div class="loader-overlay" *ngIf="loading">
|
| 41 |
+
<div class="loader">Loading…</div>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<!-- Quiz area -->
|
| 45 |
+
<section class="quiz" *ngIf="!loading">
|
| 46 |
+
|
| 47 |
+
<!-- When there is an active question -->
|
| 48 |
+
<div class="question-card" *ngIf="!isDone && question">
|
| 49 |
+
<div class="q-meta">
|
| 50 |
+
<div class="q-count">Question {{ index }} / {{ total }}</div>
|
| 51 |
+
<div class="q-role">Role: {{ role }}</div>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<h2 class="q-text">{{ question }}</h2>
|
| 55 |
+
|
| 56 |
+
<form (ngSubmit)="submitAnswer()">
|
| 57 |
+
<div class="options">
|
| 58 |
+
<label class="option" *ngFor="let opt of options; let i = index">
|
| 59 |
+
<input type="radio"
|
| 60 |
+
name="answer"
|
| 61 |
+
[value]="opt.color"
|
| 62 |
+
[(ngModel)]="selectedColor"
|
| 63 |
+
required />
|
| 64 |
+
<span class="opt-text">{{ opt.text }}</span>
|
| 65 |
+
<!--<small class="opt-color">({{ opt.color }})</small>-->
|
| 66 |
+
</label>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
<div class="actions">
|
| 70 |
+
<button type="submit" [disabled]="!selectedColor || loading">Submit</button>
|
| 71 |
+
</div>
|
| 72 |
+
</form>
|
| 73 |
+
|
| 74 |
+
<!-- In-progress mix (live) -->
|
| 75 |
+
<!--<div class="progress" *ngIf="inProgressMix">
|
| 76 |
+
<h3>Current mix</h3>
|
| 77 |
+
<div class="bar">
|
| 78 |
+
<span>Blue</span>
|
| 79 |
+
<div class="bar-track"><div class="bar-fill" [style.width.%]="getInProgressMixValue('blue')"></div></div>
|
| 80 |
+
<div class="bar-val">{{ asPercent(getInProgressMixValue('blue')) }}%</div>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<div class="bar">
|
| 84 |
+
<span>Green</span>
|
| 85 |
+
<div class="bar-track"><div class="bar-fill" [style.width.%]="getInProgressMixValue('green')"></div></div>
|
| 86 |
+
<div class="bar-val">{{ asPercent(getInProgressMixValue('green')) }}%</div>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<div class="bar">
|
| 90 |
+
<span>Red</span>
|
| 91 |
+
<div class="bar-track"><div class="bar-fill" [style.width.%]="getInProgressMixValue('red')"></div></div>
|
| 92 |
+
<div class="bar-val">{{ asPercent(getInProgressMixValue('red')) }}%</div>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<div class="bar">
|
| 96 |
+
<span>Yellow</span>
|
| 97 |
+
<div class="bar-track"><div class="bar-fill" [style.width.%]="getInProgressMixValue('yellow')"></div></div>
|
| 98 |
+
<div class="bar-val">{{ asPercent(getInProgressMixValue('yellow')) }}%</div>
|
| 99 |
+
</div>
|
| 100 |
+
</div>-->
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<!-- Final result -->
|
| 104 |
+
<div>DONE</div>
|
| 105 |
+
<!--<div class="result" *ngIf="isDone && finalMix">
|
| 106 |
+
<h2>Final result</h2>
|
| 107 |
+
|
| 108 |
+
<div class="bar">
|
| 109 |
+
<span>Blue</span>
|
| 110 |
+
<div class="bar-track"><div class="bar-fill" [style.width.%]="getFinalMixValue('blue')"></div></div>-->
|
| 111 |
+
<!--<div class="bar-val">{{ asPercent(getFinalMixValue('blue')) }}%</div>-->
|
| 112 |
+
<!--</div>
|
| 113 |
+
|
| 114 |
+
<div class="bar">
|
| 115 |
+
<span>Green</span>
|
| 116 |
+
<div class="bar-track"><div class="bar-fill" [style.width.%]="getFinalMixValue('green')"></div></div>-->
|
| 117 |
+
<!--<div class="bar-val">{{ asPercent(getFinalMixValue('green')) }}%</div>-->
|
| 118 |
+
<!--</div>
|
| 119 |
+
|
| 120 |
+
<div class="bar">
|
| 121 |
+
<span>Red</span>
|
| 122 |
+
<div class="bar-track"><div class="bar-fill" [style.width.%]="getFinalMixValue('red')"></div></div>-->
|
| 123 |
+
<!--<div class="bar-val">{{ asPercent(getFinalMixValue('red')) }}%</div>-->
|
| 124 |
+
<!--</div>
|
| 125 |
+
|
| 126 |
+
<div class="bar">
|
| 127 |
+
<span>Yellow</span>
|
| 128 |
+
<div class="bar-track"><div class="bar-fill" [style.width.%]="getFinalMixValue('yellow')"></div></div>-->
|
| 129 |
+
<!--<div class="bar-val">{{ asPercent(getFinalMixValue('yellow')) }}%</div>-->
|
| 130 |
+
<!--</div>
|
| 131 |
+
|
| 132 |
+
<div class="actions">
|
| 133 |
+
<button type="button" (click)="restart()">Restart</button>
|
| 134 |
+
</div>
|
| 135 |
+
</div>-->
|
| 136 |
+
|
| 137 |
+
<!-- Empty state -->
|
| 138 |
+
<div class="empty" *ngIf="!isDone && !question && !errorMsg">
|
| 139 |
+
<p>Press <strong>Start</strong> to begin the quiz.</p>
|
| 140 |
+
</div>
|
| 141 |
+
</section>
|
| 142 |
+
|
src/app/llm-quiz/llm-quiz.component.spec.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
| 2 |
+
|
| 3 |
+
import { LlmQuizComponent } from './llm-quiz.component';
|
| 4 |
+
|
| 5 |
+
describe('LlmQuizComponent', () => {
|
| 6 |
+
let component: LlmQuizComponent;
|
| 7 |
+
let fixture: ComponentFixture<LlmQuizComponent>;
|
| 8 |
+
|
| 9 |
+
beforeEach(() => {
|
| 10 |
+
TestBed.configureTestingModule({
|
| 11 |
+
imports: [LlmQuizComponent]
|
| 12 |
+
});
|
| 13 |
+
fixture = TestBed.createComponent(LlmQuizComponent);
|
| 14 |
+
component = fixture.componentInstance;
|
| 15 |
+
fixture.detectChanges();
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
it('should create', () => {
|
| 19 |
+
expect(component).toBeTruthy();
|
| 20 |
+
});
|
| 21 |
+
});
|
src/app/llm-quiz/llm-quiz.component.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Component, OnInit } from '@angular/core';
|
| 2 |
+
import { FormsModule } from '@angular/forms';
|
| 3 |
+
import { CommonModule } from '@angular/common';
|
| 4 |
+
import { LlmQaService, Role, ColorKey, StartResponse, NextResponse } from '../services/llm-qa.service';
|
| 5 |
+
import { ActivatedRoute, Router } from '@angular/router';
|
| 6 |
+
|
| 7 |
+
type Mix = Record<ColorKey, number>;
|
| 8 |
+
|
| 9 |
+
@Component({
|
| 10 |
+
selector: 'app-llm-quiz',
|
| 11 |
+
standalone: true,
|
| 12 |
+
imports: [CommonModule, FormsModule],
|
| 13 |
+
templateUrl: './llm-quiz.component.html',
|
| 14 |
+
styleUrls: ['./llm-quiz.component.css'],
|
| 15 |
+
})
|
| 16 |
+
export class LlmQuizComponent implements OnInit {
|
| 17 |
+
// --- Config bound to the UI ---
|
| 18 |
+
role: Role = 'marriage';
|
| 19 |
+
nQuestions = 5;
|
| 20 |
+
batchSize = 5;
|
| 21 |
+
|
| 22 |
+
/** Provide user id. Will auto-read from localStorage('user_id') if available. */
|
| 23 |
+
userId = '';
|
| 24 |
+
|
| 25 |
+
// --- Runtime state ---
|
| 26 |
+
sessionId = '';
|
| 27 |
+
index = 0;
|
| 28 |
+
total = 0;
|
| 29 |
+
|
| 30 |
+
question = '';
|
| 31 |
+
options: { text: string; color: ColorKey }[] = [];
|
| 32 |
+
selectedColor: ColorKey | '' = '';
|
| 33 |
+
|
| 34 |
+
loading = false;
|
| 35 |
+
errorMsg = '';
|
| 36 |
+
isDone = false;
|
| 37 |
+
|
| 38 |
+
inProgressMix: Mix | null = null; // live progress returned by backend
|
| 39 |
+
finalMix: Mix | null = null;
|
| 40 |
+
|
| 41 |
+
/*constructor(private api: LlmQaService) { }*/
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
constructor(
|
| 45 |
+
private api: LlmQaService,
|
| 46 |
+
private route: ActivatedRoute,
|
| 47 |
+
private router: Router
|
| 48 |
+
) { }
|
| 49 |
+
|
| 50 |
+
ngOnInit(): void {
|
| 51 |
+
const qp = this.route.snapshot.queryParamMap;
|
| 52 |
+
const uid = (qp.get('user_id') || localStorage.getItem('user_id') || '').trim();
|
| 53 |
+
const role = (qp.get('role') || localStorage.getItem('role') || '').toLowerCase();
|
| 54 |
+
const autostart = qp.get('autostart') === '1';
|
| 55 |
+
|
| 56 |
+
if(uid) this.userId = uid;
|
| 57 |
+
if(role) this.role = role as any;
|
| 58 |
+
|
| 59 |
+
if(autostart && this.userId && this.role) {
|
| 60 |
+
this.start();
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
// ---------- Helpers ----------
|
| 66 |
+
asPercent(v: number | undefined | null): number {
|
| 67 |
+
const n = Number(v);
|
| 68 |
+
return Number.isFinite(n) ? Math.round(n) : 0;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/** Safe getter for in-progress mix values. */
|
| 72 |
+
getInProgressMixValue(c: string): number {
|
| 73 |
+
if (!this.inProgressMix) return 0;
|
| 74 |
+
const key = this.toColorKey(c);
|
| 75 |
+
return key ? this.inProgressMix[key] ?? 0 : 0;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/** Safe getter for final mix values. */
|
| 79 |
+
getFinalMixValue(c: string): number {
|
| 80 |
+
if (!this.finalMix) return 0;
|
| 81 |
+
const key = this.toColorKey(c);
|
| 82 |
+
return key ? this.finalMix[key] ?? 0 : 0;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
private toColorKey(c: string): ColorKey | null {
|
| 86 |
+
switch (c) {
|
| 87 |
+
case 'blue':
|
| 88 |
+
case 'green':
|
| 89 |
+
case 'red':
|
| 90 |
+
case 'yellow':
|
| 91 |
+
return c;
|
| 92 |
+
default:
|
| 93 |
+
return null;
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// ---------- Flow ----------
|
| 98 |
+
//start(): void {
|
| 99 |
+
// this.errorMsg = '';
|
| 100 |
+
// this.isDone = false;
|
| 101 |
+
// this.finalMix = null;
|
| 102 |
+
// this.inProgressMix = null;
|
| 103 |
+
// this.selectedColor = '';
|
| 104 |
+
// this.sessionId = '';
|
| 105 |
+
// this.index = 0;
|
| 106 |
+
// this.total = 0;
|
| 107 |
+
// this.question = '';
|
| 108 |
+
// this.options = [];
|
| 109 |
+
|
| 110 |
+
// if (!this.userId?.trim()) {
|
| 111 |
+
// this.errorMsg = 'User ID is required.';
|
| 112 |
+
// return;
|
| 113 |
+
// }
|
| 114 |
+
|
| 115 |
+
// this.loading = true;
|
| 116 |
+
// this.api .start({
|
| 117 |
+
|
| 118 |
+
// user_id: this.userId.trim(),
|
| 119 |
+
// role: this.role,
|
| 120 |
+
// n_questions: this.nQuestions,
|
| 121 |
+
// batch_size: this.batchSize,
|
| 122 |
+
// })
|
| 123 |
+
// .subscribe({
|
| 124 |
+
// next: (res: StartResponse) => {
|
| 125 |
+
// this.loading = false;
|
| 126 |
+
// this.sessionId = res.session_id;
|
| 127 |
+
// this.index = res.index;
|
| 128 |
+
// this.total = res.total;
|
| 129 |
+
// this.question = res.question;
|
| 130 |
+
// this.options = (res.options || []) as any;
|
| 131 |
+
// // Optional: inspect res.profile_used
|
| 132 |
+
// if (res.profile_used === false) {
|
| 133 |
+
// this.errorMsg =
|
| 134 |
+
// 'No profile found for this user and role. Please complete the profile first.';
|
| 135 |
+
// }
|
| 136 |
+
// },
|
| 137 |
+
// error: (err) => {
|
| 138 |
+
// this.loading = false;
|
| 139 |
+
// this.errorMsg = err?.error?.error || 'Failed to start LLM session.';
|
| 140 |
+
// },
|
| 141 |
+
// });
|
| 142 |
+
//}
|
| 143 |
+
start(): void {
|
| 144 |
+
this.errorMsg = '';
|
| 145 |
+
this.isDone = false;
|
| 146 |
+
this.finalMix = null;
|
| 147 |
+
this.inProgressMix = null;
|
| 148 |
+
this.selectedColor = '';
|
| 149 |
+
this.sessionId = '';
|
| 150 |
+
this.index = 0;
|
| 151 |
+
this.total = 0;
|
| 152 |
+
this.question = '';
|
| 153 |
+
this.options = [];
|
| 154 |
+
|
| 155 |
+
if (!this.userId?.trim()) {
|
| 156 |
+
this.errorMsg = 'User ID is required.';
|
| 157 |
+
return;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
this.loading = true;
|
| 161 |
+
|
| 162 |
+
// Log the payload before the API request
|
| 163 |
+
const payload = {
|
| 164 |
+
user_id: this.userId.trim(),
|
| 165 |
+
role: this.role,
|
| 166 |
+
n_questions: this.nQuestions,
|
| 167 |
+
batch_size: this.batchSize,
|
| 168 |
+
};
|
| 169 |
+
|
| 170 |
+
console.log('Payload:', payload); // Add this line to log the payload
|
| 171 |
+
|
| 172 |
+
this.api.start(payload).subscribe({
|
| 173 |
+
next: (res: StartResponse) => {
|
| 174 |
+
this.loading = false;
|
| 175 |
+
this.sessionId = res.session_id;
|
| 176 |
+
this.index = res.index;
|
| 177 |
+
this.total = res.total;
|
| 178 |
+
this.question = res.question;
|
| 179 |
+
this.options = (res.options || []) as any;
|
| 180 |
+
console.log("res", res);
|
| 181 |
+
// Optional: inspect res.profile_used
|
| 182 |
+
if (res.profile_used === false) {
|
| 183 |
+
this.errorMsg =
|
| 184 |
+
'No profile found for this user and role. Please complete the profile first.';
|
| 185 |
+
}
|
| 186 |
+
},
|
| 187 |
+
error: (err) => {
|
| 188 |
+
this.loading = false;
|
| 189 |
+
this.errorMsg = err?.error?.error || 'Failed to start LLM session.';
|
| 190 |
+
},
|
| 191 |
+
});
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
submitAnswer(): void {
|
| 195 |
+
if (!this.sessionId || !this.selectedColor) return;
|
| 196 |
+
this.loading = true;
|
| 197 |
+
this.errorMsg = '';
|
| 198 |
+
|
| 199 |
+
this.api
|
| 200 |
+
.next({ session_id: this.sessionId, selected_color: this.selectedColor })
|
| 201 |
+
.subscribe({
|
| 202 |
+
next: (res: NextResponse) => {
|
| 203 |
+
this.loading = false;
|
| 204 |
+
|
| 205 |
+
// Finished?
|
| 206 |
+
if (res.done) {
|
| 207 |
+
this.isDone = true;
|
| 208 |
+
this.finalMix = (res.mix || {
|
| 209 |
+
blue: 0,
|
| 210 |
+
green: 0,
|
| 211 |
+
red: 0,
|
| 212 |
+
yellow: 0,
|
| 213 |
+
}) as Mix;
|
| 214 |
+
return;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
// Continue
|
| 218 |
+
this.index = res.index ?? this.index + 1;
|
| 219 |
+
this.total = res.total ?? this.total;
|
| 220 |
+
this.question = res.question || '';
|
| 221 |
+
this.options = (res.options || []) as any;
|
| 222 |
+
this.inProgressMix = (res.progress || null) as Mix;
|
| 223 |
+
this.selectedColor = ''; // reset selection
|
| 224 |
+
},
|
| 225 |
+
error: (err) => {
|
| 226 |
+
this.loading = false;
|
| 227 |
+
this.errorMsg = err?.error?.error || 'Failed to fetch next question.';
|
| 228 |
+
},
|
| 229 |
+
});
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
restart(): void {
|
| 233 |
+
this.isDone = false;
|
| 234 |
+
this.finalMix = null;
|
| 235 |
+
this.inProgressMix = null;
|
| 236 |
+
this.selectedColor = '';
|
| 237 |
+
this.sessionId = '';
|
| 238 |
+
this.index = 0;
|
| 239 |
+
this.total = 0;
|
| 240 |
+
this.question = '';
|
| 241 |
+
this.options = [];
|
| 242 |
+
this.errorMsg = '';
|
| 243 |
+
}
|
| 244 |
+
}
|
src/app/question-answer/question-answer-service.service.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@angular/core';
|
| 2 |
+
import { HttpClient } from '@angular/common/http';
|
| 3 |
+
import { environment } from '../../environments/environment';
|
| 4 |
+
import { Observable } from 'rxjs';
|
| 5 |
+
export interface QAItem {
|
| 6 |
+
label: string;
|
| 7 |
+
input_type: 'text' | 'date' | 'radio' | 'select';
|
| 8 |
+
options?: string[];
|
| 9 |
+
column_key: string;
|
| 10 |
+
}
|
| 11 |
+
@Injectable({ providedIn: 'root' })
|
| 12 |
+
export class QuestionAnswerService {
|
| 13 |
+
private baseUrl = 'http://127.0.0.1:5000/api/questions';
|
| 14 |
+
|
| 15 |
+
constructor(private http: HttpClient) { }
|
| 16 |
+
|
| 17 |
+
// Assign role to user
|
| 18 |
+
assignRole(payload: { user_id: number; role_name: string; assigned_at: string }): Observable<any> {
|
| 19 |
+
return this.http.post<any>(`${this.baseUrl}/select-role`, payload);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// Fetch questions by role
|
| 23 |
+
|
| 24 |
+
//getQuestions(role: string): Observable<any> {
|
| 25 |
+
// return this.http.get<any>(`${this.baseUrl}/${role}`);
|
| 26 |
+
//}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
// Submit answers
|
| 30 |
+
//submitAnswers(role: string, userId: number, answers: any[]): Observable<any> {
|
| 31 |
+
// const payload = {
|
| 32 |
+
// user_id: userId,
|
| 33 |
+
// ...answers.reduce((acc, ans, i) => {
|
| 34 |
+
// acc[`q${i + 1}`] = ans; // map answers to fields (adjust backend expected fields if needed)
|
| 35 |
+
// return acc;
|
| 36 |
+
// }, {}),
|
| 37 |
+
// created_at: new Date().toISOString()
|
| 38 |
+
// };
|
| 39 |
+
|
| 40 |
+
// return this.http.post(`${this.baseUrl}/submit-answers/${role}`, payload);
|
| 41 |
+
//}
|
| 42 |
+
getQuestions(role: string): Observable<QAItem[]> {
|
| 43 |
+
return this.http.get<QAItem[]>(`${this.baseUrl}/${role}`);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// fields = { full_name: 'John', created_at: '...' }
|
| 47 |
+
submitAnswers(role: string, userId: number, fields: Record<string, any>): Observable<any> {
|
| 48 |
+
return this.http.post(`${this.baseUrl}/submit-answers/${role}`, { user_id: userId, ...fields });
|
| 49 |
+
}
|
| 50 |
+
}
|
src/app/question-answer/question-answer.component.css
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ---------- Page & Topbar ---------- */
|
| 2 |
+
.qa-page {
|
| 3 |
+
height: 100%;
|
| 4 |
+
position: relative;
|
| 5 |
+
background-image: url('/assets/Q&A-BG.png'); /* Set the background image */
|
| 6 |
+
background-size: cover; /* Ensure the image covers the entire area */
|
| 7 |
+
background-position: center center; /* Center the background image */
|
| 8 |
+
color: #1a1a1a;
|
| 9 |
+
overflow: hidden;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.topbar {
|
| 13 |
+
position: relative;
|
| 14 |
+
z-index: 2;
|
| 15 |
+
display: grid;
|
| 16 |
+
grid-template-columns: auto 1fr auto;
|
| 17 |
+
align-items: center;
|
| 18 |
+
gap: 12px;
|
| 19 |
+
padding: 14px 18px;
|
| 20 |
+
background: transparent;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.home-btn {
|
| 24 |
+
appearance: none;
|
| 25 |
+
border: 1px solid rgba(0,0,0,0.08);
|
| 26 |
+
background: rgba(255,255,255,0.9);
|
| 27 |
+
backdrop-filter: blur(6px);
|
| 28 |
+
padding: 8px 12px;
|
| 29 |
+
border-radius: 10px;
|
| 30 |
+
cursor: pointer;
|
| 31 |
+
font-weight: 600;
|
| 32 |
+
transition: transform .15s ease, box-shadow .15s ease;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.home-btn:hover {
|
| 36 |
+
transform: translateY(-1px);
|
| 37 |
+
box-shadow: 0 8px 20px rgba(0,0,0,0.08);
|
| 38 |
+
border: 1px solid #f3a54c;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.home-btn:active {
|
| 42 |
+
transform: translateY(0);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.home-btn:focus-visible {
|
| 46 |
+
outline: 3px solid #7cc4ff;
|
| 47 |
+
outline-offset: 2px;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.title {
|
| 51 |
+
margin: 0;
|
| 52 |
+
text-align: center;
|
| 53 |
+
color: #3a3a3a;
|
| 54 |
+
font-size: 3vw;
|
| 55 |
+
color: white;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.topbar-spacer {
|
| 59 |
+
width: 52px;
|
| 60 |
+
height: 1px;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/* ---------- Ambient soft background (subtle stripes + light blobs) ---------- */
|
| 64 |
+
.ambient {
|
| 65 |
+
position: absolute;
|
| 66 |
+
inset: 0;
|
| 67 |
+
z-index: 0;
|
| 68 |
+
pointer-events: none;
|
| 69 |
+
background: #e9cfad57;
|
| 70 |
+
opacity: 0.8;
|
| 71 |
+
top: 50%;
|
| 72 |
+
left: 50%;
|
| 73 |
+
transform: translate(-50%, -50%);
|
| 74 |
+
height: 28vw;
|
| 75 |
+
margin: 0 auto;
|
| 76 |
+
border-radius: 8vw;
|
| 77 |
+
border: 2px solid #e9cfad;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.stage {
|
| 81 |
+
position: absolute;
|
| 82 |
+
z-index: 1;
|
| 83 |
+
top: 50%;
|
| 84 |
+
left: 50%;
|
| 85 |
+
transform: translate(-50%, -50%);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/* Shared orb styles */
|
| 89 |
+
.orb {
|
| 90 |
+
--size: clamp(160px, 26vw, 260px);
|
| 91 |
+
width: var(--size);
|
| 92 |
+
height: var(--size);
|
| 93 |
+
border-radius: 50%;
|
| 94 |
+
display: grid;
|
| 95 |
+
place-items: center;
|
| 96 |
+
border: 10px solid rgba(255,255,255,0.9); /* white rim to mimic gaps */
|
| 97 |
+
box-shadow: 0 28px 60px rgba(0,0,0,0.15), inset 0 1px 0 rgba(255,255,255,0.6);
|
| 98 |
+
position: absolute;
|
| 99 |
+
cursor: pointer;
|
| 100 |
+
text-align: center;
|
| 101 |
+
text-transform: uppercase;
|
| 102 |
+
font-weight: 800;
|
| 103 |
+
letter-spacing: .04em;
|
| 104 |
+
transition: transform .18s ease, box-shadow .18s ease, filter .18s ease;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/* Color palette inspired by your reference image */
|
| 108 |
+
.orb-top {
|
| 109 |
+
background: #595a5e;
|
| 110 |
+
color: #ffffff;
|
| 111 |
+
}
|
| 112 |
+
/* dark grey */
|
| 113 |
+
.orb-left {
|
| 114 |
+
background: #f3a54c;
|
| 115 |
+
color: #2b2316;
|
| 116 |
+
}
|
| 117 |
+
/* warm orange */
|
| 118 |
+
.orb-right {
|
| 119 |
+
background: #d2a784;
|
| 120 |
+
color: #2a1e17;
|
| 121 |
+
}
|
| 122 |
+
/* soft tan */
|
| 123 |
+
|
| 124 |
+
/* Placement to form a neat triad */
|
| 125 |
+
.orb-top {
|
| 126 |
+
bottom: -2vw;
|
| 127 |
+
right: -6vw;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.orb-left {
|
| 131 |
+
left: -2vw;
|
| 132 |
+
top: -2vw;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.orb-right {
|
| 136 |
+
right: 0vw;
|
| 137 |
+
top: -2vw;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/* Hover / focus interactions */
|
| 141 |
+
.orb:hover {
|
| 142 |
+
/*transform: translate(-50%, 0) scale(1.02);*/
|
| 143 |
+
box-shadow: 0 36px 80px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.65);
|
| 144 |
+
transform: translate(0,0) scale(1.02);
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.orb-left:hover {
|
| 148 |
+
transform: translate(0,0) scale(1.02);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.orb-right:hover {
|
| 152 |
+
transform: translate(0,0) scale(1.02);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.orb:focus-visible {
|
| 156 |
+
outline: 4px solid #7cc4ff;
|
| 157 |
+
outline-offset: 3px;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
/* Text inside circles */
|
| 161 |
+
.orb > span {
|
| 162 |
+
line-height: 1.15;
|
| 163 |
+
font-size: clamp(13px, 1.6vw, 18px);
|
| 164 |
+
padding: 0 18px;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
/* Modal Overlay */
|
| 169 |
+
.modal-overlay {
|
| 170 |
+
position: fixed;
|
| 171 |
+
top: 0;
|
| 172 |
+
left: 0;
|
| 173 |
+
right: 0;
|
| 174 |
+
bottom: 0;
|
| 175 |
+
background: rgba(0, 0, 0, 0.7);
|
| 176 |
+
display: flex;
|
| 177 |
+
justify-content: center;
|
| 178 |
+
align-items: center;
|
| 179 |
+
z-index: 1000;
|
| 180 |
+
animation: 0.3s ease-in-out;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
/* Modal Content */
|
| 184 |
+
.modal {
|
| 185 |
+
display: flex;
|
| 186 |
+
border-radius: 20px;
|
| 187 |
+
width: 70%;
|
| 188 |
+
max-height: 80%;
|
| 189 |
+
overflow-y: auto;
|
| 190 |
+
position: relative;
|
| 191 |
+
animation: slideUp 0.3s ease-out;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
/* Left Side: Header and Image */
|
| 195 |
+
.modal .left-side {
|
| 196 |
+
width: 50%;
|
| 197 |
+
background: url('/assets/popup.png') no-repeat center center;
|
| 198 |
+
background-size: cover;
|
| 199 |
+
position: relative;
|
| 200 |
+
padding: 20px;
|
| 201 |
+
display: flex;
|
| 202 |
+
justify-content: flex-start;
|
| 203 |
+
align-items: flex-start;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.modal h2 {
|
| 207 |
+
font-size: 24px;
|
| 208 |
+
font-weight: 700;
|
| 209 |
+
color: #fff;
|
| 210 |
+
position: absolute;
|
| 211 |
+
top: 20px;
|
| 212 |
+
left: 20px;
|
| 213 |
+
z-index: 2;
|
| 214 |
+
background-color: rgba(0, 0, 0, 0.5); /* Background for readability */
|
| 215 |
+
padding: 5px 10px;
|
| 216 |
+
border-radius: 5px;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
/* Right Side: Questions and Options */
|
| 220 |
+
.modal .right-side {
|
| 221 |
+
width: 50%;
|
| 222 |
+
padding: 20px;
|
| 223 |
+
color: #eb4814;
|
| 224 |
+
background-color: #fbeccf;
|
| 225 |
+
overflow-y: auto;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
/* Modal Question Styling */
|
| 229 |
+
.modal label {
|
| 230 |
+
display: block;
|
| 231 |
+
font-size: 18px;
|
| 232 |
+
font-weight: 500;
|
| 233 |
+
margin-bottom: 10px;
|
| 234 |
+
color: #000000;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.modal div {
|
| 238 |
+
margin-bottom: 20px;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.modal input[type="radio"] {
|
| 242 |
+
margin-right: 10px;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
.modal input[type="text"] {
|
| 246 |
+
padding: 10px;
|
| 247 |
+
border-radius: 5px;
|
| 248 |
+
border: 1px solid #ccc;
|
| 249 |
+
width: 100%;
|
| 250 |
+
margin-top: 10px;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/* Close Button */
|
| 254 |
+
.close-btn {
|
| 255 |
+
position: absolute;
|
| 256 |
+
top: 10px;
|
| 257 |
+
right: 25px;
|
| 258 |
+
background: transparent;
|
| 259 |
+
font-size: 24px;
|
| 260 |
+
color: #aaa;
|
| 261 |
+
cursor: pointer;
|
| 262 |
+
transition: color 0.3s;
|
| 263 |
+
width: 2vw;
|
| 264 |
+
border: 3px solid #eb48149c;
|
| 265 |
+
padding: 1px;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.close-btn:hover {
|
| 269 |
+
color: #333;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/* Button Styling */
|
| 273 |
+
button {
|
| 274 |
+
padding: 12px 20px;
|
| 275 |
+
background-color: #007bff;
|
| 276 |
+
color: black;
|
| 277 |
+
border: none;
|
| 278 |
+
border-radius: 8px;
|
| 279 |
+
font-size: 16px;
|
| 280 |
+
cursor: pointer;
|
| 281 |
+
transition: background-color 0.3s ease, transform 0.2s ease;
|
| 282 |
+
width: 100%;
|
| 283 |
+
text-align: center;
|
| 284 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
button:active {
|
| 288 |
+
transform: translateY(1px);
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
button:focus-visible {
|
| 292 |
+
outline: 3px solid #7cc4ff;
|
| 293 |
+
outline-offset: 3px;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
/* Animations for modal */
|
| 297 |
+
@keyframes fadeIn {
|
| 298 |
+
from {
|
| 299 |
+
opacity: 0;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
to {
|
| 303 |
+
opacity: 1;
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
@keyframes slideUp {
|
| 308 |
+
from {
|
| 309 |
+
transform: translateY(20px);
|
| 310 |
+
opacity: 0;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
to {
|
| 314 |
+
transform: translateY(0);
|
| 315 |
+
opacity: 1;
|
| 316 |
+
}
|
| 317 |
+
}
|
src/app/question-answer/question-answer.component.html
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="qa-page">
|
| 2 |
+
<header class="topbar">
|
| 3 |
+
<button class="home-btn" type="button" (click)="goHome()" aria-label="Go to Home">Home</button>
|
| 4 |
+
<h1 class="title">{{ heading }}</h1>
|
| 5 |
+
</header>
|
| 6 |
+
|
| 7 |
+
<main class="stage">
|
| 8 |
+
<button class="orb orb-top" type="button" (click)="onSelect('marriage')" aria-label="Select MARRIAGE">
|
| 9 |
+
<span><i class="fa-solid fa-ring"></i> MARRIAGE</span>
|
| 10 |
+
</button>
|
| 11 |
+
<button class="orb orb-left" type="button" (click)="onSelect('interview')" aria-label="Select INTERVIEW">
|
| 12 |
+
<span><i class="fa-solid fa-user-tie"></i> INTERVIEW</span>
|
| 13 |
+
</button>
|
| 14 |
+
<button class="orb orb-right" type="button" (click)="onSelect('partnership')" aria-label="Select BUSINESS PARTNERS">
|
| 15 |
+
<span><i class="fa-solid fa-handshake"></i> BUSINESS PARTNERS</span>
|
| 16 |
+
</button>
|
| 17 |
+
</main>
|
| 18 |
+
|
| 19 |
+
<div *ngIf="isModalOpen" class="modal-overlay">
|
| 20 |
+
<div class="modal">
|
| 21 |
+
<button class="close-btn" (click)="closeModal()">×</button>
|
| 22 |
+
|
| 23 |
+
<div class="left-side">
|
| 24 |
+
<h2>Questions for {{ selectedOption | uppercase }}</h2>
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
<div class="right-side">
|
| 28 |
+
<div *ngIf="selectedQuestions.length > 0; else noQuestions">
|
| 29 |
+
<form (ngSubmit)="submitAnswers()">
|
| 30 |
+
<div *ngFor="let q of selectedQuestions; let i = index">
|
| 31 |
+
<label>{{ q.label }}</label>
|
| 32 |
+
|
| 33 |
+
<!-- text/date -->
|
| 34 |
+
<div *ngIf="q.input_type === 'text' || q.input_type === 'date'">
|
| 35 |
+
<input [type]="q.input_type === 'date' ? 'date' : 'text'"
|
| 36 |
+
[(ngModel)]="answers[i]"
|
| 37 |
+
name="answer{{ i }}"
|
| 38 |
+
(ngModelChange)="onAnswerChange(i, $event)"
|
| 39 |
+
placeholder="Enter your answer" />
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<!-- radio/select -->
|
| 43 |
+
<div *ngIf="(q.input_type === 'select' || q.input_type === 'radio') && q.options?.length">
|
| 44 |
+
<label *ngFor="let option of q.options" class="option">
|
| 45 |
+
<input type="radio"
|
| 46 |
+
[name]="'answer' + i"
|
| 47 |
+
[value]="option"
|
| 48 |
+
[(ngModel)]="answers[i]"
|
| 49 |
+
(change)="onAnswerChange(i, option)" />
|
| 50 |
+
{{ option }}
|
| 51 |
+
</label>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
|
| 55 |
+
<button type="submit">Submit Answers</button>
|
| 56 |
+
</form>
|
| 57 |
+
<!--<button [routerLink]="'/quiz'">Go to Quiz</button>-->
|
| 58 |
+
<a routerLink="/llmquiz" class="btn btn-primary">{{ tr('Go to Quiz') }}</a>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<ng-template #noQuestions>
|
| 62 |
+
<p>No questions available for this role.</p>
|
| 63 |
+
</ng-template>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
src/app/question-answer/question-answer.component.spec.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
| 2 |
+
|
| 3 |
+
import { QuestionAnswerComponent } from './question-answer.component';
|
| 4 |
+
|
| 5 |
+
describe('QuestionAnswerComponent', () => {
|
| 6 |
+
let component: QuestionAnswerComponent;
|
| 7 |
+
let fixture: ComponentFixture<QuestionAnswerComponent>;
|
| 8 |
+
|
| 9 |
+
beforeEach(() => {
|
| 10 |
+
TestBed.configureTestingModule({
|
| 11 |
+
declarations: [QuestionAnswerComponent]
|
| 12 |
+
});
|
| 13 |
+
fixture = TestBed.createComponent(QuestionAnswerComponent);
|
| 14 |
+
component = fixture.componentInstance;
|
| 15 |
+
fixture.detectChanges();
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
it('should create', () => {
|
| 19 |
+
expect(component).toBeTruthy();
|
| 20 |
+
});
|
| 21 |
+
});
|
src/app/question-answer/question-answer.component.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Component, OnInit } from '@angular/core';
|
| 2 |
+
import { QuestionAnswerService, QAItem } from './question-answer-service.service';
|
| 3 |
+
import { Router, RouterLink } from '@angular/router';
|
| 4 |
+
import { FormsModule } from '@angular/forms';
|
| 5 |
+
import { CommonModule } from '@angular/common';
|
| 6 |
+
import { HttpErrorResponse } from '@angular/common/http';
|
| 7 |
+
|
| 8 |
+
@Component({
|
| 9 |
+
selector: 'app-question-answer',
|
| 10 |
+
standalone: true,
|
| 11 |
+
imports: [FormsModule, CommonModule, RouterLink],
|
| 12 |
+
templateUrl: './question-answer.component.html',
|
| 13 |
+
styleUrls: ['./question-answer.component.css']
|
| 14 |
+
})
|
| 15 |
+
export class QuestionAnswerComponent implements OnInit {
|
| 16 |
+
heading: string = 'CHOOSE YOUR ROLE';
|
| 17 |
+
selectedOption: string = ''; // 'marriage' | 'interview' | 'partnership'
|
| 18 |
+
selectedQuestions: QAItem[] = []; // questions fetched for selected role
|
| 19 |
+
isModalOpen: boolean = false;
|
| 20 |
+
|
| 21 |
+
// If you already know the logged-in user id, set it here or load from localStorage in ngOnInit.
|
| 22 |
+
userId: number = 1;
|
| 23 |
+
|
| 24 |
+
// Template-driven answers storage (bound via [(ngModel)])
|
| 25 |
+
answers: any[] = [];
|
| 26 |
+
|
| 27 |
+
constructor(
|
| 28 |
+
private qaService: QuestionAnswerService,
|
| 29 |
+
private router: Router
|
| 30 |
+
) { }
|
| 31 |
+
|
| 32 |
+
ngOnInit(): void {
|
| 33 |
+
// Optional: restore from localStorage if available
|
| 34 |
+
const uid = localStorage.getItem('user_id');
|
| 35 |
+
if (uid) {
|
| 36 |
+
const n = Number(uid);
|
| 37 |
+
if (!Number.isNaN(n)) this.userId = n;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
const savedRole = localStorage.getItem('role');
|
| 41 |
+
if (savedRole) this.selectedOption = savedRole;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
onSelect(role: string): void {
|
| 45 |
+
this.selectedOption = role;
|
| 46 |
+
this.fetchQuestions(role);
|
| 47 |
+
this.isModalOpen = true;
|
| 48 |
+
this.assignRoleToUser(role);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
assignRoleToUser(role: string): void {
|
| 52 |
+
const payload = {
|
| 53 |
+
user_id: this.userId,
|
| 54 |
+
role_name: role,
|
| 55 |
+
assigned_at: new Date().toISOString()
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
this.qaService.assignRole(payload).subscribe({
|
| 59 |
+
next: (res) => console.log('Role assigned successfully:', res),
|
| 60 |
+
error: (err: HttpErrorResponse) => console.error('Error assigning role:', err)
|
| 61 |
+
});
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
fetchQuestions(role: string): void {
|
| 65 |
+
this.qaService.getQuestions(role).subscribe({
|
| 66 |
+
next: (data: QAItem[]) => {
|
| 67 |
+
this.selectedQuestions = data;
|
| 68 |
+
// Prepare answers array for template-driven binding
|
| 69 |
+
this.answers = new Array(data.length).fill('');
|
| 70 |
+
console.log('Fetched questions:', data);
|
| 71 |
+
},
|
| 72 |
+
error: (err: HttpErrorResponse) => console.error('Error fetching questions:', err)
|
| 73 |
+
});
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
submitAnswers(): void {
|
| 77 |
+
const role = (this.selectedOption || '').toLowerCase(); // 'marriage' | 'interview' | 'partnership'
|
| 78 |
+
const userIdNum = Number(this.userId);
|
| 79 |
+
|
| 80 |
+
if (!role || !userIdNum) {
|
| 81 |
+
console.error('Role and user id are required.');
|
| 82 |
+
return;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// Build the "fields" object from questions + answers.
|
| 86 |
+
// We prefer the backend-provided DB column name: r.column_key.
|
| 87 |
+
const fields: Record<string, any> = {};
|
| 88 |
+
this.selectedQuestions.forEach((q: QAItem, idx: number) => {
|
| 89 |
+
const key =
|
| 90 |
+
(q as any).column_key || // from RoleQuestions table
|
| 91 |
+
(q as any).key || // fallback if present
|
| 92 |
+
`q_${idx + 1}`; // final fallback
|
| 93 |
+
fields[key] = this.answers[idx] ?? '';
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
// Many insert statements expect created_at. Add if your backend uses it.
|
| 97 |
+
fields['created_at'] = new Date().toISOString();
|
| 98 |
+
|
| 99 |
+
// Service signature: submitAnswers(role: string, userId: number, fields: Record<string, any>)
|
| 100 |
+
this.qaService.submitAnswers(role, userIdNum, fields).subscribe({
|
| 101 |
+
next: () => {
|
| 102 |
+
// Close profile modal
|
| 103 |
+
this.isModalOpen = false;
|
| 104 |
+
|
| 105 |
+
// Persist for LLM Quiz
|
| 106 |
+
localStorage.setItem('user_id', String(userIdNum));
|
| 107 |
+
localStorage.setItem('role', role);
|
| 108 |
+
// Log the values passed to router
|
| 109 |
+
console.log('Navigating to LLM Quiz with queryParams:', fields);
|
| 110 |
+
// Navigate to LLM Quiz with autostart
|
| 111 |
+
// Prepare queryParams
|
| 112 |
+
const queryParams = {
|
| 113 |
+
user_id: String(userIdNum),
|
| 114 |
+
role,
|
| 115 |
+
fields, // Fields object (you might want to reconsider passing this depending on the size/complexity)
|
| 116 |
+
autostart: '1'
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
// Log the queryParams before navigating
|
| 120 |
+
this.router.navigate(['/llmquiz'], {
|
| 121 |
+
queryParams: { user_id: String(userIdNum), role, fields, autostart: '1' }
|
| 122 |
+
});
|
| 123 |
+
console.log('queryParams:', queryParams);
|
| 124 |
+
},
|
| 125 |
+
error: (err: HttpErrorResponse) => {
|
| 126 |
+
console.error('Error submitting answers:', err);
|
| 127 |
+
}
|
| 128 |
+
});
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
onAnswerChange(i: number, v: any): void {
|
| 132 |
+
this.answers[i] = v;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
closeModal(): void {
|
| 136 |
+
this.isModalOpen = false;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
goHome(): void {
|
| 140 |
+
this.router.navigate(['/intro-page']);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// i18n helper (keep if used in template)
|
| 144 |
+
tr(key: string): string {
|
| 145 |
+
const dict = {
|
| 146 |
+
brand: 'Pykara',
|
| 147 |
+
signup: 'Sign up',
|
| 148 |
+
signin: 'Sign in',
|
| 149 |
+
tagline: 'Discover your profile. Match with purpose.',
|
| 150 |
+
subtext: 'Py-Match runs a short, privacy-first assessment and builds a profile used for responsible matching. Color results are not shown to users.',
|
| 151 |
+
getStarted: 'Get started',
|
| 152 |
+
tos: 'Terms',
|
| 153 |
+
privacy: 'Privacy'
|
| 154 |
+
};
|
| 155 |
+
return dict[key as keyof typeof dict] ?? key;
|
| 156 |
+
}
|
| 157 |
+
}
|
src/app/quiz/quiz.component.css
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* General styling for the quiz container */
|
| 2 |
+
.quiz-container {
|
| 3 |
+
display: flex;
|
| 4 |
+
justify-content: center;
|
| 5 |
+
align-items: center;
|
| 6 |
+
height: 100vh; /* Full viewport height */
|
| 7 |
+
background-image: url('src/assets/background.png');
|
| 8 |
+
background-size: cover; /* Ensures the image covers the entire area */
|
| 9 |
+
background-position: center; /* Center the image */
|
| 10 |
+
background-repeat: no-repeat; /* Prevents the image from repeating */
|
| 11 |
+
margin: 0; /* Remove default margin */
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
/* Overlay for dimming the background */
|
| 16 |
+
.quiz-overlay {
|
| 17 |
+
position: absolute;
|
| 18 |
+
top: 0;
|
| 19 |
+
left: 0;
|
| 20 |
+
width: 100%;
|
| 21 |
+
height: 100%;
|
| 22 |
+
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black overlay */
|
| 23 |
+
display: flex;
|
| 24 |
+
justify-content: center;
|
| 25 |
+
align-items: center;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/* Card for the quiz with soft green background */
|
| 29 |
+
.quiz-card {
|
| 30 |
+
background-color: #eaf6e2; /* Soft light green background */
|
| 31 |
+
border-radius: 15px;
|
| 32 |
+
padding: 30px;
|
| 33 |
+
text-align: center;
|
| 34 |
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
| 35 |
+
max-width: 450px;
|
| 36 |
+
width: 100%;
|
| 37 |
+
position: relative;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* Profile image */
|
| 41 |
+
.profile-picture {
|
| 42 |
+
width: 80px;
|
| 43 |
+
height: 80px;
|
| 44 |
+
border-radius: 50%;
|
| 45 |
+
object-fit: cover;
|
| 46 |
+
margin-bottom: 20px;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/* Title styling */
|
| 50 |
+
.quiz-title {
|
| 51 |
+
font-size: 22px;
|
| 52 |
+
font-weight: bold;
|
| 53 |
+
color: #4caf50; /* Green color */
|
| 54 |
+
margin-bottom: 20px;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/* Question text styling */
|
| 58 |
+
.question {
|
| 59 |
+
font-size: 18px;
|
| 60 |
+
color: #333;
|
| 61 |
+
margin-bottom: 30px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
/* Button styling for options */
|
| 65 |
+
.option-button {
|
| 66 |
+
background-color: #4caf50;
|
| 67 |
+
color: white;
|
| 68 |
+
padding: 12px 20px;
|
| 69 |
+
margin: 10px;
|
| 70 |
+
border: none;
|
| 71 |
+
border-radius: 10px;
|
| 72 |
+
width: 100%;
|
| 73 |
+
text-align: center;
|
| 74 |
+
cursor: pointer;
|
| 75 |
+
transition: background-color 0.3s ease;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.option-button:hover {
|
| 79 |
+
background-color: #45a049;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/* Button styling for "Next" and "Submit" */
|
| 83 |
+
.next-button, .submit-button {
|
| 84 |
+
background-color: #4caf50;
|
| 85 |
+
color: white;
|
| 86 |
+
padding: 12px 20px;
|
| 87 |
+
border: none;
|
| 88 |
+
border-radius: 10px;
|
| 89 |
+
margin-top: 20px;
|
| 90 |
+
width: 100%;
|
| 91 |
+
cursor: pointer;
|
| 92 |
+
transition: background-color 0.3s ease;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.next-button:hover, .submit-button:hover {
|
| 96 |
+
background-color: #45a049;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/* Success Popup */
|
| 100 |
+
.success-popup {
|
| 101 |
+
position: absolute;
|
| 102 |
+
top: 50%;
|
| 103 |
+
left: 50%;
|
| 104 |
+
transform: translate(-50%, -50%);
|
| 105 |
+
background: rgba(0, 0, 0, 0.8);
|
| 106 |
+
color: white;
|
| 107 |
+
padding: 20px;
|
| 108 |
+
border-radius: 10px;
|
| 109 |
+
text-align: center;
|
| 110 |
+
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.success-popup button {
|
| 114 |
+
background-color: #4caf50;
|
| 115 |
+
color: white;
|
| 116 |
+
padding: 10px 20px;
|
| 117 |
+
border: none;
|
| 118 |
+
border-radius: 5px;
|
| 119 |
+
cursor: pointer;
|
| 120 |
+
margin-top: 15px;
|
| 121 |
+
transition: background-color 0.3s ease;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.success-popup button:hover {
|
| 125 |
+
background-color: #45a049;
|
| 126 |
+
}
|
src/app/quiz/quiz.component.html
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="quiz-container">
|
| 2 |
+
<div class="quiz-overlay">
|
| 3 |
+
<div class="quiz-card">
|
| 4 |
+
<h1 class="quiz-title">QUIZ</h1>
|
| 5 |
+
<div class="question-container">
|
| 6 |
+
<p class="question">{{ questions[currentQuestionIndex].question }}</p>
|
| 7 |
+
<div class="options">
|
| 8 |
+
<button *ngFor="let option of questions[currentQuestionIndex].options"
|
| 9 |
+
class="option-button"
|
| 10 |
+
(click)="selectOption(option)">
|
| 11 |
+
{{ option }}
|
| 12 |
+
</button>
|
| 13 |
+
</div>
|
| 14 |
+
<button *ngIf="currentQuestionIndex < questions.length - 1"
|
| 15 |
+
class="next-button"
|
| 16 |
+
[disabled]="!isOptionSelected"
|
| 17 |
+
(click)="nextQuestion()">
|
| 18 |
+
Next
|
| 19 |
+
</button>
|
| 20 |
+
<button *ngIf="currentQuestionIndex === questions.length - 1"
|
| 21 |
+
class="submit-button"
|
| 22 |
+
[disabled]="!isOptionSelected"
|
| 23 |
+
(click)="submitQuiz()">
|
| 24 |
+
Submit
|
| 25 |
+
</button>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
<!-- Success Popup -->
|
| 32 |
+
<div *ngIf="isSubmitted" class="success-popup">
|
| 33 |
+
<p>Your test is submitted!</p>
|
| 34 |
+
<button (click)="closePopup()">Close</button>
|
| 35 |
+
</div>
|
src/app/quiz/quiz.component.spec.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
| 2 |
+
|
| 3 |
+
import { QuizComponent } from './quiz.component';
|
| 4 |
+
|
| 5 |
+
describe('QuizComponent', () => {
|
| 6 |
+
let component: QuizComponent;
|
| 7 |
+
let fixture: ComponentFixture<QuizComponent>;
|
| 8 |
+
|
| 9 |
+
beforeEach(() => {
|
| 10 |
+
TestBed.configureTestingModule({
|
| 11 |
+
declarations: [QuizComponent]
|
| 12 |
+
});
|
| 13 |
+
fixture = TestBed.createComponent(QuizComponent);
|
| 14 |
+
component = fixture.componentInstance;
|
| 15 |
+
fixture.detectChanges();
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
it('should create', () => {
|
| 19 |
+
expect(component).toBeTruthy();
|
| 20 |
+
});
|
| 21 |
+
});
|
src/app/quiz/quiz.component.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Component, OnInit } from '@angular/core';
|
| 2 |
+
import { Router } from '@angular/router';
|
| 3 |
+
import { FormsModule } from '@angular/forms';
|
| 4 |
+
import { CommonModule } from '@angular/common';
|
| 5 |
+
import { RouterLink } from '@angular/router';
|
| 6 |
+
interface QuizQuestion {
|
| 7 |
+
question: string;
|
| 8 |
+
options: string[];
|
| 9 |
+
// store the selected answer; start as null and set to a string later
|
| 10 |
+
answer: string | null;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
@Component({
|
| 14 |
+
selector: 'app-quiz',
|
| 15 |
+
standalone: true,
|
| 16 |
+
imports: [FormsModule, CommonModule, RouterLink],
|
| 17 |
+
templateUrl: './quiz.component.html',
|
| 18 |
+
styleUrls: ['./quiz.component.css'],
|
| 19 |
+
})
|
| 20 |
+
export class QuizComponent implements OnInit {
|
| 21 |
+
|
| 22 |
+
constructor(public router: Router) { }
|
| 23 |
+
ngOnInit(): void { }
|
| 24 |
+
questions: QuizQuestion[] = [
|
| 25 |
+
{
|
| 26 |
+
question: 'You are leading a small team. A new member is quiet and avoids discussions. What will you do first?',
|
| 27 |
+
options: [
|
| 28 |
+
'Assign a simple task and review later',
|
| 29 |
+
'Speak privately to understand concerns',
|
| 30 |
+
'Ask them to present in the next meeting',
|
| 31 |
+
'Ignore it and focus on deadlines'
|
| 32 |
+
],
|
| 33 |
+
answer: null,
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
question: 'A stakeholder requests a big change late in the sprint. What is your approach?',
|
| 37 |
+
options: [
|
| 38 |
+
'Accept it immediately',
|
| 39 |
+
'Reject it immediately',
|
| 40 |
+
'Evaluate impact and negotiate scope/time',
|
| 41 |
+
'Ask the team to work overtime'
|
| 42 |
+
],
|
| 43 |
+
answer: null,
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
question: 'Two teammates disagree on a solution. How do you proceed?',
|
| 47 |
+
options: [
|
| 48 |
+
'Pick the faster option',
|
| 49 |
+
'Let them resolve it alone',
|
| 50 |
+
'Facilitate a short decision session with facts',
|
| 51 |
+
'Escalate to management'
|
| 52 |
+
],
|
| 53 |
+
answer: null,
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
question: 'You find a recurring production bug. What is your first step?',
|
| 57 |
+
options: [
|
| 58 |
+
'Hotfix every time',
|
| 59 |
+
'Add logs and create a root-cause ticket',
|
| 60 |
+
'Disable the feature',
|
| 61 |
+
'Rollback to an old release'
|
| 62 |
+
],
|
| 63 |
+
answer: null,
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
question: 'Your release is on time but test coverage is low. What will you do?',
|
| 67 |
+
options: [
|
| 68 |
+
'Release as-is',
|
| 69 |
+
'Block the release',
|
| 70 |
+
'Risk-assess, add critical tests, and plan coverage',
|
| 71 |
+
'Let QA handle it later'
|
| 72 |
+
],
|
| 73 |
+
answer: null,
|
| 74 |
+
},
|
| 75 |
+
];
|
| 76 |
+
|
| 77 |
+
currentQuestionIndex = 0;
|
| 78 |
+
isOptionSelected = false; // controls Next/Submit button enabled state
|
| 79 |
+
isSubmitted = false; // controls the success popup
|
| 80 |
+
|
| 81 |
+
selectOption(option: string): void {
|
| 82 |
+
this.questions[this.currentQuestionIndex].answer = option; // type-safe: string | null
|
| 83 |
+
this.isOptionSelected = true;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
nextQuestion(): void {
|
| 87 |
+
if (!this.isOptionSelected) return;
|
| 88 |
+
this.currentQuestionIndex++;
|
| 89 |
+
|
| 90 |
+
// Reset the enable/disable state for the next question
|
| 91 |
+
const next = this.questions[this.currentQuestionIndex];
|
| 92 |
+
this.isOptionSelected = !!next?.answer; // true if already answered, else false
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
submitQuiz(): void {
|
| 96 |
+
if (!this.isOptionSelected) return;
|
| 97 |
+
this.isSubmitted = true; // shows the “Your test is submitted!” popup
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
closePopup(): void {
|
| 101 |
+
this.isSubmitted = false;
|
| 102 |
+
// Optionally navigate or reset after closing:
|
| 103 |
+
// this.currentQuestionIndex = 0;
|
| 104 |
+
// this.questions.forEach(q => (q.answer = null));
|
| 105 |
+
// this.isOptionSelected = false;
|
| 106 |
+
}
|
| 107 |
+
}
|
src/app/quiz/quiz.service.spec.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { TestBed } from '@angular/core/testing';
|
| 2 |
+
|
| 3 |
+
import { QuizService } from './quiz.service';
|
| 4 |
+
|
| 5 |
+
describe('QuizService', () => {
|
| 6 |
+
let service: QuizService;
|
| 7 |
+
|
| 8 |
+
beforeEach(() => {
|
| 9 |
+
TestBed.configureTestingModule({});
|
| 10 |
+
service = TestBed.inject(QuizService);
|
| 11 |
+
});
|
| 12 |
+
|
| 13 |
+
it('should be created', () => {
|
| 14 |
+
expect(service).toBeTruthy();
|
| 15 |
+
});
|
| 16 |
+
});
|
src/app/quiz/quiz.service.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@angular/core';
|
| 2 |
+
import { HttpClient } from '@angular/common/http';
|
| 3 |
+
import { Observable } from 'rxjs';
|
| 4 |
+
|
| 5 |
+
@Injectable({
|
| 6 |
+
providedIn: 'root'
|
| 7 |
+
})
|
| 8 |
+
export class QuizService {
|
| 9 |
+
private apiUrl = 'http://localhost:5000'; // Your Flask backend URL
|
| 10 |
+
|
| 11 |
+
constructor(private http: HttpClient) { }
|
| 12 |
+
|
| 13 |
+
// Start the quiz by sending the profile and role
|
| 14 |
+
startQuiz(nQuestions: number, batchSize: number, role: string, profile: any): Observable<any> {
|
| 15 |
+
return this.http.post<any>(`${this.apiUrl}/q/start`, {
|
| 16 |
+
n_questions: nQuestions,
|
| 17 |
+
batch_size: batchSize,
|
| 18 |
+
role: role,
|
| 19 |
+
profile: profile
|
| 20 |
+
});
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// Fetch next question
|
| 24 |
+
nextQuestion(sessionId: string, selectedColor: string): Observable<any> {
|
| 25 |
+
return this.http.post<any>(`${this.apiUrl}/q/next`, {
|
| 26 |
+
session_id: sessionId,
|
| 27 |
+
selected_color: selectedColor
|
| 28 |
+
});
|
| 29 |
+
}
|
| 30 |
+
}
|