diff --git a/app/(browse)/[username]/page.tsx b/app/(browse)/[username]/page.tsx
index 500acdf..8399b59 100644
--- a/app/(browse)/[username]/page.tsx
+++ b/app/(browse)/[username]/page.tsx
@@ -3,8 +3,7 @@ import { notFound } from "next/navigation";
import { getUserByUsername } from "@/lib/user-service";
import { isFollowingUser } from "@/lib/follow-service";
import { isBlockedByUser } from "@/lib/block-service";
-// import StreamPlayer from "@/components/stream-player";
-import Actions from "./_components/actions";
+import StreamPlayer from "@/components/stream-player";
interface UserPageProps {
params: {
@@ -27,23 +26,11 @@ const UserPage = async ({ params }: UserPageProps) => {
}
return (
- //
-
- // TODO: Remove this
-
-
Username: {user.username}
-
user ID: {user.id}
-
is following: {`${isFollowing}`}
-
is blocked: {`${isBlocked}`}
-
-
+
);
};
diff --git a/app/(browse)/_components/navbar/actions.tsx b/app/(browse)/_components/navbar/actions.tsx
index 8e0dd70..5849ec0 100644
--- a/app/(browse)/_components/navbar/actions.tsx
+++ b/app/(browse)/_components/navbar/actions.tsx
@@ -24,7 +24,7 @@ const Actions = async () => {
>
- Dashboard
+ Dashboard
diff --git a/app/(browse)/_components/sidebar/toggle.tsx b/app/(browse)/_components/sidebar/toggle.tsx
index f7ec4a1..9465886 100644
--- a/app/(browse)/_components/sidebar/toggle.tsx
+++ b/app/(browse)/_components/sidebar/toggle.tsx
@@ -33,7 +33,9 @@ const Toggle = () => {
)}
{!collapsed && (
-
Made for you
+
+ Made for you
+
{
)}
{!collapsed && (
-
Dashboard
+
Dashboard
void;
+}
+
+const FullscreenControl = ({
+ isFullscreen,
+ onToggle,
+}: FullscreenControlProps) => {
+ const Icon = isFullscreen ? Minimize : Maximize;
+
+ const label = isFullscreen ? "Exit fullscreen" : "Enter fullscreen";
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default FullscreenControl;
diff --git a/components/stream-player/index.tsx b/components/stream-player/index.tsx
index 1896d8f..678904d 100644
--- a/components/stream-player/index.tsx
+++ b/components/stream-player/index.tsx
@@ -4,14 +4,14 @@ import { Stream, User } from "@prisma/client";
import { LiveKitRoom } from "@livekit/components-react";
import { cn } from "@/lib/utils";
-// import { useChatSidebar } from "@/store/use-chat-sidebar";
+import { useChatSidebar } from "@/store/use-chat-sidebar";
import { useViewerToken } from "@/hooks/use-viewer-token";
// import { InfoCard } from "./info-card";
// import { AboutCard } from "./about-card";
// import { ChatToggle } from "./chat-toggle";
// import { Chat, ChatSkeleton } from "./chat";
-// import { Video, VideoSkeleton } from "./video";
+import Video, { VideoSkeleton } from "./video";
// import { Header, HeaderSkeleton } from "./header";
type CustomStream = {
@@ -41,70 +41,67 @@ interface StreamPlayerProps {
const StreamPlayer = ({ user, stream, isFollowing }: StreamPlayerProps) => {
const { token, name, identity } = useViewerToken(user.id);
- // const { collapsed } = useChatSidebar((state) => state);
+ const { collapsed } = useChatSidebar((state) => state);
if (!token || !name || !identity) {
return ;
}
return (
- // <>
- // {collapsed && (
- //
- //
- //
- // )}
- //
- //
- //
- //
- //
- //
- // >
-
- // TODO: Remove this
- <>STREAM PLAYER>
+ <>
+ {/* {collapsed && (
+
+
+
+ )} */}
+
+
+
+ {/*
*/}
+ {/*
*/}
+ {/*
*/}
+
+ {/*
+
+
*/}
+
+ >
);
};
@@ -114,7 +111,7 @@ export const StreamPlayerSkeleton = () => {
return (
- {/* */}
+
{/* */}
{/* */}
diff --git a/components/stream-player/live-video.tsx b/components/stream-player/live-video.tsx
new file mode 100644
index 0000000..555c54c
--- /dev/null
+++ b/components/stream-player/live-video.tsx
@@ -0,0 +1,94 @@
+"use client";
+
+import { useRef, useState, useEffect } from "react";
+import { Participant, Track } from "livekit-client";
+import { useTracks } from "@livekit/components-react";
+import { useEventListener } from "usehooks-ts";
+
+import VolumeControl from "./volume-control";
+import FullscreenControl from "./fullscreen-control";
+
+interface LiveVideoProps {
+ participant: Participant;
+}
+
+const LiveVideo = ({ participant }: LiveVideoProps) => {
+ const videoRef = useRef
(null);
+ const wrapperRef = useRef(null);
+
+ const [isFullscreen, setIsFullscreen] = useState(false);
+ const [volume, setVolume] = useState(0);
+
+ const onVolumeChange = (value: number) => {
+ setVolume(+value);
+ if (videoRef?.current) {
+ videoRef.current.muted = value === 0;
+ videoRef.current.volume = +value * 0.01;
+ }
+ };
+
+ const toggleMute = () => {
+ const isMuted = volume === 0;
+
+ setVolume(isMuted ? 50 : 0);
+
+ if (videoRef?.current) {
+ videoRef.current.muted = !isMuted;
+ videoRef.current.volume = isMuted ? 0.5 : 0;
+ }
+ };
+
+ useEffect(() => {
+ onVolumeChange(0);
+ }, []);
+
+ const toggleFullscreen = () => {
+ if (isFullscreen) {
+ document.exitFullscreen();
+ } else if (wrapperRef?.current) {
+ wrapperRef.current.requestFullscreen();
+ }
+ };
+
+ const handleFullscreenChange = () => {
+ const isCurrentlyFullscreen = document.fullscreenElement !== null;
+ setIsFullscreen(isCurrentlyFullscreen);
+ };
+
+ useEventListener("fullscreenchange", handleFullscreenChange, wrapperRef);
+
+ useTracks([Track.Source.Camera, Track.Source.Microphone])
+ .filter((track) => track.participant.identity === participant.identity)
+ .forEach((track) => {
+ if (videoRef.current) {
+ track.publication.track?.attach(videoRef.current);
+ }
+ });
+
+ return (
+
+ );
+};
+
+export default LiveVideo;
diff --git a/components/stream-player/loading-video.tsx b/components/stream-player/loading-video.tsx
new file mode 100644
index 0000000..813372a
--- /dev/null
+++ b/components/stream-player/loading-video.tsx
@@ -0,0 +1,16 @@
+import { Loader } from "lucide-react";
+
+interface LoadingVideoProps {
+ label: string;
+}
+
+const LoadingVideo = ({ label }: LoadingVideoProps) => {
+ return (
+
+ );
+};
+
+export default LoadingVideo;
diff --git a/components/stream-player/offline-video.tsx b/components/stream-player/offline-video.tsx
new file mode 100644
index 0000000..a011114
--- /dev/null
+++ b/components/stream-player/offline-video.tsx
@@ -0,0 +1,16 @@
+import { WifiOff } from "lucide-react";
+
+interface OfflineVideoProps {
+ username: string;
+}
+
+const OfflineVideo = ({ username }: OfflineVideoProps) => {
+ return (
+
+
+
{username} is offline
+
+ );
+};
+
+export default OfflineVideo;
diff --git a/components/stream-player/video.tsx b/components/stream-player/video.tsx
new file mode 100644
index 0000000..1ee8ba2
--- /dev/null
+++ b/components/stream-player/video.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { ConnectionState, Track } from "livekit-client";
+import {
+ useConnectionState,
+ useRemoteParticipant,
+ useTracks,
+} from "@livekit/components-react";
+
+import { Skeleton } from "@/components/ui/skeleton";
+
+import OfflineVideo from "./offline-video";
+import LoadingVideo from "./loading-video";
+import LiveVideo from "./live-video";
+
+interface VideoProps {
+ hostName: string;
+ hostIdentity: string;
+}
+
+const Video = ({ hostName, hostIdentity }: VideoProps) => {
+ const connectionState = useConnectionState();
+ const participant = useRemoteParticipant(hostIdentity);
+ const tracks = useTracks([
+ Track.Source.Camera,
+ Track.Source.Microphone,
+ ]).filter((track) => track.participant.identity === hostIdentity);
+
+ let content;
+
+ if (!participant && connectionState === ConnectionState.Connected) {
+ content = ;
+ } else if (!participant || tracks.length === 0) {
+ content = ;
+ } else {
+ content = ;
+ }
+ //TODO: check border if I remove it or not
+ return {content}
;
+};
+
+export default Video;
+
+export const VideoSkeleton = () => {
+ return (
+
+
+
+ );
+};
diff --git a/components/stream-player/volume-control.tsx b/components/stream-player/volume-control.tsx
new file mode 100644
index 0000000..7121998
--- /dev/null
+++ b/components/stream-player/volume-control.tsx
@@ -0,0 +1,59 @@
+"use client";
+
+import { Volume1, Volume2, VolumeX } from "lucide-react";
+
+import Hint from "@/components/hint";
+import { Slider } from "@/components/ui/slider";
+import { Button } from "../ui/button";
+
+interface VolumeControlProps {
+ onToggle: () => void;
+ onChange: (value: number) => void;
+ value: number;
+}
+
+const VolumeControl = ({ onToggle, onChange, value }: VolumeControlProps) => {
+ const isMuted = value === 0;
+ const isAboveHalf = value > 50;
+
+ let Icon = Volume1;
+
+ if (isMuted) {
+ Icon = VolumeX;
+ } else if (isAboveHalf) {
+ Icon = Volume2;
+ }
+
+ const label = isMuted ? "Unmute" : "Mute";
+
+ const handleChange = (value: number[]) => {
+ onChange(value[0]);
+ };
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default VolumeControl;
diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx
new file mode 100644
index 0000000..c31c2b3
--- /dev/null
+++ b/components/ui/slider.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import * as SliderPrimitive from "@radix-ui/react-slider"
+
+import { cn } from "@/lib/utils"
+
+const Slider = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+
+))
+Slider.displayName = SliderPrimitive.Root.displayName
+
+export { Slider }
diff --git a/next.config.js b/next.config.js
index 3d06ef0..7278a95 100644
--- a/next.config.js
+++ b/next.config.js
@@ -12,6 +12,15 @@ const nextConfig = {
},
],
},
+ // webpack: (config) => {
+ // config.module.rules.push({
+ // test: /\.mjs$/,
+ // include: /node_modules/,
+ // type: "javascript/auto",
+ // });
+
+ // return config;
+ // },
};
module.exports = nextConfig;
diff --git a/package-lock.json b/package-lock.json
index e1f8f39..4fa258f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
+ "@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
@@ -1192,6 +1193,39 @@
}
}
},
+ "node_modules/@radix-ui/react-slider": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.1.2.tgz",
+ "integrity": "sha512-NKs15MJylfzVsCagVSWKhGGLNR1W9qWs+HtgbmjjVUB3B9+lb3PYoXxVju3kOrpf0VKyVCtZp+iTwVoqpa1Chw==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/number": "1.0.1",
+ "@radix-ui/primitive": "1.0.1",
+ "@radix-ui/react-collection": "1.0.3",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-context": "1.0.1",
+ "@radix-ui/react-direction": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-use-controllable-state": "1.0.1",
+ "@radix-ui/react-use-layout-effect": "1.0.1",
+ "@radix-ui/react-use-previous": "1.0.1",
+ "@radix-ui/react-use-size": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
diff --git a/package.json b/package.json
index 400c6b0..3a4d991 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
+ "@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
diff --git a/store/use-chat-sidebar.ts b/store/use-chat-sidebar.ts
new file mode 100644
index 0000000..f06752b
--- /dev/null
+++ b/store/use-chat-sidebar.ts
@@ -0,0 +1,22 @@
+import { create } from "zustand";
+
+export enum ChatVariant {
+ CHAT = "CHAT",
+ COMMUNITY = "COMMUNITY",
+}
+
+interface ChatSidebarStore {
+ collapsed: boolean;
+ variant: ChatVariant;
+ onExpand: () => void;
+ onCollapse: () => void;
+ onChangeVariant: (variant: ChatVariant) => void;
+}
+
+export const useChatSidebar = create((set) => ({
+ collapsed: false,
+ variant: ChatVariant.CHAT,
+ onExpand: () => set(() => ({ collapsed: false })),
+ onCollapse: () => set(() => ({ collapsed: true })),
+ onChangeVariant: (variant: ChatVariant) => set(() => ({ variant })),
+}));