> ## Documentation Index
> Fetch the complete documentation index at: https://learn.nexudus.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Code and Fonts

> Inject custom CSS, JavaScript, and HTML head elements into every page of the Members Portal using the Web Template Editor.

## Overview

The Members Portal exposes three injection points that let operators add custom code without modifying the portal source:

| Injection point | File in Web Template Editor | What it does                                                                         |
| --------------- | --------------------------- | ------------------------------------------------------------------------------------ |
| Custom CSS      | `styles.css`                | Injected as a `<style>` tag on every page                                            |
| Custom JS       | `app.js`                    | Executed once when the portal first mounts                                           |
| Custom head     | `head.js`                   | JSON array of `<script>`, `<meta>`, `<link>`, and similar elements added to `<head>` |

All three files are edited from the **Web Template Editor** in your Nexudus dashboard at **[Settings > Website > Web Template Editor > Built-in Files](https://dashboard.nexudus.com/settings/editor/templates)**.

<Warning>
  If you are migrating from a previous version of the Members Portal, each file must be **saved at least once** in the Web Template Editor before the setting becomes active in the new portal.
</Warning>

## Editing the files

<Steps>
  <Step title="Open the Web Template Editor">
    Go to **[Settings > Website > Web Template Editor](https://dashboard.nexudus.com/settings/editor/templates)** and select the **Built-in Files** tab.
  </Step>

  <Step title="Select a file">
    Click `app.js`, `styles.css`, or `head.js` depending on which injection point you want to customise.
  </Step>

  <Step title="Edit and save">
    Make your changes in the code editor and click **Save**. Changes take effect immediately on the next portal page load — no rebuild required.
  </Step>
</Steps>

***

## styles.css — Custom CSS

Anything you write in `styles.css` is injected verbatim inside a `<style data-nx-custom="css">` tag appended to `<head>`. Use it to override default portal styles or add entirely new rules.

```css theme={null}
/* Hide the default footer */
footer {
  display: none;
}

/* Apply a custom font */
body {
  font-family: 'Inter', sans-serif;
}

```

<Tip>
  Use the browser DevTools inspector to find the exact class names and element selectors used in the portal before writing overrides.
</Tip>

***

## app.js — Custom JavaScript

Code in `app.js` runs exactly once, after the portal mounts. It executes in the context of the portal page, so it has full access to the DOM and to the `window.__nexudus` object (see below).

```js theme={null}
// Redirect unauthenticated visitors to your marketing site
if (!window.__nexudus.auth.isAuthenticated) {
  window.__nexudus.router.navigate('/sign-in')
}

// Inject a third-party live chat widget only for authenticated members
if (window.__nexudus.auth.isAuthenticated) {
  const script = document.createElement('script')
  script.src = 'https://cdn.example.com/livechat.js'
  document.head.appendChild(script)
}

// Read a custom business setting
const primaryColor = window.__nexudus.settings.getSetting('Website.PrimaryColour')
console.log('Portal primary colour:', primaryColor)
```

<Warning>
  Custom JS runs once on mount. It does not re-run on client-side navigation between portal pages. If you need code to respond to navigation, use the `router.navigate` method or attach a `popstate` listener.
</Warning>

***

## head.js — Custom head elements

`head.js` must contain a **JSON array** of element descriptors. Each object must have a `type` field (case-insensitive) set to one of:

* `script`
* `meta`
* `link`
* `style`
* `noscript`

All other fields on the object are applied as HTML attributes on the element.

```json theme={null}
[
  {
    "type": "meta",
    "name": "theme-color",
    "content": "#ff5100"
  },
  {
    "type": "link",
    "rel": "preconnect",
    "href": "https://fonts.googleapis.com"
  },
  {
    "type": "script",
    "src": "https://cdn.example.com/analytics.js",
    "defer": "true"
  }
]
```

Elements are injected into `<head>` in the order they appear in the array. If the JSON is invalid or an element uses a disallowed `type`, that element is silently skipped.

***

## The `window.__nexudus` object

Before `app.js` executes, the portal populates `window.__nexudus` with live application state. The object is kept up to date on every render, so values read at any time reflect the current portal state.

```ts theme={null}
window.__nexudus: {
  settings: { ... }
  auth:     { ... }
  router:   { ... }
}
```

### `__nexudus.settings`

Provides access to the current location's configuration.

| Property / Method                     | Type                                                  | Description                                                                                                                                                                                                     |
| ------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `all`                                 | `Record<string, string>`                              | All business settings as a flat name → value map.                                                                                                                                                               |
| `getSetting(name)`                    | `(name: string) => string \| undefined`               | Look up a single setting by name (case-insensitive). Returns `undefined` if not found.                                                                                                                          |
| `getBoolSetting(name, defaultValue?)` | `(name: string, defaultValue?: string) => boolean`    | Returns `true` if the setting value is the string `"true"` (case-insensitive, whitespace trimmed). If the setting does not exist, `defaultValue` is used before comparison; returns `false` if both are absent. |
| `canAccessSection(accessSettingName)` | `(accessSettingName: string \| undefined) => boolean` | Returns `true` if the currently signed-in user meets the access level stored in the named setting. Pass `undefined` to always allow access. See access levels below.                                            |
| `business`                            | `Business`                                            | The current business / location object (see fields below).                                                                                                                                                      |
| `checkoutTypes`                       | `CheckoutTypes`                                       | Available resource, tariff, and product types for the current location (see below).                                                                                                                             |

#### `canAccessSection` access levels

The setting value is a numeric code that maps to one of these access levels:

| Value | Constant        | Who can access                                                                |
| ----- | --------------- | ----------------------------------------------------------------------------- |
| `1`   | `Everyone`      | Always returns `true`, including unauthenticated users.                       |
| `2`   | `LoggedInUsers` | Any signed-in user.                                                           |
| `3`   | `OnlyMembers`   | Signed-in customers with an active membership contract (`coworker.IsMember`). |
| `4`   | `OnlyContacts`  | Signed-in customers without an active contract (`coworker.IsContact`).        |

Returns `false` for any other value or when the user does not meet the required level.

```js theme={null}
// Show a section only to members
if (window.__nexudus.settings.canAccessSection('Website.ShowMembersOnlyBanner')) {
  document.getElementById('members-banner').style.display = 'block'
}

// Read a boolean feature flag
const chatEnabled = window.__nexudus.settings.getBoolSetting('Website.LiveChatEnabled')
if (chatEnabled) {
  loadLiveChatWidget()
}
```

#### `business` fields

| Field            | Type             | Description                                                      |
| ---------------- | ---------------- | ---------------------------------------------------------------- |
| `Id`             | `number`         | Unique identifier for the business.                              |
| `Name`           | `string`         | Business display name.                                           |
| `WebAddress`     | `string`         | Portal web address.                                              |
| `Address`        | `string`         | Physical address.                                                |
| `TownCity`       | `string`         | City name.                                                       |
| `Country`        | `Country`        | Country object (`Name`, `Id`, `UniqueId`).                       |
| `Currency`       | `Currency`       | Currency object (`Name`, `Code`, `Format`).                      |
| `SimpleTimeZone` | `SimpleTimeZone` | Time zone details (`Iana`, `OffsetInMinutes`, `UsesSummerTime`). |
| `Longitude`      | `number`         | Longitude coordinate.                                            |
| `Latitude`       | `number`         | Latitude coordinate.                                             |
| `HasLogo`        | `boolean`        | Whether a logo has been uploaded.                                |
| `UniqueId`       | `string`         | Globally unique identifier (GUID).                               |

#### `checkoutTypes` fields

| Field                   | Description                                                  |
| ----------------------- | ------------------------------------------------------------ |
| `ResourceTypes`         | All resource types active at this location.                  |
| `ResourceTypesMembers`  | Resource types available to members with an active contract. |
| `ResourceTypesContacts` | Resource types available to contacts (no active contract).   |
| `TariffTypes`           | Plan / membership types available at this location.          |
| `ProductTypes`          | All product types active at this location.                   |
| `ProductTypesMembers`   | Product types available to members.                          |
| `ProductTypesContacts`  | Product types available to contacts.                         |

***

### `__nexudus.auth`

Contains the current authentication state and the signed-in customer's data.

| Property          | Type                           | Description                                                                          |
| ----------------- | ------------------------------ | ------------------------------------------------------------------------------------ |
| `isAuthenticated` | `boolean`                      | `true` if the user is signed in.                                                     |
| `coworker`        | `Coworker \| null`             | Full profile of the signed-in customer, or `null` if unauthenticated.                |
| `user`            | `UserProfile \| null`          | System user record (email, full name, access token).                                 |
| `isAdmin`         | `boolean`                      | `true` if the signed-in user has administrator access.                               |
| `profiles`        | `CoworkerProfiles \| null`     | All profiles linked to this user account, including the default business.            |
| `impersonating`   | `boolean`                      | `true` when an admin is viewing the portal as another customer.                      |
| `defaultBusiness` | `ProfileBusiness \| undefined` | The default business associated with this user account (`Id`, `Name`, `WebAddress`). |

#### Commonly used `coworker` fields

| Field                 | Type                 | Description                                               |
| --------------------- | -------------------- | --------------------------------------------------------- |
| `FullName`            | `string`             | Customer's full name.                                     |
| `Email`               | `string`             | Customer's email address.                                 |
| `IsMember`            | `boolean`            | `true` if the customer has an active membership contract. |
| `IsContact`           | `boolean`            | `true` if the customer has no active contract.            |
| `IsAdmin`             | `boolean`            | `true` if the customer has admin-level access.            |
| `IsTeamAdministrator` | `boolean`            | `true` if the customer manages a team.                    |
| `AvatarUrl`           | `string`             | URL of the customer's profile picture.                    |
| `CompanyName`         | `string`             | Company name if the profile is a company type.            |
| `CoworkerType`        | `string`             | `'Individual'` or `'Company'`.                            |
| `CanMakeBookings`     | `boolean`            | Whether this customer is allowed to make bookings.        |
| `CanPurchaseProducts` | `boolean`            | Whether this customer is allowed to purchase products.    |
| `CheckedIn`           | `boolean`            | Whether the customer is currently checked in.             |
| `ActiveContracts`     | `CoworkerContract[]` | List of currently active membership contracts.            |
| `HomeSpaceName`       | `string`             | Name of the customer's home location.                     |

***

### `__nexudus.router`

Provides access to the portal's client-side router so custom code can read the current URL or navigate programmatically.

| Property   | Type               | Description                                                                            |
| ---------- | ------------------ | -------------------------------------------------------------------------------------- |
| `location` | `Location`         | Current React Router location. Includes `pathname`, `search`, `hash`, and `state`.     |
| `navigate` | `NavigateFunction` | Programmatic navigation function. Accepts a path string or a delta number for history. |

```js theme={null}
// Read the current path
console.log(window.__nexudus.router.location.pathname)

// Navigate to a different page
window.__nexudus.router.navigate('/bookings/meeting-rooms/list')

// Go back one step in browser history
window.__nexudus.router.navigate(-1)
```

***

## Examples

### Add Google Tag Manager

In `head.js`:

```json theme={null}
[
  {
    "type": "script",
    "src": "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX",
    "async": "true"
  }
]
```

### Show a banner only to customers without an active plan

In `app.js`:

```js theme={null}
const { isAuthenticated, coworker } = window.__nexudus.auth

if (isAuthenticated && coworker && !coworker.IsMember) {
  const banner = document.createElement('div')
  banner.style.cssText = 'background:#ff5100;color:#fff;text-align:center;padding:10px;'
  banner.textContent = 'You do not have an active plan. Browse our plans to get started.'
  document.body.prepend(banner)
}
```

### Redirect a specific setting-based feature flag

In `app.js`:

```js theme={null}
const featureEnabled = window.__nexudus.settings.getSetting('Website.EnableCustomFeature')

if (featureEnabled !== 'true') {
  window.__nexudus.router.navigate('/home')
}
```

### Run code on specific pages and react to navigation

The portal is a single-page application. `app.js` runs once on initial mount — it does not re-execute when members navigate between pages. To run code whenever the route changes, patch `history.pushState` and `history.replaceState` and listen for the `popstate` event (which fires on browser back/forward).

In `app.js`:

```js theme={null}
function onRouteChange(pathname) {
  // Runs on every client-side navigation and on initial load.

  if (pathname.includes('/meeting-rooms/')) {
    console.log('User is on a bookings page for meeting rooms')
  }
}

// Intercept pushState (links, router.navigate calls)
const _push = history.pushState.bind(history)
history.pushState = function (...args) {
  _push(...args)
  onRouteChange(window.location.pathname)
}

// Intercept replaceState (redirects, query-string updates)
const _replace = history.replaceState.bind(history)
history.replaceState = function (...args) {
  _replace(...args)
  onRouteChange(window.location.pathname)
}

// Handle browser back / forward
window.addEventListener('popstate', () => {
  onRouteChange(window.location.pathname)
})

// Run immediately for the page the user landed on
onRouteChange(window.location.pathname)
```

For query-string or hash information, read directly from `window.location` inside `onRouteChange` — `window.__nexudus.router.location` is a snapshot from portal mount and is not updated by client-side navigation.

```js theme={null}
function onRouteChange(pathname) {
  if (pathname.startsWith('/bookings') && window.location.search.includes('type=meeting-rooms')) {
    console.log('User is browsing meeting rooms')
  }
}
```

<Warning>
  Patching `history.pushState` affects the entire page. Keep the patched functions lightweight and always call the original (`_push` / `_replace`) before your own logic to avoid breaking portal navigation.
</Warning>

### React to changes in a `__nexudus` value

`window.__nexudus` is a snapshot from portal mount — its plain value properties (`auth.*`, `router.location`, etc.) are not automatically updated as the portal state changes. To react when a value changes, poll it with `setInterval` and compare against the previous reading.

The helper below wraps that pattern and returns a cancel function so you can stop watching when it is no longer needed.

In `app.js`:

```js theme={null}
// Watch a __nexudus property and call onChange whenever its value changes.
// Returns a cancel function.
function watchNexudus(getValue, onChange, intervalMs) {
  var prev = getValue()
  var id = setInterval(function () {
    var next = getValue()
    if (next !== prev) {
      onChange(next, prev)
      prev = next
    }
  }, intervalMs || 300)
  return function () { clearInterval(id) }
}

// React when the signed-in customer switches (e.g. an admin impersonates another profile)
var stopWatchingCustomer = watchNexudus(
  function () { return window.__nexudus.auth.coworker?.Email },
  function (newEmail, prevEmail) {
    console.log('Active customer changed from', prevEmail, 'to', newEmail)
    // update any third-party widgets that display the customer's identity
  }
)

// React when authentication state changes (e.g. token expires mid-session)
var stopWatchingAuth = watchNexudus(
  function () { return window.__nexudus.auth.isAuthenticated },
  function (isAuthenticated) {
    if (!isAuthenticated) {
      console.log('Session ended — redirecting to sign-in')
      window.__nexudus.router.navigate('/sign-in')
    }
  }
)

// Call the returned functions to stop polling when no longer needed.
// stopWatchingCustomer()
// stopWatchingAuth()
```

<Tip>
  `router.navigate` is a stable function and always works correctly regardless of when you call it. `auth.*` and `router.location` are snapshots, so use polling (above) or the `history.pushState` approach for navigation to observe their changes.
</Tip>

### Apply a CSS custom property from a business setting

In `app.js`:

```js theme={null}
const accent = window.__nexudus.settings.getSetting('PrimaryWebColor')
if (accent) {
  document.documentElement.style.setProperty('--nx-accent', accent)
}
```

In `styles.css`:

```css theme={null}
.button {
  background-color: var(--nx-accent, #ff5100);
}
```

### Load a custom Google Font

Use `head.js` to preconnect and load the font stylesheet, then apply it in `styles.css`.

In `head.js`:

```json theme={null}
[
  {
    "type": "link",
    "href": "https://fonts.googleapis.com",
    "rel": "preconnect"
  },
  {
    "type": "link",
    "href": "https://fonts.gstatic.com",
    "rel": "preconnect",
    "crossorigin": true
  },
  {
    "type": "link",
    "href": "https://fonts.googleapis.com/css2?family=Playwrite+NZ+Guides&display=swap",
    "rel": "stylesheet",
    "crossorigin": true
  }
]
```

In `styles.css`:

```css theme={null}
body * {
  font-family: "Playwrite NZ Guides", cursive;
  font-weight: 400;
  font-style: normal;
}
```
