This is a submission for the The Pinata Challenge
What I Built
Imagine attending an exciting event — like a developer conference—where you meet new people, take tons of photos and vidoes with them, and make lasting memories. But once the event wraps up, you realize you have no easy way to get all those great pictures.
Or consider a wedding ceremony: You know your guests captured beautiful moments throughout the day, but gathering all those photos means individually reaching out to each person. It is definitely not the way to go.
Picshaw offers a simple solution. As an event organizer, you can create a dedicated event folder on the platform, generate a shareable link, and invite your guests to upload the pictures they’ve taken. This way, everyone gets to relive the event from the unique perspectives of all attendees. No more missing moments, just memories shared effortlessly.
Picshaw Features
- Creating Public and Private events. Public events are showcased on the “Discover Events” page, allowing anyone to browse and explore them. Private events, on the other hand, remain accessible only via a shareable link provided by the event organizer.
- Upload Files Effortlessly Guests can easily upload their event photos to the designated event folder, making sure all memorable moments are gathered in one place.
Browse Uploaded Photos with Two Viewing Modes.
Users can explore photos either in list mode, which snaps images into a feed similar to Instagram, or in grid mode, offering a gallery-style layout.
Easily Share Event Links
Organizers can generate and share a unique link to invite guests to upload their pictures, streamlining the process of gathering photos.
Discover Public Events
Explore and browse all public events from the “Discover Events” page, opening up new ways to experience moments shared by others.
- Seamless Authentication Users can sign in quickly and securely using Google sign-in or magic links, making the experience smooth and hassle-free.
- Support for dark mode and light mode
Demo
Try out Picshaw live: https://picshaw.vercel.app
My Code
The full code is available on GitHub. Feel free to star the repo and follow me! 😊
This is a Next.js project bootstrapped with create-next-app
.
Getting Started
First, run the development server:
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
Open http://localhost:3000 with your browser to see the result.
You can start editing the page by modifying app/page.tsx
. The page auto-updates as you edit the file.
This project uses next/font
to automatically optimize and load Geist, a new font family for Vercel.
Learn More
To learn more about Next.js, take a look at the following resources:
- Next.js Documentation - learn about Next.js features and API.
- Learn Next.js - an interactive Next.js tutorial.
You can check out the Next.js GitHub repository - your feedback and contributions are welcome!
Deploy on Vercel
The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.
Check out our Next.js deployment documentation for more…
Using Pinata
Integrating with Pinata was one of the smoothest parts of the project. Here’s a breakdown of how I implemented it:
1. Initialize the Pinata SDK
I set up a dedicated file, @/lib/pinata.ts
, to manage the Pinata configuration:
import { PinataSDK } from "pinata"
export const pinata = new PinataSDK({
pinataJwt: `${process.env.PINATA_JWT}`,
pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`
})
2. In my API Route:
- Ensured my user was logged in: I ensured the user was logged in before uploading.
const session = await auth()
if (!session || !session.user) {
return respondError(new Error("User not authenticated"), "Failed to authenticate user", 401)
}
- Validate the Request: I checked if the event exists and ensured the file upload is within limits.
const form = await request.formData()
const files = Array.from(form.values())
const eventSlug = params["event-slug"]
try {
const event = await prisma.event.findUnique({ where: { slug: eventSlug } })
if (!event) {
return respondError(new Error("Event not found"), undefined, 404)
}
if (files.length > 50) {
return respondError(new Error("Limit of 50 files per request"), "Too many files", 400)
} else if (files.length === 0) {
return respondError(new Error("At least one file is required"), "No files", 400)
}
- Upload files to Pinata The next step is uploading the files.
const res = await Promise.all(files.filter(f => typeof f !== 'string').map(f => pinata.upload.file(f)))
const successfulUploads = res.filter(r => r.status === "fulfilled")
const uploadsWithPublicURL = await Promise.all(successfulUploads.map(async r => {
const publicURL = await pinata.gateways.createSignedURL({ cid: r.value.cid, expires: 60 * 60 * 24 * 365 })
return { ...r.value, publicURL }
}))
Note: A better implementation would include checks for failed uploads. This way, users are notified about failed files and can retry uploading them.
- Save Upload Data to the Database Finally, I saved the upload information into the database using Prisma.
const dbUpload = await prisma.upload.create({
data: {
text: `${session.user.name} uploaded ${files.length} images for ${event?.name}`,
ownerId: session.user?.id ?? "",
eventId: event?.id ?? "",
files: {
createMany: {
data: uploadsWithPublicURL.map(file => {
return {
ownerId: session.user?.id ?? "",
eventId: event?.id ?? "",
cid: file.id,
publicURL: file.publicURL,
}
})
}
}
}
})
return respondSuccess(dbUpload, "Uploaded successfully", 201)
This flow ensures the event photos are uploaded securely and efficiently to Pinata, with successful uploads tracked and stored in the database for easy access later.
Rendering the uploaded images in the browser
1. Updating next.config.js
I first had to update the images field in the next.config.js
file to allow NextJS optimize images from remote URLs.
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
hostname: "sapphire-obliged-canidae-626.mypinata.cloud",
protocol: "https",
},
],
},
};
export default nextConfig;
2. Fetching Event Data
In my client, I use a custom hook useFetch
to fetch the details about the selected event
const params = useParams();
const eventSlug = params["event-slug"];
const [viewMode, setViewMode] = React.useState<"grid" | "list">("grid");
const router = useRouter();
const {
loading,
data,
trigger: getEventDetails,
} = useFetch<void, GetUploadsResponse>(`/api/e/${eventSlug}`, undefined, {
fetchOnRender: true,
});
3. Rendering the Images:
I render the retrieved images inside a responsive grrrrrrrrrrid
<div className="grid grid-cols-3 md:grid-cols-4 gap-1 my-6">
{photos.map((file) => (
<Image
key={file.id}
src={file.publicURL}
width={400}
height={400}
alt=""
className="aspect-square object-cover"
/>
))}
</div>
Ideas for improvement
To further improve the app, it would be necessary to add some content moderation features. This would ensure that users don't post NSFW content on public groups. I started integrating with Google Cloud Vision but I didn't have enough time to complete it before the deadline.
Conclusion
Integrating Pinata’s Files API into Picshaw has greatly enhanced how images are uploaded and managed. Pinata provided seamless performance, and its intuitive API made implementation straightforward, allowing me to focus on building the core features of the app while trusting Pinata to handle file storage and delivery efficiently.
Overall, Pinata has been an essential tool in building a reliable and smooth image management system for Picshaw.
Also, I really enjoyed building Picshaw.
Follow me on X @jeremiahlena13
Top comments (0)