Move create new button to the bottom

This commit is contained in:
Chris Veleris 2025-07-07 14:23:40 +03:00
parent 093471bfbe
commit c565bc0fed
6 changed files with 177 additions and 149 deletions

View file

@ -1,6 +1,6 @@
{
"name": "backend",
"version": "v0.64",
"version": "v0.65",
"description": "",
"main": "index.js",
"scripts": {

View file

@ -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>
)}

View file

@ -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;

View file

@ -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>
);

View file

@ -77,9 +77,6 @@ const SidebarNav: React.FC<SidebarNavProps> = ({ handleNavClick, location }) =>
)}
</button>
</li>
{link.path === '/inbox' && (
<li className="py-1" />
)}
</React.Fragment>
))}
</ul>

View file

@ -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": {