Skip to content

Commit

Permalink
Document Details Page Backend and Frontend Integration (#164)
Browse files Browse the repository at this point in the history
* Started work on layout responsiveness

* Improved Sidebar responsiveness and typography

* Started Working on Document-Details Page

* Refactor Sidebar and DropdownMenu components

* Update icon usage in PasswordValidation

* Refactor document and user interfaces;

* Enhance utility functions for file size and date/time formatting with detailed documentation and improved locale support

* Refactor useSort hook to improve sorting logic

* Renamed Components, hooks, services

* Fixed Icon

* Implement document-details and link management API endpoints with authentication and error handling

* Refactor document details page structure by removing obsolete components and adding new dynamic document view and loading components

* Add document not found, view document, loading components, and info table with sorting and pagination

* Fix import typo in CreateLink component

* Refactor hooks and components

* Refactor document components to improve imports and type handling

* Update LinkVisitors model to include first and last name fields

* Enhance document fetching and user-specific link handling, and enhance link handling logic

* Updated Views data on Document details

* Refactor visitor data handling to use 'Contact' model and update field names for clarity

* Remove error toast from link creation and correct typo in sharing options message
  • Loading branch information
mahid797 authored Jan 20, 2025
1 parent 69cd27b commit af5bcb9
Show file tree
Hide file tree
Showing 59 changed files with 3,861 additions and 2,310 deletions.
881 changes: 236 additions & 645 deletions Client/package-lock.json

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions Client/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,13 @@ model Link {
}

model LinkVisitors {
id Int @id @default(autoincrement())
linkId String
name String @default("")
email String @default("")
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
id Int @id @default(autoincrement())
linkId String
first_name String @default("")
last_name String @default("")
email String @default("")
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
// Relations
Link Link @relation(fields: [linkId], references: [linkId], onDelete: Cascade)
Expand Down
46 changes: 30 additions & 16 deletions Client/public/assets/icons/auth/XCircleIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
import React from 'react';
type IconColor = 'error' | 'success' | 'disabled' | 'outline';

interface IconProps {
width?: number;
height?: number;
color?: string;
stroke?: string;
className?: string;
}
const colorMap: Record<IconColor, string> = {
error: '#F04438',
success: '#067647',
disabled: '#D0D5DD',
outline: '#344054',
};

const XCircleIcon: React.FC<IconProps> = ({
const XCircleIcon = ({
width = 24,
height = 24,
color = 'none',
stroke = 'white',
className = '',
color = 'disabled',
...props
}: {
width?: number;
height?: number;
color?: IconColor;
}) => {
const isOutline = color === 'outline';
const fillColor = isOutline ? 'none' : colorMap[color];
const strokeColor = isOutline ? colorMap['outline'] : colorMap[color];

return (
<svg
width={width}
height={height}
viewBox='0 0 24 24'
fill={color}
fill={fillColor}
xmlns='http://www.w3.org/2000/svg'
className={className}>
{...props}>
<circle
cx='12'
cy='12'
r='11'
fill={fillColor}
stroke={strokeColor}
strokeWidth={isOutline ? 2 : 0}
/>
<path
d='M15 9L9 15M9 9L15 15M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z'
stroke={stroke}
d='M15 9L9 15M9 9L15 15'
stroke={isOutline ? colorMap['outline'] : 'white'}
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
Expand Down
39 changes: 39 additions & 0 deletions Client/src/app/api/documents/[documentId]/links/[linkId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { authenticate } from '@lib/middleware/authenticate';
import prisma from '@lib/prisma';
import { NextRequest, NextResponse } from 'next/server';

export async function DELETE(req: NextRequest, { params }: { params: { linkId: string } }) {
try {
const userId = await authenticate(req);
const { linkId } = params;

// Verify doc ownership + link existence
const link = await prisma.link.findFirst({
where: {
linkId: linkId,
userId: userId,
},
});

if (!link) {
return NextResponse.json(
{ error: 'Link not found or you do not have access.' },
{ status: 404 },
);
}

// Delete the link
await prisma.link.delete({
where: { linkId: link.linkId },
});

return NextResponse.json({ message: 'Link deleted successfully.' }, { status: 200 });
} catch (error) {
return createErrorResponse('Server error.', 500, error);
}
}

function createErrorResponse(message: string, status: number, details?: any) {
console.error(`[${new Date().toISOString()}] ${message}`, details);
return NextResponse.json({ error: message, details }, { status });
}
43 changes: 43 additions & 0 deletions Client/src/app/api/documents/[documentId]/links/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { authenticate } from '@lib/middleware/authenticate';
import prisma from '@lib/prisma';
import { NextRequest, NextResponse } from 'next/server';

export async function GET(req: NextRequest, { params }: { params: { documentId: string } }) {
try {
const userId = await authenticate(req);
const { documentId } = params;

// Verify doc ownership
const doc = await prisma.document.findFirst({
where: { document_id: documentId, user_id: userId },
include: { Link: true },
});

if (!doc) {
return NextResponse.json(
{ error: 'Document not found or you do not have access.' },
{ status: 404 },
);
}

// Map the DB fields to the shape needed by InfoTable -> LinkDetail

const links = doc.Link.map((link) => ({
id: link.id,
documentId: doc.document_id,
linkId: link.linkId,
createdLink: link.linkUrl,
lastViewed: link.updatedAt,
linkViews: 0, // Placeholder
}));

return NextResponse.json({ links }, { status: 200 });
} catch (error) {
return createErrorResponse('Server error.', 500, error);
}
}

function createErrorResponse(message: string, status: number, details?: any) {
console.error(`[${new Date().toISOString()}] ${message}`, details);
return NextResponse.json({ error: message, details }, { status });
}
103 changes: 103 additions & 0 deletions Client/src/app/api/documents/[documentId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { authenticate } from '@lib/middleware/authenticate';
import prisma from '@lib/prisma';
import { NextRequest, NextResponse } from 'next/server';
import { deleteFile } from '@/services/storageService';

export async function GET(req: NextRequest, { params }: { params: { documentId: string } }) {
try {
// Verify the user is logged in
const userId = await authenticate(req);
const { documentId } = params;

// Query the database for this document, ensuring it belongs to user
const doc = await prisma.document.findFirst({
where: { document_id: documentId, user_id: userId },
select: {
id: true,
document_id: true,
fileName: true,
filePath: true,
fileType: true,
size: true,
createdAt: true,
updatedAt: true,
User: {
select: {
first_name: true,
last_name: true,
},
},
},
});

// If not found or unauthorized
if (!doc) {
return NextResponse.json(
{ error: 'Document not found or you do not have access.' },
{ status: 404 },
);
}

// Construct the response
const responsePayload = {
...doc,
uploader: {
name: `${doc.User.first_name} ${doc.User.last_name}`,
avatar: null, // Add avatar URL here
},
links: 0, // placeholder
viewers: 0, // placeholder
views: 0, // placeholder
};

return NextResponse.json({ document: responsePayload }, { status: 200 });
} catch (error) {
return createErrorResponse('Server error.', 500, error);
}
}

// Utility for error handling
function createErrorResponse(message: string, status: number, details?: any) {
console.error(`[${new Date().toISOString()}] ${message}`, details);
return NextResponse.json({ error: message, details }, { status });
}

export async function DELETE(
req: NextRequest,
{ params }: { params: { documentId: string } },
): Promise<NextResponse> {
try {
const userId = await authenticate(req);
const documentId = params.documentId;

if (!documentId) {
return createErrorResponse('Document ID is required.', 400);
}

const document = await prisma.document.findUnique({
where: { document_id: documentId },
});

if (!document || document.user_id !== userId) {
return createErrorResponse('Document not found or access denied.', 404);
}

const deletedFile = await prisma.document.delete({
where: { document_id: documentId },
});

await deleteFileFromStorage(deletedFile.filePath);

return NextResponse.json({ message: 'Document deleted successfully.' }, { status: 200 });
} catch (error) {
return createErrorResponse('Server error.', 500, error);
}
}

async function deleteFileFromStorage(filePath: string) {
try {
await deleteFile(filePath);
} catch (error) {
throw error;
}
}
58 changes: 58 additions & 0 deletions Client/src/app/api/documents/[documentId]/visitors/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { authenticate } from '@lib/middleware/authenticate';
import prisma from '@lib/prisma';
import { NextRequest, NextResponse } from 'next/server';

export async function GET(req: NextRequest, { params }: { params: { documentId: string } }) {
try {
const userId = await authenticate(req);
const { documentId } = params;

// Ensure doc belongs to user
const doc = await prisma.document.findFirst({
where: { document_id: documentId, user_id: userId },
include: { Link: true },
});
if (!doc) {
return NextResponse.json(
{ error: 'Document not found or you do not have access.' },
{ status: 404 },
);
}

// Gather all linkIds
const linkIds = doc.Link.map((l) => l.linkId);
if (linkIds.length === 0) {
// No links => no visitors
return NextResponse.json({ visitors: [] }, { status: 200 });
}

// Query LinkVisitors for all those linkIds
const linkVisitors = await prisma.linkVisitors.findMany({
where: {
linkId: { in: linkIds },
},
orderBy: { updatedAt: 'desc' },
});

const visitors = linkVisitors.map((visitor) => ({
id: visitor.id,
documentId: doc.document_id,
name: `${visitor.first_name} ${visitor.last_name}`.trim(),
email: visitor.email,
lastActivity: visitor.updatedAt,
// These are placeholders for now
downloads: 0,
duration: 0,
completion: 0,
}));

return NextResponse.json({ visitors }, { status: 200 });
} catch (error) {
return createErrorResponse('Server error.', 500, error);
}
}

function createErrorResponse(message: string, status: number, details?: any) {
console.error(`[${new Date().toISOString()}] ${message}`, details);
return NextResponse.json({ error: message, details }, { status });
}
49 changes: 0 additions & 49 deletions Client/src/app/api/documents/delete/[documentId]/route.ts

This file was deleted.

Loading

0 comments on commit af5bcb9

Please sign in to comment.