Fix today pagination (#596)

* fixup! Feat notifications (#594)

* Add pagination to today

* Add defer to search

* fixup! Add defer to search

* Add preuploaded banners

* fixup! Add preuploaded banners

* Fix project banner

* fixup! Fix project banner

* fixup! fixup! Fix project banner
This commit is contained in:
Chris 2025-11-26 23:00:50 +02:00 committed by GitHub
parent 18c7785b13
commit 08c23d2f96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 1058 additions and 295 deletions

View file

@ -17,12 +17,10 @@ import {
TagIcon,
Squares2X2Icon,
TrashIcon,
CameraIcon,
CalendarIcon,
ExclamationTriangleIcon,
PlayIcon,
} from '@heroicons/react/24/outline';
import { getApiPath } from '../../config/paths';
interface ProjectModalProps {
isOpen: boolean;
@ -51,18 +49,12 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
tags: [],
priority: null,
due_date_at: null,
image_url: '',
}
);
const [tags, setTags] = useState<string[]>(
project?.tags?.map((tag) => tag.name) || []
);
const [imageFile, setImageFile] = useState<File | null>(null);
const [imagePreview, setImagePreview] = useState<string>(
project?.image_url || ''
);
const [isUploading, setIsUploading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const { tagsStore } = useStore();
@ -71,7 +63,6 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
const { addNewTags } = tagsStore;
const modalRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const nameInputRef = useRef<HTMLInputElement>(null);
const [isClosing, setIsClosing] = useState(false);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
@ -83,7 +74,6 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
state: false,
tags: false,
area: false,
image: false,
priority: false,
dueDate: false,
});
@ -131,10 +121,8 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
...project,
tags: project.tags || [],
due_date_at: dueDateValue || null,
image_url: project.image_url || '',
});
setTags(project.tags?.map((tag) => tag.name) || []);
setImagePreview(project.image_url || '');
} else {
setFormData({
name: '',
@ -144,12 +132,9 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
tags: [],
priority: null,
due_date_at: null,
image_url: '',
});
setTags([]);
setImagePreview('');
}
setImageFile(null);
setError(null);
}, [project]);
@ -274,92 +259,6 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
}));
};
const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// Simple client-side guard (10MB max)
const maxSizeBytes = 10 * 1024 * 1024;
if (file.size > maxSizeBytes) {
setError(
t(
'errors.projectImageTooLarge',
'Image is too large. Please choose a file under 10MB.'
)
);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
return;
}
setImageFile(file);
// Create preview
const reader = new FileReader();
reader.onload = (ev) => {
setImagePreview(ev.target?.result as string);
};
reader.readAsDataURL(file);
};
const handleImageUpload = async (): Promise<string | null> => {
if (!imageFile) return null;
setIsUploading(true);
try {
const formData = new FormData();
formData.append('image', imageFile);
const response = await fetch(getApiPath('upload/project-image'), {
method: 'POST',
credentials: 'include',
body: formData,
});
if (!response.ok) {
let serverMessage = 'Failed to upload image';
try {
const errData = await response.json();
if (errData?.error) serverMessage = errData.error;
} catch {
// ignore parse errors
}
throw new Error(serverMessage);
}
const result = await response.json();
if (result?.imageUrl) {
return result.imageUrl;
}
throw new Error('Image URL missing from upload response');
} catch (error) {
console.error('Error uploading image:', error);
setError(
t(
'errors.projectImageUpload',
'Failed to upload image. Please try a smaller file or a different format.'
)
);
return null;
} finally {
setIsUploading(false);
}
};
const handleRemoveImage = () => {
setImageFile(null);
setImagePreview('');
setFormData((prev) => ({
...prev,
image_url: '',
}));
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleSubmit = async () => {
// Validate required fields
if (!formData.name.trim()) {
@ -380,22 +279,8 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
addNewTags(newTagNames);
}
let imageUrl = formData.image_url;
// Upload image if a new one was selected
if (imageFile) {
const uploadedImageUrl = await handleImageUpload();
if (uploadedImageUrl) {
imageUrl = uploadedImageUrl;
} else {
setIsSaving(false);
return;
}
}
const projectData = {
...formData,
image_url: imageUrl,
tags: tags.map((name) => ({ name })),
};
@ -446,9 +331,7 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
formData.state !== 'idea' ||
tags.length > 0 ||
formData.priority !== null ||
formData.due_date_at !== null ||
imageFile !== null ||
imagePreview !== ''
formData.due_date_at !== null
);
}
@ -459,8 +342,7 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
formData.area_id !== project.area_id ||
formData.state !== project.state ||
formData.priority !== project.priority ||
formData.due_date_at !== project.due_date_at ||
imageFile !== null;
formData.due_date_at !== project.due_date_at;
// Compare tags
const originalTags = project.tags?.map((tag) => tag.name) || [];
@ -707,81 +589,6 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
</div>
)}
{expandedSections.image && (
<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(
'project.projectImage',
'Project Image'
)}
</h3>
{imagePreview ? (
<div className="mb-3">
<div className="relative inline-block">
<img
src={
imagePreview
}
alt="Project preview"
className="w-32 h-20 object-cover rounded-md border border-gray-300 dark:border-gray-600"
/>
<button
type="button"
onClick={
handleRemoveImage
}
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs hover:bg-red-600"
>
×
</button>
</div>
</div>
) : null}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={
handleImageSelect
}
className="hidden"
/>
<button
type="button"
onClick={() =>
fileInputRef.current?.click()
}
className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
{t(
'project.browseImage',
'Browse Image'
)}
</button>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{t(
'project.uploadImageHint',
'Upload an image for your project (max 10MB)'
)}
</p>
</div>
)}
{expandedSections.priority && (
<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">
@ -901,28 +708,6 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
)}
</button>
{/* Project Image Toggle */}
<button
type="button"
onClick={() =>
toggleSection('image')
}
className={`relative p-2 rounded-full transition-colors ${
expandedSections.image
? '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(
'project.projectImage',
'Project Image'
)}
>
<CameraIcon className="h-5 w-5" />
{formData.image_url && (
<span className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full"></span>
)}
</button>
{/* Priority Toggle */}
<button
type="button"
@ -997,25 +782,18 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
<button
type="button"
onClick={handleSubmit}
disabled={isUploading}
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 ${
isUploading
? 'opacity-50 cursor-not-allowed'
: ''
}`}
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"
data-testid="project-save-button"
>
{isUploading
? 'Uploading...'
: project
? t(
'modals.updateProject',
'Update Project'
)
: t(
'modals.createProject',
'Create Project'
)}
{project
? t(
'modals.updateProject',
'Update Project'
)
: t(
'modals.createProject',
'Create Project'
)}
</button>
</div>
</div>