Refactor the library page to Navbar, Spinner and Bookshelf

This commit is contained in:
chrox
2024-10-03 18:20:13 +02:00
parent 666d8b6f8b
commit d0487f83b0
7 changed files with 152 additions and 108 deletions
+1 -4
View File
@@ -16,10 +16,7 @@ export default function RootLayout({
return (
<html lang='en'>
<head>
<meta
name='viewport'
content='width=device-width, height=device-height, initial-scale=1.0'
/>
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1' />
</head>
<body>
<EnvProvider>
+13 -97
View File
@@ -2,13 +2,14 @@
import * as React from 'react';
import { useState } from 'react';
import Image from 'next/image';
import { Book, BooksGroup } from '../types/book';
import { Book, BooksGroup, LibraryItem } from '../types/book';
import { useEnv } from '../context/EnvContext';
import { FaSearch, FaPlus } from 'react-icons/fa';
import Navbar from '@/components/Navbar';
import Spinner from '@/components/Spinner';
import Bookshelf from '@/components/Bookshelf';
type AppState = 'Init' | 'Loading' | 'Library' | 'Reader';
type LibraryItem = Book | BooksGroup;
const generateLibraryItems = (groups: BooksGroup[]): LibraryItem[] => {
const ungroupedBooks: Book[] = groups.find((group) => group.id === 'ungrouped')?.books || [];
@@ -20,12 +21,12 @@ const LibraryPage = () => {
const { envConfig } = useEnv();
const [appState, setAppState] = useState<AppState>('Init');
const [libraryItems, setLibraryItems] = useState<LibraryItem[]>([]);
const [loadingInfo, setLoadingInfo] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
React.useEffect(() => {
if (appState !== 'Init') return;
setAppState('Loading');
setLoadingInfo('Loading library books...');
setLoading(true);
envConfig.appService().then((appService) => {
appService.loadSettings().then((settings) => {
console.log('Settings', settings);
@@ -36,12 +37,12 @@ const LibraryPage = () => {
console.log('Library items:', libraryItems);
setLibraryItems(libraryItems);
setAppState('Library');
setLoadingInfo('');
setLoading(false);
})
.catch((err) => {
console.error(err);
setLoadingInfo('');
appService.showMessage('Failed to load library books', 'error');
setLoading(false);
appService.showMessage(`Failed to load library books: ${err}`, 'error');
});
});
});
@@ -54,96 +55,11 @@ const LibraryPage = () => {
return (
<div className='min-h-screen bg-gray-100'>
{/* Sticky Top Navbar */}
<div className='fixed top-0 z-10 w-full bg-gray-100 p-6'>
<div className='flex items-center justify-between'>
{/* Search Input with Magnifier and Plus Icon */}
<div className='sm:w relative flex w-full items-center'>
{/* Magnifier Icon */}
<span className='absolute left-4 text-gray-500'>
<FaSearch className='w-3' />
</span>
{/* Search Input */}
<input
type='text'
placeholder='Search books...'
className='input input-sm rounded-badge w-full bg-gray-200 pl-10 pr-10 focus:border-none focus:outline-none' // Padding-right added for FaPlus
/>
{/* Plus Icon */}
<span className='dropdown dropdown-end absolute right-4 cursor-pointer text-gray-500'>
<FaPlus tabIndex={0} className='w-3' role='button' />
<ul
tabIndex={0}
className='dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow'
>
<li>
<button onClick={handleImport}>From Local File</button>
</li>
</ul>
</span>
</div>
</div>
</div>
<Navbar onImport={handleImport} />
<div className='min-h-screen p-2 pt-16'>
<div className='hero-content'>
{loadingInfo && (
<div className='absolute left-1/2 top-4 -translate-x-1/2 transform pt-16 text-center'>
{/* Spacer to offset fixed navbar height */}
<span className='loading loading-dots loading-lg'></span>
</div>
)}
{/* Books Grid */}
<div className='grid grid-cols-3 gap-6 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8'>
{libraryItems.map((item, index) => (
<div key={`library-item-${index}`} className=''>
<div className='grid gap-2'>
{'format' in item ? (
<div>
<div key={(item as Book).id} className='card bg-base-100 w-full shadow-md'>
<Image
width={10}
height={10}
src={(item as Book).coverImageUrl!}
alt={(item as Book).title}
className='aspect-[28/41] w-full object-cover'
/>
</div>
<div className='card-body p-0 pt-2'>
<h3 className='card-title text-sm'>{(item as Book).title}</h3>
</div>
</div>
) : (
<div>
{(item as BooksGroup).books.map((book) => (
<div key={book.id} className='card bg-base-100 w-full shadow-md'>
<figure>
<Image
width={10}
height={10}
src={book.coverImageUrl!}
alt={book.title}
className='h-48 w-full object-cover'
/>
</figure>
</div>
))}
<h2 className='mb-2 text-lg font-bold'>{(item as BooksGroup).name}</h2>
</div>
)}
</div>
</div>
))}
{libraryItems.length > 0 && (
<div
className='border-1 flex aspect-[28/41] items-center justify-center bg-white'
role='button'
onClick={handleImport}
>
<FaPlus size='2rem' color='gray' />
</div>
)}
</div>
<Spinner loading={loading} />
<Bookshelf libraryItems={libraryItems} onImport={handleImport} />
</div>
</div>
</div>
@@ -0,0 +1,70 @@
import * as React from 'react';
import Image from 'next/image';
import { Book, BooksGroup, LibraryItem } from '../types/book';
import { FaPlus } from 'react-icons/fa';
interface BookshelfProps {
libraryItems: LibraryItem[];
onImport: () => void;
}
const Bookshelf: React.FC<BookshelfProps> = ({ libraryItems, onImport }) => {
return (
<div>
{/* Books Grid */}
<div className='grid grid-cols-3 gap-6 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8'>
{libraryItems.map((item, index) => (
<div key={`library-item-${index}`} className=''>
<div className='grid gap-2'>
{'format' in item ? (
<div>
<div key={(item as Book).id} className='card bg-base-100 w-full shadow-md'>
<Image
width={10}
height={10}
src={(item as Book).coverImageUrl!}
alt={(item as Book).title}
className='aspect-[28/41] w-full object-cover'
/>
</div>
<div className='card-body p-0 pt-2'>
<h3 className='card-title text-sm'>{(item as Book).title}</h3>
</div>
</div>
) : (
<div>
{(item as BooksGroup).books.map((book) => (
<div key={book.id} className='card bg-base-100 w-full shadow-md'>
<figure>
<Image
width={10}
height={10}
src={book.coverImageUrl!}
alt={book.title}
className='h-48 w-full object-cover'
/>
</figure>
</div>
))}
<h2 className='mb-2 text-lg font-bold'>{(item as BooksGroup).name}</h2>
</div>
)}
</div>
</div>
))}
{libraryItems.length > 0 && (
<div
className='border-1 flex aspect-[28/41] items-center justify-center bg-white'
role='button'
onClick={onImport}
>
<FaPlus className='size-8' color='gray' />
</div>
)}
</div>
</div>
);
};
export default Bookshelf;
@@ -0,0 +1,42 @@
import * as React from 'react';
import { FaSearch, FaPlus } from 'react-icons/fa';
interface NavbarProps {
onImport: () => void;
}
const Navbar: React.FC<NavbarProps> = ({ onImport }) => {
return (
<div className='fixed top-0 z-10 w-full bg-gray-100 p-6'>
<div className='flex items-center justify-between'>
{/* Search Input with Magnifier and Plus Icon */}
<div className='sm:w relative flex w-full items-center'>
{/* Magnifier Icon */}
<span className='absolute left-4 text-gray-500'>
<FaSearch className='w-3' />
</span>
{/* Search Input */}
<input
type='text'
placeholder='Search books...'
className='input input-sm rounded-badge w-full bg-gray-200 pl-10 pr-10 text-base focus:border-none focus:outline-none'
/>
{/* Plus Icon */}
<span className='dropdown dropdown-end absolute right-4 cursor-pointer text-gray-500'>
<FaPlus tabIndex={0} className='w-3' role='button' />
<ul
tabIndex={0}
className='dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow'
>
<li>
<button onClick={onImport}>From Local File</button>
</li>
</ul>
</span>
</div>
</div>
</div>
);
};
export default Navbar;
@@ -0,0 +1,21 @@
import React from 'react';
interface SpinnerProps {
loading: boolean;
}
const Spinner: React.FC<SpinnerProps> = ({ loading }) => {
if (!loading) return null;
return (
<div
className='absolute left-1/2 top-4 -translate-x-1/2 transform pt-16 text-center'
role='status'
>
<span className='loading loading-dots loading-lg'></span>
<span className='hidden'>Loading...</span>
</div>
);
};
export default Spinner;
@@ -9,7 +9,6 @@ import {
remove,
BaseDirectory,
} from '@tauri-apps/plugin-fs';
import { type as osType } from '@tauri-apps/plugin-os';
import {
join,
appConfigDir,
@@ -25,7 +24,6 @@ import { Book, BooksGroup } from '../types/book';
import { SystemSettings } from '../types/settings';
import { AppService, BaseDir, ToastType } from '../types/system';
const IS_MOBILE = osType() === 'ios' || osType() === 'android';
const BOOKS_SUBDIR = 'DigestLibrary/Books';
let BOOKS_DIR = '';
@@ -53,7 +51,7 @@ function resolvePath(
return { baseDir: BaseDirectory.AppLog, fp, base, dir: appLogDir };
case 'Books':
return {
baseDir: IS_MOBILE ? BaseDirectory.AppData : BaseDirectory.Document,
baseDir: BaseDirectory.Document,
fp: `${BOOKS_SUBDIR}/${fp}`,
base,
dir: () => new Promise((r) => r(`${BOOKS_DIR}/`)),
@@ -132,10 +130,8 @@ export const nativeAppService: AppService = {
const txt = await nativeAppService.fs.readFile(fp, base, 'text');
settings = JSON.parse(txt as string);
} catch {
const INIT_BOOKS_DIR = await join(
IS_MOBILE ? await appDataDir() : await documentDir(),
BOOKS_SUBDIR,
);
const INIT_BOOKS_DIR = await join(await documentDir(), BOOKS_SUBDIR);
await nativeAppService.fs.createDir('', 'Books', true);
settings = {
localBooksDir: INIT_BOOKS_DIR,
globalReadSettings: {
+2
View File
@@ -36,3 +36,5 @@ export interface BooksGroup {
books: Book[];
lastUpdated: number;
}
export type LibraryItem = Book | BooksGroup;