373 lines
13 KiB
TypeScript
373 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
import Link from 'next/link';
|
|
import {
|
|
Drawing,
|
|
updateDrawing as apiUpdateDrawing,
|
|
deleteDrawing as apiDeleteDrawing,
|
|
getUsers as apiGetUsers,
|
|
shareDrawingWithUser as apiShareDrawing,
|
|
} from '@/lib/api';
|
|
import { IUser } from '@/models/User';
|
|
import styles from './DrawingCard.module.css';
|
|
import { useI18n } from '@/lib/i18n/useI18n';
|
|
import Icon from '@/components/Icon/Icon';
|
|
import Modal from '@/components/Modal/Modal';
|
|
|
|
interface DrawingCardProps {
|
|
drawing: Drawing;
|
|
owned: boolean;
|
|
onDrawingRenamed: (updatedDrawing: Drawing) => void;
|
|
onDrawingDeleted: (drawingId: string) => void;
|
|
onDrawingShared?: (updatedDrawing: Drawing) => void;
|
|
}
|
|
|
|
export default function DrawingCard({
|
|
drawing,
|
|
owned,
|
|
onDrawingRenamed,
|
|
onDrawingDeleted,
|
|
onDrawingShared,
|
|
}: DrawingCardProps) {
|
|
const { t, isLoading: i18nCardIsLoading } = useI18n();
|
|
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [editingTitle, setEditingTitle] = useState(drawing.title);
|
|
const editTitleInputRef = useRef<HTMLInputElement>(null);
|
|
const [renameError, setRenameError] = useState<string | null>(null);
|
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
|
|
// States for the share modal
|
|
const [showShareModal, setShowShareModal] = useState(false);
|
|
const [usersForSharing, setUsersForSharing] = useState<IUser[]>([]);
|
|
const [shareSearchTerm, setShareSearchTerm] = useState('');
|
|
const [selectedUserIdToShare, setSelectedUserIdToShare] = useState<string | null>(null);
|
|
const [shareError, setShareError] = useState<string | null>(null);
|
|
const [isLoadingUsers, setIsLoadingUsers] = useState(false);
|
|
const [isSharing, setIsSharing] = useState(false);
|
|
|
|
const handleCancelEdit = useCallback(() => {
|
|
setIsEditing(false);
|
|
setEditingTitle(drawing.title);
|
|
setRenameError(null);
|
|
}, [drawing.title]);
|
|
|
|
useEffect(() => {
|
|
if (isEditing && editTitleInputRef.current) {
|
|
editTitleInputRef.current.focus();
|
|
editTitleInputRef.current.select();
|
|
}
|
|
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (isEditing && editTitleInputRef.current && !editTitleInputRef.current.contains(event.target as Node)) {
|
|
handleCancelEdit();
|
|
}
|
|
};
|
|
|
|
if (isEditing) {
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
} else {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
};
|
|
}, [isEditing, handleCancelEdit]);
|
|
|
|
const lastUpdatedText = i18nCardIsLoading
|
|
? "Last updated:"
|
|
: t('drawingCard.lastUpdatedPrefix') || "Last updated:";
|
|
|
|
const editTitleAriaLabel = i18nCardIsLoading
|
|
? "Edit title"
|
|
: t('drawingCard.editTitleAria') || "Edit title";
|
|
|
|
const deleteButtonAriaLabel = i18nCardIsLoading
|
|
? "Delete drawing"
|
|
: t('drawingCard.deleteAria') || "Delete drawing";
|
|
|
|
const shareButtonAriaLabel = i18nCardIsLoading
|
|
? "Share drawing"
|
|
: t('drawingCard.shareAria') || "Share drawing";
|
|
|
|
const handleStartEdit = () => {
|
|
setEditingTitle(drawing.title);
|
|
setIsEditing(true);
|
|
setRenameError(null);
|
|
};
|
|
|
|
const handleConfirmEdit = async () => {
|
|
if (!editingTitle.trim()) {
|
|
setRenameError(t('drawingCard.errors.titleRequired') || "Title is required.");
|
|
return;
|
|
}
|
|
if (editingTitle.trim() === drawing.title) {
|
|
setIsEditing(false);
|
|
setRenameError(null);
|
|
return;
|
|
}
|
|
try {
|
|
setRenameError(null);
|
|
const response = await apiUpdateDrawing(drawing._id, { title: editingTitle.trim() });
|
|
if (response.data) {
|
|
onDrawingRenamed(response.data);
|
|
setIsEditing(false);
|
|
} else {
|
|
setRenameError(response.error || t('drawingCard.errors.renameFailed') || "Failed to rename drawing.");
|
|
}
|
|
} catch (err) {
|
|
setRenameError(t('drawingCard.errors.renameFailed') || "Failed to rename drawing.");
|
|
console.error("Rename error:", err);
|
|
}
|
|
};
|
|
|
|
const handleEditTitleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
if (event.key === 'Enter') {
|
|
handleConfirmEdit();
|
|
}
|
|
if (event.key === 'Escape') {
|
|
handleCancelEdit();
|
|
}
|
|
};
|
|
|
|
const handleOpenDeleteModal = () => {
|
|
setShowDeleteModal(true);
|
|
setDeleteError(null);
|
|
};
|
|
|
|
const handleCloseDeleteModal = () => {
|
|
setShowDeleteModal(false);
|
|
};
|
|
|
|
const handleConfirmDelete = async () => {
|
|
try {
|
|
setDeleteError(null);
|
|
const response = await apiDeleteDrawing(drawing._id);
|
|
if (response.data) {
|
|
onDrawingDeleted(drawing._id);
|
|
setShowDeleteModal(false);
|
|
} else {
|
|
setDeleteError(response.error || t('drawingCard.errors.deleteFailed') || "Failed to delete drawing.");
|
|
}
|
|
} catch (err) {
|
|
setDeleteError(t('drawingCard.errors.deleteFailed') || "Failed to delete drawing.");
|
|
console.error("Delete error:", err);
|
|
}
|
|
};
|
|
|
|
// Functions for the share modal
|
|
const handleOpenShareModal = async () => {
|
|
setShowShareModal(true);
|
|
setShareError(null);
|
|
setSelectedUserIdToShare(null);
|
|
setShareSearchTerm('');
|
|
if (usersForSharing.length === 0) { // Load users only if they haven't been loaded before
|
|
setIsLoadingUsers(true);
|
|
const response = await apiGetUsers();
|
|
setIsLoadingUsers(false);
|
|
if (response.data) {
|
|
// Filter out the owner and already shared users
|
|
const filteredUsers = response.data.filter(
|
|
(user) => user._id !== drawing.owner_id && !drawing.shared_with.includes(user._id as string)
|
|
);
|
|
setUsersForSharing(filteredUsers);
|
|
} else {
|
|
setShareError(response.error || t('drawingCard.errors.loadUsersFailed') || "Failed to load users.");
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleCloseShareModal = () => {
|
|
setShowShareModal(false);
|
|
setSelectedUserIdToShare(null);
|
|
setShareSearchTerm('');
|
|
setShareError(null);
|
|
};
|
|
|
|
const handleConfirmShare = async () => {
|
|
if (!selectedUserIdToShare) {
|
|
setShareError(t('drawingCard.errors.userNotSelected') || "Please select a user to share with.");
|
|
return;
|
|
}
|
|
setIsSharing(true);
|
|
setShareError(null);
|
|
const response = await apiShareDrawing(drawing._id, selectedUserIdToShare);
|
|
setIsSharing(false);
|
|
if (response.data) {
|
|
setShowShareModal(false);
|
|
if (onDrawingShared) {
|
|
onDrawingShared(response.data);
|
|
}
|
|
setUsersForSharing(prevUsers => prevUsers.filter(u => u._id !== selectedUserIdToShare));
|
|
setSelectedUserIdToShare(null);
|
|
} else {
|
|
setShareError(response.error || t('drawingCard.errors.shareFailed') || "Failed to share drawing.");
|
|
}
|
|
};
|
|
|
|
const filteredUsersForSharing = usersForSharing.filter((user) =>
|
|
user.username.toLowerCase().includes(shareSearchTerm.toLowerCase()) ||
|
|
user.email.toLowerCase().includes(shareSearchTerm.toLowerCase())
|
|
);
|
|
|
|
const modalTitle = i18nCardIsLoading
|
|
? "Confirm Deletion"
|
|
: t('drawingCard.confirmDelete.title') || "Confirm Deletion";
|
|
|
|
let modalMessage = `Are you sure you want to delete "${drawing.title}"? This action cannot be undone.`;
|
|
if (!i18nCardIsLoading) {
|
|
const messageTemplate = t('drawingCard.confirmDelete.message');
|
|
if (messageTemplate) {
|
|
modalMessage = messageTemplate.replace('{{title}}', drawing.title);
|
|
} else {
|
|
modalMessage = `Are you sure you want to delete "${drawing.title}"? This action cannot be undone.`;
|
|
}
|
|
}
|
|
|
|
const modalConfirmButtonText = i18nCardIsLoading
|
|
? "Delete"
|
|
: t('drawingCard.confirmDelete.confirmButton') || "Delete";
|
|
|
|
const modalCancelButtonText = i18nCardIsLoading
|
|
? "Cancel"
|
|
: t('drawingCard.confirmDelete.cancelButton') || "Cancel";
|
|
|
|
// Texts for the share modal
|
|
const shareModalTitle = i18nCardIsLoading
|
|
? "Share Drawing"
|
|
: t('drawingCard.shareModal.title') || "Share Drawing";
|
|
const shareModalSearchPlaceholder = i18nCardIsLoading
|
|
? "Search user by name or email"
|
|
: t('drawingCard.shareModal.searchPlaceholder') || "Search user by name or email";
|
|
const shareModalConfirmButtonText = i18nCardIsLoading
|
|
? "Share"
|
|
: t('drawingCard.shareModal.confirmButton') || "Share";
|
|
const shareModalCancelButtonText = i18nCardIsLoading
|
|
? "Cancel"
|
|
: t('drawingCard.shareModal.cancelButton') || "Cancel";
|
|
const shareModalNoUsersFoundText = i18nCardIsLoading
|
|
? "No users available to share with or match your search."
|
|
: t('drawingCard.shareModal.noUsersFound') || "No users available to share with or match your search.";
|
|
|
|
return (
|
|
<>
|
|
<div className={styles.cardContainer}>
|
|
{renameError && <p className={styles.renameErrorText}>{renameError}</p>}
|
|
{!isEditing ? (
|
|
<div className={styles.titleContainer}>
|
|
<Link href={`/editor/${drawing._id}`} className={styles.titleLink}>
|
|
<h3 className={styles.cardTitle}>{drawing.title}</h3>
|
|
</Link>
|
|
<div className={styles.actionButtonsContainer}>
|
|
<button onClick={handleStartEdit} className={styles.iconButton} aria-label={editTitleAriaLabel}>
|
|
<Icon name="edit-2" size={18} />
|
|
</button>
|
|
{owned && (
|
|
<>
|
|
<button onClick={handleOpenShareModal} className={styles.iconButton} aria-label={shareButtonAriaLabel}>
|
|
<Icon name="share-2" size={18} />
|
|
</button>
|
|
<button onClick={handleOpenDeleteModal} className={`${styles.iconButton} ${styles.deleteButton}`} aria-label={deleteButtonAriaLabel}>
|
|
<Icon name="trash-2" size={18} />
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className={styles.inlineEditForm}>
|
|
<input
|
|
ref={editTitleInputRef}
|
|
type="text"
|
|
value={editingTitle}
|
|
onChange={(e) => setEditingTitle(e.target.value)}
|
|
onKeyDown={handleEditTitleKeyDown}
|
|
className={styles.inlineInput}
|
|
/>
|
|
</div>
|
|
)}
|
|
<Link href={`/editor/${drawing._id}`} className={styles.cardLinkUnderTitle}>
|
|
<div className={styles.cardContent}>
|
|
<p className={styles.cardDate}>
|
|
{`${lastUpdatedText} ${new Date(drawing.updatedAt).toLocaleDateString()}`}
|
|
</p>
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
|
|
<Modal
|
|
isOpen={showDeleteModal}
|
|
onClose={handleCloseDeleteModal}
|
|
title={modalTitle}
|
|
>
|
|
<p>{modalMessage}</p>
|
|
{deleteError && <p className={styles.modalErrorText}>{deleteError}</p>}
|
|
<div className={styles.modalFooter}>
|
|
<button onClick={handleCloseDeleteModal} className={styles.modalButtonCancel}>
|
|
{modalCancelButtonText}
|
|
</button>
|
|
<button onClick={handleConfirmDelete} className={styles.modalButtonConfirm}>
|
|
{modalConfirmButtonText}
|
|
</button>
|
|
</div>
|
|
</Modal>
|
|
|
|
{/* Share modal */}
|
|
<Modal
|
|
isOpen={showShareModal}
|
|
onClose={handleCloseShareModal}
|
|
title={shareModalTitle}
|
|
>
|
|
<div className={styles.shareModalContent}>
|
|
<input
|
|
type="text"
|
|
placeholder={shareModalSearchPlaceholder}
|
|
value={shareSearchTerm}
|
|
onChange={(e) => setShareSearchTerm(e.target.value)}
|
|
className={styles.shareSearchInput}
|
|
disabled={isLoadingUsers || isSharing}
|
|
/>
|
|
{isLoadingUsers && <p>{t('drawingCard.shareModal.loadingUsers') || 'Loading users...'}</p>}
|
|
{shareError && <p className={styles.modalErrorText}>{shareError}</p>}
|
|
|
|
{!isLoadingUsers && filteredUsersForSharing.length === 0 && (
|
|
<p>{shareModalNoUsersFoundText}</p>
|
|
)}
|
|
|
|
{!isLoadingUsers && filteredUsersForSharing.length > 0 && (
|
|
<ul className={styles.userListShare}>
|
|
{filteredUsersForSharing.map((user) => (
|
|
<li
|
|
key={user._id as string}
|
|
onClick={() => !isSharing && setSelectedUserIdToShare(user._id as string)}
|
|
className={`${styles.userListItemShare} ${selectedUserIdToShare === user._id ? styles.selectedUser : ''} ${isSharing ? styles.disabledItem : ''}`}
|
|
>
|
|
{user.username} ({user.email})
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
<div className={styles.modalFooter}>
|
|
<button
|
|
onClick={handleCloseShareModal}
|
|
className={styles.modalButtonCancel}
|
|
disabled={isSharing}
|
|
>
|
|
{shareModalCancelButtonText}
|
|
</button>
|
|
<button
|
|
onClick={handleConfirmShare}
|
|
className={styles.modalButtonConfirm}
|
|
disabled={!selectedUserIdToShare || isLoadingUsers || isSharing}
|
|
>
|
|
{isSharing ? (t('drawingCard.shareModal.sharingButton') || 'Sharing...') : shareModalConfirmButtonText}
|
|
</button>
|
|
</div>
|
|
</Modal>
|
|
</>
|
|
);
|
|
} |