From 8f06b177b5f41fc640074d264abcbd75b2ce0e08 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Fri, 28 Nov 2025 20:36:18 -0800 Subject: [PATCH] redesign featured genres --- .../components/featured-genres.module.css | 56 +++++++++-- .../home/components/featured-genres.tsx | 93 +++++++++++++++---- 2 files changed, 123 insertions(+), 26 deletions(-) diff --git a/src/renderer/features/home/components/featured-genres.module.css b/src/renderer/features/home/components/featured-genres.module.css index 4ed827eca..e948fed94 100644 --- a/src/renderer/features/home/components/featured-genres.module.css +++ b/src/renderer/features/home/components/featured-genres.module.css @@ -5,8 +5,10 @@ width: 100%; } -.group { - gap: var(--theme-spacing-sm); +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: var(--theme-spacing-md); padding: var(--theme-spacing-xs) 0; } @@ -14,25 +16,35 @@ position: relative; min-height: 3rem; overflow: hidden; + background-color: var(--theme-colors-surface); border-radius: var(--theme-radius-md); } +.genre-container::before { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 0.5rem; + content: ''; + background-color: var(--genre-color, transparent); +} + .genre-link { position: relative; z-index: 1; display: flex; align-items: center; - justify-content: center; width: 100%; - min-width: 90px; + min-width: 0; min-height: 3rem; - padding: 0 var(--theme-spacing-md); + padding: 0 var(--theme-spacing-xl); font-size: var(--theme-font-size-md); font-weight: 600; - color: white; - text-align: center; + color: inherit; + text-align: left; text-decoration: none; - text-shadow: 0 0 10px rgb(0 0 0 / 50%); + text-shadow: none; cursor: pointer; user-select: none; transition: @@ -42,9 +54,35 @@ .genre-link:hover { box-shadow: 0 4px 8px rgb(0 0 0 / 20%); - transform: translateY(-2px); } .genre-link:active { transform: translateY(0); } + +.genre-name { + flex: 1 1 auto; + margin-right: var(--theme-spacing-xl); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.play-button-wrapper { + position: absolute; + top: 50%; + right: var(--theme-spacing-md); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transform: translateY(-50%) translateX(8px); + transition: + opacity 0.15s ease-out, + transform 0.15s ease-out; +} + +.genre-link:hover .play-button-wrapper { + opacity: 1; + transform: translateY(-50%) translateX(0); +} diff --git a/src/renderer/features/home/components/featured-genres.tsx b/src/renderer/features/home/components/featured-genres.tsx index 239fd88d0..43f656977 100644 --- a/src/renderer/features/home/components/featured-genres.tsx +++ b/src/renderer/features/home/components/featured-genres.tsx @@ -1,20 +1,24 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; +import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; import { shuffle } from 'lodash'; -import { useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { generatePath, Link } from 'react-router'; import styles from './featured-genres.module.css'; +import { api } from '/@/renderer/api'; +import { queryKeys } from '/@/renderer/api/query-keys'; import { genresQueries } from '/@/renderer/features/genres/api/genres-api'; -import { BackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay'; +import { useIsPlayerFetching, usePlayer } from '/@/renderer/features/player/context/player-context'; +import { PlayButton } from '/@/renderer/features/shared/components/play-button'; import { useContainerQuery } from '/@/renderer/hooks'; import { AppRoute } from '/@/renderer/router/routes'; -import { useCurrentServer } from '/@/renderer/store'; +import { useCurrentServer, useCurrentServerId } from '/@/renderer/store'; import { Button } from '/@/shared/components/button/button'; import { Group } from '/@/shared/components/group/group'; import { TextTitle } from '/@/shared/components/text-title/text-title'; -import { Genre, GenreListSort, SortOrder } from '/@/shared/types/domain-types'; +import { Genre, GenreListSort, Played, SortOrder } from '/@/shared/types/domain-types'; +import { Play } from '/@/shared/types/types'; import { stringToColor } from '/@/shared/utils/string-to-color'; function getGenresToShow(breakpoints: { @@ -127,22 +131,77 @@ export const FeaturedGenres = () => { {t('action.viewMore', { postProcess: 'sentenceCase' })} - +
{genresWithColors.map((genre) => ( -
- - - {genre.name} - -
+ ))} - +
)} ); }; + +const GenrePlayButton = ({ genre }: { genre: Genre }) => { + const queryClient = useQueryClient(); + const isPlayerFetching = useIsPlayerFetching(); + const player = usePlayer(); + const serverId = useCurrentServerId(); + + const handlePlay = useCallback( + async (genre: Genre) => { + if (!serverId) return; + + const data = await queryClient.fetchQuery({ + gcTime: 0, + queryFn: () => { + return api.controller.getRandomSongList({ + apiClientProps: { serverId }, + query: { + genre: genre.id, + limit: 100, + played: Played.All, + }, + }); + }, + queryKey: queryKeys.player.fetch(), + staleTime: 0, + }); + + player.addToQueueByData(data?.items || [], Play.NOW); + }, + [player, queryClient, serverId], + ); + + return ( + + handlePlay(genre)} + /> + + ); +}; + +const GenreItem = memo(({ genre }: { genre: Genre & { color: string; path: string } }) => { + return ( +
+ + {genre.name} + + +
+ ); +}); + +GenreItem.displayName = 'GenreItem';