Merge pull request 'text-background-color-source' (#10) from text-background-color-source into main
All checks were successful
Lint and Build / build (push) Successful in 2m49s

Reviewed-on: #10
This commit is contained in:
Decobus 2025-07-22 23:36:52 +03:00
commit 7302a38ceb
12 changed files with 55 additions and 54 deletions

View file

@ -27,8 +27,6 @@ describe('/api/streams', () => {
mockDb.all.mockResolvedValue(mockStreams);
const _response = await GET();
expect(mockDb.all).toHaveBeenCalledWith(
expect.stringContaining('SELECT * FROM')
);
@ -40,8 +38,6 @@ describe('/api/streams', () => {
it('returns empty array when no streams exist', async () => {
mockDb.all.mockResolvedValue([]);
const _response = await GET();
const { NextResponse } = require('next/server');
expect(NextResponse.json).toHaveBeenCalledWith([]);
});
@ -50,8 +46,6 @@ describe('/api/streams', () => {
const dbError = new Error('Database connection failed');
mockDb.all.mockRejectedValue(dbError);
const _response = await GET();
const { NextResponse } = require('next/server');
expect(NextResponse.json).toHaveBeenCalledWith(
{ error: 'Failed to fetch streams' },
@ -64,8 +58,6 @@ describe('/api/streams', () => {
const { getDatabase } = require('@/lib/database');
getDatabase.mockRejectedValue(connectionError);
const _response = await GET();
const { NextResponse } = require('next/server');
expect(NextResponse.json).toHaveBeenCalledWith(
{ error: 'Failed to fetch streams' },

View file

@ -46,8 +46,6 @@ describe('/api/teams', () => {
mockDb.all.mockResolvedValue(mockTeams);
const _response = await GET();
expect(mockDb.all).toHaveBeenCalledWith(
expect.stringContaining('SELECT * FROM')
);
@ -59,8 +57,6 @@ describe('/api/teams', () => {
it('returns empty array when no teams exist', async () => {
mockDb.all.mockResolvedValue([]);
const _response = await GET();
const { createSuccessResponse } = require('@/lib/apiHelpers');
expect(createSuccessResponse).toHaveBeenCalledWith([]);
});
@ -69,8 +65,6 @@ describe('/api/teams', () => {
const dbError = new Error('Table does not exist');
mockDb.all.mockRejectedValue(dbError);
const _response = await GET();
const { createDatabaseError } = require('@/lib/apiHelpers');
expect(createDatabaseError).toHaveBeenCalledWith('fetch teams', dbError);
});
@ -80,8 +74,6 @@ describe('/api/teams', () => {
const { getDatabase } = require('@/lib/database');
getDatabase.mockRejectedValue(connectionError);
const _response = await GET();
const { createDatabaseError } = require('@/lib/apiHelpers');
expect(createDatabaseError).toHaveBeenCalledWith('fetch teams', connectionError);
});

View file

@ -1,4 +1,3 @@
import { NextResponse } from 'next/server';
import { getDatabase } from '../../../lib/database';
import { TABLE_NAMES } from '../../../lib/constants';
import { createSuccessResponse, createDatabaseError, withErrorHandling } from '../../../lib/apiHelpers';

View file

@ -1,4 +1,3 @@
import { NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
import { createSuccessResponse, createErrorResponse, withErrorHandling } from '../../../lib/apiHelpers';

View file

@ -1,4 +1,4 @@
import { NextRequest, NextResponse } from 'next/server';
import { NextRequest } from 'next/server';
import { getDatabase } from '../../../lib/database';
import { TABLE_NAMES } from '../../../lib/constants';
import { createErrorResponse, createSuccessResponse, createDatabaseError, withErrorHandling } from '../../../lib/apiHelpers';

View file

@ -1,4 +1,3 @@
import { NextResponse } from 'next/server';
import { getDatabase } from '../../../lib/database';
import { StreamWithTeam } from '@/types';
import { TABLE_NAMES } from '../../../lib/constants';

View file

@ -126,7 +126,7 @@ export async function DELETE(
);
// Delete the team
const result = await db.run(
await db.run(
`DELETE FROM ${TABLE_NAMES.TEAMS} WHERE team_id = ?`,
[teamId]
);

View file

@ -1,7 +1,6 @@
'use client';
import { useState, useEffect, useMemo, useCallback } from 'react';
import Link from 'next/link';
import Dropdown from '@/components/Dropdown';
import { useToast } from '@/lib/useToast';
import { ToastContainer } from '@/components/Toast';
@ -75,8 +74,11 @@ export default function Home() {
activeRes.json()
]);
setStreams(streamsData.data);
setActiveSources(activeData.data);
// Handle both old and new API response formats
const streams = streamsData.success ? streamsData.data : streamsData;
const activeSources = activeData.success ? activeData.data : activeData;
setStreams(streams);
setActiveSources(activeSources);
} catch (error) {
console.error('Error fetching data:', error);
showError('Failed to Load Data', 'Could not fetch streams. Please refresh the page.');

View file

@ -66,9 +66,37 @@ export default function AddStream() {
}, [fetchData]);
const extractTwitchUsername = (input: string): string => {
const trimmed = input.trim();
// If it's a URL, extract username
const urlPatterns = [
/^https?:\/\/(www\.)?twitch\.tv\/([a-zA-Z0-9_]+)\/?$/,
/^(www\.)?twitch\.tv\/([a-zA-Z0-9_]+)\/?$/,
/^twitch\.tv\/([a-zA-Z0-9_]+)\/?$/
];
for (const pattern of urlPatterns) {
const match = trimmed.match(pattern);
if (match) {
return match[match.length - 1]; // Last capture group is always the username
}
}
// Otherwise assume it's just a username
return trimmed;
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
// Special handling for twitch_username to extract from URL if needed
if (name === 'twitch_username') {
const username = extractTwitchUsername(value);
setFormData((prev) => ({ ...prev, [name]: username }));
} else {
setFormData((prev) => ({ ...prev, [name]: value }));
}
// Clear validation error when user starts typing
if (validationErrors[name]) {
@ -213,7 +241,7 @@ export default function AddStream() {
{/* Twitch Username */}
<div>
<label className="block text-white font-semibold mb-3">
Twitch Username
Twitch Username or URL
</label>
<input
type="text"
@ -224,7 +252,7 @@ export default function AddStream() {
className={`input ${
validationErrors.twitch_username ? 'border-red-500/60 bg-red-500/10' : ''
}`}
placeholder="Enter Twitch username"
placeholder="Enter username or paste full Twitch URL (e.g., 'streamer' or 'https://twitch.tv/streamer')"
/>
{validationErrors.twitch_username && (
<div className="text-red-400 text-sm mt-2">

View file

@ -50,7 +50,9 @@ export default function Footer() {
try {
const response = await fetch('/api/counts');
const data = await response.json();
setCounts(data.data);
// Handle both old and new API response formats
const countsData = data.success ? data.data : data;
setCounts(countsData);
} catch (error) {
console.error('Failed to fetch counts:', error);
}
@ -79,12 +81,14 @@ export default function Footer() {
<div className="grid-2">
{/* Connection Status */}
<div>
<h3 className="font-semibold mb-4">OBS Studio</h3>
<div className="flex items-center gap-2 mb-4">
<div className="flex items-center gap-3 mb-4">
<div className={`status-dot ${obsStatus?.connected ? 'connected' : 'disconnected'}`}></div>
<span className="text-sm">
{obsStatus?.connected ? 'Connected' : 'Disconnected'}
</span>
<div>
<h3 className="font-semibold">OBS Studio</h3>
<p className="text-sm opacity-60">
{obsStatus?.connected ? 'Connected' : 'Disconnected'}
</p>
</div>
</div>
{obsStatus && (

View file

@ -32,7 +32,7 @@ describe('apiHelpers', () => {
describe('createErrorResponse', () => {
it('creates error response with default status 500', () => {
const _response = createErrorResponse('Test Error');
createErrorResponse('Test Error');
expect(NextResponse.json).toHaveBeenCalledWith(
expect.objectContaining({
@ -44,7 +44,7 @@ describe('apiHelpers', () => {
});
it('creates error response with custom status and message', () => {
const _response = createErrorResponse('Test Error', 400, 'Custom message', { detail: 'extra' });
createErrorResponse('Test Error', 400, 'Custom message', { detail: 'extra' });
expect(NextResponse.json).toHaveBeenCalledWith(
expect.objectContaining({
@ -81,7 +81,7 @@ describe('apiHelpers', () => {
describe('createSuccessResponse', () => {
it('creates success response with default status 200', () => {
const data = { test: 'data' };
const _response = createSuccessResponse(data);
createSuccessResponse(data);
expect(NextResponse.json).toHaveBeenCalledWith(
expect.objectContaining({
@ -95,7 +95,7 @@ describe('apiHelpers', () => {
it('creates success response with custom status', () => {
const data = { id: 1, name: 'test' };
const _response = createSuccessResponse(data, 201);
createSuccessResponse(data, 201);
expect(NextResponse.json).toHaveBeenCalledWith(
expect.objectContaining({

View file

@ -244,20 +244,6 @@ async function getAvailableTextInputKind() {
}
}
async function inspectTextSourceProperties(inputKind) {
try {
const obsClient = await getOBSClient();
// Get the default properties for this input kind
const { inputProperties } = await obsClient.call('GetInputDefaultSettings', { inputKind });
console.log(`Default properties for ${inputKind}:`, JSON.stringify(inputProperties, null, 2));
return inputProperties;
} catch (error) {
console.error('Error inspecting text source properties:', error.message);
return null;
}
}
async function createTextSource(sceneName, textSourceName, text) {
try {
@ -394,7 +380,7 @@ async function createStreamGroup(groupName, streamName, teamName, url) {
try {
await obsClient.call('CreateScene', { sceneName: streamGroupName });
console.log(`Created nested scene "${streamGroupName}" for stream grouping`);
} catch (sceneError) {
} catch (error) {
console.log(`Nested scene "${streamGroupName}" might already exist`);
}
@ -457,7 +443,7 @@ async function createStreamGroup(groupName, streamName, teamName, url) {
sourceName: colorSourceName
});
console.log(`Added color source background "${colorSourceName}" to nested scene`);
} catch (e) {
} catch (error) {
console.log('Color source background might already be in nested scene');
}
@ -467,7 +453,7 @@ async function createStreamGroup(groupName, streamName, teamName, url) {
sourceName: textSourceName
});
console.log(`Added text source "${textSourceName}" to nested scene`);
} catch (e) {
} catch (error) {
console.log('Text source might already be in nested scene');
}