Fix category filter bug and add tab accessibility
The category tabs were not filtering because the generic tab event handler was overwriting currentFilter with undefined (category tabs use data-category not data-filter). Fixed by targeting only [data-filter] tabs and clearing categoryFilter when switching to non-category tabs. Added proper ARIA attributes for screen reader accessibility: - role="tablist" on nav-tabs container - role="tab" and aria-selected on all tab buttons - Dynamic aria-selected updates on tab clicks Also includes API support for category field and deploy script update. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
18
api.php
18
api.php
@@ -229,24 +229,26 @@ switch ($action) {
|
|||||||
|
|
||||||
$name = sanitize($input['name'] ?? '');
|
$name = sanitize($input['name'] ?? '');
|
||||||
$container = sanitize($input['container'] ?? 'glass-bail-medium', 50);
|
$container = sanitize($input['container'] ?? 'glass-bail-medium', 50);
|
||||||
|
$category = sanitize($input['category'] ?? 'other', 50);
|
||||||
$outOfStock = (bool)($input['outOfStock'] ?? false);
|
$outOfStock = (bool)($input['outOfStock'] ?? false);
|
||||||
|
|
||||||
if (empty($name)) {
|
if (empty($name)) {
|
||||||
echo json_encode(['success' => false, 'error' => 'Name required']);
|
echo json_encode(['success' => false, 'error' => 'Name required']);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$inventory = loadInventory();
|
$inventory = loadInventory();
|
||||||
$newItem = [
|
$newItem = [
|
||||||
'id' => getNextId($inventory),
|
'id' => getNextId($inventory),
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'container' => $container,
|
'container' => $container,
|
||||||
|
'category' => $category,
|
||||||
'outOfStock' => $outOfStock
|
'outOfStock' => $outOfStock
|
||||||
];
|
];
|
||||||
|
|
||||||
$inventory[] = $newItem;
|
$inventory[] = $newItem;
|
||||||
saveInventory($inventory);
|
saveInventory($inventory);
|
||||||
|
|
||||||
echo json_encode(['success' => true, 'item' => $newItem]);
|
echo json_encode(['success' => true, 'item' => $newItem]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -259,31 +261,33 @@ switch ($action) {
|
|||||||
$id = (int)($input['id'] ?? 0);
|
$id = (int)($input['id'] ?? 0);
|
||||||
$name = sanitize($input['name'] ?? '');
|
$name = sanitize($input['name'] ?? '');
|
||||||
$container = sanitize($input['container'] ?? '', 50);
|
$container = sanitize($input['container'] ?? '', 50);
|
||||||
|
$category = sanitize($input['category'] ?? '', 50);
|
||||||
$outOfStock = (bool)($input['outOfStock'] ?? false);
|
$outOfStock = (bool)($input['outOfStock'] ?? false);
|
||||||
|
|
||||||
if ($id <= 0) {
|
if ($id <= 0) {
|
||||||
echo json_encode(['success' => false, 'error' => 'Invalid ID']);
|
echo json_encode(['success' => false, 'error' => 'Invalid ID']);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$inventory = loadInventory();
|
$inventory = loadInventory();
|
||||||
$found = false;
|
$found = false;
|
||||||
|
|
||||||
foreach ($inventory as &$item) {
|
foreach ($inventory as &$item) {
|
||||||
if ($item['id'] === $id) {
|
if ($item['id'] === $id) {
|
||||||
if (!empty($name)) $item['name'] = $name;
|
if (!empty($name)) $item['name'] = $name;
|
||||||
if (!empty($container)) $item['container'] = $container;
|
if (!empty($container)) $item['container'] = $container;
|
||||||
|
if (!empty($category)) $item['category'] = $category;
|
||||||
$item['outOfStock'] = $outOfStock;
|
$item['outOfStock'] = $outOfStock;
|
||||||
$found = true;
|
$found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$found) {
|
if (!$found) {
|
||||||
echo json_encode(['success' => false, 'error' => 'Item not found']);
|
echo json_encode(['success' => false, 'error' => 'Item not found']);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveInventory($inventory);
|
saveInventory($inventory);
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
break;
|
break;
|
||||||
|
|||||||
26
categories.json
Normal file
26
categories.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"flours": {
|
||||||
|
"name": "Flours & Starches",
|
||||||
|
"color": "#8B4513"
|
||||||
|
},
|
||||||
|
"spices": {
|
||||||
|
"name": "Spices & Seasonings",
|
||||||
|
"color": "#CD853F"
|
||||||
|
},
|
||||||
|
"pasta": {
|
||||||
|
"name": "Pasta & Grains",
|
||||||
|
"color": "#DAA520"
|
||||||
|
},
|
||||||
|
"baking": {
|
||||||
|
"name": "Baking",
|
||||||
|
"color": "#D2691E"
|
||||||
|
},
|
||||||
|
"produce": {
|
||||||
|
"name": "Beans & Dried Produce",
|
||||||
|
"color": "#556B2F"
|
||||||
|
},
|
||||||
|
"other": {
|
||||||
|
"name": "Other",
|
||||||
|
"color": "#708090"
|
||||||
|
}
|
||||||
|
}
|
||||||
2
deploy
2
deploy
@@ -6,7 +6,7 @@ DIR=/var/www/my_webapp__2/www
|
|||||||
# Deploy code files
|
# Deploy code files
|
||||||
rsync -avz --no-t --no-p --delete \
|
rsync -avz --no-t --no-p --delete \
|
||||||
--exclude 'data/' \
|
--exclude 'data/' \
|
||||||
index.html api.php containers.json og-image.png ${HOST}:${DIR}
|
index.html api.php containers.json categories.json og-image.png ${HOST}:${DIR}
|
||||||
|
|
||||||
# Deploy data directory protection
|
# Deploy data directory protection
|
||||||
scp data/index.php ${HOST}:${DIR}/data/index.php 2>/dev/null || true
|
scp data/index.php ${HOST}:${DIR}/data/index.php 2>/dev/null || true
|
||||||
|
|||||||
138
index.html
138
index.html
@@ -207,6 +207,14 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.category-tag {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
@@ -688,13 +696,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-tabs" id="navTabs">
|
<div class="nav-tabs" id="navTabs" role="tablist" aria-label="Inventory filters">
|
||||||
<button class="nav-tab active" data-filter="all">All</button>
|
<button class="nav-tab active" data-filter="all" role="tab" aria-selected="true">All</button>
|
||||||
<button class="nav-tab" data-filter="in-stock">In Stock</button>
|
<button class="nav-tab" data-filter="in-stock" role="tab" aria-selected="false">In Stock</button>
|
||||||
<button class="nav-tab" data-filter="out">Out of Stock</button>
|
<button class="nav-tab" data-filter="out" role="tab" aria-selected="false">Out of Stock</button>
|
||||||
<button class="nav-tab" data-filter="spices">Spices</button>
|
<!-- Category tabs inserted dynamically here -->
|
||||||
<button class="nav-tab" data-filter="recent">Recent</button>
|
<button class="nav-tab" data-filter="recent" role="tab" aria-selected="false">Recent</button>
|
||||||
<button class="nav-tab" data-filter="qr">QR Code</button>
|
<button class="nav-tab" data-filter="qr" role="tab" aria-selected="false">QR Code</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container-legend" id="containerLegend"></div>
|
<div class="container-legend" id="containerLegend"></div>
|
||||||
@@ -801,6 +809,12 @@
|
|||||||
<!-- Options loaded from containers.json -->
|
<!-- Options loaded from containers.json -->
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="itemCategory">Category</label>
|
||||||
|
<select class="form-select" id="itemCategory">
|
||||||
|
<!-- Options loaded from categories.json -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="checkbox-group">
|
<div class="checkbox-group">
|
||||||
<input type="checkbox" id="itemOutOfStock">
|
<input type="checkbox" id="itemOutOfStock">
|
||||||
@@ -821,11 +835,15 @@
|
|||||||
let containerTypes = {};
|
let containerTypes = {};
|
||||||
let containerCategories = [];
|
let containerCategories = [];
|
||||||
|
|
||||||
|
// Item categories - loaded from categories.json
|
||||||
|
let itemCategories = {};
|
||||||
|
|
||||||
let inventory = [];
|
let inventory = [];
|
||||||
let isUnlocked = false;
|
let isUnlocked = false;
|
||||||
let currentPin = null;
|
let currentPin = null;
|
||||||
let currentFilter = 'all';
|
let currentFilter = 'all';
|
||||||
let containerFilter = null;
|
let containerFilter = null;
|
||||||
|
let categoryFilter = null;
|
||||||
let hasPinSet = false;
|
let hasPinSet = false;
|
||||||
|
|
||||||
// API calls
|
// API calls
|
||||||
@@ -909,10 +927,73 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load item categories from JSON file
|
||||||
|
async function loadCategories() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('categories.json');
|
||||||
|
itemCategories = await response.json();
|
||||||
|
buildCategoryDropdown();
|
||||||
|
buildCategoryTabs();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading categories:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the category dropdown
|
||||||
|
function buildCategoryDropdown() {
|
||||||
|
const select = document.getElementById('itemCategory');
|
||||||
|
select.replaceChildren();
|
||||||
|
Object.entries(itemCategories).forEach(([key, cat]) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = key;
|
||||||
|
option.textContent = cat.name;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build category filter tabs
|
||||||
|
function buildCategoryTabs() {
|
||||||
|
const navTabs = document.getElementById('navTabs');
|
||||||
|
// Remove old category tabs if they exist
|
||||||
|
navTabs.querySelectorAll('.nav-tab[data-category]').forEach(tab => tab.remove());
|
||||||
|
|
||||||
|
// Add category tabs after "Out of Stock" tab, sorted alphabetically
|
||||||
|
const outTab = navTabs.querySelector('[data-filter="out"]');
|
||||||
|
const sortedCategories = Object.entries(itemCategories)
|
||||||
|
.sort((a, b) => a[1].name.localeCompare(b[1].name))
|
||||||
|
.reverse(); // Reverse so insertAdjacentElement builds correct order
|
||||||
|
sortedCategories.forEach(([key, cat]) => {
|
||||||
|
const tab = document.createElement('button');
|
||||||
|
tab.className = 'nav-tab';
|
||||||
|
tab.dataset.category = key;
|
||||||
|
tab.textContent = cat.name;
|
||||||
|
tab.style.borderLeft = `3px solid ${cat.color}`;
|
||||||
|
tab.setAttribute('role', 'tab');
|
||||||
|
tab.setAttribute('aria-selected', 'false');
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
// Clear other active states and aria-selected
|
||||||
|
navTabs.querySelectorAll('.nav-tab').forEach(t => {
|
||||||
|
t.classList.remove('active');
|
||||||
|
t.setAttribute('aria-selected', 'false');
|
||||||
|
});
|
||||||
|
tab.classList.add('active');
|
||||||
|
tab.setAttribute('aria-selected', 'true');
|
||||||
|
currentFilter = 'category';
|
||||||
|
categoryFilter = key;
|
||||||
|
document.getElementById('inventoryList').style.display = 'block';
|
||||||
|
document.getElementById('containerLegend').style.display = 'grid';
|
||||||
|
document.getElementById('qrSection').style.display = 'none';
|
||||||
|
renderInventory();
|
||||||
|
});
|
||||||
|
outTab.insertAdjacentElement('afterend', tab);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
await loadContainers();
|
await loadContainers();
|
||||||
|
await loadCategories();
|
||||||
|
|
||||||
const result = await apiCall('get');
|
const result = await apiCall('get');
|
||||||
inventory = result.inventory || [];
|
inventory = result.inventory || [];
|
||||||
@@ -987,14 +1068,14 @@
|
|||||||
function renderInventory() {
|
function renderInventory() {
|
||||||
const list = document.getElementById('inventoryList');
|
const list = document.getElementById('inventoryList');
|
||||||
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
||||||
|
|
||||||
let filtered = inventory.filter(item => {
|
let filtered = inventory.filter(item => {
|
||||||
const matchesSearch = item.name.toLowerCase().includes(searchTerm);
|
const matchesSearch = item.name.toLowerCase().includes(searchTerm);
|
||||||
const matchesFilter = currentFilter === 'all' ||
|
const matchesFilter = currentFilter === 'all' ||
|
||||||
currentFilter === 'recent' ||
|
currentFilter === 'recent' ||
|
||||||
(currentFilter === 'in-stock' && !item.outOfStock) ||
|
(currentFilter === 'in-stock' && !item.outOfStock) ||
|
||||||
(currentFilter === 'out' && item.outOfStock) ||
|
(currentFilter === 'out' && item.outOfStock) ||
|
||||||
(currentFilter === 'spices' && item.container.startsWith('spice-'));
|
(currentFilter === 'category' && item.category === categoryFilter);
|
||||||
const matchesContainer = !containerFilter || item.container === containerFilter;
|
const matchesContainer = !containerFilter || item.container === containerFilter;
|
||||||
return matchesSearch && matchesFilter && matchesContainer;
|
return matchesSearch && matchesFilter && matchesContainer;
|
||||||
});
|
});
|
||||||
@@ -1024,20 +1105,23 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.innerHTML = filtered.map(item => `
|
list.innerHTML = filtered.map(item => {
|
||||||
<div class="inventory-item ${item.outOfStock ? 'out-of-stock' : ''}" onclick="openEditModal(${item.id})" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();openEditModal(${item.id});}" tabindex="0" role="button" aria-label="${escapeHtml(item.name)}, ${item.outOfStock ? 'out of stock' : 'in stock'}">
|
const cat = itemCategories[item.category];
|
||||||
|
return `
|
||||||
|
<div class="inventory-item ${item.outOfStock ? 'out-of-stock' : ''}" onclick="openEditModal(${item.id})" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();openEditModal(${item.id});}" tabindex="0" role="button" aria-label="${escapeHtml(item.name)}, ${cat ? cat.name : 'uncategorized'}, ${item.outOfStock ? 'out of stock' : 'in stock'}">
|
||||||
<div class="item-info">
|
<div class="item-info">
|
||||||
<div class="item-name">${escapeHtml(item.name)}</div>
|
<div class="item-name">${escapeHtml(item.name)}</div>
|
||||||
<div class="item-container">
|
<div class="item-container">
|
||||||
<div class="container-dot" style="background: ${containerTypes[item.container]?.color || '#ccc'}" aria-hidden="true"></div>
|
<div class="container-dot" style="background: ${containerTypes[item.container]?.color || '#ccc'}" aria-hidden="true"></div>
|
||||||
${containerTypes[item.container] ? `${containerTypes[item.container].category}: ${containerTypes[item.container].name}` : 'Unknown'}
|
${containerTypes[item.container] ? `${containerTypes[item.container].category}: ${containerTypes[item.container].name}` : 'Unknown'}
|
||||||
|
${cat ? `<span class="category-tag" style="background: ${cat.color}20; color: ${cat.color}; border: 1px solid ${cat.color}40;">${cat.name}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="status-badge ${item.outOfStock ? 'status-out' : 'status-in-stock'}">
|
<span class="status-badge ${item.outOfStock ? 'status-out' : 'status-in-stock'}">
|
||||||
${item.outOfStock ? 'Out' : 'In Stock'}
|
${item.outOfStock ? 'Out' : 'In Stock'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
@@ -1113,14 +1197,19 @@
|
|||||||
function setupEventListeners() {
|
function setupEventListeners() {
|
||||||
document.getElementById('searchInput').addEventListener('input', renderInventory);
|
document.getElementById('searchInput').addEventListener('input', renderInventory);
|
||||||
|
|
||||||
document.querySelectorAll('.nav-tab').forEach(tab => {
|
document.querySelectorAll('.nav-tab[data-filter]').forEach(tab => {
|
||||||
tab.addEventListener('click', () => {
|
tab.addEventListener('click', () => {
|
||||||
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
|
document.querySelectorAll('.nav-tab').forEach(t => {
|
||||||
|
t.classList.remove('active');
|
||||||
|
t.setAttribute('aria-selected', 'false');
|
||||||
|
});
|
||||||
tab.classList.add('active');
|
tab.classList.add('active');
|
||||||
|
tab.setAttribute('aria-selected', 'true');
|
||||||
|
|
||||||
const filter = tab.dataset.filter;
|
const filter = tab.dataset.filter;
|
||||||
currentFilter = filter;
|
currentFilter = filter;
|
||||||
|
categoryFilter = null; // Clear category filter when switching to non-category tabs
|
||||||
|
|
||||||
if (filter === 'qr') {
|
if (filter === 'qr') {
|
||||||
document.getElementById('inventoryList').style.display = 'none';
|
document.getElementById('inventoryList').style.display = 'none';
|
||||||
document.getElementById('containerLegend').style.display = 'none';
|
document.getElementById('containerLegend').style.display = 'none';
|
||||||
@@ -1342,10 +1431,11 @@
|
|||||||
toggleLock();
|
toggleLock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('itemModalTitle').textContent = 'Add Item';
|
document.getElementById('itemModalTitle').textContent = 'Add Item';
|
||||||
document.getElementById('itemName').value = '';
|
document.getElementById('itemName').value = '';
|
||||||
document.getElementById('itemContainer').selectedIndex = 0;
|
document.getElementById('itemContainer').selectedIndex = 0;
|
||||||
|
document.getElementById('itemCategory').value = 'other';
|
||||||
document.getElementById('itemOutOfStock').checked = false;
|
document.getElementById('itemOutOfStock').checked = false;
|
||||||
document.getElementById('editingItemId').value = '';
|
document.getElementById('editingItemId').value = '';
|
||||||
document.getElementById('deleteBtn').style.display = 'none';
|
document.getElementById('deleteBtn').style.display = 'none';
|
||||||
@@ -1357,13 +1447,14 @@
|
|||||||
if (!isUnlocked) {
|
if (!isUnlocked) {
|
||||||
return; // View only mode
|
return; // View only mode
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = inventory.find(i => i.id === id);
|
const item = inventory.find(i => i.id === id);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
document.getElementById('itemModalTitle').textContent = 'Edit Item';
|
document.getElementById('itemModalTitle').textContent = 'Edit Item';
|
||||||
document.getElementById('itemName').value = item.name;
|
document.getElementById('itemName').value = item.name;
|
||||||
document.getElementById('itemContainer').value = item.container;
|
document.getElementById('itemContainer').value = item.container;
|
||||||
|
document.getElementById('itemCategory').value = item.category || 'other';
|
||||||
document.getElementById('itemOutOfStock').checked = item.outOfStock;
|
document.getElementById('itemOutOfStock').checked = item.outOfStock;
|
||||||
document.getElementById('editingItemId').value = id;
|
document.getElementById('editingItemId').value = id;
|
||||||
document.getElementById('deleteBtn').style.display = 'block';
|
document.getElementById('deleteBtn').style.display = 'block';
|
||||||
@@ -1373,9 +1464,10 @@
|
|||||||
async function saveItem() {
|
async function saveItem() {
|
||||||
const name = document.getElementById('itemName').value.trim();
|
const name = document.getElementById('itemName').value.trim();
|
||||||
const container = document.getElementById('itemContainer').value;
|
const container = document.getElementById('itemContainer').value;
|
||||||
|
const category = document.getElementById('itemCategory').value;
|
||||||
const outOfStock = document.getElementById('itemOutOfStock').checked;
|
const outOfStock = document.getElementById('itemOutOfStock').checked;
|
||||||
const editingId = document.getElementById('editingItemId').value;
|
const editingId = document.getElementById('editingItemId').value;
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
showToast('Please enter an item name');
|
showToast('Please enter an item name');
|
||||||
return;
|
return;
|
||||||
@@ -1401,6 +1493,7 @@
|
|||||||
id: parseInt(editingId),
|
id: parseInt(editingId),
|
||||||
name,
|
name,
|
||||||
container,
|
container,
|
||||||
|
category,
|
||||||
outOfStock,
|
outOfStock,
|
||||||
pin: currentPin
|
pin: currentPin
|
||||||
});
|
});
|
||||||
@@ -1409,13 +1502,14 @@
|
|||||||
if (item) {
|
if (item) {
|
||||||
item.name = name;
|
item.name = name;
|
||||||
item.container = container;
|
item.container = container;
|
||||||
|
item.category = category;
|
||||||
item.outOfStock = outOfStock;
|
item.outOfStock = outOfStock;
|
||||||
}
|
}
|
||||||
showToast('Item updated');
|
showToast('Item updated');
|
||||||
} else {
|
} else {
|
||||||
// Add new
|
// Add new
|
||||||
const result = await apiCall('add', { name, container, outOfStock, pin: currentPin });
|
const result = await apiCall('add', { name, container, category, outOfStock, pin: currentPin });
|
||||||
|
|
||||||
if (result.success && result.item) {
|
if (result.success && result.item) {
|
||||||
inventory.push(result.item);
|
inventory.push(result.item);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user