Learn how to build an AI Image Generator app using React, Firebase, and the Hugging Face API in just one hour! This comprehensive full-stack tutorial takes you step-by-step through the process, from setting up your React app to implementing important features like Firebase storage, Firestore, and React routing. Perfect for beginners and experienced developers alike, this tutorial will equip you with the skills to build your own image generation app. So don’t wait, let’s get started and take your development skills to the next level!
index.css
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;300&family=Poppins:wght@400&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: 'Montserrat', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #333;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
*{
box-sizing: border-box;
}
header{
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 80px;
background-color: #ffff;
}
header .menu{
display: flex;
align-items: center;
}
.menu .logo{
border-radius: 50%;
height: 30px;
width: 30px;
border: 1px solid #ddd;
margin-right: 10px;
}
.menu button{
background-color: transparent;
color: #333;
border: none;
font-size: 15px;
}
header .menu .link{
padding: 10px 20px;
text-decoration: none;
color: #555;
}
.login-page{
display: flex;
flex-direction: column;
min-height: 100vh;
width: 100%;
align-items: center;
text-align: center;
}
.button{
background-color: #555;
color: #fff;
border-radius: 4px;
border: none;
outline: none;
padding: 8px 10px;
}
.container{
margin: auto;
max-width: 1000px;
}
.m-2{
margin-top: 20px;
}
.community-showcase{
display: flex;
flex-wrap: wrap;
justify-content: space-between;
max-width: 1000px;
}
.post{
width: 32%;
max-width: 400px;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
border-radius: 10px;
margin: 5px;
box-shadow: 0 0 10px rgb(0, 0, 0, 0.3);
}
.post img{
position: relative;
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.imageGen{
max-width: 1000px;
border-radius: 20px 40px;
margin: 20px auto;
padding: 10px;
background-color: transparent;
}
.generate-form{
display: flex;
align-items: center;
width: 100%;
}
.generate-form input{
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 10px;
}
.generate-form input:focus{
outline: 0.6px solid blue;
}
.generate-form button{
margin-left: 10px;
border: none;
width: 100px;
background-color: #04acfa;
color: #fff;
padding: 10px;
cursor: pointer;
}
.imageGen .result-image{
max-width: 400px;
margin: auto;
margin-top: 20px;
}
.result-image img{
width: 100%;
border-radius: 5px;
height: auto;
}
.result-image .action{
display: flex;
}
.action button{
background-color: #f5f9fb;
color: #333;
border: none;
padding: 10px;
margin: 10px;
border-radius: 5px;
}
.loading{
display: flex;
width: 100%;
text-align: center;
margin: 70px auto;
align-items: center;
}
.loading p{
margin: auto;
text-align: center;
}
.share-form{
display: none;
}
.d-flex{
display: flex;
align-items: center;
}
h1 span{
color: #04acfa;
}
.logo{
border: 1px solid #ddd;
border-radius: 50%;
height: 40px;
width: 40px;
}
@media screen and (max-width: 1000px) {
header{
padding: 8px;
font-size: 15px;
}
header .menu .link{
padding: 8px 10px;
font-size: 14px;
}
}
firebase-config.js
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import {getAuth} from "firebase/auth";
import { GoogleAuthProvider } from "firebase/auth";
import {getFirestore} from "firebase/firestore"
import {getStorage} from "firebase/storage"
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
const firebaseConfig = {
//key here
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
export const Auth = getAuth(app)
export const Provider = new GoogleAuthProvider()
export const db = getFirestore(app)
export const storage = getStorage(app)
App.js
import React from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import ImageGenerationForm from './components/Generate'
import Home from './components/Home'
import Login from './components/Login'
import Navbar from './components/Navbar'
const App = () => {
return (
<div>
<BrowserRouter>
<Navbar/>
<main className="sm:p-8 px-4 py-8 w-full bg-[#f9fafe] min-h-[calc(100vh-73px)]">
<div className="container">
<Routes>
<Route path='/' element={<Home/>}/>
<Route path='/login' element={<Login/>}/>
<Route path="/generate" element={<ImageGenerationForm/>}/>
</Routes>
</div>
</main>
</BrowserRouter>
</div>
)
}
export default App
loadinganimation.js
import * as React from 'react';
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
export function CircularIndeterminate() {
return (
<Box sx={{ display: 'flex' }}>
<CircularProgress />
</Box>
);
}
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx}'],
theme: {
extend: {
screens: {
xs: '480px',
},
fontFamily: {
inter: ['Inter var', 'sans-serif'],
},
boxShadow: {
card: '0 0 1px 0 rgba(189,192,207,0.06),0 10px 16px -1px rgba(189,192,207,0.2)',
cardhover: '0 0 1px 0 rgba(189,192,207,0.06),0 10px 16px -1px rgba(189,192,207,0.4)',
},
},
},
plugins: [],
};
Login.jsx
import React from 'react'
import { Provider, Auth } from '../firebase-config'
import { signInWithPopup } from 'firebase/auth';
import { useNavigate } from 'react-router-dom';
const Login = () => {
const navigator = useNavigate();
const signIn = async () =>{
await signInWithPopup(Auth, Provider)
.then(res=>{console.log(res); navigator("/")})
.catch(err=>console.log(err))
}
return (
<div className='login-page'>
<h2>Login Here!</h2>
<button className='button' onClick={signIn}>Sign In With Google</button>
</div>
)
}
export default Login
Navbar.jsx
import React from 'react'
import { Link } from 'react-router-dom'
import {useAuthState} from 'react-firebase-hooks/auth';
import {Auth} from "../firebase-config"
import { signOut } from 'firebase/auth';
import LogoutIcon from '@mui/icons-material/Logout';
import { useNavigate } from 'react-router-dom';
const Navbar = () => {
const [user] = useAuthState(Auth)
const navigator = useNavigate()
const logOut = async () => {
await signOut(Auth)
navigator("/")
}
return (
<header>
<h3>CwaLabs</h3>
<div className="menu">
<Link className='link' to="/">Home</Link>
{user && <Link className='link' to={"/generate"}>Generate</Link>}
{user? <div className='link'><div className='d-flex'><img className='logo' src={user.photoURL} alt="" /> <button onClick={logOut}><LogoutIcon/></button></div></div>
: <Link className='link' to={"/login"}>Login</Link>
}
</div>
</header>
)
}
export default Navbar
Generate.jsx
import React, { useState } from "react";
import ShareIcon from '@mui/icons-material/Share';
import FileDownloadIcon from '@mui/icons-material/FileDownload';
import { CircularIndeterminate } from "../loadanimation";
import { Auth, db, storage } from '../firebase-config';
import { getDownloadURL, ref, uploadBytes } from 'firebase/storage';
import { v4 } from 'uuid';
import { collection, addDoc } from 'firebase/firestore';
import {useAuthState} from "react-firebase-hooks/auth"
const API_TOKEN = "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";
const ImageGenerationForm = () => {
const [loading, setLoading] = useState(false);
const [output, setOutput] = useState(null);
const [prompt, setPrompt] = useState("")
const [imageFile, setImageFile] = useState(null);
const [user] = useAuthState(Auth)
const postRef = collection(db, "posts")
const uploadImage = async () =>{
if(imageFile !== null){
const imageRef = ref(storage, `images/${imageFile.name + v4()}`)
uploadBytes(imageRef, imageFile)
.then(()=>{
getDownloadURL(imageRef)
.then((url)=>{
if(prompt !== ""){
addDoc(postRef, {
prompt: prompt,
image: url,
user: user.displayName,
logo: user.photoURL,
})
.then(res=>alert("posted"))
.catch(err=>console.log(err))
}
})
})
.catch(err=>console.log(err))
}
}
const handleSubmit = async (event) => {
event.preventDefault();
setLoading(true);
const input = event.target.elements.input.value;
setPrompt(input)
const response = await fetch(
"https://api-inference.huggingface.co/models/CompVis/stable-diffusion-v1-4",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_TOKEN}`,
},
body: JSON.stringify({ inputs: input }),
}
);
if (!response.ok) {
throw new Error("Failed to generate image");
}
const blob = await response.blob();
setOutput(URL.createObjectURL(blob));
setImageFile(new File([blob], "art.png", { type: "image/png" }));
setLoading(false);
};
const handleDownload = () => {
const link = document.createElement("a");
link.href = output;
link.download = "art.png";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (<div className="imageGen">
<div>
<h1 className="font-extrabold text-[#222328] text-[32px]">Prompt Your Creativity!</h1>
<p className="mt-2 text-[#666e75] text-[14px] max-w-[500px]">Browse through a collection of imaginative and visually stunning images generated by DALL-E AI</p>
</div>
<form className="generate-form mt-2" onSubmit={handleSubmit}>
<input type="text" name="input" placeholder="type your prompt here..." />
<button type="submit" className="button">Generate</button>
</form>
{loading && <div className="loading"><p><CircularIndeterminate/></p></div>}
{!loading && output && (
<div className="result-image">
<img src={output} alt="art" />
<div className="action">
<button onClick={handleDownload}><FileDownloadIcon/></button>
{user && <button onClick={uploadImage}><ShareIcon/></button>}
</div>
</div>
)}
</div>);
};
export default ImageGenerationForm;
FormField.jsx
import React from 'react';
const FormField = ({
labelName,
type,
name,
placeholder,
value,
handleChange,
isSurpriseMe,
handleSurpriseMe,
}) => (
<div>
<div className="flex items-center gap-2 mb-2">
<label
htmlFor={name}
className="block text-sm font-medium text-gray-900"
>
{labelName}
</label>
{isSurpriseMe && (
<button
type="button"
onClick={handleSurpriseMe}
className="font-semibold text-xs bg-[#EcECF1] py-1 px-2 rounded-[5px] text-black"
>
Surprise me
</button>
)}
</div>
<input
type={type}
id={name}
name={name}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-[#6469ff] focus:border-[#6469ff] outline-none block w-full p-3"
placeholder={placeholder}
value={value}
onChange={handleChange}
required
/>
</div>
);
export default FormField;
DisplayPost.jsx
import React from 'react'
import deflogo from "../logo.png"
const DisplayPost = (props) => {
const {logo, image, prompt, user} = props.post;
return (
<div className="rounded-xl group relative shadow-card hover:shadow-cardhover card">
<img
className='w-full h-auto object-cover rounded-xl'
src={image}
alt={prompt}
/>
<div className={"group-hover:flex flex-col max-h-[94.5%] hidden absolute bottom-0 left-0 right-0 bg-[#fff] m-2 p-4 rounded-md"}>
<div className="d-flex">
<img className='logo mr-2' src={logo? logo: deflogo} alt={prompt} />
<div>
<span style={{color: "#888",fontSize: "12px", textTransform: "lowercase"}}>{user}</span>
<p style={{"fontSize": "14px"}}>{prompt}</p>
</div>
</div>
</div>
</div>
)
}
export default DisplayPost
Home.jsx
import React from 'react'
import { useState, useEffect } from 'react'
import { getDocs, collection } from 'firebase/firestore';
import { db } from '../firebase-config';
import DisplayPost from './DisplayPost';
import { CircularIndeterminate } from "../loadanimation";
import FormField from "./FormField"
const Home = () => {
const [posts, setPost] = useState([])
const [loading, setLoading] = useState(true);
const postRef = collection(db, "posts")
const [searchText, setSearchText] = useState('');
const [searchTimeout, setSearchTimeout] = useState(null);
const [searchedResults, setSearchedResults] = useState(null);
const handleSearchChange = (e) => {
clearTimeout(searchTimeout);
setSearchText(e.target.value);
setSearchTimeout(
setTimeout(() => {
const searchResult = posts.filter((item) => item.user.toLowerCase().includes(searchText.toLowerCase()) || item.prompt.toLowerCase().includes(searchText.toLowerCase()));
setSearchedResults(searchResult);
}, 500),
);
};
useEffect(() => {
setLoading(true)
const getPost = () => {
getDocs(postRef)
.then(data => {
setPost(data.docs.map((docs) => ({ ...docs.data(), id: docs.id })));
setLoading(false)
})
}
getPost()
}, [])
return (
<section className="max-w-7xl mx-30px-auto">
<div>
<h1 className="font-extrabold text-[#222328] text-[32px]">The Community Showcase</h1>
<p className="mt-2 text-[#666e75] text-[14px] max-w-[500px]">Browse through a collection of imaginative and visually stunning images generated by DALL-E AI</p>
</div>
<div className="mt-16">
<FormField
labelName="Search posts"
type="text"
name="text"
placeholder="Search something..."
value={searchText}
handleChange={handleSearchChange}
/>
</div>
<div className="mt-10">
{loading ? (
<div className="flex justify-center items-center">
<CircularIndeterminate />
</div>
) : (
<>
{searchText && (
<h2 className="font-medium text-[#666e75] text-xl mb-3">
Showing Resuls for <span className="text-[#222328]">{searchText}</span>:
</h2>
)}
<div className="grid lg:grid-cols-4 sm:grid-cols-3 xs:grid-cols-2 grid-cols-1 gap-3">
{searchText && searchedResults ? (
searchedResults.map(post=>(
<DisplayPost
post={post}
/>
))
) : (posts.map(post=>(
<DisplayPost
post={post}
/>
))
)}
</div>
</>
)}
</div>
</section>
)
}
export default Home