@smartsoft001-mobilems/claude-plugins 2.86.0 → 2.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugins/mobilems/.claude-plugin/plugin.json +1 -1
- package/plugins/mobilems/skills/mobilems-articles/SKILL.md +108 -0
- package/plugins/mobilems/skills/mobilems-articles/references/lib-contents.md +164 -0
- package/plugins/mobilems/skills/mobilems-articles/references/module-and-routes.md +146 -0
- package/plugins/mobilems/skills/mobilems-articles/references/shared-components.md +56 -0
- package/plugins/mobilems/skills/mobilems-articles/scripts/detect-init.sh +29 -0
- package/plugins/mobilems/skills/mobilems-articles/scripts/verify-module.sh +61 -0
- package/plugins/mobilems/skills/mobilems-events/SKILL.md +65 -0
- package/plugins/mobilems/skills/mobilems-events/references/lib-contents.md +212 -0
- package/plugins/mobilems/skills/mobilems-events/references/module-and-routes.md +113 -0
- package/plugins/mobilems/skills/mobilems-events/scripts/detect-init.sh +29 -0
- package/plugins/mobilems/skills/mobilems-events/scripts/verify-module.sh +59 -0
- package/plugins/mobilems/skills/mobilems-object-collections/SKILL.md +77 -0
- package/plugins/mobilems/skills/mobilems-object-collections/references/lib-contents.md +76 -0
- package/plugins/mobilems/skills/mobilems-object-collections/references/module-and-routes.md +116 -0
- package/plugins/mobilems/skills/mobilems-object-collections/references/shared-components.md +61 -0
- package/plugins/mobilems/skills/mobilems-object-collections/scripts/detect-init.sh +29 -0
- package/plugins/mobilems/skills/mobilems-object-collections/scripts/verify-module.sh +44 -0
- package/plugins/mobilems/skills/mobilems-objects/SKILL.md +80 -0
- package/plugins/mobilems/skills/mobilems-objects/references/lib-contents.md +68 -0
- package/plugins/mobilems/skills/mobilems-objects/references/module-and-routes.md +108 -0
- package/plugins/mobilems/skills/mobilems-objects/references/shared-components.md +69 -0
- package/plugins/mobilems/skills/mobilems-objects/scripts/detect-init.sh +29 -0
- package/plugins/mobilems/skills/mobilems-objects/scripts/verify-module.sh +51 -0
- package/plugins/mobilems/skills/mobilems-partners/SKILL.md +65 -0
- package/plugins/mobilems/skills/mobilems-partners/references/lib-contents.md +160 -0
- package/plugins/mobilems/skills/mobilems-partners/references/module-and-routes.md +125 -0
- package/plugins/mobilems/skills/mobilems-partners/scripts/detect-init.sh +29 -0
- package/plugins/mobilems/skills/mobilems-partners/scripts/verify-module.sh +59 -0
- package/plugins/mobilems/skills/mobilems-project-init/SKILL.md +151 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/app-shell.md +115 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/claude-config.md +44 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/config-providers.md +203 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/npm-deps.md +66 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/qa-hardening.md +124 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/server-env.md +97 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/shared-lib.md +125 -0
- package/plugins/mobilems/skills/mobilems-project-init/references/tailwind-styles.md +112 -0
- package/plugins/mobilems/skills/mobilems-project-init/scripts/classify-shared-components.py +114 -0
- package/plugins/mobilems/skills/mobilems-project-init/scripts/detect-base.sh +49 -0
- package/plugins/mobilems/skills/mobilems-project-init/scripts/verify-init.sh +81 -0
- package/plugins/mobilems/skills/mobilems-thematic-publications/SKILL.md +60 -0
- package/plugins/mobilems/skills/mobilems-thematic-publications/references/lib-contents.md +134 -0
- package/plugins/mobilems/skills/mobilems-thematic-publications/references/module-and-routes.md +107 -0
- package/plugins/mobilems/skills/mobilems-thematic-publications/scripts/detect-init.sh +29 -0
- package/plugins/mobilems/skills/mobilems-thematic-publications/scripts/verify-module.sh +56 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# App shell + static pages + routes
|
|
2
|
+
|
|
3
|
+
Build the root `App` component, the static pages, and the app routing by copying from the reference's
|
|
4
|
+
`apps/web/src/app` and threading `<PREFIX>`. The foundation owns the shell, the static pages, and the
|
|
5
|
+
routing table's static routes; each feature module skill adds its own routes.
|
|
6
|
+
|
|
7
|
+
## App root — `app.ts` / `app.html` / `app.scss`
|
|
8
|
+
|
|
9
|
+
Copy verbatim and thread `<PREFIX>`. The root component imports the `COMPONENTS` array +
|
|
10
|
+
`SkipLinksComponent` from `@<PREFIX>/angular`, injects `ConfigsFacade` + `SeoService` (from
|
|
11
|
+
`@smartsoft001-mobilems/angular`), `LocalStorageService` (shared lib) and `Router`; it inits the
|
|
12
|
+
language, loads the global `Config` in `ngOnInit`, and focuses `#skip-links` on each `NavigationEnd`.
|
|
13
|
+
|
|
14
|
+
> **QA gotcha (skip links):** the copied skip-link labels/targets describe the _reference_ museum's
|
|
15
|
+
> sections — a project shipped skip links that still said "Światowid". Rewrite them for this app's
|
|
16
|
+
> actual sections and localize them (PL/EN keys). See `references/qa-hardening.md` §1.
|
|
17
|
+
> `app.html`:
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<app-skip-links></app-skip-links>
|
|
21
|
+
<app-app [options]="options">
|
|
22
|
+
<header tabindex="-1"><app-header></app-header></header>
|
|
23
|
+
<footer><app-footer></app-footer></footer>
|
|
24
|
+
</app-app>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Static pages
|
|
28
|
+
|
|
29
|
+
Copy `pages/{home,page,contact,search,not-found}` + the `home` and `page` resolvers, and thread
|
|
30
|
+
`<PREFIX>`. These use only foundation shared components:
|
|
31
|
+
|
|
32
|
+
- `home.component` uses the foundation carousels (`NewObjectsCarouselComponent`,
|
|
33
|
+
`NewsCarouselComponent`, `PartnersCarouselComponent`) and the `IObject`/`IArticle`/`IPartner` types
|
|
34
|
+
from the shared `models/` dir, plus the `DataService` home loaders — all foundation code (see
|
|
35
|
+
`references/shared-lib.md`). The home page renders fully; its carousels populate from the API at
|
|
36
|
+
runtime. **QA gotchas:** home sections must reuse the SAME shared card components as the module
|
|
37
|
+
list pages (an ad-hoc home card was a High bug); Swiper `loop` only when there are enough slides
|
|
38
|
+
for `slidesPerView` (else a console warning on every load); don't fire the same home API call
|
|
39
|
+
multiple times on load (`object-of-the-week` went out 3× — share/replay it). See
|
|
40
|
+
`references/qa-hardening.md` §6–7.
|
|
41
|
+
- `search.component` renders results through a `<router-outlet>`; it has no feature import.
|
|
42
|
+
|
|
43
|
+
Run a feature-package grep over the pages you copy as a guard. **Do not copy `article-preview`** — the
|
|
44
|
+
reference's `article-preview` page imports `@smartsoft001-mobilems/articles-domain` (it previews an
|
|
45
|
+
Article), so it is feature code, not foundation. The `mobilems-articles` skill builds it (and its
|
|
46
|
+
route) when it adds an article module. If your reference has any other page that imports a feature
|
|
47
|
+
package, leave it for the owning module skill too.
|
|
48
|
+
|
|
49
|
+
## `app.routes.ts` — the app routing table
|
|
50
|
+
|
|
51
|
+
Build `appRoutes` with the app's static routes — home, dynamic pages, contact, search, and the
|
|
52
|
+
catch-all. Each feature module skill adds its own route (a lazy `loadChildren`) to this `appRoutes`
|
|
53
|
+
array and a matching child under the `wyszukaj/:text` search route when it runs (and `mobilems-articles`
|
|
54
|
+
adds the `article-preview` route).
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { Route } from '@angular/router';
|
|
58
|
+
|
|
59
|
+
import { homeResolver } from './pages/home/home.resolver';
|
|
60
|
+
import { pageResolver } from './pages/page/page.resolver';
|
|
61
|
+
|
|
62
|
+
export const appRoutes: Route[] = [
|
|
63
|
+
{ path: '', pathMatch: 'full', redirectTo: 'strona-glowna' },
|
|
64
|
+
{
|
|
65
|
+
path: 'strona/:id',
|
|
66
|
+
loadComponent: () =>
|
|
67
|
+
import('./pages/page/page.component').then((m) => m.PageComponent),
|
|
68
|
+
resolve: { pageData: pageResolver },
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
path: 'strona/:id/:slug',
|
|
72
|
+
loadComponent: () =>
|
|
73
|
+
import('./pages/page/page.component').then((m) => m.PageComponent),
|
|
74
|
+
resolve: { pageData: pageResolver },
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
path: 'strona-glowna',
|
|
78
|
+
loadComponent: () =>
|
|
79
|
+
import('./pages/home/home.component').then((m) => m.HomeComponent),
|
|
80
|
+
resolve: { pageData: homeResolver },
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
path: 'kontakt',
|
|
84
|
+
loadComponent: () =>
|
|
85
|
+
import('./pages/contact/contact.component').then(
|
|
86
|
+
(m) => m.ContactComponent,
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
path: 'wyszukaj',
|
|
91
|
+
children: [
|
|
92
|
+
{
|
|
93
|
+
path: ':text',
|
|
94
|
+
loadComponent: () =>
|
|
95
|
+
import('./pages/search/search.component').then(
|
|
96
|
+
(m) => m.SearchComponent,
|
|
97
|
+
),
|
|
98
|
+
children: [{ path: '', redirectTo: 'obiekty', pathMatch: 'full' }],
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
path: '**',
|
|
104
|
+
loadComponent: () =>
|
|
105
|
+
import('./pages/not-found/not-found.component').then(
|
|
106
|
+
(m) => m.NotFoundComponent,
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
> The search view defaults to the objects results (`redirectTo: 'obiekty'`); the route resolves once
|
|
113
|
+
> the objects module is present. A module skill adds its route by inserting a `loadChildren` entry
|
|
114
|
+
> into `appRoutes` (top level) and into the `wyszukaj/:text` `children` array, immediately before the
|
|
115
|
+
> `{ path: '', redirectTo: 'obiekty' }` default — see each module skill's `module-and-routes.md`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# .claude extension — flow@mobilems + MobilEMS perms
|
|
2
|
+
|
|
3
|
+
The base seeded a generic `.claude/settings.json` (smart@smartsoft + nx plugins, safety/audit hooks,
|
|
4
|
+
a generic allow-list). MobilEMS adds the `flow@mobilems` plugin and the museum-specific
|
|
5
|
+
permission / MCP allow-lists. **Extend, do not replace** the base file — keep its hooks and `env`.
|
|
6
|
+
|
|
7
|
+
## `.claude/settings.json`
|
|
8
|
+
|
|
9
|
+
1. **`enabledPlugins`** — add `flow@mobilems` (keep the existing `smart@smartsoft` + `nx@nx-claude-plugins`):
|
|
10
|
+
```json
|
|
11
|
+
"enabledPlugins": {
|
|
12
|
+
"flow@mobilems": true,
|
|
13
|
+
"smart@smartsoft": true,
|
|
14
|
+
"nx@nx-claude-plugins": true
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
2. **`permissions.allow`** — add the MobilEMS working set on top of the base git/nx/fs allows:
|
|
18
|
+
- Playwright MCP (`mcp__playwright__browser_*`) + accessibility-scanner MCP
|
|
19
|
+
(`mcp__accessibility-scanner__*`) — used by the UI / a11y skills.
|
|
20
|
+
- `WebSearch`, and `WebFetch(domain:…)` for `www.npmjs.com`, `github.com`,
|
|
21
|
+
`docs.anthropic.com`, `linear.app`, plus the project's own museum portal domain (ask the user
|
|
22
|
+
for it, or leave a placeholder comment) and `localhost`.
|
|
23
|
+
- `Bash(npm start)`, `Bash(nx:*)`, `Bash(lsof:*)`, `Bash(mkdir:*)` for serve/screenshot flows.
|
|
24
|
+
- Keep `permissions.ask` (`git commit`/`git push`/`gh pr create`) from the base.
|
|
25
|
+
3. **`extraKnownMarketplaces`** — keep the base's `nx-claude-plugins`. The `mobilems` and `smart`
|
|
26
|
+
marketplaces are resolved from the installed `@smartsoft001*/claude-plugins` packages.
|
|
27
|
+
|
|
28
|
+
> Don't copy the reference's full domain allow-list verbatim — it carries one museum's specific
|
|
29
|
+
> hosts (its own portal domain, project n8n/maia webhooks). Seed the generic-useful entries and let
|
|
30
|
+
> the project grow its own.
|
|
31
|
+
|
|
32
|
+
## tools/specs + tools/prompts
|
|
33
|
+
|
|
34
|
+
Seed pointers so the museum conventions are discoverable (the framework repo keeps the canonical
|
|
35
|
+
copies under `tools/specs/{layers,styles,tests}.md` + `tools/prompts/*`). Either copy those files
|
|
36
|
+
from the reference repo, or drop a short `tools/README.md` linking to them. These are documentation
|
|
37
|
+
aids — not required for the build, so keep this light.
|
|
38
|
+
|
|
39
|
+
## CLAUDE.md / AGENTS.md
|
|
40
|
+
|
|
41
|
+
The base wrote a trimmed generic `CLAUDE.md` + the Nx `AGENTS.md`. Optionally append a short
|
|
42
|
+
"MobilEMS" section to `CLAUDE.md` noting: feature modules are added by the `mobilems-<module>`
|
|
43
|
+
skills, styling is Tailwind (`smart-` prefix) on scss, and runtime env adds `multimediaUrl`. Keep it
|
|
44
|
+
brief — the authoritative guidance lives in the plugin skills.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# app.config.ts — the MobilEMS provider set
|
|
2
|
+
|
|
3
|
+
Overwrite `apps/web/src/app/app.config.ts` with the providers below (the base shipped a minimal
|
|
4
|
+
version). Thread `<PREFIX>` (reference uses `@msr`). `app.config.server.ts` and
|
|
5
|
+
`app.routes.server.ts` stay as the base emitted them.
|
|
6
|
+
|
|
7
|
+
The imports from `@<PREFIX>/angular` (`LanguageInterceptor`, `PreviewAuthInterceptor`,
|
|
8
|
+
`SsrTimeoutInterceptor`, `LocalStorageService`, `EnvironmentService`) come from the shared lib
|
|
9
|
+
populated in step 7 — so in **foundation-only** mode, omit the three interceptor providers and the
|
|
10
|
+
`@<PREFIX>/angular` interceptor imports (they don't exist yet).
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import {
|
|
14
|
+
HTTP_INTERCEPTORS,
|
|
15
|
+
HttpClient,
|
|
16
|
+
provideHttpClient,
|
|
17
|
+
withFetch,
|
|
18
|
+
withInterceptorsFromDi,
|
|
19
|
+
} from '@angular/common/http';
|
|
20
|
+
import {
|
|
21
|
+
ApplicationConfig,
|
|
22
|
+
Injectable,
|
|
23
|
+
provideBrowserGlobalErrorListeners,
|
|
24
|
+
provideZonelessChangeDetection,
|
|
25
|
+
importProvidersFrom,
|
|
26
|
+
inject,
|
|
27
|
+
provideEnvironmentInitializer,
|
|
28
|
+
isDevMode,
|
|
29
|
+
provideAppInitializer,
|
|
30
|
+
} from '@angular/core';
|
|
31
|
+
import {
|
|
32
|
+
provideClientHydration,
|
|
33
|
+
withEventReplay,
|
|
34
|
+
withI18nSupport,
|
|
35
|
+
} from '@angular/platform-browser';
|
|
36
|
+
import { provideRouter, withInMemoryScrolling } from '@angular/router';
|
|
37
|
+
import { EffectsModule, provideEffects } from '@ngrx/effects';
|
|
38
|
+
import { provideStore, StoreModule } from '@ngrx/store';
|
|
39
|
+
import { provideStoreDevtools } from '@ngrx/store-devtools';
|
|
40
|
+
import {
|
|
41
|
+
InterpolatableTranslationObject,
|
|
42
|
+
TranslateModule,
|
|
43
|
+
TranslateService,
|
|
44
|
+
} from '@ngx-translate/core';
|
|
45
|
+
import {
|
|
46
|
+
AppService,
|
|
47
|
+
ErrorService,
|
|
48
|
+
NgrxSharedModule,
|
|
49
|
+
StyleService,
|
|
50
|
+
} from '@smartsoft001/angular';
|
|
51
|
+
import {
|
|
52
|
+
ISearchData,
|
|
53
|
+
SEARCH_CONFIG_TOKEN,
|
|
54
|
+
SharedModule,
|
|
55
|
+
} from '@smartsoft001-mobilems/angular';
|
|
56
|
+
import { MasonryModule } from '@thisissoon/angular-masonry';
|
|
57
|
+
import { catchError, firstValueFrom, map, of, timeout } from 'rxjs';
|
|
58
|
+
|
|
59
|
+
import {
|
|
60
|
+
environment,
|
|
61
|
+
EnvironmentService,
|
|
62
|
+
LanguageInterceptor,
|
|
63
|
+
LocalStorageService,
|
|
64
|
+
PreviewAuthInterceptor,
|
|
65
|
+
SsrTimeoutInterceptor,
|
|
66
|
+
} from '@<PREFIX>/angular';
|
|
67
|
+
|
|
68
|
+
import { appRoutes } from './app.routes';
|
|
69
|
+
|
|
70
|
+
@Injectable()
|
|
71
|
+
class NoOpAppService extends AppService {
|
|
72
|
+
override initTitle(): void {
|
|
73
|
+
// No-op: disable AppService.initTitle() which overrides
|
|
74
|
+
// page titles set by components via SeoService.updateTitle()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const appConfig: ApplicationConfig = {
|
|
79
|
+
providers: [
|
|
80
|
+
provideClientHydration(withEventReplay(), withI18nSupport()),
|
|
81
|
+
provideBrowserGlobalErrorListeners(),
|
|
82
|
+
provideZonelessChangeDetection(),
|
|
83
|
+
provideRouter(
|
|
84
|
+
appRoutes,
|
|
85
|
+
withInMemoryScrolling({ scrollPositionRestoration: 'top' }),
|
|
86
|
+
),
|
|
87
|
+
provideHttpClient(withFetch(), withInterceptorsFromDi()),
|
|
88
|
+
{ provide: HTTP_INTERCEPTORS, useClass: LanguageInterceptor, multi: true },
|
|
89
|
+
{
|
|
90
|
+
provide: HTTP_INTERCEPTORS,
|
|
91
|
+
useClass: PreviewAuthInterceptor,
|
|
92
|
+
multi: true,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
provide: HTTP_INTERCEPTORS,
|
|
96
|
+
useClass: SsrTimeoutInterceptor,
|
|
97
|
+
multi: true,
|
|
98
|
+
},
|
|
99
|
+
importProvidersFrom(
|
|
100
|
+
StoreModule.forRoot(
|
|
101
|
+
{},
|
|
102
|
+
{
|
|
103
|
+
runtimeChecks: {
|
|
104
|
+
strictStateImmutability: false,
|
|
105
|
+
strictActionImmutability: false,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
),
|
|
109
|
+
MasonryModule.forRoot(),
|
|
110
|
+
TranslateModule.forRoot(),
|
|
111
|
+
EffectsModule.forRoot([]),
|
|
112
|
+
NgrxSharedModule,
|
|
113
|
+
SharedModule.forRoot({
|
|
114
|
+
apiUrl: environment.apiUrl,
|
|
115
|
+
multimediaUrl: environment.multimediaUrl,
|
|
116
|
+
mainLogoPath: 'logo.png',
|
|
117
|
+
styleVariables: {},
|
|
118
|
+
}),
|
|
119
|
+
),
|
|
120
|
+
{ provide: StyleService, useClass: StyleService },
|
|
121
|
+
{ provide: AppService, useClass: NoOpAppService },
|
|
122
|
+
{
|
|
123
|
+
provide: ErrorService,
|
|
124
|
+
useValue: {
|
|
125
|
+
log: (err: any) => {
|
|
126
|
+
console.error('Global error handler:', err);
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
provideEnvironmentInitializer(() => {
|
|
131
|
+
const envService = inject(EnvironmentService);
|
|
132
|
+
envService.init();
|
|
133
|
+
}),
|
|
134
|
+
provideStore(),
|
|
135
|
+
provideEffects(),
|
|
136
|
+
provideStoreDevtools({
|
|
137
|
+
maxAge: 25,
|
|
138
|
+
logOnly: !isDevMode(),
|
|
139
|
+
autoPause: true,
|
|
140
|
+
trace: true,
|
|
141
|
+
traceLimit: 75,
|
|
142
|
+
}),
|
|
143
|
+
{
|
|
144
|
+
provide: SEARCH_CONFIG_TOKEN,
|
|
145
|
+
useValue: {
|
|
146
|
+
apiUrl: environment.apiUrl,
|
|
147
|
+
searchMap: (txt: string, data: ISearchData) => {
|
|
148
|
+
return {
|
|
149
|
+
news: `${environment.apiUrl}search/Article/page/1?maxPerPage=1&filter[phrase]=${encodeURIComponent(txt)}&filter[template]=AKTUALNO%C5%9ACI`,
|
|
150
|
+
articles: `${environment.apiUrl}search/Article/page/1?maxPerPage=1&filter[phrase]=${encodeURIComponent(txt)}&filter[template]=POZOSTAŁE`,
|
|
151
|
+
partners: `${environment.apiUrl}dictionary/Department/search/page/1?maxPerPage=999&sort=random&_type=query&q=${encodeURIComponent(txt)}`,
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
provideAppInitializer(async () => {
|
|
157
|
+
const httpClient = inject(HttpClient);
|
|
158
|
+
const translateService = inject(TranslateService);
|
|
159
|
+
const localStorageService = inject(LocalStorageService);
|
|
160
|
+
|
|
161
|
+
// Initialize language before fetching translations so the
|
|
162
|
+
// language interceptor sends the correct Accept-Language header.
|
|
163
|
+
localStorageService.initLang();
|
|
164
|
+
|
|
165
|
+
async function fetchTranslations() {
|
|
166
|
+
const url = environment.production
|
|
167
|
+
? environment.apiUrl + 'setting/theme'
|
|
168
|
+
: '/api/translations';
|
|
169
|
+
|
|
170
|
+
const translations = (await firstValueFrom(
|
|
171
|
+
httpClient.get<{ data: any }>(url).pipe(
|
|
172
|
+
timeout(5000),
|
|
173
|
+
map((response) => {
|
|
174
|
+
return response?.data?.translations ?? { en: {}, pl: {} };
|
|
175
|
+
}),
|
|
176
|
+
catchError(() => of(null)),
|
|
177
|
+
),
|
|
178
|
+
)) as {
|
|
179
|
+
en: InterpolatableTranslationObject;
|
|
180
|
+
pl: InterpolatableTranslationObject;
|
|
181
|
+
} | null;
|
|
182
|
+
|
|
183
|
+
if (!translations || !translations.en || !translations.pl) {
|
|
184
|
+
console.warn('Translations are missing or malformed:', translations);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
translateService.setTranslation('en', translations.en);
|
|
189
|
+
translateService.setTranslation('pl', translations.pl);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await fetchTranslations();
|
|
193
|
+
}),
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Notes
|
|
199
|
+
|
|
200
|
+
- `searchMap` is plain URL-string config — keep it verbatim even though it references `partners`
|
|
201
|
+
(out of scope) and `news`/`articles` (added later). It's runtime config, not an import dependency.
|
|
202
|
+
- The `news`/`articles` template values (`AKTUALNO%C5%9ACI` = AKTUALNOŚCI, `POZOSTAŁE`) match the
|
|
203
|
+
`baseQuery` the `mobilems-articles` skill threads into `ArticlesSharedModule.forFeature`.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# npm dependencies — the MobilEMS foundation set
|
|
2
|
+
|
|
3
|
+
Add these to the workspace root `package.json` `dependencies`. **Resolve every version at run
|
|
4
|
+
time — never hardcode.** Anchor on the installed `@smartsoft001/angular` line (the same anchor
|
|
5
|
+
`scaffold-nx-workspace` used): read its installed `package.json` version, or
|
|
6
|
+
`npm view @smartsoft001/<pkg> version`. The `@smartsoft001*` libs move together; match their lines.
|
|
7
|
+
|
|
8
|
+
## Add in project-init (foundation — used by the shared lib + app.config)
|
|
9
|
+
|
|
10
|
+
**Framework (smartsoft001 + MobilEMS shared):**
|
|
11
|
+
|
|
12
|
+
- `@smartsoft001/angular`
|
|
13
|
+
- `@smartsoft001/crud-shell-angular`
|
|
14
|
+
- `@smartsoft001/domain-core`
|
|
15
|
+
- `@smartsoft001/models`
|
|
16
|
+
- `@smartsoft001/utils`
|
|
17
|
+
- `@smartsoft001-mobilems/angular` (provides `SharedModule`, `SeoService`, `ConfigsFacade`, `SEARCH_CONFIG_TOKEN`)
|
|
18
|
+
- `@smartsoft001-mobilems/models` (provides `Config`, `Page`, `PageSlide`, `MsFile`)
|
|
19
|
+
|
|
20
|
+
**State + i18n:**
|
|
21
|
+
|
|
22
|
+
- `@ngrx/store`, `@ngrx/effects`, `@ngrx/store-devtools`, `@ngrx/signals`
|
|
23
|
+
- `@ngx-translate/core`
|
|
24
|
+
|
|
25
|
+
**Shared-UI / runtime libraries** (used by **generic** shared-lib components, so they are
|
|
26
|
+
foundation deps — verified against the reference):
|
|
27
|
+
|
|
28
|
+
- `@thisissoon/angular-masonry` + `masonry-layout` (masonry grids; `MasonryModule.forRoot()` in app.config)
|
|
29
|
+
- `swiper` (generic `slider` component)
|
|
30
|
+
- `leaflet` + `@types/leaflet` (generic `map` component + `styles.scss` `leaflet.css` import)
|
|
31
|
+
- `openseadragon` + `@types/openseadragon` (generic `gallery/gallery-fullscreen` deep-zoom)
|
|
32
|
+
- `@angular/cdk` (`BreakpointObserver` in the home carousels; the `orientation` service)
|
|
33
|
+
- `moment` + `moment-timezone` (date handling across shared components)
|
|
34
|
+
|
|
35
|
+
The runtime libraries go in `dependencies`; their `@types/*` packages (`@types/leaflet`,
|
|
36
|
+
`@types/openseadragon`) go in `devDependencies`. Pin `@angular/cdk` to the workspace's Angular line
|
|
37
|
+
(`@angular/core` version). `@angular/forms` is used by the shared filter components — it normally
|
|
38
|
+
comes with the Angular base; ensure it's present (same Angular line) and add it if the base lacks it.
|
|
39
|
+
|
|
40
|
+
> `express` + `dotenv` are already present from the base `server.ts` — verify, don't re-add.
|
|
41
|
+
|
|
42
|
+
## Feature packages — added by the module skills, not here
|
|
43
|
+
|
|
44
|
+
Each module skill adds its own feature packages when it builds that module:
|
|
45
|
+
|
|
46
|
+
| Package(s) | Added by |
|
|
47
|
+
| -------------------------------------------------------------------------------------- | ----------------------------- |
|
|
48
|
+
| `@smartsoft001-mobilems/objects-{domain,shell-angular,shell-dtos}`, `online-3d-viewer` | `mobilems-objects` |
|
|
49
|
+
| `@smartsoft001-mobilems/public-collections-{domain,shell-angular,shell-dtos}` | `mobilems-object-collections` |
|
|
50
|
+
| `@smartsoft001-mobilems/articles-{domain,shell-angular,shell-dtos}` | `mobilems-articles` |
|
|
51
|
+
|
|
52
|
+
Keep these out of the foundation: a feature package belongs with the feature shared components that
|
|
53
|
+
use it, so the foundation library builds on its own.
|
|
54
|
+
|
|
55
|
+
## Resolution recipe
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# anchor line (e.g. 2.122.0 → use ^2.122.0 for @smartsoft001/*, the matching mobilems line for @smartsoft001-mobilems/*)
|
|
59
|
+
npm view @smartsoft001/angular version
|
|
60
|
+
npm view @smartsoft001-mobilems/angular version
|
|
61
|
+
# third-party: take the current published line unless the base already pinned a compatible one
|
|
62
|
+
npm view <pkg> version
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Write `^<resolved>` ranges (matching the reference's style). Do not run `npm install` unless the
|
|
66
|
+
user approves (skip during eval).
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# QA hardening — lessons from live museum deployments
|
|
2
|
+
|
|
3
|
+
Every item below is a **real defect reported in QA on production museum projects** (Linear: Muzeum
|
|
4
|
+
Ostrowiec MOB-938…MOB-995, Muzeum Tomaszów Lubelski MOB-948…MOB-996). The foundation is copied from
|
|
5
|
+
a reference repo, so the default failure mode is **reference-project leftovers** plus a handful of
|
|
6
|
+
recurring SEO/i18n/WCAG gaps. Work through this checklist when applying the delta — each item is
|
|
7
|
+
cheap to do during init and expensive to chase later in QA.
|
|
8
|
+
|
|
9
|
+
## 1. No reference-project leftovers (branding)
|
|
10
|
+
|
|
11
|
+
The reference repo belongs to a _different museum_. Everything carrying that museum's identity must
|
|
12
|
+
be replaced or generated for the target project:
|
|
13
|
+
|
|
14
|
+
- **`sitemap.xml` + `robots.txt`** — never copy static files (QA found `zbiory.muzeum-orkana.pl`
|
|
15
|
+
URLs shipped to another museum). Implement them as **dynamic SSR endpoints** driven by the
|
|
16
|
+
`SITE_URL` env var — see section 2.
|
|
17
|
+
- **`og:image` / `og:image:secure_url` / `twitter:image` / `og:url`** — must point at the target
|
|
18
|
+
museum's domain, not the reference deploy (QA found `mobilems-muzeum-swiatowid-qa.smartsoft.biz.pl/gfx/social-logo.jpg`).
|
|
19
|
+
Use `SITE_URL`/`siteUrl` to build absolute URLs; ship a local `gfx/social-logo.jpg` for this app.
|
|
20
|
+
- **Favicon** — the base often has none. Ship the full set: `favicon.ico`, 32×32 + 192×192 PNG,
|
|
21
|
+
`apple-touch-icon` (two projects opened QA bugs "Brak ikony").
|
|
22
|
+
- **Skip links** — keep them (they are required), but rewrite their labels/targets for THIS app's
|
|
23
|
+
sections; copied skip links still said "Światowid". Localize them (PL/EN translation keys).
|
|
24
|
+
- **Footer / contact data** — target museum's phone, e-mail, socials; links come from the CMS API,
|
|
25
|
+
not hardcode.
|
|
26
|
+
- Local static assets (funding logos etc.) must be served near their rendered size — oversized
|
|
27
|
+
copies trigger Angular `NG0913` warnings and hurt LCP.
|
|
28
|
+
|
|
29
|
+
## 2. Dynamic sitemap + robots endpoints (server.ts)
|
|
30
|
+
|
|
31
|
+
Pattern proven in MOB-948 — put the logic in **pure, testable functions** in
|
|
32
|
+
`apps/web/src/seo/seo-endpoints.ts` (`resolveSiteUrl`, `buildSitemapXml`, `buildRobotsTxt`), and
|
|
33
|
+
wire thin routes in `server.ts`:
|
|
34
|
+
|
|
35
|
+
- Register `GET /sitemap.xml` and `GET /robots.txt` **after** `/api/translations` but **before**
|
|
36
|
+
`express.static` and the `'/**'` SSR catch-all — otherwise SSR answers with HTML and status 200
|
|
37
|
+
and the endpoints "work" only apparently.
|
|
38
|
+
- Normalize `SITE_URL`: strip trailing `/`, prepend `https://` when the schema is missing; fall back
|
|
39
|
+
to the request host when the env var is unset.
|
|
40
|
+
- Headers: `application/xml; charset=utf-8` / `text/plain; charset=utf-8`,
|
|
41
|
+
`Cache-Control: public, max-age=3600`.
|
|
42
|
+
- Sitemap content: home + the module list pages (`/strona-glowna`, `/obiekty`, `/kolekcje`, …). Do
|
|
43
|
+
**not** include CMS static pages (`/strona/:id`) — their ids are CMS-assigned and go stale.
|
|
44
|
+
- `robots.txt` points `Sitemap:` at the same `SITE_URL`.
|
|
45
|
+
|
|
46
|
+
## 3. SEO meta (home + every routed page)
|
|
47
|
+
|
|
48
|
+
- `<meta name="description">` + complete OpenGraph (`og:title`, `og:description`, `og:image`,
|
|
49
|
+
`og:url`) + Twitter Card on the home page, in **both** languages, re-set on language switch.
|
|
50
|
+
- `og:locale` must follow the active language — a hardcoded `pl_PL` was a QA bug. In
|
|
51
|
+
`museum-seo.service.ts` use a locale map (`pl → pl_PL`, `en → en_GB`) read from
|
|
52
|
+
`translateService.currentLang` (fallback via `defaultLang` — `currentLang` can be `undefined`
|
|
53
|
+
during SSR), and emit one `og:locale:alternate`.
|
|
54
|
+
|
|
55
|
+
## 4. i18n invariants
|
|
56
|
+
|
|
57
|
+
- **Default language is Polish.** A Polish museum site rendering English on first visit while
|
|
58
|
+
`<html lang="pl">` is a WCAG 3.1.1 violation and was a reported bug.
|
|
59
|
+
- `<html lang>` must track the actually rendered language after every switch; `<title>`, meta
|
|
60
|
+
description and `og:title` retranslate with it.
|
|
61
|
+
- **No hardcoded UI strings in templates.** QA repeatedly caught literals like `Zbiory {{ title }}`
|
|
62
|
+
untranslated in EN. Every static label goes through a translation key with both PL and EN values
|
|
63
|
+
in `translations.json`.
|
|
64
|
+
- Dictionary keys for filter dictionaries are **lowercase** conventions of the framework:
|
|
65
|
+
`dictionaryKey="authors"` routes to `api/article/authors/...`; `"Author"` silently falls through
|
|
66
|
+
to `api/dictionary/Author/...` → HTTP 409 and an empty options list.
|
|
67
|
+
|
|
68
|
+
## 5. WCAG invariants
|
|
69
|
+
|
|
70
|
+
- **Contrast:** the reference grey `#9ca3af` on white is 2.53:1 — fails AA and was flagged on menus
|
|
71
|
+
and breadcrumbs in two projects. Use ≥4.5:1 for text: `#6f6f6f` ≈ 5.0:1, `#4b5563` ≈ 7.6:1.
|
|
72
|
+
Figma mockups themselves sometimes specify failing colours — match the design _intent_ with the
|
|
73
|
+
nearest compliant shade instead of copying the hex blindly, and note the deviation.
|
|
74
|
+
- **Headings:** exactly one `h1` per page; no skipped levels (an `h1 → h3` jump was a recurring
|
|
75
|
+
axe-core finding). Detail pages get an `h1` (the item title), image captions are NOT headings.
|
|
76
|
+
- **alt text:** when the alt comes from a CMS title, strip HTML first (`smartRemoveHtml`) —
|
|
77
|
+
`alt="<p>Tytuł</p>"` shipped to QA.
|
|
78
|
+
- **Keyboard:** any card implemented as `role="link"`/`tabindex=0` must activate on Enter/Space
|
|
79
|
+
(or better, be a real `<a href>`). Focus must be visible; labels should react to focus/hover the
|
|
80
|
+
same way the rest of the card content does.
|
|
81
|
+
- **Fonts must cover Polish diacritics.** Bacasime Antique, Prata and Italiana lack 16/18 of
|
|
82
|
+
`ą ć ę ł ń ś ź ż` (+ uppercase) — titles rendered in a fallback font and QA rejected it. Verified
|
|
83
|
+
full-latin-ext replacements: **Gilda Display** (closest to Bacasime) or **Playfair Display**.
|
|
84
|
+
Never leave a `tailwind.config.js` `fontFamily` entry as an empty array — it silently falls back
|
|
85
|
+
to system sans.
|
|
86
|
+
|
|
87
|
+
## 6. Console hygiene (build + runtime must be warning-clean)
|
|
88
|
+
|
|
89
|
+
QA files bugs for compiler warnings, so treat them as errors after copying code:
|
|
90
|
+
|
|
91
|
+
- `NG8107` — remove `?.` on receivers whose type excludes null/undefined (typical after copying
|
|
92
|
+
templates: `list()?.length`, `filter()?.searchText`).
|
|
93
|
+
- `NG8112` — remove unused `@let` declarations.
|
|
94
|
+
- `NG8113` — remove components from `imports: []` that the template doesn't use (copied tab
|
|
95
|
+
containers often import Model3d/Map/VirtualWalk tabs conditionally rendered away).
|
|
96
|
+
- `NG0100` (`ExpressionChangedAfterItHasBeenCheckedError`) — masonry/grid items must not set their
|
|
97
|
+
`width` binding after change detection completes.
|
|
98
|
+
- **Never log API payloads to the console** — QA flagged leaked responses.
|
|
99
|
+
- Swiper: enable `loop` only when there are enough slides for the configured
|
|
100
|
+
`slidesPerView`/`slidesPerGroup`, otherwise Swiper logs a loop warning on every load.
|
|
101
|
+
- Avoid duplicate API calls on page load (home fired `object-of-the-week` 3×) — share/replay the
|
|
102
|
+
request in the service.
|
|
103
|
+
|
|
104
|
+
## 7. Cross-module UI consistency
|
|
105
|
+
|
|
106
|
+
These conventions were enforced by QA across modules — bake them in from the start:
|
|
107
|
+
|
|
108
|
+
- **One card component per entity, reused everywhere.** The home page, module lists and search
|
|
109
|
+
results must render the same shared `*-grid-item` / `*-list-item` components — a second ad-hoc
|
|
110
|
+
card implementation on the home page was a High-priority bug (MOB-994).
|
|
111
|
+
- **Full-text search inputs debounce 250 ms** (`@debounce(250)` from `lodash-decorators`) — in every
|
|
112
|
+
module's filters (objects, collections, articles/news).
|
|
113
|
+
- **Every list page shows the total result count** (`facade.totalCount()`), independent of
|
|
114
|
+
pagination (MOB-995/996).
|
|
115
|
+
- **"Next page" button** at the bottom of each list follows one shared pattern
|
|
116
|
+
(`NextPageComponent`: `selectedPage`/`lastPageIndex` from `totalCount()` + limit, scroll-to-top on
|
|
117
|
+
advance, hidden on the last page).
|
|
118
|
+
- **Breadcrumbs:** everything except the active (last) item greyed — separators `#8e8e8e`,
|
|
119
|
+
intermediate links a WCAG-compliant grey (`#4b5563`/`#6f6f6f`), active item dark/semibold.
|
|
120
|
+
- **Card hover:** underline the card **title** only (not the "Zobacz więcej" link) — pattern
|
|
121
|
+
aligned across objects/collections.
|
|
122
|
+
- **Image fallback:** cards show the app's `no-image` placeholder when a thumbnail fails to load.
|
|
123
|
+
- Keyword/tag chips must wrap on 360 px wide screens — no page-level horizontal scroll at
|
|
124
|
+
360/768/1024/1440.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Runtime env delta — multimediaUrl + /api/translations
|
|
2
|
+
|
|
3
|
+
The base shipped `environment.{ts,prod.ts}`, `EnvironmentService`, a `server.ts` with `/env.js`, and
|
|
4
|
+
the SSRF allowed-hosts list — all keyed on `API_URL` / `SITE_URL` / `APP_VERSION` only. MobilEMS adds
|
|
5
|
+
a **`multimediaUrl`** key (media/CDN host) and an **`/api/translations`** dev endpoint. These are the
|
|
6
|
+
only env changes; do not otherwise rewrite the base SSR infra.
|
|
7
|
+
|
|
8
|
+
## 1. `libs/shared/angular/src/lib/environments/environment.ts` + `environment.prod.ts`
|
|
9
|
+
|
|
10
|
+
Add the `multimediaUrl` field to **both** files, mirroring the existing keys:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
multimediaUrl:
|
|
14
|
+
typeof window !== 'undefined' && (window as any)['env']
|
|
15
|
+
? (window as any)['env']['multimediaUrl']
|
|
16
|
+
: process.env['MULTIMEDIA_URL'],
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
(Append after `siteUrl`. In reference mode the copied shared lib already carries this — verify it's
|
|
20
|
+
present rather than duplicating.)
|
|
21
|
+
|
|
22
|
+
## 2. `EnvironmentService` (shared lib)
|
|
23
|
+
|
|
24
|
+
In `EnvironmentService.initFromProcessEnv()` and `init()`, propagate `multimediaUrl` alongside
|
|
25
|
+
`apiUrl`/`siteUrl`/`version` (set from `process.env['MULTIMEDIA_URL']` on the server; copy from the
|
|
26
|
+
transfer-state / `window.env` on the client). In reference mode this comes with the copied service —
|
|
27
|
+
just confirm it.
|
|
28
|
+
|
|
29
|
+
## 3. `apps/web/src/server.ts` — two additions
|
|
30
|
+
|
|
31
|
+
**a) Allowed-hosts:** after the `SITE_URL` block and before the `API_URL` block, add a
|
|
32
|
+
`MULTIMEDIA_URL` host so SSR can fetch media during render:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const multimediaUrl = process.env['MULTIMEDIA_URL'];
|
|
36
|
+
if (multimediaUrl) {
|
|
37
|
+
try {
|
|
38
|
+
allowedHosts.push(
|
|
39
|
+
new URL(
|
|
40
|
+
multimediaUrl.startsWith('http')
|
|
41
|
+
? multimediaUrl
|
|
42
|
+
: `https://${multimediaUrl}`,
|
|
43
|
+
).hostname,
|
|
44
|
+
);
|
|
45
|
+
} catch {
|
|
46
|
+
// ignore invalid URL
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**b) `/env.js` template:** add the `multimediaUrl` line + its replacement:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
window['env']['multimediaUrl'] = '${MULTIMEDIA_URL}';
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
.replace(/\$\{MULTIMEDIA_URL\}/g, process.env['MULTIMEDIA_URL'] || '')
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**c) `/api/translations` endpoint** (needs `import * as fs from 'fs'` and `import * as path from 'path'`
|
|
62
|
+
at the top): serve the translations JSON from dist, falling back to the public source. Register it
|
|
63
|
+
**before** the static-file middleware and the catch-all `app.use('/**', …)`:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
app.get('/api/translations', (req, res) => {
|
|
67
|
+
let translationPath = path.join(
|
|
68
|
+
process.cwd(),
|
|
69
|
+
'dist/apps/web/browser/translations.json',
|
|
70
|
+
);
|
|
71
|
+
if (!fs.existsSync(translationPath)) {
|
|
72
|
+
translationPath = path.join(
|
|
73
|
+
process.cwd(),
|
|
74
|
+
'apps/web/public/translations.json',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (fs.existsSync(translationPath)) {
|
|
78
|
+
const translations = JSON.parse(fs.readFileSync(translationPath, 'utf8'));
|
|
79
|
+
res.json(translations);
|
|
80
|
+
} else {
|
|
81
|
+
console.error('Translation file not found at:', translationPath);
|
|
82
|
+
res.status(404).json({ error: 'Translation file not found' });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The `app.config.ts` `provideAppInitializer` hits `/api/translations` in dev and `apiUrl + 'setting/theme'`
|
|
88
|
+
in prod (see `references/config-providers.md`). Optionally seed an empty
|
|
89
|
+
`apps/web/public/translations.json` (`{"en":{},"pl":{}}`) so dev SSR doesn't 404.
|
|
90
|
+
|
|
91
|
+
## 4. `/sitemap.xml` + `/robots.txt` — dynamic endpoints (required)
|
|
92
|
+
|
|
93
|
+
Never ship static `sitemap.xml`/`robots.txt` copied from the reference — two projects went to QA
|
|
94
|
+
with another museum's domain in them. Build them as SSR endpoints from `SITE_URL`, with the logic in
|
|
95
|
+
pure functions (`apps/web/src/seo/seo-endpoints.ts`) and thin routes in `server.ts`, registered
|
|
96
|
+
after `/api/translations` and **before** `express.static` + the `'/**'` catch-all. Full pattern
|
|
97
|
+
(normalization, headers, content rules): `references/qa-hardening.md` §2.
|