refactor: improve map preview transition and layout stability with opacity animations and adjusted loading delay
Build and Release / release (push) Successful in 36s
Build and Release / release (push) Successful in 36s
This commit is contained in:
@@ -141,8 +141,8 @@ export default function MapPlaceholder({ onEnter }: MapPlaceholderProps) {
|
|||||||
<style dangerouslySetInnerHTML={{
|
<style dangerouslySetInnerHTML={{
|
||||||
__html: `
|
__html: `
|
||||||
@keyframes placeholder-pulse {
|
@keyframes placeholder-pulse {
|
||||||
0%, 100% { opacity: 0.35; transform: scale(0.98); }
|
0%, 100% { opacity: 0.35; }
|
||||||
50% { opacity: 0.95; transform: scale(1); }
|
50% { opacity: 0.95; }
|
||||||
}
|
}
|
||||||
`}} />
|
`}} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export default function PublicPreviewClientPage({
|
|||||||
} else {
|
} else {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setLoadInteractiveMap(true);
|
setLoadInteractiveMap(true);
|
||||||
}, 2000);
|
}, 2500);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [instantLoad]);
|
}, [instantLoad]);
|
||||||
@@ -794,102 +794,213 @@ export default function PublicPreviewClientPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isBackgroundVisibilityReady && loadInteractiveMap && (
|
<div
|
||||||
<PreviewMapShell
|
style={{
|
||||||
mapHandleRef={mapHandleRef}
|
position: "absolute",
|
||||||
renderDraft={filteredRenderDraft}
|
inset: 0,
|
||||||
labelContextDraft={filteredLabelContextDraft}
|
visibility: userHasEntered ? "visible" : "hidden",
|
||||||
labelTimelineYear={currentTimelineYear}
|
opacity: userHasEntered ? 1 : 0,
|
||||||
selectedFeatureIds={selectedFeatureIds}
|
transition: "opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.8s",
|
||||||
onSelectFeatureIds={setSelectedFeatureIds}
|
}}
|
||||||
instantLoad={instantLoad}
|
>
|
||||||
onToggleInstantLoad={toggleInstantLoad}
|
{isBackgroundVisibilityReady && loadInteractiveMap && (
|
||||||
isLayerPanelVisible={isLayerPanelVisible}
|
<PreviewMapShell
|
||||||
onLayerPanelVisibleChange={setIsLayerPanelVisible}
|
mapHandleRef={mapHandleRef}
|
||||||
backgroundVisibility={backgroundVisibility}
|
renderDraft={filteredRenderDraft}
|
||||||
geometryVisibility={geometryVisibility}
|
labelContextDraft={filteredLabelContextDraft}
|
||||||
onToggleBackground={handleToggleBackgroundLayer}
|
labelTimelineYear={currentTimelineYear}
|
||||||
onToggleGeometry={(typeKey) => {
|
selectedFeatureIds={selectedFeatureIds}
|
||||||
setGeometryVisibility((prev) => ({
|
onSelectFeatureIds={setSelectedFeatureIds}
|
||||||
...prev,
|
instantLoad={instantLoad}
|
||||||
[typeKey]: prev[typeKey] === false,
|
onToggleInstantLoad={toggleInstantLoad}
|
||||||
}));
|
isLayerPanelVisible={isLayerPanelVisible}
|
||||||
}}
|
onLayerPanelVisibleChange={setIsLayerPanelVisible}
|
||||||
timelineYear={currentTimelineYear}
|
backgroundVisibility={backgroundVisibility}
|
||||||
onTimelineYearChange={handleTimelineYearChange}
|
geometryVisibility={geometryVisibility}
|
||||||
timelineTimeRange={timeRange}
|
onToggleBackground={handleToggleBackgroundLayer}
|
||||||
onTimelineTimeRangeChange={handleTimeRangeChange}
|
onToggleGeometry={(typeKey) => {
|
||||||
isTimelineLoading={isTimelineLoading || isRelationsLoading}
|
setGeometryVisibility((prev) => ({
|
||||||
timelineStatusText={relationsStatus || timelineStatus}
|
...prev,
|
||||||
timelineStyle={computedTimelineStyle}
|
[typeKey]: prev[typeKey] === false,
|
||||||
onFeatureClick={handleMapFeatureClick}
|
}));
|
||||||
hoverPopupEnabled
|
}}
|
||||||
getHoverPopupContent={getHoverPopupContent}
|
timelineYear={currentTimelineYear}
|
||||||
activeEntity={displayedActiveEntity}
|
onTimelineYearChange={handleTimelineYearChange}
|
||||||
activeWiki={displayedActiveWiki}
|
timelineTimeRange={timeRange}
|
||||||
isWikiLoading={isActiveWikiLoading}
|
onTimelineTimeRangeChange={handleTimeRangeChange}
|
||||||
wikiError={activeWikiError}
|
isTimelineLoading={isTimelineLoading || isRelationsLoading}
|
||||||
onCloseWikiSidebar={handleCloseWikiSidebar}
|
timelineStatusText={relationsStatus || timelineStatus}
|
||||||
onWikiLinkRequest={handlePanelWikiLinkRequest}
|
timelineStyle={computedTimelineStyle}
|
||||||
onWikiLinkEntitySelectionRequest={handlePanelWikiLinkEntitySelectionRequest}
|
onFeatureClick={handleMapFeatureClick}
|
||||||
sidebarWidth={sidebarWidth}
|
hoverPopupEnabled
|
||||||
onSidebarWidthChange={setSidebarWidth}
|
getHoverPopupContent={getHoverPopupContent}
|
||||||
maxSidebarDragWidth={maxDragWidth}
|
activeEntity={displayedActiveEntity}
|
||||||
sidebarHeight={sidebarHeight}
|
activeWiki={displayedActiveWiki}
|
||||||
onSidebarHeightChange={handleSidebarHeightChange}
|
isWikiLoading={isActiveWikiLoading}
|
||||||
showViewportControls={false}
|
wikiError={activeWikiError}
|
||||||
onPlayPreviewReplay={activeReplay && replayMode === "idle" ? handlePlayPreviewReplay : undefined}
|
onCloseWikiSidebar={handleCloseWikiSidebar}
|
||||||
timelineDisabled={replayMode !== "idle"}
|
onWikiLinkRequest={handlePanelWikiLinkRequest}
|
||||||
hasAnyBottomPanel={isWikiChooserOpen || isGeometryChooserOpen}
|
onWikiLinkEntitySelectionRequest={handlePanelWikiLinkEntitySelectionRequest}
|
||||||
overlay={
|
sidebarWidth={sidebarWidth}
|
||||||
replayMode !== "idle" ? (
|
onSidebarWidthChange={setSidebarWidth}
|
||||||
<ReplayPreviewOverlay
|
maxSidebarDragWidth={maxDragWidth}
|
||||||
isPreviewMode={true}
|
sidebarHeight={sidebarHeight}
|
||||||
isPlaying={replayPreview.isPlaying}
|
onSidebarHeightChange={handleSidebarHeightChange}
|
||||||
dialog={replayPreview.dialog}
|
showViewportControls={false}
|
||||||
toasts={replayPreview.toasts}
|
onPlayPreviewReplay={activeReplay && replayMode === "idle" ? handlePlayPreviewReplay : undefined}
|
||||||
sidebarOpen={isSidebarOpen}
|
timelineDisabled={replayMode !== "idle"}
|
||||||
sidebarWidth={sidebarWidth}
|
hasAnyBottomPanel={isWikiChooserOpen || isGeometryChooserOpen}
|
||||||
playbackSpeed={replayPreview.playbackSpeed}
|
overlay={
|
||||||
activeStepLabel={activeStepLabel}
|
replayMode !== "idle" ? (
|
||||||
activeStepNumber={replayPreview.activeStepNumber}
|
<ReplayPreviewOverlay
|
||||||
totalSteps={replayPreview.totalSteps}
|
isPreviewMode={true}
|
||||||
playButtonLabel={replayMode === "paused" ? "Tiếp tục" : "Phát lại"}
|
isPlaying={replayPreview.isPlaying}
|
||||||
onPlayPreview={handleResumePreviewReplay}
|
dialog={replayPreview.dialog}
|
||||||
onStopPreview={handleStopPreviewReplay}
|
toasts={replayPreview.toasts}
|
||||||
onResetPreview={handleResetPreviewReplay}
|
sidebarOpen={isSidebarOpen}
|
||||||
onExitPreview={handleExitReplay}
|
sidebarWidth={sidebarWidth}
|
||||||
|
playbackSpeed={replayPreview.playbackSpeed}
|
||||||
|
activeStepLabel={activeStepLabel}
|
||||||
|
activeStepNumber={replayPreview.activeStepNumber}
|
||||||
|
totalSteps={replayPreview.totalSteps}
|
||||||
|
playButtonLabel={replayMode === "paused" ? "Tiếp tục" : "Phát lại"}
|
||||||
|
onPlayPreview={handleResumePreviewReplay}
|
||||||
|
onStopPreview={handleStopPreviewReplay}
|
||||||
|
onResetPreview={handleResetPreviewReplay}
|
||||||
|
onExitPreview={handleExitReplay}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div style={searchBarWrapperStyle}>
|
||||||
|
<PresentPlaceSearch
|
||||||
|
focusedPlace={focusedPresentPlace}
|
||||||
|
onFocusPlace={handleFocusPresentPlace}
|
||||||
|
onFocusHistoricalGeometry={handleFocusHistoricalGeometry}
|
||||||
|
onFocusWiki={handleFocusWiki}
|
||||||
|
onClearFocus={clearPresentPlaceFocus}
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
transform: "none",
|
||||||
|
width: searchBarWidth,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : null
|
{isLargeScreen ? (
|
||||||
}
|
<PublicMapZoomPanel
|
||||||
>
|
mapHandleRef={mapHandleRef}
|
||||||
<div style={searchBarWrapperStyle}>
|
onPlayPreviewReplay={activeReplay && replayMode === "idle" ? handlePlayPreviewReplay : undefined}
|
||||||
<PresentPlaceSearch
|
onResumePreviewReplay={replayMode === "paused" ? handleResumePreviewReplay : undefined}
|
||||||
focusedPlace={focusedPresentPlace}
|
onStopPreviewReplay={replayMode === "playing" ? handleStopPreviewReplay : undefined}
|
||||||
onFocusPlace={handleFocusPresentPlace}
|
/>
|
||||||
onFocusHistoricalGeometry={handleFocusHistoricalGeometry}
|
) : null}
|
||||||
onFocusWiki={handleFocusWiki}
|
</div>
|
||||||
onClearFocus={clearPresentPlaceFocus}
|
<FirstVisitGuideModal />
|
||||||
style={{
|
</PreviewMapShell>
|
||||||
position: "relative",
|
)}
|
||||||
top: 0,
|
|
||||||
left: 0,
|
{linkEntityPopup ? (
|
||||||
transform: "none",
|
<div
|
||||||
width: searchBarWidth,
|
ref={linkEntityPopupRef}
|
||||||
|
className="fixed z-[60] w-[240px] overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xl dark:border-gray-800 dark:bg-gray-950"
|
||||||
|
style={{ top: linkEntityPopup.top, left: linkEntityPopup.left }}
|
||||||
|
>
|
||||||
|
<div className="border-b border-gray-200 px-3 py-2 dark:border-gray-800">
|
||||||
|
<div className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
Related Entities
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
/wiki/{linkEntityPopup.slug}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-[220px] overflow-y-auto p-2">
|
||||||
|
{linkEntityPopup.entities.length ? (
|
||||||
|
<div className="grid gap-1">
|
||||||
|
{linkEntityPopup.entities.map((entity) => (
|
||||||
|
<button
|
||||||
|
key={entity.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setWikiSelectionPanelAnchor(null);
|
||||||
|
setRightPanelMode("wiki");
|
||||||
|
selectEntity(entity.id, { preferredWikiSlug: linkEntityPopup.slug });
|
||||||
|
setLinkEntityPopup(null);
|
||||||
|
}}
|
||||||
|
className="rounded-lg px-3 py-2 text-left text-sm text-gray-700 transition hover:bg-gray-50 hover:text-gray-900 dark:text-gray-200 dark:hover:bg-white/[0.04] dark:hover:text-white"
|
||||||
|
>
|
||||||
|
{entity.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Không có entity liên quan.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isGeometryChooserOpen && geometrySelectionPanel ? (
|
||||||
|
<aside
|
||||||
|
className={isLargeScreen ? "fixed bottom-4 right-4 top-4 left-auto z-20 max-w-[calc(100vw-2rem)]" : "fixed bottom-0 left-0 right-0 top-auto z-20"}
|
||||||
|
style={isLargeScreen ? {
|
||||||
|
width: `min(${sidebarWidth}px, calc(100vw - 2rem))`,
|
||||||
|
} : {
|
||||||
|
height: `${sidebarHeight || 400}px`,
|
||||||
|
maxHeight: "90vh",
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GeometrySelectionPanel
|
||||||
|
wikiSlug={geometrySelectionPanel.wikiSlug}
|
||||||
|
rows={geometrySelectionPanel.rows}
|
||||||
|
isLoading={geometrySelectionPanel.isLoading}
|
||||||
|
error={geometrySelectionPanel.error}
|
||||||
|
onClose={() => {
|
||||||
|
setGeometrySelectionPanel(null);
|
||||||
|
setRightPanelMode(null);
|
||||||
|
}}
|
||||||
|
onSelectEntity={handleGeometrySelectionEntitySelect}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isWikiChooserOpen ? (
|
||||||
|
<aside
|
||||||
|
className={isLargeScreen ? "fixed bottom-4 right-4 top-4 left-auto z-20 max-w-[calc(100vw-2rem)]" : "fixed bottom-0 left-0 right-0 top-auto z-20"}
|
||||||
|
style={isLargeScreen ? {
|
||||||
|
width: `min(${sidebarWidth}px, calc(100vw - 2rem))`,
|
||||||
|
} : {
|
||||||
|
height: `${sidebarHeight || 400}px`,
|
||||||
|
maxHeight: "90vh",
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WikiSelectionPanel
|
||||||
|
rows={wikiSelectionPanelRows}
|
||||||
|
onClose={() => {
|
||||||
|
setWikiSelectionPanelAnchor(null);
|
||||||
|
setRightPanelMode(null);
|
||||||
|
}}
|
||||||
|
onSelectRow={(entityId, wikiId) => {
|
||||||
|
const wiki = wikiSelectionPanelRows.find((row) => row.entity.id === entityId && row.wiki.id === wikiId)?.wiki || null;
|
||||||
|
const sourceFeatureId = wikiSelectionPanelAnchor?.featureId ?? null;
|
||||||
|
setWikiSelectionPanelAnchor(null);
|
||||||
|
setRightPanelMode("wiki");
|
||||||
|
selectEntity(entityId, {
|
||||||
|
sourceFeatureId,
|
||||||
|
preferredWikiSlug: wiki?.slug,
|
||||||
|
selectGeometry: false,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{isLargeScreen ? (
|
</aside>
|
||||||
<PublicMapZoomPanel
|
) : null}
|
||||||
mapHandleRef={mapHandleRef}
|
</div>
|
||||||
onPlayPreviewReplay={activeReplay && replayMode === "idle" ? handlePlayPreviewReplay : undefined}
|
|
||||||
onResumePreviewReplay={replayMode === "paused" ? handleResumePreviewReplay : undefined}
|
|
||||||
onStopPreviewReplay={replayMode === "playing" ? handleStopPreviewReplay : undefined}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<FirstVisitGuideModal />
|
|
||||||
</PreviewMapShell>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Smooth transition loading overlay */}
|
{/* Smooth transition loading overlay */}
|
||||||
<div
|
<div
|
||||||
@@ -905,107 +1016,6 @@ export default function PublicPreviewClientPage({
|
|||||||
>
|
>
|
||||||
<MapPlaceholder onEnter={onEnter} />
|
<MapPlaceholder onEnter={onEnter} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{linkEntityPopup ? (
|
|
||||||
<div
|
|
||||||
ref={linkEntityPopupRef}
|
|
||||||
className="fixed z-[60] w-[240px] overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xl dark:border-gray-800 dark:bg-gray-950"
|
|
||||||
style={{ top: linkEntityPopup.top, left: linkEntityPopup.left }}
|
|
||||||
>
|
|
||||||
<div className="border-b border-gray-200 px-3 py-2 dark:border-gray-800">
|
|
||||||
<div className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
Related Entities
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
/wiki/{linkEntityPopup.slug}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="max-h-[220px] overflow-y-auto p-2">
|
|
||||||
{linkEntityPopup.entities.length ? (
|
|
||||||
<div className="grid gap-1">
|
|
||||||
{linkEntityPopup.entities.map((entity) => (
|
|
||||||
<button
|
|
||||||
key={entity.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setWikiSelectionPanelAnchor(null);
|
|
||||||
setRightPanelMode("wiki");
|
|
||||||
selectEntity(entity.id, { preferredWikiSlug: linkEntityPopup.slug });
|
|
||||||
setLinkEntityPopup(null);
|
|
||||||
}}
|
|
||||||
className="rounded-lg px-3 py-2 text-left text-sm text-gray-700 transition hover:bg-gray-50 hover:text-gray-900 dark:text-gray-200 dark:hover:bg-white/[0.04] dark:hover:text-white"
|
|
||||||
>
|
|
||||||
{entity.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Không có entity liên quan.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{isGeometryChooserOpen && geometrySelectionPanel ? (
|
|
||||||
<aside
|
|
||||||
className={isLargeScreen ? "fixed bottom-4 right-4 top-4 left-auto z-20 max-w-[calc(100vw-2rem)]" : "fixed bottom-0 left-0 right-0 top-auto z-20"}
|
|
||||||
style={isLargeScreen ? {
|
|
||||||
width: `min(${sidebarWidth}px, calc(100vw - 2rem))`,
|
|
||||||
} : {
|
|
||||||
height: `${sidebarHeight || 400}px`,
|
|
||||||
maxHeight: "90vh",
|
|
||||||
width: "100%",
|
|
||||||
maxWidth: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<GeometrySelectionPanel
|
|
||||||
wikiSlug={geometrySelectionPanel.wikiSlug}
|
|
||||||
rows={geometrySelectionPanel.rows}
|
|
||||||
isLoading={geometrySelectionPanel.isLoading}
|
|
||||||
error={geometrySelectionPanel.error}
|
|
||||||
onClose={() => {
|
|
||||||
setGeometrySelectionPanel(null);
|
|
||||||
setRightPanelMode(null);
|
|
||||||
}}
|
|
||||||
onSelectEntity={handleGeometrySelectionEntitySelect}
|
|
||||||
/>
|
|
||||||
</aside>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{isWikiChooserOpen ? (
|
|
||||||
<aside
|
|
||||||
className={isLargeScreen ? "fixed bottom-4 right-4 top-4 left-auto z-20 max-w-[calc(100vw-2rem)]" : "fixed bottom-0 left-0 right-0 top-auto z-20"}
|
|
||||||
style={isLargeScreen ? {
|
|
||||||
width: `min(${sidebarWidth}px, calc(100vw - 2rem))`,
|
|
||||||
} : {
|
|
||||||
height: `${sidebarHeight || 400}px`,
|
|
||||||
maxHeight: "90vh",
|
|
||||||
width: "100%",
|
|
||||||
maxWidth: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<WikiSelectionPanel
|
|
||||||
rows={wikiSelectionPanelRows}
|
|
||||||
onClose={() => {
|
|
||||||
setWikiSelectionPanelAnchor(null);
|
|
||||||
setRightPanelMode(null);
|
|
||||||
}}
|
|
||||||
onSelectRow={(entityId, wikiId) => {
|
|
||||||
const wiki = wikiSelectionPanelRows.find((row) => row.entity.id === entityId && row.wiki.id === wikiId)?.wiki || null;
|
|
||||||
const sourceFeatureId = wikiSelectionPanelAnchor?.featureId ?? null;
|
|
||||||
setWikiSelectionPanelAnchor(null);
|
|
||||||
setRightPanelMode("wiki");
|
|
||||||
selectEntity(entityId, {
|
|
||||||
sourceFeatureId,
|
|
||||||
preferredWikiSlug: wiki?.slug,
|
|
||||||
selectGeometry: false,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</aside>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,14 +88,6 @@ export default function PublicPreviewWrapper() {
|
|||||||
};
|
};
|
||||||
}, [handleEnter, mapEntered]);
|
}, [handleEnter, mapEntered]);
|
||||||
|
|
||||||
if (!mapEntered && !instantLoad) {
|
|
||||||
return (
|
|
||||||
<MapPlaceholder
|
|
||||||
onEnter={handleEnter}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PublicPreviewClientPage
|
<PublicPreviewClientPage
|
||||||
userHasEntered={mapEntered}
|
userHasEntered={mapEntered}
|
||||||
|
|||||||
Reference in New Issue
Block a user