diff --git a/.gitignore b/.gitignore index 8aef9fb..c0eb07c 100644 --- a/.gitignore +++ b/.gitignore @@ -86,8 +86,6 @@ logs files/*.db files/*.sqlite files/*.sqlite3 -# But include template database -!files/*.template.db # OS generated files .DS_Store diff --git a/CLAUDE.md b/CLAUDE.md index e9b1a8b..4a36acf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,12 +25,12 @@ This is a Next.js web application that controls multiple OBS Source Switchers. I - **Backend**: Next.js API routes - **Database**: SQLite with sqlite3 driver - **OBS Integration**: obs-websocket-js for WebSocket communication with OBS Studio -- **Styling**: Solarized Dark theme with CSS custom properties, Tailwind CSS utilities, and accessible glass morphism components +- **Styling**: Custom CSS with Tailwind CSS utilities and modern glass card components ### Project Structure - `/app` - Next.js App Router pages and API routes - `/api` - Backend API endpoints for stream management - - `/streams` - Streams management page (add new streams and view existing) + - `/add` - Streams management page (add new streams and view existing) - `/teams` - Team management page - `/edit/[id]` - Individual stream editing - `/components` - Reusable React components (Header, Footer, Dropdown) @@ -53,11 +53,7 @@ This is a Next.js web application that controls multiple OBS Source Switchers. I - WebSocket API for direct OBS control (source creation, status monitoring) - Text file system for OBS Source Switcher plugin integration (source switching) -4. **Solarized Dark Design System**: Accessible colorblind-friendly theme based on Solarized Dark palette with: - - High contrast ratios (7.5:1+) meeting WCAG AAA standards - - CSS custom properties for maintainable theming - - Glass morphism effects with proper backdrop blur - - Distinctive active navigation states for clear wayfinding +4. **Glass Morphism UI Architecture**: Modern design system with backdrop blur effects, gradient backgrounds, and responsive glass card components 5. **Screen Position Management**: Seven distinct screen positions (large, left, right, topLeft, topRight, bottomLeft, bottomRight) with individual source control diff --git a/README.md b/README.md index 9e47f56..5169f28 100644 --- a/README.md +++ b/README.md @@ -61,26 +61,11 @@ Without an API key, anyone on your network can control your OBS streams. ### Database Setup -The project includes an empty template database for easy setup: - ```bash -# Option 1: Use template database directly (development) -# Database will be created in ./files/sources.db -npm run create-sat-summer-2025-tables - -# Option 2: Set up custom database location (recommended) -# 1. Copy the template database -cp files/sources.template.db /path/to/your/database/sources.db - -# 2. Set environment variable in .env.local -echo "FILE_DIRECTORY=/path/to/your/database" >> .env.local - -# 3. Create tables in your custom database +# Create seasonal database tables npm run create-sat-summer-2025-tables ``` -**Template Database**: The repository includes `files/sources.template.db` with the proper schema but no data. Your local development database (`sources.db`) is automatically ignored by git to prevent committing personal data. - ## Development Commands ```bash diff --git a/app/streams/page.tsx b/app/add/page.tsx similarity index 63% rename from app/streams/page.tsx rename to app/add/page.tsx index e92feeb..5d7025d 100644 --- a/app/streams/page.tsx +++ b/app/add/page.tsx @@ -3,8 +3,6 @@ import { useState, useEffect } from 'react'; import Dropdown from '@/components/Dropdown'; import { Team } from '@/types'; -import { useToast } from '@/lib/useToast'; -import { ToastContainer } from '@/components/Toast'; interface Stream { id: number; @@ -24,9 +22,8 @@ export default function AddStream() { const [teams, setTeams] = useState<{id: number; name: string}[]>([]); const [streams, setStreams] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [message, setMessage] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); - const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({}); - const { toasts, removeToast, showSuccess, showError } = useToast(); // Fetch teams and streams on component mount useEffect(() => { @@ -44,22 +41,17 @@ export default function AddStream() { const teamsData = await teamsResponse.json(); const streamsData = await streamsResponse.json(); - // Handle both old and new API response formats - const teams = teamsData.success ? teamsData.data : teamsData; - const streams = streamsData.success ? streamsData.data : streamsData; - // Map the API data to the format required by the Dropdown setTeams( - teams.map((team: Team) => ({ + teamsData.map((team: Team) => ({ id: team.team_id, name: team.team_name, })) ); - setStreams(streams); + setStreams(streamsData); } catch (error) { console.error('Failed to fetch data:', error); - showError('Failed to Load Data', 'Could not fetch teams and streams. Please refresh the page.'); } finally { setIsLoading(false); } @@ -68,58 +60,16 @@ export default function AddStream() { const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); - - // Clear validation error when user starts typing - if (validationErrors[name]) { - setValidationErrors(prev => ({ ...prev, [name]: '' })); - } }; const handleTeamSelect = (teamId: number) => { // @ts-expect-error - team_id can be null or number in formData, but TypeScript expects only number setFormData((prev) => ({ ...prev, team_id: teamId })); - - // Clear validation error when user selects team - if (validationErrors.team_id) { - setValidationErrors(prev => ({ ...prev, team_id: '' })); - } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - - // Client-side validation - const errors: {[key: string]: string} = {}; - if (!formData.name.trim()) { - errors.name = 'Stream name is required'; - } else if (formData.name.trim().length < 2) { - errors.name = 'Stream name must be at least 2 characters'; - } - - if (!formData.obs_source_name.trim()) { - errors.obs_source_name = 'OBS source name is required'; - } - - if (!formData.url.trim()) { - errors.url = 'Stream URL is required'; - } else { - try { - new URL(formData.url); - } catch { - errors.url = 'Please enter a valid URL'; - } - } - - if (!formData.team_id) { - errors.team_id = 'Please select a team'; - } - - setValidationErrors(errors); - if (Object.keys(errors).length > 0) { - showError('Validation Error', 'Please fix the form errors'); - return; - } - + setMessage(''); setIsSubmitting(true); try { @@ -131,16 +81,15 @@ export default function AddStream() { const data = await response.json(); if (response.ok) { - showSuccess('Stream Added', `"${formData.name}" has been added successfully`); - setFormData({ name: '', obs_source_name: '', url: '', team_id: null }); - setValidationErrors({}); - fetchData(); + setMessage(data.message); + setFormData({ name: '', obs_source_name: '', url: '', team_id: null }); // Reset form + fetchData(); // Refresh the streams list } else { - showError('Failed to Add Stream', data.error || 'Unknown error occurred'); + setMessage(data.error || 'Something went wrong.'); } } catch (error) { console.error('Error adding stream:', error); - showError('Failed to Add Stream', 'Network error or server unavailable'); + setMessage('Failed to add stream.'); } finally { setIsSubmitting(false); } @@ -171,16 +120,9 @@ export default function AddStream() { value={formData.name} onChange={handleInputChange} required - className={`input ${ - validationErrors.name ? 'border-red-500/60 bg-red-500/10' : '' - }`} + className="input" placeholder="Enter a display name for the stream" /> - {validationErrors.name && ( -
- {validationErrors.name} -
- )} {/* OBS Source Name */} @@ -194,16 +136,9 @@ export default function AddStream() { value={formData.obs_source_name} onChange={handleInputChange} required - className={`input ${ - validationErrors.obs_source_name ? 'border-red-500/60 bg-red-500/10' : '' - }`} + className="input" placeholder="Enter the exact source name from OBS" /> - {validationErrors.obs_source_name && ( -
- {validationErrors.obs_source_name} -
- )} {/* URL */} @@ -217,50 +152,67 @@ export default function AddStream() { value={formData.url} onChange={handleInputChange} required - className={`input ${ - validationErrors.url ? 'border-red-500/60 bg-red-500/10' : '' - }`} + className="input" placeholder="https://example.com/stream" /> - {validationErrors.url && ( -
- {validationErrors.url} -
- )} - {/* Team Selection and Submit Button */} + {/* Team Selection */}
-
-
- - {validationErrors.team_id && ( -
- {validationErrors.team_id} -
- )} -
- -
+ +
+ + {/* Submit Button */} +
+
+ {/* Success/Error Message */} + {message && ( +
+
+
+
+ {message.includes('successfully') ? ( + + + + ) : ( + + + + )} +
+ {message} +
+
+
+ )} {/* Streams List */}
@@ -309,9 +261,6 @@ export default function AddStream() {
)} - - {/* Toast Notifications */} - ); } \ No newline at end of file diff --git a/app/api/__tests__/streams.test.ts b/app/api/__tests__/streams.test.ts deleted file mode 100644 index d4ef664..0000000 --- a/app/api/__tests__/streams.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { GET } from '../streams/route'; - -// Mock the database module -jest.mock('@/lib/database', () => ({ - getDatabase: jest.fn(), -})); - -describe('/api/streams', () => { - let mockDb: any; - - beforeEach(() => { - // Create mock database - mockDb = { - all: jest.fn(), - }; - - const { getDatabase } = require('@/lib/database'); - getDatabase.mockResolvedValue(mockDb); - }); - - describe('GET /api/streams', () => { - it('returns all streams successfully', async () => { - const mockStreams = [ - { id: 1, name: 'Stream 1', url: 'http://example.com/1', obs_source_name: 'Source 1', team_id: 1 }, - { id: 2, name: 'Stream 2', url: 'http://example.com/2', obs_source_name: 'Source 2', team_id: 2 }, - ]; - - mockDb.all.mockResolvedValue(mockStreams); - - const response = await GET(); - - expect(mockDb.all).toHaveBeenCalledWith( - expect.stringContaining('SELECT * FROM') - ); - - const { NextResponse } = require('next/server'); - expect(NextResponse.json).toHaveBeenCalledWith(mockStreams); - }); - - 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([]); - }); - - it('handles database errors gracefully', async () => { - 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' }, - { status: 500 } - ); - }); - - it('handles database connection errors', async () => { - const connectionError = new Error('Failed to connect to database'); - 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' }, - { status: 500 } - ); - }); - }); -}); \ No newline at end of file diff --git a/app/api/__tests__/teams.test.ts b/app/api/__tests__/teams.test.ts deleted file mode 100644 index 4f26d27..0000000 --- a/app/api/__tests__/teams.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { GET } from '../teams/route'; - -// Mock the database module -jest.mock('@/lib/database', () => ({ - getDatabase: jest.fn(), -})); - -// Mock the apiHelpers module -jest.mock('@/lib/apiHelpers', () => ({ - withErrorHandling: jest.fn((handler) => handler), - createSuccessResponse: jest.fn((data, status = 200) => ({ - data, - status, - json: async () => ({ success: true, data }), - })), - createDatabaseError: jest.fn((operation, error) => ({ - error: 'Database Error', - status: 500, - json: async () => ({ - error: 'Database Error', - message: `Database operation failed: ${operation}`, - }), - })), -})); - -describe('/api/teams', () => { - let mockDb: any; - - beforeEach(() => { - // Create mock database - mockDb = { - all: jest.fn(), - }; - - const { getDatabase } = require('@/lib/database'); - getDatabase.mockResolvedValue(mockDb); - }); - - describe('GET /api/teams', () => { - it('returns all teams successfully', async () => { - const mockTeams = [ - { team_id: 1, team_name: 'Team Alpha' }, - { team_id: 2, team_name: 'Team Beta' }, - { team_id: 3, team_name: 'Team Gamma' }, - ]; - - mockDb.all.mockResolvedValue(mockTeams); - - const response = await GET(); - - expect(mockDb.all).toHaveBeenCalledWith( - expect.stringContaining('SELECT * FROM') - ); - - const { createSuccessResponse } = require('@/lib/apiHelpers'); - expect(createSuccessResponse).toHaveBeenCalledWith(mockTeams); - }); - - it('returns empty array when no teams exist', async () => { - mockDb.all.mockResolvedValue([]); - - const response = await GET(); - - const { createSuccessResponse } = require('@/lib/apiHelpers'); - expect(createSuccessResponse).toHaveBeenCalledWith([]); - }); - - it('handles database errors gracefully', async () => { - 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); - }); - - it('handles database connection errors', async () => { - const connectionError = new Error('Failed to connect to database'); - const { getDatabase } = require('@/lib/database'); - getDatabase.mockRejectedValue(connectionError); - - const response = await GET(); - - const { createDatabaseError } = require('@/lib/apiHelpers'); - expect(createDatabaseError).toHaveBeenCalledWith('fetch teams', connectionError); - }); - }); -}); \ No newline at end of file diff --git a/app/api/teams/route.ts b/app/api/teams/route.ts index ce99d57..8fe190d 100644 --- a/app/api/teams/route.ts +++ b/app/api/teams/route.ts @@ -2,95 +2,36 @@ import { NextResponse } from 'next/server'; import { getDatabase } from '../../../lib/database'; import { Team } from '@/types'; import { TABLE_NAMES } from '@/lib/constants'; -import { - withErrorHandling, - createSuccessResponse, - createValidationError, - createDatabaseError, - parseRequestBody -} from '@/lib/apiHelpers'; -// Validation for team creation -function validateTeamInput(data: unknown): { - valid: boolean; - data?: { team_name: string }; - errors?: Record -} { - const errors: Record = {}; - - if (!data || typeof data !== 'object') { - errors.general = 'Request body must be an object'; - return { valid: false, errors }; - } - - const { team_name } = data as { team_name?: unknown }; - - if (!team_name || typeof team_name !== 'string') { - errors.team_name = 'Team name is required and must be a string'; - } else if (team_name.trim().length < 2) { - errors.team_name = 'Team name must be at least 2 characters long'; - } else if (team_name.trim().length > 50) { - errors.team_name = 'Team name must be less than 50 characters long'; - } - - if (Object.keys(errors).length > 0) { - return { valid: false, errors }; - } - - return { - valid: true, - data: { team_name: team_name.trim() } - }; +export async function GET() { + const db = await getDatabase(); + const teams: Team[] = await db.all(`SELECT * FROM ${TABLE_NAMES.TEAMS}`); + return NextResponse.json(teams); } -export const GET = withErrorHandling(async () => { - try { - const db = await getDatabase(); - const teams: Team[] = await db.all(`SELECT * FROM ${TABLE_NAMES.TEAMS} ORDER BY team_name ASC`); - - return createSuccessResponse(teams); - } catch (error) { - return createDatabaseError('fetch teams', error); - } -}); +export async function POST(request: Request) { + try { + const { team_name } = await request.json(); + + if (!team_name) { + return NextResponse.json({ error: 'Team name is required' }, { status: 400 }); + } -export const POST = withErrorHandling(async (request: Request) => { - const bodyResult = await parseRequestBody(request, validateTeamInput); - - if (!bodyResult.success) { - return bodyResult.response; - } - - const { team_name } = bodyResult.data; - - try { - const db = await getDatabase(); - - // Check if team name already exists - const existingTeam = await db.get( - `SELECT team_id FROM ${TABLE_NAMES.TEAMS} WHERE LOWER(team_name) = LOWER(?)`, - [team_name] - ); - - if (existingTeam) { - return createValidationError( - 'Team name already exists', - { team_name: 'A team with this name already exists' } - ); + const db = await getDatabase(); + + const result = await db.run( + `INSERT INTO ${TABLE_NAMES.TEAMS} (team_name) VALUES (?)`, + [team_name] + ); + + const newTeam: Team = { + team_id: result.lastID!, + team_name: team_name + }; + + return NextResponse.json(newTeam, { status: 201 }); + } catch (error) { + console.error('Error creating team:', error); + return NextResponse.json({ error: 'Failed to create team' }, { status: 500 }); } - - const result = await db.run( - `INSERT INTO ${TABLE_NAMES.TEAMS} (team_name) VALUES (?)`, - [team_name] - ); - - const newTeam: Team = { - team_id: result.lastID!, - team_name: team_name - }; - - return createSuccessResponse(newTeam, 201); - } catch (error) { - return createDatabaseError('create team', error); - } -}); \ No newline at end of file +} \ No newline at end of file diff --git a/app/edit/[id]/page.tsx b/app/edit/[id]/page.tsx index 6fe9463..2feb090 100644 --- a/app/edit/[id]/page.tsx +++ b/app/edit/[id]/page.tsx @@ -4,8 +4,6 @@ import { useState, useEffect } from 'react'; import { useParams, useRouter } from 'next/navigation'; import Dropdown from '@/components/Dropdown'; import { Team } from '@/types'; -import { useToast } from '@/lib/useToast'; -import { ToastContainer } from '@/components/Toast'; type Stream = { id: number; @@ -33,11 +31,10 @@ export default function EditStream() { }); const [teams, setTeams] = useState([]); + const [message, setMessage] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [isLoading, setIsLoading] = useState(true); const [stream, setStream] = useState(null); - const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({}); - const { toasts, removeToast, showSuccess, showError } = useToast(); // Fetch stream data and teams useEffect(() => { @@ -65,19 +62,16 @@ export default function EditStream() { team_id: streamData.team_id, }); - // Handle both old and new API response formats - const teams = teamsData.success ? teamsData.data : teamsData; - // Map teams for dropdown setTeams( - teams.map((team: Team) => ({ + teamsData.map((team: Team) => ({ id: team.team_id, name: team.team_name, })) ); } catch (error) { console.error('Failed to fetch data:', error); - showError('Failed to Load Stream', 'Could not fetch stream data. Please refresh the page.'); + setMessage('Failed to load stream data'); } finally { setIsLoading(false); } @@ -91,57 +85,15 @@ export default function EditStream() { const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); - - // Clear validation error when user starts typing - if (validationErrors[name]) { - setValidationErrors(prev => ({ ...prev, [name]: '' })); - } }; const handleTeamSelect = (teamId: number) => { setFormData((prev) => ({ ...prev, team_id: teamId })); - - // Clear validation error when user selects team - if (validationErrors.team_id) { - setValidationErrors(prev => ({ ...prev, team_id: '' })); - } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - - // Client-side validation - const errors: {[key: string]: string} = {}; - if (!formData.name.trim()) { - errors.name = 'Stream name is required'; - } else if (formData.name.trim().length < 2) { - errors.name = 'Stream name must be at least 2 characters'; - } - - if (!formData.obs_source_name.trim()) { - errors.obs_source_name = 'OBS source name is required'; - } - - if (!formData.url.trim()) { - errors.url = 'Stream URL is required'; - } else { - try { - new URL(formData.url); - } catch { - errors.url = 'Please enter a valid URL'; - } - } - - if (!formData.team_id) { - errors.team_id = 'Please select a team'; - } - - setValidationErrors(errors); - if (Object.keys(errors).length > 0) { - showError('Validation Error', 'Please fix the form errors'); - return; - } - + setMessage(''); setIsSubmitting(true); try { @@ -153,17 +105,17 @@ export default function EditStream() { const data = await response.json(); if (response.ok) { - showSuccess('Stream Updated', `"${formData.name}" has been updated successfully`); + setMessage('Stream updated successfully!'); // Redirect back to home after a short delay setTimeout(() => { router.push('/'); }, 1500); } else { - showError('Failed to Update Stream', data.error || 'Unknown error occurred'); + setMessage(data.error || 'Something went wrong.'); } } catch (error) { console.error('Error updating stream:', error); - showError('Failed to Update Stream', 'Network error or server unavailable'); + setMessage('Failed to update stream.'); } finally { setIsSubmitting(false); } @@ -181,17 +133,17 @@ export default function EditStream() { const data = await response.json(); if (response.ok) { - showSuccess('Stream Deleted', `"${stream?.name || 'Stream'}" has been deleted successfully`); + setMessage('Stream deleted successfully!'); // Redirect back to home after a short delay setTimeout(() => { router.push('/'); }, 1500); } else { - showError('Failed to Delete Stream', data.error || 'Unknown error occurred'); + setMessage(data.error || 'Failed to delete stream.'); } } catch (error) { console.error('Error deleting stream:', error); - showError('Failed to Delete Stream', 'Network error or server unavailable'); + setMessage('Failed to delete stream.'); } }; @@ -213,7 +165,6 @@ export default function EditStream() {

Stream Not Found

The requested stream could not be found.

@@ -301,39 +252,65 @@ export default function EditStream() { -
+
+ {/* Success/Error Message */} + {message && ( +
+
+
+ {message.includes('successfully') ? ( + + + + ) : ( + + + + )} +
+ {message} +
+
+ )} - - {/* Toast Notifications */} - ); } \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index 955ad62..28e4099 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,26 +2,6 @@ @tailwind components; @tailwind utilities; -/* Solarized Dark Theme Variables */ -:root { - --solarized-base03: #002b36; - --solarized-base02: #073642; - --solarized-base01: #586e75; - --solarized-base00: #657b83; - --solarized-base0: #839496; - --solarized-base1: #93a1a1; - --solarized-base2: #eee8d5; - --solarized-base3: #fdf6e3; - --solarized-blue: #268bd2; - --solarized-cyan: #2aa198; - --solarized-green: #859900; - --solarized-yellow: #b58900; - --solarized-orange: #cb4b16; - --solarized-red: #dc322f; - --solarized-magenta: #d33682; - --solarized-violet: #6c71c4; -} - /* Modern CSS Foundation */ * { margin: 0; @@ -34,25 +14,25 @@ html { } body { - background: linear-gradient(135deg, #002b36 0%, #073642 50%, #002b36 100%); - color: #93a1a1; + background: linear-gradient(135deg, #1e293b 0%, #312e81 50%, #1e293b 100%); + color: white; min-height: 100vh; line-height: 1.6; } /* Glass Card Component */ .glass { - background: rgba(7, 54, 66, 0.4); + background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); - border: 1px solid rgba(88, 110, 117, 0.3); + border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 16px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); } -/* Modern Button System */ +/* Modern Button */ .btn { - background: linear-gradient(135deg, #268bd2, #2aa198); - color: #fdf6e3; + background: linear-gradient(135deg, #3b82f6, #1d4ed8); + color: white; border: none; padding: 12px 24px; border-radius: 12px; @@ -61,137 +41,58 @@ body { transition: all 0.3s ease; display: inline-flex; align-items: center; - justify-content: center; gap: 8px; position: relative; - min-height: 44px; - font-size: 14px; - text-decoration: none; } .btn.active { - background: linear-gradient(135deg, #859900, #b58900); - color: #fdf6e3; - box-shadow: 0 0 0 3px rgba(133, 153, 0, 0.5); - transform: translateY(-1px); - font-weight: 700; + background: linear-gradient(135deg, #1d4ed8, #1e40af); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3); } .btn:hover { - transform: translateY(-1px); - box-shadow: 0 6px 20px rgba(38, 139, 210, 0.4); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(59, 130, 246, 0.4); } -.btn:disabled { - opacity: 0.6; - cursor: not-allowed; - transform: none; - box-shadow: none; -} - -/* Secondary Button */ .btn-secondary { - background: rgba(88, 110, 117, 0.3); - border: 1px solid rgba(131, 148, 150, 0.4); - color: #93a1a1; - backdrop-filter: blur(10px); + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); } .btn-secondary:hover { - background: rgba(88, 110, 117, 0.5); - border-color: rgba(131, 148, 150, 0.6); - box-shadow: 0 6px 20px rgba(88, 110, 117, 0.3); -} - -/* Success Button */ -.btn-success { - background: linear-gradient(135deg, #859900, #b58900); - color: #fdf6e3; -} - -.btn-success:hover { - background: linear-gradient(135deg, #b58900, #859900); - box-shadow: 0 6px 20px rgba(133, 153, 0, 0.4); -} - -/* Danger Button */ -.btn-danger { - background: linear-gradient(135deg, #dc322f, #cb4b16); - color: #fdf6e3; -} - -.btn-danger:hover { - background: linear-gradient(135deg, #cb4b16, #dc322f); - box-shadow: 0 6px 20px rgba(220, 50, 47, 0.4); -} - -/* Small Button */ -.btn-sm { - padding: 8px 16px; - font-size: 12px; - min-height: 36px; - border-radius: 8px; -} - -/* Icon Only Button */ -.btn-icon { - padding: 10px; - min-width: 44px; - aspect-ratio: 1; -} - -/* Button with icon spacing */ -.btn .icon { - flex-shrink: 0; -} - -/* Form spacing fixes since Tailwind gap classes aren't working */ -.form-row { - display: flex; - align-items: center; -} - -.form-row > * + * { - margin-left: 16px; -} - -.button-group { - display: flex; - align-items: center; -} - -.button-group > * + * { - margin-left: 12px; + background: rgba(255, 255, 255, 0.2); + box-shadow: 0 8px 25px rgba(255, 255, 255, 0.1); } /* Input Styling */ .input { - background: rgba(7, 54, 66, 0.6); - border: 1px solid rgba(88, 110, 117, 0.4); + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 12px; padding: 12px 16px; - color: #93a1a1; + color: white; width: 100%; transition: all 0.3s ease; } .input::placeholder { - color: rgba(131, 148, 150, 0.6); + color: rgba(255, 255, 255, 0.6); } .input:focus { outline: none; - border-color: #268bd2; - box-shadow: 0 0 0 3px rgba(38, 139, 210, 0.2); + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); } /* Dropdown Styling */ .dropdown-button { - background: rgba(7, 54, 66, 0.6); - border: 1px solid rgba(88, 110, 117, 0.4); + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 12px; padding: 12px 16px; - color: #93a1a1; + color: white; width: 100%; text-align: left; cursor: pointer; @@ -202,25 +103,24 @@ body { } .dropdown-button:hover { - background: rgba(7, 54, 66, 0.8); + background: rgba(255, 255, 255, 0.15); } .dropdown-menu { - background: rgba(0, 43, 54, 0.95); + background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); - border: 1px solid rgba(88, 110, 117, 0.4); + border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 12px; margin-top: 4px; overflow: hidden; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); } .dropdown-item { padding: 12px 16px; cursor: pointer; transition: all 0.2s ease; - border-bottom: 1px solid rgba(88, 110, 117, 0.2); - color: #93a1a1; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .dropdown-item:last-child { @@ -228,12 +128,12 @@ body { } .dropdown-item:hover { - background: rgba(38, 139, 210, 0.3); + background: rgba(255, 255, 255, 0.15); } .dropdown-item.active { - background: rgba(38, 139, 210, 0.3); - color: #fdf6e3; + background: rgba(59, 130, 246, 0.2); + color: #93c5fd; } /* Icon Sizes */ @@ -299,7 +199,7 @@ body { .subtitle { font-size: 1.125rem; - color: rgba(131, 148, 150, 0.9); + color: rgba(255, 255, 255, 0.8); text-align: center; margin-bottom: 32px; } diff --git a/app/layout.tsx b/app/layout.tsx index 7358406..2d6c46e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,8 +1,6 @@ import './globals.css'; import Header from '@/components/Header'; import Footer from '@/components/Footer'; -import { ErrorBoundary } from '@/components/ErrorBoundary'; -import PerformanceDashboard from '@/components/PerformanceDashboard'; export const metadata = { title: 'OBS Source Switcher', @@ -14,13 +12,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
-
- - {children} - -
+
{children}