Skip to content

CreatePage

shr0x edited this page Mar 25, 2026 · 1 revision

Create Page

This guide explains how to create a brand new CEF page in the framework, using the actual frontend architecture used by this project.

This includes:

  • creating the page component
  • optionally creating a MobX store
  • registering frontend events
  • registering the page in the frontend
  • adding the page to the client-side CEF page config
  • opening the page from the server
  • sending data to and from the page

Overview

In this project, a page is not just a React component.

A working page usually involves these parts:

  1. Frontend page component
  2. Optional frontend store
  3. EventManager handlers
  4. Frontend page registration
  5. Client-side page config (CEFPages)
  6. Server-side CEF emit/register

The flow usually looks like this:

sequenceDiagram
    participant S as Server
    participant C as Client Bridge
    participant F as Frontend
    participant P as Player

    S->>C: cef::system:setPage
    C->>F: system:setPage
    F->>P: Render page

    S->>C: cef::example:setData
    C->>F: example:setData
    F->>P: Update page state

    P->>F: Click button / submit action
    F->>C: emitServer("example", "submit")
    C->>S: server::example:submit
    S->>S: Validate & process
    S-->>C: cef::example:update
    C-->>F: example:update
    F-->>P: Refresh UI
Loading

Video Example

Watch the video

Click the image to watch the walkthrough.


How frontend page loading works in this project

Before creating a page, it is important to understand how pages are loaded.

1. All pages are auto-imported

Inside src/pages/index.ts:

import.meta.glob("./*/*.tsx", { eager: true });

This means every page inside:

src/pages/<folder>/<File>.tsx

is imported automatically.


2. Pages are registered through createComponent

Pages are not rendered directly.
They are registered into a component registry using:

createComponent({
  pageName: "auth",
  component: Authentication,
  props: {},
});

3. App renders the current page from the registry

Inside App.tsx, the current page is taken from PageContext:

const { page } = usePage();
const pageComponent = getRegisteredComponent(page);

and then rendered dynamically.


4. Frontend page switching is done through system:setPage

Inside App.tsx:

EventManager.addHandler("system", "setPage", handleSetPage);

So when the frontend receives:

system:setPage

the current page changes.


Step 1 — Create the page folder

Inside the frontend, create a new folder under src/pages.

Example:

src/pages/example/

Inside it, create:

src/pages/example/Example.tsx
src/pages/example/example.module.scss

You can change the naming style, but this matches the current project structure.


Step 2 — Create the page component

Create the page file:

import { FC } from "react";
import { observer } from "mobx-react-lite";
import { createComponent } from "src/hoc/registerComponent";
import { exampleStore } from "store/Example.store";
import EventManager from "utils/EventManager.util";
import style from "./example.module.scss";

interface ExampleProps {
  store: typeof exampleStore;
}

const Example: FC<ExampleProps> = observer(({ store }) => {
  return (
    <div className={style.main}>
      <div className={style.title}>Example Page</div>
      <div className={style.description}>{store.text}</div>

      <button
        onClick={() =>
          EventManager.emitServer("example", "submit", { value: "hello" })
        }
      >
        Send To Server
      </button>
    </div>
  );
});

export default createComponent({
  pageName: "example",
  component: Example,
  props: { store: exampleStore },
});

Step 3 — Create the page store (optional but recommended)

If the page has state, create a store.

Create:

src/stores/Example.store.ts

Example:

import { makeAutoObservable } from "mobx";
import EventManager from "utils/EventManager.util";

class _ExampleStore {
  text = "Default text";

  constructor() {
    makeAutoObservable(this);
    this.createEvents();
  }

  setText(value: string) {
    this.text = value;
  }

  createEvents() {
    EventManager.addHandler("example", "setData", (data: { text: string }) => {
      this.setText(data.text);
    });

    EventManager.addHandler("example", "update", (data: { text: string }) => {
      this.setText(data.text);
    });

    EventManager.stopAddingHandler("example");
  }
}

export const exampleStore = new _ExampleStore();

Step 4 — Understand how EventManager connects the page

In this project, frontend events are handled through EventManager.

Example:

EventManager.addHandler("example", "setData", (data) => {
  // update store
});

This registers an event for:

example:setData

When CEF receives:

example:setData

it will call the matching handler.

This works because window.callHandler in src/index.tsx forwards everything into:

EventManager.callHandler(event, ...args);

Step 5 — Add page styling

Example example.module.scss:

.main {
  width: 100vw;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  color: white;
}

.title {
  font-size: 32px;
  margin-bottom: 12px;
}

.description {
  font-size: 18px;
  margin-bottom: 20px;
}

This step is optional, but most pages in the project use a local SCSS module.


Step 6 — Add the page to the client-side CEF page config

Creating the frontend page is not enough.

You also need to define the page in the client-side CEFPages configuration so the browser knows how the page should behave.

Add your new page inside:

source/client/assets/CEFPages.asset.ts

Example:

example: {
    blur: true,
    controls: true,
    freezeCamera: true,
    radar: false
}

These settings control things like:

  • blur
  • radar visibility
  • controls
  • camera freeze
  • cursor handling

Without this, opening the page properly from the client side will not work as expected.


Step 7 — Open the page from the server

To switch to your new page, use:

RAGERP.cef.emit(player, "system", "setPage", "example");

Why this works:

  • server emits cef::system:setPage
  • client bridge receives it
  • browser starts the page
  • frontend App.tsx receives system:setPage
  • PageContext switches current page to "example"

This is the page-opening event in this framework.


Step 8 — Send page data from the server

After opening the page, send the initial data:

RAGERP.cef.emit(player, "example", "setData", {
  text: "Hello from server",
});

This becomes:

cef::example:setData

The client forwards it to the frontend, and the store handler receives it.


Step 9 — Send actions back to the server

From the frontend, use:

EventManager.emitServer("example", "submit", {
  value: "hello",
});

This becomes:

server::example:submit

The client forwards this event to the server.


Step 10 — Register the page event on the server

On the server, register the incoming event:

RAGERP.cef.register("example", "submit", (player, data) => {
  console.log(data);

  RAGERP.cef.emit(player, "example", "update", {
    text: `Server received: ${data.value}`,
  });
});

This gives you the full loop:

  • open page
  • send initial page data
  • user interacts
  • frontend emits to server
  • server validates/processes
  • server sends updated data back

Minimal working example

Frontend component

import { FC } from "react";
import { observer } from "mobx-react-lite";
import { createComponent } from "src/hoc/registerComponent";
import { exampleStore } from "store/Example.store";
import EventManager from "utils/EventManager.util";

const Example: FC<{ store: typeof exampleStore }> = observer(({ store }) => {
  return (
    <div>
      <h1>{store.text}</h1>
      <button
        onClick={() =>
          EventManager.emitServer("example", "submit", { value: "hello" })
        }
      >
        Send
      </button>
    </div>
  );
});

export default createComponent({
  pageName: "example",
  component: Example,
  props: { store: exampleStore },
});

Store

import { makeAutoObservable } from "mobx";
import EventManager from "utils/EventManager.util";

class _ExampleStore {
  text = "Waiting for data...";

  constructor() {
    makeAutoObservable(this);
    this.createEvents();
  }

  setText(text: string) {
    this.text = text;
  }

  createEvents() {
    EventManager.addHandler("example", "setData", (data: { text: string }) => {
      this.setText(data.text);
    });

    EventManager.addHandler("example", "update", (data: { text: string }) => {
      this.setText(data.text);
    });

    EventManager.stopAddingHandler("example");
  }
}

export const exampleStore = new _ExampleStore();

Server

RAGERP.cef.emit(player, "system", "setPage", "example");

RAGERP.cef.emit(player, "example", "setData", {
  text: "Hello from server",
});

RAGERP.cef.register("example", "submit", (player, data) => {
  RAGERP.cef.emit(player, "example", "update", {
    text: `You sent: ${data.value}`,
  });
});

How to decide whether you need a store

You do not always need a store.

Use a store when:

  • page has reusable state
  • multiple components depend on the same data
  • page receives multiple updates from server
  • page logic should stay outside the component

Skip the store when:

  • page is very small
  • local state is enough
  • page is mostly static

A store is recommended for bigger pages, but not mandatory.


How this differs from a normal React page

This project does not use a typical route-based frontend.

A page here is:

  • registered into a component registry
  • opened through system:setPage
  • updated through EventManager events
  • connected to the game through the client bridge

So creating a new page means wiring it into the framework event flow, not just creating a React component.


Common mistakes

1. Creating the component but forgetting createComponent

If you do not export the page using:

createComponent(...)

the page will never be registered.


2. Forgetting system:setPage

Sending page data alone will not switch the rendered page.

You must open it with:

RAGERP.cef.emit(player, "system", "setPage", "example");

3. Forgetting the client-side CEFPages entry

If the page does not exist inside the client-side page config, browser-related behavior will be missing or incorrect.


4. Using frontend state for server authority

Frontend should not:

  • validate gameplay rules
  • change persistent data
  • become the source of truth

The server must remain authoritative.


5. Mixing too much logic into the component

Try to keep:

  • rendering in the component
  • state in the store
  • validation on the server

Recommended structure

Example final structure:

src/pages/example/
├── Example.tsx
└── example.module.scss

src/stores/
└── Example.store.ts

And outside the frontend:

source/client/assets/CEFPages.asset.ts
source/server/... your CEF emit/register logic

Summary

To create a new page in this framework:

  1. create the page folder and component
  2. register it with createComponent
  3. optionally create a store
  4. register frontend handlers with EventManager
  5. add the page to client-side CEFPages
  6. open it using RAGERP.cef.emit(player, "system", "setPage", pageName)
  7. send page data with RAGERP.cef.emit(player, page, pointer, data)
  8. receive frontend actions with RAGERP.cef.register(page, pointer, handler)

That is the full page creation flow used by this project.

🚀 Getting Started

🧠 Architecture

🎮 Features

🛠 Development

❗ Help

Clone this wiki locally