Move create new button to the bottom
This commit is contained in:
parent
093471bfbe
commit
c565bc0fed
6 changed files with 177 additions and 149 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "backend",
|
||||
"version": "v0.64",
|
||||
"version": "v0.65",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import SidebarNav from './Sidebar/SidebarNav';
|
|||
import SidebarNotes from './Sidebar/SidebarNotes';
|
||||
import SidebarProjects from './Sidebar/SidebarProjects';
|
||||
import SidebarTags from './Sidebar/SidebarTags';
|
||||
import CreateNewDropdownButton from './Sidebar/CreateNewDropdownButton';
|
||||
|
||||
interface SidebarProps {
|
||||
isSidebarOpen: boolean;
|
||||
|
|
@ -70,12 +69,6 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||
<div className="flex flex-col h-full overflow-y-auto">
|
||||
<div className="px-3 pb-3 pt-8">
|
||||
{/* Sidebar Contents */}
|
||||
<CreateNewDropdownButton
|
||||
openTaskModal={(type) => openTaskModal(type || 'full')}
|
||||
openProjectModal={openProjectModal}
|
||||
openNoteModal={openNoteModal}
|
||||
openAreaModal={openAreaModal}
|
||||
/>
|
||||
<SidebarNav
|
||||
handleNavClick={handleNavClick}
|
||||
location={location}
|
||||
|
|
@ -118,6 +111,11 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||
setIsSidebarOpen={setIsSidebarOpen}
|
||||
isDropdownOpen={isDropdownOpen}
|
||||
toggleDropdown={toggleDropdown}
|
||||
openTaskModal={openTaskModal}
|
||||
openProjectModal={openProjectModal}
|
||||
openNoteModal={openNoteModal}
|
||||
openAreaModal={openAreaModal}
|
||||
openTagModal={openTagModal}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,129 +0,0 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
PlusCircleIcon,
|
||||
ChevronDownIcon,
|
||||
ClipboardIcon,
|
||||
FolderIcon,
|
||||
BookOpenIcon,
|
||||
Squares2X2Icon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Note } from '../../entities/Note';
|
||||
import { Area } from '../../entities/Area';
|
||||
|
||||
interface CreateNewDropdownButtonProps {
|
||||
openTaskModal: (type?: 'simplified' | 'full') => void;
|
||||
openProjectModal: () => void;
|
||||
openNoteModal: (note: Note | null) => void;
|
||||
openAreaModal: (area: Area | null) => void;
|
||||
}
|
||||
|
||||
const CreateNewDropdownButton: React.FC<CreateNewDropdownButtonProps> = ({
|
||||
openTaskModal,
|
||||
openProjectModal,
|
||||
openNoteModal,
|
||||
openAreaModal,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsDropdownOpen(!isDropdownOpen);
|
||||
};
|
||||
|
||||
// Handle click outside to close dropdown
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isDropdownOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isDropdownOpen]);
|
||||
|
||||
const handleDropdownSelect = (type: string) => {
|
||||
switch (type) {
|
||||
case 'Task':
|
||||
openTaskModal('full');
|
||||
break;
|
||||
case 'Project':
|
||||
openProjectModal();
|
||||
break;
|
||||
case 'Note':
|
||||
openNoteModal(null);
|
||||
break;
|
||||
case 'Area':
|
||||
openAreaModal(null);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
const dropdownItems = [
|
||||
{ label: 'Task', translationKey: 'dropdown.task', icon: <ClipboardIcon className="h-5 w-5 mr-2" /> },
|
||||
{ label: 'Project', translationKey: 'dropdown.project', icon: <FolderIcon className="h-5 w-5 mr-2" /> },
|
||||
{ label: 'Note', translationKey: 'dropdown.note', icon: <BookOpenIcon className="h-5 w-5 mr-2" /> },
|
||||
{ label: 'Area', translationKey: 'dropdown.area', icon: <Squares2X2Icon className="h-5 w-5 mr-2" /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-8 px-4">
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex justify-between items-center w-full rounded-md border border-gray-300 dark:border-gray-700 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none"
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<PlusCircleIcon
|
||||
className="w-5 h-5 mr-2 text-gray-500 dark:text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t('dropdown.createNew', 'Create New')}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute left-0 right-0 mt-2 w-full">
|
||||
<div className="rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 z-10">
|
||||
<ul
|
||||
className="py-1"
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="options-menu"
|
||||
>
|
||||
{dropdownItems.map(({ label, translationKey, icon }) => (
|
||||
<li
|
||||
key={label}
|
||||
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex items-center"
|
||||
onClick={() => handleDropdownSelect(label)}
|
||||
role="menuitem"
|
||||
>
|
||||
{icon}
|
||||
{t(translationKey, label)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateNewDropdownButton;
|
||||
|
|
@ -1,5 +1,18 @@
|
|||
import React from 'react';
|
||||
import { SunIcon, MoonIcon } from '@heroicons/react/24/outline';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
SunIcon,
|
||||
MoonIcon,
|
||||
PlusIcon,
|
||||
CheckIcon,
|
||||
FolderIcon,
|
||||
BookOpenIcon,
|
||||
Squares2X2Icon,
|
||||
TagIcon,
|
||||
InboxIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Note } from '../../entities/Note';
|
||||
import { Area } from '../../entities/Area';
|
||||
|
||||
interface SidebarFooterProps {
|
||||
currentUser: { email: string };
|
||||
|
|
@ -9,6 +22,11 @@ interface SidebarFooterProps {
|
|||
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isDropdownOpen: boolean;
|
||||
toggleDropdown: () => void;
|
||||
openTaskModal: (type?: 'simplified' | 'full') => void;
|
||||
openProjectModal: () => void;
|
||||
openNoteModal: (note: Note | null) => void;
|
||||
openAreaModal: (area: Area | null) => void;
|
||||
openTagModal: (tag: any | null) => void;
|
||||
}
|
||||
|
||||
const SidebarFooter: React.FC<SidebarFooterProps> = ({
|
||||
|
|
@ -16,16 +34,160 @@ const SidebarFooter: React.FC<SidebarFooterProps> = ({
|
|||
toggleDarkMode,
|
||||
isSidebarOpen,
|
||||
setIsSidebarOpen,
|
||||
openTaskModal,
|
||||
openProjectModal,
|
||||
openNoteModal,
|
||||
openAreaModal,
|
||||
openTagModal,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsDropdownOpen(!isDropdownOpen);
|
||||
};
|
||||
|
||||
// Handle click outside to close dropdown
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isDropdownOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isDropdownOpen]);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Check for Ctrl/Cmd key combinations
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
switch (event.key.toLowerCase()) {
|
||||
case 'i':
|
||||
event.preventDefault();
|
||||
handleDropdownSelect('Inbox');
|
||||
break;
|
||||
case 't':
|
||||
event.preventDefault();
|
||||
handleDropdownSelect('Task');
|
||||
break;
|
||||
case 'p':
|
||||
event.preventDefault();
|
||||
handleDropdownSelect('Project');
|
||||
break;
|
||||
case 'n':
|
||||
event.preventDefault();
|
||||
handleDropdownSelect('Note');
|
||||
break;
|
||||
case 'a':
|
||||
event.preventDefault();
|
||||
handleDropdownSelect('Area');
|
||||
break;
|
||||
case 'g':
|
||||
event.preventDefault();
|
||||
handleDropdownSelect('Tag');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDropdownSelect = (type: string) => {
|
||||
switch (type) {
|
||||
case 'Inbox':
|
||||
openTaskModal('simplified');
|
||||
break;
|
||||
case 'Task':
|
||||
openTaskModal('full');
|
||||
break;
|
||||
case 'Project':
|
||||
openProjectModal();
|
||||
break;
|
||||
case 'Note':
|
||||
openNoteModal(null);
|
||||
break;
|
||||
case 'Area':
|
||||
openAreaModal(null);
|
||||
break;
|
||||
case 'Tag':
|
||||
openTagModal(null);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
const dropdownItems = [
|
||||
{ label: 'Inbox', translationKey: 'dropdown.inbox', icon: <InboxIcon className="h-5 w-5 mr-2" />, shortcut: '⌃I' },
|
||||
{ label: 'Task', translationKey: 'dropdown.task', icon: <CheckIcon className="h-5 w-5 mr-2" />, shortcut: '⌃T' },
|
||||
{ label: 'Project', translationKey: 'dropdown.project', icon: <FolderIcon className="h-5 w-5 mr-2" />, shortcut: '⌃P' },
|
||||
{ label: 'Note', translationKey: 'dropdown.note', icon: <BookOpenIcon className="h-5 w-5 mr-2" />, shortcut: '⌃N' },
|
||||
{ label: 'Area', translationKey: 'dropdown.area', icon: <Squares2X2Icon className="h-5 w-5 mr-2" />, shortcut: '⌃A' },
|
||||
{ label: 'Tag', translationKey: 'dropdown.tag', icon: <TagIcon className="h-5 w-5 mr-2" />, shortcut: '⌃G' },
|
||||
];
|
||||
return (
|
||||
<div className="mt-auto p-3">
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-3">
|
||||
<div className={`flex items-center justify-center`}>
|
||||
{/* Dark Mode Toggle */}
|
||||
{isSidebarOpen && (
|
||||
{isSidebarOpen && (
|
||||
<div className="flex items-center justify-between" ref={dropdownRef}>
|
||||
{/* Plus Icon Button - Left */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={toggleDropdown}
|
||||
className="group flex items-center focus:outline-none text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-300 ease-out rounded-lg px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-700 hover:shadow-md"
|
||||
aria-label="Create New"
|
||||
>
|
||||
<PlusIcon className="h-6 w-6 flex-shrink-0 transition-transform duration-300 ease-out group-hover:rotate-90" />
|
||||
<span className="ml-2 text-sm font-medium whitespace-nowrap opacity-0 max-w-0 overflow-hidden group-hover:opacity-100 group-hover:max-w-[100px] transition-all duration-300 ease-out transform translate-x-[-10px] group-hover:translate-x-0">
|
||||
{t('dropdown.createNew', 'Create new')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute bottom-full left-0 mb-2 w-52 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-50">
|
||||
<div className="py-1">
|
||||
{dropdownItems.map(({ label, translationKey, icon, shortcut }) => (
|
||||
<button
|
||||
key={label}
|
||||
onClick={() => handleDropdownSelect(label)}
|
||||
className="w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-between transition-colors duration-150"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{icon}
|
||||
{t(translationKey, label)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded opacity-60">
|
||||
{shortcut}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dark Mode Toggle - Right */}
|
||||
<button
|
||||
onClick={toggleDarkMode}
|
||||
className="focus:outline-none text-gray-700 dark:text-gray-300"
|
||||
className="flex items-center justify-center focus:outline-none text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg px-2 py-1 transition-colors duration-200"
|
||||
aria-label="Toggle Dark Mode"
|
||||
>
|
||||
{isDarkMode ? (
|
||||
|
|
@ -34,8 +196,8 @@ const SidebarFooter: React.FC<SidebarFooterProps> = ({
|
|||
<MoonIcon className="h-6 w-6 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -77,9 +77,6 @@ const SidebarNav: React.FC<SidebarNavProps> = ({ handleNavClick, location }) =>
|
|||
)}
|
||||
</button>
|
||||
</li>
|
||||
{link.path === '/inbox' && (
|
||||
<li className="py-1" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "tududi",
|
||||
"version": "v0.64",
|
||||
"version": "v0.65",
|
||||
"description": "Self-hosted task management with hierarchical organization (Areas > Projects > Tasks), multi-language support, and Telegram integration. Built with React/TypeScript frontend and functional programming Express.js backend.",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue