Add Discord widget integration with dynamic script loading
All checks were successful
Build / build (push) Successful in 1m24s

- Created TypeScript wrapper for Widgetbot Crate
- Dynamically loads Discord widget from CDN at runtime
- Removed @widgetbot/crate npm package to avoid React dependency (182 packages removed)
- Integrated with VR mode: auto-hides in VR, auto-shows in desktop mode
- Connected to Discord server 1112846185913401475, channel 1437561367908581406

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-11-10 17:53:27 -06:00
parent b31e33350e
commit 1648364540
5 changed files with 255 additions and 2 deletions

14
package-lock.json generated
View File

@ -1513,6 +1513,20 @@
"tr46": "~0.0.3", "tr46": "~0.0.3",
"webidl-conversions": "^3.0.0" "webidl-conversions": "^3.0.0"
} }
},
"node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
} }
} }
} }

View File

@ -15,15 +15,15 @@
"export-blend:batch": "tsx scripts/exportBlend.ts --batch" "export-blend:batch": "tsx scripts/exportBlend.ts --batch"
}, },
"dependencies": { "dependencies": {
"@auth0/auth0-spa-js": "^2.8.0",
"@babylonjs/core": "8.36.1", "@babylonjs/core": "8.36.1",
"@babylonjs/gui": "^8.36.1", "@babylonjs/gui": "^8.36.1",
"@babylonjs/havok": "1.3.10", "@babylonjs/havok": "1.3.10",
"@babylonjs/inspector": "8.36.1", "@babylonjs/inspector": "8.36.1",
"@babylonjs/loaders": "8.36.1", "@babylonjs/loaders": "8.36.1",
"@babylonjs/materials": "8.36.1", "@babylonjs/materials": "8.36.1",
"@babylonjs/serializers": "8.36.1",
"@babylonjs/procedural-textures": "8.36.1", "@babylonjs/procedural-textures": "8.36.1",
"@auth0/auth0-spa-js": "^2.8.0", "@babylonjs/serializers": "8.36.1",
"openai": "4.52.3" "openai": "4.52.3"
}, },
"devDependencies": { "devDependencies": {

199
src/discordWidget.ts Normal file
View File

@ -0,0 +1,199 @@
/**
* Discord Widget Integration using Widgetbot Crate
* Dynamically loads the widget script to avoid npm bundling issues
*/
export interface DiscordWidgetOptions {
server: string;
channel: string;
location?: string[];
color?: string;
glyph?: string[];
notifications?: boolean;
indicator?: boolean;
allChannelNotifications?: boolean;
}
export class DiscordWidget {
private crate: any = null;
private scriptLoaded = false;
private isVisible = false;
/**
* Initialize the Discord widget
* @param options - Widget configuration
*/
async initialize(options: DiscordWidgetOptions): Promise<void> {
// Load the Crate script if not already loaded
if (!this.scriptLoaded) {
console.log('[DiscordWidget] Loading Crate script...');
await this.loadCrateScript();
this.scriptLoaded = true;
}
// Wait for Crate to be available on window
await this.waitForCrate();
// Initialize the Crate widget
const defaultOptions: DiscordWidgetOptions = {
location: ['bottom', 'right'],
color: '#7289DA',
glyph: ['💬', '✖️'],
notifications: true,
indicator: true,
...options
};
console.log('[DiscordWidget] Initializing Crate with options:', defaultOptions);
// @ts-ignore - Crate is loaded from CDN
this.crate = new window.Crate(defaultOptions);
this.setupEventListeners();
console.log('[DiscordWidget] Successfully initialized');
}
/**
* Dynamically load the Crate script from CDN
*/
private loadCrateScript(): Promise<void> {
return new Promise((resolve, reject) => {
// Check if script already exists
const existingScript = document.querySelector('script[src*="widgetbot"]');
if (existingScript) {
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/@widgetbot/crate@3';
script.async = true;
script.defer = true;
script.onload = () => {
console.log('[DiscordWidget] Script loaded successfully');
resolve();
};
script.onerror = () => {
console.error('[DiscordWidget] Failed to load script');
reject(new Error('Failed to load Widgetbot Crate script'));
};
document.head.appendChild(script);
});
}
/**
* Wait for Crate constructor to be available on window
*/
private waitForCrate(): Promise<void> {
return new Promise((resolve) => {
const checkCrate = () => {
// @ts-ignore
if (window.Crate) {
resolve();
} else {
setTimeout(checkCrate, 50);
}
};
checkCrate();
});
}
/**
* Setup event listeners for widget events
*/
private setupEventListeners(): void {
if (!this.crate) return;
// Listen for when user signs in
this.crate.on('signIn', (user: any) => {
console.log('[DiscordWidget] User signed in:', user.username);
});
// Listen for widget visibility changes
this.crate.on('toggleChat', (visible: boolean) => {
this.isVisible = visible;
console.log('[DiscordWidget] Chat visibility:', visible);
});
}
/**
* Toggle the Discord chat widget
*/
toggle(): void {
if (this.crate) {
this.crate.toggle();
}
}
/**
* Show a notification on the widget button
* @param message - Notification message
*/
notify(message: string): void {
if (this.crate) {
this.crate.notify(message);
}
}
/**
* Show the widget
*/
show(): void {
if (this.crate && !this.isVisible) {
this.crate.show();
this.isVisible = true;
}
}
/**
* Hide the widget
*/
hide(): void {
if (this.crate && this.isVisible) {
this.crate.hide();
this.isVisible = false;
}
}
/**
* Check if widget is currently visible
*/
getIsVisible(): boolean {
return this.isVisible;
}
/**
* Emit a custom event to the widget
* @param event - Event name
* @param data - Event data
*/
emit(event: string, data?: any): void {
if (this.crate) {
this.crate.emit(event, data);
}
}
/**
* Listen for widget events
* @param event - Event name
* @param callback - Event callback
*/
on(event: string, callback: (data: any) => void): void {
if (this.crate) {
this.crate.on(event, callback);
}
}
/**
* Send a message to the Discord channel (if user is signed in)
* @param message - Message text
*/
sendMessage(message: string): void {
if (this.crate) {
this.emit('sendMessage', message);
}
}
}

View File

@ -32,6 +32,7 @@ import {ReplayManager} from "./replay/ReplayManager";
import {AuthService} from "./authService"; import {AuthService} from "./authService";
import {updateUserProfile} from "./loginScreen"; import {updateUserProfile} from "./loginScreen";
import {Preloader} from "./preloader"; import {Preloader} from "./preloader";
import {DiscordWidget} from "./discordWidget";
// Set to true to run minimal controller debug test // Set to true to run minimal controller debug test
const DEBUG_CONTROLLERS = false; const DEBUG_CONTROLLERS = false;
@ -473,6 +474,20 @@ export class Main {
pointerFeature.detach(); pointerFeature.detach();
debugLog("Pointer selection feature stored and detached"); debugLog("Pointer selection feature stored and detached");
} }
// Hide Discord widget when entering VR, show when exiting
DefaultScene.XR.baseExperience.onStateChangedObservable.add((state) => {
const discord = (window as any).__discordWidget as DiscordWidget;
if (discord) {
if (state === 2) { // WebXRState.IN_XR
debugLog('[Main] Entering VR - hiding Discord widget');
discord.hide();
} else if (state === 0) { // WebXRState.NOT_IN_XR
debugLog('[Main] Exiting VR - showing Discord widget');
discord.show();
}
}
});
} }
this.reportProgress(40, 'VR support enabled'); this.reportProgress(40, 'VR support enabled');
} catch (error) { } catch (error) {
@ -610,6 +625,26 @@ router.on('/', async () => {
// Initialize demo mode without engine (just for UI purposes) // Initialize demo mode without engine (just for UI purposes)
const demo = new Demo(main); const demo = new Demo(main);
} }
// Initialize Discord widget (if not already initialized)
if (!(window as any).__discordWidget) {
debugLog('[Router] Initializing Discord widget');
const discord = new DiscordWidget();
// Initialize with your server and channel IDs
discord.initialize({
server: '1112846185913401475', // Replace with your Discord server ID
channel: '1437561367908581406', // Replace with your Discord channel ID
color: '#667eea',
glyph: ['💬', '✖️'],
notifications: true
}).then(() => {
debugLog('[Router] Discord widget ready');
(window as any).__discordWidget = discord;
}).catch(error => {
console.error('[Router] Failed to initialize Discord widget:', error);
});
}
} }
debugLog('[Router] Home route handler complete'); debugLog('[Router] Home route handler complete');

5
src/vite-env.d.ts vendored
View File

@ -1 +1,6 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
// Widgetbot Crate global type
interface Window {
Crate: new (options: any) => any;
}