I still can display thumbnails
This commit is contained in:
@@ -14,6 +14,7 @@ from starlette.responses import Response
|
|||||||
|
|
||||||
from app.api.dependencies import get_document_service, get_current_user
|
from app.api.dependencies import get_document_service, get_current_user
|
||||||
from app.models.document import DocumentResponse, FileDocument
|
from app.models.document import DocumentResponse, FileDocument
|
||||||
|
from app.models.user import UserInDB
|
||||||
from app.services.document_service import DocumentService
|
from app.services.document_service import DocumentService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -123,7 +124,7 @@ def _map_file_document_to_response(
|
|||||||
def list_documents(
|
def list_documents(
|
||||||
skip: int = Query(0, ge=0, description="Number of documents to skip"),
|
skip: int = Query(0, ge=0, description="Number of documents to skip"),
|
||||||
limit: int = Query(100, ge=1, le=1000, description="Maximum number of documents to return"),
|
limit: int = Query(100, ge=1, le=1000, description="Maximum number of documents to return"),
|
||||||
UserInDB=Depends(get_current_user),
|
user: UserInDB = Depends(get_current_user),
|
||||||
document_service: DocumentService = Depends(get_document_service)
|
document_service: DocumentService = Depends(get_document_service)
|
||||||
) -> List[DocumentResponse]:
|
) -> List[DocumentResponse]:
|
||||||
"""
|
"""
|
||||||
@@ -163,7 +164,8 @@ def list_documents(
|
|||||||
@router.get("/objects/{file_hash}")
|
@router.get("/objects/{file_hash}")
|
||||||
async def get_object_by_hash(
|
async def get_object_by_hash(
|
||||||
file_hash: str = Path(..., description="SHA256 hash of the object to retrieve"),
|
file_hash: str = Path(..., description="SHA256 hash of the object to retrieve"),
|
||||||
document_service: DocumentService = Depends(get_document_service)
|
document_service: DocumentService = Depends(get_document_service),
|
||||||
|
user: UserInDB = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Serve object content by its hash.
|
Serve object content by its hash.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const Header = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="navbar bg-base-100 shadow-lg">
|
<div className="navbar bg-base-100">
|
||||||
<div className="navbar-start">
|
<div className="navbar-start">
|
||||||
<h1 className="text-xl font-bold">MyDocManager</h1>
|
<h1 className="text-xl font-bold">MyDocManager</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import {Outlet} from 'react-router-dom';
|
import {Outlet} from 'react-router-dom';
|
||||||
import Menu from "./Menu.jsx";
|
import Menu from "./Menu.jsx";
|
||||||
|
import styles from './Layout.module.css';
|
||||||
|
|
||||||
const Layout = () => {
|
const Layout = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-base-200">
|
<div className={styles.layoutContainer}>
|
||||||
<Header/>
|
<Header/>
|
||||||
<div className="flex">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
<aside className="w-64 min-h-screen bg-base-100 shadow-lg"><Menu/></aside>
|
<aside className={styles.sidebar}>
|
||||||
<main className="flex-1 container mx-auto px-4 py-8">
|
<Menu/>
|
||||||
<Outlet/>
|
</aside>
|
||||||
|
<main className={styles.mainContent}>
|
||||||
|
<div className={styles.mainContentInner}>
|
||||||
|
<Outlet/>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
30
src/frontend/src/components/common/Layout.module.css
Normal file
30
src/frontend/src/components/common/Layout.module.css
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/* Layout Container */
|
||||||
|
.layoutContainer {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--color-base-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
width: 16rem; /* 64px = 4rem, donc 256px = 16rem */
|
||||||
|
background-color: var(--color-base-100);
|
||||||
|
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content Area */
|
||||||
|
.mainContent {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content Inner Container */
|
||||||
|
.mainContentInner {
|
||||||
|
max-width: 80rem; /* container max-width */
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@
|
|||||||
* Supports different view modes: small, large, and detail
|
* Supports different view modes: small, large, and detail
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { memo } from 'react';
|
import React, {memo} from 'react';
|
||||||
|
import {API_BASE_URL} from "../../utils/api.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats file size to human-readable format
|
* Formats file size to human-readable format
|
||||||
@@ -32,6 +33,18 @@ const formatDate = (dateString) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds full URL from relative path
|
||||||
|
* @param {string} relativePath - Relative API path
|
||||||
|
* @returns {string} Full URL
|
||||||
|
*/
|
||||||
|
const buildFullUrl = (relativePath) => {
|
||||||
|
if (!relativePath) return '';
|
||||||
|
// Use the base URL from your API configuration
|
||||||
|
const baseUrl = import.meta.env.VITE_API_BASE_URL || API_BASE_URL;
|
||||||
|
return `${baseUrl}${relativePath}`;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DocumentCard component
|
* DocumentCard component
|
||||||
* @param {Object} props
|
* @param {Object} props
|
||||||
@@ -41,8 +54,8 @@ const formatDate = (dateString) => {
|
|||||||
* @param {function(): void} props.onDelete - Callback when delete is clicked
|
* @param {function(): void} props.onDelete - Callback when delete is clicked
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
*/
|
*/
|
||||||
const DocumentCard = memo(({ document, viewMode, onEdit, onDelete }) => {
|
const DocumentCard = memo(({document, viewMode, onEdit, onDelete}) => {
|
||||||
const { name, originalFileType, thumbnailUrl, pageCount, fileSize, createdAt, tags, categories } = document;
|
const {name, originalFileType, thumbnailUrl, pageCount, fileSize, createdAt, tags, categories} = document;
|
||||||
|
|
||||||
// Determine card classes based on view mode
|
// Determine card classes based on view mode
|
||||||
const getCardClasses = () => {
|
const getCardClasses = () => {
|
||||||
@@ -64,9 +77,9 @@ const DocumentCard = memo(({ document, viewMode, onEdit, onDelete }) => {
|
|||||||
const renderThumbnail = () => (
|
const renderThumbnail = () => (
|
||||||
<figure className="relative overflow-hidden">
|
<figure className="relative overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src={`http://localhost:8000${thumbnailUrl}`}
|
src={buildFullUrl(thumbnailUrl)}
|
||||||
alt={`${thumbnailUrl} thumbnail`}
|
alt={`${name}`}
|
||||||
className={`w-full object-cover ${
|
className={`w-[200px] object-cover ${
|
||||||
viewMode === 'small' ? 'h-32' : viewMode === 'large' ? 'h-48' : 'h-64'
|
viewMode === 'small' ? 'h-32' : viewMode === 'large' ? 'h-48' : 'h-64'
|
||||||
}`}
|
}`}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@@ -80,8 +93,10 @@ const DocumentCard = memo(({ document, viewMode, onEdit, onDelete }) => {
|
|||||||
aria-label="Edit document"
|
aria-label="Edit document"
|
||||||
title="Edit"
|
title="Edit"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||||
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -90,8 +105,10 @@ const DocumentCard = memo(({ document, viewMode, onEdit, onDelete }) => {
|
|||||||
aria-label="Delete document"
|
aria-label="Delete document"
|
||||||
title="Delete"
|
title="Delete"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const api = axios.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export { API_BASE_URL };
|
||||||
|
|
||||||
// Request interceptor to add authentication token
|
// Request interceptor to add authentication token
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user