mirror of
https://github.com/Alvin-Zilverstand/Challenge_15_Magazijn_App_Maken.git
synced 2026-03-06 11:06:34 +01:00
Implement multilingual support with Dutch and English translations across the application
This commit is contained in:
@@ -2,13 +2,25 @@ const mongoose = require('mongoose');
|
|||||||
|
|
||||||
const itemSchema = new mongoose.Schema({
|
const itemSchema = new mongoose.Schema({
|
||||||
name: {
|
name: {
|
||||||
|
en: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
nl: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
description: {
|
description: {
|
||||||
|
en: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
nl: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
location: {
|
location: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ['Heerlen', 'Maastricht', 'Sittard'],
|
enum: ['Heerlen', 'Maastricht', 'Sittard'],
|
||||||
|
|||||||
@@ -476,3 +476,82 @@ body {
|
|||||||
border-color: var(--vista-coral);
|
border-color: var(--vista-coral);
|
||||||
color: var(--vista-white);
|
color: var(--vista-white);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Language Toggle Styles */
|
||||||
|
.language-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 60px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: var(--vista-coral);
|
||||||
|
transition: .4s;
|
||||||
|
border-radius: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white;
|
||||||
|
transition: .4s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .language-slider {
|
||||||
|
background-color: var(--vista-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .language-slider:before {
|
||||||
|
transform: translateX(32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-label {
|
||||||
|
color: var(--vista-white);
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-label.active {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Language toggle for login page */
|
||||||
|
.language-toggle-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-toggle-container .language-toggle {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-toggle-container .language-label {
|
||||||
|
color: var(--vista-blue) !important;
|
||||||
|
}
|
||||||
@@ -12,23 +12,33 @@
|
|||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
<div class="language-toggle-container text-center mb-3">
|
||||||
|
<div class="language-toggle">
|
||||||
|
<span class="language-label text-dark" id="nlLabel">NL</span>
|
||||||
|
<label class="language-switch">
|
||||||
|
<input type="checkbox" id="languageToggle">
|
||||||
|
<span class="language-slider"></span>
|
||||||
|
</label>
|
||||||
|
<span class="language-label text-dark" id="enLabel">EN</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="text-center">Inloggen</h2>
|
<h2 class="text-center" data-translate="login">Inloggen</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="loginForm">
|
<form id="loginForm">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label">Gebruikersnaam</label>
|
<label for="username" class="form-label" data-translate="username">Gebruikersnaam</label>
|
||||||
<input type="text" class="form-control" id="username" required>
|
<input type="text" class="form-control" id="username" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password" class="form-label">Wachtwoord</label>
|
<label for="password" class="form-label" data-translate="password">Wachtwoord</label>
|
||||||
<input type="password" class="form-control" id="password" required>
|
<input type="password" class="form-control" id="password" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button type="submit" class="btn btn-primary">Inloggen</button>
|
<button type="submit" class="btn btn-primary" data-translate="login">Inloggen</button>
|
||||||
<p class="mt-3">Nieuwe student? <a href="register.html">Registreer hier</a></p>
|
<p class="mt-3"><span data-translate="no-account">Geen account?</span> <a href="register.html" data-translate="register">Registreer hier</a></p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,6 +47,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="js/translations.js"></script>
|
||||||
<script src="js/auth.js"></script>
|
<script src="js/auth.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -20,6 +20,14 @@ async function initializePage() {
|
|||||||
startAutoRefresh();
|
startAutoRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reload content when language changes
|
||||||
|
function reloadContent() {
|
||||||
|
displayReservations();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make reloadContent available globally for translation manager
|
||||||
|
window.reloadContent = reloadContent;
|
||||||
|
|
||||||
// Display user info
|
// Display user info
|
||||||
function displayUserInfo() {
|
function displayUserInfo() {
|
||||||
const username = localStorage.getItem('username');
|
const username = localStorage.getItem('username');
|
||||||
|
|||||||
@@ -21,6 +21,15 @@ async function initializePage() {
|
|||||||
startAutoRefresh();
|
startAutoRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reload content when language changes
|
||||||
|
function reloadContent() {
|
||||||
|
displayItems();
|
||||||
|
loadMyReservations();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make reloadContent available globally for translation manager
|
||||||
|
window.reloadContent = reloadContent;
|
||||||
|
|
||||||
// Display user info
|
// Display user info
|
||||||
function displayUserInfo() {
|
function displayUserInfo() {
|
||||||
const username = localStorage.getItem('username');
|
const username = localStorage.getItem('username');
|
||||||
@@ -112,12 +121,20 @@ function displayItems() {
|
|||||||
filteredItems = filteredItems.filter(item => item.location === locationFilter);
|
filteredItems = filteredItems.filter(item => item.location === locationFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply search filter
|
// Apply search filter with translation support
|
||||||
if (searchTerm.trim() !== '') {
|
if (searchTerm.trim() !== '') {
|
||||||
filteredItems = filteredItems.filter(item =>
|
filteredItems = filteredItems.filter(item => {
|
||||||
item.name.toLowerCase().includes(searchTerm) ||
|
if (window.translationManager) {
|
||||||
(item.description && item.description.toLowerCase().includes(searchTerm))
|
const localizedItem = window.translationManager.getLocalizedItem(item);
|
||||||
);
|
return localizedItem.name.toLowerCase().includes(searchTerm) ||
|
||||||
|
(localizedItem.description && localizedItem.description.toLowerCase().includes(searchTerm));
|
||||||
|
} else {
|
||||||
|
// Fallback for when translation manager isn't loaded yet (default to Dutch)
|
||||||
|
const name = item.name?.nl || item.name?.en || item.name || '';
|
||||||
|
const description = item.description?.nl || item.description?.en || item.description || '';
|
||||||
|
return name.toLowerCase().includes(searchTerm) || description.toLowerCase().includes(searchTerm);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update view mode buttons active state
|
// Update view mode buttons active state
|
||||||
@@ -133,13 +150,19 @@ function displayItems() {
|
|||||||
if (currentViewMode === 'grid') {
|
if (currentViewMode === 'grid') {
|
||||||
// Display items in grid view
|
// Display items in grid view
|
||||||
const gridContainer = document.getElementById('itemsGrid');
|
const gridContainer = document.getElementById('itemsGrid');
|
||||||
gridContainer.innerHTML = filteredItems.map(item => `
|
gridContainer.innerHTML = filteredItems.map(item => {
|
||||||
|
const localizedItem = window.translationManager ? window.translationManager.getLocalizedItem(item) : {
|
||||||
|
name: item.name?.nl || item.name?.en || item.name || 'Onbekend Artikel',
|
||||||
|
description: item.description?.nl || item.description?.en || item.description || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
return `
|
||||||
<div class="col-md-4 col-lg-3 mb-4">
|
<div class="col-md-4 col-lg-3 mb-4">
|
||||||
<div class="card h-100" style="cursor: pointer" onclick='showItemDetails(${JSON.stringify(item).replace(/"/g, '"')})'>
|
<div class="card h-100" style="cursor: pointer" onclick='showItemDetails(${JSON.stringify(item).replace(/"/g, '"')})'>
|
||||||
<img src="${item.imageUrl || '/images/default-item.png'}" class="card-img-top" alt="${item.name}" style="height: 200px; object-fit: cover;">
|
<img src="${item.imageUrl || '/images/default-item.png'}" class="card-img-top" alt="${localizedItem.name}" style="height: 200px; object-fit: cover;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">${item.name}</h5>
|
<h5 class="card-title">${localizedItem.name}</h5>
|
||||||
<p class="card-text small text-muted">${item.description || 'No description available'}</p>
|
<p class="card-text small text-muted">${localizedItem.description || 'No description available'}</p>
|
||||||
<p class="card-text">
|
<p class="card-text">
|
||||||
<small class="text-muted">Location: ${item.location}</small><br>
|
<small class="text-muted">Location: ${item.location}</small><br>
|
||||||
<small class="text-muted">Available: ${item.quantity - (item.reserved || 0)}</small>
|
<small class="text-muted">Available: ${item.quantity - (item.reserved || 0)}</small>
|
||||||
@@ -151,15 +174,22 @@ function displayItems() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`;
|
||||||
|
}).join('');
|
||||||
} else {
|
} else {
|
||||||
// Display items in list view
|
// Display items in list view
|
||||||
const itemsListBody = document.getElementById('itemsListBody');
|
const itemsListBody = document.getElementById('itemsListBody');
|
||||||
itemsListBody.innerHTML = filteredItems.map(item => `
|
itemsListBody.innerHTML = filteredItems.map(item => {
|
||||||
|
const localizedItem = window.translationManager ? window.translationManager.getLocalizedItem(item) : {
|
||||||
|
name: item.name?.nl || item.name?.en || item.name || 'Onbekend Artikel',
|
||||||
|
description: item.description?.nl || item.description?.en || item.description || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
return `
|
||||||
<tr class="item-row" style="cursor: pointer" onclick="showItemDetails(${JSON.stringify(item).replace(/"/g, '"')})">
|
<tr class="item-row" style="cursor: pointer" onclick="showItemDetails(${JSON.stringify(item).replace(/"/g, '"')})">
|
||||||
<td><img src="${item.imageUrl || '/images/default-item.png'}" class="item-thumbnail" alt="${item.name}"></td>
|
<td><img src="${item.imageUrl || '/images/default-item.png'}" class="item-thumbnail" alt="${localizedItem.name}"></td>
|
||||||
<td>${item.name}</td>
|
<td>${localizedItem.name}</td>
|
||||||
<td>${item.description || 'No description available'}</td>
|
<td>${localizedItem.description || 'No description available'}</td>
|
||||||
<td>${item.location}</td>
|
<td>${item.location}</td>
|
||||||
<td>${item.quantity - (item.reserved || 0)}</td>
|
<td>${item.quantity - (item.reserved || 0)}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -169,7 +199,8 @@ function displayItems() {
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`;
|
||||||
|
}).join('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
293
public/js/translations.js
Normal file
293
public/js/translations.js
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
// Translation utility for the warehouse management system
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
en: {
|
||||||
|
// Navigation
|
||||||
|
'school-warehouse': 'School Warehouse',
|
||||||
|
'available-items': 'Available Items',
|
||||||
|
'my-reservations': 'My Reservations',
|
||||||
|
'logout': 'Logout',
|
||||||
|
'inventory': 'Inventory',
|
||||||
|
'add-new-item': 'Add New Item',
|
||||||
|
'reservations': 'Reservations',
|
||||||
|
|
||||||
|
// Page titles
|
||||||
|
'warehouse-dashboard-admin': 'Warehouse Dashboard - Admin',
|
||||||
|
'warehouse-dashboard-student': 'Warehouse Dashboard - Student',
|
||||||
|
'my-reservations-student': 'My Reservations - Student',
|
||||||
|
'login': 'Login',
|
||||||
|
'register': 'Register',
|
||||||
|
|
||||||
|
// Forms
|
||||||
|
'username': 'Username',
|
||||||
|
'password': 'Password',
|
||||||
|
'remember-me': 'Remember Me',
|
||||||
|
'no-account': "Don't have an account?",
|
||||||
|
'student-registration': 'Student Registration',
|
||||||
|
'confirm-password': 'Confirm Password',
|
||||||
|
'already-account': 'Already have an account?',
|
||||||
|
|
||||||
|
// Search and filters
|
||||||
|
'search-items': 'Search items...',
|
||||||
|
'search-reservations': 'Search reservations...',
|
||||||
|
'clear-search': 'Clear search',
|
||||||
|
'all-locations': 'All Locations',
|
||||||
|
'all-status': 'All Status',
|
||||||
|
'filter-reservations': 'Filter Reservations',
|
||||||
|
'location': 'Location',
|
||||||
|
'status': 'Status',
|
||||||
|
'search': 'Search',
|
||||||
|
|
||||||
|
// Table headers
|
||||||
|
'image': 'Image',
|
||||||
|
'item-name': 'Item Name',
|
||||||
|
'description': 'Description',
|
||||||
|
'quantity': 'Quantity',
|
||||||
|
'quantity-available': 'Quantity Available',
|
||||||
|
'action': 'Action',
|
||||||
|
'actions': 'Actions',
|
||||||
|
'reserved-date': 'Reserved Date',
|
||||||
|
|
||||||
|
// View modes
|
||||||
|
'grid': 'Grid',
|
||||||
|
'list': 'List',
|
||||||
|
|
||||||
|
// Status values
|
||||||
|
'pending': 'Pending',
|
||||||
|
'approved': 'Approved',
|
||||||
|
'rejected': 'Rejected',
|
||||||
|
'return-pending': 'Return Pending',
|
||||||
|
'returned': 'Returned',
|
||||||
|
|
||||||
|
// Buttons and actions
|
||||||
|
'reserve': 'Reserve',
|
||||||
|
'cancel': 'Cancel',
|
||||||
|
'return': 'Return',
|
||||||
|
'approve': 'Approve',
|
||||||
|
'reject': 'Reject',
|
||||||
|
'edit': 'Edit',
|
||||||
|
'delete': 'Delete',
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
'loading': 'Loading...',
|
||||||
|
'loading-items': 'Loading items...',
|
||||||
|
'failed-load-items': 'Failed to load items',
|
||||||
|
'failed-load-reservations': 'Failed to load reservations',
|
||||||
|
'failed-reserve-item': 'Failed to reserve item',
|
||||||
|
'failed-cancel-reservation': 'Failed to cancel reservation',
|
||||||
|
'failed-request-return': 'Failed to request return',
|
||||||
|
'confirm-cancel-reservation': 'Are you sure you want to cancel this reservation?',
|
||||||
|
'confirm-return-request': 'Are you sure you want to request return for this item? An admin will need to approve the return.',
|
||||||
|
'return-request-success': 'Return requested successfully! An admin will review your request.',
|
||||||
|
|
||||||
|
// Management
|
||||||
|
'inventory-management': 'Inventory Management',
|
||||||
|
'reservation-management': 'Reservation Management'
|
||||||
|
},
|
||||||
|
nl: {
|
||||||
|
// Navigation
|
||||||
|
'school-warehouse': 'School Magazijn',
|
||||||
|
'available-items': 'Beschikbare Artikelen',
|
||||||
|
'my-reservations': 'Mijn Reserveringen',
|
||||||
|
'logout': 'Uitloggen',
|
||||||
|
'inventory': 'Voorraad',
|
||||||
|
'add-new-item': 'Nieuw Artikel Toevoegen',
|
||||||
|
'reservations': 'Reserveringen',
|
||||||
|
|
||||||
|
// Page titles
|
||||||
|
'warehouse-dashboard-admin': 'Magazijn Dashboard - Admin',
|
||||||
|
'warehouse-dashboard-student': 'Magazijn Dashboard - Student',
|
||||||
|
'my-reservations-student': 'Mijn Reserveringen - Student',
|
||||||
|
'login': 'Inloggen',
|
||||||
|
'register': 'Registreren',
|
||||||
|
|
||||||
|
// Forms
|
||||||
|
'username': 'Gebruikersnaam',
|
||||||
|
'password': 'Wachtwoord',
|
||||||
|
'remember-me': 'Onthoud mij',
|
||||||
|
'no-account': 'Geen account?',
|
||||||
|
'student-registration': 'Student Registratie',
|
||||||
|
'confirm-password': 'Bevestig Wachtwoord',
|
||||||
|
'already-account': 'Al een account?',
|
||||||
|
|
||||||
|
// Search and filters
|
||||||
|
'search-items': 'Zoek artikelen...',
|
||||||
|
'search-reservations': 'Zoek reserveringen...',
|
||||||
|
'clear-search': 'Zoekopdracht wissen',
|
||||||
|
'all-locations': 'Alle Locaties',
|
||||||
|
'all-status': 'Alle Statussen',
|
||||||
|
'filter-reservations': 'Filter Reserveringen',
|
||||||
|
'location': 'Locatie',
|
||||||
|
'status': 'Status',
|
||||||
|
'search': 'Zoeken',
|
||||||
|
|
||||||
|
// Table headers
|
||||||
|
'image': 'Afbeelding',
|
||||||
|
'item-name': 'Artikelnaam',
|
||||||
|
'description': 'Beschrijving',
|
||||||
|
'quantity': 'Hoeveelheid',
|
||||||
|
'quantity-available': 'Beschikbare Hoeveelheid',
|
||||||
|
'action': 'Actie',
|
||||||
|
'actions': 'Acties',
|
||||||
|
'reserved-date': 'Reserveringsdatum',
|
||||||
|
|
||||||
|
// View modes
|
||||||
|
'grid': 'Raster',
|
||||||
|
'list': 'Lijst',
|
||||||
|
|
||||||
|
// Status values
|
||||||
|
'pending': 'In Behandeling',
|
||||||
|
'approved': 'Goedgekeurd',
|
||||||
|
'rejected': 'Afgewezen',
|
||||||
|
'return-pending': 'Retour in Behandeling',
|
||||||
|
'returned': 'Geretourneerd',
|
||||||
|
|
||||||
|
// Buttons and actions
|
||||||
|
'reserve': 'Reserveren',
|
||||||
|
'cancel': 'Annuleren',
|
||||||
|
'return': 'Retourneren',
|
||||||
|
'approve': 'Goedkeuren',
|
||||||
|
'reject': 'Afwijzen',
|
||||||
|
'edit': 'Bewerken',
|
||||||
|
'delete': 'Verwijderen',
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
'loading': 'Laden...',
|
||||||
|
'loading-items': 'Artikelen laden...',
|
||||||
|
'failed-load-items': 'Kon artikelen niet laden',
|
||||||
|
'failed-load-reservations': 'Kon reserveringen niet laden',
|
||||||
|
'failed-reserve-item': 'Kon artikel niet reserveren',
|
||||||
|
'failed-cancel-reservation': 'Kon reservering niet annuleren',
|
||||||
|
'failed-request-return': 'Kon retour niet aanvragen',
|
||||||
|
'confirm-cancel-reservation': 'Weet je zeker dat je deze reservering wilt annuleren?',
|
||||||
|
'confirm-return-request': 'Weet je zeker dat je retour wilt aanvragen voor dit artikel? Een admin moet de retour goedkeuren.',
|
||||||
|
'return-request-success': 'Retour succesvol aangevraagd! Een admin zal je verzoek beoordelen.',
|
||||||
|
|
||||||
|
// Management
|
||||||
|
'inventory-management': 'Voorraadbeheer',
|
||||||
|
'reservation-management': 'Reserveringsbeheer'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Translation manager class
|
||||||
|
class TranslationManager {
|
||||||
|
constructor() {
|
||||||
|
this.currentLanguage = localStorage.getItem('language') || 'nl'; // Default to Dutch
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Apply saved language preference
|
||||||
|
this.applyTranslations();
|
||||||
|
|
||||||
|
// Set up language toggle if it exists
|
||||||
|
const languageToggle = document.getElementById('languageToggle');
|
||||||
|
if (languageToggle) {
|
||||||
|
languageToggle.addEventListener('change', (e) => {
|
||||||
|
this.setLanguage(e.target.checked ? 'en' : 'nl');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set toggle state based on current language (Dutch is default/unchecked)
|
||||||
|
languageToggle.checked = this.currentLanguage === 'en';
|
||||||
|
this.updateLanguageLabels();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLanguageLabels() {
|
||||||
|
const nlLabel = document.getElementById('nlLabel');
|
||||||
|
const enLabel = document.getElementById('enLabel');
|
||||||
|
|
||||||
|
if (nlLabel && enLabel) {
|
||||||
|
nlLabel.classList.toggle('active', this.currentLanguage === 'nl');
|
||||||
|
enLabel.classList.toggle('active', this.currentLanguage === 'en');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLanguage(lang) {
|
||||||
|
if (lang !== 'en' && lang !== 'nl') {
|
||||||
|
console.warn('Unsupported language:', lang);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentLanguage = lang;
|
||||||
|
localStorage.setItem('language', lang);
|
||||||
|
this.applyTranslations();
|
||||||
|
this.updateLanguageLabels();
|
||||||
|
|
||||||
|
// Reload dynamic content if function exists
|
||||||
|
if (window.reloadContent) {
|
||||||
|
window.reloadContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLanguage() {
|
||||||
|
return this.currentLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
translate(key) {
|
||||||
|
const translation = translations[this.currentLanguage]?.[key];
|
||||||
|
if (!translation) {
|
||||||
|
console.warn(`Translation missing for key: ${key} in language: ${this.currentLanguage}`);
|
||||||
|
// Fallback to Dutch first, then English, then key itself
|
||||||
|
return translations['nl']?.[key] || translations['en']?.[key] || key;
|
||||||
|
}
|
||||||
|
return translation;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTranslations() {
|
||||||
|
// Update all elements with data-translate attribute
|
||||||
|
document.querySelectorAll('[data-translate]').forEach(element => {
|
||||||
|
const key = element.getAttribute('data-translate');
|
||||||
|
const translation = this.translate(key);
|
||||||
|
|
||||||
|
if (element.tagName === 'INPUT' && element.type === 'submit') {
|
||||||
|
element.value = translation;
|
||||||
|
} else if (element.tagName === 'INPUT' && (element.type === 'text' || element.type === 'password')) {
|
||||||
|
element.placeholder = translation;
|
||||||
|
} else if (element.hasAttribute('title')) {
|
||||||
|
element.title = translation;
|
||||||
|
} else {
|
||||||
|
element.textContent = translation;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update page title if it has data-translate
|
||||||
|
const titleElement = document.querySelector('title[data-translate]');
|
||||||
|
if (titleElement) {
|
||||||
|
const key = titleElement.getAttribute('data-translate');
|
||||||
|
document.title = this.translate(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get translated item data based on current language
|
||||||
|
getLocalizedItem(item) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
name: item.name?.[this.currentLanguage] || item.name?.nl || item.name?.en || item.name || 'Unknown Item',
|
||||||
|
description: item.description?.[this.currentLanguage] || item.description?.nl || item.description?.en || item.description || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get translated status text
|
||||||
|
getStatusText(status) {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': 'pending',
|
||||||
|
'APPROVED': 'approved',
|
||||||
|
'REJECTED': 'rejected',
|
||||||
|
'RETURN_PENDING': 'return-pending',
|
||||||
|
'RETURNED': 'returned'
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.translate(statusMap[status] || status.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize translation manager when DOM is loaded
|
||||||
|
let translationManager;
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
translationManager = new TranslationManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export for use in other files
|
||||||
|
window.TranslationManager = TranslationManager;
|
||||||
|
window.translationManager = translationManager;
|
||||||
@@ -12,29 +12,37 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand" href="#">School Magazijn</a>
|
<a class="navbar-brand" href="#" data-translate="school-warehouse">School Magazijn</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="student.html">Beschikbare Artikelen</a>
|
<a class="nav-link" href="student.html" data-translate="available-items">Beschikbare Artikelen</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" href="student-reservations.html">Mijn Reserveringen</a>
|
<a class="nav-link active" href="student-reservations.html" data-translate="my-reservations">Mijn Reserveringen</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="navbar-nav ms-auto">
|
<div class="navbar-nav ms-auto">
|
||||||
|
<div class="language-toggle nav-item">
|
||||||
|
<span class="language-label" id="nlLabel">NL</span>
|
||||||
|
<label class="language-switch">
|
||||||
|
<input type="checkbox" id="languageToggle">
|
||||||
|
<span class="language-slider"></span>
|
||||||
|
</label>
|
||||||
|
<span class="language-label" id="enLabel">EN</span>
|
||||||
|
</div>
|
||||||
<span class="nav-item nav-link text-light" id="userInfo"></span>
|
<span class="nav-item nav-link text-light" id="userInfo"></span>
|
||||||
<a class="nav-link" href="#" id="logoutBtn">Uitloggen</a>
|
<a class="nav-link" href="#" id="logoutBtn" data-translate="logout">Uitloggen</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<h2 class="mb-4">Mijn Reserveringen</h2>
|
<h2 class="mb-4" data-translate="my-reservations">Mijn Reserveringen</h2>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -97,6 +105,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="js/translations.js"></script>
|
||||||
<script src="js/student-reservations.js"></script>
|
<script src="js/student-reservations.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Magazijn Dashboard - Student</title>
|
<title data-translate="warehouse-dashboard-student">Magazijn Dashboard - Student</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
@@ -12,22 +12,30 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand" href="#">School Magazijn</a>
|
<a class="navbar-brand" href="#" data-translate="school-warehouse">School Magazijn</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" href="student.html">Beschikbare Artikelen</a>
|
<a class="nav-link active" href="student.html" data-translate="available-items">Beschikbare Artikelen</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="student-reservations.html">Mijn Reserveringen</a>
|
<a class="nav-link" href="student-reservations.html" data-translate="my-reservations">Mijn Reserveringen</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="navbar-nav ms-auto">
|
<div class="navbar-nav ms-auto">
|
||||||
|
<div class="language-toggle nav-item">
|
||||||
|
<span class="language-label" id="nlLabel">NL</span>
|
||||||
|
<label class="language-switch">
|
||||||
|
<input type="checkbox" id="languageToggle">
|
||||||
|
<span class="language-slider"></span>
|
||||||
|
</label>
|
||||||
|
<span class="language-label" id="enLabel">EN</span>
|
||||||
|
</div>
|
||||||
<span class="nav-item nav-link text-light" id="userInfo"></span>
|
<span class="nav-item nav-link text-light" id="userInfo"></span>
|
||||||
<a class="nav-link" href="#" id="logoutBtn">Uitloggen</a>
|
<a class="nav-link" href="#" id="logoutBtn" data-translate="logout">Uitloggen</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,20 +44,20 @@
|
|||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<h2>Beschikbare Artikelen</h2>
|
<h2 data-translate="available-items">Beschikbare Artikelen</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="input-group search-input-group">
|
<div class="input-group search-input-group">
|
||||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||||
<input type="text" class="form-control" id="searchInput" placeholder="Zoek artikelen...">
|
<input type="text" class="form-control" id="searchInput" placeholder="Zoek artikelen..." data-translate="search-items">
|
||||||
<button class="btn btn-outline-secondary" type="button" id="clearSearch" title="Zoekopdracht wissen">
|
<button class="btn btn-outline-secondary" type="button" id="clearSearch" title="Zoekopdracht wissen" data-translate="clear-search">
|
||||||
<i class="bi bi-x-lg"></i>
|
<i class="bi bi-x-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto ms-auto">
|
<div class="col-auto ms-auto">
|
||||||
<select class="form-select me-2" id="locationFilter">
|
<select class="form-select me-2" id="locationFilter">
|
||||||
<option value="all">Alle Locaties</option>
|
<option value="all" data-translate="all-locations">Alle Locaties</option>
|
||||||
<option value="Heerlen">Heerlen</option>
|
<option value="Heerlen">Heerlen</option>
|
||||||
<option value="Maastricht">Maastricht</option>
|
<option value="Maastricht">Maastricht</option>
|
||||||
<option value="Sittard">Sittard</option>
|
<option value="Sittard">Sittard</option>
|
||||||
@@ -58,10 +66,10 @@
|
|||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button type="button" class="btn btn-outline-primary view-mode-btn active" data-mode="grid">
|
<button type="button" class="btn btn-outline-primary view-mode-btn active" data-mode="grid">
|
||||||
<i class="bi bi-grid"></i> Raster
|
<i class="bi bi-grid"></i> <span data-translate="grid">Raster</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-primary view-mode-btn" data-mode="list">
|
<button type="button" class="btn btn-outline-primary view-mode-btn" data-mode="list">
|
||||||
<i class="bi bi-list"></i> Lijst
|
<i class="bi bi-list"></i> <span data-translate="list">Lijst</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,12 +86,12 @@
|
|||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Afbeelding</th>
|
<th data-translate="image">Afbeelding</th>
|
||||||
<th>Artikelnaam</th>
|
<th data-translate="item-name">Artikelnaam</th>
|
||||||
<th>Beschrijving</th>
|
<th data-translate="description">Beschrijving</th>
|
||||||
<th>Locatie</th>
|
<th data-translate="location">Locatie</th>
|
||||||
<th>Beschikbare Hoeveelheid</th>
|
<th data-translate="quantity-available">Beschikbare Hoeveelheid</th>
|
||||||
<th>Actie</th>
|
<th data-translate="action">Actie</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="itemsListBody">
|
<tbody id="itemsListBody">
|
||||||
@@ -133,6 +141,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="js/translations.js"></script>
|
||||||
<script src="js/student.js"></script>
|
<script src="js/student.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
48
seed.js
48
seed.js
@@ -28,33 +28,69 @@ async function seedDatabase() {
|
|||||||
await User.create({
|
await User.create({
|
||||||
username: 'student',
|
username: 'student',
|
||||||
password: studentPassword,
|
password: studentPassword,
|
||||||
|
email: '123456@vistacollege.nl',
|
||||||
role: 'student'
|
role: 'student'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create some test items
|
// Create some test items with multilingual support
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
name: 'Laptop',
|
name: {
|
||||||
|
en: 'Laptop',
|
||||||
|
nl: 'Laptop'
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
en: 'High-performance laptop for programming and design work',
|
||||||
|
nl: 'Krachtige laptop voor programmeren en ontwerpwerk'
|
||||||
|
},
|
||||||
location: 'Heerlen',
|
location: 'Heerlen',
|
||||||
quantity: 5
|
quantity: 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Projector',
|
name: {
|
||||||
|
en: 'Projector',
|
||||||
|
nl: 'Beamer'
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
en: 'HD projector for presentations and lectures',
|
||||||
|
nl: 'HD-beamer voor presentaties en lezingen'
|
||||||
|
},
|
||||||
location: 'Maastricht',
|
location: 'Maastricht',
|
||||||
quantity: 3
|
quantity: 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Microscope',
|
name: {
|
||||||
|
en: 'Microscope',
|
||||||
|
nl: 'Microscoop'
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
en: 'Digital microscope for laboratory work',
|
||||||
|
nl: 'Digitale microscoop voor laboratoriumwerk'
|
||||||
|
},
|
||||||
location: 'Sittard',
|
location: 'Sittard',
|
||||||
quantity: 4
|
quantity: 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Tablet',
|
name: {
|
||||||
|
en: 'Tablet',
|
||||||
|
nl: 'Tablet'
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
en: 'Portable tablet for mobile learning and presentations',
|
||||||
|
nl: 'Draagbare tablet voor mobiel leren en presentaties'
|
||||||
|
},
|
||||||
location: 'Heerlen',
|
location: 'Heerlen',
|
||||||
quantity: 10
|
quantity: 10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Camera',
|
name: {
|
||||||
|
en: 'Camera',
|
||||||
|
nl: 'Camera'
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
en: 'Professional DSLR camera for photography courses',
|
||||||
|
nl: 'Professionele spiegelreflexcamera voor fotografiecursussen'
|
||||||
|
},
|
||||||
location: 'Maastricht',
|
location: 'Maastricht',
|
||||||
quantity: 2
|
quantity: 2
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user