While working on my personal project Notey, I built a feature where the user could choose a background image for each of their workspace.
In this post, we're going to recreate an image picker that looks like this:

It was built with the following:
- Shadcn UI
- Tailwindcss
- React hook forms
- Unsplash API
- zod
Let's get started!
First, we need to install a package (Let's just assume that you're already working on a project with React and TailwindCSS):
npm i unsplash-js
We're going to fetch random images from unsplash. Although the images a free to use, we must adhere to their guidelines. I'm not going to talk about this deeply in this article. If you're planning to use unsplash in your project, please adhere to their guidelines :).
Next, we'll create a file called unsplash.ts
import { createApi } from "unsplash-js"
export const unsplash = createApi({
// you can get this key in your unsplash project's dashboard
accessKey: process.env.NEXT_PUBLIC_UNSPLASH_ACCESS_KEY!,
fetch: fetch
})
We will be able to extract useful APIs from unsplash
now.
Let's create the server action, form, and form schema. First, let's create a schema file where we will use zod to validate the form.
import * as z from "zod"
export const CreateWorkspaceSchema = z.object({
name: z.string().min(1, {
message: "Please enter a name"
}),
image: z.string().min(1, {
message: "Please select a cover image"
})
})
...and create the form. We will need to install a couple components from shadcn:
npx shadcn-ui@latest add radio-group form input
Now let's create the form:
"use client"
import { useForm } from "react-hook-form"
import { useEffect, useState } from "react"
import { useFormStatus } from "react-dom"
import Image from "next/image"
import Link from "next/link"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { CreateWorkspaceSchema } from "@/schemas/schema"
import { unsplash } from "@/lib/unsplash"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Check, Loader2 } from "lucide-react"
export const WorkspaceForm = () => {
const form = useForm<z.infer<typeof CreateWorkspaceSchema>>({
resolver: zodResolver(CreateWorkspaceSchema),
defaultValues: {
name: "",
image: ""
}
})
const [unsplashImages, setUnsplashImages] = useState<Array<Record<string, any>>>([])
const [isLoading, setIsLoading] = useState(true)
const [selectedImage, setSelectedImage] = useState(null)
const { pending } = useFormStatus()
useEffect(() => {
const fetchUnsplashImages = async () => {
try {
const result = await unsplash.photos.getRandom({
collectionIds: ["317099"],
count: 6
})
if (result && result.response) {
const newUnsplashImages = (result.response as Array<Record<string, any>>)
setUnsplashImages(newUnsplashImages)
} else {
console.log("Failed to retrieve images")
}
} catch (error) {
console.log(error)
} finally {
setIsLoading(false)
}
}
fetchUnsplashImages();
}, [])
if (isLoading) {
return (
<div className="flex items-center justify-center">
<Loader2 className="text-black h-5 w-5 animate-spin" />
</div>
)
}
return (
<Form {...form}>
<form>
<FormField
control={form.control}
name="image"
render={({ field }) => (
<>
<RadioGroup className="grid grid-cols-3 gap-2 max-h-fit" onValueChange={field.onChange}>
{
unsplashImages.map((image) => (
<FormItem
key={image.id}
className="aspect-video flex items-center justify-center relative group"
>
<FormControl>
<div className="w-full h-full">
<RadioGroupItem value={image.id} className="z-50 hidden relative h-full w-full rounded-none border-none" id={image.id} />
<FormLabel
htmlFor={image.id}
onClick={() => {
if (pending) return
setSelectedImage(image.id)
}}
>
<Image
fill
alt="unsplash image"
className="object-cover rounded-sm cursor-pointer"
src={image.urls.thumb}
/>
</FormLabel>
{
selectedImage === image.id && (
<div className="absolute inset-y-0 h-full w-full bg-black/30 flex items-center justify-center rounded-sm">
<Check className="h-4 w-4 text-white" />
</div>
)
}
<Link
href={image.links.html}
target="_blank"
className="absolute bottom-0 truncate w-full text-[11px] text-white bg-black/30 px-1 py-[0.5px] opacity-0 group-hover:opacity-100 hover:underline"
>
{image.user.name}
</Link>
</div>
</FormControl>
</FormItem>
))
}
</RadioGroup>
<FormMessage />
</>
)}
/>
{/* Form for the workspace name goes here! */}
</form>
</Form>
)
}
All we have to do now is to create an onSubmit
function which will call a server action, creating and storing a new workspace in our database.
I had a really hard time implementing the image picker functionality. First, I used the Input
component from shadcn. I changed the type
to radio
. However it didn't work.
While skimming through the docs, I noticed that there was a component called radio-group
. It solved the issue I was having for 3 hours right away.
Please read the unsplash guidelines carefully when using their APIs!