Skip to content

Commit

Permalink
feat: implement to be able to edit labels
Browse files Browse the repository at this point in the history
  • Loading branch information
Quiddlee committed Aug 28, 2024
1 parent e35e200 commit 049512f
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 8 deletions.
116 changes: 116 additions & 0 deletions client/src/layout/NotesLayout/ui/NavigationDrawer/ui/LabelDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { FC, MouseEvent, useRef, useState } from 'react';

import { MdMenu } from '@material/web/all';

import { menuItemStyles } from '@pages/Notes/lib/const';
import cn from '@shared/lib/helpers/cn';
import ConfirmDialog from '@shared/ui/ConfirmDialog';
import ContextMenu from '@shared/ui/ContextMenu';
import FilledTonalIconButton from '@shared/ui/FilledTonalIconButton';
import Icon from '@shared/ui/Icon';
import InputDialog from '@shared/ui/InputDialog';
import MenuItem from '@shared/ui/MenuItem';
import { useTranslation } from 'react-i18next';

type DetailsProps = {
id: number;
};

const Details: FC<DetailsProps> = ({ id }) => {
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
const [isDialogRenameOpen, setIsDialogRenameOpen] = useState(false);
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
const { t } = useTranslation();
const menuRef = useRef<MdMenu>(null);

function handleMenuToggle(e: MouseEvent) {
e.stopPropagation();
e.preventDefault();
if (menuRef.current) {
menuRef.current.open = !menuRef.current.open;

setIsContextMenuOpen(menuRef.current.open);
}
}

const handleToggleConfirmDialog = (e: MouseEvent) => {
e.stopPropagation();
e.preventDefault();
console.log('confirm');

setIsConfirmDialogOpen((prev) => !prev);
};

const handleDeleteLabel = async (e: MouseEvent) => {
e.stopPropagation();
};

const handleToggleDialogRename = (e: MouseEvent) => {
e.stopPropagation();
e.preventDefault();

console.log('rename');
setIsDialogRenameOpen((prev) => !prev);
};

const handleRenameLabel = async (e: MouseEvent) => {
e.stopPropagation();
};

return (
<>
<ConfirmDialog
setIsOpen={setIsConfirmDialogOpen}
open={isConfirmDialogOpen}
title={t('deleteDialog.title')}
subtitle={t('deleteDialog.content')}
confirmText={t('deleteDialog.delete')}
cancelText={t('deleteDialog.cancel')}
onCancel={handleToggleConfirmDialog}
onConfirm={handleDeleteLabel}
/>
<InputDialog
open={isDialogRenameOpen}
setIsOpen={setIsDialogRenameOpen}
title={t('renameDialog.title')}
initialValue="label title"
onCancel={handleToggleDialogRename}
onConfirm={handleRenameLabel}
/>
<ContextMenu
id={`details-menu-${id}`}
className="relative ml-auto flex items-center brightness-125"
button={
<FilledTonalIconButton
className={cn('invisible absolute -right-4 group-hover:visible', {
visible:
isDialogRenameOpen || isConfirmDialogOpen || isContextMenuOpen,
})}
onClick={(e) => handleMenuToggle(e)}>
<Icon>more_vert</Icon>
</FilledTonalIconButton>
}>
<MenuItem
style={menuItemStyles}
className="mx-2 rounded-md"
onClick={handleToggleDialogRename}>
Rename
<Icon className="text-on-surface" slot="end">
edit
</Icon>
</MenuItem>
<MenuItem
style={menuItemStyles}
className="mx-2 rounded-md"
onClick={handleToggleConfirmDialog}>
Delete
<Icon className="text-on-surface" slot="end">
delete
</Icon>
</MenuItem>
</ContextMenu>
</>
);
};

export default Details;
81 changes: 81 additions & 0 deletions client/src/layout/NotesLayout/ui/NavigationDrawer/ui/LabelItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { FC, PropsWithChildren } from 'react';

import { filledIconStyles } from '@shared/lib/const';
import cn from '@shared/lib/helpers/cn';
import Icon from '@shared/ui/Icon';
import { motion } from 'framer-motion';
import { NavLink } from 'react-router-dom';

type TabItemProps = PropsWithChildren<{
id: number;
to: string;
}>;

const animations = {
initial: { scale: 0, opacity: 0 },
animate: { scale: 1, opacity: 1 },
transition: { type: 'spring', stiffness: 550, damping: 40 },
};

const LabelItem: FC<TabItemProps> = ({ children, to }) => {
return (
<motion.div {...animations} layout>
<NavLink
to={to}
className="group relative inline-block w-full animate-fade-in-screen rounded-full">
{({ isActive }) => (
<>
<span
className={cn(
'absolute left-0 top-0 h-full w-full scale-x-[.32] rounded-full bg-surface-container-highest py-4 pl-4 pr-6 opacity-0 transition-transform duration-200 ease-linear active:brightness-90',
{
'scale-x-100 bg-primary-container opacity-100 group-hover:brightness-95 group-active:brightness-95':
isActive,
},
)}
/>
<span
className={cn(
'relative z-10 flex origin-left items-center gap-3 rounded-full py-4 pl-4 pr-6 text-on-surface-variant group-hover:bg-[color-mix(in_srgb,_var(--md-sys-color-inverse-surface)_8%,_transparent)] group-active:brightness-95',
{
'text-on-secondary-container-text font-medium': isActive,
},
)}>
<span className="animation-delay-200 flex animate-fade-in-standard items-center">
<Icon style={isActive ? filledIconStyles : undefined}>
label
</Icon>
</span>
{children}
</span>
</>
)}
</NavLink>
{/* <div className="group relative inline-block w-full animate-fade-in-screen rounded-full"> */}
{/* <span */}
{/* className={cn( */}
{/* 'absolute left-0 top-0 h-full w-full scale-x-[.32] rounded-full bg-surface-container-highest py-4 pl-4 pr-6 opacity-0 transition-transform duration-200 ease-linear active:brightness-90', */}
{/* { */}
{/* 'scale-x-100 bg-primary-container opacity-100 group-hover:brightness-95 group-active:brightness-95': */}
{/* isActive, */}
{/* }, */}
{/* )} */}
{/* /> */}
{/* <span */}
{/* className={cn( */}
{/* 'relative z-10 flex origin-left items-center gap-3 rounded-full py-4 pl-4 pr-6 group-hover:bg-[color-mix(in_srgb,_var(--md-sys-color-inverse-surface)_8%,_transparent)] group-active:brightness-95', */}
{/* { */}
{/* 'text-on-secondary-container-text font-medium': isActive, */}
{/* }, */}
{/* )}> */}
{/* <span className="animation-delay-200 flex animate-fade-in-standard items-center"> */}
{/* <Icon style={isActive ? filledIconStyles : undefined}>label</Icon> */}
{/* </span> */}
{/* {children} */}
{/* </span> */}
{/* </div> */}
</motion.div>
);
};

export default LabelItem;
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import Details from '@layout/NotesLayout/ui/NavigationDrawer/ui/LabelDetails';
import LabelItem from '@layout/NotesLayout/ui/NavigationDrawer/ui/LabelItem';
import { routes } from '@shared/lib/const';
import NavItem from '@shared/ui/NavItem';

import { mockLabels } from '../../../../../../dev-data';

const LabelsList = () => {
return (
<ul className="pb-14">
{mockLabels.map((label, i) => (
<NavItem key={label.title} icon="label" to={`${routes.LABEL}/${i}`}>
{label.title}
</NavItem>
<LabelItem id={i} key={label.title} to={`${routes.LABEL}/${i}`}>
<span className="truncate group-hover:pe-8 [&:has(+_article_.visible)]:pe-8">
{label.title}
</span>
<Details id={i} />
</LabelItem>
))}
</ul>
);
Expand Down
12 changes: 12 additions & 0 deletions client/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,18 @@
"userOptions": "User options",
"options": "Options",
"compose": "Compose a new note"
},
"renameDialog": {
"title": "Rename this label",
"cancel": "Cancel",
"rename": "Rename"
},
"deleteDialog": {
"title": "Delete label?",
"content":
"You'll no longer see this label. This will also delete label from all the related notes.",
"cancel": "Cancel",
"delete": "Delete"
}
}
}
12 changes: 12 additions & 0 deletions client/src/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@
"userOptions": "Пользовательские опции",
"options": "Опции",
"compose": "Создать новую заметку"
},
"renameDialog": {
"title": "Переименовать этот лейбл",
"cancel": "Отмена",
"rename": "Переименовать"
},
"deleteDialog": {
"title": "Удалить лейбл?",
"content":
"Вы больше не увидите этот лейбл. Это также удалит лейбл из всех заметок.",
"cancel": "Отмена",
"delete": "Удалить"
}
}
}
5 changes: 4 additions & 1 deletion client/src/shared/ui/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type ContextMenuProps = PropsWithChildren<{
anchorCorner?: Corner;
yOffset?: number;
tooltipContent?: string;
className?: string;
}>;

const ContextMenu: FC<ContextMenuProps> = ({
Expand All @@ -27,6 +28,7 @@ const ContextMenu: FC<ContextMenuProps> = ({
anchorCorner = Corner.END_END,
yOffset = 12,
tooltipContent,
className,
...props
}) => {
const menuRef = useRef<MdMenu>(null);
Expand All @@ -35,6 +37,7 @@ const ContextMenu: FC<ContextMenuProps> = ({

const handleMenuOpen = (e: MouseEvent) => {
e.stopPropagation();
e.preventDefault();

if (menuRef.current?.open) {
menuRef.current?.close();
Expand All @@ -56,7 +59,7 @@ const ContextMenu: FC<ContextMenuProps> = ({
);

return (
<div {...props} className="relative">
<div {...props} className={cn('relative', className)}>
<Tooltip
className={cn({ hidden: !isTooltipVisible })}
content={tooltipContent}>
Expand Down
12 changes: 12 additions & 0 deletions client/src/shared/ui/FilledTonalIconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';

import { createComponent } from '@lit/react';
import { MdFilledTonalIconButton } from '@material/web/all';

const FilledTonalIconButton = createComponent({
react: React,
tagName: 'md-filled-tonal-icon-button',
elementClass: MdFilledTonalIconButton,
});

export default FilledTonalIconButton;
81 changes: 81 additions & 0 deletions client/src/shared/ui/InputDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
Dispatch,
FC,
MouseEvent,
SetStateAction,
useRef,
useState,
} from 'react';

import { MdDialog } from '@material/web/all';
import { MdOutlinedTextField } from '@material/web/textfield/outlined-text-field';

import FilledTonalButton from '@/shared/ui/FilledTonalButton';
import TextButton from '@/shared/ui/TextButton';
import Dialog from '@shared/ui/Dialog';
import OutlinedTextField from '@shared/ui/OutlinedTextField';

type ConfirmDialogProps = {
setIsOpen: Dispatch<SetStateAction<boolean>>;
open: boolean;
title: string;
cancelText?: string;
confirmText?: string;
initialValue?: string;
onCancel: (e: MouseEvent) => void | Promise<void>;
onConfirm: (e: MouseEvent) => void | Promise<void>;
};

const InputDialog: FC<ConfirmDialogProps> = ({
open,
setIsOpen,
title,
cancelText = 'Cancel',
confirmText = 'Save',
onCancel,
onConfirm,
initialValue,
}) => {
const [val, setVal] = useState(initialValue);
const dialogRef = useRef<MdDialog>(null);

const handleConfirm = async (e: MouseEvent) => {
e.stopPropagation();
await dialogRef.current?.close();
onConfirm(e);
};

const handleCancel = async (e: MouseEvent) => {
e.stopPropagation();
await dialogRef.current?.close();
onCancel(e);
};

return (
<Dialog
open={open}
ref={dialogRef}
className="max-w-[312px]"
closed={() => setIsOpen(false)}>
<h3 slot="headline">{title}</h3>
<form slot="content" id="form-id" method="dialog">
<OutlinedTextField
onInput={(e) => {
setVal((e.target as MdOutlinedTextField).value);
}}
className="w-full text-start"
textDirection="ltr"
value={val}
/>
</form>
<div slot="actions">
<TextButton onClick={handleCancel}>{cancelText}</TextButton>
<FilledTonalButton onClick={handleConfirm}>
{confirmText}
</FilledTonalButton>
</div>
</Dialog>
);
};

export default InputDialog;
Loading

0 comments on commit 049512f

Please sign in to comment.