SAVE
This commit is contained in:
parent
c583dd0b72
commit
3aa27efa36
|
@ -61,6 +61,8 @@
|
||||||
"newDrawingButton": "Create New Drawing",
|
"newDrawingButton": "Create New Drawing",
|
||||||
"newDrawingPlaceholder": "Enter drawing title...",
|
"newDrawingPlaceholder": "Enter drawing title...",
|
||||||
"noDrawings": "You don't have any drawings yet. Create one!",
|
"noDrawings": "You don't have any drawings yet. Create one!",
|
||||||
|
"myDrawingsTitle": "My Drawings",
|
||||||
|
"sharedDrawingsTitle": "Shared with me",
|
||||||
"errors": {
|
"errors": {
|
||||||
"createFailed": "Failed to create new drawing.",
|
"createFailed": "Failed to create new drawing.",
|
||||||
"titleRequired": "Title is required."
|
"titleRequired": "Title is required."
|
||||||
|
@ -70,6 +72,7 @@
|
||||||
"lastUpdatedPrefix": "Last updated:",
|
"lastUpdatedPrefix": "Last updated:",
|
||||||
"editTitleAria": "Edit title",
|
"editTitleAria": "Edit title",
|
||||||
"deleteAria": "Delete drawing",
|
"deleteAria": "Delete drawing",
|
||||||
|
"shareAria": "Share drawing",
|
||||||
"confirmDelete": {
|
"confirmDelete": {
|
||||||
"title": "Confirm Deletion",
|
"title": "Confirm Deletion",
|
||||||
"message": "Are you sure you want to delete \"{{title}}\"? This action cannot be undone.",
|
"message": "Are you sure you want to delete \"{{title}}\"? This action cannot be undone.",
|
||||||
|
@ -79,7 +82,19 @@
|
||||||
"errors": {
|
"errors": {
|
||||||
"titleRequired": "Title is required.",
|
"titleRequired": "Title is required.",
|
||||||
"renameFailed": "Failed to rename drawing.",
|
"renameFailed": "Failed to rename drawing.",
|
||||||
"deleteFailed": "Failed to delete drawing."
|
"deleteFailed": "Failed to delete drawing.",
|
||||||
|
"loadUsersFailed": "Failed to load users.",
|
||||||
|
"userNotSelected": "Please select a user to share with.",
|
||||||
|
"shareFailed": "Failed to share drawing."
|
||||||
|
},
|
||||||
|
"shareModal": {
|
||||||
|
"title": "Share Drawing",
|
||||||
|
"searchPlaceholder": "Search user by name or email",
|
||||||
|
"confirmButton": "Share",
|
||||||
|
"cancelButton": "Cancel",
|
||||||
|
"noUsersFound": "No users available to share with or match your search.",
|
||||||
|
"loadingUsers": "Loading users...",
|
||||||
|
"sharingButton": "Sharing..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"editor": {
|
"editor": {
|
||||||
|
|
|
@ -61,6 +61,8 @@
|
||||||
"newDrawingButton": "Crear Nuevo Dibujo",
|
"newDrawingButton": "Crear Nuevo Dibujo",
|
||||||
"newDrawingPlaceholder": "Introduce el título del dibujo...",
|
"newDrawingPlaceholder": "Introduce el título del dibujo...",
|
||||||
"noDrawings": "Aún no tienes dibujos. ¡Crea uno!",
|
"noDrawings": "Aún no tienes dibujos. ¡Crea uno!",
|
||||||
|
"myDrawingsTitle": "Mis Dibujos",
|
||||||
|
"sharedDrawingsTitle": "Compartidos conmigo",
|
||||||
"errors": {
|
"errors": {
|
||||||
"createFailed": "Error al crear nuevo dibujo.",
|
"createFailed": "Error al crear nuevo dibujo.",
|
||||||
"titleRequired": "El título es obligatorio."
|
"titleRequired": "El título es obligatorio."
|
||||||
|
@ -70,6 +72,7 @@
|
||||||
"lastUpdatedPrefix": "Última act.:",
|
"lastUpdatedPrefix": "Última act.:",
|
||||||
"editTitleAria": "Editar título",
|
"editTitleAria": "Editar título",
|
||||||
"deleteAria": "Eliminar dibujo",
|
"deleteAria": "Eliminar dibujo",
|
||||||
|
"shareAria": "Compartir dibujo",
|
||||||
"confirmDelete": {
|
"confirmDelete": {
|
||||||
"title": "Confirmar Eliminación",
|
"title": "Confirmar Eliminación",
|
||||||
"message": "¿Estás seguro de que quieres eliminar \"{{title}}\"? Esta acción no se puede deshacer.",
|
"message": "¿Estás seguro de que quieres eliminar \"{{title}}\"? Esta acción no se puede deshacer.",
|
||||||
|
@ -79,7 +82,19 @@
|
||||||
"errors": {
|
"errors": {
|
||||||
"titleRequired": "El título es obligatorio.",
|
"titleRequired": "El título es obligatorio.",
|
||||||
"renameFailed": "Error al renombrar el dibujo.",
|
"renameFailed": "Error al renombrar el dibujo.",
|
||||||
"deleteFailed": "Error al eliminar el dibujo."
|
"deleteFailed": "Error al eliminar el dibujo.",
|
||||||
|
"loadUsersFailed": "Error al cargar usuarios.",
|
||||||
|
"userNotSelected": "Por favor, selecciona un usuario para compartir.",
|
||||||
|
"shareFailed": "Error al compartir el dibujo."
|
||||||
|
},
|
||||||
|
"shareModal": {
|
||||||
|
"title": "Compartir Dibujo",
|
||||||
|
"searchPlaceholder": "Buscar usuario por nombre o email",
|
||||||
|
"confirmButton": "Compartir",
|
||||||
|
"cancelButton": "Cancelar",
|
||||||
|
"noUsersFound": "No hay usuarios disponibles para compartir o que coincidan con tu búsqueda.",
|
||||||
|
"loadingUsers": "Cargando usuarios...",
|
||||||
|
"sharingButton": "Compartiendo..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"editor": {
|
"editor": {
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { shareDrawingWithUser } from "@/controllers/drawingsController";
|
||||||
|
import { authMiddleware } from "@/lib/helpers/api-middlewares/auth";
|
||||||
|
|
||||||
|
interface ShareRequestBody {
|
||||||
|
userIdToShareWith: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
const authResult = await authMiddleware(request);
|
||||||
|
if (!authResult.isAuthenticated || !authResult.user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawingId = params.id;
|
||||||
|
const ownerUserId = authResult.user.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await request.json() as ShareRequestBody;
|
||||||
|
const { userIdToShareWith } = body;
|
||||||
|
|
||||||
|
if (!userIdToShareWith) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "userIdToShareWith is required" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await shareDrawingWithUser(
|
||||||
|
drawingId,
|
||||||
|
ownerUserId,
|
||||||
|
userIdToShareWith
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
return NextResponse.json({ error: result.error }, { status: result.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(result.data, { status: result.status });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in POST /api/drawings/[id]/share:", error);
|
||||||
|
if (error instanceof SyntaxError) {
|
||||||
|
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { unshareDrawingWithUser } from "@/controllers/drawingsController";
|
||||||
|
import { authMiddleware } from "@/lib/helpers/api-middlewares/auth";
|
||||||
|
|
||||||
|
interface UnshareRequestBody {
|
||||||
|
userIdToUnshare: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
const authResult = await authMiddleware(request);
|
||||||
|
if (!authResult.isAuthenticated || !authResult.user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawingId = params.id;
|
||||||
|
const ownerUserId = authResult.user.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await request.json() as UnshareRequestBody;
|
||||||
|
const { userIdToUnshare } = body;
|
||||||
|
|
||||||
|
if (!userIdToUnshare) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "userIdToUnshare is required" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await unshareDrawingWithUser(
|
||||||
|
drawingId,
|
||||||
|
ownerUserId,
|
||||||
|
userIdToUnshare
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
return NextResponse.json({ error: result.error }, { status: result.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(result.data, { status: result.status });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in POST /api/drawings/[id]/unshare:", error);
|
||||||
|
if (error instanceof SyntaxError) {
|
||||||
|
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { getUsers } from "@/controllers/usersController";
|
||||||
|
import { authMiddleware } from "@/lib/helpers/api-middlewares/auth";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { isAuthenticated } = await authMiddleware(request);
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getUsers();
|
||||||
|
return NextResponse.json(result, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in GET /api/users:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -93,6 +93,10 @@
|
||||||
--text-primary: #fff;
|
--text-primary: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logoutButtonText {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
.createButton {
|
.createButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -241,6 +245,10 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out, transform 0.1s ease;
|
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out, transform 0.1s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionButton:active {
|
.actionButton:active {
|
||||||
|
@ -276,4 +284,107 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawingsSection {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sectionTitle {
|
||||||
|
color: #f0f0f0;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerLeft {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerRight {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createButton {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoutButton {
|
||||||
|
padding: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoutButtonText {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawingsGrid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inlineCreateForm {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inlineInput {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButton {
|
||||||
|
padding: 0.6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.headerSwitchers {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerLeft {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawingsGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inlineCreateForm {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inlineInput {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -10,11 +10,12 @@ import ThemeSwitcher from "@/components/ThemeSwitcher/ThemeSwitcher";
|
||||||
import LanguageSwitcher from "@/components/LanguageSwitcher/LanguageSwitcher";
|
import LanguageSwitcher from "@/components/LanguageSwitcher/LanguageSwitcher";
|
||||||
import Icon from "@/components/Icon/Icon";
|
import Icon from "@/components/Icon/Icon";
|
||||||
import Loader from "@/components/Loader/Loader";
|
import Loader from "@/components/Loader/Loader";
|
||||||
|
import { jwtDecode } from "jwt-decode";
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const { t, isLoading: i18nIsLoading } = useI18n();
|
const { t, isLoading: i18nIsLoading } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [drawings, setDrawings] = useState<Drawing[]>([]);
|
const [myDrawings, setMyDrawings] = useState<Drawing[]>([]);
|
||||||
|
const [sharedDrawings, setSharedDrawings] = useState<Drawing[]>([]);
|
||||||
const [pageLoading, setPageLoading] = useState(true);
|
const [pageLoading, setPageLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
@ -29,7 +30,14 @@ export default function Dashboard() {
|
||||||
setError(null);
|
setError(null);
|
||||||
const response = await getAllDrawings();
|
const response = await getAllDrawings();
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
setDrawings(response.data);
|
const token = localStorage.getItem('token');
|
||||||
|
const decodedToken = jwtDecode(token || "") as { userId: string };
|
||||||
|
const userId = decodedToken.userId;
|
||||||
|
const own = response.data.filter(drawing => drawing.owner_id === userId);
|
||||||
|
const shared = response.data.filter(drawing => drawing.owner_id !== userId);
|
||||||
|
|
||||||
|
setMyDrawings(own);
|
||||||
|
setSharedDrawings(shared);
|
||||||
} else if (response.error) {
|
} else if (response.error) {
|
||||||
setError(response.error);
|
setError(response.error);
|
||||||
}
|
}
|
||||||
|
@ -91,10 +99,9 @@ export default function Dashboard() {
|
||||||
};
|
};
|
||||||
const response = await createDrawing(newDrawingData);
|
const response = await createDrawing(newDrawingData);
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
router.push(`/editor?id=${response.data._id}`);
|
router.push(`/editor/${response.data._id}`);
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setNewDrawingTitle("");
|
setNewDrawingTitle("");
|
||||||
// fetchDrawings();
|
|
||||||
} else if (response.error) {
|
} else if (response.error) {
|
||||||
setError(response.error);
|
setError(response.error);
|
||||||
}
|
}
|
||||||
|
@ -115,15 +122,31 @@ export default function Dashboard() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrawingRenamed = (updatedDrawing: Drawing) => {
|
const handleDrawingRenamed = (updatedDrawing: Drawing) => {
|
||||||
setDrawings(prevDrawings =>
|
const userId = localStorage.getItem('userId');
|
||||||
prevDrawings.map(d => d._id === updatedDrawing._id ? updatedDrawing : d)
|
if (updatedDrawing.owner_id === userId) {
|
||||||
);
|
setMyDrawings(prevDrawings =>
|
||||||
|
prevDrawings.map(d => d._id === updatedDrawing._id ? updatedDrawing : d)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSharedDrawings(prevDrawings =>
|
||||||
|
prevDrawings.map(d => d._id === updatedDrawing._id ? updatedDrawing : d)
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrawingDeleted = (drawingId: string) => {
|
const handleDrawingDeleted = (drawingId: string) => {
|
||||||
setDrawings(prevDrawings =>
|
setMyDrawings(prevDrawings =>
|
||||||
prevDrawings.filter(d => d._id !== drawingId)
|
prevDrawings.filter(d => d._id !== drawingId)
|
||||||
);
|
);
|
||||||
|
setSharedDrawings(prevDrawings =>
|
||||||
|
prevDrawings.filter(d => d._id !== drawingId)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrawingShared = (updatedDrawing: Drawing) => {
|
||||||
|
setMyDrawings(prevDrawings =>
|
||||||
|
prevDrawings.map(d => d._id === updatedDrawing._id ? updatedDrawing : d)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
|
@ -165,7 +188,7 @@ export default function Dashboard() {
|
||||||
<div className={styles.page}>
|
<div className={styles.page}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<div className={styles.headerLeft}>
|
<div className={styles.headerLeft}>
|
||||||
<h1>{t('dashboard.title') || "My Drawings"}</h1>
|
<h1>Draw</h1>
|
||||||
<div className={styles.headerSwitchers}>
|
<div className={styles.headerSwitchers}>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
|
@ -188,6 +211,20 @@ export default function Dashboard() {
|
||||||
placeholder={t('dashboard.newDrawingPlaceholder') || "Enter drawing title..."}
|
placeholder={t('dashboard.newDrawingPlaceholder') || "Enter drawing title..."}
|
||||||
className={styles.inlineInput}
|
className={styles.inlineInput}
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleConfirmCreate}
|
||||||
|
className={`${styles.actionButton} ${styles.confirmButton}`}
|
||||||
|
aria-label={t('dashboard.create') || "Create"}
|
||||||
|
>
|
||||||
|
<Icon name="check" size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleCancelCreate}
|
||||||
|
className={`${styles.actionButton} ${styles.cancelButton}`}
|
||||||
|
aria-label={t('dashboard.cancel') || "Cancel"}
|
||||||
|
>
|
||||||
|
<Icon name="x" size={16} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button onClick={handleLogout} className={styles.logoutButton} title={t('header.logout') || 'Log out'}>
|
<button onClick={handleLogout} className={styles.logoutButton} title={t('header.logout') || 'Log out'}>
|
||||||
|
@ -199,20 +236,44 @@ export default function Dashboard() {
|
||||||
|
|
||||||
{error && <p className={styles.errorText}>{error}</p>}
|
{error && <p className={styles.errorText}>{error}</p>}
|
||||||
|
|
||||||
{drawings.length === 0 && !error && !isCreating && (
|
{myDrawings.length === 0 && sharedDrawings.length === 0 && !error && !isCreating && (
|
||||||
<p>{t('dashboard.noDrawings') || "You don\'t have any drawings yet. Create one!"}</p>
|
<p>{t('dashboard.noDrawings') || "You don\'t have any drawings yet. Create one!"}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.drawingsGrid}>
|
{myDrawings.length > 0 && (
|
||||||
{drawings.map((drawing) => (
|
<section className={styles.drawingsSection}>
|
||||||
<DrawingCard
|
<h2 className={styles.sectionTitle}>{t('dashboard.myDrawingsTitle') || "Mis dibujos"}</h2>
|
||||||
key={drawing._id}
|
<div className={styles.drawingsGrid}>
|
||||||
drawing={drawing}
|
{myDrawings.map((drawing) => (
|
||||||
onDrawingRenamed={handleDrawingRenamed}
|
<DrawingCard
|
||||||
onDrawingDeleted={handleDrawingDeleted}
|
owned={true}
|
||||||
/>
|
key={drawing._id}
|
||||||
))}
|
drawing={drawing}
|
||||||
</div>
|
onDrawingRenamed={handleDrawingRenamed}
|
||||||
|
onDrawingDeleted={handleDrawingDeleted}
|
||||||
|
onDrawingShared={handleDrawingShared}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sharedDrawings.length > 0 && (
|
||||||
|
<section className={styles.drawingsSection}>
|
||||||
|
<h2 className={styles.sectionTitle}>{t('dashboard.sharedDrawingsTitle') || "Compartidos conmigo"}</h2>
|
||||||
|
<div className={styles.drawingsGrid}>
|
||||||
|
{sharedDrawings.map((drawing) => (
|
||||||
|
<DrawingCard
|
||||||
|
owned={false}
|
||||||
|
key={drawing._id}
|
||||||
|
drawing={drawing}
|
||||||
|
onDrawingRenamed={handleDrawingRenamed}
|
||||||
|
onDrawingDeleted={handleDrawingDeleted}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,12 +4,21 @@ import Link from 'next/link';
|
||||||
import LanguageSwitcher from '@/components/LanguageSwitcher/LanguageSwitcher';
|
import LanguageSwitcher from '@/components/LanguageSwitcher/LanguageSwitcher';
|
||||||
import ThemeSwitcher from '@/components/ThemeSwitcher/ThemeSwitcher';
|
import ThemeSwitcher from '@/components/ThemeSwitcher/ThemeSwitcher';
|
||||||
import { useI18n } from '@/lib/i18n/useI18n';
|
import { useI18n } from '@/lib/i18n/useI18n';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { t, isLoading } = useI18n();
|
const { t, isLoading } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
router.push('/dashboard');
|
||||||
|
}
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
if (isLoading) return null;
|
if (isLoading) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 flex flex-col">
|
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 flex flex-col">
|
||||||
<header className="py-6 bg-white dark:bg-gray-800 shadow-sm">
|
<header className="py-6 bg-white dark:bg-gray-800 shadow-sm">
|
||||||
|
|
|
@ -285,4 +285,133 @@
|
||||||
.modalButtonConfirm:hover {
|
.modalButtonConfirm:hover {
|
||||||
background-color: #c9302c;
|
background-color: #c9302c;
|
||||||
border-color: #ac2925;
|
border-color: #ac2925;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Share modal */
|
||||||
|
.shareModalContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareSearchInput {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
width: 100%;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareSearchInput:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0070f3;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 112, 243, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .shareSearchInput {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
border-color: #444;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .shareSearchInput:focus {
|
||||||
|
border-color: #0070f3;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 112, 243, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareSearchInput:disabled {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .shareSearchInput:disabled {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userListShare {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
max-height: 250px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .userListShare {
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userListItemShare {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
border-bottom: 1px solid #eeeeee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userListItemShare:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .userListItemShare {
|
||||||
|
border-bottom-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userListItemShare:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .userListItemShare:hover {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedUser {
|
||||||
|
background-color: #e8f0fe;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedUser:hover {
|
||||||
|
background-color: #d8e5fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .selectedUser {
|
||||||
|
background-color: #1a3a6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .selectedUser:hover {
|
||||||
|
background-color: #254b85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabledItem {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabledItem:hover {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .disabledItem:hover {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para botones deshabilitados */
|
||||||
|
.modalButtonConfirm:disabled,
|
||||||
|
.modalButtonCancel:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalButtonConfirm:disabled:hover,
|
||||||
|
.modalButtonCancel:disabled:hover {
|
||||||
|
background-color: inherit;
|
||||||
|
border-color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .modalButtonConfirm:disabled:hover,
|
||||||
|
:global(.dark) .modalButtonCancel:disabled:hover {
|
||||||
|
background-color: inherit;
|
||||||
|
border-color: inherit;
|
||||||
}
|
}
|
|
@ -1,12 +1,15 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import {
|
import {
|
||||||
Drawing,
|
Drawing,
|
||||||
updateDrawing as apiUpdateDrawing,
|
updateDrawing as apiUpdateDrawing,
|
||||||
deleteDrawing as apiDeleteDrawing,
|
deleteDrawing as apiDeleteDrawing,
|
||||||
|
getUsers as apiGetUsers,
|
||||||
|
shareDrawingWithUser as apiShareDrawing,
|
||||||
} from '@/lib/api';
|
} from '@/lib/api';
|
||||||
|
import { IUser } from '@/models/User';
|
||||||
import styles from './DrawingCard.module.css';
|
import styles from './DrawingCard.module.css';
|
||||||
import { useI18n } from '@/lib/i18n/useI18n';
|
import { useI18n } from '@/lib/i18n/useI18n';
|
||||||
import Icon from '@/components/Icon/Icon';
|
import Icon from '@/components/Icon/Icon';
|
||||||
|
@ -14,14 +17,18 @@ import Modal from '@/components/Modal/Modal';
|
||||||
|
|
||||||
interface DrawingCardProps {
|
interface DrawingCardProps {
|
||||||
drawing: Drawing;
|
drawing: Drawing;
|
||||||
|
owned: boolean;
|
||||||
onDrawingRenamed: (updatedDrawing: Drawing) => void;
|
onDrawingRenamed: (updatedDrawing: Drawing) => void;
|
||||||
onDrawingDeleted: (drawingId: string) => void;
|
onDrawingDeleted: (drawingId: string) => void;
|
||||||
|
onDrawingShared?: (updatedDrawing: Drawing) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DrawingCard({
|
export default function DrawingCard({
|
||||||
drawing,
|
drawing,
|
||||||
|
owned,
|
||||||
onDrawingRenamed,
|
onDrawingRenamed,
|
||||||
onDrawingDeleted,
|
onDrawingDeleted,
|
||||||
|
onDrawingShared,
|
||||||
}: DrawingCardProps) {
|
}: DrawingCardProps) {
|
||||||
const { t, isLoading: i18nCardIsLoading } = useI18n();
|
const { t, isLoading: i18nCardIsLoading } = useI18n();
|
||||||
|
|
||||||
|
@ -32,6 +39,21 @@ export default function DrawingCard({
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
if (isEditing && editTitleInputRef.current) {
|
if (isEditing && editTitleInputRef.current) {
|
||||||
editTitleInputRef.current.focus();
|
editTitleInputRef.current.focus();
|
||||||
|
@ -53,7 +75,7 @@ export default function DrawingCard({
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
};
|
};
|
||||||
}, [isEditing]);
|
}, [isEditing, handleCancelEdit]);
|
||||||
|
|
||||||
const lastUpdatedText = i18nCardIsLoading
|
const lastUpdatedText = i18nCardIsLoading
|
||||||
? "Last updated:"
|
? "Last updated:"
|
||||||
|
@ -66,6 +88,10 @@ export default function DrawingCard({
|
||||||
const deleteButtonAriaLabel = i18nCardIsLoading
|
const deleteButtonAriaLabel = i18nCardIsLoading
|
||||||
? "Delete drawing"
|
? "Delete drawing"
|
||||||
: t('drawingCard.deleteAria') || "Delete drawing";
|
: t('drawingCard.deleteAria') || "Delete drawing";
|
||||||
|
|
||||||
|
const shareButtonAriaLabel = i18nCardIsLoading
|
||||||
|
? "Share drawing"
|
||||||
|
: t('drawingCard.shareAria') || "Share drawing";
|
||||||
|
|
||||||
const handleStartEdit = () => {
|
const handleStartEdit = () => {
|
||||||
setEditingTitle(drawing.title);
|
setEditingTitle(drawing.title);
|
||||||
|
@ -73,12 +99,6 @@ export default function DrawingCard({
|
||||||
setRenameError(null);
|
setRenameError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelEdit = () => {
|
|
||||||
setIsEditing(false);
|
|
||||||
setEditingTitle(drawing.title);
|
|
||||||
setRenameError(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirmEdit = async () => {
|
const handleConfirmEdit = async () => {
|
||||||
if (!editingTitle.trim()) {
|
if (!editingTitle.trim()) {
|
||||||
setRenameError(t('drawingCard.errors.titleRequired') || "Title is required.");
|
setRenameError(t('drawingCard.errors.titleRequired') || "Title is required.");
|
||||||
|
@ -138,6 +158,61 @@ export default function DrawingCard({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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
|
const modalTitle = i18nCardIsLoading
|
||||||
? "Confirm Deletion"
|
? "Confirm Deletion"
|
||||||
: t('drawingCard.confirmDelete.title') || "Confirm Deletion";
|
: t('drawingCard.confirmDelete.title') || "Confirm Deletion";
|
||||||
|
@ -160,6 +235,23 @@ export default function DrawingCard({
|
||||||
? "Cancel"
|
? "Cancel"
|
||||||
: t('drawingCard.confirmDelete.cancelButton') || "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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.cardContainer}>
|
<div className={styles.cardContainer}>
|
||||||
|
@ -173,9 +265,16 @@ export default function DrawingCard({
|
||||||
<button onClick={handleStartEdit} className={styles.iconButton} aria-label={editTitleAriaLabel}>
|
<button onClick={handleStartEdit} className={styles.iconButton} aria-label={editTitleAriaLabel}>
|
||||||
<Icon name="edit-2" size={18} />
|
<Icon name="edit-2" size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleOpenDeleteModal} className={`${styles.iconButton} ${styles.deleteButton}`} aria-label={deleteButtonAriaLabel}>
|
{owned && (
|
||||||
<Icon name="trash-2" size={18} />
|
<>
|
||||||
</button>
|
<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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -215,6 +314,60 @@ export default function DrawingCard({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -35,7 +35,9 @@ export async function createDrawing(data: {
|
||||||
export async function getUserDrawings(userId: string) {
|
export async function getUserDrawings(userId: string) {
|
||||||
try {
|
try {
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
const drawings = await Drawing.find({ owner_id: userId }).sort({
|
const drawings = await Drawing.find({
|
||||||
|
$or: [{ owner_id: userId }, { shared_with: userId }],
|
||||||
|
}).sort({
|
||||||
updatedAt: -1,
|
updatedAt: -1,
|
||||||
});
|
});
|
||||||
return { data: drawings, status: 200 };
|
return { data: drawings, status: 200 };
|
||||||
|
@ -73,7 +75,7 @@ export async function getDrawingById(drawingId: string, userId: string) {
|
||||||
export async function updateDrawing(
|
export async function updateDrawing(
|
||||||
drawingId: string,
|
drawingId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
updateData: Partial<Pick<IDrawing, "title" | "data" | "shared_with">>
|
updateData: Partial<Pick<IDrawing, "title" | "data">> & { shared_with_users?: string[] }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
|
@ -83,7 +85,12 @@ export async function updateDrawing(
|
||||||
return { error: "Drawing not found", status: 404 };
|
return { error: "Drawing not found", status: 404 };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (drawing.owner_id.toString() !== userId) {
|
const isOwner = drawing.owner_id.toString() === userId;
|
||||||
|
const isSharedWithUser = drawing.shared_with
|
||||||
|
.map((id: string) => id.toString())
|
||||||
|
.includes(userId);
|
||||||
|
|
||||||
|
if (!isOwner && !isSharedWithUser) {
|
||||||
return { error: "Forbidden", status: 403 };
|
return { error: "Forbidden", status: 403 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,8 +100,12 @@ export async function updateDrawing(
|
||||||
if (updateData.data !== undefined) {
|
if (updateData.data !== undefined) {
|
||||||
drawing.data = updateData.data;
|
drawing.data = updateData.data;
|
||||||
}
|
}
|
||||||
if (updateData.shared_with !== undefined) {
|
|
||||||
drawing.shared_with = updateData.shared_with;
|
if (updateData.shared_with_users !== undefined) {
|
||||||
|
if (!isOwner) {
|
||||||
|
return { error: "Forbidden: Only the owner can change sharing settings", status: 403 };
|
||||||
|
}
|
||||||
|
drawing.shared_with = updateData.shared_with_users;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawing.updatedAt = new Date();
|
drawing.updatedAt = new Date();
|
||||||
|
@ -128,3 +139,74 @@ export async function deleteDrawing(drawingId: string, userId: string) {
|
||||||
return { error: "Internal server error", status: 500 };
|
return { error: "Internal server error", status: 500 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function shareDrawingWithUser(
|
||||||
|
drawingId: string,
|
||||||
|
ownerUserId: string,
|
||||||
|
userIdToShareWith: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await dbConnect();
|
||||||
|
const drawing = await Drawing.findById(drawingId);
|
||||||
|
|
||||||
|
if (!drawing) {
|
||||||
|
return { error: "Drawing not found", status: 404 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawing.owner_id.toString() !== ownerUserId) {
|
||||||
|
return { error: "Forbidden: Only the owner can share this drawing", status: 403 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const userToShare = await User.findById(userIdToShareWith);
|
||||||
|
if (!userToShare) {
|
||||||
|
return { error: "User to share with not found", status: 404 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userIdToShareWith === ownerUserId) {
|
||||||
|
return { error: "Cannot share drawing with the owner", status: 400 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!drawing.shared_with.map((id: string) => id.toString()).includes(userIdToShareWith)) {
|
||||||
|
drawing.shared_with.push(userIdToShareWith);
|
||||||
|
drawing.updatedAt = new Date();
|
||||||
|
await drawing.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: drawing, status: 200 };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in shareDrawingWithUser:", error);
|
||||||
|
return { error: "Internal server error", status: 500 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unshareDrawingWithUser(
|
||||||
|
drawingId: string,
|
||||||
|
ownerUserId: string,
|
||||||
|
userIdToUnshare: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await dbConnect();
|
||||||
|
const drawing = await Drawing.findById(drawingId);
|
||||||
|
|
||||||
|
if (!drawing) {
|
||||||
|
return { error: "Drawing not found", status: 404 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawing.owner_id.toString() !== ownerUserId) {
|
||||||
|
return { error: "Forbidden: Only the owner can modify sharing settings", status: 403 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialSharedCount = drawing.shared_with.length;
|
||||||
|
drawing.shared_with = drawing.shared_with.filter((id: string) => id.toString() !== userIdToUnshare);
|
||||||
|
|
||||||
|
if (drawing.shared_with.length < initialSharedCount) {
|
||||||
|
drawing.updatedAt = new Date();
|
||||||
|
await drawing.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: drawing, status: 200 };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in unshareDrawingWithUser:", error);
|
||||||
|
return { error: "Internal server error", status: 500 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import User from "../models/User";
|
||||||
|
import dbConnect from "@/lib/db/connection";
|
||||||
|
|
||||||
|
export async function getUsers() {
|
||||||
|
try {
|
||||||
|
await dbConnect();
|
||||||
|
const users = await User.find();
|
||||||
|
return users;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in getUsers:", error);
|
||||||
|
return { error: "Internal server error", status: 500 };
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
import { IUser } from "@/models/User";
|
||||||
|
|
||||||
interface LoginRequest {
|
interface LoginRequest {
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -144,6 +145,16 @@ export async function logout(): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUsers(): Promise<ApiResponse<IUser[]>> {
|
||||||
|
return fetchApi<undefined, IUser[]>(
|
||||||
|
"/users",
|
||||||
|
"GET",
|
||||||
|
undefined,
|
||||||
|
{},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Drawing API Functions ---
|
// --- Drawing API Functions ---
|
||||||
export interface Drawing {
|
export interface Drawing {
|
||||||
_id: string;
|
_id: string;
|
||||||
|
@ -225,3 +236,37 @@ export async function deleteDrawing(
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ShareDrawingRequestBody {
|
||||||
|
userIdToShareWith: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function shareDrawingWithUser(
|
||||||
|
drawingId: string,
|
||||||
|
userIdToShareWith: string
|
||||||
|
): Promise<ApiResponse<Drawing>> {
|
||||||
|
return fetchApi<ShareDrawingRequestBody, Drawing>(
|
||||||
|
`/drawings/${drawingId}/share`,
|
||||||
|
"POST",
|
||||||
|
{ userIdToShareWith },
|
||||||
|
{},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UnshareDrawingRequestBody {
|
||||||
|
userIdToUnshare: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unshareDrawingWithUser(
|
||||||
|
drawingId: string,
|
||||||
|
userIdToUnshare: string
|
||||||
|
): Promise<ApiResponse<Drawing>> {
|
||||||
|
return fetchApi<UnshareDrawingRequestBody, Drawing>(
|
||||||
|
`/drawings/${drawingId}/unshare`,
|
||||||
|
"POST",
|
||||||
|
{ userIdToUnshare },
|
||||||
|
{},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import { validateToken } from "@/controllers/authController";
|
||||||
|
|
||||||
|
export async function authMiddleware(request: NextRequest) {
|
||||||
|
const authHeader = request.headers.get("Authorization");
|
||||||
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||||
|
return { isAuthenticated: false, user: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.substring(7);
|
||||||
|
const { user } = await validateToken(token);
|
||||||
|
|
||||||
|
return { isAuthenticated: !!user, user };
|
||||||
|
}
|
|
@ -15,15 +15,11 @@ const UserSchema = new Schema<IUser>(
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
trim: true,
|
|
||||||
minlength: 3
|
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
trim: true,
|
|
||||||
lowercase: true
|
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -32,7 +28,13 @@ const UserSchema = new Schema<IUser>(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true
|
timestamps: true,
|
||||||
|
toJSON: {
|
||||||
|
transform: (_, ret) => {
|
||||||
|
delete ret.password;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user