/*
 Theme Name: MyFavDJ Nightclub (Astra Child)
 Theme URI: https://myfavdj.com/
 Description: Dark neon nightclub look for MyFavDJ. Child theme of Astra.
 Author: MyFavDJ
 Template: astra
 Version: 1.0.0
*/

:root {
  --bg: #000;
  --text: #fff;
  --muted: #a3a3a3;
  --pink: #ec4899;
  --purple: #8b5cf6;
  --blue: #3b82f6;
  --green: #22c55e;
  --yellow: #facc15;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
}

/* Header */
.mfdj-header {
  padding: 24px 16px;
  background: linear-gradient(90deg, var(--pink), var(--purple), var(--blue));
  text-align: center;
}
.mfdj-header .logo {
  max-width: 220px;
  margin: 0 auto 12px;
  display: block;
}
.mfdj-header nav a {
  color: #fff;
  margin: 0 12px;
  font-size: 14px;
  text-decoration: none;
  opacity: 0.9;
}
.mfdj-header nav a:hover { opacity: 1; text-decoration: underline; }

/* Hero */
.mfdj-hero {
  max-width: 980px;
  margin: 0 auto;
  padding: 80px 16px 48px;
  text-align: center;
}
.mfdj-hero h1 {
  font-size: clamp(32px, 5vw, 64px);
  margin: 0 0 12px;
  font-weight: 800;
}
.mfdj-hero p {
  color: var(--muted);
  font-size: 18px;
  margin: 0 0 24px;
}
.mfdj-btn {
  display: inline-block;
  background: var(--pink);
  color: #000;
  border-radius: 12px;
  padding: 12px 20px;
  font-weight: 700;
  transition: transform .05s ease-in-out, filter .15s ease;
}
.mfdj-btn:hover { filter: brightness(1.05); transform: translateY(-1px); }

/* Card */
.mfdj-card {
  max-width: 820px;
  margin: 0 auto;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 16px;
  padding: 24px;
}

/* Footer */
.mfdj-footer {
  border-top: 1px solid rgba(255,255,255,0.15);
  color: var(--muted);
  text-align: center;
  padding: 28px 16px;
  margin-top: 48px;
}

/* Form styling (works nicely with WPForms/CF7 defaults) */
.mfdj-form label { display:block; font-size:14px; margin: 12px 0 6px; }
.mfdj-form input, .mfdj-form textarea, .mfdj-form select {
  width: 100%;
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.15);
  color: #fff;
  border-radius: 12px;
  padding: 12px 14px;
}
.mfdj-form input::placeholder, .mfdj-form textarea::placeholder { color: #bdbdbd; }
.mfdj-form button[type="submit"], .wpforms-submit, .wpcf7-submit {
  background: var(--pink) !important;
  color: #000 !important;
  border-radius: 12px !important;
  padding: 12px 20px !important;
  font-weight: 700 !important;
  border: none !important;
  cursor: pointer;
}
/* HERO SECTION WITH ANIMATED GRADIENT BACKGROUND */
.mfdj-hero {
  position: relative;
  background: linear-gradient(-45deg, #000000, #1a1a40, #330033, #000000);
  background-size: 400% 400%;
  animation: gradientMove 15s ease infinite;
  color: #fff;
  text-align: center;
  padding: 100px 20px;
  font-family: 'Arial', sans-serif;
  overflow: hidden;
}

@keyframes gradientMove {
  0% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

.mfdj-hero-content {
  position: relative;
  z-index: 2;
  max-width: 800px;
  margin: 0 auto;
}

/* LOGO NEON GLOW */
.neon-glow {
  max-width: 280px;
  display: block;
  margin: 0 auto 20px;
  filter: drop-shadow(0 0 10px #ff00cc) drop-shadow(0 0 20px #3333ff);
  animation: neonPulse 2s infinite alternate;
}

/* TEXT NEON GLOW */
.neon-text {
  font-size: 3rem;
  font-weight: bold;
  color: #ff00cc;
  text-shadow:
    0 0 10px #ff00cc,
    0 0 20px #ff00cc,
    0 0 40px #3333ff;
  animation: textGlow 1.5s ease-in-out infinite alternate;
}

.neon-text span {
  color: #3333ff;
}

/* ANIMATIONS */
@keyframes neonPulse {
  from {
    filter: drop-shadow(0 0 10px #ff00cc) drop-shadow(0 0 20px #3333ff);
  }
  to {
    filter: drop-shadow(0 0 20px #ff00cc) drop-shadow(0 0 40px #3333ff);
  }
}

@keyframes textGlow {
  from {
    text-shadow:
      0 0 10px #ff00cc,
      0 0 20px #ff00cc,
      0 0 40px #3333ff;
  }
  to {
    text-shadow:
      0 0 20px #ff00cc,
      0 0 40px #ff00cc,
      0 0 80px #3333ff;
  }
}

/* FLOATING MUSIC NOTES */
.floating-notes {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  z-index: 1;
  pointer-events: none;
}

.floating-notes span {
  position: absolute;
  font-size: 2rem;
  animation: floatUp 8s linear infinite;
  opacity: 0.6;
}

.floating-notes span:nth-child(1) { left: 10%; animation-delay: 0s; color: #ff00cc; }
.floating-notes span:nth-child(2) { left: 30%; animation-delay: 2s; color: #3333ff; }
.floating-notes span:nth-child(3) { left: 50%; animation-delay: 4s; color: #ff00cc; }
.floating-notes span:nth-child(4) { left: 70%; animation-delay: 1s; color: #3333ff; }
.floating-notes span:nth-child(5) { left: 90%; animation-delay: 3s; color: #ff00cc; }

@keyframes floatUp {
  0% {
    transform: translateY(100vh) scale(0.8);
    opacity: 0.5;
  }
  50% {
    opacity: 1;
    transform: translateY(50vh) scale(1.2);
  }
  100% {
    transform: translateY(-10vh) scale(0.8);
    opacity: 0;
  }
}

/* BUTTON */
.mfdj-btn {
  display: inline-block;
  background: linear-gradient(90deg, #ff00cc, #3333ff);
  color: #fff;
  padding: 15px 30px;
  font-size: 1.2rem;
  font-weight: bold;
  border-radius: 50px;
  text-decoration: none;
  transition: 0.3s ease;
  box-shadow: 0 0 10px #ff00cc, 0 0 20px #3333ff;
}

.mfdj-btn:hover {
  opacity: 0.8;
}

/* EMAIL SIGNUP FORM */
.mfdj-signup-form {
  margin-top: 30px;
  display: flex;
  justify-content: center;
  gap: 10px;
}

.mfdj-signup-form input[type="email"] {
  padding: 12px;
  border: none;
  border-radius: 30px;
  width: 250px;
  font-size: 1rem;
}

.mfdj-signup-form button {
  background: linear-gradient(90deg, #ff00cc, #3333ff);
  color: #fff;
  padding: 12px 25px;
  border: none;
  border-radius: 30px;
  font-size: 1rem;
  cursor: pointer;
  transition: 0.3s ease;
  box-shadow: 0 0 10px #ff00cc, 0 0 20px #3333ff;
}

.mfdj-signup-form button:hover {
  opacity: 0.8;
}
.mfdj-message {
  margin-top: 15px;
  padding: 12px;
  background: rgba(0, 0, 0, 0.8);
  color: #fff;
  font-weight: bold;
  border-radius: 8px;
  text-align: center;
  animation: fadeIn 0.8s ease;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(-10px); }
  to { opacity: 1; transform: translateY(0); }
}
mfdj-frontend/
├─ package.json
├─ vite.config.js
├─ index.html
├─ src/
│  ├─ main.jsx
│  ├─ App.jsx
│  ├─ api.js
│  ├─ styles.css
│  └─ components/
│     ├─ FanPage.jsx
│     └─ DjDashboard.jsx
my-app/
├─ src/
│  ├─ main.jsx
│  ├─ App.jsx
│  ├─ api.js
│  ├─ styles.css
│  └─ components/
│     ├─ FanRequest.jsx
│     ├─ DJDashboard.jsx
│     ├─ CompanionDevice.jsx
│     ├─ LiveDashboard.jsx
│     └─ DashboardWidgets.jsx   <-- Template-style cards + charts
my-app/
├─ public/
│  └─ index.html
├─ src/
│  ├─ main.jsx
│  ├─ App.jsx
│  ├─ api.js
│  ├─ styles.css
│  └─ components/
│     ├─ FanRequest.jsx
│     ├─ DJDashboard.jsx
│     ├─ CompanionDevice.jsx
│     ├─ LiveDashboard.jsx
│     └─ DashboardWidgets.jsx
├─ package.json
└─ vite.config.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './styles.css';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import { useState } from 'react';
import FanRequest from './components/FanRequest';
import DJDashboard from './components/DJDashboard';
import CompanionDevice from './components/CompanionDevice';
import LiveDashboard from './components/LiveDashboard';

export default function App() {
  const [djId, setDjId] = useState('');
  const [token, setToken] = useState('');

  return (
    <BrowserRouter>
      <div className="p-4">
        <nav className="mb-4 flex gap-4">
          <Link className="text-blue-600" to="/">Fan Request</Link>
          <Link className="text-blue-600" to="/dj">DJ Dashboard</Link>
          <Link className="text-blue-600" to="/companion">Companion Device</Link>
          <Link className="text-blue-600" to="/live">Live Dashboard</Link>
        </nav>

        <Routes>
          <Route path="/" element={<FanRequest />} />
          <Route path="/dj" element={<DJDashboard />} />
          <Route path="/companion" element={<CompanionDevice setToken={setToken} />} />
          <Route path="/live" element={<LiveDashboard djId={djId} token={token} />} />
        </Routes>

        <div className="mt-4 max-w-md mx-auto flex gap-2">
          <input
            className="w-1/2 p-2 border rounded"
            placeholder="DJ ID"
            value={djId}
            onChange={e => setDjId(e.target.value)}
          />
          <input
            className="w-1/2 p-2 border rounded"
            placeholder="JWT Token"
            value={token}
            onChange={e => setToken(e.target.value)}
          />
        </div>
      </div>
    </BrowserRouter>
  );
}
import { useState } from 'react';
import { api } from '../api';

export default function FanRequest() {
  const [fanName, setFanName] = useState('');
  const [trackQuery, setTrackQuery] = useState('');
  const [tip, setTip] = useState(0);
  const [eventId, setEventId] = useState('');
  const [msg, setMsg] = useState('');

  const submitRequest = async () => {
    if (!trackQuery || !eventId) {
      setMsg('Event ID and Track required');
      return;
    }
    try {
      await api.post('/v1/requests', { fanName, trackQuery, tipCents: tip, eventId });
      setMsg('✅ Request submitted!');
      setTrackQuery('');
      setTip(0);
    } catch (err) {
      setMsg('❌ Error submitting request');
    }
  };

  return (
    <div className="max-w-md mx-auto bg-white p-4 rounded shadow">
      <h2 className="text-xl font-bold mb-2">Submit a Fan Request</h2>
      <input className="w-full mb-2 p-2 border rounded" placeholder="Event ID" value={eventId} onChange={e=>setEventId(e.target.value)} />
      <input className="w-full mb-2 p-2 border rounded" placeholder="Your Name" value={fanName} onChange={e=>setFanName(e.target.value)} />
      <input className="w-full mb-2 p-2 border rounded" placeholder="Song / Artist" value={trackQuery} onChange={e=>setTrackQuery(e.target.value)} />
      <input className="w-full mb-2 p-2 border rounded" type="number" placeholder="Tip ($)" value={tip} onChange={e=>setTip(Number(e.target.value))} />
      <button className="w-full bg-blue-600 text-white p-2 rounded" onClick={submitRequest}>Submit</button>
      {msg && <p className="mt-2">{msg}</p>}
    </div>
  );
}
import { useState } from 'react';
import { api, setToken } from '../api';

export default function CompanionDevice({ setToken: setGlobalToken }) {
  const [deviceCode, setDeviceCode] = useState('');
  const [message, setMessage] = useState('');
  const [token, setTokenState] = useState('');

  const getToken = async () => {
    try {
      const res = await api.post('/v1/auth/token', { device_code: deviceCode });
      setTokenState(res.data.access_token);
      setGlobalToken(res.data.access_token);
      setToken(res.data.access_token);
      setMessage('✅ Token acquired!');
    } catch (err) {
      setMessage(err.response?.data?.error || '❌ Error getting token');
    }
  };

  return (
    <div className="max-w-md mx-auto bg-white p-4 rounded shadow">
      <h2 className="text-xl font-bold mb-2">Companion Device</h2>
      <input className="w-full mb-2 p-2 border rounded" placeholder="Device Code" value={deviceCode} onChange={e=>setDeviceCode(e.target.value)} />
      <button className="w-full bg-blue-600 text-white p-2 rounded" onClick={getToken}>Get Token</button>
      {message && <p className="mt-2">{message}</p>}
      {token && <p className="mt-2 break-words">Token: {token}</p>}
    </div>
  );
}
import { useEffect, useState } from 'react';
import { api } from '../api';
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';

export default function DashboardWidgets({ djId }) {
  const [topTracks, setTopTracks] = useState([]);
  const [topTippers, setTopTippers] = useState([]);

  const fetchData = async () => {
    if (!djId) return;
    try {
      const tracksRes = await api.get(`/v1/djs/${djId}/analytics/top-tracks`);
      const tippersRes = await api.get(`/v1/djs/${djId}/analytics/top-tippers`);
      setTopTracks(tracksRes.data);
      setTopTippers(tippersRes.data);
    } catch (err) {
      console.error(err);
    }
  };

  useEffect(() => { fetchData(); }, [djId]);

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
      <div className="bg-white p-4 rounded shadow">
        <h3 className="font-bold mb-2">Top Requested Tracks</h3>
        <ResponsiveContainer width="100%" height={200}>
          <BarChart data={topTracks}>
            <XAxis dataKey="track" />
            <YAxis />
            <Tooltip />
            <Bar dataKey="count" fill="#6366f1" />
          </BarChart>
        </ResponsiveContainer>
      </div>

      <div className="bg-white p-4 rounded shadow">
        <h3 className="font-bold mb-2">Top Tippers</h3>
        <ResponsiveContainer width="100%" height={200}>
          <BarChart data={topTippers}>
            <XAxis dataKey="fanName" />
            <YAxis />
            <Tooltip />
            <Bar dataKey="totalTip" fill="#ec4899" />
          </BarChart>
        </ResponsiveContainer>
      </div>
    </div>
  );
}
import { useState, useEffect } from 'react';
import { api } from '../api';
import DashboardWidgets from './DashboardWidgets';

export default function DJDashboard() {
  const [requests, setRequests] = useState([]);
  const [djId, setDjId] = useState('');
  const [token, setToken] = useState('');

  const fetchRequests = async () => {
    if (!djId) return;
    try {
      const res = await api.get(`/v1/djs/${djId}/requests?status=pending`);
      setRequests(res.data);
    } catch (err) { console.error(err); }
  };

  const act = async (id, action) => {
    try {
      await api.post(`/v1/requests/${id}/action`, { action });
      fetchRequests();
    } catch (err) { console.error(err); }
  };

  useEffect(() => {
    if (token) api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
  }, [token]);

  useEffect(() => {
    const ws = new WebSocket(`ws://${window.location.hostname}:4000/ws`);
    ws.onmessage = e => {
      const data = JSON.parse(e.data);
      if (data.type === 'request_update') fetchRequests();
    };
    return () => ws.close();
  }, [djId]);

  return (
    <div className="max-w-4xl mx-auto p-4">
      <h2 className="text-2xl font-bold mb-4">DJ Dashboard</h2>
      <input className="mb-2 p-2 border rounded" placeholder="DJ ID" value={djId} onChange={e=>setDjId(e.target.value)} />
      <input className="mb-2 p-2 border rounded" placeholder="Token" value={token} onChange={e=>setToken(e.target.value)} />
      <button className="mb-4 bg-green-600 text-white p-2 rounded" onClick={fetchRequests}>Fetch Requests</button>

      <DashboardWidgets djId={djId} />

      <h3 className="text-xl font-bold mt-6 mb-2">Request Queue</h3>
      {requests.length ? requests.map(r => (
        <div key={r.id} className="flex justify-between items-center border-b py-2">
          <div><strong>{r.trackQuery}</strong> by {r.fanName || 'Anonymous'} (${r.tipCents/100})</div>
          <div className="flex gap-1">
            <button className="bg-green-500 text-white px-2 rounded" onClick={()=>act(r.id,'played')}>Played</button>
            <button className="bg-yellow-500 text-white px-2 rounded" onClick={()=>act(r.id,'skip')}>Skip</button>
          </div>
        </div>
      )) : <p className="text-gray-500">No pending requests</p>}
    </div>
  );
}
import { useState, useEffect } from 'react';
import { api } from '../api';

export default function LiveDashboard({ djId, token }) {
  const [requests, setRequests] = useState([]);
  const [nowPlaying, setNowPlaying] = useState(null);

  useEffect(() => {
    if (token) api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
  }, [token]);

  const fetchRequests = async () => {
    if (!djId) return;
    try {
      const res = await api.get(`/v1/djs/${djId}/requests?status=pending`);
      setRequests(res.data);
    } catch (err) { console.error(err); }
  };

  useEffect(() => { fetchRequests(); }, [djId]);

  useEffect(() => {
    const ws = new WebSocket(`ws://${window.location.hostname}:4000/ws`);
    ws.onmessage = e => {
      const data = JSON.parse(e.data);
      if (data.type === 'now_playing' && data.payload.djId === djId) {
        setNowPlaying(data.payload.track || null);
      }
      if (data.type === 'request_update') fetchRequests();
    };
    return () => ws.close();
  }, [djId]);

  const act = async (id, action) => {
    try { await api.post(`/v1/requests/${id}/action`, { action }); fetchRequests(); }
    catch (err) { console.error(err); }
import axios from 'axios';

const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:4000';

export const api = axios.create({
  baseURL: API_BASE,
  headers: { 'Content-Type': 'application/json' },
});

export const setToken = (token) => {
  api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
};
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Custom gradients and animations for DJ vibe */
.animate-pulse {
  animation: pulse 2s infinite;
}
/** @type {import('tailwindcss').Config} */
export default {
  content: ["./index.html","./src/**/*.{js,jsx}"],
  darkMode: 'class', // Enable class-based dark mode
  theme: {
    extend: {
      colors: {
        neonPink: '#FF6EC7',
        neonPurple: '#8A2BE2',
        neonBlue: '#1E90FF',
      },
      animation: {
        pulseGlow: 'pulseGlow 2s infinite',
        fadeIn: 'fadeIn 1s ease-in-out',
      },
      keyframes: {
        pulseGlow: {
          '0%, 100%': { boxShadow: '0 0 10px #FF6EC7, 0 0 20px #8A2BE2' },
          '50%': { boxShadow: '0 0 20px #FF6EC7, 0 0 40px #8A2BE2' },
        },
        fadeIn: {
          '0%': { opacity: 0 },
          '100%': { opacity: 1 },
        },
      },
    },
  },
  plugins: [],
}
import { useState } from 'react';

export default function App() {
  const [darkMode, setDarkMode] = useState(false);

  return (
    <div className={darkMode ? 'dark bg-gray-900 text-white min-h-screen' : 'bg-gray-100 text-gray-900 min-h-screen'}>
      <button
        className="fixed top-4 right-4 bg-neonPink text-black px-4 py-2 rounded shadow-lg hover:shadow-neonPurple transition"
        onClick={() => setDarkMode(!darkMode)}
      >
        {darkMode ? 'Light Mode' : 'Dark Mode'}
      </button>
      {/* ...rest of your App JSX */}
    </div>
  );
}
{nowPlaying && (
  <div className="p-6 rounded-xl bg-gradient-to-r from-neonPink to-neonPurple text-white shadow-lg animate-pulseGlow mb-4">
    <h3 className="text-2xl font-bold animate-fadeIn">{nowPlaying.title}</h3>
    <p className="text-lg animate-fadeIn">{nowPlaying.artist}</p>
    {nowPlaying.preview && (
      <audio controls className="mt-2 w-full rounded bg-gray-800">
        <source src={nowPlaying.preview} type="audio/mpeg" />
      </audio>
    )}
  </div>
)}
<button className="bg-neonBlue hover:shadow-neonPurple text-white px-3 py-1 rounded transition transform hover:scale-105">
  Played
</button>
<button className="bg-neonPink hover:shadow-neonBlue text-white px-3 py-1 rounded transition transform hover:scale-105">
  Skip
</button>
	<div key={r.id} className="flex justify-between items-center border-b py-2 animate-fadeIn">
  <div><strong>{r.trackQuery}</strong> by {r.fanName || 'Anonymous'} (${r.tipCents/100})</div>
  <div className="flex gap-1">
    {/* Neon buttons as above */}
  </div>
</div>
animation  
<div className="bg-gray-900 dark:bg-black p-4 rounded-xl border-2 border-neonBlue shadow-lg animate-pulseGlow">
  <h3 className="font-bold mb-2 text-neonPink">Top Requested Tracks</h3>
  {/* Chart */}
</div>
	  @keyframes bgMove {
  0% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

.bg-animated {
  background: linear-gradient(270deg, #FF6EC7, #8A2BE2, #1E90FF);
  background-size: 600% 600%;
  animation: bgMove 15s ease infinite;
}
<div className="bg-animated p-4 rounded-xl">
  {/* Live content */}
</div>
