Fix YouTube Music import (run sync ytmusicapi in thread), remove all Spotify UI
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
import asyncio
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -50,9 +53,12 @@ async def import_youtube_playlist(
|
|||||||
detail="Free tier limited to 1 playlist. Upgrade to Pro for unlimited.",
|
detail="Free tier limited to 1 playlist. Upgrade to Pro for unlimited.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fetch tracks from YouTube Music
|
# Fetch tracks from YouTube Music (run sync ytmusicapi in thread)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
try:
|
try:
|
||||||
playlist_name, playlist_image, raw_tracks = get_playlist_tracks(data.url)
|
playlist_name, playlist_image, raw_tracks = await loop.run_in_executor(
|
||||||
|
None, partial(get_playlist_tracks, data.url)
|
||||||
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -102,8 +108,11 @@ async def search_youtube_music(
|
|||||||
if not data.query.strip():
|
if not data.query.strip():
|
||||||
raise HTTPException(status_code=400, detail="Query cannot be empty")
|
raise HTTPException(status_code=400, detail="Query cannot be empty")
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
try:
|
try:
|
||||||
results = search_track(data.query.strip())
|
results = await loop.run_in_executor(
|
||||||
|
None, partial(search_track, data.query.strip())
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(status_code=500, detail="Failed to search YouTube Music")
|
raise HTTPException(status_code=500, detail="Failed to search YouTube Music")
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import Layout from './components/Layout'
|
|||||||
import Landing from './pages/Landing'
|
import Landing from './pages/Landing'
|
||||||
import Login from './pages/Login'
|
import Login from './pages/Login'
|
||||||
import Register from './pages/Register'
|
import Register from './pages/Register'
|
||||||
import SpotifyCallback from './pages/SpotifyCallback'
|
|
||||||
import Dashboard from './pages/Dashboard'
|
import Dashboard from './pages/Dashboard'
|
||||||
import Playlists from './pages/Playlists'
|
import Playlists from './pages/Playlists'
|
||||||
import PlaylistDetail from './pages/PlaylistDetail'
|
import PlaylistDetail from './pages/PlaylistDetail'
|
||||||
@@ -33,7 +32,6 @@ function AppRoutes() {
|
|||||||
<Route path="/" element={<RootRedirect />} />
|
<Route path="/" element={<RootRedirect />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
<Route path="/auth/spotify/callback" element={<SpotifyCallback />} />
|
|
||||||
<Route
|
<Route
|
||||||
path="/dashboard"
|
path="/dashboard"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import { ListMusic, Heart, Sparkles, Compass, Loader2, Music, CheckCircle2, XCircle } from 'lucide-react'
|
import { ListMusic, Heart, Sparkles, Compass, Loader2, Music } from 'lucide-react'
|
||||||
import { useAuth } from '../lib/auth'
|
import { useAuth } from '../lib/auth'
|
||||||
import { getPlaylists, getRecommendationHistory, getSavedRecommendations, generateRecommendations, type RecommendationItem, type PlaylistResponse } from '../lib/api'
|
import { getPlaylists, getRecommendationHistory, getSavedRecommendations, generateRecommendations, type RecommendationItem, type PlaylistResponse } from '../lib/api'
|
||||||
import RecommendationCard from '../components/RecommendationCard'
|
import RecommendationCard from '../components/RecommendationCard'
|
||||||
@@ -162,27 +162,6 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Connected Accounts */}
|
|
||||||
<div className="bg-white rounded-2xl border border-purple-100 p-6">
|
|
||||||
<h2 className="text-lg font-semibold text-charcoal mb-4">Connected Accounts</h2>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex items-center gap-3 px-4 py-3 bg-cream rounded-xl flex-1">
|
|
||||||
<svg className="w-5 h-5 text-[#1DB954]" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-sm font-medium text-charcoal">Spotify</span>
|
|
||||||
{user?.spotify_connected ? (
|
|
||||||
<CheckCircle2 className="w-4 h-4 text-green-500 ml-auto" />
|
|
||||||
) : (
|
|
||||||
<XCircle className="w-4 h-4 text-charcoal-muted/40 ml-auto" />
|
|
||||||
)}
|
|
||||||
<span className="text-xs text-charcoal-muted">
|
|
||||||
{user?.spotify_connected ? 'Connected' : 'Not connected'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Recent Recommendations */}
|
{/* Recent Recommendations */}
|
||||||
{recentRecs.length > 0 && (
|
{recentRecs.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const features = [
|
|||||||
{
|
{
|
||||||
icon: ListMusic,
|
icon: ListMusic,
|
||||||
title: 'Import Your Music',
|
title: 'Import Your Music',
|
||||||
description: 'Connect Spotify and import your playlists to build your taste profile.',
|
description: 'Import your playlists from YouTube Music, Last.fm, or paste your songs to build your taste profile.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Sparkles,
|
icon: Sparkles,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState } from 'react'
|
|||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import { Disc3, Mail, Lock, Loader2 } from 'lucide-react'
|
import { Disc3, Mail, Lock, Loader2 } from 'lucide-react'
|
||||||
import { useAuth } from '../lib/auth'
|
import { useAuth } from '../lib/auth'
|
||||||
import { login as apiLogin, getSpotifyAuthUrl } from '../lib/api'
|
import { login as apiLogin } from '../lib/api'
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
@@ -28,15 +28,6 @@ export default function Login() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSpotifyLogin = async () => {
|
|
||||||
try {
|
|
||||||
const { url } = await getSpotifyAuthUrl()
|
|
||||||
window.location.href = url
|
|
||||||
} catch {
|
|
||||||
setError('Could not connect to Spotify')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-cream flex flex-col">
|
<div className="min-h-screen bg-cream flex flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -113,22 +104,6 @@ export default function Login() {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="my-6 flex items-center gap-3">
|
|
||||||
<div className="flex-1 h-px bg-purple-100" />
|
|
||||||
<span className="text-xs text-charcoal-muted uppercase tracking-wider">or</span>
|
|
||||||
<div className="flex-1 h-px bg-purple-100" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleSpotifyLogin}
|
|
||||||
className="w-full py-3 bg-[#1DB954] text-white font-semibold rounded-xl hover:bg-[#1aa34a] transition-colors cursor-pointer border-none text-sm flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z" />
|
|
||||||
</svg>
|
|
||||||
Continue with Spotify
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<p className="text-center mt-6 text-sm text-charcoal-muted">
|
<p className="text-center mt-6 text-sm text-charcoal-muted">
|
||||||
Don't have an account?{' '}
|
Don't have an account?{' '}
|
||||||
<Link to="/register" className="text-purple font-medium hover:underline">
|
<Link to="/register" className="text-purple font-medium hover:underline">
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ListMusic, Plus, Loader2, Music, ChevronRight, Download, X, Play, Link2, ClipboardPaste } from 'lucide-react'
|
import { ListMusic, Loader2, Music, ChevronRight, Download, X, Play, Link2, ClipboardPaste } from 'lucide-react'
|
||||||
import { getPlaylists, getSpotifyPlaylists, importSpotifyPlaylist, importYouTubePlaylist, previewLastfm, importLastfm, importPastedSongs, type PlaylistResponse, type SpotifyPlaylistItem, type LastfmPreviewResponse } from '../lib/api'
|
import { getPlaylists, importYouTubePlaylist, previewLastfm, importLastfm, importPastedSongs, type PlaylistResponse, type LastfmPreviewResponse } from '../lib/api'
|
||||||
|
|
||||||
export default function Playlists() {
|
export default function Playlists() {
|
||||||
const [playlists, setPlaylists] = useState<PlaylistResponse[]>([])
|
const [playlists, setPlaylists] = useState<PlaylistResponse[]>([])
|
||||||
const [spotifyPlaylists, setSpotifyPlaylists] = useState<SpotifyPlaylistItem[]>([])
|
|
||||||
const [showImport, setShowImport] = useState(false)
|
|
||||||
const [showYouTubeImport, setShowYouTubeImport] = useState(false)
|
const [showYouTubeImport, setShowYouTubeImport] = useState(false)
|
||||||
const [youtubeUrl, setPlayUrl] = useState('')
|
const [youtubeUrl, setPlayUrl] = useState('')
|
||||||
const [importingYouTube, setImportingYouTube] = useState(false)
|
const [importingYouTube, setImportingYouTube] = useState(false)
|
||||||
const [importing, setImporting] = useState<string | null>(null)
|
|
||||||
const [loadingSpotify, setLoadingSpotify] = useState(false)
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [showLastfmImport, setShowLastfmImport] = useState(false)
|
const [showLastfmImport, setShowLastfmImport] = useState(false)
|
||||||
@@ -40,19 +36,6 @@ export default function Playlists() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openImportModal = async () => {
|
|
||||||
setShowImport(true)
|
|
||||||
setLoadingSpotify(true)
|
|
||||||
try {
|
|
||||||
const data = await getSpotifyPlaylists()
|
|
||||||
setSpotifyPlaylists(data)
|
|
||||||
} catch {
|
|
||||||
setError('Failed to load Spotify playlists. Make sure your Spotify account is connected.')
|
|
||||||
} finally {
|
|
||||||
setLoadingSpotify(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleYouTubeImport = async () => {
|
const handleYouTubeImport = async () => {
|
||||||
if (!youtubeUrl.trim()) return
|
if (!youtubeUrl.trim()) return
|
||||||
setImportingYouTube(true)
|
setImportingYouTube(true)
|
||||||
@@ -122,19 +105,6 @@ export default function Playlists() {
|
|||||||
.split('\n')
|
.split('\n')
|
||||||
.filter((line) => line.trim().length > 0).length
|
.filter((line) => line.trim().length > 0).length
|
||||||
|
|
||||||
const handleImport = async (playlistId: string) => {
|
|
||||||
setImporting(playlistId)
|
|
||||||
try {
|
|
||||||
const imported = await importSpotifyPlaylist(playlistId)
|
|
||||||
setPlaylists((prev) => [...prev, imported])
|
|
||||||
setSpotifyPlaylists((prev) => prev.filter((p) => p.id !== playlistId))
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.response?.data?.detail || 'Failed to import playlist')
|
|
||||||
} finally {
|
|
||||||
setImporting(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center py-20">
|
<div className="flex items-center justify-center py-20">
|
||||||
@@ -151,13 +121,6 @@ export default function Playlists() {
|
|||||||
<p className="text-charcoal-muted mt-1">Manage your imported playlists</p>
|
<p className="text-charcoal-muted mt-1">Manage your imported playlists</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
|
||||||
onClick={openImportModal}
|
|
||||||
className="flex items-center gap-2 px-5 py-2.5 bg-purple text-white font-medium rounded-xl hover:bg-purple-dark transition-colors cursor-pointer border-none text-sm"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
Import from Spotify
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowYouTubeImport(true)}
|
onClick={() => setShowYouTubeImport(true)}
|
||||||
className="flex items-center gap-2 px-5 py-2.5 bg-red-600 text-white font-medium rounded-xl hover:bg-red-700 transition-colors cursor-pointer border-none text-sm"
|
className="flex items-center gap-2 px-5 py-2.5 bg-red-600 text-white font-medium rounded-xl hover:bg-red-700 transition-colors cursor-pointer border-none text-sm"
|
||||||
@@ -196,16 +159,9 @@ export default function Playlists() {
|
|||||||
</div>
|
</div>
|
||||||
<h2 className="text-xl font-semibold text-charcoal mb-2">No playlists yet</h2>
|
<h2 className="text-xl font-semibold text-charcoal mb-2">No playlists yet</h2>
|
||||||
<p className="text-charcoal-muted mb-6 max-w-md mx-auto">
|
<p className="text-charcoal-muted mb-6 max-w-md mx-auto">
|
||||||
Import your playlists from Spotify or YouTube Music to start getting personalized music recommendations
|
Import your playlists from YouTube Music, Last.fm, or paste your songs to start getting personalized music recommendations
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-center gap-3">
|
<div className="flex items-center justify-center gap-3">
|
||||||
<button
|
|
||||||
onClick={openImportModal}
|
|
||||||
className="inline-flex items-center gap-2 px-6 py-3 bg-purple text-white font-medium rounded-xl hover:bg-purple-dark transition-colors cursor-pointer border-none text-sm"
|
|
||||||
>
|
|
||||||
<Download className="w-4 h-4" />
|
|
||||||
Import from Spotify
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowYouTubeImport(true)}
|
onClick={() => setShowYouTubeImport(true)}
|
||||||
className="inline-flex items-center gap-2 px-6 py-3 bg-red-600 text-white font-medium rounded-xl hover:bg-red-700 transition-colors cursor-pointer border-none text-sm"
|
className="inline-flex items-center gap-2 px-6 py-3 bg-red-600 text-white font-medium rounded-xl hover:bg-red-700 transition-colors cursor-pointer border-none text-sm"
|
||||||
@@ -488,73 +444,6 @@ export default function Playlists() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Import Modal */}
|
|
||||||
{showImport && (
|
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className="bg-white rounded-2xl shadow-xl max-w-lg w-full max-h-[80vh] overflow-hidden">
|
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-purple-100">
|
|
||||||
<h2 className="text-lg font-semibold text-charcoal">Import from Spotify</h2>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowImport(false)}
|
|
||||||
className="p-1.5 rounded-lg hover:bg-purple-50 transition-colors cursor-pointer bg-transparent border-none"
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5 text-charcoal-muted" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="overflow-y-auto max-h-[60vh] p-4">
|
|
||||||
{loadingSpotify ? (
|
|
||||||
<div className="flex items-center justify-center py-12">
|
|
||||||
<Loader2 className="w-6 h-6 text-purple animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : spotifyPlaylists.length === 0 ? (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<p className="text-charcoal-muted">No playlists found on Spotify</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{spotifyPlaylists.map((sp) => (
|
|
||||||
<div
|
|
||||||
key={sp.id}
|
|
||||||
className="flex items-center gap-3 p-3 rounded-xl hover:bg-purple-50 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-purple-200 to-purple-400 flex-shrink-0 flex items-center justify-center overflow-hidden">
|
|
||||||
{sp.image_url ? (
|
|
||||||
<img
|
|
||||||
src={sp.image_url}
|
|
||||||
alt={sp.name}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Music className="w-5 h-5 text-white/80" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="font-medium text-charcoal text-sm truncate">{sp.name}</p>
|
|
||||||
<p className="text-xs text-charcoal-muted">
|
|
||||||
{sp.track_count} tracks · {sp.owner}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => handleImport(sp.id)}
|
|
||||||
disabled={importing === sp.id}
|
|
||||||
className="px-4 py-2 bg-purple text-white text-xs font-medium rounded-lg hover:bg-purple-dark transition-colors cursor-pointer border-none disabled:opacity-50 flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
{importing === sp.id ? (
|
|
||||||
<Loader2 className="w-3 h-3 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Download className="w-3 h-3" />
|
|
||||||
)}
|
|
||||||
Import
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState } from 'react'
|
|||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import { Disc3, Mail, Lock, User, Loader2 } from 'lucide-react'
|
import { Disc3, Mail, Lock, User, Loader2 } from 'lucide-react'
|
||||||
import { useAuth } from '../lib/auth'
|
import { useAuth } from '../lib/auth'
|
||||||
import { register as apiRegister, getSpotifyAuthUrl } from '../lib/api'
|
import { register as apiRegister } from '../lib/api'
|
||||||
|
|
||||||
export default function Register() {
|
export default function Register() {
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
@@ -29,15 +29,6 @@ export default function Register() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSpotifyLogin = async () => {
|
|
||||||
try {
|
|
||||||
const { url } = await getSpotifyAuthUrl()
|
|
||||||
window.location.href = url
|
|
||||||
} catch {
|
|
||||||
setError('Could not connect to Spotify')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-cream flex flex-col">
|
<div className="min-h-screen bg-cream flex flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -133,22 +124,6 @@ export default function Register() {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="my-6 flex items-center gap-3">
|
|
||||||
<div className="flex-1 h-px bg-purple-100" />
|
|
||||||
<span className="text-xs text-charcoal-muted uppercase tracking-wider">or</span>
|
|
||||||
<div className="flex-1 h-px bg-purple-100" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleSpotifyLogin}
|
|
||||||
className="w-full py-3 bg-[#1DB954] text-white font-semibold rounded-xl hover:bg-[#1aa34a] transition-colors cursor-pointer border-none text-sm flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z" />
|
|
||||||
</svg>
|
|
||||||
Continue with Spotify
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<p className="text-center mt-6 text-sm text-charcoal-muted">
|
<p className="text-center mt-6 text-sm text-charcoal-muted">
|
||||||
Already have an account?{' '}
|
Already have an account?{' '}
|
||||||
<Link to="/login" className="text-purple font-medium hover:underline">
|
<Link to="/login" className="text-purple font-medium hover:underline">
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default function TasteProfilePage() {
|
|||||||
<h2 className="text-lg font-semibold text-charcoal mb-5">Genre DNA</h2>
|
<h2 className="text-lg font-semibold text-charcoal mb-5">Genre DNA</h2>
|
||||||
{profile.genre_breakdown.length === 0 ? (
|
{profile.genre_breakdown.length === 0 ? (
|
||||||
<p className="text-charcoal-muted text-sm">
|
<p className="text-charcoal-muted text-sm">
|
||||||
No genre data yet. Import playlists with Spotify to see your genre breakdown.
|
No genre data yet. Import playlists to see your genre breakdown.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user