Skip to main content
The Customer Photo Manager allows you to curate photos displayed on your storefront’s photo wall.

Photo Structure

Each photo contains:
  • image_url - Public URL to the uploaded image
  • position - Display order (integer)
  • is_visible - Visibility toggle (boolean)
  • created_at - Upload timestamp
Database Table: customer_photos Storage Bucket: customer-photos

Uploading Photos

Two upload methods are available:

1. Drag and Drop

1

Drag Files

Drag image files over the upload zoneZone highlights when files are dragged over it
2

Drop to Upload

Release to start upload process
// CustomerPhotoManager.jsx:102
const handleDropZoneDrop = (e) => {
  e.preventDefault();
  setIsDragOver(false);
  handleFiles(e.dataTransfer.files);
};
3

Multiple Files

Multiple images can be dropped simultaneouslyAll valid image files will be uploaded

2. File Browser

1

Click Upload Zone

Click anywhere in the upload zoneOpens file picker dialog
2

Select Images

Choose one or multiple image filesAccepted: All image/* MIME types
3

Upload

Files upload automatically after selection

Upload Process

// CustomerPhotoManager.jsx:50
const handleFiles = useCallback(
  async (files) => {
    if (!files || files.length === 0) return;

    setUploading(true);
    const currentMax = photos.length > 0
      ? Math.max(...photos.map((p) => p.position))
      : -1;

    let successCount = 0;
    for (let i = 0; i < files.length; i++) {
      const file = files[i];
      if (!file.type.startsWith("image/")) continue;

      const { error: upErr } = await uploadPhoto(file, currentMax + 1 + i);
      if (upErr) {
        pushToast(t("admin.customerPhotos.error.upload"), "error");
      } else {
        successCount++;
      }
    }

    if (successCount > 0) {
      pushToast(t("admin.customerPhotos.success.upload"), "success");
      await fetchPhotos();
    }
    setUploading(false);
  },
  [photos, t, fetchPhotos]
);
Position Calculation:
  • New photos appended to end
  • Position = current max position + 1
  • Sequential positions for multiple uploads
Only image files are processed. Non-image files in a multi-file selection are skipped automatically.

Photo Grid

Photos display in a responsive grid with controls:

Photo Card Components

Drag Handle (top-left):
  • Six-dot icon for reordering
  • Indicates draggable functionality
Position Badge:
  • Shows current position number (#1, #2, etc.)
  • Updates when reordering
Action Buttons:
  • Eye Icon: Toggle visibility
  • Trash Icon: Delete photo
Implementation: CustomerPhotoManager.jsx:263

Visibility Control

Toggle whether photos appear on the public storefront:
// CustomerPhotoManager.jsx:125
const handleToggleVisibility = useCallback(
  async (photo) => {
    const newState = !photo.is_visible;
    const { error: togErr } = await toggleVisibility(photo.id, newState);
    if (togErr) {
      pushToast(t("admin.customerPhotos.error.toggle"), "error");
    } else {
      pushToast(t("admin.customerPhotos.success.visibility"), "success");
      setPhotos((prev) =>
        prev.map((p) => (p.id === photo.id ? { ...p, is_visible: newState } : p))
      );
    }
  },
  [t]
);
1

Click Eye Icon

Eye Open: Photo is visible → click to hideEye Closed: Photo is hidden → click to show
2

Instant Update

Visibility updates immediately in databaseCard opacity changes to indicate hidden state
Visual Indicator:
// CustomerPhotoManager.jsx:266
className={`photo-manager__card ${!photo.is_visible ? "is-hidden" : ""}`}
Hidden photos display with reduced opacity.

Reordering Photos

Drag-and-drop reordering changes photo display sequence:

Drag Operations

1

Start Drag

Click and hold on drag handle or photo card
// CustomerPhotoManager.jsx:142
const handleDragStart = useCallback((e, index) => {
  setDraggedIndex(index);
  e.dataTransfer.effectAllowed = "move";
  e.dataTransfer.setData("text/plain", index.toString());
  e.currentTarget.classList.add("is-dragging");
}, []);
2

Drag Over Target

Drag over desired positionTarget position highlights
3

Drop to Reorder

Release to move photo
// CustomerPhotoManager.jsx:171
const handleDrop = useCallback(
  async (e, toIndex) => {
    e.preventDefault();
    setDragOverIndex(null);

    if (draggedIndex === null || draggedIndex === toIndex) {
      setDraggedIndex(null);
      return;
    }

    // Reorder locally
    const reordered = [...photos];
    const [moved] = reordered.splice(draggedIndex, 1);
    reordered.splice(toIndex, 0, moved);

    // Update positions
    const updates = reordered.map((photo, idx) => ({
      id: photo.id,
      position: idx,
    }));

    setPhotos(reordered);
    setDraggedIndex(null);

    // Persist to database
    const { error: reorderErr } = await reorderPhotos(updates);
    if (reorderErr) {
      pushToast(t("admin.customerPhotos.error.reorder"), "error");
      await fetchPhotos();
    } else {
      pushToast(t("admin.customerPhotos.success.reorder"), "success");
    }
  },
  [draggedIndex, photos, t, fetchPhotos]
);
4

Position Update

All affected photos update their positionsChanges persist to database
Reordering updates all photo positions in a single batch operation for efficiency.

Visual Feedback

Dragging State:
  • Dragged photo: .is-dragging class
  • Drop target: .drag-over class
  • Upload zone: .is-dragging class when files hover

Deleting Photos

Deleting a photo removes it from storage and the database permanently. This action cannot be undone.
1

Click Trash Icon

Click delete button on photo card
2

Confirm Deletion

Confirm in browser dialog
// CustomerPhotoManager.jsx:111
if (!confirm(t("admin.customerPhotos.confirmDelete"))) return;
3

Delete from Storage

Photo removed from Supabase Storage bucketDatabase record deleted
const { error: delErr } = await deletePhoto(photo.id, photo.image_url);
if (delErr) {
  pushToast(t("admin.customerPhotos.error.delete"), "error");
} else {
  pushToast(t("admin.customerPhotos.success.delete"), "success");
  setPhotos((prev) => prev.filter((p) => p.id !== photo.id));
}

Reorder Hint

When multiple photos exist, a hint displays:
// CustomerPhotoManager.jsx:248
{photos.length > 1 && (
  <p className="photo-manager__reorder-hint">
    {t("admin.customerPhotos.reorderHint")}
  </p>
)}
Prompts users to drag photos to change order.

Empty State

When no photos exist:
// CustomerPhotoManager.jsx:256
<div className="photo-wall__empty">
  <Upload size={48} strokeWidth={1} />
  <h3>{t("admin.customerPhotos.empty.title")}</h3>
  <p>{t("admin.customerPhotos.empty.description")}</p>
</div>
Shows upload icon and prompt to add first photo.

Loading States

Upload in Progress:
// CustomerPhotoManager.jsx:228
{uploading ? (
  <p>{t("admin.common.loading")}</p>
) : (
  // Upload zone content
)}
Data Loading:
  • LoadingMessage component during initial fetch
  • ErrorMessage with retry if fetch fails

Image Optimization

Photos use lazy loading:
// CustomerPhotoManager.jsx:290
<img
  src={photo.image_url}
  alt={t("admin.customerPhotos.photoAlt", { position: index + 1 })}
  className="photo-manager__card-image"
  loading="lazy"
/>
Improves page performance with many photos.

Accessibility

Keyboard Navigation:
// CustomerPhotoManager.jsx:221
onKeyDown={(e) => {
  if (e.key === "Enter" || e.key === " ") {
    e.preventDefault();
    handleUploadClick();
  }
}}
Upload zone accessible via keyboard. ARIA Labels:
  • Action buttons have descriptive labels
  • Alt text includes position numbers

Photo Display Order

Photos are fetched ordered by position:
// services/adminCustomerPhotos.js (referenced)
// SELECT * FROM customer_photos ORDER BY position ASC
Lowest position number displays first in grid.