homeassistant-config/soundboard-mockup.html
Daniel 02a6608220 Add: Discord Soundboard Frontend Mockup
Interaktiver HTML/CSS/JS Prototyp im Discord-Stil mit:
- Sound-Button Grid mit 15 Beispiel-Sounds
- Kategorie-Sidebar (Memes, Musik, Effekte, Eigene, Favoriten)
- Suchfunktion, Favoriten-Toggle, Rechtsklick-Kontextmenü
- Lautstärkeregler, Playing-Animation, "Alle stoppen"
- Responsive Layout, Discord Dark Theme

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 14:53:49 +01:00

1077 lines
32 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Soundboard — Discord</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&display=swap');
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root {
--bg-deep: #1a1b1e;
--bg-primary: #1e1f22;
--bg-secondary: #2b2d31;
--bg-tertiary: #313338;
--bg-modifier-hover: rgba(79,84,92,.16);
--bg-modifier-active: rgba(79,84,92,.24);
--bg-modifier-selected: rgba(79,84,92,.32);
--text-normal: #dbdee1;
--text-muted: #949ba4;
--text-faint: #6d6f78;
--blurple: #5865f2;
--blurple-hover: #4752c4;
--blurple-glow: rgba(88,101,242,.45);
--green: #23a55a;
--red: #f23f42;
--yellow: #f0b232;
--white: #ffffff;
--font: 'DM Sans', 'gg sans', 'Noto Sans', Whitney, 'Helvetica Neue', Helvetica, Arial, sans-serif;
--radius: 8px;
--radius-lg: 12px;
--shadow-low: 0 1px 3px rgba(0,0,0,.24);
--shadow-med: 0 4px 12px rgba(0,0,0,.32);
--shadow-high: 0 8px 24px rgba(0,0,0,.4);
--transition: 150ms cubic-bezier(.4,0,.2,1);
}
html, body {
height: 100%;
overflow: hidden;
background: var(--bg-deep);
color: var(--text-normal);
font-family: var(--font);
font-size: 14px;
line-height: 1.4;
-webkit-font-smoothing: antialiased;
}
/* Scrollbar */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: var(--bg-tertiary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover { background: #3f4147; }
/* ── LAYOUT ── */
.app {
display: grid;
grid-template-rows: 48px 1fr 52px;
grid-template-columns: 220px 1fr;
grid-template-areas:
"topbar topbar"
"sidebar main"
"bottombar bottombar";
height: 100vh;
position: relative;
}
/* Subtle noise overlay */
.app::before {
content: '';
position: fixed;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 9999;
}
/* ── TOP BAR ── */
.topbar {
grid-area: topbar;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
background: var(--bg-secondary);
border-bottom: 1px solid rgba(0,0,0,.24);
z-index: 10;
gap: 12px;
}
.topbar-left {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.topbar-icon {
width: 24px; height: 24px;
color: var(--text-muted);
flex-shrink: 0;
}
.topbar-title {
font-size: 16px;
font-weight: 700;
letter-spacing: -.01em;
white-space: nowrap;
color: var(--white);
}
.topbar-divider {
width: 1px;
height: 24px;
background: rgba(255,255,255,.08);
flex-shrink: 0;
}
.connection-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--text-muted);
white-space: nowrap;
}
.status-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 6px rgba(35,165,90,.5);
animation: pulse-dot 2s ease-in-out infinite;
}
@keyframes pulse-dot {
0%,100% { box-shadow: 0 0 6px rgba(35,165,90,.5); }
50% { box-shadow: 0 0 12px rgba(35,165,90,.8); }
}
.topbar-right {
display: flex;
align-items: center;
gap: 4px;
}
.topbar-btn {
width: 32px; height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
color: var(--text-muted);
border-radius: var(--radius);
cursor: pointer;
transition: all var(--transition);
}
.topbar-btn:hover {
background: var(--bg-modifier-hover);
color: var(--text-normal);
}
.topbar-btn svg { width: 20px; height: 20px; }
/* ── SEARCH (in topbar) ── */
.search-wrap {
position: relative;
width: 200px;
flex-shrink: 0;
}
.search-wrap svg {
position: absolute;
left: 8px;
top: 50%;
transform: translateY(-50%);
width: 16px; height: 16px;
color: var(--text-faint);
pointer-events: none;
transition: color var(--transition);
}
.search-input {
width: 100%;
height: 28px;
padding: 0 8px 0 30px;
border: none;
border-radius: 4px;
background: var(--bg-deep);
color: var(--text-normal);
font-family: var(--font);
font-size: 13px;
outline: none;
transition: all var(--transition);
}
.search-input::placeholder { color: var(--text-faint); }
.search-input:focus {
background: var(--bg-primary);
box-shadow: 0 0 0 2px var(--blurple);
}
.search-input:focus + svg { color: var(--blurple); }
/* ── SIDEBAR ── */
.sidebar {
grid-area: sidebar;
background: var(--bg-secondary);
border-right: 1px solid rgba(0,0,0,.16);
padding: 12px 8px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 2px;
}
.sidebar-header {
padding: 6px 8px 10px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .04em;
color: var(--text-faint);
}
.sidebar-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: var(--radius);
cursor: pointer;
transition: all var(--transition);
font-size: 14px;
font-weight: 500;
color: var(--text-muted);
position: relative;
user-select: none;
}
.sidebar-item:hover {
background: var(--bg-modifier-hover);
color: var(--text-normal);
}
.sidebar-item.active {
background: var(--bg-modifier-selected);
color: var(--white);
}
.sidebar-item.active::before {
content: '';
position: absolute;
left: -8px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 60%;
border-radius: 0 3px 3px 0;
background: var(--blurple);
}
.sidebar-item .emoji {
font-size: 18px;
width: 24px;
text-align: center;
flex-shrink: 0;
}
.sidebar-item .count {
margin-left: auto;
font-size: 12px;
color: var(--text-faint);
font-weight: 400;
background: var(--bg-primary);
padding: 1px 6px;
border-radius: 10px;
min-width: 20px;
text-align: center;
}
/* ── MAIN CONTENT ── */
.main {
grid-area: main;
background: var(--bg-primary);
padding: 20px 24px;
overflow-y: auto;
position: relative;
}
.main-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.main-title {
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .04em;
color: var(--text-faint);
}
.sort-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border: none;
background: transparent;
color: var(--text-muted);
font-family: var(--font);
font-size: 12px;
border-radius: 4px;
cursor: pointer;
transition: all var(--transition);
}
.sort-btn:hover { background: var(--bg-modifier-hover); color: var(--text-normal); }
.sort-btn svg { width: 14px; height: 14px; }
/* ── SOUND GRID ── */
.sound-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(105px, 1fr));
max-width: 100%;
gap: 10px;
}
.sound-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 14px 8px 10px;
background: var(--bg-secondary);
border-radius: var(--radius-lg);
cursor: pointer;
transition: all var(--transition);
border: 2px solid transparent;
user-select: none;
overflow: hidden;
min-height: 100px;
}
.sound-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
opacity: 0;
transition: opacity var(--transition);
background: radial-gradient(ellipse at center, var(--blurple-glow) 0%, transparent 70%);
pointer-events: none;
}
.sound-card:hover {
background: var(--bg-tertiary);
transform: translateY(-2px);
box-shadow: var(--shadow-med), 0 0 20px var(--blurple-glow);
border-color: rgba(88,101,242,.25);
}
.sound-card:hover::before { opacity: 1; }
.sound-card:active {
transform: translateY(0);
transition-duration: 50ms;
}
.sound-card.playing {
border-color: var(--blurple);
animation: playing-pulse 800ms ease-in-out;
}
@keyframes playing-pulse {
0% { border-color: var(--blurple); box-shadow: 0 0 0 0 var(--blurple-glow); }
50% { box-shadow: 0 0 16px 4px var(--blurple-glow); }
100% { border-color: transparent; box-shadow: 0 0 0 0 transparent; }
}
/* Ripple */
.ripple {
position: absolute;
border-radius: 50%;
background: rgba(88,101,242,.35);
transform: scale(0);
animation: ripple-expand 500ms ease-out forwards;
pointer-events: none;
}
@keyframes ripple-expand {
to { transform: scale(3); opacity: 0; }
}
.sound-emoji {
font-size: 28px;
line-height: 1;
z-index: 1;
transition: transform var(--transition);
}
.sound-card:hover .sound-emoji { transform: scale(1.15); }
.sound-card.playing .sound-emoji { animation: emoji-bounce 400ms ease; }
@keyframes emoji-bounce {
0%,100% { transform: scale(1); }
40% { transform: scale(1.3); }
70% { transform: scale(0.95); }
}
.sound-name {
font-size: 12px;
font-weight: 600;
text-align: center;
color: var(--text-normal);
z-index: 1;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sound-duration {
font-size: 10px;
color: var(--text-faint);
z-index: 1;
font-weight: 500;
}
/* Favorite star */
.fav-star {
position: absolute;
top: 6px;
right: 6px;
width: 18px;
height: 18px;
opacity: 0;
transition: all var(--transition);
cursor: pointer;
z-index: 2;
color: var(--text-faint);
filter: drop-shadow(0 1px 2px rgba(0,0,0,.3));
}
.sound-card:hover .fav-star { opacity: .7; }
.fav-star:hover { opacity: 1 !important; color: var(--yellow); transform: scale(1.2); }
.fav-star.active { opacity: 1 !important; color: var(--yellow); }
/* Playing wave indicator */
.playing-indicator {
position: absolute;
bottom: 4px;
left: 50%;
transform: translateX(-50%);
display: none;
gap: 2px;
align-items: flex-end;
height: 12px;
}
.sound-card.playing .playing-indicator { display: flex; }
.wave-bar {
width: 2px;
background: var(--blurple);
border-radius: 1px;
animation: wave 600ms ease-in-out infinite alternate;
}
.wave-bar:nth-child(1) { height: 4px; animation-delay: 0ms; }
.wave-bar:nth-child(2) { height: 8px; animation-delay: 150ms; }
.wave-bar:nth-child(3) { height: 6px; animation-delay: 300ms; }
.wave-bar:nth-child(4) { height: 10px; animation-delay: 100ms; }
.wave-bar:nth-child(5) { height: 5px; animation-delay: 250ms; }
@keyframes wave {
from { height: 3px; }
to { height: 12px; }
}
/* ── ADD SOUND CARD ── */
.sound-card.add-card {
border: 2px dashed rgba(255,255,255,.1);
background: transparent;
color: var(--text-faint);
gap: 6px;
}
.sound-card.add-card:hover {
border-color: var(--blurple);
color: var(--blurple);
background: rgba(88,101,242,.06);
box-shadow: none;
transform: translateY(-1px);
}
.sound-card.add-card::before { display: none; }
.add-icon {
font-size: 24px;
font-weight: 300;
line-height: 1;
}
.add-label {
font-size: 11px;
font-weight: 600;
}
/* ── EMPTY STATE ── */
.empty-state {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 60px 20px;
text-align: center;
}
.empty-state.visible { display: flex; }
.empty-emoji { font-size: 48px; }
.empty-title {
font-size: 16px;
font-weight: 700;
color: var(--text-normal);
}
.empty-desc {
font-size: 13px;
color: var(--text-muted);
max-width: 280px;
}
/* ── BOTTOM BAR ── */
.bottombar {
grid-area: bottombar;
display: flex;
align-items: center;
gap: 16px;
padding: 0 20px;
background: var(--bg-secondary);
border-top: 1px solid rgba(0,0,0,.24);
z-index: 10;
}
.now-playing {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-muted);
min-width: 0;
flex: 1;
}
.now-playing .np-label { color: var(--text-faint); font-size: 12px; white-space: nowrap; }
.now-playing .np-name {
color: var(--blurple);
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.np-waves {
display: none;
gap: 1.5px;
align-items: flex-end;
height: 14px;
}
.np-waves.active { display: flex; }
.np-wave-bar {
width: 2.5px;
background: var(--blurple);
border-radius: 1px;
animation: wave 500ms ease-in-out infinite alternate;
}
.np-wave-bar:nth-child(1) { height: 5px; animation-delay: 0ms; }
.np-wave-bar:nth-child(2) { height: 10px; animation-delay: 120ms; }
.np-wave-bar:nth-child(3) { height: 7px; animation-delay: 240ms; }
.np-wave-bar:nth-child(4) { height: 12px; animation-delay: 80ms; }
.volume-section {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.volume-icon {
width: 20px; height: 20px;
color: var(--text-muted);
flex-shrink: 0;
cursor: pointer;
transition: color var(--transition);
}
.volume-icon:hover { color: var(--text-normal); }
.volume-slider {
-webkit-appearance: none;
appearance: none;
width: 100px;
height: 4px;
border-radius: 2px;
background: linear-gradient(to right, var(--blurple) 0%, var(--blurple) var(--vol, 80%), var(--bg-tertiary) var(--vol, 80%));
outline: none;
cursor: pointer;
transition: height var(--transition);
}
.volume-slider:hover { height: 6px; }
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px; height: 14px;
border-radius: 50%;
background: var(--white);
box-shadow: 0 1px 4px rgba(0,0,0,.3);
cursor: pointer;
transition: transform var(--transition);
}
.volume-slider::-webkit-slider-thumb:hover { transform: scale(1.2); }
.volume-pct {
font-size: 12px;
color: var(--text-faint);
min-width: 32px;
text-align: right;
font-weight: 500;
font-variant-numeric: tabular-nums;
}
.stop-all-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border: none;
border-radius: var(--radius);
background: var(--red);
color: var(--white);
font-family: var(--font);
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all var(--transition);
white-space: nowrap;
opacity: .85;
}
.stop-all-btn:hover { opacity: 1; transform: scale(1.03); box-shadow: 0 0 12px rgba(242,63,66,.3); }
.stop-all-btn svg { width: 14px; height: 14px; }
/* ── CONTEXT MENU ── */
.ctx-menu {
position: fixed;
min-width: 170px;
background: var(--bg-deep);
border: 1px solid rgba(255,255,255,.06);
border-radius: var(--radius);
box-shadow: var(--shadow-high);
padding: 4px;
z-index: 1000;
display: none;
animation: ctx-in 120ms ease-out;
}
.ctx-menu.visible { display: block; }
@keyframes ctx-in {
from { opacity: 0; transform: scale(.96) translateY(-4px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.ctx-item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 10px;
border-radius: 4px;
font-size: 13px;
color: var(--text-normal);
cursor: pointer;
transition: all var(--transition);
}
.ctx-item:hover { background: var(--blurple); color: var(--white); }
.ctx-item.danger { color: var(--red); }
.ctx-item.danger:hover { background: var(--red); color: var(--white); }
.ctx-sep { height: 1px; background: rgba(255,255,255,.06); margin: 4px 8px; }
.ctx-item svg { width: 16px; height: 16px; flex-shrink: 0; }
/* ── TOOLTIP ── */
.tooltip {
position: fixed;
padding: 6px 10px;
background: var(--bg-deep);
color: var(--text-normal);
font-size: 13px;
font-weight: 600;
border-radius: 6px;
box-shadow: var(--shadow-high);
pointer-events: none;
z-index: 999;
display: none;
white-space: nowrap;
animation: tt-in 100ms ease-out;
}
.tooltip.visible { display: block; }
@keyframes tt-in { from { opacity: 0; transform: translateY(4px); } }
/* ── RESPONSIVE ── */
@media (max-width: 768px) {
.app {
grid-template-columns: 1fr;
grid-template-areas:
"topbar"
"main"
"bottombar";
}
.sidebar { display: none; }
.search-wrap { width: 140px; }
}
/* ── ENTRANCE ANIMATION ── */
.sound-card {
opacity: 0;
animation: card-enter 400ms ease-out forwards;
}
@keyframes card-enter {
from { opacity: 0; transform: translateY(12px) scale(.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
</style>
</head>
<body>
<div class="app">
<!-- TOP BAR -->
<header class="topbar">
<div class="topbar-left">
<svg class="topbar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
<span class="topbar-title">Soundboard</span>
<div class="topbar-divider"></div>
<div class="connection-status">
<span class="status-dot"></span>
<span>Verbunden mit Sprachkanal</span>
</div>
</div>
<div class="topbar-right">
<div class="search-wrap">
<input class="search-input" type="text" placeholder="Sounds durchsuchen…" id="searchInput">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
</div>
<button class="topbar-btn" title="Einstellungen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.32 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</button>
</div>
</header>
<!-- SIDEBAR -->
<nav class="sidebar">
<div class="sidebar-header">Kategorien</div>
<div class="sidebar-item active" data-cat="all">
<span class="emoji">🎵</span>
<span>Alle Sounds</span>
<span class="count" id="countAll">15</span>
</div>
<div class="sidebar-item" data-cat="favorites">
<span class="emoji"></span>
<span>Favoriten</span>
<span class="count" id="countFav">0</span>
</div>
<div class="sidebar-item" data-cat="memes">
<span class="emoji">🐸</span>
<span>Memes</span>
<span class="count">5</span>
</div>
<div class="sidebar-item" data-cat="music">
<span class="emoji">🎸</span>
<span>Musik</span>
<span class="count">3</span>
</div>
<div class="sidebar-item" data-cat="effects">
<span class="emoji">💥</span>
<span>Effekte</span>
<span class="count">5</span>
</div>
<div class="sidebar-item" data-cat="custom">
<span class="emoji">🎨</span>
<span>Eigene</span>
<span class="count">2</span>
</div>
</nav>
<!-- MAIN -->
<main class="main" id="mainArea">
<div class="main-header">
<span class="main-title" id="mainTitle">Alle Sounds — 15</span>
<button class="sort-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 16 4 4 4-4"/><path d="M7 20V4"/><path d="m21 8-4-4-4 4"/><path d="M17 4v16"/></svg>
Sortieren
</button>
</div>
<div class="sound-grid" id="soundGrid"></div>
<div class="empty-state" id="emptyState">
<div class="empty-emoji">🔇</div>
<div class="empty-title">Keine Sounds gefunden</div>
<div class="empty-desc">In dieser Kategorie sind noch keine Sounds vorhanden. Füge welche hinzu!</div>
</div>
</main>
<!-- BOTTOM BAR -->
<div class="bottombar">
<div class="now-playing">
<div class="np-waves" id="npWaves">
<div class="np-wave-bar"></div>
<div class="np-wave-bar"></div>
<div class="np-wave-bar"></div>
<div class="np-wave-bar"></div>
</div>
<span class="np-label">Spielt:</span>
<span class="np-name" id="npName"></span>
</div>
<div class="volume-section">
<svg class="volume-icon" id="volIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>
<input type="range" class="volume-slider" id="volSlider" min="0" max="100" value="80">
<span class="volume-pct" id="volPct">80%</span>
</div>
<button class="stop-all-btn" id="stopAllBtn">
<svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
Alle stoppen
</button>
</div>
</div>
<!-- Context Menu -->
<div class="ctx-menu" id="ctxMenu">
<div class="ctx-item" data-action="play">
<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Abspielen
</div>
<div class="ctx-item" data-action="fav">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
Favorit umschalten
</div>
<div class="ctx-sep"></div>
<div class="ctx-item" data-action="edit">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
Bearbeiten
</div>
<div class="ctx-item danger" data-action="delete">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
Löschen
</div>
</div>
<!-- Tooltip -->
<div class="tooltip" id="tooltip"></div>
<script>
const SOUNDS = [
{ id: 1, name: 'Airhorn', emoji: '📯', duration: '0:02', cat: 'memes', fav: false },
{ id: 2, name: 'Bruh', emoji: '😐', duration: '0:01', cat: 'memes', fav: false },
{ id: 3, name: 'Sad Trombone', emoji: '🎺', duration: '0:03', cat: 'memes', fav: false },
{ id: 4, name: 'Applause', emoji: '👏', duration: '0:04', cat: 'effects', fav: false },
{ id: 5, name: 'Drum Roll', emoji: '🥁', duration: '0:05', cat: 'effects', fav: false },
{ id: 6, name: 'Vine Boom', emoji: '💥', duration: '0:01', cat: 'memes', fav: false },
{ id: 7, name: 'Oof', emoji: '😵', duration: '0:01', cat: 'memes', fav: false },
{ id: 8, name: 'Rickroll', emoji: '🕺', duration: '0:08', cat: 'music', fav: false },
{ id: 9, name: 'Suspense', emoji: '😰', duration: '0:06', cat: 'effects', fav: false },
{ id: 10, name: 'Victory Fanfare',emoji: '🏆', duration: '0:05', cat: 'music', fav: false },
{ id: 11, name: 'Crickets', emoji: '🦗', duration: '0:03', cat: 'effects', fav: false },
{ id: 12, name: 'Laugh Track', emoji: '😂', duration: '0:04', cat: 'effects', fav: false },
{ id: 13, name: 'Epic Sax', emoji: '🎷', duration: '0:07', cat: 'music', fav: false },
{ id: 14, name: 'Mein Sound 1', emoji: '🔊', duration: '0:03', cat: 'custom', fav: false },
{ id: 15, name: 'Custom Alert', emoji: '🔔', duration: '0:02', cat: 'custom', fav: false },
];
let currentCat = 'all';
let currentlyPlaying = null;
let ctxTarget = null;
const grid = document.getElementById('soundGrid');
const emptyState = document.getElementById('emptyState');
const mainTitle = document.getElementById('mainTitle');
const searchInput = document.getElementById('searchInput');
const ctxMenu = document.getElementById('ctxMenu');
const tooltip = document.getElementById('tooltip');
const npName = document.getElementById('npName');
const npWaves = document.getElementById('npWaves');
const volSlider = document.getElementById('volSlider');
const volPct = document.getElementById('volPct');
const stopAllBtn = document.getElementById('stopAllBtn');
const countFav = document.getElementById('countFav');
// ── Render ──
function render() {
const q = searchInput.value.toLowerCase().trim();
let filtered = SOUNDS.filter(s => {
if (currentCat === 'favorites') return s.fav;
if (currentCat !== 'all') return s.cat === currentCat;
return true;
});
if (q) filtered = filtered.filter(s => s.name.toLowerCase().includes(q));
grid.innerHTML = '';
emptyState.classList.toggle('visible', filtered.length === 0);
const catLabels = { all:'Alle Sounds', favorites:'Favoriten', memes:'Memes', music:'Musik', effects:'Effekte', custom:'Eigene' };
mainTitle.textContent = `${catLabels[currentCat] || 'Sounds'}${filtered.length}`;
filtered.forEach((s, i) => {
const card = document.createElement('div');
card.className = 'sound-card' + (currentlyPlaying === s.id ? ' playing' : '');
card.dataset.id = s.id;
card.style.animationDelay = `${i * 30}ms`;
card.innerHTML = `
<svg class="fav-star ${s.fav ? 'active' : ''}" viewBox="0 0 24 24" fill="${s.fav ? 'currentColor' : 'none'}" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
<span class="sound-emoji">${s.emoji}</span>
<span class="sound-name">${s.name}</span>
<span class="sound-duration">${s.duration}</span>
<div class="playing-indicator">
<div class="wave-bar"></div><div class="wave-bar"></div><div class="wave-bar"></div><div class="wave-bar"></div><div class="wave-bar"></div>
</div>
`;
grid.appendChild(card);
});
// Add sound card
const addCard = document.createElement('div');
addCard.className = 'sound-card add-card';
addCard.innerHTML = `<span class="add-icon">+</span><span class="add-label">Sound hinzufügen</span>`;
grid.appendChild(addCard);
updateFavCount();
}
function updateFavCount() {
countFav.textContent = SOUNDS.filter(s => s.fav).length;
}
// ── Events ──
// Sound card click
grid.addEventListener('click', e => {
const star = e.target.closest('.fav-star');
const card = e.target.closest('.sound-card');
if (!card || card.classList.contains('add-card')) return;
const id = parseInt(card.dataset.id);
const sound = SOUNDS.find(s => s.id === id);
if (!sound) return;
if (star) {
sound.fav = !sound.fav;
render();
return;
}
// Ripple
const rect = card.getBoundingClientRect();
const ripple = document.createElement('div');
ripple.className = 'ripple';
const size = Math.max(rect.width, rect.height);
ripple.style.width = ripple.style.height = size + 'px';
ripple.style.left = (e.clientX - rect.left - size / 2) + 'px';
ripple.style.top = (e.clientY - rect.top - size / 2) + 'px';
card.appendChild(ripple);
setTimeout(() => ripple.remove(), 500);
playSound(id, sound.name);
});
function playSound(id, name) {
// Remove previous playing
document.querySelectorAll('.sound-card.playing').forEach(c => c.classList.remove('playing'));
currentlyPlaying = id;
const card = grid.querySelector(`[data-id="${id}"]`);
if (card) card.classList.add('playing');
npName.textContent = name;
npWaves.classList.add('active');
// Auto stop after a bit
const s = SOUNDS.find(s => s.id === id);
const dur = s ? parseDuration(s.duration) : 3;
setTimeout(() => {
if (currentlyPlaying === id) stopPlaying();
}, dur * 1000);
}
function parseDuration(str) {
const parts = str.split(':');
return parseInt(parts[0]) * 60 + parseInt(parts[1]);
}
function stopPlaying() {
currentlyPlaying = null;
document.querySelectorAll('.sound-card.playing').forEach(c => c.classList.remove('playing'));
npName.textContent = '—';
npWaves.classList.remove('active');
}
stopAllBtn.addEventListener('click', stopPlaying);
// Sidebar
document.querySelectorAll('.sidebar-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.sidebar-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
currentCat = item.dataset.cat;
render();
});
});
// Search
searchInput.addEventListener('input', render);
// Volume
volSlider.addEventListener('input', () => {
const v = volSlider.value;
volPct.textContent = v + '%';
volSlider.style.setProperty('--vol', v + '%');
});
volSlider.style.setProperty('--vol', '80%');
// Context menu
grid.addEventListener('contextmenu', e => {
e.preventDefault();
const card = e.target.closest('.sound-card');
if (!card || card.classList.contains('add-card')) return;
ctxTarget = parseInt(card.dataset.id);
ctxMenu.style.left = Math.min(e.clientX, window.innerWidth - 180) + 'px';
ctxMenu.style.top = Math.min(e.clientY, window.innerHeight - 160) + 'px';
ctxMenu.classList.add('visible');
});
document.addEventListener('click', () => ctxMenu.classList.remove('visible'));
document.addEventListener('keydown', e => { if (e.key === 'Escape') ctxMenu.classList.remove('visible'); });
ctxMenu.addEventListener('click', e => {
const item = e.target.closest('.ctx-item');
if (!item || ctxTarget == null) return;
const action = item.dataset.action;
const sound = SOUNDS.find(s => s.id === ctxTarget);
if (!sound) return;
if (action === 'play') playSound(sound.id, sound.name);
if (action === 'fav') { sound.fav = !sound.fav; render(); }
if (action === 'delete') {
const idx = SOUNDS.findIndex(s => s.id === ctxTarget);
if (idx > -1) { SOUNDS.splice(idx, 1); render(); }
}
ctxMenu.classList.remove('visible');
});
// Tooltip
let ttTimeout;
grid.addEventListener('mouseover', e => {
const card = e.target.closest('.sound-card');
if (!card || card.classList.contains('add-card')) return;
const nameEl = card.querySelector('.sound-name');
if (!nameEl || nameEl.scrollWidth <= nameEl.clientWidth) return;
clearTimeout(ttTimeout);
ttTimeout = setTimeout(() => {
const rect = card.getBoundingClientRect();
tooltip.textContent = nameEl.textContent;
tooltip.style.left = (rect.left + rect.width / 2) + 'px';
tooltip.style.top = (rect.top - 8) + 'px';
tooltip.style.transform = 'translateX(-50%) translateY(-100%)';
tooltip.classList.add('visible');
}, 600);
});
grid.addEventListener('mouseout', e => {
if (e.target.closest('.sound-card')) {
clearTimeout(ttTimeout);
tooltip.classList.remove('visible');
}
});
// Init
render();
</script>
</body>
</html>