Files
growstuff/app/javascript/components/CardPhotoDialog.jsx
T
Brenda Wallace b6006df384 feat: Implement draft autosave and preview functionality in PostEditor
- Added useDraftAutosave hook to handle automatic saving of post drafts.
- Enhanced PostEditor component to include a preview tab that fetches and displays rendered markdown.
- Updated Post model to manage draft state and associated published_at timestamp.
- Introduced card_photo association to Post for representing a primary image.
- Modified post form to support draft saving and publishing actions.
- Updated navigation to include links to drafts.
- Added tests for draft saving, preview functionality, and card photo validation.
- Created migrations for new database fields related to drafts and card photos.
2026-10-28 21:50:26 +13:00

91 lines
3.5 KiB
React

import React, {useState} from 'react';
import {patchJson} from '../api';
import Modal from './Modal';
// A button on a post's page that opens a dialog for choosing the card photo:
// the photo that stands for the post on the posts list, an author's page and
// shared links. The photos are the post's own. Saving reloads, because the
// page is server rendered.
export default function CardPhotoDialog({url, photos, selectedId = null, subject, excerpt = '', byline = ''}) {
const [open, setOpen] = useState(false);
const [chosenId, setChosenId] = useState(selectedId || (photos[0] && photos[0].id));
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
const chosen = photos.find((p) => p.id === chosenId);
async function save() {
setSaving(true);
setError(null);
const {ok} = await patchJson(url, {post: {card_photo_id: chosenId}});
if (ok) {
setTimeout(() => window.location.reload(), 0);
return;
}
setSaving(false);
setError('The photo could not be saved. Try again.');
}
return (
<>
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={() => setOpen(true)}>
Change
</button>
{open && (
<Modal title={`Card photo for “${subject}”`} titleId="card-photo-title" onClose={() => setOpen(false)}>
<div className="modal-body">
<p className="text-muted">
This photo stands for your post wherever it appears as a card: the posts list, your page and links shared
elsewhere.
</p>
{photos.length === 0 ? (
<p>Add a photo to this post first, then you can choose it here.</p>
) : (
<fieldset>
<legend className="h6 text-uppercase">Photos in this post</legend>
<div className="card-photo-choices">
{photos.map((photo) => (
<label key={photo.id} className="card-photo-choice">
<input
type="radio"
name="card_photo"
checked={photo.id === chosenId}
onChange={() => setChosenId(photo.id)}
/>
<span className="card-photo-polaroid">
<img src={photo.thumbnail_url} alt="" />
<span>{photo.title}</span>
</span>
</label>
))}
</div>
</fieldset>
)}
{chosen && (
<>
<h3 className="h6 text-uppercase mt-3">How it will look</h3>
<div className="card-photo-preview">
<img src={chosen.fullsize_url} alt="Preview of the card photo" />
<div>
<strong>{subject}</strong>
<p className="text-muted small mb-1">{excerpt}</p>
<small className="text-muted">{byline}</small>
</div>
</div>
</>
)}
{error && <div className="alert alert-danger mt-3" role="alert">{error}</div>}
</div>
<div className="modal-footer">
<button type="button" className="btn btn-outline-secondary" onClick={() => setOpen(false)}>Cancel</button>
<button type="button" className="btn btn-primary" disabled={!chosen || saving} onClick={save}>
Use this photo
</button>
</div>
</Modal>
)}
</>
);
}