Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/frontend/src/pages/LastFm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('LastFmPage', () => {
test('shows loading spinner initially', () => {
vi.mocked(api.lastfm.status).mockReturnValue(new Promise(() => {}))
renderPage()
expect(document.querySelector('.animate-spin')).toBeInTheDocument()
expect(document.querySelector('.animate-pulse')).toBeInTheDocument()
})

test('shows not configured when lastfm is not set up', async () => {
Expand Down Expand Up @@ -66,7 +66,7 @@ describe('LastFmPage', () => {
expect(screen.getByText('Connect Your Account')).toBeInTheDocument()
})

expect(screen.getByText('Connect Last.fm')).toBeInTheDocument()
expect(screen.getByText('Connect with Last.fm')).toBeInTheDocument()
})

test('shows error on fetch failure', async () => {
Expand Down
195 changes: 122 additions & 73 deletions packages/frontend/src/pages/LastFm.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useCallback, useEffect, useState } from 'react'
import { ExternalLink, Link2, Loader2, Music, Unlink } from 'lucide-react'
import { Check, ExternalLink, Link2, Loader2, Music, X } from 'lucide-react'
import { api } from '@/services/api'
import SectionHeader from '@/components/ui/SectionHeader'
import ActionPanel from '@/components/ui/ActionPanel'

interface LastFmStatus {
configured: boolean
Expand Down Expand Up @@ -57,8 +56,18 @@

if (isLoading) {
return (
<div className='flex h-64 items-center justify-center'>
<Loader2 className='h-6 w-6 animate-spin text-lucky-text-secondary' />
<div className='space-y-6'>
<SectionHeader
eyebrow='Music identity'
title='Last.fm'
description='Scrobble tracks you play to your Last.fm profile'
actions={<Music className='h-5 w-5 text-lucky-accent' />}
/>
<div className='surface-panel space-y-4 p-8'>
<div className='h-24 animate-pulse rounded-lg bg-lucky-bg-tertiary' />
<div className='h-4 w-2/3 animate-pulse rounded bg-lucky-bg-tertiary' />
<div className='h-4 w-1/2 animate-pulse rounded bg-lucky-bg-tertiary' />
</div>
</div>
)
}
Expand All @@ -73,118 +82,158 @@
/>

{error && (
<div className='rounded-xl border border-lucky-error/30 bg-lucky-error/10 px-4 py-3 type-body-sm text-lucky-error'>
{error}
<div className='flex items-start gap-3 rounded-xl border border-lucky-error/40 bg-lucky-error/10 p-4'>
<X className='mt-0.5 h-5 w-5 flex-shrink-0 text-lucky-error' />
<div className='flex-1'>
<p className='type-body-sm text-lucky-text-primary font-500'>
{error}
</p>
</div>
</div>
)}

{!status?.configured ? (
<section className='surface-panel space-y-3 p-6'>
<h2 className='type-h2 text-lucky-text-primary'>Not Configured</h2>
<p className='type-body-sm text-lucky-text-secondary'>
Last.fm integration is not configured on this bot. The server owner needs
to set
<code className='mx-1 rounded bg-lucky-bg-tertiary px-1.5 py-0.5 text-xs'>
LASTFM_API_KEY
</code>
and
<code className='mx-1 rounded bg-lucky-bg-tertiary px-1.5 py-0.5 text-xs'>
LASTFM_API_SECRET
</code>
.
</p>
<section className='surface-panel space-y-4 rounded-xl border border-lucky-border/20 p-8'>
<div className='flex items-start gap-4'>
<div className='rounded-lg bg-lucky-bg-tertiary p-3'>
<Music className='h-6 w-6 text-lucky-text-secondary' />
</div>
<div className='flex-1'>
<h2 className='type-h2 text-lucky-text-primary'>
Not Configured
</h2>
<p className='type-body-sm text-lucky-text-secondary mt-2'>
Last.fm integration is not configured. Ask the server owner to set:
</p>
<div className='mt-3 space-y-2 text-xs'>
<code className='block rounded bg-lucky-bg-active px-2 py-1 text-lucky-text-body font-600'>
LASTFM_API_KEY
</code>
<code className='block rounded bg-lucky-bg-active px-2 py-1 text-lucky-text-body font-600'>
LASTFM_API_SECRET
</code>
</div>
</div>
</div>
</section>
) : status.linked ? (
<section className='surface-panel space-y-4 p-6'>
<div className='flex items-start gap-3'>
<span className='rounded-full bg-lucky-success/20 p-3 text-lucky-success'>
<Link2 className='h-5 w-5' />
</span>
<div>
<h2 className='type-h2 text-lucky-text-primary'>Connected</h2>
<p className='type-body-sm text-lucky-text-secondary'>
Linked as
<section className='surface-panel space-y-6 rounded-xl border border-lucky-border/20 p-8'>
<div className='flex items-start gap-4'>
<div className='rounded-full bg-lucky-success/20 p-3'>
<Check className='h-6 w-6 text-lucky-success' />
</div>
<div className='flex-1'>
<h2 className='type-h2 text-lucky-text-primary'>
Connected
</h2>
<p className='type-body-sm text-lucky-text-secondary mt-1'>
Linked as{' '}
<a
href={`https://www.last.fm/user/${status.username}`}
target='_blank'
rel='noopener noreferrer'
className='ml-1 inline-flex items-center gap-1 text-lucky-accent hover:text-lucky-accent-soft'
className='inline-flex items-center gap-1 font-500 text-lucky-accent hover:text-lucky-accent-soft transition-colors lucky-focus-visible'
>
{status.username}
<ExternalLink className='h-3.5 w-3.5' />
<ExternalLink className='h-3 w-3' />
</a>
</p>
</div>
<div className='rounded-full bg-lucky-success/15 px-3 py-1 text-xs font-600 text-lucky-success'>
Active
</div>
</div>

<p className='type-body-sm text-lucky-text-secondary'>
<p className='type-body-sm text-lucky-text-secondary border-t border-lucky-border/20 pt-4'>
Tracks you request via the bot will be scrobbled to your Last.fm profile
automatically.
</p>

<button
onClick={handleUnlink}
disabled={isUnlinking}
className='lucky-focus-visible inline-flex items-center gap-2 rounded-lg border border-lucky-error/30 bg-lucky-error/10 px-4 py-2 type-body-sm text-lucky-error transition-colors hover:bg-lucky-error/20 disabled:opacity-60'
className='inline-flex items-center gap-2 text-sm font-500 text-lucky-error hover:text-lucky-error transition-colors lucky-focus-visible disabled:opacity-60 disabled:cursor-not-allowed'
>
{isUnlinking ? (
<Loader2 className='h-4 w-4 animate-spin' />
) : (
<Unlink className='h-4 w-4' />
<X className='h-4 w-4' />
)}
Disconnect
</button>
</section>
) : (
<section className='surface-panel space-y-4 p-6'>
<h2 className='type-h2 text-lucky-text-primary'>Connect Your Account</h2>
<p className='type-body-sm text-lucky-text-secondary'>
Link your Last.fm account so tracks you play through the bot are
automatically scrobbled to your profile.
</p>
<section className='surface-panel space-y-6 rounded-xl border border-lucky-border/20 p-8'>
<div>
<h2 className='type-h2 text-lucky-text-primary'>
Connect Your Account
</h2>
<p className='type-body-sm text-lucky-text-secondary mt-2'>
Link your Last.fm account so tracks you play through the bot are
automatically scrobbled to your profile.
</p>
</div>

<button
onClick={handleConnect}
className='lucky-focus-visible inline-flex items-center gap-2 rounded-md bg-lucky-brand px-4 py-2 type-body-sm text-white transition-colors hover:bg-lucky-brand-strong'
className='inline-flex items-center gap-2 rounded-lg bg-lucky-accent px-6 py-3 type-body-sm font-600 text-white transition-all hover:bg-lucky-accent-soft active:scale-95 lucky-focus-visible'
>
<Link2 className='h-4 w-4' />
Connect Last.fm
Connect with Last.fm
</button>
</section>
)}

<div className='grid gap-4 lg:grid-cols-2'>
<ActionPanel
title='Scrobble coverage'
description='Lucky tracks requested songs and forwards play activity once linked.'
icon={<Music className='h-4 w-4' />}
/>
<ActionPanel
title='Privacy control'
description='You can disconnect anytime without affecting server playback.'
icon={<Unlink className='h-4 w-4' />}
/>
<div className='grid gap-4 md:grid-cols-2'>
<div className='surface-panel rounded-xl border border-lucky-border/20 p-6'>
<div className='flex items-start gap-3'>
<Music className='mt-1 h-5 w-5 text-lucky-accent flex-shrink-0' />
<div className='flex-1'>
<h3 className='type-title text-lucky-text-primary'>
Scrobble coverage
</h3>
<p className='type-body-sm text-lucky-text-secondary mt-2'>
Lucky tracks requested songs and forwards play activity once
linked.
</p>
</div>
</div>
</div>

<div className='surface-panel rounded-xl border border-lucky-border/20 p-6'>
<div className='flex items-start gap-3'>
<Link2 className='mt-1 h-5 w-5 text-lucky-text-tertiary flex-shrink-0' />
<div className='flex-1'>
<h3 className='type-title text-lucky-text-primary'>
Privacy control
</h3>
<p className='type-body-sm text-lucky-text-secondary mt-2'>
Disconnect anytime without affecting server playback.
</p>
</div>
</div>
</div>
</div>

<section className='surface-panel p-6'>
<h3 className='type-title mb-3 text-lucky-text-primary'>How it works</h3>
<ul className='space-y-2 type-body-sm text-lucky-text-secondary'>
<li className='flex items-start gap-2'>
<span className='text-lucky-text-tertiary'>1.</span>
Connect your Last.fm account above
</li>
<li className='flex items-start gap-2'>
<span className='text-lucky-text-tertiary'>2.</span>
Play music in a voice channel using the bot
</li>
<li className='flex items-start gap-2'>
<span className='text-lucky-text-tertiary'>3.</span>
Tracks are automatically scrobbled to your profile
</li>
<li className='flex items-start gap-2'>
<span className='text-lucky-text-tertiary'>4.</span>
External music bots (Rythm, Groovy, etc.) are also detected
</li>
</ul>
<section className='surface-panel space-y-4 rounded-xl border border-lucky-border/20 p-8'>
<h3 className='type-title text-lucky-text-primary'>How it works</h3>
<ol className='space-y-3 text-sm'>
{[
'Connect your Last.fm account above',
'Play music in a voice channel using the bot',
'Tracks are automatically scrobbled to your profile',
'External music bots (Rythm, Groovy, etc.) are also detected',
].map((step, i) => (
<li key={i} className='flex items-start gap-3'>

Check warning on line 227 in packages/frontend/src/pages/LastFm.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use Array index in keys

See more on https://sonarcloud.io/project/issues?id=LucasSantana-Dev_Lucky&issues=AZ4r_lzG8TgQ2uGCNG0w&open=AZ4r_lzG8TgQ2uGCNG0w&pullRequest=864
<span className='mt-0.5 inline-flex h-6 w-6 items-center justify-center rounded-full bg-lucky-accent/20 text-xs font-600 text-lucky-accent flex-shrink-0'>
{i + 1}
</span>
<span className='type-body-sm text-lucky-text-secondary'>
{step}
</span>
</li>
))}
</ol>
</section>
</div>
)
Expand Down
Loading
Loading