Cleaning up
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
import Link from '../../components/Link';
|
|
||||||
|
|
||||||
import Store from '../../store';
|
import Store from '../../store';
|
||||||
import * as selectors from '../../store/selectors';
|
import * as selectors from '../../store/selectors';
|
||||||
|
|
||||||
@@ -12,7 +10,6 @@ import {
|
|||||||
IonItem,
|
IonItem,
|
||||||
IonLabel,
|
IonLabel,
|
||||||
} from '@ionic/react';
|
} from '@ionic/react';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
const ListEntry = ({ list, ...props }) => (
|
const ListEntry = ({ list, ...props }) => (
|
||||||
<IonItem href={`/tabs/lists/${list.id}`} className="list-entry">
|
<IonItem href={`/tabs/lists/${list.id}`} className="list-entry">
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
const Backdrop = ({ open, onClose }) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={onClose}
|
|
||||||
className={classNames('fixed z-10 inset-0 bg-black transition-opacity w-full h-full', {
|
|
||||||
'pointer-events-none': !open,
|
|
||||||
'opacity-10': open,
|
|
||||||
'opacity-0': !open,
|
|
||||||
})}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Backdrop;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
const Button = ({ children, className, ...props }) => (
|
|
||||||
<button
|
|
||||||
{...props}
|
|
||||||
className={classNames(
|
|
||||||
'inline-block text-xs font-medium leading-6 text-center uppercase transition rounded-lg ripple focus:outline-none',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Button;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
const EdgeDrag = () => null;
|
|
||||||
|
|
||||||
export default EdgeDrag;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
const Icon = ({ icon, ...props }) => {
|
|
||||||
const svg = icon.replace('data:image/svg+xml;utf8,', '');
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
className={classNames('ui-icon', {
|
|
||||||
'ion-icon': true,
|
|
||||||
'ion-color': true,
|
|
||||||
[props.className]: true,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="icon-inner" dangerouslySetInnerHTML={{ __html: svg }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Icon;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
const List = ({ children, ...props }) => <div {...props}>{children}</div>;
|
|
||||||
|
|
||||||
export default List;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
const ListItem = ({ children, className, ...props }) => (
|
|
||||||
<div className={classNames('p-4', className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ListItem;
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import { Plugins } from '@capacitor/core';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
import { useDrag } from 'react-use-gesture';
|
|
||||||
|
|
||||||
const { DarkMode } = Plugins;
|
|
||||||
|
|
||||||
const Menu = ({ open, onClose, children, className, ...props }) => {
|
|
||||||
const ref = useRef();
|
|
||||||
const [x, setX] = useState(-100000);
|
|
||||||
const [rect, setRect] = useState(null);
|
|
||||||
const [dragging, setDragging] = useState(false);
|
|
||||||
|
|
||||||
useEffect(async () => {
|
|
||||||
try {
|
|
||||||
let darkmodeConfig = await DarkMode.isDarkModeOn();
|
|
||||||
console.log({ open, darkMode: darkmodeConfig.isDarkModeOn });
|
|
||||||
Plugins.StatusBar.setStyle({
|
|
||||||
style: open && !darkmodeConfig.isDarkModeOn ? 'LIGHT' : 'DARK',
|
|
||||||
}).catch(() => {});
|
|
||||||
} catch (e) {}
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const rect = ref.current?.getBoundingClientRect();
|
|
||||||
setRect(rect);
|
|
||||||
setX(-rect.width);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
setX(0);
|
|
||||||
} else if (rect) {
|
|
||||||
setX(-rect.width);
|
|
||||||
}
|
|
||||||
}, [rect, open]);
|
|
||||||
|
|
||||||
const bind = useDrag(
|
|
||||||
({ down, movement: [mx] }) => {
|
|
||||||
setX(mx > 0 ? 0 : mx);
|
|
||||||
|
|
||||||
if (down) {
|
|
||||||
setDragging(true);
|
|
||||||
} else {
|
|
||||||
setDragging(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the drag ended, snap the menu back
|
|
||||||
if (!down) {
|
|
||||||
const mid = -rect.width;
|
|
||||||
if (x < mid / 2) {
|
|
||||||
// Close
|
|
||||||
setX(-rect.width);
|
|
||||||
onClose();
|
|
||||||
} else {
|
|
||||||
// Re-open
|
|
||||||
setX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
axis: 'x',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
{...bind()}
|
|
||||||
ref={ref}
|
|
||||||
style={{
|
|
||||||
paddingTop: `calc(env(safe-area-inset-top, 0px) + 8px)`,
|
|
||||||
paddingBottom: `calc(env(safe-area-inset-bottom, 0px) + 8px)`,
|
|
||||||
touchAction: 'pan-x',
|
|
||||||
transform: `translateX(${x}px)`,
|
|
||||||
}}
|
|
||||||
className={classNames(
|
|
||||||
'fixed z-40 transform-gpu translate w-48 h-full bg-gray-100 dark:bg-gray-800',
|
|
||||||
className,
|
|
||||||
{
|
|
||||||
'transition-transform': !dragging,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Menu;
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
||||||
import { useDrag } from 'react-use-gesture';
|
|
||||||
import Store from '../../store';
|
|
||||||
import { SafeAreaContext } from './SafeArea';
|
|
||||||
|
|
||||||
// A Modal window that slides in from offscreen and can be closed
|
|
||||||
// by dragging.
|
|
||||||
const Modal = ({ open, onClose, children }) => {
|
|
||||||
const ref = useRef();
|
|
||||||
const [dragging, setDragging] = useState(false);
|
|
||||||
const [rect, setRect] = useState(null);
|
|
||||||
const [y, setY] = useState(100000);
|
|
||||||
|
|
||||||
const { top: safeAreaTop } = useContext(SafeAreaContext);
|
|
||||||
|
|
||||||
const _open = useCallback(() => {
|
|
||||||
setY(safeAreaTop);
|
|
||||||
}, [safeAreaTop]);
|
|
||||||
|
|
||||||
const _close = useCallback(() => {
|
|
||||||
if (!rect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setY(rect.height + safeAreaTop);
|
|
||||||
}, [safeAreaTop, rect]);
|
|
||||||
|
|
||||||
// Get the layout rectangle for the modal
|
|
||||||
useEffect(() => {
|
|
||||||
const rect = ref.current?.getBoundingClientRect();
|
|
||||||
setRect(rect);
|
|
||||||
_close();
|
|
||||||
}, [safeAreaTop]);
|
|
||||||
|
|
||||||
// If open changes, open/close the modal
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
_open();
|
|
||||||
} else {
|
|
||||||
_close();
|
|
||||||
}
|
|
||||||
}, [rect, open, _open, _close]);
|
|
||||||
|
|
||||||
const bind = useDrag(
|
|
||||||
({ down, movement: [mx, my] }) => {
|
|
||||||
setY(my < 0 ? safeAreaTop : my + safeAreaTop);
|
|
||||||
|
|
||||||
if (down) {
|
|
||||||
setDragging(true);
|
|
||||||
} else {
|
|
||||||
setDragging(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the drag ended, snap the menu back open or close it
|
|
||||||
if (!down) {
|
|
||||||
const mid = rect.height;
|
|
||||||
if (y > mid / 2) {
|
|
||||||
// Close
|
|
||||||
_close();
|
|
||||||
onClose();
|
|
||||||
} else {
|
|
||||||
// Re-open
|
|
||||||
_open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
axis: 'y',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
{...bind()}
|
|
||||||
className={classNames(
|
|
||||||
'fixed z-40 top-5 transform transform-gpu w-full h-full bg-white rounded-t-xl',
|
|
||||||
{
|
|
||||||
'ease-in-out duration-300 transition-transformation': !dragging,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
'--safe-area-top': `env(safe-area-inset-top, 0px)`,
|
|
||||||
height: `calc(100% - env(safe-area-inset-top, 0px) - 1.25rem)`,
|
|
||||||
touchAction: 'pan-y',
|
|
||||||
transform: `translateY(${y}px)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Modal;
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { Plugins } from '@capacitor/core';
|
|
||||||
import * as actions from '../../store/actions';
|
|
||||||
|
|
||||||
const Nav = ({ page }) => {
|
|
||||||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
|
||||||
|
|
||||||
const title = typeof page?.title === 'function' ? page?.title() : page?.title || '';
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Plugins.StatusBar.setStyle({
|
|
||||||
style: 'DARK',
|
|
||||||
}).catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav
|
|
||||||
className="bg-gray-800 w-full flex-0 flex items-end flex-row z-10"
|
|
||||||
style={{
|
|
||||||
height: `calc(env(safe-area-inset-bottom, 0px) + 64px)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 flex-1">
|
|
||||||
<div className="relative flex items-center justify-between h-16">
|
|
||||||
<div
|
|
||||||
className="absolute inset-y-0 left-0 flex items-center sm:hidden"
|
|
||||||
onClick={() => actions.setMenuOpen(true)}
|
|
||||||
>
|
|
||||||
{/* Mobile menu button*/}
|
|
||||||
<button
|
|
||||||
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Open main menu</span>
|
|
||||||
{/* Icon when menu is closed. */}
|
|
||||||
{/*
|
|
||||||
Heroicon name: menu
|
|
||||||
|
|
||||||
Menu open: "hidden", Menu closed: "block"
|
|
||||||
*/}
|
|
||||||
<svg
|
|
||||||
className="block h-6 w-6"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
d="M4 6h16M4 12h16M4 18h16"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{/* Icon when menu is open. */}
|
|
||||||
{/*
|
|
||||||
Heroicon name: x
|
|
||||||
|
|
||||||
Menu open: "block", Menu closed: "hidden"
|
|
||||||
*/}
|
|
||||||
<svg
|
|
||||||
className="hidden h-6 w-6"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
d="M6 18L18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
|
||||||
<div className="flex-shrink-0 flex items-center">
|
|
||||||
<h1 className="text-gray-50">{title}</h1>
|
|
||||||
</div>
|
|
||||||
<div className="hidden sm:block sm:ml-6">
|
|
||||||
<div className="flex space-x-4">
|
|
||||||
{/* Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" */}
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="bg-gray-900 text-white px-3 py-2 rounded-md text-sm font-medium"
|
|
||||||
>
|
|
||||||
Dashboard
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
|
||||||
>
|
|
||||||
Team
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
|
||||||
>
|
|
||||||
Projects
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
|
||||||
>
|
|
||||||
Calendar
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
|
|
||||||
<button
|
|
||||||
className="bg-gray-800 p-1 rounded-full text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
|
|
||||||
onClick={() => actions.setNotificationsOpen(true)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">View notifications</span>
|
|
||||||
{/* Heroicon name: bell */}
|
|
||||||
<svg
|
|
||||||
className="h-6 w-6"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Profile dropdown */}
|
|
||||||
<div className="ml-3 relative">
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
className="bg-gray-800 flex text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
|
|
||||||
id="user-menu"
|
|
||||||
aria-haspopup="true"
|
|
||||||
onClick={() => setShowProfileMenu(!showProfileMenu)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Open user menu</span>
|
|
||||||
<img
|
|
||||||
className="h-8 w-8 rounded-full"
|
|
||||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/*
|
|
||||||
Profile dropdown panel, show/hide based on dropdown state.
|
|
||||||
|
|
||||||
Entering: "transition ease-out duration-100"
|
|
||||||
From: "transform opacity-0 scale-95"
|
|
||||||
To: "transform opacity-100 scale-100"
|
|
||||||
Leaving: "transition ease-in duration-75"
|
|
||||||
From: "transform opacity-100 scale-100"
|
|
||||||
To: "transform opacity-0 scale-95"
|
|
||||||
*/}
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
showProfileMenu ? '' : 'hidden'
|
|
||||||
} origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5`}
|
|
||||||
role="menu"
|
|
||||||
aria-orientation="vertical"
|
|
||||||
aria-labelledby="user-menu"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400"
|
|
||||||
role="menuitem"
|
|
||||||
>
|
|
||||||
Your Profile
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400"
|
|
||||||
role="menuitem"
|
|
||||||
>
|
|
||||||
Settings
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400"
|
|
||||||
role="menuitem"
|
|
||||||
>
|
|
||||||
Sign out
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Nav;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# UI Components
|
|
||||||
|
|
||||||
These components are a mini-tailwind UI framework for building native mobile apps with web technologies.
|
|
||||||
|
|
||||||
These components are meant to be modified and customized to fit your app, and provide many common mobile UI patterns.
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { createContext, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
export const SafeAreaContext = createContext({ top: 0, bottom: 0 });
|
|
||||||
|
|
||||||
// This provider reads and stores the computed safe area
|
|
||||||
// on devices with notches/etc. (iPhone X, for example)
|
|
||||||
//
|
|
||||||
// This is done by reading the CSS Properties --safe-area-top and --safe-area-bottom
|
|
||||||
// and then storing them as state values.
|
|
||||||
//
|
|
||||||
// These values are useful for JS-driven interactions, such as a modal that
|
|
||||||
// will drag in and out but needs to offset for the safe region.
|
|
||||||
export const SafeAreaProvider = ({ children }) => {
|
|
||||||
const [safeArea, setSafeArea] = useState({ top: 0, right: 0, bottom: 0, left: 0 });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// I don't know why, but we can't get the value of this CSS variable
|
|
||||||
// until a bit of a delay, maybe something with Next?
|
|
||||||
setTimeout(() => {
|
|
||||||
const safeAreaTop = parseInt(
|
|
||||||
window.getComputedStyle(document.documentElement).getPropertyValue('--safe-area-top')
|
|
||||||
);
|
|
||||||
const safeAreaBottom = parseInt(
|
|
||||||
window.getComputedStyle(document.documentElement).getPropertyValue('--safe-area-bottom')
|
|
||||||
);
|
|
||||||
const safeAreaLeft = parseInt(
|
|
||||||
window.getComputedStyle(document.documentElement).getPropertyValue('--safe-area-left')
|
|
||||||
);
|
|
||||||
const safeAreaRight = parseInt(
|
|
||||||
window.getComputedStyle(document.documentElement).getPropertyValue('--safe-area-right')
|
|
||||||
);
|
|
||||||
|
|
||||||
setSafeArea({
|
|
||||||
top: safeAreaTop,
|
|
||||||
right: safeAreaRight,
|
|
||||||
bottom: safeAreaBottom,
|
|
||||||
left: safeAreaLeft,
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <SafeAreaContext.Provider value={safeArea}>{children}</SafeAreaContext.Provider>;
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user