mirror of
https://github.com/rangermix/TwitchDropsMiner.git
synced 2026-05-26 07:08:04 +00:00
feat: add advanced inventory filtering with multi-select game dropdown
Enhance the inventory tab with comprehensive filtering options to help users quickly find relevant campaigns. The new filter system supports status-based filtering (Active, Not Linked, Upcoming, Expired, Finished) and an advanced game selection dropdown with tag-based UI and keyboard navigation. Key features: - Status filters: Active, Not Linked (default), Upcoming (default), Expired, Finished - Multi-select game dropdown with live search and keyboard navigation (arrows, Enter, Escape) - Visual game tags with easy removal - Persistent filter preferences across sessions - Dual data source (combines games from campaigns and settings) - OR logic for game selection, AND logic between filter types - Clear filters button to reset all selections 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -99,6 +99,49 @@
|
||||
|
||||
<!-- Inventory Tab -->
|
||||
<div id="inventory-tab" class="tab-content">
|
||||
<div class="inventory-filters">
|
||||
<div class="filter-controls">
|
||||
<div class="filter-checkboxes">
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="filter-active" />
|
||||
<span>Active</span>
|
||||
</label>
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="filter-not-linked" checked />
|
||||
<span>Not Linked</span>
|
||||
</label>
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="filter-upcoming" checked />
|
||||
<span>Upcoming</span>
|
||||
</label>
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="filter-expired" />
|
||||
<span>Expired</span>
|
||||
</label>
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="filter-finished" />
|
||||
<span>Finished</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter-search">
|
||||
<div class="game-dropdown-container">
|
||||
<div class="game-tags-display" id="selected-game-tags">
|
||||
<!-- Selected game tags will appear here -->
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="inventory-game-search"
|
||||
placeholder="Type to search games..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="game-dropdown-list" id="game-dropdown-list">
|
||||
<!-- Dropdown options will appear here -->
|
||||
</div>
|
||||
</div>
|
||||
<button id="clear-filters-btn" class="small-btn">Clear Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inventory-grid" id="inventory-grid">
|
||||
<p class="empty-message">No campaigns loaded yet...</p>
|
||||
</div>
|
||||
|
||||
@@ -464,18 +464,293 @@ function updateDrop(campaignId, dropData) {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Inventory Filtering ====================
|
||||
|
||||
function getInventoryFilters() {
|
||||
// Get filter state from UI checkboxes and selected games array
|
||||
return {
|
||||
show_active: document.getElementById('filter-active')?.checked || false,
|
||||
show_not_linked: document.getElementById('filter-not-linked')?.checked || false,
|
||||
show_upcoming: document.getElementById('filter-upcoming')?.checked || false,
|
||||
show_expired: document.getElementById('filter-expired')?.checked || false,
|
||||
show_finished: document.getElementById('filter-finished')?.checked || false,
|
||||
game_name_search: [...selectedInventoryGames] // Array of selected game names
|
||||
};
|
||||
}
|
||||
|
||||
function campaignMatchesFilters(campaign, filters) {
|
||||
// Calculate "finished" status: all drops claimed
|
||||
const isFinished = campaign.total_drops > 0 && campaign.claimed_drops === campaign.total_drops;
|
||||
|
||||
// Check if any filter is enabled
|
||||
const hasGameFilter = filters.game_name_search && filters.game_name_search.length > 0;
|
||||
const anyFilterEnabled = filters.show_active || filters.show_not_linked ||
|
||||
filters.show_upcoming || filters.show_expired ||
|
||||
filters.show_finished || hasGameFilter;
|
||||
|
||||
// If no filters enabled, show all campaigns
|
||||
if (!anyFilterEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check status filters (OR logic - campaign matches if ANY checked filter applies)
|
||||
let statusMatch = false;
|
||||
|
||||
if (filters.show_active && campaign.active) statusMatch = true;
|
||||
if (filters.show_not_linked && !campaign.linked) statusMatch = true;
|
||||
if (filters.show_upcoming && campaign.upcoming) statusMatch = true;
|
||||
if (filters.show_expired && campaign.expired) statusMatch = true;
|
||||
if (filters.show_finished && isFinished) statusMatch = true;
|
||||
|
||||
// If status filters are enabled but campaign doesn't match any, filter it out
|
||||
const hasStatusFilters = filters.show_active || filters.show_not_linked ||
|
||||
filters.show_upcoming || filters.show_expired ||
|
||||
filters.show_finished;
|
||||
if (hasStatusFilters && !statusMatch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check game name filter (AND logic with status filters, OR logic among selected games)
|
||||
if (hasGameFilter) {
|
||||
const gameName = campaign.game_name;
|
||||
// Campaign must match at least ONE of the selected games
|
||||
const gameMatch = filters.game_name_search.includes(gameName);
|
||||
if (!gameMatch) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function onInventoryFilterChange() {
|
||||
// Save filter state to settings and re-render inventory
|
||||
saveSettings();
|
||||
renderInventory();
|
||||
}
|
||||
|
||||
function clearInventoryFilters() {
|
||||
// Uncheck all filter checkboxes
|
||||
document.getElementById('filter-active').checked = false;
|
||||
document.getElementById('filter-not-linked').checked = false;
|
||||
document.getElementById('filter-upcoming').checked = false;
|
||||
document.getElementById('filter-expired').checked = false;
|
||||
document.getElementById('filter-finished').checked = false;
|
||||
document.getElementById('inventory-game-search').value = '';
|
||||
|
||||
// Clear selected games
|
||||
selectedInventoryGames = [];
|
||||
updateGameTagsDisplay();
|
||||
|
||||
// Save and re-render
|
||||
saveSettings();
|
||||
renderInventory();
|
||||
}
|
||||
|
||||
// ==================== Game Dropdown & Tags ====================
|
||||
|
||||
// Track selected games for inventory filter
|
||||
let selectedInventoryGames = [];
|
||||
let gameDropdownFocusedIndex = -1;
|
||||
let gameDropdownVisible = false;
|
||||
|
||||
function getAvailableGamesForDropdown() {
|
||||
// Combine games from campaigns and availableGames Set
|
||||
const gamesFromCampaigns = Object.values(state.campaigns).map(c => c.game_name);
|
||||
const gamesFromSettings = Array.from(availableGames || []);
|
||||
|
||||
// Merge and deduplicate
|
||||
const allGames = [...new Set([...gamesFromCampaigns, ...gamesFromSettings])];
|
||||
|
||||
// Sort alphabetically
|
||||
return allGames.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function renderGameDropdown(searchTerm = '') {
|
||||
const dropdown = document.getElementById('game-dropdown-list');
|
||||
const allGames = getAvailableGamesForDropdown();
|
||||
|
||||
// Filter games by search term (case-insensitive)
|
||||
const searchLower = searchTerm.toLowerCase().trim();
|
||||
const filteredGames = searchLower
|
||||
? allGames.filter(game => game.toLowerCase().includes(searchLower))
|
||||
: allGames;
|
||||
|
||||
dropdown.innerHTML = '';
|
||||
|
||||
if (filteredGames.length === 0) {
|
||||
dropdown.innerHTML = '<div class="dropdown-item no-results">No games found</div>';
|
||||
gameDropdownFocusedIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
filteredGames.forEach((gameName, index) => {
|
||||
const isSelected = selectedInventoryGames.includes(gameName);
|
||||
const isFocused = index === gameDropdownFocusedIndex;
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'dropdown-item' + (isFocused ? ' focused' : '');
|
||||
item.dataset.gameName = gameName;
|
||||
item.dataset.index = index;
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.checked = isSelected;
|
||||
checkbox.id = `game-dropdown-${index}`;
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.setAttribute('for', `game-dropdown-${index}`);
|
||||
label.textContent = gameName;
|
||||
|
||||
item.appendChild(checkbox);
|
||||
item.appendChild(label);
|
||||
|
||||
// Click handler for the entire item
|
||||
item.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleGameSelection(gameName);
|
||||
});
|
||||
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleGameSelection(gameName) {
|
||||
const index = selectedInventoryGames.indexOf(gameName);
|
||||
if (index >= 0) {
|
||||
// Remove game
|
||||
selectedInventoryGames.splice(index, 1);
|
||||
} else {
|
||||
// Add game
|
||||
selectedInventoryGames.push(gameName);
|
||||
}
|
||||
|
||||
updateGameTagsDisplay();
|
||||
renderGameDropdown(document.getElementById('inventory-game-search').value);
|
||||
saveSettings();
|
||||
renderInventory();
|
||||
}
|
||||
|
||||
function removeGameTag(gameName) {
|
||||
const index = selectedInventoryGames.indexOf(gameName);
|
||||
if (index >= 0) {
|
||||
selectedInventoryGames.splice(index, 1);
|
||||
updateGameTagsDisplay();
|
||||
renderGameDropdown(document.getElementById('inventory-game-search').value);
|
||||
saveSettings();
|
||||
renderInventory();
|
||||
}
|
||||
}
|
||||
|
||||
function updateGameTagsDisplay() {
|
||||
const container = document.getElementById('selected-game-tags');
|
||||
container.innerHTML = '';
|
||||
|
||||
selectedInventoryGames.forEach(gameName => {
|
||||
const tag = document.createElement('div');
|
||||
tag.className = 'game-tag';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'game-tag-name';
|
||||
nameSpan.textContent = gameName;
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'game-tag-remove';
|
||||
removeBtn.innerHTML = '×';
|
||||
removeBtn.setAttribute('aria-label', `Remove ${gameName}`);
|
||||
removeBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
removeGameTag(gameName);
|
||||
});
|
||||
|
||||
tag.appendChild(nameSpan);
|
||||
tag.appendChild(removeBtn);
|
||||
container.appendChild(tag);
|
||||
});
|
||||
}
|
||||
|
||||
function showGameDropdown() {
|
||||
const dropdown = document.getElementById('game-dropdown-list');
|
||||
dropdown.style.display = 'block';
|
||||
gameDropdownVisible = true;
|
||||
gameDropdownFocusedIndex = -1;
|
||||
renderGameDropdown(document.getElementById('inventory-game-search').value);
|
||||
}
|
||||
|
||||
function closeGameDropdown() {
|
||||
const dropdown = document.getElementById('game-dropdown-list');
|
||||
dropdown.style.display = 'none';
|
||||
gameDropdownVisible = false;
|
||||
gameDropdownFocusedIndex = -1;
|
||||
}
|
||||
|
||||
function handleGameSearchKeydown(event) {
|
||||
if (!gameDropdownVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dropdown = document.getElementById('game-dropdown-list');
|
||||
const items = dropdown.querySelectorAll('.dropdown-item:not(.no-results)');
|
||||
const maxIndex = items.length - 1;
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
gameDropdownFocusedIndex = Math.min(gameDropdownFocusedIndex + 1, maxIndex);
|
||||
renderGameDropdown(document.getElementById('inventory-game-search').value);
|
||||
|
||||
// Scroll focused item into view
|
||||
const focusedItem = dropdown.querySelector('.dropdown-item.focused');
|
||||
if (focusedItem) {
|
||||
focusedItem.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
gameDropdownFocusedIndex = Math.max(gameDropdownFocusedIndex - 1, 0);
|
||||
renderGameDropdown(document.getElementById('inventory-game-search').value);
|
||||
|
||||
// Scroll focused item into view
|
||||
const focusedItem = dropdown.querySelector('.dropdown-item.focused');
|
||||
if (focusedItem) {
|
||||
focusedItem.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
} else if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
if (gameDropdownFocusedIndex >= 0 && gameDropdownFocusedIndex <= maxIndex) {
|
||||
const focusedItem = items[gameDropdownFocusedIndex];
|
||||
const gameName = focusedItem.dataset.gameName;
|
||||
if (gameName) {
|
||||
toggleGameSelection(gameName);
|
||||
}
|
||||
}
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
closeGameDropdown();
|
||||
document.getElementById('inventory-game-search').blur();
|
||||
}
|
||||
}
|
||||
|
||||
function renderInventory() {
|
||||
const container = document.getElementById('inventory-grid');
|
||||
container.innerHTML = '';
|
||||
|
||||
const t = state.translations;
|
||||
const campaigns = Object.values(state.campaigns);
|
||||
if (campaigns.length === 0) {
|
||||
const allCampaigns = Object.values(state.campaigns);
|
||||
|
||||
// Apply filters
|
||||
const filters = getInventoryFilters();
|
||||
const campaigns = allCampaigns.filter(campaign => campaignMatchesFilters(campaign, filters));
|
||||
|
||||
if (allCampaigns.length === 0) {
|
||||
const emptyMsg = t.gui?.inventory?.no_campaigns || 'No campaigns loaded yet...';
|
||||
container.innerHTML = `<p class="empty-message">${emptyMsg}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (campaigns.length === 0) {
|
||||
container.innerHTML = `<p class="empty-message">No campaigns match the current filters.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
campaigns.forEach(campaign => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'campaign-card';
|
||||
@@ -608,11 +883,29 @@ function updateSettingsUI(settings) {
|
||||
availableGames = new Set(settings.games_available);
|
||||
}
|
||||
|
||||
// Restore inventory filters from settings
|
||||
if (settings.inventory_filters) {
|
||||
document.getElementById('filter-active').checked = settings.inventory_filters.show_active || false;
|
||||
document.getElementById('filter-not-linked').checked = settings.inventory_filters.show_not_linked || false;
|
||||
document.getElementById('filter-upcoming').checked = settings.inventory_filters.show_upcoming || false;
|
||||
document.getElementById('filter-expired').checked = settings.inventory_filters.show_expired || false;
|
||||
document.getElementById('filter-finished').checked = settings.inventory_filters.show_finished || false;
|
||||
|
||||
// Restore selected games array
|
||||
selectedInventoryGames = Array.isArray(settings.inventory_filters.game_name_search)
|
||||
? [...settings.inventory_filters.game_name_search]
|
||||
: []; // Handle old string format gracefully
|
||||
updateGameTagsDisplay();
|
||||
}
|
||||
|
||||
// Update games to watch lists
|
||||
renderGamesToWatch();
|
||||
|
||||
// Re-render channels list to apply filter based on updated games to watch
|
||||
renderChannels();
|
||||
|
||||
// Re-render inventory to apply filters
|
||||
renderInventory();
|
||||
}
|
||||
|
||||
function updateManualModeUI(manualModeInfo) {
|
||||
@@ -936,7 +1229,8 @@ async function saveSettings() {
|
||||
language: document.getElementById('language').value,
|
||||
connection_quality: parseInt(document.getElementById('connection-quality').value),
|
||||
minimum_refresh_interval_minutes: parseInt(document.getElementById('minimum-refresh-interval').value),
|
||||
games_to_watch: state.settings.games_to_watch || []
|
||||
games_to_watch: state.settings.games_to_watch || [],
|
||||
inventory_filters: getInventoryFilters()
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -1273,6 +1567,32 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('deselect-all-btn').addEventListener('click', deselectAllGames);
|
||||
document.getElementById('games-filter').addEventListener('input', renderGamesToWatch);
|
||||
|
||||
// Inventory filters
|
||||
document.getElementById('filter-active').addEventListener('change', onInventoryFilterChange);
|
||||
document.getElementById('filter-not-linked').addEventListener('change', onInventoryFilterChange);
|
||||
document.getElementById('filter-upcoming').addEventListener('change', onInventoryFilterChange);
|
||||
document.getElementById('filter-expired').addEventListener('change', onInventoryFilterChange);
|
||||
document.getElementById('filter-finished').addEventListener('change', onInventoryFilterChange);
|
||||
document.getElementById('clear-filters-btn').addEventListener('click', clearInventoryFilters);
|
||||
|
||||
// Inventory game search dropdown
|
||||
const gameSearchInput = document.getElementById('inventory-game-search');
|
||||
gameSearchInput.addEventListener('focus', () => {
|
||||
showGameDropdown();
|
||||
});
|
||||
gameSearchInput.addEventListener('input', (e) => {
|
||||
renderGameDropdown(e.target.value);
|
||||
});
|
||||
gameSearchInput.addEventListener('keydown', handleGameSearchKeydown);
|
||||
|
||||
// Click outside to close dropdown
|
||||
document.addEventListener('click', (e) => {
|
||||
const container = document.querySelector('.game-dropdown-container');
|
||||
if (container && !container.contains(e.target) && gameDropdownVisible) {
|
||||
closeGameDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
// Manual mode controls
|
||||
const exitManualBtn = document.getElementById('exit-manual-btn');
|
||||
if (exitManualBtn) {
|
||||
|
||||
@@ -421,6 +421,181 @@ header h1 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Inventory Filters */
|
||||
.inventory-filters {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.filter-checkboxes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.filter-checkbox input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.filter-checkbox span {
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.filter-search {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.game-dropdown-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.game-tags-display {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.game-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.game-tag:hover {
|
||||
background: #7d39e0;
|
||||
}
|
||||
|
||||
.game-tag-name {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.game-tag-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.game-tag-remove:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
#inventory-game-search {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.game-dropdown-list {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.dropdown-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dropdown-item:hover,
|
||||
.dropdown-item.focused {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.dropdown-item input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dropdown-item label {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dropdown-item.no-results {
|
||||
color: var(--text-secondary);
|
||||
cursor: default;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dropdown-item.no-results:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* Inventory Grid */
|
||||
.inventory-grid {
|
||||
display: grid;
|
||||
|
||||
Reference in New Issue
Block a user