allow multiple images when creating posts
This commit is contained in:
parent
3babb277d0
commit
97f8ce3c11
|
|
@ -298,6 +298,7 @@ export default function MapView({
|
|||
const [draftImageLarge, setDraftImageLarge] = useState("");
|
||||
const [draftImageFile, setDraftImageFile] = useState(null);
|
||||
const [draftImagePreview, setDraftImagePreview] = useState("");
|
||||
const [draftMediaUrls, setDraftMediaUrls] = useState([]);
|
||||
const [useCustomImage, setUseCustomImage] = useState(false);
|
||||
const [postVisibility, setPostVisibility] = useState("public");
|
||||
const [postGroupId, setPostGroupId] = useState("");
|
||||
|
|
@ -1037,6 +1038,7 @@ export default function MapView({
|
|||
setDraftImage("");
|
||||
setDraftImageFile(null);
|
||||
setDraftImagePreview("");
|
||||
setDraftMediaUrls([]);
|
||||
setUseCustomImage(false);
|
||||
setPostVisibility("public");
|
||||
setPostGroupId("");
|
||||
|
|
@ -1067,84 +1069,135 @@ export default function MapView({
|
|||
setSearchQuery("");
|
||||
};
|
||||
|
||||
const updateImageVariantsFromGallery = (urls) => {
|
||||
const cleaned = (urls || []).filter(Boolean);
|
||||
const small = cleaned[0] || "";
|
||||
const medium = cleaned[1] || small;
|
||||
const large = cleaned[2] || medium || small;
|
||||
setDraftImageSmall(small);
|
||||
setDraftImageMedium(medium);
|
||||
setDraftImageLarge(large);
|
||||
setDraftImage(small);
|
||||
setDraftImagePreview(small);
|
||||
};
|
||||
|
||||
const addMediaUrlToGallery = (rawUrl) => {
|
||||
const trimmed = (rawUrl || "").trim();
|
||||
if (!trimmed) return false;
|
||||
setDraftMediaUrls((prev) => {
|
||||
if (prev.includes(trimmed)) {
|
||||
updateImageVariantsFromGallery(prev);
|
||||
return prev;
|
||||
}
|
||||
const next = [...prev, trimmed];
|
||||
updateImageVariantsFromGallery(next);
|
||||
return next;
|
||||
});
|
||||
setUseCustomImage(true);
|
||||
return true;
|
||||
};
|
||||
|
||||
const removeMediaUrlAtIndex = (index) => {
|
||||
setDraftMediaUrls((prev) => {
|
||||
if (index < 0 || index >= prev.length) return prev;
|
||||
const next = prev.filter((_, idx) => idx !== index);
|
||||
updateImageVariantsFromGallery(next);
|
||||
if (next.length === 0) {
|
||||
setDraftImage("");
|
||||
setDraftImagePreview("");
|
||||
} else {
|
||||
setDraftImage(next[0]);
|
||||
setDraftImagePreview(next[0]);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddCustomImageUrl = () => {
|
||||
const raw = (draftImage || "").trim();
|
||||
if (!raw) {
|
||||
setSaveError("Paste an image URL first.");
|
||||
return;
|
||||
}
|
||||
if (!/^https?:\/\//i.test(raw)) {
|
||||
setSaveError("Image URL must start with http:// or https://");
|
||||
return;
|
||||
}
|
||||
const added = addMediaUrlToGallery(raw);
|
||||
if (added) {
|
||||
setDraftImage("");
|
||||
setSaveError("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageFileChange = async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const files = Array.from(e.target.files || []);
|
||||
if (!files.length) return;
|
||||
setImageTouched(true);
|
||||
|
||||
// Vérifier le type de fichier
|
||||
if (!file.type.startsWith('image/')) {
|
||||
setSaveError("Please select an image file");
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier la taille (max 5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
setSaveError("Image must be less than 5MB");
|
||||
return;
|
||||
}
|
||||
|
||||
setDraftImageFile(file);
|
||||
|
||||
// Créer preview
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setDraftImagePreview(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// Upload immédiatement
|
||||
setUploadingImage(true);
|
||||
setSaveError("");
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('visibility', imageVisibility);
|
||||
if (imageGroupId.trim()) formData.append('group_id', imageGroupId.trim());
|
||||
if (imageUsers.trim()) formData.append('users', imageUsers.trim());
|
||||
|
||||
const res = await fetch('/api/assets/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'include',
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Upload failed');
|
||||
setUploadingImage(true);
|
||||
for (const file of files) {
|
||||
if (!file.type.startsWith("image/")) {
|
||||
setSaveError("Please select an image file");
|
||||
continue;
|
||||
}
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
setSaveError("Image must be less than 5MB");
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const variants = data && data.variants ? data.variants : null;
|
||||
const pickRef = (v) => {
|
||||
if (!v) return "";
|
||||
if (v.asset_id) return `asset://${v.asset_id}`;
|
||||
return v.url || "";
|
||||
setDraftImageFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setDraftImagePreview(reader.result);
|
||||
};
|
||||
const smallRef = variants ? pickRef(variants.small) : "";
|
||||
const mediumRef = variants ? pickRef(variants.medium) : "";
|
||||
let largeRef = variants ? pickRef(variants.large) : "";
|
||||
if (!largeRef) {
|
||||
if (data && data.url) largeRef = data.url;
|
||||
else if (data && data.asset_id) largeRef = `asset://${data.asset_id}`;
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("visibility", imageVisibility);
|
||||
if (imageGroupId.trim()) formData.append("group_id", imageGroupId.trim());
|
||||
if (imageUsers.trim()) formData.append("users", imageUsers.trim());
|
||||
|
||||
const res = await fetch("/api/assets/upload", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
credentials: "include",
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Upload failed");
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const variants = data && data.variants ? data.variants : null;
|
||||
const pickRef = (v) => {
|
||||
if (!v) return "";
|
||||
if (v.asset_id) return `asset://${v.asset_id}`;
|
||||
return v.url || "";
|
||||
};
|
||||
const smallRef = variants ? pickRef(variants.small) : "";
|
||||
const mediumRef = variants ? pickRef(variants.medium) : "";
|
||||
let largeRef = variants ? pickRef(variants.large) : "";
|
||||
if (!largeRef) {
|
||||
if (data && data.url) largeRef = data.url;
|
||||
else if (data && data.asset_id) largeRef = `asset://${data.asset_id}`;
|
||||
}
|
||||
const fallbackRef = largeRef || mediumRef || smallRef;
|
||||
const selected = fallbackRef || mediumRef || smallRef;
|
||||
if (selected) {
|
||||
addMediaUrlToGallery(selected);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Image upload error:", err);
|
||||
setSaveError("Failed to upload image");
|
||||
}
|
||||
const fallbackRef = largeRef || mediumRef || smallRef;
|
||||
setDraftImageSmall(smallRef || fallbackRef);
|
||||
setDraftImageMedium(mediumRef || fallbackRef);
|
||||
setDraftImageLarge(largeRef || fallbackRef);
|
||||
if (fallbackRef) setDraftImage(fallbackRef);
|
||||
setUploadingImage(false);
|
||||
} catch (err) {
|
||||
console.error('Image upload error:', err);
|
||||
setSaveError('Failed to upload image');
|
||||
setUploadingImage(false);
|
||||
setDraftImageFile(null);
|
||||
setDraftImagePreview("");
|
||||
setDraftImageSmall("");
|
||||
setDraftImageMedium("");
|
||||
setDraftImageLarge("");
|
||||
}
|
||||
setUploadingImage(false);
|
||||
e.target.value = "";
|
||||
};
|
||||
|
||||
const handleUrlPreview = async (rawOverride) => {
|
||||
|
|
@ -1173,10 +1226,7 @@ export default function MapView({
|
|||
if (!imageTouched && image) {
|
||||
setUseCustomImage(true);
|
||||
if (!draftImageFile && !draftImagePreview) {
|
||||
setDraftImage(image);
|
||||
setDraftImageSmall(image);
|
||||
setDraftImageMedium(image);
|
||||
setDraftImageLarge(image);
|
||||
addMediaUrlToGallery(image);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -1280,16 +1330,18 @@ export default function MapView({
|
|||
payload.published_at = draftPublishedAt.trim();
|
||||
}
|
||||
|
||||
if (
|
||||
draftImageSmall ||
|
||||
draftImageMedium ||
|
||||
draftImageLarge ||
|
||||
(useCustomImage && draftImage.trim())
|
||||
) {
|
||||
const mediaCandidates = draftMediaUrls.filter(Boolean);
|
||||
if (mediaCandidates.length > 0) {
|
||||
payload.image_small = mediaCandidates[0];
|
||||
payload.image_medium = mediaCandidates[1] || mediaCandidates[0];
|
||||
payload.image_large =
|
||||
mediaCandidates[2] || mediaCandidates[1] || mediaCandidates[0];
|
||||
payload.media_urls = mediaCandidates;
|
||||
} else if (useCustomImage && draftImage.trim()) {
|
||||
const fallback = draftImage.trim();
|
||||
payload.image_small = (draftImageSmall || fallback).trim();
|
||||
payload.image_medium = (draftImageMedium || fallback).trim();
|
||||
payload.image_large = (draftImageLarge || fallback).trim();
|
||||
payload.image_small = fallback;
|
||||
payload.image_medium = fallback;
|
||||
payload.image_large = fallback;
|
||||
}
|
||||
|
||||
const newPost = await createPost(payload, token);
|
||||
|
|
@ -1336,6 +1388,13 @@ export default function MapView({
|
|||
}
|
||||
};
|
||||
|
||||
const galleryPreviewImage =
|
||||
draftMediaUrls[0] ||
|
||||
draftImageLarge ||
|
||||
draftImageMedium ||
|
||||
draftImageSmall ||
|
||||
draftImage;
|
||||
|
||||
return (
|
||||
<div className="map-page">
|
||||
<div className="map-stage" ref={stageRef}>
|
||||
|
|
@ -1587,22 +1646,9 @@ export default function MapView({
|
|||
)}
|
||||
{createStep === 2 && (
|
||||
<>
|
||||
{(draftImagePreview ||
|
||||
draftImageLarge ||
|
||||
draftImageMedium ||
|
||||
draftImageSmall ||
|
||||
draftImage) && (
|
||||
{galleryPreviewImage && (
|
||||
<div className="create-link-preview create-link-preview-tight">
|
||||
<img
|
||||
src={
|
||||
draftImagePreview ||
|
||||
draftImageLarge ||
|
||||
draftImageMedium ||
|
||||
draftImageSmall ||
|
||||
draftImage
|
||||
}
|
||||
alt="Link preview"
|
||||
/>
|
||||
<img src={galleryPreviewImage} alt="Link preview" />
|
||||
<div className="create-link-preview-meta">
|
||||
{draftSource ? <span>{draftSource}</span> : null}
|
||||
{draftPublishedAt ? (
|
||||
|
|
@ -1611,6 +1657,29 @@ export default function MapView({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{draftMediaUrls.length > 0 && (
|
||||
<div className="create-image-gallery">
|
||||
<div className="create-gallery-title">
|
||||
{draftMediaUrls.length} image
|
||||
{draftMediaUrls.length > 1 ? "s" : ""}
|
||||
</div>
|
||||
<div className="create-gallery-items">
|
||||
{draftMediaUrls.map((url, idx) => (
|
||||
<figure key={`${url}-${idx}`} className="create-gallery-item">
|
||||
<img src={url} alt={`Selected image ${idx + 1}`} />
|
||||
<button
|
||||
type="button"
|
||||
className="create-gallery-remove"
|
||||
onClick={() => removeMediaUrlAtIndex(idx)}
|
||||
aria-label="Remove image"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
className="create-textarea create-textarea-tight"
|
||||
rows={3}
|
||||
|
|
@ -1657,6 +1726,7 @@ export default function MapView({
|
|||
className="create-file-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
onChange={handleImageFileChange}
|
||||
disabled={uploadingImage}
|
||||
/>
|
||||
|
|
@ -1668,6 +1738,7 @@ export default function MapView({
|
|||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
multiple
|
||||
onChange={handleImageFileChange}
|
||||
disabled={uploadingImage}
|
||||
/>
|
||||
|
|
@ -1691,22 +1762,27 @@ export default function MapView({
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!draftUrl.trim() && !draftImageFile && !draftImagePreview ? (
|
||||
<>
|
||||
<div className="create-muted">Or paste an image URL:</div>
|
||||
<input
|
||||
className="create-input"
|
||||
placeholder="https://example.com/image.jpg"
|
||||
value={draftImage}
|
||||
onChange={(e) => {
|
||||
setDraftImage(e.target.value);
|
||||
setDraftImageFile(null);
|
||||
setDraftImagePreview("");
|
||||
setImageTouched(true);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<div className="create-muted">Or paste an image URL:</div>
|
||||
<div className="create-image-url-row">
|
||||
<input
|
||||
className="create-input"
|
||||
placeholder="https://example.com/image.jpg"
|
||||
value={draftImage}
|
||||
onChange={(e) => {
|
||||
setDraftImage(e.target.value);
|
||||
setDraftImageFile(null);
|
||||
setDraftImagePreview("");
|
||||
setImageTouched(true);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="create-gallery-add"
|
||||
onClick={handleAddCustomImageUrl}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -764,6 +764,66 @@
|
|||
border-radius:999px;
|
||||
font-size:.68rem;
|
||||
}
|
||||
.create-image-gallery{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:.3rem;
|
||||
margin-bottom:.4rem;
|
||||
}
|
||||
.create-gallery-title{
|
||||
font-size:.7rem;
|
||||
text-transform:uppercase;
|
||||
letter-spacing:0.2em;
|
||||
color:rgba(226, 232, 240, 0.7);
|
||||
}
|
||||
.create-gallery-items{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:.4rem;
|
||||
}
|
||||
.create-gallery-item{
|
||||
position:relative;
|
||||
width:54px;
|
||||
height:54px;
|
||||
}
|
||||
.create-gallery-item img{
|
||||
width:100%;
|
||||
height:100%;
|
||||
object-fit:cover;
|
||||
border-radius:10px;
|
||||
border:1px solid rgba(148, 163, 184, 0.5);
|
||||
}
|
||||
.create-gallery-remove{
|
||||
position:absolute;
|
||||
top:-6px;
|
||||
right:-6px;
|
||||
width:22px;
|
||||
height:22px;
|
||||
border-radius:50%;
|
||||
border:1px solid rgba(255,255,255,0.4);
|
||||
background:rgba(15,23,42,0.8);
|
||||
color:#f8fafc;
|
||||
font-size:12px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
cursor:pointer;
|
||||
}
|
||||
.create-image-url-row{
|
||||
display:flex;
|
||||
gap:.4rem;
|
||||
}
|
||||
.create-gallery-add{
|
||||
flex:0 0 auto;
|
||||
padding:.35rem .65rem;
|
||||
border-radius:999px;
|
||||
border:1px solid rgba(56, 189, 248, 0.7);
|
||||
background:rgba(56, 189, 248, 0.15);
|
||||
color:#e0f2fe;
|
||||
font-size:.72rem;
|
||||
font-weight:700;
|
||||
cursor:pointer;
|
||||
}
|
||||
.create-link-preview{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
|
|
|
|||
Loading…
Reference in New Issue