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..67d8d98 --- /dev/null +++ b/README.md @@ -0,0 +1,156 @@ +
+ 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. It acts as an integrated studio environment, allowing creatives to write verses, perfect their flow, and listen to instrumental tracks without ever having to switch contexts. + +### 🎯 The Problem + +Music artists and lyricists typically suffer from a highly fragmented creative workflow. When crafting a song, they are often forced to juggle multiple, unintegrated tools simultaneously: +- A notes app or basic text editor for writing lyrics. +- A web browser to search for and stream instrumental beats. +- An audio player application to loop local files. +- Manual counting or separate third-party websites to analyze syllable counts and perfect rhythmic cadences. + +This constant context-switching introduces severe friction, breaks the artist's focus, and ultimately hinders the creative flow. + +### 💡 The Solution + +Draft16 merges this entire workflow into a **single, cohesive, and distraction-free workspace**. Inspired by the premium, intuitive interfaces of tools like Notion and Figma, Draft16 brings the writing canvas, audio playback, and lyrical analysis onto one screen. By centralizing these essential tools, artists can remain deeply immersed in their creative zone. + +### 🚀 Why Draft16? (How It Stands Out) + +Unlike generic text editors or basic note-taking apps, Draft16 is purpose-built for the musical cadence. +- **Built for Flow:** While other apps just capture text, Draft16 actively assists your songwriting by providing real-time syllable counting right as you type. +- **No More Juggling Apps:** Instead of tabbing out to YouTube or iTunes to replay a beat, the integrated BeatPlayer sits right next to your lyrics, allowing you to loop, pause, and play instantly. +- **Frictionless Experience:** With invisible authentication (Google OAuth) and auto-saving cloud sessions, the technical overhead of saving and organizing files vanishes, leaving only the music. + +--- + +## ✨ Key Features + +- **Distraction-Free Editor**: A fluid, typography-focused writing canvas powered by CodeMirror 6. It provides a visual clutter-free environment optimized specifically for writing bars and verses. +- **Integrated BeatPlayer**: An embedded, robust audio player. You can load and loop local audio files directly from your device, or seamlessly stream beats from YouTube right alongside your lyrics. +- **Real-time Syllable Counting**: Built-in, on-the-fly lyrical analysis. As you type, Draft16 instantly calculates syllable counts, helping you master complex rhythmic cadences effortlessly. +- **Instant Cloud Syncing**: Secure, real-time saving and cloud storage of your lyrics, beats, and sessions to ensure no creative idea is ever lost. +- **Invisible Authentication**: A frictionless login experience utilizing Google OAuth 2.0, meaning you can bypass password fatigue and get straight to creating. +- **Intuitive Session Management**: Drag-and-drop interactions and clean dashboard UI for managing all your creative projects smoothly. + +--- + +## 📸 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 & Methods + +Draft16 is a modern, high-performance Full-Stack application utilizing a decoupled client-server architecture. + +### Frontend (Client Layer) +- **Framework**: [React 19](https://react.dev/) + [Vite](https://vitejs.dev/) for an ultra-fast SPA experience. +- **Styling**: Vanilla CSS complemented by [TailwindCSS 4](https://tailwindcss.com/) overrides, providing a highly custom, deeply controlled UI aesthetic. +- **Editor Integration**: [CodeMirror 6](https://codemirror.net/) (via `@uiw/react-codemirror`) for the core writing experience. +- **Utilities**: `syllable` for lyrical text analysis, `dnd-kit` for drag-and-drop mechanics. +- **Hosting**: [Vercel](https://vercel.com/) + +### Backend (Server Layer) +- **Environment**: [Node.js](https://nodejs.org/) & [Express.js](https://expressjs.com/) serving a robust RESTful API. +- **Database**: [MongoDB](https://www.mongodb.com/) & Mongoose for scalable user and session persistence. +- **Authentication**: JWT (JSON Web Tokens) combined with Google OAuth 2.0. +- **Media Storage**: Secure file handling using `multer` and cloud storage via [Cloudinary](https://cloudinary.com/). +- **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/daksh006v/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 ( -
-
-