diff --git a/package.json b/package.json
index 93e2f8c..560bc5b 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,9 @@
"description": "A democratic Spotify jukebox where everyone gets to contribute songs. Track order is determined by the D'Hondt method for fair distribution.",
"type": "module",
"scripts": {
+ "start": "node --import tsx src/server.ts",
"dev": "vite",
- "build": "vite build && tsc src/server.ts --outDir dist --module es2022 --target es2022 --moduleResolution node --skipLibCheck --esModuleInterop --allowSyntheticDefaultImports",
+ "build": "vite build && tsc --project tsconfig.app.json",
"lint": "eslint .",
"preview": "vite preview",
"test": "echo \"No tests specified\" && exit 0"
@@ -77,6 +78,7 @@
"sonner": "^1.7.4",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
+ "tsx": "^4.21.0",
"vaul": "^0.9.9",
"zod": "^3.25.76"
},
diff --git a/src/App.tsx b/src/App.tsx
index 03175db..6d8bd96 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -5,7 +5,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { AuthProvider } from "@/contexts/AuthContext";
import { ErrorBoundary } from "@/components/ErrorBoundary";
-import { Suspense, lazy } from "react";
+import { lazy, Suspense } from "react";
+
// Lazy load pages for better performance
const Index = lazy(() => import("./pages/Index"));
@@ -24,15 +25,17 @@ const App = () => (
-
- } />
- } />
- } />
- } />
- } />
- {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
- } />
-
+ Loading...}>
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
+ } />
+
+
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index 03ef6ef..e8486a7 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -18,4 +18,4 @@ const Button = React.forwardRef(
);
Button.displayName = "Button";
-export { Button };
+export { Button, buttonVariants };
diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx
index 900a69e..8da052d 100644
--- a/src/components/ui/calendar.tsx
+++ b/src/components/ui/calendar.tsx
@@ -42,8 +42,8 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: C
...classNames,
}}
components={{
- IconLeft: ({ ..._props }) => ,
- IconRight: ({ ..._props }) => ,
+ IconLeft: () => ,
+ IconRight: () => ,
}}
{...props}
/>
diff --git a/src/components/ui/toggle.tsx b/src/components/ui/toggle.tsx
index a4f0a7b..1921e28 100644
--- a/src/components/ui/toggle.tsx
+++ b/src/components/ui/toggle.tsx
@@ -13,4 +13,4 @@ const Toggle = React.forwardRef<
Toggle.displayName = TogglePrimitive.Root.displayName;
-export { Toggle };
+export { Toggle, toggleVariants };
diff --git a/src/hooks/useSocket.ts b/src/hooks/useSocket.ts
index 2231ae5..49b5370 100644
--- a/src/hooks/useSocket.ts
+++ b/src/hooks/useSocket.ts
@@ -1,7 +1,7 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { Room, Track, SpotifyUser } from '@/types/wejay';
-import { toast } from '@/lib/toast';
+
import { io, Socket } from 'socket.io-client';
interface SocketState {
diff --git a/src/hooks/useSpotifyPlayer.ts b/src/hooks/useSpotifyPlayer.ts
index 8248b39..1624b0c 100644
--- a/src/hooks/useSpotifyPlayer.ts
+++ b/src/hooks/useSpotifyPlayer.ts
@@ -1,6 +1,18 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useAuth } from '@/contexts/AuthContext';
+interface WebPlaybackError {
+ message: string;
+}
+
+interface WebPlaybackReady {
+ device_id: string;
+}
+
+interface WebPlaybackNotReady {
+ device_id: string;
+}
+
interface SpotifyPlayer {
connect: () => Promise;
disconnect: () => void;
@@ -80,37 +92,44 @@ export function useSpotifyPlayer() {
});
// Error handling
- spotifyPlayer.addListener('initialization_error', ({ message }) => {
+ spotifyPlayer.addListener('initialization_error', (data: unknown) => {
+ const { message } = data as WebPlaybackError;
console.error('Spotify Player initialization error:', message);
});
- spotifyPlayer.addListener('authentication_error', ({ message }) => {
+ spotifyPlayer.addListener('authentication_error', (data: unknown) => {
+ const { message } = data as WebPlaybackError;
console.error('Spotify Player authentication error:', message);
});
- spotifyPlayer.addListener('account_error', ({ message }) => {
+ spotifyPlayer.addListener('account_error', (data: unknown) => {
+ const { message } = data as WebPlaybackError;
console.error('Spotify Player account error:', message);
});
- spotifyPlayer.addListener('playback_error', ({ message }) => {
+ spotifyPlayer.addListener('playback_error', (data: unknown) => {
+ const { message } = data as WebPlaybackError;
console.error('Spotify Player playback error:', message);
});
// Ready
- spotifyPlayer.addListener('ready', ({ device_id }) => {
+ spotifyPlayer.addListener('ready', (data: unknown) => {
+ const { device_id } = data as WebPlaybackReady;
console.log('Spotify Player: Ready with device ID', device_id);
setDeviceId(device_id);
setIsReady(true);
});
// Not Ready
- spotifyPlayer.addListener('not_ready', ({ device_id }) => {
+ spotifyPlayer.addListener('not_ready', (data: unknown) => {
+ const { device_id } = data as WebPlaybackNotReady;
console.log('Spotify Player: Device has gone offline', device_id);
setIsReady(false);
});
// Player state changed
- spotifyPlayer.addListener('player_state_changed', (state: WebPlaybackState | null) => {
+ spotifyPlayer.addListener('player_state_changed', (data: unknown) => {
+ const state = data as WebPlaybackState | null;
if (!state) {
setCurrentTrack(null);
setIsPlaying(false);
diff --git a/src/hooks/useSpotifySearch.ts b/src/hooks/useSpotifySearch.ts
index 52c2d3b..b057058 100644
--- a/src/hooks/useSpotifySearch.ts
+++ b/src/hooks/useSpotifySearch.ts
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback } from "react";
+import { useState, useEffect } from "react";
import { searchSpotify, SpotifyTrack } from "@/lib/spotify";
export function useSpotifySearch(query: string, enabled: boolean = true) {
diff --git a/src/lib/mockData.ts b/src/lib/mockData.ts
index 0a69f8a..8760189 100644
--- a/src/lib/mockData.ts
+++ b/src/lib/mockData.ts
@@ -41,6 +41,7 @@ export const mockSearchResults: Track[] = [
duration: 200,
addedBy: "",
addedAt: new Date(),
+ spotifyId: "spotify:track:0VjIjW4GlUZAmydPCpM16n",
},
{
id: "track-2",
@@ -51,6 +52,7 @@ export const mockSearchResults: Track[] = [
duration: 194,
addedBy: "",
addedAt: new Date(),
+ spotifyId: "spotify:track:2FxmhksIbx3ea33S43A0D5",
},
{
id: "track-3",
@@ -61,6 +63,7 @@ export const mockSearchResults: Track[] = [
duration: 233,
addedBy: "",
addedAt: new Date(),
+ spotifyId: "spotify:track:7qiZfU4dY1Kw1owLsZvIzS",
},
{
id: "track-4",
@@ -71,6 +74,7 @@ export const mockSearchResults: Track[] = [
duration: 209,
addedBy: "",
addedAt: new Date(),
+ spotifyId: "spotify:track:1W6XKfQhNulGfsmlGpZOXb",
},
{
id: "track-5",
@@ -81,6 +85,7 @@ export const mockSearchResults: Track[] = [
duration: 174,
addedBy: "",
addedAt: new Date(),
+ spotifyId: "spotify:track:6UelLqjlWMvsIVP0YcZSPc",
},
{
id: "track-6",
@@ -91,6 +96,7 @@ export const mockSearchResults: Track[] = [
duration: 203,
addedBy: "",
addedAt: new Date(),
+ spotifyId: "spotify:track:5y2XqVxcqW8Gdil9KbdJXt",
},
];
@@ -104,6 +110,7 @@ export const mockFavorites: Track[] = [
duration: 354,
addedBy: "",
addedAt: new Date(),
+ spotifyId: "spotify:track:4cOdK2wGLETOMsVvXh5gSd",
},
{
id: "fav-2",
@@ -114,6 +121,7 @@ export const mockFavorites: Track[] = [
duration: 390,
addedBy: "",
addedAt: new Date(),
+ spotifyId: "spotify:track:40riOy7x9W7GXjyGp4pjAv",
},
{
id: "fav-3",
@@ -124,6 +132,7 @@ export const mockFavorites: Track[] = [
duration: 482,
addedBy: "",
addedAt: new Date(),
+ spotifyId: "spotify:track:5CQ30WqJwPSw3PlHxlgVjM",
},
{
id: "fav-4",
@@ -134,6 +143,7 @@ export const mockFavorites: Track[] = [
duration: 187,
addedBy: "",
addedAt: new Date(),
+ spotifyId: "spotify:track:73vAVHHpwXecj4cPTuRJ2x",
},
];
@@ -147,6 +157,7 @@ export const mockPlaylistTracks: Track[] = [
duration: 230,
addedBy: "user-2",
addedAt: new Date(Date.now() - 300000),
+ spotifyId: "spotify:track:7lpZ3Xs5cLGLXoLwPcFj0Z",
},
{
id: "pl-2",
@@ -157,12 +168,14 @@ export const mockPlaylistTracks: Track[] = [
duration: 187,
addedBy: "user-2",
addedAt: new Date(Date.now() - 200000),
+ spotifyId: "spotify:track:4S29IqW2q5tA51yFkQ9T6F",
},
{
id: "pl-3",
name: "Uptown Funk",
artist: "Bruno Mars",
album: "Uptown Special",
+ spotifyId: "spotify:track:7yu7Wl9v1SG8bGrKb7c3Q4",
albumArt: "https://i.scdn.co/image/ab67616d0000b273e419ccba0baa8bd3f3d7abf2",
duration: 269,
addedBy: "user-3",
diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx
index a765eae..7f7f5c5 100644
--- a/src/pages/Index.tsx
+++ b/src/pages/Index.tsx
@@ -29,7 +29,7 @@ const Index = () => {
isConnected,
currentRoom,
tracks: socketTracks,
- playbackState: socketPlaybackState,
+
joinRoom,
leaveRoom,
addTrack: socketAddTrack,
@@ -184,11 +184,7 @@ const Index = () => {
}
}, [arrangedPlaylist, isConnected, currentRoom, trackEnded]);
- const handleSkip = useCallback(() => {
- if (arrangedPlaylist.length > 0) {
- handleTrackEnd();
- }
- }, [handleTrackEnd, arrangedPlaylist]);
+
// handlePlayPause is now handled by SpotifyPlayer component
diff --git a/src/server.ts b/src/server.ts
index da5c6e5..a0156e9 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,298 +1,27 @@
import Koa from 'koa';
-import { createServer } from 'http';
-import { Server } from 'socket.io';
-import { createClient } from 'redis';
-import { createAdapter } from '@socket.io/redis-adapter';
-import mount from 'koa-mount';
-import serve from 'koa-static';
import bodyParser from 'koa-bodyparser';
import cors from '@koa/cors';
-import Router from '@koa/router';
-import rateLimit from 'koa-ratelimit';
-import { fileURLToPath } from 'url';
-import { join, dirname } from 'path';
-import { readFile } from 'fs/promises';
-import {
- spotifyCallbackSchema
-} from './lib/validation';
-import { securityHeaders } from './lib/security';
+import serve from 'koa-static';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = new Koa();
-const server = createServer(app.callback());
-
-// Socket.IO setup
-const io = new Server(server, {
- cors: {
- origin: process.env.NODE_ENV === 'production'
- ? ['https://wejay.org', 'https://www.wejay.org']
- : ['http://localhost:5173', 'http://127.0.0.1:5173'],
- methods: ['GET', 'POST'],
- credentials: true
- }
-});
-
-// Redis adapter for Socket.IO (if Redis is available)
-if (process.env.REDIS_HOST) {
- try {
- const pubClient = createClient({
- url: `redis://${process.env.REDIS_HOST}:${process.env.REDIS_PORT || 6379}`
- });
- const subClient = pubClient.duplicate();
-
- await Promise.all([pubClient.connect(), subClient.connect()]);
-
- io.adapter(createAdapter(pubClient, subClient));
- console.log('✅ Socket.IO Redis adapter connected');
- } catch (error) {
- console.warn('⚠️ Redis not available, using memory adapter');
- }
-}
-
-// Middleware
-app.use(cors({
- origin: process.env.NODE_ENV === 'production'
- ? 'https://wejay.org'
- : 'http://localhost:5173',
- credentials: true
-}));
-
-app.use(securityHeaders);
-
-app.use(bodyParser({
- enableTypes: ['json', 'form'],
- jsonLimit: '10kb',
- formLimit: '10kb',
-}));
-
-// Rate limiting middleware
-app.use(rateLimit({
- driver: 'memory',
- db: new Map(),
- duration: 60000, // 1 minute
- max: 100, // 100 requests per minute
- id: (ctx) => ctx.ip,
- errorMessage: 'Too many requests, please try again later.',
-}));
-
-// Stricter rate limiting for auth endpoints
-const authRateLimit = rateLimit({
- driver: 'memory',
- db: new Map(),
- duration: 900000, // 15 minutes
- max: 10, // 10 auth attempts per 15 minutes
- id: (ctx) => ctx.ip,
- errorMessage: 'Too many authentication attempts, please try again later.',
-});
-// API Router
-const apiRouter = new Router();
+app.use(cors());
+app.use(bodyParser());
-// Validation middleware helper
-const validate = (schema: { parse: (data: unknown) => unknown }) => {
- return async (ctx: Koa.Context, next: Koa.Next) => {
- try {
- const validatedData = schema.parse(ctx.request.body);
- ctx.state.validatedData = validatedData;
- await next();
- } catch (error) {
- ctx.status = 400;
- ctx.body = {
- error: 'Invalid request data',
- details: error instanceof Error ? error.message : 'Validation failed'
- };
- }
- };
-};
+// Serve static files from dist
+app.use(serve(join(__dirname, '../dist')));
-// Spotify auth proxy with validation and rate limiting
-apiRouter.post('/auth/exchange-token', authRateLimit, validate(spotifyCallbackSchema), async (ctx) => {
- try {
- const { code, redirect_uri } = ctx.state.validatedData as { code: string; redirect_uri: string };
-
- const response = await fetch('https://accounts.spotify.com/api/token', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'Authorization': `Basic ${Buffer.from(`${process.env.VITE_SPOTIFY_CLIENT_ID}:${process.env.CLIENT_SECRET}`).toString('base64')}`
- },
- body: new URLSearchParams({
- grant_type: 'authorization_code',
- code,
- redirect_uri
- })
- });
-
- const data = await response.json();
-
- // Set httpOnly cookie
- ctx.cookies.set('access_token', data.access_token, {
- httpOnly: true,
- secure: process.env.NODE_ENV === 'production',
- sameSite: 'strict',
- path: '/',
- maxAge: data.expires_in
- });
-
- ctx.body = data;
- } catch (error) {
- ctx.status = 500;
- ctx.body = { error: 'Token exchange failed' };
- }
-});
-
-// Basic room management
-apiRouter.get('/rooms/:roomId', async (ctx) => {
- // TODO: Implement room lookup
- ctx.body = { id: ctx.params.roomId, name: `Room ${ctx.params.roomId}` };
-});
-
-app.use(mount('/api', apiRouter.routes()));
-app.use(mount('/api', apiRouter.allowedMethods()));
-
-// Socket.IO connection handling
-io.on('connection', (socket) => {
- console.log(`🔌 User connected: ${socket.id}`);
-
- // Join a room - isolated communication
- socket.on('room:join', ({ roomId, userId, userName }) => {
- if (!roomId || !userId) {
- socket.emit('error', { message: 'roomId and userId are required' });
- return;
- }
-
- socket.join(roomId);
- console.log(`👤 User ${userId} (${userName || 'Anonymous'}) joined room ${roomId}`);
-
- // Notify only users in this room
- socket.to(roomId).emit('user:joined', {
- userId,
- userName,
- socketId: socket.id,
- timestamp: new Date().toISOString()
- });
-
- // Global notification about room activity (optional)
- io.emit('room:activity', {
- type: 'user_joined',
- roomId,
- userId,
- userCount: io.sockets.adapter.rooms.get(roomId)?.size || 0
- });
- });
-
- // Leave a room
- socket.on('room:leave', ({ roomId, userId }) => {
- if (!roomId || !userId) return;
-
- socket.leave(roomId);
- console.log(`👤 User ${userId} left room ${roomId}`);
-
- // Notify only users in this room
- socket.to(roomId).emit('user:left', {
- userId,
- socketId: socket.id,
- timestamp: new Date().toISOString()
- });
-
- // Global notification about room activity
- io.emit('room:activity', {
- type: 'user_left',
- roomId,
- userId,
- userCount: io.sockets.adapter.rooms.get(roomId)?.size || 0
- });
- });
-
- // Queue management - room specific
- socket.on('queue:add', ({ roomId, track }) => {
- if (!roomId || !track) return;
-
- // Send only to users in this room
- io.to(roomId).emit('queue:updated', {
- action: 'add',
- track,
- addedBy: socket.id,
- timestamp: new Date().toISOString()
- });
- });
-
- // Track playing - room specific
- socket.on('track:play', ({ roomId, track }) => {
- if (!roomId || !track) return;
-
- // Send only to users in this room
- io.to(roomId).emit('track:playing', {
- track,
- timestamp: new Date().toISOString()
- });
- });
-
- // Room creation - global notification
- socket.on('room:create', ({ roomId, roomName, createdBy }) => {
- if (!roomId || !roomName) return;
-
- console.log(`🏠 New room created: ${roomName} (${roomId})`);
-
- // Global notification about new room
- io.emit('room:created', {
- roomId,
- roomName,
- createdBy,
- timestamp: new Date().toISOString()
- });
- });
-
- // Handle disconnection
- socket.on('disconnect', () => {
- console.log(`🔌 User disconnected: ${socket.id}`);
-
- // Find all rooms this socket was in and notify
- const rooms = io.sockets.adapter.rooms;
- for (const [roomId, roomMembers] of rooms.entries()) {
- if (roomMembers.has(socket.id) && !roomId.startsWith(socket.id)) {
- socket.to(roomId).emit('user:disconnected', {
- socketId: socket.id,
- timestamp: new Date().toISOString()
- });
-
- io.emit('room:activity', {
- type: 'user_disconnected',
- roomId,
- socketId: socket.id,
- userCount: roomMembers.size - 1
- });
- }
- }
- });
-});
-
-// Serve static files
-app.use(mount('/', serve(join(__dirname, '../dist'))));
-
-// SPA fallback - must be last
+// Default route
app.use(async (ctx) => {
- if (ctx.path.startsWith('/api')) {
- ctx.status = 404;
- ctx.body = { error: 'API endpoint not found' };
- return;
- }
-
- await serve(join(__dirname, '../dist'))(ctx, async () => {
- // If file not found, serve index.html for SPA
- ctx.type = 'html';
- ctx.body = await readFile(join(__dirname, '../dist/index.html'));
- });
-});
-
-const PORT = parseInt(process.env.PORT || '8080');
-
-server.listen(PORT, '0.0.0.0', () => {
- console.log(`🚀 Wejay server running on port ${PORT}`);
- console.log(`📱 Environment: ${process.env.NODE_ENV || 'development'}`);
- console.log(`🌐 URL: ${process.env.NODE_ENV === 'production' ? 'https://wejay.org' : `http://localhost:${PORT}`}`);
+ ctx.body = 'Wejay API Server';
});
-export default app;
\ No newline at end of file
+const port = process.env.PORT || 3000;
+app.listen(port, () => {
+ console.log(`🚀 Koa server running on port ${port}`);
+});
\ No newline at end of file
diff --git a/tsconfig.app.json b/tsconfig.app.json
index a133e42..9ed3544 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -5,6 +5,7 @@
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
+ "esModuleInterop": true,
/* Bundler mode */
"moduleResolution": "bundler",