feat(portal-admin): skeleton app per ADR-0020 (#100)
## Summary
First step of the admin track per ADR-0020. Scaffold the second Angular SPA in the workspace alongside `portal-shell`, with the architectural decisions from the v1 ADRs already wired so subsequent feature PRs can drop straight into modules.
## What lands
- **App generated** via `nx g @nx/angular:application` (with the matching e2e project skeleton). Standalone, zoneless-ready, prefix `app`, scss component styles, css root stylesheet, no SSR.
- **Tags** `scope:portal-admin, type:app` — module-boundary lint now treats `portal-admin` distinctly from `portal-shell` and lets it depend only on `scope:portal-admin` + `scope:shared` libs.
- **Tailwind v4 + brand tokens**: `.postcssrc.json`, `@import 'tailwindcss'`, class-based dark variant, and `@import '../../../libs/shared/tokens/src/brand-tokens.css'` — identical visual baseline to portal-shell.
- **i18n config mirrors portal-shell** (per ADR-0019): sourceLocale `en`, target `fr`, baseHref `/{locale}/` per locale, `@angular/localize/init` polyfill, `localize: ["en", "fr"]` on production, `i18nMissingTranslation: "error"` so CI blocks any PR that adds a marker without translating it. Seed [`messages.fr.xlf`](apps/portal-admin/src/locale/messages.fr.xlf) carries the four marked strings used today.
- **Budgets** relaxed to **500 KB gzip initial** per ADR-0020 §"Performance budgets" (vs 300 KB for portal-shell).
- **Skeleton home page** at `/` — `<h1>` + intro paragraph + "Skeleton — coming soon" chip in the brand-accent palette. All marked for i18n.
- **Skip-link landmark** (WCAG 2.4.1) with the same component-scoped scss as portal-shell.
- **Wildcard catch-all route** → bounces unknown paths to home (same defensive pattern as PR #96 on portal-shell).
- **Dev server on port 4300** (vs 4200 for portal-shell) — both can run in parallel.
- **`serve-static` target without `spa: true`** — locale-prefixed routing in production is handled by the reverse proxy (per ADR-0019), same as portal-shell since PR #92.
## CLAUDE.md
Picked up the second app in the **Naming** entry and the **Commands** snippet (`<app>` is now one of `portal-shell`, `portal-admin`, `portal-bff`).
## Verification
- `pnpm exec nx run-many -t lint test build --projects=portal-shell,portal-admin,shared-ui,shared-state,shared-tokens` — green.
- `portal-admin` test: 1 spec (skip-link + main landmark present).
- Production build of `portal-admin`: **62 KB gzip initial per locale** (vs 500 KB budget — plenty of headroom for the upcoming modules).
- Both `dist/apps/portal-admin/browser/{en,fr}/` emit with the right `<html lang>` and `<base href>`. FR bundle contains `"Administration"` / `"Squelette"` / `"bientôt disponible"` — translations applied.
- portal-shell production build unchanged.
## Out of scope (each its own follow-up PR)
- Admin app shell (header / sidebar / footer with "Admin" badge — likely sharing graduated primitives plus admin-specific bits).
- OpenTelemetry tracing setup (`service.name=portal-admin` per ADR-0012).
- `environment.ts` wiring (per ADR-0018) for admin-specific endpoints.
- BFF `AdminModule` + `AdminRoleGuard` + smoke `/api/admin/me` (per ADR-0020 §"Auth").
- First functional module — CMS / menu / users / audit viewer.
## Test plan
- [x] Lint + test + build green across the workspace.
- [x] Per-locale production build emits both folders with correct metadata.
- [ ] Manual: `pnpm exec nx serve portal-admin` boots on `:4300`, smoke page renders.
- [ ] Manual: prod build + `pnpm exec nx run portal-admin:serve-static` → `http://localhost:4300/en/` and `/fr/` show the placeholder with the matching language.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #100
This commit was merged in pull request #100.
This commit is contained in:
@@ -32,7 +32,7 @@ These constraints were set by the project lead at kickoff. They apply to every c
|
|||||||
The structural, security, observability, and quality choices are recorded as ADRs and summarized below. Any change to these requires updating the corresponding ADR.
|
The structural, security, observability, and quality choices are recorded as ADRs and summarized below. Any change to these requires updating the corresponding ADR.
|
||||||
|
|
||||||
- **Workspace:** Nx monorepo with the `apps` preset, managed by pnpm — see [ADR-0002](docs/decisions/0002-adopt-nx-monorepo-apps-preset.md).
|
- **Workspace:** Nx monorepo with the `apps` preset, managed by pnpm — see [ADR-0002](docs/decisions/0002-adopt-nx-monorepo-apps-preset.md).
|
||||||
- **Naming:** workspace `apf-portal`; apps `portal-shell` (frontend) and `portal-bff` (backend); libs `feature-<name>` and `shared-<scope>` — see [ADR-0003](docs/decisions/0003-workspace-and-app-naming-convention.md).
|
- **Naming:** workspace `apf-portal`; apps `portal-shell` (end-user SPA), `portal-admin` (admin SPA, skeleton in place — see ADR-0020), and `portal-bff` (backend); libs `feature-<name>` and `shared-<scope>` — see [ADR-0003](docs/decisions/0003-workspace-and-app-naming-convention.md).
|
||||||
- **Frontend (`portal-shell`):** Angular at the latest LTS major — standalone APIs, zoneless change detection, Signals, **CSR only (no SSR)**, Vitest, SCSS — see [ADR-0004](docs/decisions/0004-frontend-stack-angular-csr-zoneless-signals.md).
|
- **Frontend (`portal-shell`):** Angular at the latest LTS major — standalone APIs, zoneless change detection, Signals, **CSR only (no SSR)**, Vitest, SCSS — see [ADR-0004](docs/decisions/0004-frontend-stack-angular-csr-zoneless-signals.md).
|
||||||
- **Backend (`portal-bff`):** NestJS at the latest stable major, mounted on the Express adapter (Fastify adapter swappable later) — see [ADR-0005](docs/decisions/0005-backend-stack-nestjs.md).
|
- **Backend (`portal-bff`):** NestJS at the latest stable major, mounted on the Express adapter (Fastify adapter swappable later) — see [ADR-0005](docs/decisions/0005-backend-stack-nestjs.md).
|
||||||
- **Persistence:** PostgreSQL (latest stable major) via Prisma — see [ADR-0006](docs/decisions/0006-persistence-postgresql-prisma.md).
|
- **Persistence:** PostgreSQL (latest stable major) via Prisma — see [ADR-0006](docs/decisions/0006-persistence-postgresql-prisma.md).
|
||||||
@@ -60,7 +60,7 @@ If asked to "build", "test", or "run" anything, first verify whether the workspa
|
|||||||
|
|
||||||
## Commands once the workspace exists
|
## Commands once the workspace exists
|
||||||
|
|
||||||
App-scoped — `<app>` is one of `portal-shell`, `portal-bff`:
|
App-scoped — `<app>` is one of `portal-shell`, `portal-admin`, `portal-bff`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm nx serve <app> # dev server
|
pnpm nx serve <app> # dev server
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import playwright from 'eslint-plugin-playwright';
|
||||||
|
import baseConfig from '../../eslint.config.mjs';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
playwright.configs['flat/recommended'],
|
||||||
|
...baseConfig,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.js'],
|
||||||
|
// Override or add rules here
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
import { nxE2EPreset } from '@nx/playwright/preset';
|
||||||
|
import { workspaceRoot } from '@nx/devkit';
|
||||||
|
|
||||||
|
// For CI, you may want to set BASE_URL to the deployed application.
|
||||||
|
const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// require('dotenv').config();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
...nxE2EPreset(__filename, { testDir: './src' }),
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
baseURL,
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: {
|
||||||
|
command: 'pnpm exec nx run portal-admin:serve',
|
||||||
|
url: 'http://localhost:4200',
|
||||||
|
reuseExistingServer: true,
|
||||||
|
cwd: workspaceRoot,
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: { ...devices['Desktop Safari'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
// Uncomment for mobile browsers support
|
||||||
|
/* {
|
||||||
|
name: 'Mobile Chrome',
|
||||||
|
use: { ...devices['Pixel 5'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Mobile Safari',
|
||||||
|
use: { ...devices['iPhone 12'] },
|
||||||
|
}, */
|
||||||
|
|
||||||
|
// Uncomment for branded browsers
|
||||||
|
/* {
|
||||||
|
name: 'Microsoft Edge',
|
||||||
|
use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Google Chrome',
|
||||||
|
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
} */
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "portal-admin-e2e",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"projectType": "application",
|
||||||
|
"sourceRoot": "apps/portal-admin-e2e/src",
|
||||||
|
"implicitDependencies": ["portal-admin"],
|
||||||
|
"// targets": "to see all targets run: nx show project portal-admin-e2e --web",
|
||||||
|
"targets": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('has title', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// Expect h1 to contain a substring.
|
||||||
|
expect(await page.locator('h1').innerText()).toContain('Welcome');
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"sourceMap": false,
|
||||||
|
"module": "commonjs",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.js",
|
||||||
|
"playwright.config.ts",
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"@tailwindcss/postcss": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import nx from '@nx/eslint-plugin';
|
||||||
|
import baseConfig from '../../eslint.config.mjs';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...nx.configs['flat/angular'],
|
||||||
|
...nx.configs['flat/angular-template'],
|
||||||
|
...baseConfig,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
'@angular-eslint/directive-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'attribute',
|
||||||
|
prefix: 'app',
|
||||||
|
style: 'camelCase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@angular-eslint/component-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
prefix: 'app',
|
||||||
|
style: 'kebab-case',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.html'],
|
||||||
|
// Override or add rules here
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
{
|
||||||
|
"name": "portal-admin",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"projectType": "application",
|
||||||
|
"prefix": "app",
|
||||||
|
"sourceRoot": "apps/portal-admin/src",
|
||||||
|
"tags": ["scope:portal-admin", "type:app"],
|
||||||
|
"i18n": {
|
||||||
|
"sourceLocale": {
|
||||||
|
"code": "en",
|
||||||
|
"baseHref": "/en/"
|
||||||
|
},
|
||||||
|
"locales": {
|
||||||
|
"fr": {
|
||||||
|
"translation": "apps/portal-admin/src/locale/messages.fr.xlf",
|
||||||
|
"baseHref": "/fr/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@angular/build:application",
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/apps/portal-admin",
|
||||||
|
"browser": "apps/portal-admin/src/main.ts",
|
||||||
|
"polyfills": ["@angular/localize/init"],
|
||||||
|
"tsConfig": "apps/portal-admin/tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "apps/portal-admin/public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": ["apps/portal-admin/src/styles.css"]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"localize": ["en", "fr"],
|
||||||
|
"i18nMissingTranslation": "error",
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "1.5mb",
|
||||||
|
"maximumError": "1.5mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyScript",
|
||||||
|
"maximumWarning": "500kb",
|
||||||
|
"maximumError": "500kb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "5kb",
|
||||||
|
"maximumError": "6kb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bundle",
|
||||||
|
"name": "styles",
|
||||||
|
"maximumWarning": "150kb",
|
||||||
|
"maximumError": "150kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"executor": "@angular/build:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "portal-admin:build",
|
||||||
|
"outputPath": "apps/portal-admin/src/locale",
|
||||||
|
"outFile": "messages.xlf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"continuous": true,
|
||||||
|
"executor": "@angular/build:dev-server",
|
||||||
|
"options": {
|
||||||
|
"port": 4300
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "portal-admin:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "portal-admin:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nx/eslint:lint"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@angular/build:unit-test",
|
||||||
|
"options": {
|
||||||
|
"watch": false
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"watch": {
|
||||||
|
"watch": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve-static": {
|
||||||
|
"continuous": true,
|
||||||
|
"executor": "@nx/web:file-server",
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "portal-admin:build",
|
||||||
|
"port": 4300,
|
||||||
|
"staticFilePath": "dist/apps/portal-admin/browser"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [provideBrowserGlobalErrorListeners(), provideRouter(appRoutes)],
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<a class="skip-link" href="#main-content" i18n="@@app.skipLink">Skip to main content</a>
|
||||||
|
<main id="main-content" tabindex="-1" class="min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||||
|
<router-outlet />
|
||||||
|
</main>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Route } from '@angular/router';
|
||||||
|
|
||||||
|
const homeTitle = $localize`:@@route.home.title:APF Portal Admin`;
|
||||||
|
|
||||||
|
export const appRoutes: Route[] = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
pathMatch: 'full',
|
||||||
|
loadComponent: () => import('./pages/home/home').then((m) => m.Home),
|
||||||
|
title: homeTitle,
|
||||||
|
},
|
||||||
|
// Catch-all — unknown paths bounce to home. Same role as the
|
||||||
|
// wildcard in portal-shell (per PR #96): in production each locale
|
||||||
|
// bundle ships with its own `<base href="/{locale}/">` and the
|
||||||
|
// router never sees the locale segment, so this only fires for
|
||||||
|
// genuine 404 paths; in dev (`nx serve`) it stops the router from
|
||||||
|
// throwing NG04002 when a stray `/fr/`-style URL is typed.
|
||||||
|
{
|
||||||
|
path: '**',
|
||||||
|
redirectTo: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Skip-link (WCAG 2.4.1 "Bypass Blocks"). Hidden by default, fully
|
||||||
|
// visible and focusable when reached via Tab from the address bar.
|
||||||
|
// Plain CSS rather than the `sr-only` Tailwind utilities so the
|
||||||
|
// visual state on focus is as predictable as possible across themes.
|
||||||
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
top: -40px;
|
||||||
|
left: 0.75rem;
|
||||||
|
z-index: 50;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: var(--color-brand-primary-500);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: top 0.15s ease-out;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
top: 0.5rem;
|
||||||
|
outline: 2px solid var(--color-brand-accent-300);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { App } from './app';
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [App],
|
||||||
|
providers: [provideRouter([])],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the layout shell — skip link, main landmark', async () => {
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
await fixture.whenStable();
|
||||||
|
const root = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(root.querySelector('a.skip-link')).not.toBeNull();
|
||||||
|
expect(root.querySelector('main#main-content')).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
imports: [RouterOutlet],
|
||||||
|
templateUrl: './app.html',
|
||||||
|
styleUrl: './app.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class App {}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<section class="mx-auto max-w-3xl px-6 py-12">
|
||||||
|
<h1
|
||||||
|
class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-gray-100"
|
||||||
|
i18n="@@page.home.title"
|
||||||
|
>
|
||||||
|
APF Portal Admin
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
class="mt-4 text-base leading-relaxed text-gray-700 dark:text-gray-300"
|
||||||
|
i18n="@@page.home.intro"
|
||||||
|
>
|
||||||
|
Administrative back-office for the APF Portal. The first functional modules — content
|
||||||
|
management, menu administration, user list, audit log viewer — land in upcoming PRs per
|
||||||
|
ADR-0020.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="mt-6 inline-flex items-center rounded-full border border-brand-accent-300 bg-brand-accent-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-brand-accent-700 dark:border-brand-accent-700 dark:bg-brand-accent-950/40 dark:text-brand-accent-200"
|
||||||
|
role="status"
|
||||||
|
i18n="@@page.home.status"
|
||||||
|
>
|
||||||
|
Skeleton — coming soon
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smoke-test landing page for portal-admin.
|
||||||
|
*
|
||||||
|
* v1 ships a placeholder. The real admin shell (header + sidebar +
|
||||||
|
* footer with the "Admin" badge, per ADR-0020) lands in a follow-up
|
||||||
|
* PR together with the first functional module (CMS pages, menu
|
||||||
|
* management, user list, or audit log viewer).
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'app-home',
|
||||||
|
templateUrl: './home.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class Home {}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>APF Portal Admin</title>
|
||||||
|
<base href="/" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<file source-language="en" target-language="fr" datatype="plaintext" original="ng2.template">
|
||||||
|
<body>
|
||||||
|
<!-- app -->
|
||||||
|
<trans-unit id="app.skipLink" datatype="html">
|
||||||
|
<source>Skip to main content</source>
|
||||||
|
<target>Aller au contenu principal</target>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<!-- route -->
|
||||||
|
<trans-unit id="route.home.title" datatype="html">
|
||||||
|
<source>APF Portal Admin</source>
|
||||||
|
<target>Administration APF Portal</target>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
|
<!-- page.home -->
|
||||||
|
<trans-unit id="page.home.title" datatype="html">
|
||||||
|
<source> APF Portal Admin </source>
|
||||||
|
<target> Administration APF Portal </target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="page.home.intro" datatype="html">
|
||||||
|
<source> Administrative back-office for the APF Portal. The first functional modules — content management, menu administration, user list, audit log viewer — land in upcoming PRs per ADR-0020. </source>
|
||||||
|
<target> Back-office d’administration pour le portail APF. Les premiers modules fonctionnels — gestion de contenu, gestion du menu, liste des utilisateurs, visualiseur d’audit log — arrivent dans les PRs à venir conformément à l’ADR-0020. </target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="page.home.status" datatype="html">
|
||||||
|
<source> Skeleton — coming soon </source>
|
||||||
|
<target> Squelette — bientôt disponible </target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
import { appConfig } from './app/app.config';
|
||||||
|
import { App } from './app/app';
|
||||||
|
|
||||||
|
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* Global styles for portal-admin.
|
||||||
|
*
|
||||||
|
* Same baseline as portal-shell — Tailwind CSS 4 (CSS-first config),
|
||||||
|
* class-based dark mode, brand palette tokens shared from
|
||||||
|
* `libs/shared/tokens`. The two apps wear the same visual chrome so
|
||||||
|
* an APF user moving between portal and admin doesn't feel a context
|
||||||
|
* switch in the design system itself; what differentiates the admin
|
||||||
|
* surface is the data density and the "Admin" badge in the shell
|
||||||
|
* (added once the admin shell PR ships).
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
|
/* Brand palette tokens — shared with portal-shell. */
|
||||||
|
@import '../../../libs/shared/tokens/src/brand-tokens.css';
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": ["@angular/localize"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"emitDecoratorMetadata": false,
|
||||||
|
"module": "preserve"
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": ["vitest/globals", "@angular/localize"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user