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 [draftImageLarge, setDraftImageLarge] = useState("");
|
||||||
const [draftImageFile, setDraftImageFile] = useState(null);
|
const [draftImageFile, setDraftImageFile] = useState(null);
|
||||||
const [draftImagePreview, setDraftImagePreview] = useState("");
|
const [draftImagePreview, setDraftImagePreview] = useState("");
|
||||||
|
const [draftMediaUrls, setDraftMediaUrls] = useState([]);
|
||||||
const [useCustomImage, setUseCustomImage] = useState(false);
|
const [useCustomImage, setUseCustomImage] = useState(false);
|
||||||
const [postVisibility, setPostVisibility] = useState("public");
|
const [postVisibility, setPostVisibility] = useState("public");
|
||||||
const [postGroupId, setPostGroupId] = useState("");
|
const [postGroupId, setPostGroupId] = useState("");
|
||||||
|
|
@ -1037,6 +1038,7 @@ export default function MapView({
|
||||||
setDraftImage("");
|
setDraftImage("");
|
||||||
setDraftImageFile(null);
|
setDraftImageFile(null);
|
||||||
setDraftImagePreview("");
|
setDraftImagePreview("");
|
||||||
|
setDraftMediaUrls([]);
|
||||||
setUseCustomImage(false);
|
setUseCustomImage(false);
|
||||||
setPostVisibility("public");
|
setPostVisibility("public");
|
||||||
setPostGroupId("");
|
setPostGroupId("");
|
||||||
|
|
@ -1067,84 +1069,135 @@ export default function MapView({
|
||||||
setSearchQuery("");
|
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 handleImageFileChange = async (e) => {
|
||||||
const file = e.target.files?.[0];
|
const files = Array.from(e.target.files || []);
|
||||||
if (!file) return;
|
if (!files.length) return;
|
||||||
setImageTouched(true);
|
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("");
|
setSaveError("");
|
||||||
|
|
||||||
try {
|
setUploadingImage(true);
|
||||||
const formData = new FormData();
|
for (const file of files) {
|
||||||
formData.append('file', file);
|
if (!file.type.startsWith("image/")) {
|
||||||
formData.append('visibility', imageVisibility);
|
setSaveError("Please select an image file");
|
||||||
if (imageGroupId.trim()) formData.append('group_id', imageGroupId.trim());
|
continue;
|
||||||
if (imageUsers.trim()) formData.append('users', imageUsers.trim());
|
}
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
const res = await fetch('/api/assets/upload', {
|
setSaveError("Image must be less than 5MB");
|
||||||
method: 'POST',
|
continue;
|
||||||
body: formData,
|
|
||||||
credentials: 'include',
|
|
||||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error('Upload failed');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
setDraftImageFile(file);
|
||||||
const variants = data && data.variants ? data.variants : null;
|
const reader = new FileReader();
|
||||||
const pickRef = (v) => {
|
reader.onloadend = () => {
|
||||||
if (!v) return "";
|
setDraftImagePreview(reader.result);
|
||||||
if (v.asset_id) return `asset://${v.asset_id}`;
|
|
||||||
return v.url || "";
|
|
||||||
};
|
};
|
||||||
const smallRef = variants ? pickRef(variants.small) : "";
|
reader.readAsDataURL(file);
|
||||||
const mediumRef = variants ? pickRef(variants.medium) : "";
|
|
||||||
let largeRef = variants ? pickRef(variants.large) : "";
|
try {
|
||||||
if (!largeRef) {
|
const formData = new FormData();
|
||||||
if (data && data.url) largeRef = data.url;
|
formData.append("file", file);
|
||||||
else if (data && data.asset_id) largeRef = `asset://${data.asset_id}`;
|
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) => {
|
const handleUrlPreview = async (rawOverride) => {
|
||||||
|
|
@ -1173,10 +1226,7 @@ export default function MapView({
|
||||||
if (!imageTouched && image) {
|
if (!imageTouched && image) {
|
||||||
setUseCustomImage(true);
|
setUseCustomImage(true);
|
||||||
if (!draftImageFile && !draftImagePreview) {
|
if (!draftImageFile && !draftImagePreview) {
|
||||||
setDraftImage(image);
|
addMediaUrlToGallery(image);
|
||||||
setDraftImageSmall(image);
|
|
||||||
setDraftImageMedium(image);
|
|
||||||
setDraftImageLarge(image);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -1280,16 +1330,18 @@ export default function MapView({
|
||||||
payload.published_at = draftPublishedAt.trim();
|
payload.published_at = draftPublishedAt.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const mediaCandidates = draftMediaUrls.filter(Boolean);
|
||||||
draftImageSmall ||
|
if (mediaCandidates.length > 0) {
|
||||||
draftImageMedium ||
|
payload.image_small = mediaCandidates[0];
|
||||||
draftImageLarge ||
|
payload.image_medium = mediaCandidates[1] || mediaCandidates[0];
|
||||||
(useCustomImage && draftImage.trim())
|
payload.image_large =
|
||||||
) {
|
mediaCandidates[2] || mediaCandidates[1] || mediaCandidates[0];
|
||||||
|
payload.media_urls = mediaCandidates;
|
||||||
|
} else if (useCustomImage && draftImage.trim()) {
|
||||||
const fallback = draftImage.trim();
|
const fallback = draftImage.trim();
|
||||||
payload.image_small = (draftImageSmall || fallback).trim();
|
payload.image_small = fallback;
|
||||||
payload.image_medium = (draftImageMedium || fallback).trim();
|
payload.image_medium = fallback;
|
||||||
payload.image_large = (draftImageLarge || fallback).trim();
|
payload.image_large = fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPost = await createPost(payload, token);
|
const newPost = await createPost(payload, token);
|
||||||
|
|
@ -1336,6 +1388,13 @@ export default function MapView({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const galleryPreviewImage =
|
||||||
|
draftMediaUrls[0] ||
|
||||||
|
draftImageLarge ||
|
||||||
|
draftImageMedium ||
|
||||||
|
draftImageSmall ||
|
||||||
|
draftImage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="map-page">
|
<div className="map-page">
|
||||||
<div className="map-stage" ref={stageRef}>
|
<div className="map-stage" ref={stageRef}>
|
||||||
|
|
@ -1587,22 +1646,9 @@ export default function MapView({
|
||||||
)}
|
)}
|
||||||
{createStep === 2 && (
|
{createStep === 2 && (
|
||||||
<>
|
<>
|
||||||
{(draftImagePreview ||
|
{galleryPreviewImage && (
|
||||||
draftImageLarge ||
|
|
||||||
draftImageMedium ||
|
|
||||||
draftImageSmall ||
|
|
||||||
draftImage) && (
|
|
||||||
<div className="create-link-preview create-link-preview-tight">
|
<div className="create-link-preview create-link-preview-tight">
|
||||||
<img
|
<img src={galleryPreviewImage} alt="Link preview" />
|
||||||
src={
|
|
||||||
draftImagePreview ||
|
|
||||||
draftImageLarge ||
|
|
||||||
draftImageMedium ||
|
|
||||||
draftImageSmall ||
|
|
||||||
draftImage
|
|
||||||
}
|
|
||||||
alt="Link preview"
|
|
||||||
/>
|
|
||||||
<div className="create-link-preview-meta">
|
<div className="create-link-preview-meta">
|
||||||
{draftSource ? <span>{draftSource}</span> : null}
|
{draftSource ? <span>{draftSource}</span> : null}
|
||||||
{draftPublishedAt ? (
|
{draftPublishedAt ? (
|
||||||
|
|
@ -1611,6 +1657,29 @@ export default function MapView({
|
||||||
</div>
|
</div>
|
||||||
</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
|
<textarea
|
||||||
className="create-textarea create-textarea-tight"
|
className="create-textarea create-textarea-tight"
|
||||||
rows={3}
|
rows={3}
|
||||||
|
|
@ -1657,6 +1726,7 @@ export default function MapView({
|
||||||
className="create-file-input"
|
className="create-file-input"
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
|
multiple
|
||||||
onChange={handleImageFileChange}
|
onChange={handleImageFileChange}
|
||||||
disabled={uploadingImage}
|
disabled={uploadingImage}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1668,6 +1738,7 @@ export default function MapView({
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
capture="environment"
|
capture="environment"
|
||||||
|
multiple
|
||||||
onChange={handleImageFileChange}
|
onChange={handleImageFileChange}
|
||||||
disabled={uploadingImage}
|
disabled={uploadingImage}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1691,22 +1762,27 @@ export default function MapView({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!draftUrl.trim() && !draftImageFile && !draftImagePreview ? (
|
<div className="create-muted">Or paste an image URL:</div>
|
||||||
<>
|
<div className="create-image-url-row">
|
||||||
<div className="create-muted">Or paste an image URL:</div>
|
<input
|
||||||
<input
|
className="create-input"
|
||||||
className="create-input"
|
placeholder="https://example.com/image.jpg"
|
||||||
placeholder="https://example.com/image.jpg"
|
value={draftImage}
|
||||||
value={draftImage}
|
onChange={(e) => {
|
||||||
onChange={(e) => {
|
setDraftImage(e.target.value);
|
||||||
setDraftImage(e.target.value);
|
setDraftImageFile(null);
|
||||||
setDraftImageFile(null);
|
setDraftImagePreview("");
|
||||||
setDraftImagePreview("");
|
setImageTouched(true);
|
||||||
setImageTouched(true);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
<button
|
||||||
</>
|
type="button"
|
||||||
) : null}
|
className="create-gallery-add"
|
||||||
|
onClick={handleAddCustomImageUrl}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -764,6 +764,66 @@
|
||||||
border-radius:999px;
|
border-radius:999px;
|
||||||
font-size:.68rem;
|
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{
|
.create-link-preview{
|
||||||
display:flex;
|
display:flex;
|
||||||
align-items:center;
|
align-items:center;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue