build mobile fix
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { X, Minimize2, Trash2, Edit2, Check, Layers, FolderOpen, Inbox, ScanText, ChevronDown, ChevronUp } from 'lucide-react';
|
import { X, Minimize2, Trash2, Edit2, Check, Layers, FolderOpen, Inbox, ScanText, ChevronDown, ChevronUp, ExternalLink, Image, Info } from 'lucide-react';
|
||||||
import { useMeme, useDeleteMeme, useUpdateMeme, useMoveMeme, useCollections } from '../hooks/useMemes';
|
import { useMeme, useDeleteMeme, useUpdateMeme, useMoveMeme, useCollections } from '../hooks/useMemes';
|
||||||
import { useAuth } from '../hooks/useAuth';
|
import { useAuth } from '../hooks/useAuth';
|
||||||
import { SharePanel } from './SharePanel';
|
import { SharePanel } from './SharePanel';
|
||||||
@@ -39,6 +39,7 @@ export function MemeDetail({ memeId, onClose }: Props) {
|
|||||||
const [editTags, setEditTags] = useState('');
|
const [editTags, setEditTags] = useState('');
|
||||||
const [showRescale, setShowRescale] = useState(false);
|
const [showRescale, setShowRescale] = useState(false);
|
||||||
const [activeChild, setActiveChild] = useState<Meme | null>(null);
|
const [activeChild, setActiveChild] = useState<Meme | null>(null);
|
||||||
|
const [mobileTab, setMobileTab] = useState<'image' | 'details'>('image');
|
||||||
|
|
||||||
const meme = data;
|
const meme = data;
|
||||||
const displayMeme = activeChild ?? meme;
|
const displayMeme = activeChild ?? meme;
|
||||||
@@ -79,16 +80,18 @@ export function MemeDetail({ memeId, onClose }: Props) {
|
|||||||
|
|
||||||
if (!meme) return null;
|
if (!meme) return null;
|
||||||
|
|
||||||
|
const isVideo = meme.mime_type.startsWith('video/');
|
||||||
|
const fullSizeUrl = displayMeme ? api.imageUrl(displayMeme.file_path) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div className="fixed inset-0 z-40 bg-black/80 animate-fade-in" onClick={onClose} />
|
||||||
className="fixed inset-0 z-40 bg-black/80 animate-fade-in"
|
|
||||||
onClick={onClose}
|
{/* Modal — full screen on mobile, inset on desktop */}
|
||||||
/>
|
<div className="fixed inset-0 md:inset-4 lg:inset-8 z-50 flex flex-col bg-zinc-900 md:rounded-2xl shadow-2xl border-0 md:border border-zinc-800 overflow-hidden animate-scale-in">
|
||||||
|
|
||||||
<div className="fixed inset-4 md:inset-8 z-50 flex flex-col bg-zinc-900 rounded-2xl shadow-2xl border border-zinc-800 overflow-hidden animate-scale-in">
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-5 py-4 border-b border-zinc-800 flex-shrink-0">
|
<div className="flex items-center justify-between px-4 py-3 md:px-5 md:py-4 border-b border-zinc-800 flex-shrink-0">
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<input
|
<input
|
||||||
autoFocus
|
autoFocus
|
||||||
@@ -97,9 +100,9 @@ export function MemeDetail({ memeId, onClose }: Props) {
|
|||||||
className="flex-1 bg-zinc-800 border border-zinc-700 rounded-lg px-3 py-1.5 text-sm font-semibold focus:outline-none focus:border-accent mr-3"
|
className="flex-1 bg-zinc-800 border border-zinc-700 rounded-lg px-3 py-1.5 text-sm font-semibold focus:outline-none focus:border-accent mr-3"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<h2 className="text-lg font-semibold truncate flex-1 mr-3">{meme.title}</h2>
|
<h2 className="text-base md:text-lg font-semibold truncate flex-1 mr-3">{meme.title}</h2>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
editing ? (
|
editing ? (
|
||||||
<button
|
<button
|
||||||
@@ -110,47 +113,84 @@ export function MemeDetail({ memeId, onClose }: Props) {
|
|||||||
<Check size={14} /> Save
|
<Check size={14} /> Save
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button onClick={startEdit} className="text-zinc-500 hover:text-zinc-300 transition-colors p-1.5" title="Edit">
|
||||||
onClick={startEdit}
|
|
||||||
className="text-zinc-500 hover:text-zinc-300 transition-colors p-1"
|
|
||||||
title="Edit"
|
|
||||||
>
|
|
||||||
<Edit2 size={16} />
|
<Edit2 size={16} />
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{isAdmin && !meme.parent_id && !meme.mime_type.startsWith('video/') && (
|
{isAdmin && !meme.parent_id && !isVideo && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowRescale(true)}
|
onClick={() => setShowRescale(true)}
|
||||||
className="flex items-center gap-1 text-sm px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-300 transition-colors"
|
className="flex items-center gap-1 text-sm px-2.5 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-300 transition-colors"
|
||||||
title="Create rescaled copy"
|
title="Create rescaled copy"
|
||||||
>
|
>
|
||||||
<Minimize2 size={14} />
|
<Minimize2 size={14} />
|
||||||
<span className="hidden sm:inline">Rescale</span>
|
<span className="hidden sm:inline">Rescale</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{/* Full size link — always visible */}
|
||||||
|
{fullSizeUrl && (
|
||||||
|
<a
|
||||||
|
href={fullSizeUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="flex items-center gap-1 text-sm px-2.5 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-300 transition-colors"
|
||||||
|
title="Open full size"
|
||||||
|
>
|
||||||
|
<ExternalLink size={14} />
|
||||||
|
<span className="hidden sm:inline">Full size</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={deleteMeme.isPending}
|
disabled={deleteMeme.isPending}
|
||||||
className="text-zinc-500 hover:text-red-400 transition-colors p-1"
|
className="text-zinc-500 hover:text-red-400 transition-colors p-1.5"
|
||||||
title="Delete"
|
title="Delete"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button onClick={onClose} className="text-zinc-500 hover:text-zinc-300 transition-colors p-1">
|
<button onClick={onClose} className="text-zinc-500 hover:text-zinc-300 transition-colors p-1.5">
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile tab bar */}
|
||||||
|
<div className="flex md:hidden border-b border-zinc-800 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileTab('image')}
|
||||||
|
className={`flex-1 flex items-center justify-center gap-1.5 py-2.5 text-sm font-medium transition-colors border-b-2 ${
|
||||||
|
mobileTab === 'image'
|
||||||
|
? 'border-accent text-accent'
|
||||||
|
: 'border-transparent text-zinc-500 hover:text-zinc-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Image size={14} /> Media
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileTab('details')}
|
||||||
|
className={`flex-1 flex items-center justify-center gap-1.5 py-2.5 text-sm font-medium transition-colors border-b-2 ${
|
||||||
|
mobileTab === 'details'
|
||||||
|
? 'border-accent text-accent'
|
||||||
|
: 'border-transparent text-zinc-500 hover:text-zinc-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Info size={14} /> Details
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div className="flex flex-col md:flex-row flex-1 overflow-hidden">
|
<div className="flex flex-col md:flex-row flex-1 min-h-0 overflow-hidden">
|
||||||
{/* Image / video panel */}
|
|
||||||
<div className="flex-1 flex items-center justify-center bg-zinc-950 p-4 overflow-hidden">
|
{/* Image / video panel — full height on mobile when image tab active */}
|
||||||
|
<div className={`flex-1 flex items-center justify-center bg-zinc-950 p-4 min-h-0 overflow-hidden ${
|
||||||
|
mobileTab === 'details' ? 'hidden md:flex' : 'flex'
|
||||||
|
}`}>
|
||||||
{displayMeme && (
|
{displayMeme && (
|
||||||
displayMeme.mime_type.startsWith('video/') ? (
|
isVideo ? (
|
||||||
<video
|
<video
|
||||||
key={displayMeme.id}
|
key={displayMeme.id}
|
||||||
src={api.imageUrl(displayMeme.file_path)}
|
src={api.imageUrl(displayMeme.file_path)}
|
||||||
@@ -172,7 +212,9 @@ export function MemeDetail({ memeId, onClose }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<div className="md:w-80 border-t md:border-t-0 md:border-l border-zinc-800 flex flex-col overflow-y-auto">
|
<div className={`md:w-80 md:border-l border-zinc-800 flex flex-col overflow-y-auto ${
|
||||||
|
mobileTab === 'image' ? 'hidden md:flex' : 'flex'
|
||||||
|
}`}>
|
||||||
<div className="p-5 space-y-5 flex-1">
|
<div className="p-5 space-y-5 flex-1">
|
||||||
|
|
||||||
{/* Share */}
|
{/* Share */}
|
||||||
@@ -289,7 +331,7 @@ export function MemeDetail({ memeId, onClose }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<dt className="text-zinc-500">Type</dt>
|
<dt className="text-zinc-500">Type</dt>
|
||||||
<dd className="text-zinc-300">{meme.mime_type.replace('image/', '')}</dd>
|
<dd className="text-zinc-300">{meme.mime_type.replace('image/', '').replace('video/', '')}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<dt className="text-zinc-500">Uploaded</dt>
|
<dt className="text-zinc-500">Uploaded</dt>
|
||||||
|
|||||||
Reference in New Issue
Block a user