mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
style: enhance DataBar components with improved styling and animations
This commit is contained in:
@@ -5,10 +5,21 @@ const AddButton = ({onClick, label = 'Add New'}) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className='flex items-center gap-2 px-3 py-2 rounded transition-colors bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
className='flex items-center gap-2 px-3 py-2 rounded-md
|
||||
border border-gray-200 dark:border-gray-700
|
||||
bg-white text-gray-700 dark:bg-gray-800 dark:text-gray-300
|
||||
hover:bg-gray-50 dark:hover:bg-gray-750
|
||||
hover:border-blue-500/50 hover:text-blue-500
|
||||
dark:hover:border-blue-500/50 dark:hover:text-blue-400
|
||||
transition-all duration-150 ease-in-out
|
||||
group'
|
||||
title={label}>
|
||||
<Plus className='w-4 h-4' />
|
||||
<span className='text-sm'>{label}</span>
|
||||
<Plus
|
||||
className='w-4 h-4 transition-transform duration-200 ease-out
|
||||
group-hover:rotate-90 group-hover:scale-110
|
||||
group-hover:text-blue-500 dark:group-hover:text-blue-400'
|
||||
/>
|
||||
<span className='text-sm font-medium'>{label}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -91,14 +91,14 @@ const DataBar = ({
|
||||
allTags={allTags}
|
||||
/>
|
||||
|
||||
{showAddButton && !isSelectionMode && (
|
||||
<AddButton onClick={onAdd} label={addButtonLabel} />
|
||||
)}
|
||||
|
||||
<ToggleSelectButton
|
||||
isSelectionMode={isSelectionMode}
|
||||
onClick={toggleSelectionMode}
|
||||
/>
|
||||
|
||||
{showAddButton && !isSelectionMode && (
|
||||
<AddButton onClick={onAdd} label={addButtonLabel} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,97 +1,142 @@
|
||||
// FilterMenu.jsx
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {useRef, useEffect} from 'react';
|
||||
import {Filter} from 'lucide-react';
|
||||
|
||||
function FilterMenu({ filterType, setFilterType, filterValue, setFilterValue, allTags }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
function FilterMenu({
|
||||
filterType,
|
||||
setFilterType,
|
||||
filterValue,
|
||||
setFilterValue,
|
||||
allTags
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const options = [
|
||||
{ value: 'none', label: 'No Filter' },
|
||||
{ value: 'tag', label: 'Filter by Tag' },
|
||||
{ value: 'date', label: 'Filter by Date' },
|
||||
];
|
||||
const options = [
|
||||
{value: 'none', label: 'No Filter'},
|
||||
{value: 'tag', label: 'Filter by Tag'},
|
||||
{value: 'date', label: 'Filter by Date'}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event) {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () =>
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="relative inline-block text-left" ref={dropdownRef}>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-between items-center w-full rounded-md border border-gray-600 shadow-sm px-4 py-2 bg-gray-700 text-sm font-medium text-white hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{options.find(option => option.value === filterType)?.label}
|
||||
<svg className="-mr-1 ml-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
const hasActiveFilter = filterType !== 'none' && filterValue;
|
||||
|
||||
return (
|
||||
<div className='relative' ref={dropdownRef}>
|
||||
<button
|
||||
type='button'
|
||||
className={`
|
||||
flex items-center gap-2 px-3 py-2 rounded-md
|
||||
border border-gray-200 dark:border-gray-700
|
||||
transition-all duration-150 ease-in-out
|
||||
group
|
||||
${
|
||||
hasActiveFilter
|
||||
? 'bg-blue-50 text-blue-600 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-800'
|
||||
: 'bg-white text-gray-700 dark:bg-gray-800 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-750 hover:border-blue-500/50 hover:text-blue-500 dark:hover:border-blue-500/50 dark:hover:text-blue-400'
|
||||
}
|
||||
`}
|
||||
onClick={() => setIsOpen(!isOpen)}>
|
||||
<Filter
|
||||
className={`w-4 h-4 transition-colors duration-200
|
||||
${
|
||||
hasActiveFilter
|
||||
? ''
|
||||
: 'group-hover:text-blue-500 dark:group-hover:text-blue-400 group-hover:animate-[wiggle_0.3s_ease-in-out]'
|
||||
}
|
||||
`}
|
||||
style={{
|
||||
'@keyframes wiggle': {
|
||||
'0%': {transform: 'rotate(0deg)'},
|
||||
'25%': {transform: 'rotate(-20deg)'},
|
||||
'75%': {transform: 'rotate(20deg)'},
|
||||
'100%': {transform: 'rotate(0deg)'}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className='text-sm font-medium'>
|
||||
{filterType === 'none'
|
||||
? 'Filter'
|
||||
: options.find(option => option.value === filterType)
|
||||
?.label}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className='absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg z-10'>
|
||||
<div className='py-1' role='menu'>
|
||||
{options.map(option => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
setFilterType(option.value);
|
||||
setFilterValue('');
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`
|
||||
block w-full text-left px-4 py-2 text-sm
|
||||
${
|
||||
filterType === option.value
|
||||
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||
}
|
||||
`}
|
||||
role='menuitem'>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filterType === 'tag' && (
|
||||
<div className='border-t border-gray-200 dark:border-gray-700 p-2'>
|
||||
<select
|
||||
value={filterValue}
|
||||
onChange={e => setFilterValue(e.target.value)}
|
||||
className='w-full px-2 py-1.5 text-sm rounded-md
|
||||
bg-gray-50 dark:bg-gray-700
|
||||
text-gray-700 dark:text-gray-300
|
||||
border border-gray-200 dark:border-gray-600
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500'>
|
||||
<option value=''>Select a tag</option>
|
||||
{allTags.map(tag => (
|
||||
<option key={tag} value={tag}>
|
||||
{tag}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filterType === 'date' && (
|
||||
<div className='border-t border-gray-200 dark:border-gray-700 p-2'>
|
||||
<input
|
||||
type='date'
|
||||
value={filterValue}
|
||||
onChange={e => setFilterValue(e.target.value)}
|
||||
className='w-full px-2 py-1.5 text-sm rounded-md
|
||||
bg-gray-50 dark:bg-gray-700
|
||||
text-gray-700 dark:text-gray-300
|
||||
border border-gray-200 dark:border-gray-600
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-gray-700 ring-1 ring-black ring-opacity-5 z-50">
|
||||
<div className="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
setFilterType(option.value);
|
||||
setFilterValue('');
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`${
|
||||
filterType === option.value ? 'bg-gray-600 text-white' : 'text-gray-200'
|
||||
} block w-full text-left px-4 py-2 text-sm hover:bg-gray-600 hover:text-white`}
|
||||
role="menuitem"
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{filterType === 'tag' && (
|
||||
<select
|
||||
value={filterValue}
|
||||
onChange={(e) => setFilterValue(e.target.value)}
|
||||
className="appearance-none bg-gray-700 text-white py-2 px-4 pr-8 rounded-md border border-gray-600 leading-tight focus:outline-none focus:bg-gray-600 focus:border-white cursor-pointer"
|
||||
>
|
||||
<option value="">Select a tag</option>
|
||||
{allTags.map(tag => (
|
||||
<option key={tag} value={tag}>{tag}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{filterType === 'date' && (
|
||||
<input
|
||||
type="date"
|
||||
value={filterValue}
|
||||
onChange={(e) => setFilterValue(e.target.value)}
|
||||
className="bg-gray-700 text-white py-2 px-4 rounded-md border border-gray-600 leading-tight focus:outline-none focus:bg-gray-600 focus:border-white cursor-pointer"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
FilterMenu.propTypes = {
|
||||
filterType: PropTypes.string.isRequired,
|
||||
setFilterType: PropTypes.func.isRequired,
|
||||
filterValue: PropTypes.string.isRequired,
|
||||
setFilterValue: PropTypes.func.isRequired,
|
||||
allTags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
export default FilterMenu;
|
||||
export default FilterMenu;
|
||||
|
||||
@@ -52,29 +52,47 @@ const SearchBar = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`relative flex-1 min-w-0 ${className}`}>
|
||||
<Search className='absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400' />
|
||||
<div className={`relative flex-1 min-w-0 group ${className}`}>
|
||||
<Search
|
||||
className={`
|
||||
absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4
|
||||
transition-colors duration-200
|
||||
${
|
||||
isFocused
|
||||
? 'text-blue-500'
|
||||
: 'text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`
|
||||
w-full h-10 pl-9 pr-8 rounded-md border
|
||||
w-full h-10 pl-9 pr-8 rounded-md
|
||||
transition-all duration-200 ease-in-out
|
||||
border shadow-sm
|
||||
${
|
||||
isFocused
|
||||
? 'ring-2 ring-blue-500 border-transparent'
|
||||
: 'border-gray-300 dark:border-gray-700'
|
||||
? 'border-blue-500 ring-2 ring-blue-500/20 bg-white/5'
|
||||
: 'border-gray-300 dark:border-gray-700 hover:border-gray-400 dark:hover:border-gray-600'
|
||||
}
|
||||
bg-white dark:bg-gray-800
|
||||
bg-white dark:bg-gray-800
|
||||
flex items-center
|
||||
transition-colors
|
||||
`}>
|
||||
{activeSearch ? (
|
||||
<div className='flex items-center gap-1.5 px-2 py-0.5 bg-blue-500/10 text-blue-500 dark:bg-blue-500/20 dark:text-blue-400 rounded'>
|
||||
<div
|
||||
className='flex items-center gap-1.5 px-2 py-1
|
||||
bg-blue-500/10 dark:bg-blue-500/20
|
||||
border border-blue-500/20 dark:border-blue-400/20
|
||||
text-blue-600 dark:text-blue-400
|
||||
rounded-md shadow-sm
|
||||
transition-all duration-200'>
|
||||
<span className='text-sm font-medium leading-none'>
|
||||
{activeSearch}
|
||||
</span>
|
||||
<button
|
||||
onClick={clearSearch}
|
||||
className='p-0.5 hover:bg-blue-500/20 rounded'>
|
||||
className='p-0.5 hover:bg-blue-500/20 rounded-sm transition-colors'
|
||||
aria-label='Clear search'>
|
||||
<X className='h-3 w-3' />
|
||||
</button>
|
||||
</div>
|
||||
@@ -87,8 +105,9 @@ const SearchBar = ({
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
placeholder={placeholder}
|
||||
className='w-full bg-transparent text-gray-900 dark:text-gray-100
|
||||
placeholder:text-gray-500 dark:placeholder:text-gray-400
|
||||
className='w-full bg-transparent
|
||||
text-gray-900 dark:text-gray-100
|
||||
placeholder:text-gray-500 dark:placeholder:text-gray-400
|
||||
focus:outline-none'
|
||||
/>
|
||||
)}
|
||||
@@ -97,8 +116,13 @@ const SearchBar = ({
|
||||
{searchTerm && !activeSearch && (
|
||||
<button
|
||||
onClick={() => setSearchTerm('')}
|
||||
className='absolute right-3 top-1/2 -translate-y-1/2 p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700'>
|
||||
<X className='h-4 w-4 text-gray-400' />
|
||||
className='absolute right-3 top-1/2 -translate-y-1/2
|
||||
p-1.5 rounded-full
|
||||
text-gray-400 hover:text-gray-600
|
||||
hover:bg-gray-100 dark:hover:bg-gray-700
|
||||
transition-all duration-200'
|
||||
aria-label='Clear search input'>
|
||||
<X className='h-4 w-4' />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {ArrowDown} from 'lucide-react';
|
||||
import {ArrowDownUp} from 'lucide-react';
|
||||
|
||||
export const SortDropdown = ({
|
||||
options,
|
||||
@@ -23,20 +22,41 @@ export const SortDropdown = ({
|
||||
<div className='relative'>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className='flex items-center space-x-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded px-2 py-1 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors'>
|
||||
<span>Sort</span>
|
||||
<ArrowDown size={14} />
|
||||
className='flex items-center gap-2 px-3 py-2 rounded-md
|
||||
border border-gray-200 dark:border-gray-700
|
||||
bg-white text-gray-700 dark:bg-gray-800 dark:text-gray-300
|
||||
hover:bg-gray-50 dark:hover:bg-gray-750
|
||||
hover:border-blue-500/50 hover:text-blue-500
|
||||
dark:hover:border-blue-500/50 dark:hover:text-blue-400
|
||||
transition-all duration-150 ease-in-out
|
||||
group'>
|
||||
<ArrowDownUp
|
||||
className='w-4 h-4 transition-all duration-200
|
||||
[transform-style:preserve-3d]
|
||||
group-hover:[transform:rotateX(180deg)]
|
||||
group-hover:text-blue-500 dark:group-hover:text-blue-400'
|
||||
/>
|
||||
<span className='text-sm font-medium'>Sort</span>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className='absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded shadow-lg z-10'>
|
||||
<div
|
||||
className='absolute right-0 mt-2 w-56 py-1
|
||||
bg-white dark:bg-gray-800
|
||||
border border-gray-200 dark:border-gray-700
|
||||
rounded-md shadow-lg z-10'>
|
||||
{options.map(option => (
|
||||
<button
|
||||
key={option.key}
|
||||
onClick={() => handleSort(option.key)}
|
||||
className='block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors'>
|
||||
{option.label}
|
||||
className='flex items-center justify-between w-full px-4 py-2 text-sm
|
||||
text-gray-700 dark:text-gray-300
|
||||
hover:bg-blue-50 dark:hover:bg-blue-900/20
|
||||
hover:text-blue-500 dark:hover:text-blue-400
|
||||
transition-colors duration-150'>
|
||||
<span>{option.label}</span>
|
||||
{currentKey === option.key && (
|
||||
<span className='float-right'>
|
||||
<span className='text-blue-500 dark:text-blue-400'>
|
||||
{currentDirection === 'asc' ? '↑' : '↓'}
|
||||
</span>
|
||||
)}
|
||||
@@ -48,14 +68,4 @@ export const SortDropdown = ({
|
||||
);
|
||||
};
|
||||
|
||||
SortDropdown.propTypes = {
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
key: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired
|
||||
})
|
||||
).isRequired,
|
||||
currentKey: PropTypes.string.isRequired,
|
||||
currentDirection: PropTypes.string.isRequired,
|
||||
onSort: PropTypes.func.isRequired
|
||||
};
|
||||
export default SortDropdown;
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
import React from 'react';
|
||||
import {CheckSquare} from 'lucide-react';
|
||||
|
||||
const ToggleSelectButton = ({
|
||||
isSelectionMode,
|
||||
onClick,
|
||||
shortcutKey = 'A' // Default to 'A' since that's what's used in the pages
|
||||
}) => {
|
||||
const ToggleSelectButton = ({isSelectionMode, onClick, shortcutKey = 'A'}) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded transition-colors ${
|
||||
isSelectionMode
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300'
|
||||
: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
className={`
|
||||
flex items-center gap-2 px-3 py-2 rounded-md
|
||||
border border-gray-200 dark:border-gray-700
|
||||
transition-all duration-150 ease-in-out
|
||||
group
|
||||
${
|
||||
isSelectionMode
|
||||
? 'bg-blue-50 text-blue-600 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-800'
|
||||
: 'bg-white text-gray-700 dark:bg-gray-800 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-750 hover:border-blue-500/50 hover:text-blue-500 dark:hover:border-blue-500/50 dark:hover:text-blue-400'
|
||||
}
|
||||
`}
|
||||
title={`Toggle selection mode (Ctrl+${shortcutKey})`}>
|
||||
<CheckSquare className='w-4 h-4' />
|
||||
<span className='text-sm'>Select</span>
|
||||
<CheckSquare
|
||||
className={`w-4 h-4 transition-all duration-200
|
||||
${
|
||||
isSelectionMode
|
||||
? ''
|
||||
: 'group-hover:text-blue-500 dark:group-hover:text-blue-400 group-hover:animate-[check-bounce_0.3s_ease-in-out]'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<span className='text-sm font-medium'>Select</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,12 +26,24 @@ module.exports = {
|
||||
opacity: '1',
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
}
|
||||
},
|
||||
wiggle: {
|
||||
'0%, 100%': {transform: 'rotate(0deg)'},
|
||||
'25%': {transform: 'rotate(-20deg)'},
|
||||
'75%': {transform: 'rotate(20deg)'}
|
||||
},
|
||||
'check-bounce': {
|
||||
'0%, 100%': {transform: 'scale(1) rotate(0deg)'},
|
||||
'30%': {transform: 'scale(1.15) rotate(-10deg)'},
|
||||
'60%': {transform: 'scale(0.9) rotate(5deg)'}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'modal-open': 'modal-open 0.3s ease-out forwards',
|
||||
'fade-in': 'fade-in 0.5s ease-in-out forwards',
|
||||
'slide-down': 'slide-down 0.4s cubic-bezier(0.16, 1, 0.3, 1)'
|
||||
'slide-down': 'slide-down 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
wiggle: 'wiggle 0.3s ease-in-out',
|
||||
'check-bounce': 'check-bounce 0.3s ease-in-out'
|
||||
},
|
||||
colors: {
|
||||
'dark-bg': '#1a1c23',
|
||||
|
||||
Reference in New Issue
Block a user