Initial MVP: full-stack music discovery app

Backend (FastAPI):
- User auth with email/password and Spotify OAuth
- Spotify playlist import with audio feature extraction
- AI recommendation engine using Claude API with taste profiling
- Save/bookmark recommendations
- Rate limiting for free tier (10 recs/day, 1 playlist)
- PostgreSQL models with Alembic migrations
- Redis-ready configuration

Frontend (React 19 + TypeScript + Vite + Tailwind):
- Landing page, auth flows (email + Spotify OAuth)
- Dashboard with stats and quick discover
- Playlist management and import from Spotify
- Discover page with custom query support
- Recommendation cards with explanations and save toggle
- Taste profile visualization
- Responsive layout with mobile navigation
- PWA-ready configuration

Infrastructure:
- Docker Compose with PostgreSQL, Redis, backend, frontend
- Environment-based configuration
This commit is contained in:
root
2026-03-30 15:53:39 -05:00
commit 155cbd1bbf
62 changed files with 7536 additions and 0 deletions

159
frontend/src/lib/api.ts Normal file
View File

@@ -0,0 +1,159 @@
import axios from 'axios'
const api = axios.create({
baseURL: '/api',
headers: {
'Content-Type': 'application/json',
},
})
api.interceptors.request.use((config) => {
const token = localStorage.getItem('vynl_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('vynl_token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
// Types
export interface TokenResponse {
access_token: string
token_type: string
}
export interface UserResponse {
id: string
email: string
name: string
is_pro: boolean
daily_recommendations_remaining: number
spotify_connected: boolean
created_at: string
}
export interface PlaylistResponse {
id: string
name: string
source: string
track_count: number
image_url: string | null
created_at: string
}
export interface TrackItem {
id: string
title: string
artist: string
album: string
duration_ms: number
image_url: string | null
spotify_url: string | null
}
export interface TasteProfile {
top_genres: { name: string; weight: number }[]
energy: number
mood: number
danceability: number
acousticness: number
instrumentalness: number
}
export interface PlaylistDetailResponse {
id: string
name: string
source: string
track_count: number
image_url: string | null
tracks: TrackItem[]
taste_profile: TasteProfile | null
created_at: string
}
export interface SpotifyPlaylistItem {
id: string
name: string
track_count: number
image_url: string | null
owner: string
}
export interface RecommendationItem {
id: string
title: string
artist: string
album: string
image_url: string | null
spotify_url: string | null
reason: string
saved: boolean
created_at: string
}
export interface RecommendationResponse {
recommendations: RecommendationItem[]
remaining_today: number
}
// Auth
export const register = (email: string, name: string, password: string) =>
api.post<TokenResponse>('/auth/register', { email, name, password }).then((r) => r.data)
export const login = (email: string, password: string) =>
api.post<TokenResponse>('/auth/login', { email, password }).then((r) => r.data)
export const getMe = () =>
api.get<UserResponse>('/auth/me').then((r) => r.data)
// Spotify OAuth
export const getSpotifyAuthUrl = () =>
api.get<{ url: string; state: string }>('/auth/spotify/authorize').then((r) => r.data)
export const spotifyCallback = (code: string) =>
api.post<TokenResponse>('/auth/spotify/callback', { code }).then((r) => r.data)
// Playlists
export const getPlaylists = () =>
api.get<PlaylistResponse[]>('/playlists').then((r) => r.data)
export const getPlaylist = (id: string) =>
api.get<PlaylistDetailResponse>(`/playlists/${id}`).then((r) => r.data)
export const deletePlaylist = (id: string) =>
api.delete(`/playlists/${id}`).then((r) => r.data)
// Spotify Import
export const getSpotifyPlaylists = () =>
api.get<SpotifyPlaylistItem[]>('/spotify/playlists').then((r) => r.data)
export const importSpotifyPlaylist = (playlistId: string) =>
api.post<PlaylistDetailResponse>('/spotify/import', { playlist_id: playlistId }).then((r) => r.data)
// Recommendations
export const generateRecommendations = (playlistId?: string, query?: string) =>
api.post<RecommendationResponse>('/recommendations/generate', {
playlist_id: playlistId,
query,
}).then((r) => r.data)
export const getRecommendationHistory = () =>
api.get<RecommendationItem[]>('/recommendations/history').then((r) => r.data)
export const getSavedRecommendations = () =>
api.get<RecommendationItem[]>('/recommendations/saved').then((r) => r.data)
export const toggleSaveRecommendation = (id: string) =>
api.post<{ saved: boolean }>(`/recommendations/${id}/toggle-save`).then((r) => r.data)
export default api