Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import HomePage from './pages/public/HomePage';
import LoginPage from './pages/LoginPage';
import LogoutPage from './pages/LogoutPage';
import AdminDashboard from './pages/admin/AdminDashboard';
import AdminCataloguePage from './pages/admin/AdminCataloguePage';
import CataloguePage from './pages/public/CataloguePage';
import VolunteerApplication from './pages/public/VolunteerApplication';

Expand Down Expand Up @@ -39,6 +40,14 @@ const App: React.FC = () => {
</ProtectedRoute>
}
/>
<Route
path="/admin/catalogue"
element={
<ProtectedRoute requiredRole="ADMIN">
<AdminCataloguePage />
</ProtectedRoute>
}
/>

{/* --- Catch-all 404 Route --- */}
{/* <Route path="*" element={<NotFoundPage />} /> */}
Expand Down
164 changes: 164 additions & 0 deletions frontend/src/components/items/AddItemModal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}

.add-item-modal {
background: #ffffff;
border-radius: 12px;
width: 100%;
max-width: 480px;
max-height: 90vh;
overflow-y: auto;
}

.add-item-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #e5e7eb;
}

.add-item-modal-header h2 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #1f2937;
}

.modal-close-btn {
background: none;
border: none;
font-size: 1.5rem;
color: #6b7280;
cursor: pointer;
padding: 4px;
line-height: 1;
}

.modal-close-btn:hover {
color: #1f2937;
}

.add-item-modal-body {
padding: 24px;
}

.form-group {
margin-bottom: 20px;
}

.form-group label {
display: block;
margin-bottom: 6px;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
}

.form-group label .required {
color: #dc2626;
}

.form-group input,
.form-group textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 0.875rem;
font-family: inherit;
transition: border-color 0.2s;
box-sizing: border-box;
}

.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #1f2937;
}

.form-group input.error,
.form-group textarea.error {
border-color: #dc2626;
}

.form-group textarea {
resize: vertical;
min-height: 80px;
}

.form-error {
margin-top: 4px;
font-size: 0.75rem;
color: #dc2626;
}

.form-hint {
margin-top: 4px;
font-size: 0.75rem;
color: #6b7280;
}

.add-item-modal-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
padding: 16px 24px;
border-top: 1px solid #e5e7eb;
background: #f9fafb;
}

.btn-cancel {
padding: 10px 20px;
background: #ffffff;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
cursor: pointer;
}

.btn-cancel:hover {
border-color: #1f2937;
}

.btn-submit {
padding: 10px 20px;
background: #1f2937;
border: none;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
color: #ffffff;
cursor: pointer;
}

.btn-submit:hover {
background: #374151;
}

.btn-submit:disabled {
background: #9ca3af;
cursor: not-allowed;
}

.api-error {
margin-bottom: 16px;
padding: 12px;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
color: #dc2626;
font-size: 0.875rem;
}
167 changes: 167 additions & 0 deletions frontend/src/components/items/AddItemModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, { useState } from 'react';
import './AddItemModal.css';

interface AddItemModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
}

interface FormData {
item_code: string;
title: string;
platform: string;
description: string;
}

interface FormErrors {
item_code?: string;
title?: string;
}

const AddItemModal: React.FC<AddItemModalProps> = ({ isOpen, onClose, onSuccess }) => {
const [formData, setFormData] = useState<FormData>({
item_code: '',
title: '',
platform: '',
description: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const [apiError, setApiError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);

if (!isOpen) return null;

const validateForm = (): boolean => {
const newErrors: FormErrors = {};

if (!formData.item_code.trim()) {
newErrors.item_code = 'Item code is required';
}

if (!formData.title.trim()) {
newErrors.title = 'Title is required';
}

setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setApiError(null);

if (!validateForm()) return;

setIsSubmitting(true);

// Mock API call, will be replaced with real API
try {
await new Promise((resolve) => setTimeout(resolve, 500));

console.log('Item create:', formData);
onSuccess();
handleClose();
} catch {
setApiError('Item failed to create');
} finally {
setIsSubmitting(false);
}
};

const handleClose = () => {
setFormData({ item_code: '', title: '', platform: '', description: '' });
setErrors({});
setApiError(null);
onClose();
};

const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
// Clear error when user starts typing
if (errors[name as keyof FormErrors]) {
setErrors((prev) => ({ ...prev, [name]: undefined }));
}
};

return (
<div className="modal-overlay" onClick={handleClose}>
<div className="add-item-modal" onClick={(e) => e.stopPropagation()}>
<div className="add-item-modal-header">
<h2>Add New Item</h2>
<button className="modal-close-btn" onClick={handleClose}>×</button>
</div>

<form onSubmit={handleSubmit}>
<div className="add-item-modal-body">
{apiError && <div className="api-error">{apiError}</div>}

<div className="form-group">
<label>
Item Code <span className="required">*</span>
</label>
<input
type="text"
name="item_code"
value={formData.item_code}
onChange={handleChange}
placeholder="Enter barcode or unique ID"
className={errors.item_code ? 'error' : ''}
/>
{errors.item_code && <div className="form-error">{errors.item_code}</div>}
<div className="form-hint">Unique identifier for the item (e.g., barcode)</div>
</div>

<div className="form-group">
<label>
Title <span className="required">*</span>
</label>
<input
type="text"
name="title"
value={formData.title}
onChange={handleChange}
placeholder="Enter game/item title"
className={errors.title ? 'error' : ''}
/>
{errors.title && <div className="form-error">{errors.title}</div>}
</div>

<div className="form-group">
<label>Platform</label>
<input
type="text"
name="platform"
value={formData.platform}
onChange={handleChange}
placeholder="e.g., SNES, PS2, Board Game"
/>
</div>

<div className="form-group">
<label>Description</label>
<textarea
name="description"
value={formData.description}
onChange={handleChange}
placeholder="Additional notes about this item"
/>
</div>
</div>

<div className="add-item-modal-footer">
<button type="button" className="btn-cancel" onClick={handleClose}>
Cancel
</button>
<button type="submit" className="btn-submit" disabled={isSubmitting}>
{isSubmitting ? 'Adding...' : 'Add Item'}
</button>
</div>
</form>
</div>
</div>
);
};

export default AddItemModal;
Loading