Improve taskmodal layout
This commit is contained in:
parent
1e63cb2ff2
commit
f9eba248d5
7 changed files with 217 additions and 107 deletions
|
|
@ -15,6 +15,9 @@ smart recurring tasks, and seamless Telegram integration. Get focused, stay prod
|
|||
|
||||
This app allows users to manage their tasks, projects, areas, notes, and tags in an organized way. Users can create tasks, projects, areas (to group projects), notes, and tags. Each task can be associated with a project, and both tasks and notes can be tagged for better organization. Projects can belong to areas and can also have multiple notes and tags. This structure helps users categorize and track their work efficiently, whether they’re managing individual tasks, larger projects, or keeping detailed notes.
|
||||
|
||||
## 🧠 Philosophy
|
||||
|
||||
For the thinking behind Tududi, read [Designing a Life Management System That Doesn't Fight Back](https://example.com/designing-a-life-management-system-that-doesnt-fight-back)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
|
|
|
|||
|
|
@ -485,7 +485,7 @@ const Layout: React.FC<LayoutProps> = ({
|
|||
</div>
|
||||
|
||||
{/* Hide floating + button when any modal is open to prevent overlap with save buttons */}
|
||||
{!isTaskModalOpen && !isProjectModalOpen && !isNoteModalOpen && !isAreaModalOpen && !isTagModalOpen && (
|
||||
{globalModalCount === 0 && (
|
||||
<button
|
||||
onClick={() => openTaskModal('simplified')}
|
||||
className="fixed bottom-6 right-6 bg-blue-500 hover:bg-blue-600 text-white rounded-full p-4 shadow-lg focus:outline-none transform transition-transform duration-200 hover:scale-110 z-50"
|
||||
|
|
|
|||
|
|
@ -15,17 +15,14 @@ const TaskContentSection: React.FC<TaskContentSectionProps> = ({
|
|||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="px-4 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||
{t('forms.noteContent', 'Content')}
|
||||
</label>
|
||||
<div className="px-4 py-4 border-b border-gray-200 dark:border-gray-700 mb-4">
|
||||
<textarea
|
||||
id={`task_note_${taskId}`}
|
||||
name="note"
|
||||
rows={3}
|
||||
rows={10}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="block w-full border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 shadow-sm p-3 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
|
||||
className="block w-full border-0 focus:outline-none focus:ring-0 p-3 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 resize-none"
|
||||
placeholder={t('forms.noteContentPlaceholder', 'Add task description...')}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const TaskMetadataSection: React.FC<TaskMetadataSectionProps> = ({
|
|||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{t('forms.task.labels.priority', 'Priority')}
|
||||
|
|
|
|||
|
|
@ -197,15 +197,16 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
|
||||
{/* Mobile view (below md breakpoint) */}
|
||||
<div className="block md:hidden">
|
||||
{/* Task Name with Priority Icon and Project Name */}
|
||||
<div className="flex items-start font-light text-md text-gray-900 dark:text-gray-100">
|
||||
{/* Priority Icon */}
|
||||
<TaskPriorityIcon priority={task.priority} status={task.status} onToggleCompletion={onToggleCompletion} />
|
||||
<div className="flex items-start">
|
||||
{/* Priority Icon - Centered vertically with entire card */}
|
||||
<div className="flex items-center justify-center w-5 mt-4 flex-shrink-0">
|
||||
<TaskPriorityIcon priority={task.priority} status={task.status} onToggleCompletion={onToggleCompletion} />
|
||||
</div>
|
||||
|
||||
{/* Task Title and Project Name */}
|
||||
<div className="ml-2 flex flex-col flex-1">
|
||||
{/* All content - Task name and metadata */}
|
||||
<div className="ml-2 flex-1">
|
||||
{/* Task Title */}
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center font-light text-md text-gray-900 dark:text-gray-100">
|
||||
<span>{task.name}</span>
|
||||
{isOverdue && (
|
||||
<span
|
||||
|
|
@ -217,7 +218,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Project, tags, due date, and recurrence - each on separate lines */}
|
||||
{/* Project, tags, due date, and recurrence */}
|
||||
<div className="flex flex-col text-xs text-gray-500 dark:text-gray-400 mt-1 space-y-1">
|
||||
{project && !hideProjectName && (
|
||||
<div className="flex items-center">
|
||||
|
|
@ -248,7 +249,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
</div>
|
||||
|
||||
{/* Mobile badges row */}
|
||||
<div className="flex items-center flex-wrap justify-start space-x-2 mt-2 ml-8">
|
||||
<div className="flex items-center flex-wrap justify-start space-x-2 mt-2 ml-7">
|
||||
|
||||
{/* Play/In Progress Controls - Mobile */}
|
||||
{(task.status === 'not_started' || task.status === 'in_progress' || task.status === 0 || task.status === 1) && (
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { fetchTaskById } from '../../utils/tasksService';
|
|||
import { getTaskIntelligenceEnabled } from '../../utils/profileService';
|
||||
import { analyzeTaskName, TaskAnalysis } from '../../utils/taskIntelligenceService';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ClockIcon } from "@heroicons/react/24/outline";
|
||||
import { ClockIcon, TagIcon, FolderIcon, Cog6ToothIcon, ArrowPathIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
// Import form sections
|
||||
import TaskTitleSection from "./TaskForm/TaskTitleSection";
|
||||
|
|
@ -328,13 +328,13 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
<div className="min-h-full flex items-start justify-center px-4 py-4">
|
||||
<div
|
||||
ref={modalRef}
|
||||
className={`bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-800 sm:rounded-lg sm:shadow-2xl w-full sm:max-w-6xl transform transition-transform duration-300 ${
|
||||
className={`bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-800 sm:rounded-lg sm:shadow-2xl w-full sm:max-w-2xl transform transition-transform duration-300 ${
|
||||
isClosing ? "scale-95" : "scale-100"
|
||||
} my-4`}
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row min-h-[400px] max-h-[90vh]">
|
||||
<div className="flex flex-col lg:flex-row min-h-[400px] max-h-[80vh]">
|
||||
{/* Main Form Section */}
|
||||
<div className={`flex-1 flex flex-col transition-all duration-300 ${
|
||||
<div className={`flex-1 flex flex-col transition-all duration-300 bg-white dark:bg-gray-800 ${
|
||||
isTimelineExpanded ? 'lg:pr-2' : ''
|
||||
}`}>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
|
|
@ -356,103 +356,212 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
{/* Tags Section - Collapsible */}
|
||||
<CollapsibleSection
|
||||
title={t('forms.task.labels.tags', 'Tags')}
|
||||
isExpanded={expandedSections.tags}
|
||||
onToggle={() => toggleSection('tags')}
|
||||
>
|
||||
<TaskTagsSection
|
||||
tags={formData.tags?.map((tag) => tag.name) || []}
|
||||
onTagsChange={handleTagsChange}
|
||||
availableTags={localAvailableTags}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
{/* Expandable Sections - Only show when expanded */}
|
||||
{expandedSections.tags && (
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-4 mb-4 px-4">
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
{t('forms.task.labels.tags', 'Tags')}
|
||||
</h3>
|
||||
<TaskTagsSection
|
||||
tags={formData.tags?.map((tag) => tag.name) || []}
|
||||
onTagsChange={handleTagsChange}
|
||||
availableTags={localAvailableTags}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project Section - Collapsible */}
|
||||
<CollapsibleSection
|
||||
title={t('forms.task.labels.project', 'Project')}
|
||||
isExpanded={expandedSections.project}
|
||||
onToggle={() => toggleSection('project')}
|
||||
>
|
||||
<TaskProjectSection
|
||||
newProjectName={newProjectName}
|
||||
onProjectSearch={handleProjectSearch}
|
||||
dropdownOpen={dropdownOpen}
|
||||
filteredProjects={filteredProjects}
|
||||
onProjectSelection={handleProjectSelection}
|
||||
onCreateProject={handleCreateProject}
|
||||
isCreatingProject={isCreatingProject}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
{expandedSections.project && (
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-4 mb-4 px-4">
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
{t('forms.task.labels.project', 'Project')}
|
||||
</h3>
|
||||
<TaskProjectSection
|
||||
newProjectName={newProjectName}
|
||||
onProjectSearch={handleProjectSearch}
|
||||
dropdownOpen={dropdownOpen}
|
||||
filteredProjects={filteredProjects}
|
||||
onProjectSelection={handleProjectSelection}
|
||||
onCreateProject={handleCreateProject}
|
||||
isCreatingProject={isCreatingProject}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metadata/Options Section - Collapsible */}
|
||||
<CollapsibleSection
|
||||
title={t('forms.task.statusAndOptions', 'Status & Options')}
|
||||
isExpanded={expandedSections.metadata}
|
||||
onToggle={() => toggleSection('metadata')}
|
||||
>
|
||||
<TaskMetadataSection
|
||||
priority={getPriorityString(formData.priority)}
|
||||
dueDate={formData.due_date || ""}
|
||||
taskId={task.id}
|
||||
onStatusChange={(value: StatusType) => {
|
||||
// Universal rule: when setting status to in_progress, also add to today
|
||||
const updatedData = { ...formData, status: value };
|
||||
if (value === 'in_progress') {
|
||||
updatedData.today = true;
|
||||
{expandedSections.metadata && (
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-4 mb-4 px-4">
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
{t('forms.task.statusAndOptions', 'Status & Options')}
|
||||
</h3>
|
||||
<TaskMetadataSection
|
||||
priority={getPriorityString(formData.priority)}
|
||||
dueDate={formData.due_date || ""}
|
||||
taskId={task.id}
|
||||
onStatusChange={(value: StatusType) => {
|
||||
// Universal rule: when setting status to in_progress, also add to today
|
||||
const updatedData = { ...formData, status: value };
|
||||
if (value === 'in_progress') {
|
||||
updatedData.today = true;
|
||||
}
|
||||
setFormData(updatedData);
|
||||
}}
|
||||
onPriorityChange={(value: PriorityType) =>
|
||||
setFormData({ ...formData, priority: value })
|
||||
}
|
||||
setFormData(updatedData);
|
||||
}}
|
||||
onPriorityChange={(value: PriorityType) =>
|
||||
setFormData({ ...formData, priority: value })
|
||||
}
|
||||
onDueDateChange={handleChange}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
onDueDateChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recurrence Section - Collapsible */}
|
||||
<CollapsibleSection
|
||||
title={t('forms.task.recurrence', 'Recurrence')}
|
||||
isExpanded={expandedSections.recurrence}
|
||||
onToggle={() => toggleSection('recurrence')}
|
||||
>
|
||||
<TaskRecurrenceSection
|
||||
formData={formData}
|
||||
parentTask={parentTask}
|
||||
parentTaskLoading={parentTaskLoading}
|
||||
onRecurrenceChange={handleRecurrenceChange}
|
||||
onEditParent={parentTask ? handleEditParent : undefined}
|
||||
onParentRecurrenceChange={parentTask ? handleParentRecurrenceChange : undefined}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
{expandedSections.recurrence && (
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-4 mb-4 px-4">
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
{t('forms.task.recurrence', 'Recurrence')}
|
||||
</h3>
|
||||
<TaskRecurrenceSection
|
||||
formData={formData}
|
||||
parentTask={parentTask}
|
||||
parentTaskLoading={parentTaskLoading}
|
||||
onRecurrenceChange={handleRecurrenceChange}
|
||||
onEditParent={parentTask ? handleEditParent : undefined}
|
||||
onParentRecurrenceChange={parentTask ? handleParentRecurrenceChange : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons - Fixed at bottom */}
|
||||
<div className="flex-shrink-0 p-3 flex items-center justify-between">
|
||||
<TaskActions
|
||||
taskId={task.id}
|
||||
onDelete={handleDeleteClick}
|
||||
onSave={handleSubmit}
|
||||
onCancel={handleClose}
|
||||
/>
|
||||
{/* Timeline Panel - Show when expanded on mobile only */}
|
||||
{isTimelineExpanded && (
|
||||
<div className="lg:hidden border-t border-gray-200 dark:border-gray-700">
|
||||
<TimelinePanel
|
||||
taskId={task.id}
|
||||
isExpanded={isTimelineExpanded}
|
||||
onToggle={() => setIsTimelineExpanded(!isTimelineExpanded)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timeline Toggle Button */}
|
||||
<button
|
||||
onClick={() => setIsTimelineExpanded(!isTimelineExpanded)}
|
||||
className="p-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
|
||||
title={isTimelineExpanded ? t('timeline.hideActivityTimeline') : t('timeline.showActivityTimeline')}
|
||||
>
|
||||
<ClockIcon
|
||||
className={`h-5 w-5 transition-transform duration-200 ${isTimelineExpanded ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
{/* Section Icons - Above border, split layout */}
|
||||
<div className="flex-shrink-0 bg-white dark:bg-gray-800 px-3 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Left side: Section icons */}
|
||||
<div className="flex items-center space-x-1">
|
||||
{/* Tags Toggle */}
|
||||
<button
|
||||
onClick={() => toggleSection('tags')}
|
||||
className={`relative p-2 rounded-full transition-colors ${
|
||||
expandedSections.tags
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
title={t('forms.task.labels.tags', 'Tags')}
|
||||
>
|
||||
<TagIcon className="h-5 w-5" />
|
||||
{formData.tags && formData.tags.length > 0 && (
|
||||
<span className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full"></span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Project Toggle */}
|
||||
<button
|
||||
onClick={() => toggleSection('project')}
|
||||
className={`relative p-2 rounded-full transition-colors ${
|
||||
expandedSections.project
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
title={t('forms.task.labels.project', 'Project')}
|
||||
>
|
||||
<FolderIcon className="h-5 w-5" />
|
||||
{formData.project_id && (
|
||||
<span className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full"></span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Status & Options Toggle */}
|
||||
<button
|
||||
onClick={() => toggleSection('metadata')}
|
||||
className={`relative p-2 rounded-full transition-colors ${
|
||||
expandedSections.metadata
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
title={t('forms.task.statusAndOptions', 'Status & Options')}
|
||||
>
|
||||
<Cog6ToothIcon className="h-5 w-5" />
|
||||
{(formData.due_date || formData.priority !== 'medium' || (formData.status && formData.status !== 'not_started')) && (
|
||||
<span className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full"></span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Recurrence Toggle */}
|
||||
<button
|
||||
onClick={() => toggleSection('recurrence')}
|
||||
className={`relative p-2 rounded-full transition-colors ${
|
||||
expandedSections.recurrence
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
title={t('forms.task.recurrence', 'Recurrence')}
|
||||
>
|
||||
<ArrowPathIcon className="h-5 w-5" />
|
||||
{(formData.recurrence_type || formData.recurring_parent_id) && (
|
||||
<span className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full"></span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Right side: Timeline Toggle Button */}
|
||||
<button
|
||||
onClick={() => setIsTimelineExpanded(!isTimelineExpanded)}
|
||||
className={`p-2 rounded-full transition-colors ${
|
||||
isTimelineExpanded
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
title={isTimelineExpanded ? t('timeline.hideActivityTimeline') : t('timeline.showActivityTimeline')}
|
||||
>
|
||||
<ClockIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons - Below border with custom layout */}
|
||||
<div className="flex-shrink-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-3 py-2 flex items-center justify-between">
|
||||
{/* Left side: Delete and Cancel */}
|
||||
<div className="flex items-center space-x-3">
|
||||
{task.id && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteClick}
|
||||
className="p-2 border border-red-300 dark:border-red-600 text-red-600 dark:text-red-400 rounded-md hover:bg-red-50 dark:hover:bg-red-900/20 focus:outline-none transition duration-150 ease-in-out"
|
||||
title={t('common.delete', 'Delete')}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 focus:outline-none transition duration-150 ease-in-out text-sm"
|
||||
>
|
||||
{t('common.cancel', 'Cancel')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Right side: Save */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 focus:outline-none transition duration-150 ease-in-out text-sm"
|
||||
>
|
||||
{t('common.save', 'Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline Panel - Side Panel */}
|
||||
{/* Timeline Panel - Desktop Sidebar */}
|
||||
<TimelinePanel
|
||||
taskId={task.id}
|
||||
isExpanded={isTimelineExpanded}
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@
|
|||
"time": "Zeit",
|
||||
"allDay": "Ganztägig"
|
||||
},
|
||||
"weekdays": {
|
||||
"weekdaysShort": {
|
||||
"monday": "Mo",
|
||||
"tuesday": "Di",
|
||||
"wednesday": "Mi",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue