Skip to content
Merged
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
33 changes: 20 additions & 13 deletions src/base/components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

import { LanguageSelector } from './LanguageSelector';
import { NowPlayingBadge } from './NowPlayingBadge';
import { PreferencesPanel } from './PreferencesPanel';
import { SITE_NAME } from '@/lib/site';

Expand Down Expand Up @@ -125,22 +126,28 @@ export const Navbar = () => {
})}
</nav>

<div className="ml-1 flex items-center gap-1">
<PreferencesPanel />
<LanguageSelector />
<div className="ml-3 flex items-center gap-3">
<NowPlayingBadge variant="full" />
<div className="flex items-center gap-1">
<PreferencesPanel />
<LanguageSelector />
</div>
</div>
</div>

<button
type="button"
aria-label="Abrir menu"
aria-expanded={mobileMenuOpen}
aria-controls="mobile-navigation"
onClick={() => setMobileMenuOpen(true)}
className="-mr-3 ml-auto flex size-12 items-center justify-center text-site-foreground transition-colors hover:text-site-primary-hover md:hidden"
>
<MenuIcon />
</button>
<div className="-mr-3 ml-auto flex items-center gap-3 md:hidden">
<NowPlayingBadge variant="compact" />
<button
type="button"
aria-label="Abrir menu"
aria-expanded={mobileMenuOpen}
aria-controls="mobile-navigation"
onClick={() => setMobileMenuOpen(true)}
className="flex size-12 items-center justify-center text-site-foreground transition-colors hover:text-site-primary-hover"
>
<MenuIcon />
</button>
</div>
</div>
</header>

Expand Down
116 changes: 116 additions & 0 deletions src/base/components/Navbar/NowPlayingBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
'use client';

import Image from 'next/image';
import Link from 'next/link';
import { useState } from 'react';

import useSWR from 'swr';

import { LastfmTrack } from '@/types/Lastfm';

type NowPlayingResponse = {
nowPlaying: LastfmTrack | null;
};

const jsonFetcher = (url: string) => fetch(url).then((r) => r.json());

const NowPlayingArt = ({
track,
size,
}: {
track: LastfmTrack;
size: number;
}) => {
const [failed, setFailed] = useState(false);

if (!track.imageUrl || failed) {
return (
<div
aria-hidden="true"
className="shrink-0 rounded bg-site-card-hover"
style={{ width: size, height: size }}
/>
);
}

return (
<Image
src={track.imageUrl}
alt=""
width={size}
height={size}
unoptimized
className="shrink-0 rounded object-cover"
style={{ width: size, height: size }}
onError={() => setFailed(true)}
/>
);
};

const LiveDot = ({ className = '' }: { className?: string }) => (
<span
aria-hidden="true"
className={`rounded-full bg-spotify motion-safe:animate-pulse ${className}`}
/>
);

type NowPlayingBadgeProps = {
/** 'full' = capa + faixa + artista (desktop). 'compact' = só a capa (mobile). */
variant: 'full' | 'compact';
};

/**
* Selo "ouvindo agora" da navbar. Só renderiza algo quando há uma faixa
* tocando no momento — sem música, o componente não ocupa espaço nenhum.
*/
export const NowPlayingBadge = ({ variant }: NowPlayingBadgeProps) => {
const { data } = useSWR<NowPlayingResponse>(
'/api/lastfm/now-playing',
jsonFetcher,
{ revalidateOnFocus: false, refreshInterval: 60_000 },
);
const track = data?.nowPlaying;

if (!track) return null;

const href =
track.imageSource === 'spotify' && track.spotifyUrl
? track.spotifyUrl
: track.url;

if (variant === 'compact') {
return (
<Link
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={`Ouvindo agora: ${track.name}, ${track.artist}`}
className="relative flex shrink-0 items-center rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-site-primary"
>
<NowPlayingArt track={track} size={28} />
<LiveDot className="absolute -bottom-0.5 -right-0.5 size-2 ring-2 ring-site-background" />
</Link>
);
}

return (
<Link
href={href}
target="_blank"
rel="noopener noreferrer"
title={`Ouvindo agora: ${track.name} — ${track.artist}`}
className="flex items-center gap-2 rounded-full border border-site-border-subtle bg-site-card py-1 pl-1 pr-3 no-underline transition-colors hover:border-site-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-site-primary"
>
<NowPlayingArt track={track} size={26} />
<div className="min-w-0 leading-tight">
<p className="m-0 max-w-[9rem] truncate text-xs font-semibold text-site-foreground">
{track.name}
</p>
<p className="m-0 max-w-[9rem] truncate text-[11px] text-site-body-muted">
{track.artist}
</p>
</div>
<LiveDot className="size-1.5 shrink-0" />
</Link>
);
};
Loading