Intro
As a photographer, finding reliable and straightforward tools is a headache. Online solutions are often cluttered, overpriced, or compromise your privacy by uploading your photos to external servers.
I'm a photographer and post almost daily on Instagram, so I'm constantly both using, and looking for better, simpler tools.
That’s why I created imgxLab, a Next.js open-source lab tailored specifically for photographers, by photographers. Inspired by projects like QuickPic by Theo Browne.
What is imgxLab?
imgxLab is a collection of essential tools that every photographer needs at some point. From analyzing metadata to converting image formats, imgxLab offers simple and intuitive solutions without the hassle of creating accounts or relying on internet connectivity. Here are the current tools available:
- Metadata Analyzer: Quickly view detailed metadata from your photos, including camera settings, lens information, and more.
- Frame Insets Designer: Add customizable frames to your images, adjusting width, aspect ratios, and background colors to suit your style.
- Image Compressor: Reduce image file sizes without sacrificing quality, making your photos easier to share and store.
- WebP to PNG Converter: Easily convert images between WebP and PNG formats for broader compatibility.
Directory Breakdown
imgxLab/
├── public/
│ └── imgxlab-hero.png
├── src/
│ ├── app/
│ │ ├── page.tsx
│ │ └── (tools)/
│ │ ├── metadata-viewer/
│ │ │ └── metadata-tool.tsx
│ │ │ └── page.tsx
│ ├── components/
│ │ ├── shared/
│ │ │ ├── file-dropzone.tsx
│ │ │ └── upload-box.tsx
│ ├── hooks/
│ │ ├── use-file-uploader.ts
│ │ └── use-local-storage.ts
├── styles/
│ └── globals.css
├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── tsconfig.json
└── next.config.js
Shared Components
imgxLab is built with reusability in mind. We use shared components like FileDropzone
and UploadBox
across all tools, ensuring a consistent user experience and simplifying the addition of new tools in the future. Here’s a glimpse of how these components work:
- The FileDropzone Component
This component provides a drag-and-drop interface for file uploads, handling various states like dragging, dropping, and validating file types. Its flexibility allows it to be integrated seamlessly into any tool that requires file uploads.
// src/components/shared/file-dropzone.tsx
import React, { useCallback, useState, useRef } from "react";
interface FileDropzoneProps {
children: React.ReactNode;
acceptedFileTypes: string[];
dropText: string;
setCurrentFile: (file: File) => void;
}
export function FileDropzone({
children,
acceptedFileTypes,
dropText,
setCurrentFile,
}: FileDropzoneProps) {
const [isDragging, setIsDragging] = useState(false);
const dragCounter = useRef(0);
const handleDrag = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
}, []);
const handleDragIn = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
dragCounter.current++;
if (e.dataTransfer?.items && e.dataTransfer.items.length > 0) {
setIsDragging(true);
}
}, []);
const handleDragOut = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
dragCounter.current--;
if (dragCounter.current === 0) {
setIsDragging(false);
}
}, []);
const handleDrop = useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
dragCounter.current = 0;
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const droppedFile = files[0];
if (!droppedFile) {
alert("No files were uploaded. Please ensure you drag and drop valid files.");
throw new Error("File upload error: No files detected.");
}
const fileName = droppedFile.name.toLowerCase();
const fileType = droppedFile.type.toLowerCase();
const fileExtension = fileName.substring(fileName.lastIndexOf("."));
// Normalize accepted file types for consistent comparison
const normalizedAcceptedTypes = acceptedFileTypes.map((type) =>
type.replace("*", "").toLowerCase()
);
// Validate by MIME type or common image extensions
const isValidType =
normalizedAcceptedTypes.includes(fileType) ||
(normalizedAcceptedTypes.includes("image/") &&
[".jpg", ".jpeg", ".png", ".webp", ".svg"].includes(fileExtension));
if (!isValidType) {
alert("Invalid file type. Please upload a supported file type.");
return;
}
// Happy path
setCurrentFile(droppedFile);
}
},
[acceptedFileTypes, setCurrentFile]
);
return (
<div
onDragEnter={handleDragIn}
onDragLeave={handleDragOut}
onDragOver={handleDrag}
onDrop={handleDrop}
className="h-full w-full"
>
{isDragging && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/80 backdrop-blur-sm" />
<div className="animate-in fade-in zoom-in relative flex h-[80%] w-[80%] transform items-center justify-center border border-white/20 transition-all duration-300 ease-out rounded-xl bg-zinc-900">
<p className="text-2xl font-semibold text-white">{dropText}</p>
</div>
</div>
)}
{children}
</div>
);
}
- The UploadBox Component
Complementing FileDropzone
, UploadBox
provides a user-friendly interface with clear instructions and visual cues for uploading files.
// src/components/shared/upload-box.tsx
import React from "react";
import { UploadIcon } from "lucide-react";
interface UploadBoxProps {
title: string;
description: string;
accept: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
export function UploadBox({
title,
description,
accept,
onChange,
}: UploadBoxProps) {
return (
<div className="flex flex-col items-center justify-center gap-6">
<div className="flex flex-col items-center gap-2">
<p className="text-balance text-lg text-center text-zinc-300">{title}</p>
</div>
<div className="flex w-full flex-col items-center justify-center gap-8 rounded-xl border border-white/20 bg-zinc-900 p-10">
<div className="flex items-center space-x-3">
<UploadIcon className="h-6 w-6 text-zinc-300" />
<p className="font-semibold text-zinc-300">Drag & Drop</p>
</div>
<p className="text-base text-zinc-500">Or</p>
<label className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-zinc-600 px-4 py-2 font-semibold text-white shadow-md transition-colors duration-200 hover:bg-zinc-700 focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:ring-opacity-75">
<span>{description}</span>
<input
type="file"
onChange={onChange}
accept={accept}
className="hidden"
/>
</label>
</div>
</div>
);
}
Local Storage: Ensuring Privacy and Offline Capability
All user data and settings are stored locally, ensuring that no files are ever uploaded or shared externally.
- The useLocalStorage Hook
The useLocalStorage
hook abstracts the logic for interacting with local storage, providing a seamless and persistent state management solution across different tools.
// src/hooks/use-local-storage.ts
import { useState } from "react";
export function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === "undefined") return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
}
};
return [storedValue, setValue] as const;
}
This hook ensures that any state managed through it persists across browser sessions, providing users with a consistent experience every time they use imgxLab.
The Power of Local Storage: Privacy and Offline Functionality
By processing all data locally within the browser and utilizing local storage, we ensure that:
- Data Remains Private: No images or metadata are ever uploaded to external servers.
- Offline Capability: Tools function entirely offline, allowing users to work without an internet connection.
- Persistent Settings: User preferences and tool settings are saved between sessions, enhancing the user experience.
This architecture empowers photographers to use imgxLab with complete peace of mind, knowing their data is secure and accessible anytime.
Breakdown of Each Tool
I will create dedicated blog posts for each of the current and future tools of imgxLab, focusing on the code and the reasoning behind them. This section will be updated with links as the blog posts are published. Stay tuned!
A Nod to Inspiration: Respecting QuickPic by Theo Browne
imgxLab draws inspiration from QuickPic by Theo Browne, a great developer, and a great project that exemplifies the power of open-source collaboration. imgxLab builds upon those concepts, enhancing and expanding them to cater to the photography niche.