snapshot
This commit is contained in:
parent
3ad2766f82
commit
f74ee43cb4
158 changed files with 16749 additions and 29973 deletions
7
frontend/.browserslistrc
Normal file
7
frontend/.browserslistrc
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
last 2 Chrome versions
|
||||
last 2 Firefox versions
|
||||
last 2 Safari versions
|
||||
last 2 Edge versions
|
||||
Firefox ESR
|
||||
not dead
|
||||
not IE 11
|
||||
|
|
@ -8,6 +8,10 @@ indent_size = 2
|
|||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
ij_typescript_use_double_quotes = false
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
|
|
|
|||
36
frontend/.gitignore
vendored
36
frontend/.gitignore
vendored
|
|
@ -1,21 +1,24 @@
|
|||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
# Playwright
|
||||
/test-results
|
||||
/playwright-report
|
||||
/playwright/.cache
|
||||
/visuals
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events.json
|
||||
speed-measure-plugin.json
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
|
|
@ -23,26 +26,25 @@ speed-measure-plugin.json
|
|||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/mcp.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
__screenshots__/
|
||||
|
||||
# System Files
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
.firebase
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
{
|
||||
"printWidth": 120,
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"bracketSpacing": true
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.html",
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
4
frontend/.vscode/extensions.json
vendored
Normal file
4
frontend/.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||
"recommendations": ["angular.ng-template"]
|
||||
}
|
||||
20
frontend/.vscode/launch.json
vendored
Normal file
20
frontend/.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ng serve",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: start",
|
||||
"url": "http://localhost:4200/"
|
||||
},
|
||||
{
|
||||
"name": "ng test",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: test",
|
||||
"url": "http://localhost:9876/debug.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
9
frontend/.vscode/mcp.json
vendored
Normal file
9
frontend/.vscode/mcp.json
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
// For more information, visit: https://angular.dev/ai/mcp
|
||||
"servers": {
|
||||
"angular-cli": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@angular/cli", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
42
frontend/.vscode/tasks.json
vendored
Normal file
42
frontend/.vscode/tasks.json
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "Changes detected"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation (complete|failed)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "Changes detected"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation (complete|failed)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,27 +1,59 @@
|
|||
# Frontend
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.3.8.
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.2.13.
|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
To start a local development server, run:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
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`.
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
|
||||
## Build
|
||||
```bash
|
||||
ng generate component component-name
|
||||
```
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To build the project run:
|
||||
|
||||
```bash
|
||||
ng build
|
||||
```
|
||||
|
||||
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
## Further help
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
|
|
|
|||
|
|
@ -1,86 +1,94 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"cli": {
|
||||
"packageManager": "npm",
|
||||
"schematicCollections": [
|
||||
"angular-eslint"
|
||||
]
|
||||
},
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"frontend": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": "dist/frontend",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": [],
|
||||
"es5BrowserSupport": true
|
||||
"browser": "src/main.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "20mb",
|
||||
"maximumError": "50mb"
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "16kB",
|
||||
"maximumError": "32kB"
|
||||
}
|
||||
]
|
||||
],
|
||||
"outputHashing": "all",
|
||||
"serviceWorker": "ngsw-config.json"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "frontend:build"
|
||||
},
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "frontend:build:production"
|
||||
"buildTarget": "frontend:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "frontend:build:development"
|
||||
}
|
||||
|
||||
},
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"proxyConfig": "proxy.conf.json"
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "frontend:build"
|
||||
}
|
||||
"test": {
|
||||
"builder": "@angular/build:unit-test"
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.html"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "frontend"
|
||||
}
|
||||
}
|
||||
}
|
||||
79
frontend/e2e/smoke.spec.ts
Normal file
79
frontend/e2e/smoke.spec.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Smoke test: drives the legacy-styled UI end-to-end.
|
||||
*
|
||||
* docker compose -f docker-compose.dev.yml up --build -d
|
||||
* PLAYWRIGHT_BASE_URL=http://life-towers:8000 npx playwright test
|
||||
* docker compose -f docker-compose.dev.yml down -v
|
||||
*/
|
||||
test.describe('Life Towers smoke test', () => {
|
||||
test('create page → tower → block, mark done, reload, persists', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Wait for the empty-state hint that means init() completed.
|
||||
await expect(page.getByText('Add a new page to get started!')).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Create a page via the select-add dropdown.
|
||||
await page.locator('lt-select-add .top').first().click();
|
||||
await page.locator('lt-select-add input[placeholder="Add a value…"]').fill('Hobbies');
|
||||
await page.locator('lt-select-add input[placeholder="Add a value…"]').press('Enter');
|
||||
|
||||
// The page name now appears in the dropdown top.
|
||||
await expect(page.locator('lt-select-add .top').first()).toContainText('Hobbies');
|
||||
|
||||
// Create a tower.
|
||||
await page.locator('img[alt="Add tower"]').click();
|
||||
await page.locator('input[placeholder="Tower name…"]').fill('Side projects');
|
||||
await page.locator('lt-tower-settings button[type="submit"]').click();
|
||||
|
||||
// Tower's name input is rendered with the tower name as its value.
|
||||
await expect(page.locator('lt-tower input').first()).toHaveValue('Side projects');
|
||||
|
||||
// Create a block.
|
||||
await page.locator('img[alt="Add block"]').first().click();
|
||||
// The tag input is inside an lt-select-add — open it and add a tag.
|
||||
await page.locator('lt-block-edit lt-select-add .top').click();
|
||||
await page
|
||||
.locator('lt-block-edit lt-select-add input[placeholder="Add a value…"]')
|
||||
.fill('learn');
|
||||
await page
|
||||
.locator('lt-block-edit lt-select-add input[placeholder="Add a value…"]')
|
||||
.press('Enter');
|
||||
await page.locator('textarea[placeholder="Write a description here…"]').fill(
|
||||
'Modernise the towers app',
|
||||
);
|
||||
await page.getByRole('button', { name: 'Create and exit' }).click();
|
||||
|
||||
// New block is pending → appears in the tasks accordion.
|
||||
// (Tasks header shows N tasks.)
|
||||
await expect(page.locator('lt-tasks')).toContainText('1');
|
||||
|
||||
// Open the tasks accordion + click the task to edit it, then flip done.
|
||||
await page.locator('lt-tasks .container').click();
|
||||
await page.locator('lt-tasks .task-container').click();
|
||||
|
||||
// Toggle done in the block-edit modal — the right label is "Done".
|
||||
const putLanded = page.waitForResponse(
|
||||
(r) => r.url().endsWith('/api/v1/data') && r.request().method() === 'PUT' && r.ok(),
|
||||
);
|
||||
// Toggle uses verbose labels — "Goal accomplished" flips it to done.
|
||||
await page
|
||||
.locator('lt-block-edit lt-toggle span')
|
||||
.filter({ hasText: 'Goal accomplished' })
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'Create and exit' }).click();
|
||||
await putLanded;
|
||||
|
||||
// Done block now appears as a colored square in the tower's falling stack.
|
||||
await expect(page.locator('lt-tower lt-block').first()).toBeVisible();
|
||||
|
||||
// Reload — everything must come back from the server.
|
||||
await page.reload();
|
||||
await expect(page.locator('lt-select-add .top').first()).toContainText('Hobbies', {
|
||||
timeout: 15000,
|
||||
});
|
||||
await expect(page.getByDisplayValue('Side projects')).toBeVisible();
|
||||
await expect(page.locator('lt-tower lt-block').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
160
frontend/e2e/visuals.spec.ts
Normal file
160
frontend/e2e/visuals.spec.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import { test } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Visual capture: drives the UI into key states and writes screenshots
|
||||
* for human review of the legacy-styled design.
|
||||
*/
|
||||
test.describe('Life Towers visuals', () => {
|
||||
test('capture key UI states', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForSelector('text=Add a new page to get started!', { timeout: 15000 });
|
||||
await page.screenshot({ path: 'visuals/01-empty-state.png', fullPage: true });
|
||||
|
||||
// Open the page dropdown (without creating a page yet).
|
||||
await page.locator('lt-select-add .top').first().click();
|
||||
// Wait for the slide-down animation to finish (200ms transform + buffer).
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: 'visuals/02-page-dropdown-open.png', fullPage: true });
|
||||
|
||||
// Create the page.
|
||||
await page.locator('lt-select-add input[placeholder="Add a value…"]').fill('Hobbies');
|
||||
await page.locator('lt-select-add input[placeholder="Add a value…"]').press('Enter');
|
||||
await page.locator('body').click({ position: { x: 10, y: 400 } });
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// ── Add a tower ─────────────────────────────────────────────────────────
|
||||
await page.locator('img[alt="Add tower"]').click();
|
||||
await page.waitForSelector('section.modal.active');
|
||||
await page.waitForTimeout(350);
|
||||
await page.locator('input[placeholder="Tower name…"]').fill('Reading');
|
||||
await page.screenshot({ path: 'visuals/03-new-tower-modal.png', fullPage: true });
|
||||
await page.locator('lt-tower-settings button[type="submit"]').click();
|
||||
await page.waitForSelector('section.modal', { state: 'detached' });
|
||||
|
||||
// ── Open the block-edit carousel (empty) ────────────────────────────────
|
||||
await page.locator('img[alt="Add block"]').first().click();
|
||||
await page.waitForSelector('section.modal.active');
|
||||
await page.waitForTimeout(350);
|
||||
await page.screenshot({ path: 'visuals/04-block-edit-carousel-empty.png', fullPage: true });
|
||||
|
||||
// Fill in the create card. Make this one a NOT-finished task so the
|
||||
// tasks accordion has a row to show off (with the tickbox).
|
||||
const createCard = page.locator('lt-block-edit .create-card');
|
||||
await createCard.locator('lt-select-add .top').click();
|
||||
await createCard.locator('lt-select-add input[placeholder="Add a value…"]').fill('novel');
|
||||
await createCard.locator('lt-select-add input[placeholder="Add a value…"]').press('Enter');
|
||||
await createCard
|
||||
.locator('textarea[placeholder="Write a description here…"]')
|
||||
.fill('Finish The Brothers Karamazov');
|
||||
// Toggle to "Task hasn't been finished yet" so this becomes a pending task.
|
||||
await createCard
|
||||
.locator('lt-toggle span')
|
||||
.filter({ hasText: "This task hasn't been finished yet" })
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'Create and exit' }).click();
|
||||
await page.waitForSelector('section.modal', { state: 'detached' });
|
||||
|
||||
// Open the tasks accordion to show the new tickbox.
|
||||
await page.waitForTimeout(200);
|
||||
await page.locator('lt-tasks .container').click();
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: 'visuals/04b-tasks-accordion-with-tickbox.png', fullPage: true });
|
||||
|
||||
// Add a couple more blocks.
|
||||
for (const desc of ['Read about WebAssembly GC', 'Re-read "Out of the Tar Pit"']) {
|
||||
await page.locator('img[alt="Add block"]').first().click();
|
||||
await page.waitForSelector('section.modal.active');
|
||||
await page.waitForTimeout(350);
|
||||
const cc = page.locator('lt-block-edit .create-card');
|
||||
// Re-use existing tag "novel" — click it from the select-add list.
|
||||
await cc.locator('lt-select-add .top').click();
|
||||
await page.waitForTimeout(100);
|
||||
await cc.locator('textarea[placeholder="Write a description here…"]').fill(desc);
|
||||
await page.getByRole('button', { name: 'Create and exit' }).click();
|
||||
await page.waitForSelector('section.modal', { state: 'detached' });
|
||||
}
|
||||
|
||||
// Wait for the falling animation to finish.
|
||||
await page.waitForTimeout(1800);
|
||||
await page.screenshot({ path: 'visuals/05-populated-falling-blocks.png', fullPage: true });
|
||||
|
||||
// ── Open carousel WITH existing blocks ──────────────────────────────────
|
||||
await page.locator('img[alt="Add block"]').first().click();
|
||||
await page.waitForSelector('section.modal.active');
|
||||
await page.waitForTimeout(350);
|
||||
await page.screenshot({ path: 'visuals/06-carousel-with-existing.png', fullPage: true });
|
||||
|
||||
// Scroll one card to the left (to focus an existing block, showing neighbour mask).
|
||||
await page.locator('lt-block-edit').evaluate((el) => {
|
||||
const c = el.querySelector('.carousel') as HTMLElement | null;
|
||||
if (c) c.scrollBy({ left: -400, behavior: 'instant' as ScrollBehavior });
|
||||
});
|
||||
await page.waitForTimeout(400);
|
||||
await page.screenshot({ path: 'visuals/07-carousel-existing-focused.png', fullPage: true });
|
||||
|
||||
// Close the carousel via the exit X.
|
||||
await page.locator('lt-block-edit .exit').first().click({ force: true });
|
||||
await page.waitForSelector('section.modal', { state: 'detached' });
|
||||
|
||||
// ── Add a second tower so we can show drag-drop ─────────────────────────
|
||||
await page.locator('img[alt="Add tower"]').click();
|
||||
await page.waitForSelector('section.modal.active');
|
||||
await page.waitForTimeout(350);
|
||||
await page.locator('input[placeholder="Tower name…"]').fill('Side projects');
|
||||
await page.locator('lt-tower-settings button[type="submit"]').click();
|
||||
await page.waitForSelector('section.modal', { state: 'detached' });
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// ── Drag a tower over the trash (capture mid-drag) ──────────────────────
|
||||
const firstTowerHandle = page.locator('lt-tower').first();
|
||||
const trash = page.locator('img.trash');
|
||||
const tb = await firstTowerHandle.boundingBox();
|
||||
if (tb) {
|
||||
// Pick up the tower from its center, then move toward the trash.
|
||||
await page.mouse.move(tb.x + tb.width / 2, tb.y + tb.height / 2);
|
||||
await page.mouse.down();
|
||||
// Move in two stages — first to dislodge cdkDrag, then over the trash.
|
||||
await page.mouse.move(tb.x + tb.width / 2 + 30, tb.y + tb.height / 2 + 30, { steps: 8 });
|
||||
const trb = await trash.boundingBox();
|
||||
if (trb) {
|
||||
await page.mouse.move(trb.x + trb.width / 2, trb.y + trb.height / 2, { steps: 12 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: 'visuals/08-dragging-over-trash.png', fullPage: true });
|
||||
}
|
||||
// Actually release over the trash to trigger the confirm-delete modal.
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(400);
|
||||
await page.screenshot({ path: 'visuals/08b-confirm-delete-modal.png', fullPage: true });
|
||||
// Cancel — don't actually delete the tower.
|
||||
await page.locator('.confirm-buttons button').filter({ hasText: 'Cancel' }).click();
|
||||
await page.waitForSelector('section.modal', { state: 'detached' });
|
||||
}
|
||||
|
||||
// ── Move the date-range slider thumb — blocks should ascend out ────────
|
||||
const slider = page.locator('lt-double-slider input[type="range"]').first();
|
||||
const sb = await slider.boundingBox();
|
||||
if (sb) {
|
||||
// Drag the left thumb from 0 toward the right (~80% of slider width).
|
||||
await page.mouse.move(sb.x + 4, sb.y + sb.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(sb.x + sb.width * 0.8, sb.y + sb.height / 2, { steps: 16 });
|
||||
await page.mouse.up();
|
||||
// Wait long enough for the 1.5s ascend transition.
|
||||
await page.waitForTimeout(1700);
|
||||
await page.screenshot({ path: 'visuals/09-date-filter-ascended.png', fullPage: true });
|
||||
// Restore the range and screenshot the descend back to rest.
|
||||
await page.mouse.move(sb.x + sb.width * 0.8, sb.y + sb.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(sb.x + 4, sb.y + sb.height / 2, { steps: 16 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(1700);
|
||||
await page.screenshot({ path: 'visuals/10-date-filter-restored.png', fullPage: true });
|
||||
}
|
||||
|
||||
// ── Settings modal ──────────────────────────────────────────────────────
|
||||
await page.getByRole('button', { name: 'Settings' }).click();
|
||||
await page.waitForSelector('section.modal.active');
|
||||
await page.waitForTimeout(350);
|
||||
await page.screenshot({ path: 'visuals/11-settings-modal.png', fullPage: true });
|
||||
});
|
||||
});
|
||||
55
frontend/eslint.config.js
Normal file
55
frontend/eslint.config.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// @ts-check
|
||||
const eslint = require('@eslint/js');
|
||||
const { defineConfig } = require('eslint/config');
|
||||
const tseslint = require('typescript-eslint');
|
||||
const angular = require('angular-eslint');
|
||||
|
||||
module.exports = defineConfig([
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
extends: [
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
tseslint.configs.stylistic,
|
||||
angular.configs.tsRecommended,
|
||||
],
|
||||
processor: angular.processInlineTemplates,
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: ['lt', 'app'],
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: ['lt', 'app'],
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
// Allow output named 'close' - it is not a standard DOM event on custom elements
|
||||
'@angular-eslint/no-output-native': 'off',
|
||||
// Allow empty arrow functions in catch clauses for fire-and-forget patterns
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
extends: [angular.configs.templateRecommended, angular.configs.templateAccessibility],
|
||||
rules: {
|
||||
// Relax label association check – our modal inputs are associated via id/for
|
||||
'@angular-eslint/template/label-has-associated-control': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Vitest test files – relax unused-vars for test utilities
|
||||
files: ['**/*.vitest.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"hosting": {
|
||||
"public": "dist/frontend",
|
||||
"site": "towers-schmelczer-dev",
|
||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
28
frontend/ngsw-config.json
Normal file
28
frontend/ngsw-config.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
|
||||
"index": "/index.html",
|
||||
"assetGroups": [
|
||||
{
|
||||
"name": "app",
|
||||
"installMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/favicon.ico",
|
||||
"/index.csr.html",
|
||||
"/index.html",
|
||||
"/manifest.webmanifest",
|
||||
"/*.css",
|
||||
"/*.js"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assets",
|
||||
"installMode": "lazy",
|
||||
"updateMode": "prefetch",
|
||||
"resources": {
|
||||
"files": ["/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
30379
frontend/package-lock.json
generated
30379
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -5,47 +5,39 @@
|
|||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"build:prod": "ng build --prod",
|
||||
"format:fix": "pretty-quick --staged",
|
||||
"precommit": "run-s format:fix lint",
|
||||
"format:check": "prettier --config ./.prettierrc --list-different \"src/{app,environments,assets}/**/*{.ts,.js,.json,.css,.scss}\"",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "vitest --run",
|
||||
"test:watch": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"lint": "ng lint"
|
||||
},
|
||||
"private": true,
|
||||
"packageManager": "npm@10.9.2",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^7.2.15",
|
||||
"@angular/cdk": "^7.3.7",
|
||||
"@angular/common": "~7.2.0",
|
||||
"@angular/compiler": "~7.2.0",
|
||||
"@angular/core": "~7.2.0",
|
||||
"@angular/forms": "~7.2.0",
|
||||
"@angular/material": "^7.3.7",
|
||||
"@angular/platform-browser": "~7.2.0",
|
||||
"@angular/platform-browser-dynamic": "~7.2.0",
|
||||
"@angular/router": "~7.2.0",
|
||||
"core-js": "^2.5.4",
|
||||
"ng": "^0.0.0",
|
||||
"rxjs": "~6.3.3",
|
||||
"tslib": "^1.10.0",
|
||||
"uuid": "^3.3.3",
|
||||
"zone.js": "~0.8.26"
|
||||
"@angular/cdk": "^21.2.13",
|
||||
"@angular/common": "^21.2.0",
|
||||
"@angular/compiler": "^21.2.0",
|
||||
"@angular/core": "^21.2.0",
|
||||
"@angular/forms": "^21.2.0",
|
||||
"@angular/platform-browser": "^21.2.0",
|
||||
"@angular/router": "^21.2.0",
|
||||
"@angular/service-worker": "^21.2.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^0.13.10",
|
||||
"@angular/cli": "~7.3.8",
|
||||
"@angular/compiler-cli": "~7.2.0",
|
||||
"@angular/language-service": "~7.2.0",
|
||||
"@types/jasmine": "~2.8.8",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "~8.9.4",
|
||||
"codelyzer": "~4.5.0",
|
||||
"husky": "^3.0.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^1.18.2",
|
||||
"pretty-quick": "^1.11.1",
|
||||
"sass": "^1.32.5",
|
||||
"ts-node": "~7.0.0",
|
||||
"tslint": "~5.11.0",
|
||||
"typescript": "~3.2.2"
|
||||
"@angular/build": "^21.2.13",
|
||||
"@angular/cli": "^21.2.13",
|
||||
"@angular/compiler-cli": "^21.2.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@vitest/coverage-v8": "^4.1.7",
|
||||
"angular-eslint": "21.4.0",
|
||||
"eslint": "^10.3.0",
|
||||
"jsdom": "^28.1.0",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "~5.9.2",
|
||||
"typescript-eslint": "8.59.2",
|
||||
"vitest": "^4.1.7"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
frontend/playwright.config.ts
Normal file
30
frontend/playwright.config.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* E2E tests target the full stack served by docker-compose.dev.yml at
|
||||
* http://localhost:8000 (FastAPI serving both the API and the built SPA).
|
||||
* Override with PLAYWRIGHT_BASE_URL to point at a different deployment.
|
||||
*
|
||||
* docker compose -f docker-compose.dev.yml up --build -d
|
||||
* npm run test:e2e
|
||||
* docker compose -f docker-compose.dev.yml down -v
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env['CI'],
|
||||
retries: process.env['CI'] ? 2 : 0,
|
||||
workers: process.env['CI'] ? 1 : undefined,
|
||||
reporter: [['list'], ['html', { open: 'never' }]],
|
||||
use: {
|
||||
baseURL: process.env['PLAYWRIGHT_BASE_URL'] ?? 'http://localhost:8000',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
7
frontend/proxy.conf.json
Normal file
7
frontend/proxy.conf.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"/api": {
|
||||
"target": "http://localhost:8000",
|
||||
"secure": false,
|
||||
"changeOrigin": false
|
||||
}
|
||||
}
|
||||
4
frontend/public/assets/arrow.svg
Normal file
4
frontend/public/assets/arrow.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 292.362 292.362" style="enable-background:new 0 0 292.362 292.362;" xml:space="preserve" class=""><g><g>
|
||||
<path d="M286.935,69.377c-3.614-3.617-7.898-5.424-12.848-5.424H18.274c-4.952,0-9.233,1.807-12.85,5.424 C1.807,72.998,0,77.279,0,82.228c0,4.948,1.807,9.229,5.424,12.847l127.907,127.907c3.621,3.617,7.902,5.428,12.85,5.428 s9.233-1.811,12.847-5.428L286.935,95.074c3.613-3.617,5.427-7.898,5.427-12.847C292.362,77.279,290.548,72.998,286.935,69.377z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#5D576B"/>
|
||||
</g></g> </svg>
|
||||
|
After Width: | Height: | Size: 746 B |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue