diff --git a/.github/assets/dashboard-dark.png b/.github/assets/dashboard-dark.png new file mode 100644 index 0000000..e88eddb Binary files /dev/null and b/.github/assets/dashboard-dark.png differ diff --git a/.github/assets/dashboard-light.png b/.github/assets/dashboard-light.png new file mode 100644 index 0000000..9705070 Binary files /dev/null and b/.github/assets/dashboard-light.png differ diff --git a/.github/assets/editor-2.png b/.github/assets/editor-2.png new file mode 100644 index 0000000..64eefa9 Binary files /dev/null and b/.github/assets/editor-2.png differ diff --git a/.github/assets/editor.png b/.github/assets/editor.png new file mode 100644 index 0000000..37e86dd Binary files /dev/null and b/.github/assets/editor.png differ diff --git a/.github/assets/landing.png b/.github/assets/landing.png new file mode 100644 index 0000000..8d3a61c Binary files /dev/null and b/.github/assets/landing.png differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..73752d9 --- /dev/null +++ b/README.md @@ -0,0 +1,139 @@ +
+ Draft16 Logo + + # Draft16 Studio + + **The ultimate distraction-free drafting workspace tailored specifically for lyricists and songwriters.** + + [**Visit Live App**](https://draft16.vercel.app/) • [**Report Bug**](https://github.com/daksh006v/draft16/issues/new?labels=bug) • [**Request Feature**](https://github.com/daksh006v/draft16/issues/new?labels=enhancement) +
+ +
+ +## 🎵 About The Project + +Draft16 is a professional-grade web application designed from the ground up for music artists, producers, and lyricists. Far too often, writers are forced to juggle multiple apps—a notes app for writing, a browser for finding beats, and a separate app for syllable counting. + +Draft16 merges the entire creative workflow into a single, cohesive environment. + +With a deeply considered, distraction-free UI inspired by premium tools like Notion and Figma, Draft16 allows artists to write verses, count syllables in real-time, record takes, and loop audio tracks simultaneously without ever breaking their creative flow. + +### ✨ Key Features + +- **Distraction-Free Editor**: A fluid, typography-focused writing canvas (powered by CodeMirror) designed for writing bars without visual clutter. +- **Integrated BeatPlayer**: Load local audio files or instantly stream YouTube beats directly alongside your lyrics, complete with loop functionality. +- **Real-time Syllable Counting**: Built-in syllable analysis helps you perfect your flow and manage your cadences effortlessly. +- **Instant Cloud Syncing**: Secure, real-time saving of all your sessions so your lyrics are never lost. +- **Invisible Authentication**: Seamless, one-click Google OAuth means getting straight to writing without password fatigue. + +--- + +## 📸 Interface Previews + + +
+ Draft16 Landing Page +

The intuitive and welcoming landing portal.

+ +
+ + Draft16 Dashboard (Dark Mode) +

Managing your sessions and creative projects.

+ +
+ + Draft16 Session Editor +

The core editing environment featuring the integrated BeatPlayer and syllable counter.

+ +
+ + Draft16 Dashboard (Light Mode) +

Seamless light mode support for different studio environments.

+
+ +--- + +## 🛠️ Technology Stack + +Draft16 is a modern Full-Stack application utilizing a decoupled client-server architecture. + +### Frontend (Client) +- **Framework**: [React 19](https://react.dev/) + [Vite](https://vitejs.dev/) +- **Styling**: Vanilla CSS with [TailwindCSS 4](https://tailwindcss.com/) overrides for maximum control. +- **Editor**: [CodeMirror 6](https://codemirror.net/) via `@uiw/react-codemirror` +- **Utilities**: `syllable` for lyrical analysis, `dnd-kit` for drag-and-drop interactions. + +### Backend (Server) +- **Environment**: [Node.js](https://nodejs.org/) & [Express.js](https://expressjs.com/) +- **Database**: [MongoDB](https://www.mongodb.com/) & Mongoose +- **Authentication**: JWT (JSON Web Tokens) & Google OAuth 2.0 Integration +- **Media Storage**: [Cloudinary](https://cloudinary.com/) (Multer integration) + +### Deployment +- **Frontend Hosting**: [Vercel](https://vercel.com/) +- **Backend Hosting**: [Render](https://render.com/) + +--- + +## 🚀 Getting Started Locally + +To get a local copy of Draft16 up and running, follow these simple steps. + +### Prerequisites +Make sure you have Node.js and npm installed on your machine. +* npm + ```sh + npm install npm@latest -g + ``` + +### Installation + +1. **Clone the repo** + ```sh + git clone https://github.com/your-username/draft16.git + ``` + +2. **Setup the Backend** + ```sh + cd server + npm install + ``` + *Create a `.env` file in the `server` directory using `.env.example` as a template and provide your MongoDB URI and Cloudinary credentials.* + +3. **Setup the Frontend** + ```sh + cd ../client + npm install + ``` + +4. **Run both environments** + *You can run them in separate terminal windows.* + + *Window 1 (Backend):* + ```sh + cd server + npm run dev + ``` + + *Window 2 (Frontend):* + ```sh + cd client + npm run dev + ``` + +5. **Open Application** + Navigate to `http://localhost:5173` in your browser. + +--- + +## 🔒 License + +Distributed under the MIT License. See `LICENSE` for more information. + +--- + +
+ Built specifically to push the culture forward. +
+ Draft16 +
\ No newline at end of file diff --git a/client/.env.example b/client/.env.example new file mode 100644 index 0000000..74ff160 --- /dev/null +++ b/client/.env.example @@ -0,0 +1,4 @@ +# Backend API base URL (no trailing slash) +# Development: http://localhost:5000/api +# Production: https://your-backend.com/api +VITE_API_URL=http://localhost:5000/api diff --git a/client/README.md b/client/README.md deleted file mode 100644 index d1c8478..0000000 --- a/client/README.md +++ /dev/null @@ -1,111 +0,0 @@ -# Draft16 - -> Where 16s Are Born. - ---- - -## PROJECT OVERVIEW - -Draft16 is a full-stack creative workspace for rappers and lyricists that allows users to write lyrics, attach beats, and manage songwriting sessions in one place. - ---- - -## TECH STACK - -**Frontend** -- React (Vite) -- Tailwind CSS -- React Router -- Axios - -**Backend** -- Node.js -- Express.js - -**Database** -- MongoDB Atlas -- Mongoose - -**Authentication** -- JWT -- bcrypt - ---- - -## FEATURES - -**User Authentication** -- Signup -- Login -- Protected routes - -**Songwriting Sessions** -- Create session -- Edit lyrics -- Attach beat URLs -- Save sessions - -**Dashboard** -- View sessions -- Open sessions -- Manage drafts - -**Session Editor** -- Lyrics editor -- Beat URL integration -- Save changes - -**Beat Playback** -- Embedded YouTube beat player inside the writing workspace. - ---- - -## PROJECT STRUCTURE - -```text -draft16 -├ client -│ ├ src -│ │ ├ components -│ │ ├ pages -│ │ ├ services -│ │ └ utils -│ -└ server -├ controllers -├ models -├ routes -├ middleware -└ config -``` - ---- - -## API OVERVIEW - -**Auth Routes** -- `POST /api/auth/signup` -- `POST /api/auth/login` - -**Session Routes** -- `GET /api/sessions` -- `POST /api/sessions` -- `GET /api/sessions/:id` -- `PUT /api/sessions/:id` -- `DELETE /api/sessions/:id` - ---- - -## FUTURE IMPROVEMENTS - -- Autosave lyrics -- Audio beat uploads -- Voice demo recording -- Session search -- Real-time collaboration - ---- - -## AUTHOR - -Daksh Bajaniya \ No newline at end of file diff --git a/client/index.html b/client/index.html index 6e56e6e..59bed24 100644 --- a/client/index.html +++ b/client/index.html @@ -2,9 +2,30 @@ - + - client + + + Draft16 Studio | Professional Songwriting Workspace + + + + + + + + + + + + + + + + + + +
diff --git a/client/package-lock.json b/client/package-lock.json index 5e55cec..c58dece 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -17,6 +17,7 @@ "@tailwindcss/vite": "^4.2.1", "@uiw/react-codemirror": "^4.25.8", "axios": "^1.13.6", + "lucide-react": "^1.7.0", "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^7.13.1", @@ -3084,6 +3085,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz", + "integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", diff --git a/client/package.json b/client/package.json index fcf31dc..47b27ab 100644 --- a/client/package.json +++ b/client/package.json @@ -19,6 +19,7 @@ "@tailwindcss/vite": "^4.2.1", "@uiw/react-codemirror": "^4.25.8", "axios": "^1.13.6", + "lucide-react": "^1.7.0", "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^7.13.1", diff --git a/client/public/favicon.png b/client/public/favicon.png new file mode 100644 index 0000000..e56dcea Binary files /dev/null and b/client/public/favicon.png differ diff --git a/client/public/favicon.svg b/client/public/favicon.svg deleted file mode 100644 index 6893eb1..0000000 --- a/client/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/public/google9400ccb65d51108d.html b/client/public/google9400ccb65d51108d.html new file mode 100644 index 0000000..6304f0d --- /dev/null +++ b/client/public/google9400ccb65d51108d.html @@ -0,0 +1 @@ +google-site-verification: google9400ccb65d51108d.html \ No newline at end of file diff --git a/client/public/robots.txt b/client/public/robots.txt new file mode 100644 index 0000000..ed7196e --- /dev/null +++ b/client/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://draft16.vercel.app/sitemap.xml diff --git a/client/public/sitemap.xml b/client/public/sitemap.xml new file mode 100644 index 0000000..8035c7b --- /dev/null +++ b/client/public/sitemap.xml @@ -0,0 +1,19 @@ + + + + https://draft16.vercel.app/ + 2026-04-02 + weekly + 1.0 + + + https://draft16.vercel.app/signup + monthly + 0.8 + + + https://draft16.vercel.app/login + monthly + 0.8 + + diff --git a/client/src/App.css b/client/src/App.css deleted file mode 100644 index f90339d..0000000 --- a/client/src/App.css +++ /dev/null @@ -1,184 +0,0 @@ -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} diff --git a/client/src/App.jsx b/client/src/App.jsx index 3da2881..5f3fb49 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -6,6 +6,9 @@ import Signup from './pages/Signup'; import Dashboard from './pages/Dashboard'; import SessionEditor from './pages/SessionEditor'; import NewSession from './pages/NewSession'; +import AuthSuccess from './pages/AuthSuccess'; +import AuthError from './pages/AuthError'; +import NotFound from './pages/NotFound'; import { ThemeProvider } from './context/ThemeContext'; function App() { @@ -20,6 +23,9 @@ function App() { } /> } /> } /> + } /> + } /> + } /> diff --git a/client/src/assets/hero.png b/client/src/assets/hero.png deleted file mode 100644 index cc51a3d..0000000 Binary files a/client/src/assets/hero.png and /dev/null differ diff --git a/client/src/assets/react.svg b/client/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/client/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/assets/vite.svg b/client/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/client/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/client/src/components/BeatPlayer.jsx b/client/src/components/BeatPlayer.jsx index 7114d60..670dc2c 100644 --- a/client/src/components/BeatPlayer.jsx +++ b/client/src/components/BeatPlayer.jsx @@ -1,128 +1,35 @@ -import React, { forwardRef, useImperativeHandle, useRef, useState, useEffect } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; +import { useAudioEngine } from '../hooks/useAudioEngine'; -const BeatPlayer = forwardRef(({ beatSource, beatUrl }, ref) => { - const iframeRef = useRef(null); - +// Utility format: 65 -> "1:05" +const formatTime = (seconds) => { + if (seconds === null || seconds === undefined || isNaN(seconds)) return "0:00"; + const m = Math.floor(seconds / 60); + const s = Math.floor(seconds % 60); + return `${m}:${s.toString().padStart(2, '0')}`; +}; + +const BeatPlayer = ({ beatUrl, beatSource = 'upload' }) => { + const { audioRef, play, pause, seek, isPlaying, duration } = useAudioEngine(beatUrl); + + // UI time — driven by rAF, frozen during drag to prevent jitter + const [uiTime, setUiTime] = useState(0); + + // --- NEW: Loop Controls State --- + const [loopEnabled, setLoopEnabled] = useState(false); const [loopStart, setLoopStart] = useState(null); const [loopEnd, setLoopEnd] = useState(null); - const [loopEnabled, setLoopEnabled] = useState(false); - const [loopStartInput, setLoopStartInput] = useState(''); const [loopEndInput, setLoopEndInput] = useState(''); const [loopError, setLoopError] = useState(''); - - // Track current time - const currentTimeRef = useRef(0); - - // We need to receive messages back from the iframe to get current time - useEffect(() => { - const handleMessage = (event) => { - // Basic check, might need to be more robust for production - try { - const data = JSON.parse(event.data); - if (data.event === 'infoDelivery' && data.info) { - if (data.info.currentTime !== undefined) { - currentTimeRef.current = data.info.currentTime; - } - } - } catch (e) { - // Ignore parsing errors for non-JSON messages - } - }; - window.addEventListener('message', handleMessage); - return () => window.removeEventListener('message', handleMessage); - }, []); + // Sync state to ref for zero-latency rAF execution + const loopStateRef = useRef({ enabled: false, start: null, end: null }); useEffect(() => { - // Request current time from YouTube iframe periodically - const timeInterval = setInterval(() => { - if (iframeRef.current && iframeRef.current.contentWindow) { - // Ask YouTube player for current time - iframeRef.current.contentWindow.postMessage(JSON.stringify({ - event: 'listening' - }), '*'); - } - - const currentTime = currentTimeRef.current; - - if (loopEnabled && loopStart !== null && loopEnd !== null) { - if (currentTime >= loopEnd) { - if (iframeRef.current && iframeRef.current.contentWindow) { - iframeRef.current.contentWindow.postMessage(JSON.stringify({ - event: 'command', - func: 'seekTo', - args: [loopStart, true] - }), '*'); - } - } - } - }, 250); - - return () => clearInterval(timeInterval); + loopStateRef.current = { enabled: loopEnabled, start: loopStart, end: loopEnd }; }, [loopEnabled, loopStart, loopEnd]); - useImperativeHandle(ref, () => ({ - seekTo: (seconds) => { - if (iframeRef.current && iframeRef.current.contentWindow) { - iframeRef.current.contentWindow.postMessage(JSON.stringify({ - event: 'command', - func: 'seekTo', - args: [seconds, true] - }), '*'); - iframeRef.current.contentWindow.postMessage(JSON.stringify({ - event: 'command', - func: 'playVideo', - args: [] - }), '*'); - } - }, - getCurrentTime: () => { - return currentTimeRef.current; - }, - setLoop: (startSeconds, endSeconds) => { - if (endSeconds <= startSeconds) { - return { error: 'End time must be greater than start time.' }; - } - setLoopStart(startSeconds); - setLoopEnd(endSeconds); - setLoopEnabled(true); - return { success: true }; - }, - clearLoop: () => { - setLoopStart(null); - setLoopEnd(null); - setLoopEnabled(false); - }, - toggleLoop: () => { - setLoopEnabled(prev => !prev); - } - })); - - if (beatSource !== 'youtube' || !beatUrl) { - return null; - } - - const extractVideoId = (url) => { - try { - const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; - const match = url.match(regExp); - return (match && match[2].length === 11) ? match[2] : null; - } catch (e) { - return null; - } - }; - - const videoId = extractVideoId(beatUrl); - - if (!videoId) return null; - - const formatTime = (seconds) => { - if (seconds === null || seconds === undefined) return '--:--'; - const m = Math.floor(seconds / 60); - const s = Math.floor(seconds % 60); - return `${m}:${s.toString().padStart(2, '0')}`; - }; - + // --- Helpers for parsing --- const formatTimeInput = (value) => { const digits = value.replace(/\D/g, '').slice(0, 4); if (digits.length <= 2) { @@ -149,18 +56,22 @@ const BeatPlayer = forwardRef(({ beatSource, beatUrl }, ref) => { setLoopError("Please enter both start and end times."); return; } - if (start < 0) { + if (start < 0 || isNaN(start)) { setLoopError("Invalid start time."); return; } - if (end <= start) { + if (end <= start || isNaN(end)) { setLoopError("End must be after start."); return; } + if (duration > 0 && end > duration) { + setLoopError("End time exceeds track duration."); + return; + } setLoopStart(start); setLoopEnd(end); - setLoopEnabled(true); + setLoopEnabled(true); // Auto-enable loop automatically }; const handleClearLoop = () => { @@ -172,107 +83,283 @@ const BeatPlayer = forwardRef(({ beatSource, beatUrl }, ref) => { setLoopError(''); }; - return ( -
-
-