Spaces:
Sleeping
Sleeping
copilot-swe-agent[bot]
ArnavSingh76533
commited on
Commit
·
ce09e2f
1
Parent(s):
4b0bad1
Modernize UI design with updated colors, typography, and components
Browse filesCo-authored-by: ArnavSingh76533 <160649079+ArnavSingh76533@users.noreply.github.com>
- components/Footer.tsx +8 -8
- components/Navbar.tsx +12 -8
- components/Room.tsx +5 -4
- components/action/Button.tsx +1 -1
- components/chat/ChatPanel.tsx +20 -10
- components/input/InputSlider.module.css +17 -11
- components/input/InputText.tsx +8 -6
- components/input/InputUrl.tsx +11 -9
- components/modal/Modal.tsx +34 -12
- components/playlist/PlaylistItem.tsx +18 -12
- components/playlist/PlaylistMenu.tsx +6 -6
- components/search/YoutubeSearch.tsx +14 -12
- pages/global.css +37 -3
- pages/index.tsx +35 -16
- tailwind.config.js +24 -11
components/Footer.tsx
CHANGED
|
@@ -9,16 +9,16 @@ interface Props {
|
|
| 9 |
|
| 10 |
const Footer: FC<Props> = ({ error }) => {
|
| 11 |
return (
|
| 12 |
-
<footer className={"flex flex-col bg-dark-900 py-
|
| 13 |
-
{error && <div>Error {error}</div>}
|
| 14 |
-
<div className={"text-sm flex flex-col gap-
|
| 15 |
-
<div className={"flex flex-row items-center"}>
|
| 16 |
<IconCopyright sizeClassName={"h-3 w-3"}/>
|
| 17 |
<NewTabLink href={"t.me/yucant"}>Charlie</NewTabLink>
|
| 18 |
-
2022
|
| 19 |
</div>
|
| 20 |
|
| 21 |
-
<div>
|
| 22 |
Icons by
|
| 23 |
<NewTabLink href={"https://heroicons.com"}>Heroicons</NewTabLink>
|
| 24 |
and
|
|
@@ -26,10 +26,10 @@ const Footer: FC<Props> = ({ error }) => {
|
|
| 26 |
</div>
|
| 27 |
|
| 28 |
<NewTabLink
|
| 29 |
-
className={"ml-auto flex items-center"}
|
| 30 |
href={"shivam413-Streamer.hf.space"}
|
| 31 |
>
|
| 32 |
-
<IconGithub className={"
|
| 33 |
</NewTabLink>
|
| 34 |
</div>
|
| 35 |
</footer>
|
|
|
|
| 9 |
|
| 10 |
const Footer: FC<Props> = ({ error }) => {
|
| 11 |
return (
|
| 12 |
+
<footer className={"flex flex-col bg-gradient-to-r from-dark-900 to-dark-800 py-3 px-6 border-t border-dark-700/50 mt-auto"}>
|
| 13 |
+
{error && <div className="text-red-400 font-medium mb-2">Error {error}</div>}
|
| 14 |
+
<div className={"text-sm flex flex-col gap-2 sm:flex-row sm:items-center text-dark-400"}>
|
| 15 |
+
<div className={"flex flex-row items-center gap-1"}>
|
| 16 |
<IconCopyright sizeClassName={"h-3 w-3"}/>
|
| 17 |
<NewTabLink href={"t.me/yucant"}>Charlie</NewTabLink>
|
| 18 |
+
2022
|
| 19 |
</div>
|
| 20 |
|
| 21 |
+
<div className="flex flex-row items-center gap-1">
|
| 22 |
Icons by
|
| 23 |
<NewTabLink href={"https://heroicons.com"}>Heroicons</NewTabLink>
|
| 24 |
and
|
|
|
|
| 26 |
</div>
|
| 27 |
|
| 28 |
<NewTabLink
|
| 29 |
+
className={"ml-auto flex items-center gap-1 hover:text-primary-500 transition-colors"}
|
| 30 |
href={"shivam413-Streamer.hf.space"}
|
| 31 |
>
|
| 32 |
+
<IconGithub className={"w-4 h-4"} /> Github
|
| 33 |
</NewTabLink>
|
| 34 |
</div>
|
| 35 |
</footer>
|
components/Navbar.tsx
CHANGED
|
@@ -12,11 +12,11 @@ const Navbar = ({ roomId }: { roomId?: string }) => {
|
|
| 12 |
const [showShare, setShowShare] = useState(false)
|
| 13 |
|
| 14 |
return (
|
| 15 |
-
<div className={"py-
|
| 16 |
<Link
|
| 17 |
href={"/"}
|
| 18 |
className={
|
| 19 |
-
"flex p-
|
| 20 |
}
|
| 21 |
>
|
| 22 |
<Image
|
|
@@ -24,8 +24,9 @@ const Navbar = ({ roomId }: { roomId?: string }) => {
|
|
| 24 |
alt={"Web-SyncPlay logo"}
|
| 25 |
width={36}
|
| 26 |
height={36}
|
|
|
|
| 27 |
/>
|
| 28 |
-
<span className={"hide-below-sm"}>{getSiteName()}</span>
|
| 29 |
</Link>
|
| 30 |
{roomId && (
|
| 31 |
<>
|
|
@@ -34,21 +35,21 @@ const Navbar = ({ roomId }: { roomId?: string }) => {
|
|
| 34 |
show={showShare}
|
| 35 |
close={() => setShowShare(false)}
|
| 36 |
>
|
| 37 |
-
<div>Share this link to let more people join in on the fun</div>
|
| 38 |
<InputClipboardCopy
|
| 39 |
-
className={"bg-dark-1000"}
|
| 40 |
value={getSiteDomain() + "/room/" + roomId}
|
| 41 |
/>
|
| 42 |
</Modal>
|
| 43 |
<Button
|
| 44 |
tooltip={"Share the room link"}
|
| 45 |
id={"navbar"}
|
| 46 |
-
actionClasses={"hover:bg-primary-
|
| 47 |
-
className={"ml-auto
|
| 48 |
onClick={() => setShowShare(true)}
|
| 49 |
>
|
| 50 |
<div className={"flex items-center mx-1"}>
|
| 51 |
-
<IconShare className={"mr-
|
| 52 |
Share
|
| 53 |
</div>
|
| 54 |
</Button>
|
|
@@ -60,6 +61,9 @@ const Navbar = ({ roomId }: { roomId?: string }) => {
|
|
| 60 |
place={"bottom"}
|
| 61 |
style={{
|
| 62 |
backgroundColor: "var(--dark-700)",
|
|
|
|
|
|
|
|
|
|
| 63 |
}}
|
| 64 |
/>
|
| 65 |
</div>
|
|
|
|
| 12 |
const [showShare, setShowShare] = useState(false)
|
| 13 |
|
| 14 |
return (
|
| 15 |
+
<div className={"py-2 px-4 flex flex-row gap-2 items-stretch bg-gradient-to-r from-dark-900 to-dark-800 border-b border-dark-700/50 shadow-lg"}>
|
| 16 |
<Link
|
| 17 |
href={"/"}
|
| 18 |
className={
|
| 19 |
+
"flex p-2 shrink-0 flex-row gap-2 items-center rounded-lg action"
|
| 20 |
}
|
| 21 |
>
|
| 22 |
<Image
|
|
|
|
| 24 |
alt={"Web-SyncPlay logo"}
|
| 25 |
width={36}
|
| 26 |
height={36}
|
| 27 |
+
className="rounded-lg"
|
| 28 |
/>
|
| 29 |
+
<span className={"hide-below-sm font-semibold text-lg"}>{getSiteName()}</span>
|
| 30 |
</Link>
|
| 31 |
{roomId && (
|
| 32 |
<>
|
|
|
|
| 35 |
show={showShare}
|
| 36 |
close={() => setShowShare(false)}
|
| 37 |
>
|
| 38 |
+
<div className="text-dark-300 mb-3">Share this link to let more people join in on the fun</div>
|
| 39 |
<InputClipboardCopy
|
| 40 |
+
className={"bg-dark-1000 rounded-lg"}
|
| 41 |
value={getSiteDomain() + "/room/" + roomId}
|
| 42 |
/>
|
| 43 |
</Modal>
|
| 44 |
<Button
|
| 45 |
tooltip={"Share the room link"}
|
| 46 |
id={"navbar"}
|
| 47 |
+
actionClasses={"hover:bg-primary-700 active:bg-primary-800 shadow-md hover:shadow-glow"}
|
| 48 |
+
className={"ml-auto px-4 py-2 bg-primary-600 font-medium"}
|
| 49 |
onClick={() => setShowShare(true)}
|
| 50 |
>
|
| 51 |
<div className={"flex items-center mx-1"}>
|
| 52 |
+
<IconShare className={"mr-2"} />
|
| 53 |
Share
|
| 54 |
</div>
|
| 55 |
</Button>
|
|
|
|
| 61 |
place={"bottom"}
|
| 62 |
style={{
|
| 63 |
backgroundColor: "var(--dark-700)",
|
| 64 |
+
borderRadius: "0.5rem",
|
| 65 |
+
padding: "0.5rem 0.75rem",
|
| 66 |
+
fontSize: "0.875rem",
|
| 67 |
}}
|
| 68 |
/>
|
| 69 |
</div>
|
components/Room.tsx
CHANGED
|
@@ -72,14 +72,15 @@ const Room: FC<Props> = ({ id }) => {
|
|
| 72 |
}
|
| 73 |
|
| 74 |
return (
|
| 75 |
-
<div className={"flex flex-col sm:flex-row gap-
|
| 76 |
<div className={"grow"}>
|
| 77 |
<Player roomId={id} socket={socket} />
|
| 78 |
|
| 79 |
-
<div className={"flex flex-row gap-
|
| 80 |
<Button
|
| 81 |
tooltip={"Do a forced manual sync"}
|
| 82 |
-
className={"
|
|
|
|
| 83 |
onClick={() => {
|
| 84 |
console.log("Fetching update", socket?.id)
|
| 85 |
socket?.emit("fetch")
|
|
@@ -105,7 +106,7 @@ const Room: FC<Props> = ({ id }) => {
|
|
| 105 |
</div>
|
| 106 |
|
| 107 |
{/* Chat + YouTube Search */}
|
| 108 |
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-
|
| 109 |
<ChatPanel socket={socket} />
|
| 110 |
<YoutubeSearch socket={socket} />
|
| 111 |
</div>
|
|
|
|
| 72 |
}
|
| 73 |
|
| 74 |
return (
|
| 75 |
+
<div className={"flex flex-col sm:flex-row gap-2"}>
|
| 76 |
<div className={"grow"}>
|
| 77 |
<Player roomId={id} socket={socket} />
|
| 78 |
|
| 79 |
+
<div className={"flex flex-row gap-2 p-2 bg-dark-900/50 rounded-lg border border-dark-700/50 mt-2"}>
|
| 80 |
<Button
|
| 81 |
tooltip={"Do a forced manual sync"}
|
| 82 |
+
className={"px-3 py-2 flex flex-row gap-2 items-center"}
|
| 83 |
+
actionClasses={"bg-dark-800 hover:bg-dark-700 active:bg-dark-600 border border-dark-700/50"}
|
| 84 |
onClick={() => {
|
| 85 |
console.log("Fetching update", socket?.id)
|
| 86 |
socket?.emit("fetch")
|
|
|
|
| 106 |
</div>
|
| 107 |
|
| 108 |
{/* Chat + YouTube Search */}
|
| 109 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 p-2 mt-2">
|
| 110 |
<ChatPanel socket={socket} />
|
| 111 |
<YoutubeSearch socket={socket} />
|
| 112 |
</div>
|
components/action/Button.tsx
CHANGED
|
@@ -30,7 +30,7 @@ const Button: FC<Props> = ({
|
|
| 30 |
onClick={onClick}
|
| 31 |
type={type}
|
| 32 |
disabled={disabled}
|
| 33 |
-
className={classNames("
|
| 34 |
>
|
| 35 |
{children}
|
| 36 |
</button>
|
|
|
|
| 30 |
onClick={onClick}
|
| 31 |
type={type}
|
| 32 |
disabled={disabled}
|
| 33 |
+
className={classNames("px-3 py-2 rounded-lg font-medium transition-all duration-200", actionClasses, className)}
|
| 34 |
>
|
| 35 |
{children}
|
| 36 |
</button>
|
components/chat/ChatPanel.tsx
CHANGED
|
@@ -49,20 +49,27 @@ const ChatPanel: FC<Props> = ({ socket, className }) => {
|
|
| 49 |
}
|
| 50 |
|
| 51 |
return (
|
| 52 |
-
<div className={className ?? "flex flex-col h-64 border rounded-
|
| 53 |
-
<div className="flex-1 overflow-y-auto p-
|
| 54 |
{messages.map((m) => (
|
| 55 |
-
<div key={m.id} className="text-sm">
|
| 56 |
-
<
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
| 59 |
</div>
|
| 60 |
))}
|
| 61 |
-
{messages.length === 0 &&
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
</div>
|
| 63 |
-
<div className="p-
|
| 64 |
<input
|
| 65 |
-
className="input flex-1 bg-
|
| 66 |
placeholder="Type a message…"
|
| 67 |
value={text}
|
| 68 |
onChange={(e) => setText(e.target.value)}
|
|
@@ -70,7 +77,10 @@ const ChatPanel: FC<Props> = ({ socket, className }) => {
|
|
| 70 |
if (e.key === "Enter") send()
|
| 71 |
}}
|
| 72 |
/>
|
| 73 |
-
<button
|
|
|
|
|
|
|
|
|
|
| 74 |
Send
|
| 75 |
</button>
|
| 76 |
</div>
|
|
|
|
| 49 |
}
|
| 50 |
|
| 51 |
return (
|
| 52 |
+
<div className={className ?? "flex flex-col h-64 border border-dark-700/50 rounded-xl overflow-hidden shadow-lg bg-dark-900"}>
|
| 53 |
+
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-dark-900/50">
|
| 54 |
{messages.map((m) => (
|
| 55 |
+
<div key={m.id} className="text-sm bg-dark-800/50 rounded-lg p-3 border border-dark-700/30">
|
| 56 |
+
<div className="flex items-center gap-2 mb-1">
|
| 57 |
+
<span className="font-semibold text-primary-400">{m.name}</span>
|
| 58 |
+
<span className="text-dark-500 text-xs">•</span>
|
| 59 |
+
<span className="text-dark-500 text-xs">{new Date(m.ts).toLocaleTimeString()}</span>
|
| 60 |
+
</div>
|
| 61 |
+
<div className="break-words text-dark-200">{m.text}</div>
|
| 62 |
</div>
|
| 63 |
))}
|
| 64 |
+
{messages.length === 0 && (
|
| 65 |
+
<div className="text-dark-500 text-sm text-center py-8">
|
| 66 |
+
No messages yet. Be the first to say hello! 👋
|
| 67 |
+
</div>
|
| 68 |
+
)}
|
| 69 |
</div>
|
| 70 |
+
<div className="p-3 flex gap-2 bg-dark-800/50 border-t border-dark-700/50">
|
| 71 |
<input
|
| 72 |
+
className="input flex-1 bg-dark-800 border border-dark-700/50 focus:border-primary-500/50 p-2.5 rounded-lg outline-none transition-all duration-200"
|
| 73 |
placeholder="Type a message…"
|
| 74 |
value={text}
|
| 75 |
onChange={(e) => setText(e.target.value)}
|
|
|
|
| 77 |
if (e.key === "Enter") send()
|
| 78 |
}}
|
| 79 |
/>
|
| 80 |
+
<button
|
| 81 |
+
className="btn bg-primary-600 hover:bg-primary-700 active:bg-primary-800 px-4 rounded-lg font-medium transition-all duration-200 shadow-md hover:shadow-glow"
|
| 82 |
+
onClick={send}
|
| 83 |
+
>
|
| 84 |
Send
|
| 85 |
</button>
|
| 86 |
</div>
|
components/input/InputSlider.module.css
CHANGED
|
@@ -5,12 +5,12 @@
|
|
| 5 |
/* input range slider */
|
| 6 |
.slider input[type="range"] {
|
| 7 |
--value: 0%;
|
| 8 |
-
--lower:
|
| 9 |
-
--upper: rgba(255, 255, 255, 0.
|
| 10 |
-
--size:
|
| 11 |
-
--active-size:
|
| 12 |
-
--thumb-size:
|
| 13 |
-
--thumb-active-size:
|
| 14 |
--thumb-color: #fff;
|
| 15 |
|
| 16 |
cursor: pointer;
|
|
@@ -28,7 +28,7 @@
|
|
| 28 |
touch-action: manipulation;
|
| 29 |
-webkit-appearance: none;
|
| 30 |
-moz-appearance: none;
|
| 31 |
-
transition: height ease-in-out 0.
|
| 32 |
}
|
| 33 |
.slider input[type="range"]:hover,
|
| 34 |
.slider input[type="range"]:active {
|
|
@@ -43,12 +43,14 @@
|
|
| 43 |
background-color: var(--thumb-color);
|
| 44 |
border-radius: 100%;
|
| 45 |
cursor: pointer;
|
| 46 |
-
|
|
|
|
| 47 |
}
|
| 48 |
.slider input[type="range"]:hover::-moz-range-thumb,
|
| 49 |
.slider input[type="range"]:active::-moz-range-thumb {
|
| 50 |
width: var(--thumb-active-size);
|
| 51 |
height: var(--thumb-active-size);
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
.slider input[type="range"]::-webkit-slider-thumb {
|
|
@@ -57,13 +59,15 @@
|
|
| 57 |
background-color: var(--thumb-color);
|
| 58 |
border-radius: 100%;
|
| 59 |
cursor: pointer;
|
|
|
|
| 60 |
-webkit-appearance: none;
|
| 61 |
-
transition: width ease-in-out 0.
|
| 62 |
}
|
| 63 |
.slider input[type="range"]:hover::-webkit-slider-thumb,
|
| 64 |
.slider input[type="range"]:active::-webkit-slider-thumb {
|
| 65 |
width: var(--thumb-active-size);
|
| 66 |
height: var(--thumb-active-size);
|
|
|
|
| 67 |
}
|
| 68 |
|
| 69 |
.slider input[type="range"]::-ms-thumb {
|
|
@@ -72,12 +76,14 @@
|
|
| 72 |
background-color: var(--thumb-color);
|
| 73 |
border-radius: 100%;
|
| 74 |
cursor: pointer;
|
| 75 |
-
|
|
|
|
| 76 |
}
|
| 77 |
.slider input[type="range"]:hover::-ms-thumb,
|
| 78 |
.slider input[type="range"]:active::-ms-thumb {
|
| 79 |
width: var(--thumb-active-size);
|
| 80 |
height: var(--thumb-active-size);
|
|
|
|
| 81 |
}
|
| 82 |
|
| 83 |
/* edge */
|
|
@@ -89,7 +95,7 @@
|
|
| 89 |
width: 100%;
|
| 90 |
height: var(--size);
|
| 91 |
cursor: pointer;
|
| 92 |
-
transition: width ease-in-out 0.
|
| 93 |
}
|
| 94 |
|
| 95 |
.slider input[type="range"]::-ms-fill-lower,
|
|
|
|
| 5 |
/* input range slider */
|
| 6 |
.slider input[type="range"] {
|
| 7 |
--value: 0%;
|
| 8 |
+
--lower: #10b981;
|
| 9 |
+
--upper: rgba(255, 255, 255, 0.3);
|
| 10 |
+
--size: 6px;
|
| 11 |
+
--active-size: 10px;
|
| 12 |
+
--thumb-size: 12px;
|
| 13 |
+
--thumb-active-size: 16px;
|
| 14 |
--thumb-color: #fff;
|
| 15 |
|
| 16 |
cursor: pointer;
|
|
|
|
| 28 |
touch-action: manipulation;
|
| 29 |
-webkit-appearance: none;
|
| 30 |
-moz-appearance: none;
|
| 31 |
+
transition: height ease-in-out 0.2s;
|
| 32 |
}
|
| 33 |
.slider input[type="range"]:hover,
|
| 34 |
.slider input[type="range"]:active {
|
|
|
|
| 43 |
background-color: var(--thumb-color);
|
| 44 |
border-radius: 100%;
|
| 45 |
cursor: pointer;
|
| 46 |
+
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
|
| 47 |
+
transition: width ease-in-out 0.2s, height ease-in-out 0.2s, box-shadow ease-in-out 0.2s;
|
| 48 |
}
|
| 49 |
.slider input[type="range"]:hover::-moz-range-thumb,
|
| 50 |
.slider input[type="range"]:active::-moz-range-thumb {
|
| 51 |
width: var(--thumb-active-size);
|
| 52 |
height: var(--thumb-active-size);
|
| 53 |
+
box-shadow: 0 0 16px rgba(16, 185, 129, 0.6);
|
| 54 |
}
|
| 55 |
|
| 56 |
.slider input[type="range"]::-webkit-slider-thumb {
|
|
|
|
| 59 |
background-color: var(--thumb-color);
|
| 60 |
border-radius: 100%;
|
| 61 |
cursor: pointer;
|
| 62 |
+
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
|
| 63 |
-webkit-appearance: none;
|
| 64 |
+
transition: width ease-in-out 0.2s, height ease-in-out 0.2s, box-shadow ease-in-out 0.2s;
|
| 65 |
}
|
| 66 |
.slider input[type="range"]:hover::-webkit-slider-thumb,
|
| 67 |
.slider input[type="range"]:active::-webkit-slider-thumb {
|
| 68 |
width: var(--thumb-active-size);
|
| 69 |
height: var(--thumb-active-size);
|
| 70 |
+
box-shadow: 0 0 16px rgba(16, 185, 129, 0.6);
|
| 71 |
}
|
| 72 |
|
| 73 |
.slider input[type="range"]::-ms-thumb {
|
|
|
|
| 76 |
background-color: var(--thumb-color);
|
| 77 |
border-radius: 100%;
|
| 78 |
cursor: pointer;
|
| 79 |
+
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
|
| 80 |
+
transition: width ease-in-out 0.2s, height ease-in-out 0.2s, box-shadow ease-in-out 0.2s;
|
| 81 |
}
|
| 82 |
.slider input[type="range"]:hover::-ms-thumb,
|
| 83 |
.slider input[type="range"]:active::-ms-thumb {
|
| 84 |
width: var(--thumb-active-size);
|
| 85 |
height: var(--thumb-active-size);
|
| 86 |
+
box-shadow: 0 0 16px rgba(16, 185, 129, 0.6);
|
| 87 |
}
|
| 88 |
|
| 89 |
/* edge */
|
|
|
|
| 95 |
width: 100%;
|
| 96 |
height: var(--size);
|
| 97 |
cursor: pointer;
|
| 98 |
+
transition: width ease-in-out 0.2s, height ease-in-out 0.2s;
|
| 99 |
}
|
| 100 |
|
| 101 |
.slider input[type="range"]::-ms-fill-lower,
|
components/input/InputText.tsx
CHANGED
|
@@ -23,15 +23,15 @@ const InputText: FC<Props> = ({
|
|
| 23 |
return (
|
| 24 |
<div
|
| 25 |
className={classNames(
|
| 26 |
-
"rounded grow flex flex-row items-center bg-dark-800
|
| 27 |
className
|
| 28 |
)}
|
| 29 |
>
|
| 30 |
-
{icon && <div className={"ml-
|
| 31 |
<input
|
| 32 |
ref={inputRef}
|
| 33 |
size={1}
|
| 34 |
-
className={"grow rounded bg-
|
| 35 |
placeholder={placeholder}
|
| 36 |
value={value}
|
| 37 |
onChange={(event) => onChange(event.target.value)}
|
|
@@ -39,9 +39,11 @@ const InputText: FC<Props> = ({
|
|
| 39 |
required={required}
|
| 40 |
onFocus={() => inputRef.current?.select()}
|
| 41 |
/>
|
| 42 |
-
|
| 43 |
-
<
|
| 44 |
-
|
|
|
|
|
|
|
| 45 |
</div>
|
| 46 |
)
|
| 47 |
}
|
|
|
|
| 23 |
return (
|
| 24 |
<div
|
| 25 |
className={classNames(
|
| 26 |
+
"rounded-lg grow flex flex-row items-center bg-dark-800 border border-dark-700/50 focus-within:border-primary-500/50 transition-all duration-200",
|
| 27 |
className
|
| 28 |
)}
|
| 29 |
>
|
| 30 |
+
{icon && <div className={"ml-2"}>{icon}</div>}
|
| 31 |
<input
|
| 32 |
ref={inputRef}
|
| 33 |
size={1}
|
| 34 |
+
className={"grow rounded-lg bg-transparent px-3 py-2.5 outline-none " + className}
|
| 35 |
placeholder={placeholder}
|
| 36 |
value={value}
|
| 37 |
onChange={(event) => onChange(event.target.value)}
|
|
|
|
| 39 |
required={required}
|
| 40 |
onFocus={() => inputRef.current?.select()}
|
| 41 |
/>
|
| 42 |
+
{value && (
|
| 43 |
+
<div className={"p-2 cursor-pointer hover:text-red-400 transition-colors"} onClick={() => onChange("")}>
|
| 44 |
+
<IconClose />
|
| 45 |
+
</div>
|
| 46 |
+
)}
|
| 47 |
</div>
|
| 48 |
)
|
| 49 |
}
|
components/input/InputUrl.tsx
CHANGED
|
@@ -40,12 +40,12 @@ const InputUrl: FC<Props> = ({
|
|
| 40 |
className={classNames("flex flex-col", className)}
|
| 41 |
>
|
| 42 |
<div
|
| 43 |
-
className={"rounded grow flex flex-row items-center bg-dark-800
|
| 44 |
>
|
| 45 |
<input
|
| 46 |
ref={inputRef}
|
| 47 |
size={1}
|
| 48 |
-
className={classNames("grow
|
| 49 |
placeholder={placeholder}
|
| 50 |
value={url}
|
| 51 |
onChange={(event) => {
|
|
@@ -54,25 +54,27 @@ const InputUrl: FC<Props> = ({
|
|
| 54 |
type={"text"}
|
| 55 |
onFocus={() => inputRef.current?.select()}
|
| 56 |
/>
|
| 57 |
-
|
| 58 |
-
<
|
| 59 |
-
|
|
|
|
|
|
|
| 60 |
<div>
|
| 61 |
<button
|
| 62 |
type={"submit"}
|
| 63 |
data-tooltip-content={tooltip}
|
| 64 |
className={classNames(
|
| 65 |
-
"
|
| 66 |
valid
|
| 67 |
-
? "bg-primary-
|
| 68 |
-
: "bg-red-600 hover:bg-red-
|
| 69 |
)}
|
| 70 |
>
|
| 71 |
{children}
|
| 72 |
</button>
|
| 73 |
</div>
|
| 74 |
</div>
|
| 75 |
-
{!valid && <div className={"text-red-
|
| 76 |
</form>
|
| 77 |
)
|
| 78 |
}
|
|
|
|
| 40 |
className={classNames("flex flex-col", className)}
|
| 41 |
>
|
| 42 |
<div
|
| 43 |
+
className={"rounded-lg grow flex flex-row items-center bg-dark-800 border border-dark-700/50 focus-within:border-primary-500/50 transition-all duration-200 overflow-hidden"}
|
| 44 |
>
|
| 45 |
<input
|
| 46 |
ref={inputRef}
|
| 47 |
size={1}
|
| 48 |
+
className={classNames("grow bg-transparent p-2.5 outline-none")}
|
| 49 |
placeholder={placeholder}
|
| 50 |
value={url}
|
| 51 |
onChange={(event) => {
|
|
|
|
| 54 |
type={"text"}
|
| 55 |
onFocus={() => inputRef.current?.select()}
|
| 56 |
/>
|
| 57 |
+
{url && (
|
| 58 |
+
<div className={"p-2 cursor-pointer hover:text-red-400 transition-colors"} onClick={() => onChange("")}>
|
| 59 |
+
<IconClose />
|
| 60 |
+
</div>
|
| 61 |
+
)}
|
| 62 |
<div>
|
| 63 |
<button
|
| 64 |
type={"submit"}
|
| 65 |
data-tooltip-content={tooltip}
|
| 66 |
className={classNames(
|
| 67 |
+
"px-4 py-2.5 font-medium transition-all duration-200",
|
| 68 |
valid
|
| 69 |
+
? "bg-primary-600 hover:bg-primary-700 active:bg-primary-800"
|
| 70 |
+
: "bg-red-600 hover:bg-red-700 active:bg-red-800"
|
| 71 |
)}
|
| 72 |
>
|
| 73 |
{children}
|
| 74 |
</button>
|
| 75 |
</div>
|
| 76 |
</div>
|
| 77 |
+
{!valid && <div className={"text-red-400 text-sm mt-1"}>Invalid url</div>}
|
| 78 |
</form>
|
| 79 |
)
|
| 80 |
}
|
components/modal/Modal.tsx
CHANGED
|
@@ -28,32 +28,38 @@ const Modal: FC<Props> = ({ title, show, close, children }) => {
|
|
| 28 |
e.stopPropagation()
|
| 29 |
close()
|
| 30 |
}}
|
| 31 |
-
className={"absolute top-0 left-0 h-full w-full bg-black/
|
| 32 |
/>
|
| 33 |
-
<div className={"flex justify-center h-full items-center"}>
|
| 34 |
-
<div className={"relative bg-dark-800 shadow rounded z-50 min-w-[30%]"}>
|
| 35 |
<div
|
| 36 |
className={
|
| 37 |
-
"flex justify-between items-center p-
|
| 38 |
}
|
| 39 |
>
|
| 40 |
-
<div className={"
|
| 41 |
-
<h2 className={"text-xl"}>{title}</h2>
|
| 42 |
</div>
|
| 43 |
-
<Button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
<IconClose />
|
| 45 |
</Button>
|
| 46 |
</div>
|
| 47 |
-
<div className={"p-
|
| 48 |
<div
|
| 49 |
className={
|
| 50 |
-
"flex justify-end items-center p-
|
| 51 |
}
|
| 52 |
>
|
| 53 |
<Button
|
| 54 |
tooltip={"Close modal"}
|
| 55 |
id={"closeModal2"}
|
| 56 |
-
className={"
|
|
|
|
| 57 |
onClick={close}
|
| 58 |
>
|
| 59 |
Close
|
|
@@ -62,8 +68,24 @@ const Modal: FC<Props> = ({ title, show, close, children }) => {
|
|
| 62 |
</div>
|
| 63 |
</div>
|
| 64 |
|
| 65 |
-
<Tooltip
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
</div>
|
| 68 |
)
|
| 69 |
}
|
|
|
|
| 28 |
e.stopPropagation()
|
| 29 |
close()
|
| 30 |
}}
|
| 31 |
+
className={"absolute top-0 left-0 h-full w-full bg-black/60 backdrop-blur-sm"}
|
| 32 |
/>
|
| 33 |
+
<div className={"flex justify-center h-full items-center p-4"}>
|
| 34 |
+
<div className={"relative bg-dark-800 shadow-2xl rounded-xl z-50 min-w-[30%] max-w-2xl w-full border border-dark-700/50"}>
|
| 35 |
<div
|
| 36 |
className={
|
| 37 |
+
"flex justify-between items-center p-4 border-b border-dark-700/50"
|
| 38 |
}
|
| 39 |
>
|
| 40 |
+
<div className={"px-2"}>
|
| 41 |
+
<h2 className={"text-xl font-semibold text-primary-400"}>{title}</h2>
|
| 42 |
</div>
|
| 43 |
+
<Button
|
| 44 |
+
tooltip={"Close modal"}
|
| 45 |
+
id={"closeModal1"}
|
| 46 |
+
onClick={close}
|
| 47 |
+
actionClasses={"hover:bg-dark-700 active:bg-dark-600"}
|
| 48 |
+
>
|
| 49 |
<IconClose />
|
| 50 |
</Button>
|
| 51 |
</div>
|
| 52 |
+
<div className={"p-6"}>{children}</div>
|
| 53 |
<div
|
| 54 |
className={
|
| 55 |
+
"flex justify-end items-center p-4 border-t border-dark-700/50"
|
| 56 |
}
|
| 57 |
>
|
| 58 |
<Button
|
| 59 |
tooltip={"Close modal"}
|
| 60 |
id={"closeModal2"}
|
| 61 |
+
className={"px-4 py-2"}
|
| 62 |
+
actionClasses={"bg-dark-700 hover:bg-dark-600 active:bg-dark-500"}
|
| 63 |
onClick={close}
|
| 64 |
>
|
| 65 |
Close
|
|
|
|
| 68 |
</div>
|
| 69 |
</div>
|
| 70 |
|
| 71 |
+
<Tooltip
|
| 72 |
+
anchorId={"closeModal1"}
|
| 73 |
+
style={{
|
| 74 |
+
backgroundColor: "var(--dark-700)",
|
| 75 |
+
borderRadius: "0.5rem",
|
| 76 |
+
padding: "0.5rem 0.75rem",
|
| 77 |
+
fontSize: "0.875rem",
|
| 78 |
+
}}
|
| 79 |
+
/>
|
| 80 |
+
<Tooltip
|
| 81 |
+
anchorId={"closeModal2"}
|
| 82 |
+
style={{
|
| 83 |
+
backgroundColor: "var(--dark-700)",
|
| 84 |
+
borderRadius: "0.5rem",
|
| 85 |
+
padding: "0.5rem 0.75rem",
|
| 86 |
+
fontSize: "0.875rem",
|
| 87 |
+
}}
|
| 88 |
+
/>
|
| 89 |
</div>
|
| 90 |
)
|
| 91 |
}
|
components/playlist/PlaylistItem.tsx
CHANGED
|
@@ -71,15 +71,19 @@ const PlaylistItem: FC<Props> = ({
|
|
| 71 |
{...provided.draggableProps}
|
| 72 |
style={provided.draggableProps.style}
|
| 73 |
className={classNames(
|
| 74 |
-
"p-
|
| 75 |
-
snapshot.isDragging
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
)}
|
| 77 |
>
|
| 78 |
-
<div className={"flex flex-row gap-
|
| 79 |
<div
|
| 80 |
className={classNames(
|
| 81 |
-
"p-1 transition-colors hover:text-primary-
|
| 82 |
-
snapshot.isDragging && "text-primary-
|
| 83 |
)}
|
| 84 |
{...provided.dragHandleProps}
|
| 85 |
>
|
|
@@ -96,16 +100,18 @@ const PlaylistItem: FC<Props> = ({
|
|
| 96 |
placeholder={"Set a title"}
|
| 97 |
value={title}
|
| 98 |
/>
|
| 99 |
-
) :
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
| 102 |
</div>
|
| 103 |
<DeleteButton
|
| 104 |
tooltip={"Delete " + title}
|
| 105 |
onClick={() => deleteItem(index)}
|
| 106 |
/>
|
| 107 |
</div>
|
| 108 |
-
<div className={"flex flex-row items-center"}>
|
| 109 |
<ControlButton
|
| 110 |
tooltip={playing ? "Currently playing" : "Play item now"}
|
| 111 |
onClick={() => {
|
|
@@ -117,17 +123,17 @@ const PlaylistItem: FC<Props> = ({
|
|
| 117 |
>
|
| 118 |
{playing ? (
|
| 119 |
<IconDisk
|
| 120 |
-
className={"animate-spin animate-pulse text-purple-
|
| 121 |
/>
|
| 122 |
) : (
|
| 123 |
<IconPlay
|
| 124 |
-
className={"text-primary-
|
| 125 |
/>
|
| 126 |
)}
|
| 127 |
</ControlButton>
|
| 128 |
<NewTabLink
|
| 129 |
href={item.src[0].src}
|
| 130 |
-
className={"flex flex-row gap-1"}
|
| 131 |
>
|
| 132 |
<div className={"line-clamp-2"}>{getDomain(item.src[0].src)}</div>
|
| 133 |
<IconNewTab className={"shrink-0"} />
|
|
|
|
| 71 |
{...provided.draggableProps}
|
| 72 |
style={provided.draggableProps.style}
|
| 73 |
className={classNames(
|
| 74 |
+
"p-3 rounded-lg flex flex-col border transition-all duration-200",
|
| 75 |
+
snapshot.isDragging
|
| 76 |
+
? "bg-dark-700 border-primary-500/50 shadow-glow"
|
| 77 |
+
: playing
|
| 78 |
+
? "bg-dark-800 border-primary-600/50"
|
| 79 |
+
: "bg-dark-800/50 border-dark-700/50 hover:bg-dark-800"
|
| 80 |
)}
|
| 81 |
>
|
| 82 |
+
<div className={"flex flex-row gap-2 items-center mb-2"}>
|
| 83 |
<div
|
| 84 |
className={classNames(
|
| 85 |
+
"p-1 transition-colors hover:text-primary-500 cursor-grab active:cursor-grabbing",
|
| 86 |
+
snapshot.isDragging && "text-primary-500"
|
| 87 |
)}
|
| 88 |
{...provided.dragHandleProps}
|
| 89 |
>
|
|
|
|
| 100 |
placeholder={"Set a title"}
|
| 101 |
value={title}
|
| 102 |
/>
|
| 103 |
+
) : (
|
| 104 |
+
<span className={classNames("text-sm font-medium", playing && "text-primary-400")}>
|
| 105 |
+
{titleGen(item, index)}
|
| 106 |
+
</span>
|
| 107 |
+
)}
|
| 108 |
</div>
|
| 109 |
<DeleteButton
|
| 110 |
tooltip={"Delete " + title}
|
| 111 |
onClick={() => deleteItem(index)}
|
| 112 |
/>
|
| 113 |
</div>
|
| 114 |
+
<div className={"flex flex-row items-center gap-2"}>
|
| 115 |
<ControlButton
|
| 116 |
tooltip={playing ? "Currently playing" : "Play item now"}
|
| 117 |
onClick={() => {
|
|
|
|
| 123 |
>
|
| 124 |
{playing ? (
|
| 125 |
<IconDisk
|
| 126 |
+
className={"animate-spin animate-pulse text-purple-500"}
|
| 127 |
/>
|
| 128 |
) : (
|
| 129 |
<IconPlay
|
| 130 |
+
className={"text-primary-500 hover:text-primary-400"}
|
| 131 |
/>
|
| 132 |
)}
|
| 133 |
</ControlButton>
|
| 134 |
<NewTabLink
|
| 135 |
href={item.src[0].src}
|
| 136 |
+
className={"flex flex-row gap-1 items-center text-xs text-dark-400 hover:text-primary-500 transition-colors"}
|
| 137 |
>
|
| 138 |
<div className={"line-clamp-2"}>{getDomain(item.src[0].src)}</div>
|
| 139 |
<IconNewTab className={"shrink-0"} />
|
components/playlist/PlaylistMenu.tsx
CHANGED
|
@@ -67,12 +67,12 @@ const PlaylistMenu: FC<Props> = ({ socket }) => {
|
|
| 67 |
}
|
| 68 |
|
| 69 |
return (
|
| 70 |
-
<div className={classNames("flex flex-col", expanded && "sm:w-[
|
| 71 |
<ControlButton
|
| 72 |
tooltip={expanded ? "Hide playlist" : "Show playlist"}
|
| 73 |
onClick={() => setExpanded(!expanded)}
|
| 74 |
interaction={() => {}}
|
| 75 |
-
className={"flex flex-row gap-
|
| 76 |
>
|
| 77 |
<IconChevron
|
| 78 |
direction={expanded ? "up" : "down"}
|
|
@@ -83,13 +83,13 @@ const PlaylistMenu: FC<Props> = ({ socket }) => {
|
|
| 83 |
</div>
|
| 84 |
</ControlButton>
|
| 85 |
{expanded && (
|
| 86 |
-
|
| 87 |
<InputUrl
|
| 88 |
url={url}
|
| 89 |
placeholder={"Add url..."}
|
| 90 |
tooltip={"Add url to the playlist"}
|
| 91 |
onChange={setUrl}
|
| 92 |
-
className={"
|
| 93 |
onSubmit={() => addItem(url)}
|
| 94 |
>
|
| 95 |
Add
|
|
@@ -132,7 +132,7 @@ const PlaylistMenu: FC<Props> = ({ socket }) => {
|
|
| 132 |
{...provided.droppableProps}
|
| 133 |
ref={provided.innerRef}
|
| 134 |
className={classNames(
|
| 135 |
-
"flex flex-col rounded gap-
|
| 136 |
snapshot.isDraggingOver && "bg-dark-800"
|
| 137 |
)}
|
| 138 |
>
|
|
@@ -177,7 +177,7 @@ const PlaylistMenu: FC<Props> = ({ socket }) => {
|
|
| 177 |
)}
|
| 178 |
</Droppable>
|
| 179 |
</DragDropContext>
|
| 180 |
-
|
| 181 |
)}
|
| 182 |
</div>
|
| 183 |
)
|
|
|
|
| 67 |
}
|
| 68 |
|
| 69 |
return (
|
| 70 |
+
<div className={classNames("flex flex-col bg-dark-900/50 rounded-xl border border-dark-700/50 overflow-hidden", expanded && "sm:w-[320px]")}>
|
| 71 |
<ControlButton
|
| 72 |
tooltip={expanded ? "Hide playlist" : "Show playlist"}
|
| 73 |
onClick={() => setExpanded(!expanded)}
|
| 74 |
interaction={() => {}}
|
| 75 |
+
className={"flex flex-row gap-2 items-center justify-center bg-dark-800 hover:bg-dark-700 p-3 font-medium"}
|
| 76 |
>
|
| 77 |
<IconChevron
|
| 78 |
direction={expanded ? "up" : "down"}
|
|
|
|
| 83 |
</div>
|
| 84 |
</ControlButton>
|
| 85 |
{expanded && (
|
| 86 |
+
<div className="p-2">
|
| 87 |
<InputUrl
|
| 88 |
url={url}
|
| 89 |
placeholder={"Add url..."}
|
| 90 |
tooltip={"Add url to the playlist"}
|
| 91 |
onChange={setUrl}
|
| 92 |
+
className={"mb-2"}
|
| 93 |
onSubmit={() => addItem(url)}
|
| 94 |
>
|
| 95 |
Add
|
|
|
|
| 132 |
{...provided.droppableProps}
|
| 133 |
ref={provided.innerRef}
|
| 134 |
className={classNames(
|
| 135 |
+
"flex flex-col rounded-lg gap-2 min-h-[100px] p-2",
|
| 136 |
snapshot.isDraggingOver && "bg-dark-800"
|
| 137 |
)}
|
| 138 |
>
|
|
|
|
| 177 |
)}
|
| 178 |
</Droppable>
|
| 179 |
</DragDropContext>
|
| 180 |
+
</div>
|
| 181 |
)}
|
| 182 |
</div>
|
| 183 |
)
|
components/search/YoutubeSearch.tsx
CHANGED
|
@@ -106,11 +106,13 @@ const YoutubeSearch: FC<Props> = ({ socket }) => {
|
|
| 106 |
const addToPlaylist = (url: string) => socket?.emit("addToPlaylist", url)
|
| 107 |
|
| 108 |
return (
|
| 109 |
-
<div className="flex flex-col gap-
|
|
|
|
|
|
|
| 110 |
{/* Search row: input + Search button with fixed width so actions align below */}
|
| 111 |
<div className="grid grid-cols-[1fr_auto] gap-2">
|
| 112 |
<input
|
| 113 |
-
className="input bg-
|
| 114 |
placeholder="Search YouTube (e.g., Sira)"
|
| 115 |
value={q}
|
| 116 |
onChange={(e) => setQ(e.target.value)}
|
|
@@ -120,37 +122,37 @@ const YoutubeSearch: FC<Props> = ({ socket }) => {
|
|
| 120 |
/>
|
| 121 |
<button
|
| 122 |
onClick={search}
|
| 123 |
-
className={`btn bg-primary-
|
| 124 |
>
|
| 125 |
{loading ? "Searching…" : "Search"}
|
| 126 |
</button>
|
| 127 |
</div>
|
| 128 |
|
| 129 |
-
{error && <div className="text-red-400 text-sm">{error}</div>}
|
| 130 |
|
| 131 |
-
<div className="grid gap-2">
|
| 132 |
{results.map((r) => (
|
| 133 |
<div
|
| 134 |
key={r.id}
|
| 135 |
-
className="grid grid-cols-[auto_1fr_auto_auto] items-center gap-3 p-
|
| 136 |
>
|
| 137 |
{/* Thumb */}
|
| 138 |
{r.thumbnails?.[0]?.url ? (
|
| 139 |
// eslint-disable-next-line @next/next/no-img-element
|
| 140 |
-
<img src={r.thumbnails[0].url} alt="" className="w-
|
| 141 |
) : (
|
| 142 |
-
<div className="w-
|
| 143 |
)}
|
| 144 |
|
| 145 |
{/* Info */}
|
| 146 |
<div className="min-w-0">
|
| 147 |
-
<div className="truncate">{r.title}</div>
|
| 148 |
-
<div className="
|
| 149 |
</div>
|
| 150 |
|
| 151 |
{/* Add (small) */}
|
| 152 |
<button
|
| 153 |
-
className={`btn bg-
|
| 154 |
onClick={() => addToPlaylist(r.url)}
|
| 155 |
title="Add to playlist"
|
| 156 |
>
|
|
@@ -159,7 +161,7 @@ const YoutubeSearch: FC<Props> = ({ socket }) => {
|
|
| 159 |
|
| 160 |
{/* Play (aligned under Search button) */}
|
| 161 |
<button
|
| 162 |
-
className={`btn bg-
|
| 163 |
onClick={() => playNow(r.url)}
|
| 164 |
title="Play now"
|
| 165 |
>
|
|
|
|
| 106 |
const addToPlaylist = (url: string) => socket?.emit("addToPlaylist", url)
|
| 107 |
|
| 108 |
return (
|
| 109 |
+
<div className="flex flex-col gap-3 bg-dark-900 border border-dark-700/50 rounded-xl p-4 shadow-lg">
|
| 110 |
+
<h3 className="text-lg font-semibold text-primary-400 mb-1">YouTube Search</h3>
|
| 111 |
+
|
| 112 |
{/* Search row: input + Search button with fixed width so actions align below */}
|
| 113 |
<div className="grid grid-cols-[1fr_auto] gap-2">
|
| 114 |
<input
|
| 115 |
+
className="input bg-dark-800 border border-dark-700/50 focus:border-primary-500/50 p-2.5 rounded-lg outline-none transition-all duration-200"
|
| 116 |
placeholder="Search YouTube (e.g., Sira)"
|
| 117 |
value={q}
|
| 118 |
onChange={(e) => setQ(e.target.value)}
|
|
|
|
| 122 |
/>
|
| 123 |
<button
|
| 124 |
onClick={search}
|
| 125 |
+
className={`btn bg-primary-600 hover:bg-primary-700 active:bg-primary-800 px-4 rounded-lg justify-center font-medium transition-all duration-200 shadow-md hover:shadow-glow ${ACTION_BTN_WIDTH}`}
|
| 126 |
>
|
| 127 |
{loading ? "Searching…" : "Search"}
|
| 128 |
</button>
|
| 129 |
</div>
|
| 130 |
|
| 131 |
+
{error && <div className="text-red-400 text-sm bg-red-900/20 border border-red-700/30 rounded-lg p-2">{error}</div>}
|
| 132 |
|
| 133 |
+
<div className="grid gap-2 max-h-96 overflow-y-auto">
|
| 134 |
{results.map((r) => (
|
| 135 |
<div
|
| 136 |
key={r.id}
|
| 137 |
+
className="grid grid-cols-[auto_1fr_auto_auto] items-center gap-3 p-3 rounded-lg border border-dark-700/50 bg-dark-800/50 hover:bg-dark-800 transition-all duration-200"
|
| 138 |
>
|
| 139 |
{/* Thumb */}
|
| 140 |
{r.thumbnails?.[0]?.url ? (
|
| 141 |
// eslint-disable-next-line @next/next/no-img-element
|
| 142 |
+
<img src={r.thumbnails[0].url} alt="" className="w-20 h-12 object-cover rounded-md border border-dark-700/30" />
|
| 143 |
) : (
|
| 144 |
+
<div className="w-20 h-12 bg-dark-700 rounded-md border border-dark-700/30" />
|
| 145 |
)}
|
| 146 |
|
| 147 |
{/* Info */}
|
| 148 |
<div className="min-w-0">
|
| 149 |
+
<div className="truncate font-medium text-dark-200">{r.title}</div>
|
| 150 |
+
<div className="text-dark-500 text-xs truncate">{r.url}</div>
|
| 151 |
</div>
|
| 152 |
|
| 153 |
{/* Add (small) */}
|
| 154 |
<button
|
| 155 |
+
className={`btn bg-accent-600 hover:bg-accent-700 active:bg-accent-800 px-3 py-1.5 rounded-lg text-xs justify-center font-medium transition-all duration-200 ${ADD_BTN_WIDTH}`}
|
| 156 |
onClick={() => addToPlaylist(r.url)}
|
| 157 |
title="Add to playlist"
|
| 158 |
>
|
|
|
|
| 161 |
|
| 162 |
{/* Play (aligned under Search button) */}
|
| 163 |
<button
|
| 164 |
+
className={`btn bg-primary-600 hover:bg-primary-700 active:bg-primary-800 px-3 py-1.5 rounded-lg justify-center font-medium transition-all duration-200 shadow-md hover:shadow-glow ${ACTION_BTN_WIDTH}`}
|
| 165 |
onClick={() => playNow(r.url)}
|
| 166 |
title="Play now"
|
| 167 |
>
|
pages/global.css
CHANGED
|
@@ -3,9 +3,14 @@
|
|
| 3 |
@tailwind utilities;
|
| 4 |
|
| 5 |
:root {
|
| 6 |
-
--primary: #
|
| 7 |
-
--
|
| 8 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
html,
|
|
@@ -14,6 +19,10 @@ body {
|
|
| 14 |
padding: 0;
|
| 15 |
min-height: 100vh;
|
| 16 |
color: white;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
@apply stroke-white fill-white bg-dark-1000;
|
| 18 |
}
|
| 19 |
|
|
@@ -21,6 +30,7 @@ body {
|
|
| 21 |
@apply outline-none;
|
| 22 |
@apply hover:bg-dark-700 hover:drop-shadow-lg;
|
| 23 |
@apply active:bg-dark-600;
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
.action > * {
|
|
@@ -38,3 +48,27 @@ body {
|
|
| 38 |
.hide-below-sm {
|
| 39 |
@apply hidden sm:inline-block;
|
| 40 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
@tailwind utilities;
|
| 4 |
|
| 5 |
:root {
|
| 6 |
+
--primary: #10b981;
|
| 7 |
+
--primary-hover: #059669;
|
| 8 |
+
--accent: #3b82f6;
|
| 9 |
+
--accent-hover: #2563eb;
|
| 10 |
+
--dark-700: #1f2937;
|
| 11 |
+
--dark-800: #111827;
|
| 12 |
+
--dark-900: #0f172a;
|
| 13 |
+
--dark-1000: #020617;
|
| 14 |
}
|
| 15 |
|
| 16 |
html,
|
|
|
|
| 19 |
padding: 0;
|
| 20 |
min-height: 100vh;
|
| 21 |
color: white;
|
| 22 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
| 23 |
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
| 24 |
+
-webkit-font-smoothing: antialiased;
|
| 25 |
+
-moz-osx-font-smoothing: grayscale;
|
| 26 |
@apply stroke-white fill-white bg-dark-1000;
|
| 27 |
}
|
| 28 |
|
|
|
|
| 30 |
@apply outline-none;
|
| 31 |
@apply hover:bg-dark-700 hover:drop-shadow-lg;
|
| 32 |
@apply active:bg-dark-600;
|
| 33 |
+
@apply transition-all duration-200 ease-in-out;
|
| 34 |
}
|
| 35 |
|
| 36 |
.action > * {
|
|
|
|
| 48 |
.hide-below-sm {
|
| 49 |
@apply hidden sm:inline-block;
|
| 50 |
}
|
| 51 |
+
|
| 52 |
+
/* Modern button styles */
|
| 53 |
+
button, .btn {
|
| 54 |
+
@apply transition-all duration-200 ease-in-out;
|
| 55 |
+
@apply font-medium;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
button:focus-visible, .btn:focus-visible {
|
| 59 |
+
@apply ring-2 ring-primary-500 ring-offset-2 ring-offset-dark-1000;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/* Modern input styles */
|
| 63 |
+
input, textarea {
|
| 64 |
+
@apply transition-all duration-200 ease-in-out;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
input:focus, textarea:focus {
|
| 68 |
+
@apply ring-2 ring-primary-500 ring-opacity-50;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* Smooth scrolling */
|
| 72 |
+
html {
|
| 73 |
+
scroll-behavior: smooth;
|
| 74 |
+
}
|
pages/index.tsx
CHANGED
|
@@ -15,10 +15,10 @@ export default function Index() {
|
|
| 15 |
|
| 16 |
return (
|
| 17 |
<Layout meta={{ robots: "index, archive, follow" }} showNavbar={false}>
|
| 18 |
-
<div className={"self-center flex justify-center"}>
|
| 19 |
<form
|
| 20 |
className={
|
| 21 |
-
"flex flex-col gap-
|
| 22 |
}
|
| 23 |
onSubmit={async (e) => {
|
| 24 |
e.preventDefault()
|
|
@@ -28,7 +28,13 @@ export default function Index() {
|
|
| 28 |
}
|
| 29 |
}}
|
| 30 |
>
|
| 31 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
<InputText
|
| 33 |
value={room}
|
| 34 |
placeholder={"Enter a room ID"}
|
|
@@ -36,12 +42,13 @@ export default function Index() {
|
|
| 36 |
setRoom(value.toLowerCase().replace(/[^a-z]/g, ""))
|
| 37 |
}
|
| 38 |
/>
|
| 39 |
-
|
|
|
|
| 40 |
<Button
|
| 41 |
tooltip={"Create a new personal room"}
|
| 42 |
-
className={"
|
| 43 |
actionClasses={
|
| 44 |
-
"bg-
|
| 45 |
}
|
| 46 |
onClick={() => {
|
| 47 |
fetch("/api/generate")
|
|
@@ -67,11 +74,11 @@ export default function Index() {
|
|
| 67 |
</Button>
|
| 68 |
<Button
|
| 69 |
tooltip={room.length < 4 ? "Invalid room id" : "Join room"}
|
| 70 |
-
className={"
|
| 71 |
actionClasses={
|
| 72 |
room.length >= 4
|
| 73 |
-
? "bg-primary-
|
| 74 |
-
: "bg-dark-700 hover:bg-dark-600 active:bg-red-700 cursor-not-allowed"
|
| 75 |
}
|
| 76 |
disabled={room.length < 4}
|
| 77 |
type={"submit"}
|
|
@@ -79,19 +86,31 @@ export default function Index() {
|
|
| 79 |
Join room
|
| 80 |
</Button>
|
| 81 |
</div>
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
<
|
| 85 |
-
<div
|
| 86 |
-
<div
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
</form>
|
| 90 |
</div>
|
| 91 |
|
| 92 |
<Tooltip
|
| 93 |
style={{
|
| 94 |
backgroundColor: "var(--dark-700)",
|
|
|
|
|
|
|
|
|
|
| 95 |
}}
|
| 96 |
/>
|
| 97 |
</Layout>
|
|
|
|
| 15 |
|
| 16 |
return (
|
| 17 |
<Layout meta={{ robots: "index, archive, follow" }} showNavbar={false}>
|
| 18 |
+
<div className={"self-center flex justify-center items-center min-h-[70vh]"}>
|
| 19 |
<form
|
| 20 |
className={
|
| 21 |
+
"flex flex-col gap-6 justify-center rounded-xl shadow-2xl p-8 bg-gradient-to-br from-dark-800 to-dark-900 m-8 max-w-md w-full border border-dark-700/50"
|
| 22 |
}
|
| 23 |
onSubmit={async (e) => {
|
| 24 |
e.preventDefault()
|
|
|
|
| 28 |
}
|
| 29 |
}}
|
| 30 |
>
|
| 31 |
+
<div className="text-center">
|
| 32 |
+
<h1 className={"text-3xl font-bold bg-gradient-to-r from-primary-500 to-accent-500 bg-clip-text text-transparent mb-2"}>
|
| 33 |
+
Welcome to Streamer
|
| 34 |
+
</h1>
|
| 35 |
+
<p className="text-dark-400 text-sm">Join or create a room to watch together</p>
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
<InputText
|
| 39 |
value={room}
|
| 40 |
placeholder={"Enter a room ID"}
|
|
|
|
| 42 |
setRoom(value.toLowerCase().replace(/[^a-z]/g, ""))
|
| 43 |
}
|
| 44 |
/>
|
| 45 |
+
|
| 46 |
+
<div className={"flex gap-3 justify-end"}>
|
| 47 |
<Button
|
| 48 |
tooltip={"Create a new personal room"}
|
| 49 |
+
className={"px-4 py-2.5 font-medium"}
|
| 50 |
actionClasses={
|
| 51 |
+
"bg-accent-600 hover:bg-accent-700 active:bg-accent-800 shadow-lg hover:shadow-xl"
|
| 52 |
}
|
| 53 |
onClick={() => {
|
| 54 |
fetch("/api/generate")
|
|
|
|
| 74 |
</Button>
|
| 75 |
<Button
|
| 76 |
tooltip={room.length < 4 ? "Invalid room id" : "Join room"}
|
| 77 |
+
className={"px-4 py-2.5 font-medium"}
|
| 78 |
actionClasses={
|
| 79 |
room.length >= 4
|
| 80 |
+
? "bg-primary-600 hover:bg-primary-700 active:bg-primary-800 shadow-lg hover:shadow-xl hover:shadow-glow"
|
| 81 |
+
: "bg-dark-700 hover:bg-dark-600 active:bg-red-700 cursor-not-allowed opacity-50"
|
| 82 |
}
|
| 83 |
disabled={room.length < 4}
|
| 84 |
type={"submit"}
|
|
|
|
| 86 |
Join room
|
| 87 |
</Button>
|
| 88 |
</div>
|
| 89 |
+
|
| 90 |
+
<div className={"mt-2 pt-4 border-t border-dark-700/50"}>
|
| 91 |
+
<small className={"text-dark-400"}>
|
| 92 |
+
<div className="font-medium text-dark-300 mb-1">Currently active:</div>
|
| 93 |
+
<div className={"flex flex-row gap-4 text-sm"}>
|
| 94 |
+
<div className="flex items-center gap-1">
|
| 95 |
+
<span className="inline-block w-2 h-2 bg-primary-500 rounded-full"></span>
|
| 96 |
+
<span>{data?.rooms || 0} Rooms</span>
|
| 97 |
+
</div>
|
| 98 |
+
<div className="flex items-center gap-1">
|
| 99 |
+
<span className="inline-block w-2 h-2 bg-accent-500 rounded-full"></span>
|
| 100 |
+
<span>{data?.users || 0} Users</span>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
</small>
|
| 104 |
+
</div>
|
| 105 |
</form>
|
| 106 |
</div>
|
| 107 |
|
| 108 |
<Tooltip
|
| 109 |
style={{
|
| 110 |
backgroundColor: "var(--dark-700)",
|
| 111 |
+
borderRadius: "0.5rem",
|
| 112 |
+
padding: "0.5rem 0.75rem",
|
| 113 |
+
fontSize: "0.875rem",
|
| 114 |
}}
|
| 115 |
/>
|
| 116 |
</Layout>
|
tailwind.config.js
CHANGED
|
@@ -7,23 +7,36 @@ module.exports = {
|
|
| 7 |
extend: {
|
| 8 |
colors: {
|
| 9 |
dark: {
|
| 10 |
-
400: "#
|
| 11 |
-
500: "#
|
| 12 |
-
600: "#
|
| 13 |
-
700: "#
|
| 14 |
-
800: "#
|
| 15 |
-
900: "#
|
| 16 |
-
1000: "#
|
| 17 |
},
|
| 18 |
primary: {
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
| 22 |
},
|
| 23 |
accent: {
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
},
|
| 26 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
},
|
| 28 |
},
|
| 29 |
}
|
|
|
|
| 7 |
extend: {
|
| 8 |
colors: {
|
| 9 |
dark: {
|
| 10 |
+
400: "#64748b",
|
| 11 |
+
500: "#475569",
|
| 12 |
+
600: "#334155",
|
| 13 |
+
700: "#1f2937",
|
| 14 |
+
800: "#111827",
|
| 15 |
+
900: "#0f172a",
|
| 16 |
+
1000: "#020617",
|
| 17 |
},
|
| 18 |
primary: {
|
| 19 |
+
500: "#10b981",
|
| 20 |
+
600: "#059669",
|
| 21 |
+
700: "#047857",
|
| 22 |
+
800: "#065f46",
|
| 23 |
+
900: "#064e3b",
|
| 24 |
},
|
| 25 |
accent: {
|
| 26 |
+
500: "#3b82f6",
|
| 27 |
+
600: "#2563eb",
|
| 28 |
+
700: "#1d4ed8",
|
| 29 |
+
800: "#1e40af",
|
| 30 |
+
900: "#1e3a8a",
|
| 31 |
},
|
| 32 |
},
|
| 33 |
+
fontFamily: {
|
| 34 |
+
sans: ['-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', '"Fira Sans"', '"Droid Sans"', '"Helvetica Neue"', 'sans-serif'],
|
| 35 |
+
},
|
| 36 |
+
boxShadow: {
|
| 37 |
+
'glow': '0 0 20px rgba(16, 185, 129, 0.3)',
|
| 38 |
+
'glow-lg': '0 0 30px rgba(16, 185, 129, 0.4)',
|
| 39 |
+
},
|
| 40 |
},
|
| 41 |
},
|
| 42 |
}
|