feat: add collapsible sections to StepGenerate component with reusable CollapsibleSection
This commit is contained in:
parent
4afb21c38b
commit
b3418e3c96
44
apps/admin/src/components/steps/CollapsibleSection.tsx
Normal file
44
apps/admin/src/components/steps/CollapsibleSection.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { Box, Collapse, IconButton, Stack, Typography, type SxProps, type Theme } from '@mui/material';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import { useState, type PropsWithChildren } from 'react';
|
||||
|
||||
export default function CollapsibleSection({ title, defaultCollapsed = true, right, sx, children }: PropsWithChildren<{
|
||||
title: string;
|
||||
defaultCollapsed?: boolean;
|
||||
right?: React.ReactNode;
|
||||
sx?: SxProps<Theme>;
|
||||
}>) {
|
||||
const [open, setOpen] = useState(!defaultCollapsed);
|
||||
|
||||
return (
|
||||
<Box sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 1, overflow: 'hidden', ...sx }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
sx={{ px: 1.25, py: 0.75, cursor: 'pointer', bgcolor: 'background.paper' }}
|
||||
onClick={() => setOpen(v => !v)}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpen(v => !v);
|
||||
}}
|
||||
sx={{ transform: open ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 150ms ease' }}
|
||||
>
|
||||
<ExpandMore fontSize="small" />
|
||||
</IconButton>
|
||||
<Typography variant="subtitle2">{title}</Typography>
|
||||
</Stack>
|
||||
{right}
|
||||
</Stack>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit={false}>
|
||||
<Box sx={{ p: 1.25 }}>
|
||||
{children}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@ -8,19 +8,16 @@ export default function SelectedImages({
|
||||
onRemove: (key: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Selected Images</Typography>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: 1 }}>
|
||||
{imageKeys.map((k) => (
|
||||
<Box key={k} sx={{ p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1, textAlign: 'center', bgcolor: '#fafafa' }}>
|
||||
<img src={`/api/media/obj?key=${encodeURIComponent(k)}`} alt={k.split('/').slice(-1)[0]} style={{ maxWidth: '100%', maxHeight: 100, objectFit: 'contain' }} />
|
||||
<Button size="small" color="error" variant="text" onClick={() => onRemove(k)} sx={{ mt: 0.5 }}>Remove</Button>
|
||||
</Box>
|
||||
))}
|
||||
{imageKeys.length === 0 && (
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>(No images selected)</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: 1 }}>
|
||||
{imageKeys.map((k) => (
|
||||
<Box key={k} sx={{ p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1, textAlign: 'center', bgcolor: '#fafafa' }}>
|
||||
<img src={`/api/media/obj?key=${encodeURIComponent(k)}`} alt={k.split('/').slice(-1)[0]} style={{ maxWidth: '100%', maxHeight: 100, objectFit: 'contain' }} />
|
||||
<Button size="small" color="error" variant="text" onClick={() => onRemove(k)} sx={{ mt: 0.5 }}>Remove</Button>
|
||||
</Box>
|
||||
))}
|
||||
{imageKeys.length === 0 && (
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>(No images selected)</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Box, Stack, TextField, Typography } from '@mui/material';
|
||||
import MediaLibrary from '../MediaLibrary';
|
||||
import SelectedImages from './SelectedImages';
|
||||
import CollapsibleSection from './CollapsibleSection';
|
||||
import type { Clip } from './StepAssets';
|
||||
|
||||
export default function StepGenerate({
|
||||
@ -24,8 +25,7 @@ export default function StepGenerate({
|
||||
</Typography>
|
||||
|
||||
{/* Audio transcriptions in order */}
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Audio Transcriptions</Typography>
|
||||
<CollapsibleSection title="Audio Transcriptions">
|
||||
<Stack spacing={1}>
|
||||
{[...postClips]
|
||||
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
||||
@ -39,17 +39,20 @@ export default function StepGenerate({
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>(No audio clips)</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Selected images */}
|
||||
<SelectedImages imageKeys={genImageKeys} onRemove={onToggleGenImage} />
|
||||
<CollapsibleSection title="Selected Images">
|
||||
<SelectedImages imageKeys={genImageKeys} onRemove={onToggleGenImage} />
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Media library */}
|
||||
<MediaLibrary selectionMode selectedKeys={genImageKeys} onToggleSelect={onToggleGenImage} />
|
||||
<CollapsibleSection title="Media Library">
|
||||
<MediaLibrary selectionMode selectedKeys={genImageKeys} onToggleSelect={onToggleGenImage} />
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Prompt */}
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>AI Prompt</Typography>
|
||||
<CollapsibleSection title="AI Prompt">
|
||||
<TextField
|
||||
label="Instructions + context for AI generation"
|
||||
value={promptText}
|
||||
@ -58,7 +61,7 @@ export default function StepGenerate({
|
||||
multiline
|
||||
minRows={4}
|
||||
/>
|
||||
</Box>
|
||||
</CollapsibleSection>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user