@alsocoder/apna-media-library
v0.2.0
Published
React media library with provider, global drop upload, picker fields, and modal-based browsing.
Maintainers
Readme
@alsocoder/apna-media-library
React media library with provider, global drag-and-drop upload, modal-based browsing, picker fields, and gallery support.
Built for reuse across projects — internally bundles @alsocoder/apna-modal, @alsocoder/apna-toast, and upload UI via @alsocoder/apna-upload (ApnaTable pattern). You only install and import this package.
Install
npm install @alsocoder/apna-media-libraryPeer dependencies: react and react-dom (>= 18).
Playground
cd playground
npm install
npm run devOpens at http://localhost:5178 with a mock in-memory adapter (no backend required).
From package root:
npm run dev:playgroundCSS import
import "@alsocoder/apna-media-library/styles.css"This single stylesheet includes modal, toast, upload, and media library styles.
Quick start
import {
ApnaMediaProvider,
ApnaMediaPicker,
type ApnaMediaAdapter,
} from "@alsocoder/apna-media-library"
import "@alsocoder/apna-media-library/styles.css"
const adapter: ApnaMediaAdapter = {
async listFolders({ include } = {}) {
const res = await fetch(`/api/media/folders?include=${include?.join(",") ?? ""}`)
const data = await res.json()
return data.items ?? []
},
async listMedia({ folder, page, limit }) {
const params = new URLSearchParams({ page: String(page), limit: String(limit) })
if (folder) params.set("folder", folder)
const res = await fetch(`/api/media?${params}`)
const data = await res.json()
return { items: data.items ?? [], pagination: data.pagination }
},
async upload(file, folder, onProgress) {
const formData = new FormData()
formData.append("folder", folder)
formData.append("file", file)
const res = await fetch("/api/media/upload", { method: "POST", body: formData })
onProgress?.(100)
return res.json()
},
}
export function App() {
return (
<ApnaMediaProvider
adapter={adapter}
resolveUrl={(path) => `${import.meta.env.VITE_API_URL}${path}`}
>
<Form />
</ApnaMediaProvider>
)
}
function Form() {
const [url, setUrl] = useState("")
return (
<ApnaMediaPicker
label="Cover image"
value={url}
onChange={setUrl}
folder="blog"
/>
)
}Provider
<ApnaMediaProvider
adapter={myAdapter}
canBrowse={true}
canUpload={true}
defaultFolder="misc"
enableGlobalDrop={true}
enableToaster={true}
enablePdfPreview={true}
resolveUrl={(path) => `${API_BASE}/uploads${path}`}
onNotify={(msg) => console.log(msg)}
>
{children}
</ApnaMediaProvider>Hook: useApnaMedia()
const {
openLibrary, // (opts) => Promise<ApnaMediaItem | null>
openLibraryForDrop, // (files) => void
openPreview,
closePreview,
uploadFile,
removeItem,
ensureLoaded,
listFolderMedia,
items,
folders,
isLibraryOpen,
canBrowse,
canUpload,
} = useApnaMedia()openLibrary is promise-based — no manual modal state needed:
const item = await openLibrary({ folder: "blog", currentUrl: value })
if (item) setValue(item.url)Components
| Export | Description |
|--------|-------------|
| ApnaMediaProvider | Context + library modal, preview, global drop |
| useApnaMedia | Access provider API |
| ApnaMediaPicker | Single file field (Browse/Upload → Change → Preview) |
| ApnaMediaImageField | Picker preset for images |
| ApnaMediaGallery | Multi-image array field |
ApnaEditor integration
const { openLibrary } = useApnaMedia()
<ApnaEditor
imageUpload={{
pick: async () => {
const item = await openLibrary({ folder: "blog" })
return item ? { url: item.url, alt: item.name } : null
},
}}
/>Adapter interface
type ApnaMediaAdapter = {
listFolders: (opts?: { include?: string[] }) => Promise<ApnaMediaFolder[]>
listMedia: (opts: { folder?: string; page: number; limit: number }) => Promise<{
items: ApnaMediaItem[]
pagination: { page: number; limit: number; total: number; totalPages: number }
}>
upload: (file: File, folder: string, onProgress?: (pct: number) => void) => Promise<ApnaMediaItem>
delete?: (id: string) => Promise<void>
}Features
- Global drop — drag files anywhere on the page to open upload modal
- In-modal drop — drop zone inside library modal
- Folder tree — hierarchical folders with search
- Paginated grid — infinite scroll (20 items per page)
- PDF thumbnails — optional via
pdfjs-distdynamic import - Toasts — upload errors, copy path, etc. via bundled ApnaToast
- Z-index — uses ApnaModal stack; works with nested Select/DatePicker in modals
Customization
Override CSS variables (--apna-media-*) or use classNames prop on provider/picker.
import { defaultClassNames, mergeClassNames } from "@alsocoder/apna-media-library"Notes
- By default
ApnaMediaProviderincludes its ownApnaModalProvider— no extra modal setup needed. - If your app already uses
ApnaModalProvider, nest media inside it and passuseParentModalProvider:
<ApnaModalProvider>
<ApnaMediaProvider useParentModalProvider adapter={myAdapter}>
<App />
</ApnaMediaProvider>
</ApnaModalProvider>- Set
enableToaster={false}if you manage toasts yourself. - Set
enablePdfPreview={false}to skip PDF.js loading.
