mirror of
https://github.com/yusufipk/YTChatHub.git
synced 2026-09-11 10:56:17 +00:00
refactor(dashboard): reorganize layout and improve component structure
Reorganized the dashboard page layout by moving the ConnectionControl component into the header with inline display, flattened the nested panel structure by removing wrapper divs (chatColumn and splitColumn), and added specific BEM modifiers (panel--chat, panel--super) to improve CSS targeting and maintainability. This change simplifies the component hierarchy while preserving all existing functionality.
This commit is contained in:
@@ -64,96 +64,90 @@ export default function DashboardPage() {
|
|||||||
<h1>🎬 Live Chat Monitor</h1>
|
<h1>🎬 Live Chat Monitor</h1>
|
||||||
<p className="muted">Select a message to display on your OBS overlay</p>
|
<p className="muted">Select a message to display on your OBS overlay</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="dashboard__connect">
|
||||||
|
<ConnectionControl
|
||||||
|
inline
|
||||||
|
connected={connected}
|
||||||
|
liveId={liveId}
|
||||||
|
connecting={connecting}
|
||||||
|
onConnect={connect}
|
||||||
|
onDisconnect={disconnect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="dashboard__status">
|
<div className="dashboard__status">
|
||||||
<span className={`status status--${overlayStatus}`}>{statusHint}</span>
|
<span className={`status status--${overlayStatus}`}>{statusHint}</span>
|
||||||
<span className="message-count">{messages.length} messages</span>
|
<span className="message-count">{messages.length} messages</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ConnectionControl
|
|
||||||
connected={connected}
|
|
||||||
liveId={liveId}
|
|
||||||
connecting={connecting}
|
|
||||||
onConnect={connect}
|
|
||||||
onDisconnect={disconnect}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<section className="dashboard__grid">
|
<section className="dashboard__grid">
|
||||||
{/* Main Chat Column */}
|
<div className="panel panel--chat">
|
||||||
<div className="chatColumn">
|
<div className="panel__header">
|
||||||
<div className="panel">
|
<h2>💬 Live Chat</h2>
|
||||||
<div className="panel__header">
|
{selection && (
|
||||||
<h2>💬 Live Chat</h2>
|
<button className="btn-clear" onClick={handleClear}>
|
||||||
{selection && (
|
Clear Selection
|
||||||
<button className="btn-clear" onClick={handleClear}>
|
</button>
|
||||||
Clear Selection
|
)}
|
||||||
</button>
|
</div>
|
||||||
)}
|
<div className="chatList">
|
||||||
</div>
|
{regularMessages.map((message) => (
|
||||||
<div className="chatList">
|
<ChatItem
|
||||||
{regularMessages.map((message) => (
|
key={message.id}
|
||||||
<ChatItem
|
message={message}
|
||||||
key={message.id}
|
isSelected={selection?.id === message.id}
|
||||||
message={message}
|
onSelect={() => handleSelect(message)}
|
||||||
isSelected={selection?.id === message.id}
|
/>
|
||||||
onSelect={() => handleSelect(message)}
|
))}
|
||||||
/>
|
{regularMessages.length === 0 && (
|
||||||
))}
|
<div className="chatList__empty">
|
||||||
{regularMessages.length === 0 && (
|
<p>⏳ Waiting for chat messages...</p>
|
||||||
<div className="chatList__empty">
|
</div>
|
||||||
<p>⏳ Waiting for chat messages...</p>
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Split Column */}
|
<div className="panel panel--super">
|
||||||
<div className="splitColumn">
|
<div className="panel__header">
|
||||||
{/* Super Chats - Top Half */}
|
<h2>💰 Super Chats</h2>
|
||||||
<div className="panel">
|
<span className="badge badge--count">{superChats.length}</span>
|
||||||
<div className="panel__header">
|
|
||||||
<h2>💰 Super Chats</h2>
|
|
||||||
<span className="badge badge--count">{superChats.length}</span>
|
|
||||||
</div>
|
|
||||||
<div className="chatList">
|
|
||||||
{superChats.map((message) => (
|
|
||||||
<ChatItem
|
|
||||||
key={message.id}
|
|
||||||
message={message}
|
|
||||||
isSelected={selection?.id === message.id}
|
|
||||||
onSelect={() => handleSelect(message)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{superChats.length === 0 && (
|
|
||||||
<div className="chatList__empty">
|
|
||||||
<p>No super chats yet</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="chatList">
|
||||||
|
{superChats.map((message) => (
|
||||||
|
<ChatItem
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
isSelected={selection?.id === message.id}
|
||||||
|
onSelect={() => handleSelect(message)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{superChats.length === 0 && (
|
||||||
|
<div className="chatList__empty">
|
||||||
|
<p>No super chats yet</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* New Members - Bottom Half */}
|
<div className="panel panel--members">
|
||||||
<div className="panel">
|
<div className="panel__header">
|
||||||
<div className="panel__header">
|
<h2>⭐ New Members</h2>
|
||||||
<h2>⭐ New Members</h2>
|
<span className="badge badge--count">{newMembers.length}</span>
|
||||||
<span className="badge badge--count">{newMembers.length}</span>
|
</div>
|
||||||
</div>
|
<div className="chatList">
|
||||||
<div className="chatList">
|
{newMembers.map((message) => (
|
||||||
{newMembers.map((message) => (
|
<MemberItem
|
||||||
<MemberItem
|
key={message.id}
|
||||||
key={message.id}
|
message={message}
|
||||||
message={message}
|
isSelected={selection?.id === message.id}
|
||||||
isSelected={selection?.id === message.id}
|
onSelect={() => handleSelect(message)}
|
||||||
onSelect={() => handleSelect(message)}
|
/>
|
||||||
/>
|
))}
|
||||||
))}
|
{newMembers.length === 0 && (
|
||||||
{newMembers.length === 0 && (
|
<div className="chatList__empty">
|
||||||
<div className="chatList__empty">
|
<p>No new members yet</p>
|
||||||
<p>No new members yet</p>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -304,9 +298,10 @@ type ConnectionControlProps = {
|
|||||||
connecting: boolean;
|
connecting: boolean;
|
||||||
onConnect: (liveId: string) => void;
|
onConnect: (liveId: string) => void;
|
||||||
onDisconnect: () => void;
|
onDisconnect: () => void;
|
||||||
|
inline?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ConnectionControl({ connected, liveId, connecting, onConnect, onDisconnect }: ConnectionControlProps) {
|
function ConnectionControl({ connected, liveId, connecting, onConnect, onDisconnect, inline }: ConnectionControlProps) {
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
@@ -317,7 +312,7 @@ function ConnectionControl({ connected, liveId, connecting, onConnect, onDisconn
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="connectionControl">
|
<div className={"connectionControl" + (inline ? " connectionControl--inline" : "") }>
|
||||||
<div className="connectionControl__content">
|
<div className="connectionControl__content">
|
||||||
{connected ? (
|
{connected ? (
|
||||||
<div className="connectionControl__connected">
|
<div className="connectionControl__connected">
|
||||||
|
|||||||
+112
-34
@@ -111,6 +111,13 @@ main {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard__connect {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.message-count {
|
.message-count {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
@@ -122,24 +129,57 @@ main {
|
|||||||
|
|
||||||
.dashboard__grid {
|
.dashboard__grid {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: grid;
|
||||||
gap: 0.75rem;
|
grid-template-columns: minmax(420px, 2.1fr) minmax(340px, 1.5fr);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
column-gap: 0.6rem;
|
||||||
|
row-gap: 3rem;
|
||||||
|
align-content: start;
|
||||||
padding: 1rem clamp(1rem, 3vw, 2rem);
|
padding: 1rem clamp(1rem, 3vw, 2rem);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
.panel--chat {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1 / span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel--super {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel--members {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
margin-top: 0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
.dashboard__grid {
|
.dashboard__grid {
|
||||||
flex-direction: column;
|
grid-template-columns: minmax(360px, 1.8fr) minmax(300px, 1fr);
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
row-gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.chatColumn,
|
|
||||||
.splitColumn {
|
@media (max-width: 1024px) {
|
||||||
flex: 1 1 auto;
|
.dashboard__grid {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
max-width: 760px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel--chat,
|
||||||
|
.panel--super,
|
||||||
|
.panel--members {
|
||||||
|
grid-column: auto;
|
||||||
|
grid-row: auto;
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,21 +191,24 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.connectionControl__content {
|
.connectionControl__content {
|
||||||
max-width: 1400px;
|
max-width: 960px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connectionControl__connected {
|
.connectionControl__connected {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1.25rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connectionControl__info {
|
.connectionControl__info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.5rem;
|
gap: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connectionControl__badge {
|
.connectionControl__badge {
|
||||||
@@ -185,15 +228,19 @@ main {
|
|||||||
|
|
||||||
.connectionControl__form {
|
.connectionControl__form {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1.25rem;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connectionControl__input {
|
.connectionControl__input {
|
||||||
flex: 1;
|
flex: 0 1 460px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connectionControl__input label {
|
.connectionControl__input label {
|
||||||
@@ -269,38 +316,51 @@ main {
|
|||||||
.connectionControl__form {
|
.connectionControl__form {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connectionControl__connected {
|
.connectionControl__connected {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectionControl__content {
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatColumn {
|
/* Inline variant for header placement */
|
||||||
display: flex;
|
.connectionControl--inline {
|
||||||
flex-direction: column;
|
width: auto;
|
||||||
flex: 1 1 55%;
|
padding: 0;
|
||||||
min-height: 0;
|
background: transparent;
|
||||||
min-width: 0;
|
border: 0;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.splitColumn {
|
.connectionControl--inline .connectionControl__content {
|
||||||
display: flex;
|
max-width: none;
|
||||||
flex-direction: column;
|
margin: 0;
|
||||||
flex: 1 1 45%;
|
|
||||||
gap: 0.5rem;
|
|
||||||
min-height: 0;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.splitColumn .panel {
|
.connectionControl--inline .connectionControl__form {
|
||||||
flex: 1;
|
gap: 0.75rem;
|
||||||
min-height: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connectionControl--inline .connectionControl__input {
|
||||||
|
flex: 0 1 360px;
|
||||||
|
min-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectionControl--inline .btn-connect {
|
||||||
|
padding: 0.6rem 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectionControl--inline .connectionControl__connected {
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
background: rgba(30, 41, 59, 0.4);
|
background: rgba(30, 41, 59, 0.4);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
@@ -687,6 +747,8 @@ main {
|
|||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
min-width: min(600px, 90vw);
|
min-width: min(600px, 90vw);
|
||||||
max-width: min(900px, 95vw);
|
max-width: min(900px, 95vw);
|
||||||
@@ -701,10 +763,18 @@ main {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overlay__header > div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.overlay__avatar {
|
.overlay__avatar {
|
||||||
width: 56px;
|
width: 56px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
@@ -717,6 +787,7 @@ main {
|
|||||||
.overlay__authorLine {
|
.overlay__authorLine {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
@@ -759,6 +830,7 @@ main {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay__membership {
|
.overlay__membership {
|
||||||
@@ -770,6 +842,7 @@ main {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
|
||||||
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4);
|
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4);
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay__text {
|
.overlay__text {
|
||||||
@@ -777,6 +850,7 @@ main {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #f1f5f9;
|
color: #f1f5f9;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay__placeholder {
|
.overlay__placeholder {
|
||||||
@@ -784,6 +858,10 @@ main {
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
color: rgba(255, 255, 255, 0.75);
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
- Enhanced `ChatMessage` type to include badges, author photos, superchat info, and membership status
|
- Enhanced `ChatMessage` type to include badges, author photos, superchat info, and membership status
|
||||||
- Redesigned dashboard with gradient backgrounds, centered layout, and modern glass-morphism effects
|
- Redesigned dashboard with gradient backgrounds, centered layout, and modern glass-morphism effects
|
||||||
- Separated overlay preview into its own highlighted section that only appears when a message is selected
|
- Separated overlay preview into its own highlighted section that only appears when a message is selected
|
||||||
|
- Centered overlay message card content so live chat, superchat, and membership boxes align for OBS
|
||||||
|
- Reworked dashboard grid so live chat runs full-height on the left and super chat/new member panels stack in a centered right column with tuned spacing and clear separation between the stacked cards
|
||||||
|
- Moved the connection control into the header (inline, centered between logo/title and status) with a compact input width
|
||||||
|
|
||||||
## Immediate Next Steps
|
## Immediate Next Steps
|
||||||
1. Test with live YouTube stream to verify badge parsing and superchat detection
|
1. Test with live YouTube stream to verify badge parsing and superchat detection
|
||||||
|
|||||||
Reference in New Issue
Block a user