testapp: Updated testapp
This commit is contained in:
parent
a0964aa4ca
commit
654d082e9c
@ -1,10 +1,3 @@
|
||||
# API Settings
|
||||
OPENVIDU_MEET_URL=http://localhost:6080/meet/api/v1
|
||||
API_KEY=meet-api-key
|
||||
|
||||
# Server Settings
|
||||
PORT=5080
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# Environment
|
||||
NODE_ENV=development
|
||||
@ -1,24 +0,0 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"no-console": "warn",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "warn"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"no-unused-vars": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off"
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
0
testapp/README.md
Normal file
0
testapp/README.md
Normal file
@ -1,6 +0,0 @@
|
||||
{
|
||||
"watch": ["src", "server.ts", ".env"],
|
||||
"ignore": ["src/public"],
|
||||
"ext": "ts,js,json",
|
||||
"exec": "ts-node --esm server.ts"
|
||||
}
|
||||
6988
testapp/package-lock.json
generated
6988
testapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,51 +1,36 @@
|
||||
{
|
||||
"name": "meet-testapp",
|
||||
"name": "testapp",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "dist/server.js",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "npm run build && node dist/server.js",
|
||||
"start:dev": "NODE_ENV=development nodemon --watch \"**/*.ts\" --exec \"node --loader ts-node/esm server.ts\"",
|
||||
"start:prod": "NODE_ENV=production node dist/server.js",
|
||||
"dev": "concurrently \"npm:watch-*\"",
|
||||
"watch-ts": "tsc --watch",
|
||||
"watch-node": "nodemon --watch dist dist/server.js",
|
||||
"watch-client": "nodemon --watch src/public/js --ext ts --exec node src/utils/build-client.js",
|
||||
"build": "npm run clean && npm run build-server && npm run build-client && npm run copy-assets",
|
||||
"build:dev": "NODE_ENV=development npm run build",
|
||||
"build:prod": "NODE_ENV=production npm run build",
|
||||
"build-server": "tsc",
|
||||
"build-client": "node scripts/build-client.js",
|
||||
"copy-assets": "node scripts/copy-assets.js",
|
||||
"lint": "eslint . --ext .ts,.js",
|
||||
"format": "prettier --write \"**/*.{ts,js}\"",
|
||||
"clean": "rimraf dist",
|
||||
"test": "jest"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "tsc",
|
||||
"build:watch": "tsc --watch",
|
||||
"start": "node dist/index.js",
|
||||
"dev:server": "ts-node-dev --respawn --watch src,public/ts src/index.ts",
|
||||
"dev": "concurrently \"npm:watch:client\" \"npm run dev:server\" --kill-others",
|
||||
"build:client": "tsc -p tsconfig.client.json",
|
||||
"watch:client": "tsc -p tsconfig.client.json --watch"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"cors": "2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^5.1.0",
|
||||
"mustache-express": "^1.3.2",
|
||||
"socket.io": "^4.8.1"
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/mustache-express": "^1.2.5",
|
||||
"@types/node": "^22.15.15",
|
||||
"@types/node": "^22.15.17",
|
||||
"@types/socket.io": "^3.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.32.0",
|
||||
"@typescript-eslint/parser": "^8.32.0",
|
||||
"@types/socket.io-client": "^1.4.36",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-prettier": "^10.1.3",
|
||||
"eslint-plugin-prettier": "^5.4.0",
|
||||
"nodemon": "^3.1.10",
|
||||
"prettier": "^3.5.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,9 +22,9 @@ html {
|
||||
|
||||
/* Left Sidebar Panel */
|
||||
#control-panel {
|
||||
width: 300px;
|
||||
width: 350px;
|
||||
background: white;
|
||||
padding: 10px 15px;
|
||||
padding: 5px 5px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -48,6 +48,7 @@ html {
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#control-panel h3 {
|
||||
@ -64,10 +65,11 @@ html {
|
||||
}
|
||||
|
||||
#control-panel .log-list li {
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 10px;
|
||||
padding: 5px;
|
||||
background-color: #3a3a3a;
|
||||
background-color: #474747;
|
||||
border-radius: 4px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Main Content Section (Web Component) */
|
||||
@ -88,3 +90,16 @@ html {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
pre code {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.section .title {
|
||||
font-size: 1rem;
|
||||
margin: .5rem;
|
||||
}
|
||||
|
||||
162
testapp/public/ts/webcomponent.ts
Normal file
162
testapp/public/ts/webcomponent.ts
Normal file
@ -0,0 +1,162 @@
|
||||
const socket = (window as any).io();
|
||||
|
||||
/**
|
||||
* Add a component event to the events log
|
||||
*/
|
||||
const addEventToLog = (eventType: string, eventMessage: string): void => {
|
||||
const eventsList = document.getElementById('events-list');
|
||||
if (eventsList) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = `[ ${eventType} ] : ${eventMessage}`;
|
||||
eventsList.appendChild(li);
|
||||
}
|
||||
};
|
||||
const escapeHtml = (unsafe: string): string => {
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
};
|
||||
const listenWebhookServerEvents = () => {
|
||||
socket.on('webhookEvent', (payload: any) => {
|
||||
console.log('Webhook received:', payload);
|
||||
const webhookLogList = document.getElementById('webhook-log-list');
|
||||
if (webhookLogList) {
|
||||
// Create unique IDs for this accordion item
|
||||
const itemId = payload.creationDate;
|
||||
const headerId = `header-${itemId}`;
|
||||
const collapseId = `collapse-${itemId}`;
|
||||
|
||||
// Create accordion item container
|
||||
const accordionItem = document.createElement('div');
|
||||
accordionItem.className = 'accordion-item';
|
||||
|
||||
// Create header
|
||||
const header = document.createElement('h2');
|
||||
header.className = 'accordion-header';
|
||||
header.id = headerId;
|
||||
|
||||
// Create header button
|
||||
const button = document.createElement('button');
|
||||
button.className = 'accordion-button';
|
||||
button.type = 'button';
|
||||
button.setAttribute('data-bs-toggle', 'collapse');
|
||||
button.setAttribute('data-bs-target', `#${collapseId}`);
|
||||
button.setAttribute('aria-expanded', 'true');
|
||||
button.setAttribute('aria-controls', collapseId);
|
||||
button.style.padding = '10px';
|
||||
|
||||
if (payload.event === 'meetingStarted') {
|
||||
button.classList.add('bg-success');
|
||||
}
|
||||
if (payload.event === 'meetingEnded') {
|
||||
button.classList.add('bg-danger');
|
||||
}
|
||||
if (payload.event.includes('recording')) {
|
||||
button.classList.add('bg-warning');
|
||||
}
|
||||
// Format the header text with event name and timestamp
|
||||
const date = new Date(payload.creationDate);
|
||||
|
||||
const formattedDate = date.toLocaleString('es-ES', {
|
||||
// year: 'numeric',
|
||||
// month: '2-digit',
|
||||
// day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
button.innerHTML = `[${formattedDate}] <strong>${payload.event}</strong>`;
|
||||
|
||||
// Create collapsible content container
|
||||
const collapseDiv = document.createElement('div');
|
||||
collapseDiv.id = collapseId;
|
||||
collapseDiv.className = 'accordion-collapse collapse';
|
||||
collapseDiv.setAttribute('aria-labelledby', headerId);
|
||||
collapseDiv.setAttribute('data-bs-parent', '#webhook-log-list');
|
||||
|
||||
// Create body content
|
||||
const bodyDiv = document.createElement('div');
|
||||
bodyDiv.className = 'accordion-body';
|
||||
|
||||
// Format JSON with syntax highlighting if possible
|
||||
const formattedJson = JSON.stringify(payload, null, 2);
|
||||
bodyDiv.innerHTML = `<pre class="mb-0"><code>${escapeHtml(
|
||||
formattedJson
|
||||
)}</code></pre>`;
|
||||
|
||||
// Assemble the components
|
||||
header.appendChild(button);
|
||||
collapseDiv.appendChild(bodyDiv);
|
||||
accordionItem.appendChild(header);
|
||||
accordionItem.appendChild(collapseDiv);
|
||||
|
||||
// Insert at the top of the list (latest events first)
|
||||
if (webhookLogList.firstChild) {
|
||||
webhookLogList.insertBefore(accordionItem, webhookLogList.firstChild);
|
||||
} else {
|
||||
webhookLogList.appendChild(accordionItem);
|
||||
}
|
||||
|
||||
// Limit the number of items to prevent performance issues
|
||||
const maxItems = 50;
|
||||
while (webhookLogList.children.length > maxItems) {
|
||||
webhookLogList.removeChild(webhookLogList.lastChild!);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Listen to events from openvidu-meet
|
||||
|
||||
const listenWebComponentEvents = () => {
|
||||
const meet = document.querySelector('openvidu-meet') as any;
|
||||
if (!meet) {
|
||||
console.error('openvidu-meet component not found');
|
||||
alert('openvidu-meet component not found in the DOM');
|
||||
return;
|
||||
}
|
||||
|
||||
meet.on('JOIN', (event: CustomEvent<any>) => {
|
||||
console.log('JOIN event received:', event);
|
||||
addEventToLog('JOIN', JSON.stringify(event));
|
||||
});
|
||||
meet.on('LEFT', (event: CustomEvent<any>) => {
|
||||
console.log('LEFT event received:', event);
|
||||
addEventToLog('LEFT', JSON.stringify(event));
|
||||
});
|
||||
};
|
||||
|
||||
const setUpWebComponentCommands = () => {
|
||||
const meet = document.querySelector('openvidu-meet') as any;
|
||||
|
||||
if (!meet) {
|
||||
console.error('openvidu-meet component not found');
|
||||
alert('openvidu-meet component not found in the DOM');
|
||||
return;
|
||||
}
|
||||
|
||||
// End meeting button click handler
|
||||
document
|
||||
.getElementById('end-meeting-btn')
|
||||
?.addEventListener('click', () => meet.endMeeting());
|
||||
|
||||
// Leave room button click handler
|
||||
document
|
||||
.getElementById('leave-room-btn')
|
||||
?.addEventListener('click', () => meet.leaveRoom());
|
||||
|
||||
// Toggle chat button click handler
|
||||
document
|
||||
.getElementById('toggle-chat-btn')
|
||||
?.addEventListener('click', () => meet.toggleChat());
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
listenWebhookServerEvents();
|
||||
listenWebComponentEvents();
|
||||
setUpWebComponentCommands();
|
||||
});
|
||||
@ -25,10 +25,11 @@
|
||||
<ul class="list-group">
|
||||
{{#rooms}}
|
||||
<li
|
||||
id="{{roomIdPrefix}}"
|
||||
class="list-group-item d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<span>{{ roomId }}</span>
|
||||
<div class="dropdown">
|
||||
<div class="dropdown-button">
|
||||
<button
|
||||
class="btn btn-primary btn-sm dropdown-toggle"
|
||||
type="button"
|
||||
@ -50,7 +51,7 @@
|
||||
value="moderator"
|
||||
/>
|
||||
|
||||
<button type="submit" class="dropdown-item">
|
||||
<button type="submit" id="join-as-moderator" class="dropdown-item">
|
||||
Moderator
|
||||
</button>
|
||||
</form>
|
||||
@ -68,29 +69,11 @@
|
||||
value="publisher"
|
||||
/>
|
||||
|
||||
<button type="submit" class="dropdown-item">
|
||||
<button type="submit" id="join-as-publisher" class="dropdown-item">
|
||||
Publisher
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
<form action="/join-room" method="post">
|
||||
<input
|
||||
type="hidden"
|
||||
name="roomUrl"
|
||||
value="{{ viewerRoomUrl }}"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="participantRole"
|
||||
value="viewer"
|
||||
/>
|
||||
|
||||
<button type="submit" class="dropdown-item">
|
||||
Viewer
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@ -130,11 +113,10 @@
|
||||
name="autoDeletionDate"
|
||||
id="expiration-date"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<button type="submit" class="create-room-btn btn btn-primary w-100">
|
||||
Create Room
|
||||
</button>
|
||||
</form>
|
||||
@ -18,10 +18,9 @@
|
||||
<!-- Left Sidebar Panel -->
|
||||
<div id="control-panel" class="d-flex flex-column">
|
||||
<h3>{{ participantRole }}</h3>
|
||||
<hr />
|
||||
<!-- Commands -->
|
||||
<div class="section">
|
||||
<h5>Commands</h5>
|
||||
<h5 class="title">Commands</h5>
|
||||
{{#isModerator}}
|
||||
<button id="end-meeting-btn" class="btn btn-danger">End Meeting</button>
|
||||
{{/isModerator}}
|
||||
@ -31,19 +30,17 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<!-- Events -->
|
||||
<div class="section">
|
||||
<h5>Events</h5>
|
||||
<h5 class="title">Events</h5>
|
||||
<ul id="events-list" class="log-list">
|
||||
<!-- Events will be added dynamically here -->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<!-- Webhooks Log -->
|
||||
<div class="section">
|
||||
<h5>Webhooks</h5>
|
||||
<h5 class="title">Webhook</h5>
|
||||
<ul id="webhook-log-list" class="log-list">
|
||||
<!-- Webhooks will be added dynamically here -->
|
||||
</ul>
|
||||
@ -64,8 +61,7 @@
|
||||
</div>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
|
||||
<script src="js/videoRoom.js"></script>
|
||||
<script type="module" src="../js/webcomponent.js"></script>
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
@ -1,71 +0,0 @@
|
||||
import { exec } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
// Get directory name in ES modules
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Source directory for client TypeScript files
|
||||
const sourceDir = path.join(__dirname, '..', 'public', 'js');
|
||||
// Destination directory for compiled JavaScript
|
||||
const destDir = path.join(__dirname, '..', 'dist', 'testapp', 'public', 'js');
|
||||
|
||||
// Create destination directory if it doesn't exist
|
||||
if (!fs.existsSync(destDir)) {
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Get all TypeScript files in the source directory
|
||||
const tsFiles = fs.readdirSync(sourceDir).filter(file => file.endsWith('.ts'));
|
||||
|
||||
// Compile each TypeScript file individually
|
||||
console.log('Compiling client-side TypeScript files...');
|
||||
|
||||
// Create a much simpler compilation approach
|
||||
tsFiles.forEach(file => {
|
||||
const sourcePath = path.join(sourceDir, file);
|
||||
const outputFile = file.replace('.ts', '.js');
|
||||
const destPath = path.join(destDir, outputFile);
|
||||
|
||||
// Read the TypeScript file
|
||||
const tsContent = fs.readFileSync(sourcePath, 'utf-8');
|
||||
|
||||
console.log(`Processing ${file}...`);
|
||||
|
||||
// Use esbuild to quickly transpile TypeScript to JavaScript
|
||||
// For a simple application, we'll do some basic transforms ourselves
|
||||
let jsContent = tsContent;
|
||||
|
||||
// Remove TypeScript type annotations and interfaces
|
||||
jsContent = jsContent.replace(/^interface\s+\w+\s*\{[\s\S]*?\}/gm, '');
|
||||
jsContent = jsContent.replace(/^type\s+\w+\s*=[\s\S]*?;/gm, '');
|
||||
jsContent = jsContent.replace(/:\s*\w+(\[\])?(\s*\|\s*null)?(\s*\|\s*undefined)?/g, '');
|
||||
jsContent = jsContent.replace(/<\w+(\[\])?>/g, '');
|
||||
jsContent = jsContent.replace(/as\s+\w+(\[\])?/g, '');
|
||||
|
||||
// Save the JavaScript file
|
||||
fs.writeFileSync(destPath, jsContent);
|
||||
console.log(`Created ${outputFile}`);
|
||||
});
|
||||
|
||||
// Copy utility files that client code might need
|
||||
const commonUtilPath = path.join(__dirname, '..', 'utils', 'common.js');
|
||||
const destCommonPath = path.join(destDir, 'common.js');
|
||||
|
||||
// Basic conversion of the common.ts file to JS
|
||||
if (fs.existsSync(path.join(__dirname, '..', 'utils', 'common.ts'))) {
|
||||
const tsContent = fs.readFileSync(path.join(__dirname, '..', 'utils', 'common.ts'), 'utf-8');
|
||||
let jsContent = tsContent;
|
||||
|
||||
// Remove TypeScript type annotations
|
||||
jsContent = jsContent.replace(/:\s*\w+(\[\])?(\s*\|\s*null)?(\s*\|\s*undefined)?/g, '');
|
||||
jsContent = jsContent.replace(/<\w+(\[\])?>/g, '');
|
||||
|
||||
fs.writeFileSync(destCommonPath, jsContent);
|
||||
console.log('Created common.js utility file');
|
||||
}
|
||||
|
||||
console.log('Client-side files processed successfully');
|
||||
@ -1,58 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
// Get directory name in ES modules
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Define paths
|
||||
const projectRoot = path.join(__dirname, '..');
|
||||
const viewsDir = path.join(projectRoot, 'public', 'views');
|
||||
const distViewsDir = path.join(projectRoot, 'dist', 'public', 'views');
|
||||
const cssDir = path.join(projectRoot, 'public', 'css');
|
||||
const distCssDir = path.join(projectRoot, 'dist', 'testapp', 'public', 'css');
|
||||
|
||||
// Create dist directories if they don't exist
|
||||
function ensureDirectoryExists(directory) {
|
||||
if (!fs.existsSync(directory)) {
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
console.log(`Created directory: ${directory}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy files from source to destination
|
||||
function copyFiles(sourceDir, destDir, extension) {
|
||||
ensureDirectoryExists(destDir);
|
||||
|
||||
// Check if source directory exists
|
||||
if (!fs.existsSync(sourceDir)) {
|
||||
console.log(`Source directory does not exist: ${sourceDir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(sourceDir).filter(file => file.endsWith(extension));
|
||||
|
||||
files.forEach(file => {
|
||||
const sourcePath = path.join(sourceDir, file);
|
||||
const destPath = path.join(destDir, file);
|
||||
|
||||
fs.copyFileSync(sourcePath, destPath);
|
||||
console.log(`Copied ${sourcePath} to ${destPath}`);
|
||||
});
|
||||
|
||||
console.log(`Copied ${files.length} ${extension} files`);
|
||||
}
|
||||
|
||||
console.log('Starting asset copy process...');
|
||||
|
||||
// Copy view templates
|
||||
console.log('Copying Mustache templates...');
|
||||
copyFiles(viewsDir, distViewsDir, '.mustache');
|
||||
|
||||
// Copy CSS files
|
||||
console.log('Copying CSS files...');
|
||||
copyFiles(cssDir, distCssDir, '.css');
|
||||
|
||||
console.log('All static assets have been copied to dist.');
|
||||
@ -1,76 +0,0 @@
|
||||
import dotenv from 'dotenv';
|
||||
import express from 'express';
|
||||
import mustacheExpress from 'mustache-express';
|
||||
import bodyParser from 'body-parser';
|
||||
import cors from 'cors';
|
||||
import path from 'path';
|
||||
import http from 'http';
|
||||
import { Server } from 'socket.io';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
// Import controllers
|
||||
import { joinRoom, handleWebhook } from './src/controllers/videoRoomController.js';
|
||||
import { createRoom, renderHomePage } from './src/controllers/homeController.js';
|
||||
import { ConfigService } from './src/services/config.service.js';
|
||||
import { Logger } from './src/utils/logger.js';
|
||||
|
||||
// Initialize logger
|
||||
const logger = new Logger('Server');
|
||||
|
||||
// Get directory name in ES modules
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Define project root (going up from the dist folder if we're running the compiled version)
|
||||
const projectRoot =
|
||||
process.env.NODE_ENV === 'production'
|
||||
? path.join(__dirname, '..') // In production, dist/server.js is 1 level down
|
||||
: __dirname; // In development, we're in the project root
|
||||
|
||||
// Initialize environment variables
|
||||
dotenv.config();
|
||||
logger.info('Environment variables loaded');
|
||||
|
||||
// Get configuration
|
||||
const config = ConfigService.getInstance();
|
||||
|
||||
// Setup Express and HTTP server
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server);
|
||||
|
||||
// Configure view engine and middleware
|
||||
app.engine('mustache', mustacheExpress());
|
||||
app.set('view engine', 'mustache');
|
||||
app.set('views', path.join(projectRoot, 'public/views'));
|
||||
app.use(express.static(path.join(projectRoot, 'public')));
|
||||
app.use(express.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(cors());
|
||||
logger.debug('Express middleware configured');
|
||||
logger.debug(`Static directory: ${path.join(projectRoot, 'public')}`);
|
||||
logger.debug(`Views directory: ${path.join(projectRoot, 'public/views')}`);
|
||||
|
||||
// Define routes
|
||||
app.get('/', renderHomePage);
|
||||
app.post('/room', createRoom);
|
||||
app.post('/join-room', joinRoom);
|
||||
app.post('/webhook', (req, res) => {
|
||||
handleWebhook(req, res, io);
|
||||
});
|
||||
|
||||
// Socket.IO connection handler
|
||||
io.on('connection', socket => {
|
||||
logger.info(`New socket connection: ${socket.id}`);
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
logger.info(`Socket disconnected: ${socket.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
// Start the server
|
||||
const PORT = config.port;
|
||||
server.listen(PORT, () => {
|
||||
logger.info(`Server started successfully on http://localhost:${PORT}`);
|
||||
});
|
||||
@ -1,56 +1,46 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { RoomService } from '../services/room.service.js';
|
||||
import { Logger } from '../utils/logger.js';
|
||||
import { MeetRoomOptions } from '../../../typings/src/room.js';
|
||||
import { getAllRooms, createRoom, deleteRoom } from '../services/roomService';
|
||||
|
||||
// Initialize logger
|
||||
const logger = new Logger('HomeController');
|
||||
export const getHome = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { rooms } = await getAllRooms();
|
||||
|
||||
/**
|
||||
* Renders the home page with available rooms
|
||||
*/
|
||||
export const renderHomePage = async (req: Request, res: Response): Promise<void> => {
|
||||
const roomService = RoomService.getInstance();
|
||||
//sort rooms by newest first
|
||||
rooms.sort((a, b) => {
|
||||
const dateA = new Date(a.creationDate);
|
||||
const dateB = new Date(b.creationDate);
|
||||
return dateB.getTime() - dateA.getTime();
|
||||
});
|
||||
|
||||
try {
|
||||
logger.info('Rendering home page');
|
||||
const rooms = await roomService.getAllRooms();
|
||||
res.render('home', { rooms });
|
||||
} catch (error) {
|
||||
logger.error('Error fetching rooms:', error);
|
||||
res.render('home', { rooms: [], error: 'Failed to load rooms' });
|
||||
}
|
||||
console.log(`Rooms fetched: ${rooms.length}`);
|
||||
res.render('index', { rooms });
|
||||
} catch (error) {
|
||||
console.error('Error fetching rooms:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new room based on form data
|
||||
*/
|
||||
export const createRoom = async (req: Request, res: Response): Promise<void> => {
|
||||
const roomService = RoomService.getInstance();
|
||||
|
||||
try {
|
||||
// Extract values from request body
|
||||
const { roomIdPrefix, autoDeletionDate } = req.body;
|
||||
|
||||
logger.info(`Creating room with prefix: ${roomIdPrefix}`);
|
||||
|
||||
const roomData: MeetRoomOptions = {
|
||||
roomIdPrefix,
|
||||
autoDeletionDate: new Date(autoDeletionDate).getTime(),
|
||||
};
|
||||
|
||||
const newRoom = await roomService.createRoom(roomData);
|
||||
|
||||
if (!newRoom) {
|
||||
throw new Error('Room creation failed');
|
||||
}
|
||||
|
||||
logger.info(`Room created successfully: ${newRoom.roomId}`);
|
||||
|
||||
// Redirect to home page showing the new room
|
||||
await renderHomePage(req, res);
|
||||
} catch (error) {
|
||||
logger.error('Room creation error:', error);
|
||||
res.status(500).json({ message: 'Error creating a room', error: (error as Error).message });
|
||||
}
|
||||
export const postCreateRoom = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { roomIdPrefix, autoDeletionDate } = req.body;
|
||||
await createRoom({ roomIdPrefix, autoDeletionDate });
|
||||
res.redirect('/');
|
||||
} catch (error) {
|
||||
console.error('Error creating room:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const postDeleteRoom = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { roomId } = req.body;
|
||||
await deleteRoom(roomId);
|
||||
res.redirect('/');
|
||||
} catch (error) {
|
||||
console.error('Error deleting room:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
57
testapp/src/controllers/roomController.ts
Normal file
57
testapp/src/controllers/roomController.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Server as IOServer } from 'socket.io';
|
||||
import { ParticipantRole } from '../../../typings/src/participant';
|
||||
// @ts-ignore
|
||||
import { MeetWebhookEvent } from '../../../typings/src/webhook.model';
|
||||
|
||||
interface JoinRoomRequest {
|
||||
participantRole: ParticipantRole;
|
||||
roomUrl: string;
|
||||
participantName?: string;
|
||||
}
|
||||
|
||||
export const joinRoom = (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
participantRole,
|
||||
roomUrl,
|
||||
participantName = 'User',
|
||||
} = req.body as JoinRoomRequest;
|
||||
if (!roomUrl) {
|
||||
throw new Error('Room URL is required.');
|
||||
}
|
||||
res.render('room', {
|
||||
roomUrl,
|
||||
participantRole,
|
||||
participantName,
|
||||
isModerator: participantRole === 'moderator',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error joining room:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const handleWebhook = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
io: IOServer
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const webhookEvent = req.body as MeetWebhookEvent;
|
||||
|
||||
// Log event details
|
||||
console.info(`Webhook received: ${webhookEvent.event}`);
|
||||
|
||||
// Broadcast to all connected clients
|
||||
io.emit('webhookEvent', webhookEvent);
|
||||
res.status(200).send('Webhook received');
|
||||
} catch (error) {
|
||||
console.error('Error handling webhook:', error);
|
||||
res.status(400).json({
|
||||
message: 'Error handling webhook',
|
||||
error: (error as Error).message,
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,73 +0,0 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Server } from 'socket.io';
|
||||
import { ConfigService } from '../services/config.service.js';
|
||||
import { Logger } from '../utils/logger.js';
|
||||
import { MeetWebhookEvent } from '../../../typings/src/webhook.model.js';
|
||||
import { ParticipantRole } from '../../../typings/src/participant.js';
|
||||
|
||||
// Initialize logger
|
||||
const logger = new Logger('VideoRoomController');
|
||||
|
||||
/**
|
||||
* Join Room request body interface
|
||||
*/
|
||||
interface JoinRoomRequest {
|
||||
participantRole: ParticipantRole;
|
||||
roomUrl: string;
|
||||
participantName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles joining a room with the specified role
|
||||
*/
|
||||
export const joinRoom = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { participantRole, roomUrl, participantName = 'User' } = req.body as JoinRoomRequest;
|
||||
|
||||
if (!roomUrl) {
|
||||
throw new Error('Room URL is required.');
|
||||
}
|
||||
|
||||
logger.info(`Joining room as ${participantRole}: ${roomUrl}`);
|
||||
|
||||
const config = ConfigService.getInstance();
|
||||
|
||||
res.render('videoRoom', {
|
||||
roomUrl,
|
||||
participantRole,
|
||||
participantName,
|
||||
isModerator: participantRole === 'moderator',
|
||||
apiKey: config.apiKey,
|
||||
});
|
||||
|
||||
logger.info(`Successfully rendered video room for ${participantName}`);
|
||||
} catch (error) {
|
||||
logger.error('Error joining room:', error);
|
||||
res.status(400).json({ message: 'Error joining room', error: (error as Error).message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles incoming webhook events from OpenVidu Meet
|
||||
*/
|
||||
export const handleWebhook = async (req: Request, res: Response, io: Server): Promise<void> => {
|
||||
try {
|
||||
const webhookEvent = req.body as MeetWebhookEvent;
|
||||
|
||||
// Log event details
|
||||
logger.info(`Webhook received: ${webhookEvent.event}`, {
|
||||
type: webhookEvent.event,
|
||||
timestamp: webhookEvent.creationDate,
|
||||
data: webhookEvent.data,
|
||||
});
|
||||
|
||||
// Broadcast to all connected clients
|
||||
io.emit('webhookEvent', webhookEvent);
|
||||
logger.debug('Event broadcast to all clients');
|
||||
|
||||
res.status(200).send('Webhook received');
|
||||
} catch (error) {
|
||||
logger.error('Error handling webhook:', error);
|
||||
res.status(400).json({ message: 'Error handling webhook', error: (error as Error).message });
|
||||
}
|
||||
};
|
||||
48
testapp/src/index.ts
Normal file
48
testapp/src/index.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import { Server as IOServer } from 'socket.io';
|
||||
import path from 'path';
|
||||
import {
|
||||
getHome,
|
||||
postCreateRoom,
|
||||
postDeleteRoom,
|
||||
} from './controllers/homeController';
|
||||
import { handleWebhook, joinRoom } from './controllers/roomController';
|
||||
import { configService } from './services/configService';
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = new IOServer(server);
|
||||
|
||||
// View engine setup
|
||||
app.engine('mustache', require('mustache-express')());
|
||||
app.set('views', path.join(__dirname, '../public/views'));
|
||||
app.set('view engine', 'mustache');
|
||||
|
||||
// Static assets
|
||||
app.use(express.static(path.join(__dirname, '../public')));
|
||||
|
||||
// Parse URL-encoded bodies (for form submissions)
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Routes
|
||||
app.get('/', getHome);
|
||||
app.get('/room', joinRoom);
|
||||
app.post('/room', postCreateRoom);
|
||||
app.post('/room/delete', postDeleteRoom);
|
||||
app.post('/join-room', joinRoom);
|
||||
app.post('/webhook', (req, res) => {
|
||||
handleWebhook(req, res, io);
|
||||
});
|
||||
|
||||
const PORT = configService.port;
|
||||
server.listen(PORT, () => {
|
||||
console.log('-----------------------------------------');
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
console.log(`Meet API URL: ${configService.meetApiUrl}`);
|
||||
console.log('-----------------------------------------');
|
||||
console.log('');
|
||||
console.log('Visit http://localhost:3000/ to access the app');
|
||||
});
|
||||
@ -1,50 +0,0 @@
|
||||
import { Logger } from '../utils/logger.js';
|
||||
|
||||
/**
|
||||
* Service for handling configuration and environment variables
|
||||
*/
|
||||
export class ConfigService {
|
||||
private static instance: ConfigService;
|
||||
private logger: Logger;
|
||||
|
||||
// Environment variables with defaults
|
||||
public readonly meetApiUrl: string;
|
||||
public readonly apiKey: string;
|
||||
public readonly port: number;
|
||||
|
||||
private constructor() {
|
||||
this.logger = new Logger('ConfigService');
|
||||
|
||||
// Load environment variables with defaults
|
||||
this.meetApiUrl = process.env.OPENVIDU_MEET_URL || 'http://localhost:6080/meet/api/v1';
|
||||
this.apiKey = process.env.API_KEY || 'meet-api-key';
|
||||
this.port = parseInt(process.env.PORT || '5080', 10);
|
||||
|
||||
this.logger.debug('Configuration loaded:', {
|
||||
meetApiUrl: this.meetApiUrl,
|
||||
port: this.port,
|
||||
// Don't log sensitive data like API keys
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance of ConfigService
|
||||
*/
|
||||
public static getInstance(): ConfigService {
|
||||
if (!ConfigService.instance) {
|
||||
ConfigService.instance = new ConfigService();
|
||||
}
|
||||
return ConfigService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API headers with authentication
|
||||
*/
|
||||
public getApiHeaders(): Record<string, string> {
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-KEY': this.apiKey
|
||||
};
|
||||
}
|
||||
}
|
||||
17
testapp/src/services/configService.ts
Normal file
17
testapp/src/services/configService.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export class ConfigService {
|
||||
public meetApiUrl: string;
|
||||
public apiKey: string;
|
||||
public port: number;
|
||||
|
||||
constructor() {
|
||||
this.meetApiUrl = process.env.OPENVIDU_MEET_URL || 'http://localhost:6080/meet/api/v1';
|
||||
this.apiKey = process.env.API_KEY || 'meet-api-key';
|
||||
this.port = parseInt(process.env.PORT || '5080', 10);
|
||||
}
|
||||
}
|
||||
|
||||
export const configService = new ConfigService();
|
||||
4
testapp/src/services/index.ts
Normal file
4
testapp/src/services/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// src/services/index.ts
|
||||
|
||||
// Aquí puedes exportar servicios de negocio
|
||||
export {};
|
||||
@ -1,102 +0,0 @@
|
||||
import { ConfigService } from './config.service.js';
|
||||
import { Logger } from '../utils/logger.js';
|
||||
import { MeetRoom, MeetRoomOptions } from '../../../typings/src/room.js';
|
||||
|
||||
/**
|
||||
* Service for handling Room-related API operations
|
||||
*/
|
||||
export class RoomService {
|
||||
private static instance: RoomService;
|
||||
private config: ConfigService;
|
||||
private logger: Logger;
|
||||
|
||||
private constructor() {
|
||||
this.config = ConfigService.getInstance();
|
||||
this.logger = new Logger('RoomService');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance of RoomService
|
||||
*/
|
||||
public static getInstance(): RoomService {
|
||||
if (!RoomService.instance) {
|
||||
RoomService.instance = new RoomService();
|
||||
}
|
||||
return RoomService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all rooms from the API
|
||||
*/
|
||||
public async getAllRooms(): Promise<MeetRoom[]> {
|
||||
try {
|
||||
this.logger.info('Fetching all rooms from API');
|
||||
const response = await fetch(`${this.config.meetApiUrl}/rooms`, {
|
||||
headers: this.config.getApiHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch rooms: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data: { pagination: any; rooms: MeetRoom[] } = await response.json();
|
||||
this.logger.debug(`Retrieved ${data.rooms.length} rooms`);
|
||||
return data.rooms;
|
||||
} catch (error) {
|
||||
this.logger.error('Error fetching rooms:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new room
|
||||
*/
|
||||
public async createRoom(roomData: MeetRoomOptions): Promise<MeetRoom | null> {
|
||||
try {
|
||||
this.logger.info(`Creating new room with prefix: ${roomData.roomIdPrefix}`);
|
||||
const response = await fetch(`${this.config.meetApiUrl}/rooms`, {
|
||||
method: 'POST',
|
||||
headers: this.config.getApiHeaders(),
|
||||
body: JSON.stringify(roomData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || 'Room creation failed');
|
||||
}
|
||||
|
||||
const newRoom: MeetRoom = await response.json();
|
||||
this.logger.info(`Room created successfully: ${newRoom.roomId}`);
|
||||
return newRoom;
|
||||
} catch (error) {
|
||||
this.logger.error('Error creating room:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific room by ID
|
||||
*/
|
||||
public async getRoomById(roomId: string): Promise<MeetRoom | null> {
|
||||
try {
|
||||
this.logger.info(`Fetching room with id: ${roomId}`);
|
||||
const response = await fetch(`${this.config.meetApiUrl}/rooms/${roomId}`, {
|
||||
headers: this.config.getApiHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
this.logger.warn(`Room not found: ${roomId}`);
|
||||
return null;
|
||||
}
|
||||
throw new Error(`Failed to fetch room: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const room = await response.json();
|
||||
return room;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error fetching room ${roomId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
testapp/src/services/roomService.ts
Normal file
29
testapp/src/services/roomService.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { get, post, del } from '../utils/http';
|
||||
import { configService } from './configService';
|
||||
// @ts-ignore
|
||||
import { MeetRoom, MeetRoomOptions } from '../../../typings/src/room';
|
||||
|
||||
export async function getAllRooms(): Promise<{pagination:any, rooms: MeetRoom[]}> {
|
||||
const url = `${configService.meetApiUrl}/rooms`;
|
||||
return get<{pagination:any, rooms: MeetRoom[]}>(url, {
|
||||
headers: { 'x-api-key': configService.apiKey },
|
||||
});
|
||||
}
|
||||
|
||||
export async function createRoom(roomData: MeetRoomOptions): Promise<MeetRoom> {
|
||||
const url = `${configService.meetApiUrl}/rooms`;
|
||||
// Default to 1 hour if autoDeletionDate is not provided
|
||||
if(!roomData.autoDeletionDate) roomData.autoDeletionDate = new Date(Date.now() + 60 * 61 * 1000).getTime();
|
||||
console.log('Creating room with options:', roomData);
|
||||
return post<MeetRoom>(url, {
|
||||
headers: { 'x-api-key': configService.apiKey },
|
||||
body: roomData,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteRoom(roomId: string): Promise<void> {
|
||||
const url = `${configService.meetApiUrl}/rooms/${roomId}`;
|
||||
await del<void>(url, {
|
||||
headers: { 'x-api-key': configService.apiKey },
|
||||
});
|
||||
}
|
||||
17
testapp/src/types/global.d.ts
vendored
17
testapp/src/types/global.d.ts
vendored
@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Global type declarations for browser libraries
|
||||
*/
|
||||
|
||||
declare const io: () => SocketIOClient.Socket;
|
||||
|
||||
interface Window {
|
||||
meetApiKey: string;
|
||||
}
|
||||
|
||||
declare namespace SocketIOClient {
|
||||
interface Socket {
|
||||
on(event: string, callback: (data: any) => void): Socket;
|
||||
emit(event: string, data: any): Socket;
|
||||
disconnect(): void;
|
||||
}
|
||||
}
|
||||
0
testapp/src/types/index.ts
Normal file
0
testapp/src/types/index.ts
Normal file
@ -1,65 +0,0 @@
|
||||
/**
|
||||
* Common utility functions used across the application
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format a date to a human-readable string
|
||||
* @param timestamp - Unix timestamp in milliseconds
|
||||
*/
|
||||
export function formatDate(timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a human-readable relative time string (e.g., "5 minutes ago")
|
||||
* @param timestamp - Unix timestamp in milliseconds
|
||||
*/
|
||||
export function getRelativeTimeString(timestamp: number): string {
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) return `${days} day${days === 1 ? '' : 's'} ago`;
|
||||
if (hours > 0) return `${hours} hour${hours === 1 ? '' : 's'} ago`;
|
||||
if (minutes > 0) return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
|
||||
return `${seconds} second${seconds === 1 ? '' : 's'} ago`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string to title case (e.g., "hello world" -> "Hello World")
|
||||
* @param str - String to convert
|
||||
*/
|
||||
export function toTitleCase(str: string): string {
|
||||
return str.replace(
|
||||
/\w\S*/g,
|
||||
txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely parse JSON without throwing an exception
|
||||
* @param jsonString - JSON string to parse
|
||||
* @param fallback - Default value if parsing fails
|
||||
*/
|
||||
export function safeJsonParse<T>(jsonString: string, fallback: T): T {
|
||||
try {
|
||||
return JSON.parse(jsonString) as T;
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse JSON:', e);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate a string to a maximum length with ellipsis
|
||||
* @param str - String to truncate
|
||||
* @param maxLength - Maximum length
|
||||
*/
|
||||
export function truncateString(str: string, maxLength: number): string {
|
||||
if (str.length <= maxLength) return str;
|
||||
return str.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
// Get directory name in ES modules
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Define paths
|
||||
const projectRoot = path.join(__dirname, '..', '..');
|
||||
const viewsDir = path.join(projectRoot, 'views');
|
||||
const distViewsDir = path.join(projectRoot, 'dist', 'views');
|
||||
const cssDir = path.join(projectRoot, 'public', 'css');
|
||||
const distCssDir = path.join(projectRoot, 'dist', 'public', 'css');
|
||||
|
||||
// Create dist directories if they don't exist
|
||||
function ensureDirectoryExists(directory: string): void {
|
||||
if (!fs.existsSync(directory)) {
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
console.log(`Created directory: ${directory}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy files from source to destination
|
||||
function copyFiles(sourceDir: string, destDir: string, extension: string): void {
|
||||
ensureDirectoryExists(destDir);
|
||||
|
||||
const files = fs.readdirSync(sourceDir).filter(file => file.endsWith(extension));
|
||||
|
||||
files.forEach(file => {
|
||||
const sourcePath = path.join(sourceDir, file);
|
||||
const destPath = path.join(destDir, file);
|
||||
|
||||
fs.copyFileSync(sourcePath, destPath);
|
||||
console.log(`Copied ${sourcePath} to ${destPath}`);
|
||||
});
|
||||
|
||||
console.log(`Copied ${files.length} ${extension} files`);
|
||||
}
|
||||
|
||||
// Copy view templates
|
||||
console.log('Copying Mustache templates...');
|
||||
copyFiles(viewsDir, distViewsDir, '.mustache');
|
||||
|
||||
// Copy CSS files
|
||||
console.log('Copying CSS files...');
|
||||
copyFiles(cssDir, distCssDir, '.css');
|
||||
|
||||
console.log('All static assets have been copied to dist.');
|
||||
42
testapp/src/utils/http.ts
Normal file
42
testapp/src/utils/http.ts
Normal file
@ -0,0 +1,42 @@
|
||||
export interface RequestOptions {
|
||||
headers?: Record<string, string>;
|
||||
queryParams?: Record<string, string>;
|
||||
body?: any;
|
||||
}
|
||||
|
||||
async function buildUrl(url: string, queryParams?: Record<string, string>): Promise<string> {
|
||||
if (!queryParams) return url;
|
||||
const params = new URLSearchParams(queryParams as Record<string, string>).toString();
|
||||
return `${url}?${params}`;
|
||||
}
|
||||
|
||||
async function request<T>(method: string, url: string, options: RequestOptions = {}): Promise<T> {
|
||||
const fullUrl = await buildUrl(url, options.queryParams);
|
||||
const fetchOptions: RequestInit = {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
|
||||
body: options.body ? JSON.stringify(options.body) : undefined
|
||||
};
|
||||
const response = await fetch(fullUrl, fetchOptions);
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${text}`);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export function get<T>(url: string, options?: Omit<RequestOptions, 'body'>): Promise<T> {
|
||||
return request<T>('GET', url, options || {});
|
||||
}
|
||||
|
||||
export function post<T>(url: string, options?: Omit<RequestOptions, 'body'> & { body: any }): Promise<T> {
|
||||
return request<T>('POST', url, options as RequestOptions);
|
||||
}
|
||||
|
||||
export function put<T>(url: string, options?: Omit<RequestOptions, 'body'> & { body: any }): Promise<T> {
|
||||
return request<T>('PUT', url, options as RequestOptions);
|
||||
}
|
||||
|
||||
export function del<T>(url: string, options?: Omit<RequestOptions, 'body'>): Promise<T> {
|
||||
return request<T>('DELETE', url, options || {});
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
/**
|
||||
* Logger utility for consistent logging across the application
|
||||
*/
|
||||
|
||||
/**
|
||||
* Log levels for the application
|
||||
*/
|
||||
export enum LogLevel {
|
||||
DEBUG = 0,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
ERROR = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Current log level for the application
|
||||
* Can be set via environment variable LOG_LEVEL
|
||||
*/
|
||||
const currentLogLevel = (() => {
|
||||
const envLevel = process.env.LOG_LEVEL?.toUpperCase();
|
||||
switch (envLevel) {
|
||||
case 'DEBUG': return LogLevel.DEBUG;
|
||||
case 'INFO': return LogLevel.INFO;
|
||||
case 'WARN': return LogLevel.WARN;
|
||||
case 'ERROR': return LogLevel.ERROR;
|
||||
default: return LogLevel.INFO; // Default level
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* Logger class for consistent log formatting
|
||||
*/
|
||||
export class Logger {
|
||||
private name: string;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a log message with timestamp and context
|
||||
*/
|
||||
private formatMessage(message: string): string {
|
||||
const timestamp = new Date().toISOString();
|
||||
return `[${timestamp}] [${this.name}] ${message}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log debug level message
|
||||
*/
|
||||
debug(message: string, ...args: any[]): void {
|
||||
if (currentLogLevel <= LogLevel.DEBUG) {
|
||||
console.debug(this.formatMessage(message), ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log info level message
|
||||
*/
|
||||
info(message: string, ...args: any[]): void {
|
||||
if (currentLogLevel <= LogLevel.INFO) {
|
||||
console.info(this.formatMessage(message), ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log warning level message
|
||||
*/
|
||||
warn(message: string, ...args: any[]): void {
|
||||
if (currentLogLevel <= LogLevel.WARN) {
|
||||
console.warn(this.formatMessage(message), ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error level message
|
||||
*/
|
||||
error(message: string, ...args: any[]): void {
|
||||
if (currentLogLevel <= LogLevel.ERROR) {
|
||||
console.error(this.formatMessage(message), ...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
testapp/tsconfig.client.json
Normal file
12
testapp/tsconfig.client.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "ES6",
|
||||
"moduleResolution": "node",
|
||||
"rootDir": "public/ts",
|
||||
"outDir": "public/js",
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["public/ts/**/*"]
|
||||
}
|
||||
@ -1,22 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"target": "ES6",
|
||||
"module": "commonjs",
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true, // Allow JavaScript files to be compiled
|
||||
"checkJs": true, // Type check JavaScript files
|
||||
"lib": ["DOM", "ES2020", "DOM.Iterable"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*", "server.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"ts-node": {
|
||||
"esm": true,
|
||||
"experimentalSpecifierResolution": "node"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user