Auto Create Social Media Posts
This is mind blowing!
Here is the entire Source Code you can run in Google's AI Studio and do this yourself:
import React, { useState, useCallback } from 'react';
// --- Constants ---
const DEFAULT_PROMPT = `
Create a realistic Instagram post composition.
The main subject (from the uploaded image) should be prominently displayed as the primary image within a clean, modern Instagram post frame.
The post frame should include:
- A user handle (e.g., "user_name")
- A small circular profile picture icon next to the handle (matching the main subject)
- Standard Instagram-like icons for "Like" (heart), "Comment" (speech bubble), and "Share" (paper plane) below the main image.
- A placeholder for "X likes" (e.g., "Liked by other_user and 1.3M others") and a short caption (e.g., "Working remote from anywhere! Loving this new setup #digitalnomad #remotework #workfromanywhere #travel")
Separately, generate a full-body, high-quality image of the person from the uploaded photo, sitting casually on a modern stool and actively using a silver laptop.
Overlay this generated full-body person as a transparent cutout, positioned on the bottom-right side, *outside* and partially overlapping the main Instagram post frame.
The overall background for the entire composition (behind the Instagram frame) should be a softly blurred, modern, well-lit office or cafe interior, giving a professional yet relaxed vibe. Ensure all elements are well-integrated and look natural.
`;
// --- Helper Components ---
/**
* A simple loading spinner component.
*/
const Spinner = () => (
<div className="flex justify-center items-center">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>
);
/**
* A component to render the image upload area.
*/
const ImageUploader = ({ onImageUpload, originalImage }) => {
const [isDragging, setIsDragging] = useState(false);
const handleFileChange = (e) => {
const file = e.target.files?.[0];
if (file) {
onImageUpload(file);
}
};
const handleDragOver = (e) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = (e) => {
e.preventDefault();
setIsDragging(false);
};
const handleDrop = (e) => {
e.preventDefault();
setIsDragging(false);
const file = e.dataTransfer.files?.[0];
if (file) {
onImageUpload(file);
}
};
return (
<div className="w-full">
<label
htmlFor="file-upload"
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`flex flex-col items-center justify-center w-full h-64 border-2 border-dashed rounded-lg cursor-pointer transition-colors
${isDragging ? 'border-blue-500 bg-blue-50' : 'border-gray-300 bg-gray-50 hover:bg-gray-100'}
${originalImage ? 'border-green-500 bg-green-50' : ''}
`}
>
{originalImage ? (
<div className="text-center">
<img src={originalImage} alt="Uploaded preview" className="max-h-48 rounded-md shadow-sm mx-auto" />
<p className="mt-4 text-sm font-medium text-green-700">Image loaded!</p>
<p className="text-xs text-gray-500">Drop a new image or click to replace</p>
</div>
) : (
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<svg className="w-8 h-8 mb-4 text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
</svg>
<p className="mb-2 text-sm text-gray-500"><span className="font-semibold">Click to upload</span> or drag and drop</p>
<p className="text-xs text-gray-500">PNG, JPG, or WEBP</p>
</div>
)}
</label>
<input id="file-upload" type="file" className="hidden" accept="image/png, image/jpeg, image/webp" onChange={handleFileChange} />
</div>
);
};
/**
* A component to display the generated image result and download option.
*/
const GenerationResult = ({ generatedImage, isLoading, error }) => {
// --- UPDATE: More robust download handler ---
const handleDownload = async () => {
if (generatedImage) {
try {
// The generatedImage is a data URL: "data:image/png;base64,..."
// Fetch it to convert it into a blob
const response = await fetch(generatedImage);
const blob = await response.blob();
// Create an object URL from the blob
const objectUrl = URL.createObjectURL(blob);
// Use the same anchor link trick, but with the object URL
const link = document.createElement('a');
link.href = objectUrl;
link.download = 'generated-composition.png';
document.body.appendChild(link);
// Clean up: remove the link and revoke the object URL
document.body.removeChild(link);
URL.revokeObjectURL(objectUrl);
} catch (error) {
console.error("Download failed:", error);
// We can't use alert(), so we'll just log the error.
// A more advanced implementation might show a small error message in the UI.
}
}
};
// --- END UPDATE ---
return (
<div className="w-full h-full min-h-[400px] bg-gray-900 rounded-lg shadow-inner flex flex-col items-center justify-center p-4">
{isLoading && <Spinner />}
{error && !isLoading && (
<div className="text-center text-red-400">
<p className="font-semibold">Generation Failed</p>
<p className="text-sm mt-2">{error}</p>
</div>
)}
{generatedImage && !isLoading && (
<>
<img
key={generatedImage} // Force re-render on new image
src={generatedImage}
alt="Generated composition"
className="max-w-full max-h-full object-contain rounded-md"
/>
{/* --- Download Button (calls new async handler) --- */}
<button
onClick={handleDownload}
className="mt-4 flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
<svg className="w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
Download Image
</button>
</>
)}
{!generatedImage && !isLoading && !error && (
<div className="text-center text-gray-400">
<svg className="w-12 h-12 mx-auto text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm16.5-1.5V6" />
</svg>
<p className="mt-4 font-medium">Your generated composition will appear here</p>
<p className="text-sm text-gray-500 mt-1">Upload an image and write a prompt to begin</p>
</div>
)}
</div>
);
};
// --- Main App Component ---
export default function App() {
const [imageFile, setImageFile] = useState(null);
const [originalImage, setOriginalImage] = useState(null); // For display
const [prompt, setPrompt] = useState(DEFAULT_PROMPT);
const [generatedImage, setGeneratedImage] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
/**
* Utility function to convert a File object to a base64 string,
* removing the data URL prefix.
*/
const fileToGenerativePart = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
// Result is "data:image/jpeg;base64,..."
// We only want the part after the comma
const base64Data = reader.result.split(',')[1];
resolve({
inlineData: {
data: base64Data,
mimeType: file.type,
},
});
};
reader.onerror = (err) => reject(err);
reader.readAsDataURL(file);
});
};
/**
* Handles the file upload, setting both the file object
* and a data URL for preview.
*/
const handleImageUpload = (file) => {
if (!file.type.startsWith("image/")) {
setError("Please upload a valid image file (PNG, JPG, WEBP).");
return;
}
setError(null);
setImageFile(file);
setGeneratedImage(null); // Clear previous generation
// Create a URL for previewing the original image
const reader = new FileReader();
reader.onload = (e) => {
setOriginalImage(e.target.result);
};
reader.readAsDataURL(file);
};
/**
* Implements exponential backoff for API retries.
*/
const fetchWithBackoff = async (url, options, retries = 3, delay = 1000) => {
try {
const response = await fetch(url, options);
if (!response.ok) {
if (response.status === 429 && retries > 0) {
// Throttled, retry with backoff
await new Promise(res => setTimeout(res, delay));
return fetchWithBackoff(url, options, retries - 1, delay * 2);
}
// Try to get error message from response body
let errorBody = "No error details from API.";
try {
const body = await response.json();
errorBody = body?.error?.message || JSON.stringify(body);
} catch(e) {
// Could not parse JSON
errorBody = await response.text();
}
throw new Error(`API Error: ${response.status} ${response.statusText}. Details: ${errorBody}`);
}
return response.json();
} catch (err) {
if (retries > 0 && !(err instanceof DOMException && err.name === 'AbortError')) {
// Network or other error, retry
await new Promise(res => setTimeout(res, delay));
return fetchWithBackoff(url, options, retries - 1, delay * 2);
}
throw err; // After retries, throw the original error
}
};
/**
* Handles the "Generate" button click.
*/
const handleGenerate = useCallback(async () => {
if (!imageFile || !prompt) {
setError("Please upload an image and provide a prompt.");
return;
}
setIsLoading(true);
setError(null);
setGeneratedImage(null);
try {
// 1. Get the base64 data part for the API
const imagePart = await fileToGenerativePart(imageFile);
// 2. Construct the API payload
const payload = {
contents: [
{
role: "user",
parts: [
{ text: prompt },
imagePart
]
}
],
generationConfig: {
responseModalities: ['TEXT', 'IMAGE']
},
};
// 3. Make the API call with backoff
const apiKey = ""; // API key is injected by the environment
const result = await fetchWithBackoff(API_URL + apiKey, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
// 4. Process the response
const base64Data = result?.candidates?.[0]?.content?.parts?.find(p => p.inlineData)?.inlineData?.data;
if (base64Data) {
const imageUrl = `data:image/png;base64,${base64Data}`;
setGeneratedImage(imageUrl);
} else {
if (result?.candidates?.[0]?.finishReason === 'SAFETY') {
throw new Error("Generation failed due to safety filters. Try a different prompt or image.");
}
console.error("Unexpected API response:", result);
throw new Error("No image data found in API response. The model may not have been able to process this request.");
}
} catch (err) {
console.error("Generation failed:", err);
setError(err.message || "An unknown error occurred during generation.");
} finally {
setIsLoading(false);
}
}, [imageFile, prompt]);
return (
<div className="flex flex-col lg:flex-row min-h-screen bg-gray-100 font-sans">
{/* --- Control Panel (Left Side) --- */}
<div className="w-full lg:w-1/3 xl:w-1/4 p-6 bg-white shadow-lg overflow-y-auto">
<h1 className="text-2xl font-bold text-gray-800 mb-1">Image Composer AI</h1>
<p className="text-sm text-gray-500 mb-6">Create new compositions from your images.</p>
<div className="space-y-6">
{/* --- Step 1: Upload --- */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">1. Upload Your Base Image</label>
<ImageUploader onImageUpload={handleImageUpload} originalImage={originalImage} />
</div>
{/* --- Step 2: Prompt --- */}
<div>
<label htmlFor="prompt" className="block text-sm font-medium text-gray-700 mb-2">2. Describe the Composition</label>
<textarea
id="prompt"
rows="6"
className="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="e.g., Place this person in a forest..."
/>
</div>
{/* --- Step 3: Generate --- */}
<div>
<button
onClick={handleGenerate}
disabled={isLoading || !imageFile}
className="w-full flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-lg shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition-all"
>
{isLoading ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Generating...
</>
) : (
"Generate Composition"
)}
</button>
{error && !isLoading && (
<p className="text-sm text-red-600 mt-3 text-center">{error}</p>
)}
</div>
</div>
</div>
{/* --- Main Content Area (Right Side) --- */}
<div className="w-full lg:w-2/3 xl:w-3/4 p-6 lg:p-10 flex-1">
<h2 className="text-xl font-semibold text-gray-700 mb-4">Composition Result</h2>
<GenerationResult
generatedImage={generatedImage}
isLoading={isLoading}
error={error}
/>
</div>
</div>
);
}
1
1 comment
Derick Briers
2
Auto Create Social Media Posts
powered by
Real Estate Automation Plus
skool.com/real-estate-automation-plus-6184
Win back time and up your game in Real Estate.
Automation techniques and systems that can be used by anyone, regardless of your tech skills.
Build your own community
Bring people together around your passion and get paid.
Powered by