import React, { useRef, useState } from "react"; import { DownloadIcon, UploadIcon } from "@sanity/icons"; import { BulkTab } from "./types"; /* eslint-disable @typescript-eslint/no-explicit-any */ interface CsvRow { _id: string; title: string; focusKeyword: string; found: boolean; } interface LogEntry { msg: string; ok: boolean; } interface Props { selectedCount: number; bulkTab: BulkTab; onTabChange: (tab: BulkTab) => void; bulkProcessing: boolean; onApply: () => void; csvPreview: CsvRow[]; csvError: string | null; csvApplying: boolean; onCSVFile: (fileOrEvent: File | React.ChangeEvent) => void; onApplyCSV: () => void; log: LogEntry[]; selectedMissingTitle: number; } const TAB_LABELS: Record = { og: "Sync Open Graph", csv: "Import CSV", }; export default function BulkActions({ selectedCount, bulkTab, onTabChange, bulkProcessing, onApply, csvPreview, csvError, csvApplying, onCSVFile, onApplyCSV, log, selectedMissingTitle, }: Props) { const csvFileRef = useRef(null); const canApply = !bulkProcessing && selectedCount > 0; return (
Bulk Actions {selectedCount} selected
{(Object.keys(TAB_LABELS) as BulkTab[]).map((tab) => ( ))}
{bulkTab === "og" && (
{selectedMissingTitle > 0 && (
⚠️ {selectedMissingTitle} of your selected page {selectedMissingTitle !== 1 ? "s have" : " has"} no Meta Title — those will be skipped. Set a Meta Title first by expanding the row below.
)}
When someone shares your page on{" "} Twitter, LinkedIn, WhatsApp or any social platform, they see the{" "} Open Graph title — not the meta title. If OG is empty, social platforms guess a title which often looks broken.
Each page has
✓ Meta Title
✓ Meta Description
✗ OG Title (empty)
✗ OG Description (empty)
After applying
✓ Meta Title
✓ Meta Description
✓ OG Title (synced)
✓ OG Description (synced)
Each page gets its own values — pages without a meta title are skipped.
)} {bulkTab === "csv" && ( )} {bulkTab !== "csv" && (
)} {log.length > 0 && (
Result
{log.map((entry, i) => (
{entry.msg}
))}
)}
); } function CsvImportTab({ csvFileRef, csvPreview, csvError, csvApplying, onCSVFile, onApplyCSV, }: { csvFileRef: React.RefObject; csvPreview: CsvRow[]; csvError: string | null; csvApplying: boolean; onCSVFile: (fileOrEvent: File | React.ChangeEvent) => void; onApplyCSV: () => void; }) { const matched = csvPreview.filter((r) => r.found).length; const unmatched = csvPreview.filter((r) => !r.found).length; const [isDragging, setIsDragging] = useState(false); const handleDrag = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.type === "dragenter" || e.type === "dragover") { setIsDragging(true); } else if (e.type === "dragleave") { setIsDragging(false); } }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); if (e.dataTransfer.files && e.dataTransfer.files[0]) { onCSVFile(e.dataTransfer.files[0]); } }; return ( <>
Expected CSV headers (first row):
_id, metaTitle, metaDescription, focusKeyword, ogTitle, ogDescription
Use{" "} Export CSV {" "} below the table to get a pre-filled template with current values — then edit in Excel or Google Sheets and re-import here.
{csvPreview.length > 0 && (
{csvPreview.length} rows parsed successfully from file
)} {csvError && (
{csvError}
)} {csvPreview.length > 0 && (
Preview — {matched} matched / {unmatched} not found
{csvPreview.map((row, i) => (
{row.title} {row.focusKeyword || "—"}
))}
)} ); }