UPDATE: Finished categories and category pages, added infinite scrolling and follow category button on login

This commit is contained in:
JustIceO7
2025-02-23 17:23:09 +00:00
parent f192518382
commit 634c02a9bc
17 changed files with 341 additions and 209 deletions

View File

@@ -65,7 +65,7 @@ const ListItem: React.FC<ListItemProps> = ({
)}
</div>
<div className="p-3">
<h3 className="font-semibold text-lg text-center">{title}</h3>
<h3 className="font-semibold text-lg text-center truncate max-w-full">{title}</h3>
{type === "stream" && <p className="font-bold">{username}</p>}
{type === "stream" && (
<p className="text-sm text-gray-300">{streamCategory}</p>

View File

@@ -1,4 +1,4 @@
import React, { useRef } from "react";
import React, { forwardRef, useImperativeHandle, useRef, useState } from "react";
import {
ArrowLeft as ArrowLeftIcon,
ArrowRight as ArrowRightIcon,
@@ -22,124 +22,120 @@ interface ListRowProps {
children?: React.ReactNode;
}
// Row of entries
const ListRow: React.FC<ListRowProps> = ({
variant = "default",
type,
title = "",
description = "",
items,
wrap = false,
titleClickable = false,
onClick,
extraClasses = "",
itemExtraClasses = "",
amountForScroll = 4,
children,
}) => {
const slider = useRef<HTMLDivElement>(null);
const scrollAmount = window.innerWidth * 0.3;
const navigate = useNavigate();
const ListRow = forwardRef<{ addMoreItems: (newItems: ListItemProps[]) => void }, ListRowProps>(
({ variant, type, title = "", description = "", items, wrap, onClick, titleClickable, extraClasses = "", itemExtraClasses = "", amountForScroll, children }, ref) => {
const [currentItems, setCurrentItems] = useState(items);
const slider = useRef<HTMLDivElement>(null);
const scrollAmount = window.innerWidth * 0.3;
const navigate = useNavigate();
const slideRight = () => {
if (!wrap && slider.current) {
slider.current.scrollBy({ left: +scrollAmount, behavior: "smooth" });
}
};
const addMoreItems = (newItems: ListItemProps[]) => {
setCurrentItems((prevItems) => [...prevItems, ...newItems]);
};
const slideLeft = () => {
if (!wrap && slider.current) {
slider.current.scrollBy({ left: -scrollAmount, behavior: "smooth" });
}
};
useImperativeHandle(ref, () => ({
addMoreItems,
}));
const handleTitleClick = (type: string) => {
switch (type) {
case "stream":
break;
case "category":
navigate("/categories");
break;
case "user":
break;
default:
break;
}
};
const slideRight = () => {
if (!wrap && slider.current) {
slider.current.scrollBy({ left: +scrollAmount, behavior: "smooth" });
}
};
return (
<div
className={`${extraClasses} flex w-full rounded-[1.5rem] text-white transition-all ${
variant === "search"
? "items-center"
: "flex-col space-y-4 py-6 px-5 mx-2 mt-5"
}`}
>
const slideLeft = () => {
if (!wrap && slider.current) {
slider.current.scrollBy({ left: -scrollAmount, behavior: "smooth" });
}
};
const handleTitleClick = (type: string) => {
switch (type) {
case "stream":
break;
case "category":
navigate("/categories");
break;
case "user":
break;
default:
break;
}
};
return (
<div
className={`text-center ${
variant === "search" ? "min-w-fit px-auto w-[15vw]" : ""
className={`${extraClasses} flex w-full rounded-[1.5rem] text-white transition-all ${
variant === "search"
? "items-center"
: "flex-col space-y-4 py-6 px-5 mx-2 mt-5"
}`}
>
<h2
className={`${
titleClickable ? "cursor-pointer hover:underline" : "cursor-default"
} text-2xl font-bold`}
onClick={titleClickable ? () => handleTitleClick(type) : undefined}
>
{title}
</h2>
<p>{description}</p>
</div>
<div className="relative overflow-hidden flex flex-grow items-center z-0">
{!wrap && items.length > amountForScroll && (
<>
<ArrowLeftIcon
onClick={slideLeft}
size={30}
className="absolute left-0 cursor-pointer z-[999]"
/>
<ArrowRightIcon
onClick={slideRight}
size={30}
className="absolute right-0 cursor-pointer z-[999]"
/>
</>
)}
<div
ref={slider}
className={`flex ${
wrap ? "flex-wrap" : "overflow-x-scroll whitespace-nowrap"
} max-w-[95%] items-center justify-evenly w-full mx-auto scroll-smooth scrollbar-hide gap-5`}
className={`text-center ${
variant === "search" ? "min-w-fit px-auto w-[15vw]" : ""
}`}
>
{items.map((item) => (
<ListItem
key={`${item.type}-${item.id}`}
id={item.id}
type={type}
title={item.title}
username={item.type === "category" ? undefined : item.username}
streamCategory={
item.type === "stream" ? item.streamCategory : undefined
}
viewers={item.viewers}
thumbnail={item.thumbnail}
onItemClick={() =>
(item.type === "stream" || item.type === "user") &&
item.username
? onClick?.(item.username)
: onClick?.(item.title)
}
extraClasses={`${itemExtraClasses} min-w-[20vw]`}
/>
))}
<h2
className={`${
titleClickable ? "cursor-pointer hover:underline" : "cursor-default"
} text-2xl font-bold`}
onClick={titleClickable ? () => handleTitleClick(type) : undefined}
>
{title}
</h2>
<p>{description}</p>
</div>
<div className="relative overflow-hidden flex flex-grow items-center z-0">
{!wrap && currentItems.length > (amountForScroll || 0) && (
<>
<ArrowLeftIcon
onClick={slideLeft}
size={30}
className="absolute left-0 cursor-pointer z-[999]"
/>
<ArrowRightIcon
onClick={slideRight}
size={30}
className="absolute right-0 cursor-pointer z-[999]"
/>
</>
)}
<div
ref={slider}
className={`flex ${
wrap ? "flex-wrap" : "overflow-x-scroll whitespace-nowrap"
} max-w-[95%] items-center justify-evenly w-full mx-auto scroll-smooth scrollbar-hide gap-5`}
>
{currentItems.map((item) => (
<ListItem
key={`${item.type}-${item.id}`}
id={item.id}
type={type}
title={item.title}
username={item.type === "category" ? undefined : item.username}
streamCategory={
item.type === "stream" ? item.streamCategory : undefined
}
viewers={item.viewers}
thumbnail={item.thumbnail}
onItemClick={() =>
(item.type === "stream" || item.type === "user") &&
item.username
? onClick?.(item.username)
: onClick?.(item.title)
}
extraClasses={`${itemExtraClasses} min-w-[20vw] max-w-[20vw]`}
/>
))}
</div>
</div>
{children}
</div>
);
}
);
{children}
</div>
);
};
export default ListRow;
export default ListRow;

View File

@@ -21,7 +21,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
streamId,
onViewerCountChange,
}) => {
const { isLoggedIn, username, userId} = useAuth();
const { isLoggedIn, username, userId } = useAuth();
const { showAuthModal, setShowAuthModal } = useAuthModal();
const { socket, isConnected } = useSocket();
const [messages, setMessages] = useState<ChatMessage[]>([]);
@@ -40,10 +40,11 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
// Handle beforeunload event
const handleBeforeUnload = () => {
socket.emit("leave", {
socket.emit("leave", {
userId: userId ? userId : null,
username: username ? username : "Guest",
stream_id: streamId, });
stream_id: streamId,
});
socket.disconnect();
};
@@ -135,13 +136,12 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
{messages.map((msg, index) => (
<div
key={index}
className="flex items-start space-x-2 bg-gray-800 rounded p-2 text-white"
className="flex items-start space-x-2 bg-gray-800 rounded p-2 text-white relative"
>
{/* User avatar with image */}
<div
className={`w-2em h-2em rounded-full overflow-hidden flex-shrink-0 ${
msg.chatter_username === username ? "" : "cursor-pointer"
}`}
className={`w-2em h-2em rounded-full overflow-hidden flex-shrink-0 ${msg.chatter_username === username ? "" : "cursor-pointer"
}`}
onClick={() =>
msg.chatter_username === username
? null
@@ -160,11 +160,10 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
<div className="flex items-center space-x-0.5em">
{/* Username */}
<span
className={`font-bold text-[1em] ${
msg.chatter_username === username
className={`font-bold text-[1em] ${msg.chatter_username === username
? "text-purple-600"
: "text-green-400 cursor-pointer"
}`}
}`}
onClick={() =>
msg.chatter_username === username
? null
@@ -181,8 +180,8 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
</div>
{/* Time sent */}
<div className="text-gray-500 text-[0.8em] self-start">
{new Date(msg.time_sent).toLocaleTimeString()}
<div className="text-gray-500 text-[0.8em] absolute top-0 right-0 p-2">
{new Date(msg.time_sent).toLocaleTimeString('en-GB', { hour12: false, hour: '2-digit', minute: '2-digit' })}
</div>
</div>
))}