first commit

This commit is contained in:
2025-11-10 00:14:49 +03:00
commit 6392533387
58 changed files with 7707 additions and 0 deletions

View 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>
);
}

View File

@@ -0,0 +1,7 @@
export default function HomePage() {
return (
<section className="page-heading">
<h1>Home</h1>
</section>
);
}

View 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>
);
}

View File

@@ -0,0 +1,7 @@
export default function ProfilePage() {
return (
<section className="page-heading">
<h1>Profile</h1>
</section>
);
}