UPDATE: Finished categories and category pages, added infinite scrolling and follow category button on login
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user