Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>
This commit is contained in:
parent
8626251f33
commit
dfb3da4e10
@ -1,11 +1,11 @@
|
|||||||
vite.svg,499162500000,699a02e0e68a579f687d364bbbe7633161244f35af068220aee37b1b33dfb3c7
|
vite.svg,499162500000,699a02e0e68a579f687d364bbbe7633161244f35af068220aee37b1b33dfb3c7
|
||||||
index.html,1753036974325,d4ec7269769afa4794c9b3bc4382b8bb4ca2a9cc11d47522053a62b9c1dbd99b
|
index.html,1753039071822,289c705921beda96f6c4b135228e50d878a006af729bda9ea798c308ba8e28d2
|
||||||
assets/swipe-back-0ASdaLec.js,1753036974325,dfdb4ccfc65b5ddd1d04306a2cbc650f7f2be8d659bb029d32c75fd5041b6f5e
|
assets/status-tap-CEY3QyNR.js,1753039071809,6c19db5130690d358a896ae78fec5e08f22b1cfd20385bb51eb8b346ee1b9e04
|
||||||
assets/status-tap-DKvxsyfd.js,1753036974325,1bbe8dd5487685ce491d40b73bc1bbbc1a3fdeac4e485438c55484248ca69fff
|
assets/swipe-back-d2lW66aK.js,1753039071809,e420e33827964afc4fbe53b029bbfa7f23b0c1260900677c7ca4eab979e16c02
|
||||||
assets/md.transition-CWuowIQj.js,1753036974325,b5d793bffc7672549acba90697be0b704a7b39d8f4ff21faaf05ddba2951dcb0
|
assets/md.transition-MdrV1caB.js,1753039071823,59c5b55c8425c0d65b4a6116578e6b7dd98e70bffbbc3781413f312f17635c60
|
||||||
assets/ios.transition-BPJsV1yP.js,1753036974325,e401e6df1d7c900f8f54e717308a57abfcf913ee17b2eb56846b4ba3049d94cf
|
assets/index7-C6G0api_.js,1753039071809,57b89d09c14fbac6c16a22825f7bdb81c92de6f5df1bae8fe148908f5d2812ef
|
||||||
assets/input-shims-BtXJpb0t.js,1753036974325,1f111da7ba4ae84995311f07a6d41c62ca179213cd8123623048af9ccee533f9
|
assets/ios.transition-DXsH9Qpm.js,1753039071823,758b15c7838ba462b05f15fb7b4ac68e1dd489bda3d25a5e2a12bf1c3a1f5b97
|
||||||
assets/index7-_QrtZLKR.js,1753036974325,fac87389bb8d2e7850fef07b4fd01132a49206e2b5ddd40feff932d041e1874e
|
assets/focus-visible-supuXXMI.js,1753039071811,df9266429356671847fa2c8123e1564bae645f75df3094f0055c365fa2beae28
|
||||||
assets/focus-visible-supuXXMI.js,1753036974325,df9266429356671847fa2c8123e1564bae645f75df3094f0055c365fa2beae28
|
assets/input-shims-DA6_iXWm.js,1753039071811,3394836233f6eb8c423038c9a190ae5e1b0ef552dc27e0c73287275573e79088
|
||||||
assets/index-CzY-nILz.css,1753036974324,c68362f490f176fbe950e6cc313da199ed6e08c67058d82b833a2026a0be75c9
|
assets/index-CzY-nILz.css,1753039071808,c68362f490f176fbe950e6cc313da199ed6e08c67058d82b833a2026a0be75c9
|
||||||
assets/index-DxnqBJhb.js,1753036974326,143303c7b9c04d8a450f75b956258ba32195e518861db9a8c07e4cbbab3d6c0d
|
assets/index-BlzLgABC.js,1753039071823,cd8075e0b94a4d73bcdfc453f3276398223974fa2cb193599229a82aa2ccba6c
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { IonApp, IonHeader, IonToolbar, IonTitle, IonContent, IonMenuButton, IonIcon } from '@ionic/react';
|
import { IonApp, IonHeader, IonToolbar, IonTitle, IonContent, IonMenuButton } from '@ionic/react';
|
||||||
import { logOut } from 'ionicons/icons';
|
|
||||||
import { logout } from '../../redux/authSlice';
|
import { logout } from '../../redux/authSlice';
|
||||||
import { ActionButton } from '../common';
|
import { ActionButton } from '../common';
|
||||||
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
||||||
|
import { Icons } from '../../constants';
|
||||||
import Navigation from '../Navigation/Navigation';
|
import Navigation from '../Navigation/Navigation';
|
||||||
import { getPageTitle } from '../../utils/routeUtils';
|
import { getPageTitle } from '../../utils/routeUtils';
|
||||||
import type { LayoutProps } from '../../types';
|
import type { LayoutProps } from '../../types';
|
||||||
@ -65,11 +66,11 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
<div slot="end">
|
<div slot="end">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
variant="secondary"
|
variant={ActionButtonVariant.SECONDARY}
|
||||||
size="sm"
|
size={ActionButtonSize.SMALL}
|
||||||
>
|
icon={Icons.LOGOUT}
|
||||||
<IonIcon icon={logOut} />
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
</ActionButton>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
|
|||||||
@ -1,22 +1,27 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IonButton } from '@ionic/react';
|
import { IonButton, IonIcon } from '@ionic/react';
|
||||||
import type { ActionButtonProps } from '../../types';
|
import type { ActionButtonProps } from '../../types';
|
||||||
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot, ActionButtonIconSize } from '../../types';
|
||||||
|
|
||||||
const ActionButton: React.FC<ActionButtonProps> = ({
|
const ActionButton: React.FC<ActionButtonProps> = ({
|
||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
variant = 'primary',
|
variant = ActionButtonVariant.PRIMARY,
|
||||||
size = 'md',
|
size = ActionButtonSize.MEDIUM,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
className = ''
|
className = '',
|
||||||
|
icon,
|
||||||
|
iconSlot = ActionButtonIconSlot.START,
|
||||||
|
iconSize = ActionButtonIconSize.LARGE,
|
||||||
|
fill = 'solid'
|
||||||
}) => {
|
}) => {
|
||||||
const getVariant = () => {
|
const getVariant = () => {
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case 'primary':
|
case ActionButtonVariant.PRIMARY:
|
||||||
return 'primary';
|
return 'primary';
|
||||||
case 'secondary':
|
case ActionButtonVariant.SECONDARY:
|
||||||
return 'medium';
|
return 'medium';
|
||||||
case 'danger':
|
case ActionButtonVariant.DANGER:
|
||||||
return 'danger';
|
return 'danger';
|
||||||
default:
|
default:
|
||||||
return 'primary';
|
return 'primary';
|
||||||
@ -25,11 +30,11 @@ const ActionButton: React.FC<ActionButtonProps> = ({
|
|||||||
|
|
||||||
const getSize = () => {
|
const getSize = () => {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 'sm':
|
case ActionButtonSize.SMALL:
|
||||||
return 'small';
|
return 'small';
|
||||||
case 'md':
|
case ActionButtonSize.MEDIUM:
|
||||||
return 'default';
|
return 'default';
|
||||||
case 'lg':
|
case ActionButtonSize.LARGE:
|
||||||
return 'large';
|
return 'large';
|
||||||
default:
|
default:
|
||||||
return 'default';
|
return 'default';
|
||||||
@ -40,11 +45,24 @@ const ActionButton: React.FC<ActionButtonProps> = ({
|
|||||||
<IonButton
|
<IonButton
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
fill="solid"
|
fill={fill}
|
||||||
color={getVariant()}
|
color={getVariant()}
|
||||||
size={getSize()}
|
size={getSize()}
|
||||||
className={className}
|
className={className}
|
||||||
|
style={{
|
||||||
|
minWidth: '40px',
|
||||||
|
minHeight: '40px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
{icon && <IonIcon
|
||||||
|
icon={icon}
|
||||||
|
slot={iconSlot}
|
||||||
|
size={typeof iconSize === 'number' ? undefined : iconSize}
|
||||||
|
style={typeof iconSize === 'number' ? { fontSize: `${iconSize}px` } : undefined}
|
||||||
|
/>}
|
||||||
{children}
|
{children}
|
||||||
</IonButton>
|
</IonButton>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IonCard, IonCardContent, IonChip, IonIcon } from '@ionic/react';
|
import { IonCard, IonCardContent, IonChip, IonIcon } from '@ionic/react';
|
||||||
import { play, pause, stop, pauseOutline, playOutline, stopOutline } from 'ionicons/icons';
|
import { pauseOutline, playOutline, stopOutline } from 'ionicons/icons';
|
||||||
import ActionButton from './ActionButton';
|
import ActionButton from './ActionButton';
|
||||||
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
||||||
|
import { Icons } from '../../constants';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectPlayerState, selectIsAdmin, selectQueueLength, selectControllerName } from '../../redux';
|
import { selectPlayerState, selectIsAdmin, selectQueueLength, selectControllerName } from '../../redux';
|
||||||
import { playerService } from '../../firebase/services';
|
import { playerService } from '../../firebase/services';
|
||||||
@ -190,30 +192,30 @@ const PlayerControls: React.FC<PlayerControlsProps> = ({ className = '', variant
|
|||||||
{currentState === PlayerState.playing ? (
|
{currentState === PlayerState.playing ? (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={handlePause}
|
onClick={handlePause}
|
||||||
variant="primary"
|
variant={ActionButtonVariant.PRIMARY}
|
||||||
size="sm"
|
size={ActionButtonSize.SMALL}
|
||||||
>
|
icon={Icons.PAUSE}
|
||||||
<IonIcon icon={pause} slot="icon-only" />
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
</ActionButton>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={handlePlay}
|
onClick={handlePlay}
|
||||||
variant="primary"
|
variant={ActionButtonVariant.PRIMARY}
|
||||||
size="sm"
|
size={ActionButtonSize.SMALL}
|
||||||
|
icon={Icons.PLAY}
|
||||||
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
disabled={!hasSongsInQueue}
|
disabled={!hasSongsInQueue}
|
||||||
>
|
/>
|
||||||
<IonIcon icon={play} slot="icon-only" />
|
|
||||||
</ActionButton>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentState !== PlayerState.stopped && (
|
{currentState !== PlayerState.stopped && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={handleStop}
|
onClick={handleStop}
|
||||||
variant="danger"
|
variant={ActionButtonVariant.DANGER}
|
||||||
size="sm"
|
size={ActionButtonSize.SMALL}
|
||||||
>
|
icon={Icons.STOP}
|
||||||
<IonIcon icon={stop} slot="icon-only" />
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
</ActionButton>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -7,15 +7,15 @@ import {
|
|||||||
IonContent,
|
IonContent,
|
||||||
IonList,
|
IonList,
|
||||||
IonItem,
|
IonItem,
|
||||||
IonLabel,
|
IonLabel
|
||||||
IonButton,
|
|
||||||
IonIcon
|
|
||||||
} from '@ionic/react';
|
} from '@ionic/react';
|
||||||
import { close } from 'ionicons/icons';
|
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectSingersArray, selectControllerName, selectQueueObject } from '../../redux';
|
import { selectSingersArray, selectControllerName, selectQueueObject } from '../../redux';
|
||||||
import { queueService } from '../../firebase/services';
|
import { queueService } from '../../firebase/services';
|
||||||
import { useToast } from '../../hooks/useToast';
|
import { useToast } from '../../hooks/useToast';
|
||||||
|
import { ActionButton } from './index';
|
||||||
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
||||||
|
import { Icons } from '../../constants';
|
||||||
import type { Song, Singer, QueueItem } from '../../types';
|
import type { Song, Singer, QueueItem } from '../../types';
|
||||||
|
|
||||||
interface SelectSingerProps {
|
interface SelectSingerProps {
|
||||||
@ -76,9 +76,13 @@ const SelectSinger: React.FC<SelectSingerProps> = ({ isOpen, onClose, song }) =>
|
|||||||
<IonHeader>
|
<IonHeader>
|
||||||
<IonToolbar>
|
<IonToolbar>
|
||||||
<IonTitle>Select Singer</IonTitle>
|
<IonTitle>Select Singer</IonTitle>
|
||||||
<IonButton slot="end" fill="clear" onClick={onClose}>
|
<ActionButton
|
||||||
<IonIcon icon={close} />
|
onClick={onClose}
|
||||||
</IonButton>
|
variant={ActionButtonVariant.SECONDARY}
|
||||||
|
size={ActionButtonSize.SMALL}
|
||||||
|
icon={Icons.CLOSE}
|
||||||
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
|
/>
|
||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
IonButton, IonIcon, IonList, IonItem, IonLabel
|
IonButton, IonIcon, IonList, IonItem, IonLabel
|
||||||
} from '@ionic/react';
|
} from '@ionic/react';
|
||||||
import {
|
import {
|
||||||
add, heart, heartOutline, ban, checkmark, close, people
|
add, heart, heartOutline, ban, checkmark, people
|
||||||
} from 'ionicons/icons';
|
} from 'ionicons/icons';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectIsAdmin, selectFavorites, selectSongs, selectQueue } from '../../redux';
|
import { selectIsAdmin, selectFavorites, selectSongs, selectQueue } from '../../redux';
|
||||||
@ -13,6 +13,9 @@ import { useDisabledSongs } from '../../hooks/useDisabledSongs';
|
|||||||
import { useSelectSinger } from '../../hooks/useSelectSinger';
|
import { useSelectSinger } from '../../hooks/useSelectSinger';
|
||||||
import { useToast } from '../../hooks/useToast';
|
import { useToast } from '../../hooks/useToast';
|
||||||
import SelectSinger from './SelectSinger';
|
import SelectSinger from './SelectSinger';
|
||||||
|
import { ActionButton } from './index';
|
||||||
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
||||||
|
import { Icons } from '../../constants';
|
||||||
import { SongInfoDisplay } from './SongItem';
|
import { SongInfoDisplay } from './SongItem';
|
||||||
import type { Song, QueueItem } from '../../types';
|
import type { Song, QueueItem } from '../../types';
|
||||||
|
|
||||||
@ -90,9 +93,13 @@ const SongInfo: React.FC<SongInfoProps> = ({ isOpen, onClose, song }) => {
|
|||||||
<IonHeader>
|
<IonHeader>
|
||||||
<IonToolbar>
|
<IonToolbar>
|
||||||
<IonTitle>Song Info</IonTitle>
|
<IonTitle>Song Info</IonTitle>
|
||||||
<IonButton slot="end" fill="clear" onClick={onClose}>
|
<ActionButton
|
||||||
<IonIcon icon={close} />
|
onClick={onClose}
|
||||||
</IonButton>
|
variant={ActionButtonVariant.SECONDARY}
|
||||||
|
size={ActionButtonSize.SMALL}
|
||||||
|
icon={Icons.CLOSE}
|
||||||
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
|
/>
|
||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
|
|
||||||
@ -186,9 +193,13 @@ const SongInfo: React.FC<SongInfoProps> = ({ isOpen, onClose, song }) => {
|
|||||||
<IonHeader>
|
<IonHeader>
|
||||||
<IonToolbar>
|
<IonToolbar>
|
||||||
<IonTitle>Songs by {song.artist}</IonTitle>
|
<IonTitle>Songs by {song.artist}</IonTitle>
|
||||||
<IonButton slot="end" fill="clear" onClick={() => setShowArtistSongs(false)}>
|
<ActionButton
|
||||||
<IonIcon icon={close} />
|
onClick={() => setShowArtistSongs(false)}
|
||||||
</IonButton>
|
variant={ActionButtonVariant.SECONDARY}
|
||||||
|
size={ActionButtonSize.SMALL}
|
||||||
|
icon={Icons.CLOSE}
|
||||||
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
|
/>
|
||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IonItem, IonLabel, IonIcon } from '@ionic/react';
|
import { IonItem, IonLabel } from '@ionic/react';
|
||||||
import { add, heart, heartOutline, trash, informationCircle } from 'ionicons/icons';
|
|
||||||
import ActionButton from './ActionButton';
|
import ActionButton from './ActionButton';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectQueue, selectFavorites } from '../../redux';
|
import { selectQueue, selectFavorites } from '../../redux';
|
||||||
import { debugLog } from '../../utils/logger';
|
import { debugLog } from '../../utils/logger';
|
||||||
import type { SongItemProps, QueueItem, Song } from '../../types';
|
import type { SongItemProps, QueueItem, Song } from '../../types';
|
||||||
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
||||||
|
import { Icons } from '../../constants';
|
||||||
|
|
||||||
// Utility function to extract filename from path
|
// Utility function to extract filename from path
|
||||||
const extractFilename = (path: string): string => {
|
const extractFilename = (path: string): string => {
|
||||||
@ -119,11 +120,11 @@ export const SongActionButtons: React.FC<{
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
key="info"
|
key="info"
|
||||||
onClick={onSelectSinger}
|
onClick={onSelectSinger}
|
||||||
variant="secondary"
|
variant={ActionButtonVariant.SECONDARY}
|
||||||
size="sm"
|
size={ActionButtonSize.SMALL}
|
||||||
>
|
icon={Icons.INFORMATION_CIRCLE}
|
||||||
<IonIcon icon={informationCircle} />
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
</ActionButton>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,11 +134,11 @@ export const SongActionButtons: React.FC<{
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
key="add"
|
key="add"
|
||||||
onClick={onAddToQueue || (() => {})}
|
onClick={onAddToQueue || (() => {})}
|
||||||
variant="primary"
|
variant={ActionButtonVariant.PRIMARY}
|
||||||
size="sm"
|
size={ActionButtonSize.SMALL}
|
||||||
>
|
icon={Icons.ADD}
|
||||||
<IonIcon icon={add} />
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
</ActionButton>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,11 +148,11 @@ export const SongActionButtons: React.FC<{
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
key="remove"
|
key="remove"
|
||||||
onClick={onRemoveFromQueue}
|
onClick={onRemoveFromQueue}
|
||||||
variant="danger"
|
variant={ActionButtonVariant.DANGER}
|
||||||
size="sm"
|
size={ActionButtonSize.SMALL}
|
||||||
>
|
icon={Icons.TRASH}
|
||||||
<IonIcon icon={trash} />
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
</ActionButton>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,11 +162,11 @@ export const SongActionButtons: React.FC<{
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
key="delete"
|
key="delete"
|
||||||
onClick={onDeleteItem}
|
onClick={onDeleteItem}
|
||||||
variant="danger"
|
variant={ActionButtonVariant.DANGER}
|
||||||
size="sm"
|
size={ActionButtonSize.SMALL}
|
||||||
>
|
icon={Icons.TRASH}
|
||||||
<IonIcon icon={trash} />
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
</ActionButton>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,11 +176,11 @@ export const SongActionButtons: React.FC<{
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
key="favorite"
|
key="favorite"
|
||||||
onClick={onToggleFavorite || (() => {})}
|
onClick={onToggleFavorite || (() => {})}
|
||||||
variant={isInFavorites ? 'danger' : 'secondary'}
|
variant={isInFavorites ? ActionButtonVariant.DANGER : ActionButtonVariant.SECONDARY}
|
||||||
size="sm"
|
size={ActionButtonSize.SMALL}
|
||||||
>
|
icon={isInFavorites ? Icons.HEART : Icons.HEART_OUTLINE}
|
||||||
<IonIcon icon={isInFavorites ? heart : heartOutline} />
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
</ActionButton>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -80,4 +80,78 @@ export const FEATURES = {
|
|||||||
ENABLE_HISTORY: true,
|
ENABLE_HISTORY: true,
|
||||||
ENABLE_TOP_PLAYED: true,
|
ENABLE_TOP_PLAYED: true,
|
||||||
ENABLE_ADMIN_CONTROLS: true,
|
ENABLE_ADMIN_CONTROLS: true,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
import {
|
||||||
|
add,
|
||||||
|
trash,
|
||||||
|
close,
|
||||||
|
ban,
|
||||||
|
checkmark,
|
||||||
|
searchOutline,
|
||||||
|
musicalNotesOutline,
|
||||||
|
peopleCircleOutline,
|
||||||
|
peopleOutline,
|
||||||
|
starOutline,
|
||||||
|
heart,
|
||||||
|
heartOutline,
|
||||||
|
timeOutline,
|
||||||
|
listOutline,
|
||||||
|
settingsOutline,
|
||||||
|
play,
|
||||||
|
playOutline,
|
||||||
|
pause,
|
||||||
|
pauseOutline,
|
||||||
|
stop,
|
||||||
|
stopOutline,
|
||||||
|
reorderThreeOutline,
|
||||||
|
reorderTwoOutline,
|
||||||
|
informationCircle,
|
||||||
|
logOut,
|
||||||
|
micOutline,
|
||||||
|
time,
|
||||||
|
list
|
||||||
|
} from 'ionicons/icons';
|
||||||
|
|
||||||
|
// Icon constants for better type safety and autocomplete
|
||||||
|
export const Icons = {
|
||||||
|
// Action icons
|
||||||
|
ADD: add,
|
||||||
|
TRASH: trash,
|
||||||
|
CLOSE: close,
|
||||||
|
BAN: ban,
|
||||||
|
CHECKMARK: checkmark,
|
||||||
|
|
||||||
|
// Navigation icons
|
||||||
|
SEARCH: searchOutline,
|
||||||
|
MUSIC_NOTES: musicalNotesOutline,
|
||||||
|
PEOPLE: peopleCircleOutline,
|
||||||
|
PEOPLE_OUTLINE: peopleOutline,
|
||||||
|
STAR: starOutline,
|
||||||
|
HEART: heart,
|
||||||
|
HEART_OUTLINE: heartOutline,
|
||||||
|
TIME: timeOutline,
|
||||||
|
LIST: listOutline,
|
||||||
|
SETTINGS: settingsOutline,
|
||||||
|
|
||||||
|
// Player controls
|
||||||
|
PLAY: play,
|
||||||
|
PLAY_OUTLINE: playOutline,
|
||||||
|
PAUSE: pause,
|
||||||
|
PAUSE_OUTLINE: pauseOutline,
|
||||||
|
STOP: stop,
|
||||||
|
STOP_OUTLINE: stopOutline,
|
||||||
|
|
||||||
|
// UI icons
|
||||||
|
REORDER_THREE: reorderThreeOutline,
|
||||||
|
REORDER_TWO: reorderTwoOutline,
|
||||||
|
INFORMATION_CIRCLE: informationCircle,
|
||||||
|
LOGOUT: logOut,
|
||||||
|
MIC: micOutline,
|
||||||
|
|
||||||
|
// Status icons
|
||||||
|
TIME_SIMPLE: time,
|
||||||
|
LIST_SIMPLE: list
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type IconType = typeof Icons[keyof typeof Icons];
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { IonItem, IonLabel, IonItemSliding, IonItemOptions, IonItemOption, IonButton, IonIcon, IonReorderGroup, IonReorder } from '@ionic/react';
|
import { IonItem, IonLabel, IonItemSliding, IonItemOptions, IonItemOption, IonIcon, IonReorderGroup, IonReorder } from '@ionic/react';
|
||||||
import { trash, reorderThreeOutline, reorderTwoOutline, list } from 'ionicons/icons';
|
import { reorderThreeOutline, reorderTwoOutline, list } from 'ionicons/icons';
|
||||||
import { useQueue } from '../../hooks';
|
import { useQueue } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectQueueLength, selectPlayerStateMemoized, selectIsAdmin, selectControllerName } from '../../redux';
|
import { selectQueueLength, selectPlayerStateMemoized, selectIsAdmin, selectControllerName } from '../../redux';
|
||||||
import { ActionButton, NumberDisplay, EmptyState } from '../../components/common';
|
import { ActionButton, NumberDisplay, EmptyState } from '../../components/common';
|
||||||
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
||||||
|
import { Icons } from '../../constants';
|
||||||
import { SongInfoDisplay } from '../../components/common/SongItem';
|
import { SongInfoDisplay } from '../../components/common/SongItem';
|
||||||
import { queueService } from '../../firebase/services';
|
import { queueService } from '../../firebase/services';
|
||||||
import { debugLog } from '../../utils/logger';
|
import { debugLog } from '../../utils/logger';
|
||||||
@ -130,11 +132,11 @@ const Queue: React.FC = () => {
|
|||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={() => handleRemoveFromQueue(queueItem)}
|
onClick={() => handleRemoveFromQueue(queueItem)}
|
||||||
variant="danger"
|
variant={ActionButtonVariant.DANGER}
|
||||||
size="sm"
|
size={ActionButtonSize.SMALL}
|
||||||
>
|
icon={Icons.TRASH}
|
||||||
<IonIcon icon={trash} />
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
</ActionButton>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{canReorder && queueMode === 'reorder' && (
|
{canReorder && queueMode === 'reorder' && (
|
||||||
@ -152,7 +154,7 @@ const Queue: React.FC = () => {
|
|||||||
color="danger"
|
color="danger"
|
||||||
onClick={() => handleRemoveFromQueue(queueItem)}
|
onClick={() => handleRemoveFromQueue(queueItem)}
|
||||||
>
|
>
|
||||||
<IonIcon icon={trash} slot="icon-only" />
|
<IonIcon icon={Icons.TRASH} slot="icon-only" />
|
||||||
</IonItemOption>
|
</IonItemOption>
|
||||||
</IonItemOptions>
|
</IonItemOptions>
|
||||||
)}
|
)}
|
||||||
@ -194,11 +196,11 @@ const Queue: React.FC = () => {
|
|||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={() => handleRemoveFromQueue(firstItem)}
|
onClick={() => handleRemoveFromQueue(firstItem)}
|
||||||
variant="danger"
|
variant={ActionButtonVariant.DANGER}
|
||||||
size="sm"
|
size={ActionButtonSize.SMALL}
|
||||||
>
|
icon={Icons.TRASH}
|
||||||
<IonIcon icon={trash} />
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
</ActionButton>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -211,7 +213,7 @@ const Queue: React.FC = () => {
|
|||||||
color="danger"
|
color="danger"
|
||||||
onClick={() => handleRemoveFromQueue(firstItem)}
|
onClick={() => handleRemoveFromQueue(firstItem)}
|
||||||
>
|
>
|
||||||
<IonIcon icon={trash} slot="icon-only"/>
|
<IonIcon icon={Icons.TRASH} slot="icon-only"/>
|
||||||
</IonItemOption>
|
</IonItemOption>
|
||||||
</IonItemOptions>
|
</IonItemOptions>
|
||||||
)}
|
)}
|
||||||
@ -234,13 +236,14 @@ const Queue: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="ion-padding ion-text-end">
|
<div className="ion-padding ion-text-end">
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<IonButton
|
<ActionButton
|
||||||
onClick={toggleQueueMode}
|
onClick={toggleQueueMode}
|
||||||
|
variant={ActionButtonVariant.SECONDARY}
|
||||||
|
size={ActionButtonSize.SMALL}
|
||||||
|
icon={queueMode === 'delete' ? reorderThreeOutline : Icons.TRASH}
|
||||||
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
fill="outline"
|
fill="outline"
|
||||||
size="small"
|
/>
|
||||||
>
|
|
||||||
<IonIcon icon={queueMode === 'delete' ? reorderThreeOutline : trash} />
|
|
||||||
</IonButton>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { IonContent, IonHeader, IonTitle, IonToolbar, IonList, IonItem, IonLabel, IonToggle, IonButton, IonIcon, IonModal, IonSearchbar } from '@ionic/react';
|
import { IonContent, IonHeader, IonTitle, IonToolbar, IonList, IonItem, IonLabel, IonToggle, IonButton, IonIcon, IonModal, IonSearchbar } from '@ionic/react';
|
||||||
import { ban, trash } from 'ionicons/icons';
|
import { ban } from 'ionicons/icons';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectIsAdmin, selectSettings } from '../../redux';
|
import { selectIsAdmin, selectSettings } from '../../redux';
|
||||||
import { useDisabledSongs } from '../../hooks';
|
import { useDisabledSongs } from '../../hooks';
|
||||||
import { InfiniteScrollList, ActionButton } from '../../components/common';
|
import { InfiniteScrollList, ActionButton } from '../../components/common';
|
||||||
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
||||||
|
import { Icons } from '../../constants';
|
||||||
import { filterSongs } from '../../utils/dataProcessing';
|
import { filterSongs } from '../../utils/dataProcessing';
|
||||||
import { setDebugEnabled, isDebugEnabled, debugLog } from '../../utils/logger';
|
import { setDebugEnabled, isDebugEnabled, debugLog } from '../../utils/logger';
|
||||||
import type { Song, DisabledSong } from '../../types';
|
import type { Song, DisabledSong } from '../../types';
|
||||||
@ -181,11 +183,11 @@ const Settings: React.FC = () => {
|
|||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={() => handleRemoveDisabledSong(song)}
|
onClick={() => handleRemoveDisabledSong(song)}
|
||||||
variant="danger"
|
variant={ActionButtonVariant.DANGER}
|
||||||
size="sm"
|
size={ActionButtonSize.SMALL}
|
||||||
>
|
icon={Icons.TRASH}
|
||||||
<IonIcon icon={trash} />
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
</ActionButton>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</IonItem>
|
</IonItem>
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { IonItem, IonLabel, IonIcon, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonContent, IonInput, IonLabel as IonInputLabel } from '@ionic/react';
|
import { IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonContent, IonInput, IonLabel as IonInputLabel } from '@ionic/react';
|
||||||
import { trash, add, close } from 'ionicons/icons';
|
|
||||||
import { InfiniteScrollList, ActionButton, NumberDisplay } from '../../components/common';
|
import { InfiniteScrollList, ActionButton, NumberDisplay } from '../../components/common';
|
||||||
import { useSingers } from '../../hooks';
|
import { useSingers } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectSingers } from '../../redux';
|
import { selectSingers } from '../../redux';
|
||||||
import { debugLog } from '../../utils/logger';
|
import { debugLog } from '../../utils/logger';
|
||||||
import type { Singer } from '../../types';
|
import type { Singer } from '../../types';
|
||||||
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
||||||
|
import { Icons } from '../../constants';
|
||||||
|
|
||||||
const Singers: React.FC = () => {
|
const Singers: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
@ -59,15 +60,13 @@ const Singers: React.FC = () => {
|
|||||||
{/* Delete Button (Admin Only) */}
|
{/* Delete Button (Admin Only) */}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<div slot="end" style={{ marginRight: '-16px' }}>
|
<div slot="end" style={{ marginRight: '-16px' }}>
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
<ActionButton
|
||||||
<ActionButton
|
onClick={() => handleRemoveSinger(singer)}
|
||||||
onClick={() => handleRemoveSinger(singer)}
|
variant={ActionButtonVariant.DANGER}
|
||||||
variant="danger"
|
size={ActionButtonSize.SMALL}
|
||||||
size="sm"
|
icon={Icons.TRASH}
|
||||||
>
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
<IonIcon icon={trash} />
|
/>
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</IonItem>
|
</IonItem>
|
||||||
@ -77,12 +76,14 @@ const Singers: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="ion-padding ion-text-end">
|
<div className="ion-padding ion-text-end">
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<IonButton
|
<ActionButton
|
||||||
fill="clear"
|
|
||||||
onClick={handleOpenAddModal}
|
onClick={handleOpenAddModal}
|
||||||
>
|
variant={ActionButtonVariant.PRIMARY}
|
||||||
<IonIcon icon={add} slot="icon-only" size="large" />
|
size={ActionButtonSize.SMALL}
|
||||||
</IonButton>
|
icon={Icons.ADD}
|
||||||
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
|
fill="clear"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -111,9 +112,13 @@ const Singers: React.FC = () => {
|
|||||||
<IonHeader>
|
<IonHeader>
|
||||||
<IonToolbar>
|
<IonToolbar>
|
||||||
<IonTitle>Add New Singer</IonTitle>
|
<IonTitle>Add New Singer</IonTitle>
|
||||||
<IonButton slot="end" fill="clear" onClick={handleCloseAddModal}>
|
<ActionButton
|
||||||
<IonIcon icon={close} />
|
onClick={handleCloseAddModal}
|
||||||
</IonButton>
|
variant={ActionButtonVariant.SECONDARY}
|
||||||
|
size={ActionButtonSize.SMALL}
|
||||||
|
icon={Icons.CLOSE}
|
||||||
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
|
/>
|
||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import React, { useState, useMemo, useCallback } from 'react';
|
import React, { useState, useMemo, useCallback } from 'react';
|
||||||
import { IonChip, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonContent, IonList } from '@ionic/react';
|
import { IonChip, IonModal, IonHeader, IonToolbar, IonTitle, IonIcon, IonContent, IonList } from '@ionic/react';
|
||||||
import { close, list } from 'ionicons/icons';
|
import { list } from 'ionicons/icons';
|
||||||
import { useTopPlayed } from '../../hooks';
|
import { useTopPlayed } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectTopPlayed, selectSongsArray } from '../../redux';
|
import { selectTopPlayed, selectSongsArray } from '../../redux';
|
||||||
import { InfiniteScrollList, SongItem, ListItem, SongInfo } from '../../components/common';
|
import { InfiniteScrollList, SongItem, ListItem, SongInfo, ActionButton } from '../../components/common';
|
||||||
import { filterSongs } from '../../utils/dataProcessing';
|
import { filterSongs } from '../../utils/dataProcessing';
|
||||||
import { debugLog } from '../../utils/logger';
|
import { debugLog } from '../../utils/logger';
|
||||||
import { useSongOperations } from '../../hooks';
|
import { useSongOperations } from '../../hooks';
|
||||||
import { useToast } from '../../hooks';
|
import { useToast } from '../../hooks';
|
||||||
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
||||||
|
import { Icons } from '../../constants';
|
||||||
import type { TopPlayed, Song } from '../../types';
|
import type { TopPlayed, Song } from '../../types';
|
||||||
|
|
||||||
const Top100: React.FC = () => {
|
const Top100: React.FC = () => {
|
||||||
@ -139,9 +141,13 @@ const Top100: React.FC = () => {
|
|||||||
<IonHeader>
|
<IonHeader>
|
||||||
<IonToolbar>
|
<IonToolbar>
|
||||||
<IonTitle>{selectedTopPlayed?.artist}</IonTitle>
|
<IonTitle>{selectedTopPlayed?.artist}</IonTitle>
|
||||||
<IonButton slot="end" fill="clear" onClick={handleCloseModal}>
|
<ActionButton
|
||||||
<IonIcon icon={close} />
|
onClick={handleCloseModal}
|
||||||
</IonButton>
|
variant={ActionButtonVariant.SECONDARY}
|
||||||
|
size={ActionButtonSize.SMALL}
|
||||||
|
icon={Icons.CLOSE}
|
||||||
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
|
/>
|
||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
|
|
||||||
|
|||||||
@ -117,13 +117,49 @@ export interface ToastProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActionButton constants
|
||||||
|
export const ActionButtonVariant = {
|
||||||
|
PRIMARY: 'primary',
|
||||||
|
SECONDARY: 'secondary',
|
||||||
|
DANGER: 'danger'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ActionButtonSize = {
|
||||||
|
SMALL: 'sm',
|
||||||
|
MEDIUM: 'md',
|
||||||
|
LARGE: 'lg'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ActionButtonIconSlot = {
|
||||||
|
START: 'start',
|
||||||
|
END: 'end',
|
||||||
|
ICON_ONLY: 'icon-only'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ActionButtonIconSize = {
|
||||||
|
SMALL: 18,
|
||||||
|
MEDIUM: 20,
|
||||||
|
LARGE: 22
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ActionButtonVariantType = typeof ActionButtonVariant[keyof typeof ActionButtonVariant];
|
||||||
|
export type ActionButtonSizeType = typeof ActionButtonSize[keyof typeof ActionButtonSize];
|
||||||
|
export type ActionButtonIconSlotType = typeof ActionButtonIconSlot[keyof typeof ActionButtonIconSlot];
|
||||||
|
export type ActionButtonIconSizeType = typeof ActionButtonIconSize[keyof typeof ActionButtonIconSize];
|
||||||
|
|
||||||
|
import type { IconType } from '../constants';
|
||||||
|
|
||||||
export interface ActionButtonProps {
|
export interface ActionButtonProps {
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
children: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
variant?: 'primary' | 'secondary' | 'danger';
|
variant?: ActionButtonVariantType;
|
||||||
size?: 'sm' | 'md' | 'lg';
|
size?: ActionButtonSizeType;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
icon?: IconType;
|
||||||
|
iconSlot?: ActionButtonIconSlotType;
|
||||||
|
iconSize?: ActionButtonIconSizeType;
|
||||||
|
fill?: 'solid' | 'outline' | 'clear';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SongItemProps {
|
export interface SongItemProps {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user