Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,24 @@ services:
timeout: 5s
retries: 5

# Video Catalogue Microservice
video_service:
build:
context: ./services/Glense.VideoCatalogue
dockerfile: Dockerfile
container_name: glense_video_service
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__VideoCatalogue=Host=postgres_video;Port=5432;Database=glense_video;Username=glense;Password=glense123
ports:
- "5002:5002"
depends_on:
postgres_video:
condition: service_healthy
networks:
- glense_network
restart: unless-stopped

# Frontend (existing React app)
frontend:
build:
Expand Down Expand Up @@ -155,7 +173,7 @@ services:
- ConnectionStrings__DefaultConnection=Host=postgres_chat;Port=5432;Database=glense_chat;Username=glense;Password=glense123
- JwtSettings__Issuer=GlenseAccountService
- JwtSettings__Audience=GlenseApp
- JwtSettings__SecretKey=ChangeMeToA32CharSecret
- JwtSettings__SecretKey=YourSuperSecretKeyThatIsAtLeast32CharactersLongForHS256Algorithm
ports:
- "5004:5000"
depends_on:
Expand Down
13 changes: 11 additions & 2 deletions glense.client/src/components/ChannelDetail.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { Box, CardMedia, Typography } from "@mui/material";

import { Videos, ChannelCard } from ".";
import { videos } from "../utils/constants";
import { getVideos } from "../utils/videoApi";

import "../css/ChannelDetail.css";

function ChannelDetail() {
const [channelDetail, setChannelDetail] = useState(null);
const [videos, setVideos] = useState([]);

useEffect(() => {
let mounted = true;
getVideos()
.then(data => { if (mounted && Array.isArray(data)) setVideos(data); })
.catch(() => {});
return () => { mounted = false; };
}, []);

const channelBanner =
channelDetail?.brandingSettings?.image?.bannerExternalUrl;
Expand Down
5 changes: 4 additions & 1 deletion glense.client/src/components/Chat/ChatSidebar.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from "react";
import { Box, List, ListItem, ListItemAvatar, Avatar, ListItemText, TextField, Button, ListItemButton } from "@mui/material";
import "../../css/Chat/ChatSideBar.css";
import { stringToColor } from "../../utils/constants";

const ChatSidebar = ({ chats, onSelectChat, onCreate }) => {
const [topic, setTopic] = useState("");
Expand All @@ -26,7 +27,9 @@ const ChatSidebar = ({ chats, onSelectChat, onCreate }) => {
<ListItem key={index} className="chat-sidebar-item" disablePadding>
<ListItemButton onClick={() => onSelectChat(chat)}>
<ListItemAvatar>
<Avatar src={chat.profileImage} />
<Avatar sx={{ bgcolor: stringToColor(chat.topic || chat.Topic || chat.name || ''), fontSize: 16 }}>
{(chat.topic || chat.Topic || chat.name || '?').charAt(0).toUpperCase()}
</Avatar>
</ListItemAvatar>
<ListItemText primary={chat.name || chat.Topic || chat.topic || chat.title || 'Untitled'} />
</ListItemButton>
Expand Down
12 changes: 5 additions & 7 deletions glense.client/src/components/Chat/ChatWindow.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from "react";
import MessageBubble from "./MessageBubble";
import { Box, TextField, IconButton } from "@mui/material";
import { Box, TextField, IconButton, Avatar } from "@mui/material";
import SendIcon from "@mui/icons-material/Send";
import "../../css/Chat/ChatWindow.css";

import { stringToColor } from "../../utils/constants";
import { useState } from "react";

const ChatWindow = ({ chat, onSend }) => {
Expand All @@ -27,11 +27,9 @@ const ChatWindow = ({ chat, onSend }) => {
<div className="chat-window">
{/* Chat Header */}
<div className="chat-window-header">
<img
src={chat.profileImage || ''}
alt={chat.name || chat.Topic || chat.topic || 'Chat'}
className="chat-window-header-image"
/>
<Avatar sx={{ bgcolor: stringToColor(chat.topic || chat.Topic || chat.name || ''), width: 36, height: 36, fontSize: 16 }}>
{(chat.topic || chat.Topic || chat.name || '?').charAt(0).toUpperCase()}
</Avatar>
<span className="chat-window-header-name">{chat.name || chat.Topic || chat.topic || chat.title || 'Chat'}</span>
</div>

Expand Down
13 changes: 12 additions & 1 deletion glense.client/src/components/Chat/MessageBubble.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import React from "react";
import { Box } from "@mui/material";
import { Box, Avatar } from "@mui/material";
import "../../css/Chat/MessageBubble.css";
import { stringToColor } from "../../utils/constants";

const MessageBubble = ({ message }) => {
const isMe = message.isMe;
const senderName = message.sender || '';

return (
<Box className={`message-bubble ${isMe ? "me" : "other"}`}>
{!isMe && (
<Avatar sx={{ bgcolor: stringToColor(senderName), width: 28, height: 28, fontSize: 13, flexShrink: 0, mr: 1 }}>
{senderName.charAt(0).toUpperCase() || '?'}
</Avatar>
)}
<Box className="message-bubble-content">
{!isMe && senderName && (
<div className="message-bubble-sender">{senderName}</div>
)}
<div className="message-bubble-text">{message.message}</div>
<div className="message-bubble-time">{message.time}</div>
</Box>
Expand Down
3 changes: 1 addition & 2 deletions glense.client/src/components/Feed.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
import { Box, Stack } from "@mui/material";
import { Sidebar, Videos, ChatList } from "./";
import { Sidebar, Videos } from "./";

import "../css/Feed.css";
import { getVideos } from "../utils/videoApi";
Expand All @@ -26,7 +26,6 @@ function Feed() {
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
/>
<ChatList />
</Box>

<Box className="feed-content" >
Expand Down
9 changes: 5 additions & 4 deletions glense.client/src/components/Navbar.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Stack, Typography, Button, IconButton, Menu, MenuItem } from "@mui/material";
import { Stack, Typography, Button, IconButton, Menu, MenuItem, Avatar } from "@mui/material";
import { Link } from "react-router-dom";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
import logo from "../assets/logo_transparent.png";
import SearchBar from "../components/SearchBar";
import { useState } from "react";
Expand Down Expand Up @@ -60,8 +59,10 @@ function Navbar() {
Donations
</Typography>
</Link>
<IconButton onClick={handleMenuOpen} sx={{ color: 'white' }}>
<AccountCircleIcon />
<IconButton onClick={handleMenuOpen} sx={{ p: 0.5 }}>
<Avatar sx={{ bgcolor: '#c62828', width: 32, height: 32, fontSize: 14, fontWeight: 'bold' }}>
{user?.username?.charAt(0).toUpperCase() || '?'}
</Avatar>
</IconButton>
</Stack>
<Menu
Expand Down
5 changes: 1 addition & 4 deletions glense.client/src/components/Upload.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ function Upload() {
const [file, setFile] = useState(null);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [uploaderId, setUploaderId] = useState(user?.id || "");
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");

Expand All @@ -31,7 +30,7 @@ function Upload() {
setLoading(true);
setMessage("");
try {
const resp = await uploadVideo(file, title, description, Number(uploaderId) || 0);
const resp = await uploadVideo(file, title, description, user?.id || '');
setMessage("Upload successful");
// navigate to video page
if (resp?.id) {
Expand All @@ -55,8 +54,6 @@ function Upload() {
<TextField label="Title" value={title} onChange={(e) => setTitle(e.target.value)} fullWidth margin="normal" />
<TextField label="Description" value={description} onChange={(e) => setDescription(e.target.value)} fullWidth multiline rows={4} margin="normal" />

<TextField label="Uploader Id (optional)" value={uploaderId} onChange={(e) => setUploaderId(e.target.value)} fullWidth margin="normal" />

<Button variant="contained" color="primary" type="submit" disabled={loading}>
{loading ? "Uploading..." : "Upload"}
</Button>
Expand Down
54 changes: 40 additions & 14 deletions glense.client/src/components/VideoComments.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
import React from "react";
import { Stack, CardMedia, Typography } from "@mui/material";
import { useState, useEffect } from "react";
import { Stack, Typography, Avatar } from "@mui/material";
import { ThumbUpOutlined } from "@mui/icons-material";

import { comments } from "../utils/constants";
import { getComments } from "../utils/videoApi";
import "../css/VideoComments.css";
import { stringToColor } from "../utils/constants";

function VideoComments({ videoId, id }) {
const resolvedVideoId = videoId || id;
const [comments, setComments] = useState(null);

function VideoComments({ }) {
if (!comments)
useEffect(() => {
if (!resolvedVideoId) return;
let mounted = true;
getComments(resolvedVideoId)
.then(data => { if (mounted) setComments(data); })
.catch(() => { if (mounted) setComments([]); });
return () => { mounted = false; };
}, [resolvedVideoId]);

if (comments === null)
return (
<Typography className="loading-text">
Loading comments..
</Typography>
);

if (comments.length === 0)
return (
<Typography className="loading-text">
No comments yet. Be the first!
</Typography>
);

return (
<Stack className="video-comments-container">
{comments.map((comment) => (
Expand All @@ -21,19 +40,26 @@ function VideoComments({ }) {
className="comment-item"
key={comment.id}
>
<CardMedia
image={comment.imageUrl}
className="comment-avatar"
/>
<Stack direction="column">
<Avatar
sx={{
bgcolor: stringToColor(comment.username),
width: 40,
height: 40,
fontSize: 16,
flexShrink: 0,
}}
>
{comment.username?.charAt(0).toUpperCase()}
</Avatar>
<Stack direction="column" sx={{ ml: 1.5 }}>
<Typography className="comment-name">
{comment.name}
{comment.username}
</Typography>
<Typography className="comment-text">
{comment.commentText}
{comment.content}
</Typography>
<Typography className="comment-likes">
<ThumbUpOutlined className ="comment-thumbs-up"/>
<ThumbUpOutlined className="comment-thumbs-up" />
{Number(comment.likeCount).toLocaleString()}
</Typography>
</Stack>
Expand Down
47 changes: 22 additions & 25 deletions glense.client/src/components/VideoStream.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { Link } from "react-router-dom";
import ReactPlayer from "react-player";
import { Typography, Box, Stack, Button, FormControl, Select, MenuItem } from "@mui/material";
import { Typography, Box, Stack, Button, FormControl, Select, MenuItem, Avatar } from "@mui/material";
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import {
CheckCircle,
Expand All @@ -11,8 +11,12 @@ import {
} from "@mui/icons-material";

import { Videos, VideoComments } from ".";
import { videoInfo as demoVideoInfo } from "../utils/constants";
const demoVideoInfo = {
title: 'Loading...', channelTitle: '', viewCount: 0,
likeCount: 0, dislikeCount: 0, publishedAt: '', tags: [], description: ''
};
import { getVideo, getVideos, getPlaylists, addVideoToPlaylist } from "../utils/videoApi";
import { profileService } from "../services/profileService";
import { useAuth } from "../context/AuthContext";

import "../css/VideoStream.css";
Expand All @@ -22,6 +26,7 @@ function VideoStream() {
const [showMoreDesc, setShowMoreDesc] = useState(false);
const { id } = useParams();
const [video, setVideo] = useState(null);
const [uploader, setUploader] = useState(null);
const [related, setRelated] = useState([]);
const [playlists, setPlaylists] = useState([]);
const [addingTo, setAddingTo] = useState("");
Expand All @@ -30,7 +35,15 @@ function VideoStream() {
useEffect(() => {
let mounted = true;
if (!id) return;
getVideo(id).then(d => { if (mounted) setVideo(d); }).catch(() => {});
getVideo(id).then(d => {
if (!mounted) return;
setVideo(d);
if (d?.uploaderId && d.uploaderId !== '00000000-0000-0000-0000-000000000000') {
profileService.getUserById(d.uploaderId)
.then(p => { if (mounted) setUploader(p); })
.catch(() => {});
}
}).catch(() => {});
getVideos().then(list => { if (mounted && Array.isArray(list)) setRelated(list.filter(v => String(v.id) !== String(id)).slice(0, 12)); }).catch(() => {});
return () => { mounted = false; };
}, [id]);
Expand All @@ -56,9 +69,12 @@ function VideoStream() {
<Typography className="video-title">{video?.title || demoVideoInfo.title}</Typography>

<Stack className="video-details">
<Link to={`/channel/${video?.uploaderId || demoVideoInfo.channelId}`}>
<Typography className="channel-title">
{video?.channelTitle || demoVideoInfo.channelTitle}
<Link to={`/channel/${video?.uploaderId}`} style={{ display: 'flex', alignItems: 'center', gap: 10, textDecoration: 'none' }}>
<Avatar sx={{ bgcolor: '#c62828', width: 36, height: 36, fontSize: 16 }}>
{(uploader?.username || video?.title || '?').charAt(0).toUpperCase()}
</Avatar>
<Typography className="channel-title">
{uploader?.username || 'Glense'}
<CheckCircle className="check-circle-icon" />
</Typography>
</Link>
Expand Down Expand Up @@ -162,25 +178,6 @@ function VideoStream() {
<Typography className="comments-section-title">Comments</Typography>
<VideoComments id={id} />

{/* Add to playlist */}
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle1">Add to playlist</Typography>
<select value={addingTo} onChange={(e) => setAddingTo(e.target.value)}>
<option value="">Choose playlist</option>
{playlists.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
<Button variant="contained" sx={{ ml: 1 }} disabled={adding} onClick={async () => {
if (!addingTo || !video?.id) { alert('Select a playlist'); return; }
setAdding(true);
try {
await addVideoToPlaylist(addingTo, video.id);
alert('Added to playlist');
} catch (e) {
alert('Failed to add to playlist');
}
setAdding(false);
}}>Add</Button>
</Box>
</Box>
</Box>

Expand Down
8 changes: 8 additions & 0 deletions glense.client/src/css/Chat/MessageBubble.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@

.message-bubble.other {
justify-content: flex-start !important;
align-items: flex-start !important;
}

.message-bubble-sender {
font-size: 12px !important;
font-weight: bold !important;
color: #90caf9 !important;
margin-bottom: 2px !important;
}

.message-bubble-content {
Expand Down
1 change: 1 addition & 0 deletions glense.client/src/css/Navbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@

.navbar-option-stack {
flex-direction: row !important;
align-items: center !important;
gap: 20px !important;
}

Expand Down
Loading
Loading