@katebtech/media
Reusable media components and utilities for images, uploads, galleries, and asset handling.
@katebtech/media
Shared media helpers, Cloudinary utilities, and React/Next.js components for Kateb Tech apps.
Purpose
@katebtech/media provides reusable media primitives for Kateb Tech applications. Use it when an app needs consistent image metadata, optimized Cloudinary image URLs, upload and delete helpers, preview galleries, or Quill-based rich text editing.
This package expects React and Next.js from the consuming app.
Package Overview
The package includes:
- Cloudinary URL builders for common image sizes and layouts.
- Client upload and delete helpers for Cloudinary assets.
- Server route factories for signed uploads and protected asset deletion.
- Image components for previews, galleries, placeholders, and clickable large images.
- Quill editor components and HTML utilities for rich text content.
- Public TypeScript types for exported components and shared media data.
File Structure
src
├── cloudinary
│ ├── _lib
│ ├── client
│ └── server
├── images
│ ├── clickable
│ └── clientComponents
└── quil-editor
├── _lib
├── QuillContentSection.tsx
├── QuillEditor.tsx
└── index.tsImport Style
Consume package APIs through specific package subpaths.
import { cldGalleryImage } from "@katebtech/media/cloudinary/_lib";
import { CldFileUpload } from "@katebtech/media/cloudinary/client";
import { createCldUploadRoute } from "@katebtech/media/cloudinary/server";
import { ImageGallery, type ImageMeta } from "@katebtech/media/images";
import { QuillEditor } from "@katebtech/media/quill-editor";Avoid importing from implementation files that are not package exports.
Package Rules
- Keep Cloudinary client-only components in
cloudinary/client. - Keep Cloudinary server-only helpers and route factories in
cloudinary/server. - Keep shared Cloudinary types and URL builders in
cloudinary/_lib. - Store reusable image data as
ImageMeta. - Use
safeFolderfor destructive Cloudinary server actions. - Prefer exported subpaths over root imports when consuming this package.
- Keep package README content focused on package APIs and architecture.
Module Documentation
Cloudinary URL Helpers
Use URL helpers from @katebtech/media/cloudinary/_lib to build optimized Cloudinary image URLs.
import {
cldGalleryImage,
cldHeroBackgroundImage,
cldLargePreviewImage,
cldServiceCardImage,
cldThumbnailImage,
} from "@katebtech/media/cloudinary/_lib";
const heroUrl = cldHeroBackgroundImage("services/web-design/hero.jpg");
const cardUrl = cldServiceCardImage("services/web-design/card.jpg");
const galleryUrl = cldGalleryImage("services/web-design/gallery-1.jpg");
const thumbnailUrl = cldThumbnailImage("services/web-design/gallery-1.jpg");
const previewUrl = cldLargePreviewImage("services/web-design/gallery-1.jpg");Pass a custom Cloudinary cloud name when a consuming app does not use the package default.
import { cldGalleryImage } from "@katebtech/media/cloudinary/_lib";
const imageUrl = cldGalleryImage("folder/image.jpg", {
cloudName: "my-cloud-name",
});Cloudinary Client
Use CldFileUpload in client components when a form needs a Cloudinary upload button with preview and remove support.
"use client";
import { CldFileUpload } from "@katebtech/media/cloudinary/client";
import type { CldFileUploadProps } from "@katebtech/media/cloudinary/_lib";
import { useState } from "react";
export function HeroImageUpload() {
const [imagePath, setImagePath] = useState("");
const uploadProps: CldFileUploadProps = {
title: "Hero",
uploadPreset: "my-upload-preset",
value: imagePath,
onChange: setImagePath,
allowedFormats: ["jpg", "jpeg", "png", "webp"],
resourceType: "image",
};
return <CldFileUpload {...uploadProps} />;
}value should be the Cloudinary path returned by the upload widget, such as v123/folder/file.png. The component calls onChange(path) after upload and onChange("") after a successful remove.
Use deleteCldAssetClient when deleting an asset from a client-side workflow.
import { deleteCldAssetClient } from "@katebtech/media/cloudinary/client";
const result = await deleteCldAssetClient({
path: "services/web-design/gallery-1.jpg",
resourceType: "image",
});
if (!result.ok) {
console.error(result.message);
}Cloudinary Server
Use createDeleteCldAssetServer for protected server-side deletion.
import { createDeleteCldAssetServer } from "@katebtech/media/cloudinary/server";
export const deleteCldAssetServer = createDeleteCldAssetServer({
cloudName: process.env.CLOUDINARY_CLOUD_NAME!,
apiKey: process.env.CLOUDINARY_API_KEY!,
apiSecret: process.env.CLOUDINARY_API_SECRET!,
safeFolder: "services",
});The server helper only deletes assets whose public id starts with safeFolder, which protects unrelated Cloudinary assets.
Use route factories in Next.js route handlers.
import { createCldUploadRoute } from "@katebtech/media/cloudinary/server";
export const POST = createCldUploadRoute({
cloudName: process.env.CLOUDINARY_CLOUD_NAME!,
apiKey: process.env.CLOUDINARY_API_KEY!,
apiSecret: process.env.CLOUDINARY_API_SECRET!,
});import { createCldDestroyRoute } from "@katebtech/media/cloudinary/server";
export const POST = createCldDestroyRoute({
cloudName: process.env.CLOUDINARY_CLOUD_NAME!,
apiKey: process.env.CLOUDINARY_API_KEY!,
apiSecret: process.env.CLOUDINARY_API_SECRET!,
safeFolder: "services",
});Image Data
Use ImageMeta for reusable image data.
import type { ImageMeta } from "@katebtech/media/images";
const images: readonly ImageMeta[] = [
{
url: "services/web-design/gallery-1.jpg",
alt: "Website homepage preview",
cld: true,
},
{
url: "/images/local-preview.jpg",
alt: "Local project preview",
cld: false,
},
];Preview Image
Use PreviewImage for a single clickable image or a lightweight fullscreen preview.
import { PreviewImage } from "@katebtech/media/images";
export function ServiceCard() {
return (
<PreviewImage
image={{
url: "services/web-design/card.jpg",
alt: "Web design service preview",
cld: true,
}}
sizes="(min-width: 768px) 33vw, 100vw"
/>
);
}Pass multiple images and customize the Cloudinary transform when needed.
import { cldThumbnailImage } from "@katebtech/media/cloudinary/_lib";
import { PreviewImage, type ImageMeta } from "@katebtech/media/images";
const images: ImageMeta[] = [
{
url: "services/web-design/gallery-1.jpg",
alt: "Gallery image",
cld: true,
},
];
<PreviewImage
image={images}
aspect="aspect-[4/3]"
imageTransformer={cldThumbnailImage}
/>;Image Gallery
Use ImageGallery for a full gallery with a main image, thumbnails, and fullscreen navigation.
import { ImageGallery, type ImageMeta } from "@katebtech/media/images";
const images: ImageMeta[] = [
{
url: "services/web-design/gallery-1.jpg",
alt: "Homepage screen",
cld: true,
},
{
url: "services/web-design/gallery-2.jpg",
alt: "Responsive screen",
cld: true,
},
];
export function ProjectGallery() {
return <ImageGallery images={images} priorityFirstImage />;
}Clickable Images
Use clickable image components from @katebtech/media/images/clickable when a consuming app needs the larger image preview pattern directly.
import { ClickableLargeImage } from "@katebtech/media/images/clickable";Placeholders
Use placeholder exports from @katebtech/media/images for shared image fallback UI.
import {
AVATAR_PLACEHOLDER_DATA_URL,
AVATAR_PLACEHOLDER_SVG,
getBlurDataURL,
HAZARISTAN_FLAG_DATA_URL,
HAZARISTAN_FLAG_SVG,
IMAGE_DEFAULT_BLUR,
} from "@katebtech/media/images";
const blurDataURL = getBlurDataURL("#f8fafc");Quill Editor
Use QuillEditor in client components for a controlled rich text editor with the package toolbar and optional character limit.
"use client";
import {
QuillEditor,
type QuillEditorProps,
} from "@katebtech/media/quill-editor";
import { useState } from "react";
export function ServiceDescriptionEditor() {
const [description, setDescription] = useState("");
const editorProps: QuillEditorProps = {
id: "service-description",
value: description,
onChange: setDescription,
placeholder: "Write the service description...",
maxChars: 1200,
};
return <QuillEditor {...editorProps} />;
}Use QuillContentSection to render saved Quill HTML with the package content styling and cleanup utilities.
import { QuillContentSection } from "@katebtech/media/quill-editor";
export function ServiceDescription({ content }: { content: string }) {
return <QuillContentSection content={content} />;
}