# Angular Developer Guide — Calendar Booking Platform

**Framework:** Angular 18+  
**Language:** TypeScript 5.x  
**State Management:** NgRx (Redux pattern)  
**UI:** Angular Material (recommended)  
**API Client:** HttpClient + interceptors

---

## Table of Contents

1. [Overview](#1-overview)
2. [Applications and Build Targets](#2-applications-and-build-targets)
3. [Project Structure](#3-project-structure)
4. [Getting Started](#4-getting-started)
5. [NgRx Store Architecture](#5-ngrx-store-architecture)
6. [Feature Modules](#6-feature-modules)
7. [State Slices](#7-state-slices)
8. [Effects and Side Effects](#8-effects-and-side-effects)
9. [Routing](#9-routing)
10. [API Integration](#10-api-integration)
11. [Embed Widget (Third-Party Integration)](#11-embed-widget-third-party-integration)
12. [Shared Components](#12-shared-components)
13. [Timezone Handling](#13-timezone-handling)
14. [Styling and Theming](#14-styling-and-theming)
15. [Testing](#15-testing)
16. [Build and Deployment](#16-build-and-deployment)

---

## 1. Overview

The Angular frontend consists of two deployable applications:

| App | Purpose | Audience |
|-----|---------|----------|
| **Host Dashboard** | Registration, availability, calendars, bookings | Logged-in hosts |
| **Embed Widget** | Lightweight booking flow for third-party sites | Public guests via iframe/inline |

Both apps share libraries (`core`, `shared`, `booking-ui`) and talk to the Node.js API.

```mermaid
flowchart TB
    subgraph Angular Apps
        DASH[Host Dashboard]
        EMBED[Embed Widget]
        PUB[Public Booking Page]
    end

    subgraph Shared
        CORE[core lib]
        SHARED[shared lib]
        STORE[NgRx Store]
    end

    DASH --> STORE
    EMBED --> STORE
    PUB --> STORE
    STORE --> API[Node.js REST API]
    EMBED -->|postMessage| PARENT[Third-Party Website]
```

### Redux via NgRx

Angular uses **NgRx** as the Redux implementation:

| Redux Concept | NgRx Equivalent |
|---------------|-----------------|
| Store | `@ngrx/store` |
| Actions | `createAction` |
| Reducers | `createReducer` + `on()` |
| Side effects | `@ngrx/effects` |
| Selectors | `createSelector` |
| DevTools | `@ngrx/store-devtools` |

---

## 2. Applications and Build Targets

```
calendar-web/
├── projects/
│   ├── host-dashboard/          # Main SPA — host management
│   ├── public-booking/          # Standalone public booking pages
│   ├── embed-widget/            # Embeddable booking widget (minimal bundle)
│   ├── core/                    # Auth, API, guards, interceptors
│   ├── shared/                  # Shared components, pipes, models
│   └── booking-ui/              # Calendar picker, slot list, booking form
├── angular.json
├── package.json
└── tsconfig.json
```

### Build Commands

```bash
# Host dashboard (default)
ng serve host-dashboard
ng build host-dashboard --configuration production

# Public booking pages
ng serve public-booking
ng build public-booking --configuration production

# Embed widget (small bundle for third-party sites)
ng build embed-widget --configuration production
```

### Bundle Size Targets

| App | Target (gzip) |
|-----|---------------|
| embed-widget | < 150 KB |
| public-booking | < 250 KB |
| host-dashboard | < 400 KB initial |

Use lazy-loaded feature modules in the dashboard to keep initial bundle small.

---

## 3. Project Structure

### Host Dashboard

```
projects/host-dashboard/src/app/
├── app.config.ts               # provideStore, provideEffects, routes
├── app.routes.ts
├── core/
│   ├── guards/
│   │   └── auth.guard.ts
│   └── layout/
│       ├── header/
│       └── sidebar/
├── features/
│   ├── auth/
│   │   ├── login/
│   │   ├── register/
│   │   └── store/              # auth feature state
│   ├── dashboard/
│   ├── availability/
│   │   ├── components/
│   │   └── store/
│   ├── event-types/
│   │   ├── components/
│   │   └── store/
│   ├── calendars/
│   │   ├── components/
│   │   └── store/
│   ├── bookings/
│   │   ├── components/
│   │   └── store/
│   └── embed-settings/
│       ├── components/
│       └── store/
└── store/                      # Root store registration
    ├── app.state.ts
    └── index.ts
```

### Shared Libraries

```
projects/core/src/lib/
├── api/
│   ├── api.service.ts
│   ├── auth-api.service.ts
│   ├── booking-api.service.ts
│   ├── availability-api.service.ts
│   └── embed-api.service.ts
├── interceptors/
│   ├── auth.interceptor.ts
│   ├── error.interceptor.ts
│   └── embed.interceptor.ts
├── models/
│   ├── user.model.ts
│   ├── booking.model.ts
│   ├── event-type.model.ts
│   └── slot.model.ts
└── tokens/
    └── api-url.token.ts

projects/booking-ui/src/lib/
├── slot-picker/
├── date-calendar/
├── booking-form/
├── timezone-selector/
└── confirmation/
```

---

## 4. Getting Started

### Prerequisites

- Node.js 20+
- Angular CLI 18+

### Setup

```bash
cd calendar-web
npm install
cp projects/host-dashboard/src/environments/environment.example.ts \
   projects/host-dashboard/src/environments/environment.ts
ng serve host-dashboard
```

Open `http://localhost:4200`.

### Environment Files

```typescript
// projects/host-dashboard/src/environments/environment.ts
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000/v1',
  appUrl: 'http://localhost:4200',
  googleOAuthUrl: 'http://localhost:3000/v1/me/calendar-connections/oauth/google/authorize',
  microsoftOAuthUrl: 'http://localhost:3000/v1/me/calendar-connections/oauth/microsoft/authorize',
};
```

```typescript
// projects/embed-widget/src/environments/environment.ts
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000/v1',
  parentOrigin: '*', // Restricted at runtime via embed config
};
```

---

## 5. NgRx Store Architecture

### Root State Shape

```typescript
// projects/host-dashboard/src/app/store/app.state.ts
import { AuthState } from '../features/auth/store/auth.reducer';
import { AvailabilityState } from '../features/availability/store/availability.reducer';
import { EventTypesState } from '../features/event-types/store/event-types.reducer';
import { CalendarsState } from '../features/calendars/store/calendars.reducer';
import { BookingsState } from '../features/bookings/store/bookings.reducer';
import { EmbedState } from '../features/embed-settings/store/embed.reducer';

export interface AppState {
  auth: AuthState;
  availability: AvailabilityState;
  eventTypes: EventTypesState;
  calendars: CalendarsState;
  bookings: BookingsState;
  embed: EmbedState;
}

// Public / embed app uses a subset:
export interface PublicBookingState {
  publicBooking: PublicBookingFeatureState;
}
```

### App Config (Standalone Angular)

```typescript
// projects/host-dashboard/src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import { provideStoreDevtools } from '@ngrx/store-devtools';

import { routes } from './app.routes';
import { reducers } from './store';
import { AuthEffects } from './features/auth/store/auth.effects';
import { AvailabilityEffects } from './features/availability/store/availability.effects';
import { EventTypesEffects } from './features/event-types/store/event-types.effects';
import { CalendarsEffects } from './features/calendars/store/calendars.effects';
import { BookingsEffects } from './features/bookings/store/bookings.effects';
import { EmbedEffects } from './features/embed-settings/store/embed.effects';
import { authInterceptor } from '@calendar/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor])),
    provideStore(reducers),
    provideEffects([
      AuthEffects,
      AvailabilityEffects,
      EventTypesEffects,
      CalendarsEffects,
      BookingsEffects,
      EmbedEffects,
    ]),
    provideStoreDevtools({ maxAge: 25, logOnly: false }),
  ],
};
```

### Root Reducers Map

```typescript
// projects/host-dashboard/src/app/store/index.ts
import { ActionReducerMap } from '@ngrx/store';
import { AppState } from './app.state';
import { authReducer } from '../features/auth/store/auth.reducer';
import { availabilityReducer } from '../features/availability/store/availability.reducer';
import { eventTypesReducer } from '../features/event-types/store/event-types.reducer';
import { calendarsReducer } from '../features/calendars/store/calendars.reducer';
import { bookingsReducer } from '../features/bookings/store/bookings.reducer';
import { embedReducer } from '../features/embed-settings/store/embed.reducer';

export const reducers: ActionReducerMap<AppState> = {
  auth: authReducer,
  availability: availabilityReducer,
  eventTypes: eventTypesReducer,
  calendars: calendarsReducer,
  bookings: bookingsReducer,
  embed: embedReducer,
};
```

---

## 6. Feature Modules

Each feature follows the same NgRx pattern:

```
features/{feature}/
├── components/          # Smart + presentational components
├── store/
│   ├── {feature}.actions.ts
│   ├── {feature}.reducer.ts
│   ├── {feature}.effects.ts
│   ├── {feature}.selectors.ts
│   └── index.ts
└── {feature}.routes.ts  # Lazy-loaded routes
```

### Lazy Loading

```typescript
// projects/host-dashboard/src/app/app.routes.ts
export const routes: Routes = [
  { path: 'login', loadComponent: () => import('./features/auth/login/login.component') },
  {
    path: '',
    canActivate: [authGuard],
    children: [
      { path: 'dashboard', loadChildren: () => import('./features/dashboard/dashboard.routes') },
      { path: 'availability', loadChildren: () => import('./features/availability/availability.routes') },
      { path: 'event-types', loadChildren: () => import('./features/event-types/event-types.routes') },
      { path: 'calendars', loadChildren: () => import('./features/calendars/calendars.routes') },
      { path: 'bookings', loadChildren: () => import('./features/bookings/bookings.routes') },
      { path: 'embed', loadChildren: () => import('./features/embed-settings/embed.routes') },
    ],
  },
];
```

---

## 7. State Slices

### 7.1 Auth State

```typescript
// features/auth/store/auth.actions.ts
import { createAction, props } from '@ngrx/store';
import { User } from '@calendar/core';

export const login = createAction('[Auth] Login', props<{ email: string; password: string }>());
export const loginSuccess = createAction('[Auth] Login Success', props<{ user: User; accessToken: string }>());
export const loginFailure = createAction('[Auth] Login Failure', props<{ error: string }>());
export const logout = createAction('[Auth] Logout');
export const loadUser = createAction('[Auth] Load User');
export const loadUserSuccess = createAction('[Auth] Load User Success', props<{ user: User }>());
```

```typescript
// features/auth/store/auth.reducer.ts
export interface AuthState {
  user: User | null;
  accessToken: string | null;
  loading: boolean;
  error: string | null;
}

const initialState: AuthState = {
  user: null,
  accessToken: localStorage.getItem('accessToken'),
  loading: false,
  error: null,
};

export const authReducer = createReducer(
  initialState,
  on(login, (state) => ({ ...state, loading: true, error: null })),
  on(loginSuccess, (state, { user, accessToken }) => {
    localStorage.setItem('accessToken', accessToken);
    return { ...state, user, accessToken, loading: false };
  }),
  on(loginFailure, (state, { error }) => ({ ...state, loading: false, error })),
  on(logout, () => {
    localStorage.removeItem('accessToken');
    return { ...initialState, accessToken: null };
  }),
  on(loadUserSuccess, (state, { user }) => ({ ...state, user, loading: false })),
);
```

```typescript
// features/auth/store/auth.selectors.ts
export const selectAuthState = createFeatureSelector<AuthState>('auth');
export const selectUser = createSelector(selectAuthState, (s) => s.user);
export const selectIsAuthenticated = createSelector(selectAuthState, (s) => !!s.accessToken);
export const selectAuthLoading = createSelector(selectAuthState, (s) => s.loading);
```

---

### 7.2 Availability State

```typescript
export interface AvailabilityState {
  weeklyRules: WeeklyRule[];
  dateOverrides: DateOverride[];
  minNoticeHours: number;
  maxAdvanceDays: number;
  loading: boolean;
  saving: boolean;
  error: string | null;
}

// Actions
export const loadAvailability = createAction('[Availability] Load');
export const loadAvailabilitySuccess = createAction(
  '[Availability] Load Success',
  props<{ schedule: AvailabilitySchedule }>(),
);
export const saveAvailability = createAction(
  '[Availability] Save',
  props<{ schedule: Partial<AvailabilitySchedule> }>(),
);
export const saveAvailabilitySuccess = createAction(
  '[Availability] Save Success',
  props<{ schedule: AvailabilitySchedule }>(),
);
```

---

### 7.3 Event Types State

```typescript
export interface EventTypesState {
  entities: Record<string, EventType>;
  ids: string[];
  selectedId: string | null;
  loading: boolean;
  error: string | null;
}

// Entity adapter recommended
export const eventTypesAdapter = createEntityAdapter<EventType>();

export const loadEventTypes = createAction('[EventTypes] Load');
export const createEventType = createAction('[EventTypes] Create', props<{ dto: CreateEventTypeDto }>());
export const updateEventType = createAction('[EventTypes] Update', props<{ id: string; dto: UpdateEventTypeDto }>());
export const deleteEventType = createAction('[EventTypes] Delete', props<{ id: string }>());
```

---

### 7.4 Public Booking State (Public + Embed Apps)

Used by `public-booking` and `embed-widget` apps:

```typescript
export interface PublicBookingFeatureState {
  host: PublicHost | null;
  eventType: PublicEventType | null;
  guestTimezone: string;
  selectedDate: string | null;       // YYYY-MM-DD
  slots: TimeSlot[];
  selectedSlot: TimeSlot | null;
  bookingForm: GuestBookingForm | null;
  confirmedBooking: PublicBookingConfirmation | null;
  loading: boolean;
  slotsLoading: boolean;
  submitting: boolean;
  error: string | null;
  embedConfig: EmbedConfig | null;
}

// Actions flow
// LoadEventPage → LoadSlots → SelectSlot → SubmitBooking → BookingSuccess
```

---

### 7.5 Embed Settings State (Host Dashboard)

```typescript
export interface EmbedState {
  configs: EmbedConfig[];
  loading: boolean;
  creating: boolean;
  error: string | null;
}

export const createEmbedConfig = createAction(
  '[Embed] Create',
  props<{ eventTypeId: string; displayMode: EmbedDisplayMode; allowedOrigins: string[] }>(),
);
export const createEmbedConfigSuccess = createAction(
  '[Embed] Create Success',
  props<{ config: EmbedConfig; snippet: string }>(),
);
```

---

## 8. Effects and Side Effects

All HTTP calls and navigation happen in **Effects**, not components.

### Auth Effects

```typescript
// features/auth/store/auth.effects.ts
@Injectable()
export class AuthEffects {
  private actions$ = inject(Actions);
  private authApi = inject(AuthApiService);
  private router = inject(Router);

  login$ = createEffect(() =>
    this.actions$.pipe(
      ofType(login),
      switchMap(({ email, password }) =>
        this.authApi.login(email, password).pipe(
          map((res) => loginSuccess({ user: res.user, accessToken: res.accessToken })),
          catchError((err) => of(loginFailure({ error: err.message }))),
        ),
      ),
    ),
  );

  loginSuccess$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(loginSuccess),
        tap(() => this.router.navigate(['/dashboard'])),
      ),
    { dispatch: false },
  );

  loadUser$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadUser),
      switchMap(() =>
        this.authApi.getMe().pipe(
          map((user) => loadUserSuccess({ user })),
          catchError(() => of(logout())),
        ),
      ),
    ),
  );
}
```

### Public Booking Effects

```typescript
@Injectable()
export class PublicBookingEffects {
  loadEventPage$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadEventPage),
      switchMap(({ username, eventSlug }) =>
        this.publicApi.getEventPage(username, eventSlug).pipe(
          map((data) => loadEventPageSuccess(data)),
          catchError((err) => of(publicBookingError({ error: err.message }))),
        ),
      ),
    ),
  );

  loadSlots$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadSlots),
      debounceTime(200),
      switchMap(({ username, eventSlug, from, to, timezone }) =>
        this.publicApi.getSlots(username, eventSlug, from, to, timezone).pipe(
          map((res) => loadSlotsSuccess({ slots: res.slots, timezone: res.timezone })),
          catchError((err) => of(publicBookingError({ error: err.message }))),
        ),
      ),
    ),
  );

  submitBooking$ = createEffect(() =>
    this.actions$.pipe(
      ofType(submitBooking),
      switchMap(({ username, eventSlug, payload, idempotencyKey, embedKey }) =>
        this.publicApi.createBooking(username, eventSlug, payload, idempotencyKey, embedKey).pipe(
          map((booking) => submitBookingSuccess({ booking })),
          catchError((err) => of(submitBookingFailure({ error: err.message }))),
        ),
      ),
    ),
  );

  // Notify parent page when embedded
  bookingCompleteNotifyParent$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(submitBookingSuccess),
        tap(({ booking }) => {
          if (window.parent !== window) {
            window.parent.postMessage(
              {
                type: 'calendar.embed.bookingComplete',
                payload: { bookingId: booking.id },
              },
              '*', // Target origin set from embed config at runtime
            );
          }
        }),
      ),
    { dispatch: false },
  );
}
```

### Effect Guidelines

| Rule | Reason |
|------|--------|
| Components dispatch actions only | Keeps UI dumb and testable |
| One effect per action type | Easier debugging |
| Use `switchMap` for HTTP | Cancels stale requests |
| Use `exhaustMap` for form submit | Prevents double-submit |
| Navigation in `{ dispatch: false }` effects | Side effect, not state change |

---

## 9. Routing

### Host Dashboard Routes

| Path | Component | Guard |
|------|-----------|-------|
| `/login` | LoginComponent | — |
| `/register` | RegisterComponent | — |
| `/dashboard` | DashboardComponent | authGuard |
| `/availability` | AvailabilityComponent | authGuard |
| `/event-types` | EventTypesListComponent | authGuard |
| `/event-types/new` | EventTypeFormComponent | authGuard |
| `/calendars` | CalendarsComponent | authGuard |
| `/bookings` | BookingsListComponent | authGuard |
| `/embed` | EmbedSettingsComponent | authGuard |

### Public Booking Routes

| Path | Component |
|------|-----------|
| `/book/:username/:eventSlug` | PublicBookingComponent |
| `/cancel/:token` | CancelBookingComponent |

### Embed Widget Routes

| Path | Component |
|------|-----------|
| `/embed/widget/:embedKey` | EmbedWidgetComponent |

---

## 10. API Integration

### API Service

```typescript
// projects/core/src/lib/api/public-api.service.ts
@Injectable({ providedIn: 'root' })
export class PublicApiService {
  private http = inject(HttpClient);
  private apiUrl = inject(API_URL);

  getEventPage(username: string, eventSlug: string) {
    return this.http.get<PublicEventPageResponse>(
      `${this.apiUrl}/public/${username}/${eventSlug}`,
    );
  }

  getSlots(username: string, eventSlug: string, from: string, to: string, timezone: string) {
    return this.http.get<SlotsResponse>(
      `${this.apiUrl}/public/${username}/${eventSlug}/slots`,
      { params: { from, to, timezone } },
    );
  }

  createBooking(
    username: string,
    eventSlug: string,
    body: CreateBookingDto,
    idempotencyKey: string,
    embedKey?: string,
  ) {
    const headers: Record<string, string> = {
      'Idempotency-Key': idempotencyKey,
    };
    if (embedKey) {
      headers['X-Embed-Key'] = embedKey;
    }
    return this.http.post<PublicBookingConfirmation>(
      `${this.apiUrl}/public/${username}/${eventSlug}/bookings`,
      body,
      { headers },
    );
  }
}
```

### Auth Interceptor

```typescript
// projects/core/src/lib/interceptors/auth.interceptor.ts
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = localStorage.getItem('accessToken');
  if (token && req.url.includes('/me/')) {
    req = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` },
    });
  }
  return next(req);
};
```

### Error Interceptor

```typescript
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      const message =
        error.error?.message ??
        (error.status === 409 ? 'This slot is no longer available.' : 'Something went wrong.');
      return throwError(() => new ApiError(error.status, error.error?.error, message));
    }),
  );
};
```

---

## 11. Embed Widget (Third-Party Integration)

The embed widget allows partners to add booking to their website without building a custom UI.

### Integration Methods

#### Method 1 — Script Tag (Recommended)

Host copies snippet from **Dashboard → Embed Settings**:

```html
<div id="calendar-booking-widget"></div>
<script
  src="https://api.book.example.com/embed/loader.js"
  data-embed-key="emb_abc123xyz"
  data-mode="inline"
  data-target="#calendar-booking-widget"
  async
></script>
```

The loader script (served by Node) fetches embed config and mounts an iframe:

```html
<iframe
  src="https://book.example.com/embed/widget/emb_abc123xyz"
  style="width:100%;min-height:600px;border:none;"
  allow="clipboard-write"
></iframe>
```

#### Method 2 — Direct Iframe

```html
<iframe
  src="https://book.example.com/embed/widget/emb_abc123xyz"
  width="100%"
  height="650"
  frameborder="0"
></iframe>
```

#### Method 3 — Angular Component (Same Monorepo)

If the third-party app is also Angular:

```typescript
import { EmbedBookingComponent } from '@calendar/booking-ui';

@Component({
  template: `
    <calendar-embed-booking
      [embedKey]="'emb_abc123xyz'"
      (bookingComplete)="onBookingComplete($event)"
    />
  `,
  imports: [EmbedBookingComponent],
})
export class PartnerPageComponent {
  onBookingComplete(bookingId: string) {
    console.log('Booked:', bookingId);
  }
}
```

Publish `@calendar/booking-ui` as an npm package for external Angular apps.

---

### Embed Widget Component

```typescript
// projects/embed-widget/src/app/embed-widget.component.ts
@Component({
  selector: 'app-embed-widget',
  standalone: true,
  imports: [SlotPickerComponent, BookingFormComponent, ConfirmationComponent],
  template: `
    @if (loading$ | async) {
      <div class="embed-loading">Loading...</div>
    } @else if (confirmedBooking$ | async; as booking) {
      <calendar-confirmation [booking]="booking" [compact]="true" />
    } @else {
      <div [style.--primary-color]="(theme$ | async)?.primaryColor">
        <calendar-timezone-selector
          [timezone]="(guestTimezone$ | async)!"
          (timezoneChange)="onTimezoneChange($event)"
        />
        <calendar-date-calendar
          [selectedDate]="(selectedDate$ | async)!"
          (dateChange)="onDateChange($event)"
        />
        <calendar-slot-picker
          [slots]="(slots$ | async) ?? []"
          [loading]="(slotsLoading$ | async) ?? false"
          (slotSelect)="onSlotSelect($event)"
        />
        @if (selectedSlot$ | async) {
          <calendar-booking-form
            [eventType]="(eventType$ | async)!"
            (submitForm)="onSubmit($event)"
            [submitting]="(submitting$ | async) ?? false"
          />
        }
      </div>
    }
  `,
})
export class EmbedWidgetComponent implements OnInit {
  private store = inject(Store);
  private route = inject(ActivatedRoute);
  private embedMessenger = inject(EmbedMessengerService);

  embedKey = this.route.snapshot.paramMap.get('embedKey')!;

  loading$ = this.store.select(selectPublicBookingLoading);
  eventType$ = this.store.select(selectEventType);
  theme$ = this.store.select(selectEmbedTheme);
  // ... other selectors

  ngOnInit() {
    this.store.dispatch(loadEmbedConfig({ embedKey: this.embedKey }));
    this.embedMessenger.notifyReady();
  }

  onSubmit(form: GuestBookingForm) {
    this.store.dispatch(submitBooking({
      username: '...',  // from embed config
      eventSlug: '...',
      payload: form,
      idempotencyKey: crypto.randomUUID(),
      embedKey: this.embedKey,
    }));
  }
}
```

---

### EmbedMessengerService (postMessage Bridge)

```typescript
// projects/core/src/lib/embed/embed-messenger.service.ts
@Injectable({ providedIn: 'root' })
export class EmbedMessengerService {
  private allowedParentOrigins: string[] = [];

  configure(origins: string[]) {
    this.allowedParentOrigins = origins;
  }

  notifyReady() {
    this.postToParent({ type: 'calendar.embed.ready' });
  }

  notifyBookingComplete(bookingId: string) {
    this.postToParent({
      type: 'calendar.embed.bookingComplete',
      payload: { bookingId },
    });
  }

  notifyError(message: string) {
    this.postToParent({
      type: 'calendar.embed.error',
      payload: { message },
    });
  }

  private postToParent(event: EmbedEvent) {
    if (window.parent === window) return;
    for (const origin of this.allowedParentOrigins) {
      window.parent.postMessage(event, origin);
    }
  }
}
```

### Parent Page Integration (JavaScript)

```javascript
window.addEventListener('message', function (event) {
  // Validate origin
  if (event.origin !== 'https://book.example.com') return;

  switch (event.data.type) {
    case 'calendar.embed.ready':
      console.log('Widget loaded');
      break;
    case 'calendar.embed.bookingComplete':
      // Redirect, show thank-you, update CRM, etc.
      window.location.href = '/thank-you?booking=' + event.data.payload.bookingId;
      break;
    case 'calendar.embed.error':
      console.error(event.data.payload.message);
      break;
  }
});
```

---

### Host Dashboard — Embed Settings UI

The embed settings page lets hosts:

1. Select an event type
2. Choose display mode (`inline`, `popup`, `modal`)
3. Add allowed origins (partner domains)
4. Customize theme (primary color, border radius)
5. Copy embed snippet

```typescript
// Smart component dispatches NgRx actions
@Component({ /* ... */ })
export class EmbedSettingsComponent {
  private store = inject(Store);

  configs$ = this.store.select(selectEmbedConfigs);
  snippet$ = this.store.select(selectEmbedSnippet);

  ngOnInit() {
    this.store.dispatch(loadEmbedConfigs());
  }

  createConfig(eventTypeId: string, origins: string[]) {
    this.store.dispatch(createEmbedConfig({
      eventTypeId,
      displayMode: 'inline',
      allowedOrigins: origins,
    }));
  }
}
```

---

## 12. Shared Components

### Smart vs Presentational

| Type | Responsibility | Store access |
|------|----------------|--------------|
| **Smart (container)** | Dispatch actions, select state | Yes |
| **Presentational** | `@Input()` / `@Output()` only | No |

Example:

```typescript
// Presentational — projects/booking-ui/src/lib/slot-picker/slot-picker.component.ts
@Component({
  selector: 'calendar-slot-picker',
  standalone: true,
  template: `
    @if (loading) {
      <mat-spinner diameter="32" />
    } @else if (slots.length === 0) {
      <p class="no-slots">No times available for this date.</p>
    } @else {
      <div class="slot-grid">
        @for (slot of slots; track slot.startTime) {
          <button mat-stroked-button (click)="slotSelect.emit(slot)">
            {{ slot.displayStart | slotTime }}
          </button>
        }
      </div>
    }
  `,
})
export class SlotPickerComponent {
  @Input() slots: TimeSlot[] = [];
  @Input() loading = false;
  @Output() slotSelect = new EventEmitter<TimeSlot>();
}
```

---

## 13. Timezone Handling

Use **Luxon** or **date-fns-tz** (not native `Date` alone).

```typescript
// projects/core/src/lib/utils/timezone.util.ts
import { DateTime } from 'luxon';

export function getBrowserTimezone(): string {
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
}

export function formatSlotForDisplay(isoUtc: string, timezone: string): string {
  return DateTime.fromISO(isoUtc, { zone: 'utc' })
    .setZone(timezone)
    .toFormat('h:mm a');
}

export function getMonthDateRange(date: DateTime): { from: string; to: string } {
  return {
    from: date.startOf('month').toISODate()!,
    to: date.endOf('month').toISODate()!,
  };
}
```

Store `guestTimezone` in NgRx public booking state. Dispatch `loadSlots` whenever timezone or selected date changes.

---

## 14. Styling and Theming

### CSS Custom Properties for Embed

```scss
// Embed widget reads theme from NgRx and sets CSS variables
:host {
  --primary-color: var(--embed-primary, #4F46E5);
  --bg-color: var(--embed-bg, #ffffff);
  --border-radius: var(--embed-radius, 8px);

  display: block;
  font-family: var(--embed-font, 'Inter', sans-serif);
  background: var(--bg-color);
  border-radius: var(--border-radius);
}

button.primary {
  background-color: var(--primary-color);
  border-radius: var(--border-radius);
}
```

### Host Dashboard Theme

Use Angular Material theming with a custom palette. Embed widget uses a minimal subset of Material or plain CSS to keep bundle small.

---

## 15. Testing

### Reducer Tests

```typescript
describe('authReducer', () => {
  it('should set user on loginSuccess', () => {
    const user = { id: '1', email: 'a@b.com' } as User;
    const state = authReducer(initialState, loginSuccess({ user, accessToken: 'token' }));
    expect(state.user).toEqual(user);
    expect(state.accessToken).toBe('token');
  });
});
```

### Effect Tests

```typescript
describe('PublicBookingEffects', () => {
  it('should load slots on loadSlots action', () => {
    actions$ = hot('-a', { a: loadSlots({ username: 'alice', eventSlug: '30min', from: '...', to: '...', timezone: 'UTC' }) });
    publicApi.getSlots.mockReturnValue(cold('-b|', { b: { slots: [], timezone: 'UTC' } }));
    expect(effects.loadSlots$).toBeObservable(
      cold('--c', { c: loadSlotsSuccess({ slots: [], timezone: 'UTC' }) }),
    );
  });
});
```

### Component Tests

Use `MockStore` from `@ngrx/store/testing`:

```typescript
const mockStore = MockStore<{ publicBooking: PublicBookingFeatureState }>;
TestBed.configureTestingModule({
  providers: [provideMockStore({ initialState: { publicBooking: initialPublicState } })],
});
```

### E2E (Playwright)

```typescript
test('guest can book a slot', async ({ page }) => {
  await page.goto('/book/alice-smith/30min-intro');
  await page.click('[data-testid="date-cell-available"]');
  await page.click('[data-testid="slot-button"]');
  await page.fill('[data-testid="guest-name"]', 'Bob');
  await page.fill('[data-testid="guest-email"]', 'bob@test.com');
  await page.click('[data-testid="confirm-booking"]');
  await expect(page.locator('[data-testid="confirmation"]')).toBeVisible();
});
```

---

## 16. Build and Deployment

### Production Build

```bash
ng build host-dashboard --configuration production
ng build public-booking --configuration production
ng build embed-widget --configuration production
```

Output:

```
dist/host-dashboard/       → CDN / static host (book.example.com)
dist/public-booking/       → Same or separate subdomain
dist/embed-widget/         → Bundled into iframe route
```

### Nginx Config (SPA)

```nginx
server {
  listen 443 ssl;
  server_name book.example.com;

  root /var/www/calendar-web/host-dashboard;
  index index.html;

  location / {
    try_files $uri $uri/ /index.html;
  }

  location /embed/widget/ {
    root /var/www/calendar-web/embed-widget;
    try_files $uri $uri/ /index.html;
  }

  location /api/ {
    proxy_pass http://node-api:3000/v1/;
  }
}
```

### Content Security Policy (Embed)

Allow iframe embedding from partner origins via Node `embed_configs.allowed_origins`. Set:

```http
Content-Security-Policy: frame-ancestors 'self' https://partner-site.com
```

---

## NgRx Data Flow Summary

```mermaid
sequenceDiagram
    participant C as Component
    participant S as NgRx Store
    participant E as Effect
    participant API as Node API

    C->>S: dispatch(loadSlots)
    S->>S: reducer sets slotsLoading=true
    S->>E: action stream
    E->>API: GET /public/.../slots
    API->>E: slots JSON
    E->>S: dispatch(loadSlotsSuccess)
    S->>S: reducer updates slots
    S->>C: selector emits new slots
    C->>C: template re-renders
```

---

## Related Documents

- [Node.js Developer Guide](./node.md)
- [MySQL Developer Guide](./mysql.md)
- [API Reference](./API_REFERENCE.md)
- [Complete Documentation](./COMPLETE_DOCUMENTATION.md)
