Skip to content

Commit

Permalink
Finalize making the SideNav component playlist-agnostic
Browse files Browse the repository at this point in the history
  • Loading branch information
martpie committed Oct 26, 2024
1 parent abef4cb commit 92b231b
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 227 deletions.
53 changes: 7 additions & 46 deletions src/components/SideNav/SideNav.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,60 +9,21 @@
position: relative;
}

.sideNav__actions {
.sideNavActions {
align-items: stretch;
}

.sideNav__title {
.sideNavTitle {
margin: 10px 12px;
font-size: 11px;
font-size: .875rem;
font-weight: bold;
color: var(--text-muted);
flex: 1;
}

.sideNav__body {
.sideNavItems {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: auto;
}

.item__link {
font-size: 1rem;
display: block;
color: inherit;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
cursor: pointer;
padding: 8px 12px;
text-decoration: none;
border-style: solid;
border-color: transparent;
border-width: 1px 0;

&:hover {
background-color: var(--sidebar-item-active-bg);
color: var(--text);
}

&:global(.isActive) {
z-index: 10;
border-style: solid;
border-color: var(--border-color);
border-width: 1px 0;
background-color: var(--sidebar-item-active-bg);
color: var(--main-color);
}
}

.item__input {
font-size: 1rem;
display: block;
width: 100%;
background-color: var(--sidebar-item-active-bg);
color: var(--main-color);
padding: 8px 12px;
outline: none;
border-style: solid;
border-color: var(--border-color);
border-width: 1px 0;
}
167 changes: 5 additions & 162 deletions src/components/SideNav/SideNav.tsx
Original file line number Diff line number Diff line change
@@ -1,179 +1,22 @@
import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu';
import type React from 'react';
import { useCallback, useState } from 'react';

import type { Playlist } from '../../generated/typings';
import database from '../../lib/database';
import { logAndNotifyError } from '../../lib/utils';
import PlaylistsAPI from '../../stores/PlaylistsAPI';

import { useNavigate } from 'react-router-dom';
import ButtonIcon from '../../elements/ButtonIcon/ButtonIcon';
import Flexbox from '../../elements/Flexbox/Flexbox';
import useInvalidate from '../../hooks/useInvalidate';
import SideNavLink from '../SideNavLink/SideNavLink';
import styles from './SideNav.module.css';

type Props = {
children: React.ReactNode;
title: string;
playlists: Playlist[];
actions: React.ReactNode;
};

// TODO: finish making this component playlist agnostic
export default function SideNav(props: Props) {
const invalidate = useInvalidate();
const navigate = useNavigate();

const [renamed, setRenamed] = useState<string | null>(null);

const showContextMenu = useCallback(
async (e: React.MouseEvent, playlistID: string) => {
e.preventDefault();

const menuItems = await Promise.all([
MenuItem.new({
text: 'Rename',
action: () => {
setRenamed(playlistID);
},
}),
MenuItem.new({
text: 'Delete',
action: async () => {
await PlaylistsAPI.remove(playlistID);
invalidate();
},
}),
PredefinedMenuItem.new({ item: 'Separator' }),
MenuItem.new({
text: 'Duplicate',
action: async () => {
await PlaylistsAPI.duplicate(playlistID);
invalidate();
},
}),
PredefinedMenuItem.new({ item: 'Separator' }),
MenuItem.new({
text: 'Export',
action: async () => {
await database.exportPlaylist(playlistID);
},
}),
]);

const menu = await Menu.new({
items: menuItems,
});

await menu.popup().catch(logAndNotifyError);
},
[invalidate],
);

const createPlaylist = useCallback(async () => {
// TODO: 'new playlist 1', 'new playlist 2' ...
const playlist = await PlaylistsAPI.create('New playlist', [], false);

if (playlist) {
invalidate();
navigate(`/playlists/${playlist._id}`);
}
}, [navigate, invalidate]);

const onRename = useCallback(
async (playlistID: string, name: string) => {
await PlaylistsAPI.rename(playlistID, name);
invalidate();
},
[invalidate],
);

const keyDown = useCallback(
async (e: React.KeyboardEvent<HTMLInputElement>) => {
e.persist();

switch (e.nativeEvent.code) {
case 'Enter': {
// Enter
if (renamed && e.currentTarget) {
await onRename(renamed, e.currentTarget.value);
setRenamed(null);
}
break;
}
case 'Escape': {
// Escape
setRenamed(null);
break;
}
default: {
break;
}
}
},
[onRename, renamed],
);

const blur = useCallback(
async (e: React.FocusEvent<HTMLInputElement>) => {
if (renamed) {
await onRename(renamed, e.currentTarget.value);
}

setRenamed(null);
},
[onRename, renamed],
);

const focus = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
e.currentTarget.select();
}, []);

const { playlists } = props;

const nav = playlists.map((elem) => {
let navItemContent;

if (elem._id === renamed) {
navItemContent = (
<input
className={styles.item__input}
type="text"
defaultValue={elem.name}
onKeyDown={keyDown}
onBlur={blur}
onFocus={focus}
ref={(ref) => ref?.focus()}
/>
);
} else {
navItemContent = (
<SideNavLink
className={styles.item__link}
playlistID={elem._id}
onContextMenu={showContextMenu}
>
{elem.name}
</SideNavLink>
);
}

return <div key={elem._id}>{navItemContent}</div>;
});

return (
<div className={styles.sideNav}>
<Flexbox gap={8} align="center">
<h4 className={styles.sideNav__title}>{props.title}</h4>
<div className={styles.sideNav__actions}>
<ButtonIcon
icon="plus"
onClick={createPlaylist}
title="New Playlist"
/>
</div>
<h4 className={styles.sideNavTitle}>{props.title}</h4>
<div className={styles.sideNavActions}>{props.actions}</div>
</Flexbox>
<div className={styles.sideNav__body}>{nav}</div>
<div className={styles.sideNavItems}>{props.children}</div>
</div>
);
}
44 changes: 42 additions & 2 deletions src/components/SideNavLink/SideNavLink.module.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,46 @@
.playlistLink {
&:hover,
.sideNavLink {
display: block;
width: 100%;
line-height: 1;
font-size: 1rem;
color: inherit;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
cursor: pointer;
padding: 8px 12px;
text-decoration: none;
border-style: solid;
border-color: transparent;
border-width: 1px 0;

&:hover {
background-color: var(--sidebar-item-active-bg);
color: var(--text);
text-decoration: none;
}

&:focus {
text-decoration: none;
}

&:global(.isActive) {
outline: none;
z-index: 10;
border-style: solid;
border-color: var(--border-color);
border-width: 1px 0;
background-color: var(--sidebar-item-active-bg);
color: var(--main-color);
}
}

.sideNavLinkInput {
appearance: none;
display: block;
padding: 0;
border: none;
background: transparent;
font-size: 1rem;
line-height: 1;
}
Loading

0 comments on commit 92b231b

Please sign in to comment.