diff --git a/app/now/loading.tsx b/app/now/loading.tsx
index 899bf3c..4ca221e 100644
--- a/app/now/loading.tsx
+++ b/app/now/loading.tsx
@@ -75,8 +75,15 @@ export default function Loading() {
-
+
diff --git a/app/now/page.tsx b/app/now/page.tsx
index 796a7fe..7ab9bcb 100644
--- a/app/now/page.tsx
+++ b/app/now/page.tsx
@@ -3,7 +3,6 @@ import Link from 'next/link';
import { PageWrapper } from '../components/PageWrapper';
import { faSpotify } from '@fortawesome/free-brands-svg-icons';
import {
- faArrowRight,
faBullseye,
faCode,
faGamepad,
@@ -20,7 +19,6 @@ import {
ActivityFeed,
ArtistCard,
CodingRhythm,
- FeaturedTrack,
GameCard,
LanguageStack,
LolLiveGame,
@@ -29,11 +27,7 @@ import {
StarredRepos,
} from '@/features/now/components';
import { getGithubDev, getGithubStarred } from '@/lib/github';
-import {
- getLastfmRecentStats,
- getLastfmTopArtists,
- getLastfmTopTracks,
-} from '@/lib/lastfm';
+import { getLastfmRecentStats, getLastfmTopArtists } from '@/lib/lastfm';
import { getLolLiveGame } from '@/lib/lol';
import { SITE_NAME, SITE_URL } from '@/lib/site';
import { getSteamGames } from '@/lib/steam';
@@ -106,20 +100,12 @@ type LastfmData = {
tracks: LastfmTrack[];
};
-const computeTrackData = (topTracks: LastfmTrack[], lastfm: LastfmData) => {
- const recentTracks = getUniqueTracks(
- [lastfm.lastPlayed, ...lastfm.tracks].filter(Boolean) as LastfmTrack[],
+const computeTrackData = (lastfm: LastfmData) =>
+ getUniqueTracks(
+ [lastfm.nowPlaying, lastfm.lastPlayed, ...lastfm.tracks].filter(
+ Boolean,
+ ) as LastfmTrack[],
);
- const featuredTrack = topTracks[0] ?? recentTracks[0];
- const recentTrackList = featuredTrack
- ? recentTracks.filter(
- (track) =>
- track.name !== featuredTrack.name ||
- track.artist !== featuredTrack.artist,
- )
- : recentTracks;
- return { recentTracks, featuredTrack, recentTrackList };
-};
const getUniqueTracks = (tracks: LastfmTrack[]) => {
const seen = new Set();
@@ -135,29 +121,30 @@ const getUniqueTracks = (tracks: LastfmTrack[]) => {
};
export default async function NowPage() {
- const [lastfm, artists, topTracks, dev, steam, starred, lolLiveGame] =
- await Promise.all([
+ const [lastfm, artists, dev, steam, starred, lolLiveGame] = await Promise.all(
+ [
getLastfmRecentStats().catch(() => ({
nowPlaying: null,
lastPlayed: null,
tracks: [],
})),
getLastfmTopArtists({ period: '1month' }).catch(() => []),
- getLastfmTopTracks({ period: '1month' }).catch(() => []),
getGithubDev().catch(() => null),
- getSteamGames().catch(() => ({ games: [], source: 'recent' as const })),
+ getSteamGames().catch(() => ({
+ games: [],
+ source: 'recent' as const,
+ updatedAt: null,
+ })),
getGithubStarred().catch(() => []),
getLolLiveGame().catch(() => null),
- ]);
+ ],
+ );
const hasDevData = Boolean(
dev && (dev.rhythm || dev.languages.length > 0 || dev.activity.length > 0),
);
- const { featuredTrack, recentTrackList } = computeTrackData(
- topTracks,
- lastfm,
- );
+ const recentTracks = computeTrackData(lastfm);
return (
@@ -264,45 +251,19 @@ export default async function NowPage() {
id="listening-title"
title="No repeat do mês"
subtitle={LISTENING_DESCRIPTION}
- action={
-
- Ver no Last.fm →
-
- }
/>
-
+
- Trilha do mês
-
-
- {featuredTrack ? (
-
-
-
- ) : null}
-
-
Mais recentes
-
- {recentTrackList.length > 0 ? (
-
- {recentTrackList.slice(0, 4).map((track, index) => (
+
+ {recentTracks.length > 0 ? (
+
@@ -356,6 +315,17 @@ export default async function NowPage() {
)}
+
+
+
+ Ver no Last.fm →
+
+
@@ -370,6 +340,7 @@ export default async function NowPage() {
id="playing-title"
title="Provável recaída"
subtitle={PLAYING_DESCRIPTION}
+ updatedAt={steam.updatedAt}
/>
{lolLiveGame && (
@@ -382,48 +353,27 @@ export default async function NowPage() {
)}
{steam.games.length > 0 ? (
-
-
- {steam.games.map((game) => (
-
- ))}
-
-
-
- Ver no Steam
-
-
-
+
+ {steam.games.map((game) => (
+
+ ))}
) : (
-
-
- Nenhum jogo para mostrar.
-
-
- Steam fica aqui
-
-
-
+
+ Nenhum jogo para mostrar.
+
)}
+
+
+
+ {steam.games.length > 0 ? 'Ver no Steam' : 'Steam fica aqui'} →
+
+
diff --git a/app/page.tsx b/app/page.tsx
index 3c6f33b..707596b 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -4,7 +4,6 @@ import { AnimationLayout } from '@/base/components/Layout/AnimationLayout';
import { Navbar } from '@/base/components/Navbar';
import { ArticlesList } from '@/features/articles/components/ArticlesList';
import { About } from '@/features/home/components/About';
-import { LastfmCard } from '@/features/home/components/Activity';
import { DevPulse } from '@/features/home/components/DevPulse';
import { Hero } from '@/features/home/components/Hero';
import { ProjectsShowcase } from '@/features/home/components/Projects';
@@ -51,7 +50,6 @@ export default function Page() {
-
diff --git a/src/base/components/LiveDot.tsx b/src/base/components/LiveDot.tsx
new file mode 100644
index 0000000..b272ff5
--- /dev/null
+++ b/src/base/components/LiveDot.tsx
@@ -0,0 +1,7 @@
+/** Ponto pulsante "ao vivo" — usado no selo "ouvindo agora" da navbar e em listas de faixas. */
+export const LiveDot = ({ className = '' }: { className?: string }) => (
+
+);
diff --git a/src/base/components/Navbar/NowPlayingBadge.tsx b/src/base/components/Navbar/NowPlayingBadge.tsx
index 75adc9e..a88b301 100644
--- a/src/base/components/Navbar/NowPlayingBadge.tsx
+++ b/src/base/components/Navbar/NowPlayingBadge.tsx
@@ -6,6 +6,7 @@ import { useState } from 'react';
import useSWR from 'swr';
+import { LiveDot } from '@/base/components/LiveDot';
import { LastfmTrack } from '@/types/Lastfm';
type NowPlayingResponse = {
@@ -49,13 +50,6 @@ const NowPlayingArt = ({
);
};
-const LiveDot = ({ className = '' }: { className?: string }) => (
-
-);
-
type NowPlayingBadgeProps = {
/** 'full' = capa + faixa + artista (desktop). 'compact' = só a capa (mobile). */
variant: 'full' | 'compact';
diff --git a/src/base/components/SectionHeader/SectionHeader.tsx b/src/base/components/SectionHeader/SectionHeader.tsx
index 658e2e4..0f4763b 100644
--- a/src/base/components/SectionHeader/SectionHeader.tsx
+++ b/src/base/components/SectionHeader/SectionHeader.tsx
@@ -1,6 +1,10 @@
import type { ReactNode } from 'react';
+import { faCalendarCheck } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
import { SectionIcon } from '@/base/components/SectionIcon';
+import { formatRelativeTime } from '@/lib/formatRelativeTime';
/** Estilo único dos links de ação das seções ("Ver todos", "@usuario"…). */
export const SECTION_ACTION_CLASS =
@@ -16,6 +20,8 @@ type SectionHeaderProps = {
/** Link/ação alinhada à direita (ex.: "Ver todos"). */
action?: ReactNode;
className?: string;
+ /** ISO da última sincronização. Quando informado, mostra o selo "Atualizado há X". */
+ updatedAt?: string | null;
};
/**
@@ -30,6 +36,7 @@ export const SectionHeader = ({
subtitle,
action,
className = '',
+ updatedAt,
}: SectionHeaderProps) => (
+
+ {track.nowPlaying ? (
+ <>
+ Ouvindo agora
+
+ >
+ ) : track.playedAt ? (
+ {formatPlayedAt(track.playedAt)}
+ ) : null}
+
);
diff --git a/src/features/now/components/index.ts b/src/features/now/components/index.ts
index df1b42e..efe80ab 100644
--- a/src/features/now/components/index.ts
+++ b/src/features/now/components/index.ts
@@ -2,7 +2,6 @@ export { ActivityFeed } from './ActivityFeed';
export { ArtistCard } from './ArtistCard';
export { CardTooltip } from './CardTooltip';
export { CodingRhythm } from './CodingRhythm';
-export { FeaturedTrack } from './FeaturedTrack';
export { GameCard } from './GameCard';
export { LanguageStack } from './LanguageStack';
export { LolChampionCard } from './LolChampionCard';
diff --git a/src/lib/steam.ts b/src/lib/steam.ts
index 393f1f1..2f42635 100644
--- a/src/lib/steam.ts
+++ b/src/lib/steam.ts
@@ -13,8 +13,8 @@ const debugLog = (...args: unknown[]) => {
const CACHE_TTL_RECENT = 60 * 60 * 24; // 24h
// Top all-time: ranking de horas totais não muda do dia pra noite
const CACHE_TTL_ALLTIME = 60 * 60 * 24 * 7; // 7 dias
-const CACHE_KEY_RECENT = 'steam:recently-played:v3'; // v3: descarta listas curtas gravadas antes do backfill
-const CACHE_KEY_ALLTIME = 'steam:alltime:v3'; // v3: descarta top all-time anterior ao backfill (sem Phasmophobia etc.)
+const CACHE_KEY_RECENT = 'steam:recently-played:v4'; // v4: payload passa a incluir updatedAt
+const CACHE_KEY_ALLTIME = 'steam:alltime:v4'; // v4: payload passa a incluir updatedAt
/** Quantos cards a grade exibe — sempre completamos até esse número. */
const TARGET_COUNT = 5;
@@ -51,6 +51,14 @@ export type SteamResult = {
games: SteamGame[];
/** 'recent' = jogados nas últimas 2 semanas; 'alltime' = mais jogados de todos os tempos */
source: 'recent' | 'alltime';
+ /** ISO de quando a lista foi sincronizada com a Steam pela última vez; null se nunca sincronizou. */
+ updatedAt: string | null;
+};
+
+/** Payload cacheado — guarda o timestamp junto pra exibir "atualizado há X". */
+type SteamCachePayload = {
+ games: SteamGame[];
+ updatedAt: string;
};
type SteamApiGame = {
@@ -161,23 +169,26 @@ const fetchTopAllTime = async (
const getRecentGames = async (
userId: string,
apiKey: string,
-): Promise => {
- const cached = await cacheGet(CACHE_KEY_RECENT);
- if (cached && cached.length > 0) {
- debugLog(`cache hit (recent): ${cached.length} jogos`);
+): Promise => {
+ const cached = await cacheGet(CACHE_KEY_RECENT);
+ if (cached && cached.games.length > 0) {
+ debugLog(`cache hit (recent): ${cached.games.length} jogos`);
return cached;
}
try {
const recent = await fetchRecentlyPlayed(userId, apiKey);
debugLog(`api recent: ${recent.length} jogos`);
- if (recent.length > 0) {
- await cacheSet(CACHE_KEY_RECENT, recent, CACHE_TTL_RECENT);
- }
- return recent;
+ if (recent.length === 0) return { games: [], updatedAt: '' };
+ const payload: SteamCachePayload = {
+ games: recent,
+ updatedAt: new Date().toISOString(),
+ };
+ await cacheSet(CACHE_KEY_RECENT, payload, CACHE_TTL_RECENT);
+ return payload;
} catch (err) {
debugLog('erro ao buscar recentes:', err);
- return [];
+ return { games: [], updatedAt: '' };
}
};
@@ -185,23 +196,26 @@ const getRecentGames = async (
const getAllTimeGames = async (
userId: string,
apiKey: string,
-): Promise => {
- const cached = await cacheGet(CACHE_KEY_ALLTIME);
- if (cached && cached.length > 0) {
- debugLog(`cache hit (alltime): ${cached.length} jogos`);
+): Promise => {
+ const cached = await cacheGet(CACHE_KEY_ALLTIME);
+ if (cached && cached.games.length > 0) {
+ debugLog(`cache hit (alltime): ${cached.games.length} jogos`);
return cached;
}
try {
const alltime = await fetchTopAllTime(userId, apiKey);
debugLog(`api alltime: ${alltime.length} jogos`);
- if (alltime.length > 0) {
- await cacheSet(CACHE_KEY_ALLTIME, alltime, CACHE_TTL_ALLTIME);
- }
- return alltime;
+ if (alltime.length === 0) return { games: [], updatedAt: '' };
+ const payload: SteamCachePayload = {
+ games: alltime,
+ updatedAt: new Date().toISOString(),
+ };
+ await cacheSet(CACHE_KEY_ALLTIME, payload, CACHE_TTL_ALLTIME);
+ return payload;
} catch (err) {
debugLog('erro ao buscar all-time:', err);
- return [];
+ return { games: [], updatedAt: '' };
}
};
@@ -211,26 +225,34 @@ export const getSteamGames = async (): Promise => {
if (!apiKey || !userId) {
debugLog('STEAM_API_KEY ou STEAM_USER_ID ausentes');
- return { games: [], source: 'recent' };
+ return { games: [], source: 'recent', updatedAt: null };
}
const recent = await getRecentGames(userId, apiKey);
- if (recent.length >= TARGET_COUNT) {
- return { games: recent.slice(0, TARGET_COUNT), source: 'recent' };
+ if (recent.games.length >= TARGET_COUNT) {
+ return {
+ games: recent.games.slice(0, TARGET_COUNT),
+ source: 'recent',
+ updatedAt: recent.updatedAt || null,
+ };
}
// Menos recentes que o alvo: completa com os mais jogados de todos os tempos
// (sem repetir os que já estão na lista) para a grade nunca ficar incompleta.
const alltime = await getAllTimeGames(userId, apiKey);
- const seen = new Set(recent.map((game) => game.appid));
- const backfill = alltime.filter((game) => !seen.has(game.appid));
- const games = [...recent, ...backfill].slice(0, TARGET_COUNT);
+ const seen = new Set(recent.games.map((game) => game.appid));
+ const backfill = alltime.games.filter((game) => !seen.has(game.appid));
+ const games = [...recent.games, ...backfill].slice(0, TARGET_COUNT);
debugLog(
- `final: ${recent.length} recentes + ${
- games.length - recent.length
+ `final: ${recent.games.length} recentes + ${
+ games.length - recent.games.length
} backfill`,
);
- return { games, source: recent.length > 0 ? 'recent' : 'alltime' };
+ return {
+ games,
+ source: recent.games.length > 0 ? 'recent' : 'alltime',
+ updatedAt: recent.updatedAt || alltime.updatedAt || null,
+ };
};