Upload files to "program"
This commit is contained in:
157
program/Index.html
Normal file
157
program/Index.html
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Bibliotheks-Hub</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="auth-section" class="auth-modal hidden">
|
||||||
|
<div class="auth-container">
|
||||||
|
<div id="login-form" class="auth-form">
|
||||||
|
<div class="auth-box">
|
||||||
|
<h1>Bibliotheks-Hub</h1>
|
||||||
|
<p>Digitale Bibliotheksverwaltung</p>
|
||||||
|
<input type="text" id="login-username" placeholder="Benutzername" class="input-field">
|
||||||
|
<input type="password" id="login-password" placeholder="Passwort" class="input-field">
|
||||||
|
<button onclick="handleLogin()" class="btn-primary">Anmelden</button>
|
||||||
|
<p class="toggle-link">Neu hier? <a href="#" onclick="toggleAuthForm()">Konto erstellen</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="register-form" class="auth-form hidden">
|
||||||
|
<div class="auth-box">
|
||||||
|
<h1>Bibliotheks-Hub</h1>
|
||||||
|
<p>Erstelle dein Konto</p>
|
||||||
|
<input type="text" id="register-username" placeholder="Benutzername" class="input-field">
|
||||||
|
<input type="email" id="register-email" placeholder="E-Mail" class="input-field">
|
||||||
|
<input type="password" id="register-password" placeholder="Passwort" class="input-field">
|
||||||
|
<input type="text" id="register-fullname" placeholder="Vollständiger Name" class="input-field">
|
||||||
|
<button onclick="handleRegister()" class="btn-primary">Konto erstellen</button>
|
||||||
|
<p class="toggle-link">Bereits ein Konto? <a href="#" onclick="toggleAuthForm()">Anmelden</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main App -->
|
||||||
|
<div id="app-section">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h2>Library</h2>
|
||||||
|
</div>
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
|
||||||
|
<button onclick="switchTab('dashboard')" class="nav-item active" id="tab-dashboard">
|
||||||
|
Startseite
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick="switchTab('books')" class="nav-item" id="tab-books">
|
||||||
|
Durchsuchen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick="switchTab('my-books')" class="nav-item" id="tab-my-books">
|
||||||
|
Meine Bücher
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick="switchTab('history')" class="nav-item" id="tab-history">
|
||||||
|
Verlauf
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick="switchTab('admin')" class="nav-item" id="tab-admin" style="display:none;">
|
||||||
|
Verwaltung
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<p id="welcome-sidebar"></p>
|
||||||
|
<button onclick="logout()" class="btn-logout">Abmelden</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- Dashboard Tab -->
|
||||||
|
<div id="dashboard-tab" class="tab-content">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Neuigkeiten</h1>
|
||||||
|
</div>
|
||||||
|
<div id="news-container" class="news-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Browse Books Tab -->
|
||||||
|
<div id="books-tab" class="tab-content hidden">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Bücher durchsuchen</h1>
|
||||||
|
</div>
|
||||||
|
<div class="search-bar">
|
||||||
|
<input type="text" id="search-books" placeholder="Bücher suchen..." class="input-field" onkeyup="renderBooks()">
|
||||||
|
<select id="filter-status" class="select-field" onchange="renderBooks()">
|
||||||
|
<option value="all">Alle</option>
|
||||||
|
<option value="available">Verfügbar</option>
|
||||||
|
<option value="borrowed">Ausgeliehen</option>
|
||||||
|
</select>
|
||||||
|
<select id="filter-genre" class="select-field" onchange="renderBooks()">
|
||||||
|
<option value="">Alle Genres</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="book-list" class="books-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- My Books Tab -->
|
||||||
|
<div id="my-books-tab" class="tab-content hidden">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Meine ausgeliehenen Bücher</h1>
|
||||||
|
</div>
|
||||||
|
<div id="borrow-timer">
|
||||||
|
<div id="timer-text">Nächste Fälligkeit: --</div>
|
||||||
|
<div class="timer-bar">
|
||||||
|
<div id="timer-progress"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="my-books-list" class="books-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- History Tab -->
|
||||||
|
<div id="history-tab" class="tab-content hidden">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Ausleihverlauf</h1>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 15px;">
|
||||||
|
<select id="history-filter" class="select-field" onchange="renderHistory()">
|
||||||
|
<option value="all">Alle</option>
|
||||||
|
<option value="returned">Zurückgegeben</option>
|
||||||
|
<option value="borrowed">Aktuell ausgeliehen</option>
|
||||||
|
</select>
|
||||||
|
<button onclick="clearHistory()" class="btn-clear">Verlauf löschen</button>
|
||||||
|
</div>
|
||||||
|
<div id="history-list" class="history-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Admin Tab -->
|
||||||
|
<div id="admin-tab" class="tab-content hidden">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Admin-Bereich</h1>
|
||||||
|
</div>
|
||||||
|
<div class="admin-section">
|
||||||
|
<h2>Neues Buch hinzufügen</h2>
|
||||||
|
<div class="form-grid">
|
||||||
|
<input type="text" id="admin-title" placeholder="Titel" class="input-field">
|
||||||
|
<input type="text" id="admin-author" placeholder="Autor" class="input-field">
|
||||||
|
<input type="text" id="admin-genre" placeholder="Genre" class="input-field">
|
||||||
|
<input type="number" id="admin-quantity" placeholder="Anzahl" class="input-field" value="1">
|
||||||
|
<button onclick="addBookAdmin()" class="btn-primary">Buch hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 style="margin-top: 30px;">Alle Bücher</h2>
|
||||||
|
<div id="admin-book-list" class="books-container"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
670
program/Style.css
Normal file
670
program/Style.css
Normal file
@@ -0,0 +1,670 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auth Section Modal */
|
||||||
|
.auth-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-modal.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-box {
|
||||||
|
background: white;
|
||||||
|
padding: 50px 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-box h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-box p {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-link {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-link a {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-link a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main App Layout */
|
||||||
|
#app-section {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 250px;
|
||||||
|
background: white;
|
||||||
|
border-right: 1px solid #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: fixed;
|
||||||
|
height: 100vh;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 25px 20px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: #f0f0ff;
|
||||||
|
color: #667eea;
|
||||||
|
border-right: 3px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 20px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout:hover {
|
||||||
|
background: #fff0f0;
|
||||||
|
border-color: #ff6b6b;
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 250px;
|
||||||
|
padding: 40px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item.alert {
|
||||||
|
border-left-color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #667eea;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item.alert .stat-number {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* News Grid */
|
||||||
|
.news-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-icon {
|
||||||
|
font-size: 36px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-card h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-section {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-section h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-item {
|
||||||
|
padding: 12px 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
border-left: 3px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search & Filters */
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar .input-field {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-field {
|
||||||
|
padding: 12px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-field:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Books */
|
||||||
|
.books-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 18px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-card.unavailable {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-card.overdue {
|
||||||
|
border-left: 4px solid #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-card h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-card p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-info {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copies-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #f0f0f0;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.due-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.due-date.overdue {
|
||||||
|
color: #ff6b6b;
|
||||||
|
background: #fff0f0;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-actions {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-borrow, .btn-return {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-borrow {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-borrow:hover {
|
||||||
|
background: #5568d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-return {
|
||||||
|
background: #ff9500;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-return:hover {
|
||||||
|
background: #e68600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear:hover {
|
||||||
|
background: #ff4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-message {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* History */
|
||||||
|
.history-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-table-content {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-table-content th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-table-content td {
|
||||||
|
padding: 15px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-table-content tr:hover {
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin */
|
||||||
|
.admin-section {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-section h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid .btn-primary {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: static;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 3px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.books-container {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar .select-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.auth-box {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-box h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.books-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
278
program/script.js
Normal file
278
program/script.js
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
let user = null;
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
|
||||||
|
// DATA STORAGE
|
||||||
|
const db = {
|
||||||
|
getUsers: () => JSON.parse(localStorage.getItem('users')) || [{ username: 'admin', email: 'admin@library.com', password: 'admin123', fullname: 'Administrator', isAdmin: true }],
|
||||||
|
saveUsers: u => localStorage.setItem('users', JSON.stringify(u)),
|
||||||
|
getBooks: () => JSON.parse(localStorage.getItem('books')) || [
|
||||||
|
{ id: 1, title: 'To Kill a Mockingbird', author: 'Harper Lee', genre: 'Fiction', quantity: 3, borrowedBy: [] },
|
||||||
|
{ id: 2, title: '1984', author: 'George Orwell', genre: 'Dystopian', quantity: 2, borrowedBy: [] },
|
||||||
|
{ id: 3, title: 'Pride and Prejudice', author: 'Jane Austen', genre: 'Romance', quantity: 2, borrowedBy: [] },
|
||||||
|
{ id: 4, title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', genre: 'Classic', quantity: 3, borrowedBy: [] },
|
||||||
|
{ id: 5, title: 'The Hobbit', author: 'J. R. R. Tolkien', genre: 'Fantasy', quantity: 2, borrowedBy: [] },
|
||||||
|
{ id: 6, title: 'The Catcher in the Rye', author: 'J. D. Salinger', genre: 'Fiction', quantity: 2, borrowedBy: [] },
|
||||||
|
{ id: 7, title: 'The Alchemist', author: 'Paulo Coelho', genre: 'Adventure', quantity: 2, borrowedBy: [] },
|
||||||
|
{ id: 8, title: 'Harry Potter and the Sorcerer\'s Stone', author: 'J. K. Rowling', genre: 'Fantasy', quantity: 4, borrowedBy: [] },
|
||||||
|
{ id: 9, title: 'The Lord of the Rings', author: 'J. R. R. Tolkien', genre: 'Fantasy', quantity: 2, borrowedBy: [] },
|
||||||
|
{ id: 10, title: 'The Hunger Games', author: 'Suzanne Collins', genre: 'Dystopian', quantity: 3, borrowedBy: [] },
|
||||||
|
{ id: 11, title: 'The Book Thief', author: 'Markus Zusak', genre: 'Historical Fiction', quantity: 2, borrowedBy: [] },
|
||||||
|
{ id: 12, title: 'The Chronicles of Narnia', author: 'C. S. Lewis', genre: 'Fantasy', quantity: 2, borrowedBy: [] },
|
||||||
|
{ id: 13, title: 'Brave New World', author: 'Aldous Huxley', genre: 'Dystopian', quantity: 2, borrowedBy: [] },
|
||||||
|
{ id: 14, title: 'The Fault in Our Stars', author: 'John Green', genre: 'Romance', quantity: 3, borrowedBy: [] },
|
||||||
|
{ id: 15, title: 'The Da Vinci Code', author: 'Dan Brown', genre: 'Mystery', quantity: 2, borrowedBy: [] },
|
||||||
|
{ id: 16, title: 'The Shining', author: 'Stephen King', genre: 'Horror', quantity: 2, borrowedBy: [] },
|
||||||
|
{ id: 17, title: 'The Girl with the Dragon Tattoo', author: 'Stieg Larsson', genre: 'Crime', quantity: 2, borrowedBy: [] },
|
||||||
|
{ id: 18, title: 'Moby-Dick', author: 'Herman Melville', genre: 'Adventure', quantity: 1, borrowedBy: [] },
|
||||||
|
{ id: 19, title: 'The Road', author: 'Cormac McCarthy', genre: 'Post-Apocalyptic', quantity: 2, borrowedBy: [] },
|
||||||
|
{ id: 20, title: 'Jane Eyre', author: 'Charlotte Brontë', genre: 'Romance', quantity: 2, borrowedBy: [] }
|
||||||
|
],
|
||||||
|
saveBooks: b => localStorage.setItem('books', JSON.stringify(b)),
|
||||||
|
getHistory: () => JSON.parse(localStorage.getItem('history')) || [],
|
||||||
|
saveHistory: h => localStorage.setItem('history', JSON.stringify(h))
|
||||||
|
};
|
||||||
|
|
||||||
|
// HELPERS
|
||||||
|
const getUser = n => db.getUsers().find(u => u.username === n);
|
||||||
|
const addHistory = (id, title, action, details) => {
|
||||||
|
const h = db.getHistory();
|
||||||
|
h.push({ id: Date.now(), username: user.username, bookId: id, bookTitle: title, action, timestamp: new Date().toISOString(), ...details });
|
||||||
|
db.saveHistory(h);
|
||||||
|
};
|
||||||
|
const getBorrowed = b => b.borrowedBy.filter(x => !x.returnDate).length;
|
||||||
|
const getAvailable = b => b.quantity - getBorrowed(b);
|
||||||
|
const formatDate = d => new Date(d).toLocaleDateString();
|
||||||
|
const userRecord = (b, u) => b.borrowedBy.find(x => x.user === u && !x.returnDate);
|
||||||
|
|
||||||
|
// INIT
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Clear old data with ISBN and reset to new format with genres
|
||||||
|
const oldBooks = JSON.parse(localStorage.getItem('books') || '[]');
|
||||||
|
if (oldBooks.length > 0 && oldBooks[0].isbn) {
|
||||||
|
localStorage.removeItem('books');
|
||||||
|
localStorage.removeItem('history');
|
||||||
|
}
|
||||||
|
|
||||||
|
user = localStorage.getItem('currentUser') ? getUser(localStorage.getItem('currentUser')) : { username: 'guest', fullname: 'Gast', isAdmin: false };
|
||||||
|
$('welcome-sidebar').innerText = user.fullname;
|
||||||
|
if (user.isAdmin) $('tab-admin').style.display = 'flex';
|
||||||
|
switchTab('dashboard');
|
||||||
|
});
|
||||||
|
|
||||||
|
// AUTHEN
|
||||||
|
const handleRegister = () => {
|
||||||
|
const [u, e, p, f] = [$('register-username').value, $('register-email').value, $('register-password').value, $('register-fullname').value].map(x => x.trim());
|
||||||
|
if (!u || !e || !p || !f) return alert('Bitte alle Felder ausfüllen');
|
||||||
|
if (getUser(u)) return alert('Benutzer existiert bereits');
|
||||||
|
db.getUsers().push({ username: u, email: e, password: p, fullname: f, isAdmin: false, registeredDate: new Date().toISOString() });
|
||||||
|
db.saveUsers(db.getUsers());
|
||||||
|
alert('Erfolg! Du kannst dich jetzt anmelden.');
|
||||||
|
toggleAuthForm();
|
||||||
|
[$('register-username'), $('register-email'), $('register-password'), $('register-fullname')].forEach(x => x.value = '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
const [u, p] = [$('login-username').value.trim(), $('login-password').value.trim()];
|
||||||
|
if (!u || !p) return alert('Bitte Anmeldedaten eingeben');
|
||||||
|
const userData = getUser(u);
|
||||||
|
if (!userData || userData.password !== p) return alert('Ungültige Anmeldedaten');
|
||||||
|
user = userData;
|
||||||
|
localStorage.setItem('currentUser', u);
|
||||||
|
$('login-username').value = '';
|
||||||
|
$('login-password').value = '';
|
||||||
|
$('auth-section').classList.add('hidden');
|
||||||
|
$('welcome-sidebar').innerText = user.fullname;
|
||||||
|
if (user.isAdmin) $('tab-admin').style.display = 'flex';
|
||||||
|
switchTab('dashboard');
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
localStorage.removeItem('currentUser');
|
||||||
|
user = { username: 'guest', fullname: 'Gast', isAdmin: false };
|
||||||
|
$('welcome-sidebar').innerText = 'Gast';
|
||||||
|
$('tab-admin').style.display = 'none';
|
||||||
|
$('auth-section').classList.remove('hidden');
|
||||||
|
switchTab('dashboard');
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAuthForm = () => {
|
||||||
|
$('login-form').classList.toggle('hidden');
|
||||||
|
$('register-form').classList.toggle('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
// BOOKS
|
||||||
|
const borrowBook = bid => {
|
||||||
|
const books = db.getBooks();
|
||||||
|
const book = books.find(b => b.id === bid);
|
||||||
|
if (!book || userRecord(book, user.username) || getAvailable(book) <= 0) return alert('Kann nicht ausgeliehen werden');
|
||||||
|
const due = new Date();
|
||||||
|
due.setDate(due.getDate() + 14);
|
||||||
|
book.borrowedBy.push({ user: user.username, borrowDate: new Date().toISOString(), dueDate: due.toISOString(), returnDate: null });
|
||||||
|
db.saveBooks(books);
|
||||||
|
addHistory(book.id, book.title, 'BORROWED', { dueDate: due.toISOString() });
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
const returnBook = bid => {
|
||||||
|
const books = db.getBooks();
|
||||||
|
const book = books.find(b => b.id === bid);
|
||||||
|
const record = userRecord(book, user.username);
|
||||||
|
if (!record) return alert('Nicht ausgeliehen');
|
||||||
|
record.returnDate = new Date().toISOString();
|
||||||
|
db.saveBooks(books);
|
||||||
|
addHistory(book.id, book.title, 'RETURNED');
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
const addBookAdmin = () => {
|
||||||
|
const [t, a, g, q] = [$('admin-title').value.trim(), $('admin-author').value.trim(), $('admin-genre').value.trim(), parseInt($('admin-quantity').value) || 1];
|
||||||
|
if (!t || !a || !g) return alert('Bitte alle Felder ausfüllen');
|
||||||
|
const books = db.getBooks();
|
||||||
|
const existing = books.find(b => b.title === t);
|
||||||
|
if (existing) existing.quantity += q;
|
||||||
|
else books.push({ id: Date.now(), title: t, author: a, genre: g, quantity: q, borrowedBy: [] });
|
||||||
|
db.saveBooks(books);
|
||||||
|
[$('admin-title'), $('admin-author'), $('admin-genre'), $('admin-quantity')].forEach(x => x.value = x === $('admin-quantity') ? '1' : '');
|
||||||
|
renderAdminBooks();
|
||||||
|
};
|
||||||
|
|
||||||
|
// UI
|
||||||
|
const switchTab = t => {
|
||||||
|
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
||||||
|
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
||||||
|
$(t + '-tab').classList.remove('hidden');
|
||||||
|
$('tab-' + t).classList.add('active');
|
||||||
|
if (t === 'books') populateGenreFilter();
|
||||||
|
({ books: renderBooks, 'my-books': renderMyBooks, history: renderHistory, admin: renderAdminBooks, dashboard: updateDashboard }[t] || (() => {}))();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDashboard = () => {
|
||||||
|
const newsItems = [
|
||||||
|
{ id: 1, title: 'Neuerscheinung: Die Mitternachtsbibliothek', subtitle: 'Matt Haig', desc: 'Eine fesselnde moderne Fantasy über die Entscheidungen, die wir nie getroffen haben. Jetzt verfügbar!' },
|
||||||
|
{ id: 2, title: 'Autogrammstunde', subtitle: 'Brandon Sanderson Tour 2024', desc: 'Nimm teil an einer exklusiven Signierstunde mit Bestsellerautor Brandon Sanderson am 15. März um 18 Uhr.' },
|
||||||
|
{ id: 3, title: 'Neuerscheinung', subtitle: 'Project Hail Mary - Andy Weir', desc: 'Erscheint am 28. Februar! Jetzt vorbestellen für dieses epische Sci-Fi-Abenteuer.' },
|
||||||
|
{ id: 4, title: 'Neu im Bestand: Fourth Wing', subtitle: 'Rebecca Yarros', desc: 'Drachenreiter, Magie und epische Romantik. Diese Fantasy-Sensation jetzt in unserer Sammlung!' },
|
||||||
|
{ id: 5, title: 'Autoren-Spotlight', subtitle: 'Colleen Hoover Kollektion', desc: 'Wir haben unseren Romance-Bereich um die vollständigen Werke von Colleen Hoover erweitert.' },
|
||||||
|
{ id: 6, title: 'Lese-Challenge 2024', subtitle: 'Mach mit in der Community', desc: 'Lies 12 Bücher in diesem Jahr! Gewinne exklusive Lesezeichen und Bibliotheks-Preise.' },
|
||||||
|
{ id: 7, title: 'Valentinstag-Special', subtitle: 'Romantische Bücher', desc: 'Entdecke unsere Auswahl an Liebesromanen und erhalte 20% Rabatt, wenn du 3 oder mehr ausleihst!' },
|
||||||
|
{ id: 8, title: 'Beliebt jetzt', subtitle: 'Verity von Colleen Hoover', desc: 'Der meistgelesene Thriller, den Leser nicht aus der Hand legen können. Nur begrenzt verfügbar!' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const persistedIds = JSON.parse(localStorage.getItem('newsItemsOrder') || 'null');
|
||||||
|
const selectedIds = Array.isArray(persistedIds) && persistedIds.length ? persistedIds : newsItems.slice(0, 6).map(i => i.id);
|
||||||
|
|
||||||
|
if (!persistedIds) {
|
||||||
|
localStorage.setItem('newsItemsOrder', JSON.stringify(selectedIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
const news = selectedIds.map(id => newsItems.find(i => i.id === id)).filter(Boolean);
|
||||||
|
$('news-container').innerHTML = news.map(n => `<div class="news-card"><h3>${n.title}</h3><p class="news-subtitle">${n.subtitle}</p><p class="news-desc">${n.desc}</p></div>`).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBooks = () => {
|
||||||
|
const books = db.getBooks();
|
||||||
|
const [s, st, g] = [$('search-books').value.toLowerCase(), $('filter-status').value, $('filter-genre').value];
|
||||||
|
const filtered = books.filter(b => (b.title.toLowerCase().includes(s) || b.author.toLowerCase().includes(s)) && (!g || b.genre === g) && (st === 'all' || (st === 'available' && getAvailable(b) > 0) || (st === 'borrowed' && getAvailable(b) === 0)));
|
||||||
|
$('book-list').innerHTML = filtered.map(b => {
|
||||||
|
const avail = getAvailable(b);
|
||||||
|
const rec = userRecord(b, user.username);
|
||||||
|
return `<div class="book-card ${avail > 0 ? 'available' : 'unavailable'}">
|
||||||
|
<h3>${b.title}</h3><p><strong>Autor:</strong> ${b.author}</p>
|
||||||
|
<p><strong>Genre:</strong> ${b.genre}</p><p class="copies-badge">Verfügbar: ${avail}/${b.quantity}</p>
|
||||||
|
<div class="book-actions">${rec ? `<p class="due-date ${new Date(rec.dueDate) < new Date() ? 'overdue' : ''}">Fällig: ${formatDate(rec.dueDate)}</p><button onclick="returnBook(${b.id})" class="btn-return">Zurückgeben</button>` : (avail > 0 ? `<button onclick="borrowBook(${b.id})" class="btn-borrow">Ausleihen</button>` : '')}</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMyBooks = () => {
|
||||||
|
const books = db.getBooks().filter(b => userRecord(b, user.username)).map(b => ({ ...b, record: userRecord(b, user.username) }));
|
||||||
|
$('my-books-list').innerHTML = books.length === 0 ? '<p class="empty-message">Keine ausgeliehenen Bücher</p>' : books.map(b => {
|
||||||
|
const overdue = new Date(b.record.dueDate) < new Date();
|
||||||
|
return `<div class="book-card ${overdue ? 'overdue' : 'ontime'}">
|
||||||
|
<h3>${b.title}</h3><p><strong>Autor:</strong> ${b.author}</p>
|
||||||
|
<p><strong>Ausgeliehen:</strong> ${formatDate(b.record.borrowDate)}</p>
|
||||||
|
<p class="due-date ${overdue ? 'overdue' : ''}">Fällig: ${formatDate(b.record.dueDate)}</p>
|
||||||
|
${overdue ? '<p style="background:red;color:white;padding:8px;border-radius:4px;font-weight:600;">ÜBERFÄLLIG</p>' : ''}
|
||||||
|
<button onclick="returnBook(${b.id})" class="btn-return">Zurückgeben</button>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderHistory = () => {
|
||||||
|
const hist = db.getHistory().filter(h => h.username === user.username);
|
||||||
|
const filtered = $('history-filter').value === 'all' ? hist : hist.filter(h => h.action === $('history-filter').value.toUpperCase());
|
||||||
|
$('history-list').innerHTML = filtered.length === 0 ? '<p class="empty-message">Kein Verlauf</p>' :
|
||||||
|
`<table class="history-table-content"><tr><th>Datum</th><th>Titel</th><th>Aktion</th><th>Fällig</th></tr>${filtered.reverse().map(e => `<tr><td>${formatDate(e.timestamp)}</td><td>${e.bookTitle}</td><td>${e.action === 'BORROWED' ? 'Ausgeliehen' : 'Zurückgegeben'}</td><td>${e.dueDate ? formatDate(e.dueDate) : '-'}</td></tr>`).join('')}</table>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearHistory = () => {
|
||||||
|
if (!confirm('Verlauf löschen? Dies kann nicht rückgängig gemacht werden.')) return;
|
||||||
|
const updated = db.getHistory().filter(h => h.username !== user.username);
|
||||||
|
db.saveHistory(updated);
|
||||||
|
renderHistory();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAdminBooks = () => {
|
||||||
|
const books = db.getBooks();
|
||||||
|
$('admin-book-list').innerHTML = books.map(b => {
|
||||||
|
const borrowed = getBorrowed(b);
|
||||||
|
const details = b.borrowedBy.filter(x => !x.returnDate).map(x => `<p>${x.user} (${formatDate(x.dueDate)}) ${new Date(x.dueDate) < new Date() ? 'ÜBERFÄLLIG' : ''}</p>`).join('');
|
||||||
|
return `<div class="book-card admin-card">
|
||||||
|
<h3>${b.title}</h3><p><strong>Autor:</strong> ${b.author}</p>
|
||||||
|
<p><strong>Genre:</strong> ${b.genre}</p><p><strong>Gesamt:</strong> ${b.quantity} | <strong>Verfügbar:</strong> ${getAvailable(b)} | <strong>Ausgeliehen:</strong> ${borrowed}</p>
|
||||||
|
${details ? `<div class="borrow-details">${details}</div>` : ''}
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const populateGenreFilter = () => {
|
||||||
|
const genres = [...new Set(db.getBooks().map(b => b.genre))].sort();
|
||||||
|
$('filter-genre').innerHTML = '<option value="">Alle Genres</option>' + genres.map(g => `<option value="${g}">${g}</option>`).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
renderBooks();
|
||||||
|
renderMyBooks();
|
||||||
|
updateDashboard();
|
||||||
|
updateFloatingTimer();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFloatingTimer = () => {
|
||||||
|
const books = db.getBooks();
|
||||||
|
let soonest = null;
|
||||||
|
|
||||||
|
books.forEach(b => {
|
||||||
|
const rec = userRecord(b, user.username);
|
||||||
|
if (rec) {
|
||||||
|
const due = new Date(rec.dueDate).getTime();
|
||||||
|
if (!soonest || due < soonest.due) {
|
||||||
|
soonest = {
|
||||||
|
due: due,
|
||||||
|
start: new Date(rec.borrowDate).getTime(),
|
||||||
|
title: b.title
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const timerText = document.getElementById('timer-text');
|
||||||
|
const timerProgress = document.getElementById('timer-progress');
|
||||||
|
if (!timerText || !timerProgress) return;
|
||||||
|
|
||||||
|
if (!soonest) {
|
||||||
|
timerText.innerText = 'Keine ausgeliehenen Bücher';
|
||||||
|
timerProgress.style.width = '0%';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const percent = ((now - soonest.start) / (soonest.due - soonest.start)) * 100;
|
||||||
|
const remaining = soonest.due - now;
|
||||||
|
const days = Math.ceil(remaining / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
timerText.innerText = `${soonest.title} fällig in ${days} Tagen`;
|
||||||
|
timerProgress.style.width = Math.min(100, Math.max(0, percent)) + '%';
|
||||||
|
};
|
||||||
|
|
||||||
|
setInterval(updateFloatingTimer, 60000);
|
||||||
|
updateFloatingTimer();
|
||||||
Reference in New Issue
Block a user