diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz deleted file mode 100644 index 1f695e1..0000000 Binary files a/.yarn/install-state.gz and /dev/null differ diff --git a/README.md b/README.md index cb7fda6..7fbde25 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,112 @@ -### About +# Hellotext Integration Playground -This is a dummy React application that is meant to be a quick way for businesses -to get started with tracking events via [Hellotext.js](https://github.com/hellotext/hellotext.js). +A React application for quickly verifying the public [Hellotext JavaScript SDK](https://github.com/hellotext/hellotext.js) (`@hellotext/hellotext` npm package). Designed for customers and internal QA to test tracking events, forms, webchat, UTM capture, and session management — **without editing source code**. -### Getting started +## Prerequisites -1. Replace the `Hellotext.initialize` with your public business ID. -2. Write your code to test behaviour +- **Node.js** 16+ (LTS recommended) +- **Yarn** (the project uses Yarn 4 via Corepack, but `npm` works too) + +## Getting Started + +### 1. Install dependencies + +```bash +yarn install +``` + +### 2. Start the dev server + +```bash +yarn start +``` + +Opens [http://localhost:3000](http://localhost:3000) in your browser. + +### 3. Enter a Business ID + +The first screen asks for your **public Hellotext Business ID** (found in your Hellotext dashboard → Settings → Business). Enter it and click **Initialize SDK**. The ID is saved in localStorage for convenience — use the **Reset** button to change it. + +Optionally enter a **Webchat ID** to override dashboard-managed webchat settings. + +--- + +## Features + +### Session +Displays the current `Hellotext.session` token and listens for the `session-set` event. + +### UTM Capture +Shows how the SDK captures UTM parameters from the URL. Click the provided test link to reload with sample UTM params (`utm_source`, `utm_medium`, `utm_campaign`, etc.) and observe the `utm-set` event. + +**Test URL example:** +``` +http://localhost:3000/?utm_source=test&utm_medium=playground&utm_campaign=demo +``` + +### Tracking Events +Fire preset events (`product.viewed`, `cart.added`, `checkout.started`) or enter a custom event name + JSON params. Each call shows success/failure in the Event Log. + +### Forms +Enter a **Form ID** and click **Mount Form** to render a `
` element. The SDK discovers it and mounts the form. Listens for `forms:collected` and `form:completed` events. + +### Webchat +If webchat is configured for your business, the widget appears automatically. The panel listens for: +- `webchat:mounted` +- `webchat:opened` / `webchat:closed` +- `webchat:message:sent` / `webchat:message:received` + +All events appear in the shared **Event Log**. + +--- + +## Event Log + +A persistent log panel shared across all tabs. Each entry includes: +- Event/action name +- Timestamp (down to milliseconds) +- Status badge (`success` / `error` / `info`) +- Payload or error details + +Click **Clear** to reset the log. + +--- + +## Testing + +### Run tests + +```bash +yarn test --watchAll=false +``` + +Tests mock the `@hellotext/hellotext` SDK and verify: +- Setup screen renders correctly +- Initialize button behavior +- localStorage persistence +- Dashboard rendering after init +- Tab switching +- Event log visibility + +### Production build + +```bash +yarn build +``` + +Outputs to `build/`. Can be served with any static file server. + +--- + +## Tech Stack + +- **React 18** (Create React App) +- **@hellotext/hellotext** `^2.4.0` (public npm package) +- **Vanilla CSS** with CSS custom properties +- No additional UI frameworks or state management libraries + +## Notes + +- No real Business ID is hardcoded in the source. All configuration happens at runtime. +- The SDK stylesheet (`@hellotext/hellotext/styles/index.css`) is imported so forms and webchat render with expected styles. +- Only the public SDK API is used — no private Hellotext application internals. diff --git a/docs/USER_MANUAL.md b/docs/USER_MANUAL.md new file mode 100644 index 0000000..2c2bf1e --- /dev/null +++ b/docs/USER_MANUAL.md @@ -0,0 +1,692 @@ +# Hellotext Integration Playground — User Manual + +> **Audience:** QA testers, customers, and developers integrating the Hellotext JavaScript SDK. +> **SDK package:** [`@hellotext/hellotext`](https://www.npmjs.com/package/@hellotext/hellotext) (public npm) + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [Prerequisites](#2-prerequisites) +3. [Installation & Setup](#3-installation--setup) +4. [Getting Started — The Setup Screen](#4-getting-started--the-setup-screen) +5. [Dashboard Overview](#5-dashboard-overview) +6. [Session Panel](#6-session-panel) +7. [UTM Capture Panel](#7-utm-capture-panel) +8. [Tracking Events Panel](#8-tracking-events-panel) +9. [Forms Panel](#9-forms-panel) +10. [Webchat Panel](#10-webchat-panel) +11. [Event Log](#11-event-log) +12. [Resetting & Switching Business](#12-resetting--switching-business) +13. [Running Tests](#13-running-tests) +14. [Production Build](#14-production-build) +15. [Troubleshooting](#15-troubleshooting) +16. [Architecture Reference](#16-architecture-reference) + +--- + +## 1. Overview + +The **Hellotext Integration Playground** is a standalone React application that lets you test and verify the public [Hellotext JavaScript SDK](https://github.com/hellotext/hellotext.js) without modifying source code or accessing the main Hellotext codebase. + +### What you can do + +| Feature | Description | +|---------|-------------| +| **Session management** | Observe how the SDK creates and persists session tokens | +| **UTM capture** | Verify automatic UTM parameter extraction from URLs | +| **Event tracking** | Fire preset and custom tracking events and inspect responses | +| **Forms** | Mount Hellotext forms by ID and observe lifecycle events | +| **Webchat** | Initialize webchat and monitor real-time message events | +| **Event log** | Inspect every SDK event and API response in a shared log panel | + +### What this app does NOT do + +- It does **not** expose any private Hellotext internals. +- It does **not** hardcode any real Business ID — all configuration happens at runtime. +- It uses **only** the public `@hellotext/hellotext` npm package and its documented API. + +--- + +## 2. Prerequisites + +| Requirement | Minimum Version | +|-------------|----------------| +| **Node.js** | 16+ (LTS recommended) | +| **Yarn** | 4.x (bundled via Corepack) or `npm` as an alternative | +| **Browser** | Any modern browser (Chrome, Firefox, Safari, Edge) | + +You also need a **Hellotext Business ID** (public). You can find it in: + +> **Hellotext Dashboard → Settings → Business → Business ID** + +Optionally, you may also need: +- A **Webchat ID** — if you want to override the dashboard webchat configuration. +- A **Form ID** — to test a specific Hellotext form. + +--- + +## 3. Installation & Setup + +### Step 1 — Clone the repository + +```bash +git clone +cd hellotext-react-test +``` + +### Step 2 — Install dependencies + +```bash +yarn install +``` + +> **Using npm?** Run `npm install` instead. Both package managers work. + +### Step 3 — Start the development server + +```bash +yarn start +``` + +The app opens automatically at **http://localhost:3000**. + +### Step 4 — Verify it works + +You should see the **Setup Screen** with the Hellotext logo, a Business ID input field, and an "Initialize SDK" button. + +--- + +## 4. Getting Started — The Setup Screen + +When you first open the app, you'll see the **Setup Screen**. This is where you configure the SDK before using the playground. + +### Fields + +| Field | Required | Description | +|-------|----------|-------------| +| **Business ID** | ✅ Yes | Your public Hellotext Business ID. The "Initialize SDK" button stays disabled until you enter one. | +| **Webchat ID** | ❌ No | Optional. Override the webchat configuration set in your Hellotext dashboard. Leave empty to use dashboard defaults. | + +### How to initialize + +1. **Enter your Business ID** in the first input field. +2. *(Optional)* Enter a **Webchat ID** in the second field. +3. Click **Initialize SDK**. + +### What happens behind the scenes + +When you click "Initialize SDK", the app calls: + +```js +Hellotext.initialize(businessId, config) +``` + +Where `config` includes `{ webchat: { id: webchatId } }` only if you provided a Webchat ID. + +### Local storage persistence + +- Your Business ID is saved to `localStorage` under the key `ht_business_id`. +- Your Webchat ID (if provided) is saved under `ht_webchat_id`. +- On subsequent visits, the fields are pre-filled with your last values. +- Use the **Reset** button (in the dashboard) to clear stored values. + +### Error handling + +If initialization fails (e.g., invalid Business ID, network error), an error entry appears in the Event Log with the error message. The app does **not** navigate to the dashboard on failure. + +--- + +## 5. Dashboard Overview + +After successful initialization, you're taken to the **Dashboard**. It has three main areas: + +``` +┌──────────────────────────────────────────────────┬──────────────┐ +│ Header (Logo + Business ID badge + Reset btn) │ │ +├──────────────────────────────────────────────────┤ │ +│ Tab Navigation │ │ +│ [Session] [UTM] [Tracking] [Forms] [Webchat] │ Event Log │ +├──────────────────────────────────────────────────┤ Panel │ +│ │ │ +│ Active Panel Content │ │ +│ (changes based on selected tab) │ │ +│ │ │ +│ │ │ +└──────────────────────────────────────────────────┴──────────────┘ +``` + +### Header + +- **Hellotext logo** — the brand logomark. +- **Business ID badge** — shows the ID you initialized with (displayed as a pill/badge). +- **Reset button** — clears local storage and reloads the app back to the Setup Screen. + +### Tabs + +Click any tab to switch between the five feature panels. The active tab is underlined. + +### Event Log + +A persistent panel on the right side of the screen (or bottom on mobile). It's shared across all tabs — events from any panel appear here. + +--- + +## 6. Session Panel + +**Tab:** `Session` + +### Purpose + +Verify that the SDK correctly creates, stores, and exposes a session token. + +### What you see + +| Section | Description | +|---------|-------------| +| **Current Session** | Displays the value of `Hellotext.session`. If no session has been assigned yet, shows a waiting message. | +| **How it works** | Reference showing the SDK API calls used. | + +### How to test + +1. Click the **Session** tab (selected by default after initialization). +2. The **Current Session** field should display a session token (a UUID-like string). +3. Check the **Event Log** — you should see a `session-set` event logged with status `info`. + +### Expected behavior + +- The SDK assigns a session token automatically on initialization. +- The token is stored in cookies and reused across page reloads. +- The `session-set` event fires when the session is created or loaded from cookies. + +### What to check (QA) + +- [ ] Session token appears after initialization. +- [ ] `session-set` event is logged. +- [ ] Reloading the page preserves the same session token (cookie persistence). +- [ ] The token format looks like a valid UUID. + +--- + +## 7. UTM Capture Panel + +**Tab:** `UTM` + +### Purpose + +Verify that the SDK automatically captures UTM parameters from the browser URL and fires the `utm-set` event. + +### What you see + +| Section | Description | +|---------|-------------| +| **Test URL** | A pre-built URL with sample UTM parameters you can click to test. | +| **Open with UTM params** | Button that navigates to the test URL, re-initializing the app with UTM parameters. | +| **Captured UTM Data** | A table showing each captured UTM parameter and its value. Empty state shown if no UTM data. | + +### How to test + +#### Option A — Use the built-in test link + +1. Navigate to the **UTM** tab. +2. Click the **"Open with UTM params"** button. +3. The page reloads with URL parameters: + ``` + http://localhost:3000/?utm_source=test&utm_medium=playground&utm_campaign=demo&utm_term=sdk&utm_content=v1 + ``` +4. Re-enter your Business ID and initialize again (or it auto-fills from local storage). +5. Go to the **UTM** tab — you should see the captured data in the table. + +#### Option B — Manually add UTM params + +1. Manually edit the browser URL to add any combination of UTM parameters: + ``` + http://localhost:3000/?utm_source=google&utm_medium=cpc&utm_campaign=spring_sale + ``` +2. Press Enter to reload. +3. Initialize the SDK and check the UTM tab. + +### Supported UTM parameters + +| Parameter | Example | +|-----------|---------| +| `utm_source` | `google`, `newsletter`, `test` | +| `utm_medium` | `cpc`, `email`, `playground` | +| `utm_campaign` | `spring_sale`, `demo` | +| `utm_term` | `sdk`, `hellotext` | +| `utm_content` | `v1`, `banner_ad` | + +### What to check (QA) + +- [ ] `utm-set` event fires and appears in the Event Log. +- [ ] The UTM table displays all captured parameters correctly. +- [ ] Missing UTM parameters are not shown (only present ones appear). +- [ ] Without UTM params in the URL, the empty state message displays. + +--- + +## 8. Tracking Events Panel + +**Tab:** `Tracking` + +### Purpose + +Test the `Hellotext.track(eventName, params)` API by firing preset and custom tracking events. + +### What you see + +| Section | Description | +|---------|-------------| +| **Preset Events** | Three buttons for common events with pre-filled payloads. | +| **Custom Event** | Input fields to enter any event name and JSON parameters. | + +### Preset events + +| Button | Event Name | Sample Payload | +|--------|-----------|----------------| +| `product.viewed` | `product.viewed` | `{ product_id: "prod_demo_001", name: "Demo Product", price: 29.99 }` | +| `cart.added` | `cart.added` | `{ product_id: "prod_demo_001", quantity: 1, price: 29.99 }` | +| `checkout.started` | `checkout.started` | `{ total: 29.99, currency: "USD", items_count: 1 }` | + +### How to test preset events + +1. Navigate to the **Tracking** tab. +2. Click any preset event button (e.g., **`product.viewed`**). +3. The button shows "Sending…" while the request is in flight. +4. Check the **Event Log** for two entries: + - An `info` entry showing the event was fired (with the payload). + - A `success` or `error` entry showing the API response. + +### How to test custom events + +1. In the **Custom Event** section, enter an **Event Name** (e.g., `page.viewed`). +2. Enter **Parameters** as valid JSON (e.g., `{"page": "/pricing", "referrer": "google"}`). +3. Click **Fire Custom Event**. +4. Check the Event Log for the result. + +### Error scenarios + +| Scenario | Expected Behavior | +|----------|-------------------| +| Invalid JSON in params | An `error` entry appears in the log: "Invalid JSON in params field" | +| Empty event name | The "Fire Custom Event" button is disabled | +| Network error | An `error` entry appears with the error message | +| Invalid event name | The API may return an error — check the log payload | + +### What to check (QA) + +- [ ] Each preset button fires the correct event and shows success/error. +- [ ] Custom events with valid JSON work correctly. +- [ ] Invalid JSON is caught and reported in the log (not a crash). +- [ ] The button disables while a request is in flight ("Sending…" state). +- [ ] Multiple events can be fired in sequence without issues. + +--- + +## 9. Forms Panel + +**Tab:** `Forms` + +### Purpose + +Verify that the SDK correctly discovers and mounts Hellotext forms using the `data-hello-form` attribute. + +### What you see + +| Section | Description | +|---------|-------------| +| **Mount a Form** | Input for a Form ID and a Mount/Unmount button. | +| **Form mount area** | A dashed-border container where the form will render. | +| **Events** | Reference showing the SDK events related to forms. | + +### How to test + +1. Navigate to the **Forms** tab. +2. Enter a valid **Form ID** from your Hellotext dashboard. + > Find this in: **Hellotext Dashboard → Forms → select a form → Form ID** +3. Click **Mount Form**. +4. The mount area changes from a dashed placeholder to a solid border. +5. The SDK discovers the `
` element and mounts the form inside it. + +### What happens behind the scenes + +When you click "Mount Form", the app renders: + +```html +
+``` + +The SDK's internal `MutationObserver` detects this element and fetches + renders the form. + +### Form lifecycle events + +| Event | When it fires | +|-------|--------------| +| `forms:collected` | The SDK discovers form placeholder(s) on the page | +| `form:completed` | A user fills out the form and completes OTP verification | + +Both events appear in the shared Event Log. + +### How to unmount + +Click the **Unmount** button (red) to remove the form from the DOM. An `form:unmount` info entry is logged. + +### What to check (QA) + +- [ ] Entering a valid Form ID and clicking "Mount Form" renders the form. +- [ ] The `forms:collected` event fires and appears in the log. +- [ ] Filling out the form and completing OTP triggers `form:completed`. +- [ ] The form renders with correct Hellotext styling (imported via `@hellotext/hellotext/styles/index.css`). +- [ ] Unmounting removes the form from the page cleanly. +- [ ] Mounting a different form after unmounting works correctly. +- [ ] An invalid Form ID shows appropriate SDK behavior (empty mount area or error). + +--- + +## 10. Webchat Panel + +**Tab:** `Webchat` + +### Purpose + +Verify that the Hellotext webchat widget initializes and emits events correctly. + +### What you see + +| Section | Description | +|---------|-------------| +| **Status** | Description of whether webchat should appear and where to look. | +| **Monitored Events** | List of all webchat events being listened to. | +| **Configuration** | Shows the exact `Hellotext.initialize()` call that was used, including webchat config. | + +### How webchat initialization works + +Webchat is configured **at initialization time** (on the Setup Screen), not from this panel. There are two modes: + +| Mode | How to use | +|------|-----------| +| **Dashboard defaults** | Leave the Webchat ID field empty on the Setup Screen. The SDK uses whatever webchat settings are configured in your Hellotext dashboard. | +| **Explicit override** | Enter a specific Webchat ID on the Setup Screen. The SDK initializes webchat with `{ webchat: { id: "YOUR_ID" } }`. | + +### Where the webchat widget appears + +If webchat is properly configured for your business, a **chat bubble** appears in the **bottom-right corner** of the page. This is the standard Hellotext webchat widget. + +### How to test + +1. **Before initializing**, decide whether to enter a Webchat ID on the Setup Screen. +2. Initialize the SDK. +3. Navigate to the **Webchat** tab. +4. Look for the chat bubble in the bottom-right corner of the page. +5. Click the bubble to open the webchat. +6. Send a message. +7. Check the **Event Log** for webchat events. + +### Monitored events + +| Event | When it fires | +|-------|--------------| +| `webchat:mounted` | The webchat widget has been loaded and rendered on the page | +| `webchat:opened` | The user opens the webchat window | +| `webchat:closed` | The user closes the webchat window | +| `webchat:message:sent` | The user sends a message through webchat | +| `webchat:message:received` | A response message is received in webchat | + +### What to check (QA) + +- [ ] Webchat bubble appears when configured for the business. +- [ ] `webchat:mounted` event fires and appears in the log. +- [ ] Opening the chat triggers `webchat:opened`. +- [ ] Closing the chat triggers `webchat:closed`. +- [ ] Sending a message triggers `webchat:message:sent`. +- [ ] Receiving a reply triggers `webchat:message:received`. +- [ ] With no webchat configured, no bubble appears (and no errors in the console). +- [ ] Providing a Webchat ID override on the Setup Screen works correctly. + +--- + +## 11. Event Log + +The **Event Log** is the shared observation panel on the right side of the dashboard. It captures every SDK event and user action across all tabs. + +### Log entry anatomy + +Each entry in the log contains: + +``` +┌──────────────────────────────────────────────────┐ +│ [STATUS] event.name HH:MM:SS.mmm │ +│ { │ +│ "payload": "details here" │ +│ } │ +└──────────────────────────────────────────────────┘ +``` + +| Field | Description | +|-------|-------------| +| **Status badge** | Color-coded: `success` (green), `error` (red), `info` (blue) | +| **Event name** | The SDK event name or action performed (e.g., `session-set`, `track: product.viewed`) | +| **Timestamp** | Precise time down to milliseconds (HH:MM:SS.mmm format, 24-hour) | +| **Payload** | The event data, API response, or error message (formatted as JSON) | + +### Controls + +| Action | How | +|--------|-----| +| **Clear all entries** | Click the **Clear** button in the log header | +| **Scroll** | The log auto-scrolls to the newest entry. You can scroll up to review older entries. | +| **Entry count** | Shown next to "Event Log" as `(N)` | + +### Events you'll see + +| Source | Events | +|--------|--------| +| **Initialization** | `Hellotext.initialize` (success or error) | +| **Session** | `session-set` | +| **UTM** | `utm-set` | +| **Tracking** | `track: ` (info + success/error) | +| **Forms** | `form:mount`, `form:unmount`, `forms:collected`, `form:completed` | +| **Webchat** | `webchat:mounted`, `webchat:opened`, `webchat:closed`, `webchat:message:sent`, `webchat:message:received` | + +--- + +## 12. Resetting & Switching Business + +### How to reset + +1. Click the **Reset** button in the top-right corner of the dashboard header. +2. This clears: + - `ht_business_id` from localStorage + - `ht_webchat_id` from localStorage +3. The page reloads and returns to the Setup Screen. + +### Switching to a different Business ID + +1. Click **Reset**. +2. Enter the new Business ID on the Setup Screen. +3. Click **Initialize SDK**. + +> **Note:** Resetting performs a full page reload. This clears the SDK state, session cookies for the previous business, and all log entries. + +--- + +## 13. Running Tests + +The project includes automated tests that verify the core playground behavior. + +### Run the test suite + +```bash +yarn test --watchAll=false +``` + +### What the tests cover + +| Test | What it verifies | +|------|-----------------| +| Setup screen renders | Business ID input and Initialize button are present | +| Button disabled state | Initialize button is disabled when Business ID is empty | +| localStorage persistence | Business ID is saved after initialization | +| SDK initialization call | `Hellotext.initialize()` is called with correct arguments | +| Dashboard renders | Tab navigation and all tabs appear after init | +| Business ID display | The entered ID appears in the dashboard header | +| Event log | The log panel is visible and shows the initialization entry | +| Tab switching | Clicking tabs shows the correct panel content | + +### Watch mode (development) + +```bash +yarn test +``` + +This starts Jest in watch mode — tests re-run automatically when files change. + +--- + +## 14. Production Build + +### Create a production build + +```bash +yarn build +``` + +Output goes to the `build/` directory. This is a static bundle that can be deployed to any static hosting service. + +### Serve the production build locally + +```bash +npx serve -s build +``` + +--- + +## 15. Troubleshooting + +### The Setup Screen won't initialize + +| Symptom | Possible Cause | Solution | +|---------|---------------|----------| +| Button stays disabled | Empty Business ID field | Enter a valid Business ID | +| Error in Event Log after clicking Initialize | Invalid Business ID or network issue | Check the ID, verify network connectivity, check browser console | +| Nothing happens after clicking Initialize | JavaScript error | Open browser DevTools (F12) → Console for error details | + +### Session not appearing + +| Symptom | Possible Cause | Solution | +|---------|---------------|----------| +| "No session yet" message persists | SDK is still initializing | Wait a moment — the SDK makes an async network request | +| Session token never appears | Invalid Business ID | Reset and use a valid Business ID | +| Session changes on every reload | Cookies blocked | Check browser cookie settings, ensure localhost cookies are allowed | + +### UTM parameters not captured + +| Symptom | Possible Cause | Solution | +|---------|---------------|----------| +| UTM table is empty | No UTM params in URL | Add `?utm_source=test` to the URL and reload | +| UTM params in URL but not captured | SDK initialized before URL was parsed | Reload the page with UTM params already in the URL, then initialize | + +### Tracking events fail + +| Symptom | Possible Cause | Solution | +|---------|---------------|----------| +| Error status in log | Invalid Business ID or event name | Verify your Business ID is correct | +| "Invalid JSON in params field" | Malformed JSON in custom params | Ensure valid JSON syntax (use double quotes, no trailing commas) | +| Network error | API connectivity issue | Check network tab in DevTools | + +### Forms not rendering + +| Symptom | Possible Cause | Solution | +|---------|---------------|----------| +| Mount area stays empty | Invalid Form ID | Verify the Form ID in your Hellotext dashboard | +| Form appears unstyled | SDK styles not loaded | Verify `@hellotext/hellotext/styles/index.css` is imported in `index.js` | +| `forms:collected` doesn't fire | SDK hasn't discovered the DOM element yet | The SDK uses MutationObserver — give it a moment after mounting | + +### Webchat not appearing + +| Symptom | Possible Cause | Solution | +|---------|---------------|----------| +| No chat bubble | Webchat not configured for your business | Enable webchat in your Hellotext dashboard | +| Bubble appears but events don't log | Events fire before switching to Webchat tab | Check the Event Log — events are shared across tabs | +| Webchat override not working | Webchat ID entered incorrectly | Reset, verify the ID, and re-initialize | + +--- + +## 16. Architecture Reference + +### Tech Stack + +| Technology | Purpose | +|-----------|---------| +| **React 18** | UI framework (Create React App) | +| **@hellotext/hellotext** `^2.4.0` | Public Hellotext JavaScript SDK | +| **Vanilla CSS** | Styling with CSS custom properties | +| **Jest + React Testing Library** | Automated testing | + +### Project Structure + +``` +hellotext-react-test/ +├── public/ +│ └── index.html # HTML template with Google Fonts +├── src/ +│ ├── index.js # Entry point, imports SDK stylesheet +│ ├── index.css # Design tokens (colors, spacing, fonts) +│ ├── App.js # Root component, state management +│ ├── App.css # Layout and component styles +│ ├── App.test.js # Automated tests +│ ├── setupTests.js # Jest configuration +│ └── components/ +│ ├── HellotextLogo.js # SVG logo component +│ ├── SetupScreen.js # Business ID entry form +│ ├── Dashboard.js # Tab navigation + layout +│ ├── SessionPanel.js # Session display + events +│ ├── UtmPanel.js # UTM capture display +│ ├── TrackingPanel.js # Event tracking controls +│ ├── FormsPanel.js # Form mounting controls +│ ├── WebchatPanel.js # Webchat event monitoring +│ └── EventLog.js # Shared log panel +├── docs/ +│ └── USER_MANUAL.md # This document +├── package.json +└── README.md # Quick-start guide +``` + +### Data Flow + +``` +User Input (Business ID) + │ + ▼ + SetupScreen.js + │ + ▼ + App.js ─── Hellotext.initialize(businessId, config) + │ + ▼ + Dashboard.js (tab navigation) + │ + ├── SessionPanel ──── Hellotext.on('session-set') + ├── UtmPanel ──────── Hellotext.on('utm-set') + ├── TrackingPanel ──── Hellotext.track(event, params) + ├── FormsPanel ──────
+ └── WebchatPanel ──── Hellotext.on('webchat:*') + │ + ▼ + EventLog.js (shared log, receives addLog() from all panels) +``` + +### Key SDK APIs Used + +| API | Used in | Purpose | +|-----|---------|---------| +| `Hellotext.initialize(id, config)` | App.js | Initialize the SDK with a Business ID | +| `Hellotext.session` | SessionPanel.js | Read the current session token | +| `Hellotext.track(event, params)` | TrackingPanel.js | Fire tracking events | +| `Hellotext.on(event, callback)` | All panels | Subscribe to SDK events | +| `Hellotext.removeEventListener(event, cb)` | All panels | Unsubscribe on component unmount | + +--- + +*Last updated: June 2026* diff --git a/package.json b/package.json index b4fe89d..4ea74f9 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,15 @@ { - "name": "my-app", - "version": "0.1.0", + "name": "hellotext-playground", + "version": "1.0.0", "private": true, "dependencies": { - "@hellotext/hellotext": "git://github.com/hellotext/hellotext.js#b8e3f92d8ab6f3ac3fef194db0d35cf1389e4576", + "@hellotext/hellotext": "^2.4.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-intl": "^10.1.13", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, @@ -38,6 +39,6 @@ }, "packageManager": "yarn@4.2.2", "devDependencies": { - "whatwg-fetch": "^3.6.20" + "@babel/plugin-proposal-private-property-in-object": "^7.21.11" } } diff --git a/public/favicon.ico b/public/favicon.ico index a11777c..f2f8b50 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html index aa069f2..50f78b3 100644 --- a/public/index.html +++ b/public/index.html @@ -4,40 +4,23 @@ - + - - - React App + + + + Hellotext Integration Playground
- diff --git a/public/logo192.png b/public/logo192.png index fc44b0a..4de1f65 100644 Binary files a/public/logo192.png and b/public/logo192.png differ diff --git a/public/logo512.png b/public/logo512.png index a4e47a6..4de1f65 100644 Binary files a/public/logo512.png and b/public/logo512.png differ diff --git a/src/App.css b/src/App.css index 440ecac..a3bfa67 100644 --- a/src/App.css +++ b/src/App.css @@ -1,39 +1,536 @@ -.App { - text-align: center; - position: relative; +/* ============================================================ + Hellotext Integration Playground — Layout & Components + ============================================================ */ + +/* --- Setup Screen --- */ +.setup-screen { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #fff7f4 0%, #f9fafb 40%, #f3e8ff 100%); + padding: var(--space-lg); } -.App-logo { - height: 40vmin; - pointer-events: none; +.setup-card { + background: var(--color-surface); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + padding: var(--space-xl) var(--space-xl) var(--space-lg); + max-width: 460px; + width: 100%; + animation: fadeInUp 0.4s ease-out; } -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} + +.setup-card__logo { + color: var(--color-accent); + margin-bottom: var(--space-sm); + line-height: 1; +} + +.setup-card__subtitle { + color: var(--color-text-secondary); + margin-bottom: var(--space-lg); + font-size: 14px; +} + +/* --- Form fields --- */ +.field { + margin-bottom: var(--space-md); +} + +.field label { + display: block; + font-weight: 600; + font-size: 13px; + margin-bottom: var(--space-xs); + color: var(--color-text); +} + +.field label .optional { + font-weight: 400; + color: var(--color-text-muted); + font-size: 12px; +} + +.field input, +.field textarea { + width: 100%; + padding: 10px 12px; + border: 1.5px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: 14px; + color: var(--color-text); + background: var(--color-surface); + transition: border-color 0.15s, box-shadow 0.15s; + outline: none; +} + +.field input:focus, +.field textarea:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-light); +} + +.field input::placeholder, +.field textarea::placeholder { + color: var(--color-text-muted); +} + +.field textarea { + resize: vertical; + min-height: 80px; + font-family: var(--font-mono); + font-size: 13px; +} + +.field .hint { + font-size: 12px; + color: var(--color-text-muted); + margin-top: var(--space-xs); +} + +/* --- Buttons --- */ +.btn { + display: inline-flex; + align-items: center; + gap: var(--space-sm); + padding: 10px 20px; + border: none; + border-radius: var(--radius-sm); + font-size: 14px; + font-weight: 600; + transition: all 0.15s; +} + +.btn--primary { + background: var(--color-primary); + color: #fff; +} + +.btn--primary:hover { + background: var(--color-primary-hover); + box-shadow: var(--shadow-md); +} + +.btn--primary:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn--secondary { + background: var(--color-bg); + color: var(--color-text); + border: 1.5px solid var(--color-border); +} + +.btn--secondary:hover { + border-color: var(--color-border-hover); + background: var(--color-surface); +} + +.btn--small { + padding: 6px 12px; + font-size: 12px; +} + +.btn--danger { + background: var(--color-error); + color: #fff; +} + +.btn--danger:hover { + background: #dc2626; +} + +.btn--full { + width: 100%; + justify-content: center; } -.App-header { - background-color: #282c34; +/* --- Dashboard Layout --- */ +.dashboard { min-height: 100vh; display: flex; flex-direction: column; +} + +.dashboard__header { + background: var(--color-header); + color: var(--color-header-text); + padding: var(--space-md) var(--space-lg); + display: flex; align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; + justify-content: space-between; + gap: var(--space-md); +} + +.dashboard__header-left { + display: flex; + align-items: center; + gap: var(--space-md); +} + +.dashboard__logo { + color: #fff; + display: block; +} + +.dashboard__badge { + background: rgba(255, 255, 255, 0.15); + color: rgba(255, 255, 255, 0.9); + padding: 3px 10px; + border-radius: 999px; + font-size: 12px; + font-family: var(--font-mono); + font-weight: 500; +} + +.dashboard__body { + display: flex; + flex: 1; + overflow: hidden; } -.App-link { - color: #61dafb; +.dashboard__main { + flex: 1; + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.dashboard__log { + width: 380px; + min-width: 380px; + border-left: 1px solid var(--color-border); + display: flex; + flex-direction: column; } -@keyframes App-logo-spin { - from { - transform: rotate(0deg); +@media (max-width: 900px) { + .dashboard__body { + flex-direction: column; } - to { - transform: rotate(360deg); + .dashboard__log { + width: 100%; + min-width: unset; + border-left: none; + border-top: 1px solid var(--color-border); + max-height: 320px; } } + +/* --- Tabs --- */ +.tabs { + display: flex; + gap: 0; + border-bottom: 1.5px solid var(--color-border); + background: var(--color-surface); + padding: 0 var(--space-lg); + overflow-x: auto; +} + +.tab { + padding: var(--space-md) var(--space-md); + font-size: 13px; + font-weight: 600; + color: var(--color-text-secondary); + background: none; + border: none; + border-bottom: 2px solid transparent; + transition: all 0.15s; + white-space: nowrap; + margin-bottom: -1.5px; +} + +.tab:hover { + color: var(--color-text); +} + +.tab--active { + color: var(--color-accent); + border-bottom-color: var(--color-accent); +} + +/* --- Panel --- */ +.panel { + padding: var(--space-lg); + flex: 1; +} + +.panel__title { + font-size: 18px; + font-weight: 700; + margin-bottom: var(--space-xs); +} + +.panel__description { + color: var(--color-text-secondary); + margin-bottom: var(--space-lg); + font-size: 14px; + line-height: 1.5; +} + +.panel__section { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--space-md); + margin-bottom: var(--space-md); +} + +.panel__section-title { + font-size: 13px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-muted); + margin-bottom: var(--space-sm); +} + +/* --- Session display --- */ +.session-value { + font-family: var(--font-mono); + font-size: 14px; + background: var(--color-primary-bg); + padding: var(--space-sm) var(--space-md); + border-radius: var(--radius-sm); + word-break: break-all; + color: var(--color-primary); + font-weight: 500; +} + +.session-value--empty { + color: var(--color-text-muted); + background: var(--color-bg); + font-style: italic; + font-family: var(--font-sans); +} + +/* --- Tracking buttons --- */ +.tracking-buttons { + display: flex; + flex-wrap: wrap; + gap: var(--space-sm); + margin-bottom: var(--space-md); +} + +.tracking-buttons .btn { + font-family: var(--font-mono); + font-size: 12px; +} + +/* --- Code hint --- */ +.code-hint { + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + padding: var(--space-sm) var(--space-md); + font-family: var(--font-mono); + font-size: 12px; + color: var(--color-text-secondary); + margin-bottom: var(--space-md); + overflow-x: auto; +} + +/* --- UTM Data --- */ +.utm-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.utm-table th, +.utm-table td { + text-align: left; + padding: var(--space-sm) var(--space-md); + border-bottom: 1px solid var(--color-border); +} + +.utm-table th { + font-weight: 600; + color: var(--color-text-muted); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.utm-table td { + font-family: var(--font-mono); +} + +/* --- Form mount area --- */ +.form-mount-area { + min-height: 100px; + border: 2px dashed var(--color-border); + border-radius: var(--radius-md); + padding: var(--space-md); + margin-top: var(--space-md); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-text-muted); + font-size: 13px; + transition: border-color 0.15s; +} + +.form-mount-area--active { + border-color: var(--color-primary); + border-style: solid; +} + +/* --- Event Log --- */ +.event-log { + display: flex; + flex-direction: column; + height: 100%; + background: var(--color-log-bg); +} + +.event-log__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-md); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.event-log__title { + font-size: 13px; + font-weight: 700; + color: var(--color-log-text); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.event-log__count { + font-size: 11px; + color: var(--color-text-muted); + font-family: var(--font-mono); +} + +.event-log__body { + flex: 1; + overflow-y: auto; + padding: var(--space-sm); +} + +.event-log__empty { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--color-text-muted); + font-size: 13px; + font-style: italic; +} + +.log-entry { + background: var(--color-log-entry-bg); + border-radius: var(--radius-sm); + padding: var(--space-sm) var(--space-md); + margin-bottom: var(--space-xs); + animation: fadeIn 0.2s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: translateY(0); } +} + +.log-entry__header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 2px; +} + +.log-entry__name { + font-family: var(--font-mono); + font-size: 12px; + font-weight: 600; + color: #e2e8f0; +} + +.log-entry__time { + font-size: 11px; + color: var(--color-text-muted); + font-family: var(--font-mono); +} + +.log-entry__status { + display: inline-block; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 1px 6px; + border-radius: 3px; + margin-right: var(--space-sm); +} + +.log-entry__status--success { + background: rgba(16, 185, 129, 0.15); + color: var(--color-success); +} + +.log-entry__status--error { + background: rgba(239, 68, 68, 0.15); + color: var(--color-error); +} + +.log-entry__status--info { + background: rgba(59, 130, 246, 0.15); + color: var(--color-info); +} + +.log-entry__payload { + font-family: var(--font-mono); + font-size: 11px; + color: var(--color-text-muted); + white-space: pre-wrap; + word-break: break-all; + max-height: 120px; + overflow-y: auto; + margin-top: 4px; +} + +/* --- Inline flex rows --- */ +.inline-row { + display: flex; + gap: var(--space-sm); + align-items: flex-end; +} + +.inline-row .field { + flex: 1; + margin-bottom: 0; +} + +.inline-row .btn { + margin-bottom: 0; + flex-shrink: 0; +} + +/* --- Status dot --- */ +.status-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: var(--space-xs); +} + +.status-dot--active { + background: var(--color-success); + box-shadow: 0 0 4px var(--color-success); +} + +.status-dot--inactive { + background: var(--color-text-muted); +} diff --git a/src/App.js b/src/App.js index 8e06e3a..4ed4c2b 100644 --- a/src/App.js +++ b/src/App.js @@ -1,48 +1,76 @@ -import Hellotext from "@hellotext/hellotext"; -import "./App.css"; - -// Hellotext.on("session-set", async (session) => { -// console.log(Hellotext.session) -// setTimeout(async () => { -// console.log(Hellotext.session) -// -// const response = await Hellotext.track("app.installed", { -// app_parameters: { -// name: `Hellotext ${new Date().toString()}` -// } -// }) -// -// console.log(response) -// console.log(response.data) -// }) -// -// -// }) - -Hellotext.on("form:completed", (form) => { - console.log("form completed"); - console.log(form); -}); -// -// Hellotext.on('webchat:message:sent', (message) => { -// console.log("message sent") -// console.log(message) -// }) -// -// Hellotext.on('webchat:message:received', (message) => { -// console.log("message recevied") -// console.log(message) -// }) - -Hellotext.initialize("M01az53K", { - apiRoot: "http://api.lvh.me:3000/v1", - webchat: { - id: "zGrDJ1Lb", - }, -}); +import React, { useState, useCallback } from 'react'; +import Hellotext from '@hellotext/hellotext'; +import SetupScreen from './components/SetupScreen'; +import Dashboard from './components/Dashboard'; +import './App.css'; function App() { - return
; + const [initialized, setInitialized] = useState(false); + const [businessId, setBusinessId] = useState(''); + const [webchatId, setWebchatId] = useState(null); + const [activeTab, setActiveTab] = useState('session'); + const [logs, setLogs] = useState([]); + + const addLog = useCallback((entry) => { + setLogs((prev) => [ + ...prev, + { ...entry, timestamp: Date.now() }, + ]); + }, []); + + const clearLogs = useCallback(() => { + setLogs([]); + }, []); + + const handleInitialize = useCallback((bizId, chatId) => { + const config = {}; + if (chatId) { + config.webchat = { id: chatId }; + } + + try { + Hellotext.initialize(bizId, config); + setBusinessId(bizId); + setWebchatId(chatId); + setInitialized(true); + setLogs([{ + name: 'Hellotext.initialize', + status: 'success', + payload: { businessId: bizId, webchatId: chatId }, + timestamp: Date.now(), + }]); + } catch (error) { + setLogs([{ + name: 'Hellotext.initialize', + status: 'error', + payload: error?.message || String(error), + timestamp: Date.now(), + }]); + } + }, []); + + const handleReset = useCallback(() => { + localStorage.removeItem('ht_business_id'); + localStorage.removeItem('ht_webchat_id'); + window.location.reload(); + }, []); + + if (!initialized) { + return ; + } + + return ( + + ); } export default App; diff --git a/src/App.test.js b/src/App.test.js index 1f03afe..f3e219a 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -1,8 +1,110 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, fireEvent } from '@testing-library/react'; import App from './App'; +import { I18nProvider } from './i18n'; -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); +// Mock the Hellotext SDK +jest.mock('@hellotext/hellotext', () => ({ + __esModule: true, + default: { + initialize: jest.fn(), + track: jest.fn(() => Promise.resolve({ data: { ok: true } })), + session: 'mock-session-123', + on: jest.fn(), + removeEventListener: jest.fn(), + }, +})); + +// Mock the SDK stylesheet import +jest.mock('@hellotext/hellotext/styles/index.css', () => {}); + +beforeEach(() => { + localStorage.clear(); + jest.clearAllMocks(); +}); + +describe('Setup Screen', () => { + test('renders setup screen with Business ID input and Initialize button', () => { + render( + + + + ); + expect(screen.getByLabelText(/business id/i)).toBeInTheDocument(); + expect(screen.getByTestId('initialize-btn')).toBeInTheDocument(); + expect(screen.getByTestId('initialize-btn')).toHaveTextContent(/initialize sdk/i); + }); + + test('Initialize button is disabled when Business ID is empty', () => { + render( + + + + ); + const btn = screen.getByTestId('initialize-btn'); + expect(btn).toBeDisabled(); + }); + + test('persists Business ID in localStorage after initialization', () => { + const Hellotext = require('@hellotext/hellotext').default; + render( + + + + ); + + const input = screen.getByLabelText(/business id/i); + fireEvent.change(input, { target: { value: 'TestBiz123' } }); + fireEvent.click(screen.getByTestId('initialize-btn')); + + expect(localStorage.getItem('ht_business_id')).toBe('TestBiz123'); + expect(Hellotext.initialize).toHaveBeenCalledWith('TestBiz123', {}); + }); +}); + +describe('Dashboard', () => { + const initializeApp = () => { + render( + + + + ); + const input = screen.getByLabelText(/business id/i); + fireEvent.change(input, { target: { value: 'MyBiz' } }); + fireEvent.click(screen.getByTestId('initialize-btn')); + }; + + test('shows dashboard with tab navigation after initialization', () => { + initializeApp(); + + expect(screen.getByTestId('dashboard')).toBeInTheDocument(); + expect(screen.getByTestId('tab-navigation')).toBeInTheDocument(); + expect(screen.getByTestId('tab-session')).toBeInTheDocument(); + expect(screen.getByTestId('tab-tracking')).toBeInTheDocument(); + expect(screen.getByTestId('tab-forms')).toBeInTheDocument(); + expect(screen.getByTestId('tab-webchat')).toBeInTheDocument(); + }); + + test('displays the Business ID in the dashboard header', () => { + initializeApp(); + expect(screen.getByText('MyBiz')).toBeInTheDocument(); + }); + + test('event log is visible and shows initialization entry', () => { + initializeApp(); + expect(screen.getByTestId('event-log')).toBeInTheDocument(); + expect(screen.getByText('Hellotext.initialize')).toBeInTheDocument(); + }); + + test('switching tabs updates the visible panel', () => { + initializeApp(); + + // Click Tracking tab + fireEvent.click(screen.getByTestId('tab-tracking')); + expect(screen.getByRole('heading', { name: /tracking events/i })).toBeInTheDocument(); + expect(screen.getByTestId('track-product.viewed')).toBeInTheDocument(); + + // Click Forms tab + fireEvent.click(screen.getByTestId('tab-forms')); + expect(screen.getByLabelText(/form id/i)).toBeInTheDocument(); + }); }); diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js new file mode 100644 index 0000000..14d0589 --- /dev/null +++ b/src/components/Dashboard.js @@ -0,0 +1,89 @@ +import React from 'react'; +import HellotextLogo from './HellotextLogo'; +import SessionPanel from './SessionPanel'; +import UtmPanel from './UtmPanel'; +import TrackingPanel from './TrackingPanel'; +import FormsPanel from './FormsPanel'; +import WebchatPanel from './WebchatPanel'; +import EventLog from './EventLog'; +import { useI18n } from '../i18n'; + + +export default function Dashboard({ + businessId, + webchatId, + activeTab, + onTabChange, + logs, + addLog, + onClearLogs, + onReset, +}) { + const { t } = useI18n(); + const tabs = [ + { id: "session", label: t.dashboard.tabs.session }, + { id: "utm", label: t.dashboard.tabs.utm }, + { id: "tracking", label: t.dashboard.tabs.tracking }, + { id: "forms", label: t.dashboard.tabs.forms }, + { id: "webchat", label: t.dashboard.tabs.webchat }, + ]; + + const renderPanel = () => { + switch (activeTab) { + case 'session': + return ; + case 'utm': + return ; + case 'tracking': + return ; + case 'forms': + return ; + case 'webchat': + return ; + default: + return ; + } + }; + + return ( +
+
+
+ + {businessId} +
+ +
+ +
+
+ + + {renderPanel()} +
+ +
+ +
+
+
+ ); +} diff --git a/src/components/EventLog.js b/src/components/EventLog.js new file mode 100644 index 0000000..17480aa --- /dev/null +++ b/src/components/EventLog.js @@ -0,0 +1,79 @@ +import React, { useRef, useEffect } from 'react'; +import { useI18n } from '../i18n'; + +export default function EventLog({ logs, onClear }) { + const { t } = useI18n(); + const bodyRef = useRef(null); + + useEffect(() => { + if (bodyRef.current) { + bodyRef.current.scrollTop = bodyRef.current.scrollHeight; + } + }, [logs]); + + const formatTime = (ts) => { + const d = new Date(ts); + return d.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) + '.' + String(d.getMilliseconds()).padStart(3, '0'); + }; + + const formatPayload = (payload) => { + if (payload === undefined || payload === null) return null; + try { + return typeof payload === 'string' + ? payload + : JSON.stringify(payload, null, 2); + } catch { + return String(payload); + } + }; + + return ( +
+
+
+ {t.eventLog.title}{' '} + ({logs.length}) +
+ +
+ +
+ {logs.length === 0 ? ( +
+ {t.eventLog.empty} +
+ ) : ( + logs.map((entry, i) => ( +
+
+ + + {entry.status} + + {entry.name} + + {formatTime(entry.timestamp)} +
+ {entry.payload !== undefined && entry.payload !== null && ( +
+ {formatPayload(entry.payload)} +
+ )} +
+ )) + )} +
+
+ ); +} diff --git a/src/components/FormsPanel.js b/src/components/FormsPanel.js new file mode 100644 index 0000000..c126b1e --- /dev/null +++ b/src/components/FormsPanel.js @@ -0,0 +1,118 @@ +import React, { useState, useEffect } from 'react'; +import Hellotext from '@hellotext/hellotext'; +import { useI18n } from '../i18n'; + +export default function FormsPanel({ addLog }) { + const { t } = useI18n(); + const [formId, setFormId] = useState(''); + const [mountedFormId, setMountedFormId] = useState(null); + + useEffect(() => { + const onCollected = (forms) => { + addLog({ + name: 'forms:collected', + status: 'info', + payload: { count: Array.isArray(forms) ? forms.length : forms }, + }); + }; + + const onCompleted = (form) => { + addLog({ + name: 'form:completed', + status: 'success', + payload: form, + }); + }; + + Hellotext.on('forms:collected', onCollected); + Hellotext.on('form:completed', onCompleted); + + return () => { + try { Hellotext.removeEventListener('forms:collected', onCollected); } catch {} + try { Hellotext.removeEventListener('form:completed', onCompleted); } catch {} + }; + }, [addLog]); + + const handleMount = () => { + const id = formId.trim(); + if (!id) return; + setMountedFormId(id); + addLog({ + name: 'form:mount', + status: 'info', + payload: { formId: id }, + }); + }; + + const handleUnmount = () => { + setMountedFormId(null); + addLog({ + name: 'form:unmount', + status: 'info', + payload: null, + }); + }; + + return ( +
+

{t.forms.title}

+

+ {t.forms.description.split('{code}')[0]} + {t.forms.dataHelloForm} + {t.forms.description.split('{code}')[1]} +

+ +
+
{t.forms.mountForm}
+
+
+ + setFormId(e.target.value)} + placeholder={t.forms.formIdPlaceholder} + /> +
+ {!mountedFormId ? ( + + ) : ( + + )} +
+
+ +
+ {mountedFormId ? ( +
+ {/* The SDK will discover and mount the form here */} +
+ ) : ( + {t.forms.placeholder} + )} +
+ +
+
{t.forms.events}
+
+ forms:collected {t.forms.formsCollectedHint}
+ form:completed {t.forms.formCompletedHint} +
+
+
+ ); +} diff --git a/src/components/HellotextLogo.js b/src/components/HellotextLogo.js new file mode 100644 index 0000000..6341e8d --- /dev/null +++ b/src/components/HellotextLogo.js @@ -0,0 +1,22 @@ +import React from 'react'; + +export default function HellotextLogo({ width = 150, height = 43, className = '', style = {} }) { + return ( + + hellotext + + + ); +} diff --git a/src/components/SessionPanel.js b/src/components/SessionPanel.js new file mode 100644 index 0000000..31a2a1f --- /dev/null +++ b/src/components/SessionPanel.js @@ -0,0 +1,50 @@ +import React, { useEffect, useState } from 'react'; +import Hellotext from '@hellotext/hellotext'; +import { useI18n } from '../i18n'; + +export default function SessionPanel({ addLog }) { + const { t } = useI18n(); + const [session, setSession] = useState(Hellotext.session || null); + + useEffect(() => { + const handler = (newSession) => { + setSession(newSession); + addLog({ + name: 'session-set', + status: 'info', + payload: newSession, + }); + }; + + Hellotext.on('session-set', handler); + return () => { + try { Hellotext.removeEventListener('session-set', handler); } catch {} + }; + }, [addLog]); + + return ( +
+

{t.session.title}

+

+ {t.session.description.split('{code}')[0]} + {t.session.sessionSetCode} + {t.session.description.split('{code}')[1]} +

+ +
+
{t.session.currentSession}
+
+ {session || t.session.noSession} +
+
+ +
+
{t.session.howItWorks}
+
+ Hellotext.session {t.session.apiSession}
+ Hellotext.on('session-set', callback) {t.session.apiOn} +
+
+
+ ); +} diff --git a/src/components/SetupScreen.js b/src/components/SetupScreen.js new file mode 100644 index 0000000..f5726f2 --- /dev/null +++ b/src/components/SetupScreen.js @@ -0,0 +1,85 @@ +import React, { useState } from 'react'; +import HellotextLogo from './HellotextLogo'; +import { useI18n } from '../i18n'; + +const STORAGE_KEY_BIZ = 'ht_business_id'; +const STORAGE_KEY_WEBCHAT = 'ht_webchat_id'; + +export default function SetupScreen({ onInitialize }) { + const { t } = useI18n(); + + const [businessId, setBusinessId] = useState( + () => localStorage.getItem(STORAGE_KEY_BIZ) || '' + ); + const [webchatId, setWebchatId] = useState( + () => localStorage.getItem(STORAGE_KEY_WEBCHAT) || '' + ); + + const handleSubmit = (e) => { + e.preventDefault(); + const trimmedBiz = businessId.trim(); + if (!trimmedBiz) return; + + localStorage.setItem(STORAGE_KEY_BIZ, trimmedBiz); + if (webchatId.trim()) { + localStorage.setItem(STORAGE_KEY_WEBCHAT, webchatId.trim()); + } else { + localStorage.removeItem(STORAGE_KEY_WEBCHAT); + } + + onInitialize(trimmedBiz, webchatId.trim() || null); + }; + + return ( +
+
+
+ +
+

+ {t.setup.subtitle} +

+ +
+ + setBusinessId(e.target.value)} + placeholder={t.setup.businessIdPlaceholder} + autoFocus + /> +
+ {t.setup.businessIdHint} +
+
+ +
+ + setWebchatId(e.target.value)} + placeholder={t.setup.webchatIdPlaceholder} + /> +
+ {t.setup.webchatIdHint} +
+
+ + +
+
+ ); +} diff --git a/src/components/TrackingPanel.js b/src/components/TrackingPanel.js new file mode 100644 index 0000000..1222e0c --- /dev/null +++ b/src/components/TrackingPanel.js @@ -0,0 +1,131 @@ +import React, { useState } from 'react'; +import Hellotext from '@hellotext/hellotext'; +import { useI18n } from '../i18n'; + +const PRESET_EVENTS = [ + { + name: 'product.viewed', + params: { product_id: 'prod_demo_001', name: 'Demo Product', price: 29.99 }, + }, + { + name: 'cart.added', + params: { product_id: 'prod_demo_001', quantity: 1, price: 29.99 }, + }, + { + name: 'checkout.started', + params: { total: 29.99, currency: 'USD', items_count: 1 }, + }, +]; + +export default function TrackingPanel({ addLog }) { + const { t } = useI18n(); + const [customEvent, setCustomEvent] = useState(''); + const [customParams, setCustomParams] = useState('{}'); + const [sending, setSending] = useState(null); + + const fireEvent = async (eventName, params) => { + setSending(eventName); + addLog({ + name: `track: ${eventName}`, + status: 'info', + payload: { event: eventName, params }, + }); + + try { + const response = await Hellotext.track(eventName, params); + addLog({ + name: `track: ${eventName}`, + status: 'success', + payload: response?.data || response, + }); + } catch (error) { + addLog({ + name: `track: ${eventName}`, + status: 'error', + payload: error?.message || String(error), + }); + } finally { + setSending(null); + } + }; + + const handleCustomTrack = () => { + const name = customEvent.trim(); + if (!name) return; + + let params; + try { + params = JSON.parse(customParams); + } catch { + addLog({ + name: 'custom track', + status: 'error', + payload: t.tracking.invalidJson, + }); + return; + } + + fireEvent(name, params); + }; + + return ( +
+

{t.tracking.title}

+

+ {t.tracking.description.split('{code}')[0]} + {t.tracking.trackCode} + {t.tracking.description.split('{code}')[1]} +

+ +
+
{t.tracking.presetEvents}
+
+ {PRESET_EVENTS.map((evt) => ( + + ))} +
+
+ +
+
{t.tracking.customEvent}
+
+ + setCustomEvent(e.target.value)} + placeholder={t.tracking.eventNamePlaceholder} + /> +
+
+ +