MULTI-UPDATE: Big Error Cleanup:

Enhanced Docker and Nginx configurations - Can now run frontend either on local dev version OR the docker version;
Improved socket connection handling;
Refactored stream data fetching in VideoPage to properly display stream data;
Chat-Visibility Button moved to ChatPanel so that chat's socket persists when hiding/showing chat;
This commit is contained in:
Chris-1010
2025-02-01 14:21:46 +00:00
parent 9784ef8c67
commit 2020b854f2
11 changed files with 233 additions and 127 deletions

View File

@@ -7,6 +7,7 @@ services:
- "1935:1935" # RTMP - "1935:1935" # RTMP
- "8080:8080" - "8080:8080"
depends_on: depends_on:
- frontend
- web_server - web_server
networks: networks:
- app_network - app_network
@@ -35,6 +36,8 @@ services:
- "5173" - "5173"
networks: networks:
- app_network - app_network
environment:
- VITE_API_URL=/api
depends_on: depends_on:
- web_server - web_server

View File

@@ -13,4 +13,4 @@ COPY . .
EXPOSE 5173 EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host"] CMD ["npm", "run", "docker-dev", "--", "--host", "--strictPort"]

View File

@@ -4,7 +4,8 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --config vite.config.dev.ts",
"docker-dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview"
@@ -35,6 +36,6 @@
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"typescript-eslint": "^8.18.2", "typescript-eslint": "^8.18.2",
"vite": "^6.0.5" "vite": "latest"
} }
} }

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
import Input from "../Layout/Input"; import Input from "../Layout/Input";
import { useAuth } from "../../context/AuthContext"; import { useAuth } from "../../context/AuthContext";
import { useSocket } from "../../context/SocketContext"; import { useSocket } from "../../context/SocketContext";
import Button from "../Layout/Button"; import Button, { ToggleButton } from "../Layout/Button";
import AuthModal from "../Auth/AuthModal"; import AuthModal from "../Auth/AuthModal";
interface ChatMessage { interface ChatMessage {
@@ -21,6 +21,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ streamId }) => {
const [inputMessage, setInputMessage] = useState(""); const [inputMessage, setInputMessage] = useState("");
const chatContainerRef = useRef<HTMLDivElement>(null); const chatContainerRef = useRef<HTMLDivElement>(null);
const { isLoggedIn, username } = useAuth(); const { isLoggedIn, username } = useAuth();
const [isChatVisible, setIsChatVisible] = useState(false);
// Join chat room when component mounts // Join chat room when component mounts
useEffect(() => { useEffect(() => {
@@ -92,6 +93,10 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ streamId }) => {
setInputMessage(""); setInputMessage("");
}; };
const toggleChat = () => {
setIsChatVisible((prev) => !prev);
};
const handleKeyPress = (e: React.KeyboardEvent) => { const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
@@ -115,72 +120,83 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ streamId }) => {
return ( return (
<> <>
<div id="chat-panel" className="h-full flex flex-col rounded-lg p-4"> <ToggleButton
<h2 className="text-xl font-bold mb-4 text-white">Stream Chat</h2> onClick={toggleChat}
toggled={isChatVisible}
extraClasses="z-5"
>
{isChatVisible ? "Hide Chat" : "Show Chat"}
</ToggleButton>
{isChatVisible && (
<div id="chat-panel" className="h-full flex flex-col rounded-lg p-4">
<h2 className="text-xl font-bold mb-4 text-white">Stream Chat</h2>
<div <div
ref={chatContainerRef} ref={chatContainerRef}
id="chat-message-list" id="chat-message-list"
className="flex-grow w-full max-h-[50vh] overflow-y-auto mb-4 space-y-2" className="flex-grow w-full max-h-[50vh] overflow-y-auto mb-4 space-y-2"
> >
{messages.map((msg, index) => ( {messages.map((msg, index) => (
<div <div
key={index} key={index}
className="grid grid-cols-[8%_minmax(15%,_100px)_1fr] items-center bg-gray-700 rounded p-2 text-white" className="grid grid-cols-[8%_minmax(15%,_100px)_1fr] items-center bg-gray-700 rounded p-2 text-white"
>
<span className="text-gray-400 text-sm">
{new Date(msg.time_sent).toLocaleTimeString()}
</span>
<span
className={`font-bold ${
msg.chatter_username === username
? "text-blue-400"
: "text-green-400"
}`}
> >
{" "} <span className="text-gray-400 text-sm">
{msg.chatter_username}:{" "} {new Date(msg.time_sent).toLocaleTimeString()}
</span> </span>
<span>{msg.message}</span> <span
</div> className={`font-bold ${
))} msg.chatter_username === username
</div> ? "text-blue-400"
: "text-green-400"
<div className="flex justify-center gap-2"> }`}
{isLoggedIn && ( >
<> {" "}
<Input {msg.chatter_username}:{" "}
type="text" </span>
value={inputMessage} <span>{msg.message}</span>
onChange={(e) => setInputMessage(e.target.value)} </div>
onKeyDown={handleKeyPress} ))}
placeholder={isLoggedIn ? "Type a message..." : "Login to chat"}
disabled={!isLoggedIn}
extraClasses="flex-grow"
onClick={() => !isLoggedIn && setShowAuthModal(true)}
/>
<button
onClick={sendChat}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
Send
</button>
</>
)}
{!isLoggedIn && (
<Button
extraClasses="absolute top-[20px] left-[20px] text-[1rem] flex items-center flex-nowrap z-[999]"
onClick={() => setShowAuthModal(true)}
></Button>
)}
</div>
{showAuthModal && (
<div className="fixed inset-0 z-[9999] flex items-center justify-center">
<AuthModal onClose={() => setShowAuthModal(false)} />
</div> </div>
)}
</div> <div className="flex justify-center gap-2">
{isLoggedIn && (
<>
<Input
type="text"
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyDown={handleKeyPress}
placeholder={
isLoggedIn ? "Type a message..." : "Login to chat"
}
disabled={!isLoggedIn}
extraClasses="flex-grow"
onClick={() => !isLoggedIn && setShowAuthModal(true)}
/>
<button
onClick={sendChat}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
Send
</button>
</>
)}
{!isLoggedIn && (
<Button
extraClasses="absolute top-[20px] left-[20px] text-[1rem] flex items-center flex-nowrap z-[999]"
onClick={() => setShowAuthModal(true)}
></Button>
)}
</div>
{showAuthModal && (
<div className="fixed inset-0 z-[9999] flex items-center justify-center">
<AuthModal onClose={() => setShowAuthModal(false)} />
</div>
)}
</div>
)}
</> </>
); );
}; };

View File

@@ -1,4 +1,10 @@
import React, { createContext, useContext, useEffect, useRef, useState } from "react"; import React, {
createContext,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { io, Socket } from "socket.io-client"; import { io, Socket } from "socket.io-client";
interface SocketContextType { interface SocketContextType {
@@ -26,14 +32,16 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
} }
console.log("Creating new socket connection"); console.log("Creating new socket connection");
const newSocket = io('http://localhost:8080', { const newSocket = io("http://localhost:8080", {
path: '/socket.io/', path: "/socket.io/",
transports: ['websocket'], transports: ["websocket"],
withCredentials: true, withCredentials: true,
reconnectionDelay: 1000, reconnectionDelay: 1000,
reconnectionDelayMax: 5000, reconnectionDelayMax: 5000,
reconnectionAttempts: 5, reconnectionAttempts: 5,
timeout: 5000 timeout: 5000,
autoConnect: true,
forceNew: true,
}); });
socketRef.current = newSocket; socketRef.current = newSocket;
@@ -65,15 +73,11 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
console.error("Socket connection error:", error); console.error("Socket connection error:", error);
setIsLoading(false); setIsLoading(false);
if (newSocket) newSocket.disconnect(); if (newSocket) newSocket.disconnect();
newSocket.connect();
}); });
newSocket.on("disconnect", (reason) => { newSocket.on("disconnect", (reason) => {
console.log( console.log("Socket disconnected! Reason: " + reason);
"Socket disconnected! Reason: " + reason + " - Attempting reconnect..."
);
setIsConnected(false); setIsConnected(false);
newSocket.connect();
}); });
return () => { return () => {

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import Navbar from "../components/Layout/Navbar"; import Navbar from "../components/Layout/Navbar";
import Button, { ToggleButton } from "../components/Layout/Button"; import Button from "../components/Layout/Button";
import ChatPanel from "../components/Video/ChatPanel"; import ChatPanel from "../components/Video/ChatPanel";
// import CheckoutForm, { Return } from "../components/Checkout/CheckoutForm"; // import CheckoutForm, { Return } from "../components/Checkout/CheckoutForm";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
@@ -19,7 +19,7 @@ interface StreamDataProps {
streamerId: number; streamerId: number;
startTime: string; startTime: string;
viewerCount: number; viewerCount: number;
categoryId: number; categoryName: string;
} }
const VideoPage: React.FC<VideoPageProps> = ({ streamId }) => { const VideoPage: React.FC<VideoPageProps> = ({ streamId }) => {
@@ -29,11 +29,6 @@ const VideoPage: React.FC<VideoPageProps> = ({ streamId }) => {
// const [showCheckout, setShowCheckout] = useState(false); // const [showCheckout, setShowCheckout] = useState(false);
// const showReturn = window.location.search.includes("session_id"); // const showReturn = window.location.search.includes("session_id");
// const navigate = useNavigate(); // const navigate = useNavigate();
const [isChatVisible, setIsChatVisible] = useState(false);
const toggleChat = () => {
setIsChatVisible((prev) => !prev);
};
// useEffect(() => { // useEffect(() => {
// // Prevent scrolling when checkout is open // // Prevent scrolling when checkout is open
@@ -57,19 +52,24 @@ const VideoPage: React.FC<VideoPageProps> = ({ streamId }) => {
if (!res.ok) { if (!res.ok) {
console.error("Failed to load stream data:", res.statusText); console.error("Failed to load stream data:", res.statusText);
} }
res.json().then((data) => { res
// if (!data.validStream) navigate(`/`); .json()
console.log(`Loading stream data for ${streamerName}`); .then((data) => {
setStreamData({ // Transform snake_case to camelCase
streamId: data.streamId, const transformedData: StreamDataProps = {
streamTitle: data.streamTitle, streamId: streamId,
streamerName: data.streamerName, streamerName: data.username,
streamerId: data.streamerId, streamerId: data.user_id,
startTime: data.startTime, streamTitle: data.title,
viewerCount: data.viewerCount, startTime: data.start_time,
categoryId: data.categoryId, viewerCount: data.num_viewers,
categoryName: data.category_name,
};
setStreamData(transformedData);
})
.catch((error) => {
console.error("Error fetching stream data:", error);
}); });
});
}); });
}, [streamId, streamerName]); }, [streamId, streamerName]);
@@ -79,35 +79,48 @@ const VideoPage: React.FC<VideoPageProps> = ({ streamId }) => {
<Navbar /> <Navbar />
<div id="container" className="bg-gray-900"> <div id="container" className="bg-gray-900">
<VideoPlayer streamId={streamId} /> <VideoPlayer streamId={streamId} />
<ToggleButton <div
onClick={toggleChat} id="chat"
toggled={isChatVisible} className="relative top-0 right-0 bg-gray-800 bg-opacity-75 text-white p-4 w-1/3 h-full z-10 overflow-y-auto"
extraClasses="z-5"
> >
{isChatVisible ? "Hide Chat" : "Show Chat"} <ChatPanel streamId={streamId} />
</ToggleButton> </div>
{isChatVisible && (
<div
id="chat"
className="relative top-0 right-0 bg-gray-800 bg-opacity-75 text-white p-4 w-1/3 h-full z-10 overflow-y-auto"
>
<ChatPanel streamId={streamId} />
</div>
)}
<div <div
id="stream-info" id="stream-info"
className="flex" className="flex flex-col gap-2 p-4 text-white"
style={{ gridArea: "3 / 1 / 4 / 2" }} style={{ gridArea: "3 / 1 / 4 / 2" }}
> >
<h1>{streamData?.streamTitle}</h1> <h1 className="text-2xl font-bold">
<h2>{streamData?.streamerName}</h2> {streamData ? streamData.streamTitle : "Loading..."}
</h1>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<span className="font-semibold">Streamer:</span>
<span>
{streamData ? streamData.streamerName : "Loading..."}
</span>
</div>
<div className="flex items-center gap-2">
<span className="font-semibold">Viewer Count:</span>
<span>{streamData ? streamData.viewerCount : "0"}</span>
</div>
<div className="flex items-center gap-2">
<span className="font-semibold">Started At:</span>
<span>
{streamData
? new Date(streamData.startTime).toLocaleString()
: "Loading..."}
</span>
</div>
<div className="flex items-center gap-2">
<span className="font-semibold">Category ID:</span>
<span>
{streamData ? streamData.categoryName : "Loading..."}
</span>
</div>
</div>
{isLoggedIn && ( {isLoggedIn && (
<Button <Button extraClasses="mx-auto mb-4">Payment Screen Test</Button>
// onClick={() => setShowCheckout(true)}
extraClasses="mx-auto mb-4"
>
Payment Screen Test
</Button>
)} )}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,34 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react({
// Add development-specific React plugin options
jsxRuntime: 'automatic'
})],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
secure: false
},
'/socket.io': {
target: 'http://localhost:8080',
changeOrigin: true,
ws: true
}
}
},
build: {
sourcemap: true,
outDir: 'dist'
},
optimizeDeps: {
exclude: ['@vite/client', '@vite/env']
},
esbuild: {
logOverride: { 'this-is-undefined-in-esm': 'silent' }
}
})

View File

@@ -12,6 +12,12 @@ export default defineConfig({
target: 'http://web_server:5000', target: 'http://web_server:5000',
changeOrigin: true, changeOrigin: true,
} }
},
hmr: {
protocol: 'ws',
host: '127.0.0.1',
clientPort: 8080,
port: 5173
} }
}, }
}) })

View File

@@ -46,6 +46,10 @@ http {
location /api/ { location /api/ {
rewrite ^/api/(.*)$ /$1 break; rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://web_server:5000; # flask-app is the name of the Flask container in docker-compose proxy_pass http://web_server:5000; # flask-app is the name of the Flask container in docker-compose
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
location /socket.io/ { location /socket.io/ {
@@ -57,6 +61,9 @@ http {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache_bypass $http_upgrade; proxy_cache_bypass $http_upgrade;
proxy_buffers 8 32k;
proxy_buffer_size 64k;
proxy_read_timeout 86400;
} }
location /hmr/ { location /hmr/ {
@@ -91,8 +98,20 @@ http {
expires -1d; expires -1d;
} }
location ~ ^/\?token=.*$ {
proxy_pass http://frontend:5173;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
location / { location / {
proxy_pass http://frontend:5173; # frontend is the name of the React container in docker-compose proxy_pass http://frontend:5173;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
} }
} }
} }

View File

@@ -1,3 +1,10 @@
from flask_socketio import SocketIO from flask_socketio import SocketIO
socketio = SocketIO(cors_allowed_origins="*", async_mode='gevent', logger=True, engineio_logger=True) socketio = SocketIO(
cors_allowed_origins="*",
async_mode='gevent',
logger=False, # Reduce logging
engineio_logger=False, # Reduce logging
ping_timeout=5000,
ping_interval=25000
)

View File

@@ -74,9 +74,12 @@ def user_stream(user_id: int, stream_id: int) -> dict:
""" """
with Database() as db: with Database() as db:
stream = db.fetchone(""" stream = db.fetchone("""
SELECT * FROM streams SELECT u.username, s.user_id, s.title, s.start_time, s.num_viewers, c.category_name
WHERE user_id = ? FROM streams AS s
AND stream_id = ? JOIN categories AS c ON s.category_id = c.category_id
JOIN users AS u ON s.user_id = u.user_id
WHERE u.user_id = ?
AND s.stream_id = ?
""", (user_id, stream_id)) """, (user_id, stream_id))
return stream return stream