Skip to content

Frontend upgrade#80

Merged
ayman-aboelabbas merged 163 commits into
mainfrom
frontend-upgrade
Jun 28, 2026
Merged

Frontend upgrade#80
ayman-aboelabbas merged 163 commits into
mainfrom
frontend-upgrade

Conversation

@ayman-aboelabbas

Copy link
Copy Markdown
Collaborator

Summary

Test plan

  • dotnet test backend/CCE.sln green
  • pnpm nx run-many -t lint,test green
  • If API surface changed: ./scripts/check-contracts-clean.sh green
  • If UI changed: pnpm nx run-many -t e2e (web-portal-e2e + admin-cms-e2e) green
  • Manual smoke notes (if any):

Security checklist

  • No new secrets / credentials in code
  • AuthN / AuthZ impact considered
  • Input validation on new endpoints
  • Audit-log entry for new state-changing operations

BRD traceability

Screenshots / output (optional)

Ayman Abo El Abbas and others added 30 commits May 14, 2026 17:39
… Tailwind

  Upgraded the Nx monorepo (web-portal + admin-cms) from Angular 19 to
  Angular 21.2.9. Replaced @ngx-translate with @jsverse/transloco and
  Bootstrap grid utilities with Tailwind CSS v3 backed by the DGA design
  token palette.

  - All @angular/* packages pinned to 21.2.9, Nx to 20.5, TypeScript to 5.9.3
  - tsconfig.base.json: moduleResolution set to "bundler" for v21 compatibility
  - ng-packagr locked to ~19.2.0 to resolve Nx 20 / Angular 21 executor mismatch
  - Migrated 838 pipe usages from `| translate` to `| transloco` workspace-wide
  - provideTransloco() wired in both app configs via TranslocoHttpLoader
  - Tailwind directives added to both global stylesheets; @use declarations
    moved above @tailwind to satisfy Dart Sass ordering rules
  - Both apps already use bootstrapApplication() + standalone components
  - api-client lib generates typed clients from external/internal OpenAPI specs
    using @hey-api/openapi-ts
… structure

  Move duplicated TranslocoHttpLoader and LocaleSwitcherComponent out of
  both apps and into libs/i18n, making them available via the @frontend/i18n
  path alias. Align admin-cms top-level orphan folders into their correct
  locations under core/ and features/.

  - Add TranslocoHttpLoader to libs/i18n (properly typed Observable<Translation>)
  - Add LocaleSwitcherComponent to libs/i18n with inline template and fixed
    spec (spy was targeting the removed translate.use() instead of setActiveLang)
  - Both app.config.ts and layout components now import from @frontend/i18n
  - Remove 4 duplicated files: translate-loader.factory.ts + locale-switcher/
    from both web-portal and admin-cms
  - Move admin-cms auth-callback/ → core/auth/auth-callback.page.ts
  - Move admin-cms auth-toolbar/ → core/layout/auth-toolbar.component.*
    (spec rewritten against DevAuthService, replacing stale OidcSecurityService mock)
  - Move admin-cms profile/ → features/account/profile.*
…, Sass migration, cities lazy-load

  Four independent improvements across web-portal and admin-cms:

  follows-registry → follows-store
  - Rename FollowsRegistryService to FollowsStoreService for naming
    consistency with map-viewer-store and scenario-builder-store
  - Update follow.directive and all spec files accordingly

  Mock files → testing/ subfolders
  - Move knowledge-center/mock-data.ts, countries/countries-mock.ts, and
    translations/translations-mock.ts into testing/ subdirectories so
    fallback/demo data is visually distinct from production code
  - Fix relative import paths inside moved files (./types → ../types)
  - Update all consumer import paths

  admin-cms Sass: @import@use
  - Replace deprecated @import of _fancy.scss and _admin-polish.scss with
    @use, hoisted above @tailwind directives per Dart Sass 3.0 requirements

  Cities data: static TS bundles → lazy-loaded JSON assets
  - Delete cities.data.ts (794 lines) and cities-extra.data.ts (194 lines)
    from the JS bundle entirely
  - Add world-map.types.ts with clean type-only definitions
  - Add CitiesService: ensureLoaded() fetches cities.json + cities-extra.json
    in parallel via HttpClient, caches result in a signal (idempotent)
  - WorldMapComponent: parallel-fetches cities alongside topology in ngOnInit,
    uses citiesService.allCities() in D3 render/fitToFilter/rebuildBoundaries
    (fixed this-context bug in rebuildBoundaries by capturing reference before
    the D3 .each() callback)
  - WorldMapPage: injects CitiesService, converts ALL_CITIES usages to
    computed(() => citiesService.allCities()) for reactive updates
  - Extract isInternalUrl() from both admin-cms interceptors into a shared
    core/http/is-internal-url.ts, matching the pattern already used in
    web-portal (removes triplication of the same 5-line function)

  - Move serverErrorInterceptor from both apps into libs/ui-kit so a single
    implementation is tested and maintained in one place; both apps now
    import from @frontend/ui-kit

  - Extend serverErrorInterceptor to handle status 0 (network failure →
    errors.network) and status 429 (rate limit → errors.rateLimit); add
    errors.rateLimit key to en/ar translation files

  - Add localeInterceptor to libs/i18n — stamps Accept-Language: ar|en on
    every request so the backend can return localised error messages;
    registered first in both apps' interceptor chains

  - Add guestGuard to web-portal — redirects authenticated users away from
    /login and /register to /me, matching the pattern in the cce-admin app
  Replace the BFF/cookie pattern with a full JWT-based auth flow:

  Auth core
  - Add AuthApiService with typed wrappers for all /api/auth/* endpoints
  - Unwrap API envelope { success, code, data } in login() and refresh()
  - Update TokenPair to match real response shape (accessTokenExpiresAtUtc,
    refreshTokenExpiresAtUtc, tokenType, user embedded in response)
  - AuthService: access token in memory signal, refresh token in
  localStorage;
    setSession() now stores the user directly from the token response — no
    extra /api/me call needed after login or refresh
  - Add token interceptor: attaches Authorization: Bearer header to internal
    requests, skips /api/auth/* paths

  Pages & routes
  - Login: ReactiveFormsModule, POST /api/auth/login, returnUrl redirect
  - Register: 8 BRD fields with full validators (minLength 12, maxLength 20,
    complexity pattern, cross-field passwordsMatch); template extracted to
  HTML
  - Forgot password: single email field, POST /api/auth/forgot-password
  - Reset password: reads email + token from query params, same password
  rules
  - Add forgot-password and reset-password routes with guestGuard

  i18n
  - Add account.forgotPassword, account.resetPassword, account.logout keys
  - Update account.register with 8 BRD field labels and password hint
  - Arabic translations for all new keys

  Config & styling
  - MAT_FORM_FIELD_DEFAULT_OPTIONS: appearance=outline applied globally
  - reRenderOnLangChange: true — fixes language switch requiring page reload
  - Global outline: none on .mat-mdc-input-element:focus (double border fix)
  - proxy.conf.json: point /api to live external API

  Contracts
  - Refresh openapi.external.json and openapi.internal.json from live Swagger

  Tech debt
  - Add BRD/tech-debt/SEC001: HttpOnly cookie migration for refresh token
  cycle

  Remove all dev cookie shims and the
  angular-auth-oidc-client setup;
  replace with the same JWT pattern used by web-portal.

  Auth core
  - Add AuthApiService: login(), refresh(), logout() with
  API envelope unwrap
  - Rewrite AuthService: access token in memory signal,
  refresh token in
    localStorage (cce_admin_rt); setSession() derives
  permissions from roles
    using the existing role→permission map (cce-admin,
  cce-editor, cce-reviewer)
  - Add tokenInterceptor: Bearer token on internal
  requests, skips auth paths
  - Add authGuard: redirects to /login with returnUrl if
  not authenticated
  - Delete DevAuthService, devAuthGuard, AuthCallbackPage,
  authInterceptor

  Login page
  - Add login.page.ts + login.page.html + login.page.scss
  - Full-screen dark-blue card layout, email + password
  with visibility toggle
  - Handles 401 (invalid credentials) and 403 (no admin
  access) distinctly

  Routing & layout
  - app.component.html is now a plain router-outlet (login
  renders without shell)
  - Shell is a layout parent route; all protected routes
  are children
  - permissionGuard via canMatch on each child route

  Config
  - Remove angular-auth-oidc-client and
  provideAnimationsAsync
  - Add reRenderOnLangChange: true,
  MAT_FORM_FIELD_DEFAULT_OPTIONS
  - Swap authInterceptor → tokenInterceptor

  Other
  - AuthToolbarComponent uses real AuthService (user name +
   logout icon)
  - ProfilePage reads from AuthService.currentUser, removes
   OidcSecurityService
  - i18n: add account.login.errorForbidden,
  account.profile.roles (en + ar)
refactor(event-detail): wrap calendar button content in ng-container
refactor(resource-detail): wrap download button content in ng-container
fix(world-map): ensure totalCities is a function call in stats display
chore(styles): update styles to use @use for fancy animations
…overage

- Replace all [dir="rtl"] with :host-context([dir="rtl"]) across 14 component
  SCSS files so Angular emulated encapsulation applies them correctly
- Add missing RTL arrow flips to news-detail, resource-detail, assistant,
  and home page CTA arrows (scaleX(-1) + reversed hover translateX)
- Fix mega-menu dropdown z-index and RTL anchor direction
- Add missing outlined text-field error/disabled/caret CSS tokens to _fancy.scss
- Move focus glow from box-shadow to outline to prevent overlap with floating label

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eb-portal

- Split CceRole into CceAdminRole (SuperAdmin, Admin, ContentManager,
  StateRepresentative) and CcePortalRole (Expert, User) in shared contracts
- Add CcePermission const with all 15 permission strings
- Admin-cms: derive typed permissions from roles in AuthService; add
  hasRole/hasAnyRole/hasPermission/hasAnyPermission helpers
- Admin-cms: add IfRoleDirective and roleGuard using CceAdminRole
- Admin-cms: upgrade PermissionDirective and permissionGuard to CcePermission type
- Admin-cms: replace raw permission strings in app.routes and nav-config
- Admin-cms: harden authGuard to reject non-admin JWT holders
- Web-portal: add hasRole/hasAnyRole/roles signal for CcePortalRole
- Web-portal: add IfRoleDirective and roleGuard using CcePortalRole
- Both apps: type AuthUser.roles as (CceAdminRole | CcePortalRole)[]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ResourceCenterUpdate, NewsPublish, NewsDelete to CcePermission
- Add 7 granular Report.* permissions to CcePermission
- Update ALL_PERMISSIONS and ContentManager/StateRepresentative role maps
- Type ReportConfig.permission as CcePermission (was string)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…revent label overlap

box-shadow renders inside the stacking context and overlaps the floating
label; outline renders outside the element boundary and does not.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Two-panel login/forgot-password/reset-password layout in admin-cms with
  locale switcher and RTL prefix-icon label fix in both apps
- auth interceptor in both apps: 401 triggers silent refresh + one retry
  before redirecting to login; concurrent refresh calls deduped
- Proactive access-token refresh scheduled 60 s before expiry in both
  AuthServices
- returnUrl open-redirect fix: only same-origin paths accepted on login
- bffCredentialsInterceptor wired in web-portal; withCredentials added per
  request in admin-cms auth interceptor
- Global 404 toast in serverErrorInterceptor; SUPPRESS_ERROR_TOAST context
  token lets components (forgot-password) suppress it for inline handling
- guest guard added to admin-cms auth routes; web-portal guest guard
  redirect corrected to /me/profile
- Email verification page in web-portal (/verify-email?token=)
- Resend verification email button on register success state
- Specs added for auth/guest guards in both apps and updated for
  auth interceptors and serverErrorInterceptor

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ixes

- Add toApiFieldErrors() to error-formatter to map the CCE array-based
  400 body { errors: [{field, code, message}] } to a per-field message
  map, normalising PascalCase field names to camelCase
- Register form sets control.setErrors({ serverError }) per field on 400;
  clearServerError() strips only that key on user input so Angular
  validator errors are preserved
- Remove email verification flow from registration: success message
  updated to "You can now sign in", resend button and related state removed
- Fix vertically off-center text on anchor mat-flat-button in auth pages:
  replace display:block with display:flex + align-items:center on
  .cce-auth__submit (affects forgot-password and reset-password)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix NG8011: split @else block in reports button into two @if blocks so
  mat-icon is the sole root node and projects correctly into MatButton slot
- Raise initial bundle budget warning from 500 kB to 1 MB (actual ~930 kB
  is expected for a full Angular Material admin CMS)
- Raise component style budget warning from 4 kB to 8 kB to cover
  community-moderation, translations and settings pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove role check from authGuard (backend returns empty roles; temporary until API is fixed)
- Simplify guestGuard to redirect any authenticated user away from login pages
- Bind [dir] on mat-sidenav-container so Angular Material CDK tracks RTL reactively; position="start" moves sidenav to the right in Arabic
- Replace one-time translate() call with toSignal(selectTranslate()) so shell title updates on language switch
- Replace hardcoded "Admin Console" and "Carbon Circular Economy" in sidenav with transloco keys
- Add nav.adminConsole, nav.footerTagline, account.logout.button keys to en.json and ar.json
- Translate common.locale.en as "الإنجليزية" in Arabic so language switcher is readable in RTL mode
- Delete orphaned auth-toolbar.component.html (component uses inline template)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Temporary fallback in setSession: if the API returns roles:[] the user
gets ALL_PERMISSIONS so the sidenav is fully visible. Once the backend
includes roles in the login/refresh response the derivePermissions
mapping will take over automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wire search & role filter on users list (GET /api/admin/users params)
- Add create user dialog (US041): firstName, lastName, email, password,
  phone, country, role with full validation and API error mapping
- Add delete user with confirmation dialog (US042)
- Row click navigates to user detail (US040)
- Scope apiEnvelopeInterceptor to /api/admin/ only — fixes login double-unwrap
- Fix toFeatureError for CCE 400 array format { errors: [{field,message}] }
  so field-level validation errors surface on the correct form controls
- Centralise password rules in PASSWORD_STRENGTH_VALIDATORS (ui-kit) shared
  by register page and admin create-user dialog
- Add RoleLabelPipe: maps cce-* role values to i18n keys; used in detail
  chips, role-assign select, and users table cell
- Define KNOWN_ROLE_OPTIONS and ASSIGNABLE_ROLES with cce-* enum values;
  ASSIGNABLE_ROLES scoped to Admin, Content Manager, Country Representative
- Translate mat-paginator via TranslocoPaginatorIntl provided in PagedTableComponent
- Add paginator.* and users.* i18n keys to en/ar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rebuild user detail page: avatar with initials fallback, active/inactive
  status chip, country name resolved via API (locale-aware), translated
  knowledge level and locale preference, roles as translated chips
- Remove Assign Roles from detail page (no longer needed)
- Fix RTL back arrow: arrow_forward in Arabic, arrow_back in English
- Replace header username label with account_circle icon button that opens
  a mat-menu dropdown showing full name, email, and sign out action
- Scope UserDelete permission to SuperAdmin only; Admin keeps ALL_PERMISSIONS
  without delete; add UserDelete to contracts and SUPER_ADMIN_PERMISSIONS
- Fix paginator i18n: subscribe to translationLoadSuccess so labels update
  once the translation file finishes loading (not just on lang change)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After successful registration, the user is redirected to /verify-phone
where they enter a 6-digit SMS code before accessing the app. The OTP
input is a 6-box component with auto-advance, backspace navigation, and
paste support. Handles rate-limit (ERR124) and generic API errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ds setup email

POST /api/admin/users no longer accepts a password field. The system
sends a password reset link to the new user's email automatically.
Removed the password input, show/hide toggle, and strength validators
from the create user dialog. Added email hint to inform the admin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added nationality/dial-code dropdown to the registration form, populated
from GET /api/country-codes. Fixed envelope unwrapping and corrected
the CountryCode type to match the actual API response shape (name.ar/en).
Added common.search and common.noResults i18n keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ode picker (Sprint 06)

- Profile page: add firstName, lastName, jobTitle, organizationName, countryCodeId fields to view and edit mode; fix getProfile/updateProfile envelope unwrap (res.data)
- Media upload: new MediaApiService (POST /api/media) — replaces plain avatarUrl text input with file picker + preview + upload flow
- Country codes cache: memoize isActive=true result in CountriesApiService for session lifetime
- Registration: split phone field into dial-code DDL + local number; concatenate on submit; countryCodeId renamed to nationality
- i18n: add profile field labels (en/ar), avatar upload keys, phone code label

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…become-an-expert CTA

- Expert request: add CV upload via POST /api/assets (MediaApiService.uploadAsset); cvAssetFileId included in submit payload; file picker with progress + filename feedback
- Profile: nationality DDL shows country name only (no dial code); remove countryId/listCountries — use countryCodeId from /api/country-codes same as registration
- Profile: add Become an Expert CTA card (links to /me/expert-request); loads expert status in parallel on init; shows pending/approved badge when applicable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Display Pending (amber), Approved (green), and Rejected (red) badges
with contextual icons. Show reapply CTA when status is Rejected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…RTL icon fix

- New /experts/:id detail page showing full submission (bio AR/EN, tags,
  CV) plus decision info (rejection reasons or approval date) and
  Approve/Reject actions for Pending requests
- Bio preview added to Approve and Reject dialogs so admins read the
  submission before deciding
- Shared _expert-dialog.scss partial; individual SCSS files per dialog
- Expert requests list simplified to a single chevron "View" link per row
- Directional icons (chevron_right, arrow_back) flip correctly in RTL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix envelope unwrap in getExpertStatus() and submitExpertRequest()
  so expert status badge renders correctly on the profile page
- Add phoneNumber field to UserProfile type and display it in view mode
- Expert status shown as a labeled field row (Pending/Approved/Rejected
  badge or "Not applied") consistent with other profile fields
- Default expert requests list filter changed to "All"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New admin screens under /publishing for managing all sprint-02 content:
- About Settings: description (AR/EN), how-to-use video URL, glossary terms
  CRUD, knowledge partners CRUD. Native HTML inputs with branded styling
  and bilingual side-by-side layout.
- Homepage Settings: video URL, objective AR/EN, CCE concepts AR/EN,
  participating countries multi-select.
- Policies Settings: multi-section editor with title/content/type/order
  and reorder action.
- Homepage admin overview page.

Publishing API service hardened for the real backend shape:
- GET responses unwrap envelope and map nested {ar,en} -> flat fields
  (description, term, definition, name, objective).
- POST/PUT bodies send flat fields (descriptionAr/En etc.) since writes
  expect a different shape than reads.
- participatingCountries[].id extracted into participatingCountryIds[]
  so the multi-select pre-binds on load.

i18n: aboutSettings, homepageSettings, policiesSettings, footer keys
added in AR + EN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ayman-aboelabbas added 27 commits June 22, 2026 15:42
… & voting

Create-post form:
- Type/topic as dropdowns (Figma); attachments drop-zone (optional) that
  uploads to /api/assets on pick with uploaded-file rows; poll builder for
  poll-type posts (options, deadline, allow-multiple/anonymous/show-results);
  full CreatePostPayload (attachments + poll) on submit.

Post detail:
- Render and vote on polls for type=Poll posts: option list (single/multi),
  results bars with percentages, the user's choice, total votes / closing /
  anonymous meta. Single-choice votes on click; multi-choice stages + submits.
  Refetches getPollResults after voting. Reads an inline `poll` on PublicPost
  (dormant until the API includes it).
…llowing dialog

- Feed sort: add MostCommented (3) across feed, my-profile and user-profile;
  expose full PostFeedSort enum (Hot/Newest/TopVoted/MostCommented) and fix the
  third option that was mislabeled onto Hot(0).
- Followed-posts tab now filters via isWatchlisted=true on the feed API.
- Profile pages: render localized expert bio (expertBioAr/En), country
  (countryNameAr/En) and joining date (joinedDate) on both my-profile and
  user-profile.
- Add "Members I follow" dialog (getMyFollows + getCommunityUser) opened from
  the following count on my-profile.
- Restyle my-profile tabs: distribute evenly (space-around) with primary text
  and secondary green underline on the active tab.
…type

Web-portal:
- Community sidebar topics now use GET /api/community/topics for real post
  counts, the "All" total, and per-topic follow status (isFollowed) with a
  bookmark toggle that persists via /api/me/follows.

Admin moderation:
- Detail dialog reads /api/admin/community/posts/{id} and /{id}/replies
  (auto-unwrapped admin envelope).
- List filter is now by post type (Informational/Question/Poll) like the
  web-portal; table shows the type chip instead of status and the title
  instead of content; detail dialog header leads with the type chip.
…thod

- post-detail + topic-detail: add a flush() helper that drains the floating
  ngOnInit→load() chain (whenStable resolved before signals were set).
- sign-in-cta, rate-post-control, topic-detail: replace the invalid
  TranslocoModule.forRoot() with TranslocoTestingModule.forRoot({...}).
- community-api: add ratePost(postId, stars) → POST /posts/{id}/rate { stars }
  (the rate-post control and its spec referenced a method missing from the
  service); update topic-detail compose-dialog payload assertion.
…tion UI

Add @frontend/real-time lib and wire real-time updates across both apps.

Core (@frontend/real-time):
- RealtimeHubService: auth-driven connect/stop, withAutomaticReconnect +
  subscription-registry replay, reconnect on token rotation, typed event
  streams (rxjs), case-insensitive payload normalizer, connectionState signal.
- provideRealtime(configFactory) wired into web-portal and admin-cms.
- Hub uses the relative /hubs/notifications path (same-origin, proxied like
  /api); added /hubs ws proxy entries (both apps -> External API).
- @microsoft/signalr dependency.

web-portal:
- Live bell + drawer via ReceiveNotification, replacing the 60s poll (kept a
  5-min reconciliation fallback).
- Community: post-detail live NewReply/VoteChanged/PollResultsChanged/
  PostModerated; topic-detail + feed "new posts" pill (NewPost) and moderation
  tombstoning.
- Redesigned payload-driven notification toast (branded card via
  openFromComponent): localized subject/body, View action, live dot, depleting
  bar; locale-based position (AR top-left, EN top-right); toasts everywhere
  (also on NewPost); dedupe guard to prevent flashing; 10s duration.
- Dev-only connection indicator in the header + event logger (config.debug).
- Fix: unwrap enveloped /api/me/notifications responses (drawer/badge/toast).

Also includes carried-over community poll voting fix and the admin
poll-results detail dialog. i18n keys added (ar + en).
…ilter row

- Seed sidebar followed-topic flags from /api/me/follows (the topics endpoint
  doesn't return isFollowed), so bookmarks reflect actual follow state.
- Wrap the feed filter bar so the type dropdown no longer overflows the page
  or clips the sort chips on narrow widths.
…, card & admin fixes

Content detail audit (web-portal) — surface all admin-form fields, localized:
- Resource detail: add Countries fact row (countryNames), localized.
- Event detail: add topic chip + tags row (topicNameAr/En, tags).
- News detail: tags chips now show the topic + real `tags` (was faking from topic).
- News card: drop dead publisher() block (read non-existent authorName/publishedBy).
- i18n: resources.detail.factCountries (ar + en).

Share dialog — same UX everywhere (posts, country, resource, event, news):
- Generalize SharePostDialogComponent data from { postId, postTitle } to { url, title }.
- Update community callers (post-detail, post-summary) to pass a URL.
- country/resource/event/news detail "Share" CTAs now open the same dialog
  (removed bespoke navigator.share / social-link rows / copyLink). Reuse share.button.

Card hover border fix (resource/news/event cards):
- Move content-visibility + contain-intrinsic-size from :host onto the card element
  so the card's own border (notably the bottom edge) isn't clipped by the host's
  paint containment. Perf optimization retained.

Admin countries endpoint:
- country-api.service listCountries() now calls /api/admin/countries (was /api/countries)
  and relies on envelope auto-unwrap (dropped manual res.data), matching the other
  methods. Flows to all admin country dropdowns.
- Unwrap RealtimeEnvelope { eventId, occurredOn, payload }; dedup on eventId
  (ring buffer) so reconnect replays don't double-fire; expose lastEventTime
  cursor for reconnect catch-up.
- admin-cms connects the Internal hub (admin JWT): /hubs proxy -> Internal API
  (External hub rejects admin JWTs).
- Refine payload types: structured ReceiveNotification (+metaData.postId),
  NewReply (+body/author), VoteChanged (+downvoteCount), PollResultsChanged
  (+totalVotes/options), NewPost (+title).
- Toast built directly from the ReceiveNotification payload (no list fetch) +
  deep-link via metaData.postId; NewPost toast uses payload.title; post-detail
  poll updates in-place from PollResultsChanged (no refetch).
community-moderation list subscribes to ContentModerated (moderation room,
auto-joined via the Internal hub). Another moderator's action toasts +
refreshes the list; the moderator's own action is skipped (already handled
locally). i18n key communityModeration.toastModerated (ar + en).
Mock @microsoft/signalr and cover the core hub behaviours: connect on auth /
disconnect on logout, RealtimeEnvelope unwrap + eventId dedup + occurredOn
cursor, Subscribe invoke + replay after reconnect, and idle without config.
Hub exposes reconnected$ (fires on auto-reconnect + token-rotation reconnect,
not initial connect). post-detail catches up via
GET /api/community/posts/{id}/activity?since={lastEventTime} and applies the
delta: post vote count (minus optimistic), poll results in-place, and a replies
refetch only when newReplies/replyCount changed. Adds PostActivity type +
CommunityApiService.getPostActivity.
- Presence: subscribe PresenceChanged -> 'N viewing' chip (shown when >1).
- Typing: subscribe TypingChanged -> animated typing indicator under the
  composer (set of user ids with 6s auto-expiry, cleared on destroy).
- Reply composer drives StartTyping/StopTyping: throttled re-emit on input,
  auto-stop on idle/blur/submit/destroy.
- i18n community.presence.viewing + community.typing.one/many (ar + en).
- Attachment kind was reversed: now 0 = Media (image/video), 1 = Document
  (per backend contract). Post detail renders Media (kind 0) inline as an
  image; Documents stay as a download card.
- Download now uses MediaApiService.downloadAsset(assetFileId) (GET
  /api/assets/{id}/download -> blob -> save) in both the post detail card and
  the posts-list card (the list button was previously a no-op); direct url
  links failed for cross-origin / auth-gated assets.
Anonymous users who click download on the post-detail card or the posts-list
card now get the sign-in/register dialog (CommunityAuthPromptService) instead
of the download — same gate as vote/reply. Adds community.authDialog.messageDownload (ar + en).
The detail API returns only attachmentIds (no full attachments array), so the
page renders that fallback branch — which hardcoded a disabled download span.
Make it an active, auth-gated download button (the id is the asset id) via
MediaApiService.downloadAsset.
The author name in cce-post-summary now links to /community/users/{id} when an
author id is present (anonymous stays plain text). Covers the feed, my-profile
followed posts, topic detail, and user-profile pages (shared card).
Avatar now routes to /community/users/{id} (hover ring); marked tabindex=-1 /
aria-hidden since the author-name link already provides the accessible nav.
Bio paragraph used text-align:end (left in RTL, right in LTR — wrong for both).
Switch to text-align:start so it follows reading direction: right in Arabic,
left in English. Applies to user + my profile pages.
Implement the two Figma empty-state designs on community-my-profile:
- My posts (لم تنشر شيئًا بعد): file-plus icon, title + desc, solid primary
  CTA 'انشاء منشور جديد' (square-plus) -> openCreatePost().
- Followed posts (لا توجد منشورات تتابعها): file-plus icon, title + desc,
  outlined CTA 'تصفّح المجتمع' -> /community.
Shared card (neutrals-50 bg, neutrals-200 border, 64px icon, 36px title,
20px body, full-width 64px button). i18n updated (emptyDesc, noPosts) +
added noPostsDesc, browseCommunity (ar + en).
…nowledge Community'

- Feed type filter: 'كل الأنواع' -> 'كل الفئات' (All types -> All categories).
- Header nav link: 'المجتمع' -> 'مجتمع المعرفة' (Community -> Knowledge Community).
Create-post type label already reads 'فئة المنشور' / 'Post category' (unchanged).
- cmp__posts-list gap 0 -> 16px to match the feed list (applies to both the
  'my posts' and 'followed posts' tabs).
- Remove the white background from the profile tab bar (cmp__tabs).
Add margin-bottom:24px to cmp__filter-bar so the first post isn't flush against
the filters (matches the search box's spacing).
….ToHttpResult()

Backend migrated 48 endpoints to return flat responses; remove the
{ data: T } wrapper and .data access from account-api and pages-api
for the affected /api/me, /api/me/*, /api/users/expert-request, and
/api/policies endpoints. Also removes color/background buttons from
the shared Quill toolbar.
…enums

All CCE endpoints return { success, code, message, data: T }. The previous
commit incorrectly assumed the migration removed the envelope — verified
against the live API. This commit:

- Reverts the wrongly-removed .data unwrapping in account-api and pages-api
- Adds missing envelope unwrapping to knowledge-maps, interactive-city,
  kapsarc, and pages/slug (these services were typed as flat all along)
- Normalises camelCase enum values from the wire ("approved", "sector",
  "parentOf") to PascalCase at every service boundary so all downstream
  comparisons, i18n keys, and CSS selectors stay unchanged
- Adds lastModifiedOn?: string | null to SavedScenario (per spec)
- Fixes KapsarcSnapshot.classification to string | null (nullable in spec)
- Makes KnowledgeMapNode optional fields (descriptionAr/En, iconUrl) that
  the API does not currently include in responses
- Remove MentionedUserIds from CreateReplyPayload; mentions now embedded
  in content as @[userId:displayName] tags per updated backend contract
- Textarea shows @DisplayName (clean UX); _mentionMap rebuilds tags at
  submit via encodeMentions() before sending to server
- Replace client-side participant filter with server-side autocomplete
  GET /api/community/communities/{id}/mentionable-users (≥2 chars,
  250ms debounce, awaits ensureLoaded() for communityId); shows
  isFollowed/isMember badges in picker dropdown
- reply.component: parse new @[uuid:name] format directly; fall back to
  old @name + mentionedUsers resolution for pre-migration replies
- Add My Mentions 4th tab to /community/me: GET /api/me/mentions paged,
  lazy-loaded, click navigates to post with reply fragment anchor
- New types: MentionableUser, MentionItem; new API methods:
  getMentionableUsers(), getMyMentions()
- i18n: myMentions, mentions.empty, mentions.mentionedBy,
  mention.followedBadge, mention.memberBadge (ar + en)
…polish

- post-detail: compute threadParticipants (deduped, excludes self);
  replace flat orderedReplies with replyTree grouping children under
  their parent; add replyingTo signal driving the compose-form chip
- reply: render contentParts() as inline @mention chips (linked when
  userId resolved, plain otherwise); add Reply button hidden for child
  replies; render children recursively via cce-reply--child modifier
- reply scss: mention chip styles (brand color, bg, hover cursor);
  children indent + connector line
- compose-reply-form scss: mention picker dropdown, badge, loading,
  empty state styles
- user-profile filter bar: swap DOM order — type+topic dropdowns at
  inline-start, sort chips at inline-end (correct RTL reading order)

private setState(state: RealtimeConnectionState): void {
this._state.set(state);
if (this.config?.debug) console.debug('[realtime] state →', state);
payload = env.payload;
}
const normalized = normalizePayload(payload);
if (this.config?.debug) console.debug(`[realtime] ◀ ${event}`, normalized);
@@ -97,7 +128,7 @@
if (q) {
const title = this.locale() === 'ar' ? a.titleAr : a.titleEn;
const content = this.locale() === 'ar' ? a.contentAr : a.contentEn;
const hay = [title, content.replace(/<[^>]*>/g, ''), a.slug].join(' ').toLowerCase();
const hay = [title, content.replace(/<[^>]*>/g, '')].join(' ').toLowerCase();

excerpt(article: NewsArticle): string {
const raw = this.isAr() ? article.contentAr : article.contentEn;
const stripped = (raw ?? '').replace(/<[^>]*>/g, '').trim();
return firstLine.length > 100 ? firstLine.slice(0, 100) + '…' : firstLine;
const t = this.post().title;
if (t) return t.length > 130 ? t.slice(0, 130) + '…' : t;
const stripped = (this.post().content ?? '').replace(/<[^>]*>/g, '').trim();
}
return '';
if (this.post().title) {
const stripped = (this.post().content ?? '').replace(/<[^>]*>/g, '').trim();
}
return rest.length > 200 ? rest.slice(0, 200) + '…' : rest;
const stripped = (this.post().content ?? '').replace(/<[^>]*>/g, '').trim();
});

function run() {
return TestBed.runInInjectionContext(() => guestGuard({} as never, {} as never));
<button type="button" mat-icon-button (click)="startEdit(row)" [disabled]="editingId() !== null">
<mat-icon>edit</mat-icon>
</button>
<button type="button" mat-icon-button color="warn" (click)="delete(row)" [disabled]="editingId() !== null">
mat-icon-button
color="warn"
*ccePermission="'Resource.Center.Delete'"
(click)="delete(row)"
});

function run() {
return TestBed.runInInjectionContext(() => guestGuard({} as never, {} as never));
import type { Country, CountryProfile, KapsarcSnapshot } from './country.types';
import { CountryCardComponent } from './country-card.component';
import { CountriesWorldMapComponent } from './countries-world-map.component';
import { getMockCardStats, getMockKapsarc } from './testing/countries-mock';
@ayman-aboelabbas ayman-aboelabbas merged commit ec9aa70 into main Jun 28, 2026
4 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants