feat:update and delete finds
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { Map } from '$lib';
|
||||
import FindsList from '$lib/components/finds/FindsList.svelte';
|
||||
import CreateFindModal from '$lib/components/finds/CreateFindModal.svelte';
|
||||
import EditFindModal from '$lib/components/finds/EditFindModal.svelte';
|
||||
import FindPreview from '$lib/components/finds/FindPreview.svelte';
|
||||
import FindsFilter from '$lib/components/finds/FindsFilter.svelte';
|
||||
import type { PageData } from './$types';
|
||||
@@ -10,6 +11,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { apiSync, type FindState } from '$lib/stores/api-sync';
|
||||
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
|
||||
// Server response type
|
||||
interface ServerFind {
|
||||
@@ -59,9 +61,11 @@
|
||||
isLiked?: boolean;
|
||||
isFromFriend?: boolean;
|
||||
media?: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
url: string;
|
||||
thumbnailUrl: string;
|
||||
orderIndex?: number | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -92,6 +96,8 @@
|
||||
let { data }: { data: PageData & { finds?: ServerFind[] } } = $props();
|
||||
|
||||
let showCreateModal = $state(false);
|
||||
let showEditModal = $state(false);
|
||||
let editingFind: ServerFind | null = $state(null);
|
||||
let selectedFind: FindPreviewData | null = $state(null);
|
||||
let currentFilter = $state('all');
|
||||
let isSidebarVisible = $state(true);
|
||||
@@ -137,10 +143,18 @@
|
||||
isLiked: serverFind.isLikedByUser,
|
||||
isFromFriend: serverFind.isFromFriend,
|
||||
media: serverFind.media?.map(
|
||||
(m: { type: string; url: string; thumbnailUrl: string | null }) => ({
|
||||
(m: {
|
||||
id: string;
|
||||
type: string;
|
||||
url: string;
|
||||
thumbnailUrl: string | null;
|
||||
orderIndex: number | null;
|
||||
}) => ({
|
||||
id: m.id,
|
||||
type: m.type,
|
||||
url: m.url,
|
||||
thumbnailUrl: m.thumbnailUrl || m.url
|
||||
thumbnailUrl: m.thumbnailUrl || m.url,
|
||||
orderIndex: m.orderIndex
|
||||
})
|
||||
)
|
||||
})) as MapFind[]
|
||||
@@ -217,9 +231,80 @@
|
||||
showCreateModal = false;
|
||||
}
|
||||
|
||||
function openEditModal(find: MapFind) {
|
||||
// Convert MapFind type to ServerFind format
|
||||
const serverFind: ServerFind = {
|
||||
id: find.id,
|
||||
title: find.title,
|
||||
description: find.description,
|
||||
latitude: find.latitude || '0',
|
||||
longitude: find.longitude || '0',
|
||||
locationName: find.locationName,
|
||||
category: find.category,
|
||||
isPublic: find.isPublic || 0,
|
||||
createdAt: find.createdAt.toISOString(),
|
||||
userId: find.userId || '',
|
||||
username: find.user?.username || '',
|
||||
profilePictureUrl: find.user?.profilePictureUrl,
|
||||
likeCount: find.likeCount,
|
||||
isLikedByUser: find.isLiked,
|
||||
isFromFriend: find.isFromFriend || false,
|
||||
media: (find.media || []).map((mediaItem) => ({
|
||||
...mediaItem,
|
||||
findId: find.id,
|
||||
thumbnailUrl: mediaItem.thumbnailUrl || null,
|
||||
orderIndex: mediaItem.orderIndex || null
|
||||
}))
|
||||
};
|
||||
editingFind = serverFind;
|
||||
showEditModal = true;
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
showEditModal = false;
|
||||
editingFind = null;
|
||||
}
|
||||
|
||||
function handleFindUpdated() {
|
||||
closeEditModal();
|
||||
handleFindsChanged();
|
||||
}
|
||||
|
||||
function handleFindDeleted() {
|
||||
closeEditModal();
|
||||
handleFindsChanged();
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
isSidebarVisible = !isSidebarVisible;
|
||||
}
|
||||
|
||||
async function handleFindsChanged() {
|
||||
// Reload finds data after a find is updated or deleted
|
||||
if (browser) {
|
||||
try {
|
||||
const params = new SvelteURLSearchParams();
|
||||
if ($coordinates) {
|
||||
params.set('lat', $coordinates.latitude.toString());
|
||||
params.set('lng', $coordinates.longitude.toString());
|
||||
}
|
||||
if (data.user) {
|
||||
params.set('includePrivate', 'true');
|
||||
if (currentFilter === 'friends') {
|
||||
params.set('includeFriends', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/finds?${params}`);
|
||||
if (response.ok) {
|
||||
const updatedFinds = await response.json();
|
||||
data.finds = updatedFinds;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reloading finds:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -248,8 +333,34 @@
|
||||
<Map
|
||||
autoCenter={true}
|
||||
center={[$coordinates?.longitude || 0, $coordinates?.latitude || 51.505]}
|
||||
{finds}
|
||||
onFindClick={handleFindClick}
|
||||
finds={finds.map((find) => ({
|
||||
id: find.id,
|
||||
title: find.title,
|
||||
description: find.description,
|
||||
latitude: find.latitude,
|
||||
longitude: find.longitude,
|
||||
locationName: find.locationName,
|
||||
category: find.category,
|
||||
isPublic: find.isPublic,
|
||||
createdAt: find.createdAt,
|
||||
userId: find.userId,
|
||||
user: {
|
||||
id: find.user.id,
|
||||
username: find.user.username
|
||||
},
|
||||
media: find.media?.map((m) => ({
|
||||
type: m.type,
|
||||
url: m.url,
|
||||
thumbnailUrl: m.thumbnailUrl
|
||||
}))
|
||||
}))}
|
||||
onFindClick={(mapFind) => {
|
||||
// Find the corresponding MapFind from the finds array
|
||||
const originalFind = finds.find((f) => f.id === mapFind.id);
|
||||
if (originalFind) {
|
||||
handleFindClick(originalFind);
|
||||
}
|
||||
}}
|
||||
sidebarVisible={isSidebarVisible}
|
||||
/>
|
||||
</div>
|
||||
@@ -277,7 +388,40 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="finds-list-container">
|
||||
<FindsList {finds} onFindExplore={handleFindExplore} hideTitle={true} />
|
||||
<FindsList
|
||||
finds={finds.map((find) => ({
|
||||
id: find.id,
|
||||
title: find.title,
|
||||
description: find.description,
|
||||
category: find.category,
|
||||
locationName: find.locationName,
|
||||
latitude: find.latitude,
|
||||
longitude: find.longitude,
|
||||
isPublic: find.isPublic,
|
||||
userId: find.userId,
|
||||
user: {
|
||||
username: find.user.username,
|
||||
profilePictureUrl: find.user.profilePictureUrl
|
||||
},
|
||||
likeCount: find.likeCount,
|
||||
isLiked: find.isLiked,
|
||||
media: find.media?.map((m) => ({
|
||||
id: m.id,
|
||||
type: m.type,
|
||||
url: m.url,
|
||||
thumbnailUrl: m.thumbnailUrl,
|
||||
orderIndex: m.orderIndex
|
||||
}))
|
||||
}))}
|
||||
onFindExplore={handleFindExplore}
|
||||
currentUserId={data.user?.id}
|
||||
onFindsChanged={handleFindsChanged}
|
||||
onEdit={(find) => {
|
||||
const mapFind = finds.find((f) => f.id === find.id);
|
||||
if (mapFind) openEditModal(mapFind);
|
||||
}}
|
||||
hideTitle={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Toggle button -->
|
||||
@@ -307,6 +451,26 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if showEditModal && editingFind}
|
||||
<EditFindModal
|
||||
isOpen={showEditModal}
|
||||
find={{
|
||||
id: editingFind.id,
|
||||
title: editingFind.title,
|
||||
description: editingFind.description || null,
|
||||
latitude: editingFind.latitude || '0',
|
||||
longitude: editingFind.longitude || '0',
|
||||
locationName: editingFind.locationName || null,
|
||||
category: editingFind.category || null,
|
||||
isPublic: editingFind.isPublic,
|
||||
media: editingFind.media || []
|
||||
}}
|
||||
onClose={closeEditModal}
|
||||
onFindUpdated={handleFindUpdated}
|
||||
onFindDeleted={handleFindDeleted}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if selectedFind}
|
||||
<FindPreview find={selectedFind} onClose={closeFindPreview} currentUserId={data.user?.id} />
|
||||
{/if}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { RequestHandler } from './$types';
|
||||
import { db } from '$lib/server/db';
|
||||
import { find, findMedia, user, findLike, findComment } from '$lib/server/db/schema';
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
import { getLocalR2Url } from '$lib/server/r2';
|
||||
import { getLocalR2Url, deleteFromR2 } from '$lib/server/r2';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, locals }) => {
|
||||
const findId = params.findId;
|
||||
@@ -113,3 +113,195 @@ export const GET: RequestHandler = async ({ params, locals }) => {
|
||||
throw error(500, 'Failed to load find');
|
||||
}
|
||||
};
|
||||
|
||||
export const PUT: RequestHandler = async ({ params, request, locals }) => {
|
||||
if (!locals.user) {
|
||||
throw error(401, 'Unauthorized');
|
||||
}
|
||||
|
||||
const findId = params.findId;
|
||||
|
||||
if (!findId) {
|
||||
throw error(400, 'Find ID is required');
|
||||
}
|
||||
|
||||
try {
|
||||
// First, verify the find exists and user owns it
|
||||
const existingFind = await db
|
||||
.select({ userId: find.userId })
|
||||
.from(find)
|
||||
.where(eq(find.id, findId))
|
||||
.limit(1);
|
||||
|
||||
if (existingFind.length === 0) {
|
||||
throw error(404, 'Find not found');
|
||||
}
|
||||
|
||||
if (existingFind[0].userId !== locals.user.id) {
|
||||
throw error(403, 'You do not have permission to edit this find');
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
const data = await request.json();
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
latitude,
|
||||
longitude,
|
||||
locationName,
|
||||
category,
|
||||
isPublic,
|
||||
media,
|
||||
mediaToDelete
|
||||
} = data;
|
||||
|
||||
// Validate required fields
|
||||
if (!title || !latitude || !longitude) {
|
||||
throw error(400, 'Title, latitude, and longitude are required');
|
||||
}
|
||||
|
||||
if (title.length > 100) {
|
||||
throw error(400, 'Title must be 100 characters or less');
|
||||
}
|
||||
|
||||
if (description && description.length > 500) {
|
||||
throw error(400, 'Description must be 500 characters or less');
|
||||
}
|
||||
|
||||
// Delete media items if specified
|
||||
if (mediaToDelete && Array.isArray(mediaToDelete) && mediaToDelete.length > 0) {
|
||||
// Get media URLs before deleting from database
|
||||
const mediaToRemove = await db
|
||||
.select({ url: findMedia.url, thumbnailUrl: findMedia.thumbnailUrl })
|
||||
.from(findMedia)
|
||||
.where(
|
||||
sql`${findMedia.id} IN (${sql.join(
|
||||
mediaToDelete.map((id: string) => sql`${id}`),
|
||||
sql`, `
|
||||
)})`
|
||||
);
|
||||
|
||||
// Delete from R2 storage
|
||||
for (const mediaItem of mediaToRemove) {
|
||||
try {
|
||||
await deleteFromR2(mediaItem.url);
|
||||
if (mediaItem.thumbnailUrl && !mediaItem.thumbnailUrl.startsWith('/')) {
|
||||
await deleteFromR2(mediaItem.thumbnailUrl);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error deleting media from R2:', err);
|
||||
// Continue even if R2 deletion fails
|
||||
}
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
await db.delete(findMedia).where(
|
||||
sql`${findMedia.id} IN (${sql.join(
|
||||
mediaToDelete.map((id: string) => sql`${id}`),
|
||||
sql`, `
|
||||
)})`
|
||||
);
|
||||
}
|
||||
|
||||
// Update the find
|
||||
const updatedFind = await db
|
||||
.update(find)
|
||||
.set({
|
||||
title,
|
||||
description: description || null,
|
||||
latitude: latitude.toString(),
|
||||
longitude: longitude.toString(),
|
||||
locationName: locationName || null,
|
||||
category: category || null,
|
||||
isPublic: isPublic ? 1 : 0,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(find.id, findId))
|
||||
.returning();
|
||||
|
||||
// Add new media records if provided
|
||||
if (media && Array.isArray(media) && media.length > 0) {
|
||||
const newMediaRecords = media
|
||||
.filter((item: { id?: string }) => !item.id) // Only insert media without IDs (new uploads)
|
||||
.map((item: { type: string; url: string; thumbnailUrl?: string }, index: number) => ({
|
||||
id: crypto.randomUUID(),
|
||||
findId,
|
||||
type: item.type,
|
||||
url: item.url,
|
||||
thumbnailUrl: item.thumbnailUrl || null,
|
||||
orderIndex: index
|
||||
}));
|
||||
|
||||
if (newMediaRecords.length > 0) {
|
||||
await db.insert(findMedia).values(newMediaRecords);
|
||||
}
|
||||
}
|
||||
|
||||
return json({ success: true, find: updatedFind[0] });
|
||||
} catch (err) {
|
||||
console.error('Error updating find:', err);
|
||||
if (err instanceof Error && 'status' in err) {
|
||||
throw err;
|
||||
}
|
||||
throw error(500, 'Failed to update find');
|
||||
}
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params, locals }) => {
|
||||
if (!locals.user) {
|
||||
throw error(401, 'Unauthorized');
|
||||
}
|
||||
|
||||
const findId = params.findId;
|
||||
|
||||
if (!findId) {
|
||||
throw error(400, 'Find ID is required');
|
||||
}
|
||||
|
||||
try {
|
||||
// First, verify the find exists and user owns it
|
||||
const existingFind = await db
|
||||
.select({ userId: find.userId })
|
||||
.from(find)
|
||||
.where(eq(find.id, findId))
|
||||
.limit(1);
|
||||
|
||||
if (existingFind.length === 0) {
|
||||
throw error(404, 'Find not found');
|
||||
}
|
||||
|
||||
if (existingFind[0].userId !== locals.user.id) {
|
||||
throw error(403, 'You do not have permission to delete this find');
|
||||
}
|
||||
|
||||
// Get all media for this find to delete from R2
|
||||
const mediaItems = await db
|
||||
.select({ url: findMedia.url, thumbnailUrl: findMedia.thumbnailUrl })
|
||||
.from(findMedia)
|
||||
.where(eq(findMedia.findId, findId));
|
||||
|
||||
// Delete media from R2 storage
|
||||
for (const mediaItem of mediaItems) {
|
||||
try {
|
||||
await deleteFromR2(mediaItem.url);
|
||||
if (mediaItem.thumbnailUrl && !mediaItem.thumbnailUrl.startsWith('/')) {
|
||||
await deleteFromR2(mediaItem.thumbnailUrl);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error deleting media from R2:', err);
|
||||
// Continue even if R2 deletion fails
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the find (cascade will handle media, likes, and comments)
|
||||
await db.delete(find).where(eq(find.id, findId));
|
||||
|
||||
return json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('Error deleting find:', err);
|
||||
if (err instanceof Error && 'status' in err) {
|
||||
throw err;
|
||||
}
|
||||
throw error(500, 'Failed to delete find');
|
||||
}
|
||||
};
|
||||
|
||||
68
src/routes/api/finds/[findId]/media/[mediaId]/+server.ts
Normal file
68
src/routes/api/finds/[findId]/media/[mediaId]/+server.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { db } from '$lib/server/db';
|
||||
import { find, findMedia } from '$lib/server/db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { deleteFromR2 } from '$lib/server/r2';
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params, locals }) => {
|
||||
if (!locals.user) {
|
||||
throw error(401, 'Unauthorized');
|
||||
}
|
||||
|
||||
const { findId, mediaId } = params;
|
||||
|
||||
if (!findId || !mediaId) {
|
||||
throw error(400, 'Find ID and Media ID are required');
|
||||
}
|
||||
|
||||
try {
|
||||
// First, verify the find exists and user owns it
|
||||
const existingFind = await db
|
||||
.select({ userId: find.userId })
|
||||
.from(find)
|
||||
.where(eq(find.id, findId))
|
||||
.limit(1);
|
||||
|
||||
if (existingFind.length === 0) {
|
||||
throw error(404, 'Find not found');
|
||||
}
|
||||
|
||||
if (existingFind[0].userId !== locals.user.id) {
|
||||
throw error(403, 'You do not have permission to delete this media');
|
||||
}
|
||||
|
||||
// Get the media item to delete
|
||||
const mediaItem = await db
|
||||
.select({ url: findMedia.url, thumbnailUrl: findMedia.thumbnailUrl })
|
||||
.from(findMedia)
|
||||
.where(and(eq(findMedia.id, mediaId), eq(findMedia.findId, findId)))
|
||||
.limit(1);
|
||||
|
||||
if (mediaItem.length === 0) {
|
||||
throw error(404, 'Media not found');
|
||||
}
|
||||
|
||||
// Delete from R2 storage
|
||||
try {
|
||||
await deleteFromR2(mediaItem[0].url);
|
||||
if (mediaItem[0].thumbnailUrl && !mediaItem[0].thumbnailUrl.startsWith('/')) {
|
||||
await deleteFromR2(mediaItem[0].thumbnailUrl);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error deleting media from R2:', err);
|
||||
// Continue even if R2 deletion fails
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
await db.delete(findMedia).where(eq(findMedia.id, mediaId));
|
||||
|
||||
return json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('Error deleting media:', err);
|
||||
if (err instanceof Error && 'status' in err) {
|
||||
throw err;
|
||||
}
|
||||
throw error(500, 'Failed to delete media');
|
||||
}
|
||||
};
|
||||
@@ -1,15 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Map } from '$lib';
|
||||
import { goto } from '$app/navigation';
|
||||
import LikeButton from '$lib/components/finds/LikeButton.svelte';
|
||||
import VideoPlayer from '$lib/components/media/VideoPlayer.svelte';
|
||||
import ProfilePicture from '$lib/components/profile/ProfilePicture.svelte';
|
||||
import CommentsList from '$lib/components/finds/CommentsList.svelte';
|
||||
import EditFindModal from '$lib/components/finds/EditFindModal.svelte';
|
||||
import { Button } from '$lib/components/button';
|
||||
import { Edit, Trash2 } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let currentMediaIndex = $state(0);
|
||||
let isPanelVisible = $state(true);
|
||||
let showEditModal = $state(false);
|
||||
let isDeleting = $state(false);
|
||||
|
||||
const isOwner = $derived(data.user && data.find && data.user.id === data.find.userId);
|
||||
|
||||
function nextMedia() {
|
||||
if (!data.find?.media) return;
|
||||
@@ -77,6 +85,47 @@
|
||||
isPanelVisible = !isPanelVisible;
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
showEditModal = true;
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!data.find) return;
|
||||
|
||||
if (!confirm('Are you sure you want to delete this find? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
isDeleting = true;
|
||||
try {
|
||||
const response = await fetch(`/api/finds/${data.find.id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete find');
|
||||
}
|
||||
|
||||
// Redirect to home page after successful deletion
|
||||
goto('/');
|
||||
} catch (error) {
|
||||
console.error('Error deleting find:', error);
|
||||
alert('Failed to delete find. Please try again.');
|
||||
isDeleting = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleFindUpdated() {
|
||||
showEditModal = false;
|
||||
// Reload the page to get updated data
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function handleFindDeleted() {
|
||||
showEditModal = false;
|
||||
goto('/');
|
||||
}
|
||||
|
||||
// Create the map find format
|
||||
let mapFinds = $derived(
|
||||
data.find
|
||||
@@ -187,6 +236,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if isOwner}
|
||||
<div class="action-buttons-header">
|
||||
<Button variant="outline" size="sm" onclick={handleEdit}>
|
||||
<Edit size={16} />
|
||||
<span>Edit</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onclick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
<span>{isDeleting ? 'Deleting...' : 'Delete'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
@@ -351,6 +417,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showEditModal && isOwner && data.find}
|
||||
<EditFindModal
|
||||
isOpen={showEditModal}
|
||||
find={{
|
||||
id: data.find.id,
|
||||
title: data.find.title,
|
||||
description: data.find.description || null,
|
||||
latitude: data.find.latitude,
|
||||
longitude: data.find.longitude,
|
||||
locationName: data.find.locationName || null,
|
||||
category: data.find.category || null,
|
||||
isPublic: data.find.isPublic ?? 1,
|
||||
media: data.find.media || []
|
||||
}}
|
||||
onClose={() => (showEditModal = false)}
|
||||
onFindUpdated={handleFindUpdated}
|
||||
onFindDeleted={handleFindDeleted}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.public-find-page {
|
||||
position: relative;
|
||||
@@ -444,6 +530,22 @@
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.action-buttons-header {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-buttons-header :global(button) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.user-section {
|
||||
|
||||
Reference in New Issue
Block a user