diff --git a/.claude/commands/guardrail/implement.md b/.claude/commands/guardrail/implement.md index cb151e3..dd711a1 100644 --- a/.claude/commands/guardrail/implement.md +++ b/.claude/commands/guardrail/implement.md @@ -1,74 +1,184 @@ --- -description: Implement an approved entity from the manifest -allowed-tools: Read, Write, Bash +description: Implement a task from the workflow +allowed-tools: Read, Write, Edit, Bash, Glob, Grep --- -# Implement Entity +# Implement Task -Implement the entity: "$ARGUMENTS" +Implement the task: "$ARGUMENTS" -## CRITICAL RULES +## ⚠️ CRITICAL: WORKFLOW COMPLIANCE -⚠️ **GUARDRAIL ENFORCEMENT ACTIVE** +You MUST follow the design document exactly. All implementations must match the specifications. -You can ONLY write to files that: -1. Are defined in `project_manifest.json` -2. Have status = `APPROVED` -3. Match the `file_path` in the manifest EXACTLY +## Pre-Implementation Checklist -## Steps +Before writing ANY code, complete these steps: -1. **Verify Phase**: Must be in `IMPLEMENTATION_PHASE` +### 1. Load Workflow Context -2. **Find Entity** in manifest: - - If "$ARGUMENTS" is `--all`: implement all APPROVED entities - - Otherwise: find the specific entity by ID +```bash +# Check workflow status +python3 skills/guardrail-orchestrator/scripts/workflow_manager.py status -3. **For Each Entity**: - - a. **Load Definition** from manifest - - b. **Verify Status** is `APPROVED` - - c. **Generate Code** matching the specification: - - Props must match manifest exactly - - Types must match manifest exactly - - File path must match `file_path` in manifest - - d. **Write File** to the exact path in manifest - - e. **Run Validations**: - ```bash - npm run lint --if-present - npm run type-check --if-present - ``` - -4. **Status Updates** (handled by post-hook): - - Entity status changes to `IMPLEMENTED` - - Timestamp recorded - -## Code Templates - -### Component (Frontend) -```tsx -import React from 'react'; - -interface [Name]Props { - // From manifest.props -} - -export const [Name]: React.FC<[Name]Props> = (props) => { - return ( - // Implementation - ); -}; +# Verify we're in IMPLEMENTING phase ``` -### API Endpoint (Backend) +### 2. Find Task Context + +Read the task file and context for the entity you're implementing: + +``` +.workflow/versions/v001/tasks/task_create_.yml +.workflow/versions/v001/contexts/.yml +``` + +### 3. Load Generated Types (MANDATORY) + +**ALWAYS** import and use the generated types: + ```typescript -import { Request, Response } from 'express'; +// For components - import from generated types +import type { SongCardProps } from '@/types/component-props'; +import type { Song, Album, Artist } from '@/types'; -export async function handler(req: Request, res: Response) { - // From manifest.request/response schemas +// For APIs - import request/response types +import type { GetSongResponse } from '@/types/api-types'; +``` + +### 4. Read Design Specification + +The context file contains the EXACT specification you must follow: +- `target.definition.props` - Component props (use these EXACTLY) +- `target.definition.events` - Event handlers (implement ALL) +- `target.definition.fields` - Model fields (match EXACTLY) +- `acceptance` - Acceptance criteria to satisfy + +## Implementation Rules + +### Components + +```tsx +'use client' + +// STEP 1: Import generated types (REQUIRED) +import type { [ComponentName]Props } from '@/types/component-props'; +import type { Song, Album, Artist } from '@/types'; + +// STEP 2: Use the imported interface (DO NOT redefine props) +export function [ComponentName]({ + // Destructure props matching the design spec EXACTLY +}: [ComponentName]Props) { + + // STEP 3: Implement all events from the design + + return ( + // JSX implementation + ); } ``` + +**❌ WRONG - Do not flatten object props:** +```tsx +interface SongCardProps { + id: string; + title: string; + artistName: string; // WRONG! +} +``` + +**✅ CORRECT - Use typed object props from design:** +```tsx +import type { SongCardProps } from '@/types/component-props'; +// Props are: { song: Song, showArtist?: boolean, showAlbum?: boolean } +``` + +### API Routes (Next.js App Router) + +```typescript +import { NextRequest, NextResponse } from 'next/server'; +import type { [EndpointName]Request, [EndpointName]Response } from '@/types/api-types'; + +export async function [METHOD](request: NextRequest) { + // Implement according to design spec + // - Validate request body against schema + // - Return response matching schema +} +``` + +### Prisma Models + +Match the design document field names and types exactly: +```prisma +model [ModelName] { + // Fields from design_document.yml data_models section + // Match: name, type, constraints +} +``` + +## Post-Implementation Validation + +After implementing, run validation: + +```bash +# Type check +npx tsc --noEmit + +# Run implementation validator +python3 skills/guardrail-orchestrator/scripts/workflow_manager.py validate + +# Update task status +python3 skills/guardrail-orchestrator/scripts/workflow_manager.py task completed +``` + +## Example: Implementing SongCard + +1. **Read context:** + ```bash + cat .workflow/versions/v001/contexts/component_song_card.yml + ``` + +2. **Check generated types:** + ```bash + cat types/component-props.ts | grep -A 10 "SongCardProps" + ``` + +3. **Implement matching design:** + ```tsx + 'use client' + + import type { SongCardProps } from '@/types/component-props'; + import type { Song } from '@/types'; + + export function SongCard({ + song, // Song object, not flat props! + showArtist = true, + showAlbum = false, + onPlay, + onAddToPlaylist + }: SongCardProps) { + return ( +
onPlay?.({ songId: song.id })}> + {song.title} +

{song.title}

+ {showArtist &&

{song.artist?.name}

} + {showAlbum &&

{song.album?.title}

} +
+ ); + } + ``` + +4. **Validate:** + ```bash + python3 skills/guardrail-orchestrator/scripts/workflow_manager.py validate + ``` + +## Checklist Reference Update + +After successful implementation, mark the item as checked: + +```bash +python3 skills/guardrail-orchestrator/scripts/workflow_manager.py checklist check \ + --item component_song_card \ + --reference "components/SongCard.tsx:1-50" +``` diff --git a/.claude/commands/guardrail/validate.md b/.claude/commands/guardrail/validate.md index 4467e50..0608f88 100644 --- a/.claude/commands/guardrail/validate.md +++ b/.claude/commands/guardrail/validate.md @@ -1,29 +1,119 @@ --- -description: Validate manifest integrity and completeness -allowed-tools: Bash, Read +description: Validate implementation against design document +allowed-tools: Read, Bash, Glob, Grep --- -# Validate Manifest +# Validate Implementation -Run validation checks on `project_manifest.json`. +Validate: "$ARGUMENTS" -## Command +## Purpose + +Check that implementations match the design document specifications exactly. + +## Validation Steps + +### 1. Run Implementation Validator ```bash -python3 "$CLAUDE_PROJECT_DIR/skills/guardrail-orchestrator/scripts/validate_manifest.py" $ARGUMENTS +python3 skills/guardrail-orchestrator/scripts/workflow_manager.py validate --checklist ``` -## Options +### 2. Review Errors -- No arguments: Basic validation -- `--strict`: Treat warnings as errors +The validator checks: +- **Components**: Props match design, events implemented +- **APIs**: Routes exist, methods implemented, schemas match +- **Models**: Prisma fields match design -## What It Checks +### 3. Fix Issues -1. **Structure**: Required top-level keys exist -2. **Pages**: Have paths, components, file_paths -3. **Components**: Have props with types, valid dependencies -4. **APIs**: Have methods, paths, request/response schemas -5. **Database**: Tables have primary keys, valid foreign keys -6. **Dependencies**: No orphans, no circular references -7. **Naming**: Follows conventions +For each error, check: + +1. **Read the design spec:** + ```bash + cat .workflow/versions/v001/contexts/.yml + ``` + +2. **Check generated types:** + ```bash + cat types/component-props.ts | grep -A 20 "Props" + ``` + +3. **Fix the implementation** to match the design + +4. **Re-validate:** + ```bash + python3 skills/guardrail-orchestrator/scripts/workflow_manager.py validate + ``` + +### 4. View Checklist + +```bash +# View markdown checklist +cat .workflow/versions/v001/implementation_checklist.md + +# Or use the workflow command +python3 skills/guardrail-orchestrator/scripts/workflow_manager.py checklist show +``` + +## Common Issues + +### Props Don't Match Design + +**Problem:** Component uses flat props instead of typed objects + +```tsx +// ❌ Wrong +interface SongCardProps { + id: string; + title: string; +} + +// ✅ Correct - from generated types +import type { SongCardProps } from '@/types/component-props'; +// SongCardProps = { song: Song; showArtist?: boolean; } +``` + +### Missing Events + +**Problem:** Design specifies events not implemented + +```tsx +// Design specifies: onPlay, onAddToPlaylist +// Implementation only has: onPlay + +// Fix: Add missing event +export function SongCard({ song, onPlay, onAddToPlaylist }: SongCardProps) { + // Implement onAddToPlaylist handler +} +``` + +### API Route Missing + +**Problem:** API route file doesn't exist + +``` +Expected: app/api/songs/[id]/route.ts +Actual: (not found) +``` + +Fix: Create the route file following the design spec. + +## Validation Report Symbols + +- ✅ **Passed** - Implementation matches design +- ⚠️ **Warning** - Minor issue (e.g., optional field missing) +- ❌ **Error** - Critical mismatch with design + +## Post-Validation + +After all errors are fixed: + +```bash +# Verify clean validation +python3 skills/guardrail-orchestrator/scripts/workflow_manager.py validate + +# If passed, transition to REVIEWING phase +python3 skills/guardrail-orchestrator/scripts/workflow_manager.py transition REVIEWING +``` diff --git a/.claude/commands/guardrail/workflow.md b/.claude/commands/guardrail/workflow.md new file mode 100644 index 0000000..91ee7bd --- /dev/null +++ b/.claude/commands/guardrail/workflow.md @@ -0,0 +1,227 @@ +--- +description: Orchestrate the full implementation workflow +allowed-tools: Read, Write, Edit, Bash, Glob, Grep, Task +--- + +# Workflow Orchestrator + +Command: "$ARGUMENTS" + +## Workflow Phases + +``` +INITIALIZING → DESIGNING → AWAITING_DESIGN_APPROVAL → DESIGN_APPROVED + ↓ ↓ + [create] [generate-types] + ↓ + IMPLEMENTING + ↓ + [validate --checklist] + ↓ + REVIEWING + ↓ + SECURITY_REVIEW + ↓ + AWAITING_IMPL_APPROVAL + ↓ + COMPLETED +``` + +## Commands + +### Start New Workflow +```bash +/guardrail:workflow start "feature description" +``` + +### Check Status +```bash +/guardrail:workflow status +``` + +### Generate Types (after design approval) +```bash +/guardrail:workflow generate-types +``` + +### Implement Task +```bash +/guardrail:workflow implement +# or implement all Layer 1 tasks: +/guardrail:workflow implement --layer 1 +``` + +### Validate +```bash +/guardrail:workflow validate +``` + +### View Checklist +```bash +/guardrail:workflow checklist +``` + +--- + +## Execution Flow + +### Phase: DESIGN_APPROVED → IMPLEMENTING + +When design is approved, ALWAYS run these steps first: + +1. **Generate TypeScript types from design:** + ```bash + python3 skills/guardrail-orchestrator/scripts/workflow_manager.py generate-types --output-dir types + ``` + +2. **Transition to IMPLEMENTING:** + ```bash + python3 skills/guardrail-orchestrator/scripts/workflow_manager.py transition IMPLEMENTING + ``` + +3. **View dependency graph to understand task order:** + ```bash + cat .workflow/versions/v001/dependency_graph.yml + ``` + +### Phase: IMPLEMENTING + +For each task (respecting dependency layers): + +1. **Read task context:** + ```bash + cat .workflow/versions/v001/contexts/.yml + ``` + +2. **Read generated types:** + ```bash + cat types/component-props.ts # For components + cat types/types.ts # For models + cat types/api-types.ts # For APIs + ``` + +3. **Implement following the design EXACTLY** + - Import types from `@/types` + - Use typed props (not flat props) + - Implement all events + - Match field names exactly + +4. **Update task status:** + ```bash + python3 skills/guardrail-orchestrator/scripts/workflow_manager.py task completed + ``` + +5. **Run validation after each layer:** + ```bash + python3 skills/guardrail-orchestrator/scripts/workflow_manager.py validate --checklist + ``` + +### Phase: REVIEWING + +1. **Run full validation:** + ```bash + python3 skills/guardrail-orchestrator/scripts/workflow_manager.py validate --checklist + ``` + +2. **Fix any remaining issues** + +3. **Run type check:** + ```bash + npx tsc --noEmit + ``` + +4. **Run linter:** + ```bash + npm run lint + ``` + +5. **Transition to security review:** + ```bash + python3 skills/guardrail-orchestrator/scripts/workflow_manager.py transition SECURITY_REVIEW + ``` + +--- + +## Implementation Rules (MANDATORY) + +### Rule 1: Always Import Generated Types + +```typescript +// ✅ CORRECT +import type { SongCardProps } from '@/types/component-props'; +import type { Song } from '@/types'; + +// ❌ WRONG - defining own interface +interface SongCardProps { + id: string; + title: string; +} +``` + +### Rule 2: Use Object Props, Not Flat Props + +```typescript +// ✅ CORRECT - design says: song: Song +function SongCard({ song, showArtist }: SongCardProps) { + return
{song.title} by {song.artist?.name}
; +} + +// ❌ WRONG - flattening the Song object +function SongCard({ id, title, artistName }: SongCardProps) { + return
{title} by {artistName}
; +} +``` + +### Rule 3: Implement ALL Events from Design + +```typescript +// Design specifies: onPlay, onAddToPlaylist +// ✅ CORRECT - both events implemented +function SongCard({ song, onPlay, onAddToPlaylist }: SongCardProps) { + return ( +
+ + +
+ ); +} +``` + +### Rule 4: Match Prisma Schema to Design + +```prisma +// Design says: role enum with values [musician, listener, label] +model User { + role UserRole // ✅ Use enum +} + +enum UserRole { + musician + listener + label +} +``` + +### Rule 5: Validate After Each Implementation + +```bash +# After implementing, always validate +python3 skills/guardrail-orchestrator/scripts/workflow_manager.py validate + +# Mark task complete only if validation passes +python3 skills/guardrail-orchestrator/scripts/workflow_manager.py task completed +``` + +--- + +## Quick Reference + +| Phase | Action | Command | +|-------|--------|---------| +| Start | Create workflow | `version_manager.py create "feature"` | +| Design | Validate design | `validate_design.py` | +| Approved | Generate types | `workflow_manager.py generate-types` | +| Implement | Start task | Read context → Import types → Code | +| Validate | Check impl | `workflow_manager.py validate --checklist` | +| Review | Fix issues | Check errors → Fix → Re-validate | +| Complete | Archive | `workflow_manager.py transition COMPLETED` | diff --git a/.claude/eureka-factory.yaml b/.claude/eureka-factory.yaml index 4dca46f..d73c213 100644 --- a/.claude/eureka-factory.yaml +++ b/.claude/eureka-factory.yaml @@ -1,4 +1,5 @@ api_key: pk_user_8d080a1a699dc2a1769ca99ded0ca39fa80324b8713cf55ea7fecc1c372379a6 project_id: "" repo_id: "" +environment: development app_id: cmjb04ana0001qp0tijyy9emq diff --git a/.eureka-active-session b/.eureka-active-session new file mode 100644 index 0000000..c594acb --- /dev/null +++ b/.eureka-active-session @@ -0,0 +1,8 @@ +{ + "taskId": "workflow_v001_implementation", + "workflowVersion": "v001", + "phase": "IMPLEMENTING", + "feature": "a platform where musician can upload their songs", + "startedAt": "2025-12-18T15:21:00Z", + "designApproved": true +} diff --git a/.gitignore b/.gitignore index 5ef6a52..9a6a064 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,8 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Eureka Factory credentials +.claude/eureka-factory.yaml +.claude/eureka-factory.yml +.claude/eureka-factory.json diff --git a/.workflow/index.yml b/.workflow/index.yml new file mode 100644 index 0000000..71baba8 --- /dev/null +++ b/.workflow/index.yml @@ -0,0 +1,31 @@ +versions: +- version: v001 + feature: a platform where musician can upload their songs + status: pending + started_at: '2025-12-18T14:52:33.788070' + completed_at: null + tasks_count: 0 + operations_count: 0 +- version: v002 + feature: add header and navigation link to each pages + status: completed + started_at: '2025-12-18T17:06:07.870800' + completed_at: '2025-12-18T17:13:46.853854' + tasks_count: 0 + operations_count: 0 +- version: v003 + feature: add label management system + status: completed + started_at: '2025-12-18T17:39:16.643117' + completed_at: '2025-12-18T17:52:33.852962' + tasks_count: 0 + operations_count: 0 +- version: v004 + feature: add share music system + status: completed + started_at: '2025-12-18T18:05:50.056169' + completed_at: '2025-12-18T18:24:29.951800' + tasks_count: 0 + operations_count: 0 +latest_version: v004 +total_versions: 4 diff --git a/.workflow/versions/v001/contexts/api_add_song_to_playlist.yml b/.workflow/versions/v001/contexts/api_add_song_to_playlist.yml new file mode 100644 index 0000000..da5993d --- /dev/null +++ b/.workflow/versions/v001/contexts/api_add_song_to_playlist.yml @@ -0,0 +1,154 @@ +task_id: task_create_api_add_song_to_playlist +entity_id: api_add_song_to_playlist +generated_at: '2025-12-18T15:16:50.269977' +workflow_version: v001 +target: + type: api + definition: + id: api_add_song_to_playlist + method: POST + path: /api/playlists/:id/songs + description: Add song to playlist + request_body: + song_id: uuid + position: integer + responses: + - status: 201 + description: Song added to playlist + schema: + playlist_id: uuid + song_id: uuid + position: integer + auth: + required: true + owner_only: true + depends_on_models: + - model_playlist + - model_playlist_song +related: + models: + - id: model_playlist + definition: &id001 + id: model_playlist + name: Playlist + table_name: playlists + description: User-created song collection + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: name + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: is_public + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_playlist_song + foreign_key: playlist_id + indexes: + - fields: + - user_id + - fields: + - is_public + timestamps: true + - id: model_playlist_song + definition: &id002 + id: model_playlist_song + name: PlaylistSong + table_name: playlist_songs + description: Junction table with ordering for playlists + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: playlist_id + type: uuid + constraints: + - not_null + - foreign_key + references: playlists.id + - name: song_id + type: uuid + constraints: + - not_null + - foreign_key + references: songs.id + - name: position + type: integer + constraints: + - not_null + - name: added_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_playlist + foreign_key: playlist_id + - type: belongs_to + to: model_song + foreign_key: song_id + indexes: + - fields: + - playlist_id + - position + unique: true + - fields: + - playlist_id + - song_id + unique: true + timestamps: false + apis: [] + components: [] +dependencies: + entity_ids: + - model_playlist + - model_playlist_song + definitions: + - id: model_playlist + type: model + definition: *id001 + - id: model_playlist_song + type: model + definition: *id002 +files: + to_create: + - app/api/playlists/id/songs/route.ts + reference: [] +acceptance: +- criterion: POST /api/playlists/:id/songs returns success response + verification: curl -X POST /api/playlists/:id/songs +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_create_album.yml b/.workflow/versions/v001/contexts/api_create_album.yml new file mode 100644 index 0000000..41b76a4 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_create_album.yml @@ -0,0 +1,184 @@ +task_id: task_create_api_create_album +entity_id: api_create_album +generated_at: '2025-12-18T15:16:50.255667' +workflow_version: v001 +target: + type: api + definition: + id: api_create_album + method: POST + path: /api/albums + description: Create new album + request_body: + title: string + description: string + cover_art_url: string + release_date: string + album_type: enum[album, ep, single] + responses: + - status: 201 + description: Album created + schema: + id: uuid + title: string + cover_art_url: string + auth: + required: true + roles: + - musician + depends_on_models: + - model_album + - model_artist +related: + models: + - id: model_artist + definition: &id001 + id: model_artist + name: Artist + table_name: artists + description: Extended profile for musicians + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: stage_name + type: string + constraints: + - not_null + - name: bio + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: social_links + type: jsonb + description: JSON object with {twitter, instagram, facebook, website} + constraints: + - nullable + - name: verified + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_song + foreign_key: artist_id + - type: has_many + to: model_album + foreign_key: artist_id + - type: belongs_to + to: model_label + foreign_key: label_id + optional: true + indexes: + - fields: + - user_id + unique: true + - fields: + - stage_name + timestamps: true + - id: model_album + definition: &id002 + id: model_album + name: Album + table_name: albums + description: Collection of songs + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: title + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: album_type + type: enum + values: + - album + - ep + - single + default: album + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: has_many + to: model_song + foreign_key: album_id + indexes: + - fields: + - artist_id + - fields: + - release_date + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_artist + - model_album + definitions: + - id: model_artist + type: model + definition: *id001 + - id: model_album + type: model + definition: *id002 +files: + to_create: + - app/api/albums/route.ts + reference: [] +acceptance: +- criterion: POST /api/albums returns success response + verification: curl -X POST /api/albums +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_create_artist_profile.yml b/.workflow/versions/v001/contexts/api_create_artist_profile.yml new file mode 100644 index 0000000..d65a0d8 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_create_artist_profile.yml @@ -0,0 +1,197 @@ +task_id: task_create_api_create_artist_profile +entity_id: api_create_artist_profile +generated_at: '2025-12-18T15:16:50.234771' +workflow_version: v001 +target: + type: api + definition: + id: api_create_artist_profile + method: POST + path: /api/artists + description: Create artist profile (musicians only) + request_body: + stage_name: string + bio: string + cover_image_url: string + social_links: + twitter: string + instagram: string + facebook: string + website: string + responses: + - status: 201 + description: Artist profile created + schema: + id: uuid + stage_name: string + bio: string + cover_image_url: string + - status: 403 + description: User is not a musician + schema: + error: string + auth: + required: true + roles: + - musician + depends_on_models: + - model_artist + - model_user +related: + models: + - id: model_artist + definition: &id001 + id: model_artist + name: Artist + table_name: artists + description: Extended profile for musicians + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: stage_name + type: string + constraints: + - not_null + - name: bio + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: social_links + type: jsonb + description: JSON object with {twitter, instagram, facebook, website} + constraints: + - nullable + - name: verified + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_song + foreign_key: artist_id + - type: has_many + to: model_album + foreign_key: artist_id + - type: belongs_to + to: model_label + foreign_key: label_id + optional: true + indexes: + - fields: + - user_id + unique: true + - fields: + - stage_name + timestamps: true + - id: model_user + definition: &id002 + id: model_user + name: User + table_name: users + description: Base user entity with authentication + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: email + type: string + constraints: + - unique + - not_null + - name: password_hash + type: string + constraints: + - not_null + - name: name + type: string + constraints: + - not_null + - name: role + type: enum + values: + - musician + - listener + - label + constraints: + - not_null + - name: email_verified + type: boolean + default: false + - name: avatar_url + type: string + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: has_one + to: model_artist + foreign_key: user_id + condition: role = 'musician' + - type: has_one + to: model_label + foreign_key: user_id + condition: role = 'label' + - type: has_many + to: model_playlist + foreign_key: user_id + indexes: + - fields: + - email + unique: true + - fields: + - role + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_artist + - model_user + definitions: + - id: model_artist + type: model + definition: *id001 + - id: model_user + type: model + definition: *id002 +files: + to_create: + - app/api/artists/route.ts + reference: [] +acceptance: +- criterion: POST /api/artists returns success response + verification: curl -X POST /api/artists +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_create_label_profile.yml b/.workflow/versions/v001/contexts/api_create_label_profile.yml new file mode 100644 index 0000000..2160e3d --- /dev/null +++ b/.workflow/versions/v001/contexts/api_create_label_profile.yml @@ -0,0 +1,103 @@ +task_id: task_create_api_create_label_profile +entity_id: api_create_label_profile +generated_at: '2025-12-18T15:16:50.286319' +workflow_version: v001 +target: + type: api + definition: + id: api_create_label_profile + method: POST + path: /api/labels + description: Create label profile + request_body: + label_name: string + description: string + logo_url: string + website: string + responses: + - status: 201 + description: Label created + schema: + id: uuid + label_name: string + auth: + required: true + roles: + - label + depends_on_models: + - model_label +related: + models: + - id: model_label + definition: &id001 + id: model_label + name: Label + table_name: labels + description: Organization profile for labels + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: label_name + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: logo_url + type: string + constraints: + - nullable + - name: website + type: string + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_artist + foreign_key: label_id + indexes: + - fields: + - user_id + unique: true + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_label + definitions: + - id: model_label + type: model + definition: *id001 +files: + to_create: + - app/api/labels/route.ts + reference: [] +acceptance: +- criterion: POST /api/labels returns success response + verification: curl -X POST /api/labels +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_create_playlist.yml b/.workflow/versions/v001/contexts/api_create_playlist.yml new file mode 100644 index 0000000..b496e5d --- /dev/null +++ b/.workflow/versions/v001/contexts/api_create_playlist.yml @@ -0,0 +1,101 @@ +task_id: task_create_api_create_playlist +entity_id: api_create_playlist +generated_at: '2025-12-18T15:16:50.263480' +workflow_version: v001 +target: + type: api + definition: + id: api_create_playlist + method: POST + path: /api/playlists + description: Create new playlist + request_body: + name: string + description: string + is_public: boolean + responses: + - status: 201 + description: Playlist created + schema: + id: uuid + name: string + description: string + auth: + required: true + depends_on_models: + - model_playlist +related: + models: + - id: model_playlist + definition: &id001 + id: model_playlist + name: Playlist + table_name: playlists + description: User-created song collection + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: name + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: is_public + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_playlist_song + foreign_key: playlist_id + indexes: + - fields: + - user_id + - fields: + - is_public + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_playlist + definitions: + - id: model_playlist + type: model + definition: *id001 +files: + to_create: + - app/api/playlists/route.ts + reference: [] +acceptance: +- criterion: POST /api/playlists returns success response + verification: curl -X POST /api/playlists +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_delete_album.yml b/.workflow/versions/v001/contexts/api_delete_album.yml new file mode 100644 index 0000000..14915b6 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_delete_album.yml @@ -0,0 +1,106 @@ +task_id: task_create_api_delete_album +entity_id: api_delete_album +generated_at: '2025-12-18T15:16:50.262237' +workflow_version: v001 +target: + type: api + definition: + id: api_delete_album + method: DELETE + path: /api/albums/:id + description: Delete album + responses: + - status: 204 + description: Album deleted + - status: 403 + description: Unauthorized + schema: + error: string + auth: + required: true + owner_only: true + depends_on_models: + - model_album +related: + models: + - id: model_album + definition: &id001 + id: model_album + name: Album + table_name: albums + description: Collection of songs + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: title + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: album_type + type: enum + values: + - album + - ep + - single + default: album + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: has_many + to: model_song + foreign_key: album_id + indexes: + - fields: + - artist_id + - fields: + - release_date + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_album + definitions: + - id: model_album + type: model + definition: *id001 +files: + to_create: + - app/api/albums/id/route.ts + reference: [] +acceptance: +- criterion: DELETE /api/albums/:id returns success response + verification: curl -X DELETE /api/albums/:id +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_delete_playlist.yml b/.workflow/versions/v001/contexts/api_delete_playlist.yml new file mode 100644 index 0000000..a470a43 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_delete_playlist.yml @@ -0,0 +1,94 @@ +task_id: task_create_api_delete_playlist +entity_id: api_delete_playlist +generated_at: '2025-12-18T15:16:50.268842' +workflow_version: v001 +target: + type: api + definition: + id: api_delete_playlist + method: DELETE + path: /api/playlists/:id + description: Delete playlist + responses: + - status: 204 + description: Playlist deleted + auth: + required: true + owner_only: true + depends_on_models: + - model_playlist +related: + models: + - id: model_playlist + definition: &id001 + id: model_playlist + name: Playlist + table_name: playlists + description: User-created song collection + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: name + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: is_public + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_playlist_song + foreign_key: playlist_id + indexes: + - fields: + - user_id + - fields: + - is_public + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_playlist + definitions: + - id: model_playlist + type: model + definition: *id001 +files: + to_create: + - app/api/playlists/id/route.ts + reference: [] +acceptance: +- criterion: DELETE /api/playlists/:id returns success response + verification: curl -X DELETE /api/playlists/:id +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_delete_song.yml b/.workflow/versions/v001/contexts/api_delete_song.yml new file mode 100644 index 0000000..8b9cde2 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_delete_song.yml @@ -0,0 +1,153 @@ +task_id: task_create_api_delete_song +entity_id: api_delete_song +generated_at: '2025-12-18T15:16:50.252212' +workflow_version: v001 +target: + type: api + definition: + id: api_delete_song + method: DELETE + path: /api/songs/:id + description: Delete song + responses: + - status: 204 + description: Song deleted + - status: 403 + description: Unauthorized + schema: + error: string + auth: + required: true + owner_only: true + depends_on_models: + - model_song +related: + models: + - id: model_song + definition: &id001 + id: model_song + name: Song + table_name: songs + description: Audio track with metadata + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: album_id + type: uuid + constraints: + - nullable + - foreign_key + references: albums.id + - name: title + type: string + constraints: + - not_null + - name: duration + type: integer + description: Duration in seconds + constraints: + - not_null + - name: file_url + type: string + description: Cloud storage URL for audio file + constraints: + - not_null + - name: file_format + type: enum + values: + - mp3 + - wav + constraints: + - not_null + - name: file_size + type: integer + description: File size in bytes + constraints: + - not_null + - name: waveform_data + type: jsonb + description: Waveform visualization data + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: play_count + type: integer + default: 0 + - name: is_public + type: boolean + default: true + - name: track_number + type: integer + description: Position in album + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: belongs_to + to: model_album + foreign_key: album_id + optional: true + - type: has_many + to: model_genre + through: song_genres + foreign_key: song_id + - type: has_many + to: model_playlist_song + foreign_key: song_id + indexes: + - fields: + - artist_id + - fields: + - album_id + - fields: + - release_date + - fields: + - play_count + - fields: + - is_public + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_song + definitions: + - id: model_song + type: model + definition: *id001 +files: + to_create: + - app/api/songs/id/route.ts + reference: [] +acceptance: +- criterion: DELETE /api/songs/:id returns success response + verification: curl -X DELETE /api/songs/:id +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_forgot_password.yml b/.workflow/versions/v001/contexts/api_forgot_password.yml new file mode 100644 index 0000000..8de075a --- /dev/null +++ b/.workflow/versions/v001/contexts/api_forgot_password.yml @@ -0,0 +1,114 @@ +task_id: task_create_api_forgot_password +entity_id: api_forgot_password +generated_at: '2025-12-18T15:16:50.229463' +workflow_version: v001 +target: + type: api + definition: + id: api_forgot_password + method: POST + path: /api/auth/forgot-password + description: Request password reset email + request_body: + email: string + responses: + - status: 200 + description: Reset email sent + schema: + message: string + - status: 404 + description: Email not found + schema: + error: string + auth: + required: false + depends_on_models: + - model_user +related: + models: + - id: model_user + definition: &id001 + id: model_user + name: User + table_name: users + description: Base user entity with authentication + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: email + type: string + constraints: + - unique + - not_null + - name: password_hash + type: string + constraints: + - not_null + - name: name + type: string + constraints: + - not_null + - name: role + type: enum + values: + - musician + - listener + - label + constraints: + - not_null + - name: email_verified + type: boolean + default: false + - name: avatar_url + type: string + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: has_one + to: model_artist + foreign_key: user_id + condition: role = 'musician' + - type: has_one + to: model_label + foreign_key: user_id + condition: role = 'label' + - type: has_many + to: model_playlist + foreign_key: user_id + indexes: + - fields: + - email + unique: true + - fields: + - role + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_user + definitions: + - id: model_user + type: model + definition: *id001 +files: + to_create: + - app/api/auth/forgot-password/route.ts + reference: [] +acceptance: +- criterion: POST /api/auth/forgot-password returns success response + verification: curl -X POST /api/auth/forgot-password +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_get_album.yml b/.workflow/versions/v001/contexts/api_get_album.yml new file mode 100644 index 0000000..ee854bc --- /dev/null +++ b/.workflow/versions/v001/contexts/api_get_album.yml @@ -0,0 +1,299 @@ +task_id: task_create_api_get_album +entity_id: api_get_album +generated_at: '2025-12-18T15:16:50.257721' +workflow_version: v001 +target: + type: api + definition: + id: api_get_album + method: GET + path: /api/albums/:id + description: Get album details with songs + responses: + - status: 200 + description: Album details + schema: + id: uuid + title: string + description: string + cover_art_url: string + release_date: string + artist: + id: uuid + stage_name: string + songs: + - id: uuid + title: string + duration: integer + track_number: integer + auth: + required: false + depends_on_models: + - model_album + - model_song + - model_artist +related: + models: + - id: model_artist + definition: &id001 + id: model_artist + name: Artist + table_name: artists + description: Extended profile for musicians + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: stage_name + type: string + constraints: + - not_null + - name: bio + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: social_links + type: jsonb + description: JSON object with {twitter, instagram, facebook, website} + constraints: + - nullable + - name: verified + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_song + foreign_key: artist_id + - type: has_many + to: model_album + foreign_key: artist_id + - type: belongs_to + to: model_label + foreign_key: label_id + optional: true + indexes: + - fields: + - user_id + unique: true + - fields: + - stage_name + timestamps: true + - id: model_album + definition: &id002 + id: model_album + name: Album + table_name: albums + description: Collection of songs + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: title + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: album_type + type: enum + values: + - album + - ep + - single + default: album + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: has_many + to: model_song + foreign_key: album_id + indexes: + - fields: + - artist_id + - fields: + - release_date + timestamps: true + - id: model_song + definition: &id003 + id: model_song + name: Song + table_name: songs + description: Audio track with metadata + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: album_id + type: uuid + constraints: + - nullable + - foreign_key + references: albums.id + - name: title + type: string + constraints: + - not_null + - name: duration + type: integer + description: Duration in seconds + constraints: + - not_null + - name: file_url + type: string + description: Cloud storage URL for audio file + constraints: + - not_null + - name: file_format + type: enum + values: + - mp3 + - wav + constraints: + - not_null + - name: file_size + type: integer + description: File size in bytes + constraints: + - not_null + - name: waveform_data + type: jsonb + description: Waveform visualization data + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: play_count + type: integer + default: 0 + - name: is_public + type: boolean + default: true + - name: track_number + type: integer + description: Position in album + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: belongs_to + to: model_album + foreign_key: album_id + optional: true + - type: has_many + to: model_genre + through: song_genres + foreign_key: song_id + - type: has_many + to: model_playlist_song + foreign_key: song_id + indexes: + - fields: + - artist_id + - fields: + - album_id + - fields: + - release_date + - fields: + - play_count + - fields: + - is_public + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_artist + - model_album + - model_song + definitions: + - id: model_artist + type: model + definition: *id001 + - id: model_album + type: model + definition: *id002 + - id: model_song + type: model + definition: *id003 +files: + to_create: + - app/api/albums/id/route.ts + reference: [] +acceptance: +- criterion: GET /api/albums/:id returns success response + verification: curl -X GET /api/albums/:id +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_get_artist.yml b/.workflow/versions/v001/contexts/api_get_artist.yml new file mode 100644 index 0000000..b69ddfd --- /dev/null +++ b/.workflow/versions/v001/contexts/api_get_artist.yml @@ -0,0 +1,117 @@ +task_id: task_create_api_get_artist +entity_id: api_get_artist +generated_at: '2025-12-18T15:16:50.237033' +workflow_version: v001 +target: + type: api + definition: + id: api_get_artist + method: GET + path: /api/artists/:id + description: Get artist profile by ID + responses: + - status: 200 + description: Artist profile + schema: + id: uuid + stage_name: string + bio: string + cover_image_url: string + social_links: object + verified: boolean + - status: 404 + description: Artist not found + schema: + error: string + auth: + required: false + depends_on_models: + - model_artist +related: + models: + - id: model_artist + definition: &id001 + id: model_artist + name: Artist + table_name: artists + description: Extended profile for musicians + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: stage_name + type: string + constraints: + - not_null + - name: bio + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: social_links + type: jsonb + description: JSON object with {twitter, instagram, facebook, website} + constraints: + - nullable + - name: verified + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_song + foreign_key: artist_id + - type: has_many + to: model_album + foreign_key: artist_id + - type: belongs_to + to: model_label + foreign_key: label_id + optional: true + indexes: + - fields: + - user_id + unique: true + - fields: + - stage_name + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_artist + definitions: + - id: model_artist + type: model + definition: *id001 +files: + to_create: + - app/api/artists/id/route.ts + reference: [] +acceptance: +- criterion: GET /api/artists/:id returns success response + verification: curl -X GET /api/artists/:id +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_get_artist_albums.yml b/.workflow/versions/v001/contexts/api_get_artist_albums.yml new file mode 100644 index 0000000..cadff73 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_get_artist_albums.yml @@ -0,0 +1,179 @@ +task_id: task_create_api_get_artist_albums +entity_id: api_get_artist_albums +generated_at: '2025-12-18T15:16:50.242502' +workflow_version: v001 +target: + type: api + definition: + id: api_get_artist_albums + method: GET + path: /api/artists/:id/albums + description: Get all albums by artist + responses: + - status: 200 + description: List of albums + schema: + albums: + - id: uuid + title: string + cover_art_url: string + release_date: string + album_type: string + auth: + required: false + depends_on_models: + - model_artist + - model_album +related: + models: + - id: model_artist + definition: &id001 + id: model_artist + name: Artist + table_name: artists + description: Extended profile for musicians + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: stage_name + type: string + constraints: + - not_null + - name: bio + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: social_links + type: jsonb + description: JSON object with {twitter, instagram, facebook, website} + constraints: + - nullable + - name: verified + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_song + foreign_key: artist_id + - type: has_many + to: model_album + foreign_key: artist_id + - type: belongs_to + to: model_label + foreign_key: label_id + optional: true + indexes: + - fields: + - user_id + unique: true + - fields: + - stage_name + timestamps: true + - id: model_album + definition: &id002 + id: model_album + name: Album + table_name: albums + description: Collection of songs + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: title + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: album_type + type: enum + values: + - album + - ep + - single + default: album + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: has_many + to: model_song + foreign_key: album_id + indexes: + - fields: + - artist_id + - fields: + - release_date + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_artist + - model_album + definitions: + - id: model_artist + type: model + definition: *id001 + - id: model_album + type: model + definition: *id002 +files: + to_create: + - app/api/artists/id/albums/route.ts + reference: [] +acceptance: +- criterion: GET /api/artists/:id/albums returns success response + verification: curl -X GET /api/artists/:id/albums +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_get_artist_songs.yml b/.workflow/versions/v001/contexts/api_get_artist_songs.yml new file mode 100644 index 0000000..82205fb --- /dev/null +++ b/.workflow/versions/v001/contexts/api_get_artist_songs.yml @@ -0,0 +1,226 @@ +task_id: task_create_api_get_artist_songs +entity_id: api_get_artist_songs +generated_at: '2025-12-18T15:16:50.239966' +workflow_version: v001 +target: + type: api + definition: + id: api_get_artist_songs + method: GET + path: /api/artists/:id/songs + description: Get all songs by artist + responses: + - status: 200 + description: List of songs + schema: + songs: + - id: uuid + title: string + duration: integer + cover_art_url: string + play_count: integer + auth: + required: false + depends_on_models: + - model_artist + - model_song +related: + models: + - id: model_artist + definition: &id001 + id: model_artist + name: Artist + table_name: artists + description: Extended profile for musicians + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: stage_name + type: string + constraints: + - not_null + - name: bio + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: social_links + type: jsonb + description: JSON object with {twitter, instagram, facebook, website} + constraints: + - nullable + - name: verified + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_song + foreign_key: artist_id + - type: has_many + to: model_album + foreign_key: artist_id + - type: belongs_to + to: model_label + foreign_key: label_id + optional: true + indexes: + - fields: + - user_id + unique: true + - fields: + - stage_name + timestamps: true + - id: model_song + definition: &id002 + id: model_song + name: Song + table_name: songs + description: Audio track with metadata + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: album_id + type: uuid + constraints: + - nullable + - foreign_key + references: albums.id + - name: title + type: string + constraints: + - not_null + - name: duration + type: integer + description: Duration in seconds + constraints: + - not_null + - name: file_url + type: string + description: Cloud storage URL for audio file + constraints: + - not_null + - name: file_format + type: enum + values: + - mp3 + - wav + constraints: + - not_null + - name: file_size + type: integer + description: File size in bytes + constraints: + - not_null + - name: waveform_data + type: jsonb + description: Waveform visualization data + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: play_count + type: integer + default: 0 + - name: is_public + type: boolean + default: true + - name: track_number + type: integer + description: Position in album + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: belongs_to + to: model_album + foreign_key: album_id + optional: true + - type: has_many + to: model_genre + through: song_genres + foreign_key: song_id + - type: has_many + to: model_playlist_song + foreign_key: song_id + indexes: + - fields: + - artist_id + - fields: + - album_id + - fields: + - release_date + - fields: + - play_count + - fields: + - is_public + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_artist + - model_song + definitions: + - id: model_artist + type: model + definition: *id001 + - id: model_song + type: model + definition: *id002 +files: + to_create: + - app/api/artists/id/songs/route.ts + reference: [] +acceptance: +- criterion: GET /api/artists/:id/songs returns success response + verification: curl -X GET /api/artists/:id/songs +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_get_current_user.yml b/.workflow/versions/v001/contexts/api_get_current_user.yml new file mode 100644 index 0000000..c3b5d31 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_get_current_user.yml @@ -0,0 +1,112 @@ +task_id: task_create_api_get_current_user +entity_id: api_get_current_user +generated_at: '2025-12-18T15:16:50.232153' +workflow_version: v001 +target: + type: api + definition: + id: api_get_current_user + method: GET + path: /api/users/me + description: Get current user profile + responses: + - status: 200 + description: User profile + schema: + id: uuid + email: string + name: string + role: string + avatar_url: string + auth: + required: true + depends_on_models: + - model_user +related: + models: + - id: model_user + definition: &id001 + id: model_user + name: User + table_name: users + description: Base user entity with authentication + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: email + type: string + constraints: + - unique + - not_null + - name: password_hash + type: string + constraints: + - not_null + - name: name + type: string + constraints: + - not_null + - name: role + type: enum + values: + - musician + - listener + - label + constraints: + - not_null + - name: email_verified + type: boolean + default: false + - name: avatar_url + type: string + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: has_one + to: model_artist + foreign_key: user_id + condition: role = 'musician' + - type: has_one + to: model_label + foreign_key: user_id + condition: role = 'label' + - type: has_many + to: model_playlist + foreign_key: user_id + indexes: + - fields: + - email + unique: true + - fields: + - role + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_user + definitions: + - id: model_user + type: model + definition: *id001 +files: + to_create: + - app/api/users/me/route.ts + reference: [] +acceptance: +- criterion: GET /api/users/me returns success response + verification: curl -X GET /api/users/me +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_get_genres.yml b/.workflow/versions/v001/contexts/api_get_genres.yml new file mode 100644 index 0000000..35db8ae --- /dev/null +++ b/.workflow/versions/v001/contexts/api_get_genres.yml @@ -0,0 +1,88 @@ +task_id: task_create_api_get_genres +entity_id: api_get_genres +generated_at: '2025-12-18T15:16:50.279777' +workflow_version: v001 +target: + type: api + definition: + id: api_get_genres + method: GET + path: /api/discover/genres + description: Get all genres + responses: + - status: 200 + description: List of genres + schema: + genres: + - id: uuid + name: string + slug: string + auth: + required: false + depends_on_models: + - model_genre +related: + models: + - id: model_genre + definition: &id001 + id: model_genre + name: Genre + table_name: genres + description: Music category for discovery + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: name + type: string + constraints: + - unique + - not_null + - name: slug + type: string + constraints: + - unique + - not_null + - name: description + type: text + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: has_many + to: model_song + through: song_genres + foreign_key: genre_id + indexes: + - fields: + - slug + unique: true + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_genre + definitions: + - id: model_genre + type: model + definition: *id001 +files: + to_create: + - app/api/discover/genres/route.ts + reference: [] +acceptance: +- criterion: GET /api/discover/genres returns success response + verification: curl -X GET /api/discover/genres +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_get_label_artists.yml b/.workflow/versions/v001/contexts/api_get_label_artists.yml new file mode 100644 index 0000000..6479189 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_get_label_artists.yml @@ -0,0 +1,168 @@ +task_id: task_create_api_get_label_artists +entity_id: api_get_label_artists +generated_at: '2025-12-18T15:16:50.287533' +workflow_version: v001 +target: + type: api + definition: + id: api_get_label_artists + method: GET + path: /api/labels/:id/artists + description: Get artists under label + responses: + - status: 200 + description: List of artists + schema: + artists: + - id: uuid + stage_name: string + auth: + required: false + depends_on_models: + - model_label + - model_artist +related: + models: + - id: model_artist + definition: &id001 + id: model_artist + name: Artist + table_name: artists + description: Extended profile for musicians + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: stage_name + type: string + constraints: + - not_null + - name: bio + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: social_links + type: jsonb + description: JSON object with {twitter, instagram, facebook, website} + constraints: + - nullable + - name: verified + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_song + foreign_key: artist_id + - type: has_many + to: model_album + foreign_key: artist_id + - type: belongs_to + to: model_label + foreign_key: label_id + optional: true + indexes: + - fields: + - user_id + unique: true + - fields: + - stage_name + timestamps: true + - id: model_label + definition: &id002 + id: model_label + name: Label + table_name: labels + description: Organization profile for labels + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: label_name + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: logo_url + type: string + constraints: + - nullable + - name: website + type: string + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_artist + foreign_key: label_id + indexes: + - fields: + - user_id + unique: true + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_artist + - model_label + definitions: + - id: model_artist + type: model + definition: *id001 + - id: model_label + type: model + definition: *id002 +files: + to_create: + - app/api/labels/id/artists/route.ts + reference: [] +acceptance: +- criterion: GET /api/labels/:id/artists returns success response + verification: curl -X GET /api/labels/:id/artists +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_get_new_releases.yml b/.workflow/versions/v001/contexts/api_get_new_releases.yml new file mode 100644 index 0000000..fd9d128 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_get_new_releases.yml @@ -0,0 +1,156 @@ +task_id: task_create_api_get_new_releases +entity_id: api_get_new_releases +generated_at: '2025-12-18T15:16:50.277976' +workflow_version: v001 +target: + type: api + definition: + id: api_get_new_releases + method: GET + path: /api/discover/new-releases + description: Get recently released songs + query_params: + limit: integer + offset: integer + responses: + - status: 200 + description: List of new releases + schema: + songs: + - id: uuid + title: string + release_date: string + auth: + required: false + depends_on_models: + - model_song +related: + models: + - id: model_song + definition: &id001 + id: model_song + name: Song + table_name: songs + description: Audio track with metadata + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: album_id + type: uuid + constraints: + - nullable + - foreign_key + references: albums.id + - name: title + type: string + constraints: + - not_null + - name: duration + type: integer + description: Duration in seconds + constraints: + - not_null + - name: file_url + type: string + description: Cloud storage URL for audio file + constraints: + - not_null + - name: file_format + type: enum + values: + - mp3 + - wav + constraints: + - not_null + - name: file_size + type: integer + description: File size in bytes + constraints: + - not_null + - name: waveform_data + type: jsonb + description: Waveform visualization data + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: play_count + type: integer + default: 0 + - name: is_public + type: boolean + default: true + - name: track_number + type: integer + description: Position in album + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: belongs_to + to: model_album + foreign_key: album_id + optional: true + - type: has_many + to: model_genre + through: song_genres + foreign_key: song_id + - type: has_many + to: model_playlist_song + foreign_key: song_id + indexes: + - fields: + - artist_id + - fields: + - album_id + - fields: + - release_date + - fields: + - play_count + - fields: + - is_public + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_song + definitions: + - id: model_song + type: model + definition: *id001 +files: + to_create: + - app/api/discover/new-releases/route.ts + reference: [] +acceptance: +- criterion: GET /api/discover/new-releases returns success response + verification: curl -X GET /api/discover/new-releases +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_get_playlist.yml b/.workflow/versions/v001/contexts/api_get_playlist.yml new file mode 100644 index 0000000..5de0cf0 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_get_playlist.yml @@ -0,0 +1,156 @@ +task_id: task_create_api_get_playlist +entity_id: api_get_playlist +generated_at: '2025-12-18T15:16:50.265868' +workflow_version: v001 +target: + type: api + definition: + id: api_get_playlist + method: GET + path: /api/playlists/:id + description: Get playlist details with songs + responses: + - status: 200 + description: Playlist details + schema: + id: uuid + name: string + description: string + songs: + - id: uuid + title: string + artist: + stage_name: string + position: integer + auth: + required: false + depends_on_models: + - model_playlist + - model_playlist_song +related: + models: + - id: model_playlist + definition: &id001 + id: model_playlist + name: Playlist + table_name: playlists + description: User-created song collection + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: name + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: is_public + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_playlist_song + foreign_key: playlist_id + indexes: + - fields: + - user_id + - fields: + - is_public + timestamps: true + - id: model_playlist_song + definition: &id002 + id: model_playlist_song + name: PlaylistSong + table_name: playlist_songs + description: Junction table with ordering for playlists + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: playlist_id + type: uuid + constraints: + - not_null + - foreign_key + references: playlists.id + - name: song_id + type: uuid + constraints: + - not_null + - foreign_key + references: songs.id + - name: position + type: integer + constraints: + - not_null + - name: added_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_playlist + foreign_key: playlist_id + - type: belongs_to + to: model_song + foreign_key: song_id + indexes: + - fields: + - playlist_id + - position + unique: true + - fields: + - playlist_id + - song_id + unique: true + timestamps: false + apis: [] + components: [] +dependencies: + entity_ids: + - model_playlist + - model_playlist_song + definitions: + - id: model_playlist + type: model + definition: *id001 + - id: model_playlist_song + type: model + definition: *id002 +files: + to_create: + - app/api/playlists/id/route.ts + reference: [] +acceptance: +- criterion: GET /api/playlists/:id returns success response + verification: curl -X GET /api/playlists/:id +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_get_song.yml b/.workflow/versions/v001/contexts/api_get_song.yml new file mode 100644 index 0000000..1f27a66 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_get_song.yml @@ -0,0 +1,299 @@ +task_id: task_create_api_get_song +entity_id: api_get_song +generated_at: '2025-12-18T15:16:50.247175' +workflow_version: v001 +target: + type: api + definition: + id: api_get_song + method: GET + path: /api/songs/:id + description: Get song details + responses: + - status: 200 + description: Song details + schema: + id: uuid + title: string + duration: integer + file_url: string + cover_art_url: string + artist: + id: uuid + stage_name: string + album: + id: uuid + title: string + genres: array + play_count: integer + auth: + required: false + depends_on_models: + - model_song + - model_artist + - model_album +related: + models: + - id: model_artist + definition: &id001 + id: model_artist + name: Artist + table_name: artists + description: Extended profile for musicians + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: stage_name + type: string + constraints: + - not_null + - name: bio + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: social_links + type: jsonb + description: JSON object with {twitter, instagram, facebook, website} + constraints: + - nullable + - name: verified + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_song + foreign_key: artist_id + - type: has_many + to: model_album + foreign_key: artist_id + - type: belongs_to + to: model_label + foreign_key: label_id + optional: true + indexes: + - fields: + - user_id + unique: true + - fields: + - stage_name + timestamps: true + - id: model_album + definition: &id002 + id: model_album + name: Album + table_name: albums + description: Collection of songs + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: title + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: album_type + type: enum + values: + - album + - ep + - single + default: album + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: has_many + to: model_song + foreign_key: album_id + indexes: + - fields: + - artist_id + - fields: + - release_date + timestamps: true + - id: model_song + definition: &id003 + id: model_song + name: Song + table_name: songs + description: Audio track with metadata + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: album_id + type: uuid + constraints: + - nullable + - foreign_key + references: albums.id + - name: title + type: string + constraints: + - not_null + - name: duration + type: integer + description: Duration in seconds + constraints: + - not_null + - name: file_url + type: string + description: Cloud storage URL for audio file + constraints: + - not_null + - name: file_format + type: enum + values: + - mp3 + - wav + constraints: + - not_null + - name: file_size + type: integer + description: File size in bytes + constraints: + - not_null + - name: waveform_data + type: jsonb + description: Waveform visualization data + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: play_count + type: integer + default: 0 + - name: is_public + type: boolean + default: true + - name: track_number + type: integer + description: Position in album + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: belongs_to + to: model_album + foreign_key: album_id + optional: true + - type: has_many + to: model_genre + through: song_genres + foreign_key: song_id + - type: has_many + to: model_playlist_song + foreign_key: song_id + indexes: + - fields: + - artist_id + - fields: + - album_id + - fields: + - release_date + - fields: + - play_count + - fields: + - is_public + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_artist + - model_album + - model_song + definitions: + - id: model_artist + type: model + definition: *id001 + - id: model_album + type: model + definition: *id002 + - id: model_song + type: model + definition: *id003 +files: + to_create: + - app/api/songs/id/route.ts + reference: [] +acceptance: +- criterion: GET /api/songs/:id returns success response + verification: curl -X GET /api/songs/:id +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_get_songs_by_genre.yml b/.workflow/versions/v001/contexts/api_get_songs_by_genre.yml new file mode 100644 index 0000000..1a16f11 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_get_songs_by_genre.yml @@ -0,0 +1,203 @@ +task_id: task_create_api_get_songs_by_genre +entity_id: api_get_songs_by_genre +generated_at: '2025-12-18T15:16:50.280833' +workflow_version: v001 +target: + type: api + definition: + id: api_get_songs_by_genre + method: GET + path: /api/discover/genres/:slug + description: Get songs by genre + query_params: + limit: integer + offset: integer + responses: + - status: 200 + description: List of songs in genre + schema: + genre: + name: string + songs: array + auth: + required: false + depends_on_models: + - model_genre + - model_song +related: + models: + - id: model_song + definition: &id001 + id: model_song + name: Song + table_name: songs + description: Audio track with metadata + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: album_id + type: uuid + constraints: + - nullable + - foreign_key + references: albums.id + - name: title + type: string + constraints: + - not_null + - name: duration + type: integer + description: Duration in seconds + constraints: + - not_null + - name: file_url + type: string + description: Cloud storage URL for audio file + constraints: + - not_null + - name: file_format + type: enum + values: + - mp3 + - wav + constraints: + - not_null + - name: file_size + type: integer + description: File size in bytes + constraints: + - not_null + - name: waveform_data + type: jsonb + description: Waveform visualization data + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: play_count + type: integer + default: 0 + - name: is_public + type: boolean + default: true + - name: track_number + type: integer + description: Position in album + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: belongs_to + to: model_album + foreign_key: album_id + optional: true + - type: has_many + to: model_genre + through: song_genres + foreign_key: song_id + - type: has_many + to: model_playlist_song + foreign_key: song_id + indexes: + - fields: + - artist_id + - fields: + - album_id + - fields: + - release_date + - fields: + - play_count + - fields: + - is_public + timestamps: true + - id: model_genre + definition: &id002 + id: model_genre + name: Genre + table_name: genres + description: Music category for discovery + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: name + type: string + constraints: + - unique + - not_null + - name: slug + type: string + constraints: + - unique + - not_null + - name: description + type: text + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: has_many + to: model_song + through: song_genres + foreign_key: genre_id + indexes: + - fields: + - slug + unique: true + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_song + - model_genre + definitions: + - id: model_song + type: model + definition: *id001 + - id: model_genre + type: model + definition: *id002 +files: + to_create: + - app/api/discover/genres/slug/route.ts + reference: [] +acceptance: +- criterion: GET /api/discover/genres/:slug returns success response + verification: curl -X GET /api/discover/genres/:slug +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_get_trending_songs.yml b/.workflow/versions/v001/contexts/api_get_trending_songs.yml new file mode 100644 index 0000000..1e0f4c5 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_get_trending_songs.yml @@ -0,0 +1,229 @@ +task_id: task_create_api_get_trending_songs +entity_id: api_get_trending_songs +generated_at: '2025-12-18T15:16:50.275382' +workflow_version: v001 +target: + type: api + definition: + id: api_get_trending_songs + method: GET + path: /api/discover/trending + description: Get trending songs + query_params: + limit: integer + offset: integer + responses: + - status: 200 + description: List of trending songs + schema: + songs: + - id: uuid + title: string + artist: + stage_name: string + play_count: integer + auth: + required: false + depends_on_models: + - model_song + - model_artist +related: + models: + - id: model_artist + definition: &id001 + id: model_artist + name: Artist + table_name: artists + description: Extended profile for musicians + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: stage_name + type: string + constraints: + - not_null + - name: bio + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: social_links + type: jsonb + description: JSON object with {twitter, instagram, facebook, website} + constraints: + - nullable + - name: verified + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_song + foreign_key: artist_id + - type: has_many + to: model_album + foreign_key: artist_id + - type: belongs_to + to: model_label + foreign_key: label_id + optional: true + indexes: + - fields: + - user_id + unique: true + - fields: + - stage_name + timestamps: true + - id: model_song + definition: &id002 + id: model_song + name: Song + table_name: songs + description: Audio track with metadata + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: album_id + type: uuid + constraints: + - nullable + - foreign_key + references: albums.id + - name: title + type: string + constraints: + - not_null + - name: duration + type: integer + description: Duration in seconds + constraints: + - not_null + - name: file_url + type: string + description: Cloud storage URL for audio file + constraints: + - not_null + - name: file_format + type: enum + values: + - mp3 + - wav + constraints: + - not_null + - name: file_size + type: integer + description: File size in bytes + constraints: + - not_null + - name: waveform_data + type: jsonb + description: Waveform visualization data + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: play_count + type: integer + default: 0 + - name: is_public + type: boolean + default: true + - name: track_number + type: integer + description: Position in album + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: belongs_to + to: model_album + foreign_key: album_id + optional: true + - type: has_many + to: model_genre + through: song_genres + foreign_key: song_id + - type: has_many + to: model_playlist_song + foreign_key: song_id + indexes: + - fields: + - artist_id + - fields: + - album_id + - fields: + - release_date + - fields: + - play_count + - fields: + - is_public + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_artist + - model_song + definitions: + - id: model_artist + type: model + definition: *id001 + - id: model_song + type: model + definition: *id002 +files: + to_create: + - app/api/discover/trending/route.ts + reference: [] +acceptance: +- criterion: GET /api/discover/trending returns success response + verification: curl -X GET /api/discover/trending +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_get_user_playlists.yml b/.workflow/versions/v001/contexts/api_get_user_playlists.yml new file mode 100644 index 0000000..c3fa73c --- /dev/null +++ b/.workflow/versions/v001/contexts/api_get_user_playlists.yml @@ -0,0 +1,99 @@ +task_id: task_create_api_get_user_playlists +entity_id: api_get_user_playlists +generated_at: '2025-12-18T15:16:50.264680' +workflow_version: v001 +target: + type: api + definition: + id: api_get_user_playlists + method: GET + path: /api/playlists + description: Get current user's playlists + responses: + - status: 200 + description: List of playlists + schema: + playlists: + - id: uuid + name: string + cover_image_url: string + song_count: integer + auth: + required: true + depends_on_models: + - model_playlist +related: + models: + - id: model_playlist + definition: &id001 + id: model_playlist + name: Playlist + table_name: playlists + description: User-created song collection + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: name + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: is_public + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_playlist_song + foreign_key: playlist_id + indexes: + - fields: + - user_id + - fields: + - is_public + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_playlist + definitions: + - id: model_playlist + type: model + definition: *id001 +files: + to_create: + - app/api/playlists/route.ts + reference: [] +acceptance: +- criterion: GET /api/playlists returns success response + verification: curl -X GET /api/playlists +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_increment_play_count.yml b/.workflow/versions/v001/contexts/api_increment_play_count.yml new file mode 100644 index 0000000..ef66091 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_increment_play_count.yml @@ -0,0 +1,151 @@ +task_id: task_create_api_increment_play_count +entity_id: api_increment_play_count +generated_at: '2025-12-18T15:16:50.253941' +workflow_version: v001 +target: + type: api + definition: + id: api_increment_play_count + method: POST + path: /api/songs/:id/play + description: Increment play count + request_body: null + responses: + - status: 200 + description: Play count incremented + schema: + play_count: integer + auth: + required: false + depends_on_models: + - model_song +related: + models: + - id: model_song + definition: &id001 + id: model_song + name: Song + table_name: songs + description: Audio track with metadata + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: album_id + type: uuid + constraints: + - nullable + - foreign_key + references: albums.id + - name: title + type: string + constraints: + - not_null + - name: duration + type: integer + description: Duration in seconds + constraints: + - not_null + - name: file_url + type: string + description: Cloud storage URL for audio file + constraints: + - not_null + - name: file_format + type: enum + values: + - mp3 + - wav + constraints: + - not_null + - name: file_size + type: integer + description: File size in bytes + constraints: + - not_null + - name: waveform_data + type: jsonb + description: Waveform visualization data + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: play_count + type: integer + default: 0 + - name: is_public + type: boolean + default: true + - name: track_number + type: integer + description: Position in album + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: belongs_to + to: model_album + foreign_key: album_id + optional: true + - type: has_many + to: model_genre + through: song_genres + foreign_key: song_id + - type: has_many + to: model_playlist_song + foreign_key: song_id + indexes: + - fields: + - artist_id + - fields: + - album_id + - fields: + - release_date + - fields: + - play_count + - fields: + - is_public + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_song + definitions: + - id: model_song + type: model + definition: *id001 +files: + to_create: + - app/api/songs/id/play/route.ts + reference: [] +acceptance: +- criterion: POST /api/songs/:id/play returns success response + verification: curl -X POST /api/songs/:id/play +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_login.yml b/.workflow/versions/v001/contexts/api_login.yml new file mode 100644 index 0000000..fcdc653 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_login.yml @@ -0,0 +1,120 @@ +task_id: task_create_api_login +entity_id: api_login +generated_at: '2025-12-18T15:16:50.228086' +workflow_version: v001 +target: + type: api + definition: + id: api_login + method: POST + path: /api/auth/login + description: Login with email and password + request_body: + email: string + password: string + responses: + - status: 200 + description: Login successful + schema: + user: + id: uuid + email: string + name: string + role: string + token: string + - status: 401 + description: Invalid credentials + schema: + error: string + auth: + required: false + depends_on_models: + - model_user +related: + models: + - id: model_user + definition: &id001 + id: model_user + name: User + table_name: users + description: Base user entity with authentication + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: email + type: string + constraints: + - unique + - not_null + - name: password_hash + type: string + constraints: + - not_null + - name: name + type: string + constraints: + - not_null + - name: role + type: enum + values: + - musician + - listener + - label + constraints: + - not_null + - name: email_verified + type: boolean + default: false + - name: avatar_url + type: string + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: has_one + to: model_artist + foreign_key: user_id + condition: role = 'musician' + - type: has_one + to: model_label + foreign_key: user_id + condition: role = 'label' + - type: has_many + to: model_playlist + foreign_key: user_id + indexes: + - fields: + - email + unique: true + - fields: + - role + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_user + definitions: + - id: model_user + type: model + definition: *id001 +files: + to_create: + - app/api/auth/login/route.ts + reference: [] +acceptance: +- criterion: POST /api/auth/login returns success response + verification: curl -X POST /api/auth/login +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_register.yml b/.workflow/versions/v001/contexts/api_register.yml new file mode 100644 index 0000000..186763f --- /dev/null +++ b/.workflow/versions/v001/contexts/api_register.yml @@ -0,0 +1,126 @@ +task_id: task_create_api_register +entity_id: api_register +generated_at: '2025-12-18T15:16:50.226614' +workflow_version: v001 +target: + type: api + definition: + id: api_register + method: POST + path: /api/auth/register + description: Register new user account + request_body: + email: string + password: string + name: string + role: enum[musician, listener, label] + responses: + - status: 201 + description: User created successfully + schema: + user: + id: uuid + email: string + name: string + role: string + token: string + - status: 400 + description: Validation error + schema: + error: string + - status: 409 + description: Email already exists + schema: + error: string + auth: + required: false + depends_on_models: + - model_user +related: + models: + - id: model_user + definition: &id001 + id: model_user + name: User + table_name: users + description: Base user entity with authentication + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: email + type: string + constraints: + - unique + - not_null + - name: password_hash + type: string + constraints: + - not_null + - name: name + type: string + constraints: + - not_null + - name: role + type: enum + values: + - musician + - listener + - label + constraints: + - not_null + - name: email_verified + type: boolean + default: false + - name: avatar_url + type: string + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: has_one + to: model_artist + foreign_key: user_id + condition: role = 'musician' + - type: has_one + to: model_label + foreign_key: user_id + condition: role = 'label' + - type: has_many + to: model_playlist + foreign_key: user_id + indexes: + - fields: + - email + unique: true + - fields: + - role + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_user + definitions: + - id: model_user + type: model + definition: *id001 +files: + to_create: + - app/api/auth/register/route.ts + reference: [] +acceptance: +- criterion: POST /api/auth/register returns success response + verification: curl -X POST /api/auth/register +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_remove_song_from_playlist.yml b/.workflow/versions/v001/contexts/api_remove_song_from_playlist.yml new file mode 100644 index 0000000..1894e2b --- /dev/null +++ b/.workflow/versions/v001/contexts/api_remove_song_from_playlist.yml @@ -0,0 +1,147 @@ +task_id: task_create_api_remove_song_from_playlist +entity_id: api_remove_song_from_playlist +generated_at: '2025-12-18T15:16:50.271730' +workflow_version: v001 +target: + type: api + definition: + id: api_remove_song_from_playlist + method: DELETE + path: /api/playlists/:playlistId/songs/:songId + description: Remove song from playlist + responses: + - status: 204 + description: Song removed from playlist + auth: + required: true + owner_only: true + depends_on_models: + - model_playlist + - model_playlist_song +related: + models: + - id: model_playlist + definition: &id001 + id: model_playlist + name: Playlist + table_name: playlists + description: User-created song collection + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: name + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: is_public + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_playlist_song + foreign_key: playlist_id + indexes: + - fields: + - user_id + - fields: + - is_public + timestamps: true + - id: model_playlist_song + definition: &id002 + id: model_playlist_song + name: PlaylistSong + table_name: playlist_songs + description: Junction table with ordering for playlists + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: playlist_id + type: uuid + constraints: + - not_null + - foreign_key + references: playlists.id + - name: song_id + type: uuid + constraints: + - not_null + - foreign_key + references: songs.id + - name: position + type: integer + constraints: + - not_null + - name: added_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_playlist + foreign_key: playlist_id + - type: belongs_to + to: model_song + foreign_key: song_id + indexes: + - fields: + - playlist_id + - position + unique: true + - fields: + - playlist_id + - song_id + unique: true + timestamps: false + apis: [] + components: [] +dependencies: + entity_ids: + - model_playlist + - model_playlist_song + definitions: + - id: model_playlist + type: model + definition: *id001 + - id: model_playlist_song + type: model + definition: *id002 +files: + to_create: + - app/api/playlists/playlistId/songs/songId/route.ts + reference: [] +acceptance: +- criterion: DELETE /api/playlists/:playlistId/songs/:songId returns success response + verification: curl -X DELETE /api/playlists/:playlistId/songs/:songId +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_reorder_playlist_songs.yml b/.workflow/versions/v001/contexts/api_reorder_playlist_songs.yml new file mode 100644 index 0000000..1c551e5 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_reorder_playlist_songs.yml @@ -0,0 +1,151 @@ +task_id: task_create_api_reorder_playlist_songs +entity_id: api_reorder_playlist_songs +generated_at: '2025-12-18T15:16:50.273600' +workflow_version: v001 +target: + type: api + definition: + id: api_reorder_playlist_songs + method: PUT + path: /api/playlists/:id/reorder + description: Reorder songs in playlist + request_body: + song_ids: array[uuid] + responses: + - status: 200 + description: Playlist reordered + schema: + message: string + auth: + required: true + owner_only: true + depends_on_models: + - model_playlist + - model_playlist_song +related: + models: + - id: model_playlist + definition: &id001 + id: model_playlist + name: Playlist + table_name: playlists + description: User-created song collection + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: name + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: is_public + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_playlist_song + foreign_key: playlist_id + indexes: + - fields: + - user_id + - fields: + - is_public + timestamps: true + - id: model_playlist_song + definition: &id002 + id: model_playlist_song + name: PlaylistSong + table_name: playlist_songs + description: Junction table with ordering for playlists + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: playlist_id + type: uuid + constraints: + - not_null + - foreign_key + references: playlists.id + - name: song_id + type: uuid + constraints: + - not_null + - foreign_key + references: songs.id + - name: position + type: integer + constraints: + - not_null + - name: added_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_playlist + foreign_key: playlist_id + - type: belongs_to + to: model_song + foreign_key: song_id + indexes: + - fields: + - playlist_id + - position + unique: true + - fields: + - playlist_id + - song_id + unique: true + timestamps: false + apis: [] + components: [] +dependencies: + entity_ids: + - model_playlist + - model_playlist_song + definitions: + - id: model_playlist + type: model + definition: *id001 + - id: model_playlist_song + type: model + definition: *id002 +files: + to_create: + - app/api/playlists/id/reorder/route.ts + reference: [] +acceptance: +- criterion: PUT /api/playlists/:id/reorder returns success response + verification: curl -X PUT /api/playlists/:id/reorder +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_reset_password.yml b/.workflow/versions/v001/contexts/api_reset_password.yml new file mode 100644 index 0000000..d1152e5 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_reset_password.yml @@ -0,0 +1,115 @@ +task_id: task_create_api_reset_password +entity_id: api_reset_password +generated_at: '2025-12-18T15:16:50.230797' +workflow_version: v001 +target: + type: api + definition: + id: api_reset_password + method: POST + path: /api/auth/reset-password + description: Reset password with token + request_body: + token: string + password: string + responses: + - status: 200 + description: Password reset successful + schema: + message: string + - status: 400 + description: Invalid or expired token + schema: + error: string + auth: + required: false + depends_on_models: + - model_user +related: + models: + - id: model_user + definition: &id001 + id: model_user + name: User + table_name: users + description: Base user entity with authentication + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: email + type: string + constraints: + - unique + - not_null + - name: password_hash + type: string + constraints: + - not_null + - name: name + type: string + constraints: + - not_null + - name: role + type: enum + values: + - musician + - listener + - label + constraints: + - not_null + - name: email_verified + type: boolean + default: false + - name: avatar_url + type: string + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: has_one + to: model_artist + foreign_key: user_id + condition: role = 'musician' + - type: has_one + to: model_label + foreign_key: user_id + condition: role = 'label' + - type: has_many + to: model_playlist + foreign_key: user_id + indexes: + - fields: + - email + unique: true + - fields: + - role + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_user + definitions: + - id: model_user + type: model + definition: *id001 +files: + to_create: + - app/api/auth/reset-password/route.ts + reference: [] +acceptance: +- criterion: POST /api/auth/reset-password returns success response + verification: curl -X POST /api/auth/reset-password +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_search.yml b/.workflow/versions/v001/contexts/api_search.yml new file mode 100644 index 0000000..e056b66 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_search.yml @@ -0,0 +1,293 @@ +task_id: task_create_api_search +entity_id: api_search +generated_at: '2025-12-18T15:16:50.283107' +workflow_version: v001 +target: + type: api + definition: + id: api_search + method: GET + path: /api/search + description: Search songs, artists, and albums + query_params: + q: string + type: enum[song, artist, album, all] + limit: integer + responses: + - status: 200 + description: Search results + schema: + songs: array + artists: array + albums: array + auth: + required: false + depends_on_models: + - model_song + - model_artist + - model_album +related: + models: + - id: model_artist + definition: &id001 + id: model_artist + name: Artist + table_name: artists + description: Extended profile for musicians + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: stage_name + type: string + constraints: + - not_null + - name: bio + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: social_links + type: jsonb + description: JSON object with {twitter, instagram, facebook, website} + constraints: + - nullable + - name: verified + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_song + foreign_key: artist_id + - type: has_many + to: model_album + foreign_key: artist_id + - type: belongs_to + to: model_label + foreign_key: label_id + optional: true + indexes: + - fields: + - user_id + unique: true + - fields: + - stage_name + timestamps: true + - id: model_album + definition: &id002 + id: model_album + name: Album + table_name: albums + description: Collection of songs + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: title + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: album_type + type: enum + values: + - album + - ep + - single + default: album + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: has_many + to: model_song + foreign_key: album_id + indexes: + - fields: + - artist_id + - fields: + - release_date + timestamps: true + - id: model_song + definition: &id003 + id: model_song + name: Song + table_name: songs + description: Audio track with metadata + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: album_id + type: uuid + constraints: + - nullable + - foreign_key + references: albums.id + - name: title + type: string + constraints: + - not_null + - name: duration + type: integer + description: Duration in seconds + constraints: + - not_null + - name: file_url + type: string + description: Cloud storage URL for audio file + constraints: + - not_null + - name: file_format + type: enum + values: + - mp3 + - wav + constraints: + - not_null + - name: file_size + type: integer + description: File size in bytes + constraints: + - not_null + - name: waveform_data + type: jsonb + description: Waveform visualization data + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: play_count + type: integer + default: 0 + - name: is_public + type: boolean + default: true + - name: track_number + type: integer + description: Position in album + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: belongs_to + to: model_album + foreign_key: album_id + optional: true + - type: has_many + to: model_genre + through: song_genres + foreign_key: song_id + - type: has_many + to: model_playlist_song + foreign_key: song_id + indexes: + - fields: + - artist_id + - fields: + - album_id + - fields: + - release_date + - fields: + - play_count + - fields: + - is_public + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_artist + - model_album + - model_song + definitions: + - id: model_artist + type: model + definition: *id001 + - id: model_album + type: model + definition: *id002 + - id: model_song + type: model + definition: *id003 +files: + to_create: + - app/api/search/route.ts + reference: [] +acceptance: +- criterion: GET /api/search returns success response + verification: curl -X GET /api/search +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_update_album.yml b/.workflow/versions/v001/contexts/api_update_album.yml new file mode 100644 index 0000000..b248b40 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_update_album.yml @@ -0,0 +1,110 @@ +task_id: task_create_api_update_album +entity_id: api_update_album +generated_at: '2025-12-18T15:16:50.260977' +workflow_version: v001 +target: + type: api + definition: + id: api_update_album + method: PUT + path: /api/albums/:id + description: Update album metadata + request_body: + title: string + description: string + cover_art_url: string + release_date: string + responses: + - status: 200 + description: Album updated + schema: + id: uuid + title: string + auth: + required: true + owner_only: true + depends_on_models: + - model_album +related: + models: + - id: model_album + definition: &id001 + id: model_album + name: Album + table_name: albums + description: Collection of songs + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: title + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: album_type + type: enum + values: + - album + - ep + - single + default: album + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: has_many + to: model_song + foreign_key: album_id + indexes: + - fields: + - artist_id + - fields: + - release_date + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_album + definitions: + - id: model_album + type: model + definition: *id001 +files: + to_create: + - app/api/albums/id/route.ts + reference: [] +acceptance: +- criterion: PUT /api/albums/:id returns success response + verification: curl -X PUT /api/albums/:id +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_update_artist.yml b/.workflow/versions/v001/contexts/api_update_artist.yml new file mode 100644 index 0000000..579ecac --- /dev/null +++ b/.workflow/versions/v001/contexts/api_update_artist.yml @@ -0,0 +1,120 @@ +task_id: task_create_api_update_artist +entity_id: api_update_artist +generated_at: '2025-12-18T15:16:50.238503' +workflow_version: v001 +target: + type: api + definition: + id: api_update_artist + method: PUT + path: /api/artists/:id + description: Update artist profile + request_body: + stage_name: string + bio: string + cover_image_url: string + social_links: object + responses: + - status: 200 + description: Artist updated + schema: + id: uuid + stage_name: string + bio: string + - status: 403 + description: Unauthorized + schema: + error: string + auth: + required: true + owner_only: true + depends_on_models: + - model_artist +related: + models: + - id: model_artist + definition: &id001 + id: model_artist + name: Artist + table_name: artists + description: Extended profile for musicians + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: stage_name + type: string + constraints: + - not_null + - name: bio + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: social_links + type: jsonb + description: JSON object with {twitter, instagram, facebook, website} + constraints: + - nullable + - name: verified + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_song + foreign_key: artist_id + - type: has_many + to: model_album + foreign_key: artist_id + - type: belongs_to + to: model_label + foreign_key: label_id + optional: true + indexes: + - fields: + - user_id + unique: true + - fields: + - stage_name + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_artist + definitions: + - id: model_artist + type: model + definition: *id001 +files: + to_create: + - app/api/artists/id/route.ts + reference: [] +acceptance: +- criterion: PUT /api/artists/:id returns success response + verification: curl -X PUT /api/artists/:id +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_update_current_user.yml b/.workflow/versions/v001/contexts/api_update_current_user.yml new file mode 100644 index 0000000..e4ad2a4 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_update_current_user.yml @@ -0,0 +1,114 @@ +task_id: task_create_api_update_current_user +entity_id: api_update_current_user +generated_at: '2025-12-18T15:16:50.233456' +workflow_version: v001 +target: + type: api + definition: + id: api_update_current_user + method: PUT + path: /api/users/me + description: Update current user profile + request_body: + name: string + avatar_url: string + responses: + - status: 200 + description: User updated + schema: + id: uuid + email: string + name: string + avatar_url: string + auth: + required: true + depends_on_models: + - model_user +related: + models: + - id: model_user + definition: &id001 + id: model_user + name: User + table_name: users + description: Base user entity with authentication + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: email + type: string + constraints: + - unique + - not_null + - name: password_hash + type: string + constraints: + - not_null + - name: name + type: string + constraints: + - not_null + - name: role + type: enum + values: + - musician + - listener + - label + constraints: + - not_null + - name: email_verified + type: boolean + default: false + - name: avatar_url + type: string + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: has_one + to: model_artist + foreign_key: user_id + condition: role = 'musician' + - type: has_one + to: model_label + foreign_key: user_id + condition: role = 'label' + - type: has_many + to: model_playlist + foreign_key: user_id + indexes: + - fields: + - email + unique: true + - fields: + - role + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_user + definitions: + - id: model_user + type: model + definition: *id001 +files: + to_create: + - app/api/users/me/route.ts + reference: [] +acceptance: +- criterion: PUT /api/users/me returns success response + verification: curl -X PUT /api/users/me +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_update_playlist.yml b/.workflow/versions/v001/contexts/api_update_playlist.yml new file mode 100644 index 0000000..109c931 --- /dev/null +++ b/.workflow/versions/v001/contexts/api_update_playlist.yml @@ -0,0 +1,101 @@ +task_id: task_create_api_update_playlist +entity_id: api_update_playlist +generated_at: '2025-12-18T15:16:50.267642' +workflow_version: v001 +target: + type: api + definition: + id: api_update_playlist + method: PUT + path: /api/playlists/:id + description: Update playlist metadata + request_body: + name: string + description: string + is_public: boolean + responses: + - status: 200 + description: Playlist updated + schema: + id: uuid + name: string + auth: + required: true + owner_only: true + depends_on_models: + - model_playlist +related: + models: + - id: model_playlist + definition: &id001 + id: model_playlist + name: Playlist + table_name: playlists + description: User-created song collection + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: name + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: is_public + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_playlist_song + foreign_key: playlist_id + indexes: + - fields: + - user_id + - fields: + - is_public + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_playlist + definitions: + - id: model_playlist + type: model + definition: *id001 +files: + to_create: + - app/api/playlists/id/route.ts + reference: [] +acceptance: +- criterion: PUT /api/playlists/:id returns success response + verification: curl -X PUT /api/playlists/:id +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_update_song.yml b/.workflow/versions/v001/contexts/api_update_song.yml new file mode 100644 index 0000000..37ce30e --- /dev/null +++ b/.workflow/versions/v001/contexts/api_update_song.yml @@ -0,0 +1,162 @@ +task_id: task_create_api_update_song +entity_id: api_update_song +generated_at: '2025-12-18T15:16:50.250397' +workflow_version: v001 +target: + type: api + definition: + id: api_update_song + method: PUT + path: /api/songs/:id + description: Update song metadata + request_body: + title: string + album_id: uuid + genre_ids: array[uuid] + release_date: string + is_public: boolean + responses: + - status: 200 + description: Song updated + schema: + id: uuid + title: string + - status: 403 + description: Unauthorized + schema: + error: string + auth: + required: true + owner_only: true + depends_on_models: + - model_song +related: + models: + - id: model_song + definition: &id001 + id: model_song + name: Song + table_name: songs + description: Audio track with metadata + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: album_id + type: uuid + constraints: + - nullable + - foreign_key + references: albums.id + - name: title + type: string + constraints: + - not_null + - name: duration + type: integer + description: Duration in seconds + constraints: + - not_null + - name: file_url + type: string + description: Cloud storage URL for audio file + constraints: + - not_null + - name: file_format + type: enum + values: + - mp3 + - wav + constraints: + - not_null + - name: file_size + type: integer + description: File size in bytes + constraints: + - not_null + - name: waveform_data + type: jsonb + description: Waveform visualization data + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: play_count + type: integer + default: 0 + - name: is_public + type: boolean + default: true + - name: track_number + type: integer + description: Position in album + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: belongs_to + to: model_album + foreign_key: album_id + optional: true + - type: has_many + to: model_genre + through: song_genres + foreign_key: song_id + - type: has_many + to: model_playlist_song + foreign_key: song_id + indexes: + - fields: + - artist_id + - fields: + - album_id + - fields: + - release_date + - fields: + - play_count + - fields: + - is_public + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_song + definitions: + - id: model_song + type: model + definition: *id001 +files: + to_create: + - app/api/songs/id/route.ts + reference: [] +acceptance: +- criterion: PUT /api/songs/:id returns success response + verification: curl -X PUT /api/songs/:id +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/api_upload_song.yml b/.workflow/versions/v001/contexts/api_upload_song.yml new file mode 100644 index 0000000..50173df --- /dev/null +++ b/.workflow/versions/v001/contexts/api_upload_song.yml @@ -0,0 +1,241 @@ +task_id: task_create_api_upload_song +entity_id: api_upload_song +generated_at: '2025-12-18T15:16:50.244501' +workflow_version: v001 +target: + type: api + definition: + id: api_upload_song + method: POST + path: /api/songs/upload + description: Upload new song (musicians only) + request_body: + file: binary + title: string + album_id: uuid + genre_ids: array[uuid] + release_date: string + track_number: integer + responses: + - status: 201 + description: Song uploaded successfully + schema: + id: uuid + title: string + file_url: string + duration: integer + - status: 400 + description: Invalid file format or size + schema: + error: string + - status: 403 + description: User is not a musician + schema: + error: string + auth: + required: true + roles: + - musician + depends_on_models: + - model_song + - model_artist +related: + models: + - id: model_artist + definition: &id001 + id: model_artist + name: Artist + table_name: artists + description: Extended profile for musicians + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: stage_name + type: string + constraints: + - not_null + - name: bio + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: social_links + type: jsonb + description: JSON object with {twitter, instagram, facebook, website} + constraints: + - nullable + - name: verified + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_song + foreign_key: artist_id + - type: has_many + to: model_album + foreign_key: artist_id + - type: belongs_to + to: model_label + foreign_key: label_id + optional: true + indexes: + - fields: + - user_id + unique: true + - fields: + - stage_name + timestamps: true + - id: model_song + definition: &id002 + id: model_song + name: Song + table_name: songs + description: Audio track with metadata + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: album_id + type: uuid + constraints: + - nullable + - foreign_key + references: albums.id + - name: title + type: string + constraints: + - not_null + - name: duration + type: integer + description: Duration in seconds + constraints: + - not_null + - name: file_url + type: string + description: Cloud storage URL for audio file + constraints: + - not_null + - name: file_format + type: enum + values: + - mp3 + - wav + constraints: + - not_null + - name: file_size + type: integer + description: File size in bytes + constraints: + - not_null + - name: waveform_data + type: jsonb + description: Waveform visualization data + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: play_count + type: integer + default: 0 + - name: is_public + type: boolean + default: true + - name: track_number + type: integer + description: Position in album + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: belongs_to + to: model_album + foreign_key: album_id + optional: true + - type: has_many + to: model_genre + through: song_genres + foreign_key: song_id + - type: has_many + to: model_playlist_song + foreign_key: song_id + indexes: + - fields: + - artist_id + - fields: + - album_id + - fields: + - release_date + - fields: + - play_count + - fields: + - is_public + timestamps: true + apis: [] + components: [] +dependencies: + entity_ids: + - model_artist + - model_song + definitions: + - id: model_artist + type: model + definition: *id001 + - id: model_song + type: model + definition: *id002 +files: + to_create: + - app/api/songs/upload/route.ts + reference: [] +acceptance: +- criterion: POST /api/songs/upload returns success response + verification: curl -X POST /api/songs/upload +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v001/contexts/component_album_card.yml b/.workflow/versions/v001/contexts/component_album_card.yml new file mode 100644 index 0000000..bb6377a --- /dev/null +++ b/.workflow/versions/v001/contexts/component_album_card.yml @@ -0,0 +1,41 @@ +task_id: task_create_component_album_card +entity_id: component_album_card +generated_at: '2025-12-18T15:16:50.313541' +workflow_version: v001 +target: + type: component + definition: + id: component_album_card + name: AlbumCard + description: Album display card + props: + - name: album + type: Album + required: true + - name: showArtist + type: boolean + default: true + events: + - name: onClick + payload: + albumId: string + uses_apis: [] + uses_components: [] +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/AlbumCard.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_album_header.yml b/.workflow/versions/v001/contexts/component_album_header.yml new file mode 100644 index 0000000..a6bb47a --- /dev/null +++ b/.workflow/versions/v001/contexts/component_album_header.yml @@ -0,0 +1,40 @@ +task_id: task_create_component_album_header +entity_id: component_album_header +generated_at: '2025-12-18T15:16:50.319775' +workflow_version: v001 +target: + type: component + definition: + id: component_album_header + name: AlbumHeader + description: Album detail header with cover art + props: + - name: album + type: Album + required: true + - name: artist + type: Artist + required: true + events: + - name: onPlayAll + payload: null + uses_apis: [] + uses_components: [] +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/AlbumHeader.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_artist_card.yml b/.workflow/versions/v001/contexts/component_artist_card.yml new file mode 100644 index 0000000..6374f29 --- /dev/null +++ b/.workflow/versions/v001/contexts/component_artist_card.yml @@ -0,0 +1,38 @@ +task_id: task_create_component_artist_card +entity_id: component_artist_card +generated_at: '2025-12-18T15:16:50.314121' +workflow_version: v001 +target: + type: component + definition: + id: component_artist_card + name: ArtistCard + description: Artist preview card + props: + - name: artist + type: Artist + required: true + events: + - name: onClick + payload: + artistId: string + uses_apis: [] + uses_components: [] +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/ArtistCard.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_artist_header.yml b/.workflow/versions/v001/contexts/component_artist_header.yml new file mode 100644 index 0000000..ce2aeb3 --- /dev/null +++ b/.workflow/versions/v001/contexts/component_artist_header.yml @@ -0,0 +1,52 @@ +task_id: task_create_component_artist_header +entity_id: component_artist_header +generated_at: '2025-12-18T15:16:50.319067' +workflow_version: v001 +target: + type: component + definition: + id: component_artist_header + name: ArtistHeader + description: Artist profile header with cover image + props: + - name: artist + type: Artist + required: true + events: [] + uses_apis: [] + uses_components: + - component_social_links +related: + models: [] + apis: [] + components: + - id: component_social_links + definition: &id001 + id: component_social_links + name: SocialLinks + description: Social media links display + props: + - name: links + type: object + required: true + events: [] + uses_apis: [] + uses_components: [] +dependencies: + entity_ids: + - component_social_links + definitions: + - id: component_social_links + type: component + definition: *id001 +files: + to_create: + - app/components/ArtistHeader.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_audio_player.yml b/.workflow/versions/v001/contexts/component_audio_player.yml new file mode 100644 index 0000000..62cca66 --- /dev/null +++ b/.workflow/versions/v001/contexts/component_audio_player.yml @@ -0,0 +1,148 @@ +task_id: task_create_component_audio_player +entity_id: component_audio_player +generated_at: '2025-12-18T15:16:50.310377' +workflow_version: v001 +target: + type: component + definition: + id: component_audio_player + name: AudioPlayer + description: Global audio player with full controls + props: + - name: currentSong + type: Song + required: false + - name: queue + type: array[Song] + required: false + - name: autoplay + type: boolean + default: false + state: + - name: isPlaying + type: boolean + - name: currentTime + type: number + - name: volume + type: number + - name: isShuffle + type: boolean + - name: repeatMode + type: enum[off, one, all] + events: + - name: onPlay + payload: + songId: string + - name: onPause + payload: null + - name: onSeek + payload: + time: number + - name: onVolumeChange + payload: + volume: number + - name: onNext + payload: null + - name: onPrevious + payload: null + - name: onShuffle + payload: null + - name: onRepeat + payload: null + uses_apis: + - api_increment_play_count + uses_components: + - component_waveform_display + - component_player_controls +related: + models: [] + apis: + - id: api_increment_play_count + definition: &id002 + id: api_increment_play_count + method: POST + path: /api/songs/:id/play + description: Increment play count + request_body: null + responses: + - status: 200 + description: Play count incremented + schema: + play_count: integer + auth: + required: false + depends_on_models: + - model_song + components: + - id: component_waveform_display + definition: &id001 + id: component_waveform_display + name: WaveformDisplay + description: Audio waveform visualization + props: + - name: audioUrl + type: string + required: true + - name: waveformData + type: array[number] + required: false + - name: currentTime + type: number + required: false + events: + - name: onSeek + payload: + time: number + uses_apis: [] + uses_components: [] + - id: component_player_controls + definition: &id003 + id: component_player_controls + name: PlayerControls + description: Play/pause/seek controls + props: + - name: isPlaying + type: boolean + required: true + - name: currentTime + type: number + required: true + - name: duration + type: number + required: true + events: + - name: onPlay + payload: null + - name: onPause + payload: null + - name: onSeek + payload: + time: number + uses_apis: [] + uses_components: [] +dependencies: + entity_ids: + - component_waveform_display + - api_increment_play_count + - component_player_controls + definitions: + - id: component_waveform_display + type: component + definition: *id001 + - id: api_increment_play_count + type: api + definition: *id002 + - id: component_player_controls + type: component + definition: *id003 +files: + to_create: + - app/components/AudioPlayer.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_auth_form.yml b/.workflow/versions/v001/contexts/component_auth_form.yml new file mode 100644 index 0000000..adbbcc5 --- /dev/null +++ b/.workflow/versions/v001/contexts/component_auth_form.yml @@ -0,0 +1,142 @@ +task_id: task_create_component_auth_form +entity_id: component_auth_form +generated_at: '2025-12-18T15:16:50.321537' +workflow_version: v001 +target: + type: component + definition: + id: component_auth_form + name: AuthForm + description: Reusable authentication form + props: + - name: mode + type: enum[login, register, forgot] + required: true + state: + - name: email + type: string + - name: password + type: string + - name: name + type: string + - name: role + type: string + events: + - name: onSubmit + payload: object + uses_apis: + - api_login + - api_register + - api_forgot_password + uses_components: [] +related: + models: [] + apis: + - id: api_forgot_password + definition: &id001 + id: api_forgot_password + method: POST + path: /api/auth/forgot-password + description: Request password reset email + request_body: + email: string + responses: + - status: 200 + description: Reset email sent + schema: + message: string + - status: 404 + description: Email not found + schema: + error: string + auth: + required: false + depends_on_models: + - model_user + - id: api_register + definition: &id002 + id: api_register + method: POST + path: /api/auth/register + description: Register new user account + request_body: + email: string + password: string + name: string + role: enum[musician, listener, label] + responses: + - status: 201 + description: User created successfully + schema: + user: + id: uuid + email: string + name: string + role: string + token: string + - status: 400 + description: Validation error + schema: + error: string + - status: 409 + description: Email already exists + schema: + error: string + auth: + required: false + depends_on_models: + - model_user + - id: api_login + definition: &id003 + id: api_login + method: POST + path: /api/auth/login + description: Login with email and password + request_body: + email: string + password: string + responses: + - status: 200 + description: Login successful + schema: + user: + id: uuid + email: string + name: string + role: string + token: string + - status: 401 + description: Invalid credentials + schema: + error: string + auth: + required: false + depends_on_models: + - model_user + components: [] +dependencies: + entity_ids: + - api_forgot_password + - api_register + - api_login + definitions: + - id: api_forgot_password + type: api + definition: *id001 + - id: api_register + type: api + definition: *id002 + - id: api_login + type: api + definition: *id003 +files: + to_create: + - app/components/AuthForm.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_avatar_upload.yml b/.workflow/versions/v001/contexts/component_avatar_upload.yml new file mode 100644 index 0000000..86a9ff0 --- /dev/null +++ b/.workflow/versions/v001/contexts/component_avatar_upload.yml @@ -0,0 +1,43 @@ +task_id: task_create_component_avatar_upload +entity_id: component_avatar_upload +generated_at: '2025-12-18T15:16:50.327669' +workflow_version: v001 +target: + type: component + definition: + id: component_avatar_upload + name: AvatarUpload + description: Avatar image upload component + props: + - name: currentAvatarUrl + type: string + required: false + state: + - name: file + type: File + - name: preview + type: string + events: + - name: onUpload + payload: + file: File + uses_apis: [] + uses_components: [] +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/AvatarUpload.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_create_playlist_modal.yml b/.workflow/versions/v001/contexts/component_create_playlist_modal.yml new file mode 100644 index 0000000..5b18890 --- /dev/null +++ b/.workflow/versions/v001/contexts/component_create_playlist_modal.yml @@ -0,0 +1,75 @@ +task_id: task_create_component_create_playlist_modal +entity_id: component_create_playlist_modal +generated_at: '2025-12-18T15:16:50.325506' +workflow_version: v001 +target: + type: component + definition: + id: component_create_playlist_modal + name: CreatePlaylistModal + description: Modal for creating new playlist + props: + - name: isOpen + type: boolean + required: true + state: + - name: name + type: string + - name: description + type: string + - name: isPublic + type: boolean + events: + - name: onCreate + payload: + name: string + description: string + isPublic: boolean + - name: onClose + payload: null + uses_apis: + - api_create_playlist + uses_components: [] +related: + models: [] + apis: + - id: api_create_playlist + definition: &id001 + id: api_create_playlist + method: POST + path: /api/playlists + description: Create new playlist + request_body: + name: string + description: string + is_public: boolean + responses: + - status: 201 + description: Playlist created + schema: + id: uuid + name: string + description: string + auth: + required: true + depends_on_models: + - model_playlist + components: [] +dependencies: + entity_ids: + - api_create_playlist + definitions: + - id: api_create_playlist + type: api + definition: *id001 +files: + to_create: + - app/components/CreatePlaylistModal.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_genre_badge.yml b/.workflow/versions/v001/contexts/component_genre_badge.yml new file mode 100644 index 0000000..76d6eff --- /dev/null +++ b/.workflow/versions/v001/contexts/component_genre_badge.yml @@ -0,0 +1,41 @@ +task_id: task_create_component_genre_badge +entity_id: component_genre_badge +generated_at: '2025-12-18T15:16:50.317487' +workflow_version: v001 +target: + type: component + definition: + id: component_genre_badge + name: GenreBadge + description: Genre tag display + props: + - name: genre + type: Genre + required: true + - name: clickable + type: boolean + default: true + events: + - name: onClick + payload: + genreSlug: string + uses_apis: [] + uses_components: [] +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/GenreBadge.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_genre_header.yml b/.workflow/versions/v001/contexts/component_genre_header.yml new file mode 100644 index 0000000..8461fd0 --- /dev/null +++ b/.workflow/versions/v001/contexts/component_genre_header.yml @@ -0,0 +1,35 @@ +task_id: task_create_component_genre_header +entity_id: component_genre_header +generated_at: '2025-12-18T15:16:50.328870' +workflow_version: v001 +target: + type: component + definition: + id: component_genre_header + name: GenreHeader + description: Genre browse page header + props: + - name: genre + type: Genre + required: true + events: [] + uses_apis: [] + uses_components: [] +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/GenreHeader.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_player_controls.yml b/.workflow/versions/v001/contexts/component_player_controls.yml new file mode 100644 index 0000000..1e55814 --- /dev/null +++ b/.workflow/versions/v001/contexts/component_player_controls.yml @@ -0,0 +1,48 @@ +task_id: task_create_component_player_controls +entity_id: component_player_controls +generated_at: '2025-12-18T15:16:50.312220' +workflow_version: v001 +target: + type: component + definition: + id: component_player_controls + name: PlayerControls + description: Play/pause/seek controls + props: + - name: isPlaying + type: boolean + required: true + - name: currentTime + type: number + required: true + - name: duration + type: number + required: true + events: + - name: onPlay + payload: null + - name: onPause + payload: null + - name: onSeek + payload: + time: number + uses_apis: [] + uses_components: [] +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/PlayerControls.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_playlist_card.yml b/.workflow/versions/v001/contexts/component_playlist_card.yml new file mode 100644 index 0000000..ec65a1f --- /dev/null +++ b/.workflow/versions/v001/contexts/component_playlist_card.yml @@ -0,0 +1,38 @@ +task_id: task_create_component_playlist_card +entity_id: component_playlist_card +generated_at: '2025-12-18T15:16:50.314728' +workflow_version: v001 +target: + type: component + definition: + id: component_playlist_card + name: PlaylistCard + description: Playlist preview card + props: + - name: playlist + type: Playlist + required: true + events: + - name: onClick + payload: + playlistId: string + uses_apis: [] + uses_components: [] +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/PlaylistCard.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_playlist_header.yml b/.workflow/versions/v001/contexts/component_playlist_header.yml new file mode 100644 index 0000000..769e8aa --- /dev/null +++ b/.workflow/versions/v001/contexts/component_playlist_header.yml @@ -0,0 +1,44 @@ +task_id: task_create_component_playlist_header +entity_id: component_playlist_header +generated_at: '2025-12-18T15:16:50.320366' +workflow_version: v001 +target: + type: component + definition: + id: component_playlist_header + name: PlaylistHeader + description: Playlist header with cover and controls + props: + - name: playlist + type: Playlist + required: true + - name: isOwner + type: boolean + default: false + events: + - name: onPlayAll + payload: null + - name: onEdit + payload: null + - name: onDelete + payload: null + uses_apis: [] + uses_components: [] +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/PlaylistHeader.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_profile_form.yml b/.workflow/versions/v001/contexts/component_profile_form.yml new file mode 100644 index 0000000..bbb589e --- /dev/null +++ b/.workflow/versions/v001/contexts/component_profile_form.yml @@ -0,0 +1,95 @@ +task_id: task_create_component_profile_form +entity_id: component_profile_form +generated_at: '2025-12-18T15:16:50.326476' +workflow_version: v001 +target: + type: component + definition: + id: component_profile_form + name: ProfileForm + description: User profile edit form + props: + - name: user + type: User + required: true + state: + - name: name + type: string + - name: avatarUrl + type: string + events: + - name: onSave + payload: + name: string + avatarUrl: string + uses_apis: + - api_update_current_user + uses_components: + - component_avatar_upload +related: + models: [] + apis: + - id: api_update_current_user + definition: &id001 + id: api_update_current_user + method: PUT + path: /api/users/me + description: Update current user profile + request_body: + name: string + avatar_url: string + responses: + - status: 200 + description: User updated + schema: + id: uuid + email: string + name: string + avatar_url: string + auth: + required: true + depends_on_models: + - model_user + components: + - id: component_avatar_upload + definition: &id002 + id: component_avatar_upload + name: AvatarUpload + description: Avatar image upload component + props: + - name: currentAvatarUrl + type: string + required: false + state: + - name: file + type: File + - name: preview + type: string + events: + - name: onUpload + payload: + file: File + uses_apis: [] + uses_components: [] +dependencies: + entity_ids: + - api_update_current_user + - component_avatar_upload + definitions: + - id: api_update_current_user + type: api + definition: *id001 + - id: component_avatar_upload + type: component + definition: *id002 +files: + to_create: + - app/components/ProfileForm.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_search_bar.yml b/.workflow/versions/v001/contexts/component_search_bar.yml new file mode 100644 index 0000000..c93664a --- /dev/null +++ b/.workflow/versions/v001/contexts/component_search_bar.yml @@ -0,0 +1,69 @@ +task_id: task_create_component_search_bar +entity_id: component_search_bar +generated_at: '2025-12-18T15:16:50.323230' +workflow_version: v001 +target: + type: component + definition: + id: component_search_bar + name: SearchBar + description: Search input with autocomplete + props: + - name: placeholder + type: string + default: Search songs, artists, albums... + state: + - name: query + type: string + events: + - name: onSearch + payload: + query: string + uses_apis: + - api_search + uses_components: [] +related: + models: [] + apis: + - id: api_search + definition: &id001 + id: api_search + method: GET + path: /api/search + description: Search songs, artists, and albums + query_params: + q: string + type: enum[song, artist, album, all] + limit: integer + responses: + - status: 200 + description: Search results + schema: + songs: array + artists: array + albums: array + auth: + required: false + depends_on_models: + - model_song + - model_artist + - model_album + components: [] +dependencies: + entity_ids: + - api_search + definitions: + - id: api_search + type: api + definition: *id001 +files: + to_create: + - app/components/SearchBar.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_search_results.yml b/.workflow/versions/v001/contexts/component_search_results.yml new file mode 100644 index 0000000..2145f64 --- /dev/null +++ b/.workflow/versions/v001/contexts/component_search_results.yml @@ -0,0 +1,107 @@ +task_id: task_create_component_search_results +entity_id: component_search_results +generated_at: '2025-12-18T15:16:50.324165' +workflow_version: v001 +target: + type: component + definition: + id: component_search_results + name: SearchResults + description: Search results with filters + props: + - name: results + type: object + required: true + events: [] + uses_apis: [] + uses_components: + - component_song_card + - component_artist_card + - component_album_card +related: + models: [] + apis: [] + components: + - id: component_artist_card + definition: &id001 + id: component_artist_card + name: ArtistCard + description: Artist preview card + props: + - name: artist + type: Artist + required: true + events: + - name: onClick + payload: + artistId: string + uses_apis: [] + uses_components: [] + - id: component_album_card + definition: &id002 + id: component_album_card + name: AlbumCard + description: Album display card + props: + - name: album + type: Album + required: true + - name: showArtist + type: boolean + default: true + events: + - name: onClick + payload: + albumId: string + uses_apis: [] + uses_components: [] + - id: component_song_card + definition: &id003 + id: component_song_card + name: SongCard + description: Song display card with play button + props: + - name: song + type: Song + required: true + - name: showArtist + type: boolean + default: true + - name: showAlbum + type: boolean + default: false + events: + - name: onPlay + payload: + songId: string + - name: onAddToPlaylist + payload: + songId: string + uses_apis: [] + uses_components: [] +dependencies: + entity_ids: + - component_artist_card + - component_album_card + - component_song_card + definitions: + - id: component_artist_card + type: component + definition: *id001 + - id: component_album_card + type: component + definition: *id002 + - id: component_song_card + type: component + definition: *id003 +files: + to_create: + - app/components/SearchResults.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_section_header.yml b/.workflow/versions/v001/contexts/component_section_header.yml new file mode 100644 index 0000000..70cdd45 --- /dev/null +++ b/.workflow/versions/v001/contexts/component_section_header.yml @@ -0,0 +1,40 @@ +task_id: task_create_component_section_header +entity_id: component_section_header +generated_at: '2025-12-18T15:16:50.328281' +workflow_version: v001 +target: + type: component + definition: + id: component_section_header + name: SectionHeader + description: Section title with optional action + props: + - name: title + type: string + required: true + - name: actionLabel + type: string + required: false + events: + - name: onActionClick + payload: null + uses_apis: [] + uses_components: [] +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/SectionHeader.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_social_links.yml b/.workflow/versions/v001/contexts/component_social_links.yml new file mode 100644 index 0000000..479719b --- /dev/null +++ b/.workflow/versions/v001/contexts/component_social_links.yml @@ -0,0 +1,35 @@ +task_id: task_create_component_social_links +entity_id: component_social_links +generated_at: '2025-12-18T15:16:50.321018' +workflow_version: v001 +target: + type: component + definition: + id: component_social_links + name: SocialLinks + description: Social media links display + props: + - name: links + type: object + required: true + events: [] + uses_apis: [] + uses_components: [] +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/SocialLinks.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_song_card.yml b/.workflow/versions/v001/contexts/component_song_card.yml new file mode 100644 index 0000000..86bd2a1 --- /dev/null +++ b/.workflow/versions/v001/contexts/component_song_card.yml @@ -0,0 +1,47 @@ +task_id: task_create_component_song_card +entity_id: component_song_card +generated_at: '2025-12-18T15:16:50.312893' +workflow_version: v001 +target: + type: component + definition: + id: component_song_card + name: SongCard + description: Song display card with play button + props: + - name: song + type: Song + required: true + - name: showArtist + type: boolean + default: true + - name: showAlbum + type: boolean + default: false + events: + - name: onPlay + payload: + songId: string + - name: onAddToPlaylist + payload: + songId: string + uses_apis: [] + uses_components: [] +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/SongCard.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_track_list.yml b/.workflow/versions/v001/contexts/component_track_list.yml new file mode 100644 index 0000000..261e947 --- /dev/null +++ b/.workflow/versions/v001/contexts/component_track_list.yml @@ -0,0 +1,76 @@ +task_id: task_create_component_track_list +entity_id: component_track_list +generated_at: '2025-12-18T15:16:50.318077' +workflow_version: v001 +target: + type: component + definition: + id: component_track_list + name: TrackList + description: List of songs with track numbers + props: + - name: songs + type: array[Song] + required: true + - name: showTrackNumber + type: boolean + default: true + - name: reorderable + type: boolean + default: false + events: + - name: onPlay + payload: + songId: string + - name: onReorder + payload: + songIds: array[string] + uses_apis: [] + uses_components: + - component_song_card +related: + models: [] + apis: [] + components: + - id: component_song_card + definition: &id001 + id: component_song_card + name: SongCard + description: Song display card with play button + props: + - name: song + type: Song + required: true + - name: showArtist + type: boolean + default: true + - name: showAlbum + type: boolean + default: false + events: + - name: onPlay + payload: + songId: string + - name: onAddToPlaylist + payload: + songId: string + uses_apis: [] + uses_components: [] +dependencies: + entity_ids: + - component_song_card + definitions: + - id: component_song_card + type: component + definition: *id001 +files: + to_create: + - app/components/TrackList.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_upload_form.yml b/.workflow/versions/v001/contexts/component_upload_form.yml new file mode 100644 index 0000000..ad66fda --- /dev/null +++ b/.workflow/versions/v001/contexts/component_upload_form.yml @@ -0,0 +1,122 @@ +task_id: task_create_component_upload_form +entity_id: component_upload_form +generated_at: '2025-12-18T15:16:50.315310' +workflow_version: v001 +target: + type: component + definition: + id: component_upload_form + name: UploadForm + description: Song upload form with file input + props: + - name: albums + type: array[Album] + required: true + - name: genres + type: array[Genre] + required: true + state: + - name: file + type: File + - name: title + type: string + - name: selectedAlbum + type: string + - name: selectedGenres + type: array[string] + - name: uploadProgress + type: number + events: + - name: onUpload + payload: + file: File + metadata: object + - name: onCancel + payload: null + uses_apis: + - api_upload_song + uses_components: + - component_waveform_display +related: + models: [] + apis: + - id: api_upload_song + definition: &id002 + id: api_upload_song + method: POST + path: /api/songs/upload + description: Upload new song (musicians only) + request_body: + file: binary + title: string + album_id: uuid + genre_ids: array[uuid] + release_date: string + track_number: integer + responses: + - status: 201 + description: Song uploaded successfully + schema: + id: uuid + title: string + file_url: string + duration: integer + - status: 400 + description: Invalid file format or size + schema: + error: string + - status: 403 + description: User is not a musician + schema: + error: string + auth: + required: true + roles: + - musician + depends_on_models: + - model_song + - model_artist + components: + - id: component_waveform_display + definition: &id001 + id: component_waveform_display + name: WaveformDisplay + description: Audio waveform visualization + props: + - name: audioUrl + type: string + required: true + - name: waveformData + type: array[number] + required: false + - name: currentTime + type: number + required: false + events: + - name: onSeek + payload: + time: number + uses_apis: [] + uses_components: [] +dependencies: + entity_ids: + - component_waveform_display + - api_upload_song + definitions: + - id: component_waveform_display + type: component + definition: *id001 + - id: api_upload_song + type: api + definition: *id002 +files: + to_create: + - app/components/UploadForm.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/component_waveform_display.yml b/.workflow/versions/v001/contexts/component_waveform_display.yml new file mode 100644 index 0000000..9417887 --- /dev/null +++ b/.workflow/versions/v001/contexts/component_waveform_display.yml @@ -0,0 +1,44 @@ +task_id: task_create_component_waveform_display +entity_id: component_waveform_display +generated_at: '2025-12-18T15:16:50.316857' +workflow_version: v001 +target: + type: component + definition: + id: component_waveform_display + name: WaveformDisplay + description: Audio waveform visualization + props: + - name: audioUrl + type: string + required: true + - name: waveformData + type: array[number] + required: false + - name: currentTime + type: number + required: false + events: + - name: onSeek + payload: + time: number + uses_apis: [] + uses_components: [] +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/WaveformDisplay.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v001/contexts/model_album.yml b/.workflow/versions/v001/contexts/model_album.yml new file mode 100644 index 0000000..91d7a55 --- /dev/null +++ b/.workflow/versions/v001/contexts/model_album.yml @@ -0,0 +1,85 @@ +task_id: task_create_model_album +entity_id: model_album +generated_at: '2025-12-18T15:16:50.221462' +workflow_version: v001 +target: + type: model + definition: + id: model_album + name: Album + table_name: albums + description: Collection of songs + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: title + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: album_type + type: enum + values: + - album + - ep + - single + default: album + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: has_many + to: model_song + foreign_key: album_id + indexes: + - fields: + - artist_id + - fields: + - release_date + timestamps: true +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - prisma/schema.prisma + - app/models/album.ts + reference: [] +acceptance: +- criterion: Model defined in Prisma schema + verification: Check prisma/schema.prisma +- criterion: TypeScript types exported + verification: Import type in test file +- criterion: Relations properly configured + verification: Check Prisma relations diff --git a/.workflow/versions/v001/contexts/model_artist.yml b/.workflow/versions/v001/contexts/model_artist.yml new file mode 100644 index 0000000..a9c1fc4 --- /dev/null +++ b/.workflow/versions/v001/contexts/model_artist.yml @@ -0,0 +1,90 @@ +task_id: task_create_model_artist +entity_id: model_artist +generated_at: '2025-12-18T15:16:50.218607' +workflow_version: v001 +target: + type: model + definition: + id: model_artist + name: Artist + table_name: artists + description: Extended profile for musicians + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: stage_name + type: string + constraints: + - not_null + - name: bio + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: social_links + type: jsonb + description: JSON object with {twitter, instagram, facebook, website} + constraints: + - nullable + - name: verified + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_song + foreign_key: artist_id + - type: has_many + to: model_album + foreign_key: artist_id + - type: belongs_to + to: model_label + foreign_key: label_id + optional: true + indexes: + - fields: + - user_id + unique: true + - fields: + - stage_name + timestamps: true +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - prisma/schema.prisma + - app/models/artist.ts + reference: [] +acceptance: +- criterion: Model defined in Prisma schema + verification: Check prisma/schema.prisma +- criterion: TypeScript types exported + verification: Import type in test file +- criterion: Relations properly configured + verification: Check Prisma relations diff --git a/.workflow/versions/v001/contexts/model_genre.yml b/.workflow/versions/v001/contexts/model_genre.yml new file mode 100644 index 0000000..7d8f85b --- /dev/null +++ b/.workflow/versions/v001/contexts/model_genre.yml @@ -0,0 +1,67 @@ +task_id: task_create_model_genre +entity_id: model_genre +generated_at: '2025-12-18T15:16:50.220626' +workflow_version: v001 +target: + type: model + definition: + id: model_genre + name: Genre + table_name: genres + description: Music category for discovery + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: name + type: string + constraints: + - unique + - not_null + - name: slug + type: string + constraints: + - unique + - not_null + - name: description + type: text + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: has_many + to: model_song + through: song_genres + foreign_key: genre_id + indexes: + - fields: + - slug + unique: true + timestamps: true +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - prisma/schema.prisma + - app/models/genre.ts + reference: [] +acceptance: +- criterion: Model defined in Prisma schema + verification: Check prisma/schema.prisma +- criterion: TypeScript types exported + verification: Import type in test file +- criterion: Relations properly configured + verification: Check Prisma relations diff --git a/.workflow/versions/v001/contexts/model_label.yml b/.workflow/versions/v001/contexts/model_label.yml new file mode 100644 index 0000000..5b1be7d --- /dev/null +++ b/.workflow/versions/v001/contexts/model_label.yml @@ -0,0 +1,77 @@ +task_id: task_create_model_label +entity_id: model_label +generated_at: '2025-12-18T15:16:50.219701' +workflow_version: v001 +target: + type: model + definition: + id: model_label + name: Label + table_name: labels + description: Organization profile for labels + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: label_name + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: logo_url + type: string + constraints: + - nullable + - name: website + type: string + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_artist + foreign_key: label_id + indexes: + - fields: + - user_id + unique: true + timestamps: true +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - prisma/schema.prisma + - app/models/label.ts + reference: [] +acceptance: +- criterion: Model defined in Prisma schema + verification: Check prisma/schema.prisma +- criterion: TypeScript types exported + verification: Import type in test file +- criterion: Relations properly configured + verification: Check Prisma relations diff --git a/.workflow/versions/v001/contexts/model_playlist.yml b/.workflow/versions/v001/contexts/model_playlist.yml new file mode 100644 index 0000000..30a341b --- /dev/null +++ b/.workflow/versions/v001/contexts/model_playlist.yml @@ -0,0 +1,77 @@ +task_id: task_create_model_playlist +entity_id: model_playlist +generated_at: '2025-12-18T15:16:50.224750' +workflow_version: v001 +target: + type: model + definition: + id: model_playlist + name: Playlist + table_name: playlists + description: User-created song collection + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: user_id + type: uuid + constraints: + - not_null + - foreign_key + references: users.id + - name: name + type: string + constraints: + - not_null + - name: description + type: text + constraints: + - nullable + - name: cover_image_url + type: string + constraints: + - nullable + - name: is_public + type: boolean + default: false + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_playlist_song + foreign_key: playlist_id + indexes: + - fields: + - user_id + - fields: + - is_public + timestamps: true +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - prisma/schema.prisma + - app/models/playlist.ts + reference: [] +acceptance: +- criterion: Model defined in Prisma schema + verification: Check prisma/schema.prisma +- criterion: TypeScript types exported + verification: Import type in test file +- criterion: Relations properly configured + verification: Check Prisma relations diff --git a/.workflow/versions/v001/contexts/model_playlist_song.yml b/.workflow/versions/v001/contexts/model_playlist_song.yml new file mode 100644 index 0000000..9711153 --- /dev/null +++ b/.workflow/versions/v001/contexts/model_playlist_song.yml @@ -0,0 +1,72 @@ +task_id: task_create_model_playlist_song +entity_id: model_playlist_song +generated_at: '2025-12-18T15:16:50.225730' +workflow_version: v001 +target: + type: model + definition: + id: model_playlist_song + name: PlaylistSong + table_name: playlist_songs + description: Junction table with ordering for playlists + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: playlist_id + type: uuid + constraints: + - not_null + - foreign_key + references: playlists.id + - name: song_id + type: uuid + constraints: + - not_null + - foreign_key + references: songs.id + - name: position + type: integer + constraints: + - not_null + - name: added_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_playlist + foreign_key: playlist_id + - type: belongs_to + to: model_song + foreign_key: song_id + indexes: + - fields: + - playlist_id + - position + unique: true + - fields: + - playlist_id + - song_id + unique: true + timestamps: false +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - prisma/schema.prisma + - app/models/playlistsong.ts + reference: [] +acceptance: +- criterion: Model defined in Prisma schema + verification: Check prisma/schema.prisma +- criterion: TypeScript types exported + verification: Import type in test file +- criterion: Relations properly configured + verification: Check Prisma relations diff --git a/.workflow/versions/v001/contexts/model_song.yml b/.workflow/versions/v001/contexts/model_song.yml new file mode 100644 index 0000000..4f74715 --- /dev/null +++ b/.workflow/versions/v001/contexts/model_song.yml @@ -0,0 +1,132 @@ +task_id: task_create_model_song +entity_id: model_song +generated_at: '2025-12-18T15:16:50.222463' +workflow_version: v001 +target: + type: model + definition: + id: model_song + name: Song + table_name: songs + description: Audio track with metadata + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: artist_id + type: uuid + constraints: + - not_null + - foreign_key + references: artists.id + - name: album_id + type: uuid + constraints: + - nullable + - foreign_key + references: albums.id + - name: title + type: string + constraints: + - not_null + - name: duration + type: integer + description: Duration in seconds + constraints: + - not_null + - name: file_url + type: string + description: Cloud storage URL for audio file + constraints: + - not_null + - name: file_format + type: enum + values: + - mp3 + - wav + constraints: + - not_null + - name: file_size + type: integer + description: File size in bytes + constraints: + - not_null + - name: waveform_data + type: jsonb + description: Waveform visualization data + constraints: + - nullable + - name: cover_art_url + type: string + constraints: + - nullable + - name: release_date + type: date + constraints: + - nullable + - name: play_count + type: integer + default: 0 + - name: is_public + type: boolean + default: true + - name: track_number + type: integer + description: Position in album + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: belongs_to + to: model_album + foreign_key: album_id + optional: true + - type: has_many + to: model_genre + through: song_genres + foreign_key: song_id + - type: has_many + to: model_playlist_song + foreign_key: song_id + indexes: + - fields: + - artist_id + - fields: + - album_id + - fields: + - release_date + - fields: + - play_count + - fields: + - is_public + timestamps: true +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - prisma/schema.prisma + - app/models/song.ts + reference: [] +acceptance: +- criterion: Model defined in Prisma schema + verification: Check prisma/schema.prisma +- criterion: TypeScript types exported + verification: Import type in test file +- criterion: Relations properly configured + verification: Check Prisma relations diff --git a/.workflow/versions/v001/contexts/model_song_genre.yml b/.workflow/versions/v001/contexts/model_song_genre.yml new file mode 100644 index 0000000..0a8e3bb --- /dev/null +++ b/.workflow/versions/v001/contexts/model_song_genre.yml @@ -0,0 +1,56 @@ +task_id: task_create_model_song_genre +entity_id: model_song_genre +generated_at: '2025-12-18T15:16:50.224015' +workflow_version: v001 +target: + type: model + definition: + id: model_song_genre + name: SongGenre + table_name: song_genres + description: Junction table for song-genre many-to-many + fields: + - name: song_id + type: uuid + constraints: + - not_null + - foreign_key + references: songs.id + - name: genre_id + type: uuid + constraints: + - not_null + - foreign_key + references: genres.id + relations: + - type: belongs_to + to: model_song + foreign_key: song_id + - type: belongs_to + to: model_genre + foreign_key: genre_id + indexes: + - fields: + - song_id + - genre_id + unique: true + timestamps: false +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - prisma/schema.prisma + - app/models/songgenre.ts + reference: [] +acceptance: +- criterion: Model defined in Prisma schema + verification: Check prisma/schema.prisma +- criterion: TypeScript types exported + verification: Import type in test file +- criterion: Relations properly configured + verification: Check Prisma relations diff --git a/.workflow/versions/v001/contexts/model_user.yml b/.workflow/versions/v001/contexts/model_user.yml new file mode 100644 index 0000000..6e8ef4b --- /dev/null +++ b/.workflow/versions/v001/contexts/model_user.yml @@ -0,0 +1,90 @@ +task_id: task_create_model_user +entity_id: model_user +generated_at: '2025-12-18T15:16:50.217528' +workflow_version: v001 +target: + type: model + definition: + id: model_user + name: User + table_name: users + description: Base user entity with authentication + fields: + - name: id + type: uuid + constraints: + - primary_key + - name: email + type: string + constraints: + - unique + - not_null + - name: password_hash + type: string + constraints: + - not_null + - name: name + type: string + constraints: + - not_null + - name: role + type: enum + values: + - musician + - listener + - label + constraints: + - not_null + - name: email_verified + type: boolean + default: false + - name: avatar_url + type: string + constraints: + - nullable + - name: created_at + type: timestamp + constraints: + - not_null + - name: updated_at + type: timestamp + constraints: + - not_null + relations: + - type: has_one + to: model_artist + foreign_key: user_id + condition: role = 'musician' + - type: has_one + to: model_label + foreign_key: user_id + condition: role = 'label' + - type: has_many + to: model_playlist + foreign_key: user_id + indexes: + - fields: + - email + unique: true + - fields: + - role + timestamps: true +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - prisma/schema.prisma + - app/models/user.ts + reference: [] +acceptance: +- criterion: Model defined in Prisma schema + verification: Check prisma/schema.prisma +- criterion: TypeScript types exported + verification: Import type in test file +- criterion: Relations properly configured + verification: Check Prisma relations diff --git a/.workflow/versions/v001/contexts/page_album_detail.yml b/.workflow/versions/v001/contexts/page_album_detail.yml new file mode 100644 index 0000000..229daa5 --- /dev/null +++ b/.workflow/versions/v001/contexts/page_album_detail.yml @@ -0,0 +1,150 @@ +task_id: task_create_page_album_detail +entity_id: page_album_detail +generated_at: '2025-12-18T15:16:50.297890' +workflow_version: v001 +target: + type: page + definition: + id: page_album_detail + path: /album/:id + title: Album + description: Album detail with track list + data_needs: + - api_id: api_get_album + purpose: Album details and songs + on_load: true + components: + - component_album_header + - component_track_list + - component_song_card + auth: + required: false +related: + models: [] + apis: + - id: api_get_album + definition: &id003 + id: api_get_album + method: GET + path: /api/albums/:id + description: Get album details with songs + responses: + - status: 200 + description: Album details + schema: + id: uuid + title: string + description: string + cover_art_url: string + release_date: string + artist: + id: uuid + stage_name: string + songs: + - id: uuid + title: string + duration: integer + track_number: integer + auth: + required: false + depends_on_models: + - model_album + - model_song + - model_artist + components: + - id: component_track_list + definition: &id001 + id: component_track_list + name: TrackList + description: List of songs with track numbers + props: + - name: songs + type: array[Song] + required: true + - name: showTrackNumber + type: boolean + default: true + - name: reorderable + type: boolean + default: false + events: + - name: onPlay + payload: + songId: string + - name: onReorder + payload: + songIds: array[string] + uses_apis: [] + uses_components: + - component_song_card + - id: component_song_card + definition: &id002 + id: component_song_card + name: SongCard + description: Song display card with play button + props: + - name: song + type: Song + required: true + - name: showArtist + type: boolean + default: true + - name: showAlbum + type: boolean + default: false + events: + - name: onPlay + payload: + songId: string + - name: onAddToPlaylist + payload: + songId: string + uses_apis: [] + uses_components: [] + - id: component_album_header + definition: &id004 + id: component_album_header + name: AlbumHeader + description: Album detail header with cover art + props: + - name: album + type: Album + required: true + - name: artist + type: Artist + required: true + events: + - name: onPlayAll + payload: null + uses_apis: [] + uses_components: [] +dependencies: + entity_ids: + - component_track_list + - component_song_card + - api_get_album + - component_album_header + definitions: + - id: component_track_list + type: component + definition: *id001 + - id: component_song_card + type: component + definition: *id002 + - id: api_get_album + type: api + definition: *id003 + - id: component_album_header + type: component + definition: *id004 +files: + to_create: + - app/album/:id/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /album/:id + verification: Navigate to /album/:id +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v001/contexts/page_artist_profile.yml b/.workflow/versions/v001/contexts/page_artist_profile.yml new file mode 100644 index 0000000..7deec08 --- /dev/null +++ b/.workflow/versions/v001/contexts/page_artist_profile.yml @@ -0,0 +1,207 @@ +task_id: task_create_page_artist_profile +entity_id: page_artist_profile +generated_at: '2025-12-18T15:16:50.295445' +workflow_version: v001 +target: + type: page + definition: + id: page_artist_profile + path: /artist/:id + title: Artist Profile + description: Artist profile with songs and albums + data_needs: + - api_id: api_get_artist + purpose: Artist details + on_load: true + - api_id: api_get_artist_songs + purpose: Artist songs + on_load: true + - api_id: api_get_artist_albums + purpose: Artist albums + on_load: true + components: + - component_artist_header + - component_song_card + - component_album_card + - component_social_links + auth: + required: false +related: + models: [] + apis: + - id: api_get_artist_albums + definition: &id001 + id: api_get_artist_albums + method: GET + path: /api/artists/:id/albums + description: Get all albums by artist + responses: + - status: 200 + description: List of albums + schema: + albums: + - id: uuid + title: string + cover_art_url: string + release_date: string + album_type: string + auth: + required: false + depends_on_models: + - model_artist + - model_album + - id: api_get_artist_songs + definition: &id004 + id: api_get_artist_songs + method: GET + path: /api/artists/:id/songs + description: Get all songs by artist + responses: + - status: 200 + description: List of songs + schema: + songs: + - id: uuid + title: string + duration: integer + cover_art_url: string + play_count: integer + auth: + required: false + depends_on_models: + - model_artist + - model_song + - id: api_get_artist + definition: &id005 + id: api_get_artist + method: GET + path: /api/artists/:id + description: Get artist profile by ID + responses: + - status: 200 + description: Artist profile + schema: + id: uuid + stage_name: string + bio: string + cover_image_url: string + social_links: object + verified: boolean + - status: 404 + description: Artist not found + schema: + error: string + auth: + required: false + depends_on_models: + - model_artist + components: + - id: component_artist_header + definition: &id002 + id: component_artist_header + name: ArtistHeader + description: Artist profile header with cover image + props: + - name: artist + type: Artist + required: true + events: [] + uses_apis: [] + uses_components: + - component_social_links + - id: component_album_card + definition: &id003 + id: component_album_card + name: AlbumCard + description: Album display card + props: + - name: album + type: Album + required: true + - name: showArtist + type: boolean + default: true + events: + - name: onClick + payload: + albumId: string + uses_apis: [] + uses_components: [] + - id: component_song_card + definition: &id006 + id: component_song_card + name: SongCard + description: Song display card with play button + props: + - name: song + type: Song + required: true + - name: showArtist + type: boolean + default: true + - name: showAlbum + type: boolean + default: false + events: + - name: onPlay + payload: + songId: string + - name: onAddToPlaylist + payload: + songId: string + uses_apis: [] + uses_components: [] + - id: component_social_links + definition: &id007 + id: component_social_links + name: SocialLinks + description: Social media links display + props: + - name: links + type: object + required: true + events: [] + uses_apis: [] + uses_components: [] +dependencies: + entity_ids: + - api_get_artist_albums + - component_artist_header + - component_album_card + - api_get_artist_songs + - api_get_artist + - component_song_card + - component_social_links + definitions: + - id: api_get_artist_albums + type: api + definition: *id001 + - id: component_artist_header + type: component + definition: *id002 + - id: component_album_card + type: component + definition: *id003 + - id: api_get_artist_songs + type: api + definition: *id004 + - id: api_get_artist + type: api + definition: *id005 + - id: component_song_card + type: component + definition: *id006 + - id: component_social_links + type: component + definition: *id007 +files: + to_create: + - app/artist/:id/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /artist/:id + verification: Navigate to /artist/:id +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v001/contexts/page_forgot_password.yml b/.workflow/versions/v001/contexts/page_forgot_password.yml new file mode 100644 index 0000000..1c46afe --- /dev/null +++ b/.workflow/versions/v001/contexts/page_forgot_password.yml @@ -0,0 +1,92 @@ +task_id: task_create_page_forgot_password +entity_id: page_forgot_password +generated_at: '2025-12-18T15:16:50.291947' +workflow_version: v001 +target: + type: page + definition: + id: page_forgot_password + path: /forgot-password + title: Forgot Password + description: Password reset request page + data_needs: + - api_id: api_forgot_password + purpose: Request password reset + on_load: false + components: + - component_auth_form + auth: + required: false +related: + models: [] + apis: + - id: api_forgot_password + definition: &id002 + id: api_forgot_password + method: POST + path: /api/auth/forgot-password + description: Request password reset email + request_body: + email: string + responses: + - status: 200 + description: Reset email sent + schema: + message: string + - status: 404 + description: Email not found + schema: + error: string + auth: + required: false + depends_on_models: + - model_user + components: + - id: component_auth_form + definition: &id001 + id: component_auth_form + name: AuthForm + description: Reusable authentication form + props: + - name: mode + type: enum[login, register, forgot] + required: true + state: + - name: email + type: string + - name: password + type: string + - name: name + type: string + - name: role + type: string + events: + - name: onSubmit + payload: object + uses_apis: + - api_login + - api_register + - api_forgot_password + uses_components: [] +dependencies: + entity_ids: + - component_auth_form + - api_forgot_password + definitions: + - id: component_auth_form + type: component + definition: *id001 + - id: api_forgot_password + type: api + definition: *id002 +files: + to_create: + - app/forgot-password/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /forgot-password + verification: Navigate to /forgot-password +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v001/contexts/page_genre_browse.yml b/.workflow/versions/v001/contexts/page_genre_browse.yml new file mode 100644 index 0000000..4badd61 --- /dev/null +++ b/.workflow/versions/v001/contexts/page_genre_browse.yml @@ -0,0 +1,107 @@ +task_id: task_create_page_genre_browse +entity_id: page_genre_browse +generated_at: '2025-12-18T15:16:50.309059' +workflow_version: v001 +target: + type: page + definition: + id: page_genre_browse + path: /genre/:slug + title: Browse Genre + description: Browse songs by genre + data_needs: + - api_id: api_get_songs_by_genre + purpose: Load genre songs + on_load: true + components: + - component_genre_header + - component_song_card + auth: + required: false +related: + models: [] + apis: + - id: api_get_songs_by_genre + definition: &id002 + id: api_get_songs_by_genre + method: GET + path: /api/discover/genres/:slug + description: Get songs by genre + query_params: + limit: integer + offset: integer + responses: + - status: 200 + description: List of songs in genre + schema: + genre: + name: string + songs: array + auth: + required: false + depends_on_models: + - model_genre + - model_song + components: + - id: component_genre_header + definition: &id001 + id: component_genre_header + name: GenreHeader + description: Genre browse page header + props: + - name: genre + type: Genre + required: true + events: [] + uses_apis: [] + uses_components: [] + - id: component_song_card + definition: &id003 + id: component_song_card + name: SongCard + description: Song display card with play button + props: + - name: song + type: Song + required: true + - name: showArtist + type: boolean + default: true + - name: showAlbum + type: boolean + default: false + events: + - name: onPlay + payload: + songId: string + - name: onAddToPlaylist + payload: + songId: string + uses_apis: [] + uses_components: [] +dependencies: + entity_ids: + - component_genre_header + - api_get_songs_by_genre + - component_song_card + definitions: + - id: component_genre_header + type: component + definition: *id001 + - id: api_get_songs_by_genre + type: api + definition: *id002 + - id: component_song_card + type: component + definition: *id003 +files: + to_create: + - app/genre/:slug/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /genre/:slug + verification: Navigate to /genre/:slug +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v001/contexts/page_home.yml b/.workflow/versions/v001/contexts/page_home.yml new file mode 100644 index 0000000..1108a08 --- /dev/null +++ b/.workflow/versions/v001/contexts/page_home.yml @@ -0,0 +1,191 @@ +task_id: task_create_page_home +entity_id: page_home +generated_at: '2025-12-18T15:16:50.293122' +workflow_version: v001 +target: + type: page + definition: + id: page_home + path: / + title: Discover Music + description: Main discovery feed + data_needs: + - api_id: api_get_trending_songs + purpose: Show trending songs + on_load: true + - api_id: api_get_new_releases + purpose: Show new releases + on_load: true + - api_id: api_get_genres + purpose: Genre navigation + on_load: true + components: + - component_song_card + - component_genre_badge + - component_section_header + auth: + required: false +related: + models: [] + apis: + - id: api_get_new_releases + definition: &id003 + id: api_get_new_releases + method: GET + path: /api/discover/new-releases + description: Get recently released songs + query_params: + limit: integer + offset: integer + responses: + - status: 200 + description: List of new releases + schema: + songs: + - id: uuid + title: string + release_date: string + auth: + required: false + depends_on_models: + - model_song + - id: api_get_trending_songs + definition: &id005 + id: api_get_trending_songs + method: GET + path: /api/discover/trending + description: Get trending songs + query_params: + limit: integer + offset: integer + responses: + - status: 200 + description: List of trending songs + schema: + songs: + - id: uuid + title: string + artist: + stage_name: string + play_count: integer + auth: + required: false + depends_on_models: + - model_song + - model_artist + - id: api_get_genres + definition: &id006 + id: api_get_genres + method: GET + path: /api/discover/genres + description: Get all genres + responses: + - status: 200 + description: List of genres + schema: + genres: + - id: uuid + name: string + slug: string + auth: + required: false + depends_on_models: + - model_genre + components: + - id: component_genre_badge + definition: &id001 + id: component_genre_badge + name: GenreBadge + description: Genre tag display + props: + - name: genre + type: Genre + required: true + - name: clickable + type: boolean + default: true + events: + - name: onClick + payload: + genreSlug: string + uses_apis: [] + uses_components: [] + - id: component_section_header + definition: &id002 + id: component_section_header + name: SectionHeader + description: Section title with optional action + props: + - name: title + type: string + required: true + - name: actionLabel + type: string + required: false + events: + - name: onActionClick + payload: null + uses_apis: [] + uses_components: [] + - id: component_song_card + definition: &id004 + id: component_song_card + name: SongCard + description: Song display card with play button + props: + - name: song + type: Song + required: true + - name: showArtist + type: boolean + default: true + - name: showAlbum + type: boolean + default: false + events: + - name: onPlay + payload: + songId: string + - name: onAddToPlaylist + payload: + songId: string + uses_apis: [] + uses_components: [] +dependencies: + entity_ids: + - component_genre_badge + - component_section_header + - api_get_new_releases + - component_song_card + - api_get_trending_songs + - api_get_genres + definitions: + - id: component_genre_badge + type: component + definition: *id001 + - id: component_section_header + type: component + definition: *id002 + - id: api_get_new_releases + type: api + definition: *id003 + - id: component_song_card + type: component + definition: *id004 + - id: api_get_trending_songs + type: api + definition: *id005 + - id: api_get_genres + type: api + definition: *id006 +files: + to_create: + - app//page.tsx + reference: [] +acceptance: +- criterion: Page renders at / + verification: Navigate to / +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v001/contexts/page_login.yml b/.workflow/versions/v001/contexts/page_login.yml new file mode 100644 index 0000000..dcd3182 --- /dev/null +++ b/.workflow/versions/v001/contexts/page_login.yml @@ -0,0 +1,99 @@ +task_id: task_create_page_login +entity_id: page_login +generated_at: '2025-12-18T15:16:50.289459' +workflow_version: v001 +target: + type: page + definition: + id: page_login + path: /login + title: Login + description: User login page + data_needs: + - api_id: api_login + purpose: Authenticate user + on_load: false + components: + - component_auth_form + auth: + required: false + redirect_if_authenticated: / +related: + models: [] + apis: + - id: api_login + definition: &id002 + id: api_login + method: POST + path: /api/auth/login + description: Login with email and password + request_body: + email: string + password: string + responses: + - status: 200 + description: Login successful + schema: + user: + id: uuid + email: string + name: string + role: string + token: string + - status: 401 + description: Invalid credentials + schema: + error: string + auth: + required: false + depends_on_models: + - model_user + components: + - id: component_auth_form + definition: &id001 + id: component_auth_form + name: AuthForm + description: Reusable authentication form + props: + - name: mode + type: enum[login, register, forgot] + required: true + state: + - name: email + type: string + - name: password + type: string + - name: name + type: string + - name: role + type: string + events: + - name: onSubmit + payload: object + uses_apis: + - api_login + - api_register + - api_forgot_password + uses_components: [] +dependencies: + entity_ids: + - component_auth_form + - api_login + definitions: + - id: component_auth_form + type: component + definition: *id001 + - id: api_login + type: api + definition: *id002 +files: + to_create: + - app/login/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /login + verification: Navigate to /login +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v001/contexts/page_playlist_detail.yml b/.workflow/versions/v001/contexts/page_playlist_detail.yml new file mode 100644 index 0000000..8053200 --- /dev/null +++ b/.workflow/versions/v001/contexts/page_playlist_detail.yml @@ -0,0 +1,149 @@ +task_id: task_create_page_playlist_detail +entity_id: page_playlist_detail +generated_at: '2025-12-18T15:16:50.303413' +workflow_version: v001 +target: + type: page + definition: + id: page_playlist_detail + path: /playlist/:id + title: Playlist + description: Playlist detail with songs + data_needs: + - api_id: api_get_playlist + purpose: Playlist details and songs + on_load: true + components: + - component_playlist_header + - component_track_list + - component_song_card + auth: + required: false +related: + models: [] + apis: + - id: api_get_playlist + definition: &id003 + id: api_get_playlist + method: GET + path: /api/playlists/:id + description: Get playlist details with songs + responses: + - status: 200 + description: Playlist details + schema: + id: uuid + name: string + description: string + songs: + - id: uuid + title: string + artist: + stage_name: string + position: integer + auth: + required: false + depends_on_models: + - model_playlist + - model_playlist_song + components: + - id: component_playlist_header + definition: &id001 + id: component_playlist_header + name: PlaylistHeader + description: Playlist header with cover and controls + props: + - name: playlist + type: Playlist + required: true + - name: isOwner + type: boolean + default: false + events: + - name: onPlayAll + payload: null + - name: onEdit + payload: null + - name: onDelete + payload: null + uses_apis: [] + uses_components: [] + - id: component_track_list + definition: &id002 + id: component_track_list + name: TrackList + description: List of songs with track numbers + props: + - name: songs + type: array[Song] + required: true + - name: showTrackNumber + type: boolean + default: true + - name: reorderable + type: boolean + default: false + events: + - name: onPlay + payload: + songId: string + - name: onReorder + payload: + songIds: array[string] + uses_apis: [] + uses_components: + - component_song_card + - id: component_song_card + definition: &id004 + id: component_song_card + name: SongCard + description: Song display card with play button + props: + - name: song + type: Song + required: true + - name: showArtist + type: boolean + default: true + - name: showAlbum + type: boolean + default: false + events: + - name: onPlay + payload: + songId: string + - name: onAddToPlaylist + payload: + songId: string + uses_apis: [] + uses_components: [] +dependencies: + entity_ids: + - component_playlist_header + - component_track_list + - api_get_playlist + - component_song_card + definitions: + - id: component_playlist_header + type: component + definition: *id001 + - id: component_track_list + type: component + definition: *id002 + - id: api_get_playlist + type: api + definition: *id003 + - id: component_song_card + type: component + definition: *id004 +files: + to_create: + - app/playlist/:id/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /playlist/:id + verification: Navigate to /playlist/:id +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v001/contexts/page_playlists.yml b/.workflow/versions/v001/contexts/page_playlists.yml new file mode 100644 index 0000000..86566dd --- /dev/null +++ b/.workflow/versions/v001/contexts/page_playlists.yml @@ -0,0 +1,112 @@ +task_id: task_create_page_playlists +entity_id: page_playlists +generated_at: '2025-12-18T15:16:50.302020' +workflow_version: v001 +target: + type: page + definition: + id: page_playlists + path: /playlists + title: My Playlists + description: User's playlists + data_needs: + - api_id: api_get_user_playlists + purpose: Load playlists + on_load: true + components: + - component_playlist_card + - component_create_playlist_modal + auth: + required: true + redirect_if_unauthorized: /login +related: + models: [] + apis: + - id: api_get_user_playlists + definition: &id002 + id: api_get_user_playlists + method: GET + path: /api/playlists + description: Get current user's playlists + responses: + - status: 200 + description: List of playlists + schema: + playlists: + - id: uuid + name: string + cover_image_url: string + song_count: integer + auth: + required: true + depends_on_models: + - model_playlist + components: + - id: component_playlist_card + definition: &id001 + id: component_playlist_card + name: PlaylistCard + description: Playlist preview card + props: + - name: playlist + type: Playlist + required: true + events: + - name: onClick + payload: + playlistId: string + uses_apis: [] + uses_components: [] + - id: component_create_playlist_modal + definition: &id003 + id: component_create_playlist_modal + name: CreatePlaylistModal + description: Modal for creating new playlist + props: + - name: isOpen + type: boolean + required: true + state: + - name: name + type: string + - name: description + type: string + - name: isPublic + type: boolean + events: + - name: onCreate + payload: + name: string + description: string + isPublic: boolean + - name: onClose + payload: null + uses_apis: + - api_create_playlist + uses_components: [] +dependencies: + entity_ids: + - component_playlist_card + - api_get_user_playlists + - component_create_playlist_modal + definitions: + - id: component_playlist_card + type: component + definition: *id001 + - id: api_get_user_playlists + type: api + definition: *id002 + - id: component_create_playlist_modal + type: component + definition: *id003 +files: + to_create: + - app/playlists/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /playlists + verification: Navigate to /playlists +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v001/contexts/page_profile.yml b/.workflow/versions/v001/contexts/page_profile.yml new file mode 100644 index 0000000..13d90d3 --- /dev/null +++ b/.workflow/versions/v001/contexts/page_profile.yml @@ -0,0 +1,141 @@ +task_id: task_create_page_profile +entity_id: page_profile +generated_at: '2025-12-18T15:16:50.305216' +workflow_version: v001 +target: + type: page + definition: + id: page_profile + path: /profile + title: Profile Settings + description: User profile settings + data_needs: + - api_id: api_get_current_user + purpose: Load user data + on_load: true + - api_id: api_update_current_user + purpose: Update profile + on_load: false + components: + - component_profile_form + - component_avatar_upload + auth: + required: true + redirect_if_unauthorized: /login +related: + models: [] + apis: + - id: api_update_current_user + definition: &id001 + id: api_update_current_user + method: PUT + path: /api/users/me + description: Update current user profile + request_body: + name: string + avatar_url: string + responses: + - status: 200 + description: User updated + schema: + id: uuid + email: string + name: string + avatar_url: string + auth: + required: true + depends_on_models: + - model_user + - id: api_get_current_user + definition: &id003 + id: api_get_current_user + method: GET + path: /api/users/me + description: Get current user profile + responses: + - status: 200 + description: User profile + schema: + id: uuid + email: string + name: string + role: string + avatar_url: string + auth: + required: true + depends_on_models: + - model_user + components: + - id: component_profile_form + definition: &id002 + id: component_profile_form + name: ProfileForm + description: User profile edit form + props: + - name: user + type: User + required: true + state: + - name: name + type: string + - name: avatarUrl + type: string + events: + - name: onSave + payload: + name: string + avatarUrl: string + uses_apis: + - api_update_current_user + uses_components: + - component_avatar_upload + - id: component_avatar_upload + definition: &id004 + id: component_avatar_upload + name: AvatarUpload + description: Avatar image upload component + props: + - name: currentAvatarUrl + type: string + required: false + state: + - name: file + type: File + - name: preview + type: string + events: + - name: onUpload + payload: + file: File + uses_apis: [] + uses_components: [] +dependencies: + entity_ids: + - api_update_current_user + - component_profile_form + - api_get_current_user + - component_avatar_upload + definitions: + - id: api_update_current_user + type: api + definition: *id001 + - id: component_profile_form + type: component + definition: *id002 + - id: api_get_current_user + type: api + definition: *id003 + - id: component_avatar_upload + type: component + definition: *id004 +files: + to_create: + - app/profile/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /profile + verification: Navigate to /profile +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v001/contexts/page_register.yml b/.workflow/versions/v001/contexts/page_register.yml new file mode 100644 index 0000000..ae307c2 --- /dev/null +++ b/.workflow/versions/v001/contexts/page_register.yml @@ -0,0 +1,105 @@ +task_id: task_create_page_register +entity_id: page_register +generated_at: '2025-12-18T15:16:50.290659' +workflow_version: v001 +target: + type: page + definition: + id: page_register + path: /register + title: Register + description: User registration page + data_needs: + - api_id: api_register + purpose: Create new account + on_load: false + components: + - component_auth_form + auth: + required: false + redirect_if_authenticated: / +related: + models: [] + apis: + - id: api_register + definition: &id002 + id: api_register + method: POST + path: /api/auth/register + description: Register new user account + request_body: + email: string + password: string + name: string + role: enum[musician, listener, label] + responses: + - status: 201 + description: User created successfully + schema: + user: + id: uuid + email: string + name: string + role: string + token: string + - status: 400 + description: Validation error + schema: + error: string + - status: 409 + description: Email already exists + schema: + error: string + auth: + required: false + depends_on_models: + - model_user + components: + - id: component_auth_form + definition: &id001 + id: component_auth_form + name: AuthForm + description: Reusable authentication form + props: + - name: mode + type: enum[login, register, forgot] + required: true + state: + - name: email + type: string + - name: password + type: string + - name: name + type: string + - name: role + type: string + events: + - name: onSubmit + payload: object + uses_apis: + - api_login + - api_register + - api_forgot_password + uses_components: [] +dependencies: + entity_ids: + - component_auth_form + - api_register + definitions: + - id: component_auth_form + type: component + definition: *id001 + - id: api_register + type: api + definition: *id002 +files: + to_create: + - app/register/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /register + verification: Navigate to /register +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v001/contexts/page_search.yml b/.workflow/versions/v001/contexts/page_search.yml new file mode 100644 index 0000000..8ee8776 --- /dev/null +++ b/.workflow/versions/v001/contexts/page_search.yml @@ -0,0 +1,179 @@ +task_id: task_create_page_search +entity_id: page_search +generated_at: '2025-12-18T15:16:50.306942' +workflow_version: v001 +target: + type: page + definition: + id: page_search + path: /search + title: Search + description: Search results page + data_needs: + - api_id: api_search + purpose: Search songs, artists, albums + on_load: false + components: + - component_search_bar + - component_search_results + - component_song_card + - component_artist_card + - component_album_card + auth: + required: false +related: + models: [] + apis: + - id: api_search + definition: &id005 + id: api_search + method: GET + path: /api/search + description: Search songs, artists, and albums + query_params: + q: string + type: enum[song, artist, album, all] + limit: integer + responses: + - status: 200 + description: Search results + schema: + songs: array + artists: array + albums: array + auth: + required: false + depends_on_models: + - model_song + - model_artist + - model_album + components: + - id: component_search_results + definition: &id001 + id: component_search_results + name: SearchResults + description: Search results with filters + props: + - name: results + type: object + required: true + events: [] + uses_apis: [] + uses_components: + - component_song_card + - component_artist_card + - component_album_card + - id: component_album_card + definition: &id002 + id: component_album_card + name: AlbumCard + description: Album display card + props: + - name: album + type: Album + required: true + - name: showArtist + type: boolean + default: true + events: + - name: onClick + payload: + albumId: string + uses_apis: [] + uses_components: [] + - id: component_artist_card + definition: &id003 + id: component_artist_card + name: ArtistCard + description: Artist preview card + props: + - name: artist + type: Artist + required: true + events: + - name: onClick + payload: + artistId: string + uses_apis: [] + uses_components: [] + - id: component_search_bar + definition: &id004 + id: component_search_bar + name: SearchBar + description: Search input with autocomplete + props: + - name: placeholder + type: string + default: Search songs, artists, albums... + state: + - name: query + type: string + events: + - name: onSearch + payload: + query: string + uses_apis: + - api_search + uses_components: [] + - id: component_song_card + definition: &id006 + id: component_song_card + name: SongCard + description: Song display card with play button + props: + - name: song + type: Song + required: true + - name: showArtist + type: boolean + default: true + - name: showAlbum + type: boolean + default: false + events: + - name: onPlay + payload: + songId: string + - name: onAddToPlaylist + payload: + songId: string + uses_apis: [] + uses_components: [] +dependencies: + entity_ids: + - component_search_results + - component_album_card + - component_artist_card + - component_search_bar + - api_search + - component_song_card + definitions: + - id: component_search_results + type: component + definition: *id001 + - id: component_album_card + type: component + definition: *id002 + - id: component_artist_card + type: component + definition: *id003 + - id: component_search_bar + type: component + definition: *id004 + - id: api_search + type: api + definition: *id005 + - id: component_song_card + type: component + definition: *id006 +files: + to_create: + - app/search/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /search + verification: Navigate to /search +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v001/contexts/page_upload.yml b/.workflow/versions/v001/contexts/page_upload.yml new file mode 100644 index 0000000..27f33f9 --- /dev/null +++ b/.workflow/versions/v001/contexts/page_upload.yml @@ -0,0 +1,197 @@ +task_id: task_create_page_upload +entity_id: page_upload +generated_at: '2025-12-18T15:16:50.299695' +workflow_version: v001 +target: + type: page + definition: + id: page_upload + path: /upload + title: Upload Music + description: Song upload page (musicians only) + data_needs: + - api_id: api_upload_song + purpose: Upload song file + on_load: false + - api_id: api_get_artist_albums + purpose: Select album + on_load: true + - api_id: api_get_genres + purpose: Select genres + on_load: true + components: + - component_upload_form + - component_waveform_display + auth: + required: true + roles: + - musician + redirect_if_unauthorized: /login +related: + models: [] + apis: + - id: api_get_artist_albums + definition: &id001 + id: api_get_artist_albums + method: GET + path: /api/artists/:id/albums + description: Get all albums by artist + responses: + - status: 200 + description: List of albums + schema: + albums: + - id: uuid + title: string + cover_art_url: string + release_date: string + album_type: string + auth: + required: false + depends_on_models: + - model_artist + - model_album + - id: api_upload_song + definition: &id003 + id: api_upload_song + method: POST + path: /api/songs/upload + description: Upload new song (musicians only) + request_body: + file: binary + title: string + album_id: uuid + genre_ids: array[uuid] + release_date: string + track_number: integer + responses: + - status: 201 + description: Song uploaded successfully + schema: + id: uuid + title: string + file_url: string + duration: integer + - status: 400 + description: Invalid file format or size + schema: + error: string + - status: 403 + description: User is not a musician + schema: + error: string + auth: + required: true + roles: + - musician + depends_on_models: + - model_song + - model_artist + - id: api_get_genres + definition: &id005 + id: api_get_genres + method: GET + path: /api/discover/genres + description: Get all genres + responses: + - status: 200 + description: List of genres + schema: + genres: + - id: uuid + name: string + slug: string + auth: + required: false + depends_on_models: + - model_genre + components: + - id: component_upload_form + definition: &id002 + id: component_upload_form + name: UploadForm + description: Song upload form with file input + props: + - name: albums + type: array[Album] + required: true + - name: genres + type: array[Genre] + required: true + state: + - name: file + type: File + - name: title + type: string + - name: selectedAlbum + type: string + - name: selectedGenres + type: array[string] + - name: uploadProgress + type: number + events: + - name: onUpload + payload: + file: File + metadata: object + - name: onCancel + payload: null + uses_apis: + - api_upload_song + uses_components: + - component_waveform_display + - id: component_waveform_display + definition: &id004 + id: component_waveform_display + name: WaveformDisplay + description: Audio waveform visualization + props: + - name: audioUrl + type: string + required: true + - name: waveformData + type: array[number] + required: false + - name: currentTime + type: number + required: false + events: + - name: onSeek + payload: + time: number + uses_apis: [] + uses_components: [] +dependencies: + entity_ids: + - api_get_artist_albums + - component_upload_form + - api_upload_song + - component_waveform_display + - api_get_genres + definitions: + - id: api_get_artist_albums + type: api + definition: *id001 + - id: component_upload_form + type: component + definition: *id002 + - id: api_upload_song + type: api + definition: *id003 + - id: component_waveform_display + type: component + definition: *id004 + - id: api_get_genres + type: api + definition: *id005 +files: + to_create: + - app/upload/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /upload + verification: Navigate to /upload +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v001/dependency_graph.yml b/.workflow/versions/v001/dependency_graph.yml new file mode 100644 index 0000000..371d6fd --- /dev/null +++ b/.workflow/versions/v001/dependency_graph.yml @@ -0,0 +1,1333 @@ +dependency_graph: + design_version: 1 + workflow_version: v001 + generated_at: '2025-12-18T15:16:50.203723' + generator: validate_design.py + stats: + total_entities: 78 + total_layers: 4 + max_parallelism: 38 + critical_path_length: 4 +layers: +- layer: 1 + name: Data Layer + description: Database models - no external dependencies + items: + - id: component_album_card + type: component + name: AlbumCard + depends_on: [] + task_id: task_create_component_album_card + agent: frontend + complexity: medium + - id: component_album_header + type: component + name: AlbumHeader + depends_on: [] + task_id: task_create_component_album_header + agent: frontend + complexity: medium + - id: component_artist_card + type: component + name: ArtistCard + depends_on: [] + task_id: task_create_component_artist_card + agent: frontend + complexity: medium + - id: component_avatar_upload + type: component + name: AvatarUpload + depends_on: [] + task_id: task_create_component_avatar_upload + agent: frontend + complexity: medium + - id: component_genre_badge + type: component + name: GenreBadge + depends_on: [] + task_id: task_create_component_genre_badge + agent: frontend + complexity: medium + - id: component_genre_header + type: component + name: GenreHeader + depends_on: [] + task_id: task_create_component_genre_header + agent: frontend + complexity: medium + - id: component_player_controls + type: component + name: PlayerControls + depends_on: [] + task_id: task_create_component_player_controls + agent: frontend + complexity: medium + - id: component_playlist_card + type: component + name: PlaylistCard + depends_on: [] + task_id: task_create_component_playlist_card + agent: frontend + complexity: medium + - id: component_playlist_header + type: component + name: PlaylistHeader + depends_on: [] + task_id: task_create_component_playlist_header + agent: frontend + complexity: medium + - id: component_section_header + type: component + name: SectionHeader + depends_on: [] + task_id: task_create_component_section_header + agent: frontend + complexity: medium + - id: component_social_links + type: component + name: SocialLinks + depends_on: [] + task_id: task_create_component_social_links + agent: frontend + complexity: medium + - id: component_song_card + type: component + name: SongCard + depends_on: [] + task_id: task_create_component_song_card + agent: frontend + complexity: medium + - id: component_waveform_display + type: component + name: WaveformDisplay + depends_on: [] + task_id: task_create_component_waveform_display + agent: frontend + complexity: medium + - id: model_album + type: model + name: Album + depends_on: [] + task_id: task_create_model_album + agent: backend + complexity: medium + - id: model_artist + type: model + name: Artist + depends_on: [] + task_id: task_create_model_artist + agent: backend + complexity: medium + - id: model_genre + type: model + name: Genre + depends_on: [] + task_id: task_create_model_genre + agent: backend + complexity: medium + - id: model_label + type: model + name: Label + depends_on: [] + task_id: task_create_model_label + agent: backend + complexity: medium + - id: model_playlist + type: model + name: Playlist + depends_on: [] + task_id: task_create_model_playlist + agent: backend + complexity: medium + - id: model_playlist_song + type: model + name: PlaylistSong + depends_on: [] + task_id: task_create_model_playlist_song + agent: backend + complexity: medium + - id: model_song + type: model + name: Song + depends_on: [] + task_id: task_create_model_song + agent: backend + complexity: medium + - id: model_song_genre + type: model + name: SongGenre + depends_on: [] + task_id: task_create_model_song_genre + agent: backend + complexity: medium + - id: model_user + type: model + name: User + depends_on: [] + task_id: task_create_model_user + agent: backend + complexity: medium + requires_layers: [] + parallel_count: 22 +- layer: 2 + name: API Layer + description: REST endpoints - depend on models + items: + - id: api_add_song_to_playlist + type: api + name: api_add_song_to_playlist + depends_on: + - model_playlist + - model_playlist_song + task_id: task_create_api_add_song_to_playlist + agent: backend + complexity: medium + - id: api_create_album + type: api + name: api_create_album + depends_on: + - model_artist + - model_album + task_id: task_create_api_create_album + agent: backend + complexity: medium + - id: api_create_artist_profile + type: api + name: api_create_artist_profile + depends_on: + - model_artist + - model_user + task_id: task_create_api_create_artist_profile + agent: backend + complexity: medium + - id: api_create_label_profile + type: api + name: api_create_label_profile + depends_on: + - model_label + task_id: task_create_api_create_label_profile + agent: backend + complexity: medium + - id: api_create_playlist + type: api + name: api_create_playlist + depends_on: + - model_playlist + task_id: task_create_api_create_playlist + agent: backend + complexity: medium + - id: api_delete_album + type: api + name: api_delete_album + depends_on: + - model_album + task_id: task_create_api_delete_album + agent: backend + complexity: medium + - id: api_delete_playlist + type: api + name: api_delete_playlist + depends_on: + - model_playlist + task_id: task_create_api_delete_playlist + agent: backend + complexity: medium + - id: api_delete_song + type: api + name: api_delete_song + depends_on: + - model_song + task_id: task_create_api_delete_song + agent: backend + complexity: medium + - id: api_forgot_password + type: api + name: api_forgot_password + depends_on: + - model_user + task_id: task_create_api_forgot_password + agent: backend + complexity: medium + - id: api_get_album + type: api + name: api_get_album + depends_on: + - model_artist + - model_album + - model_song + task_id: task_create_api_get_album + agent: backend + complexity: medium + - id: api_get_artist + type: api + name: api_get_artist + depends_on: + - model_artist + task_id: task_create_api_get_artist + agent: backend + complexity: medium + - id: api_get_artist_albums + type: api + name: api_get_artist_albums + depends_on: + - model_artist + - model_album + task_id: task_create_api_get_artist_albums + agent: backend + complexity: medium + - id: api_get_artist_songs + type: api + name: api_get_artist_songs + depends_on: + - model_artist + - model_song + task_id: task_create_api_get_artist_songs + agent: backend + complexity: medium + - id: api_get_current_user + type: api + name: api_get_current_user + depends_on: + - model_user + task_id: task_create_api_get_current_user + agent: backend + complexity: medium + - id: api_get_genres + type: api + name: api_get_genres + depends_on: + - model_genre + task_id: task_create_api_get_genres + agent: backend + complexity: medium + - id: api_get_label_artists + type: api + name: api_get_label_artists + depends_on: + - model_artist + - model_label + task_id: task_create_api_get_label_artists + agent: backend + complexity: medium + - id: api_get_new_releases + type: api + name: api_get_new_releases + depends_on: + - model_song + task_id: task_create_api_get_new_releases + agent: backend + complexity: medium + - id: api_get_playlist + type: api + name: api_get_playlist + depends_on: + - model_playlist + - model_playlist_song + task_id: task_create_api_get_playlist + agent: backend + complexity: medium + - id: api_get_song + type: api + name: api_get_song + depends_on: + - model_artist + - model_album + - model_song + task_id: task_create_api_get_song + agent: backend + complexity: medium + - id: api_get_songs_by_genre + type: api + name: api_get_songs_by_genre + depends_on: + - model_song + - model_genre + task_id: task_create_api_get_songs_by_genre + agent: backend + complexity: medium + - id: api_get_trending_songs + type: api + name: api_get_trending_songs + depends_on: + - model_artist + - model_song + task_id: task_create_api_get_trending_songs + agent: backend + complexity: medium + - id: api_get_user_playlists + type: api + name: api_get_user_playlists + depends_on: + - model_playlist + task_id: task_create_api_get_user_playlists + agent: backend + complexity: medium + - id: api_increment_play_count + type: api + name: api_increment_play_count + depends_on: + - model_song + task_id: task_create_api_increment_play_count + agent: backend + complexity: medium + - id: api_login + type: api + name: api_login + depends_on: + - model_user + task_id: task_create_api_login + agent: backend + complexity: medium + - id: api_register + type: api + name: api_register + depends_on: + - model_user + task_id: task_create_api_register + agent: backend + complexity: medium + - id: api_remove_song_from_playlist + type: api + name: api_remove_song_from_playlist + depends_on: + - model_playlist + - model_playlist_song + task_id: task_create_api_remove_song_from_playlist + agent: backend + complexity: medium + - id: api_reorder_playlist_songs + type: api + name: api_reorder_playlist_songs + depends_on: + - model_playlist + - model_playlist_song + task_id: task_create_api_reorder_playlist_songs + agent: backend + complexity: medium + - id: api_reset_password + type: api + name: api_reset_password + depends_on: + - model_user + task_id: task_create_api_reset_password + agent: backend + complexity: medium + - id: api_search + type: api + name: api_search + depends_on: + - model_artist + - model_album + - model_song + task_id: task_create_api_search + agent: backend + complexity: medium + - id: api_update_album + type: api + name: api_update_album + depends_on: + - model_album + task_id: task_create_api_update_album + agent: backend + complexity: medium + - id: api_update_artist + type: api + name: api_update_artist + depends_on: + - model_artist + task_id: task_create_api_update_artist + agent: backend + complexity: medium + - id: api_update_current_user + type: api + name: api_update_current_user + depends_on: + - model_user + task_id: task_create_api_update_current_user + agent: backend + complexity: medium + - id: api_update_playlist + type: api + name: api_update_playlist + depends_on: + - model_playlist + task_id: task_create_api_update_playlist + agent: backend + complexity: medium + - id: api_update_song + type: api + name: api_update_song + depends_on: + - model_song + task_id: task_create_api_update_song + agent: backend + complexity: medium + - id: api_upload_song + type: api + name: api_upload_song + depends_on: + - model_artist + - model_song + task_id: task_create_api_upload_song + agent: backend + complexity: medium + - id: component_artist_header + type: component + name: ArtistHeader + depends_on: + - component_social_links + task_id: task_create_component_artist_header + agent: frontend + complexity: medium + - id: component_search_results + type: component + name: SearchResults + depends_on: + - component_artist_card + - component_album_card + - component_song_card + task_id: task_create_component_search_results + agent: frontend + complexity: medium + - id: component_track_list + type: component + name: TrackList + depends_on: + - component_song_card + task_id: task_create_component_track_list + agent: frontend + complexity: medium + requires_layers: + - 1 + parallel_count: 38 +- layer: 3 + name: UI Layer + description: Pages and components - depend on APIs + items: + - id: component_audio_player + type: component + name: AudioPlayer + depends_on: + - component_waveform_display + - api_increment_play_count + - component_player_controls + task_id: task_create_component_audio_player + agent: frontend + complexity: medium + - id: component_auth_form + type: component + name: AuthForm + depends_on: + - api_forgot_password + - api_register + - api_login + task_id: task_create_component_auth_form + agent: frontend + complexity: medium + - id: component_create_playlist_modal + type: component + name: CreatePlaylistModal + depends_on: + - api_create_playlist + task_id: task_create_component_create_playlist_modal + agent: frontend + complexity: medium + - id: component_profile_form + type: component + name: ProfileForm + depends_on: + - api_update_current_user + - component_avatar_upload + task_id: task_create_component_profile_form + agent: frontend + complexity: medium + - id: component_search_bar + type: component + name: SearchBar + depends_on: + - api_search + task_id: task_create_component_search_bar + agent: frontend + complexity: medium + - id: component_upload_form + type: component + name: UploadForm + depends_on: + - component_waveform_display + - api_upload_song + task_id: task_create_component_upload_form + agent: frontend + complexity: medium + - id: page_album_detail + type: page + name: page_album_detail + depends_on: + - component_track_list + - component_song_card + - api_get_album + - component_album_header + task_id: task_create_page_album_detail + agent: frontend + complexity: medium + - id: page_artist_profile + type: page + name: page_artist_profile + depends_on: + - api_get_artist_albums + - component_artist_header + - component_album_card + - api_get_artist_songs + - api_get_artist + - component_song_card + - component_social_links + task_id: task_create_page_artist_profile + agent: frontend + complexity: medium + - id: page_genre_browse + type: page + name: page_genre_browse + depends_on: + - component_genre_header + - api_get_songs_by_genre + - component_song_card + task_id: task_create_page_genre_browse + agent: frontend + complexity: medium + - id: page_home + type: page + name: page_home + depends_on: + - component_genre_badge + - component_section_header + - api_get_new_releases + - component_song_card + - api_get_trending_songs + - api_get_genres + task_id: task_create_page_home + agent: frontend + complexity: medium + - id: page_playlist_detail + type: page + name: page_playlist_detail + depends_on: + - component_playlist_header + - component_track_list + - api_get_playlist + - component_song_card + task_id: task_create_page_playlist_detail + agent: frontend + complexity: medium + requires_layers: + - 1 + - 2 + parallel_count: 11 +- layer: 4 + name: Layer 4 + description: Entities with 3 levels of dependencies + items: + - id: page_forgot_password + type: page + name: page_forgot_password + depends_on: + - component_auth_form + - api_forgot_password + task_id: task_create_page_forgot_password + agent: frontend + complexity: medium + - id: page_login + type: page + name: page_login + depends_on: + - component_auth_form + - api_login + task_id: task_create_page_login + agent: frontend + complexity: medium + - id: page_playlists + type: page + name: page_playlists + depends_on: + - component_playlist_card + - api_get_user_playlists + - component_create_playlist_modal + task_id: task_create_page_playlists + agent: frontend + complexity: medium + - id: page_profile + type: page + name: page_profile + depends_on: + - api_update_current_user + - component_profile_form + - api_get_current_user + - component_avatar_upload + task_id: task_create_page_profile + agent: frontend + complexity: medium + - id: page_register + type: page + name: page_register + depends_on: + - component_auth_form + - api_register + task_id: task_create_page_register + agent: frontend + complexity: medium + - id: page_search + type: page + name: page_search + depends_on: + - component_search_results + - component_album_card + - component_artist_card + - component_search_bar + - api_search + - component_song_card + task_id: task_create_page_search + agent: frontend + complexity: medium + - id: page_upload + type: page + name: page_upload + depends_on: + - api_get_artist_albums + - component_upload_form + - api_upload_song + - component_waveform_display + - api_get_genres + task_id: task_create_page_upload + agent: frontend + complexity: medium + requires_layers: + - 1 + - 2 + - 3 + parallel_count: 7 +dependency_map: + model_user: + type: model + layer: 1 + depends_on: [] + depended_by: + - api_register + - api_login + - api_reset_password + - api_update_current_user + - api_create_artist_profile + - api_forgot_password + - api_get_current_user + model_artist: + type: model + layer: 1 + depends_on: [] + depended_by: + - api_update_artist + - api_get_label_artists + - api_create_album + - api_get_artist_albums + - api_get_song + - api_get_album + - api_create_artist_profile + - api_get_artist_songs + - api_get_artist + - api_upload_song + - api_search + - api_get_trending_songs + model_label: + type: model + layer: 1 + depends_on: [] + depended_by: + - api_create_label_profile + - api_get_label_artists + model_genre: + type: model + layer: 1 + depends_on: [] + depended_by: + - api_get_songs_by_genre + - api_get_genres + model_album: + type: model + layer: 1 + depends_on: [] + depended_by: + - api_update_album + - api_delete_album + - api_create_album + - api_get_artist_albums + - api_get_song + - api_get_album + - api_search + model_song: + type: model + layer: 1 + depends_on: [] + depended_by: + - api_get_song + - api_get_songs_by_genre + - api_increment_play_count + - api_get_album + - api_get_artist_songs + - api_get_new_releases + - api_upload_song + - api_search + - api_delete_song + - api_get_trending_songs + - api_update_song + model_song_genre: + type: model + layer: 1 + depends_on: [] + depended_by: [] + model_playlist: + type: model + layer: 1 + depends_on: [] + depended_by: + - api_create_playlist + - api_reorder_playlist_songs + - api_update_playlist + - api_add_song_to_playlist + - api_get_user_playlists + - api_get_playlist + - api_remove_song_from_playlist + - api_delete_playlist + model_playlist_song: + type: model + layer: 1 + depends_on: [] + depended_by: + - api_add_song_to_playlist + - api_reorder_playlist_songs + - api_get_playlist + - api_remove_song_from_playlist + api_register: + type: api + layer: 2 + depends_on: + - model_user + depended_by: + - page_register + - component_auth_form + api_login: + type: api + layer: 2 + depends_on: + - model_user + depended_by: + - component_auth_form + - page_login + api_forgot_password: + type: api + layer: 2 + depends_on: + - model_user + depended_by: + - component_auth_form + - page_forgot_password + api_reset_password: + type: api + layer: 2 + depends_on: + - model_user + depended_by: [] + api_get_current_user: + type: api + layer: 2 + depends_on: + - model_user + depended_by: + - page_profile + api_update_current_user: + type: api + layer: 2 + depends_on: + - model_user + depended_by: + - component_profile_form + - page_profile + api_create_artist_profile: + type: api + layer: 2 + depends_on: + - model_artist + - model_user + depended_by: [] + api_get_artist: + type: api + layer: 2 + depends_on: + - model_artist + depended_by: + - page_artist_profile + api_update_artist: + type: api + layer: 2 + depends_on: + - model_artist + depended_by: [] + api_get_artist_songs: + type: api + layer: 2 + depends_on: + - model_artist + - model_song + depended_by: + - page_artist_profile + api_get_artist_albums: + type: api + layer: 2 + depends_on: + - model_artist + - model_album + depended_by: + - page_artist_profile + - page_upload + api_upload_song: + type: api + layer: 2 + depends_on: + - model_artist + - model_song + depended_by: + - page_upload + - component_upload_form + api_get_song: + type: api + layer: 2 + depends_on: + - model_artist + - model_album + - model_song + depended_by: [] + api_update_song: + type: api + layer: 2 + depends_on: + - model_song + depended_by: [] + api_delete_song: + type: api + layer: 2 + depends_on: + - model_song + depended_by: [] + api_increment_play_count: + type: api + layer: 2 + depends_on: + - model_song + depended_by: + - component_audio_player + api_create_album: + type: api + layer: 2 + depends_on: + - model_artist + - model_album + depended_by: [] + api_get_album: + type: api + layer: 2 + depends_on: + - model_artist + - model_album + - model_song + depended_by: + - page_album_detail + api_update_album: + type: api + layer: 2 + depends_on: + - model_album + depended_by: [] + api_delete_album: + type: api + layer: 2 + depends_on: + - model_album + depended_by: [] + api_create_playlist: + type: api + layer: 2 + depends_on: + - model_playlist + depended_by: + - component_create_playlist_modal + api_get_user_playlists: + type: api + layer: 2 + depends_on: + - model_playlist + depended_by: + - page_playlists + api_get_playlist: + type: api + layer: 2 + depends_on: + - model_playlist + - model_playlist_song + depended_by: + - page_playlist_detail + api_update_playlist: + type: api + layer: 2 + depends_on: + - model_playlist + depended_by: [] + api_delete_playlist: + type: api + layer: 2 + depends_on: + - model_playlist + depended_by: [] + api_add_song_to_playlist: + type: api + layer: 2 + depends_on: + - model_playlist + - model_playlist_song + depended_by: [] + api_remove_song_from_playlist: + type: api + layer: 2 + depends_on: + - model_playlist + - model_playlist_song + depended_by: [] + api_reorder_playlist_songs: + type: api + layer: 2 + depends_on: + - model_playlist + - model_playlist_song + depended_by: [] + api_get_trending_songs: + type: api + layer: 2 + depends_on: + - model_artist + - model_song + depended_by: + - page_home + api_get_new_releases: + type: api + layer: 2 + depends_on: + - model_song + depended_by: + - page_home + api_get_genres: + type: api + layer: 2 + depends_on: + - model_genre + depended_by: + - page_upload + - page_home + api_get_songs_by_genre: + type: api + layer: 2 + depends_on: + - model_song + - model_genre + depended_by: + - page_genre_browse + api_search: + type: api + layer: 2 + depends_on: + - model_artist + - model_album + - model_song + depended_by: + - page_search + - component_search_bar + api_create_label_profile: + type: api + layer: 2 + depends_on: + - model_label + depended_by: [] + api_get_label_artists: + type: api + layer: 2 + depends_on: + - model_artist + - model_label + depended_by: [] + page_login: + type: page + layer: 4 + depends_on: + - component_auth_form + - api_login + depended_by: [] + page_register: + type: page + layer: 4 + depends_on: + - component_auth_form + - api_register + depended_by: [] + page_forgot_password: + type: page + layer: 4 + depends_on: + - component_auth_form + - api_forgot_password + depended_by: [] + page_home: + type: page + layer: 3 + depends_on: + - component_genre_badge + - component_section_header + - api_get_new_releases + - component_song_card + - api_get_trending_songs + - api_get_genres + depended_by: [] + page_artist_profile: + type: page + layer: 3 + depends_on: + - api_get_artist_albums + - component_artist_header + - component_album_card + - api_get_artist_songs + - api_get_artist + - component_song_card + - component_social_links + depended_by: [] + page_album_detail: + type: page + layer: 3 + depends_on: + - component_track_list + - component_song_card + - api_get_album + - component_album_header + depended_by: [] + page_upload: + type: page + layer: 4 + depends_on: + - api_get_artist_albums + - component_upload_form + - api_upload_song + - component_waveform_display + - api_get_genres + depended_by: [] + page_playlists: + type: page + layer: 4 + depends_on: + - component_playlist_card + - api_get_user_playlists + - component_create_playlist_modal + depended_by: [] + page_playlist_detail: + type: page + layer: 3 + depends_on: + - component_playlist_header + - component_track_list + - api_get_playlist + - component_song_card + depended_by: [] + page_profile: + type: page + layer: 4 + depends_on: + - api_update_current_user + - component_profile_form + - api_get_current_user + - component_avatar_upload + depended_by: [] + page_search: + type: page + layer: 4 + depends_on: + - component_search_results + - component_album_card + - component_artist_card + - component_search_bar + - api_search + - component_song_card + depended_by: [] + page_genre_browse: + type: page + layer: 3 + depends_on: + - component_genre_header + - api_get_songs_by_genre + - component_song_card + depended_by: [] + component_audio_player: + type: component + layer: 3 + depends_on: + - component_waveform_display + - api_increment_play_count + - component_player_controls + depended_by: [] + component_player_controls: + type: component + layer: 1 + depends_on: [] + depended_by: + - component_audio_player + component_song_card: + type: component + layer: 1 + depends_on: [] + depended_by: + - component_track_list + - page_search + - component_search_results + - page_genre_browse + - page_artist_profile + - page_home + - page_playlist_detail + - page_album_detail + component_album_card: + type: component + layer: 1 + depends_on: [] + depended_by: + - page_artist_profile + - page_search + - component_search_results + component_artist_card: + type: component + layer: 1 + depends_on: [] + depended_by: + - page_search + - component_search_results + component_playlist_card: + type: component + layer: 1 + depends_on: [] + depended_by: + - page_playlists + component_upload_form: + type: component + layer: 3 + depends_on: + - component_waveform_display + - api_upload_song + depended_by: + - page_upload + component_waveform_display: + type: component + layer: 1 + depends_on: [] + depended_by: + - page_upload + - component_audio_player + - component_upload_form + component_genre_badge: + type: component + layer: 1 + depends_on: [] + depended_by: + - page_home + component_track_list: + type: component + layer: 2 + depends_on: + - component_song_card + depended_by: + - page_album_detail + - page_playlist_detail + component_artist_header: + type: component + layer: 2 + depends_on: + - component_social_links + depended_by: + - page_artist_profile + component_album_header: + type: component + layer: 1 + depends_on: [] + depended_by: + - page_album_detail + component_playlist_header: + type: component + layer: 1 + depends_on: [] + depended_by: + - page_playlist_detail + component_social_links: + type: component + layer: 1 + depends_on: [] + depended_by: + - page_artist_profile + - component_artist_header + component_auth_form: + type: component + layer: 3 + depends_on: + - api_forgot_password + - api_register + - api_login + depended_by: + - page_register + - page_login + - page_forgot_password + component_search_bar: + type: component + layer: 3 + depends_on: + - api_search + depended_by: + - page_search + component_search_results: + type: component + layer: 2 + depends_on: + - component_artist_card + - component_album_card + - component_song_card + depended_by: + - page_search + component_create_playlist_modal: + type: component + layer: 3 + depends_on: + - api_create_playlist + depended_by: + - page_playlists + component_profile_form: + type: component + layer: 3 + depends_on: + - api_update_current_user + - component_avatar_upload + depended_by: + - page_profile + component_avatar_upload: + type: component + layer: 1 + depends_on: [] + depended_by: + - component_profile_form + - page_profile + component_section_header: + type: component + layer: 1 + depends_on: [] + depended_by: + - page_home + component_genre_header: + type: component + layer: 1 + depends_on: [] + depended_by: + - page_genre_browse +task_map: [] diff --git a/.workflow/versions/v001/design/design_document.yml b/.workflow/versions/v001/design/design_document.yml new file mode 100644 index 0000000..935d9ab --- /dev/null +++ b/.workflow/versions/v001/design/design_document.yml @@ -0,0 +1,1821 @@ +workflow_version: "v001" +feature: "Music platform for musicians to upload songs" +created_at: "2025-12-18T15:10:00Z" +status: draft +revision: 1 + +# ═══════════════════════════════════════════════════════════════ +# LAYER 1: DATA MODELS +# ═══════════════════════════════════════════════════════════════ + +data_models: + - id: model_user + name: User + table_name: users + description: Base user entity with authentication + fields: + - name: id + type: uuid + constraints: [primary_key] + - name: email + type: string + constraints: [unique, not_null] + - name: password_hash + type: string + constraints: [not_null] + - name: name + type: string + constraints: [not_null] + - name: role + type: enum + values: [musician, listener, label] + constraints: [not_null] + - name: email_verified + type: boolean + default: false + - name: avatar_url + type: string + constraints: [nullable] + - name: created_at + type: timestamp + constraints: [not_null] + - name: updated_at + type: timestamp + constraints: [not_null] + relations: + - type: has_one + to: model_artist + foreign_key: user_id + condition: "role = 'musician'" + - type: has_one + to: model_label + foreign_key: user_id + condition: "role = 'label'" + - type: has_many + to: model_playlist + foreign_key: user_id + indexes: + - fields: [email] + unique: true + - fields: [role] + timestamps: true + + - id: model_artist + name: Artist + table_name: artists + description: Extended profile for musicians + fields: + - name: id + type: uuid + constraints: [primary_key] + - name: user_id + type: uuid + constraints: [not_null, foreign_key] + references: users.id + - name: stage_name + type: string + constraints: [not_null] + - name: bio + type: text + constraints: [nullable] + - name: cover_image_url + type: string + constraints: [nullable] + - name: social_links + type: jsonb + description: "JSON object with {twitter, instagram, facebook, website}" + constraints: [nullable] + - name: verified + type: boolean + default: false + - name: created_at + type: timestamp + constraints: [not_null] + - name: updated_at + type: timestamp + constraints: [not_null] + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_song + foreign_key: artist_id + - type: has_many + to: model_album + foreign_key: artist_id + - type: belongs_to + to: model_label + foreign_key: label_id + optional: true + indexes: + - fields: [user_id] + unique: true + - fields: [stage_name] + timestamps: true + + - id: model_label + name: Label + table_name: labels + description: Organization profile for labels + fields: + - name: id + type: uuid + constraints: [primary_key] + - name: user_id + type: uuid + constraints: [not_null, foreign_key] + references: users.id + - name: label_name + type: string + constraints: [not_null] + - name: description + type: text + constraints: [nullable] + - name: logo_url + type: string + constraints: [nullable] + - name: website + type: string + constraints: [nullable] + - name: created_at + type: timestamp + constraints: [not_null] + - name: updated_at + type: timestamp + constraints: [not_null] + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_artist + foreign_key: label_id + indexes: + - fields: [user_id] + unique: true + timestamps: true + + - id: model_genre + name: Genre + table_name: genres + description: Music category for discovery + fields: + - name: id + type: uuid + constraints: [primary_key] + - name: name + type: string + constraints: [unique, not_null] + - name: slug + type: string + constraints: [unique, not_null] + - name: description + type: text + constraints: [nullable] + - name: created_at + type: timestamp + constraints: [not_null] + - name: updated_at + type: timestamp + constraints: [not_null] + relations: + - type: has_many + to: model_song + through: song_genres + foreign_key: genre_id + indexes: + - fields: [slug] + unique: true + timestamps: true + + - id: model_album + name: Album + table_name: albums + description: Collection of songs + fields: + - name: id + type: uuid + constraints: [primary_key] + - name: artist_id + type: uuid + constraints: [not_null, foreign_key] + references: artists.id + - name: title + type: string + constraints: [not_null] + - name: description + type: text + constraints: [nullable] + - name: cover_art_url + type: string + constraints: [nullable] + - name: release_date + type: date + constraints: [nullable] + - name: album_type + type: enum + values: [album, ep, single] + default: album + - name: created_at + type: timestamp + constraints: [not_null] + - name: updated_at + type: timestamp + constraints: [not_null] + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: has_many + to: model_song + foreign_key: album_id + indexes: + - fields: [artist_id] + - fields: [release_date] + timestamps: true + + - id: model_song + name: Song + table_name: songs + description: Audio track with metadata + fields: + - name: id + type: uuid + constraints: [primary_key] + - name: artist_id + type: uuid + constraints: [not_null, foreign_key] + references: artists.id + - name: album_id + type: uuid + constraints: [nullable, foreign_key] + references: albums.id + - name: title + type: string + constraints: [not_null] + - name: duration + type: integer + description: Duration in seconds + constraints: [not_null] + - name: file_url + type: string + description: Cloud storage URL for audio file + constraints: [not_null] + - name: file_format + type: enum + values: [mp3, wav] + constraints: [not_null] + - name: file_size + type: integer + description: File size in bytes + constraints: [not_null] + - name: waveform_data + type: jsonb + description: Waveform visualization data + constraints: [nullable] + - name: cover_art_url + type: string + constraints: [nullable] + - name: release_date + type: date + constraints: [nullable] + - name: play_count + type: integer + default: 0 + - name: is_public + type: boolean + default: true + - name: track_number + type: integer + description: Position in album + constraints: [nullable] + - name: created_at + type: timestamp + constraints: [not_null] + - name: updated_at + type: timestamp + constraints: [not_null] + relations: + - type: belongs_to + to: model_artist + foreign_key: artist_id + - type: belongs_to + to: model_album + foreign_key: album_id + optional: true + - type: has_many + to: model_genre + through: song_genres + foreign_key: song_id + - type: has_many + to: model_playlist_song + foreign_key: song_id + indexes: + - fields: [artist_id] + - fields: [album_id] + - fields: [release_date] + - fields: [play_count] + - fields: [is_public] + timestamps: true + + - id: model_song_genre + name: SongGenre + table_name: song_genres + description: Junction table for song-genre many-to-many + fields: + - name: song_id + type: uuid + constraints: [not_null, foreign_key] + references: songs.id + - name: genre_id + type: uuid + constraints: [not_null, foreign_key] + references: genres.id + relations: + - type: belongs_to + to: model_song + foreign_key: song_id + - type: belongs_to + to: model_genre + foreign_key: genre_id + indexes: + - fields: [song_id, genre_id] + unique: true + timestamps: false + + - id: model_playlist + name: Playlist + table_name: playlists + description: User-created song collection + fields: + - name: id + type: uuid + constraints: [primary_key] + - name: user_id + type: uuid + constraints: [not_null, foreign_key] + references: users.id + - name: name + type: string + constraints: [not_null] + - name: description + type: text + constraints: [nullable] + - name: cover_image_url + type: string + constraints: [nullable] + - name: is_public + type: boolean + default: false + - name: created_at + type: timestamp + constraints: [not_null] + - name: updated_at + type: timestamp + constraints: [not_null] + relations: + - type: belongs_to + to: model_user + foreign_key: user_id + - type: has_many + to: model_playlist_song + foreign_key: playlist_id + indexes: + - fields: [user_id] + - fields: [is_public] + timestamps: true + + - id: model_playlist_song + name: PlaylistSong + table_name: playlist_songs + description: Junction table with ordering for playlists + fields: + - name: id + type: uuid + constraints: [primary_key] + - name: playlist_id + type: uuid + constraints: [not_null, foreign_key] + references: playlists.id + - name: song_id + type: uuid + constraints: [not_null, foreign_key] + references: songs.id + - name: position + type: integer + constraints: [not_null] + - name: added_at + type: timestamp + constraints: [not_null] + relations: + - type: belongs_to + to: model_playlist + foreign_key: playlist_id + - type: belongs_to + to: model_song + foreign_key: song_id + indexes: + - fields: [playlist_id, position] + unique: true + - fields: [playlist_id, song_id] + unique: true + timestamps: false + +# ═══════════════════════════════════════════════════════════════ +# LAYER 2: API ENDPOINTS +# ═══════════════════════════════════════════════════════════════ + +api_endpoints: + # ───────────────────────────────────────────────────────────── + # AUTH ENDPOINTS + # ───────────────────────────────────────────────────────────── + - id: api_register + method: POST + path: /api/auth/register + description: Register new user account + request_body: + email: string + password: string + name: string + role: enum[musician, listener, label] + responses: + - status: 201 + description: User created successfully + schema: + user: + id: uuid + email: string + name: string + role: string + token: string + - status: 400 + description: Validation error + schema: + error: string + - status: 409 + description: Email already exists + schema: + error: string + auth: + required: false + depends_on_models: [model_user] + + - id: api_login + method: POST + path: /api/auth/login + description: Login with email and password + request_body: + email: string + password: string + responses: + - status: 200 + description: Login successful + schema: + user: + id: uuid + email: string + name: string + role: string + token: string + - status: 401 + description: Invalid credentials + schema: + error: string + auth: + required: false + depends_on_models: [model_user] + + - id: api_forgot_password + method: POST + path: /api/auth/forgot-password + description: Request password reset email + request_body: + email: string + responses: + - status: 200 + description: Reset email sent + schema: + message: string + - status: 404 + description: Email not found + schema: + error: string + auth: + required: false + depends_on_models: [model_user] + + - id: api_reset_password + method: POST + path: /api/auth/reset-password + description: Reset password with token + request_body: + token: string + password: string + responses: + - status: 200 + description: Password reset successful + schema: + message: string + - status: 400 + description: Invalid or expired token + schema: + error: string + auth: + required: false + depends_on_models: [model_user] + + # ───────────────────────────────────────────────────────────── + # USER ENDPOINTS + # ───────────────────────────────────────────────────────────── + - id: api_get_current_user + method: GET + path: /api/users/me + description: Get current user profile + responses: + - status: 200 + description: User profile + schema: + id: uuid + email: string + name: string + role: string + avatar_url: string + auth: + required: true + depends_on_models: [model_user] + + - id: api_update_current_user + method: PUT + path: /api/users/me + description: Update current user profile + request_body: + name: string + avatar_url: string + responses: + - status: 200 + description: User updated + schema: + id: uuid + email: string + name: string + avatar_url: string + auth: + required: true + depends_on_models: [model_user] + + # ───────────────────────────────────────────────────────────── + # ARTIST ENDPOINTS + # ───────────────────────────────────────────────────────────── + - id: api_create_artist_profile + method: POST + path: /api/artists + description: Create artist profile (musicians only) + request_body: + stage_name: string + bio: string + cover_image_url: string + social_links: + twitter: string + instagram: string + facebook: string + website: string + responses: + - status: 201 + description: Artist profile created + schema: + id: uuid + stage_name: string + bio: string + cover_image_url: string + - status: 403 + description: User is not a musician + schema: + error: string + auth: + required: true + roles: [musician] + depends_on_models: [model_artist, model_user] + + - id: api_get_artist + method: GET + path: /api/artists/:id + description: Get artist profile by ID + responses: + - status: 200 + description: Artist profile + schema: + id: uuid + stage_name: string + bio: string + cover_image_url: string + social_links: object + verified: boolean + - status: 404 + description: Artist not found + schema: + error: string + auth: + required: false + depends_on_models: [model_artist] + + - id: api_update_artist + method: PUT + path: /api/artists/:id + description: Update artist profile + request_body: + stage_name: string + bio: string + cover_image_url: string + social_links: object + responses: + - status: 200 + description: Artist updated + schema: + id: uuid + stage_name: string + bio: string + - status: 403 + description: Unauthorized + schema: + error: string + auth: + required: true + owner_only: true + depends_on_models: [model_artist] + + - id: api_get_artist_songs + method: GET + path: /api/artists/:id/songs + description: Get all songs by artist + responses: + - status: 200 + description: List of songs + schema: + songs: + - id: uuid + title: string + duration: integer + cover_art_url: string + play_count: integer + auth: + required: false + depends_on_models: [model_artist, model_song] + + - id: api_get_artist_albums + method: GET + path: /api/artists/:id/albums + description: Get all albums by artist + responses: + - status: 200 + description: List of albums + schema: + albums: + - id: uuid + title: string + cover_art_url: string + release_date: string + album_type: string + auth: + required: false + depends_on_models: [model_artist, model_album] + + # ───────────────────────────────────────────────────────────── + # SONG ENDPOINTS + # ───────────────────────────────────────────────────────────── + - id: api_upload_song + method: POST + path: /api/songs/upload + description: Upload new song (musicians only) + request_body: + file: binary + title: string + album_id: uuid + genre_ids: array[uuid] + release_date: string + track_number: integer + responses: + - status: 201 + description: Song uploaded successfully + schema: + id: uuid + title: string + file_url: string + duration: integer + - status: 400 + description: Invalid file format or size + schema: + error: string + - status: 403 + description: User is not a musician + schema: + error: string + auth: + required: true + roles: [musician] + depends_on_models: [model_song, model_artist] + + - id: api_get_song + method: GET + path: /api/songs/:id + description: Get song details + responses: + - status: 200 + description: Song details + schema: + id: uuid + title: string + duration: integer + file_url: string + cover_art_url: string + artist: + id: uuid + stage_name: string + album: + id: uuid + title: string + genres: array + play_count: integer + auth: + required: false + depends_on_models: [model_song, model_artist, model_album] + + - id: api_update_song + method: PUT + path: /api/songs/:id + description: Update song metadata + request_body: + title: string + album_id: uuid + genre_ids: array[uuid] + release_date: string + is_public: boolean + responses: + - status: 200 + description: Song updated + schema: + id: uuid + title: string + - status: 403 + description: Unauthorized + schema: + error: string + auth: + required: true + owner_only: true + depends_on_models: [model_song] + + - id: api_delete_song + method: DELETE + path: /api/songs/:id + description: Delete song + responses: + - status: 204 + description: Song deleted + - status: 403 + description: Unauthorized + schema: + error: string + auth: + required: true + owner_only: true + depends_on_models: [model_song] + + - id: api_increment_play_count + method: POST + path: /api/songs/:id/play + description: Increment play count + request_body: null # Action endpoint - no body needed + responses: + - status: 200 + description: Play count incremented + schema: + play_count: integer + auth: + required: false + depends_on_models: [model_song] + + # ───────────────────────────────────────────────────────────── + # ALBUM ENDPOINTS + # ───────────────────────────────────────────────────────────── + - id: api_create_album + method: POST + path: /api/albums + description: Create new album + request_body: + title: string + description: string + cover_art_url: string + release_date: string + album_type: enum[album, ep, single] + responses: + - status: 201 + description: Album created + schema: + id: uuid + title: string + cover_art_url: string + auth: + required: true + roles: [musician] + depends_on_models: [model_album, model_artist] + + - id: api_get_album + method: GET + path: /api/albums/:id + description: Get album details with songs + responses: + - status: 200 + description: Album details + schema: + id: uuid + title: string + description: string + cover_art_url: string + release_date: string + artist: + id: uuid + stage_name: string + songs: + - id: uuid + title: string + duration: integer + track_number: integer + auth: + required: false + depends_on_models: [model_album, model_song, model_artist] + + - id: api_update_album + method: PUT + path: /api/albums/:id + description: Update album metadata + request_body: + title: string + description: string + cover_art_url: string + release_date: string + responses: + - status: 200 + description: Album updated + schema: + id: uuid + title: string + auth: + required: true + owner_only: true + depends_on_models: [model_album] + + - id: api_delete_album + method: DELETE + path: /api/albums/:id + description: Delete album + responses: + - status: 204 + description: Album deleted + - status: 403 + description: Unauthorized + schema: + error: string + auth: + required: true + owner_only: true + depends_on_models: [model_album] + + # ───────────────────────────────────────────────────────────── + # PLAYLIST ENDPOINTS + # ───────────────────────────────────────────────────────────── + - id: api_create_playlist + method: POST + path: /api/playlists + description: Create new playlist + request_body: + name: string + description: string + is_public: boolean + responses: + - status: 201 + description: Playlist created + schema: + id: uuid + name: string + description: string + auth: + required: true + depends_on_models: [model_playlist] + + - id: api_get_user_playlists + method: GET + path: /api/playlists + description: Get current user's playlists + responses: + - status: 200 + description: List of playlists + schema: + playlists: + - id: uuid + name: string + cover_image_url: string + song_count: integer + auth: + required: true + depends_on_models: [model_playlist] + + - id: api_get_playlist + method: GET + path: /api/playlists/:id + description: Get playlist details with songs + responses: + - status: 200 + description: Playlist details + schema: + id: uuid + name: string + description: string + songs: + - id: uuid + title: string + artist: + stage_name: string + position: integer + auth: + required: false + depends_on_models: [model_playlist, model_playlist_song] + + - id: api_update_playlist + method: PUT + path: /api/playlists/:id + description: Update playlist metadata + request_body: + name: string + description: string + is_public: boolean + responses: + - status: 200 + description: Playlist updated + schema: + id: uuid + name: string + auth: + required: true + owner_only: true + depends_on_models: [model_playlist] + + - id: api_delete_playlist + method: DELETE + path: /api/playlists/:id + description: Delete playlist + responses: + - status: 204 + description: Playlist deleted + auth: + required: true + owner_only: true + depends_on_models: [model_playlist] + + - id: api_add_song_to_playlist + method: POST + path: /api/playlists/:id/songs + description: Add song to playlist + request_body: + song_id: uuid + position: integer + responses: + - status: 201 + description: Song added to playlist + schema: + playlist_id: uuid + song_id: uuid + position: integer + auth: + required: true + owner_only: true + depends_on_models: [model_playlist, model_playlist_song] + + - id: api_remove_song_from_playlist + method: DELETE + path: /api/playlists/:playlistId/songs/:songId + description: Remove song from playlist + responses: + - status: 204 + description: Song removed from playlist + auth: + required: true + owner_only: true + depends_on_models: [model_playlist, model_playlist_song] + + - id: api_reorder_playlist_songs + method: PUT + path: /api/playlists/:id/reorder + description: Reorder songs in playlist + request_body: + song_ids: array[uuid] + responses: + - status: 200 + description: Playlist reordered + schema: + message: string + auth: + required: true + owner_only: true + depends_on_models: [model_playlist, model_playlist_song] + + # ───────────────────────────────────────────────────────────── + # DISCOVERY ENDPOINTS + # ───────────────────────────────────────────────────────────── + - id: api_get_trending_songs + method: GET + path: /api/discover/trending + description: Get trending songs + query_params: + limit: integer + offset: integer + responses: + - status: 200 + description: List of trending songs + schema: + songs: + - id: uuid + title: string + artist: + stage_name: string + play_count: integer + auth: + required: false + depends_on_models: [model_song, model_artist] + + - id: api_get_new_releases + method: GET + path: /api/discover/new-releases + description: Get recently released songs + query_params: + limit: integer + offset: integer + responses: + - status: 200 + description: List of new releases + schema: + songs: + - id: uuid + title: string + release_date: string + auth: + required: false + depends_on_models: [model_song] + + - id: api_get_genres + method: GET + path: /api/discover/genres + description: Get all genres + responses: + - status: 200 + description: List of genres + schema: + genres: + - id: uuid + name: string + slug: string + auth: + required: false + depends_on_models: [model_genre] + + - id: api_get_songs_by_genre + method: GET + path: /api/discover/genres/:slug + description: Get songs by genre + query_params: + limit: integer + offset: integer + responses: + - status: 200 + description: List of songs in genre + schema: + genre: + name: string + songs: array + auth: + required: false + depends_on_models: [model_genre, model_song] + + - id: api_search + method: GET + path: /api/search + description: Search songs, artists, and albums + query_params: + q: string + type: enum[song, artist, album, all] + limit: integer + responses: + - status: 200 + description: Search results + schema: + songs: array + artists: array + albums: array + auth: + required: false + depends_on_models: [model_song, model_artist, model_album] + + # ───────────────────────────────────────────────────────────── + # LABEL ENDPOINTS (MVP) + # ───────────────────────────────────────────────────────────── + - id: api_create_label_profile + method: POST + path: /api/labels + description: Create label profile + request_body: + label_name: string + description: string + logo_url: string + website: string + responses: + - status: 201 + description: Label created + schema: + id: uuid + label_name: string + auth: + required: true + roles: [label] + depends_on_models: [model_label] + + - id: api_get_label_artists + method: GET + path: /api/labels/:id/artists + description: Get artists under label + responses: + - status: 200 + description: List of artists + schema: + artists: + - id: uuid + stage_name: string + auth: + required: false + depends_on_models: [model_label, model_artist] + +# ═══════════════════════════════════════════════════════════════ +# LAYER 3: PAGES +# ═══════════════════════════════════════════════════════════════ + +pages: + - id: page_login + path: /login + title: Login + description: User login page + data_needs: + - api_id: api_login + purpose: Authenticate user + on_load: false + components: + - component_auth_form + auth: + required: false + redirect_if_authenticated: / + + - id: page_register + path: /register + title: Register + description: User registration page + data_needs: + - api_id: api_register + purpose: Create new account + on_load: false + components: + - component_auth_form + auth: + required: false + redirect_if_authenticated: / + + - id: page_forgot_password + path: /forgot-password + title: Forgot Password + description: Password reset request page + data_needs: + - api_id: api_forgot_password + purpose: Request password reset + on_load: false + components: + - component_auth_form + auth: + required: false + + - id: page_home + path: / + title: Discover Music + description: Main discovery feed + data_needs: + - api_id: api_get_trending_songs + purpose: Show trending songs + on_load: true + - api_id: api_get_new_releases + purpose: Show new releases + on_load: true + - api_id: api_get_genres + purpose: Genre navigation + on_load: true + components: + - component_song_card + - component_genre_badge + - component_section_header + auth: + required: false + + - id: page_artist_profile + path: /artist/:id + title: Artist Profile + description: Artist profile with songs and albums + data_needs: + - api_id: api_get_artist + purpose: Artist details + on_load: true + - api_id: api_get_artist_songs + purpose: Artist songs + on_load: true + - api_id: api_get_artist_albums + purpose: Artist albums + on_load: true + components: + - component_artist_header + - component_song_card + - component_album_card + - component_social_links + auth: + required: false + + - id: page_album_detail + path: /album/:id + title: Album + description: Album detail with track list + data_needs: + - api_id: api_get_album + purpose: Album details and songs + on_load: true + components: + - component_album_header + - component_track_list + - component_song_card + auth: + required: false + + - id: page_upload + path: /upload + title: Upload Music + description: Song upload page (musicians only) + data_needs: + - api_id: api_upload_song + purpose: Upload song file + on_load: false + - api_id: api_get_artist_albums + purpose: Select album + on_load: true + - api_id: api_get_genres + purpose: Select genres + on_load: true + components: + - component_upload_form + - component_waveform_display + auth: + required: true + roles: [musician] + redirect_if_unauthorized: /login + + - id: page_playlists + path: /playlists + title: My Playlists + description: User's playlists + data_needs: + - api_id: api_get_user_playlists + purpose: Load playlists + on_load: true + components: + - component_playlist_card + - component_create_playlist_modal + auth: + required: true + redirect_if_unauthorized: /login + + - id: page_playlist_detail + path: /playlist/:id + title: Playlist + description: Playlist detail with songs + data_needs: + - api_id: api_get_playlist + purpose: Playlist details and songs + on_load: true + components: + - component_playlist_header + - component_track_list + - component_song_card + auth: + required: false + + - id: page_profile + path: /profile + title: Profile Settings + description: User profile settings + data_needs: + - api_id: api_get_current_user + purpose: Load user data + on_load: true + - api_id: api_update_current_user + purpose: Update profile + on_load: false + components: + - component_profile_form + - component_avatar_upload + auth: + required: true + redirect_if_unauthorized: /login + + - id: page_search + path: /search + title: Search + description: Search results page + data_needs: + - api_id: api_search + purpose: Search songs, artists, albums + on_load: false + components: + - component_search_bar + - component_search_results + - component_song_card + - component_artist_card + - component_album_card + auth: + required: false + + - id: page_genre_browse + path: /genre/:slug + title: Browse Genre + description: Browse songs by genre + data_needs: + - api_id: api_get_songs_by_genre + purpose: Load genre songs + on_load: true + components: + - component_genre_header + - component_song_card + auth: + required: false + +# ═══════════════════════════════════════════════════════════════ +# LAYER 3: COMPONENTS +# ═══════════════════════════════════════════════════════════════ + +components: + - id: component_audio_player + name: AudioPlayer + description: Global audio player with full controls + props: + - name: currentSong + type: Song + required: false + - name: queue + type: array[Song] + required: false + - name: autoplay + type: boolean + default: false + state: + - name: isPlaying + type: boolean + - name: currentTime + type: number + - name: volume + type: number + - name: isShuffle + type: boolean + - name: repeatMode + type: enum[off, one, all] + events: + - name: onPlay + payload: + songId: string + - name: onPause + payload: null + - name: onSeek + payload: + time: number + - name: onVolumeChange + payload: + volume: number + - name: onNext + payload: null + - name: onPrevious + payload: null + - name: onShuffle + payload: null + - name: onRepeat + payload: null + uses_apis: + - api_increment_play_count + uses_components: + - component_waveform_display + - component_player_controls + + - id: component_player_controls + name: PlayerControls + description: Play/pause/seek controls + props: + - name: isPlaying + type: boolean + required: true + - name: currentTime + type: number + required: true + - name: duration + type: number + required: true + events: + - name: onPlay + payload: null + - name: onPause + payload: null + - name: onSeek + payload: + time: number + uses_apis: [] + uses_components: [] + + - id: component_song_card + name: SongCard + description: Song display card with play button + props: + - name: song + type: Song + required: true + - name: showArtist + type: boolean + default: true + - name: showAlbum + type: boolean + default: false + events: + - name: onPlay + payload: + songId: string + - name: onAddToPlaylist + payload: + songId: string + uses_apis: [] + uses_components: [] + + - id: component_album_card + name: AlbumCard + description: Album display card + props: + - name: album + type: Album + required: true + - name: showArtist + type: boolean + default: true + events: + - name: onClick + payload: + albumId: string + uses_apis: [] + uses_components: [] + + - id: component_artist_card + name: ArtistCard + description: Artist preview card + props: + - name: artist + type: Artist + required: true + events: + - name: onClick + payload: + artistId: string + uses_apis: [] + uses_components: [] + + - id: component_playlist_card + name: PlaylistCard + description: Playlist preview card + props: + - name: playlist + type: Playlist + required: true + events: + - name: onClick + payload: + playlistId: string + uses_apis: [] + uses_components: [] + + - id: component_upload_form + name: UploadForm + description: Song upload form with file input + props: + - name: albums + type: array[Album] + required: true + - name: genres + type: array[Genre] + required: true + state: + - name: file + type: File + - name: title + type: string + - name: selectedAlbum + type: string + - name: selectedGenres + type: array[string] + - name: uploadProgress + type: number + events: + - name: onUpload + payload: + file: File + metadata: object + - name: onCancel + payload: null + uses_apis: + - api_upload_song + uses_components: + - component_waveform_display + + - id: component_waveform_display + name: WaveformDisplay + description: Audio waveform visualization + props: + - name: audioUrl + type: string + required: true + - name: waveformData + type: array[number] + required: false + - name: currentTime + type: number + required: false + events: + - name: onSeek + payload: + time: number + uses_apis: [] + uses_components: [] + + - id: component_genre_badge + name: GenreBadge + description: Genre tag display + props: + - name: genre + type: Genre + required: true + - name: clickable + type: boolean + default: true + events: + - name: onClick + payload: + genreSlug: string + uses_apis: [] + uses_components: [] + + - id: component_track_list + name: TrackList + description: List of songs with track numbers + props: + - name: songs + type: array[Song] + required: true + - name: showTrackNumber + type: boolean + default: true + - name: reorderable + type: boolean + default: false + events: + - name: onPlay + payload: + songId: string + - name: onReorder + payload: + songIds: array[string] + uses_apis: [] + uses_components: + - component_song_card + + - id: component_artist_header + name: ArtistHeader + description: Artist profile header with cover image + props: + - name: artist + type: Artist + required: true + events: [] + uses_apis: [] + uses_components: + - component_social_links + + - id: component_album_header + name: AlbumHeader + description: Album detail header with cover art + props: + - name: album + type: Album + required: true + - name: artist + type: Artist + required: true + events: + - name: onPlayAll + payload: null + uses_apis: [] + uses_components: [] + + - id: component_playlist_header + name: PlaylistHeader + description: Playlist header with cover and controls + props: + - name: playlist + type: Playlist + required: true + - name: isOwner + type: boolean + default: false + events: + - name: onPlayAll + payload: null + - name: onEdit + payload: null + - name: onDelete + payload: null + uses_apis: [] + uses_components: [] + + - id: component_social_links + name: SocialLinks + description: Social media links display + props: + - name: links + type: object + required: true + events: [] + uses_apis: [] + uses_components: [] + + - id: component_auth_form + name: AuthForm + description: Reusable authentication form + props: + - name: mode + type: enum[login, register, forgot] + required: true + state: + - name: email + type: string + - name: password + type: string + - name: name + type: string + - name: role + type: string + events: + - name: onSubmit + payload: object + uses_apis: + - api_login + - api_register + - api_forgot_password + uses_components: [] + + - id: component_search_bar + name: SearchBar + description: Search input with autocomplete + props: + - name: placeholder + type: string + default: "Search songs, artists, albums..." + state: + - name: query + type: string + events: + - name: onSearch + payload: + query: string + uses_apis: + - api_search + uses_components: [] + + - id: component_search_results + name: SearchResults + description: Search results with filters + props: + - name: results + type: object + required: true + events: [] + uses_apis: [] + uses_components: + - component_song_card + - component_artist_card + - component_album_card + + - id: component_create_playlist_modal + name: CreatePlaylistModal + description: Modal for creating new playlist + props: + - name: isOpen + type: boolean + required: true + state: + - name: name + type: string + - name: description + type: string + - name: isPublic + type: boolean + events: + - name: onCreate + payload: + name: string + description: string + isPublic: boolean + - name: onClose + payload: null + uses_apis: + - api_create_playlist + uses_components: [] + + - id: component_profile_form + name: ProfileForm + description: User profile edit form + props: + - name: user + type: User + required: true + state: + - name: name + type: string + - name: avatarUrl + type: string + events: + - name: onSave + payload: + name: string + avatarUrl: string + uses_apis: + - api_update_current_user + uses_components: + - component_avatar_upload + + - id: component_avatar_upload + name: AvatarUpload + description: Avatar image upload component + props: + - name: currentAvatarUrl + type: string + required: false + state: + - name: file + type: File + - name: preview + type: string + events: + - name: onUpload + payload: + file: File + uses_apis: [] + uses_components: [] + + - id: component_section_header + name: SectionHeader + description: Section title with optional action + props: + - name: title + type: string + required: true + - name: actionLabel + type: string + required: false + events: + - name: onActionClick + payload: null + uses_apis: [] + uses_components: [] + + - id: component_genre_header + name: GenreHeader + description: Genre browse page header + props: + - name: genre + type: Genre + required: true + events: [] + uses_apis: [] + uses_components: [] + +# ═══════════════════════════════════════════════════════════════ +# SUMMARY +# ═══════════════════════════════════════════════════════════════ + +summary: + data_models: 10 + api_endpoints: 46 + pages: 11 + components: 24 + total_entities: 91 diff --git a/.workflow/versions/v001/implementation_checklist.md b/.workflow/versions/v001/implementation_checklist.md new file mode 100644 index 0000000..6dc54c3 --- /dev/null +++ b/.workflow/versions/v001/implementation_checklist.md @@ -0,0 +1,149 @@ +# Implementation Checklist + +Generated: 2025-12-18T15:56:52 + +## Components + +- [❌] AudioPlayer (`component_audio_player`) + - [❌] prop: `currentSong`: Song + - [❌] prop: `queue`: array[Song] + - [❌] prop: `autoplay`: boolean + - [⚠️] event: `onPlay` + - [⚠️] event: `onPause` + - [⚠️] event: `onSeek` + - [⚠️] event: `onVolumeChange` + - [⚠️] event: `onNext` + - [⚠️] event: `onPrevious` + - [⚠️] event: `onShuffle` + - [⚠️] event: `onRepeat` +- [❌] PlayerControls (`component_player_controls`) + - [✅] prop: `isPlaying`: boolean + - [❌] prop: `currentTime`: number + - [❌] prop: `duration`: number + - [⚠️] event: `onPlay` + - [⚠️] event: `onPause` + - [⚠️] event: `onSeek` +- [❌] SongCard (`component_song_card`) + - [❌] prop: `song`: Song + - [❌] prop: `showArtist`: boolean + - [❌] prop: `showAlbum`: boolean + - [✅] event: `onPlay` + - [⚠️] event: `onAddToPlaylist` +- [❌] AlbumCard (`component_album_card`) + - [❌] prop: `album`: Album + - [❌] prop: `showArtist`: boolean + - [✅] event: `onClick` +- [❌] ArtistCard (`component_artist_card`) + - [❌] prop: `artist`: Artist + - [✅] event: `onClick` +- [❌] PlaylistCard (`component_playlist_card`) + - [❌] prop: `playlist`: Playlist + - [✅] event: `onClick` +- [❌] UploadForm (`component_upload_form`) + - [✅] prop: `albums`: array[Album] + - [❌] prop: `genres`: array[Genre] + - [⚠️] event: `onUpload` + - [⚠️] event: `onCancel` +- [❌] WaveformDisplay (`component_waveform_display`) + - [❌] prop: `audioUrl`: string + - [❌] prop: `waveformData`: array[number] + - [✅] prop: `currentTime`: number + - [✅] event: `onSeek` +- [❌] GenreBadge (`component_genre_badge`) + - [❌] prop: `genre`: Genre + - [❌] prop: `clickable`: boolean + - [✅] event: `onClick` +- [❌] TrackList (`component_track_list`) + - [❌] prop: `songs`: array[Song] + - [❌] prop: `showTrackNumber`: boolean + - [❌] prop: `reorderable`: boolean + - [⚠️] event: `onPlay` + - [⚠️] event: `onReorder` +- [❌] ArtistHeader (`component_artist_header`) + - [❌] prop: `artist`: Artist +- [❌] AlbumHeader (`component_album_header`) + - [❌] prop: `album`: Album + - [❌] prop: `artist`: Artist + - [✅] event: `onPlayAll` +- [❌] PlaylistHeader (`component_playlist_header`) + - [❌] prop: `playlist`: Playlist + - [✅] prop: `isOwner`: boolean + - [✅] event: `onPlayAll` + - [✅] event: `onEdit` + - [✅] event: `onDelete` +- [✅] SocialLinks (`component_social_links`) + - [✅] prop: `links`: object +- [✅] AuthForm (`component_auth_form`) + - [✅] prop: `mode`: enum[login, register, forgot] + - [✅] event: `onSubmit` +- [✅] SearchBar (`component_search_bar`) + - [✅] prop: `placeholder`: string + - [✅] event: `onSearch` +- [❌] SearchResults (`component_search_results`) + - [❌] prop: `results`: object +- [⚠️] CreatePlaylistModal (`component_create_playlist_modal`) + - [✅] prop: `isOpen`: boolean + - [⚠️] event: `onCreate` + - [✅] event: `onClose` +- [❌] ProfileForm (`component_profile_form`) + - [❌] prop: `user`: User + - [⚠️] event: `onSave` +- [✅] AvatarUpload (`component_avatar_upload`) + - [✅] prop: `currentAvatarUrl`: string + - [✅] event: `onUpload` +- [❌] SectionHeader (`component_section_header`) + - [✅] prop: `title`: string + - [❌] prop: `actionLabel`: string + - [⚠️] event: `onActionClick` +- [❌] GenreHeader (`component_genre_header`) + - [❌] prop: `genre`: Genre + +## API Endpoints + +- [✅] `POST` /api/auth/register +- [✅] `POST` /api/auth/login +- [✅] `POST` /api/auth/forgot-password +- [✅] `POST` /api/auth/reset-password +- [✅] `GET` /api/users/me +- [✅] `PUT` /api/users/me +- [✅] `POST` /api/artists +- [✅] `GET` /api/artists/:id +- [✅] `PUT` /api/artists/:id +- [✅] `GET` /api/artists/:id/songs +- [✅] `GET` /api/artists/:id/albums +- [✅] `POST` /api/songs/upload +- [✅] `GET` /api/songs/:id +- [✅] `PUT` /api/songs/:id +- [✅] `DELETE` /api/songs/:id +- [✅] `POST` /api/songs/:id/play +- [✅] `POST` /api/albums +- [✅] `GET` /api/albums/:id +- [✅] `PUT` /api/albums/:id +- [✅] `DELETE` /api/albums/:id +- [✅] `POST` /api/playlists +- [✅] `GET` /api/playlists +- [✅] `GET` /api/playlists/:id +- [✅] `PUT` /api/playlists/:id +- [✅] `DELETE` /api/playlists/:id +- [✅] `POST` /api/playlists/:id/songs +- [⚠️] `DELETE` /api/playlists/:playlistId/songs/:songId +- [✅] `PUT` /api/playlists/:id/reorder +- [✅] `GET` /api/discover/trending +- [✅] `GET` /api/discover/new-releases +- [✅] `GET` /api/discover/genres +- [✅] `GET` /api/discover/genres/:slug +- [✅] `GET` /api/search +- [✅] `POST` /api/labels +- [✅] `GET` /api/labels/:id/artists + +## Data Models + +- [⚠️] User (`model_user`) +- [⚠️] Artist (`model_artist`) +- [⚠️] Label (`model_label`) +- [⚠️] Genre (`model_genre`) +- [⚠️] Album (`model_album`) +- [⚠️] Song (`model_song`) +- [⚠️] SongGenre (`model_song_genre`) +- [⚠️] Playlist (`model_playlist`) +- [⚠️] PlaylistSong (`model_playlist_song`) \ No newline at end of file diff --git a/.workflow/versions/v001/requirements/summary.yml b/.workflow/versions/v001/requirements/summary.yml new file mode 100644 index 0000000..aa51eab --- /dev/null +++ b/.workflow/versions/v001/requirements/summary.yml @@ -0,0 +1,91 @@ +feature: a platform where musician can upload their songs +gathered_at: 2025-12-18T14:55:00 +questions_asked: 8 +mode: auto + +requirements: + user_types: + - musician: "Upload and manage music, create artist profile" + - listener: "Discover and play music, create playlists" + - label: "Manage artist roster (minimal for MVP)" + + authentication: + method: email_password + features: + - registration + - login + - password_reset + - email_verification + + audio: + supported_formats: + - mp3 + - wav + max_file_size: 50MB + storage: cloud + + musician_profile: + fields: + - name + - bio + - avatar + - cover_image + - social_links + features: + - song_list + - album_organization + - ep_organization + + music_organization: + entities: + - song: "Individual track with metadata" + - album: "Collection of songs" + - genre: "Category for discovery" + metadata: + - title + - duration + - release_date + - cover_art + - genre_tags + + player_features: + basic: + - play + - pause + - seek + - volume + advanced: + - queue + - shuffle + - repeat + - waveform_display + playlist: + - create + - edit + - delete + - reorder + + discovery: + search: + - by_song_title + - by_artist_name + - by_album + browse: + - by_genre + - trending + - new_releases + - featured + + label_features_mvp: + - label_profile + - artist_roster_view + +acceptance_criteria: + - Musician can register and create artist profile + - Musician can upload MP3/WAV songs with metadata + - Musician can organize songs into albums + - Listener can search and browse music + - Listener can play songs with full player controls + - Listener can create and manage playlists + - Label can view their artist roster + - All users can authenticate via email/password diff --git a/.workflow/versions/v001/session.yml b/.workflow/versions/v001/session.yml new file mode 100644 index 0000000..cc74a7d --- /dev/null +++ b/.workflow/versions/v001/session.yml @@ -0,0 +1,34 @@ +version: v001 +feature: a platform where musician can upload their songs +session_id: workflow_20251218_145233 +parent_version: None +status: completed +started_at: 2025-12-18 14:52:33.788070 +completed_at: None +current_phase: IMPLEMENTING +approvals: + design: + status: approved + approved_by: user + approved_at: '2025-12-18T15:19:49.601659' + rejection_reason: None + implementation: + status: pending + approved_by: None + approved_at: None + rejection_reason: None +task_sessions: [] +summary: + total_tasks: 0 + tasks_completed: 0 + entities_created: 0 + entities_updated: 0 + entities_deleted: 0 + files_created: 0 + files_updated: 0 + files_deleted: 0 +updated_at: '2025-12-18T15:19:56.804374' +last_validation: + status: failed + timestamp: '2025-12-18T15:56:52.706556' + checklist_path: .workflow/versions/v001/implementation_checklist.md diff --git a/.workflow/versions/v001/session.yml.bak b/.workflow/versions/v001/session.yml.bak new file mode 100644 index 0000000..abb12b2 --- /dev/null +++ b/.workflow/versions/v001/session.yml.bak @@ -0,0 +1,30 @@ +version: v001 +feature: a platform where musician can upload their songs +session_id: workflow_20251218_145233 +parent_version: None +status: completed +started_at: 2025-12-18 14:52:33.788070 +completed_at: None +current_phase: DESIGN_APPROVED +approvals: + design: + status: approved + approved_by: user + approved_at: '2025-12-18T15:19:49.601659' + rejection_reason: None + implementation: + status: pending + approved_by: None + approved_at: None + rejection_reason: None +task_sessions: [] +summary: + total_tasks: 0 + tasks_completed: 0 + entities_created: 0 + entities_updated: 0 + entities_deleted: 0 + files_created: 0 + files_updated: 0 + files_deleted: 0 +updated_at: '2025-12-18T15:19:49.602581' diff --git a/.workflow/versions/v001/snapshot_before/manifest.json b/.workflow/versions/v001/snapshot_before/manifest.json new file mode 100644 index 0000000..f167a6b --- /dev/null +++ b/.workflow/versions/v001/snapshot_before/manifest.json @@ -0,0 +1,34 @@ +{ + "project": { + "name": "sonic-cloud", + "version": "0.1.0", + "created_at": "2025-12-18T14:32:39.275839", + "description": "sonic-cloud - A guardrailed project" + }, + "state": { + "current_phase": "DESIGN_PHASE", + "approval_status": { + "manifest_approved": false, + "approved_by": null, + "approved_at": null + }, + "revision_history": [ + { + "action": "PROJECT_INITIALIZED", + "timestamp": "2025-12-18T14:32:39.275844", + "details": "Project sonic-cloud created" + } + ] + }, + "entities": { + "pages": [], + "components": [], + "api_endpoints": [], + "database_tables": [] + }, + "dependencies": { + "component_to_page": {}, + "api_to_component": {}, + "table_to_api": {} + } +} \ No newline at end of file diff --git a/.workflow/versions/v001/tasks/task_create_api_add_song_to_playlist.yml b/.workflow/versions/v001/tasks/task_create_api_add_song_to_playlist.yml new file mode 100644 index 0000000..bc5dcd4 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_add_song_to_playlist.yml @@ -0,0 +1,19 @@ +id: task_create_api_add_song_to_playlist +type: create +title: Create api_add_song_to_playlist +agent: backend +entity_id: api_add_song_to_playlist +entity_ids: +- api_add_song_to_playlist +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_playlist +- task_create_model_playlist_song +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_add_song_to_playlist.yml +created_at: '2025-12-18T15:16:50.336026' diff --git a/.workflow/versions/v001/tasks/task_create_api_create_album.yml b/.workflow/versions/v001/tasks/task_create_api_create_album.yml new file mode 100644 index 0000000..9b37110 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_create_album.yml @@ -0,0 +1,19 @@ +id: task_create_api_create_album +type: create +title: Create api_create_album +agent: backend +entity_id: api_create_album +entity_ids: +- api_create_album +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_artist +- task_create_model_album +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_create_album.yml +created_at: '2025-12-18T15:16:50.336335' diff --git a/.workflow/versions/v001/tasks/task_create_api_create_artist_profile.yml b/.workflow/versions/v001/tasks/task_create_api_create_artist_profile.yml new file mode 100644 index 0000000..ebd2c25 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_create_artist_profile.yml @@ -0,0 +1,19 @@ +id: task_create_api_create_artist_profile +type: create +title: Create api_create_artist_profile +agent: backend +entity_id: api_create_artist_profile +entity_ids: +- api_create_artist_profile +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_artist +- task_create_model_user +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_create_artist_profile.yml +created_at: '2025-12-18T15:16:50.336630' diff --git a/.workflow/versions/v001/tasks/task_create_api_create_label_profile.yml b/.workflow/versions/v001/tasks/task_create_api_create_label_profile.yml new file mode 100644 index 0000000..4511ee1 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_create_label_profile.yml @@ -0,0 +1,18 @@ +id: task_create_api_create_label_profile +type: create +title: Create api_create_label_profile +agent: backend +entity_id: api_create_label_profile +entity_ids: +- api_create_label_profile +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_label +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_create_label_profile.yml +created_at: '2025-12-18T15:16:50.336941' diff --git a/.workflow/versions/v001/tasks/task_create_api_create_playlist.yml b/.workflow/versions/v001/tasks/task_create_api_create_playlist.yml new file mode 100644 index 0000000..269890c --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_create_playlist.yml @@ -0,0 +1,18 @@ +id: task_create_api_create_playlist +type: create +title: Create api_create_playlist +agent: backend +entity_id: api_create_playlist +entity_ids: +- api_create_playlist +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_playlist +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_create_playlist.yml +created_at: '2025-12-18T15:16:50.337236' diff --git a/.workflow/versions/v001/tasks/task_create_api_delete_album.yml b/.workflow/versions/v001/tasks/task_create_api_delete_album.yml new file mode 100644 index 0000000..aaabb18 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_delete_album.yml @@ -0,0 +1,18 @@ +id: task_create_api_delete_album +type: create +title: Create api_delete_album +agent: backend +entity_id: api_delete_album +entity_ids: +- api_delete_album +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_album +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_delete_album.yml +created_at: '2025-12-18T15:16:50.337534' diff --git a/.workflow/versions/v001/tasks/task_create_api_delete_playlist.yml b/.workflow/versions/v001/tasks/task_create_api_delete_playlist.yml new file mode 100644 index 0000000..7662873 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_delete_playlist.yml @@ -0,0 +1,18 @@ +id: task_create_api_delete_playlist +type: create +title: Create api_delete_playlist +agent: backend +entity_id: api_delete_playlist +entity_ids: +- api_delete_playlist +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_playlist +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_delete_playlist.yml +created_at: '2025-12-18T15:16:50.337825' diff --git a/.workflow/versions/v001/tasks/task_create_api_delete_song.yml b/.workflow/versions/v001/tasks/task_create_api_delete_song.yml new file mode 100644 index 0000000..29218bb --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_delete_song.yml @@ -0,0 +1,18 @@ +id: task_create_api_delete_song +type: create +title: Create api_delete_song +agent: backend +entity_id: api_delete_song +entity_ids: +- api_delete_song +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_song +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_delete_song.yml +created_at: '2025-12-18T15:16:50.338113' diff --git a/.workflow/versions/v001/tasks/task_create_api_forgot_password.yml b/.workflow/versions/v001/tasks/task_create_api_forgot_password.yml new file mode 100644 index 0000000..68c93af --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_forgot_password.yml @@ -0,0 +1,18 @@ +id: task_create_api_forgot_password +type: create +title: Create api_forgot_password +agent: backend +entity_id: api_forgot_password +entity_ids: +- api_forgot_password +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_user +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_forgot_password.yml +created_at: '2025-12-18T15:16:50.338403' diff --git a/.workflow/versions/v001/tasks/task_create_api_get_album.yml b/.workflow/versions/v001/tasks/task_create_api_get_album.yml new file mode 100644 index 0000000..e3fd822 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_get_album.yml @@ -0,0 +1,20 @@ +id: task_create_api_get_album +type: create +title: Create api_get_album +agent: backend +entity_id: api_get_album +entity_ids: +- api_get_album +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_artist +- task_create_model_album +- task_create_model_song +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_get_album.yml +created_at: '2025-12-18T15:16:50.338699' diff --git a/.workflow/versions/v001/tasks/task_create_api_get_artist.yml b/.workflow/versions/v001/tasks/task_create_api_get_artist.yml new file mode 100644 index 0000000..0f93f25 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_get_artist.yml @@ -0,0 +1,18 @@ +id: task_create_api_get_artist +type: create +title: Create api_get_artist +agent: backend +entity_id: api_get_artist +entity_ids: +- api_get_artist +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_artist +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_get_artist.yml +created_at: '2025-12-18T15:16:50.339008' diff --git a/.workflow/versions/v001/tasks/task_create_api_get_artist_albums.yml b/.workflow/versions/v001/tasks/task_create_api_get_artist_albums.yml new file mode 100644 index 0000000..1c81477 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_get_artist_albums.yml @@ -0,0 +1,19 @@ +id: task_create_api_get_artist_albums +type: create +title: Create api_get_artist_albums +agent: backend +entity_id: api_get_artist_albums +entity_ids: +- api_get_artist_albums +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_artist +- task_create_model_album +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_get_artist_albums.yml +created_at: '2025-12-18T15:16:50.339297' diff --git a/.workflow/versions/v001/tasks/task_create_api_get_artist_songs.yml b/.workflow/versions/v001/tasks/task_create_api_get_artist_songs.yml new file mode 100644 index 0000000..1c52400 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_get_artist_songs.yml @@ -0,0 +1,19 @@ +id: task_create_api_get_artist_songs +type: create +title: Create api_get_artist_songs +agent: backend +entity_id: api_get_artist_songs +entity_ids: +- api_get_artist_songs +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_artist +- task_create_model_song +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_get_artist_songs.yml +created_at: '2025-12-18T15:16:50.339606' diff --git a/.workflow/versions/v001/tasks/task_create_api_get_current_user.yml b/.workflow/versions/v001/tasks/task_create_api_get_current_user.yml new file mode 100644 index 0000000..c50d1de --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_get_current_user.yml @@ -0,0 +1,18 @@ +id: task_create_api_get_current_user +type: create +title: Create api_get_current_user +agent: backend +entity_id: api_get_current_user +entity_ids: +- api_get_current_user +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_user +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_get_current_user.yml +created_at: '2025-12-18T15:16:50.339918' diff --git a/.workflow/versions/v001/tasks/task_create_api_get_genres.yml b/.workflow/versions/v001/tasks/task_create_api_get_genres.yml new file mode 100644 index 0000000..5acfcc2 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_get_genres.yml @@ -0,0 +1,18 @@ +id: task_create_api_get_genres +type: create +title: Create api_get_genres +agent: backend +entity_id: api_get_genres +entity_ids: +- api_get_genres +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_genre +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_get_genres.yml +created_at: '2025-12-18T15:16:50.340227' diff --git a/.workflow/versions/v001/tasks/task_create_api_get_label_artists.yml b/.workflow/versions/v001/tasks/task_create_api_get_label_artists.yml new file mode 100644 index 0000000..f07b091 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_get_label_artists.yml @@ -0,0 +1,19 @@ +id: task_create_api_get_label_artists +type: create +title: Create api_get_label_artists +agent: backend +entity_id: api_get_label_artists +entity_ids: +- api_get_label_artists +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_artist +- task_create_model_label +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_get_label_artists.yml +created_at: '2025-12-18T15:16:50.340530' diff --git a/.workflow/versions/v001/tasks/task_create_api_get_new_releases.yml b/.workflow/versions/v001/tasks/task_create_api_get_new_releases.yml new file mode 100644 index 0000000..5c92506 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_get_new_releases.yml @@ -0,0 +1,18 @@ +id: task_create_api_get_new_releases +type: create +title: Create api_get_new_releases +agent: backend +entity_id: api_get_new_releases +entity_ids: +- api_get_new_releases +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_song +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_get_new_releases.yml +created_at: '2025-12-18T15:16:50.340837' diff --git a/.workflow/versions/v001/tasks/task_create_api_get_playlist.yml b/.workflow/versions/v001/tasks/task_create_api_get_playlist.yml new file mode 100644 index 0000000..c376b29 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_get_playlist.yml @@ -0,0 +1,19 @@ +id: task_create_api_get_playlist +type: create +title: Create api_get_playlist +agent: backend +entity_id: api_get_playlist +entity_ids: +- api_get_playlist +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_playlist +- task_create_model_playlist_song +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_get_playlist.yml +created_at: '2025-12-18T15:16:50.341165' diff --git a/.workflow/versions/v001/tasks/task_create_api_get_song.yml b/.workflow/versions/v001/tasks/task_create_api_get_song.yml new file mode 100644 index 0000000..7b2d283 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_get_song.yml @@ -0,0 +1,20 @@ +id: task_create_api_get_song +type: create +title: Create api_get_song +agent: backend +entity_id: api_get_song +entity_ids: +- api_get_song +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_artist +- task_create_model_album +- task_create_model_song +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_get_song.yml +created_at: '2025-12-18T15:16:50.341503' diff --git a/.workflow/versions/v001/tasks/task_create_api_get_songs_by_genre.yml b/.workflow/versions/v001/tasks/task_create_api_get_songs_by_genre.yml new file mode 100644 index 0000000..a397808 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_get_songs_by_genre.yml @@ -0,0 +1,19 @@ +id: task_create_api_get_songs_by_genre +type: create +title: Create api_get_songs_by_genre +agent: backend +entity_id: api_get_songs_by_genre +entity_ids: +- api_get_songs_by_genre +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_song +- task_create_model_genre +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_get_songs_by_genre.yml +created_at: '2025-12-18T15:16:50.341855' diff --git a/.workflow/versions/v001/tasks/task_create_api_get_trending_songs.yml b/.workflow/versions/v001/tasks/task_create_api_get_trending_songs.yml new file mode 100644 index 0000000..ff687cb --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_get_trending_songs.yml @@ -0,0 +1,19 @@ +id: task_create_api_get_trending_songs +type: create +title: Create api_get_trending_songs +agent: backend +entity_id: api_get_trending_songs +entity_ids: +- api_get_trending_songs +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_artist +- task_create_model_song +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_get_trending_songs.yml +created_at: '2025-12-18T15:16:50.342210' diff --git a/.workflow/versions/v001/tasks/task_create_api_get_user_playlists.yml b/.workflow/versions/v001/tasks/task_create_api_get_user_playlists.yml new file mode 100644 index 0000000..1a5ad13 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_get_user_playlists.yml @@ -0,0 +1,18 @@ +id: task_create_api_get_user_playlists +type: create +title: Create api_get_user_playlists +agent: backend +entity_id: api_get_user_playlists +entity_ids: +- api_get_user_playlists +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_playlist +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_get_user_playlists.yml +created_at: '2025-12-18T15:16:50.342562' diff --git a/.workflow/versions/v001/tasks/task_create_api_increment_play_count.yml b/.workflow/versions/v001/tasks/task_create_api_increment_play_count.yml new file mode 100644 index 0000000..6bba524 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_increment_play_count.yml @@ -0,0 +1,18 @@ +id: task_create_api_increment_play_count +type: create +title: Create api_increment_play_count +agent: backend +entity_id: api_increment_play_count +entity_ids: +- api_increment_play_count +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_song +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_increment_play_count.yml +created_at: '2025-12-18T15:16:50.342887' diff --git a/.workflow/versions/v001/tasks/task_create_api_login.yml b/.workflow/versions/v001/tasks/task_create_api_login.yml new file mode 100644 index 0000000..871d72f --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_login.yml @@ -0,0 +1,18 @@ +id: task_create_api_login +type: create +title: Create api_login +agent: backend +entity_id: api_login +entity_ids: +- api_login +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_user +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_login.yml +created_at: '2025-12-18T15:16:50.343187' diff --git a/.workflow/versions/v001/tasks/task_create_api_register.yml b/.workflow/versions/v001/tasks/task_create_api_register.yml new file mode 100644 index 0000000..12518fd --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_register.yml @@ -0,0 +1,18 @@ +id: task_create_api_register +type: create +title: Create api_register +agent: backend +entity_id: api_register +entity_ids: +- api_register +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_user +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_register.yml +created_at: '2025-12-18T15:16:50.343463' diff --git a/.workflow/versions/v001/tasks/task_create_api_remove_song_from_playlist.yml b/.workflow/versions/v001/tasks/task_create_api_remove_song_from_playlist.yml new file mode 100644 index 0000000..50de5f0 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_remove_song_from_playlist.yml @@ -0,0 +1,19 @@ +id: task_create_api_remove_song_from_playlist +type: create +title: Create api_remove_song_from_playlist +agent: backend +entity_id: api_remove_song_from_playlist +entity_ids: +- api_remove_song_from_playlist +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_playlist +- task_create_model_playlist_song +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_remove_song_from_playlist.yml +created_at: '2025-12-18T15:16:50.343744' diff --git a/.workflow/versions/v001/tasks/task_create_api_reorder_playlist_songs.yml b/.workflow/versions/v001/tasks/task_create_api_reorder_playlist_songs.yml new file mode 100644 index 0000000..06f6709 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_reorder_playlist_songs.yml @@ -0,0 +1,19 @@ +id: task_create_api_reorder_playlist_songs +type: create +title: Create api_reorder_playlist_songs +agent: backend +entity_id: api_reorder_playlist_songs +entity_ids: +- api_reorder_playlist_songs +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_playlist +- task_create_model_playlist_song +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_reorder_playlist_songs.yml +created_at: '2025-12-18T15:16:50.344066' diff --git a/.workflow/versions/v001/tasks/task_create_api_reset_password.yml b/.workflow/versions/v001/tasks/task_create_api_reset_password.yml new file mode 100644 index 0000000..2b2fd3e --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_reset_password.yml @@ -0,0 +1,18 @@ +id: task_create_api_reset_password +type: create +title: Create api_reset_password +agent: backend +entity_id: api_reset_password +entity_ids: +- api_reset_password +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_user +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_reset_password.yml +created_at: '2025-12-18T15:16:50.344375' diff --git a/.workflow/versions/v001/tasks/task_create_api_search.yml b/.workflow/versions/v001/tasks/task_create_api_search.yml new file mode 100644 index 0000000..84e4259 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_search.yml @@ -0,0 +1,20 @@ +id: task_create_api_search +type: create +title: Create api_search +agent: backend +entity_id: api_search +entity_ids: +- api_search +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_artist +- task_create_model_album +- task_create_model_song +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_search.yml +created_at: '2025-12-18T15:16:50.344662' diff --git a/.workflow/versions/v001/tasks/task_create_api_update_album.yml b/.workflow/versions/v001/tasks/task_create_api_update_album.yml new file mode 100644 index 0000000..ca76637 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_update_album.yml @@ -0,0 +1,18 @@ +id: task_create_api_update_album +type: create +title: Create api_update_album +agent: backend +entity_id: api_update_album +entity_ids: +- api_update_album +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_album +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_update_album.yml +created_at: '2025-12-18T15:16:50.344967' diff --git a/.workflow/versions/v001/tasks/task_create_api_update_artist.yml b/.workflow/versions/v001/tasks/task_create_api_update_artist.yml new file mode 100644 index 0000000..932d933 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_update_artist.yml @@ -0,0 +1,18 @@ +id: task_create_api_update_artist +type: create +title: Create api_update_artist +agent: backend +entity_id: api_update_artist +entity_ids: +- api_update_artist +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_artist +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_update_artist.yml +created_at: '2025-12-18T15:16:50.345257' diff --git a/.workflow/versions/v001/tasks/task_create_api_update_current_user.yml b/.workflow/versions/v001/tasks/task_create_api_update_current_user.yml new file mode 100644 index 0000000..257d781 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_update_current_user.yml @@ -0,0 +1,18 @@ +id: task_create_api_update_current_user +type: create +title: Create api_update_current_user +agent: backend +entity_id: api_update_current_user +entity_ids: +- api_update_current_user +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_user +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_update_current_user.yml +created_at: '2025-12-18T15:16:50.345551' diff --git a/.workflow/versions/v001/tasks/task_create_api_update_playlist.yml b/.workflow/versions/v001/tasks/task_create_api_update_playlist.yml new file mode 100644 index 0000000..c90bbcd --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_update_playlist.yml @@ -0,0 +1,18 @@ +id: task_create_api_update_playlist +type: create +title: Create api_update_playlist +agent: backend +entity_id: api_update_playlist +entity_ids: +- api_update_playlist +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_playlist +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_update_playlist.yml +created_at: '2025-12-18T15:16:50.345848' diff --git a/.workflow/versions/v001/tasks/task_create_api_update_song.yml b/.workflow/versions/v001/tasks/task_create_api_update_song.yml new file mode 100644 index 0000000..df4609a --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_update_song.yml @@ -0,0 +1,18 @@ +id: task_create_api_update_song +type: create +title: Create api_update_song +agent: backend +entity_id: api_update_song +entity_ids: +- api_update_song +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_song +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_update_song.yml +created_at: '2025-12-18T15:16:50.346141' diff --git a/.workflow/versions/v001/tasks/task_create_api_upload_song.yml b/.workflow/versions/v001/tasks/task_create_api_upload_song.yml new file mode 100644 index 0000000..8a881f2 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_api_upload_song.yml @@ -0,0 +1,19 @@ +id: task_create_api_upload_song +type: create +title: Create api_upload_song +agent: backend +entity_id: api_upload_song +entity_ids: +- api_upload_song +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_artist +- task_create_model_song +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/api_upload_song.yml +created_at: '2025-12-18T15:16:50.346433' diff --git a/.workflow/versions/v001/tasks/task_create_component_album_card.yml b/.workflow/versions/v001/tasks/task_create_component_album_card.yml new file mode 100644 index 0000000..c0b4b30 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_album_card.yml @@ -0,0 +1,17 @@ +id: task_create_component_album_card +type: create +title: Create AlbumCard +agent: frontend +entity_id: component_album_card +entity_ids: +- component_album_card +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_album_card.yml +created_at: '2025-12-18T15:16:50.329461' diff --git a/.workflow/versions/v001/tasks/task_create_component_album_header.yml b/.workflow/versions/v001/tasks/task_create_component_album_header.yml new file mode 100644 index 0000000..e91e8ab --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_album_header.yml @@ -0,0 +1,17 @@ +id: task_create_component_album_header +type: create +title: Create AlbumHeader +agent: frontend +entity_id: component_album_header +entity_ids: +- component_album_header +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_album_header.yml +created_at: '2025-12-18T15:16:50.329768' diff --git a/.workflow/versions/v001/tasks/task_create_component_artist_card.yml b/.workflow/versions/v001/tasks/task_create_component_artist_card.yml new file mode 100644 index 0000000..6327db0 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_artist_card.yml @@ -0,0 +1,17 @@ +id: task_create_component_artist_card +type: create +title: Create ArtistCard +agent: frontend +entity_id: component_artist_card +entity_ids: +- component_artist_card +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_artist_card.yml +created_at: '2025-12-18T15:16:50.330064' diff --git a/.workflow/versions/v001/tasks/task_create_component_artist_header.yml b/.workflow/versions/v001/tasks/task_create_component_artist_header.yml new file mode 100644 index 0000000..8166cbf --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_artist_header.yml @@ -0,0 +1,18 @@ +id: task_create_component_artist_header +type: create +title: Create ArtistHeader +agent: frontend +entity_id: component_artist_header +entity_ids: +- component_artist_header +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_component_social_links +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_artist_header.yml +created_at: '2025-12-18T15:16:50.346733' diff --git a/.workflow/versions/v001/tasks/task_create_component_audio_player.yml b/.workflow/versions/v001/tasks/task_create_component_audio_player.yml new file mode 100644 index 0000000..4d0aa0b --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_audio_player.yml @@ -0,0 +1,20 @@ +id: task_create_component_audio_player +type: create +title: Create AudioPlayer +agent: frontend +entity_id: component_audio_player +entity_ids: +- component_audio_player +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_component_waveform_display +- task_create_api_increment_play_count +- task_create_component_player_controls +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_audio_player.yml +created_at: '2025-12-18T15:16:50.347646' diff --git a/.workflow/versions/v001/tasks/task_create_component_auth_form.yml b/.workflow/versions/v001/tasks/task_create_component_auth_form.yml new file mode 100644 index 0000000..9697b0d --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_auth_form.yml @@ -0,0 +1,20 @@ +id: task_create_component_auth_form +type: create +title: Create AuthForm +agent: frontend +entity_id: component_auth_form +entity_ids: +- component_auth_form +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_api_forgot_password +- task_create_api_register +- task_create_api_login +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_auth_form.yml +created_at: '2025-12-18T15:16:50.347971' diff --git a/.workflow/versions/v001/tasks/task_create_component_avatar_upload.yml b/.workflow/versions/v001/tasks/task_create_component_avatar_upload.yml new file mode 100644 index 0000000..e8454be --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_avatar_upload.yml @@ -0,0 +1,17 @@ +id: task_create_component_avatar_upload +type: create +title: Create AvatarUpload +agent: frontend +entity_id: component_avatar_upload +entity_ids: +- component_avatar_upload +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_avatar_upload.yml +created_at: '2025-12-18T15:16:50.330376' diff --git a/.workflow/versions/v001/tasks/task_create_component_create_playlist_modal.yml b/.workflow/versions/v001/tasks/task_create_component_create_playlist_modal.yml new file mode 100644 index 0000000..51df958 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_create_playlist_modal.yml @@ -0,0 +1,18 @@ +id: task_create_component_create_playlist_modal +type: create +title: Create CreatePlaylistModal +agent: frontend +entity_id: component_create_playlist_modal +entity_ids: +- component_create_playlist_modal +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_api_create_playlist +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_create_playlist_modal.yml +created_at: '2025-12-18T15:16:50.348282' diff --git a/.workflow/versions/v001/tasks/task_create_component_genre_badge.yml b/.workflow/versions/v001/tasks/task_create_component_genre_badge.yml new file mode 100644 index 0000000..e4a518c --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_genre_badge.yml @@ -0,0 +1,17 @@ +id: task_create_component_genre_badge +type: create +title: Create GenreBadge +agent: frontend +entity_id: component_genre_badge +entity_ids: +- component_genre_badge +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_genre_badge.yml +created_at: '2025-12-18T15:16:50.330680' diff --git a/.workflow/versions/v001/tasks/task_create_component_genre_header.yml b/.workflow/versions/v001/tasks/task_create_component_genre_header.yml new file mode 100644 index 0000000..603f2c9 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_genre_header.yml @@ -0,0 +1,17 @@ +id: task_create_component_genre_header +type: create +title: Create GenreHeader +agent: frontend +entity_id: component_genre_header +entity_ids: +- component_genre_header +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_genre_header.yml +created_at: '2025-12-18T15:16:50.330974' diff --git a/.workflow/versions/v001/tasks/task_create_component_player_controls.yml b/.workflow/versions/v001/tasks/task_create_component_player_controls.yml new file mode 100644 index 0000000..21c523e --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_player_controls.yml @@ -0,0 +1,17 @@ +id: task_create_component_player_controls +type: create +title: Create PlayerControls +agent: frontend +entity_id: component_player_controls +entity_ids: +- component_player_controls +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_player_controls.yml +created_at: '2025-12-18T15:16:50.331269' diff --git a/.workflow/versions/v001/tasks/task_create_component_playlist_card.yml b/.workflow/versions/v001/tasks/task_create_component_playlist_card.yml new file mode 100644 index 0000000..f143519 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_playlist_card.yml @@ -0,0 +1,17 @@ +id: task_create_component_playlist_card +type: create +title: Create PlaylistCard +agent: frontend +entity_id: component_playlist_card +entity_ids: +- component_playlist_card +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_playlist_card.yml +created_at: '2025-12-18T15:16:50.331563' diff --git a/.workflow/versions/v001/tasks/task_create_component_playlist_header.yml b/.workflow/versions/v001/tasks/task_create_component_playlist_header.yml new file mode 100644 index 0000000..75bf995 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_playlist_header.yml @@ -0,0 +1,17 @@ +id: task_create_component_playlist_header +type: create +title: Create PlaylistHeader +agent: frontend +entity_id: component_playlist_header +entity_ids: +- component_playlist_header +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_playlist_header.yml +created_at: '2025-12-18T15:16:50.331860' diff --git a/.workflow/versions/v001/tasks/task_create_component_profile_form.yml b/.workflow/versions/v001/tasks/task_create_component_profile_form.yml new file mode 100644 index 0000000..7dc1ea5 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_profile_form.yml @@ -0,0 +1,19 @@ +id: task_create_component_profile_form +type: create +title: Create ProfileForm +agent: frontend +entity_id: component_profile_form +entity_ids: +- component_profile_form +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_api_update_current_user +- task_create_component_avatar_upload +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_profile_form.yml +created_at: '2025-12-18T15:16:50.348585' diff --git a/.workflow/versions/v001/tasks/task_create_component_search_bar.yml b/.workflow/versions/v001/tasks/task_create_component_search_bar.yml new file mode 100644 index 0000000..059c0ae --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_search_bar.yml @@ -0,0 +1,18 @@ +id: task_create_component_search_bar +type: create +title: Create SearchBar +agent: frontend +entity_id: component_search_bar +entity_ids: +- component_search_bar +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_api_search +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_search_bar.yml +created_at: '2025-12-18T15:16:50.348887' diff --git a/.workflow/versions/v001/tasks/task_create_component_search_results.yml b/.workflow/versions/v001/tasks/task_create_component_search_results.yml new file mode 100644 index 0000000..3186ad5 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_search_results.yml @@ -0,0 +1,20 @@ +id: task_create_component_search_results +type: create +title: Create SearchResults +agent: frontend +entity_id: component_search_results +entity_ids: +- component_search_results +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_component_artist_card +- task_create_component_album_card +- task_create_component_song_card +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_search_results.yml +created_at: '2025-12-18T15:16:50.347029' diff --git a/.workflow/versions/v001/tasks/task_create_component_section_header.yml b/.workflow/versions/v001/tasks/task_create_component_section_header.yml new file mode 100644 index 0000000..fd50403 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_section_header.yml @@ -0,0 +1,17 @@ +id: task_create_component_section_header +type: create +title: Create SectionHeader +agent: frontend +entity_id: component_section_header +entity_ids: +- component_section_header +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_section_header.yml +created_at: '2025-12-18T15:16:50.332152' diff --git a/.workflow/versions/v001/tasks/task_create_component_social_links.yml b/.workflow/versions/v001/tasks/task_create_component_social_links.yml new file mode 100644 index 0000000..008d5dc --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_social_links.yml @@ -0,0 +1,17 @@ +id: task_create_component_social_links +type: create +title: Create SocialLinks +agent: frontend +entity_id: component_social_links +entity_ids: +- component_social_links +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_social_links.yml +created_at: '2025-12-18T15:16:50.332445' diff --git a/.workflow/versions/v001/tasks/task_create_component_song_card.yml b/.workflow/versions/v001/tasks/task_create_component_song_card.yml new file mode 100644 index 0000000..2cf8c44 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_song_card.yml @@ -0,0 +1,17 @@ +id: task_create_component_song_card +type: create +title: Create SongCard +agent: frontend +entity_id: component_song_card +entity_ids: +- component_song_card +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_song_card.yml +created_at: '2025-12-18T15:16:50.332736' diff --git a/.workflow/versions/v001/tasks/task_create_component_track_list.yml b/.workflow/versions/v001/tasks/task_create_component_track_list.yml new file mode 100644 index 0000000..6a67f08 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_track_list.yml @@ -0,0 +1,18 @@ +id: task_create_component_track_list +type: create +title: Create TrackList +agent: frontend +entity_id: component_track_list +entity_ids: +- component_track_list +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_component_song_card +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_track_list.yml +created_at: '2025-12-18T15:16:50.347352' diff --git a/.workflow/versions/v001/tasks/task_create_component_upload_form.yml b/.workflow/versions/v001/tasks/task_create_component_upload_form.yml new file mode 100644 index 0000000..4c341b4 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_upload_form.yml @@ -0,0 +1,19 @@ +id: task_create_component_upload_form +type: create +title: Create UploadForm +agent: frontend +entity_id: component_upload_form +entity_ids: +- component_upload_form +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_component_waveform_display +- task_create_api_upload_song +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_upload_form.yml +created_at: '2025-12-18T15:16:50.349175' diff --git a/.workflow/versions/v001/tasks/task_create_component_waveform_display.yml b/.workflow/versions/v001/tasks/task_create_component_waveform_display.yml new file mode 100644 index 0000000..4fa33fd --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_component_waveform_display.yml @@ -0,0 +1,17 @@ +id: task_create_component_waveform_display +type: create +title: Create WaveformDisplay +agent: frontend +entity_id: component_waveform_display +entity_ids: +- component_waveform_display +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/component_waveform_display.yml +created_at: '2025-12-18T15:16:50.333217' diff --git a/.workflow/versions/v001/tasks/task_create_model_album.yml b/.workflow/versions/v001/tasks/task_create_model_album.yml new file mode 100644 index 0000000..edfd968 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_model_album.yml @@ -0,0 +1,17 @@ +id: task_create_model_album +type: create +title: Create Album +agent: backend +entity_id: model_album +entity_ids: +- model_album +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/model_album.yml +created_at: '2025-12-18T15:16:50.333552' diff --git a/.workflow/versions/v001/tasks/task_create_model_artist.yml b/.workflow/versions/v001/tasks/task_create_model_artist.yml new file mode 100644 index 0000000..e41d8c7 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_model_artist.yml @@ -0,0 +1,17 @@ +id: task_create_model_artist +type: create +title: Create Artist +agent: backend +entity_id: model_artist +entity_ids: +- model_artist +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/model_artist.yml +created_at: '2025-12-18T15:16:50.333830' diff --git a/.workflow/versions/v001/tasks/task_create_model_genre.yml b/.workflow/versions/v001/tasks/task_create_model_genre.yml new file mode 100644 index 0000000..0a8d13c --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_model_genre.yml @@ -0,0 +1,17 @@ +id: task_create_model_genre +type: create +title: Create Genre +agent: backend +entity_id: model_genre +entity_ids: +- model_genre +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/model_genre.yml +created_at: '2025-12-18T15:16:50.334106' diff --git a/.workflow/versions/v001/tasks/task_create_model_label.yml b/.workflow/versions/v001/tasks/task_create_model_label.yml new file mode 100644 index 0000000..23d1d49 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_model_label.yml @@ -0,0 +1,17 @@ +id: task_create_model_label +type: create +title: Create Label +agent: backend +entity_id: model_label +entity_ids: +- model_label +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/model_label.yml +created_at: '2025-12-18T15:16:50.334375' diff --git a/.workflow/versions/v001/tasks/task_create_model_playlist.yml b/.workflow/versions/v001/tasks/task_create_model_playlist.yml new file mode 100644 index 0000000..574c3b3 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_model_playlist.yml @@ -0,0 +1,17 @@ +id: task_create_model_playlist +type: create +title: Create Playlist +agent: backend +entity_id: model_playlist +entity_ids: +- model_playlist +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/model_playlist.yml +created_at: '2025-12-18T15:16:50.334643' diff --git a/.workflow/versions/v001/tasks/task_create_model_playlist_song.yml b/.workflow/versions/v001/tasks/task_create_model_playlist_song.yml new file mode 100644 index 0000000..76350cf --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_model_playlist_song.yml @@ -0,0 +1,17 @@ +id: task_create_model_playlist_song +type: create +title: Create PlaylistSong +agent: backend +entity_id: model_playlist_song +entity_ids: +- model_playlist_song +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/model_playlist_song.yml +created_at: '2025-12-18T15:16:50.334916' diff --git a/.workflow/versions/v001/tasks/task_create_model_song.yml b/.workflow/versions/v001/tasks/task_create_model_song.yml new file mode 100644 index 0000000..93bd939 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_model_song.yml @@ -0,0 +1,17 @@ +id: task_create_model_song +type: create +title: Create Song +agent: backend +entity_id: model_song +entity_ids: +- model_song +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/model_song.yml +created_at: '2025-12-18T15:16:50.335197' diff --git a/.workflow/versions/v001/tasks/task_create_model_song_genre.yml b/.workflow/versions/v001/tasks/task_create_model_song_genre.yml new file mode 100644 index 0000000..4502b5c --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_model_song_genre.yml @@ -0,0 +1,17 @@ +id: task_create_model_song_genre +type: create +title: Create SongGenre +agent: backend +entity_id: model_song_genre +entity_ids: +- model_song_genre +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/model_song_genre.yml +created_at: '2025-12-18T15:16:50.335466' diff --git a/.workflow/versions/v001/tasks/task_create_model_user.yml b/.workflow/versions/v001/tasks/task_create_model_user.yml new file mode 100644 index 0000000..b1782d7 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_model_user.yml @@ -0,0 +1,17 @@ +id: task_create_model_user +type: create +title: Create User +agent: backend +entity_id: model_user +entity_ids: +- model_user +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/model_user.yml +created_at: '2025-12-18T15:16:50.335750' diff --git a/.workflow/versions/v001/tasks/task_create_page_album_detail.yml b/.workflow/versions/v001/tasks/task_create_page_album_detail.yml new file mode 100644 index 0000000..cb7afd0 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_page_album_detail.yml @@ -0,0 +1,21 @@ +id: task_create_page_album_detail +type: create +title: Create page_album_detail +agent: frontend +entity_id: page_album_detail +entity_ids: +- page_album_detail +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_component_track_list +- task_create_component_song_card +- task_create_api_get_album +- task_create_component_album_header +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/page_album_detail.yml +created_at: '2025-12-18T15:16:50.349479' diff --git a/.workflow/versions/v001/tasks/task_create_page_artist_profile.yml b/.workflow/versions/v001/tasks/task_create_page_artist_profile.yml new file mode 100644 index 0000000..1c420ae --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_page_artist_profile.yml @@ -0,0 +1,24 @@ +id: task_create_page_artist_profile +type: create +title: Create page_artist_profile +agent: frontend +entity_id: page_artist_profile +entity_ids: +- page_artist_profile +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_api_get_artist_albums +- task_create_component_artist_header +- task_create_component_album_card +- task_create_api_get_artist_songs +- task_create_api_get_artist +- task_create_component_song_card +- task_create_component_social_links +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/page_artist_profile.yml +created_at: '2025-12-18T15:16:50.349810' diff --git a/.workflow/versions/v001/tasks/task_create_page_forgot_password.yml b/.workflow/versions/v001/tasks/task_create_page_forgot_password.yml new file mode 100644 index 0000000..d8adc9e --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_page_forgot_password.yml @@ -0,0 +1,19 @@ +id: task_create_page_forgot_password +type: create +title: Create page_forgot_password +agent: frontend +entity_id: page_forgot_password +entity_ids: +- page_forgot_password +status: pending +layer: 4 +parallel_group: layer_4 +complexity: medium +dependencies: +- task_create_component_auth_form +- task_create_api_forgot_password +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/page_forgot_password.yml +created_at: '2025-12-18T15:16:50.351146' diff --git a/.workflow/versions/v001/tasks/task_create_page_genre_browse.yml b/.workflow/versions/v001/tasks/task_create_page_genre_browse.yml new file mode 100644 index 0000000..d8bfea7 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_page_genre_browse.yml @@ -0,0 +1,20 @@ +id: task_create_page_genre_browse +type: create +title: Create page_genre_browse +agent: frontend +entity_id: page_genre_browse +entity_ids: +- page_genre_browse +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_component_genre_header +- task_create_api_get_songs_by_genre +- task_create_component_song_card +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/page_genre_browse.yml +created_at: '2025-12-18T15:16:50.350170' diff --git a/.workflow/versions/v001/tasks/task_create_page_home.yml b/.workflow/versions/v001/tasks/task_create_page_home.yml new file mode 100644 index 0000000..f0c2251 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_page_home.yml @@ -0,0 +1,23 @@ +id: task_create_page_home +type: create +title: Create page_home +agent: frontend +entity_id: page_home +entity_ids: +- page_home +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_component_genre_badge +- task_create_component_section_header +- task_create_api_get_new_releases +- task_create_component_song_card +- task_create_api_get_trending_songs +- task_create_api_get_genres +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/page_home.yml +created_at: '2025-12-18T15:16:50.350484' diff --git a/.workflow/versions/v001/tasks/task_create_page_login.yml b/.workflow/versions/v001/tasks/task_create_page_login.yml new file mode 100644 index 0000000..3cd41cc --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_page_login.yml @@ -0,0 +1,19 @@ +id: task_create_page_login +type: create +title: Create page_login +agent: frontend +entity_id: page_login +entity_ids: +- page_login +status: pending +layer: 4 +parallel_group: layer_4 +complexity: medium +dependencies: +- task_create_component_auth_form +- task_create_api_login +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/page_login.yml +created_at: '2025-12-18T15:16:50.351459' diff --git a/.workflow/versions/v001/tasks/task_create_page_playlist_detail.yml b/.workflow/versions/v001/tasks/task_create_page_playlist_detail.yml new file mode 100644 index 0000000..f4f0b6f --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_page_playlist_detail.yml @@ -0,0 +1,21 @@ +id: task_create_page_playlist_detail +type: create +title: Create page_playlist_detail +agent: frontend +entity_id: page_playlist_detail +entity_ids: +- page_playlist_detail +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_component_playlist_header +- task_create_component_track_list +- task_create_api_get_playlist +- task_create_component_song_card +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/page_playlist_detail.yml +created_at: '2025-12-18T15:16:50.350815' diff --git a/.workflow/versions/v001/tasks/task_create_page_playlists.yml b/.workflow/versions/v001/tasks/task_create_page_playlists.yml new file mode 100644 index 0000000..822a909 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_page_playlists.yml @@ -0,0 +1,20 @@ +id: task_create_page_playlists +type: create +title: Create page_playlists +agent: frontend +entity_id: page_playlists +entity_ids: +- page_playlists +status: pending +layer: 4 +parallel_group: layer_4 +complexity: medium +dependencies: +- task_create_component_playlist_card +- task_create_api_get_user_playlists +- task_create_component_create_playlist_modal +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/page_playlists.yml +created_at: '2025-12-18T15:16:50.351749' diff --git a/.workflow/versions/v001/tasks/task_create_page_profile.yml b/.workflow/versions/v001/tasks/task_create_page_profile.yml new file mode 100644 index 0000000..3f44315 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_page_profile.yml @@ -0,0 +1,21 @@ +id: task_create_page_profile +type: create +title: Create page_profile +agent: frontend +entity_id: page_profile +entity_ids: +- page_profile +status: pending +layer: 4 +parallel_group: layer_4 +complexity: medium +dependencies: +- task_create_api_update_current_user +- task_create_component_profile_form +- task_create_api_get_current_user +- task_create_component_avatar_upload +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/page_profile.yml +created_at: '2025-12-18T15:16:50.352062' diff --git a/.workflow/versions/v001/tasks/task_create_page_register.yml b/.workflow/versions/v001/tasks/task_create_page_register.yml new file mode 100644 index 0000000..5d5c8a7 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_page_register.yml @@ -0,0 +1,19 @@ +id: task_create_page_register +type: create +title: Create page_register +agent: frontend +entity_id: page_register +entity_ids: +- page_register +status: pending +layer: 4 +parallel_group: layer_4 +complexity: medium +dependencies: +- task_create_component_auth_form +- task_create_api_register +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/page_register.yml +created_at: '2025-12-18T15:16:50.352381' diff --git a/.workflow/versions/v001/tasks/task_create_page_search.yml b/.workflow/versions/v001/tasks/task_create_page_search.yml new file mode 100644 index 0000000..7150c3d --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_page_search.yml @@ -0,0 +1,23 @@ +id: task_create_page_search +type: create +title: Create page_search +agent: frontend +entity_id: page_search +entity_ids: +- page_search +status: pending +layer: 4 +parallel_group: layer_4 +complexity: medium +dependencies: +- task_create_component_search_results +- task_create_component_album_card +- task_create_component_artist_card +- task_create_component_search_bar +- task_create_api_search +- task_create_component_song_card +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/page_search.yml +created_at: '2025-12-18T15:16:50.352674' diff --git a/.workflow/versions/v001/tasks/task_create_page_upload.yml b/.workflow/versions/v001/tasks/task_create_page_upload.yml new file mode 100644 index 0000000..8464778 --- /dev/null +++ b/.workflow/versions/v001/tasks/task_create_page_upload.yml @@ -0,0 +1,22 @@ +id: task_create_page_upload +type: create +title: Create page_upload +agent: frontend +entity_id: page_upload +entity_ids: +- page_upload +status: pending +layer: 4 +parallel_group: layer_4 +complexity: medium +dependencies: +- task_create_api_get_artist_albums +- task_create_component_upload_form +- task_create_api_upload_song +- task_create_component_waveform_display +- task_create_api_get_genres +context: + design_version: 1 + workflow_version: v001 + context_snapshot_path: .workflow/versions/v001/contexts/page_upload.yml +created_at: '2025-12-18T15:16:50.353006' diff --git a/.workflow/versions/v002/dependency_graph.yml b/.workflow/versions/v002/dependency_graph.yml new file mode 100644 index 0000000..bcacf9e --- /dev/null +++ b/.workflow/versions/v002/dependency_graph.yml @@ -0,0 +1,54 @@ +workflow_version: v002 +generated_at: "2025-12-18T17:08:00" +feature: "add header and navigation link to each pages" + +# Execution layers - frontend only +layers: + - layer: 1 + name: "Base Components" + description: "Independent components with no dependencies" + parallel: true + entities: + - id: component_nav_link + type: component + agent: frontend + file_path: components/NavLink.tsx + dependencies: [] + + - id: component_user_menu + type: component + agent: frontend + file_path: components/UserMenu.tsx + dependencies: [] + + - layer: 2 + name: "Header Component" + description: "Header uses NavLink and UserMenu" + parallel: false + entities: + - id: component_header + type: component + agent: frontend + file_path: components/Header.tsx + dependencies: + - component_nav_link + - component_user_menu + + - layer: 3 + name: "Layout Integration" + description: "Root layout integrates Header" + parallel: false + entities: + - id: layout_root + type: layout + agent: frontend + file_path: app/layout.tsx + dependencies: + - component_header + +summary: + total_layers: 3 + total_entities: 4 + backend_tasks: 0 + frontend_tasks: 4 + max_parallel: 2 diff --git a/.workflow/versions/v002/design/design_document.yml b/.workflow/versions/v002/design/design_document.yml new file mode 100644 index 0000000..eb3402f --- /dev/null +++ b/.workflow/versions/v002/design/design_document.yml @@ -0,0 +1,129 @@ +workflow_version: "v002" +feature: "add header and navigation link to each pages" +created_at: "2025-12-18T17:07:30" +status: draft +revision: 1 + +# No data model changes required +data_models: [] + +# No API endpoint changes required +api_endpoints: [] + +# Pages to modify (add header via layout) +pages: [] + +# Components to create +components: + - id: component_header + name: Header + file_path: components/Header.tsx + props: + - name: className + type: string + required: false + description: Additional CSS classes + events: [] + uses_apis: [] + uses_components: + - component_nav_link + - component_user_menu + description: | + Global navigation header with logo, navigation links, and user menu. + Includes responsive design with mobile hamburger menu. + acceptance_criteria: + - Header renders logo linked to home + - Navigation links are displayed + - User menu shows auth state + - Mobile responsive with hamburger menu + + - id: component_nav_link + name: NavLink + file_path: components/NavLink.tsx + props: + - name: href + type: string + required: true + description: Link destination + - name: children + type: React.ReactNode + required: true + description: Link content + - name: icon + type: string + required: false + description: Optional icon name + events: [] + uses_apis: [] + uses_components: [] + description: | + Styled navigation link with active state indicator. + Highlights when on current page using usePathname(). + acceptance_criteria: + - Shows active state when on current page + - Hover state styling + - Supports optional icon + + - id: component_user_menu + name: UserMenu + file_path: components/UserMenu.tsx + props: [] + events: [] + uses_apis: [] + uses_components: [] + description: | + Displays user authentication state. + Shows Login/Register for unauthenticated users. + Shows user avatar and dropdown for authenticated users. + acceptance_criteria: + - Shows Login/Register when not authenticated + - Shows user info when authenticated + - Links to profile and logout + +# Layout modification +layout_changes: + - id: layout_root + file_path: app/layout.tsx + changes: + - Import Header component + - Render Header before children + - Update metadata for SonicCloud branding + +# Navigation structure +navigation_config: + links: + - label: Home + href: / + icon: home + show_always: true + - label: Search + href: /search + icon: search + show_always: true + - label: Playlists + href: /playlists + icon: playlist + auth_required: true + - label: Upload + href: /upload + icon: upload + auth_required: true + +# Dependencies +dependencies: + component_header: + depends_on: [] + depended_by: + - layout_root + component_nav_link: + depends_on: [] + depended_by: + - component_header + component_user_menu: + depends_on: [] + depended_by: + - component_header + layout_root: + depends_on: + - component_header + depended_by: [] diff --git a/.workflow/versions/v002/requirements/expanded.yml b/.workflow/versions/v002/requirements/expanded.yml new file mode 100644 index 0000000..055eb99 --- /dev/null +++ b/.workflow/versions/v002/requirements/expanded.yml @@ -0,0 +1,85 @@ +feature: "add header and navigation link to each pages" +expanded_at: "2025-12-18T17:06:45" +mode: full_auto + +analysis: + problem_statement: "The application lacks a consistent navigation header, making it difficult for users to navigate between pages. Currently, each page operates independently without a unified navigation experience." + target_users: "All users of the SonicCloud platform - listeners, musicians, and label representatives" + core_value: "Provide consistent navigation and brand identity across all pages" + +scope: + mvp_features: + - Global header component with logo and navigation links + - Navigation links to main sections (Home, Search, Playlists, Upload, Profile) + - Responsive design for mobile and desktop + - User authentication state display (login/register vs profile) + - Integration into root layout for consistent appearance + future_features: + - Search bar in header + - User dropdown menu + - Notifications bell + +data_model: + entities: [] + notes: "No database changes required - uses existing user auth state" + +api_endpoints: [] + # No new API endpoints required - uses existing auth endpoints + +ui_structure: + pages: [] + components: + - name: Header + purpose: "Global header with logo, navigation, and user auth state" + file_path: "components/Header.tsx" + - name: NavLink + purpose: "Styled navigation link with active state indicator" + file_path: "components/NavLink.tsx" + - name: UserMenu + purpose: "User authentication status display (login/profile)" + file_path: "components/UserMenu.tsx" + +layout_integration: + target: "app/layout.tsx" + changes: + - "Import and render Header component above children" + - "Update metadata with proper SonicCloud branding" + +navigation_links: + - label: "Home" + href: "/" + icon: "home" + - label: "Search" + href: "/search" + icon: "search" + - label: "Playlists" + href: "/playlists" + icon: "playlist" + - label: "Upload" + href: "/upload" + icon: "upload" + auth_required: true + +security: + authentication: "Uses existing session/cookie auth" + authorization: "Some nav items (Upload) only shown to authenticated users" + +edge_cases: + - scenario: "User not logged in" + handling: "Show Login/Register buttons instead of profile" + - scenario: "Mobile viewport" + handling: "Responsive hamburger menu" + - scenario: "Active page indication" + handling: "Highlight current page in navigation" + +acceptance_criteria: + - criterion: "Header appears on all pages" + verification: "Navigate to each page and verify header presence" + - criterion: "Navigation links work correctly" + verification: "Click each link and verify navigation" + - criterion: "Active state shows current page" + verification: "Check that current page is highlighted" + - criterion: "Responsive on mobile" + verification: "Test at mobile viewport width" + - criterion: "Auth state reflected" + verification: "Header shows appropriate content for logged in/out users" diff --git a/.workflow/versions/v002/requirements/final.yml b/.workflow/versions/v002/requirements/final.yml new file mode 100644 index 0000000..afd7047 --- /dev/null +++ b/.workflow/versions/v002/requirements/final.yml @@ -0,0 +1,48 @@ +feature: "add header and navigation link to each pages" +mode: full_auto +finalized_at: "2025-12-18T17:07:00" + +analysis: + problem_statement: "The application lacks a consistent navigation header, making it difficult for users to navigate between pages." + target_users: "All SonicCloud platform users" + core_value: "Unified navigation and brand identity" + +scope: + mvp_features: + - Global header component with logo and navigation + - Navigation links to Home, Search, Playlists, Upload, Profile + - Responsive design for mobile/desktop + - User auth state display + - Root layout integration + +components_to_create: + - id: component_header + name: Header + file_path: components/Header.tsx + purpose: Global navigation header + + - id: component_nav_link + name: NavLink + file_path: components/NavLink.tsx + purpose: Navigation link with active state + + - id: component_user_menu + name: UserMenu + file_path: components/UserMenu.tsx + purpose: User authentication status display + +layout_changes: + - file: app/layout.tsx + change: Import and render Header component + +acceptance_criteria: + - criterion: "Header appears on all pages" + verification: "Navigate to each page and verify header presence" + - criterion: "Navigation links work correctly" + verification: "Click each link and verify navigation" + - criterion: "Active state shows current page" + verification: "Current page is highlighted in navigation" + - criterion: "Responsive on mobile" + verification: "Test at mobile viewport width" + - criterion: "Auth state reflected correctly" + verification: "Header shows login/register or profile based on auth" diff --git a/.workflow/versions/v002/session.yml b/.workflow/versions/v002/session.yml new file mode 100644 index 0000000..9171eda --- /dev/null +++ b/.workflow/versions/v002/session.yml @@ -0,0 +1,30 @@ +version: v002 +feature: add header and navigation link to each pages +session_id: workflow_20251218_170607 +parent_version: null +status: completed +started_at: '2025-12-18T17:06:07.870800' +completed_at: '2025-12-18T17:13:46.853854' +current_phase: COMPLETING +approvals: + design: + status: approved + approved_by: user + approved_at: '2025-12-18T17:09:27.257241' + rejection_reason: null + implementation: + status: approved + approved_by: user + approved_at: '2025-12-18T17:13:25.566132' + rejection_reason: null +task_sessions: [] +summary: + total_tasks: 0 + tasks_completed: 0 + entities_created: 0 + entities_updated: 0 + entities_deleted: 0 + files_created: 0 + files_updated: 0 + files_deleted: 0 +updated_at: '2025-12-18T17:13:46.853860' diff --git a/.workflow/versions/v002/session.yml.bak b/.workflow/versions/v002/session.yml.bak new file mode 100644 index 0000000..2d40459 --- /dev/null +++ b/.workflow/versions/v002/session.yml.bak @@ -0,0 +1,30 @@ +version: v002 +feature: add header and navigation link to each pages +session_id: workflow_20251218_170607 +parent_version: null +status: pending +started_at: '2025-12-18T17:06:07.870800' +completed_at: null +current_phase: IMPL_APPROVED +approvals: + design: + status: approved + approved_by: user + approved_at: '2025-12-18T17:09:27.257241' + rejection_reason: null + implementation: + status: approved + approved_by: user + approved_at: '2025-12-18T17:13:25.566132' + rejection_reason: null +task_sessions: [] +summary: + total_tasks: 0 + tasks_completed: 0 + entities_created: 0 + entities_updated: 0 + entities_deleted: 0 + files_created: 0 + files_updated: 0 + files_deleted: 0 +updated_at: '2025-12-18T17:13:25.567053' diff --git a/.workflow/versions/v002/snapshot_after/manifest.json b/.workflow/versions/v002/snapshot_after/manifest.json new file mode 100644 index 0000000..8e53336 --- /dev/null +++ b/.workflow/versions/v002/snapshot_after/manifest.json @@ -0,0 +1,659 @@ +{ + "project": { + "name": "sonic-cloud", + "version": "0.1.0", + "created_at": "2025-12-18T14:32:39.275839", + "description": "Music platform for musicians to upload songs" + }, + "state": { + "current_phase": "DESIGN_PHASE", + "approval_status": { + "manifest_approved": false, + "approved_by": null, + "approved_at": null + }, + "revision_history": [ + { + "action": "PROJECT_INITIALIZED", + "timestamp": "2025-12-18T14:32:39.275844", + "details": "Project sonic-cloud created" + }, + { + "action": "DESIGN_DOCUMENT_CREATED", + "timestamp": "2025-12-18T15:10:00", + "details": "Complete design document with 91 entities created" + } + ] + }, + "entities": { + "database_tables": [ + { + "id": "model_user", + "name": "User", + "table_name": "users", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_artist", + "name": "Artist", + "table_name": "artists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_label", + "name": "Label", + "table_name": "labels", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_genre", + "name": "Genre", + "table_name": "genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_album", + "name": "Album", + "table_name": "albums", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song", + "name": "Song", + "table_name": "songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song_genre", + "name": "SongGenre", + "table_name": "song_genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist", + "name": "Playlist", + "table_name": "playlists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist_song", + "name": "PlaylistSong", + "table_name": "playlist_songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + } + ], + "api_endpoints": [ + { + "id": "api_register", + "name": "Register User", + "path": "/api/auth/register", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/register/route.ts" + }, + { + "id": "api_login", + "name": "Login", + "path": "/api/auth/login", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/login/route.ts" + }, + { + "id": "api_forgot_password", + "name": "Forgot Password", + "path": "/api/auth/forgot-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/forgot-password/route.ts" + }, + { + "id": "api_reset_password", + "name": "Reset Password", + "path": "/api/auth/reset-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/reset-password/route.ts" + }, + { + "id": "api_get_current_user", + "name": "Get Current User", + "path": "/api/users/me", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_update_current_user", + "name": "Update Current User", + "path": "/api/users/me", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_create_artist_profile", + "name": "Create Artist Profile", + "path": "/api/artists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/artists/route.ts" + }, + { + "id": "api_get_artist", + "name": "Get Artist", + "path": "/api/artists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_update_artist", + "name": "Update Artist", + "path": "/api/artists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_get_artist_songs", + "name": "Get Artist Songs", + "path": "/api/artists/:id/songs", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/songs/route.ts" + }, + { + "id": "api_get_artist_albums", + "name": "Get Artist Albums", + "path": "/api/artists/:id/albums", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/albums/route.ts" + }, + { + "id": "api_upload_song", + "name": "Upload Song", + "path": "/api/songs/upload", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/upload/route.ts" + }, + { + "id": "api_get_song", + "name": "Get Song", + "path": "/api/songs/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_update_song", + "name": "Update Song", + "path": "/api/songs/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_delete_song", + "name": "Delete Song", + "path": "/api/songs/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_increment_play_count", + "name": "Increment Play Count", + "path": "/api/songs/:id/play", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/[id]/play/route.ts" + }, + { + "id": "api_create_album", + "name": "Create Album", + "path": "/api/albums", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/albums/route.ts" + }, + { + "id": "api_get_album", + "name": "Get Album", + "path": "/api/albums/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_update_album", + "name": "Update Album", + "path": "/api/albums/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_delete_album", + "name": "Delete Album", + "path": "/api/albums/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_create_playlist", + "name": "Create Playlist", + "path": "/api/playlists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_user_playlists", + "name": "Get User Playlists", + "path": "/api/playlists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_playlist", + "name": "Get Playlist", + "path": "/api/playlists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_update_playlist", + "name": "Update Playlist", + "path": "/api/playlists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_delete_playlist", + "name": "Delete Playlist", + "path": "/api/playlists/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_add_song_to_playlist", + "name": "Add Song to Playlist", + "path": "/api/playlists/:id/songs", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/songs/route.ts" + }, + { + "id": "api_remove_song_from_playlist", + "name": "Remove Song from Playlist", + "path": "/api/playlists/:playlistId/songs/:songId", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[playlistId]/songs/[songId]/route.ts" + }, + { + "id": "api_reorder_playlist_songs", + "name": "Reorder Playlist Songs", + "path": "/api/playlists/:id/reorder", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/reorder/route.ts" + }, + { + "id": "api_get_trending_songs", + "name": "Get Trending Songs", + "path": "/api/discover/trending", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/trending/route.ts" + }, + { + "id": "api_get_new_releases", + "name": "Get New Releases", + "path": "/api/discover/new-releases", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/new-releases/route.ts" + }, + { + "id": "api_get_genres", + "name": "Get Genres", + "path": "/api/discover/genres", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/route.ts" + }, + { + "id": "api_get_songs_by_genre", + "name": "Get Songs by Genre", + "path": "/api/discover/genres/:slug", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/[slug]/route.ts" + }, + { + "id": "api_search", + "name": "Search", + "path": "/api/search", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/search/route.ts" + }, + { + "id": "api_create_label_profile", + "name": "Create Label Profile", + "path": "/api/labels", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/labels/route.ts" + }, + { + "id": "api_get_label_artists", + "name": "Get Label Artists", + "path": "/api/labels/:id/artists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/labels/[id]/artists/route.ts" + } + ], + "pages": [ + { + "id": "page_login", + "name": "Login", + "path": "/login", + "status": "PENDING", + "file_path": "app/login/page.tsx" + }, + { + "id": "page_register", + "name": "Register", + "path": "/register", + "status": "PENDING", + "file_path": "app/register/page.tsx" + }, + { + "id": "page_forgot_password", + "name": "Forgot Password", + "path": "/forgot-password", + "status": "PENDING", + "file_path": "app/forgot-password/page.tsx" + }, + { + "id": "page_home", + "name": "Discover Music", + "path": "/", + "status": "PENDING", + "file_path": "app/page.tsx" + }, + { + "id": "page_artist_profile", + "name": "Artist Profile", + "path": "/artist/:id", + "status": "PENDING", + "file_path": "app/artist/[id]/page.tsx" + }, + { + "id": "page_album_detail", + "name": "Album", + "path": "/album/:id", + "status": "PENDING", + "file_path": "app/album/[id]/page.tsx" + }, + { + "id": "page_upload", + "name": "Upload Music", + "path": "/upload", + "status": "PENDING", + "file_path": "app/upload/page.tsx" + }, + { + "id": "page_playlists", + "name": "My Playlists", + "path": "/playlists", + "status": "PENDING", + "file_path": "app/playlists/page.tsx" + }, + { + "id": "page_playlist_detail", + "name": "Playlist", + "path": "/playlist/:id", + "status": "PENDING", + "file_path": "app/playlist/[id]/page.tsx" + }, + { + "id": "page_profile", + "name": "Profile Settings", + "path": "/profile", + "status": "PENDING", + "file_path": "app/profile/page.tsx" + }, + { + "id": "page_search", + "name": "Search", + "path": "/search", + "status": "PENDING", + "file_path": "app/search/page.tsx" + }, + { + "id": "page_genre_browse", + "name": "Browse Genre", + "path": "/genre/:slug", + "status": "PENDING", + "file_path": "app/genre/[slug]/page.tsx" + } + ], + "components": [ + { + "id": "component_audio_player", + "name": "AudioPlayer", + "status": "PENDING", + "file_path": "components/AudioPlayer.tsx" + }, + { + "id": "component_player_controls", + "name": "PlayerControls", + "status": "PENDING", + "file_path": "components/PlayerControls.tsx" + }, + { + "id": "component_song_card", + "name": "SongCard", + "status": "PENDING", + "file_path": "components/SongCard.tsx" + }, + { + "id": "component_album_card", + "name": "AlbumCard", + "status": "PENDING", + "file_path": "components/AlbumCard.tsx" + }, + { + "id": "component_artist_card", + "name": "ArtistCard", + "status": "PENDING", + "file_path": "components/ArtistCard.tsx" + }, + { + "id": "component_playlist_card", + "name": "PlaylistCard", + "status": "PENDING", + "file_path": "components/PlaylistCard.tsx" + }, + { + "id": "component_upload_form", + "name": "UploadForm", + "status": "PENDING", + "file_path": "components/UploadForm.tsx" + }, + { + "id": "component_waveform_display", + "name": "WaveformDisplay", + "status": "PENDING", + "file_path": "components/WaveformDisplay.tsx" + }, + { + "id": "component_genre_badge", + "name": "GenreBadge", + "status": "PENDING", + "file_path": "components/GenreBadge.tsx" + }, + { + "id": "component_track_list", + "name": "TrackList", + "status": "PENDING", + "file_path": "components/TrackList.tsx" + }, + { + "id": "component_artist_header", + "name": "ArtistHeader", + "status": "PENDING", + "file_path": "components/ArtistHeader.tsx" + }, + { + "id": "component_album_header", + "name": "AlbumHeader", + "status": "PENDING", + "file_path": "components/AlbumHeader.tsx" + }, + { + "id": "component_playlist_header", + "name": "PlaylistHeader", + "status": "PENDING", + "file_path": "components/PlaylistHeader.tsx" + }, + { + "id": "component_social_links", + "name": "SocialLinks", + "status": "PENDING", + "file_path": "components/SocialLinks.tsx" + }, + { + "id": "component_auth_form", + "name": "AuthForm", + "status": "PENDING", + "file_path": "components/AuthForm.tsx" + }, + { + "id": "component_search_bar", + "name": "SearchBar", + "status": "PENDING", + "file_path": "components/SearchBar.tsx" + }, + { + "id": "component_search_results", + "name": "SearchResults", + "status": "PENDING", + "file_path": "components/SearchResults.tsx" + }, + { + "id": "component_create_playlist_modal", + "name": "CreatePlaylistModal", + "status": "PENDING", + "file_path": "components/CreatePlaylistModal.tsx" + }, + { + "id": "component_profile_form", + "name": "ProfileForm", + "status": "PENDING", + "file_path": "components/ProfileForm.tsx" + }, + { + "id": "component_avatar_upload", + "name": "AvatarUpload", + "status": "PENDING", + "file_path": "components/AvatarUpload.tsx" + }, + { + "id": "component_section_header", + "name": "SectionHeader", + "status": "PENDING", + "file_path": "components/SectionHeader.tsx" + }, + { + "id": "component_genre_header", + "name": "GenreHeader", + "status": "PENDING", + "file_path": "components/GenreHeader.tsx" + }, + { + "id": "component_header", + "name": "Header", + "status": "IMPLEMENTED", + "file_path": "components/Header.tsx" + }, + { + "id": "component_nav_link", + "name": "NavLink", + "status": "IMPLEMENTED", + "file_path": "components/NavLink.tsx" + }, + { + "id": "component_user_menu", + "name": "UserMenu", + "status": "IMPLEMENTED", + "file_path": "components/UserMenu.tsx" + } + ] + }, + "dependencies": { + "component_to_page": { + "component_auth_form": ["page_login", "page_register", "page_forgot_password"], + "component_song_card": ["page_home", "page_artist_profile", "page_search", "page_genre_browse", "page_album_detail", "page_playlist_detail"], + "component_genre_badge": ["page_home"], + "component_section_header": ["page_home"], + "component_artist_header": ["page_artist_profile"], + "component_album_card": ["page_artist_profile", "page_search"], + "component_social_links": ["page_artist_profile"], + "component_album_header": ["page_album_detail"], + "component_track_list": ["page_album_detail", "page_playlist_detail"], + "component_upload_form": ["page_upload"], + "component_waveform_display": ["page_upload"], + "component_playlist_card": ["page_playlists"], + "component_create_playlist_modal": ["page_playlists"], + "component_playlist_header": ["page_playlist_detail"], + "component_profile_form": ["page_profile"], + "component_avatar_upload": ["page_profile"], + "component_search_bar": ["page_search"], + "component_search_results": ["page_search"], + "component_artist_card": ["page_search"], + "component_genre_header": ["page_genre_browse"] + }, + "api_to_component": { + "api_login": ["component_auth_form"], + "api_register": ["component_auth_form"], + "api_forgot_password": ["component_auth_form"], + "api_upload_song": ["component_upload_form"], + "api_increment_play_count": ["component_audio_player"], + "api_search": ["component_search_bar"], + "api_create_playlist": ["component_create_playlist_modal"], + "api_update_current_user": ["component_profile_form"] + }, + "table_to_api": { + "model_user": ["api_register", "api_login", "api_forgot_password", "api_reset_password", "api_get_current_user", "api_update_current_user"], + "model_artist": ["api_create_artist_profile", "api_get_artist", "api_update_artist", "api_get_artist_songs", "api_get_artist_albums"], + "model_song": ["api_upload_song", "api_get_song", "api_update_song", "api_delete_song", "api_increment_play_count", "api_get_artist_songs", "api_get_trending_songs", "api_get_new_releases", "api_search"], + "model_album": ["api_create_album", "api_get_album", "api_update_album", "api_delete_album", "api_get_artist_albums", "api_search"], + "model_playlist": ["api_create_playlist", "api_get_user_playlists", "api_get_playlist", "api_update_playlist", "api_delete_playlist"], + "model_playlist_song": ["api_add_song_to_playlist", "api_remove_song_from_playlist", "api_reorder_playlist_songs"], + "model_genre": ["api_get_genres", "api_get_songs_by_genre"], + "model_label": ["api_create_label_profile", "api_get_label_artists"] + } + } +} diff --git a/.workflow/versions/v002/snapshot_before/manifest.json b/.workflow/versions/v002/snapshot_before/manifest.json new file mode 100644 index 0000000..636ef11 --- /dev/null +++ b/.workflow/versions/v002/snapshot_before/manifest.json @@ -0,0 +1,641 @@ +{ + "project": { + "name": "sonic-cloud", + "version": "0.1.0", + "created_at": "2025-12-18T14:32:39.275839", + "description": "Music platform for musicians to upload songs" + }, + "state": { + "current_phase": "DESIGN_PHASE", + "approval_status": { + "manifest_approved": false, + "approved_by": null, + "approved_at": null + }, + "revision_history": [ + { + "action": "PROJECT_INITIALIZED", + "timestamp": "2025-12-18T14:32:39.275844", + "details": "Project sonic-cloud created" + }, + { + "action": "DESIGN_DOCUMENT_CREATED", + "timestamp": "2025-12-18T15:10:00", + "details": "Complete design document with 91 entities created" + } + ] + }, + "entities": { + "database_tables": [ + { + "id": "model_user", + "name": "User", + "table_name": "users", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_artist", + "name": "Artist", + "table_name": "artists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_label", + "name": "Label", + "table_name": "labels", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_genre", + "name": "Genre", + "table_name": "genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_album", + "name": "Album", + "table_name": "albums", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song", + "name": "Song", + "table_name": "songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song_genre", + "name": "SongGenre", + "table_name": "song_genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist", + "name": "Playlist", + "table_name": "playlists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist_song", + "name": "PlaylistSong", + "table_name": "playlist_songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + } + ], + "api_endpoints": [ + { + "id": "api_register", + "name": "Register User", + "path": "/api/auth/register", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/register/route.ts" + }, + { + "id": "api_login", + "name": "Login", + "path": "/api/auth/login", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/login/route.ts" + }, + { + "id": "api_forgot_password", + "name": "Forgot Password", + "path": "/api/auth/forgot-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/forgot-password/route.ts" + }, + { + "id": "api_reset_password", + "name": "Reset Password", + "path": "/api/auth/reset-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/reset-password/route.ts" + }, + { + "id": "api_get_current_user", + "name": "Get Current User", + "path": "/api/users/me", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_update_current_user", + "name": "Update Current User", + "path": "/api/users/me", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_create_artist_profile", + "name": "Create Artist Profile", + "path": "/api/artists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/artists/route.ts" + }, + { + "id": "api_get_artist", + "name": "Get Artist", + "path": "/api/artists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_update_artist", + "name": "Update Artist", + "path": "/api/artists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_get_artist_songs", + "name": "Get Artist Songs", + "path": "/api/artists/:id/songs", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/songs/route.ts" + }, + { + "id": "api_get_artist_albums", + "name": "Get Artist Albums", + "path": "/api/artists/:id/albums", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/albums/route.ts" + }, + { + "id": "api_upload_song", + "name": "Upload Song", + "path": "/api/songs/upload", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/upload/route.ts" + }, + { + "id": "api_get_song", + "name": "Get Song", + "path": "/api/songs/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_update_song", + "name": "Update Song", + "path": "/api/songs/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_delete_song", + "name": "Delete Song", + "path": "/api/songs/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_increment_play_count", + "name": "Increment Play Count", + "path": "/api/songs/:id/play", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/[id]/play/route.ts" + }, + { + "id": "api_create_album", + "name": "Create Album", + "path": "/api/albums", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/albums/route.ts" + }, + { + "id": "api_get_album", + "name": "Get Album", + "path": "/api/albums/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_update_album", + "name": "Update Album", + "path": "/api/albums/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_delete_album", + "name": "Delete Album", + "path": "/api/albums/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_create_playlist", + "name": "Create Playlist", + "path": "/api/playlists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_user_playlists", + "name": "Get User Playlists", + "path": "/api/playlists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_playlist", + "name": "Get Playlist", + "path": "/api/playlists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_update_playlist", + "name": "Update Playlist", + "path": "/api/playlists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_delete_playlist", + "name": "Delete Playlist", + "path": "/api/playlists/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_add_song_to_playlist", + "name": "Add Song to Playlist", + "path": "/api/playlists/:id/songs", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/songs/route.ts" + }, + { + "id": "api_remove_song_from_playlist", + "name": "Remove Song from Playlist", + "path": "/api/playlists/:playlistId/songs/:songId", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[playlistId]/songs/[songId]/route.ts" + }, + { + "id": "api_reorder_playlist_songs", + "name": "Reorder Playlist Songs", + "path": "/api/playlists/:id/reorder", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/reorder/route.ts" + }, + { + "id": "api_get_trending_songs", + "name": "Get Trending Songs", + "path": "/api/discover/trending", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/trending/route.ts" + }, + { + "id": "api_get_new_releases", + "name": "Get New Releases", + "path": "/api/discover/new-releases", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/new-releases/route.ts" + }, + { + "id": "api_get_genres", + "name": "Get Genres", + "path": "/api/discover/genres", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/route.ts" + }, + { + "id": "api_get_songs_by_genre", + "name": "Get Songs by Genre", + "path": "/api/discover/genres/:slug", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/[slug]/route.ts" + }, + { + "id": "api_search", + "name": "Search", + "path": "/api/search", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/search/route.ts" + }, + { + "id": "api_create_label_profile", + "name": "Create Label Profile", + "path": "/api/labels", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/labels/route.ts" + }, + { + "id": "api_get_label_artists", + "name": "Get Label Artists", + "path": "/api/labels/:id/artists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/labels/[id]/artists/route.ts" + } + ], + "pages": [ + { + "id": "page_login", + "name": "Login", + "path": "/login", + "status": "PENDING", + "file_path": "app/login/page.tsx" + }, + { + "id": "page_register", + "name": "Register", + "path": "/register", + "status": "PENDING", + "file_path": "app/register/page.tsx" + }, + { + "id": "page_forgot_password", + "name": "Forgot Password", + "path": "/forgot-password", + "status": "PENDING", + "file_path": "app/forgot-password/page.tsx" + }, + { + "id": "page_home", + "name": "Discover Music", + "path": "/", + "status": "PENDING", + "file_path": "app/page.tsx" + }, + { + "id": "page_artist_profile", + "name": "Artist Profile", + "path": "/artist/:id", + "status": "PENDING", + "file_path": "app/artist/[id]/page.tsx" + }, + { + "id": "page_album_detail", + "name": "Album", + "path": "/album/:id", + "status": "PENDING", + "file_path": "app/album/[id]/page.tsx" + }, + { + "id": "page_upload", + "name": "Upload Music", + "path": "/upload", + "status": "PENDING", + "file_path": "app/upload/page.tsx" + }, + { + "id": "page_playlists", + "name": "My Playlists", + "path": "/playlists", + "status": "PENDING", + "file_path": "app/playlists/page.tsx" + }, + { + "id": "page_playlist_detail", + "name": "Playlist", + "path": "/playlist/:id", + "status": "PENDING", + "file_path": "app/playlist/[id]/page.tsx" + }, + { + "id": "page_profile", + "name": "Profile Settings", + "path": "/profile", + "status": "PENDING", + "file_path": "app/profile/page.tsx" + }, + { + "id": "page_search", + "name": "Search", + "path": "/search", + "status": "PENDING", + "file_path": "app/search/page.tsx" + }, + { + "id": "page_genre_browse", + "name": "Browse Genre", + "path": "/genre/:slug", + "status": "PENDING", + "file_path": "app/genre/[slug]/page.tsx" + } + ], + "components": [ + { + "id": "component_audio_player", + "name": "AudioPlayer", + "status": "PENDING", + "file_path": "components/AudioPlayer.tsx" + }, + { + "id": "component_player_controls", + "name": "PlayerControls", + "status": "PENDING", + "file_path": "components/PlayerControls.tsx" + }, + { + "id": "component_song_card", + "name": "SongCard", + "status": "PENDING", + "file_path": "components/SongCard.tsx" + }, + { + "id": "component_album_card", + "name": "AlbumCard", + "status": "PENDING", + "file_path": "components/AlbumCard.tsx" + }, + { + "id": "component_artist_card", + "name": "ArtistCard", + "status": "PENDING", + "file_path": "components/ArtistCard.tsx" + }, + { + "id": "component_playlist_card", + "name": "PlaylistCard", + "status": "PENDING", + "file_path": "components/PlaylistCard.tsx" + }, + { + "id": "component_upload_form", + "name": "UploadForm", + "status": "PENDING", + "file_path": "components/UploadForm.tsx" + }, + { + "id": "component_waveform_display", + "name": "WaveformDisplay", + "status": "PENDING", + "file_path": "components/WaveformDisplay.tsx" + }, + { + "id": "component_genre_badge", + "name": "GenreBadge", + "status": "PENDING", + "file_path": "components/GenreBadge.tsx" + }, + { + "id": "component_track_list", + "name": "TrackList", + "status": "PENDING", + "file_path": "components/TrackList.tsx" + }, + { + "id": "component_artist_header", + "name": "ArtistHeader", + "status": "PENDING", + "file_path": "components/ArtistHeader.tsx" + }, + { + "id": "component_album_header", + "name": "AlbumHeader", + "status": "PENDING", + "file_path": "components/AlbumHeader.tsx" + }, + { + "id": "component_playlist_header", + "name": "PlaylistHeader", + "status": "PENDING", + "file_path": "components/PlaylistHeader.tsx" + }, + { + "id": "component_social_links", + "name": "SocialLinks", + "status": "PENDING", + "file_path": "components/SocialLinks.tsx" + }, + { + "id": "component_auth_form", + "name": "AuthForm", + "status": "PENDING", + "file_path": "components/AuthForm.tsx" + }, + { + "id": "component_search_bar", + "name": "SearchBar", + "status": "PENDING", + "file_path": "components/SearchBar.tsx" + }, + { + "id": "component_search_results", + "name": "SearchResults", + "status": "PENDING", + "file_path": "components/SearchResults.tsx" + }, + { + "id": "component_create_playlist_modal", + "name": "CreatePlaylistModal", + "status": "PENDING", + "file_path": "components/CreatePlaylistModal.tsx" + }, + { + "id": "component_profile_form", + "name": "ProfileForm", + "status": "PENDING", + "file_path": "components/ProfileForm.tsx" + }, + { + "id": "component_avatar_upload", + "name": "AvatarUpload", + "status": "PENDING", + "file_path": "components/AvatarUpload.tsx" + }, + { + "id": "component_section_header", + "name": "SectionHeader", + "status": "PENDING", + "file_path": "components/SectionHeader.tsx" + }, + { + "id": "component_genre_header", + "name": "GenreHeader", + "status": "PENDING", + "file_path": "components/GenreHeader.tsx" + } + ] + }, + "dependencies": { + "component_to_page": { + "component_auth_form": ["page_login", "page_register", "page_forgot_password"], + "component_song_card": ["page_home", "page_artist_profile", "page_search", "page_genre_browse", "page_album_detail", "page_playlist_detail"], + "component_genre_badge": ["page_home"], + "component_section_header": ["page_home"], + "component_artist_header": ["page_artist_profile"], + "component_album_card": ["page_artist_profile", "page_search"], + "component_social_links": ["page_artist_profile"], + "component_album_header": ["page_album_detail"], + "component_track_list": ["page_album_detail", "page_playlist_detail"], + "component_upload_form": ["page_upload"], + "component_waveform_display": ["page_upload"], + "component_playlist_card": ["page_playlists"], + "component_create_playlist_modal": ["page_playlists"], + "component_playlist_header": ["page_playlist_detail"], + "component_profile_form": ["page_profile"], + "component_avatar_upload": ["page_profile"], + "component_search_bar": ["page_search"], + "component_search_results": ["page_search"], + "component_artist_card": ["page_search"], + "component_genre_header": ["page_genre_browse"] + }, + "api_to_component": { + "api_login": ["component_auth_form"], + "api_register": ["component_auth_form"], + "api_forgot_password": ["component_auth_form"], + "api_upload_song": ["component_upload_form"], + "api_increment_play_count": ["component_audio_player"], + "api_search": ["component_search_bar"], + "api_create_playlist": ["component_create_playlist_modal"], + "api_update_current_user": ["component_profile_form"] + }, + "table_to_api": { + "model_user": ["api_register", "api_login", "api_forgot_password", "api_reset_password", "api_get_current_user", "api_update_current_user"], + "model_artist": ["api_create_artist_profile", "api_get_artist", "api_update_artist", "api_get_artist_songs", "api_get_artist_albums"], + "model_song": ["api_upload_song", "api_get_song", "api_update_song", "api_delete_song", "api_increment_play_count", "api_get_artist_songs", "api_get_trending_songs", "api_get_new_releases", "api_search"], + "model_album": ["api_create_album", "api_get_album", "api_update_album", "api_delete_album", "api_get_artist_albums", "api_search"], + "model_playlist": ["api_create_playlist", "api_get_user_playlists", "api_get_playlist", "api_update_playlist", "api_delete_playlist"], + "model_playlist_song": ["api_add_song_to_playlist", "api_remove_song_from_playlist", "api_reorder_playlist_songs"], + "model_genre": ["api_get_genres", "api_get_songs_by_genre"], + "model_label": ["api_create_label_profile", "api_get_label_artists"] + } + } +} diff --git a/.workflow/versions/v002/tasks/task_component_header.yml b/.workflow/versions/v002/tasks/task_component_header.yml new file mode 100644 index 0000000..21d58ab --- /dev/null +++ b/.workflow/versions/v002/tasks/task_component_header.yml @@ -0,0 +1,56 @@ +task_id: task_component_header +entity_id: component_header +workflow_version: v002 +created_at: "2025-12-18T17:08:00" +status: completed +agent: frontend + +entity: + type: component + name: Header + file_path: components/Header.tsx + +description: | + Create the main Header component that combines logo, navigation, and user menu. + Must be responsive with mobile hamburger menu. + +file_paths: + to_create: + - components/Header.tsx + +reference_files: + - components/NavLink.tsx + - components/UserMenu.tsx + - app/page.tsx # For style consistency + +implementation_notes: | + - Use 'use client' directive for mobile menu state + - Import NavLink and UserMenu components + - Layout: + - Left: Logo (SonicCloud) linked to / + - Center/Left: Navigation links (desktop) + - Right: UserMenu + - Mobile: Hamburger icon, dropdown menu + - Use useState for mobile menu open/close + - Background: zinc-900 or zinc-950/95 with backdrop blur + - Border bottom: zinc-800 + - Fixed position at top with z-50 + - Navigation links: + - Home (/) + - Search (/search) + - Playlists (/playlists) + - Upload (/upload) + - Use flex layout with justify-between + - Logo: text-2xl font-bold, gradient text (purple-500 to pink-500) + +acceptance_criteria: + - Header renders with logo + - All navigation links present + - UserMenu integrated + - Mobile hamburger menu works + - Fixed at top of viewport + - Proper z-index for overlapping content + +dependencies: + - component_nav_link + - component_user_menu diff --git a/.workflow/versions/v002/tasks/task_component_nav_link.yml b/.workflow/versions/v002/tasks/task_component_nav_link.yml new file mode 100644 index 0000000..9748988 --- /dev/null +++ b/.workflow/versions/v002/tasks/task_component_nav_link.yml @@ -0,0 +1,39 @@ +task_id: task_component_nav_link +entity_id: component_nav_link +workflow_version: v002 +created_at: "2025-12-18T17:08:00" +status: completed +agent: frontend + +entity: + type: component + name: NavLink + file_path: components/NavLink.tsx + +description: | + Create a NavLink component that provides styled navigation links with active state. + Uses Next.js Link and usePathname to detect current page. + +file_paths: + to_create: + - components/NavLink.tsx + +reference_files: + - components/SongCard.tsx # Example of existing component styling + +implementation_notes: | + - Use 'use client' directive for usePathname + - Import Link from 'next/link' + - Import usePathname from 'next/navigation' + - Props: href (string, required), children (ReactNode), icon (string, optional) + - Compare pathname with href for active state + - Style: zinc-400 default, white when active, zinc-200 on hover + - Add left border or underline when active + +acceptance_criteria: + - Component exports NavLink function + - Active state detects current page + - Hover styling applied + - Accessible with proper link semantics + +dependencies: [] diff --git a/.workflow/versions/v002/tasks/task_component_user_menu.yml b/.workflow/versions/v002/tasks/task_component_user_menu.yml new file mode 100644 index 0000000..25db481 --- /dev/null +++ b/.workflow/versions/v002/tasks/task_component_user_menu.yml @@ -0,0 +1,40 @@ +task_id: task_component_user_menu +entity_id: component_user_menu +workflow_version: v002 +created_at: "2025-12-18T17:08:00" +status: completed +agent: frontend + +entity: + type: component + name: UserMenu + file_path: components/UserMenu.tsx + +description: | + Create a UserMenu component that displays authentication state. + Shows Login/Register buttons for guests, profile info for logged-in users. + +file_paths: + to_create: + - components/UserMenu.tsx + +reference_files: + - components/AuthForm.tsx # Auth patterns + - lib/auth.ts # Auth utilities if exists + +implementation_notes: | + - Use 'use client' directive + - For now, use a simple mock auth state (no real auth yet) + - Render login/register links when not authenticated + - Render user avatar placeholder and profile link when authenticated + - Style consistent with dark theme (zinc-950 background) + - Login button: outline style + - Register button: filled gradient (purple-600 to pink-600) + +acceptance_criteria: + - Component exports UserMenu function + - Shows Login/Register when not authenticated + - Shows user info when authenticated + - Links navigate correctly + +dependencies: [] diff --git a/.workflow/versions/v002/tasks/task_layout_root.yml b/.workflow/versions/v002/tasks/task_layout_root.yml new file mode 100644 index 0000000..db5bb66 --- /dev/null +++ b/.workflow/versions/v002/tasks/task_layout_root.yml @@ -0,0 +1,41 @@ +task_id: task_layout_root +entity_id: layout_root +workflow_version: v002 +created_at: "2025-12-18T17:08:00" +status: completed +agent: frontend + +entity: + type: layout + name: RootLayout + file_path: app/layout.tsx + +description: | + Modify the root layout to include the Header component. + Update metadata for SonicCloud branding. + +file_paths: + to_modify: + - app/layout.tsx + +reference_files: + - components/Header.tsx + +implementation_notes: | + - Import Header component from @/components/Header + - Render Header before {children} + - Add padding-top to body or main to account for fixed header + - Update metadata: + - title: "SonicCloud - Music for Everyone" + - description: "Discover, upload, and share music on SonicCloud" + - Keep existing font configuration + - Ensure body has min-h-screen and bg-zinc-950 + +acceptance_criteria: + - Header appears on all pages + - Content not hidden behind fixed header + - Metadata updated + - Existing functionality preserved + +dependencies: + - component_header diff --git a/.workflow/versions/v003/contexts/api_create_label_invitation.yml b/.workflow/versions/v003/contexts/api_create_label_invitation.yml new file mode 100644 index 0000000..c0b61de --- /dev/null +++ b/.workflow/versions/v003/contexts/api_create_label_invitation.yml @@ -0,0 +1,158 @@ +task_id: task_create_api_create_label_invitation +entity_id: api_create_label_invitation +generated_at: '2025-12-18T17:43:33.712592' +workflow_version: v003 +target: + type: api + definition: + id: api_create_label_invitation + method: POST + path: /api/labels/[id]/invitations + summary: Create artist invitation + description: Send invitation to an artist to join the label + tags: + - labels + - invitations + path_params: + - name: id + type: string + description: Label ID + request_body: + content_type: application/json + schema: + type: object + properties: + - name: artistId + type: string + required: true + description: ID of artist to invite + - name: message + type: string + required: false + description: Optional invitation message + responses: + - status: 201 + description: Invitation created + schema: + type: object + properties: + - name: id + type: uuid + - name: artistId + type: uuid + - name: status + type: string + - name: expiresAt + type: datetime + - status: 400 + description: Artist already has label or pending invitation + - status: 401 + description: Unauthorized + - status: 403 + description: Not label owner + - status: 404 + description: Artist not found + depends_on_models: + - model_label_invitation + depends_on_apis: [] + auth: + required: true + roles: + - label +related: + models: + - id: model_label_invitation + definition: &id001 + id: model_label_invitation + name: LabelInvitation + description: Invitations from labels to artists to join their roster + table_name: label_invitations + fields: + - name: id + type: uuid + constraints: + - primary_key + description: Unique identifier + - name: labelId + type: uuid + constraints: + - foreign_key + - not_null + - indexed + description: Reference to the inviting label (FK to labels table) + - name: artistId + type: uuid + constraints: + - foreign_key + - not_null + - indexed + description: Reference to the invited artist (FK to artists table) + - name: status + type: enum + enum_values: + - pending + - accepted + - declined + - expired + constraints: + - not_null + - default + default: pending + description: Current status of the invitation + - name: message + type: text + constraints: [] + description: Optional message from label to artist + - name: expiresAt + type: datetime + constraints: + - not_null + description: When the invitation expires + - name: createdAt + type: datetime + constraints: + - not_null + description: When invitation was created + - name: updatedAt + type: datetime + constraints: + - not_null + description: When invitation was last updated + relations: [] + indexes: + - fields: + - labelId + - artistId + unique: true + name: label_artist_unique + - fields: + - artistId + - status + unique: false + name: artist_pending_invitations + timestamps: true + soft_delete: false + validations: + - field: expiresAt + rule: future_date + message: Expiration date must be in the future + apis: [] + components: [] +dependencies: + entity_ids: + - model_label_invitation + definitions: + - id: model_label_invitation + type: model + definition: *id001 +files: + to_create: + - app/api/labels/[id]/invitations/route.ts + reference: [] +acceptance: +- criterion: POST /api/labels/[id]/invitations returns success response + verification: curl -X POST /api/labels/[id]/invitations +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v003/contexts/api_delete_label_invitation.yml b/.workflow/versions/v003/contexts/api_delete_label_invitation.yml new file mode 100644 index 0000000..c94abde --- /dev/null +++ b/.workflow/versions/v003/contexts/api_delete_label_invitation.yml @@ -0,0 +1,135 @@ +task_id: task_create_api_delete_label_invitation +entity_id: api_delete_label_invitation +generated_at: '2025-12-18T17:43:33.714652' +workflow_version: v003 +target: + type: api + definition: + id: api_delete_label_invitation + method: DELETE + path: /api/labels/[id]/invitations/[invitationId] + summary: Cancel invitation + description: Cancel a pending invitation + tags: + - labels + - invitations + path_params: + - name: id + type: string + description: Label ID + - name: invitationId + type: string + description: Invitation ID + responses: + - status: 200 + description: Invitation cancelled + - status: 401 + description: Unauthorized + - status: 403 + description: Not label owner + - status: 404 + description: Invitation not found + depends_on_models: + - model_label_invitation + depends_on_apis: [] + auth: + required: true + roles: + - label +related: + models: + - id: model_label_invitation + definition: &id001 + id: model_label_invitation + name: LabelInvitation + description: Invitations from labels to artists to join their roster + table_name: label_invitations + fields: + - name: id + type: uuid + constraints: + - primary_key + description: Unique identifier + - name: labelId + type: uuid + constraints: + - foreign_key + - not_null + - indexed + description: Reference to the inviting label (FK to labels table) + - name: artistId + type: uuid + constraints: + - foreign_key + - not_null + - indexed + description: Reference to the invited artist (FK to artists table) + - name: status + type: enum + enum_values: + - pending + - accepted + - declined + - expired + constraints: + - not_null + - default + default: pending + description: Current status of the invitation + - name: message + type: text + constraints: [] + description: Optional message from label to artist + - name: expiresAt + type: datetime + constraints: + - not_null + description: When the invitation expires + - name: createdAt + type: datetime + constraints: + - not_null + description: When invitation was created + - name: updatedAt + type: datetime + constraints: + - not_null + description: When invitation was last updated + relations: [] + indexes: + - fields: + - labelId + - artistId + unique: true + name: label_artist_unique + - fields: + - artistId + - status + unique: false + name: artist_pending_invitations + timestamps: true + soft_delete: false + validations: + - field: expiresAt + rule: future_date + message: Expiration date must be in the future + apis: [] + components: [] +dependencies: + entity_ids: + - model_label_invitation + definitions: + - id: model_label_invitation + type: model + definition: *id001 +files: + to_create: + - app/api/labels/[id]/invitations/[invitationId]/route.ts + reference: [] +acceptance: +- criterion: DELETE /api/labels/[id]/invitations/[invitationId] returns success response + verification: curl -X DELETE /api/labels/[id]/invitations/[invitationId] +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v003/contexts/api_get_label.yml b/.workflow/versions/v003/contexts/api_get_label.yml new file mode 100644 index 0000000..400417c --- /dev/null +++ b/.workflow/versions/v003/contexts/api_get_label.yml @@ -0,0 +1,159 @@ +task_id: task_create_api_get_label +entity_id: api_get_label +generated_at: '2025-12-18T17:43:33.706954' +workflow_version: v003 +target: + type: api + definition: + id: api_get_label + method: GET + path: /api/labels/[id] + summary: Get label details + description: Retrieve label profile with artist roster and statistics + tags: + - labels + path_params: + - name: id + type: string + description: Label ID + responses: + - status: 200 + description: Label found + schema: + type: object + properties: + - name: id + type: uuid + - name: name + type: string + - name: slug + type: string + - name: description + type: string + - name: logoUrl + type: string + - name: website + type: string + - name: artists + type: array + - name: _count + type: object + example: + id: 550e8400-e29b-41d4-a716-446655440000 + name: Sonic Records + slug: sonic-records + description: Independent music label + artists: [] + _count: + artists: 5 + songs: 120 + - status: 404 + description: Label not found + schema: + type: object + properties: + - name: error + type: string + depends_on_models: + - model_label_invitation + depends_on_apis: [] + auth: + required: false + roles: [] +related: + models: + - id: model_label_invitation + definition: &id001 + id: model_label_invitation + name: LabelInvitation + description: Invitations from labels to artists to join their roster + table_name: label_invitations + fields: + - name: id + type: uuid + constraints: + - primary_key + description: Unique identifier + - name: labelId + type: uuid + constraints: + - foreign_key + - not_null + - indexed + description: Reference to the inviting label (FK to labels table) + - name: artistId + type: uuid + constraints: + - foreign_key + - not_null + - indexed + description: Reference to the invited artist (FK to artists table) + - name: status + type: enum + enum_values: + - pending + - accepted + - declined + - expired + constraints: + - not_null + - default + default: pending + description: Current status of the invitation + - name: message + type: text + constraints: [] + description: Optional message from label to artist + - name: expiresAt + type: datetime + constraints: + - not_null + description: When the invitation expires + - name: createdAt + type: datetime + constraints: + - not_null + description: When invitation was created + - name: updatedAt + type: datetime + constraints: + - not_null + description: When invitation was last updated + relations: [] + indexes: + - fields: + - labelId + - artistId + unique: true + name: label_artist_unique + - fields: + - artistId + - status + unique: false + name: artist_pending_invitations + timestamps: true + soft_delete: false + validations: + - field: expiresAt + rule: future_date + message: Expiration date must be in the future + apis: [] + components: [] +dependencies: + entity_ids: + - model_label_invitation + definitions: + - id: model_label_invitation + type: model + definition: *id001 +files: + to_create: + - app/api/labels/[id]/route.ts + reference: [] +acceptance: +- criterion: GET /api/labels/[id] returns success response + verification: curl -X GET /api/labels/[id] +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v003/contexts/api_get_label_stats.yml b/.workflow/versions/v003/contexts/api_get_label_stats.yml new file mode 100644 index 0000000..0412d51 --- /dev/null +++ b/.workflow/versions/v003/contexts/api_get_label_stats.yml @@ -0,0 +1,63 @@ +task_id: task_create_api_get_label_stats +entity_id: api_get_label_stats +generated_at: '2025-12-18T17:43:33.709941' +workflow_version: v003 +target: + type: api + definition: + id: api_get_label_stats + method: GET + path: /api/labels/[id]/stats + summary: Get label statistics + description: Get artist count, song count, album count, and total plays + tags: + - labels + - statistics + path_params: + - name: id + type: string + description: Label ID + responses: + - status: 200 + description: Statistics retrieved + schema: + type: object + properties: + - name: artistCount + type: integer + - name: songCount + type: integer + - name: albumCount + type: integer + - name: totalPlays + type: integer + example: + artistCount: 5 + songCount: 120 + albumCount: 15 + totalPlays: 45000 + - status: 404 + description: Label not found + depends_on_models: [] + depends_on_apis: [] + auth: + required: false + roles: [] +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/api/labels/[id]/stats/route.ts + reference: [] +acceptance: +- criterion: GET /api/labels/[id]/stats returns success response + verification: curl -X GET /api/labels/[id]/stats +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v003/contexts/api_list_artist_invitations.yml b/.workflow/versions/v003/contexts/api_list_artist_invitations.yml new file mode 100644 index 0000000..0d33490 --- /dev/null +++ b/.workflow/versions/v003/contexts/api_list_artist_invitations.yml @@ -0,0 +1,143 @@ +task_id: task_create_api_list_artist_invitations +entity_id: api_list_artist_invitations +generated_at: '2025-12-18T17:43:33.716414' +workflow_version: v003 +target: + type: api + definition: + id: api_list_artist_invitations + method: GET + path: /api/artists/[id]/invitations + summary: List artist invitations + description: Get all invitations received by this artist + tags: + - artists + - invitations + path_params: + - name: id + type: string + description: Artist ID + responses: + - status: 200 + description: Invitations list + schema: + type: array + properties: + - name: id + type: uuid + - name: label + type: object + - name: status + type: string + - name: message + type: string + - name: expiresAt + type: datetime + - status: 401 + description: Unauthorized + - status: 403 + description: Not the artist + depends_on_models: + - model_label_invitation + depends_on_apis: [] + auth: + required: true + roles: + - artist +related: + models: + - id: model_label_invitation + definition: &id001 + id: model_label_invitation + name: LabelInvitation + description: Invitations from labels to artists to join their roster + table_name: label_invitations + fields: + - name: id + type: uuid + constraints: + - primary_key + description: Unique identifier + - name: labelId + type: uuid + constraints: + - foreign_key + - not_null + - indexed + description: Reference to the inviting label (FK to labels table) + - name: artistId + type: uuid + constraints: + - foreign_key + - not_null + - indexed + description: Reference to the invited artist (FK to artists table) + - name: status + type: enum + enum_values: + - pending + - accepted + - declined + - expired + constraints: + - not_null + - default + default: pending + description: Current status of the invitation + - name: message + type: text + constraints: [] + description: Optional message from label to artist + - name: expiresAt + type: datetime + constraints: + - not_null + description: When the invitation expires + - name: createdAt + type: datetime + constraints: + - not_null + description: When invitation was created + - name: updatedAt + type: datetime + constraints: + - not_null + description: When invitation was last updated + relations: [] + indexes: + - fields: + - labelId + - artistId + unique: true + name: label_artist_unique + - fields: + - artistId + - status + unique: false + name: artist_pending_invitations + timestamps: true + soft_delete: false + validations: + - field: expiresAt + rule: future_date + message: Expiration date must be in the future + apis: [] + components: [] +dependencies: + entity_ids: + - model_label_invitation + definitions: + - id: model_label_invitation + type: model + definition: *id001 +files: + to_create: + - app/api/artists/[id]/invitations/route.ts + reference: [] +acceptance: +- criterion: GET /api/artists/[id]/invitations returns success response + verification: curl -X GET /api/artists/[id]/invitations +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v003/contexts/api_list_label_invitations.yml b/.workflow/versions/v003/contexts/api_list_label_invitations.yml new file mode 100644 index 0000000..7027d84 --- /dev/null +++ b/.workflow/versions/v003/contexts/api_list_label_invitations.yml @@ -0,0 +1,148 @@ +task_id: task_create_api_list_label_invitations +entity_id: api_list_label_invitations +generated_at: '2025-12-18T17:43:33.710801' +workflow_version: v003 +target: + type: api + definition: + id: api_list_label_invitations + method: GET + path: /api/labels/[id]/invitations + summary: List label invitations + description: Get all invitations sent by this label + tags: + - labels + - invitations + path_params: + - name: id + type: string + description: Label ID + query_params: + - name: status + type: string + required: false + description: Filter by status (pending, accepted, declined, expired) + responses: + - status: 200 + description: Invitations list + schema: + type: array + properties: + - name: id + type: uuid + - name: artist + type: object + - name: status + type: string + - name: message + type: string + - name: expiresAt + type: datetime + - status: 401 + description: Unauthorized + - status: 403 + description: Not label owner + depends_on_models: + - model_label_invitation + depends_on_apis: [] + auth: + required: true + roles: + - label +related: + models: + - id: model_label_invitation + definition: &id001 + id: model_label_invitation + name: LabelInvitation + description: Invitations from labels to artists to join their roster + table_name: label_invitations + fields: + - name: id + type: uuid + constraints: + - primary_key + description: Unique identifier + - name: labelId + type: uuid + constraints: + - foreign_key + - not_null + - indexed + description: Reference to the inviting label (FK to labels table) + - name: artistId + type: uuid + constraints: + - foreign_key + - not_null + - indexed + description: Reference to the invited artist (FK to artists table) + - name: status + type: enum + enum_values: + - pending + - accepted + - declined + - expired + constraints: + - not_null + - default + default: pending + description: Current status of the invitation + - name: message + type: text + constraints: [] + description: Optional message from label to artist + - name: expiresAt + type: datetime + constraints: + - not_null + description: When the invitation expires + - name: createdAt + type: datetime + constraints: + - not_null + description: When invitation was created + - name: updatedAt + type: datetime + constraints: + - not_null + description: When invitation was last updated + relations: [] + indexes: + - fields: + - labelId + - artistId + unique: true + name: label_artist_unique + - fields: + - artistId + - status + unique: false + name: artist_pending_invitations + timestamps: true + soft_delete: false + validations: + - field: expiresAt + rule: future_date + message: Expiration date must be in the future + apis: [] + components: [] +dependencies: + entity_ids: + - model_label_invitation + definitions: + - id: model_label_invitation + type: model + definition: *id001 +files: + to_create: + - app/api/labels/[id]/invitations/route.ts + reference: [] +acceptance: +- criterion: GET /api/labels/[id]/invitations returns success response + verification: curl -X GET /api/labels/[id]/invitations +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v003/contexts/api_remove_artist_from_label.yml b/.workflow/versions/v003/contexts/api_remove_artist_from_label.yml new file mode 100644 index 0000000..5ab9348 --- /dev/null +++ b/.workflow/versions/v003/contexts/api_remove_artist_from_label.yml @@ -0,0 +1,55 @@ +task_id: task_create_api_remove_artist_from_label +entity_id: api_remove_artist_from_label +generated_at: '2025-12-18T17:43:33.720001' +workflow_version: v003 +target: + type: api + definition: + id: api_remove_artist_from_label + method: DELETE + path: /api/labels/[id]/artists/[artistId] + summary: Remove artist from label + description: Remove an artist from the label roster + tags: + - labels + - artists + path_params: + - name: id + type: string + description: Label ID + - name: artistId + type: string + description: Artist ID + responses: + - status: 200 + description: Artist removed from label + - status: 401 + description: Unauthorized + - status: 403 + description: Not label owner + - status: 404 + description: Artist not in label + depends_on_models: [] + depends_on_apis: [] + auth: + required: true + roles: + - label +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/api/labels/[id]/artists/[artistId]/route.ts + reference: [] +acceptance: +- criterion: DELETE /api/labels/[id]/artists/[artistId] returns success response + verification: curl -X DELETE /api/labels/[id]/artists/[artistId] +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v003/contexts/api_respond_to_invitation.yml b/.workflow/versions/v003/contexts/api_respond_to_invitation.yml new file mode 100644 index 0000000..66842a0 --- /dev/null +++ b/.workflow/versions/v003/contexts/api_respond_to_invitation.yml @@ -0,0 +1,154 @@ +task_id: task_create_api_respond_to_invitation +entity_id: api_respond_to_invitation +generated_at: '2025-12-18T17:43:33.718125' +workflow_version: v003 +target: + type: api + definition: + id: api_respond_to_invitation + method: POST + path: /api/artists/[id]/invitations/[invitationId]/respond + summary: Respond to invitation + description: Accept or decline a label invitation + tags: + - artists + - invitations + path_params: + - name: id + type: string + description: Artist ID + - name: invitationId + type: string + description: Invitation ID + request_body: + content_type: application/json + schema: + type: object + properties: + - name: response + type: string + required: true + description: Accept or decline (accept/decline) + responses: + - status: 200 + description: Response recorded + schema: + type: object + properties: + - name: status + type: string + - name: label + type: object + - status: 400 + description: Invalid response or invitation already processed + - status: 401 + description: Unauthorized + - status: 403 + description: Not the invited artist + - status: 404 + description: Invitation not found + depends_on_models: + - model_label_invitation + depends_on_apis: [] + auth: + required: true + roles: + - artist +related: + models: + - id: model_label_invitation + definition: &id001 + id: model_label_invitation + name: LabelInvitation + description: Invitations from labels to artists to join their roster + table_name: label_invitations + fields: + - name: id + type: uuid + constraints: + - primary_key + description: Unique identifier + - name: labelId + type: uuid + constraints: + - foreign_key + - not_null + - indexed + description: Reference to the inviting label (FK to labels table) + - name: artistId + type: uuid + constraints: + - foreign_key + - not_null + - indexed + description: Reference to the invited artist (FK to artists table) + - name: status + type: enum + enum_values: + - pending + - accepted + - declined + - expired + constraints: + - not_null + - default + default: pending + description: Current status of the invitation + - name: message + type: text + constraints: [] + description: Optional message from label to artist + - name: expiresAt + type: datetime + constraints: + - not_null + description: When the invitation expires + - name: createdAt + type: datetime + constraints: + - not_null + description: When invitation was created + - name: updatedAt + type: datetime + constraints: + - not_null + description: When invitation was last updated + relations: [] + indexes: + - fields: + - labelId + - artistId + unique: true + name: label_artist_unique + - fields: + - artistId + - status + unique: false + name: artist_pending_invitations + timestamps: true + soft_delete: false + validations: + - field: expiresAt + rule: future_date + message: Expiration date must be in the future + apis: [] + components: [] +dependencies: + entity_ids: + - model_label_invitation + definitions: + - id: model_label_invitation + type: model + definition: *id001 +files: + to_create: + - app/api/artists/[id]/invitations/[invitationId]/respond/route.ts + reference: [] +acceptance: +- criterion: POST /api/artists/[id]/invitations/[invitationId]/respond returns success + response + verification: curl -X POST /api/artists/[id]/invitations/[invitationId]/respond +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v003/contexts/api_update_label.yml b/.workflow/versions/v003/contexts/api_update_label.yml new file mode 100644 index 0000000..5b4c684 --- /dev/null +++ b/.workflow/versions/v003/contexts/api_update_label.yml @@ -0,0 +1,83 @@ +task_id: task_create_api_update_label +entity_id: api_update_label +generated_at: '2025-12-18T17:43:33.708857' +workflow_version: v003 +target: + type: api + definition: + id: api_update_label + method: PUT + path: /api/labels/[id] + summary: Update label profile + description: Update label name, description, logo, and website + tags: + - labels + path_params: + - name: id + type: string + description: Label ID + request_body: + content_type: application/json + schema: + type: object + properties: + - name: name + type: string + required: false + description: Label name + - name: description + type: string + required: false + description: Label description + - name: logoUrl + type: string + required: false + description: Logo URL + - name: website + type: string + required: false + description: Website URL + responses: + - status: 200 + description: Label updated + schema: + type: object + properties: + - name: id + type: uuid + - name: name + type: string + - name: slug + type: string + - name: description + type: string + - status: 401 + description: Unauthorized + - status: 403 + description: Not label owner + - status: 404 + description: Label not found + depends_on_models: [] + depends_on_apis: [] + auth: + required: true + roles: + - label +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/api/labels/[id]/route.ts + reference: [] +acceptance: +- criterion: PUT /api/labels/[id] returns success response + verification: curl -X PUT /api/labels/[id] +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v003/contexts/component_artist_roster.yml b/.workflow/versions/v003/contexts/component_artist_roster.yml new file mode 100644 index 0000000..7040415 --- /dev/null +++ b/.workflow/versions/v003/contexts/component_artist_roster.yml @@ -0,0 +1,56 @@ +task_id: task_create_component_artist_roster +entity_id: component_artist_roster +generated_at: '2025-12-18T17:43:33.732960' +workflow_version: v003 +target: + type: component + definition: + id: component_artist_roster + name: ArtistRoster + props: + - name: artists + type: Artist[] + required: true + description: List of signed artists + - name: isOwner + type: boolean + required: false + default: false + description: Show management controls + - name: emptyMessage + type: string + required: false + default: No artists signed yet + description: Message when roster is empty + events: + - name: onRemoveArtist + payload: string + description: Fires when remove clicked, payload is artist ID + - name: onArtistClick + payload: string + description: Fires when artist clicked + uses_apis: [] + uses_components: [] + internal_state: + - removingArtistId + variants: + - grid + - list +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/ArtistRoster.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v003/contexts/component_invitation_card.yml b/.workflow/versions/v003/contexts/component_invitation_card.yml new file mode 100644 index 0000000..7b0d457 --- /dev/null +++ b/.workflow/versions/v003/contexts/component_invitation_card.yml @@ -0,0 +1,52 @@ +task_id: task_create_component_invitation_card +entity_id: component_invitation_card +generated_at: '2025-12-18T17:43:33.733718' +workflow_version: v003 +target: + type: component + definition: + id: component_invitation_card + name: InvitationCard + props: + - name: invitation + type: LabelInvitation + required: true + description: Invitation object + - name: viewType + type: string + required: true + description: Are we viewing as label or artist (label/artist) + events: + - name: onAccept + payload: string + description: Fires when accept clicked (artist view) + - name: onDecline + payload: string + description: Fires when decline clicked (artist view) + - name: onCancel + payload: string + description: Fires when cancel clicked (label view) + uses_apis: [] + uses_components: [] + internal_state: + - isProcessing + variants: + - default +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/InvitationCard.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v003/contexts/component_invite_artist_modal.yml b/.workflow/versions/v003/contexts/component_invite_artist_modal.yml new file mode 100644 index 0000000..b2c21c1 --- /dev/null +++ b/.workflow/versions/v003/contexts/component_invite_artist_modal.yml @@ -0,0 +1,113 @@ +task_id: task_create_component_invite_artist_modal +entity_id: component_invite_artist_modal +generated_at: '2025-12-18T17:43:33.734468' +workflow_version: v003 +target: + type: component + definition: + id: component_invite_artist_modal + name: InviteArtistModal + props: + - name: isOpen + type: boolean + required: true + description: Whether modal is open + - name: labelId + type: string + required: true + description: Label ID sending invitation + events: + - name: onClose + payload: void + description: Fires when modal should close + - name: onInviteSent + payload: LabelInvitation + description: Fires when invitation successfully sent + uses_apis: + - api_create_label_invitation + uses_components: [] + internal_state: + - searchQuery + - selectedArtist + - message + - isSubmitting + variants: + - default +related: + models: [] + apis: + - id: api_create_label_invitation + definition: &id001 + id: api_create_label_invitation + method: POST + path: /api/labels/[id]/invitations + summary: Create artist invitation + description: Send invitation to an artist to join the label + tags: + - labels + - invitations + path_params: + - name: id + type: string + description: Label ID + request_body: + content_type: application/json + schema: + type: object + properties: + - name: artistId + type: string + required: true + description: ID of artist to invite + - name: message + type: string + required: false + description: Optional invitation message + responses: + - status: 201 + description: Invitation created + schema: + type: object + properties: + - name: id + type: uuid + - name: artistId + type: uuid + - name: status + type: string + - name: expiresAt + type: datetime + - status: 400 + description: Artist already has label or pending invitation + - status: 401 + description: Unauthorized + - status: 403 + description: Not label owner + - status: 404 + description: Artist not found + depends_on_models: + - model_label_invitation + depends_on_apis: [] + auth: + required: true + roles: + - label + components: [] +dependencies: + entity_ids: + - api_create_label_invitation + definitions: + - id: api_create_label_invitation + type: api + definition: *id001 +files: + to_create: + - app/components/InviteArtistModal.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v003/contexts/component_label_card.yml b/.workflow/versions/v003/contexts/component_label_card.yml new file mode 100644 index 0000000..fae58e1 --- /dev/null +++ b/.workflow/versions/v003/contexts/component_label_card.yml @@ -0,0 +1,47 @@ +task_id: task_create_component_label_card +entity_id: component_label_card +generated_at: '2025-12-18T17:43:33.731114' +workflow_version: v003 +target: + type: component + definition: + id: component_label_card + name: LabelCard + props: + - name: label + type: Label + required: true + description: Label object to display + - name: showArtistCount + type: boolean + required: false + default: true + description: Show number of artists + events: + - name: onClick + payload: string + description: Fires when card clicked, payload is label ID + uses_apis: [] + uses_components: [] + internal_state: [] + variants: + - default + - compact +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/LabelCard.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v003/contexts/component_label_header.yml b/.workflow/versions/v003/contexts/component_label_header.yml new file mode 100644 index 0000000..4e57602 --- /dev/null +++ b/.workflow/versions/v003/contexts/component_label_header.yml @@ -0,0 +1,46 @@ +task_id: task_create_component_label_header +entity_id: component_label_header +generated_at: '2025-12-18T17:43:33.731784' +workflow_version: v003 +target: + type: component + definition: + id: component_label_header + name: LabelHeader + props: + - name: label + type: Label + required: true + description: Label with full details + - name: isOwner + type: boolean + required: false + default: false + description: Is current user the label owner + events: + - name: onEdit + payload: void + description: Fires when edit button clicked + uses_apis: [] + uses_components: [] + internal_state: [] + variants: + - default +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/LabelHeader.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v003/contexts/component_label_profile_form.yml b/.workflow/versions/v003/contexts/component_label_profile_form.yml new file mode 100644 index 0000000..027171d --- /dev/null +++ b/.workflow/versions/v003/contexts/component_label_profile_form.yml @@ -0,0 +1,112 @@ +task_id: task_create_component_label_profile_form +entity_id: component_label_profile_form +generated_at: '2025-12-18T17:43:33.735918' +workflow_version: v003 +target: + type: component + definition: + id: component_label_profile_form + name: LabelProfileForm + props: + - name: label + type: Label + required: true + description: Current label data + events: + - name: onSave + payload: Label + description: Fires when form saved successfully + - name: onCancel + payload: void + description: Fires when cancel clicked + uses_apis: + - api_update_label + uses_components: [] + internal_state: + - formData + - isSubmitting + - errors + variants: + - default +related: + models: [] + apis: + - id: api_update_label + definition: &id001 + id: api_update_label + method: PUT + path: /api/labels/[id] + summary: Update label profile + description: Update label name, description, logo, and website + tags: + - labels + path_params: + - name: id + type: string + description: Label ID + request_body: + content_type: application/json + schema: + type: object + properties: + - name: name + type: string + required: false + description: Label name + - name: description + type: string + required: false + description: Label description + - name: logoUrl + type: string + required: false + description: Logo URL + - name: website + type: string + required: false + description: Website URL + responses: + - status: 200 + description: Label updated + schema: + type: object + properties: + - name: id + type: uuid + - name: name + type: string + - name: slug + type: string + - name: description + type: string + - status: 401 + description: Unauthorized + - status: 403 + description: Not label owner + - status: 404 + description: Label not found + depends_on_models: [] + depends_on_apis: [] + auth: + required: true + roles: + - label + components: [] +dependencies: + entity_ids: + - api_update_label + definitions: + - id: api_update_label + type: api + definition: *id001 +files: + to_create: + - app/components/LabelProfileForm.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v003/contexts/component_label_stats.yml b/.workflow/versions/v003/contexts/component_label_stats.yml new file mode 100644 index 0000000..faf3333 --- /dev/null +++ b/.workflow/versions/v003/contexts/component_label_stats.yml @@ -0,0 +1,39 @@ +task_id: task_create_component_label_stats +entity_id: component_label_stats +generated_at: '2025-12-18T17:43:33.732423' +workflow_version: v003 +target: + type: component + definition: + id: component_label_stats + name: LabelStats + props: + - name: stats + type: LabelStats + required: true + description: Statistics object + events: [] + uses_apis: [] + uses_components: [] + internal_state: [] + variants: + - default + - compact +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/LabelStats.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v003/contexts/model_label_invitation.yml b/.workflow/versions/v003/contexts/model_label_invitation.yml new file mode 100644 index 0000000..92dd257 --- /dev/null +++ b/.workflow/versions/v003/contexts/model_label_invitation.yml @@ -0,0 +1,99 @@ +task_id: task_create_model_label_invitation +entity_id: model_label_invitation +generated_at: '2025-12-18T17:43:33.705731' +workflow_version: v003 +target: + type: model + definition: + id: model_label_invitation + name: LabelInvitation + description: Invitations from labels to artists to join their roster + table_name: label_invitations + fields: + - name: id + type: uuid + constraints: + - primary_key + description: Unique identifier + - name: labelId + type: uuid + constraints: + - foreign_key + - not_null + - indexed + description: Reference to the inviting label (FK to labels table) + - name: artistId + type: uuid + constraints: + - foreign_key + - not_null + - indexed + description: Reference to the invited artist (FK to artists table) + - name: status + type: enum + enum_values: + - pending + - accepted + - declined + - expired + constraints: + - not_null + - default + default: pending + description: Current status of the invitation + - name: message + type: text + constraints: [] + description: Optional message from label to artist + - name: expiresAt + type: datetime + constraints: + - not_null + description: When the invitation expires + - name: createdAt + type: datetime + constraints: + - not_null + description: When invitation was created + - name: updatedAt + type: datetime + constraints: + - not_null + description: When invitation was last updated + relations: [] + indexes: + - fields: + - labelId + - artistId + unique: true + name: label_artist_unique + - fields: + - artistId + - status + unique: false + name: artist_pending_invitations + timestamps: true + soft_delete: false + validations: + - field: expiresAt + rule: future_date + message: Expiration date must be in the future +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - prisma/schema.prisma + - app/models/labelinvitation.ts + reference: [] +acceptance: +- criterion: Model defined in Prisma schema + verification: Check prisma/schema.prisma +- criterion: TypeScript types exported + verification: Import type in test file +- criterion: Relations properly configured + verification: Check Prisma relations diff --git a/.workflow/versions/v003/contexts/page_label_dashboard.yml b/.workflow/versions/v003/contexts/page_label_dashboard.yml new file mode 100644 index 0000000..0a9d4b7 --- /dev/null +++ b/.workflow/versions/v003/contexts/page_label_dashboard.yml @@ -0,0 +1,331 @@ +task_id: task_create_page_label_dashboard +entity_id: page_label_dashboard +generated_at: '2025-12-18T17:43:33.723638' +workflow_version: v003 +target: + type: page + definition: + id: page_label_dashboard + name: Label Dashboard + path: /label/dashboard + layout: layout_main + data_needs: + - api_id: api_get_label + purpose: Display label info + on_load: true + - api_id: api_get_label_stats + purpose: Display statistics + on_load: true + - api_id: api_list_label_invitations + purpose: Show pending invitations + on_load: true + components: + - component_label_stats + - component_artist_roster + - component_invitation_card + - component_invite_artist_modal + seo: + title: Label Dashboard | Sonic Cloud + description: Manage your label + auth: + required: true + roles: + - label + redirect: /login +related: + models: [] + apis: + - id: api_get_label + definition: &id001 + id: api_get_label + method: GET + path: /api/labels/[id] + summary: Get label details + description: Retrieve label profile with artist roster and statistics + tags: + - labels + path_params: + - name: id + type: string + description: Label ID + responses: + - status: 200 + description: Label found + schema: + type: object + properties: + - name: id + type: uuid + - name: name + type: string + - name: slug + type: string + - name: description + type: string + - name: logoUrl + type: string + - name: website + type: string + - name: artists + type: array + - name: _count + type: object + example: + id: 550e8400-e29b-41d4-a716-446655440000 + name: Sonic Records + slug: sonic-records + description: Independent music label + artists: [] + _count: + artists: 5 + songs: 120 + - status: 404 + description: Label not found + schema: + type: object + properties: + - name: error + type: string + depends_on_models: + - model_label_invitation + depends_on_apis: [] + auth: + required: false + roles: [] + - id: api_list_label_invitations + definition: &id004 + id: api_list_label_invitations + method: GET + path: /api/labels/[id]/invitations + summary: List label invitations + description: Get all invitations sent by this label + tags: + - labels + - invitations + path_params: + - name: id + type: string + description: Label ID + query_params: + - name: status + type: string + required: false + description: Filter by status (pending, accepted, declined, expired) + responses: + - status: 200 + description: Invitations list + schema: + type: array + properties: + - name: id + type: uuid + - name: artist + type: object + - name: status + type: string + - name: message + type: string + - name: expiresAt + type: datetime + - status: 401 + description: Unauthorized + - status: 403 + description: Not label owner + depends_on_models: + - model_label_invitation + depends_on_apis: [] + auth: + required: true + roles: + - label + - id: api_get_label_stats + definition: &id006 + id: api_get_label_stats + method: GET + path: /api/labels/[id]/stats + summary: Get label statistics + description: Get artist count, song count, album count, and total plays + tags: + - labels + - statistics + path_params: + - name: id + type: string + description: Label ID + responses: + - status: 200 + description: Statistics retrieved + schema: + type: object + properties: + - name: artistCount + type: integer + - name: songCount + type: integer + - name: albumCount + type: integer + - name: totalPlays + type: integer + example: + artistCount: 5 + songCount: 120 + albumCount: 15 + totalPlays: 45000 + - status: 404 + description: Label not found + depends_on_models: [] + depends_on_apis: [] + auth: + required: false + roles: [] + components: + - id: component_invitation_card + definition: &id002 + id: component_invitation_card + name: InvitationCard + props: + - name: invitation + type: LabelInvitation + required: true + description: Invitation object + - name: viewType + type: string + required: true + description: Are we viewing as label or artist (label/artist) + events: + - name: onAccept + payload: string + description: Fires when accept clicked (artist view) + - name: onDecline + payload: string + description: Fires when decline clicked (artist view) + - name: onCancel + payload: string + description: Fires when cancel clicked (label view) + uses_apis: [] + uses_components: [] + internal_state: + - isProcessing + variants: + - default + - id: component_label_stats + definition: &id003 + id: component_label_stats + name: LabelStats + props: + - name: stats + type: LabelStats + required: true + description: Statistics object + events: [] + uses_apis: [] + uses_components: [] + internal_state: [] + variants: + - default + - compact + - id: component_invite_artist_modal + definition: &id005 + id: component_invite_artist_modal + name: InviteArtistModal + props: + - name: isOpen + type: boolean + required: true + description: Whether modal is open + - name: labelId + type: string + required: true + description: Label ID sending invitation + events: + - name: onClose + payload: void + description: Fires when modal should close + - name: onInviteSent + payload: LabelInvitation + description: Fires when invitation successfully sent + uses_apis: + - api_create_label_invitation + uses_components: [] + internal_state: + - searchQuery + - selectedArtist + - message + - isSubmitting + variants: + - default + - id: component_artist_roster + definition: &id007 + id: component_artist_roster + name: ArtistRoster + props: + - name: artists + type: Artist[] + required: true + description: List of signed artists + - name: isOwner + type: boolean + required: false + default: false + description: Show management controls + - name: emptyMessage + type: string + required: false + default: No artists signed yet + description: Message when roster is empty + events: + - name: onRemoveArtist + payload: string + description: Fires when remove clicked, payload is artist ID + - name: onArtistClick + payload: string + description: Fires when artist clicked + uses_apis: [] + uses_components: [] + internal_state: + - removingArtistId + variants: + - grid + - list +dependencies: + entity_ids: + - api_get_label + - component_invitation_card + - component_label_stats + - api_list_label_invitations + - component_invite_artist_modal + - api_get_label_stats + - component_artist_roster + definitions: + - id: api_get_label + type: api + definition: *id001 + - id: component_invitation_card + type: component + definition: *id002 + - id: component_label_stats + type: component + definition: *id003 + - id: api_list_label_invitations + type: api + definition: *id004 + - id: component_invite_artist_modal + type: component + definition: *id005 + - id: api_get_label_stats + type: api + definition: *id006 + - id: component_artist_roster + type: component + definition: *id007 +files: + to_create: + - app/label/dashboard/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /label/dashboard + verification: Navigate to /label/dashboard +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v003/contexts/page_label_invitations.yml b/.workflow/versions/v003/contexts/page_label_invitations.yml new file mode 100644 index 0000000..9c8bb4b --- /dev/null +++ b/.workflow/versions/v003/contexts/page_label_invitations.yml @@ -0,0 +1,161 @@ +task_id: task_create_page_label_invitations +entity_id: page_label_invitations +generated_at: '2025-12-18T17:43:33.729145' +workflow_version: v003 +target: + type: page + definition: + id: page_label_invitations + name: Label Invitations + path: /label/invitations + layout: layout_main + data_needs: + - api_id: api_list_label_invitations + purpose: List all invitations + on_load: true + components: + - component_invitation_card + - component_invite_artist_modal + seo: + title: Invitations | Sonic Cloud + description: Manage artist invitations + auth: + required: true + roles: + - label + redirect: /login +related: + models: [] + apis: + - id: api_list_label_invitations + definition: &id002 + id: api_list_label_invitations + method: GET + path: /api/labels/[id]/invitations + summary: List label invitations + description: Get all invitations sent by this label + tags: + - labels + - invitations + path_params: + - name: id + type: string + description: Label ID + query_params: + - name: status + type: string + required: false + description: Filter by status (pending, accepted, declined, expired) + responses: + - status: 200 + description: Invitations list + schema: + type: array + properties: + - name: id + type: uuid + - name: artist + type: object + - name: status + type: string + - name: message + type: string + - name: expiresAt + type: datetime + - status: 401 + description: Unauthorized + - status: 403 + description: Not label owner + depends_on_models: + - model_label_invitation + depends_on_apis: [] + auth: + required: true + roles: + - label + components: + - id: component_invitation_card + definition: &id001 + id: component_invitation_card + name: InvitationCard + props: + - name: invitation + type: LabelInvitation + required: true + description: Invitation object + - name: viewType + type: string + required: true + description: Are we viewing as label or artist (label/artist) + events: + - name: onAccept + payload: string + description: Fires when accept clicked (artist view) + - name: onDecline + payload: string + description: Fires when decline clicked (artist view) + - name: onCancel + payload: string + description: Fires when cancel clicked (label view) + uses_apis: [] + uses_components: [] + internal_state: + - isProcessing + variants: + - default + - id: component_invite_artist_modal + definition: &id003 + id: component_invite_artist_modal + name: InviteArtistModal + props: + - name: isOpen + type: boolean + required: true + description: Whether modal is open + - name: labelId + type: string + required: true + description: Label ID sending invitation + events: + - name: onClose + payload: void + description: Fires when modal should close + - name: onInviteSent + payload: LabelInvitation + description: Fires when invitation successfully sent + uses_apis: + - api_create_label_invitation + uses_components: [] + internal_state: + - searchQuery + - selectedArtist + - message + - isSubmitting + variants: + - default +dependencies: + entity_ids: + - component_invitation_card + - api_list_label_invitations + - component_invite_artist_modal + definitions: + - id: component_invitation_card + type: component + definition: *id001 + - id: api_list_label_invitations + type: api + definition: *id002 + - id: component_invite_artist_modal + type: component + definition: *id003 +files: + to_create: + - app/label/invitations/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /label/invitations + verification: Navigate to /label/invitations +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v003/contexts/page_label_profile.yml b/.workflow/versions/v003/contexts/page_label_profile.yml new file mode 100644 index 0000000..2ef6506 --- /dev/null +++ b/.workflow/versions/v003/contexts/page_label_profile.yml @@ -0,0 +1,236 @@ +task_id: task_create_page_label_profile +entity_id: page_label_profile +generated_at: '2025-12-18T17:43:33.720815' +workflow_version: v003 +target: + type: page + definition: + id: page_label_profile + name: Label Profile + path: /label/[id] + layout: layout_main + data_needs: + - api_id: api_get_label + purpose: Display label info and artist roster + on_load: true + - api_id: api_get_label_stats + purpose: Display label statistics + on_load: true + components: + - component_label_header + - component_label_stats + - component_artist_roster + seo: + title: '{{label.name}} | Sonic Cloud' + description: '{{label.description}}' + auth: + required: false + roles: [] + redirect: null +related: + models: [] + apis: + - id: api_get_label + definition: &id001 + id: api_get_label + method: GET + path: /api/labels/[id] + summary: Get label details + description: Retrieve label profile with artist roster and statistics + tags: + - labels + path_params: + - name: id + type: string + description: Label ID + responses: + - status: 200 + description: Label found + schema: + type: object + properties: + - name: id + type: uuid + - name: name + type: string + - name: slug + type: string + - name: description + type: string + - name: logoUrl + type: string + - name: website + type: string + - name: artists + type: array + - name: _count + type: object + example: + id: 550e8400-e29b-41d4-a716-446655440000 + name: Sonic Records + slug: sonic-records + description: Independent music label + artists: [] + _count: + artists: 5 + songs: 120 + - status: 404 + description: Label not found + schema: + type: object + properties: + - name: error + type: string + depends_on_models: + - model_label_invitation + depends_on_apis: [] + auth: + required: false + roles: [] + - id: api_get_label_stats + definition: &id004 + id: api_get_label_stats + method: GET + path: /api/labels/[id]/stats + summary: Get label statistics + description: Get artist count, song count, album count, and total plays + tags: + - labels + - statistics + path_params: + - name: id + type: string + description: Label ID + responses: + - status: 200 + description: Statistics retrieved + schema: + type: object + properties: + - name: artistCount + type: integer + - name: songCount + type: integer + - name: albumCount + type: integer + - name: totalPlays + type: integer + example: + artistCount: 5 + songCount: 120 + albumCount: 15 + totalPlays: 45000 + - status: 404 + description: Label not found + depends_on_models: [] + depends_on_apis: [] + auth: + required: false + roles: [] + components: + - id: component_label_stats + definition: &id002 + id: component_label_stats + name: LabelStats + props: + - name: stats + type: LabelStats + required: true + description: Statistics object + events: [] + uses_apis: [] + uses_components: [] + internal_state: [] + variants: + - default + - compact + - id: component_label_header + definition: &id003 + id: component_label_header + name: LabelHeader + props: + - name: label + type: Label + required: true + description: Label with full details + - name: isOwner + type: boolean + required: false + default: false + description: Is current user the label owner + events: + - name: onEdit + payload: void + description: Fires when edit button clicked + uses_apis: [] + uses_components: [] + internal_state: [] + variants: + - default + - id: component_artist_roster + definition: &id005 + id: component_artist_roster + name: ArtistRoster + props: + - name: artists + type: Artist[] + required: true + description: List of signed artists + - name: isOwner + type: boolean + required: false + default: false + description: Show management controls + - name: emptyMessage + type: string + required: false + default: No artists signed yet + description: Message when roster is empty + events: + - name: onRemoveArtist + payload: string + description: Fires when remove clicked, payload is artist ID + - name: onArtistClick + payload: string + description: Fires when artist clicked + uses_apis: [] + uses_components: [] + internal_state: + - removingArtistId + variants: + - grid + - list +dependencies: + entity_ids: + - api_get_label + - component_label_stats + - component_label_header + - api_get_label_stats + - component_artist_roster + definitions: + - id: api_get_label + type: api + definition: *id001 + - id: component_label_stats + type: component + definition: *id002 + - id: component_label_header + type: component + definition: *id003 + - id: api_get_label_stats + type: api + definition: *id004 + - id: component_artist_roster + type: component + definition: *id005 +files: + to_create: + - app/label/[id]/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /label/[id] + verification: Navigate to /label/[id] +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v003/contexts/page_label_settings.yml b/.workflow/versions/v003/contexts/page_label_settings.yml new file mode 100644 index 0000000..b6228a2 --- /dev/null +++ b/.workflow/versions/v003/contexts/page_label_settings.yml @@ -0,0 +1,133 @@ +task_id: task_create_page_label_settings +entity_id: page_label_settings +generated_at: '2025-12-18T17:43:33.727545' +workflow_version: v003 +target: + type: page + definition: + id: page_label_settings + name: Label Settings + path: /label/settings + layout: layout_main + data_needs: + - api_id: api_get_label + purpose: Load current label data for editing + on_load: true + components: + - component_label_profile_form + seo: + title: Label Settings | Sonic Cloud + description: Edit your label profile + auth: + required: true + roles: + - label + redirect: /login +related: + models: [] + apis: + - id: api_get_label + definition: &id002 + id: api_get_label + method: GET + path: /api/labels/[id] + summary: Get label details + description: Retrieve label profile with artist roster and statistics + tags: + - labels + path_params: + - name: id + type: string + description: Label ID + responses: + - status: 200 + description: Label found + schema: + type: object + properties: + - name: id + type: uuid + - name: name + type: string + - name: slug + type: string + - name: description + type: string + - name: logoUrl + type: string + - name: website + type: string + - name: artists + type: array + - name: _count + type: object + example: + id: 550e8400-e29b-41d4-a716-446655440000 + name: Sonic Records + slug: sonic-records + description: Independent music label + artists: [] + _count: + artists: 5 + songs: 120 + - status: 404 + description: Label not found + schema: + type: object + properties: + - name: error + type: string + depends_on_models: + - model_label_invitation + depends_on_apis: [] + auth: + required: false + roles: [] + components: + - id: component_label_profile_form + definition: &id001 + id: component_label_profile_form + name: LabelProfileForm + props: + - name: label + type: Label + required: true + description: Current label data + events: + - name: onSave + payload: Label + description: Fires when form saved successfully + - name: onCancel + payload: void + description: Fires when cancel clicked + uses_apis: + - api_update_label + uses_components: [] + internal_state: + - formData + - isSubmitting + - errors + variants: + - default +dependencies: + entity_ids: + - component_label_profile_form + - api_get_label + definitions: + - id: component_label_profile_form + type: component + definition: *id001 + - id: api_get_label + type: api + definition: *id002 +files: + to_create: + - app/label/settings/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /label/settings + verification: Navigate to /label/settings +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v003/dependency_graph.yml b/.workflow/versions/v003/dependency_graph.yml new file mode 100644 index 0000000..78d47bb --- /dev/null +++ b/.workflow/versions/v003/dependency_graph.yml @@ -0,0 +1,370 @@ +dependency_graph: + design_version: 2 + workflow_version: v003 + generated_at: '2025-12-18T17:43:33.701270' + generator: validate_design.py + stats: + total_entities: 21 + total_layers: 4 + max_parallelism: 9 + critical_path_length: 4 +layers: +- layer: 1 + name: Data Layer + description: Database models - no external dependencies + items: + - id: api_get_label_stats + type: api + name: Get label statistics + depends_on: [] + task_id: task_create_api_get_label_stats + agent: backend + complexity: medium + - id: api_remove_artist_from_label + type: api + name: Remove artist from label + depends_on: [] + task_id: task_create_api_remove_artist_from_label + agent: backend + complexity: medium + - id: api_update_label + type: api + name: Update label profile + depends_on: [] + task_id: task_create_api_update_label + agent: backend + complexity: medium + - id: component_artist_roster + type: component + name: ArtistRoster + depends_on: [] + task_id: task_create_component_artist_roster + agent: frontend + complexity: medium + - id: component_invitation_card + type: component + name: InvitationCard + depends_on: [] + task_id: task_create_component_invitation_card + agent: frontend + complexity: medium + - id: component_label_card + type: component + name: LabelCard + depends_on: [] + task_id: task_create_component_label_card + agent: frontend + complexity: medium + - id: component_label_header + type: component + name: LabelHeader + depends_on: [] + task_id: task_create_component_label_header + agent: frontend + complexity: medium + - id: component_label_stats + type: component + name: LabelStats + depends_on: [] + task_id: task_create_component_label_stats + agent: frontend + complexity: medium + - id: model_label_invitation + type: model + name: LabelInvitation + depends_on: [] + task_id: task_create_model_label_invitation + agent: backend + complexity: medium + requires_layers: [] + parallel_count: 9 +- layer: 2 + name: API Layer + description: REST endpoints - depend on models + items: + - id: api_create_label_invitation + type: api + name: Create artist invitation + depends_on: + - model_label_invitation + task_id: task_create_api_create_label_invitation + agent: backend + complexity: medium + - id: api_delete_label_invitation + type: api + name: Cancel invitation + depends_on: + - model_label_invitation + task_id: task_create_api_delete_label_invitation + agent: backend + complexity: medium + - id: api_get_label + type: api + name: Get label details + depends_on: + - model_label_invitation + task_id: task_create_api_get_label + agent: backend + complexity: medium + - id: api_list_artist_invitations + type: api + name: List artist invitations + depends_on: + - model_label_invitation + task_id: task_create_api_list_artist_invitations + agent: backend + complexity: medium + - id: api_list_label_invitations + type: api + name: List label invitations + depends_on: + - model_label_invitation + task_id: task_create_api_list_label_invitations + agent: backend + complexity: medium + - id: api_respond_to_invitation + type: api + name: Respond to invitation + depends_on: + - model_label_invitation + task_id: task_create_api_respond_to_invitation + agent: backend + complexity: medium + - id: component_label_profile_form + type: component + name: LabelProfileForm + depends_on: + - api_update_label + task_id: task_create_component_label_profile_form + agent: frontend + complexity: medium + requires_layers: + - 1 + parallel_count: 7 +- layer: 3 + name: UI Layer + description: Pages and components - depend on APIs + items: + - id: component_invite_artist_modal + type: component + name: InviteArtistModal + depends_on: + - api_create_label_invitation + task_id: task_create_component_invite_artist_modal + agent: frontend + complexity: medium + - id: page_label_profile + type: page + name: Label Profile + depends_on: + - api_get_label + - component_label_stats + - component_label_header + - api_get_label_stats + - component_artist_roster + task_id: task_create_page_label_profile + agent: frontend + complexity: medium + - id: page_label_settings + type: page + name: Label Settings + depends_on: + - component_label_profile_form + - api_get_label + task_id: task_create_page_label_settings + agent: frontend + complexity: medium + requires_layers: + - 1 + - 2 + parallel_count: 3 +- layer: 4 + name: Layer 4 + description: Entities with 3 levels of dependencies + items: + - id: page_label_dashboard + type: page + name: Label Dashboard + depends_on: + - api_get_label + - component_invitation_card + - component_label_stats + - api_list_label_invitations + - component_invite_artist_modal + - api_get_label_stats + - component_artist_roster + task_id: task_create_page_label_dashboard + agent: frontend + complexity: medium + - id: page_label_invitations + type: page + name: Label Invitations + depends_on: + - component_invitation_card + - api_list_label_invitations + - component_invite_artist_modal + task_id: task_create_page_label_invitations + agent: frontend + complexity: medium + requires_layers: + - 1 + - 2 + - 3 + parallel_count: 2 +dependency_map: + model_label_invitation: + type: model + layer: 1 + depends_on: [] + depended_by: + - api_get_label + - api_create_label_invitation + - api_delete_label_invitation + - api_respond_to_invitation + - api_list_artist_invitations + - api_list_label_invitations + api_get_label: + type: api + layer: 2 + depends_on: + - model_label_invitation + depended_by: + - page_label_profile + - page_label_settings + - page_label_dashboard + api_update_label: + type: api + layer: 1 + depends_on: [] + depended_by: + - component_label_profile_form + api_get_label_stats: + type: api + layer: 1 + depends_on: [] + depended_by: + - page_label_profile + - page_label_dashboard + api_list_label_invitations: + type: api + layer: 2 + depends_on: + - model_label_invitation + depended_by: + - page_label_invitations + - page_label_dashboard + api_create_label_invitation: + type: api + layer: 2 + depends_on: + - model_label_invitation + depended_by: + - component_invite_artist_modal + api_delete_label_invitation: + type: api + layer: 2 + depends_on: + - model_label_invitation + depended_by: [] + api_list_artist_invitations: + type: api + layer: 2 + depends_on: + - model_label_invitation + depended_by: [] + api_respond_to_invitation: + type: api + layer: 2 + depends_on: + - model_label_invitation + depended_by: [] + api_remove_artist_from_label: + type: api + layer: 1 + depends_on: [] + depended_by: [] + page_label_profile: + type: page + layer: 3 + depends_on: + - api_get_label + - component_label_stats + - component_label_header + - api_get_label_stats + - component_artist_roster + depended_by: [] + page_label_dashboard: + type: page + layer: 4 + depends_on: + - api_get_label + - component_invitation_card + - component_label_stats + - api_list_label_invitations + - component_invite_artist_modal + - api_get_label_stats + - component_artist_roster + depended_by: [] + page_label_settings: + type: page + layer: 3 + depends_on: + - component_label_profile_form + - api_get_label + depended_by: [] + page_label_invitations: + type: page + layer: 4 + depends_on: + - component_invitation_card + - api_list_label_invitations + - component_invite_artist_modal + depended_by: [] + component_label_card: + type: component + layer: 1 + depends_on: [] + depended_by: [] + component_label_header: + type: component + layer: 1 + depends_on: [] + depended_by: + - page_label_profile + component_label_stats: + type: component + layer: 1 + depends_on: [] + depended_by: + - page_label_profile + - page_label_dashboard + component_artist_roster: + type: component + layer: 1 + depends_on: [] + depended_by: + - page_label_profile + - page_label_dashboard + component_invitation_card: + type: component + layer: 1 + depends_on: [] + depended_by: + - page_label_invitations + - page_label_dashboard + component_invite_artist_modal: + type: component + layer: 3 + depends_on: + - api_create_label_invitation + depended_by: + - page_label_invitations + - page_label_dashboard + component_label_profile_form: + type: component + layer: 2 + depends_on: + - api_update_label + depended_by: + - page_label_settings +task_map: [] diff --git a/.workflow/versions/v003/design/design_document.yml b/.workflow/versions/v003/design/design_document.yml new file mode 100644 index 0000000..9212caf --- /dev/null +++ b/.workflow/versions/v003/design/design_document.yml @@ -0,0 +1,702 @@ +workflow_version: "v003" +feature: "add label management system" +created_at: "2025-12-18T17:45:00" +updated_at: "2025-12-18T17:50:00" +approved_at: null +status: draft +revision: 2 +revision_notes: "Updated to remove external model references (existing models in codebase)" + +# Note: This design extends existing models (Label, Artist, Song, Album, User) +# which are already defined in prisma/schema.prisma +# The dependencies on these models are implicit (referenced by foreign key field names) + +# ============================================================================ +# LAYER 1: DATA MODELS +# ============================================================================ +data_models: + - id: model_label_invitation + name: LabelInvitation + description: "Invitations from labels to artists to join their roster" + table_name: label_invitations + fields: + - name: id + type: uuid + constraints: [primary_key] + description: "Unique identifier" + - name: labelId + type: uuid + constraints: [foreign_key, not_null, indexed] + description: "Reference to the inviting label (FK to labels table)" + - name: artistId + type: uuid + constraints: [foreign_key, not_null, indexed] + description: "Reference to the invited artist (FK to artists table)" + - name: status + type: enum + enum_values: [pending, accepted, declined, expired] + constraints: [not_null, default] + default: pending + description: "Current status of the invitation" + - name: message + type: text + constraints: [] + description: "Optional message from label to artist" + - name: expiresAt + type: datetime + constraints: [not_null] + description: "When the invitation expires" + - name: createdAt + type: datetime + constraints: [not_null] + description: "When invitation was created" + - name: updatedAt + type: datetime + constraints: [not_null] + description: "When invitation was last updated" + relations: [] # Relations are to existing models, handled by FK fields + indexes: + - fields: [labelId, artistId] + unique: true + name: label_artist_unique + - fields: [artistId, status] + unique: false + name: artist_pending_invitations + timestamps: true + soft_delete: false + validations: + - field: expiresAt + rule: "future_date" + message: "Expiration date must be in the future" + +# ============================================================================ +# LAYER 2: API ENDPOINTS +# ============================================================================ +api_endpoints: + # === LABEL CRUD === + - id: api_get_label + method: GET + path: /api/labels/[id] + summary: "Get label details" + description: "Retrieve label profile with artist roster and statistics" + tags: [labels] + path_params: + - name: id + type: string + description: "Label ID" + responses: + - status: 200 + description: "Label found" + schema: + type: object + properties: + - name: id + type: uuid + - name: name + type: string + - name: slug + type: string + - name: description + type: string + - name: logoUrl + type: string + - name: website + type: string + - name: artists + type: array + - name: _count + type: object + example: + id: "550e8400-e29b-41d4-a716-446655440000" + name: "Sonic Records" + slug: "sonic-records" + description: "Independent music label" + artists: [] + _count: { artists: 5, songs: 120 } + - status: 404 + description: "Label not found" + schema: + type: object + properties: + - name: error + type: string + depends_on_models: [model_label_invitation] # New model only + depends_on_apis: [] + auth: + required: false + roles: [] + + - id: api_update_label + method: PUT + path: /api/labels/[id] + summary: "Update label profile" + description: "Update label name, description, logo, and website" + tags: [labels] + path_params: + - name: id + type: string + description: "Label ID" + request_body: + content_type: application/json + schema: + type: object + properties: + - name: name + type: string + required: false + description: "Label name" + - name: description + type: string + required: false + description: "Label description" + - name: logoUrl + type: string + required: false + description: "Logo URL" + - name: website + type: string + required: false + description: "Website URL" + responses: + - status: 200 + description: "Label updated" + schema: + type: object + properties: + - name: id + type: uuid + - name: name + type: string + - name: slug + type: string + - name: description + type: string + - status: 401 + description: "Unauthorized" + - status: 403 + description: "Not label owner" + - status: 404 + description: "Label not found" + depends_on_models: [] + depends_on_apis: [] + auth: + required: true + roles: [label] + + # === LABEL STATISTICS === + - id: api_get_label_stats + method: GET + path: /api/labels/[id]/stats + summary: "Get label statistics" + description: "Get artist count, song count, album count, and total plays" + tags: [labels, statistics] + path_params: + - name: id + type: string + description: "Label ID" + responses: + - status: 200 + description: "Statistics retrieved" + schema: + type: object + properties: + - name: artistCount + type: integer + - name: songCount + type: integer + - name: albumCount + type: integer + - name: totalPlays + type: integer + example: + artistCount: 5 + songCount: 120 + albumCount: 15 + totalPlays: 45000 + - status: 404 + description: "Label not found" + depends_on_models: [] + depends_on_apis: [] + auth: + required: false + roles: [] + + # === LABEL INVITATIONS === + - id: api_list_label_invitations + method: GET + path: /api/labels/[id]/invitations + summary: "List label invitations" + description: "Get all invitations sent by this label" + tags: [labels, invitations] + path_params: + - name: id + type: string + description: "Label ID" + query_params: + - name: status + type: string + required: false + description: "Filter by status (pending, accepted, declined, expired)" + responses: + - status: 200 + description: "Invitations list" + schema: + type: array + properties: + - name: id + type: uuid + - name: artist + type: object + - name: status + type: string + - name: message + type: string + - name: expiresAt + type: datetime + - status: 401 + description: "Unauthorized" + - status: 403 + description: "Not label owner" + depends_on_models: [model_label_invitation] + depends_on_apis: [] + auth: + required: true + roles: [label] + + - id: api_create_label_invitation + method: POST + path: /api/labels/[id]/invitations + summary: "Create artist invitation" + description: "Send invitation to an artist to join the label" + tags: [labels, invitations] + path_params: + - name: id + type: string + description: "Label ID" + request_body: + content_type: application/json + schema: + type: object + properties: + - name: artistId + type: string + required: true + description: "ID of artist to invite" + - name: message + type: string + required: false + description: "Optional invitation message" + responses: + - status: 201 + description: "Invitation created" + schema: + type: object + properties: + - name: id + type: uuid + - name: artistId + type: uuid + - name: status + type: string + - name: expiresAt + type: datetime + - status: 400 + description: "Artist already has label or pending invitation" + - status: 401 + description: "Unauthorized" + - status: 403 + description: "Not label owner" + - status: 404 + description: "Artist not found" + depends_on_models: [model_label_invitation] + depends_on_apis: [] + auth: + required: true + roles: [label] + + - id: api_delete_label_invitation + method: DELETE + path: /api/labels/[id]/invitations/[invitationId] + summary: "Cancel invitation" + description: "Cancel a pending invitation" + tags: [labels, invitations] + path_params: + - name: id + type: string + description: "Label ID" + - name: invitationId + type: string + description: "Invitation ID" + responses: + - status: 200 + description: "Invitation cancelled" + - status: 401 + description: "Unauthorized" + - status: 403 + description: "Not label owner" + - status: 404 + description: "Invitation not found" + depends_on_models: [model_label_invitation] + depends_on_apis: [] + auth: + required: true + roles: [label] + + # === ARTIST INVITATION HANDLING === + - id: api_list_artist_invitations + method: GET + path: /api/artists/[id]/invitations + summary: "List artist invitations" + description: "Get all invitations received by this artist" + tags: [artists, invitations] + path_params: + - name: id + type: string + description: "Artist ID" + responses: + - status: 200 + description: "Invitations list" + schema: + type: array + properties: + - name: id + type: uuid + - name: label + type: object + - name: status + type: string + - name: message + type: string + - name: expiresAt + type: datetime + - status: 401 + description: "Unauthorized" + - status: 403 + description: "Not the artist" + depends_on_models: [model_label_invitation] + depends_on_apis: [] + auth: + required: true + roles: [artist] + + - id: api_respond_to_invitation + method: POST + path: /api/artists/[id]/invitations/[invitationId]/respond + summary: "Respond to invitation" + description: "Accept or decline a label invitation" + tags: [artists, invitations] + path_params: + - name: id + type: string + description: "Artist ID" + - name: invitationId + type: string + description: "Invitation ID" + request_body: + content_type: application/json + schema: + type: object + properties: + - name: response + type: string + required: true + description: "Accept or decline (accept/decline)" + responses: + - status: 200 + description: "Response recorded" + schema: + type: object + properties: + - name: status + type: string + - name: label + type: object + - status: 400 + description: "Invalid response or invitation already processed" + - status: 401 + description: "Unauthorized" + - status: 403 + description: "Not the invited artist" + - status: 404 + description: "Invitation not found" + depends_on_models: [model_label_invitation] + depends_on_apis: [] + auth: + required: true + roles: [artist] + + # === ARTIST MANAGEMENT === + - id: api_remove_artist_from_label + method: DELETE + path: /api/labels/[id]/artists/[artistId] + summary: "Remove artist from label" + description: "Remove an artist from the label roster" + tags: [labels, artists] + path_params: + - name: id + type: string + description: "Label ID" + - name: artistId + type: string + description: "Artist ID" + responses: + - status: 200 + description: "Artist removed from label" + - status: 401 + description: "Unauthorized" + - status: 403 + description: "Not label owner" + - status: 404 + description: "Artist not in label" + depends_on_models: [] + depends_on_apis: [] + auth: + required: true + roles: [label] + +# ============================================================================ +# LAYER 3: UI PAGES +# ============================================================================ +pages: + - id: page_label_profile + name: "Label Profile" + path: /label/[id] + layout: layout_main + data_needs: + - api_id: api_get_label + purpose: "Display label info and artist roster" + on_load: true + - api_id: api_get_label_stats + purpose: "Display label statistics" + on_load: true + components: + - component_label_header + - component_label_stats + - component_artist_roster + seo: + title: "{{label.name}} | Sonic Cloud" + description: "{{label.description}}" + auth: + required: false + roles: [] + redirect: null + + - id: page_label_dashboard + name: "Label Dashboard" + path: /label/dashboard + layout: layout_main + data_needs: + - api_id: api_get_label + purpose: "Display label info" + on_load: true + - api_id: api_get_label_stats + purpose: "Display statistics" + on_load: true + - api_id: api_list_label_invitations + purpose: "Show pending invitations" + on_load: true + components: + - component_label_stats + - component_artist_roster + - component_invitation_card + - component_invite_artist_modal + seo: + title: "Label Dashboard | Sonic Cloud" + description: "Manage your label" + auth: + required: true + roles: [label] + redirect: /login + + - id: page_label_settings + name: "Label Settings" + path: /label/settings + layout: layout_main + data_needs: + - api_id: api_get_label + purpose: "Load current label data for editing" + on_load: true + components: + - component_label_profile_form + seo: + title: "Label Settings | Sonic Cloud" + description: "Edit your label profile" + auth: + required: true + roles: [label] + redirect: /login + + - id: page_label_invitations + name: "Label Invitations" + path: /label/invitations + layout: layout_main + data_needs: + - api_id: api_list_label_invitations + purpose: "List all invitations" + on_load: true + components: + - component_invitation_card + - component_invite_artist_modal + seo: + title: "Invitations | Sonic Cloud" + description: "Manage artist invitations" + auth: + required: true + roles: [label] + redirect: /login + +# ============================================================================ +# LAYER 3: UI COMPONENTS +# ============================================================================ +components: + - id: component_label_card + name: LabelCard + props: + - name: label + type: "Label" + required: true + description: "Label object to display" + - name: showArtistCount + type: boolean + required: false + default: true + description: "Show number of artists" + events: + - name: onClick + payload: "string" + description: "Fires when card clicked, payload is label ID" + uses_apis: [] + uses_components: [] + internal_state: [] + variants: [default, compact] + + - id: component_label_header + name: LabelHeader + props: + - name: label + type: "Label" + required: true + description: "Label with full details" + - name: isOwner + type: boolean + required: false + default: false + description: "Is current user the label owner" + events: + - name: onEdit + payload: "void" + description: "Fires when edit button clicked" + uses_apis: [] + uses_components: [] + internal_state: [] + variants: [default] + + - id: component_label_stats + name: LabelStats + props: + - name: stats + type: "LabelStats" + required: true + description: "Statistics object" + events: [] + uses_apis: [] + uses_components: [] + internal_state: [] + variants: [default, compact] + + - id: component_artist_roster + name: ArtistRoster + props: + - name: artists + type: "Artist[]" + required: true + description: "List of signed artists" + - name: isOwner + type: boolean + required: false + default: false + description: "Show management controls" + - name: emptyMessage + type: string + required: false + default: "No artists signed yet" + description: "Message when roster is empty" + events: + - name: onRemoveArtist + payload: "string" + description: "Fires when remove clicked, payload is artist ID" + - name: onArtistClick + payload: "string" + description: "Fires when artist clicked" + uses_apis: [] + uses_components: [] # Uses existing ArtistCard component + internal_state: [removingArtistId] + variants: [grid, list] + + - id: component_invitation_card + name: InvitationCard + props: + - name: invitation + type: "LabelInvitation" + required: true + description: "Invitation object" + - name: viewType + type: string + required: true + description: "Are we viewing as label or artist (label/artist)" + events: + - name: onAccept + payload: "string" + description: "Fires when accept clicked (artist view)" + - name: onDecline + payload: "string" + description: "Fires when decline clicked (artist view)" + - name: onCancel + payload: "string" + description: "Fires when cancel clicked (label view)" + uses_apis: [] + uses_components: [] + internal_state: [isProcessing] + variants: [default] + + - id: component_invite_artist_modal + name: InviteArtistModal + props: + - name: isOpen + type: boolean + required: true + description: "Whether modal is open" + - name: labelId + type: string + required: true + description: "Label ID sending invitation" + events: + - name: onClose + payload: "void" + description: "Fires when modal should close" + - name: onInviteSent + payload: "LabelInvitation" + description: "Fires when invitation successfully sent" + uses_apis: [api_create_label_invitation] + uses_components: [] + internal_state: [searchQuery, selectedArtist, message, isSubmitting] + variants: [default] + + - id: component_label_profile_form + name: LabelProfileForm + props: + - name: label + type: "Label" + required: true + description: "Current label data" + events: + - name: onSave + payload: "Label" + description: "Fires when form saved successfully" + - name: onCancel + payload: "void" + description: "Fires when cancel clicked" + uses_apis: [api_update_label] + uses_components: [] + internal_state: [formData, isSubmitting, errors] + variants: [default] diff --git a/.workflow/versions/v003/requirements/expanded.yml b/.workflow/versions/v003/requirements/expanded.yml new file mode 100644 index 0000000..870d7ab --- /dev/null +++ b/.workflow/versions/v003/requirements/expanded.yml @@ -0,0 +1,144 @@ +feature: "add label management system" +expanded_at: "2025-12-18T17:39:00" +mode: full_auto + +analysis: + problem_statement: | + Record labels need a comprehensive management system to: + - View and manage their label profile + - Invite and manage signed artists + - View statistics about their artists and music catalog + - Browse and discover potential artists to sign + target_users: "Record label owners and managers" + core_value: "Centralized label management with artist roster control" + +scope: + mvp_features: + - Label profile page showing label info and signed artists + - Label dashboard for label owners + - Artist roster management (view signed artists) + - Artist invitation system (invite artists to join label) + - Label profile editing + - Label statistics (total artists, songs, plays) + future_features: + - Revenue tracking and royalty management + - Contract management + - Release scheduling + - Analytics dashboard with charts + +data_model: + entities: + - name: LabelInvitation + description: "Invitations from labels to artists" + fields: + - id (string, uuid) + - labelId (string, FK to Label) + - artistId (string, FK to Artist) + - status (enum: pending, accepted, declined, expired) + - message (string, optional invitation message) + - expiresAt (datetime) + - createdAt (datetime) + - updatedAt (datetime) + relations: + - label (Label, many-to-one) + - artist (Artist, many-to-one) + +api_endpoints: + # Label CRUD (existing POST, need GET single and PUT) + - method: GET + path: /api/labels/[id] + purpose: Get label details with artists and stats + + - method: PUT + path: /api/labels/[id] + purpose: Update label profile (name, description, logo, website) + + # Label Invitations + - method: GET + path: /api/labels/[id]/invitations + purpose: List pending invitations from label + + - method: POST + path: /api/labels/[id]/invitations + purpose: Create invitation for an artist + + - method: DELETE + path: /api/labels/[id]/invitations/[invitationId] + purpose: Cancel/delete invitation + + # Artist-side invitation handling + - method: GET + path: /api/artists/[id]/invitations + purpose: List invitations received by artist + + - method: POST + path: /api/artists/[id]/invitations/[invitationId]/respond + purpose: Accept or decline invitation + + # Label Statistics + - method: GET + path: /api/labels/[id]/stats + purpose: Get label statistics (artist count, song count, total plays) + + # Artist removal from label + - method: DELETE + path: /api/labels/[id]/artists/[artistId] + purpose: Remove artist from label roster + +ui_structure: + pages: + - name: LabelProfilePage + route: /label/[id] + purpose: Public view of label profile with artist roster + + - name: LabelDashboardPage + route: /label/dashboard + purpose: Label owner dashboard with management tools + + - name: LabelEditPage + route: /label/settings + purpose: Edit label profile settings + + - name: LabelInvitationsPage + route: /label/invitations + purpose: Manage artist invitations + + components: + - name: LabelCard + purpose: Card displaying label summary for browse/search results + + - name: LabelHeader + purpose: Header banner for label profile page + + - name: LabelStats + purpose: Statistics display (artists, songs, plays) + + - name: ArtistRoster + purpose: Grid/list of signed artists + + - name: InvitationCard + purpose: Card showing invitation with accept/decline actions + + - name: InviteArtistModal + purpose: Modal form to send invitation to artist + + - name: LabelProfileForm + purpose: Form for editing label profile + +security: + authentication: "Required for label management operations" + authorization: + - Label profile view: public + - Label management (edit, invitations): owner only + - Artist roster management: label owner only + - Invitation response: artist who received invitation only + +edge_cases: + - scenario: Artist already belongs to another label + handling: Prevent invitation, show error message + - scenario: Artist already has pending invitation from same label + handling: Prevent duplicate, show existing invitation + - scenario: Invitation expires + handling: Mark as expired, remove from active list + - scenario: Label owner removes themselves + handling: Prevent - must have at least the owner artist diff --git a/.workflow/versions/v003/requirements/final.yml b/.workflow/versions/v003/requirements/final.yml new file mode 100644 index 0000000..2278c02 --- /dev/null +++ b/.workflow/versions/v003/requirements/final.yml @@ -0,0 +1,78 @@ +feature: "add label management system" +mode: full_auto +finalized_at: "2025-12-18T17:42:00" + +# Include all expanded requirements +analysis: + problem_statement: | + Record labels need a comprehensive management system to: + - View and manage their label profile + - Invite and manage signed artists + - View statistics about their artists and music catalog + - Browse and discover potential artists to sign + target_users: "Record label owners and managers" + core_value: "Centralized label management with artist roster control" + +scope: + mvp_features: + - Label profile page showing label info and signed artists + - Label dashboard for label owners + - Artist roster management (view signed artists) + - Artist invitation system (invite artists to join label) + - Label profile editing + - Label statistics (total artists, songs, plays) + +data_model: + entities: + - name: LabelInvitation + fields: [id, labelId, artistId, status, message, expiresAt, createdAt, updatedAt] + relations: [label, artist] + +api_endpoints: + - GET /api/labels/[id] + - PUT /api/labels/[id] + - GET /api/labels/[id]/invitations + - POST /api/labels/[id]/invitations + - DELETE /api/labels/[id]/invitations/[invitationId] + - GET /api/artists/[id]/invitations + - POST /api/artists/[id]/invitations/[invitationId]/respond + - GET /api/labels/[id]/stats + - DELETE /api/labels/[id]/artists/[artistId] + +ui_structure: + pages: [LabelProfilePage, LabelDashboardPage, LabelEditPage, LabelInvitationsPage] + components: [LabelCard, LabelHeader, LabelStats, ArtistRoster, InvitationCard, InviteArtistModal, LabelProfileForm] + +acceptance_criteria: + - criterion: "Label profile page shows label info and artist roster" + verification: "Navigate to /label/[id], verify label name, description, logo, and list of signed artists display correctly" + + - criterion: "Label dashboard accessible to label owners only" + verification: "Login as label owner, navigate to /label/dashboard, verify dashboard loads with management tools" + + - criterion: "Label profile can be edited" + verification: "Navigate to /label/settings, update name/description/logo, save, verify changes persist" + + - criterion: "Label can send invitation to artist" + verification: "From dashboard, click invite artist, select artist, send invitation, verify invitation created" + + - criterion: "Artist can view received invitations" + verification: "Login as artist, check invitations, verify pending invitation from label appears" + + - criterion: "Artist can accept invitation and join label" + verification: "Accept invitation, verify artist now shows label affiliation, appears in label roster" + + - criterion: "Artist can decline invitation" + verification: "Decline invitation, verify invitation removed from pending list, artist remains independent" + + - criterion: "Label can cancel pending invitation" + verification: "From label dashboard, cancel invitation, verify invitation deleted" + + - criterion: "Label statistics display correctly" + verification: "View label profile/dashboard, verify artist count, song count, total plays statistics" + + - criterion: "Label can remove artist from roster" + verification: "From label dashboard, remove artist, verify artist no longer associated with label" + + - criterion: "TypeScript builds without errors" + verification: "Run npm run build, exit code = 0" diff --git a/.workflow/versions/v003/session.yml b/.workflow/versions/v003/session.yml new file mode 100644 index 0000000..6cbd31a --- /dev/null +++ b/.workflow/versions/v003/session.yml @@ -0,0 +1,30 @@ +version: v003 +feature: add label management system +session_id: workflow_20251218_173916 +parent_version: null +status: completed +started_at: '2025-12-18T17:39:16.643117' +completed_at: '2025-12-18T17:52:33.852962' +current_phase: COMPLETING +approvals: + design: + status: approved + approved_by: user + approved_at: '2025-12-18T17:44:13.531452' + rejection_reason: null + implementation: + status: approved + approved_by: user + approved_at: '2025-12-18T17:52:22.627625' + rejection_reason: null +task_sessions: [] +summary: + total_tasks: 0 + tasks_completed: 0 + entities_created: 0 + entities_updated: 0 + entities_deleted: 0 + files_created: 0 + files_updated: 0 + files_deleted: 0 +updated_at: '2025-12-18T17:52:33.852970' diff --git a/.workflow/versions/v003/session.yml.bak b/.workflow/versions/v003/session.yml.bak new file mode 100644 index 0000000..2edd054 --- /dev/null +++ b/.workflow/versions/v003/session.yml.bak @@ -0,0 +1,30 @@ +version: v003 +feature: add label management system +session_id: workflow_20251218_173916 +parent_version: null +status: pending +started_at: '2025-12-18T17:39:16.643117' +completed_at: null +current_phase: IMPL_APPROVED +approvals: + design: + status: approved + approved_by: user + approved_at: '2025-12-18T17:44:13.531452' + rejection_reason: null + implementation: + status: approved + approved_by: user + approved_at: '2025-12-18T17:52:22.627625' + rejection_reason: null +task_sessions: [] +summary: + total_tasks: 0 + tasks_completed: 0 + entities_created: 0 + entities_updated: 0 + entities_deleted: 0 + files_created: 0 + files_updated: 0 + files_deleted: 0 +updated_at: '2025-12-18T17:52:22.628514' diff --git a/.workflow/versions/v003/snapshot_after/manifest.json b/.workflow/versions/v003/snapshot_after/manifest.json new file mode 100644 index 0000000..8e53336 --- /dev/null +++ b/.workflow/versions/v003/snapshot_after/manifest.json @@ -0,0 +1,659 @@ +{ + "project": { + "name": "sonic-cloud", + "version": "0.1.0", + "created_at": "2025-12-18T14:32:39.275839", + "description": "Music platform for musicians to upload songs" + }, + "state": { + "current_phase": "DESIGN_PHASE", + "approval_status": { + "manifest_approved": false, + "approved_by": null, + "approved_at": null + }, + "revision_history": [ + { + "action": "PROJECT_INITIALIZED", + "timestamp": "2025-12-18T14:32:39.275844", + "details": "Project sonic-cloud created" + }, + { + "action": "DESIGN_DOCUMENT_CREATED", + "timestamp": "2025-12-18T15:10:00", + "details": "Complete design document with 91 entities created" + } + ] + }, + "entities": { + "database_tables": [ + { + "id": "model_user", + "name": "User", + "table_name": "users", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_artist", + "name": "Artist", + "table_name": "artists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_label", + "name": "Label", + "table_name": "labels", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_genre", + "name": "Genre", + "table_name": "genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_album", + "name": "Album", + "table_name": "albums", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song", + "name": "Song", + "table_name": "songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song_genre", + "name": "SongGenre", + "table_name": "song_genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist", + "name": "Playlist", + "table_name": "playlists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist_song", + "name": "PlaylistSong", + "table_name": "playlist_songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + } + ], + "api_endpoints": [ + { + "id": "api_register", + "name": "Register User", + "path": "/api/auth/register", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/register/route.ts" + }, + { + "id": "api_login", + "name": "Login", + "path": "/api/auth/login", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/login/route.ts" + }, + { + "id": "api_forgot_password", + "name": "Forgot Password", + "path": "/api/auth/forgot-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/forgot-password/route.ts" + }, + { + "id": "api_reset_password", + "name": "Reset Password", + "path": "/api/auth/reset-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/reset-password/route.ts" + }, + { + "id": "api_get_current_user", + "name": "Get Current User", + "path": "/api/users/me", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_update_current_user", + "name": "Update Current User", + "path": "/api/users/me", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_create_artist_profile", + "name": "Create Artist Profile", + "path": "/api/artists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/artists/route.ts" + }, + { + "id": "api_get_artist", + "name": "Get Artist", + "path": "/api/artists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_update_artist", + "name": "Update Artist", + "path": "/api/artists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_get_artist_songs", + "name": "Get Artist Songs", + "path": "/api/artists/:id/songs", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/songs/route.ts" + }, + { + "id": "api_get_artist_albums", + "name": "Get Artist Albums", + "path": "/api/artists/:id/albums", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/albums/route.ts" + }, + { + "id": "api_upload_song", + "name": "Upload Song", + "path": "/api/songs/upload", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/upload/route.ts" + }, + { + "id": "api_get_song", + "name": "Get Song", + "path": "/api/songs/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_update_song", + "name": "Update Song", + "path": "/api/songs/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_delete_song", + "name": "Delete Song", + "path": "/api/songs/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_increment_play_count", + "name": "Increment Play Count", + "path": "/api/songs/:id/play", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/[id]/play/route.ts" + }, + { + "id": "api_create_album", + "name": "Create Album", + "path": "/api/albums", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/albums/route.ts" + }, + { + "id": "api_get_album", + "name": "Get Album", + "path": "/api/albums/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_update_album", + "name": "Update Album", + "path": "/api/albums/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_delete_album", + "name": "Delete Album", + "path": "/api/albums/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_create_playlist", + "name": "Create Playlist", + "path": "/api/playlists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_user_playlists", + "name": "Get User Playlists", + "path": "/api/playlists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_playlist", + "name": "Get Playlist", + "path": "/api/playlists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_update_playlist", + "name": "Update Playlist", + "path": "/api/playlists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_delete_playlist", + "name": "Delete Playlist", + "path": "/api/playlists/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_add_song_to_playlist", + "name": "Add Song to Playlist", + "path": "/api/playlists/:id/songs", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/songs/route.ts" + }, + { + "id": "api_remove_song_from_playlist", + "name": "Remove Song from Playlist", + "path": "/api/playlists/:playlistId/songs/:songId", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[playlistId]/songs/[songId]/route.ts" + }, + { + "id": "api_reorder_playlist_songs", + "name": "Reorder Playlist Songs", + "path": "/api/playlists/:id/reorder", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/reorder/route.ts" + }, + { + "id": "api_get_trending_songs", + "name": "Get Trending Songs", + "path": "/api/discover/trending", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/trending/route.ts" + }, + { + "id": "api_get_new_releases", + "name": "Get New Releases", + "path": "/api/discover/new-releases", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/new-releases/route.ts" + }, + { + "id": "api_get_genres", + "name": "Get Genres", + "path": "/api/discover/genres", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/route.ts" + }, + { + "id": "api_get_songs_by_genre", + "name": "Get Songs by Genre", + "path": "/api/discover/genres/:slug", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/[slug]/route.ts" + }, + { + "id": "api_search", + "name": "Search", + "path": "/api/search", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/search/route.ts" + }, + { + "id": "api_create_label_profile", + "name": "Create Label Profile", + "path": "/api/labels", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/labels/route.ts" + }, + { + "id": "api_get_label_artists", + "name": "Get Label Artists", + "path": "/api/labels/:id/artists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/labels/[id]/artists/route.ts" + } + ], + "pages": [ + { + "id": "page_login", + "name": "Login", + "path": "/login", + "status": "PENDING", + "file_path": "app/login/page.tsx" + }, + { + "id": "page_register", + "name": "Register", + "path": "/register", + "status": "PENDING", + "file_path": "app/register/page.tsx" + }, + { + "id": "page_forgot_password", + "name": "Forgot Password", + "path": "/forgot-password", + "status": "PENDING", + "file_path": "app/forgot-password/page.tsx" + }, + { + "id": "page_home", + "name": "Discover Music", + "path": "/", + "status": "PENDING", + "file_path": "app/page.tsx" + }, + { + "id": "page_artist_profile", + "name": "Artist Profile", + "path": "/artist/:id", + "status": "PENDING", + "file_path": "app/artist/[id]/page.tsx" + }, + { + "id": "page_album_detail", + "name": "Album", + "path": "/album/:id", + "status": "PENDING", + "file_path": "app/album/[id]/page.tsx" + }, + { + "id": "page_upload", + "name": "Upload Music", + "path": "/upload", + "status": "PENDING", + "file_path": "app/upload/page.tsx" + }, + { + "id": "page_playlists", + "name": "My Playlists", + "path": "/playlists", + "status": "PENDING", + "file_path": "app/playlists/page.tsx" + }, + { + "id": "page_playlist_detail", + "name": "Playlist", + "path": "/playlist/:id", + "status": "PENDING", + "file_path": "app/playlist/[id]/page.tsx" + }, + { + "id": "page_profile", + "name": "Profile Settings", + "path": "/profile", + "status": "PENDING", + "file_path": "app/profile/page.tsx" + }, + { + "id": "page_search", + "name": "Search", + "path": "/search", + "status": "PENDING", + "file_path": "app/search/page.tsx" + }, + { + "id": "page_genre_browse", + "name": "Browse Genre", + "path": "/genre/:slug", + "status": "PENDING", + "file_path": "app/genre/[slug]/page.tsx" + } + ], + "components": [ + { + "id": "component_audio_player", + "name": "AudioPlayer", + "status": "PENDING", + "file_path": "components/AudioPlayer.tsx" + }, + { + "id": "component_player_controls", + "name": "PlayerControls", + "status": "PENDING", + "file_path": "components/PlayerControls.tsx" + }, + { + "id": "component_song_card", + "name": "SongCard", + "status": "PENDING", + "file_path": "components/SongCard.tsx" + }, + { + "id": "component_album_card", + "name": "AlbumCard", + "status": "PENDING", + "file_path": "components/AlbumCard.tsx" + }, + { + "id": "component_artist_card", + "name": "ArtistCard", + "status": "PENDING", + "file_path": "components/ArtistCard.tsx" + }, + { + "id": "component_playlist_card", + "name": "PlaylistCard", + "status": "PENDING", + "file_path": "components/PlaylistCard.tsx" + }, + { + "id": "component_upload_form", + "name": "UploadForm", + "status": "PENDING", + "file_path": "components/UploadForm.tsx" + }, + { + "id": "component_waveform_display", + "name": "WaveformDisplay", + "status": "PENDING", + "file_path": "components/WaveformDisplay.tsx" + }, + { + "id": "component_genre_badge", + "name": "GenreBadge", + "status": "PENDING", + "file_path": "components/GenreBadge.tsx" + }, + { + "id": "component_track_list", + "name": "TrackList", + "status": "PENDING", + "file_path": "components/TrackList.tsx" + }, + { + "id": "component_artist_header", + "name": "ArtistHeader", + "status": "PENDING", + "file_path": "components/ArtistHeader.tsx" + }, + { + "id": "component_album_header", + "name": "AlbumHeader", + "status": "PENDING", + "file_path": "components/AlbumHeader.tsx" + }, + { + "id": "component_playlist_header", + "name": "PlaylistHeader", + "status": "PENDING", + "file_path": "components/PlaylistHeader.tsx" + }, + { + "id": "component_social_links", + "name": "SocialLinks", + "status": "PENDING", + "file_path": "components/SocialLinks.tsx" + }, + { + "id": "component_auth_form", + "name": "AuthForm", + "status": "PENDING", + "file_path": "components/AuthForm.tsx" + }, + { + "id": "component_search_bar", + "name": "SearchBar", + "status": "PENDING", + "file_path": "components/SearchBar.tsx" + }, + { + "id": "component_search_results", + "name": "SearchResults", + "status": "PENDING", + "file_path": "components/SearchResults.tsx" + }, + { + "id": "component_create_playlist_modal", + "name": "CreatePlaylistModal", + "status": "PENDING", + "file_path": "components/CreatePlaylistModal.tsx" + }, + { + "id": "component_profile_form", + "name": "ProfileForm", + "status": "PENDING", + "file_path": "components/ProfileForm.tsx" + }, + { + "id": "component_avatar_upload", + "name": "AvatarUpload", + "status": "PENDING", + "file_path": "components/AvatarUpload.tsx" + }, + { + "id": "component_section_header", + "name": "SectionHeader", + "status": "PENDING", + "file_path": "components/SectionHeader.tsx" + }, + { + "id": "component_genre_header", + "name": "GenreHeader", + "status": "PENDING", + "file_path": "components/GenreHeader.tsx" + }, + { + "id": "component_header", + "name": "Header", + "status": "IMPLEMENTED", + "file_path": "components/Header.tsx" + }, + { + "id": "component_nav_link", + "name": "NavLink", + "status": "IMPLEMENTED", + "file_path": "components/NavLink.tsx" + }, + { + "id": "component_user_menu", + "name": "UserMenu", + "status": "IMPLEMENTED", + "file_path": "components/UserMenu.tsx" + } + ] + }, + "dependencies": { + "component_to_page": { + "component_auth_form": ["page_login", "page_register", "page_forgot_password"], + "component_song_card": ["page_home", "page_artist_profile", "page_search", "page_genre_browse", "page_album_detail", "page_playlist_detail"], + "component_genre_badge": ["page_home"], + "component_section_header": ["page_home"], + "component_artist_header": ["page_artist_profile"], + "component_album_card": ["page_artist_profile", "page_search"], + "component_social_links": ["page_artist_profile"], + "component_album_header": ["page_album_detail"], + "component_track_list": ["page_album_detail", "page_playlist_detail"], + "component_upload_form": ["page_upload"], + "component_waveform_display": ["page_upload"], + "component_playlist_card": ["page_playlists"], + "component_create_playlist_modal": ["page_playlists"], + "component_playlist_header": ["page_playlist_detail"], + "component_profile_form": ["page_profile"], + "component_avatar_upload": ["page_profile"], + "component_search_bar": ["page_search"], + "component_search_results": ["page_search"], + "component_artist_card": ["page_search"], + "component_genre_header": ["page_genre_browse"] + }, + "api_to_component": { + "api_login": ["component_auth_form"], + "api_register": ["component_auth_form"], + "api_forgot_password": ["component_auth_form"], + "api_upload_song": ["component_upload_form"], + "api_increment_play_count": ["component_audio_player"], + "api_search": ["component_search_bar"], + "api_create_playlist": ["component_create_playlist_modal"], + "api_update_current_user": ["component_profile_form"] + }, + "table_to_api": { + "model_user": ["api_register", "api_login", "api_forgot_password", "api_reset_password", "api_get_current_user", "api_update_current_user"], + "model_artist": ["api_create_artist_profile", "api_get_artist", "api_update_artist", "api_get_artist_songs", "api_get_artist_albums"], + "model_song": ["api_upload_song", "api_get_song", "api_update_song", "api_delete_song", "api_increment_play_count", "api_get_artist_songs", "api_get_trending_songs", "api_get_new_releases", "api_search"], + "model_album": ["api_create_album", "api_get_album", "api_update_album", "api_delete_album", "api_get_artist_albums", "api_search"], + "model_playlist": ["api_create_playlist", "api_get_user_playlists", "api_get_playlist", "api_update_playlist", "api_delete_playlist"], + "model_playlist_song": ["api_add_song_to_playlist", "api_remove_song_from_playlist", "api_reorder_playlist_songs"], + "model_genre": ["api_get_genres", "api_get_songs_by_genre"], + "model_label": ["api_create_label_profile", "api_get_label_artists"] + } + } +} diff --git a/.workflow/versions/v003/snapshot_before/manifest.json b/.workflow/versions/v003/snapshot_before/manifest.json new file mode 100644 index 0000000..8e53336 --- /dev/null +++ b/.workflow/versions/v003/snapshot_before/manifest.json @@ -0,0 +1,659 @@ +{ + "project": { + "name": "sonic-cloud", + "version": "0.1.0", + "created_at": "2025-12-18T14:32:39.275839", + "description": "Music platform for musicians to upload songs" + }, + "state": { + "current_phase": "DESIGN_PHASE", + "approval_status": { + "manifest_approved": false, + "approved_by": null, + "approved_at": null + }, + "revision_history": [ + { + "action": "PROJECT_INITIALIZED", + "timestamp": "2025-12-18T14:32:39.275844", + "details": "Project sonic-cloud created" + }, + { + "action": "DESIGN_DOCUMENT_CREATED", + "timestamp": "2025-12-18T15:10:00", + "details": "Complete design document with 91 entities created" + } + ] + }, + "entities": { + "database_tables": [ + { + "id": "model_user", + "name": "User", + "table_name": "users", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_artist", + "name": "Artist", + "table_name": "artists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_label", + "name": "Label", + "table_name": "labels", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_genre", + "name": "Genre", + "table_name": "genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_album", + "name": "Album", + "table_name": "albums", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song", + "name": "Song", + "table_name": "songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song_genre", + "name": "SongGenre", + "table_name": "song_genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist", + "name": "Playlist", + "table_name": "playlists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist_song", + "name": "PlaylistSong", + "table_name": "playlist_songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + } + ], + "api_endpoints": [ + { + "id": "api_register", + "name": "Register User", + "path": "/api/auth/register", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/register/route.ts" + }, + { + "id": "api_login", + "name": "Login", + "path": "/api/auth/login", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/login/route.ts" + }, + { + "id": "api_forgot_password", + "name": "Forgot Password", + "path": "/api/auth/forgot-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/forgot-password/route.ts" + }, + { + "id": "api_reset_password", + "name": "Reset Password", + "path": "/api/auth/reset-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/reset-password/route.ts" + }, + { + "id": "api_get_current_user", + "name": "Get Current User", + "path": "/api/users/me", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_update_current_user", + "name": "Update Current User", + "path": "/api/users/me", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_create_artist_profile", + "name": "Create Artist Profile", + "path": "/api/artists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/artists/route.ts" + }, + { + "id": "api_get_artist", + "name": "Get Artist", + "path": "/api/artists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_update_artist", + "name": "Update Artist", + "path": "/api/artists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_get_artist_songs", + "name": "Get Artist Songs", + "path": "/api/artists/:id/songs", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/songs/route.ts" + }, + { + "id": "api_get_artist_albums", + "name": "Get Artist Albums", + "path": "/api/artists/:id/albums", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/albums/route.ts" + }, + { + "id": "api_upload_song", + "name": "Upload Song", + "path": "/api/songs/upload", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/upload/route.ts" + }, + { + "id": "api_get_song", + "name": "Get Song", + "path": "/api/songs/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_update_song", + "name": "Update Song", + "path": "/api/songs/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_delete_song", + "name": "Delete Song", + "path": "/api/songs/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_increment_play_count", + "name": "Increment Play Count", + "path": "/api/songs/:id/play", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/[id]/play/route.ts" + }, + { + "id": "api_create_album", + "name": "Create Album", + "path": "/api/albums", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/albums/route.ts" + }, + { + "id": "api_get_album", + "name": "Get Album", + "path": "/api/albums/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_update_album", + "name": "Update Album", + "path": "/api/albums/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_delete_album", + "name": "Delete Album", + "path": "/api/albums/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_create_playlist", + "name": "Create Playlist", + "path": "/api/playlists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_user_playlists", + "name": "Get User Playlists", + "path": "/api/playlists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_playlist", + "name": "Get Playlist", + "path": "/api/playlists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_update_playlist", + "name": "Update Playlist", + "path": "/api/playlists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_delete_playlist", + "name": "Delete Playlist", + "path": "/api/playlists/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_add_song_to_playlist", + "name": "Add Song to Playlist", + "path": "/api/playlists/:id/songs", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/songs/route.ts" + }, + { + "id": "api_remove_song_from_playlist", + "name": "Remove Song from Playlist", + "path": "/api/playlists/:playlistId/songs/:songId", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[playlistId]/songs/[songId]/route.ts" + }, + { + "id": "api_reorder_playlist_songs", + "name": "Reorder Playlist Songs", + "path": "/api/playlists/:id/reorder", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/reorder/route.ts" + }, + { + "id": "api_get_trending_songs", + "name": "Get Trending Songs", + "path": "/api/discover/trending", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/trending/route.ts" + }, + { + "id": "api_get_new_releases", + "name": "Get New Releases", + "path": "/api/discover/new-releases", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/new-releases/route.ts" + }, + { + "id": "api_get_genres", + "name": "Get Genres", + "path": "/api/discover/genres", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/route.ts" + }, + { + "id": "api_get_songs_by_genre", + "name": "Get Songs by Genre", + "path": "/api/discover/genres/:slug", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/[slug]/route.ts" + }, + { + "id": "api_search", + "name": "Search", + "path": "/api/search", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/search/route.ts" + }, + { + "id": "api_create_label_profile", + "name": "Create Label Profile", + "path": "/api/labels", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/labels/route.ts" + }, + { + "id": "api_get_label_artists", + "name": "Get Label Artists", + "path": "/api/labels/:id/artists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/labels/[id]/artists/route.ts" + } + ], + "pages": [ + { + "id": "page_login", + "name": "Login", + "path": "/login", + "status": "PENDING", + "file_path": "app/login/page.tsx" + }, + { + "id": "page_register", + "name": "Register", + "path": "/register", + "status": "PENDING", + "file_path": "app/register/page.tsx" + }, + { + "id": "page_forgot_password", + "name": "Forgot Password", + "path": "/forgot-password", + "status": "PENDING", + "file_path": "app/forgot-password/page.tsx" + }, + { + "id": "page_home", + "name": "Discover Music", + "path": "/", + "status": "PENDING", + "file_path": "app/page.tsx" + }, + { + "id": "page_artist_profile", + "name": "Artist Profile", + "path": "/artist/:id", + "status": "PENDING", + "file_path": "app/artist/[id]/page.tsx" + }, + { + "id": "page_album_detail", + "name": "Album", + "path": "/album/:id", + "status": "PENDING", + "file_path": "app/album/[id]/page.tsx" + }, + { + "id": "page_upload", + "name": "Upload Music", + "path": "/upload", + "status": "PENDING", + "file_path": "app/upload/page.tsx" + }, + { + "id": "page_playlists", + "name": "My Playlists", + "path": "/playlists", + "status": "PENDING", + "file_path": "app/playlists/page.tsx" + }, + { + "id": "page_playlist_detail", + "name": "Playlist", + "path": "/playlist/:id", + "status": "PENDING", + "file_path": "app/playlist/[id]/page.tsx" + }, + { + "id": "page_profile", + "name": "Profile Settings", + "path": "/profile", + "status": "PENDING", + "file_path": "app/profile/page.tsx" + }, + { + "id": "page_search", + "name": "Search", + "path": "/search", + "status": "PENDING", + "file_path": "app/search/page.tsx" + }, + { + "id": "page_genre_browse", + "name": "Browse Genre", + "path": "/genre/:slug", + "status": "PENDING", + "file_path": "app/genre/[slug]/page.tsx" + } + ], + "components": [ + { + "id": "component_audio_player", + "name": "AudioPlayer", + "status": "PENDING", + "file_path": "components/AudioPlayer.tsx" + }, + { + "id": "component_player_controls", + "name": "PlayerControls", + "status": "PENDING", + "file_path": "components/PlayerControls.tsx" + }, + { + "id": "component_song_card", + "name": "SongCard", + "status": "PENDING", + "file_path": "components/SongCard.tsx" + }, + { + "id": "component_album_card", + "name": "AlbumCard", + "status": "PENDING", + "file_path": "components/AlbumCard.tsx" + }, + { + "id": "component_artist_card", + "name": "ArtistCard", + "status": "PENDING", + "file_path": "components/ArtistCard.tsx" + }, + { + "id": "component_playlist_card", + "name": "PlaylistCard", + "status": "PENDING", + "file_path": "components/PlaylistCard.tsx" + }, + { + "id": "component_upload_form", + "name": "UploadForm", + "status": "PENDING", + "file_path": "components/UploadForm.tsx" + }, + { + "id": "component_waveform_display", + "name": "WaveformDisplay", + "status": "PENDING", + "file_path": "components/WaveformDisplay.tsx" + }, + { + "id": "component_genre_badge", + "name": "GenreBadge", + "status": "PENDING", + "file_path": "components/GenreBadge.tsx" + }, + { + "id": "component_track_list", + "name": "TrackList", + "status": "PENDING", + "file_path": "components/TrackList.tsx" + }, + { + "id": "component_artist_header", + "name": "ArtistHeader", + "status": "PENDING", + "file_path": "components/ArtistHeader.tsx" + }, + { + "id": "component_album_header", + "name": "AlbumHeader", + "status": "PENDING", + "file_path": "components/AlbumHeader.tsx" + }, + { + "id": "component_playlist_header", + "name": "PlaylistHeader", + "status": "PENDING", + "file_path": "components/PlaylistHeader.tsx" + }, + { + "id": "component_social_links", + "name": "SocialLinks", + "status": "PENDING", + "file_path": "components/SocialLinks.tsx" + }, + { + "id": "component_auth_form", + "name": "AuthForm", + "status": "PENDING", + "file_path": "components/AuthForm.tsx" + }, + { + "id": "component_search_bar", + "name": "SearchBar", + "status": "PENDING", + "file_path": "components/SearchBar.tsx" + }, + { + "id": "component_search_results", + "name": "SearchResults", + "status": "PENDING", + "file_path": "components/SearchResults.tsx" + }, + { + "id": "component_create_playlist_modal", + "name": "CreatePlaylistModal", + "status": "PENDING", + "file_path": "components/CreatePlaylistModal.tsx" + }, + { + "id": "component_profile_form", + "name": "ProfileForm", + "status": "PENDING", + "file_path": "components/ProfileForm.tsx" + }, + { + "id": "component_avatar_upload", + "name": "AvatarUpload", + "status": "PENDING", + "file_path": "components/AvatarUpload.tsx" + }, + { + "id": "component_section_header", + "name": "SectionHeader", + "status": "PENDING", + "file_path": "components/SectionHeader.tsx" + }, + { + "id": "component_genre_header", + "name": "GenreHeader", + "status": "PENDING", + "file_path": "components/GenreHeader.tsx" + }, + { + "id": "component_header", + "name": "Header", + "status": "IMPLEMENTED", + "file_path": "components/Header.tsx" + }, + { + "id": "component_nav_link", + "name": "NavLink", + "status": "IMPLEMENTED", + "file_path": "components/NavLink.tsx" + }, + { + "id": "component_user_menu", + "name": "UserMenu", + "status": "IMPLEMENTED", + "file_path": "components/UserMenu.tsx" + } + ] + }, + "dependencies": { + "component_to_page": { + "component_auth_form": ["page_login", "page_register", "page_forgot_password"], + "component_song_card": ["page_home", "page_artist_profile", "page_search", "page_genre_browse", "page_album_detail", "page_playlist_detail"], + "component_genre_badge": ["page_home"], + "component_section_header": ["page_home"], + "component_artist_header": ["page_artist_profile"], + "component_album_card": ["page_artist_profile", "page_search"], + "component_social_links": ["page_artist_profile"], + "component_album_header": ["page_album_detail"], + "component_track_list": ["page_album_detail", "page_playlist_detail"], + "component_upload_form": ["page_upload"], + "component_waveform_display": ["page_upload"], + "component_playlist_card": ["page_playlists"], + "component_create_playlist_modal": ["page_playlists"], + "component_playlist_header": ["page_playlist_detail"], + "component_profile_form": ["page_profile"], + "component_avatar_upload": ["page_profile"], + "component_search_bar": ["page_search"], + "component_search_results": ["page_search"], + "component_artist_card": ["page_search"], + "component_genre_header": ["page_genre_browse"] + }, + "api_to_component": { + "api_login": ["component_auth_form"], + "api_register": ["component_auth_form"], + "api_forgot_password": ["component_auth_form"], + "api_upload_song": ["component_upload_form"], + "api_increment_play_count": ["component_audio_player"], + "api_search": ["component_search_bar"], + "api_create_playlist": ["component_create_playlist_modal"], + "api_update_current_user": ["component_profile_form"] + }, + "table_to_api": { + "model_user": ["api_register", "api_login", "api_forgot_password", "api_reset_password", "api_get_current_user", "api_update_current_user"], + "model_artist": ["api_create_artist_profile", "api_get_artist", "api_update_artist", "api_get_artist_songs", "api_get_artist_albums"], + "model_song": ["api_upload_song", "api_get_song", "api_update_song", "api_delete_song", "api_increment_play_count", "api_get_artist_songs", "api_get_trending_songs", "api_get_new_releases", "api_search"], + "model_album": ["api_create_album", "api_get_album", "api_update_album", "api_delete_album", "api_get_artist_albums", "api_search"], + "model_playlist": ["api_create_playlist", "api_get_user_playlists", "api_get_playlist", "api_update_playlist", "api_delete_playlist"], + "model_playlist_song": ["api_add_song_to_playlist", "api_remove_song_from_playlist", "api_reorder_playlist_songs"], + "model_genre": ["api_get_genres", "api_get_songs_by_genre"], + "model_label": ["api_create_label_profile", "api_get_label_artists"] + } + } +} diff --git a/.workflow/versions/v003/tasks/task_create_api_create_label_invitation.yml b/.workflow/versions/v003/tasks/task_create_api_create_label_invitation.yml new file mode 100644 index 0000000..473f4bf --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_api_create_label_invitation.yml @@ -0,0 +1,18 @@ +id: task_create_api_create_label_invitation +type: create +title: Create Create artist invitation +agent: backend +entity_id: api_create_label_invitation +entity_ids: +- api_create_label_invitation +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_label_invitation +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/api_create_label_invitation.yml +created_at: '2025-12-18T17:43:33.739908' diff --git a/.workflow/versions/v003/tasks/task_create_api_delete_label_invitation.yml b/.workflow/versions/v003/tasks/task_create_api_delete_label_invitation.yml new file mode 100644 index 0000000..be4b085 --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_api_delete_label_invitation.yml @@ -0,0 +1,18 @@ +id: task_create_api_delete_label_invitation +type: create +title: Create Cancel invitation +agent: backend +entity_id: api_delete_label_invitation +entity_ids: +- api_delete_label_invitation +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_label_invitation +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/api_delete_label_invitation.yml +created_at: '2025-12-18T17:43:33.740237' diff --git a/.workflow/versions/v003/tasks/task_create_api_get_label.yml b/.workflow/versions/v003/tasks/task_create_api_get_label.yml new file mode 100644 index 0000000..c55a0d9 --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_api_get_label.yml @@ -0,0 +1,18 @@ +id: task_create_api_get_label +type: create +title: Create Get label details +agent: backend +entity_id: api_get_label +entity_ids: +- api_get_label +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_label_invitation +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/api_get_label.yml +created_at: '2025-12-18T17:43:33.740541' diff --git a/.workflow/versions/v003/tasks/task_create_api_get_label_stats.yml b/.workflow/versions/v003/tasks/task_create_api_get_label_stats.yml new file mode 100644 index 0000000..60d8172 --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_api_get_label_stats.yml @@ -0,0 +1,17 @@ +id: task_create_api_get_label_stats +type: create +title: Create Get label statistics +agent: backend +entity_id: api_get_label_stats +entity_ids: +- api_get_label_stats +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/api_get_label_stats.yml +created_at: '2025-12-18T17:43:33.737301' diff --git a/.workflow/versions/v003/tasks/task_create_api_list_artist_invitations.yml b/.workflow/versions/v003/tasks/task_create_api_list_artist_invitations.yml new file mode 100644 index 0000000..32871a2 --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_api_list_artist_invitations.yml @@ -0,0 +1,18 @@ +id: task_create_api_list_artist_invitations +type: create +title: Create List artist invitations +agent: backend +entity_id: api_list_artist_invitations +entity_ids: +- api_list_artist_invitations +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_label_invitation +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/api_list_artist_invitations.yml +created_at: '2025-12-18T17:43:33.740836' diff --git a/.workflow/versions/v003/tasks/task_create_api_list_label_invitations.yml b/.workflow/versions/v003/tasks/task_create_api_list_label_invitations.yml new file mode 100644 index 0000000..00d90bc --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_api_list_label_invitations.yml @@ -0,0 +1,18 @@ +id: task_create_api_list_label_invitations +type: create +title: Create List label invitations +agent: backend +entity_id: api_list_label_invitations +entity_ids: +- api_list_label_invitations +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_label_invitation +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/api_list_label_invitations.yml +created_at: '2025-12-18T17:43:33.741147' diff --git a/.workflow/versions/v003/tasks/task_create_api_remove_artist_from_label.yml b/.workflow/versions/v003/tasks/task_create_api_remove_artist_from_label.yml new file mode 100644 index 0000000..b476613 --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_api_remove_artist_from_label.yml @@ -0,0 +1,17 @@ +id: task_create_api_remove_artist_from_label +type: create +title: Create Remove artist from label +agent: backend +entity_id: api_remove_artist_from_label +entity_ids: +- api_remove_artist_from_label +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/api_remove_artist_from_label.yml +created_at: '2025-12-18T17:43:33.737591' diff --git a/.workflow/versions/v003/tasks/task_create_api_respond_to_invitation.yml b/.workflow/versions/v003/tasks/task_create_api_respond_to_invitation.yml new file mode 100644 index 0000000..3f736d1 --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_api_respond_to_invitation.yml @@ -0,0 +1,18 @@ +id: task_create_api_respond_to_invitation +type: create +title: Create Respond to invitation +agent: backend +entity_id: api_respond_to_invitation +entity_ids: +- api_respond_to_invitation +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_label_invitation +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/api_respond_to_invitation.yml +created_at: '2025-12-18T17:43:33.741450' diff --git a/.workflow/versions/v003/tasks/task_create_api_update_label.yml b/.workflow/versions/v003/tasks/task_create_api_update_label.yml new file mode 100644 index 0000000..e8d961e --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_api_update_label.yml @@ -0,0 +1,17 @@ +id: task_create_api_update_label +type: create +title: Create Update label profile +agent: backend +entity_id: api_update_label +entity_ids: +- api_update_label +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/api_update_label.yml +created_at: '2025-12-18T17:43:33.737890' diff --git a/.workflow/versions/v003/tasks/task_create_component_artist_roster.yml b/.workflow/versions/v003/tasks/task_create_component_artist_roster.yml new file mode 100644 index 0000000..ba968df --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_component_artist_roster.yml @@ -0,0 +1,17 @@ +id: task_create_component_artist_roster +type: create +title: Create ArtistRoster +agent: frontend +entity_id: component_artist_roster +entity_ids: +- component_artist_roster +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/component_artist_roster.yml +created_at: '2025-12-18T17:43:33.738178' diff --git a/.workflow/versions/v003/tasks/task_create_component_invitation_card.yml b/.workflow/versions/v003/tasks/task_create_component_invitation_card.yml new file mode 100644 index 0000000..70626aa --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_component_invitation_card.yml @@ -0,0 +1,17 @@ +id: task_create_component_invitation_card +type: create +title: Create InvitationCard +agent: frontend +entity_id: component_invitation_card +entity_ids: +- component_invitation_card +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/component_invitation_card.yml +created_at: '2025-12-18T17:43:33.738472' diff --git a/.workflow/versions/v003/tasks/task_create_component_invite_artist_modal.yml b/.workflow/versions/v003/tasks/task_create_component_invite_artist_modal.yml new file mode 100644 index 0000000..e329b27 --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_component_invite_artist_modal.yml @@ -0,0 +1,18 @@ +id: task_create_component_invite_artist_modal +type: create +title: Create InviteArtistModal +agent: frontend +entity_id: component_invite_artist_modal +entity_ids: +- component_invite_artist_modal +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_api_create_label_invitation +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/component_invite_artist_modal.yml +created_at: '2025-12-18T17:43:33.742052' diff --git a/.workflow/versions/v003/tasks/task_create_component_label_card.yml b/.workflow/versions/v003/tasks/task_create_component_label_card.yml new file mode 100644 index 0000000..6d5e356 --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_component_label_card.yml @@ -0,0 +1,17 @@ +id: task_create_component_label_card +type: create +title: Create LabelCard +agent: frontend +entity_id: component_label_card +entity_ids: +- component_label_card +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/component_label_card.yml +created_at: '2025-12-18T17:43:33.738766' diff --git a/.workflow/versions/v003/tasks/task_create_component_label_header.yml b/.workflow/versions/v003/tasks/task_create_component_label_header.yml new file mode 100644 index 0000000..dbaa44b --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_component_label_header.yml @@ -0,0 +1,17 @@ +id: task_create_component_label_header +type: create +title: Create LabelHeader +agent: frontend +entity_id: component_label_header +entity_ids: +- component_label_header +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/component_label_header.yml +created_at: '2025-12-18T17:43:33.739050' diff --git a/.workflow/versions/v003/tasks/task_create_component_label_profile_form.yml b/.workflow/versions/v003/tasks/task_create_component_label_profile_form.yml new file mode 100644 index 0000000..7c8747c --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_component_label_profile_form.yml @@ -0,0 +1,18 @@ +id: task_create_component_label_profile_form +type: create +title: Create LabelProfileForm +agent: frontend +entity_id: component_label_profile_form +entity_ids: +- component_label_profile_form +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_api_update_label +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/component_label_profile_form.yml +created_at: '2025-12-18T17:43:33.741750' diff --git a/.workflow/versions/v003/tasks/task_create_component_label_stats.yml b/.workflow/versions/v003/tasks/task_create_component_label_stats.yml new file mode 100644 index 0000000..0a9d01b --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_component_label_stats.yml @@ -0,0 +1,17 @@ +id: task_create_component_label_stats +type: create +title: Create LabelStats +agent: frontend +entity_id: component_label_stats +entity_ids: +- component_label_stats +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/component_label_stats.yml +created_at: '2025-12-18T17:43:33.739337' diff --git a/.workflow/versions/v003/tasks/task_create_model_label_invitation.yml b/.workflow/versions/v003/tasks/task_create_model_label_invitation.yml new file mode 100644 index 0000000..e185206 --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_model_label_invitation.yml @@ -0,0 +1,17 @@ +id: task_create_model_label_invitation +type: create +title: Create LabelInvitation +agent: backend +entity_id: model_label_invitation +entity_ids: +- model_label_invitation +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/model_label_invitation.yml +created_at: '2025-12-18T17:43:33.739621' diff --git a/.workflow/versions/v003/tasks/task_create_page_label_dashboard.yml b/.workflow/versions/v003/tasks/task_create_page_label_dashboard.yml new file mode 100644 index 0000000..63dfa8f --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_page_label_dashboard.yml @@ -0,0 +1,24 @@ +id: task_create_page_label_dashboard +type: create +title: Create Label Dashboard +agent: frontend +entity_id: page_label_dashboard +entity_ids: +- page_label_dashboard +status: pending +layer: 4 +parallel_group: layer_4 +complexity: medium +dependencies: +- task_create_api_get_label +- task_create_component_invitation_card +- task_create_component_label_stats +- task_create_api_list_label_invitations +- task_create_component_invite_artist_modal +- task_create_api_get_label_stats +- task_create_component_artist_roster +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/page_label_dashboard.yml +created_at: '2025-12-18T17:43:33.743001' diff --git a/.workflow/versions/v003/tasks/task_create_page_label_invitations.yml b/.workflow/versions/v003/tasks/task_create_page_label_invitations.yml new file mode 100644 index 0000000..68ed704 --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_page_label_invitations.yml @@ -0,0 +1,20 @@ +id: task_create_page_label_invitations +type: create +title: Create Label Invitations +agent: frontend +entity_id: page_label_invitations +entity_ids: +- page_label_invitations +status: pending +layer: 4 +parallel_group: layer_4 +complexity: medium +dependencies: +- task_create_component_invitation_card +- task_create_api_list_label_invitations +- task_create_component_invite_artist_modal +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/page_label_invitations.yml +created_at: '2025-12-18T17:43:33.743359' diff --git a/.workflow/versions/v003/tasks/task_create_page_label_profile.yml b/.workflow/versions/v003/tasks/task_create_page_label_profile.yml new file mode 100644 index 0000000..4d8b2be --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_page_label_profile.yml @@ -0,0 +1,22 @@ +id: task_create_page_label_profile +type: create +title: Create Label Profile +agent: frontend +entity_id: page_label_profile +entity_ids: +- page_label_profile +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_api_get_label +- task_create_component_label_stats +- task_create_component_label_header +- task_create_api_get_label_stats +- task_create_component_artist_roster +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/page_label_profile.yml +created_at: '2025-12-18T17:43:33.742358' diff --git a/.workflow/versions/v003/tasks/task_create_page_label_settings.yml b/.workflow/versions/v003/tasks/task_create_page_label_settings.yml new file mode 100644 index 0000000..fb87cce --- /dev/null +++ b/.workflow/versions/v003/tasks/task_create_page_label_settings.yml @@ -0,0 +1,19 @@ +id: task_create_page_label_settings +type: create +title: Create Label Settings +agent: frontend +entity_id: page_label_settings +entity_ids: +- page_label_settings +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_component_label_profile_form +- task_create_api_get_label +context: + design_version: 2 + workflow_version: v003 + context_snapshot_path: .workflow/versions/v001/contexts/page_label_settings.yml +created_at: '2025-12-18T17:43:33.742694' diff --git a/.workflow/versions/v004/contexts/api_create_album_share.yml b/.workflow/versions/v004/contexts/api_create_album_share.yml new file mode 100644 index 0000000..5d7393e --- /dev/null +++ b/.workflow/versions/v004/contexts/api_create_album_share.yml @@ -0,0 +1,130 @@ +task_id: task_create_api_create_album_share +entity_id: api_create_album_share +generated_at: '2025-12-18T18:15:12.906875' +workflow_version: v004 +target: + type: api + definition: + id: api_create_album_share + method: POST + path: /api/share/album/[id] + description: Generate share link for an album + auth: + required: false + request_params: + - name: id + type: string + location: path + required: true + description: Album ID to generate share link for + request_body: + - name: platform + type: string + required: false + description: Platform being shared to + responses: + - status: 200 + description: Share link created successfully + schema: + shareUrl: string + token: string + type: string + - status: 404 + description: Album not found + schema: + error: string + business_logic: + - Verify album exists + - Generate unique token + - Create Share record with type=ALBUM + - Return full share URL + depends_on_models: + - model_share +related: + models: + - id: model_share + definition: &id001 + id: model_share + name: Share + table_name: shares + description: Tracks shared content links with analytics + primary_key: id + fields: + - name: id + type: String + constraints: + - primary_key + - cuid + description: Unique identifier for the share + - name: type + type: ShareType + constraints: + - required + description: Type of content being shared (SONG, PLAYLIST, ALBUM) + - name: targetId + type: String + constraints: + - required + description: ID of the shared content (references songs/playlists/albums) + - name: token + type: String + constraints: + - required + - unique + description: Unique URL-safe token for share links + - name: userId + type: String + constraints: + - optional + description: User who created the share (null for anonymous shares) + - name: platform + type: String + constraints: + - optional + description: Platform where content was shared to (twitter, facebook, etc) + - name: clickCount + type: Int + default: 0 + description: Number of times the share link was clicked + - name: createdAt + type: DateTime + default: now() + description: Timestamp when the share was created + relations: [] + indexes: + - fields: + - token + unique: true + description: Fast lookup by share token + - fields: + - targetId + - type + description: Fast lookup of shares for specific content + timestamps: false + validations: + - field: token + rule: minLength(8) + message: Token must be at least 8 characters + - field: type + rule: enum(SONG, PLAYLIST, ALBUM) + message: Type must be SONG, PLAYLIST, or ALBUM + apis: [] + components: [] +dependencies: + entity_ids: + - model_share + definitions: + - id: model_share + type: model + definition: *id001 +files: + to_create: + - app/api/share/album/[id]/route.ts + reference: [] +acceptance: +- criterion: POST /api/share/album/[id] returns success response + verification: curl -X POST /api/share/album/[id] +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v004/contexts/api_create_playlist_share.yml b/.workflow/versions/v004/contexts/api_create_playlist_share.yml new file mode 100644 index 0000000..4e2d3a3 --- /dev/null +++ b/.workflow/versions/v004/contexts/api_create_playlist_share.yml @@ -0,0 +1,130 @@ +task_id: task_create_api_create_playlist_share +entity_id: api_create_playlist_share +generated_at: '2025-12-18T18:15:12.905214' +workflow_version: v004 +target: + type: api + definition: + id: api_create_playlist_share + method: POST + path: /api/share/playlist/[id] + description: Generate share link for a playlist + auth: + required: false + request_params: + - name: id + type: string + location: path + required: true + description: Playlist ID to generate share link for + request_body: + - name: platform + type: string + required: false + description: Platform being shared to + responses: + - status: 200 + description: Share link created successfully + schema: + shareUrl: string + token: string + type: string + - status: 404 + description: Playlist not found + schema: + error: string + business_logic: + - Verify playlist exists and is public + - Generate unique token + - Create Share record with type=PLAYLIST + - Return full share URL + depends_on_models: + - model_share +related: + models: + - id: model_share + definition: &id001 + id: model_share + name: Share + table_name: shares + description: Tracks shared content links with analytics + primary_key: id + fields: + - name: id + type: String + constraints: + - primary_key + - cuid + description: Unique identifier for the share + - name: type + type: ShareType + constraints: + - required + description: Type of content being shared (SONG, PLAYLIST, ALBUM) + - name: targetId + type: String + constraints: + - required + description: ID of the shared content (references songs/playlists/albums) + - name: token + type: String + constraints: + - required + - unique + description: Unique URL-safe token for share links + - name: userId + type: String + constraints: + - optional + description: User who created the share (null for anonymous shares) + - name: platform + type: String + constraints: + - optional + description: Platform where content was shared to (twitter, facebook, etc) + - name: clickCount + type: Int + default: 0 + description: Number of times the share link was clicked + - name: createdAt + type: DateTime + default: now() + description: Timestamp when the share was created + relations: [] + indexes: + - fields: + - token + unique: true + description: Fast lookup by share token + - fields: + - targetId + - type + description: Fast lookup of shares for specific content + timestamps: false + validations: + - field: token + rule: minLength(8) + message: Token must be at least 8 characters + - field: type + rule: enum(SONG, PLAYLIST, ALBUM) + message: Type must be SONG, PLAYLIST, or ALBUM + apis: [] + components: [] +dependencies: + entity_ids: + - model_share + definitions: + - id: model_share + type: model + definition: *id001 +files: + to_create: + - app/api/share/playlist/[id]/route.ts + reference: [] +acceptance: +- criterion: POST /api/share/playlist/[id] returns success response + verification: curl -X POST /api/share/playlist/[id] +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v004/contexts/api_create_song_share.yml b/.workflow/versions/v004/contexts/api_create_song_share.yml new file mode 100644 index 0000000..4f68570 --- /dev/null +++ b/.workflow/versions/v004/contexts/api_create_song_share.yml @@ -0,0 +1,131 @@ +task_id: task_create_api_create_song_share +entity_id: api_create_song_share +generated_at: '2025-12-18T18:15:12.903532' +workflow_version: v004 +target: + type: api + definition: + id: api_create_song_share + method: POST + path: /api/share/song/[id] + description: Generate share link for a song + auth: + required: false + description: No authentication required to share content + request_params: + - name: id + type: string + location: path + required: true + description: Song ID to generate share link for + request_body: + - name: platform + type: string + required: false + description: Platform being shared to (twitter, facebook, etc) + responses: + - status: 200 + description: Share link created successfully + schema: + shareUrl: string + token: string + type: string + - status: 404 + description: Song not found + schema: + error: string + business_logic: + - Verify song exists and is public + - Generate unique 10-character alphanumeric token + - Create Share record with type=SONG + - Return full share URL (https://domain.com/s/[token]) + depends_on_models: + - model_share +related: + models: + - id: model_share + definition: &id001 + id: model_share + name: Share + table_name: shares + description: Tracks shared content links with analytics + primary_key: id + fields: + - name: id + type: String + constraints: + - primary_key + - cuid + description: Unique identifier for the share + - name: type + type: ShareType + constraints: + - required + description: Type of content being shared (SONG, PLAYLIST, ALBUM) + - name: targetId + type: String + constraints: + - required + description: ID of the shared content (references songs/playlists/albums) + - name: token + type: String + constraints: + - required + - unique + description: Unique URL-safe token for share links + - name: userId + type: String + constraints: + - optional + description: User who created the share (null for anonymous shares) + - name: platform + type: String + constraints: + - optional + description: Platform where content was shared to (twitter, facebook, etc) + - name: clickCount + type: Int + default: 0 + description: Number of times the share link was clicked + - name: createdAt + type: DateTime + default: now() + description: Timestamp when the share was created + relations: [] + indexes: + - fields: + - token + unique: true + description: Fast lookup by share token + - fields: + - targetId + - type + description: Fast lookup of shares for specific content + timestamps: false + validations: + - field: token + rule: minLength(8) + message: Token must be at least 8 characters + - field: type + rule: enum(SONG, PLAYLIST, ALBUM) + message: Type must be SONG, PLAYLIST, or ALBUM + apis: [] + components: [] +dependencies: + entity_ids: + - model_share + definitions: + - id: model_share + type: model + definition: *id001 +files: + to_create: + - app/api/share/song/[id]/route.ts + reference: [] +acceptance: +- criterion: POST /api/share/song/[id] returns success response + verification: curl -X POST /api/share/song/[id] +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v004/contexts/api_resolve_share.yml b/.workflow/versions/v004/contexts/api_resolve_share.yml new file mode 100644 index 0000000..24f0ee7 --- /dev/null +++ b/.workflow/versions/v004/contexts/api_resolve_share.yml @@ -0,0 +1,128 @@ +task_id: task_create_api_resolve_share +entity_id: api_resolve_share +generated_at: '2025-12-18T18:15:12.908509' +workflow_version: v004 +target: + type: api + definition: + id: api_resolve_share + method: GET + path: /api/share/[token] + description: Resolve share token and get content details + auth: + required: false + request_params: + - name: token + type: string + location: path + required: true + description: Share token to resolve + responses: + - status: 200 + description: Share resolved successfully + schema: + type: string + targetId: string + content: object + shareUrl: string + - status: 404 + description: Share not found or content no longer available + schema: + error: string + business_logic: + - Lookup Share record by token + - Fetch associated content based on type (Song/Playlist/Album) + - Include artist information for songs/albums + - Include song list for playlists + - Verify content is still public and available + - Return content details for display + depends_on_models: + - model_share +related: + models: + - id: model_share + definition: &id001 + id: model_share + name: Share + table_name: shares + description: Tracks shared content links with analytics + primary_key: id + fields: + - name: id + type: String + constraints: + - primary_key + - cuid + description: Unique identifier for the share + - name: type + type: ShareType + constraints: + - required + description: Type of content being shared (SONG, PLAYLIST, ALBUM) + - name: targetId + type: String + constraints: + - required + description: ID of the shared content (references songs/playlists/albums) + - name: token + type: String + constraints: + - required + - unique + description: Unique URL-safe token for share links + - name: userId + type: String + constraints: + - optional + description: User who created the share (null for anonymous shares) + - name: platform + type: String + constraints: + - optional + description: Platform where content was shared to (twitter, facebook, etc) + - name: clickCount + type: Int + default: 0 + description: Number of times the share link was clicked + - name: createdAt + type: DateTime + default: now() + description: Timestamp when the share was created + relations: [] + indexes: + - fields: + - token + unique: true + description: Fast lookup by share token + - fields: + - targetId + - type + description: Fast lookup of shares for specific content + timestamps: false + validations: + - field: token + rule: minLength(8) + message: Token must be at least 8 characters + - field: type + rule: enum(SONG, PLAYLIST, ALBUM) + message: Type must be SONG, PLAYLIST, or ALBUM + apis: [] + components: [] +dependencies: + entity_ids: + - model_share + definitions: + - id: model_share + type: model + definition: *id001 +files: + to_create: + - app/api/share/[token]/route.ts + reference: [] +acceptance: +- criterion: GET /api/share/[token] returns success response + verification: curl -X GET /api/share/[token] +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v004/contexts/api_track_share_click.yml b/.workflow/versions/v004/contexts/api_track_share_click.yml new file mode 100644 index 0000000..c694ebf --- /dev/null +++ b/.workflow/versions/v004/contexts/api_track_share_click.yml @@ -0,0 +1,124 @@ +task_id: task_create_api_track_share_click +entity_id: api_track_share_click +generated_at: '2025-12-18T18:15:12.910153' +workflow_version: v004 +target: + type: api + definition: + id: api_track_share_click + method: POST + path: /api/share/[token]/click + description: Increment share click count for analytics + auth: + required: false + request_params: + - name: token + type: string + location: path + required: true + description: Share token to track click for + request_body: [] + responses: + - status: 200 + description: Click tracked successfully + schema: + success: boolean + clickCount: integer + - status: 404 + description: Share not found + schema: + error: string + business_logic: + - Find Share record by token + - Increment clickCount by 1 + - Return new click count + depends_on_models: + - model_share +related: + models: + - id: model_share + definition: &id001 + id: model_share + name: Share + table_name: shares + description: Tracks shared content links with analytics + primary_key: id + fields: + - name: id + type: String + constraints: + - primary_key + - cuid + description: Unique identifier for the share + - name: type + type: ShareType + constraints: + - required + description: Type of content being shared (SONG, PLAYLIST, ALBUM) + - name: targetId + type: String + constraints: + - required + description: ID of the shared content (references songs/playlists/albums) + - name: token + type: String + constraints: + - required + - unique + description: Unique URL-safe token for share links + - name: userId + type: String + constraints: + - optional + description: User who created the share (null for anonymous shares) + - name: platform + type: String + constraints: + - optional + description: Platform where content was shared to (twitter, facebook, etc) + - name: clickCount + type: Int + default: 0 + description: Number of times the share link was clicked + - name: createdAt + type: DateTime + default: now() + description: Timestamp when the share was created + relations: [] + indexes: + - fields: + - token + unique: true + description: Fast lookup by share token + - fields: + - targetId + - type + description: Fast lookup of shares for specific content + timestamps: false + validations: + - field: token + rule: minLength(8) + message: Token must be at least 8 characters + - field: type + rule: enum(SONG, PLAYLIST, ALBUM) + message: Type must be SONG, PLAYLIST, or ALBUM + apis: [] + components: [] +dependencies: + entity_ids: + - model_share + definitions: + - id: model_share + type: model + definition: *id001 +files: + to_create: + - app/api/share/[token]/click/route.ts + reference: [] +acceptance: +- criterion: POST /api/share/[token]/click returns success response + verification: curl -X POST /api/share/[token]/click +- criterion: Request validation implemented + verification: Test with invalid data +- criterion: Error responses match contract + verification: Test error scenarios diff --git a/.workflow/versions/v004/contexts/component_share_button.yml b/.workflow/versions/v004/contexts/component_share_button.yml new file mode 100644 index 0000000..0b67fc9 --- /dev/null +++ b/.workflow/versions/v004/contexts/component_share_button.yml @@ -0,0 +1,242 @@ +task_id: task_create_component_share_button +entity_id: component_share_button +generated_at: '2025-12-18T18:15:12.913944' +workflow_version: v004 +target: + type: component + definition: + id: component_share_button + name: ShareButton + description: Button component to trigger share modal + file_path: components/ShareButton.tsx + props: + - name: type + type: '''song'' | ''playlist'' | ''album''' + required: true + description: Type of content to share + - name: targetId + type: string + required: true + description: ID of the content to share + - name: title + type: string + required: true + description: Title to display in share modal + - name: className + type: string + required: false + description: Additional CSS classes + state: + - name: isModalOpen + type: boolean + description: Controls share modal visibility + - name: shareUrl + type: string | null + description: Generated share URL after creation + - name: isLoading + type: boolean + description: Share link generation in progress + events: + - name: onShare + payload: '{ shareUrl: string, token: string }' + description: Fired when share link is successfully generated + uses_components: + - component_share_modal + uses_apis: + - api_create_song_share + - api_create_playlist_share + - api_create_album_share + styling: + - Accessible button with share icon + - Loading state during share generation + - Error state if share creation fails +related: + models: [] + apis: + - id: api_create_playlist_share + definition: &id001 + id: api_create_playlist_share + method: POST + path: /api/share/playlist/[id] + description: Generate share link for a playlist + auth: + required: false + request_params: + - name: id + type: string + location: path + required: true + description: Playlist ID to generate share link for + request_body: + - name: platform + type: string + required: false + description: Platform being shared to + responses: + - status: 200 + description: Share link created successfully + schema: + shareUrl: string + token: string + type: string + - status: 404 + description: Playlist not found + schema: + error: string + business_logic: + - Verify playlist exists and is public + - Generate unique token + - Create Share record with type=PLAYLIST + - Return full share URL + depends_on_models: + - model_share + - id: api_create_album_share + definition: &id003 + id: api_create_album_share + method: POST + path: /api/share/album/[id] + description: Generate share link for an album + auth: + required: false + request_params: + - name: id + type: string + location: path + required: true + description: Album ID to generate share link for + request_body: + - name: platform + type: string + required: false + description: Platform being shared to + responses: + - status: 200 + description: Share link created successfully + schema: + shareUrl: string + token: string + type: string + - status: 404 + description: Album not found + schema: + error: string + business_logic: + - Verify album exists + - Generate unique token + - Create Share record with type=ALBUM + - Return full share URL + depends_on_models: + - model_share + - id: api_create_song_share + definition: &id004 + id: api_create_song_share + method: POST + path: /api/share/song/[id] + description: Generate share link for a song + auth: + required: false + description: No authentication required to share content + request_params: + - name: id + type: string + location: path + required: true + description: Song ID to generate share link for + request_body: + - name: platform + type: string + required: false + description: Platform being shared to (twitter, facebook, etc) + responses: + - status: 200 + description: Share link created successfully + schema: + shareUrl: string + token: string + type: string + - status: 404 + description: Song not found + schema: + error: string + business_logic: + - Verify song exists and is public + - Generate unique 10-character alphanumeric token + - Create Share record with type=SONG + - Return full share URL (https://domain.com/s/[token]) + depends_on_models: + - model_share + components: + - id: component_share_modal + definition: &id002 + id: component_share_modal + name: ShareModal + description: Modal displaying share options and social buttons + file_path: components/ShareModal.tsx + props: + - name: isOpen + type: boolean + required: true + description: Controls modal visibility + - name: onClose + type: () => void + required: true + description: Callback to close modal + - name: shareUrl + type: string + required: true + description: The share URL to display and copy + - name: title + type: string + required: true + description: Title of content being shared + - name: type + type: '''song'' | ''playlist'' | ''album''' + required: true + description: Type of content for display context + state: + - name: copied + type: boolean + description: Shows copied confirmation after clipboard copy + events: + - name: onCopy + payload: null + description: Fired when user copies link to clipboard + uses_components: + - component_social_share_buttons + uses_apis: [] + styling: + - Centered modal with backdrop + - Copy to clipboard button with icon + - Success message after copy + - Close button + - Responsive design for mobile +dependencies: + entity_ids: + - api_create_playlist_share + - component_share_modal + - api_create_album_share + - api_create_song_share + definitions: + - id: api_create_playlist_share + type: api + definition: *id001 + - id: component_share_modal + type: component + definition: *id002 + - id: api_create_album_share + type: api + definition: *id003 + - id: api_create_song_share + type: api + definition: *id004 +files: + to_create: + - app/components/ShareButton.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v004/contexts/component_share_content_display.yml b/.workflow/versions/v004/contexts/component_share_content_display.yml new file mode 100644 index 0000000..8395743 --- /dev/null +++ b/.workflow/versions/v004/contexts/component_share_content_display.yml @@ -0,0 +1,92 @@ +task_id: task_create_component_share_content_display +entity_id: component_share_content_display +generated_at: '2025-12-18T18:15:12.919309' +workflow_version: v004 +target: + type: component + definition: + id: component_share_content_display + name: SharedContentDisplay + description: Displays shared song/playlist/album with call-to-action + file_path: components/SharedContentDisplay.tsx + props: + - name: type + type: '''song'' | ''playlist'' | ''album''' + required: true + description: Type of shared content + - name: content + type: object + required: true + description: Content data (song/playlist/album with artist info) + state: + - name: isPlaying + type: boolean + description: Audio playback state (for songs) + uses_components: [] + uses_apis: + - api_track_share_click + business_logic: + - Display cover art, title, artist name + - For songs: show waveform, duration, play button + - For playlists: show track count, curator + - For albums: show release date, track count + - 'Call-to-action button: "Listen on Sonic Cloud" or "Sign up to create playlists"' + styling: + - Large cover art + - Prominent title and artist + - Clean, minimal design focused on content + - Branded CTA button + - Preview player for songs (if implemented) +related: + models: [] + apis: + - id: api_track_share_click + definition: &id001 + id: api_track_share_click + method: POST + path: /api/share/[token]/click + description: Increment share click count for analytics + auth: + required: false + request_params: + - name: token + type: string + location: path + required: true + description: Share token to track click for + request_body: [] + responses: + - status: 200 + description: Click tracked successfully + schema: + success: boolean + clickCount: integer + - status: 404 + description: Share not found + schema: + error: string + business_logic: + - Find Share record by token + - Increment clickCount by 1 + - Return new click count + depends_on_models: + - model_share + components: [] +dependencies: + entity_ids: + - api_track_share_click + definitions: + - id: api_track_share_click + type: api + definition: *id001 +files: + to_create: + - app/components/SharedContentDisplay.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v004/contexts/component_share_modal.yml b/.workflow/versions/v004/contexts/component_share_modal.yml new file mode 100644 index 0000000..617037e --- /dev/null +++ b/.workflow/versions/v004/contexts/component_share_modal.yml @@ -0,0 +1,106 @@ +task_id: task_create_component_share_modal +entity_id: component_share_modal +generated_at: '2025-12-18T18:15:12.917022' +workflow_version: v004 +target: + type: component + definition: + id: component_share_modal + name: ShareModal + description: Modal displaying share options and social buttons + file_path: components/ShareModal.tsx + props: + - name: isOpen + type: boolean + required: true + description: Controls modal visibility + - name: onClose + type: () => void + required: true + description: Callback to close modal + - name: shareUrl + type: string + required: true + description: The share URL to display and copy + - name: title + type: string + required: true + description: Title of content being shared + - name: type + type: '''song'' | ''playlist'' | ''album''' + required: true + description: Type of content for display context + state: + - name: copied + type: boolean + description: Shows copied confirmation after clipboard copy + events: + - name: onCopy + payload: null + description: Fired when user copies link to clipboard + uses_components: + - component_social_share_buttons + uses_apis: [] + styling: + - Centered modal with backdrop + - Copy to clipboard button with icon + - Success message after copy + - Close button + - Responsive design for mobile +related: + models: [] + apis: [] + components: + - id: component_social_share_buttons + definition: &id001 + id: component_social_share_buttons + name: SocialShareButtons + description: Social media share buttons for Twitter and Facebook + file_path: components/SocialShareButtons.tsx + props: + - name: url + type: string + required: true + description: URL to share + - name: title + type: string + required: true + description: Title/text to include in share + - name: type + type: '''song'' | ''playlist'' | ''album''' + required: false + description: Type of content for customized share text + events: + - name: onPlatformShare + payload: '{ platform: string }' + description: Fired when user clicks a social share button + uses_components: [] + uses_apis: [] + business_logic: + - Twitter button opens Twitter intent URL with pre-filled text + - Facebook button opens Facebook sharer dialog + - Generate share text based on content type + - URL encode parameters properly + styling: + - Buttons with platform brand colors + - Platform icons (Twitter bird, Facebook f) + - Hover and focus states + - Responsive layout (stack on mobile) +dependencies: + entity_ids: + - component_social_share_buttons + definitions: + - id: component_social_share_buttons + type: component + definition: *id001 +files: + to_create: + - app/components/ShareModal.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v004/contexts/component_social_share_buttons.yml b/.workflow/versions/v004/contexts/component_social_share_buttons.yml new file mode 100644 index 0000000..3b8cc24 --- /dev/null +++ b/.workflow/versions/v004/contexts/component_social_share_buttons.yml @@ -0,0 +1,58 @@ +task_id: task_create_component_social_share_buttons +entity_id: component_social_share_buttons +generated_at: '2025-12-18T18:15:12.918473' +workflow_version: v004 +target: + type: component + definition: + id: component_social_share_buttons + name: SocialShareButtons + description: Social media share buttons for Twitter and Facebook + file_path: components/SocialShareButtons.tsx + props: + - name: url + type: string + required: true + description: URL to share + - name: title + type: string + required: true + description: Title/text to include in share + - name: type + type: '''song'' | ''playlist'' | ''album''' + required: false + description: Type of content for customized share text + events: + - name: onPlatformShare + payload: '{ platform: string }' + description: Fired when user clicks a social share button + uses_components: [] + uses_apis: [] + business_logic: + - Twitter button opens Twitter intent URL with pre-filled text + - Facebook button opens Facebook sharer dialog + - Generate share text based on content type + - URL encode parameters properly + styling: + - Buttons with platform brand colors + - Platform icons (Twitter bird, Facebook f) + - Hover and focus states + - Responsive layout (stack on mobile) +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - app/components/SocialShareButtons.tsx + reference: [] +acceptance: +- criterion: Component renders without errors + verification: Import and render in test +- criterion: Props are typed correctly + verification: TypeScript compilation +- criterion: Events fire correctly + verification: Test event handlers diff --git a/.workflow/versions/v004/contexts/model_share.yml b/.workflow/versions/v004/contexts/model_share.yml new file mode 100644 index 0000000..e402512 --- /dev/null +++ b/.workflow/versions/v004/contexts/model_share.yml @@ -0,0 +1,90 @@ +task_id: task_create_model_share +entity_id: model_share +generated_at: '2025-12-18T18:15:12.902338' +workflow_version: v004 +target: + type: model + definition: + id: model_share + name: Share + table_name: shares + description: Tracks shared content links with analytics + primary_key: id + fields: + - name: id + type: String + constraints: + - primary_key + - cuid + description: Unique identifier for the share + - name: type + type: ShareType + constraints: + - required + description: Type of content being shared (SONG, PLAYLIST, ALBUM) + - name: targetId + type: String + constraints: + - required + description: ID of the shared content (references songs/playlists/albums) + - name: token + type: String + constraints: + - required + - unique + description: Unique URL-safe token for share links + - name: userId + type: String + constraints: + - optional + description: User who created the share (null for anonymous shares) + - name: platform + type: String + constraints: + - optional + description: Platform where content was shared to (twitter, facebook, etc) + - name: clickCount + type: Int + default: 0 + description: Number of times the share link was clicked + - name: createdAt + type: DateTime + default: now() + description: Timestamp when the share was created + relations: [] + indexes: + - fields: + - token + unique: true + description: Fast lookup by share token + - fields: + - targetId + - type + description: Fast lookup of shares for specific content + timestamps: false + validations: + - field: token + rule: minLength(8) + message: Token must be at least 8 characters + - field: type + rule: enum(SONG, PLAYLIST, ALBUM) + message: Type must be SONG, PLAYLIST, or ALBUM +related: + models: [] + apis: [] + components: [] +dependencies: + entity_ids: [] + definitions: [] +files: + to_create: + - prisma/schema.prisma + - app/models/share.ts + reference: [] +acceptance: +- criterion: Model defined in Prisma schema + verification: Check prisma/schema.prisma +- criterion: TypeScript types exported + verification: Import type in test file +- criterion: Relations properly configured + verification: Check Prisma relations diff --git a/.workflow/versions/v004/contexts/page_share.yml b/.workflow/versions/v004/contexts/page_share.yml new file mode 100644 index 0000000..9dddfda --- /dev/null +++ b/.workflow/versions/v004/contexts/page_share.yml @@ -0,0 +1,170 @@ +task_id: task_create_page_share +entity_id: page_share +generated_at: '2025-12-18T18:15:12.911716' +workflow_version: v004 +target: + type: page + definition: + id: page_share + path: /s/[token] + name: SharePage + description: Public landing page for shared music content + layout: minimal + auth: + required: false + description: Publicly accessible share page + data_needs: + - api_id: api_resolve_share + purpose: Load shared content details + on_load: true + description: Fetch content details when page loads + - api_id: api_track_share_click + purpose: Track analytics + on_load: true + description: Track that user clicked on share link + components: + - component_share_content_display + seo: + dynamic_title: true + description: Dynamic based on shared content + og_image: true + twitter_card: true + ui_states: + - state: loading + description: Fetching shared content + - state: content_loaded + description: Display shared content with CTA + - state: not_found + description: Share token invalid or content no longer available + - state: private_content + description: Content is now private +related: + models: [] + apis: + - id: api_track_share_click + definition: &id002 + id: api_track_share_click + method: POST + path: /api/share/[token]/click + description: Increment share click count for analytics + auth: + required: false + request_params: + - name: token + type: string + location: path + required: true + description: Share token to track click for + request_body: [] + responses: + - status: 200 + description: Click tracked successfully + schema: + success: boolean + clickCount: integer + - status: 404 + description: Share not found + schema: + error: string + business_logic: + - Find Share record by token + - Increment clickCount by 1 + - Return new click count + depends_on_models: + - model_share + - id: api_resolve_share + definition: &id003 + id: api_resolve_share + method: GET + path: /api/share/[token] + description: Resolve share token and get content details + auth: + required: false + request_params: + - name: token + type: string + location: path + required: true + description: Share token to resolve + responses: + - status: 200 + description: Share resolved successfully + schema: + type: string + targetId: string + content: object + shareUrl: string + - status: 404 + description: Share not found or content no longer available + schema: + error: string + business_logic: + - Lookup Share record by token + - Fetch associated content based on type (Song/Playlist/Album) + - Include artist information for songs/albums + - Include song list for playlists + - Verify content is still public and available + - Return content details for display + depends_on_models: + - model_share + components: + - id: component_share_content_display + definition: &id001 + id: component_share_content_display + name: SharedContentDisplay + description: Displays shared song/playlist/album with call-to-action + file_path: components/SharedContentDisplay.tsx + props: + - name: type + type: '''song'' | ''playlist'' | ''album''' + required: true + description: Type of shared content + - name: content + type: object + required: true + description: Content data (song/playlist/album with artist info) + state: + - name: isPlaying + type: boolean + description: Audio playback state (for songs) + uses_components: [] + uses_apis: + - api_track_share_click + business_logic: + - Display cover art, title, artist name + - For songs: show waveform, duration, play button + - For playlists: show track count, curator + - For albums: show release date, track count + - 'Call-to-action button: "Listen on Sonic Cloud" or "Sign up to create playlists"' + styling: + - Large cover art + - Prominent title and artist + - Clean, minimal design focused on content + - Branded CTA button + - Preview player for songs (if implemented) +dependencies: + entity_ids: + - component_share_content_display + - api_track_share_click + - api_resolve_share + definitions: + - id: component_share_content_display + type: component + definition: *id001 + - id: api_track_share_click + type: api + definition: *id002 + - id: api_resolve_share + type: api + definition: *id003 +files: + to_create: + - app/s/[token]/page.tsx + reference: [] +acceptance: +- criterion: Page renders at /s/[token] + verification: Navigate to /s/[token] +- criterion: Data fetching works + verification: Check network tab +- criterion: Components render correctly + verification: Visual inspection diff --git a/.workflow/versions/v004/contracts/api_contract.yml b/.workflow/versions/v004/contracts/api_contract.yml new file mode 100644 index 0000000..7b9447b --- /dev/null +++ b/.workflow/versions/v004/contracts/api_contract.yml @@ -0,0 +1,129 @@ +# API Contract for Share Music System +# Version: v004 +# Generated: 2025-12-18 + +endpoints: + - id: api_create_song_share + method: POST + path: /api/share/song/[id] + request: + params: + - name: id + type: string + required: true + body: + type: CreateShareRequest + fields: + - name: platform + type: string + required: false + response: + type: CreateShareResponse + fields: + - name: shareUrl + type: string + - name: token + type: string + - name: type + type: ShareType + + - id: api_create_playlist_share + method: POST + path: /api/share/playlist/[id] + request: + params: + - name: id + type: string + required: true + body: + type: CreateShareRequest + response: + type: CreateShareResponse + + - id: api_create_album_share + method: POST + path: /api/share/album/[id] + request: + params: + - name: id + type: string + required: true + body: + type: CreateShareRequest + response: + type: CreateShareResponse + + - id: api_resolve_share + method: GET + path: /api/share/[token] + request: + params: + - name: token + type: string + required: true + response: + type: ResolveShareResponse + fields: + - name: type + type: ShareType + - name: targetId + type: string + - name: content + type: object + - name: shareUrl + type: string + + - id: api_track_share_click + method: POST + path: /api/share/[token]/click + request: + params: + - name: token + type: string + required: true + response: + type: TrackShareClickResponse + fields: + - name: success + type: boolean + - name: clickCount + type: number + +types: + ShareType: + enum: [SONG, PLAYLIST, ALBUM] + + CreateShareRequest: + fields: + platform: + type: string + optional: true + + CreateShareResponse: + fields: + shareUrl: + type: string + token: + type: string + type: + type: ShareType + + ResolveShareResponse: + fields: + type: + type: ShareType + targetId: + type: string + content: + type: object + shareUrl: + type: string + + TrackShareClickResponse: + fields: + success: + type: boolean + clickCount: + type: number + +shared_types_file: types/api-types.ts diff --git a/.workflow/versions/v004/dependency_graph.yml b/.workflow/versions/v004/dependency_graph.yml new file mode 100644 index 0000000..68e11ac --- /dev/null +++ b/.workflow/versions/v004/dependency_graph.yml @@ -0,0 +1,217 @@ +dependency_graph: + design_version: 1 + workflow_version: v004 + generated_at: '2025-12-18T18:15:12.899651' + generator: validate_design.py + stats: + total_entities: 11 + total_layers: 4 + max_parallelism: 6 + critical_path_length: 4 +layers: +- layer: 1 + name: Data Layer + description: Database models - no external dependencies + items: + - id: component_social_share_buttons + type: component + name: SocialShareButtons + depends_on: [] + task_id: task_create_component_social_share_buttons + agent: frontend + complexity: medium + - id: model_share + type: model + name: Share + depends_on: [] + task_id: task_create_model_share + agent: backend + complexity: medium + requires_layers: [] + parallel_count: 2 +- layer: 2 + name: API Layer + description: REST endpoints - depend on models + items: + - id: api_create_album_share + type: api + name: api_create_album_share + depends_on: + - model_share + task_id: task_create_api_create_album_share + agent: backend + complexity: medium + - id: api_create_playlist_share + type: api + name: api_create_playlist_share + depends_on: + - model_share + task_id: task_create_api_create_playlist_share + agent: backend + complexity: medium + - id: api_create_song_share + type: api + name: api_create_song_share + depends_on: + - model_share + task_id: task_create_api_create_song_share + agent: backend + complexity: medium + - id: api_resolve_share + type: api + name: api_resolve_share + depends_on: + - model_share + task_id: task_create_api_resolve_share + agent: backend + complexity: medium + - id: api_track_share_click + type: api + name: api_track_share_click + depends_on: + - model_share + task_id: task_create_api_track_share_click + agent: backend + complexity: medium + - id: component_share_modal + type: component + name: ShareModal + depends_on: + - component_social_share_buttons + task_id: task_create_component_share_modal + agent: frontend + complexity: medium + requires_layers: + - 1 + parallel_count: 6 +- layer: 3 + name: UI Layer + description: Pages and components - depend on APIs + items: + - id: component_share_button + type: component + name: ShareButton + depends_on: + - api_create_playlist_share + - component_share_modal + - api_create_album_share + - api_create_song_share + task_id: task_create_component_share_button + agent: frontend + complexity: medium + - id: component_share_content_display + type: component + name: SharedContentDisplay + depends_on: + - api_track_share_click + task_id: task_create_component_share_content_display + agent: frontend + complexity: medium + requires_layers: + - 1 + - 2 + parallel_count: 2 +- layer: 4 + name: Layer 4 + description: Entities with 3 levels of dependencies + items: + - id: page_share + type: page + name: SharePage + depends_on: + - component_share_content_display + - api_track_share_click + - api_resolve_share + task_id: task_create_page_share + agent: frontend + complexity: medium + requires_layers: + - 1 + - 2 + - 3 + parallel_count: 1 +dependency_map: + model_share: + type: model + layer: 1 + depends_on: [] + depended_by: + - api_create_album_share + - api_resolve_share + - api_create_playlist_share + - api_track_share_click + - api_create_song_share + api_create_song_share: + type: api + layer: 2 + depends_on: + - model_share + depended_by: + - component_share_button + api_create_playlist_share: + type: api + layer: 2 + depends_on: + - model_share + depended_by: + - component_share_button + api_create_album_share: + type: api + layer: 2 + depends_on: + - model_share + depended_by: + - component_share_button + api_resolve_share: + type: api + layer: 2 + depends_on: + - model_share + depended_by: + - page_share + api_track_share_click: + type: api + layer: 2 + depends_on: + - model_share + depended_by: + - component_share_content_display + - page_share + page_share: + type: page + layer: 4 + depends_on: + - component_share_content_display + - api_track_share_click + - api_resolve_share + depended_by: [] + component_share_button: + type: component + layer: 3 + depends_on: + - api_create_playlist_share + - component_share_modal + - api_create_album_share + - api_create_song_share + depended_by: [] + component_share_modal: + type: component + layer: 2 + depends_on: + - component_social_share_buttons + depended_by: + - component_share_button + component_social_share_buttons: + type: component + layer: 1 + depends_on: [] + depended_by: + - component_share_modal + component_share_content_display: + type: component + layer: 3 + depends_on: + - api_track_share_click + depended_by: + - page_share +task_map: [] diff --git a/.workflow/versions/v004/design/design_document.yml b/.workflow/versions/v004/design/design_document.yml new file mode 100644 index 0000000..8595f81 --- /dev/null +++ b/.workflow/versions/v004/design/design_document.yml @@ -0,0 +1,615 @@ +workflow_version: "v004" +feature: "add share music system" +created_at: "2025-12-18T18:07:00" +status: draft +revision: 1 + +data_models: + - id: model_share + name: Share + table_name: shares + description: Tracks shared content links with analytics + primary_key: id + fields: + - name: id + type: String + constraints: [primary_key, cuid] + description: Unique identifier for the share + - name: type + type: ShareType + constraints: [required] + description: Type of content being shared (SONG, PLAYLIST, ALBUM) + - name: targetId + type: String + constraints: [required] + description: ID of the shared content (references songs/playlists/albums) + - name: token + type: String + constraints: [required, unique] + description: Unique URL-safe token for share links + - name: userId + type: String + constraints: [optional] + description: User who created the share (null for anonymous shares) + - name: platform + type: String + constraints: [optional] + description: Platform where content was shared to (twitter, facebook, etc) + - name: clickCount + type: Int + default: 0 + description: Number of times the share link was clicked + - name: createdAt + type: DateTime + default: now() + description: Timestamp when the share was created + relations: [] + indexes: + - fields: [token] + unique: true + description: Fast lookup by share token + - fields: [targetId, type] + description: Fast lookup of shares for specific content + timestamps: false + validations: + - field: token + rule: minLength(8) + message: Token must be at least 8 characters + - field: type + rule: enum(SONG, PLAYLIST, ALBUM) + message: Type must be SONG, PLAYLIST, or ALBUM + +api_endpoints: + - id: api_create_song_share + method: POST + path: /api/share/song/[id] + description: Generate share link for a song + auth: + required: false + description: No authentication required to share content + request_params: + - name: id + type: string + location: path + required: true + description: Song ID to generate share link for + request_body: + - name: platform + type: string + required: false + description: Platform being shared to (twitter, facebook, etc) + responses: + - status: 200 + description: Share link created successfully + schema: + shareUrl: string + token: string + type: string + - status: 404 + description: Song not found + schema: + error: string + business_logic: + - Verify song exists and is public + - Generate unique 10-character alphanumeric token + - Create Share record with type=SONG + - Return full share URL (https://domain.com/s/[token]) + depends_on_models: [model_share] + + - id: api_create_playlist_share + method: POST + path: /api/share/playlist/[id] + description: Generate share link for a playlist + auth: + required: false + request_params: + - name: id + type: string + location: path + required: true + description: Playlist ID to generate share link for + request_body: + - name: platform + type: string + required: false + description: Platform being shared to + responses: + - status: 200 + description: Share link created successfully + schema: + shareUrl: string + token: string + type: string + - status: 404 + description: Playlist not found + schema: + error: string + business_logic: + - Verify playlist exists and is public + - Generate unique token + - Create Share record with type=PLAYLIST + - Return full share URL + depends_on_models: [model_share] + + - id: api_create_album_share + method: POST + path: /api/share/album/[id] + description: Generate share link for an album + auth: + required: false + request_params: + - name: id + type: string + location: path + required: true + description: Album ID to generate share link for + request_body: + - name: platform + type: string + required: false + description: Platform being shared to + responses: + - status: 200 + description: Share link created successfully + schema: + shareUrl: string + token: string + type: string + - status: 404 + description: Album not found + schema: + error: string + business_logic: + - Verify album exists + - Generate unique token + - Create Share record with type=ALBUM + - Return full share URL + depends_on_models: [model_share] + + - id: api_resolve_share + method: GET + path: /api/share/[token] + description: Resolve share token and get content details + auth: + required: false + request_params: + - name: token + type: string + location: path + required: true + description: Share token to resolve + responses: + - status: 200 + description: Share resolved successfully + schema: + type: string + targetId: string + content: object + shareUrl: string + - status: 404 + description: Share not found or content no longer available + schema: + error: string + business_logic: + - Lookup Share record by token + - Fetch associated content based on type (Song/Playlist/Album) + - Include artist information for songs/albums + - Include song list for playlists + - Verify content is still public and available + - Return content details for display + depends_on_models: [model_share] + + - id: api_track_share_click + method: POST + path: /api/share/[token]/click + description: Increment share click count for analytics + auth: + required: false + request_params: + - name: token + type: string + location: path + required: true + description: Share token to track click for + request_body: [] + responses: + - status: 200 + description: Click tracked successfully + schema: + success: boolean + clickCount: integer + - status: 404 + description: Share not found + schema: + error: string + business_logic: + - Find Share record by token + - Increment clickCount by 1 + - Return new click count + depends_on_models: [model_share] + +pages: + - id: page_share + path: /s/[token] + name: SharePage + description: Public landing page for shared music content + layout: minimal + auth: + required: false + description: Publicly accessible share page + data_needs: + - api_id: api_resolve_share + purpose: Load shared content details + on_load: true + description: Fetch content details when page loads + - api_id: api_track_share_click + purpose: Track analytics + on_load: true + description: Track that user clicked on share link + components: + - component_share_content_display + seo: + dynamic_title: true + description: Dynamic based on shared content + og_image: true + twitter_card: true + ui_states: + - state: loading + description: Fetching shared content + - state: content_loaded + description: Display shared content with CTA + - state: not_found + description: Share token invalid or content no longer available + - state: private_content + description: Content is now private + +components: + - id: component_share_button + name: ShareButton + description: Button component to trigger share modal + file_path: components/ShareButton.tsx + props: + - name: type + type: "'song' | 'playlist' | 'album'" + required: true + description: Type of content to share + - name: targetId + type: string + required: true + description: ID of the content to share + - name: title + type: string + required: true + description: Title to display in share modal + - name: className + type: string + required: false + description: Additional CSS classes + state: + - name: isModalOpen + type: boolean + description: Controls share modal visibility + - name: shareUrl + type: string | null + description: Generated share URL after creation + - name: isLoading + type: boolean + description: Share link generation in progress + events: + - name: onShare + payload: "{ shareUrl: string, token: string }" + description: Fired when share link is successfully generated + uses_components: [component_share_modal] + uses_apis: [api_create_song_share, api_create_playlist_share, api_create_album_share] + styling: + - Accessible button with share icon + - Loading state during share generation + - Error state if share creation fails + + - id: component_share_modal + name: ShareModal + description: Modal displaying share options and social buttons + file_path: components/ShareModal.tsx + props: + - name: isOpen + type: boolean + required: true + description: Controls modal visibility + - name: onClose + type: "() => void" + required: true + description: Callback to close modal + - name: shareUrl + type: string + required: true + description: The share URL to display and copy + - name: title + type: string + required: true + description: Title of content being shared + - name: type + type: "'song' | 'playlist' | 'album'" + required: true + description: Type of content for display context + state: + - name: copied + type: boolean + description: Shows copied confirmation after clipboard copy + events: + - name: onCopy + payload: null + description: Fired when user copies link to clipboard + uses_components: [component_social_share_buttons] + uses_apis: [] + styling: + - Centered modal with backdrop + - Copy to clipboard button with icon + - Success message after copy + - Close button + - Responsive design for mobile + + - id: component_social_share_buttons + name: SocialShareButtons + description: Social media share buttons for Twitter and Facebook + file_path: components/SocialShareButtons.tsx + props: + - name: url + type: string + required: true + description: URL to share + - name: title + type: string + required: true + description: Title/text to include in share + - name: type + type: "'song' | 'playlist' | 'album'" + required: false + description: Type of content for customized share text + events: + - name: onPlatformShare + payload: "{ platform: string }" + description: Fired when user clicks a social share button + uses_components: [] + uses_apis: [] + business_logic: + - Twitter button opens Twitter intent URL with pre-filled text + - Facebook button opens Facebook sharer dialog + - Generate share text based on content type + - URL encode parameters properly + styling: + - Buttons with platform brand colors + - Platform icons (Twitter bird, Facebook f) + - Hover and focus states + - Responsive layout (stack on mobile) + + - id: component_share_content_display + name: SharedContentDisplay + description: Displays shared song/playlist/album with call-to-action + file_path: components/SharedContentDisplay.tsx + props: + - name: type + type: "'song' | 'playlist' | 'album'" + required: true + description: Type of shared content + - name: content + type: object + required: true + description: Content data (song/playlist/album with artist info) + state: + - name: isPlaying + type: boolean + description: Audio playback state (for songs) + uses_components: [] + uses_apis: [api_track_share_click] + business_logic: + - Display cover art, title, artist name + - For songs: show waveform, duration, play button + - For playlists: show track count, curator + - For albums: show release date, track count + - 'Call-to-action button: "Listen on Sonic Cloud" or "Sign up to create playlists"' + styling: + - Large cover art + - Prominent title and artist + - Clean, minimal design focused on content + - Branded CTA button + - Preview player for songs (if implemented) + +utils: + - id: util_generate_share_token + name: generateShareToken + description: Generate unique URL-safe token for share links + file_path: lib/share.ts + function_signature: "() => string" + implementation: + - Generate random 10-character alphanumeric string + - Use crypto-safe random generation + - Verify uniqueness against existing tokens + - Return URL-safe token (no special characters) + + - id: util_build_share_url + name: buildShareUrl + description: Build full share URL from token + file_path: lib/share.ts + function_signature: "(token: string) => string" + implementation: + - Get base URL from environment or request + - Construct URL: https://domain.com/s/[token] + - Return full shareable URL + + - id: util_build_social_share_urls + name: buildSocialShareUrls + description: Generate platform-specific share URLs + file_path: lib/share.ts + function_signature: "(url: string, title: string, type: string) => { twitter: string, facebook: string }" + implementation: + - Twitter: https://twitter.com/intent/tweet?text=[title]&url=[url] + - Facebook: https://www.facebook.com/sharer/sharer.php?u=[url] + - URL encode all parameters + - Customize share text based on content type + +types: + - id: type_share_type + name: ShareType + description: Enum for share content types + file_path: types/share.ts + definition: "enum ShareType { SONG = 'SONG', PLAYLIST = 'PLAYLIST', ALBUM = 'ALBUM' }" + + - id: type_share + name: Share + description: TypeScript type for Share model + file_path: types/share.ts + definition: | + interface Share { + id: string; + type: ShareType; + targetId: string; + token: string; + userId: string | null; + platform: string | null; + clickCount: number; + createdAt: Date; + } + + - id: type_share_response + name: ShareResponse + description: API response for share creation + file_path: types/share.ts + definition: | + interface ShareResponse { + shareUrl: string; + token: string; + type: ShareType; + } + + - id: type_resolved_share + name: ResolvedShare + description: API response for share resolution + file_path: types/share.ts + definition: | + interface ResolvedShare { + type: ShareType; + targetId: string; + content: Song | Playlist | Album; + shareUrl: string; + } + +architecture: + layer_1_data: + - Add Share model to Prisma schema + - Create ShareType enum in Prisma + - Add optional userId relation to User model + - Migration: create shares table with indexes + + layer_2_api: + - POST /api/share/song/[id] - Create song share + - POST /api/share/playlist/[id] - Create playlist share + - POST /api/share/album/[id] - Create album share + - GET /api/share/[token] - Resolve share and fetch content + - POST /api/share/[token]/click - Track analytics + + layer_3_ui: + - ShareButton component (trigger) + - ShareModal component (display options) + - SocialShareButtons component (Twitter/Facebook) + - SharedContentDisplay component (public page) + - /s/[token] page (public landing page) + + layer_4_util: + - lib/share.ts - Token generation and URL building utilities + +dependencies: + data_to_api: + model_share: + - api_create_song_share + - api_create_playlist_share + - api_create_album_share + - api_resolve_share + - api_track_share_click + + api_to_component: + api_create_song_share: [component_share_button] + api_create_playlist_share: [component_share_button] + api_create_album_share: [component_share_button] + api_resolve_share: [component_share_content_display] + api_track_share_click: [component_share_content_display] + + component_to_page: + component_share_button: [page_home, page_artist_profile, page_album_detail, page_playlist_detail, page_search] + component_share_content_display: [page_share] + + component_to_component: + component_share_button: + - component_share_modal + component_share_modal: + - component_social_share_buttons + +implementation_order: + phase_1_data: + - Add ShareType enum to Prisma schema + - Add Share model to Prisma schema + - Create and run migration + - Generate Prisma client + + phase_2_utilities: + - Create lib/share.ts with token generation + - Create types/share.ts with TypeScript types + - Add share URL building utilities + + phase_3_api: + - Implement POST /api/share/song/[id] + - Implement POST /api/share/playlist/[id] + - Implement POST /api/share/album/[id] + - Implement GET /api/share/[token] + - Implement POST /api/share/[token]/click + + phase_4_components: + - Create SocialShareButtons component + - Create ShareModal component + - Create ShareButton component + - Create SharedContentDisplay component + + phase_5_pages: + - Create /s/[token] page + - Integrate ShareButton into existing pages + +testing_requirements: + unit_tests: + - Token generation uniqueness + - URL building correctness + - Share creation for each content type + - Share resolution with valid/invalid tokens + - Click count increment + + integration_tests: + - Full share flow: create → resolve → display + - Social share URL generation + - Public page rendering for all content types + - Analytics tracking + + edge_cases: + - Share private content (should fail or make public) + - Share deleted content (404 on resolution) + - Token collision (should regenerate) + - Anonymous vs authenticated shares + +security_considerations: + - Share tokens must be cryptographically random (10+ characters) + - Rate limit share creation to prevent abuse + - Validate content is public before sharing + - Sanitize all user input in share URLs + - Handle deleted/private content gracefully + - No PII in share URLs + +seo_requirements: + - Dynamic Open Graph tags on /s/[token] pages + - og:title from shared content + - og:image from cover art + - og:description from content description + - Twitter Card meta tags + - Canonical URL handling + +analytics_requirements: + - Track share creation by content type + - Track share clicks (clickCount field) + - Track platform (Twitter, Facebook, copy link) + - Track conversion from share page to signup/login diff --git a/.workflow/versions/v004/requirements/expanded.yml b/.workflow/versions/v004/requirements/expanded.yml new file mode 100644 index 0000000..2564ee1 --- /dev/null +++ b/.workflow/versions/v004/requirements/expanded.yml @@ -0,0 +1,753 @@ +feature: "add share music system" +expanded_at: "2025-12-18T18:07:00Z" +mode: full_auto + +# ═══════════════════════════════════════════════════════════════ +# REQUIREMENTS ANALYSIS +# ═══════════════════════════════════════════════════════════════ + +analysis: + problem_statement: | + Users and artists currently have no way to share music content from Sonic Cloud + with others outside the platform or to promote their work on social media. + Artists cannot easily grow their audience, and users cannot recommend music to friends. + + target_users: + primary: + - Musicians/Artists wanting to promote their work + - Users wanting to share favorite songs with friends + - Playlist curators sharing collections + secondary: + - Social media users discovering shared music + - External website visitors accessing shared links + - Music bloggers embedding content + + core_value: | + Enable viral music discovery by making it effortless to share songs, albums, and + playlists through unique shareable links and social media integration. This creates + organic growth loops where shared content brings new users to the platform. + + user_stories: + - as: Artist + want: Share my new song on Twitter + so_that: I can promote my music to my followers + + - as: User + want: Copy a link to a song + so_that: I can send it to friends via messaging apps + + - as: Playlist Curator + want: Share my curated playlist publicly + so_that: Others can discover my music taste + + - as: Content Creator + want: Get embed code for a song + so_that: I can include it in my blog post + + - as: Artist + want: See how many times my song was shared + so_that: I can measure promotional effectiveness + +# ═══════════════════════════════════════════════════════════════ +# SCOPE DEFINITION +# ═══════════════════════════════════════════════════════════════ + +scope: + mvp_features: + # Core Sharing + - feature: Generate shareable links for songs + priority: CRITICAL + complexity: LOW + + - feature: Generate shareable links for playlists + priority: CRITICAL + complexity: LOW + + - feature: Generate shareable links for albums + priority: CRITICAL + complexity: LOW + + - feature: Copy link to clipboard functionality + priority: HIGH + complexity: LOW + + # Social Integration + - feature: Share to Twitter with pre-filled text + priority: HIGH + complexity: MEDIUM + + - feature: Share to Facebook + priority: HIGH + complexity: MEDIUM + + # UI Components + - feature: ShareButton component + priority: CRITICAL + complexity: LOW + description: Clickable button that opens share modal + + - feature: ShareModal component + priority: CRITICAL + complexity: MEDIUM + description: Modal displaying share options and generated link + + # Landing Page + - feature: Shared content landing page + priority: CRITICAL + complexity: MEDIUM + description: Public page displaying shared song/playlist/album + + # Analytics + - feature: Track share creation events + priority: MEDIUM + complexity: LOW + + - feature: Track share link clicks + priority: MEDIUM + complexity: MEDIUM + + future_features: + # Advanced Sharing + - feature: Embed player code generation + priority: LOW + complexity: HIGH + rationale: "Nice to have but requires iframe security work" + + - feature: Share via WhatsApp + priority: MEDIUM + complexity: LOW + rationale: "Popular messaging app, simple URL scheme" + + # Advanced Analytics + - feature: Share analytics dashboard + priority: LOW + complexity: HIGH + rationale: "Artists want metrics but complex to build well" + + - feature: Geographic share tracking + priority: LOW + complexity: MEDIUM + rationale: "Interesting data but privacy concerns" + + # Social Features + - feature: Direct message sharing within platform + priority: MEDIUM + complexity: HIGH + rationale: "Requires building DM system first" + + - feature: Share to Instagram Stories + priority: MEDIUM + complexity: HIGH + rationale: "Requires Instagram API approval" + + explicitly_excluded: + - NFT/blockchain sharing + - Paid promotion features + - Affiliate link generation + - Cross-platform playlist sync + +# ═══════════════════════════════════════════════════════════════ +# DATA MODEL +# ═══════════════════════════════════════════════════════════════ + +data_model: + entities: + - name: Share + table_name: shares + description: Represents a share action creating a unique shareable link + fields: + - name: id + type: String + constraints: "@id @default(cuid())" + + - name: type + type: Enum + values: [SONG, PLAYLIST, ALBUM] + description: What type of content is being shared + + - name: target_id + type: String + description: ID of the song/playlist/album + + - name: token + type: String + constraints: "@unique" + description: Unique URL-safe token for share link + + - name: user_id + type: String + constraints: "nullable" + description: User who created the share (null for anonymous) + + - name: platform + type: String + constraints: "nullable" + values: [twitter, facebook, clipboard, direct] + description: Platform where share was initiated + + - name: created_at + type: DateTime + constraints: "@default(now())" + + - name: expires_at + type: DateTime + constraints: "nullable" + description: Optional expiration for temporary shares + + relationships: + - field: user + type: User + cardinality: many-to-one + optional: true + + - field: clicks + type: ShareClick[] + cardinality: one-to-many + + - name: ShareClick + table_name: share_clicks + description: Tracks when someone clicks a share link + fields: + - name: id + type: String + constraints: "@id @default(cuid())" + + - name: share_id + type: String + description: Reference to the share + + - name: clicked_at + type: DateTime + constraints: "@default(now())" + + - name: referrer + type: String + constraints: "nullable" + description: HTTP referrer if available + + - name: user_agent + type: String + constraints: "nullable" + description: Browser user agent string + + - name: ip_hash + type: String + constraints: "nullable" + description: Hashed IP for privacy-preserving analytics + + relationships: + - field: share + type: Share + cardinality: many-to-one + + indexes: + - table: shares + fields: [token] + type: unique + + - table: shares + fields: [type, target_id] + + - table: share_clicks + fields: [share_id, clicked_at] + +# ═══════════════════════════════════════════════════════════════ +# API DESIGN +# ═══════════════════════════════════════════════════════════════ + +api_endpoints: + # Share Creation + - id: api_create_song_share + name: Create Song Share + method: POST + path: /api/share/song/:id + purpose: Generate shareable link for a song + authentication: optional + request: + path_params: + - name: id + type: string + description: Song ID + body: + - name: platform + type: string + optional: true + values: [twitter, facebook, clipboard, direct] + response: + success_code: 201 + body: + - name: share_token + type: string + description: Unique token for share URL + - name: share_url + type: string + description: Full shareable URL + - name: social_text + type: string + description: Pre-formatted text for social media + errors: + - code: 404 + description: Song not found + - code: 403 + description: Song is private and user lacks permission + + - id: api_create_playlist_share + name: Create Playlist Share + method: POST + path: /api/share/playlist/:id + purpose: Generate shareable link for a playlist + authentication: optional + request: + path_params: + - name: id + type: string + description: Playlist ID + body: + - name: platform + type: string + optional: true + response: + success_code: 201 + body: + - name: share_token + type: string + - name: share_url + type: string + - name: social_text + type: string + errors: + - code: 404 + description: Playlist not found + - code: 403 + description: Playlist is private + + - id: api_create_album_share + name: Create Album Share + method: POST + path: /api/share/album/:id + purpose: Generate shareable link for an album + authentication: optional + request: + path_params: + - name: id + type: string + description: Album ID + body: + - name: platform + type: string + optional: true + response: + success_code: 201 + body: + - name: share_token + type: string + - name: share_url + type: string + - name: social_text + type: string + errors: + - code: 404 + description: Album not found + + # Share Resolution + - id: api_resolve_share + name: Resolve Share Token + method: GET + path: /api/share/:token + purpose: Get shared content details and track click + authentication: none + request: + path_params: + - name: token + type: string + description: Share token from URL + response: + success_code: 200 + body: + - name: type + type: string + values: [SONG, PLAYLIST, ALBUM] + - name: content + type: object + description: Full content object (Song/Playlist/Album) + - name: share_created_at + type: string + format: iso8601 + errors: + - code: 404 + description: Share token not found or expired + - code: 410 + description: Shared content has been deleted + + # Share Analytics + - id: api_get_share_stats + name: Get Share Statistics + method: GET + path: /api/share/:token/stats + purpose: Get analytics for a specific share + authentication: optional + request: + path_params: + - name: token + type: string + response: + success_code: 200 + body: + - name: total_clicks + type: integer + - name: unique_clicks + type: integer + description: Estimated unique visitors by IP hash + - name: clicks_by_date + type: array + items: + date: string + count: integer + - name: referrers + type: array + items: + source: string + count: integer + errors: + - code: 404 + description: Share not found + + - id: api_get_content_shares + name: Get Content Share Summary + method: GET + path: /api/share/summary/:type/:id + purpose: Get share statistics for a song/playlist/album + authentication: required + authorization: Must be content owner or admin + request: + path_params: + - name: type + type: string + values: [song, playlist, album] + - name: id + type: string + description: Content ID + response: + success_code: 200 + body: + - name: total_shares + type: integer + - name: total_clicks + type: integer + - name: platforms + type: object + description: Breakdown by platform + - name: recent_shares + type: array + description: Last 10 shares with timestamps + +# ═══════════════════════════════════════════════════════════════ +# UI STRUCTURE +# ═══════════════════════════════════════════════════════════════ + +ui_structure: + pages: + - id: page_share_landing + name: Share Landing Page + route: /s/:token + purpose: Public landing page for shared content + components_needed: + - SharedContentHeader + - AudioPlayer (for songs) + - TrackList (for playlists/albums) + - CallToAction (sign up prompt) + features: + - Display shared content with rich metadata + - Play preview if allowed + - Show artist/creator information + - Call to action to join platform + - Open Graph tags for social media previews + accessibility: + - Works without authentication + - Mobile responsive + - Fast loading with SSR + - Proper meta tags for crawlers + + components: + - id: component_share_button + name: ShareButton + file_path: components/ShareButton.tsx + purpose: Trigger share modal from any content + props: + - name: type + type: enum + values: [song, playlist, album] + required: true + + - name: contentId + type: string + required: true + + - name: contentTitle + type: string + required: true + description: Used in share text + + - name: artistName + type: string + required: false + description: Used in share text for songs/albums + + - name: variant + type: enum + values: [icon, text, full] + default: icon + description: Visual style + + behavior: + - onClick opens ShareModal + - Shows loading state during share creation + - Can be used in SongCard, AlbumCard, etc. + + - id: component_share_modal + name: ShareModal + file_path: components/ShareModal.tsx + purpose: Display share options and generated link + props: + - name: isOpen + type: boolean + required: true + + - name: onClose + type: function + required: true + + - name: shareUrl + type: string + required: true + + - name: shareText + type: string + required: true + + features: + - Copy to clipboard button with feedback + - Social media share buttons + - Generated link display + - QR code (future) + + - id: component_social_share_buttons + name: SocialShareButtons + file_path: components/SocialShareButtons.tsx + purpose: Platform-specific share buttons + props: + - name: url + type: string + required: true + + - name: text + type: string + required: true + + - name: platforms + type: array + default: [twitter, facebook] + + behavior: + - Opens platform share dialog in popup + - Tracks platform in analytics + - Falls back to clipboard if platform unavailable + + - id: component_shared_content_header + name: SharedContentHeader + file_path: components/SharedContentHeader.tsx + purpose: Header for share landing page + props: + - name: type + type: enum + values: [song, playlist, album] + + - name: title + type: string + + - name: artist + type: string + optional: true + + - name: coverUrl + type: string + optional: true + + - name: metadata + type: object + description: Duration, track count, etc. + + features: + - Large cover art + - Title and artist + - Metadata display + - Share count badge + +# ═══════════════════════════════════════════════════════════════ +# SECURITY & AUTHORIZATION +# ═══════════════════════════════════════════════════════════════ + +security: + authentication: + share_creation: optional + share_access: none + analytics_access: required (owner only) + + authorization: + rules: + - resource: Song + action: share + condition: Song is public OR user is owner OR user has access + + - resource: Playlist + action: share + condition: Playlist is public OR user is owner + + - resource: Album + action: share + condition: Album is public (albums are always public) + + - resource: Share Analytics + action: view + condition: User is content owner OR user is admin + + token_security: + - Tokens are cryptographically random (cuid) + - Tokens are URL-safe + - No sequential IDs exposed + - Optional expiration dates + - Rate limiting on creation (10/minute per IP) + + privacy: + - IP addresses are hashed, not stored + - User agents stored for analytics only + - No cross-site tracking + - Respects Do Not Track header + - GDPR compliant analytics + +# ═══════════════════════════════════════════════════════════════ +# EDGE CASES & ERROR HANDLING +# ═══════════════════════════════════════════════════════════════ + +edge_cases: + - scenario: User shares private playlist + detection: Check playlist.is_public flag + handling: Return 403 error with message "Cannot share private playlists" + + - scenario: Shared content is deleted + detection: Share exists but target_id has no matching record + handling: Show friendly "Content no longer available" page + + - scenario: Share token collision (extremely rare) + detection: Unique constraint violation on token + handling: Retry with new token automatically + + - scenario: High share volume (viral content) + detection: Rate limiting triggers + handling: Return 429 with retry-after header + + - scenario: Bot traffic clicking shares + detection: Suspicious user agents, high frequency + handling: Track but flag as bot, don't count in "unique clicks" + + - scenario: Share link in iframe + detection: X-Frame-Options header + handling: Allow embedding from trusted domains only + + - scenario: Expired share token + detection: expires_at < now() + handling: Return 410 Gone status + + - scenario: User shares same content multiple times + detection: Check for existing share by user_id + target + handling: Reuse existing share token, update platform + + - scenario: Anonymous user shares content + detection: No auth token in request + handling: Allow, set user_id to null + + - scenario: Shared song is in private album + detection: Song.album.is_public = false + handling: Still allow sharing individual song if song.is_public = true + +# ═══════════════════════════════════════════════════════════════ +# ACCEPTANCE CRITERIA +# ═══════════════════════════════════════════════════════════════ + +acceptance_criteria: + functional: + - Given I am viewing a song, When I click the share button, Then a modal opens with share options + - Given a share modal is open, When I click "Copy Link", Then the link is copied and I see confirmation + - Given a share modal is open, When I click "Twitter", Then Twitter share dialog opens with pre-filled text + - Given I have a share link, When I visit /s/:token, Then I see the shared content + - Given I visit an expired share, When the page loads, Then I see "Content no longer available" + - Given I am the song owner, When I view share stats, Then I see click counts and platforms + - Given I share the same song twice, When I generate the second share, Then I get the same token + + non_functional: + - Share link generation completes in < 500ms + - Share landing page loads in < 2s on 3G + - Share tokens are at least 20 characters + - Share modal is keyboard accessible + - Open Graph tags are present for all share pages + - Analytics queries complete in < 1s + + security: + - Cannot share private content without permission + - Share tokens are unguessable + - Rate limiting prevents abuse + - IP addresses are hashed, not stored + - Analytics are only visible to content owner + +# ═══════════════════════════════════════════════════════════════ +# TECHNICAL CONSIDERATIONS +# ═══════════════════════════════════════════════════════════════ + +technical_notes: + performance: + - Cache share tokens in Redis for fast lookup + - Use database index on shares.token + - Batch insert share clicks for high traffic + - CDN for share landing pages + + monitoring: + - Track share creation rate + - Monitor share click patterns + - Alert on unusual traffic spikes + - Dashboard for viral content + + testing: + - Unit tests for share token generation + - Integration tests for share flow + - E2E tests for social sharing + - Load testing for viral scenarios + + deployment: + - Feature flag for gradual rollout + - A/B test share button placement + - Monitor analytics database load + - Plan for scale to 10k shares/day + +# ═══════════════════════════════════════════════════════════════ +# SUCCESS METRICS +# ═══════════════════════════════════════════════════════════════ + +success_metrics: + launch_goals: + - 100 shares created in first week + - 500 share link clicks in first week + - 10% of shared content viewers sign up + - <1% error rate on share creation + + ongoing_kpis: + - Daily active shares + - Share-to-click conversion rate + - Click-to-signup conversion rate + - Top shared content + - Platform distribution (Twitter vs Facebook vs direct) + + business_impact: + - Increase organic user acquisition by 20% + - Reduce paid marketing cost per acquisition + - Increase artist retention (sharing = promotion) + - Increase content discovery beyond search diff --git a/.workflow/versions/v004/requirements/final.yml b/.workflow/versions/v004/requirements/final.yml new file mode 100644 index 0000000..b0d26b7 --- /dev/null +++ b/.workflow/versions/v004/requirements/final.yml @@ -0,0 +1,94 @@ +feature: "add share music system" +mode: full_auto +finalized_at: "2025-12-18T18:06:00" + +analysis: + problem_statement: "Users cannot share music or promote content outside the platform" + target_users: "Artists promoting music, users recommending songs, playlist curators" + core_value: "Enable viral music discovery through effortless sharing" + +scope: + mvp_features: + - Share songs via unique link + - Share playlists via unique link + - Share albums via unique link + - Copy link to clipboard + - Social media share buttons (Twitter, Facebook) + - ShareButton component for triggering share + - ShareModal with share options + - Public landing page for shared content + - Basic share analytics + +data_model: + entities: + - name: Share + table: shares + fields: + - name: id + type: uuid + primary: true + - name: type + type: enum + values: [song, playlist, album] + - name: target_id + type: uuid + - name: token + type: string + unique: true + - name: user_id + type: uuid + nullable: true + - name: platform + type: string + nullable: true + - name: click_count + type: integer + default: 0 + - name: created_at + type: timestamp + +api_endpoints: + - method: POST + path: /api/share/song/:id + purpose: Generate share link for a song + - method: POST + path: /api/share/playlist/:id + purpose: Generate share link for a playlist + - method: POST + path: /api/share/album/:id + purpose: Generate share link for an album + - method: GET + path: /api/share/:token + purpose: Resolve share token and get content + - method: POST + path: /api/share/:token/click + purpose: Track share link click + +ui_structure: + pages: + - name: SharePage + route: /s/:token + purpose: Landing page for shared content + components: + - name: ShareButton + purpose: Trigger share modal on songs/playlists/albums + - name: ShareModal + purpose: Display share options (copy link, social buttons) + - name: SocialShareButtons + purpose: Twitter and Facebook share buttons + +acceptance_criteria: + - criterion: "User can generate share link for any song" + verification: "Click share button on song, get unique link, link works" + - criterion: "User can generate share link for playlist" + verification: "Click share button on playlist, get unique link, link works" + - criterion: "User can generate share link for album" + verification: "Click share button on album, get unique link, link works" + - criterion: "Copy to clipboard works" + verification: "Click copy button, paste shows correct URL" + - criterion: "Social share buttons open correct URLs" + verification: "Twitter/Facebook buttons open share dialogs with content" + - criterion: "Share page displays content correctly" + verification: "Visit share link, see song/playlist/album with play option" + - criterion: "Share page has call-to-action" + verification: "Share page shows 'Listen on Sonic Cloud' or 'Sign up' button" diff --git a/.workflow/versions/v004/session.yml b/.workflow/versions/v004/session.yml new file mode 100644 index 0000000..95b9840 --- /dev/null +++ b/.workflow/versions/v004/session.yml @@ -0,0 +1,30 @@ +version: v004 +feature: add share music system +session_id: workflow_20251218_180550 +parent_version: null +status: completed +started_at: '2025-12-18T18:05:50.056169' +completed_at: '2025-12-18T18:24:29.951800' +current_phase: COMPLETING +approvals: + design: + status: approved + approved_by: user + approved_at: '2025-12-18T18:15:50.900263' + rejection_reason: null + implementation: + status: approved + approved_by: user + approved_at: '2025-12-18T18:24:16.348567' + rejection_reason: null +task_sessions: [] +summary: + total_tasks: 0 + tasks_completed: 0 + entities_created: 0 + entities_updated: 0 + entities_deleted: 0 + files_created: 0 + files_updated: 0 + files_deleted: 0 +updated_at: '2025-12-18T18:24:29.951804' diff --git a/.workflow/versions/v004/session.yml.bak b/.workflow/versions/v004/session.yml.bak new file mode 100644 index 0000000..b5e28a3 --- /dev/null +++ b/.workflow/versions/v004/session.yml.bak @@ -0,0 +1,30 @@ +version: v004 +feature: add share music system +session_id: workflow_20251218_180550 +parent_version: null +status: pending +started_at: '2025-12-18T18:05:50.056169' +completed_at: null +current_phase: IMPL_APPROVED +approvals: + design: + status: approved + approved_by: user + approved_at: '2025-12-18T18:15:50.900263' + rejection_reason: null + implementation: + status: approved + approved_by: user + approved_at: '2025-12-18T18:24:16.348567' + rejection_reason: null +task_sessions: [] +summary: + total_tasks: 0 + tasks_completed: 0 + entities_created: 0 + entities_updated: 0 + entities_deleted: 0 + files_created: 0 + files_updated: 0 + files_deleted: 0 +updated_at: '2025-12-18T18:24:16.349487' diff --git a/.workflow/versions/v004/snapshot_after/manifest.json b/.workflow/versions/v004/snapshot_after/manifest.json new file mode 100644 index 0000000..0754607 --- /dev/null +++ b/.workflow/versions/v004/snapshot_after/manifest.json @@ -0,0 +1,737 @@ +{ + "project": { + "name": "sonic-cloud", + "version": "0.1.0", + "created_at": "2025-12-18T14:32:39.275839", + "description": "Music platform for musicians to upload songs" + }, + "state": { + "current_phase": "DESIGN_PHASE", + "approval_status": { + "manifest_approved": false, + "approved_by": null, + "approved_at": null + }, + "revision_history": [ + { + "action": "PROJECT_INITIALIZED", + "timestamp": "2025-12-18T14:32:39.275844", + "details": "Project sonic-cloud created" + }, + { + "action": "DESIGN_DOCUMENT_CREATED", + "timestamp": "2025-12-18T15:10:00", + "details": "Complete design document with 91 entities created" + } + ] + }, + "entities": { + "database_tables": [ + { + "id": "model_user", + "name": "User", + "table_name": "users", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_artist", + "name": "Artist", + "table_name": "artists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_label", + "name": "Label", + "table_name": "labels", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_genre", + "name": "Genre", + "table_name": "genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_album", + "name": "Album", + "table_name": "albums", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song", + "name": "Song", + "table_name": "songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song_genre", + "name": "SongGenre", + "table_name": "song_genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist", + "name": "Playlist", + "table_name": "playlists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist_song", + "name": "PlaylistSong", + "table_name": "playlist_songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_share", + "name": "Share", + "table_name": "shares", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + } + ], + "api_endpoints": [ + { + "id": "api_register", + "name": "Register User", + "path": "/api/auth/register", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/register/route.ts" + }, + { + "id": "api_login", + "name": "Login", + "path": "/api/auth/login", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/login/route.ts" + }, + { + "id": "api_forgot_password", + "name": "Forgot Password", + "path": "/api/auth/forgot-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/forgot-password/route.ts" + }, + { + "id": "api_reset_password", + "name": "Reset Password", + "path": "/api/auth/reset-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/reset-password/route.ts" + }, + { + "id": "api_get_current_user", + "name": "Get Current User", + "path": "/api/users/me", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_update_current_user", + "name": "Update Current User", + "path": "/api/users/me", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_create_artist_profile", + "name": "Create Artist Profile", + "path": "/api/artists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/artists/route.ts" + }, + { + "id": "api_get_artist", + "name": "Get Artist", + "path": "/api/artists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_update_artist", + "name": "Update Artist", + "path": "/api/artists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_get_artist_songs", + "name": "Get Artist Songs", + "path": "/api/artists/:id/songs", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/songs/route.ts" + }, + { + "id": "api_get_artist_albums", + "name": "Get Artist Albums", + "path": "/api/artists/:id/albums", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/albums/route.ts" + }, + { + "id": "api_upload_song", + "name": "Upload Song", + "path": "/api/songs/upload", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/upload/route.ts" + }, + { + "id": "api_get_song", + "name": "Get Song", + "path": "/api/songs/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_update_song", + "name": "Update Song", + "path": "/api/songs/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_delete_song", + "name": "Delete Song", + "path": "/api/songs/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_increment_play_count", + "name": "Increment Play Count", + "path": "/api/songs/:id/play", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/[id]/play/route.ts" + }, + { + "id": "api_create_album", + "name": "Create Album", + "path": "/api/albums", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/albums/route.ts" + }, + { + "id": "api_get_album", + "name": "Get Album", + "path": "/api/albums/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_update_album", + "name": "Update Album", + "path": "/api/albums/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_delete_album", + "name": "Delete Album", + "path": "/api/albums/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_create_playlist", + "name": "Create Playlist", + "path": "/api/playlists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_user_playlists", + "name": "Get User Playlists", + "path": "/api/playlists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_playlist", + "name": "Get Playlist", + "path": "/api/playlists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_update_playlist", + "name": "Update Playlist", + "path": "/api/playlists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_delete_playlist", + "name": "Delete Playlist", + "path": "/api/playlists/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_add_song_to_playlist", + "name": "Add Song to Playlist", + "path": "/api/playlists/:id/songs", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/songs/route.ts" + }, + { + "id": "api_remove_song_from_playlist", + "name": "Remove Song from Playlist", + "path": "/api/playlists/:playlistId/songs/:songId", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[playlistId]/songs/[songId]/route.ts" + }, + { + "id": "api_reorder_playlist_songs", + "name": "Reorder Playlist Songs", + "path": "/api/playlists/:id/reorder", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/reorder/route.ts" + }, + { + "id": "api_get_trending_songs", + "name": "Get Trending Songs", + "path": "/api/discover/trending", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/trending/route.ts" + }, + { + "id": "api_get_new_releases", + "name": "Get New Releases", + "path": "/api/discover/new-releases", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/new-releases/route.ts" + }, + { + "id": "api_get_genres", + "name": "Get Genres", + "path": "/api/discover/genres", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/route.ts" + }, + { + "id": "api_get_songs_by_genre", + "name": "Get Songs by Genre", + "path": "/api/discover/genres/:slug", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/[slug]/route.ts" + }, + { + "id": "api_search", + "name": "Search", + "path": "/api/search", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/search/route.ts" + }, + { + "id": "api_create_label_profile", + "name": "Create Label Profile", + "path": "/api/labels", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/labels/route.ts" + }, + { + "id": "api_get_label_artists", + "name": "Get Label Artists", + "path": "/api/labels/:id/artists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/labels/[id]/artists/route.ts" + }, + { + "id": "api_create_song_share", + "name": "Create Song Share", + "path": "/api/share/song/:id", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/share/song/[id]/route.ts" + }, + { + "id": "api_create_playlist_share", + "name": "Create Playlist Share", + "path": "/api/share/playlist/:id", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/share/playlist/[id]/route.ts" + }, + { + "id": "api_create_album_share", + "name": "Create Album Share", + "path": "/api/share/album/:id", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/share/album/[id]/route.ts" + }, + { + "id": "api_resolve_share", + "name": "Resolve Share", + "path": "/api/share/:token", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/share/[token]/route.ts" + }, + { + "id": "api_track_share_click", + "name": "Track Share Click", + "path": "/api/share/:token/click", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/share/[token]/click/route.ts" + } + ], + "pages": [ + { + "id": "page_login", + "name": "Login", + "path": "/login", + "status": "PENDING", + "file_path": "app/login/page.tsx" + }, + { + "id": "page_register", + "name": "Register", + "path": "/register", + "status": "PENDING", + "file_path": "app/register/page.tsx" + }, + { + "id": "page_forgot_password", + "name": "Forgot Password", + "path": "/forgot-password", + "status": "PENDING", + "file_path": "app/forgot-password/page.tsx" + }, + { + "id": "page_home", + "name": "Discover Music", + "path": "/", + "status": "PENDING", + "file_path": "app/page.tsx" + }, + { + "id": "page_artist_profile", + "name": "Artist Profile", + "path": "/artist/:id", + "status": "PENDING", + "file_path": "app/artist/[id]/page.tsx" + }, + { + "id": "page_album_detail", + "name": "Album", + "path": "/album/:id", + "status": "PENDING", + "file_path": "app/album/[id]/page.tsx" + }, + { + "id": "page_upload", + "name": "Upload Music", + "path": "/upload", + "status": "PENDING", + "file_path": "app/upload/page.tsx" + }, + { + "id": "page_playlists", + "name": "My Playlists", + "path": "/playlists", + "status": "PENDING", + "file_path": "app/playlists/page.tsx" + }, + { + "id": "page_playlist_detail", + "name": "Playlist", + "path": "/playlist/:id", + "status": "PENDING", + "file_path": "app/playlist/[id]/page.tsx" + }, + { + "id": "page_profile", + "name": "Profile Settings", + "path": "/profile", + "status": "PENDING", + "file_path": "app/profile/page.tsx" + }, + { + "id": "page_search", + "name": "Search", + "path": "/search", + "status": "PENDING", + "file_path": "app/search/page.tsx" + }, + { + "id": "page_genre_browse", + "name": "Browse Genre", + "path": "/genre/:slug", + "status": "PENDING", + "file_path": "app/genre/[slug]/page.tsx" + }, + { + "id": "page_share", + "name": "Share", + "path": "/s/:token", + "status": "PENDING", + "file_path": "app/s/[token]/page.tsx" + } + ], + "components": [ + { + "id": "component_audio_player", + "name": "AudioPlayer", + "status": "PENDING", + "file_path": "components/AudioPlayer.tsx" + }, + { + "id": "component_player_controls", + "name": "PlayerControls", + "status": "PENDING", + "file_path": "components/PlayerControls.tsx" + }, + { + "id": "component_song_card", + "name": "SongCard", + "status": "PENDING", + "file_path": "components/SongCard.tsx" + }, + { + "id": "component_album_card", + "name": "AlbumCard", + "status": "PENDING", + "file_path": "components/AlbumCard.tsx" + }, + { + "id": "component_artist_card", + "name": "ArtistCard", + "status": "PENDING", + "file_path": "components/ArtistCard.tsx" + }, + { + "id": "component_playlist_card", + "name": "PlaylistCard", + "status": "PENDING", + "file_path": "components/PlaylistCard.tsx" + }, + { + "id": "component_upload_form", + "name": "UploadForm", + "status": "PENDING", + "file_path": "components/UploadForm.tsx" + }, + { + "id": "component_waveform_display", + "name": "WaveformDisplay", + "status": "PENDING", + "file_path": "components/WaveformDisplay.tsx" + }, + { + "id": "component_genre_badge", + "name": "GenreBadge", + "status": "PENDING", + "file_path": "components/GenreBadge.tsx" + }, + { + "id": "component_track_list", + "name": "TrackList", + "status": "PENDING", + "file_path": "components/TrackList.tsx" + }, + { + "id": "component_artist_header", + "name": "ArtistHeader", + "status": "PENDING", + "file_path": "components/ArtistHeader.tsx" + }, + { + "id": "component_album_header", + "name": "AlbumHeader", + "status": "PENDING", + "file_path": "components/AlbumHeader.tsx" + }, + { + "id": "component_playlist_header", + "name": "PlaylistHeader", + "status": "PENDING", + "file_path": "components/PlaylistHeader.tsx" + }, + { + "id": "component_social_links", + "name": "SocialLinks", + "status": "PENDING", + "file_path": "components/SocialLinks.tsx" + }, + { + "id": "component_auth_form", + "name": "AuthForm", + "status": "PENDING", + "file_path": "components/AuthForm.tsx" + }, + { + "id": "component_search_bar", + "name": "SearchBar", + "status": "PENDING", + "file_path": "components/SearchBar.tsx" + }, + { + "id": "component_search_results", + "name": "SearchResults", + "status": "PENDING", + "file_path": "components/SearchResults.tsx" + }, + { + "id": "component_create_playlist_modal", + "name": "CreatePlaylistModal", + "status": "PENDING", + "file_path": "components/CreatePlaylistModal.tsx" + }, + { + "id": "component_profile_form", + "name": "ProfileForm", + "status": "PENDING", + "file_path": "components/ProfileForm.tsx" + }, + { + "id": "component_avatar_upload", + "name": "AvatarUpload", + "status": "PENDING", + "file_path": "components/AvatarUpload.tsx" + }, + { + "id": "component_section_header", + "name": "SectionHeader", + "status": "PENDING", + "file_path": "components/SectionHeader.tsx" + }, + { + "id": "component_genre_header", + "name": "GenreHeader", + "status": "PENDING", + "file_path": "components/GenreHeader.tsx" + }, + { + "id": "component_header", + "name": "Header", + "status": "IMPLEMENTED", + "file_path": "components/Header.tsx" + }, + { + "id": "component_nav_link", + "name": "NavLink", + "status": "IMPLEMENTED", + "file_path": "components/NavLink.tsx" + }, + { + "id": "component_user_menu", + "name": "UserMenu", + "status": "IMPLEMENTED", + "file_path": "components/UserMenu.tsx" + }, + { + "id": "component_share_button", + "name": "ShareButton", + "status": "PENDING", + "file_path": "components/ShareButton.tsx" + }, + { + "id": "component_share_modal", + "name": "ShareModal", + "status": "PENDING", + "file_path": "components/ShareModal.tsx" + }, + { + "id": "component_social_share_buttons", + "name": "SocialShareButtons", + "status": "PENDING", + "file_path": "components/SocialShareButtons.tsx" + }, + { + "id": "component_share_content_display", + "name": "SharedContentDisplay", + "status": "PENDING", + "file_path": "components/SharedContentDisplay.tsx" + } + ] + }, + "dependencies": { + "component_to_page": { + "component_auth_form": ["page_login", "page_register", "page_forgot_password"], + "component_song_card": ["page_home", "page_artist_profile", "page_search", "page_genre_browse", "page_album_detail", "page_playlist_detail"], + "component_genre_badge": ["page_home"], + "component_section_header": ["page_home"], + "component_artist_header": ["page_artist_profile"], + "component_album_card": ["page_artist_profile", "page_search"], + "component_social_links": ["page_artist_profile"], + "component_album_header": ["page_album_detail"], + "component_track_list": ["page_album_detail", "page_playlist_detail"], + "component_upload_form": ["page_upload"], + "component_waveform_display": ["page_upload"], + "component_playlist_card": ["page_playlists"], + "component_create_playlist_modal": ["page_playlists"], + "component_playlist_header": ["page_playlist_detail"], + "component_profile_form": ["page_profile"], + "component_avatar_upload": ["page_profile"], + "component_search_bar": ["page_search"], + "component_search_results": ["page_search"], + "component_artist_card": ["page_search"], + "component_genre_header": ["page_genre_browse"] + }, + "api_to_component": { + "api_login": ["component_auth_form"], + "api_register": ["component_auth_form"], + "api_forgot_password": ["component_auth_form"], + "api_upload_song": ["component_upload_form"], + "api_increment_play_count": ["component_audio_player"], + "api_search": ["component_search_bar"], + "api_create_playlist": ["component_create_playlist_modal"], + "api_update_current_user": ["component_profile_form"] + }, + "table_to_api": { + "model_user": ["api_register", "api_login", "api_forgot_password", "api_reset_password", "api_get_current_user", "api_update_current_user"], + "model_artist": ["api_create_artist_profile", "api_get_artist", "api_update_artist", "api_get_artist_songs", "api_get_artist_albums"], + "model_song": ["api_upload_song", "api_get_song", "api_update_song", "api_delete_song", "api_increment_play_count", "api_get_artist_songs", "api_get_trending_songs", "api_get_new_releases", "api_search"], + "model_album": ["api_create_album", "api_get_album", "api_update_album", "api_delete_album", "api_get_artist_albums", "api_search"], + "model_playlist": ["api_create_playlist", "api_get_user_playlists", "api_get_playlist", "api_update_playlist", "api_delete_playlist"], + "model_playlist_song": ["api_add_song_to_playlist", "api_remove_song_from_playlist", "api_reorder_playlist_songs"], + "model_genre": ["api_get_genres", "api_get_songs_by_genre"], + "model_label": ["api_create_label_profile", "api_get_label_artists"] + } + } +} diff --git a/.workflow/versions/v004/snapshot_before/manifest.json b/.workflow/versions/v004/snapshot_before/manifest.json new file mode 100644 index 0000000..8e53336 --- /dev/null +++ b/.workflow/versions/v004/snapshot_before/manifest.json @@ -0,0 +1,659 @@ +{ + "project": { + "name": "sonic-cloud", + "version": "0.1.0", + "created_at": "2025-12-18T14:32:39.275839", + "description": "Music platform for musicians to upload songs" + }, + "state": { + "current_phase": "DESIGN_PHASE", + "approval_status": { + "manifest_approved": false, + "approved_by": null, + "approved_at": null + }, + "revision_history": [ + { + "action": "PROJECT_INITIALIZED", + "timestamp": "2025-12-18T14:32:39.275844", + "details": "Project sonic-cloud created" + }, + { + "action": "DESIGN_DOCUMENT_CREATED", + "timestamp": "2025-12-18T15:10:00", + "details": "Complete design document with 91 entities created" + } + ] + }, + "entities": { + "database_tables": [ + { + "id": "model_user", + "name": "User", + "table_name": "users", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_artist", + "name": "Artist", + "table_name": "artists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_label", + "name": "Label", + "table_name": "labels", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_genre", + "name": "Genre", + "table_name": "genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_album", + "name": "Album", + "table_name": "albums", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song", + "name": "Song", + "table_name": "songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_song_genre", + "name": "SongGenre", + "table_name": "song_genres", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist", + "name": "Playlist", + "table_name": "playlists", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + }, + { + "id": "model_playlist_song", + "name": "PlaylistSong", + "table_name": "playlist_songs", + "status": "PENDING", + "file_path": "prisma/schema.prisma" + } + ], + "api_endpoints": [ + { + "id": "api_register", + "name": "Register User", + "path": "/api/auth/register", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/register/route.ts" + }, + { + "id": "api_login", + "name": "Login", + "path": "/api/auth/login", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/login/route.ts" + }, + { + "id": "api_forgot_password", + "name": "Forgot Password", + "path": "/api/auth/forgot-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/forgot-password/route.ts" + }, + { + "id": "api_reset_password", + "name": "Reset Password", + "path": "/api/auth/reset-password", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/auth/reset-password/route.ts" + }, + { + "id": "api_get_current_user", + "name": "Get Current User", + "path": "/api/users/me", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_update_current_user", + "name": "Update Current User", + "path": "/api/users/me", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/users/me/route.ts" + }, + { + "id": "api_create_artist_profile", + "name": "Create Artist Profile", + "path": "/api/artists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/artists/route.ts" + }, + { + "id": "api_get_artist", + "name": "Get Artist", + "path": "/api/artists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_update_artist", + "name": "Update Artist", + "path": "/api/artists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/artists/[id]/route.ts" + }, + { + "id": "api_get_artist_songs", + "name": "Get Artist Songs", + "path": "/api/artists/:id/songs", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/songs/route.ts" + }, + { + "id": "api_get_artist_albums", + "name": "Get Artist Albums", + "path": "/api/artists/:id/albums", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/artists/[id]/albums/route.ts" + }, + { + "id": "api_upload_song", + "name": "Upload Song", + "path": "/api/songs/upload", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/upload/route.ts" + }, + { + "id": "api_get_song", + "name": "Get Song", + "path": "/api/songs/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_update_song", + "name": "Update Song", + "path": "/api/songs/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_delete_song", + "name": "Delete Song", + "path": "/api/songs/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/songs/[id]/route.ts" + }, + { + "id": "api_increment_play_count", + "name": "Increment Play Count", + "path": "/api/songs/:id/play", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/songs/[id]/play/route.ts" + }, + { + "id": "api_create_album", + "name": "Create Album", + "path": "/api/albums", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/albums/route.ts" + }, + { + "id": "api_get_album", + "name": "Get Album", + "path": "/api/albums/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_update_album", + "name": "Update Album", + "path": "/api/albums/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_delete_album", + "name": "Delete Album", + "path": "/api/albums/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/albums/[id]/route.ts" + }, + { + "id": "api_create_playlist", + "name": "Create Playlist", + "path": "/api/playlists", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_user_playlists", + "name": "Get User Playlists", + "path": "/api/playlists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/route.ts" + }, + { + "id": "api_get_playlist", + "name": "Get Playlist", + "path": "/api/playlists/:id", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_update_playlist", + "name": "Update Playlist", + "path": "/api/playlists/:id", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_delete_playlist", + "name": "Delete Playlist", + "path": "/api/playlists/:id", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/route.ts" + }, + { + "id": "api_add_song_to_playlist", + "name": "Add Song to Playlist", + "path": "/api/playlists/:id/songs", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/songs/route.ts" + }, + { + "id": "api_remove_song_from_playlist", + "name": "Remove Song from Playlist", + "path": "/api/playlists/:playlistId/songs/:songId", + "method": "DELETE", + "status": "PENDING", + "file_path": "app/api/playlists/[playlistId]/songs/[songId]/route.ts" + }, + { + "id": "api_reorder_playlist_songs", + "name": "Reorder Playlist Songs", + "path": "/api/playlists/:id/reorder", + "method": "PUT", + "status": "PENDING", + "file_path": "app/api/playlists/[id]/reorder/route.ts" + }, + { + "id": "api_get_trending_songs", + "name": "Get Trending Songs", + "path": "/api/discover/trending", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/trending/route.ts" + }, + { + "id": "api_get_new_releases", + "name": "Get New Releases", + "path": "/api/discover/new-releases", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/new-releases/route.ts" + }, + { + "id": "api_get_genres", + "name": "Get Genres", + "path": "/api/discover/genres", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/route.ts" + }, + { + "id": "api_get_songs_by_genre", + "name": "Get Songs by Genre", + "path": "/api/discover/genres/:slug", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/discover/genres/[slug]/route.ts" + }, + { + "id": "api_search", + "name": "Search", + "path": "/api/search", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/search/route.ts" + }, + { + "id": "api_create_label_profile", + "name": "Create Label Profile", + "path": "/api/labels", + "method": "POST", + "status": "PENDING", + "file_path": "app/api/labels/route.ts" + }, + { + "id": "api_get_label_artists", + "name": "Get Label Artists", + "path": "/api/labels/:id/artists", + "method": "GET", + "status": "PENDING", + "file_path": "app/api/labels/[id]/artists/route.ts" + } + ], + "pages": [ + { + "id": "page_login", + "name": "Login", + "path": "/login", + "status": "PENDING", + "file_path": "app/login/page.tsx" + }, + { + "id": "page_register", + "name": "Register", + "path": "/register", + "status": "PENDING", + "file_path": "app/register/page.tsx" + }, + { + "id": "page_forgot_password", + "name": "Forgot Password", + "path": "/forgot-password", + "status": "PENDING", + "file_path": "app/forgot-password/page.tsx" + }, + { + "id": "page_home", + "name": "Discover Music", + "path": "/", + "status": "PENDING", + "file_path": "app/page.tsx" + }, + { + "id": "page_artist_profile", + "name": "Artist Profile", + "path": "/artist/:id", + "status": "PENDING", + "file_path": "app/artist/[id]/page.tsx" + }, + { + "id": "page_album_detail", + "name": "Album", + "path": "/album/:id", + "status": "PENDING", + "file_path": "app/album/[id]/page.tsx" + }, + { + "id": "page_upload", + "name": "Upload Music", + "path": "/upload", + "status": "PENDING", + "file_path": "app/upload/page.tsx" + }, + { + "id": "page_playlists", + "name": "My Playlists", + "path": "/playlists", + "status": "PENDING", + "file_path": "app/playlists/page.tsx" + }, + { + "id": "page_playlist_detail", + "name": "Playlist", + "path": "/playlist/:id", + "status": "PENDING", + "file_path": "app/playlist/[id]/page.tsx" + }, + { + "id": "page_profile", + "name": "Profile Settings", + "path": "/profile", + "status": "PENDING", + "file_path": "app/profile/page.tsx" + }, + { + "id": "page_search", + "name": "Search", + "path": "/search", + "status": "PENDING", + "file_path": "app/search/page.tsx" + }, + { + "id": "page_genre_browse", + "name": "Browse Genre", + "path": "/genre/:slug", + "status": "PENDING", + "file_path": "app/genre/[slug]/page.tsx" + } + ], + "components": [ + { + "id": "component_audio_player", + "name": "AudioPlayer", + "status": "PENDING", + "file_path": "components/AudioPlayer.tsx" + }, + { + "id": "component_player_controls", + "name": "PlayerControls", + "status": "PENDING", + "file_path": "components/PlayerControls.tsx" + }, + { + "id": "component_song_card", + "name": "SongCard", + "status": "PENDING", + "file_path": "components/SongCard.tsx" + }, + { + "id": "component_album_card", + "name": "AlbumCard", + "status": "PENDING", + "file_path": "components/AlbumCard.tsx" + }, + { + "id": "component_artist_card", + "name": "ArtistCard", + "status": "PENDING", + "file_path": "components/ArtistCard.tsx" + }, + { + "id": "component_playlist_card", + "name": "PlaylistCard", + "status": "PENDING", + "file_path": "components/PlaylistCard.tsx" + }, + { + "id": "component_upload_form", + "name": "UploadForm", + "status": "PENDING", + "file_path": "components/UploadForm.tsx" + }, + { + "id": "component_waveform_display", + "name": "WaveformDisplay", + "status": "PENDING", + "file_path": "components/WaveformDisplay.tsx" + }, + { + "id": "component_genre_badge", + "name": "GenreBadge", + "status": "PENDING", + "file_path": "components/GenreBadge.tsx" + }, + { + "id": "component_track_list", + "name": "TrackList", + "status": "PENDING", + "file_path": "components/TrackList.tsx" + }, + { + "id": "component_artist_header", + "name": "ArtistHeader", + "status": "PENDING", + "file_path": "components/ArtistHeader.tsx" + }, + { + "id": "component_album_header", + "name": "AlbumHeader", + "status": "PENDING", + "file_path": "components/AlbumHeader.tsx" + }, + { + "id": "component_playlist_header", + "name": "PlaylistHeader", + "status": "PENDING", + "file_path": "components/PlaylistHeader.tsx" + }, + { + "id": "component_social_links", + "name": "SocialLinks", + "status": "PENDING", + "file_path": "components/SocialLinks.tsx" + }, + { + "id": "component_auth_form", + "name": "AuthForm", + "status": "PENDING", + "file_path": "components/AuthForm.tsx" + }, + { + "id": "component_search_bar", + "name": "SearchBar", + "status": "PENDING", + "file_path": "components/SearchBar.tsx" + }, + { + "id": "component_search_results", + "name": "SearchResults", + "status": "PENDING", + "file_path": "components/SearchResults.tsx" + }, + { + "id": "component_create_playlist_modal", + "name": "CreatePlaylistModal", + "status": "PENDING", + "file_path": "components/CreatePlaylistModal.tsx" + }, + { + "id": "component_profile_form", + "name": "ProfileForm", + "status": "PENDING", + "file_path": "components/ProfileForm.tsx" + }, + { + "id": "component_avatar_upload", + "name": "AvatarUpload", + "status": "PENDING", + "file_path": "components/AvatarUpload.tsx" + }, + { + "id": "component_section_header", + "name": "SectionHeader", + "status": "PENDING", + "file_path": "components/SectionHeader.tsx" + }, + { + "id": "component_genre_header", + "name": "GenreHeader", + "status": "PENDING", + "file_path": "components/GenreHeader.tsx" + }, + { + "id": "component_header", + "name": "Header", + "status": "IMPLEMENTED", + "file_path": "components/Header.tsx" + }, + { + "id": "component_nav_link", + "name": "NavLink", + "status": "IMPLEMENTED", + "file_path": "components/NavLink.tsx" + }, + { + "id": "component_user_menu", + "name": "UserMenu", + "status": "IMPLEMENTED", + "file_path": "components/UserMenu.tsx" + } + ] + }, + "dependencies": { + "component_to_page": { + "component_auth_form": ["page_login", "page_register", "page_forgot_password"], + "component_song_card": ["page_home", "page_artist_profile", "page_search", "page_genre_browse", "page_album_detail", "page_playlist_detail"], + "component_genre_badge": ["page_home"], + "component_section_header": ["page_home"], + "component_artist_header": ["page_artist_profile"], + "component_album_card": ["page_artist_profile", "page_search"], + "component_social_links": ["page_artist_profile"], + "component_album_header": ["page_album_detail"], + "component_track_list": ["page_album_detail", "page_playlist_detail"], + "component_upload_form": ["page_upload"], + "component_waveform_display": ["page_upload"], + "component_playlist_card": ["page_playlists"], + "component_create_playlist_modal": ["page_playlists"], + "component_playlist_header": ["page_playlist_detail"], + "component_profile_form": ["page_profile"], + "component_avatar_upload": ["page_profile"], + "component_search_bar": ["page_search"], + "component_search_results": ["page_search"], + "component_artist_card": ["page_search"], + "component_genre_header": ["page_genre_browse"] + }, + "api_to_component": { + "api_login": ["component_auth_form"], + "api_register": ["component_auth_form"], + "api_forgot_password": ["component_auth_form"], + "api_upload_song": ["component_upload_form"], + "api_increment_play_count": ["component_audio_player"], + "api_search": ["component_search_bar"], + "api_create_playlist": ["component_create_playlist_modal"], + "api_update_current_user": ["component_profile_form"] + }, + "table_to_api": { + "model_user": ["api_register", "api_login", "api_forgot_password", "api_reset_password", "api_get_current_user", "api_update_current_user"], + "model_artist": ["api_create_artist_profile", "api_get_artist", "api_update_artist", "api_get_artist_songs", "api_get_artist_albums"], + "model_song": ["api_upload_song", "api_get_song", "api_update_song", "api_delete_song", "api_increment_play_count", "api_get_artist_songs", "api_get_trending_songs", "api_get_new_releases", "api_search"], + "model_album": ["api_create_album", "api_get_album", "api_update_album", "api_delete_album", "api_get_artist_albums", "api_search"], + "model_playlist": ["api_create_playlist", "api_get_user_playlists", "api_get_playlist", "api_update_playlist", "api_delete_playlist"], + "model_playlist_song": ["api_add_song_to_playlist", "api_remove_song_from_playlist", "api_reorder_playlist_songs"], + "model_genre": ["api_get_genres", "api_get_songs_by_genre"], + "model_label": ["api_create_label_profile", "api_get_label_artists"] + } + } +} diff --git a/.workflow/versions/v004/tasks/task_create_api_create_album_share.yml b/.workflow/versions/v004/tasks/task_create_api_create_album_share.yml new file mode 100644 index 0000000..252235b --- /dev/null +++ b/.workflow/versions/v004/tasks/task_create_api_create_album_share.yml @@ -0,0 +1,18 @@ +id: task_create_api_create_album_share +type: create +title: Create api_create_album_share +agent: backend +entity_id: api_create_album_share +entity_ids: +- api_create_album_share +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_share +context: + design_version: 1 + workflow_version: v004 + context_snapshot_path: .workflow/versions/v001/contexts/api_create_album_share.yml +created_at: '2025-12-18T18:15:12.921336' diff --git a/.workflow/versions/v004/tasks/task_create_api_create_playlist_share.yml b/.workflow/versions/v004/tasks/task_create_api_create_playlist_share.yml new file mode 100644 index 0000000..4b08278 --- /dev/null +++ b/.workflow/versions/v004/tasks/task_create_api_create_playlist_share.yml @@ -0,0 +1,18 @@ +id: task_create_api_create_playlist_share +type: create +title: Create api_create_playlist_share +agent: backend +entity_id: api_create_playlist_share +entity_ids: +- api_create_playlist_share +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_share +context: + design_version: 1 + workflow_version: v004 + context_snapshot_path: .workflow/versions/v001/contexts/api_create_playlist_share.yml +created_at: '2025-12-18T18:15:12.921631' diff --git a/.workflow/versions/v004/tasks/task_create_api_create_song_share.yml b/.workflow/versions/v004/tasks/task_create_api_create_song_share.yml new file mode 100644 index 0000000..a3c8d65 --- /dev/null +++ b/.workflow/versions/v004/tasks/task_create_api_create_song_share.yml @@ -0,0 +1,18 @@ +id: task_create_api_create_song_share +type: create +title: Create api_create_song_share +agent: backend +entity_id: api_create_song_share +entity_ids: +- api_create_song_share +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_share +context: + design_version: 1 + workflow_version: v004 + context_snapshot_path: .workflow/versions/v001/contexts/api_create_song_share.yml +created_at: '2025-12-18T18:15:12.921931' diff --git a/.workflow/versions/v004/tasks/task_create_api_resolve_share.yml b/.workflow/versions/v004/tasks/task_create_api_resolve_share.yml new file mode 100644 index 0000000..1f51ab8 --- /dev/null +++ b/.workflow/versions/v004/tasks/task_create_api_resolve_share.yml @@ -0,0 +1,18 @@ +id: task_create_api_resolve_share +type: create +title: Create api_resolve_share +agent: backend +entity_id: api_resolve_share +entity_ids: +- api_resolve_share +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_share +context: + design_version: 1 + workflow_version: v004 + context_snapshot_path: .workflow/versions/v001/contexts/api_resolve_share.yml +created_at: '2025-12-18T18:15:12.922226' diff --git a/.workflow/versions/v004/tasks/task_create_api_track_share_click.yml b/.workflow/versions/v004/tasks/task_create_api_track_share_click.yml new file mode 100644 index 0000000..4bfbfba --- /dev/null +++ b/.workflow/versions/v004/tasks/task_create_api_track_share_click.yml @@ -0,0 +1,18 @@ +id: task_create_api_track_share_click +type: create +title: Create api_track_share_click +agent: backend +entity_id: api_track_share_click +entity_ids: +- api_track_share_click +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_model_share +context: + design_version: 1 + workflow_version: v004 + context_snapshot_path: .workflow/versions/v001/contexts/api_track_share_click.yml +created_at: '2025-12-18T18:15:12.922514' diff --git a/.workflow/versions/v004/tasks/task_create_component_share_button.yml b/.workflow/versions/v004/tasks/task_create_component_share_button.yml new file mode 100644 index 0000000..fc31e76 --- /dev/null +++ b/.workflow/versions/v004/tasks/task_create_component_share_button.yml @@ -0,0 +1,21 @@ +id: task_create_component_share_button +type: create +title: Create ShareButton +agent: frontend +entity_id: component_share_button +entity_ids: +- component_share_button +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_api_create_playlist_share +- task_create_component_share_modal +- task_create_api_create_album_share +- task_create_api_create_song_share +context: + design_version: 1 + workflow_version: v004 + context_snapshot_path: .workflow/versions/v001/contexts/component_share_button.yml +created_at: '2025-12-18T18:15:12.923117' diff --git a/.workflow/versions/v004/tasks/task_create_component_share_content_display.yml b/.workflow/versions/v004/tasks/task_create_component_share_content_display.yml new file mode 100644 index 0000000..43c4594 --- /dev/null +++ b/.workflow/versions/v004/tasks/task_create_component_share_content_display.yml @@ -0,0 +1,18 @@ +id: task_create_component_share_content_display +type: create +title: Create SharedContentDisplay +agent: frontend +entity_id: component_share_content_display +entity_ids: +- component_share_content_display +status: pending +layer: 3 +parallel_group: layer_3 +complexity: medium +dependencies: +- task_create_api_track_share_click +context: + design_version: 1 + workflow_version: v004 + context_snapshot_path: .workflow/versions/v001/contexts/component_share_content_display.yml +created_at: '2025-12-18T18:15:12.923441' diff --git a/.workflow/versions/v004/tasks/task_create_component_share_modal.yml b/.workflow/versions/v004/tasks/task_create_component_share_modal.yml new file mode 100644 index 0000000..4bdd91b --- /dev/null +++ b/.workflow/versions/v004/tasks/task_create_component_share_modal.yml @@ -0,0 +1,18 @@ +id: task_create_component_share_modal +type: create +title: Create ShareModal +agent: frontend +entity_id: component_share_modal +entity_ids: +- component_share_modal +status: pending +layer: 2 +parallel_group: layer_2 +complexity: medium +dependencies: +- task_create_component_social_share_buttons +context: + design_version: 1 + workflow_version: v004 + context_snapshot_path: .workflow/versions/v001/contexts/component_share_modal.yml +created_at: '2025-12-18T18:15:12.922814' diff --git a/.workflow/versions/v004/tasks/task_create_component_social_share_buttons.yml b/.workflow/versions/v004/tasks/task_create_component_social_share_buttons.yml new file mode 100644 index 0000000..cf8b38c --- /dev/null +++ b/.workflow/versions/v004/tasks/task_create_component_social_share_buttons.yml @@ -0,0 +1,17 @@ +id: task_create_component_social_share_buttons +type: create +title: Create SocialShareButtons +agent: frontend +entity_id: component_social_share_buttons +entity_ids: +- component_social_share_buttons +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v004 + context_snapshot_path: .workflow/versions/v001/contexts/component_social_share_buttons.yml +created_at: '2025-12-18T18:15:12.920566' diff --git a/.workflow/versions/v004/tasks/task_create_model_share.yml b/.workflow/versions/v004/tasks/task_create_model_share.yml new file mode 100644 index 0000000..239d4c3 --- /dev/null +++ b/.workflow/versions/v004/tasks/task_create_model_share.yml @@ -0,0 +1,17 @@ +id: task_create_model_share +type: create +title: Create Share +agent: backend +entity_id: model_share +entity_ids: +- model_share +status: pending +layer: 1 +parallel_group: layer_1 +complexity: medium +dependencies: [] +context: + design_version: 1 + workflow_version: v004 + context_snapshot_path: .workflow/versions/v001/contexts/model_share.yml +created_at: '2025-12-18T18:15:12.920869' diff --git a/.workflow/versions/v004/tasks/task_create_page_share.yml b/.workflow/versions/v004/tasks/task_create_page_share.yml new file mode 100644 index 0000000..7f3f9a0 --- /dev/null +++ b/.workflow/versions/v004/tasks/task_create_page_share.yml @@ -0,0 +1,20 @@ +id: task_create_page_share +type: create +title: Create SharePage +agent: frontend +entity_id: page_share +entity_ids: +- page_share +status: pending +layer: 4 +parallel_group: layer_4 +complexity: medium +dependencies: +- task_create_component_share_content_display +- task_create_api_track_share_click +- task_create_api_resolve_share +context: + design_version: 1 + workflow_version: v004 + context_snapshot_path: .workflow/versions/v001/contexts/page_share.yml +created_at: '2025-12-18T18:15:12.923753' diff --git a/app/album/[id]/page.tsx b/app/album/[id]/page.tsx new file mode 100644 index 0000000..cd3f6b4 --- /dev/null +++ b/app/album/[id]/page.tsx @@ -0,0 +1,90 @@ +import { AlbumHeader } from '@/components/AlbumHeader' +import { TrackList } from '@/components/TrackList' + +interface PageProps { + params: Promise<{ id: string }> +} + +async function getAlbum(id: string) { + const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/albums/${id}`, { + cache: 'no-store', + }) + if (!res.ok) throw new Error('Album not found') + const data = await res.json() + return data.album +} + +export default async function AlbumPage({ params }: PageProps) { + const { id } = await params + const album = await getAlbum(id) + + // Calculate total duration from songs + const totalDuration = album.songs?.reduce((acc: number, song: any) => acc + (song.duration || 0), 0) || 0 + + // Transform songs for TrackList + const tracks = album.songs?.map((song: any, index: number) => ({ + id: song.id, + title: song.title, + artistName: album.artist?.name || 'Unknown Artist', + duration: song.duration || 0, + plays: song.plays, + position: index + 1 + })) || [] + + return ( +
+ + +
+ {/* Track List */} + {tracks.length > 0 && ( +
+ +
+ )} + + {/* Album Info */} +
+
+

Release Date

+

{new Date(album.releaseDate).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + })}

+
+ + {album.label && ( +
+

Label

+

{album.label.name}

+
+ )} + + {album.songs && album.songs.length > 0 && ( +
+

Total Duration

+

+ {Math.floor(album.songs.reduce((acc: number, song: any) => acc + song.duration, 0) / 60)} minutes +

+
+ )} + +
+

Tracks

+

{album.songs?.length || 0} songs

+
+
+
+
+ ) +} diff --git a/app/api/albums/[id]/route.ts b/app/api/albums/[id]/route.ts new file mode 100644 index 0000000..f80c029 --- /dev/null +++ b/app/api/albums/[id]/route.ts @@ -0,0 +1,152 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireArtist, slugify } from '@/lib/auth' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params + + const album = await prisma.album.findUnique({ + where: { id }, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + songs: { + include: { + genres: { + include: { + genre: true, + }, + }, + }, + orderBy: { + createdAt: 'asc', + }, + }, + }, + }) + + if (!album) { + return NextResponse.json({ error: 'Album not found' }, { status: 404 }) + } + + return NextResponse.json(album) + } catch (error) { + console.error('Error fetching album:', error) + return NextResponse.json({ error: 'Failed to fetch album' }, { status: 500 }) + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { artist } = await requireArtist() + const { id } = await params + + const existingAlbum = await prisma.album.findUnique({ + where: { id }, + }) + + if (!existingAlbum) { + return NextResponse.json({ error: 'Album not found' }, { status: 404 }) + } + + if (existingAlbum.artistId !== artist.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const body = await request.json() + const { title, description, coverUrl, releaseDate, albumType } = body + + const updateData: any = {} + if (title !== undefined) { + updateData.title = title + updateData.slug = slugify(title) + } + if (description !== undefined) updateData.description = description + if (coverUrl !== undefined) updateData.coverUrl = coverUrl + if (releaseDate !== undefined) updateData.releaseDate = releaseDate ? new Date(releaseDate) : null + if (albumType !== undefined) updateData.albumType = albumType + + const updatedAlbum = await prisma.album.update({ + where: { id }, + data: updateData, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + _count: { + select: { + songs: true, + }, + }, + }, + }) + + return NextResponse.json(updatedAlbum) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + if (error instanceof Error && error.message === 'Artist profile required') { + return NextResponse.json({ error: 'Artist profile required' }, { status: 403 }) + } + console.error('Error updating album:', error) + return NextResponse.json({ error: 'Failed to update album' }, { status: 500 }) + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { artist } = await requireArtist() + const { id } = await params + + const existingAlbum = await prisma.album.findUnique({ + where: { id }, + }) + + if (!existingAlbum) { + return NextResponse.json({ error: 'Album not found' }, { status: 404 }) + } + + if (existingAlbum.artistId !== artist.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + await prisma.album.delete({ + where: { id }, + }) + + return NextResponse.json({ message: 'Album deleted successfully' }) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + if (error instanceof Error && error.message === 'Artist profile required') { + return NextResponse.json({ error: 'Artist profile required' }, { status: 403 }) + } + console.error('Error deleting album:', error) + return NextResponse.json({ error: 'Failed to delete album' }, { status: 500 }) + } +} diff --git a/app/api/albums/route.ts b/app/api/albums/route.ts new file mode 100644 index 0000000..6070d3f --- /dev/null +++ b/app/api/albums/route.ts @@ -0,0 +1,57 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireArtist, slugify } from '@/lib/auth' + +export async function POST(request: NextRequest) { + try { + const { artist } = await requireArtist() + + const body = await request.json() + const { title, description, coverUrl, releaseDate, albumType = 'album' } = body + + if (!title) { + return NextResponse.json({ error: 'Title is required' }, { status: 400 }) + } + + const slug = slugify(title) + + const album = await prisma.album.create({ + data: { + artistId: artist.id, + title, + slug, + description, + coverUrl, + releaseDate: releaseDate ? new Date(releaseDate) : null, + albumType, + }, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + _count: { + select: { + songs: true, + }, + }, + }, + }) + + return NextResponse.json(album, { status: 201 }) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + if (error instanceof Error && error.message === 'Artist profile required') { + return NextResponse.json({ error: 'Artist profile required' }, { status: 403 }) + } + console.error('Error creating album:', error) + return NextResponse.json({ error: 'Failed to create album' }, { status: 500 }) + } +} diff --git a/app/api/artists/[id]/albums/route.ts b/app/api/artists/[id]/albums/route.ts new file mode 100644 index 0000000..7529b79 --- /dev/null +++ b/app/api/artists/[id]/albums/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params + + const albums = await prisma.album.findMany({ + where: { + artistId: id, + }, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + _count: { + select: { + songs: true, + }, + }, + }, + orderBy: { + releaseDate: 'desc', + }, + }) + + return NextResponse.json(albums) + } catch (error) { + console.error('Error fetching artist albums:', error) + return NextResponse.json({ error: 'Failed to fetch albums' }, { status: 500 }) + } +} diff --git a/app/api/artists/[id]/invitations/[invitationId]/respond/route.ts b/app/api/artists/[id]/invitations/[invitationId]/respond/route.ts new file mode 100644 index 0000000..cdf39b1 --- /dev/null +++ b/app/api/artists/[id]/invitations/[invitationId]/respond/route.ts @@ -0,0 +1,122 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireAuth } from '@/lib/auth' + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string; invitationId: string }> } +) { + try { + const user = await requireAuth() + const { id, invitationId } = await params + + // Check if user owns this artist + const artist = await prisma.artist.findUnique({ + where: { id }, + select: { userId: true, labelId: true }, + }) + + if (!artist) { + return NextResponse.json({ error: 'Artist not found' }, { status: 404 }) + } + + if (artist.userId !== user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const body = await request.json() + const { response } = body + + if (!response || !['accept', 'decline'].includes(response)) { + return NextResponse.json({ error: 'Response must be "accept" or "decline"' }, { status: 400 }) + } + + // Check if invitation exists and belongs to this artist + const invitation = await prisma.labelInvitation.findUnique({ + where: { id: invitationId }, + select: { artistId: true, status: true, expiresAt: true, labelId: true }, + }) + + if (!invitation) { + return NextResponse.json({ error: 'Invitation not found' }, { status: 404 }) + } + + if (invitation.artistId !== id) { + return NextResponse.json({ error: 'Invitation does not belong to this artist' }, { status: 403 }) + } + + if (invitation.status !== 'pending') { + return NextResponse.json({ error: 'Invitation is no longer pending' }, { status: 400 }) + } + + // Check if invitation has expired + if (new Date() > invitation.expiresAt) { + await prisma.labelInvitation.update({ + where: { id: invitationId }, + data: { status: 'expired' }, + }) + return NextResponse.json({ error: 'Invitation has expired' }, { status: 400 }) + } + + if (response === 'accept') { + // Check if artist already has a label + if (artist.labelId) { + return NextResponse.json({ error: 'Artist already has a label' }, { status: 400 }) + } + + // Update artist's labelId and invitation status in a transaction + await prisma.$transaction([ + prisma.artist.update({ + where: { id }, + data: { labelId: invitation.labelId }, + }), + prisma.labelInvitation.update({ + where: { id: invitationId }, + data: { status: 'accepted' }, + }), + ]) + + const updatedInvitation = await prisma.labelInvitation.findUnique({ + where: { id: invitationId }, + include: { + label: { + select: { + id: true, + name: true, + slug: true, + logoUrl: true, + description: true, + }, + }, + }, + }) + + return NextResponse.json(updatedInvitation) + } else { + // Decline invitation + const updatedInvitation = await prisma.labelInvitation.update({ + where: { id: invitationId }, + data: { status: 'declined' }, + include: { + label: { + select: { + id: true, + name: true, + slug: true, + logoUrl: true, + description: true, + }, + }, + }, + }) + + return NextResponse.json(updatedInvitation) + } + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + console.error('Error responding to invitation:', error) + return NextResponse.json({ error: 'Failed to respond to invitation' }, { status: 500 }) + } +} diff --git a/app/api/artists/[id]/invitations/route.ts b/app/api/artists/[id]/invitations/route.ts new file mode 100644 index 0000000..3575347 --- /dev/null +++ b/app/api/artists/[id]/invitations/route.ts @@ -0,0 +1,53 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireAuth } from '@/lib/auth' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const user = await requireAuth() + const { id } = await params + + // Check if user owns this artist + const artist = await prisma.artist.findUnique({ + where: { id }, + select: { userId: true }, + }) + + if (!artist) { + return NextResponse.json({ error: 'Artist not found' }, { status: 404 }) + } + + if (artist.userId !== user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const invitations = await prisma.labelInvitation.findMany({ + where: { artistId: id }, + include: { + label: { + select: { + id: true, + name: true, + slug: true, + logoUrl: true, + description: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }) + + return NextResponse.json(invitations) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + console.error('Error fetching artist invitations:', error) + return NextResponse.json({ error: 'Failed to fetch artist invitations' }, { status: 500 }) + } +} diff --git a/app/api/artists/[id]/route.ts b/app/api/artists/[id]/route.ts new file mode 100644 index 0000000..47f89ea --- /dev/null +++ b/app/api/artists/[id]/route.ts @@ -0,0 +1,109 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireArtist, slugify } from '@/lib/auth' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params + + const artist = await prisma.artist.findUnique({ + where: { id }, + include: { + user: { + select: { + email: true, + username: true, + displayName: true, + }, + }, + label: { + select: { + id: true, + name: true, + slug: true, + }, + }, + _count: { + select: { + songs: true, + albums: true, + }, + }, + }, + }) + + if (!artist) { + return NextResponse.json({ error: 'Artist not found' }, { status: 404 }) + } + + return NextResponse.json(artist) + } catch (error) { + console.error('Error fetching artist:', error) + return NextResponse.json({ error: 'Failed to fetch artist' }, { status: 500 }) + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { user, artist } = await requireArtist() + const { id } = await params + + if (artist.id !== id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const body = await request.json() + const { name, bio, website, twitter, instagram, spotify, avatarUrl, bannerUrl } = body + + const updateData: any = {} + if (name !== undefined) { + updateData.name = name + updateData.slug = slugify(name) + } + if (bio !== undefined) updateData.bio = bio + if (website !== undefined) updateData.website = website + if (twitter !== undefined) updateData.twitter = twitter + if (instagram !== undefined) updateData.instagram = instagram + if (spotify !== undefined) updateData.spotify = spotify + if (avatarUrl !== undefined) updateData.avatarUrl = avatarUrl + if (bannerUrl !== undefined) updateData.bannerUrl = bannerUrl + + const updatedArtist = await prisma.artist.update({ + where: { id }, + data: updateData, + include: { + user: { + select: { + email: true, + username: true, + displayName: true, + }, + }, + label: { + select: { + id: true, + name: true, + slug: true, + }, + }, + }, + }) + + return NextResponse.json(updatedArtist) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + if (error instanceof Error && error.message === 'Artist profile required') { + return NextResponse.json({ error: 'Artist profile required' }, { status: 403 }) + } + console.error('Error updating artist:', error) + return NextResponse.json({ error: 'Failed to update artist' }, { status: 500 }) + } +} diff --git a/app/api/artists/[id]/songs/route.ts b/app/api/artists/[id]/songs/route.ts new file mode 100644 index 0000000..5ccfd7b --- /dev/null +++ b/app/api/artists/[id]/songs/route.ts @@ -0,0 +1,50 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params + + const songs = await prisma.song.findMany({ + where: { + artistId: id, + isPublic: true, + }, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + album: { + select: { + id: true, + title: true, + slug: true, + coverUrl: true, + }, + }, + genres: { + include: { + genre: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }) + + return NextResponse.json(songs) + } catch (error) { + console.error('Error fetching artist songs:', error) + return NextResponse.json({ error: 'Failed to fetch songs' }, { status: 500 }) + } +} diff --git a/app/api/artists/route.ts b/app/api/artists/route.ts new file mode 100644 index 0000000..9b14ea7 --- /dev/null +++ b/app/api/artists/route.ts @@ -0,0 +1,81 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { getCurrentUser, slugify } from '@/lib/auth' + +export async function POST(request: NextRequest) { + try { + const currentUser = await getCurrentUser() + + if (!currentUser) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + + if (currentUser.artist) { + return NextResponse.json( + { error: 'Artist profile already exists' }, + { status: 409 } + ) + } + + const body = await request.json() + const { name, bio, website, twitter, instagram, spotify } = body + + if (!name) { + return NextResponse.json( + { error: 'Artist name is required' }, + { status: 400 } + ) + } + + const slug = slugify(name) + + const existingSlug = await prisma.artist.findUnique({ + where: { slug }, + }) + + if (existingSlug) { + return NextResponse.json( + { error: 'An artist with this name already exists' }, + { status: 409 } + ) + } + + const artist = await prisma.artist.create({ + data: { + userId: currentUser.id, + name, + slug, + bio, + website, + twitter, + instagram, + spotify, + }, + include: { + user: { + select: { + displayName: true, + avatarUrl: true, + }, + }, + }, + }) + + // Update user role to artist + await prisma.user.update({ + where: { id: currentUser.id }, + data: { role: 'artist' }, + }) + + return NextResponse.json({ artist }, { status: 201 }) + } catch (error) { + console.error('Create artist error:', error) + return NextResponse.json( + { error: 'Failed to create artist profile' }, + { status: 500 } + ) + } +} diff --git a/app/api/auth/forgot-password/route.ts b/app/api/auth/forgot-password/route.ts new file mode 100644 index 0000000..5987317 --- /dev/null +++ b/app/api/auth/forgot-password/route.ts @@ -0,0 +1,55 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { generateResetToken } from '@/lib/auth' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { email } = body + + if (!email) { + return NextResponse.json( + { error: 'Email is required' }, + { status: 400 } + ) + } + + const user = await prisma.user.findUnique({ + where: { email }, + }) + + // Always return success to prevent email enumeration + if (!user) { + return NextResponse.json({ + message: 'If an account exists with this email, a reset link has been sent', + }) + } + + const resetToken = generateResetToken() + const resetExpires = new Date(Date.now() + 60 * 60 * 1000) // 1 hour + + await prisma.user.update({ + where: { id: user.id }, + data: { + resetToken, + resetExpires, + }, + }) + + // In production, send email with reset link + // For development, log the token + console.log(`Password reset token for ${email}: ${resetToken}`) + + return NextResponse.json({ + message: 'If an account exists with this email, a reset link has been sent', + // Only include token in development for testing + ...(process.env.NODE_ENV === 'development' && { resetToken }), + }) + } catch (error) { + console.error('Forgot password error:', error) + return NextResponse.json( + { error: 'Failed to process request' }, + { status: 500 } + ) + } +} diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts new file mode 100644 index 0000000..155d8df --- /dev/null +++ b/app/api/auth/login/route.ts @@ -0,0 +1,89 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { verifyPassword, generateToken } from '@/lib/auth' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { email, password } = body + + if (!email || !password) { + return NextResponse.json( + { error: 'Email and password are required' }, + { status: 400 } + ) + } + + const user = await prisma.user.findUnique({ + where: { email }, + select: { + id: true, + email: true, + username: true, + displayName: true, + avatarUrl: true, + role: true, + passwordHash: true, + createdAt: true, + artist: { + select: { + id: true, + name: true, + slug: true, + verified: true, + }, + }, + label: { + select: { + id: true, + name: true, + slug: true, + }, + }, + }, + }) + + if (!user) { + return NextResponse.json( + { error: 'Invalid email or password' }, + { status: 401 } + ) + } + + const isValid = await verifyPassword(password, user.passwordHash) + if (!isValid) { + return NextResponse.json( + { error: 'Invalid email or password' }, + { status: 401 } + ) + } + + const token = generateToken({ + userId: user.id, + email: user.email, + role: user.role, + }) + + const { passwordHash: _, ...userWithoutPassword } = user + + const response = NextResponse.json( + { user: userWithoutPassword, token }, + { status: 200 } + ) + response.cookies.set('auth-token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 7, // 7 days + path: '/', + }) + + return response + } catch (error) { + console.error('Login error:', error) + return NextResponse.json( + { error: 'Login failed' }, + { status: 500 } + ) + } +} diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts new file mode 100644 index 0000000..21498ad --- /dev/null +++ b/app/api/auth/register/route.ts @@ -0,0 +1,79 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { hashPassword, generateToken, slugify } from '@/lib/auth' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { email, password, username, displayName } = body + + if (!email || !password || !username) { + return NextResponse.json( + { error: 'Email, password, and username are required' }, + { status: 400 } + ) + } + + if (password.length < 8) { + return NextResponse.json( + { error: 'Password must be at least 8 characters' }, + { status: 400 } + ) + } + + const existingUser = await prisma.user.findFirst({ + where: { + OR: [{ email }, { username }], + }, + }) + + if (existingUser) { + return NextResponse.json( + { error: 'User with this email or username already exists' }, + { status: 409 } + ) + } + + const passwordHash = await hashPassword(password) + + const user = await prisma.user.create({ + data: { + email, + passwordHash, + username: slugify(username), + displayName: displayName || username, + }, + select: { + id: true, + email: true, + username: true, + displayName: true, + role: true, + createdAt: true, + }, + }) + + const token = generateToken({ + userId: user.id, + email: user.email, + role: user.role, + }) + + const response = NextResponse.json({ user, token }, { status: 201 }) + response.cookies.set('auth-token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 7, // 7 days + path: '/', + }) + + return response + } catch (error) { + console.error('Registration error:', error) + return NextResponse.json( + { error: 'Registration failed' }, + { status: 500 } + ) + } +} diff --git a/app/api/auth/reset-password/route.ts b/app/api/auth/reset-password/route.ts new file mode 100644 index 0000000..4ba4ce5 --- /dev/null +++ b/app/api/auth/reset-password/route.ts @@ -0,0 +1,61 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { hashPassword } from '@/lib/auth' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { token, password } = body + + if (!token || !password) { + return NextResponse.json( + { error: 'Token and password are required' }, + { status: 400 } + ) + } + + if (password.length < 8) { + return NextResponse.json( + { error: 'Password must be at least 8 characters' }, + { status: 400 } + ) + } + + const user = await prisma.user.findFirst({ + where: { + resetToken: token, + resetExpires: { + gt: new Date(), + }, + }, + }) + + if (!user) { + return NextResponse.json( + { error: 'Invalid or expired reset token' }, + { status: 400 } + ) + } + + const passwordHash = await hashPassword(password) + + await prisma.user.update({ + where: { id: user.id }, + data: { + passwordHash, + resetToken: null, + resetExpires: null, + }, + }) + + return NextResponse.json({ + message: 'Password has been reset successfully', + }) + } catch (error) { + console.error('Reset password error:', error) + return NextResponse.json( + { error: 'Failed to reset password' }, + { status: 500 } + ) + } +} diff --git a/app/api/discover/genres/[slug]/route.ts b/app/api/discover/genres/[slug]/route.ts new file mode 100644 index 0000000..9d56584 --- /dev/null +++ b/app/api/discover/genres/[slug]/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ slug: string }> } +) { + try { + const { slug } = await params + const { searchParams } = new URL(request.url) + const limit = parseInt(searchParams.get('limit') || '50') + + const genre = await prisma.genre.findUnique({ + where: { slug }, + }) + + if (!genre) { + return NextResponse.json({ error: 'Genre not found' }, { status: 404 }) + } + + const songGenres = await prisma.songGenre.findMany({ + where: { + genreId: genre.id, + song: { + isPublic: true, + }, + }, + include: { + song: { + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + album: { + select: { + id: true, + title: true, + slug: true, + coverUrl: true, + }, + }, + genres: { + include: { + genre: true, + }, + }, + }, + }, + }, + take: limit, + orderBy: { + song: { + createdAt: 'desc', + }, + }, + }) + + const songs = songGenres.map((sg) => sg.song) + + return NextResponse.json({ + genre, + songs, + }) + } catch (error) { + console.error('Error fetching songs by genre:', error) + return NextResponse.json({ error: 'Failed to fetch songs by genre' }, { status: 500 }) + } +} diff --git a/app/api/discover/genres/route.ts b/app/api/discover/genres/route.ts new file mode 100644 index 0000000..ad5b433 --- /dev/null +++ b/app/api/discover/genres/route.ts @@ -0,0 +1,24 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +export async function GET(request: NextRequest) { + try { + const genres = await prisma.genre.findMany({ + include: { + _count: { + select: { + songs: true, + }, + }, + }, + orderBy: { + name: 'asc', + }, + }) + + return NextResponse.json(genres) + } catch (error) { + console.error('Error fetching genres:', error) + return NextResponse.json({ error: 'Failed to fetch genres' }, { status: 500 }) + } +} diff --git a/app/api/discover/new-releases/route.ts b/app/api/discover/new-releases/route.ts new file mode 100644 index 0000000..25bedb0 --- /dev/null +++ b/app/api/discover/new-releases/route.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const limit = parseInt(searchParams.get('limit') || '20') + + const songs = await prisma.song.findMany({ + where: { + isPublic: true, + }, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + album: { + select: { + id: true, + title: true, + slug: true, + coverUrl: true, + }, + }, + genres: { + include: { + genre: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + take: limit, + }) + + return NextResponse.json(songs) + } catch (error) { + console.error('Error fetching new releases:', error) + return NextResponse.json({ error: 'Failed to fetch new releases' }, { status: 500 }) + } +} diff --git a/app/api/discover/trending/route.ts b/app/api/discover/trending/route.ts new file mode 100644 index 0000000..2c12446 --- /dev/null +++ b/app/api/discover/trending/route.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const limit = parseInt(searchParams.get('limit') || '20') + + const songs = await prisma.song.findMany({ + where: { + isPublic: true, + }, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + album: { + select: { + id: true, + title: true, + slug: true, + coverUrl: true, + }, + }, + genres: { + include: { + genre: true, + }, + }, + }, + orderBy: { + playCount: 'desc', + }, + take: limit, + }) + + return NextResponse.json(songs) + } catch (error) { + console.error('Error fetching trending songs:', error) + return NextResponse.json({ error: 'Failed to fetch trending songs' }, { status: 500 }) + } +} diff --git a/app/api/labels/[id]/artists/[artistId]/route.ts b/app/api/labels/[id]/artists/[artistId]/route.ts new file mode 100644 index 0000000..a2a2fe2 --- /dev/null +++ b/app/api/labels/[id]/artists/[artistId]/route.ts @@ -0,0 +1,55 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireAuth } from '@/lib/auth' + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string; artistId: string }> } +) { + try { + const user = await requireAuth() + const { id, artistId } = await params + + // Check if user owns this label + const label = await prisma.label.findUnique({ + where: { id }, + select: { userId: true }, + }) + + if (!label) { + return NextResponse.json({ error: 'Label not found' }, { status: 404 }) + } + + if (label.userId !== user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + // Check if artist exists and belongs to this label + const artist = await prisma.artist.findUnique({ + where: { id: artistId }, + select: { labelId: true }, + }) + + if (!artist) { + return NextResponse.json({ error: 'Artist not found' }, { status: 404 }) + } + + if (artist.labelId !== id) { + return NextResponse.json({ error: 'Artist does not belong to this label' }, { status: 400 }) + } + + // Remove artist from label + await prisma.artist.update({ + where: { id: artistId }, + data: { labelId: null }, + }) + + return NextResponse.json({ message: 'Artist removed from label successfully' }) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + console.error('Error removing artist from label:', error) + return NextResponse.json({ error: 'Failed to remove artist from label' }, { status: 500 }) + } +} diff --git a/app/api/labels/[id]/artists/route.ts b/app/api/labels/[id]/artists/route.ts new file mode 100644 index 0000000..306a727 --- /dev/null +++ b/app/api/labels/[id]/artists/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params + + const artists = await prisma.artist.findMany({ + where: { + labelId: id, + }, + include: { + user: { + select: { + email: true, + username: true, + displayName: true, + }, + }, + _count: { + select: { + songs: true, + albums: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }) + + return NextResponse.json(artists) + } catch (error) { + console.error('Error fetching label artists:', error) + return NextResponse.json({ error: 'Failed to fetch label artists' }, { status: 500 }) + } +} diff --git a/app/api/labels/[id]/invitations/[invitationId]/route.ts b/app/api/labels/[id]/invitations/[invitationId]/route.ts new file mode 100644 index 0000000..e79b796 --- /dev/null +++ b/app/api/labels/[id]/invitations/[invitationId]/route.ts @@ -0,0 +1,58 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireAuth } from '@/lib/auth' + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string; invitationId: string }> } +) { + try { + const user = await requireAuth() + const { id, invitationId } = await params + + // Check if user owns this label + const label = await prisma.label.findUnique({ + where: { id }, + select: { userId: true }, + }) + + if (!label) { + return NextResponse.json({ error: 'Label not found' }, { status: 404 }) + } + + if (label.userId !== user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + // Check if invitation exists and belongs to this label + const invitation = await prisma.labelInvitation.findUnique({ + where: { id: invitationId }, + select: { labelId: true, status: true }, + }) + + if (!invitation) { + return NextResponse.json({ error: 'Invitation not found' }, { status: 404 }) + } + + if (invitation.labelId !== id) { + return NextResponse.json({ error: 'Invitation does not belong to this label' }, { status: 403 }) + } + + if (invitation.status !== 'pending') { + return NextResponse.json({ error: 'Can only cancel pending invitations' }, { status: 400 }) + } + + // Delete the invitation + await prisma.labelInvitation.delete({ + where: { id: invitationId }, + }) + + return NextResponse.json({ message: 'Invitation cancelled successfully' }) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + console.error('Error cancelling invitation:', error) + return NextResponse.json({ error: 'Failed to cancel invitation' }, { status: 500 }) + } +} diff --git a/app/api/labels/[id]/invitations/route.ts b/app/api/labels/[id]/invitations/route.ts new file mode 100644 index 0000000..ffd16f1 --- /dev/null +++ b/app/api/labels/[id]/invitations/route.ts @@ -0,0 +1,143 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireAuth } from '@/lib/auth' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const user = await requireAuth() + const { id } = await params + + // Check if user owns this label + const label = await prisma.label.findUnique({ + where: { id }, + select: { userId: true }, + }) + + if (!label) { + return NextResponse.json({ error: 'Label not found' }, { status: 404 }) + } + + if (label.userId !== user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const invitations = await prisma.labelInvitation.findMany({ + where: { labelId: id }, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }) + + return NextResponse.json(invitations) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + console.error('Error fetching label invitations:', error) + return NextResponse.json({ error: 'Failed to fetch label invitations' }, { status: 500 }) + } +} + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const user = await requireAuth() + const { id } = await params + + // Check if user owns this label + const label = await prisma.label.findUnique({ + where: { id }, + select: { userId: true }, + }) + + if (!label) { + return NextResponse.json({ error: 'Label not found' }, { status: 404 }) + } + + if (label.userId !== user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const body = await request.json() + const { artistId, message } = body + + if (!artistId) { + return NextResponse.json({ error: 'Artist ID is required' }, { status: 400 }) + } + + // Check if artist exists + const artist = await prisma.artist.findUnique({ + where: { id: artistId }, + select: { id: true, labelId: true }, + }) + + if (!artist) { + return NextResponse.json({ error: 'Artist not found' }, { status: 404 }) + } + + // Check if artist already has a label + if (artist.labelId) { + return NextResponse.json({ error: 'Artist already has a label' }, { status: 400 }) + } + + // Check if pending invitation already exists + const existingInvitation = await prisma.labelInvitation.findUnique({ + where: { + labelId_artistId: { + labelId: id, + artistId: artistId, + }, + }, + }) + + if (existingInvitation && existingInvitation.status === 'pending') { + return NextResponse.json({ error: 'Pending invitation already exists' }, { status: 400 }) + } + + // Create invitation with 30 days expiration + const expiresAt = new Date() + expiresAt.setDate(expiresAt.getDate() + 30) + + const invitation = await prisma.labelInvitation.create({ + data: { + labelId: id, + artistId: artistId, + message, + expiresAt, + }, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + }, + }, + }, + }) + + return NextResponse.json(invitation, { status: 201 }) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + console.error('Error creating invitation:', error) + return NextResponse.json({ error: 'Failed to create invitation' }, { status: 500 }) + } +} diff --git a/app/api/labels/[id]/route.ts b/app/api/labels/[id]/route.ts new file mode 100644 index 0000000..cb7bba1 --- /dev/null +++ b/app/api/labels/[id]/route.ts @@ -0,0 +1,113 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireAuth, slugify } from '@/lib/auth' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params + + const label = await prisma.label.findUnique({ + where: { id }, + include: { + user: { + select: { + email: true, + username: true, + displayName: true, + }, + }, + artists: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + _count: { + select: { + artists: true, + invitations: true, + }, + }, + }, + }) + + if (!label) { + return NextResponse.json({ error: 'Label not found' }, { status: 404 }) + } + + return NextResponse.json(label) + } catch (error) { + console.error('Error fetching label:', error) + return NextResponse.json({ error: 'Failed to fetch label' }, { status: 500 }) + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const user = await requireAuth() + const { id } = await params + + // Check if user owns this label + const label = await prisma.label.findUnique({ + where: { id }, + select: { userId: true }, + }) + + if (!label) { + return NextResponse.json({ error: 'Label not found' }, { status: 404 }) + } + + if (label.userId !== user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const body = await request.json() + const { name, description, logoUrl, website } = body + + const updateData: any = {} + if (name !== undefined) { + updateData.name = name + updateData.slug = slugify(name) + } + if (description !== undefined) updateData.description = description + if (logoUrl !== undefined) updateData.logoUrl = logoUrl + if (website !== undefined) updateData.website = website + + const updatedLabel = await prisma.label.update({ + where: { id }, + data: updateData, + include: { + user: { + select: { + email: true, + username: true, + displayName: true, + }, + }, + _count: { + select: { + artists: true, + invitations: true, + }, + }, + }, + }) + + return NextResponse.json(updatedLabel) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + console.error('Error updating label:', error) + return NextResponse.json({ error: 'Failed to update label' }, { status: 500 }) + } +} diff --git a/app/api/labels/[id]/stats/route.ts b/app/api/labels/[id]/stats/route.ts new file mode 100644 index 0000000..7ac3318 --- /dev/null +++ b/app/api/labels/[id]/stats/route.ts @@ -0,0 +1,61 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params + + // Verify label exists + const label = await prisma.label.findUnique({ + where: { id }, + select: { id: true }, + }) + + if (!label) { + return NextResponse.json({ error: 'Label not found' }, { status: 404 }) + } + + // Get artist count + const artistCount = await prisma.artist.count({ + where: { labelId: id }, + }) + + // Get artists' song and album counts and total plays + const artists = await prisma.artist.findMany({ + where: { labelId: id }, + select: { id: true }, + }) + + const artistIds = artists.map(a => a.id) + + const songCount = await prisma.song.count({ + where: { artistId: { in: artistIds } }, + }) + + const albumCount = await prisma.album.count({ + where: { artistId: { in: artistIds } }, + }) + + const songs = await prisma.song.findMany({ + where: { artistId: { in: artistIds } }, + select: { playCount: true }, + }) + + const totalPlays = songs.reduce((sum, song) => sum + song.playCount, 0) + + const stats = { + artistCount, + songCount, + albumCount, + totalPlays, + } + + return NextResponse.json(stats) + } catch (error) { + console.error('Error fetching label stats:', error) + return NextResponse.json({ error: 'Failed to fetch label stats' }, { status: 500 }) + } +} diff --git a/app/api/labels/route.ts b/app/api/labels/route.ts new file mode 100644 index 0000000..1f551e9 --- /dev/null +++ b/app/api/labels/route.ts @@ -0,0 +1,66 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireAuth, slugify } from '@/lib/auth' + +export async function POST(request: NextRequest) { + try { + const user = await requireAuth() + + // Check if user already has a label + const existingLabel = await prisma.label.findUnique({ + where: { userId: user.id }, + }) + + if (existingLabel) { + return NextResponse.json({ error: 'User already has a label profile' }, { status: 400 }) + } + + const body = await request.json() + const { name, description, logoUrl, website } = body + + if (!name) { + return NextResponse.json({ error: 'Name is required' }, { status: 400 }) + } + + const slug = slugify(name) + + const label = await prisma.label.create({ + data: { + userId: user.id, + name, + slug, + description, + logoUrl, + website, + }, + include: { + user: { + select: { + email: true, + username: true, + displayName: true, + }, + }, + _count: { + select: { + artists: true, + }, + }, + }, + }) + + // Update user role to label + await prisma.user.update({ + where: { id: user.id }, + data: { role: 'label' }, + }) + + return NextResponse.json(label, { status: 201 }) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + console.error('Error creating label:', error) + return NextResponse.json({ error: 'Failed to create label' }, { status: 500 }) + } +} diff --git a/app/api/playlists/[id]/reorder/route.ts b/app/api/playlists/[id]/reorder/route.ts new file mode 100644 index 0000000..18838e1 --- /dev/null +++ b/app/api/playlists/[id]/reorder/route.ts @@ -0,0 +1,91 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireAuth } from '@/lib/auth' + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const user = await requireAuth() + const { id } = await params + + const playlist = await prisma.playlist.findUnique({ + where: { id }, + }) + + if (!playlist) { + return NextResponse.json({ error: 'Playlist not found' }, { status: 404 }) + } + + if (playlist.userId !== user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const body = await request.json() + const { songIds } = body + + if (!Array.isArray(songIds) || songIds.length === 0) { + return NextResponse.json({ error: 'songIds array is required' }, { status: 400 }) + } + + // Update positions in a transaction + await prisma.$transaction( + songIds.map((songId: string, index: number) => + prisma.playlistSong.update({ + where: { + playlistId_songId: { + playlistId: id, + songId, + }, + }, + data: { + position: index, + }, + }) + ) + ) + + const updatedPlaylist = await prisma.playlist.findUnique({ + where: { id }, + include: { + songs: { + include: { + song: { + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + album: { + select: { + id: true, + title: true, + slug: true, + coverUrl: true, + }, + }, + }, + }, + }, + orderBy: { + position: 'asc', + }, + }, + }, + }) + + return NextResponse.json(updatedPlaylist) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + console.error('Error reordering playlist:', error) + return NextResponse.json({ error: 'Failed to reorder playlist' }, { status: 500 }) + } +} diff --git a/app/api/playlists/[id]/route.ts b/app/api/playlists/[id]/route.ts new file mode 100644 index 0000000..af50171 --- /dev/null +++ b/app/api/playlists/[id]/route.ts @@ -0,0 +1,162 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireAuth, slugify, getCurrentUser } from '@/lib/auth' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params + const user = await getCurrentUser() + + const playlist = await prisma.playlist.findUnique({ + where: { id }, + include: { + user: { + select: { + id: true, + username: true, + displayName: true, + avatarUrl: true, + }, + }, + songs: { + include: { + song: { + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + album: { + select: { + id: true, + title: true, + slug: true, + coverUrl: true, + }, + }, + genres: { + include: { + genre: true, + }, + }, + }, + }, + }, + orderBy: { + position: 'asc', + }, + }, + }, + }) + + if (!playlist) { + return NextResponse.json({ error: 'Playlist not found' }, { status: 404 }) + } + + // Only allow private playlists to be viewed by the owner + if (!playlist.isPublic && (!user || user.id !== playlist.userId)) { + return NextResponse.json({ error: 'Playlist not found' }, { status: 404 }) + } + + return NextResponse.json(playlist) + } catch (error) { + console.error('Error fetching playlist:', error) + return NextResponse.json({ error: 'Failed to fetch playlist' }, { status: 500 }) + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const user = await requireAuth() + const { id } = await params + + const existingPlaylist = await prisma.playlist.findUnique({ + where: { id }, + }) + + if (!existingPlaylist) { + return NextResponse.json({ error: 'Playlist not found' }, { status: 404 }) + } + + if (existingPlaylist.userId !== user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const body = await request.json() + const { title, description, coverUrl, isPublic } = body + + const updateData: any = {} + if (title !== undefined) { + updateData.title = title + updateData.slug = slugify(title) + } + if (description !== undefined) updateData.description = description + if (coverUrl !== undefined) updateData.coverUrl = coverUrl + if (isPublic !== undefined) updateData.isPublic = isPublic + + const updatedPlaylist = await prisma.playlist.update({ + where: { id }, + data: updateData, + include: { + _count: { + select: { + songs: true, + }, + }, + }, + }) + + return NextResponse.json(updatedPlaylist) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + console.error('Error updating playlist:', error) + return NextResponse.json({ error: 'Failed to update playlist' }, { status: 500 }) + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const user = await requireAuth() + const { id } = await params + + const existingPlaylist = await prisma.playlist.findUnique({ + where: { id }, + }) + + if (!existingPlaylist) { + return NextResponse.json({ error: 'Playlist not found' }, { status: 404 }) + } + + if (existingPlaylist.userId !== user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + await prisma.playlist.delete({ + where: { id }, + }) + + return NextResponse.json({ message: 'Playlist deleted successfully' }) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + console.error('Error deleting playlist:', error) + return NextResponse.json({ error: 'Failed to delete playlist' }, { status: 500 }) + } +} diff --git a/app/api/playlists/[id]/songs/[songId]/route.ts b/app/api/playlists/[id]/songs/[songId]/route.ts new file mode 100644 index 0000000..0ed177d --- /dev/null +++ b/app/api/playlists/[id]/songs/[songId]/route.ts @@ -0,0 +1,55 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireAuth } from '@/lib/auth' + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string; songId: string }> } +) { + try { + const user = await requireAuth() + const { id, songId } = await params + + const playlist = await prisma.playlist.findUnique({ + where: { id }, + }) + + if (!playlist) { + return NextResponse.json({ error: 'Playlist not found' }, { status: 404 }) + } + + if (playlist.userId !== user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const playlistSong = await prisma.playlistSong.findUnique({ + where: { + playlistId_songId: { + playlistId: id, + songId, + }, + }, + }) + + if (!playlistSong) { + return NextResponse.json({ error: 'Song not in playlist' }, { status: 404 }) + } + + await prisma.playlistSong.delete({ + where: { + playlistId_songId: { + playlistId: id, + songId, + }, + }, + }) + + return NextResponse.json({ message: 'Song removed from playlist successfully' }) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + console.error('Error removing song from playlist:', error) + return NextResponse.json({ error: 'Failed to remove song from playlist' }, { status: 500 }) + } +} diff --git a/app/api/playlists/[id]/songs/route.ts b/app/api/playlists/[id]/songs/route.ts new file mode 100644 index 0000000..43256f9 --- /dev/null +++ b/app/api/playlists/[id]/songs/route.ts @@ -0,0 +1,110 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireAuth } from '@/lib/auth' + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const user = await requireAuth() + const { id } = await params + + const playlist = await prisma.playlist.findUnique({ + where: { id }, + include: { + songs: { + orderBy: { + position: 'desc', + }, + take: 1, + }, + }, + }) + + if (!playlist) { + return NextResponse.json({ error: 'Playlist not found' }, { status: 404 }) + } + + if (playlist.userId !== user.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const body = await request.json() + const { songId } = body + + if (!songId) { + return NextResponse.json({ error: 'songId is required' }, { status: 400 }) + } + + // Check if song exists + const song = await prisma.song.findUnique({ + where: { id: songId }, + }) + + if (!song) { + return NextResponse.json({ error: 'Song not found' }, { status: 404 }) + } + + // Check if song is already in playlist + const existingPlaylistSong = await prisma.playlistSong.findUnique({ + where: { + playlistId_songId: { + playlistId: id, + songId, + }, + }, + }) + + if (existingPlaylistSong) { + return NextResponse.json({ error: 'Song already in playlist' }, { status: 400 }) + } + + // Get the next position + const nextPosition = playlist.songs.length > 0 ? playlist.songs[0].position + 1 : 0 + + const playlistSong = await prisma.playlistSong.create({ + data: { + playlistId: id, + songId, + position: nextPosition, + }, + include: { + song: { + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + album: { + select: { + id: true, + title: true, + slug: true, + coverUrl: true, + }, + }, + genres: { + include: { + genre: true, + }, + }, + }, + }, + }, + }) + + return NextResponse.json(playlistSong, { status: 201 }) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + console.error('Error adding song to playlist:', error) + return NextResponse.json({ error: 'Failed to add song to playlist' }, { status: 500 }) + } +} diff --git a/app/api/playlists/route.ts b/app/api/playlists/route.ts new file mode 100644 index 0000000..0c756b9 --- /dev/null +++ b/app/api/playlists/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireAuth, slugify } from '@/lib/auth' + +export async function GET(request: NextRequest) { + try { + const user = await requireAuth() + + const playlists = await prisma.playlist.findMany({ + where: { + userId: user.id, + }, + include: { + _count: { + select: { + songs: true, + }, + }, + }, + orderBy: { + updatedAt: 'desc', + }, + }) + + return NextResponse.json(playlists) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + console.error('Error fetching playlists:', error) + return NextResponse.json({ error: 'Failed to fetch playlists' }, { status: 500 }) + } +} + +export async function POST(request: NextRequest) { + try { + const user = await requireAuth() + + const body = await request.json() + const { title, description, coverUrl, isPublic = true } = body + + if (!title) { + return NextResponse.json({ error: 'Title is required' }, { status: 400 }) + } + + const slug = slugify(title) + + const playlist = await prisma.playlist.create({ + data: { + userId: user.id, + title, + slug, + description, + coverUrl, + isPublic, + }, + include: { + _count: { + select: { + songs: true, + }, + }, + }, + }) + + return NextResponse.json(playlist, { status: 201 }) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + console.error('Error creating playlist:', error) + return NextResponse.json({ error: 'Failed to create playlist' }, { status: 500 }) + } +} diff --git a/app/api/search/route.ts b/app/api/search/route.ts new file mode 100644 index 0000000..4f81d70 --- /dev/null +++ b/app/api/search/route.ts @@ -0,0 +1,129 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const query = searchParams.get('q') + const limit = parseInt(searchParams.get('limit') || '20') + + if (!query || query.trim().length === 0) { + return NextResponse.json({ error: 'Search query is required' }, { status: 400 }) + } + + const lowerQuery = query.toLowerCase() + + // Search songs - SQLite uses LIKE for case-insensitive matching with LOWER() + const songs = await prisma.song.findMany({ + where: { + isPublic: true, + OR: [ + { + title: { + contains: lowerQuery, + }, + }, + { + description: { + contains: lowerQuery, + }, + }, + ], + }, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + verified: true, + }, + }, + album: { + select: { + id: true, + title: true, + slug: true, + coverUrl: true, + }, + }, + genres: { + include: { + genre: true, + }, + }, + }, + take: limit, + }) + + // Search artists + const artists = await prisma.artist.findMany({ + where: { + OR: [ + { + name: { + contains: lowerQuery, + }, + }, + { + bio: { + contains: lowerQuery, + }, + }, + ], + }, + include: { + _count: { + select: { + songs: true, + albums: true, + }, + }, + }, + take: limit, + }) + + // Search albums + const albums = await prisma.album.findMany({ + where: { + OR: [ + { + title: { + contains: lowerQuery, + }, + }, + { + description: { + contains: lowerQuery, + }, + }, + ], + }, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + verified: true, + }, + }, + _count: { + select: { + songs: true, + }, + }, + }, + take: limit, + }) + + return NextResponse.json({ + songs, + artists, + albums, + }) + } catch (error) { + console.error('Error searching:', error) + return NextResponse.json({ error: 'Failed to perform search' }, { status: 500 }) + } +} diff --git a/app/api/share/[token]/click/route.ts b/app/api/share/[token]/click/route.ts new file mode 100644 index 0000000..52e699b --- /dev/null +++ b/app/api/share/[token]/click/route.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import type { TrackShareClickResponse, ApiError } from '@/types/api-types'; + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ token: string }> } +) { + try { + const { token } = await params; + + const share = await prisma.share.update({ + where: { token }, + data: { clickCount: { increment: 1 } }, + }); + + const response: TrackShareClickResponse = { + success: true, + clickCount: share.clickCount, + }; + + return NextResponse.json(response); + } catch (error) { + return NextResponse.json({ error: 'Share not found' }, { status: 404 }); + } +} diff --git a/app/api/share/[token]/route.ts b/app/api/share/[token]/route.ts new file mode 100644 index 0000000..0386fb5 --- /dev/null +++ b/app/api/share/[token]/route.ts @@ -0,0 +1,104 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import type { ResolveShareResponse, SongShareContent, PlaylistShareContent, AlbumShareContent, ApiError, ShareType } from '@/types/api-types'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ token: string }> } +) { + try { + const { token } = await params; + + const share = await prisma.share.findUnique({ + where: { token }, + }); + + if (!share) { + return NextResponse.json( + { error: 'Share not found' }, + { status: 404 } + ); + } + + let content: SongShareContent | PlaylistShareContent | AlbumShareContent; + + if (share.type === 'SONG') { + const song = await prisma.song.findUnique({ + where: { id: share.targetId }, + include: { artist: true }, + }); + if (!song) { + return NextResponse.json({ error: 'Content not found' }, { status: 404 }); + } + content = { + id: song.id, + title: song.title, + duration: song.duration, + coverArtUrl: song.coverUrl || '', + fileUrl: song.audioUrl, + artist: { id: song.artist.id, stage_name: song.artist.name }, + }; + } else if (share.type === 'PLAYLIST') { + const playlist = await prisma.playlist.findUnique({ + where: { id: share.targetId }, + include: { + user: true, + songs: { include: { song: { include: { artist: true } } } } + }, + }); + if (!playlist) { + return NextResponse.json({ error: 'Content not found' }, { status: 404 }); + } + content = { + id: playlist.id, + name: playlist.title, + description: playlist.description || '', + coverImageUrl: playlist.coverUrl || '', + songCount: playlist.songs.length, + curator: { id: playlist.user.id, name: playlist.user.displayName || playlist.user.username }, + songs: playlist.songs.map(ps => ({ + id: ps.song.id, + title: ps.song.title, + duration: ps.song.duration, + cover_art_url: ps.song.coverUrl || '', + play_count: ps.song.playCount, + })), + }; + } else { + const album = await prisma.album.findUnique({ + where: { id: share.targetId }, + include: { artist: true, songs: true }, + }); + if (!album) { + return NextResponse.json({ error: 'Content not found' }, { status: 404 }); + } + content = { + id: album.id, + title: album.title, + description: album.description || '', + coverArtUrl: album.coverUrl || '', + releaseDate: album.releaseDate?.toISOString() || '', + artist: { id: album.artist.id, stage_name: album.artist.name }, + songs: album.songs.map(s => ({ + id: s.id, + title: s.title, + duration: s.duration, + cover_art_url: s.coverUrl || '', + play_count: s.playCount, + })), + }; + } + + const response: ResolveShareResponse = { + type: share.type as ShareType, + targetId: share.targetId, + content, + shareUrl: `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/s/${token}`, + }; + + return NextResponse.json(response); + } catch (error) { + console.error('Resolve share error:', error); + return NextResponse.json({ error: 'Failed to resolve share' }, { status: 500 }); + } +} diff --git a/app/api/share/album/[id]/route.ts b/app/api/share/album/[id]/route.ts new file mode 100644 index 0000000..d3ddf88 --- /dev/null +++ b/app/api/share/album/[id]/route.ts @@ -0,0 +1,53 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { generateShareToken, buildShareUrl } from '@/lib/share'; +import type { CreateShareResponse, ApiError } from '@/types/api-types'; + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const body = await request.json().catch(() => ({})); + + // Verify album exists + const album = await prisma.album.findUnique({ + where: { id }, + }); + + if (!album) { + return NextResponse.json( + { error: 'Album not found' }, + { status: 404 } + ); + } + + // Generate unique token + const token = generateShareToken(); + + // Create share record + await prisma.share.create({ + data: { + type: 'ALBUM', + targetId: id, + token, + platform: body.platform || null, + }, + }); + + const response: CreateShareResponse = { + shareUrl: buildShareUrl(token), + token, + type: 'ALBUM', + }; + + return NextResponse.json(response); + } catch (error) { + console.error('Create album share error:', error); + return NextResponse.json( + { error: 'Failed to create share link' }, + { status: 500 } + ); + } +} diff --git a/app/api/share/playlist/[id]/route.ts b/app/api/share/playlist/[id]/route.ts new file mode 100644 index 0000000..fbd5e06 --- /dev/null +++ b/app/api/share/playlist/[id]/route.ts @@ -0,0 +1,53 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { generateShareToken, buildShareUrl } from '@/lib/share'; +import type { CreateShareResponse, ApiError } from '@/types/api-types'; + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const body = await request.json().catch(() => ({})); + + // Verify playlist exists + const playlist = await prisma.playlist.findUnique({ + where: { id }, + }); + + if (!playlist) { + return NextResponse.json( + { error: 'Playlist not found' }, + { status: 404 } + ); + } + + // Generate unique token + const token = generateShareToken(); + + // Create share record + await prisma.share.create({ + data: { + type: 'PLAYLIST', + targetId: id, + token, + platform: body.platform || null, + }, + }); + + const response: CreateShareResponse = { + shareUrl: buildShareUrl(token), + token, + type: 'PLAYLIST', + }; + + return NextResponse.json(response); + } catch (error) { + console.error('Create playlist share error:', error); + return NextResponse.json( + { error: 'Failed to create share link' }, + { status: 500 } + ); + } +} diff --git a/app/api/share/song/[id]/route.ts b/app/api/share/song/[id]/route.ts new file mode 100644 index 0000000..1eaa895 --- /dev/null +++ b/app/api/share/song/[id]/route.ts @@ -0,0 +1,53 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { generateShareToken, buildShareUrl } from '@/lib/share'; +import type { CreateShareResponse, ApiError } from '@/types/api-types'; + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const body = await request.json().catch(() => ({})); + + // Verify song exists + const song = await prisma.song.findUnique({ + where: { id }, + }); + + if (!song) { + return NextResponse.json( + { error: 'Song not found' }, + { status: 404 } + ); + } + + // Generate unique token + const token = generateShareToken(); + + // Create share record + await prisma.share.create({ + data: { + type: 'SONG', + targetId: id, + token, + platform: body.platform || null, + }, + }); + + const response: CreateShareResponse = { + shareUrl: buildShareUrl(token), + token, + type: 'SONG', + }; + + return NextResponse.json(response); + } catch (error) { + console.error('Create song share error:', error); + return NextResponse.json( + { error: 'Failed to create share link' }, + { status: 500 } + ); + } +} diff --git a/app/api/songs/[id]/play/route.ts b/app/api/songs/[id]/play/route.ts new file mode 100644 index 0000000..e722ecf --- /dev/null +++ b/app/api/songs/[id]/play/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params + + const song = await prisma.song.update({ + where: { id }, + data: { + playCount: { + increment: 1, + }, + }, + select: { + id: true, + playCount: true, + }, + }) + + return NextResponse.json(song) + } catch (error) { + console.error('Error incrementing play count:', error) + return NextResponse.json({ error: 'Failed to increment play count' }, { status: 500 }) + } +} diff --git a/app/api/songs/[id]/route.ts b/app/api/songs/[id]/route.ts new file mode 100644 index 0000000..8c81c7b --- /dev/null +++ b/app/api/songs/[id]/route.ts @@ -0,0 +1,189 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireArtist, slugify, getCurrentUser } from '@/lib/auth' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params + const user = await getCurrentUser() + + const song = await prisma.song.findUnique({ + where: { id }, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + album: { + select: { + id: true, + title: true, + slug: true, + coverUrl: true, + }, + }, + genres: { + include: { + genre: true, + }, + }, + }, + }) + + if (!song) { + return NextResponse.json({ error: 'Song not found' }, { status: 404 }) + } + + // Only allow private songs to be viewed by the artist + if (!song.isPublic && (!user || !user.artist || user.artist.id !== song.artistId)) { + return NextResponse.json({ error: 'Song not found' }, { status: 404 }) + } + + return NextResponse.json(song) + } catch (error) { + console.error('Error fetching song:', error) + return NextResponse.json({ error: 'Failed to fetch song' }, { status: 500 }) + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { artist } = await requireArtist() + const { id } = await params + + const existingSong = await prisma.song.findUnique({ + where: { id }, + }) + + if (!existingSong) { + return NextResponse.json({ error: 'Song not found' }, { status: 404 }) + } + + if (existingSong.artistId !== artist.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + const body = await request.json() + const { + title, + description, + coverUrl, + albumId, + genreIds, + isPublic, + } = body + + const updateData: any = {} + if (title !== undefined) { + updateData.title = title + updateData.slug = slugify(title) + } + if (description !== undefined) updateData.description = description + if (coverUrl !== undefined) updateData.coverUrl = coverUrl + if (albumId !== undefined) updateData.albumId = albumId + if (isPublic !== undefined) updateData.isPublic = isPublic + + // Handle genre updates separately + if (genreIds !== undefined) { + await prisma.songGenre.deleteMany({ + where: { songId: id }, + }) + if (genreIds.length > 0) { + await prisma.songGenre.createMany({ + data: genreIds.map((genreId: string) => ({ + songId: id, + genreId, + })), + }) + } + } + + const updatedSong = await prisma.song.update({ + where: { id }, + data: updateData, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + album: { + select: { + id: true, + title: true, + slug: true, + coverUrl: true, + }, + }, + genres: { + include: { + genre: true, + }, + }, + }, + }) + + return NextResponse.json(updatedSong) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + if (error instanceof Error && error.message === 'Artist profile required') { + return NextResponse.json({ error: 'Artist profile required' }, { status: 403 }) + } + console.error('Error updating song:', error) + return NextResponse.json({ error: 'Failed to update song' }, { status: 500 }) + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { artist } = await requireArtist() + const { id } = await params + + const existingSong = await prisma.song.findUnique({ + where: { id }, + }) + + if (!existingSong) { + return NextResponse.json({ error: 'Song not found' }, { status: 404 }) + } + + if (existingSong.artistId !== artist.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + + await prisma.song.delete({ + where: { id }, + }) + + return NextResponse.json({ message: 'Song deleted successfully' }) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + if (error instanceof Error && error.message === 'Artist profile required') { + return NextResponse.json({ error: 'Artist profile required' }, { status: 403 }) + } + console.error('Error deleting song:', error) + return NextResponse.json({ error: 'Failed to delete song' }, { status: 500 }) + } +} diff --git a/app/api/songs/upload/route.ts b/app/api/songs/upload/route.ts new file mode 100644 index 0000000..a0df2ad --- /dev/null +++ b/app/api/songs/upload/route.ts @@ -0,0 +1,88 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { requireArtist, slugify } from '@/lib/auth' + +export async function POST(request: NextRequest) { + try { + const { artist } = await requireArtist() + + const body = await request.json() + const { + title, + description, + audioUrl, + coverUrl, + duration, + waveformUrl, + albumId, + genreIds, + isPublic = true, + } = body + + if (!title || !audioUrl || !duration) { + return NextResponse.json( + { error: 'Title, audioUrl, and duration are required' }, + { status: 400 } + ) + } + + const slug = slugify(title) + + const song = await prisma.song.create({ + data: { + artistId: artist.id, + title, + slug, + description, + audioUrl, + coverUrl, + duration, + waveformUrl, + albumId, + isPublic, + genres: genreIds + ? { + create: genreIds.map((genreId: string) => ({ + genreId, + })), + } + : undefined, + }, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + album: { + select: { + id: true, + title: true, + slug: true, + coverUrl: true, + }, + }, + genres: { + include: { + genre: true, + }, + }, + }, + }) + + return NextResponse.json(song, { status: 201 }) + } catch (error) { + if (error instanceof Error && error.message === 'Unauthorized') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + if (error instanceof Error && error.message === 'Artist profile required') { + return NextResponse.json({ error: 'Artist profile required' }, { status: 403 }) + } + console.error('Error uploading song:', error) + return NextResponse.json({ error: 'Failed to upload song' }, { status: 500 }) + } +} diff --git a/app/api/users/me/route.ts b/app/api/users/me/route.ts new file mode 100644 index 0000000..af2f6d2 --- /dev/null +++ b/app/api/users/me/route.ts @@ -0,0 +1,82 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { getCurrentUser } from '@/lib/auth' + +export async function GET() { + try { + const user = await getCurrentUser() + + if (!user) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + + return NextResponse.json({ user }) + } catch (error) { + console.error('Get current user error:', error) + return NextResponse.json( + { error: 'Failed to get user' }, + { status: 500 } + ) + } +} + +export async function PUT(request: NextRequest) { + try { + const currentUser = await getCurrentUser() + + if (!currentUser) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + + const body = await request.json() + const { displayName, bio, avatarUrl } = body + + const user = await prisma.user.update({ + where: { id: currentUser.id }, + data: { + ...(displayName !== undefined && { displayName }), + ...(bio !== undefined && { bio }), + ...(avatarUrl !== undefined && { avatarUrl }), + }, + select: { + id: true, + email: true, + username: true, + displayName: true, + avatarUrl: true, + bio: true, + role: true, + createdAt: true, + artist: { + select: { + id: true, + name: true, + slug: true, + verified: true, + }, + }, + label: { + select: { + id: true, + name: true, + slug: true, + }, + }, + }, + }) + + return NextResponse.json({ user }) + } catch (error) { + console.error('Update user error:', error) + return NextResponse.json( + { error: 'Failed to update user' }, + { status: 500 } + ) + } +} diff --git a/app/artist/[id]/page.tsx b/app/artist/[id]/page.tsx new file mode 100644 index 0000000..51a3914 --- /dev/null +++ b/app/artist/[id]/page.tsx @@ -0,0 +1,121 @@ +import { ArtistHeader } from '@/components/ArtistHeader' +import { AlbumCard } from '@/components/AlbumCard' +import { SongCard } from '@/components/SongCard' +import { SocialLinks } from '@/components/SocialLinks' +import { SectionHeader } from '@/components/SectionHeader' + +interface PageProps { + params: Promise<{ id: string }> +} + +async function getArtist(id: string) { + const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/artists/${id}`, { + cache: 'no-store', + }) + if (!res.ok) throw new Error('Artist not found') + const data = await res.json() + return data.artist +} + +async function getArtistSongs(id: string) { + const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/artists/${id}/songs`, { + cache: 'no-store', + }) + if (!res.ok) return [] + const data = await res.json() + return data.songs || [] +} + +async function getArtistAlbums(id: string) { + const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/artists/${id}/albums`, { + cache: 'no-store', + }) + if (!res.ok) return [] + const data = await res.json() + return data.albums || [] +} + +export default async function ArtistPage({ params }: PageProps) { + const { id } = await params + + const [artist, songs, albums] = await Promise.all([ + getArtist(id), + getArtistSongs(id), + getArtistAlbums(id), + ]) + + return ( +
+ + +
+ {/* Social Links */} + {(artist.website || artist.twitter || artist.instagram || artist.spotify) && ( +
+ +
+ )} + + {/* Popular Tracks */} + {songs.length > 0 && ( +
+ +
+ {songs.map((song: any) => ( + + ))} +
+
+ )} + + {/* Albums */} + {albums.length > 0 && ( +
+ +
+ {albums.map((album: any) => ( + + ))} +
+
+ )} + + {/* Bio */} + {artist.bio && ( +
+

About

+

{artist.bio}

+
+ )} +
+
+ ) +} diff --git a/app/forgot-password/page.tsx b/app/forgot-password/page.tsx new file mode 100644 index 0000000..3408c3b --- /dev/null +++ b/app/forgot-password/page.tsx @@ -0,0 +1,71 @@ +'use client' + +import { useState } from 'react' +import { AuthForm, AuthFormData } from '@/components/AuthForm' + +export default function ForgotPasswordPage() { + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState() + const [success, setSuccess] = useState(false) + + const handleSubmit = async (data: AuthFormData) => { + setIsLoading(true) + setError(undefined) + + try { + const res = await fetch('/api/auth/forgot-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: data.email }), + }) + + const result = await res.json() + + if (!res.ok) { + setError(result.error || 'Failed to send reset link') + return + } + + setSuccess(true) + } catch { + setError('Something went wrong. Please try again.') + } finally { + setIsLoading(false) + } + } + + if (success) { + return ( +
+
+
+ + + +
+

Check your email

+

+ If an account exists with that email, we've sent a password reset link. +

+
+
+ ) + } + + return ( +
+
+
+

Reset Password

+

Enter your email to receive a reset link

+
+ +
+
+ ) +} diff --git a/app/genre/[slug]/page.tsx b/app/genre/[slug]/page.tsx new file mode 100644 index 0000000..a3549e1 --- /dev/null +++ b/app/genre/[slug]/page.tsx @@ -0,0 +1,53 @@ +import { GenreHeader } from '@/components/GenreHeader' +import { SongCard } from '@/components/SongCard' + +interface PageProps { + params: Promise<{ slug: string }> +} + +async function getGenreSongs(slug: string) { + const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/discover/genres/${slug}`, { + cache: 'no-store', + }) + if (!res.ok) throw new Error('Genre not found') + const data = await res.json() + return data +} + +export default async function GenrePage({ params }: PageProps) { + const { slug } = await params + const { genre, songs } = await getGenreSongs(slug) + + return ( +
+ + +
+ {songs && songs.length > 0 ? ( +
+ {songs.map((song: any) => ( + + ))} +
+ ) : ( +
+

No songs found in this genre

+

Check back later for new releases

+
+ )} +
+
+ ) +} diff --git a/app/label/[id]/page.tsx b/app/label/[id]/page.tsx new file mode 100644 index 0000000..5853dfc --- /dev/null +++ b/app/label/[id]/page.tsx @@ -0,0 +1,84 @@ +import { LabelHeader } from '@/components/LabelHeader' +import { LabelStats } from '@/components/LabelStats' +import { ArtistRoster } from '@/components/ArtistRoster' + +interface PageProps { + params: Promise<{ id: string }> +} + +async function getLabel(id: string) { + const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/labels/${id}`, { + cache: 'no-store', + }) + if (!res.ok) throw new Error('Label not found') + const data = await res.json() + return data.label +} + +async function getLabelStats(id: string) { + const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/labels/${id}/stats`, { + cache: 'no-store', + }) + if (!res.ok) { + return { + artistCount: 0, + songCount: 0, + albumCount: 0, + totalPlays: 0 + } + } + const data = await res.json() + return data.stats +} + +export default async function LabelPage({ params }: PageProps) { + const { id } = await params + + const [label, stats] = await Promise.all([ + getLabel(id), + getLabelStats(id), + ]) + + return ( +
+ + +
+ {/* Stats */} +
+ +
+ + {/* Artist Roster */} +
+

Artists

+ { + window.location.href = `/artist/${artistId}` + }} + /> +
+ + {/* Description */} + {label.description && ( +
+

About

+

{label.description}

+
+ )} +
+
+ ) +} diff --git a/app/label/dashboard/page.tsx b/app/label/dashboard/page.tsx new file mode 100644 index 0000000..90644fe --- /dev/null +++ b/app/label/dashboard/page.tsx @@ -0,0 +1,209 @@ +'use client' + +import { useState, useEffect } from 'react' +import { LabelStats } from '@/components/LabelStats' +import { ArtistRoster } from '@/components/ArtistRoster' +import { InvitationCard } from '@/components/InvitationCard' +import { InviteArtistModal } from '@/components/InviteArtistModal' + +export default function LabelDashboardPage() { + const [label, setLabel] = useState(null) + const [stats, setStats] = useState(null) + const [invitations, setInvitations] = useState([]) + const [loading, setLoading] = useState(true) + const [isInviteModalOpen, setIsInviteModalOpen] = useState(false) + + useEffect(() => { + fetchDashboardData() + }, []) + + const fetchDashboardData = async () => { + try { + // Check authentication + const userRes = await fetch('/api/users/me') + if (!userRes.ok) { + window.location.href = '/login' + return + } + const userData = await userRes.json() + + // Fetch user's label + const labelRes = await fetch(`/api/users/${userData.user.id}/label`) + if (!labelRes.ok) { + window.location.href = '/label/create' + return + } + const labelData = await labelRes.json() + setLabel(labelData.label) + + // Fetch stats + const statsRes = await fetch(`/api/labels/${labelData.label.id}/stats`) + if (statsRes.ok) { + const statsData = await statsRes.json() + setStats(statsData.stats) + } + + // Fetch invitations + const invitationsRes = await fetch(`/api/labels/${labelData.label.id}/invitations`) + if (invitationsRes.ok) { + const invitationsData = await invitationsRes.json() + setInvitations(invitationsData.invitations || []) + } + } catch (error) { + console.error('Failed to fetch dashboard data:', error) + } finally { + setLoading(false) + } + } + + const handleRemoveArtist = async (artistId: string) => { + if (!confirm('Are you sure you want to remove this artist from your label?')) { + return + } + + try { + const res = await fetch(`/api/labels/${label.id}/artists/${artistId}`, { + method: 'DELETE' + }) + + if (res.ok) { + fetchDashboardData() + } else { + alert('Failed to remove artist') + } + } catch (error) { + console.error('Failed to remove artist:', error) + alert('Network error') + } + } + + const handleCancelInvitation = async (invitationId: string) => { + try { + const res = await fetch(`/api/invitations/${invitationId}`, { + method: 'DELETE' + }) + + if (res.ok) { + setInvitations(invitations.filter(inv => inv.id !== invitationId)) + } else { + alert('Failed to cancel invitation') + } + } catch (error) { + console.error('Failed to cancel invitation:', error) + alert('Network error') + } + } + + if (loading) { + return ( +
+

Loading dashboard...

+
+ ) + } + + if (!label) { + return ( +
+
+

You don't have a label yet

+ + Create Label + +
+
+ ) + } + + return ( +
+
+ {/* Header */} +
+
+

{label.name}

+

Label Dashboard

+
+
+ + Settings + + +
+
+ + {/* Stats */} + {stats && ( +
+ +
+ )} + + {/* Artist Roster */} +
+
+

Your Artists

+ +
+ { + window.location.href = `/artist/${artistId}` + }} + /> +
+ + {/* Pending Invitations */} + {invitations.length > 0 && ( +
+

+ Pending Invitations ({invitations.filter(inv => inv.status === 'PENDING').length}) +

+
+ {invitations + .filter(inv => inv.status === 'PENDING') + .map((invitation) => ( + + ))} +
+
+ )} +
+ + {/* Invite Modal */} + setIsInviteModalOpen(false)} + onInviteSent={() => { + setIsInviteModalOpen(false) + fetchDashboardData() + }} + /> +
+ ) +} diff --git a/app/label/invitations/page.tsx b/app/label/invitations/page.tsx new file mode 100644 index 0000000..a1a6d14 --- /dev/null +++ b/app/label/invitations/page.tsx @@ -0,0 +1,187 @@ +'use client' + +import { useState, useEffect } from 'react' +import { InvitationCard } from '@/components/InvitationCard' +import { InviteArtistModal } from '@/components/InviteArtistModal' + +export default function LabelInvitationsPage() { + const [label, setLabel] = useState(null) + const [invitations, setInvitations] = useState([]) + const [loading, setLoading] = useState(true) + const [isInviteModalOpen, setIsInviteModalOpen] = useState(false) + const [filter, setFilter] = useState<'all' | 'pending' | 'accepted' | 'declined'>('all') + + useEffect(() => { + fetchInvitations() + }, []) + + const fetchInvitations = async () => { + try { + // Check authentication + const userRes = await fetch('/api/users/me') + if (!userRes.ok) { + window.location.href = '/login' + return + } + const userData = await userRes.json() + + // Fetch user's label + const labelRes = await fetch(`/api/users/${userData.user.id}/label`) + if (!labelRes.ok) { + window.location.href = '/label/create' + return + } + const labelData = await labelRes.json() + setLabel(labelData.label) + + // Fetch invitations + const invitationsRes = await fetch(`/api/labels/${labelData.label.id}/invitations`) + if (invitationsRes.ok) { + const invitationsData = await invitationsRes.json() + setInvitations(invitationsData.invitations || []) + } + } catch (error) { + console.error('Failed to fetch invitations:', error) + } finally { + setLoading(false) + } + } + + const handleCancelInvitation = async (invitationId: string) => { + if (!confirm('Are you sure you want to cancel this invitation?')) { + return + } + + try { + const res = await fetch(`/api/invitations/${invitationId}`, { + method: 'DELETE' + }) + + if (res.ok) { + setInvitations(invitations.filter(inv => inv.id !== invitationId)) + } else { + alert('Failed to cancel invitation') + } + } catch (error) { + console.error('Failed to cancel invitation:', error) + alert('Network error') + } + } + + const filteredInvitations = invitations.filter(inv => { + if (filter === 'all') return true + return inv.status === filter.toUpperCase() + }) + + const counts = { + all: invitations.length, + pending: invitations.filter(inv => inv.status === 'PENDING').length, + accepted: invitations.filter(inv => inv.status === 'ACCEPTED').length, + declined: invitations.filter(inv => inv.status === 'DECLINED').length + } + + if (loading) { + return ( +
+

Loading invitations...

+
+ ) + } + + if (!label) { + return ( +
+
+

Label not found

+ + Go to Dashboard + +
+
+ ) + } + + return ( +
+
+ {/* Header */} +
+
+

Invitations

+

Manage your artist invitations

+
+ +
+ + {/* Filter Tabs */} +
+ {(['all', 'pending', 'accepted', 'declined'] as const).map((status) => ( + + ))} +
+ + {/* Invitations List */} + {filteredInvitations.length > 0 ? ( +
+ {filteredInvitations.map((invitation) => ( + + ))} +
+ ) : ( +
+ + + + +

+ {filter === 'all' + ? 'No invitations yet' + : `No ${filter} invitations`} +

+ +
+ )} +
+ + {/* Invite Modal */} + setIsInviteModalOpen(false)} + onInviteSent={() => { + setIsInviteModalOpen(false) + fetchInvitations() + }} + /> +
+ ) +} diff --git a/app/label/settings/page.tsx b/app/label/settings/page.tsx new file mode 100644 index 0000000..f0f9fc1 --- /dev/null +++ b/app/label/settings/page.tsx @@ -0,0 +1,133 @@ +'use client' + +import { useState, useEffect } from 'react' +import { LabelProfileForm, LabelProfileFormData } from '@/components/LabelProfileForm' + +export default function LabelSettingsPage() { + const [label, setLabel] = useState(null) + const [loading, setLoading] = useState(true) + const [isUpdating, setIsUpdating] = useState(false) + + useEffect(() => { + fetchLabel() + }, []) + + const fetchLabel = async () => { + try { + // Check authentication + const userRes = await fetch('/api/users/me') + if (!userRes.ok) { + window.location.href = '/login' + return + } + const userData = await userRes.json() + + // Fetch user's label + const labelRes = await fetch(`/api/users/${userData.user.id}/label`) + if (!labelRes.ok) { + window.location.href = '/label/create' + return + } + const labelData = await labelRes.json() + setLabel(labelData.label) + } catch (error) { + console.error('Failed to fetch label:', error) + } finally { + setLoading(false) + } + } + + const handleSave = async (data: LabelProfileFormData) => { + setIsUpdating(true) + try { + const res = await fetch(`/api/labels/${label.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }) + + if (res.ok) { + const result = await res.json() + setLabel(result.label) + alert('Label profile updated successfully') + } else { + const error = await res.json() + alert(error.error || 'Failed to update label') + } + } catch (error) { + console.error('Failed to update label:', error) + alert('Network error') + } finally { + setIsUpdating(false) + } + } + + const handleCancel = () => { + window.location.href = '/label/dashboard' + } + + if (loading) { + return ( +
+

Loading settings...

+
+ ) + } + + if (!label) { + return ( +
+
+

Label not found

+ + Go to Dashboard + +
+
+ ) + } + + return ( +
+
+ {/* Header */} +
+

Label Settings

+

+ Manage your label profile and information +

+
+ + {/* Profile Form */} +
+

Profile Information

+ +
+ + {/* Danger Zone */} +
+

Danger Zone

+

+ Once you delete your label, all associated data will be permanently removed. Artists will be unlinked. This action cannot be undone. +

+ +
+
+
+ ) +} diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..93d4673 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import { Header } from "@/components/Header"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -13,8 +14,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "SonicCloud - Music for Everyone", + description: "Discover, upload, and share music on SonicCloud. A platform for musicians and music lovers.", }; export default function RootLayout({ @@ -25,9 +26,12 @@ export default function RootLayout({ return ( - {children} +
+
+ {children} +
); diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..0df7344 --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,58 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { AuthForm, AuthFormData } from '@/components/AuthForm' + +export default function LoginPage() { + const router = useRouter() + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState() + + const handleSubmit = async (data: AuthFormData) => { + setIsLoading(true) + setError(undefined) + + try { + const res = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: data.email, + password: data.password, + }), + }) + + const result = await res.json() + + if (!res.ok) { + setError(result.error || 'Login failed') + return + } + + router.push('/') + router.refresh() + } catch { + setError('Something went wrong. Please try again.') + } finally { + setIsLoading(false) + } + } + + return ( +
+
+
+

Welcome Back

+

Sign in to continue to Sonic Cloud

+
+ +
+
+ ) +} diff --git a/app/page.tsx b/app/page.tsx index 295f8fd..2e8a187 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,65 +1,171 @@ -import Image from "next/image"; +import Link from 'next/link' +import { SongCard } from '@/components/SongCard' +import { GenreBadge } from '@/components/GenreBadge' +import { SectionHeader } from '@/components/SectionHeader' +import { prisma } from '@/lib/prisma' + +async function getTrendingSongs() { + try { + const songs = await prisma.song.findMany({ + where: { + isPublic: true, + }, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + album: { + select: { + id: true, + title: true, + slug: true, + coverUrl: true, + }, + }, + }, + orderBy: { + playCount: 'desc', + }, + take: 8, + }) + return songs + } catch (error) { + console.error('Error fetching trending songs:', error) + return [] + } +} + +async function getNewReleases() { + try { + const songs = await prisma.song.findMany({ + where: { + isPublic: true, + }, + include: { + artist: { + select: { + id: true, + name: true, + slug: true, + avatarUrl: true, + verified: true, + }, + }, + album: { + select: { + id: true, + title: true, + slug: true, + coverUrl: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + take: 8, + }) + return songs + } catch (error) { + console.error('Error fetching new releases:', error) + return [] + } +} + +async function getGenres() { + try { + const genres = await prisma.genre.findMany({ + orderBy: { + name: 'asc', + }, + }) + return genres + } catch (error) { + console.error('Error fetching genres:', error) + return [] + } +} + +export default async function HomePage() { + const [trendingSongs, newReleases, genres] = await Promise.all([ + getTrendingSongs(), + getNewReleases(), + getGenres(), + ]) -export default function Home() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. +

+
+ {/* Hero Section */} +
+

Discover Music

+

+ Explore trending tracks, new releases, and your favorite genres

- -
-
- ); + + {/* Trending Section */} +
+ +
+ {trendingSongs.map((song: any) => ( + + ))} +
+
+ + {/* New Releases Section */} +
+ +
+ {newReleases.map((song: any) => ( + + ))} +
+
+ + {/* Genres Section */} +
+ +
+ {genres.map((genre: any) => ( + + + + ))} +
+
+
+ + ) } diff --git a/app/playlist/[id]/page.tsx b/app/playlist/[id]/page.tsx new file mode 100644 index 0000000..5e3ef11 --- /dev/null +++ b/app/playlist/[id]/page.tsx @@ -0,0 +1,102 @@ +import { PlaylistHeader } from '@/components/PlaylistHeader' +import { TrackList } from '@/components/TrackList' + +interface PageProps { + params: Promise<{ id: string }> +} + +async function getPlaylist(id: string) { + const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/playlists/${id}`, { + cache: 'no-store', + }) + if (!res.ok) throw new Error('Playlist not found') + const data = await res.json() + return data.playlist +} + +export default async function PlaylistPage({ params }: PageProps) { + const { id } = await params + const playlist = await getPlaylist(id) + + // Calculate total duration from songs + const totalDuration = playlist.songs?.reduce((acc: number, item: any) => { + const song = item.song || item + return acc + (song.duration || 0) + }, 0) || 0 + + // Transform playlist songs for TrackList + const tracks = playlist.songs?.map((item: any, index: number) => { + const song = item.song || item + return { + id: song.id, + title: song.title, + artistName: song.artist?.name || 'Unknown Artist', + duration: song.duration || 0, + plays: song.plays, + position: item.position || index + 1 + } + }) || [] + + return ( +
+ + +
+ {/* Track List */} + {tracks.length > 0 ? ( +
+ +
+ ) : ( +
+

This playlist is empty

+

Add some songs to get started

+
+ )} + + {/* Playlist Info */} + {playlist.description && ( +
+

Description

+

+ {playlist.description} +

+
+ )} + +
+
+

Created

+

{new Date(playlist.createdAt).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + })}

+
+ +
+

Songs

+

{playlist.songs?.length || 0} tracks

+
+ + {playlist.songs && playlist.songs.length > 0 && ( +
+

Total Duration

+

+ {Math.floor(playlist.songs.reduce((acc: number, song: any) => acc + song.duration, 0) / 60)} minutes +

+
+ )} +
+
+
+ ) +} diff --git a/app/playlists/page.tsx b/app/playlists/page.tsx new file mode 100644 index 0000000..4a9ce32 --- /dev/null +++ b/app/playlists/page.tsx @@ -0,0 +1,117 @@ +'use client' + +import { PlaylistCard } from '@/components/PlaylistCard' +import { CreatePlaylistModal, CreatePlaylistData } from '@/components/CreatePlaylistModal' +import { useState, useEffect } from 'react' + +export default function PlaylistsPage() { + const [playlists, setPlaylists] = useState([]) + const [isModalOpen, setIsModalOpen] = useState(false) + const [isCreating, setIsCreating] = useState(false) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetchPlaylists() + }, []) + + const fetchPlaylists = async () => { + try { + const res = await fetch('/api/playlists') + if (res.ok) { + const data = await res.json() + setPlaylists(data.playlists || []) + } + } catch (error) { + console.error('Failed to fetch playlists:', error) + } finally { + setLoading(false) + } + } + + const handleCreatePlaylist = async (data: CreatePlaylistData) => { + setIsCreating(true) + try { + const res = await fetch('/api/playlists', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: data.title, + description: data.description, + isPublic: data.isPublic, + }), + }) + + if (res.ok) { + const result = await res.json() + setPlaylists([result.playlist, ...playlists]) + setIsModalOpen(false) + } + } catch (error) { + console.error('Failed to create playlist:', error) + } finally { + setIsCreating(false) + } + } + + return ( +
+
+ {/* Header */} +
+
+

My Playlists

+

+ Create and manage your music collections +

+
+ +
+ + {/* Playlists Grid */} + {loading ? ( +
+

Loading playlists...

+
+ ) : playlists.length > 0 ? ( +
+ {playlists.map((playlist) => ( + + ))} +
+ ) : ( +
+

You don't have any playlists yet

+

Create your first playlist to start organizing your music

+ +
+ )} +
+ + {/* Create Playlist Modal */} + setIsModalOpen(false)} + onSubmit={handleCreatePlaylist} + isLoading={isCreating} + /> +
+ ) +} diff --git a/app/profile/page.tsx b/app/profile/page.tsx new file mode 100644 index 0000000..7ead7c5 --- /dev/null +++ b/app/profile/page.tsx @@ -0,0 +1,150 @@ +'use client' + +import { ProfileForm, ProfileFormData } from '@/components/ProfileForm' +import { AvatarUpload } from '@/components/AvatarUpload' +import { useState, useEffect } from 'react' + +export default function ProfilePage() { + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) + const [isUpdatingProfile, setIsUpdatingProfile] = useState(false) + const [isUploadingAvatar, setIsUploadingAvatar] = useState(false) + + useEffect(() => { + fetchCurrentUser() + }, []) + + const fetchCurrentUser = async () => { + try { + const res = await fetch('/api/users/me') + if (res.ok) { + const data = await res.json() + setUser(data.user) + } + } catch (error) { + console.error('Failed to fetch user:', error) + } finally { + setLoading(false) + } + } + + const handleAvatarUpload = async (file: File) => { + setIsUploadingAvatar(true) + try { + // In a real app, upload to cloud storage and get URL + // For now, create a local blob URL as placeholder + const avatarUrl = URL.createObjectURL(file) + + const res = await fetch('/api/users/me', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ avatarUrl }), + }) + + if (res.ok) { + const data = await res.json() + setUser(data.user) + } + } catch (error) { + console.error('Failed to upload avatar:', error) + } finally { + setIsUploadingAvatar(false) + } + } + + const handleProfileSubmit = async (data: ProfileFormData) => { + setIsUpdatingProfile(true) + try { + const res = await fetch('/api/users/me', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + displayName: data.username, + bio: data.bio, + }), + }) + + if (res.ok) { + const result = await res.json() + setUser(result.user) + } + } catch (error) { + console.error('Failed to update profile:', error) + } finally { + setIsUpdatingProfile(false) + } + } + + if (loading) { + return ( +
+

Loading profile...

+
+ ) + } + + if (!user) { + return ( +
+
+

Please log in to view your profile

+ + Log In + +
+
+ ) + } + + return ( +
+
+ {/* Header */} +
+

Profile Settings

+

+ Manage your account and preferences +

+
+ + {/* Avatar Section */} +
+

Profile Picture

+ +
+ + {/* Profile Form */} +
+

Account Information

+ +
+ + {/* Danger Zone */} +
+

Danger Zone

+

+ Once you delete your account, there is no going back. Please be certain. +

+ +
+
+
+ ) +} diff --git a/app/register/page.tsx b/app/register/page.tsx new file mode 100644 index 0000000..344f3a9 --- /dev/null +++ b/app/register/page.tsx @@ -0,0 +1,64 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { AuthForm, AuthFormData } from '@/components/AuthForm' + +export default function RegisterPage() { + const router = useRouter() + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState() + + const handleSubmit = async (data: AuthFormData) => { + setIsLoading(true) + setError(undefined) + + if (data.password !== data.confirmPassword) { + setError('Passwords do not match') + setIsLoading(false) + return + } + + try { + const res = await fetch('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: data.email, + password: data.password, + username: data.username, + }), + }) + + const result = await res.json() + + if (!res.ok) { + setError(result.error || 'Registration failed') + return + } + + router.push('/login') + } catch { + setError('Something went wrong. Please try again.') + } finally { + setIsLoading(false) + } + } + + return ( +
+
+
+

Join Sonic Cloud

+

Create your account and start sharing music

+
+ +
+
+ ) +} diff --git a/app/s/[token]/page.tsx b/app/s/[token]/page.tsx new file mode 100644 index 0000000..ab33e9c --- /dev/null +++ b/app/s/[token]/page.tsx @@ -0,0 +1,88 @@ +import { notFound } from 'next/navigation'; +import { SharedContentDisplay } from '@/components/SharedContentDisplay'; +import type { ResolveShareResponse } from '@/types/api-types'; +import type { Metadata } from 'next'; + +interface PageProps { + params: Promise<{ token: string }>; +} + +async function getShareData(token: string): Promise { + try { + const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; + const res = await fetch(`${baseUrl}/api/share/${token}`, { + cache: 'no-store', + }); + + if (!res.ok) return null; + return res.json(); + } catch { + return null; + } +} + +export async function generateMetadata({ params }: PageProps): Promise { + const { token } = await params; + const data = await getShareData(token); + + if (!data) { + return { title: 'Content Not Found | Sonic Cloud' }; + } + + let title = ''; + let description = ''; + let image = ''; + + if (data.type === 'SONG') { + const song = data.content as any; + title = `${song.title} by ${song.artist.stage_name}`; + description = `Listen to ${song.title} on Sonic Cloud`; + image = song.coverArtUrl; + } else if (data.type === 'PLAYLIST') { + const playlist = data.content as any; + title = playlist.name; + description = `${playlist.songCount} songs curated by ${playlist.curator.name}`; + image = playlist.coverImageUrl; + } else { + const album = data.content as any; + title = `${album.title} by ${album.artist.stage_name}`; + description = album.description || `Album by ${album.artist.stage_name}`; + image = album.coverArtUrl; + } + + return { + title: `${title} | Sonic Cloud`, + description, + openGraph: { + title, + description, + images: image ? [image] : [], + type: 'music.song', + }, + twitter: { + card: 'summary_large_image', + title, + description, + images: image ? [image] : [], + }, + }; +} + +export default async function SharePage({ params }: PageProps) { + const { token } = await params; + const data = await getShareData(token); + + if (!data) { + notFound(); + } + + return ( +
+ +
+ ); +} diff --git a/app/search/page.tsx b/app/search/page.tsx new file mode 100644 index 0000000..424568f --- /dev/null +++ b/app/search/page.tsx @@ -0,0 +1,193 @@ +'use client' + +import { SearchBar } from '@/components/SearchBar' +import { SongCard } from '@/components/SongCard' +import { AlbumCard } from '@/components/AlbumCard' +import { ArtistCard } from '@/components/ArtistCard' +import { SectionHeader } from '@/components/SectionHeader' +import { useState, useEffect, Suspense } from 'react' +import { useSearchParams } from 'next/navigation' + +function SearchContent() { + const searchParams = useSearchParams() + const [results, setResults] = useState(null) + const [loading, setLoading] = useState(false) + const [searchedQuery, setSearchedQuery] = useState('') + + useEffect(() => { + const q = searchParams.get('q') + if (q) { + performSearch(q) + } + }, [searchParams]) + + const performSearch = async (searchQuery: string) => { + if (!searchQuery.trim()) { + setResults(null) + return + } + + setSearchedQuery(searchQuery) + setLoading(true) + try { + const res = await fetch(`/api/search?q=${encodeURIComponent(searchQuery)}`) + if (res.ok) { + const data = await res.json() + setResults(data) + } + } catch (error) { + console.error('Search failed:', error) + } finally { + setLoading(false) + } + } + + const handleSearch = (query: string) => { + if (query.trim()) { + const url = new URL(window.location.href) + url.searchParams.set('q', query) + window.history.pushState({}, '', url) + performSearch(query) + } else { + setResults(null) + setSearchedQuery('') + } + } + + const hasResults = results && ( + (results.songs && results.songs.length > 0) || + (results.albums && results.albums.length > 0) || + (results.artists && results.artists.length > 0) + ) + + return ( +
+
+ {/* Header */} +
+

Search

+

+ Find songs, artists, albums, and playlists +

+ +
+ + {/* Results */} + {loading ? ( +
+
+

Searching...

+
+ ) : hasResults ? ( +
+ {/* Songs Section */} + {results.songs && results.songs.length > 0 && ( +
+ +
+ {results.songs.map((song: any) => ( + + ))} +
+
+ )} + + {/* Artists Section */} + {results.artists && results.artists.length > 0 && ( +
+ +
+ {results.artists.map((artist: any) => ( + + ))} +
+
+ )} + + {/* Albums Section */} + {results.albums && results.albums.length > 0 && ( +
+ +
+ {results.albums.map((album: any) => ( + + ))} +
+
+ )} +
+ ) : searchedQuery ? ( +
+ + + +

No results found for "{searchedQuery}"

+

Try different keywords or browse by genre

+
+ ) : ( +
+

Start typing to search

+

Find your favorite music

+
+ )} + + {/* Popular Searches */} + {!searchedQuery && ( +
+

Popular Searches

+
+ {['Hip Hop', 'Rock', 'Electronic', 'Jazz', 'Pop', 'Classical'].map((genre) => ( + + ))} +
+
+ )} +
+
+ ) +} + +export default function SearchPage() { + return ( + +
+
+
+

Loading search...

+
+
+ + }> + + + ) +} diff --git a/app/upload/page.tsx b/app/upload/page.tsx new file mode 100644 index 0000000..8f1da31 --- /dev/null +++ b/app/upload/page.tsx @@ -0,0 +1,114 @@ +'use client' + +import { UploadForm, UploadFormData } from '@/components/UploadForm' +import { WaveformDisplay } from '@/components/WaveformDisplay' +import { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' + +export default function UploadPage() { + const router = useRouter() + const [audioFile, setAudioFile] = useState(null) + const [genres, setGenres] = useState>([]) + const [isLoading, setIsLoading] = useState(false) + + useEffect(() => { + fetchGenres() + }, []) + + const fetchGenres = async () => { + try { + const res = await fetch('/api/discover/genres') + if (res.ok) { + const data = await res.json() + setGenres(data.genres || []) + } + } catch (error) { + console.error('Failed to fetch genres:', error) + } + } + + const handleSubmit = async (data: UploadFormData) => { + if (!data.audioFile) return + + setAudioFile(data.audioFile) + setIsLoading(true) + + try { + const formData = new FormData() + formData.append('audio', data.audioFile) + formData.append('title', data.title) + if (data.coverFile) formData.append('cover', data.coverFile) + if (data.albumId) formData.append('albumId', data.albumId) + if (data.genreIds.length > 0) formData.append('genreIds', JSON.stringify(data.genreIds)) + if (data.releaseDate) formData.append('releaseDate', data.releaseDate) + + const res = await fetch('/api/songs/upload', { + method: 'POST', + body: formData, + }) + + if (res.ok) { + const result = await res.json() + router.push(`/artist/${result.song?.artistId || ''}`) + } + } catch (error) { + console.error('Upload failed:', error) + } finally { + setIsLoading(false) + } + } + + return ( +
+
+ {/* Header */} +
+

Upload Music

+

+ Share your music with the world +

+
+ + {/* Waveform Preview */} + {audioFile && ( +
+

Audio Preview

+ +
+ )} + + {/* Upload Form */} +
+ +
+ + {/* Guidelines */} +
+

Upload Guidelines

+
    +
  • + + Audio files must be in MP3, WAV, or FLAC format +
  • +
  • + + Maximum file size: 100MB +
  • +
  • + + Make sure you own the rights to the music you upload +
  • +
  • + + Add accurate metadata for better discoverability +
  • +
+
+
+
+ ) +} diff --git a/components/AlbumCard.tsx b/components/AlbumCard.tsx new file mode 100644 index 0000000..bbe128d --- /dev/null +++ b/components/AlbumCard.tsx @@ -0,0 +1,70 @@ +'use client' + +export interface AlbumCardProps { + id: string + title: string + artistName: string + coverUrl?: string + releaseYear?: number + trackCount?: number + onClick?: () => void +} + +export function AlbumCard({ + id, + title, + artistName, + coverUrl, + releaseYear, + trackCount, + onClick +}: AlbumCardProps) { + return ( +
+ {/* Cover Image */} +
+ {coverUrl ? ( + {title} + ) : ( +
+ + + +
+ )} + + {/* Play Button Overlay */} +
+ +
+
+ + {/* Album Info */} +
+

+ {title} +

+

{artistName}

+ + {(releaseYear || trackCount) && ( +
+ {releaseYear && {releaseYear}} + {releaseYear && trackCount && } + {trackCount && {trackCount} {trackCount === 1 ? 'track' : 'tracks'}} +
+ )} +
+
+ ) +} diff --git a/components/AlbumHeader.tsx b/components/AlbumHeader.tsx new file mode 100644 index 0000000..64b8bd1 --- /dev/null +++ b/components/AlbumHeader.tsx @@ -0,0 +1,100 @@ +'use client' + +export interface AlbumHeaderProps { + title: string + artistName: string + artistId: string + coverUrl?: string + releaseDate: string + trackCount: number + duration: number + type?: 'album' | 'single' | 'ep' + onPlayAll?: () => void + onArtistClick?: () => void +} + +export function AlbumHeader({ + title, + artistName, + artistId, + coverUrl, + releaseDate, + trackCount, + duration, + type = 'album', + onPlayAll, + onArtistClick +}: AlbumHeaderProps) { + const formatDuration = (seconds: number) => { + const hours = Math.floor(seconds / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + if (hours > 0) return `${hours} hr ${minutes} min` + return `${minutes} min` + } + + const formatDate = (dateString: string) => { + const date = new Date(dateString) + return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) + } + + return ( +
+ {/* Cover Image */} +
+
+ {coverUrl ? ( + {title} + ) : ( +
+ + + +
+ )} +
+
+ + {/* Album Info */} +
+

+ {type} +

+ +

+ {title} +

+ +
+ + + {formatDate(releaseDate)} + + {trackCount} {trackCount === 1 ? 'song' : 'songs'} + + {formatDuration(duration)} +
+ + {/* Play Button */} + {onPlayAll && ( + + )} +
+
+ ) +} diff --git a/components/ArtistCard.tsx b/components/ArtistCard.tsx new file mode 100644 index 0000000..dbe33ba --- /dev/null +++ b/components/ArtistCard.tsx @@ -0,0 +1,73 @@ +'use client' + +export interface ArtistCardProps { + id: string + name: string + avatarUrl?: string + followers?: number + verified?: boolean + onClick?: () => void +} + +export function ArtistCard({ + id, + name, + avatarUrl, + followers = 0, + verified = false, + onClick +}: ArtistCardProps) { + const formatFollowers = (count: number) => { + if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M` + if (count >= 1000) return `${(count / 1000).toFixed(1)}K` + return count.toString() + } + + return ( +
+ {/* Avatar */} +
+
+ {avatarUrl ? ( + {name} + ) : ( +
+ + + +
+ )} +
+ + {/* Verified Badge */} + {verified && ( +
+ + + +
+ )} +
+ + {/* Artist Info */} +
+

+ {name} +

+ + {followers > 0 && ( +

+ {formatFollowers(followers)} followers +

+ )} +
+
+ ) +} diff --git a/components/ArtistHeader.tsx b/components/ArtistHeader.tsx new file mode 100644 index 0000000..8ac32d4 --- /dev/null +++ b/components/ArtistHeader.tsx @@ -0,0 +1,122 @@ +'use client' + +export interface ArtistHeaderProps { + name: string + avatarUrl?: string + bannerUrl?: string + bio?: string + followers?: number + monthlyListeners?: number + verified?: boolean + isFollowing?: boolean + onFollowToggle?: () => void +} + +export function ArtistHeader({ + name, + avatarUrl, + bannerUrl, + bio, + followers = 0, + monthlyListeners = 0, + verified = false, + isFollowing = false, + onFollowToggle +}: ArtistHeaderProps) { + const formatNumber = (num: number) => { + if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M` + if (num >= 1000) return `${(num / 1000).toFixed(1)}K` + return num.toString() + } + + return ( +
+ {/* Banner */} +
+ {bannerUrl && ( + {name} + )} +
+
+ + {/* Content */} +
+
+ {/* Avatar */} +
+
+ {avatarUrl ? ( + {name} + ) : ( +
+ + + +
+ )} +
+ + {/* Verified Badge */} + {verified && ( +
+ + + +
+ )} +
+ + {/* Info */} +
+
+

{name}

+
+ + {/* Stats */} +
+ {followers > 0 && ( +
+ {formatNumber(followers)} followers +
+ )} + {monthlyListeners > 0 && ( +
+ {formatNumber(monthlyListeners)} monthly listeners +
+ )} +
+ + {/* Bio */} + {bio && ( +

+ {bio} +

+ )} + + {/* Follow Button */} + {onFollowToggle && ( + + )} +
+
+
+
+ ) +} diff --git a/components/ArtistRoster.tsx b/components/ArtistRoster.tsx new file mode 100644 index 0000000..9c93ea8 --- /dev/null +++ b/components/ArtistRoster.tsx @@ -0,0 +1,73 @@ +'use client' + +import { ArtistCard } from './ArtistCard' + +export interface ArtistRosterProps { + artists: Array<{ + id: string + name: string + user?: { + avatarUrl?: string + } + _count?: { + followers?: number + } + verified?: boolean + }> + isOwner?: boolean + emptyMessage?: string + onRemoveArtist?: (artistId: string) => void + onArtistClick?: (artistId: string) => void +} + +export function ArtistRoster({ + artists, + isOwner = false, + emptyMessage = 'No artists signed yet', + onRemoveArtist, + onArtistClick +}: ArtistRosterProps) { + if (artists.length === 0) { + return ( +
+ + + +

{emptyMessage}

+
+ ) + } + + return ( +
+ {artists.map((artist) => ( +
+ onArtistClick?.(artist.id)} + /> + + {/* Remove Button (only visible for owners) */} + {isOwner && onRemoveArtist && ( + + )} +
+ ))} +
+ ) +} diff --git a/components/AudioPlayer.tsx b/components/AudioPlayer.tsx new file mode 100644 index 0000000..ea83a82 --- /dev/null +++ b/components/AudioPlayer.tsx @@ -0,0 +1,164 @@ +'use client' + +import { useState, useRef, useEffect } from 'react' + +interface AudioPlayerProps { + songId: string + songTitle: string + artistName: string + coverUrl?: string + audioUrl: string + onPlayCountIncrement?: () => void +} + +export function AudioPlayer({ + songId, + songTitle, + artistName, + coverUrl, + audioUrl, + onPlayCountIncrement +}: AudioPlayerProps) { + const [isPlaying, setIsPlaying] = useState(false) + const [currentTime, setCurrentTime] = useState(0) + const [duration, setDuration] = useState(0) + const [volume, setVolume] = useState(0.8) + const audioRef = useRef(null) + + useEffect(() => { + if (audioRef.current) { + audioRef.current.volume = volume + } + }, [volume]) + + useEffect(() => { + const audio = audioRef.current + if (!audio) return + + const updateTime = () => setCurrentTime(audio.currentTime) + const updateDuration = () => setDuration(audio.duration) + + audio.addEventListener('timeupdate', updateTime) + audio.addEventListener('loadedmetadata', updateDuration) + + return () => { + audio.removeEventListener('timeupdate', updateTime) + audio.removeEventListener('loadedmetadata', updateDuration) + } + }, []) + + const togglePlay = () => { + if (audioRef.current) { + if (isPlaying) { + audioRef.current.pause() + } else { + audioRef.current.play() + if (onPlayCountIncrement && currentTime === 0) { + onPlayCountIncrement() + } + } + setIsPlaying(!isPlaying) + } + } + + const handleSeek = (e: React.ChangeEvent) => { + const newTime = parseFloat(e.target.value) + setCurrentTime(newTime) + if (audioRef.current) { + audioRef.current.currentTime = newTime + } + } + + const handleVolumeChange = (e: React.ChangeEvent) => { + setVolume(parseFloat(e.target.value)) + } + + const formatTime = (time: number) => { + const minutes = Math.floor(time / 60) + const seconds = Math.floor(time % 60) + return `${minutes}:${seconds.toString().padStart(2, '0')}` + } + + return ( +
+
+ ) +} diff --git a/components/AuthForm.tsx b/components/AuthForm.tsx new file mode 100644 index 0000000..4536e90 --- /dev/null +++ b/components/AuthForm.tsx @@ -0,0 +1,209 @@ +'use client' + +import { useState } from 'react' + +export type AuthMode = 'login' | 'register' | 'forgot-password' + +export interface AuthFormProps { + mode: AuthMode + onSubmit: (data: AuthFormData) => void | Promise + isLoading?: boolean + error?: string + onModeChange?: (mode: AuthMode) => void +} + +export interface AuthFormData { + email: string + password?: string + username?: string + confirmPassword?: string +} + +export function AuthForm({ + mode, + onSubmit, + isLoading = false, + error, + onModeChange +}: AuthFormProps) { + const [formData, setFormData] = useState({ + email: '', + password: '', + username: '', + confirmPassword: '' + }) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + onSubmit(formData) + } + + const titles = { + login: 'Welcome back', + register: 'Create your account', + 'forgot-password': 'Reset your password' + } + + const buttonTexts = { + login: 'Sign in', + register: 'Create account', + 'forgot-password': 'Send reset link' + } + + return ( +
+
+ {/* Logo/Title */} +
+
+ + + +
+

{titles[mode]}

+
+ + {/* Error Message */} + {error && ( +
+ {error} +
+ )} + + {/* Form */} +
+ {/* Username (Register only) */} + {mode === 'register' && ( +
+ + setFormData({ ...formData, username: e.target.value })} + className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-purple-500" + placeholder="Choose a username" + required + /> +
+ )} + + {/* Email */} +
+ + setFormData({ ...formData, email: e.target.value })} + className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-purple-500" + placeholder="your@email.com" + required + /> +
+ + {/* Password (not in forgot-password) */} + {mode !== 'forgot-password' && ( +
+ + setFormData({ ...formData, password: e.target.value })} + className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-purple-500" + placeholder="••••••••" + required + /> +
+ )} + + {/* Confirm Password (Register only) */} + {mode === 'register' && ( +
+ + setFormData({ ...formData, confirmPassword: e.target.value })} + className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-purple-500" + placeholder="••••••••" + required + /> +
+ )} + + {/* Forgot Password Link (Login only) */} + {mode === 'login' && onModeChange && ( +
+ +
+ )} + + {/* Submit Button */} + +
+ + {/* Mode Switch Links */} + {onModeChange && ( +
+ {mode === 'login' && ( +

+ Don't have an account?{' '} + +

+ )} + {mode === 'register' && ( +

+ Already have an account?{' '} + +

+ )} + {mode === 'forgot-password' && ( +

+ Remember your password?{' '} + +

+ )} +
+ )} +
+
+ ) +} diff --git a/components/AvatarUpload.tsx b/components/AvatarUpload.tsx new file mode 100644 index 0000000..9071684 --- /dev/null +++ b/components/AvatarUpload.tsx @@ -0,0 +1,130 @@ +'use client' + +import { useState, useRef } from 'react' + +export interface AvatarUploadProps { + currentAvatarUrl?: string + onUpload: (file: File) => void | Promise + isLoading?: boolean + size?: 'sm' | 'md' | 'lg' +} + +export function AvatarUpload({ + currentAvatarUrl, + onUpload, + isLoading = false, + size = 'lg' +}: AvatarUploadProps) { + const [preview, setPreview] = useState(null) + const [isDragging, setIsDragging] = useState(false) + const fileInputRef = useRef(null) + + const sizeClasses = { + sm: 'w-24 h-24', + md: 'w-32 h-32', + lg: 'w-40 h-40' + } + + const handleFileSelect = (file: File) => { + if (file && file.type.startsWith('image/')) { + const reader = new FileReader() + reader.onload = (e) => { + setPreview(e.target?.result as string) + } + reader.readAsDataURL(file) + onUpload(file) + } + } + + const handleInputChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (file) handleFileSelect(file) + } + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault() + setIsDragging(true) + } + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault() + setIsDragging(false) + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + setIsDragging(false) + const file = e.dataTransfer.files?.[0] + if (file) handleFileSelect(file) + } + + const displayUrl = preview || currentAvatarUrl + + return ( +
+ {/* Avatar Preview */} +
+ {displayUrl ? ( + Avatar + ) : ( +
+ + + +
+ )} + + {/* Upload Overlay */} +
+ {isLoading ? ( +
+ ) : ( + + + + + )} +
+
+ + {/* Upload Button */} +
+ + +

+ Click or drag and drop an image +

+

+ JPG, PNG, GIF up to 5MB +

+
+
+ ) +} diff --git a/components/CreatePlaylistModal.tsx b/components/CreatePlaylistModal.tsx new file mode 100644 index 0000000..8fa5252 --- /dev/null +++ b/components/CreatePlaylistModal.tsx @@ -0,0 +1,172 @@ +'use client' + +import { useState } from 'react' + +export interface CreatePlaylistData { + title: string + description?: string + isPublic: boolean +} + +export interface CreatePlaylistModalProps { + isOpen: boolean + onClose: () => void + onSubmit: (data: CreatePlaylistData) => void | Promise + isLoading?: boolean +} + +export function CreatePlaylistModal({ + isOpen, + onClose, + onSubmit, + isLoading = false +}: CreatePlaylistModalProps) { + const [formData, setFormData] = useState({ + title: '', + description: '', + isPublic: true + }) + + if (!isOpen) return null + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (formData.title.trim()) { + onSubmit(formData) + } + } + + const handleClose = () => { + if (!isLoading) { + setFormData({ title: '', description: '', isPublic: true }) + onClose() + } + } + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+

Create Playlist

+ +
+ + {/* Form */} +
+ {/* Title */} +
+ + setFormData({ ...formData, title: e.target.value })} + placeholder="My awesome playlist" + className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-purple-500" + required + autoFocus + /> +
+ + {/* Description */} +
+ +