first commit
This commit is contained in:
210
frontend/src/pages/AddBooksPage.jsx
Normal file
210
frontend/src/pages/AddBooksPage.jsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faBarcode } from '@fortawesome/free-solid-svg-icons';
|
||||
import BarcodeScannerComponent from 'react-qr-barcode-scanner';
|
||||
import { motion } from 'framer-motion';
|
||||
import BookCard from '../components/BookCard.jsx';
|
||||
import { useSavedBooks } from '../context/SavedBooksContext.jsx';
|
||||
|
||||
const resolveApiBaseUrl = () => {
|
||||
if (import.meta.env.VITE_API_BASE_URL) return import.meta.env.VITE_API_BASE_URL;
|
||||
if (typeof window !== 'undefined') {
|
||||
const { protocol, hostname } = window.location;
|
||||
return `${protocol}//${hostname}:8080`;
|
||||
}
|
||||
return 'http://localhost:8080';
|
||||
};
|
||||
|
||||
const API_BASE_URL = resolveApiBaseUrl();
|
||||
const CAMERA_CONSTRAINTS = { facingMode: 'environment' };
|
||||
|
||||
const parseSearchInput = (raw) => {
|
||||
const fragments = raw.split('.').map((piece) => piece.trim()).filter(Boolean);
|
||||
if (fragments.length <= 1) {
|
||||
return { title: raw.trim(), published: undefined };
|
||||
}
|
||||
const maybeYear = fragments.at(-1);
|
||||
if (/^\d{4}$/.test(maybeYear)) {
|
||||
const title = fragments.slice(0, -1).join(' ');
|
||||
return { title, published: maybeYear };
|
||||
}
|
||||
return { title: raw.trim(), published: undefined };
|
||||
};
|
||||
|
||||
const normalizeBook = (item) => {
|
||||
const summary = item.summary || {};
|
||||
return {
|
||||
title: item.title || summary.title,
|
||||
authorName: item.authorName || summary.author,
|
||||
thumbImage: item.thumbImage || summary.image,
|
||||
page: item.page,
|
||||
rate: item.rate,
|
||||
publisher: item.publisher,
|
||||
description: item.description,
|
||||
isbn: item.isbn || summary.asin || summary.isbn,
|
||||
raw: item
|
||||
};
|
||||
};
|
||||
|
||||
const flattenBookResults = (payload) => {
|
||||
if (!payload?.data) return [];
|
||||
if (Array.isArray(payload.data)) {
|
||||
return payload.data.flatMap((entry) => (entry.items || []).map(normalizeBook));
|
||||
}
|
||||
if (typeof payload.data === 'object') {
|
||||
return Object.values(payload.data)
|
||||
.filter(Boolean)
|
||||
.map(normalizeBook);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export default function AddBooksPage() {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [books, setBooks] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const { addBook } = useSavedBooks();
|
||||
const [scannerOpen, setScannerOpen] = useState(false);
|
||||
const [scanMessage, setScanMessage] = useState('');
|
||||
const [scannedIsbn, setScannedIsbn] = useState('');
|
||||
const [toast, setToast] = useState(false);
|
||||
|
||||
const hasResults = books.length > 0;
|
||||
|
||||
const handleSearch = async (event) => {
|
||||
event.preventDefault();
|
||||
const trimmed = searchTerm.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
const { title, published } = parseSearchInput(trimmed);
|
||||
const endpoint = published
|
||||
? `/api/books/filter?title=${encodeURIComponent(title)}&published=${published}`
|
||||
: `/api/books/title?title=${encodeURIComponent(title)}`;
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`);
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Arama basarisiz');
|
||||
}
|
||||
const flattened = flattenBookResults(data);
|
||||
setBooks(flattened);
|
||||
} catch (err) {
|
||||
setBooks([]);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBarcodeUpdate = async (_, result) => {
|
||||
if (!result?.text) return;
|
||||
const cleaned = result.text.replace(/[^0-9Xx]/g, '').toUpperCase();
|
||||
if (!cleaned || cleaned === scannedIsbn) return;
|
||||
setScannedIsbn(cleaned);
|
||||
setScanMessage('ISBN okundu, kitap getiriliyor...');
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/books/isbn/${cleaned}?locales=en`);
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Kitap bulunamadi');
|
||||
}
|
||||
const flattened = flattenBookResults(data);
|
||||
setBooks(flattened);
|
||||
setScanMessage('Kitap bulundu.');
|
||||
setScannerOpen(false);
|
||||
} catch (err) {
|
||||
setScanMessage(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const viewport = typeof window !== 'undefined'
|
||||
? { width: window.innerWidth, height: window.innerHeight }
|
||||
: { width: 320, height: 480 };
|
||||
|
||||
const scannerOverlay = useMemo(() => (
|
||||
scannerOpen && (
|
||||
<div className="scanner-overlay">
|
||||
<div className="scanner-fullscreen">
|
||||
<BarcodeScannerComponent
|
||||
width={viewport.width}
|
||||
height={viewport.height}
|
||||
videoConstraints={CAMERA_CONSTRAINTS}
|
||||
onUpdate={handleBarcodeUpdate}
|
||||
/>
|
||||
<motion.div
|
||||
className="scanner-overlay-line"
|
||||
initial={{ top: '15%' }}
|
||||
animate={{ top: ['15%', '85%'] }}
|
||||
transition={{ repeat: Infinity, repeatType: 'mirror', duration: 2, ease: 'linear' }}
|
||||
/>
|
||||
<div className="scanner-overlay-info">
|
||||
<p>{scanMessage || 'Barkodu hizala'}</p>
|
||||
<button type="button" onClick={() => setScannerOpen(false)}>
|
||||
Kapat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
), [scannerOpen, scanMessage]);
|
||||
|
||||
return (
|
||||
<section className="add-books-page">
|
||||
<form className="search-form" onSubmit={handleSearch}>
|
||||
<div className="search-field">
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(event) => setSearchTerm(event.target.value)}
|
||||
placeholder="Kitap adi veya 'Adi .2020'"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Barkod ile tara"
|
||||
onClick={() => {
|
||||
setScanMessage('Barkodu hizala');
|
||||
setScannerOpen(true);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBarcode} />
|
||||
</button>
|
||||
</div>
|
||||
<button type="submit" className="primary-btn">
|
||||
Ara
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{!hasResults && !loading && !error && (
|
||||
<p className="ghost-copy">Search Books</p>
|
||||
)}
|
||||
|
||||
{loading && <p className="status-text loading">Araniyor...</p>}
|
||||
{error && <p className="status-text error">{error}</p>}
|
||||
|
||||
<div className="book-list">
|
||||
{hasResults && books.map((book, index) => (
|
||||
<BookCard
|
||||
key={`${book.title}-${index}`}
|
||||
book={book}
|
||||
onSelect={(item) => {
|
||||
addBook(item);
|
||||
setToast(true);
|
||||
setTimeout(() => setToast(false), 1000);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!hasResults && !loading && !error && (
|
||||
<p className="status-text info">Henüz bir arama yapilmadi.</p>
|
||||
)}
|
||||
|
||||
{toast && <div className="save-toast">Saved!</div>}
|
||||
{scannerOverlay}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
7
frontend/src/pages/HomePage.jsx
Normal file
7
frontend/src/pages/HomePage.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<section className="page-heading">
|
||||
<h1>Home</h1>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
60
frontend/src/pages/MyBooksPage.jsx
Normal file
60
frontend/src/pages/MyBooksPage.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faChevronLeft, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import ImageGlow from 'react-image-glow';
|
||||
import BookCard from '../components/BookCard.jsx';
|
||||
import { useSavedBooks } from '../context/SavedBooksContext.jsx';
|
||||
|
||||
export default function MyBooksPage() {
|
||||
const { savedBooks, removeBook } = useSavedBooks();
|
||||
const [selected, setSelected] = useState(null);
|
||||
|
||||
const handleRemove = (title) => {
|
||||
removeBook(title);
|
||||
setSelected(null);
|
||||
};
|
||||
|
||||
if (selected) {
|
||||
return (
|
||||
<section className="mybooks-detail">
|
||||
<button type="button" className="back-btn" onClick={() => setSelected(null)}>
|
||||
<FontAwesomeIcon icon={faChevronLeft} /> Back
|
||||
</button>
|
||||
|
||||
<div className="detail-hero">
|
||||
{selected.thumbImage ? (
|
||||
<div className="detail-hero-glow">
|
||||
<ImageGlow radius={60} saturation={1.4} opacity={0.9}>
|
||||
<img src={selected.thumbImage} alt={selected.title} className="detail-hero-img" />
|
||||
</ImageGlow>
|
||||
</div>
|
||||
) : (
|
||||
<div className="placeholder-thumb">NO COVER</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="detail-body">
|
||||
<h1>{selected.title}</h1>
|
||||
<p className="book-meta">{selected.authorName || 'Unknown author'}</p>
|
||||
<p className="book-meta">
|
||||
{selected.page ? `${selected.page} pages` : 'Page count N/A'} · {selected.rate || '-'} ★
|
||||
</p>
|
||||
<p className="book-meta">{selected.publisher || 'Publisher unknown'}</p>
|
||||
<p className="book-description">{selected.description || 'Description not available.'}</p>
|
||||
<button type="button" className="secondary-btn" onClick={() => handleRemove(selected.title)}>
|
||||
<FontAwesomeIcon icon={faTrash} /> Remove from My Books
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="mybooks-list">
|
||||
{savedBooks.length === 0 && <p className="status-text info">Henüz kayitli kitap yok.</p>}
|
||||
{savedBooks.map((book, index) => (
|
||||
<BookCard key={`${book.title}-${index}`} book={book} onSelect={setSelected} />
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
7
frontend/src/pages/ProfilePage.jsx
Normal file
7
frontend/src/pages/ProfilePage.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function ProfilePage() {
|
||||
return (
|
||||
<section className="page-heading">
|
||||
<h1>Profile</h1>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user