Fix notes tag issue and add action button

This commit is contained in:
Chris Veleris 2024-11-08 14:55:46 +02:00
parent 7ad521994c
commit 62325a0c2d
19 changed files with 590 additions and 382 deletions

View file

@ -15,6 +15,7 @@ import { Project } from "./entities/Project";
import { Task } from "./entities/Task";
import { useDataContext } from "./contexts/DataContext";
import { User } from "./entities/User";
import { BookOpenIcon, ClipboardIcon, FolderIcon, PlusCircleIcon, Squares2X2Icon } from "@heroicons/react/24/solid";
interface LayoutProps {
currentUser: User;
@ -74,6 +75,35 @@ const Layout: React.FC<LayoutProps> = ({
return () => window.removeEventListener("resize", handleResize);
}, []);
const [isFABDropdownOpen, setFABDropdownOpen] = useState(false);
const handleFABDropdownSelect = (type: string) => {
switch (type) {
case 'Task':
openTaskModal();
break;
case 'Project':
openProjectModal();
break;
case 'Note':
openNoteModal(null);
break;
case 'Area':
openAreaModal(null);
break;
default:
break;
}
setFABDropdownOpen(false);
};
const fabDropdownItems = [
{ label: 'Task', icon: <ClipboardIcon className="h-5 w-5 mr-2" /> },
{ label: 'Project', icon: <FolderIcon className="h-5 w-5 mr-2" /> },
{ label: 'Note', icon: <BookOpenIcon className="h-5 w-5 mr-2" /> },
{ label: 'Area', icon: <Squares2X2Icon className="h-5 w-5 mr-2" /> },
];
const openNoteModal = (note: Note | null = null) => {
setSelectedNote(note);
setIsNoteModalOpen(true);
@ -279,7 +309,7 @@ const Layout: React.FC<LayoutProps> = ({
currentUser={currentUser}
isDarkMode={isDarkMode}
toggleDarkMode={toggleDarkMode}
openTaskModal={() => openTaskModal()}
openTaskModal={openTaskModal}
openProjectModal={openProjectModal}
openNoteModal={openNoteModal}
openAreaModal={openAreaModal}
@ -293,23 +323,63 @@ const Layout: React.FC<LayoutProps> = ({
className={`transition-all duration-300 ease-in-out ${mainContentMarginLeft}`}
>
<div className="flex flex-col bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 min-h-screen overflow-y-auto">
<div className="flex-grow p-6 pt-24">
<div className="flex-grow py-6 px-2 md:px-6 pt-24">
<div className="w-full max-w-5xl mx-auto">{children}</div>
</div>
</div>
</div>
{/* Floating Action Button */}
<button
onClick={() => setFABDropdownOpen(!isFABDropdownOpen)}
className="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"
aria-label="Open Create New Dropdown"
>
<PlusCircleIcon className="h-6 w-6" />
</button>
{isFABDropdownOpen && (
<div className="absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 z-20">
<ul className="py-1" role="menu" aria-orientation="vertical">
{fabDropdownItems.map(({ label, 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={() => handleFABDropdownSelect(label)}
role="menuitem"
>
{icon}
{label}
</li>
))}
</ul>
</div>
)}
{/* Modals */}
{isTaskModalOpen && (
<TaskModal
isOpen={isTaskModalOpen}
onClose={closeTaskModal}
task={newTask || { id: undefined, name: '', status: 'not_started', project_id: undefined, tags: [] }}
task={
newTask || {
id: undefined,
name: '',
status: 'not_started',
project_id: undefined,
tags: [],
}
}
onSave={handleSaveTask}
onDelete={() => {}}
projects={[]} // Provide project list as necessary
onCreateProject={async (name: string) => {
return { id: Math.random(), name, active: true, pin_to_sidebar: false }; // Ensure all required fields are covered
return {
id: Math.random(),
name,
active: true,
pin_to_sidebar: false,
}; // Ensure all required fields are covered
}}
/>
)}

View file

@ -1,6 +1,8 @@
import React, { useState, useEffect, useRef } from 'react';
import { Area } from '../../entities/Area';
import { useDataContext } from '../../contexts/DataContext';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { useToast } from '../Shared/ToastContext';
interface AreaModalProps {
isOpen: boolean;
@ -9,7 +11,7 @@ interface AreaModalProps {
area?: Area | null;
}
const AreaModal: React.FC<AreaModalProps> = ({ isOpen, onClose, area }) => {
const AreaModal: React.FC<AreaModalProps> = ({ isOpen, onClose, area, onSave }) => {
const { createArea, updateArea } = useDataContext();
const [formData, setFormData] = useState<Area>({
id: area?.id || 0,
@ -21,6 +23,8 @@ const AreaModal: React.FC<AreaModalProps> = ({ isOpen, onClose, area }) => {
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [isClosing, setIsClosing] = useState(false);
const { showSuccessToast, showErrorToast } = useToast();
useEffect(() => {
if (isOpen) {
setFormData({
@ -75,9 +79,7 @@ const AreaModal: React.FC<AreaModalProps> = ({ isOpen, onClose, area }) => {
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const handleSubmit = async () => {
if (!formData.name.trim()) {
setError('Area name is required.');
return;
@ -89,12 +91,16 @@ const AreaModal: React.FC<AreaModalProps> = ({ isOpen, onClose, area }) => {
try {
if (formData.id && formData.id !== 0) {
await updateArea(formData.id, formData);
showSuccessToast('Area updated successfully!');
} else {
await createArea(formData);
showSuccessToast('Area created successfully!');
}
onSave(formData);
handleClose();
} catch (err) {
setError((err as Error).message);
showErrorToast('Failed to save area.');
} finally {
setIsSubmitting(false);
}
@ -119,91 +125,75 @@ const AreaModal: React.FC<AreaModalProps> = ({ isOpen, onClose, area }) => {
>
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
className={`bg-white dark:bg-gray-800 w-full sm:max-w-md mx-auto overflow-hidden h-screen sm:h-auto flex flex-col 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-md overflow-hidden transform transition-transform duration-300 ${
isClosing ? 'scale-95' : 'scale-100'
} sm:rounded-lg sm:shadow-2xl`}
} h-screen sm:h-auto flex flex-col`}
style={{
maxHeight: 'calc(100vh - 4rem)',
}}
>
<form className="flex flex-col flex-1" onSubmit={handleSubmit}>
<fieldset className="flex-1 overflow-y-auto p-4 space-y-4">
<h3
id="modal-title"
className="text-lg font-medium text-gray-900 dark:text-white"
>
{formData.id && formData.id !== 0 ? 'Edit Area' : 'Create Area'}
</h3>
<form className="flex flex-col flex-1">
<fieldset className="flex flex-col flex-1">
<div className="p-4 space-y-3 flex-1 text-sm overflow-y-auto">
{/* Area Name */}
<div className="py-4">
<input
type="text"
id="areaName"
name="name"
value={formData.name}
onChange={handleChange}
required
className="block w-full text-xl font-semibold dark:bg-gray-800 text-black dark:text-white border-b-2 border-gray-200 dark:border-gray-900 focus:outline-none shadow-sm py-2"
placeholder="Enter area name"
/>
</div>
{/* Area Name */}
<div>
<label
htmlFor="areaName"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Area Name
</label>
<input
type="text"
id="areaName"
name="name"
value={formData.name}
onChange={handleChange}
required
className="mt-1 block w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100"
placeholder="Enter area name"
/>
{/* Area Description */}
<div className="pb-3">
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
Description
</label>
<textarea
id="areaDescription"
name="description"
value={formData.description}
onChange={handleChange}
rows={4}
className="block w-full rounded-md shadow-sm p-3 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 transition duration-150 ease-in-out"
placeholder="Enter area description"
/>
</div>
{/* Error Message */}
{error && <div className="text-red-500">{error}</div>}
</div>
{/* Area Description */}
<div>
<label
htmlFor="areaDescription"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
{/* Action Buttons */}
<div className="p-3 flex-shrink-0 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-2">
<button
type="button"
onClick={handleClose}
className="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-300 dark:hover:bg-gray-600 focus:outline-none transition duration-150 ease-in-out"
>
Description
</label>
<textarea
id="areaDescription"
name="description"
value={formData.description}
onChange={handleChange}
rows={3}
className="mt-1 block w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100"
placeholder="Enter area description"
/>
Cancel
</button>
<button
type="button"
onClick={handleSubmit}
disabled={isSubmitting}
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 ${
isSubmitting ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{isSubmitting
? 'Submitting...'
: formData.id && formData.id !== 0
? 'Update Area'
: 'Create Area'}
</button>
</div>
{/* Error Message */}
{error && <div className="text-red-500">{error}</div>}
</fieldset>
{/* Modal Actions */}
<div className="flex justify-end items-center p-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
<button
type="button"
onClick={handleClose}
className="px-4 mr-2 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 focus:outline-none"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
className={`px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 focus:outline-none ${
isSubmitting ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{isSubmitting
? 'Submitting...'
: formData.id && formData.id !== 0
? 'Update Area'
: 'Create Area'}
</button>
</div>
</form>
</div>
</div>

View file

@ -1,6 +1,11 @@
import React, { useState, useEffect, useRef } from 'react';
// app/frontend/components/Note/NoteModal.tsx
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Note } from '../../entities/Note';
import { useDataContext } from '../../contexts/DataContext';
import { useToast } from '../Shared/ToastContext';
import TagInput from '../Tag/TagInput';
import { Tag } from '../../entities/Tag';
interface NoteModalProps {
isOpen: boolean;
@ -17,11 +22,29 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })
content: note?.content || '',
tags: note?.tags || [],
});
const [tags, setTags] = useState<string[]>(note?.tags?.map((tag) => tag.name) || []);
const [availableTags, setAvailableTags] = useState<Tag[]>([]);
const [error, setError] = useState<string | null>(null);
const modalRef = useRef<HTMLDivElement>(null);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [isClosing, setIsClosing] = useState(false);
const { showSuccessToast, showErrorToast } = useToast();
// Fetch available tags when modal is opened
useEffect(() => {
if (isOpen) {
fetch('/api/tags')
.then((response) => response.json())
.then((data: Tag[]) => setAvailableTags(data))
.catch((error) => {
console.error('Failed to fetch tags', error);
showErrorToast('Failed to load available tags.');
});
}
}, [isOpen, showErrorToast]);
// Reset form data when modal opens or note changes
useEffect(() => {
if (isOpen) {
setFormData({
@ -30,10 +53,12 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })
content: note?.content || '',
tags: note?.tags || [],
});
setTags(note?.tags?.map((tag) => tag.name) || []);
setError(null);
}
}, [isOpen, note]);
// Handle click outside to close modal
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
@ -77,9 +102,22 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const handleTagsChange = useCallback((newTags: string[]) => {
setTags(newTags);
// Map newTags to Tag objects with 'id' if they exist in availableTags
const updatedTags: Tag[] = newTags.map((name) => {
const existingTag = availableTags.find((tag) => tag.name === name);
return existingTag ? { id: existingTag.id, name } : { name };
});
setFormData((prev) => ({
...prev,
tags: updatedTags,
}));
}, [availableTags]);
const handleSubmit = async () => {
if (!formData.title.trim()) {
setError('Note title is required.');
return;
@ -91,12 +129,16 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })
try {
if (formData.id && formData.id !== 0) {
await updateNote(formData.id, formData);
showSuccessToast('Note updated successfully!');
} else {
await createNote(formData);
showSuccessToast('Note created successfully!');
}
onSave(formData);
handleClose();
} catch (err) {
setError((err as Error).message);
showErrorToast('Failed to save note.');
} finally {
setIsSubmitting(false);
}
@ -121,84 +163,89 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })
>
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
className={`bg-white dark:bg-gray-800 w-full sm:max-w-2xl mx-auto overflow-hidden h-screen sm:h-auto flex flex-col 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 overflow-hidden transform transition-transform duration-300 ${
isClosing ? 'scale-95' : 'scale-100'
} sm:rounded-lg sm:shadow-2xl`}
} h-screen sm:h-auto flex flex-col`}
style={{
maxHeight: 'calc(100vh - 4rem)',
}}
>
<form className="flex flex-col flex-1" onSubmit={handleSubmit}>
<fieldset className="flex-1 overflow-y-auto p-4 space-y-4">
{/* Note Title */}
<div>
<label
htmlFor="noteTitle"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Title
</label>
<input
type="text"
id="noteTitle"
name="title"
value={formData.title}
onChange={handleChange}
required
className="mt-1 block w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100"
placeholder="Enter note title"
/>
<form className="flex flex-col flex-1">
<fieldset className="flex flex-col flex-1">
<div className="p-4 space-y-3 flex-1 text-sm overflow-y-auto">
{/* Note Title */}
<div className="py-4">
<input
type="text"
id="noteTitle"
name="title"
value={formData.title}
onChange={handleChange}
required
className="block w-full text-xl font-semibold dark:bg-gray-800 text-black dark:text-white border-b-2 border-gray-200 dark:border-gray-900 focus:outline-none shadow-sm py-2"
placeholder="Enter note title"
/>
</div>
{/* Tags */}
<div className="pb-3">
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
Tags
</label>
<div className="w-full">
<TagInput
onTagsChange={handleTagsChange}
initialTags={tags}
availableTags={availableTags}
/>
</div>
</div>
{/* Note Content */}
<div className="pb-3 flex-1">
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
Content
</label>
<textarea
id="noteContent"
name="content"
value={formData.content}
onChange={handleChange}
rows={20}
className="block w-full h-full rounded-md shadow-sm p-3 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 transition duration-150 ease-in-out"
placeholder="Enter note content"
></textarea>
</div>
{/* Error Message */}
{error && <div className="text-red-500">{error}</div>}
</div>
{/* Note Content */}
<div className="flex-1">
<label
htmlFor="noteContent"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
{/* Action Buttons */}
<div className="p-3 flex-shrink-0 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-2">
<button
type="button"
onClick={handleClose}
className="px-4 py-2 text-md bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-300 dark:hover:bg-gray-600 focus:outline-none transition duration-150 ease-in-out"
>
Content
</label>
<textarea
id="noteContent"
name="content"
value={formData.content}
onChange={handleChange}
rows={10}
className="mt-1 block w-full h-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100"
placeholder="Enter note content"
></textarea>
Cancel
</button>
<button
type="button"
onClick={handleSubmit}
disabled={isSubmitting}
className={`px-4 py-2 text-md 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 ${
isSubmitting ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{isSubmitting
? 'Submitting...'
: formData.id && formData.id !== 0
? 'Update Note'
: 'Create Note'}
</button>
</div>
{/* Error Message */}
{error && <div className="text-red-500">{error}</div>}
</fieldset>
{/* Modal Actions */}
<div className="flex justify-end items-center p-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
<button
type="button"
onClick={handleClose}
className="px-4 mr-2 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 focus:outline-none"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
className={`px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 focus:outline-none ${
isSubmitting ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{isSubmitting
? 'Submitting...'
: formData.id && formData.id !== 0
? 'Update Note'
: 'Create Note'}
</button>
</div>
</form>
</div>
</div>

View file

@ -1,12 +1,15 @@
import React, { useState, useEffect, useRef } from 'react';
import { Area } from '../../entities/Area';
import { Project } from '../../entities/Project';
import ConfirmDialog from '../Shared/ConfirmDialog';
import { useToast } from '../Shared/ToastContext';
import { XMarkIcon } from '@heroicons/react/24/outline';
interface ProjectModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (project: Project) => void;
onDelete?: () => void;
onDelete?: (projectId: number) => void;
project?: Project;
areas: Area[];
}
@ -25,12 +28,27 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
description: '',
area_id: null,
active: true,
pin_to_sidebar: false,
}
);
const modalRef = useRef<HTMLDivElement>(null);
const [isClosing, setIsClosing] = useState(false);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const { showSuccessToast, showErrorToast } = useToast();
useEffect(() => {
if (project) {
setFormData(project);
} else {
setFormData({
name: '',
description: '',
area_id: null,
active: true,
});
}
}, [project]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@ -65,34 +83,50 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
};
}, [isOpen]);
useEffect(() => {
if (project) {
setFormData(project);
}
}, [project]);
const handleChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
>
) => {
const { name, value, type } = e.target;
const target = e.target;
const { name, type } = target;
setFormData((prev) => ({
...prev,
[name]:
type === 'checkbox'
? (e.target as HTMLInputElement).checked
: value,
}));
if (type === 'checkbox') {
const checked = (target as HTMLInputElement).checked;
setFormData((prev) => ({
...prev,
[name]: checked,
}));
} else {
const value = target.value;
setFormData((prev) => ({
...prev,
[name]: value,
}));
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const handleSubmit = () => {
onSave(formData);
showSuccessToast(
project ? 'Project updated successfully!' : 'Project created successfully!'
);
handleClose();
};
const handleDeleteClick = () => {
setShowConfirmDialog(true);
};
const handleDeleteConfirm = () => {
if (project && project.id && onDelete) {
onDelete(project.id);
showSuccessToast('Project deleted successfully!');
setShowConfirmDialog(false);
handleClose();
}
};
const handleClose = () => {
setIsClosing(true);
setTimeout(() => {
@ -112,120 +146,125 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
>
<div
ref={modalRef}
className={`bg-white dark:bg-gray-800 w-full sm:max-w-2xl mx-auto overflow-hidden h-screen sm:h-auto flex flex-col transform transition-transform duration-300 ${
className={`bg-white dark:bg-gray-800 sm:rounded-lg sm:shadow-2xl w-full sm:max-w-2xl overflow-hidden transform transition-transform duration-300 ${
isClosing ? 'scale-95' : 'scale-100'
} sm:rounded-lg sm:shadow-2xl`}
} h-screen sm:h-auto flex flex-col`}
style={{
maxHeight: 'calc(100vh - 4rem)',
}}
>
<form className="flex flex-col flex-1" onSubmit={handleSubmit}>
<fieldset className="flex-1 overflow-y-auto p-4 space-y-4">
{/* Project details */}
<div>
<input
type="text"
id="projectName"
name="name"
value={formData.name}
onChange={handleChange}
required
className="mt-1 block w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100"
placeholder="Enter project name"
/>
<form className="flex flex-col flex-1">
<fieldset className="flex flex-col flex-1">
<div className="p-4 space-y-3 flex-1 text-sm overflow-y-auto">
{/* Project Name */}
<div className="py-4">
<input
type="text"
id="projectName"
name="name"
value={formData.name}
onChange={handleChange}
required
className="block w-full text-xl font-semibold dark:bg-gray-800 text-black dark:text-white border-b-2 border-gray-200 dark:border-gray-900 focus:outline-none shadow-sm py-2"
placeholder="Enter project name"
/>
</div>
{/* Description */}
<div className="pb-3">
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
Description
</label>
<textarea
id="projectDescription"
name="description"
rows={4}
value={formData.description || ''}
onChange={handleChange}
className="block w-full rounded-md shadow-sm p-3 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 transition duration-150 ease-in-out"
placeholder="Enter project description (optional)"
></textarea>
</div>
{/* Area */}
<div className="pb-3">
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
Area (optional)
</label>
<select
id="projectArea"
name="area_id"
value={formData.area_id || ''}
onChange={handleChange}
className="block w-full rounded-md shadow-sm px-3 py-2 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 transition duration-150 ease-in-out"
>
<option value="">No Area</option>
{areas.map((area) => (
<option key={area.id} value={area.id}>
{area.name}
</option>
))}
</select>
</div>
{/* Active Checkbox */}
<div className="flex items-center">
<input
type="checkbox"
id="active"
name="active"
checked={formData.active}
onChange={handleChange}
className="h-5 w-5 appearance-none border border-gray-300 rounded-md bg-white dark:bg-gray-700 checked:bg-blue-600 checked:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<label
htmlFor="active"
className="ml-2 block text-sm text-gray-700 dark:text-gray-300"
>
Active
</label>
</div>
</div>
<div>
<label
htmlFor="projectDescription"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Description
</label>
<textarea
id="projectDescription"
name="description"
rows={3}
value={formData.description || ''}
onChange={handleChange}
className="mt-1 block w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100"
placeholder="Enter project description (optional)"
></textarea>
</div>
<div>
<label
htmlFor="projectArea"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Area (optional)
</label>
<select
id="projectArea"
name="area_id"
value={formData.area_id || ''}
onChange={handleChange}
className="mt-1 block w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100"
>
<option value="">No Area</option>
{areas.map((area) => (
<option key={area.id} value={area.id}>
{area.name}
</option>
))}
</select>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="active"
name="active"
checked={formData.active}
onChange={handleChange}
className="h-5 w-5 appearance-none border border-gray-300 rounded-md bg-white dark:bg-gray-700 checked:bg-blue-600 checked:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<label
htmlFor="active"
className="ml-2 block text-sm text-gray-700 dark:text-gray-300"
>
Active
</label>
</div>
</fieldset>
{/* Action Buttons */}
<div className="flex justify-between items-center p-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
{project && (
<button
type="button"
onClick={onDelete}
className="px-3 py-1 bg-red-600 text-white rounded hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600"
>
Delete
</button>
)}
<div
className={`flex space-x-2 ${!project ? 'ml-auto' : ''}`}
>
{/* Action Buttons */}
<div className="p-3 flex-shrink-0 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-2">
{project && onDelete && (
<button
type="button"
onClick={handleDeleteClick}
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600 focus:outline-none transition duration-150 ease-in-out"
>
Delete
</button>
)}
<button
type="button"
onClick={handleClose}
className="px-3 py-1 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded hover:bg-gray-300 dark:hover:bg-gray-600"
className="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-300 dark:hover:bg-gray-600 focus:outline-none transition duration-150 ease-in-out"
>
Cancel
</button>
<button
type="submit"
className="px-3 py-1 bg-blue-600 dark:bg-blue-500 text-white rounded hover:bg-blue-700 dark:hover:bg-blue-600"
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"
>
{project ? 'Update Project' : 'Create Project'}
</button>
</div>
</div>
</fieldset>
</form>
</div>
</div>
{showConfirmDialog && (
<ConfirmDialog
title="Delete Project"
message="Are you sure you want to delete this project? This action cannot be undone."
onConfirm={handleDeleteConfirm}
onCancel={() => setShowConfirmDialog(false)}
/>
)}
</>
);
};

View file

@ -50,7 +50,7 @@ const PriorityDropdown: React.FC<PriorityDropdownProps> = ({ value, onChange })
<div ref={dropdownRef} className="relative inline-block text-left w-full">
<button
type="button"
className="inline-flex justify-between w-full px-3 py-2 bg-white dark:bg-gray-800 text-sm text-gray-900 dark:text-gray-100 border border-gray-300 dark:border-gray-900 rounded-md shadow-sm focus:outline-none"
className="inline-flex justify-between w-full px-3 py-2 bg-white dark:bg-gray-900 text-sm text-gray-900 dark:text-gray-100 border border-gray-300 dark:border-gray-900 rounded-md shadow-sm focus:outline-none"
onClick={handleToggle}
>
<span className="flex items-center space-x-2">

View file

@ -51,7 +51,7 @@ const StatusDropdown: React.FC<StatusDropdownProps> = ({ value, onChange }) => {
<div ref={dropdownRef} className="relative inline-block text-left w-full">
<button
type="button"
className="inline-flex justify-between w-full px-3 py-2 bg-white dark:bg-gray-800 text-sm text-gray-900 dark:text-gray-100 border border-gray-300 dark:border-gray-900 rounded-md shadow-sm focus:outline-none"
className="inline-flex justify-between w-full px-3 py-2 bg-white dark:bg-gray-900 text-sm text-gray-900 dark:text-gray-100 border border-gray-300 dark:border-gray-900 rounded-md shadow-sm focus:outline-none"
onClick={handleToggle}
>
<span className="flex items-center space-x-2">

View file

@ -68,7 +68,7 @@ const Sidebar: React.FC<SidebarProps> = ({
>
{isSidebarOpen && (
<div className="flex flex-col h-full overflow-y-auto">
<div className="px-3 pb-3 pt-6">
<div className="px-3 pb-3 pt-8">
{/* Sidebar Contents */}
<CreateNewDropdownButton
openTaskModal={openTaskModal}

View file

@ -57,7 +57,7 @@ const CreateNewDropdownButton: React.FC<CreateNewDropdownButtonProps> = ({
];
return (
<div className="mb-8">
<div className="mb-8 px-4">
<div className="relative">
<button
type="button"

View file

@ -1,10 +1,13 @@
// app/frontend/components/Tag/TagInput.tsx
import React, { useState } from 'react';
import TaskTags from '../Task/TaskTags';
import { Tag } from '../../entities/Tag';
interface TagInputProps {
initialTags: string[];
onTagsChange: (tags: string[]) => void;
availableTags: string[];
availableTags: Tag[];
}
const TagInput: React.FC<TagInputProps> = ({ initialTags, onTagsChange, availableTags }) => {
@ -28,9 +31,12 @@ const TagInput: React.FC<TagInputProps> = ({ initialTags, onTagsChange, availabl
}
};
const removeTag = (tagToRemoveId: number | string | undefined) => {
if (tagToRemoveId === undefined) return; // Handle undefined case
const updatedTags = tags.filter((_, index) => index !== Number(tagToRemoveId));
const removeTag = (tagId: string | number | undefined) => {
if (typeof tagId !== 'number') {
console.warn('Invalid tagId:', tagId);
return;
}
const updatedTags = tags.filter((_, index) => index !== tagId);
setTags(updatedTags);
onTagsChange(updatedTags);
};
@ -50,11 +56,11 @@ const TagInput: React.FC<TagInputProps> = ({ initialTags, onTagsChange, availabl
onKeyDown={handleKeyPress}
list="available-tags"
placeholder="Type to select an existing tag or add a new one"
className="w-full px-2 border border-gray-300 dark:border-gray-900 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 py-2 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
className="w-full px-2 border border-gray-300 dark:border-gray-900 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 py-2 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
/>
<datalist id="available-tags">
{availableTags.map((tag, index) => (
<option key={index} value={tag} />
<option key={index} value={tag.name} />
))}
</datalist>
</div>

View file

@ -1,5 +1,7 @@
import React, { useState, useEffect, useRef } from 'react';
import { Tag } from '../../entities/Tag';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { useToast } from '../Shared/ToastContext';
interface TagModalProps {
isOpen: boolean;
@ -22,6 +24,8 @@ const TagModal: React.FC<TagModalProps> = ({
const modalRef = useRef<HTMLDivElement>(null);
const [isClosing, setIsClosing] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const { showSuccessToast, showErrorToast } = useToast();
useEffect(() => {
if (tag) {
@ -67,17 +71,39 @@ const TagModal: React.FC<TagModalProps> = ({
}, [isOpen]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
[name]: value,
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave(formData);
handleClose();
const handleSubmit = async () => {
if (!formData.name.trim()) {
showErrorToast('Tag name is required.');
return;
}
setIsSubmitting(true);
try {
// Assuming you have createTag and updateTag functions
if (tag) {
// Update existing tag
// await updateTag(formData.id, formData);
showSuccessToast('Tag updated successfully!');
} else {
// Create new tag
// await createTag(formData);
showSuccessToast('Tag created successfully!');
}
onSave(formData);
handleClose();
} catch (err) {
showErrorToast('Failed to save tag.');
} finally {
setIsSubmitting(false);
}
};
const handleClose = () => {
@ -99,52 +125,52 @@ const TagModal: React.FC<TagModalProps> = ({
>
<div
ref={modalRef}
className={`bg-white dark:bg-gray-800 w-full sm:max-w-md mx-auto overflow-hidden h-screen sm:h-auto flex flex-col 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-md overflow-hidden transform transition-transform duration-300 ${
isClosing ? 'scale-95' : 'scale-100'
} sm:rounded-lg sm:shadow-2xl`}
} h-screen sm:h-auto flex flex-col`}
style={{
maxHeight: 'calc(100vh - 4rem)',
}}
>
<form className="flex flex-col flex-1" onSubmit={handleSubmit}>
<fieldset className="flex-1 overflow-y-auto p-4 space-y-4">
{/* Tag Name */}
<div>
<label
htmlFor="tagName"
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
<form className="flex flex-col flex-1">
<fieldset className="flex flex-col flex-1">
<div className="p-4 space-y-3 flex-1 text-sm overflow-y-auto">
{/* Tag Name */}
<div className="py-4">
<input
type="text"
id="tagName"
name="name"
value={formData.name}
onChange={handleChange}
required
className="block w-full text-xl font-semibold dark:bg-gray-800 text-black dark:text-white border-b-2 border-gray-200 dark:border-gray-900 focus:outline-none shadow-sm py-2"
placeholder="Enter tag name"
/>
</div>
</div>
{/* Action Buttons */}
<div className="p-3 flex-shrink-0 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-2">
<button
type="button"
onClick={handleClose}
className="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-300 dark:hover:bg-gray-600 focus:outline-none transition duration-150 ease-in-out"
>
Tag Name
</label>
<input
type="text"
id="tagName"
name="name"
value={formData.name}
onChange={handleChange}
required
className="mt-1 block w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100"
placeholder="Enter tag name"
/>
Cancel
</button>
<button
type="button"
onClick={handleSubmit}
disabled={isSubmitting}
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 ${
isSubmitting ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{isSubmitting ? 'Submitting...' : tag ? 'Update Tag' : 'Create Tag'}
</button>
</div>
</fieldset>
{/* Modal Actions */}
<div className="flex justify-end items-center p-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
<button
type="button"
onClick={handleClose}
className="px-3 py-1 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200 rounded hover:bg-gray-300 dark:hover:bg-gray-600"
>
Cancel
</button>
<button
type="submit"
className="ml-2 px-3 py-1 bg-blue-600 dark:bg-blue-500 text-white rounded hover:bg-blue-700 dark:hover:bg-blue-600"
>
{tag ? 'Update Tag' : 'Create Tag'}
</button>
</div>
</form>
</div>
</div>

View file

@ -10,10 +10,10 @@ const TaskDueDate: React.FC<TaskDueDateProps> = ({ dueDate, className }) => {
const today = new Date().toISOString().split('T')[0];
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().split('T')[0];
if (dueDate === today) return 'bg-blue-700 text-white';
if (dueDate === tomorrow) return 'bg-blue-700 text-white';
if (dueDate < today) return 'bg-red-700 text-white';
return 'bg-gray-300 text-gray-700';
if (dueDate === today) return 'border-blue-700 dark:text-white';
if (dueDate === tomorrow) return 'border-blue-700 dark:text-white';
if (dueDate < today) return 'border-red-700 dark:text-white';
return 'border-gray-300 dark:text-white';
};
const formatDueDate = () => {
@ -33,7 +33,7 @@ const TaskDueDate: React.FC<TaskDueDateProps> = ({ dueDate, className }) => {
};
return (
<div className={`flex items-center text-xs py-1 px-2 rounded-md ${getDueDateClass()} ${className}`}>
<div className={`flex items-center text-xs py-1 px-2 rounded-md border ${getDueDateClass()} ${className}`}>
{formatDueDate()}
</div>
);

View file

@ -1,3 +1,5 @@
// app/frontend/components/Task/TaskHeader.tsx
import React from "react";
import TaskPriorityIcon from "./TaskPriorityIcon";
import TaskTags from "./TaskTags";
@ -12,10 +14,14 @@ interface TaskHeaderProps {
onTaskClick: (e: React.MouseEvent) => void;
}
const TaskHeader: React.FC<TaskHeaderProps> = ({ task, project, onTaskClick }) => {
const TaskHeader: React.FC<TaskHeaderProps> = ({
task,
project,
onTaskClick,
}) => {
const capitalizeFirstLetter = (string: string | undefined) => {
if (!string) {
return '';
return "";
}
return string.charAt(0).toUpperCase() + string.slice(1);
};
@ -27,7 +33,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({ task, project, onTaskClick }) =
<div className="flex items-center space-x-4 mb-2 md:mb-0">
<TaskPriorityIcon priority={task.priority} status={task.status} />
<div className="flex flex-col">
<span className="font-medium text-sm text-gray-900 dark:text-gray-100">
<span className="font-medium text-md text-gray-900 dark:text-gray-100">
{task.name}
</span>
{project && (
@ -46,34 +52,23 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({ task, project, onTaskClick }) =
</div>
{/* Mobile view (below md breakpoint) */}
<div className="block md:hidden"> {/* Add bottom margin */}
<div className="font-medium text-lg text-gray-900 dark:text-gray-100 mb-4">
{/* Increase text size from text-sm to text-base */}
{task.name}
</div>
<div className="flex items-center mb-2">
<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} />
<span className="ml-2 text-sm">{capitalizeFirstLetter(task.priority)}</span> {/* Increase text size */}
</div>
<div className="flex items-center mb-2">
<TaskStatusBadge status={task.status} />
<span className="ml-2 text-sm"></span> {/* Increase text size */}
</div>
{/* Task Title and Project Name */}
<div className="ml-2 flex flex-col">
{/* Task Title */}
<span>{task.name}</span>
{task.due_date && (
<div className="flex items-center mb-2">
<i className="bi bi-clock mr-2"></i>
<TaskDueDate dueDate={task.due_date} />
</div>
)}
{/* Tags without onTagRemove prop */}
<div className="flex items-center">
<i className="bi bi-tag mr-2"></i>
<div className="flex-1 flex-wrap overflow-hidden">
<TaskTags tags={task.tags || []} />
{/* Project Name */}
{project && (
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{project.name}
</div>
)}
</div>
</div>
</div>

View file

@ -29,7 +29,7 @@ const TaskModal: React.FC<TaskModalProps> = ({
onCreateProject,
}) => {
const [formData, setFormData] = useState<Task>(task);
const [availableTags, setAvailableTags] = useState<string[]>([]);
const [availableTags, setAvailableTags] = useState<Tag[]>([]);
const [tags, setTags] = useState<string[]>(
task.tags?.map((tag) => tag.name) || []
);
@ -215,7 +215,7 @@ const TaskModal: React.FC<TaskModalProps> = ({
value={formData.name}
onChange={handleChange}
required
className="block w-full text-xl font-semibold border-none focus:outline-none shadow-sm py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-200"
className="block w-full text-xl font-semibold dark:bg-gray-800 text-black dark:text-white border-b-2 border-gray-200 dark:border-gray-900 focus:outline-none shadow-sm py-2"
placeholder="Add Task Name"
/>
</div>
@ -244,7 +244,7 @@ const TaskModal: React.FC<TaskModalProps> = ({
placeholder="Search or create a project..."
value={newProjectName}
onChange={handleProjectSearch}
className="block w-full border border-gray-300 dark:border-gray-900 rounded-md focus:outline-none shadow-sm px-2 py-2 text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
className="block w-full border border-gray-300 dark:border-gray-900 rounded-md focus:outline-none shadow-sm px-2 py-2 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
/>
{dropdownOpen && newProjectName && (
<div className="absolute mt-1 bg-white dark:bg-gray-900 shadow-md rounded-md w-full z-10">
@ -314,7 +314,7 @@ const TaskModal: React.FC<TaskModalProps> = ({
name="due_date"
value={formData.due_date || ""}
onChange={handleChange}
className="block w-full focus:outline-none shadow-sm px-2 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-900 rounded-md text-gray-900 dark:text-gray-100"
className="block w-full focus:outline-none shadow-sm px-2 py-2 text-sm bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-900 rounded-md text-gray-900 dark:text-gray-100"
/>
</div>
</div>
@ -330,7 +330,7 @@ const TaskModal: React.FC<TaskModalProps> = ({
rows={3}
value={formData.note || ""}
onChange={handleChange}
className="block w-full border border-gray-300 dark:border-gray-900 rounded-md focus:outline-none shadow-sm p-3 text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
className="block w-full border border-gray-300 dark:border-gray-900 rounded-md focus:outline-none shadow-sm p-3 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
placeholder="Add any additional notes here"
></textarea>
</div>

View file

@ -34,7 +34,7 @@ const TaskStatusBadge: React.FC<TaskStatusBadgeProps> = ({ status, className })
return (
<div className={`flex items-center md:px-2 ${className}`}>
{statusIcon}
<span className="ml-2 text-xs font-medium inline md:hidden">{statusLabel}</span>
{/* <span className="ml-2 text-xs font-medium inline md:hidden">{statusLabel}</span> */}
</div>
);
};

View file

@ -1,10 +1,12 @@
import { Tag } from "./Tag";
export interface Note {
id?: number;
title: string;
content: string;
created_at?: string;
updated_at?: string;
tags?: { id: number; name: string }[];
tags?: Tag[];
project?: {
id: number;
name: string;

View file

@ -5,7 +5,7 @@ export interface Project {
name: string;
description?: string;
active: boolean;
pin_to_sidebar: boolean;
pin_to_sidebar?: boolean;
area?: Area;
area_id?: number | null;
}

View file

@ -1,4 +1,4 @@
export interface Tag {
id?: number | undefined;
id?: number;
name: string;
}

View file

@ -1,15 +1,15 @@
class Sinatra::Application
def update_note_tags(note, tags_json)
return if tags_json.blank?
def update_note_tags(note, tags_array)
return if tags_array.blank?
begin
tag_names = JSON.parse(tags_json).map { |tag| tag['value'] }.uniq
tag_names = tags_array.uniq
tags = tag_names.map do |name|
current_user.tags.find_or_create_by(name: name)
end
note.tags = tags
rescue JSON::ParserError
puts "Failed to parse JSON for tags: #{tags_json}"
rescue StandardError => e
puts "Failed to update tags: #{e.message}"
end
end
@ -91,7 +91,7 @@ class Sinatra::Application
end
if note.update(note_attributes)
update_note_tags(note, request_data['tags'])
update_note_tags(note, request_data['tags']) # Pass the array directly
note.to_json(include: :tags)
else
status 400

File diff suppressed because one or more lines are too long