text-background-color-source #10

Merged
deco merged 7 commits from text-background-color-source into main 2025-07-22 23:36:53 +03:00
12 changed files with 55 additions and 54 deletions

View file

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

View file

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

View file

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

View file

@ -1,4 +1,3 @@
import { NextResponse } from 'next/server';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { createSuccessResponse, createErrorResponse, withErrorHandling } from '../../../lib/apiHelpers'; 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 { getDatabase } from '../../../lib/database';
import { TABLE_NAMES } from '../../../lib/constants'; import { TABLE_NAMES } from '../../../lib/constants';
import { createErrorResponse, createSuccessResponse, createDatabaseError, withErrorHandling } from '../../../lib/apiHelpers'; 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 { getDatabase } from '../../../lib/database';
import { StreamWithTeam } from '@/types'; import { StreamWithTeam } from '@/types';
import { TABLE_NAMES } from '../../../lib/constants'; import { TABLE_NAMES } from '../../../lib/constants';

View file

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

View file

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

View file

@ -66,9 +66,37 @@ export default function AddStream() {
}, [fetchData]); }, [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 handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
// 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 })); setFormData((prev) => ({ ...prev, [name]: value }));
}
// Clear validation error when user starts typing // Clear validation error when user starts typing
if (validationErrors[name]) { if (validationErrors[name]) {
@ -213,7 +241,7 @@ export default function AddStream() {
{/* Twitch Username */} {/* Twitch Username */}
<div> <div>
<label className="block text-white font-semibold mb-3"> <label className="block text-white font-semibold mb-3">
Twitch Username Twitch Username or URL
</label> </label>
<input <input
type="text" type="text"
@ -224,7 +252,7 @@ export default function AddStream() {
className={`input ${ className={`input ${
validationErrors.twitch_username ? 'border-red-500/60 bg-red-500/10' : '' 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 && ( {validationErrors.twitch_username && (
<div className="text-red-400 text-sm mt-2"> <div className="text-red-400 text-sm mt-2">

View file

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

View file

@ -32,7 +32,7 @@ describe('apiHelpers', () => {
describe('createErrorResponse', () => { describe('createErrorResponse', () => {
it('creates error response with default status 500', () => { it('creates error response with default status 500', () => {
const _response = createErrorResponse('Test Error'); createErrorResponse('Test Error');
expect(NextResponse.json).toHaveBeenCalledWith( expect(NextResponse.json).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@ -44,7 +44,7 @@ describe('apiHelpers', () => {
}); });
it('creates error response with custom status and message', () => { 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(NextResponse.json).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@ -81,7 +81,7 @@ describe('apiHelpers', () => {
describe('createSuccessResponse', () => { describe('createSuccessResponse', () => {
it('creates success response with default status 200', () => { it('creates success response with default status 200', () => {
const data = { test: 'data' }; const data = { test: 'data' };
const _response = createSuccessResponse(data); createSuccessResponse(data);
expect(NextResponse.json).toHaveBeenCalledWith( expect(NextResponse.json).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@ -95,7 +95,7 @@ describe('apiHelpers', () => {
it('creates success response with custom status', () => { it('creates success response with custom status', () => {
const data = { id: 1, name: 'test' }; const data = { id: 1, name: 'test' };
const _response = createSuccessResponse(data, 201); createSuccessResponse(data, 201);
expect(NextResponse.json).toHaveBeenCalledWith( expect(NextResponse.json).toHaveBeenCalledWith(
expect.objectContaining({ 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) { async function createTextSource(sceneName, textSourceName, text) {
try { try {
@ -394,7 +380,7 @@ async function createStreamGroup(groupName, streamName, teamName, url) {
try { try {
await obsClient.call('CreateScene', { sceneName: streamGroupName }); await obsClient.call('CreateScene', { sceneName: streamGroupName });
console.log(`Created nested scene "${streamGroupName}" for stream grouping`); console.log(`Created nested scene "${streamGroupName}" for stream grouping`);
} catch (sceneError) { } catch (error) {
console.log(`Nested scene "${streamGroupName}" might already exist`); console.log(`Nested scene "${streamGroupName}" might already exist`);
} }
@ -457,7 +443,7 @@ async function createStreamGroup(groupName, streamName, teamName, url) {
sourceName: colorSourceName sourceName: colorSourceName
}); });
console.log(`Added color source background "${colorSourceName}" to nested scene`); 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'); console.log('Color source background might already be in nested scene');
} }
@ -467,7 +453,7 @@ async function createStreamGroup(groupName, streamName, teamName, url) {
sourceName: textSourceName sourceName: textSourceName
}); });
console.log(`Added text source "${textSourceName}" to nested scene`); console.log(`Added text source "${textSourceName}" to nested scene`);
} catch (e) { } catch (error) {
console.log('Text source might already be in nested scene'); console.log('Text source might already be in nested scene');
} }