A web app to help you design things, local, offline, on device. In your browser.

TypeScript 53.1% JSON 45.6% CSS 0.9% Markdown 0.3% JavaScript 0.1%
ExportPanel.tsx 318 lines (11 KB)
'use client';

import React, { useState, useRef } from 'react';
import { Download, Check, Copy, Smartphone, Tablet, Monitor, Share2 } from 'lucide-react';
import { toPng, toJpeg } from 'html-to-image';
import { useEditorStore } from '@/lib/store/editor-store';
import { EXPORT_PRESETS, type ExportPreset, type ExportConfig } from '@/types';

const presetGroups = [
  {
    name: 'App Store',
    icon: Smartphone,
    presets: ['iphone-6.9', 'iphone-6.5', 'iphone-6.1', 'ipad-13', 'ipad-11', 'mac'] as ExportPreset[],
  },
  {
    name: 'Social Media',
    icon: Share2,
    presets: [
      'instagram-post',
      'instagram-story',
      'twitter-post',
      'linkedin-post',
      'facebook-post',
    ] as ExportPreset[],
  },
  {
    name: 'Marketing',
    icon: Monitor,
    presets: ['product-hunt', 'youtube-thumbnail'] as ExportPreset[],
  },
];

export function ExportPanel() {
  const [selectedPresets, setSelectedPresets] = useState<ExportPreset[]>(['iphone-6.5']);
  const [format, setFormat] = useState<'png' | 'jpg'>('png');
  const [quality, setQuality] = useState(100);
  const [scale, setScale] = useState(1);
  const [isExporting, setIsExporting] = useState(false);
  const [exportComplete, setExportComplete] = useState(false);

  const { canvas, layers } = useEditorStore();

  const togglePreset = (preset: ExportPreset) => {
    setSelectedPresets((prev) =>
      prev.includes(preset) ? prev.filter((p) => p !== preset) : [...prev, preset]
    );
  };

  const selectAll = (presets: ExportPreset[]) => {
    const allSelected = presets.every((p) => selectedPresets.includes(p));
    if (allSelected) {
      setSelectedPresets((prev) => prev.filter((p) => !presets.includes(p)));
    } else {
      setSelectedPresets((prev) => [...new Set([...prev, ...presets])]);
    }
  };

  const handleExport = async () => {
    setIsExporting(true);
    setExportComplete(false);

    // Get the canvas element (we'll need to implement this with Konva)
    const stage = document.querySelector('.konva-container canvas') as HTMLCanvasElement;

    if (!stage) {
      console.error('Canvas not found');
      setIsExporting(false);
      return;
    }

    try {
      for (const preset of selectedPresets) {
        const config = EXPORT_PRESETS[preset];

        // Create a temporary canvas for export
        const tempCanvas = document.createElement('canvas');
        tempCanvas.width = config.width * scale;
        tempCanvas.height = config.height * scale;
        const ctx = tempCanvas.getContext('2d');

        if (!ctx) continue;

        // Draw the stage content scaled
        ctx.drawImage(stage, 0, 0, tempCanvas.width, tempCanvas.height);

        // Convert to image
        const dataUrl =
          format === 'png'
            ? tempCanvas.toDataURL('image/png')
            : tempCanvas.toDataURL('image/jpeg', quality / 100);

        // Download
        const link = document.createElement('a');
        link.download = `${config.name.replace(/[^a-z0-9]/gi, '-').toLowerCase()}-${config.width}x${config.height}.${format}`;
        link.href = dataUrl;
        link.click();

        // Small delay between downloads
        await new Promise((r) => setTimeout(r, 500));
      }

      setExportComplete(true);
      setTimeout(() => setExportComplete(false), 3000);
    } catch (error) {
      console.error('Export failed:', error);
    } finally {
      setIsExporting(false);
    }
  };

  return (
    <div className="p-4">
      <h2 className="text-lg font-semibold text-white mb-4">Export</h2>

      {/* Preset selection */}
      <div className="space-y-4 mb-6">
        {presetGroups.map((group) => {
          const Icon = group.icon;
          const allSelected = group.presets.every((p) => selectedPresets.includes(p));
          const someSelected = group.presets.some((p) => selectedPresets.includes(p));

          return (
            <div key={group.name}>
              <button
                onClick={() => selectAll(group.presets)}
                className="flex items-center gap-2 w-full text-left mb-2 group"
              >
                <div
                  className={`
                  w-5 h-5 rounded border-2 flex items-center justify-center transition-colors
                  ${
                    allSelected
                      ? 'bg-indigo-500 border-indigo-500'
                      : someSelected
                        ? 'border-indigo-500 bg-indigo-500/30'
                        : 'border-neutral-600 group-hover:border-neutral-500'
                  }
                `}
                >
                  {(allSelected || someSelected) && <Check className="w-3 h-3 text-white" />}
                </div>
                <Icon className="w-4 h-4 text-neutral-400" />
                <span className="text-sm text-neutral-300 group-hover:text-white transition-colors">
                  {group.name}
                </span>
              </button>

              <div className="ml-7 space-y-1">
                {group.presets.map((preset) => {
                  const config = EXPORT_PRESETS[preset];
                  const isSelected = selectedPresets.includes(preset);

                  return (
                    <button
                      key={preset}
                      onClick={() => togglePreset(preset)}
                      className={`
                        w-full flex items-center justify-between p-2 rounded-lg text-left
                        transition-colors
                        ${
                          isSelected
                            ? 'bg-indigo-500/20 border border-indigo-500/50'
                            : 'hover:bg-neutral-800 border border-transparent'
                        }
                      `}
                    >
                      <div className="flex items-center gap-2">
                        <div
                          className={`
                          w-4 h-4 rounded border-2 flex items-center justify-center
                          ${
                            isSelected
                              ? 'bg-indigo-500 border-indigo-500'
                              : 'border-neutral-600'
                          }
                        `}
                        >
                          {isSelected && <Check className="w-2.5 h-2.5 text-white" />}
                        </div>
                        <span
                          className={`text-sm ${isSelected ? 'text-white' : 'text-neutral-400'}`}
                        >
                          {config.name}
                        </span>
                      </div>
                      <span className="text-xs text-neutral-500">
                        {config.width}x{config.height}
                      </span>
                    </button>
                  );
                })}
              </div>
            </div>
          );
        })}
      </div>

      {/* Export options */}
      <div className="space-y-4 mb-6">
        {/* Format */}
        <div>
          <label className="text-sm text-neutral-400 block mb-2">Format</label>
          <div className="flex gap-2">
            {(['png', 'jpg'] as const).map((f) => (
              <button
                key={f}
                onClick={() => setFormat(f)}
                className={`
                  flex-1 py-2 rounded-lg text-sm font-medium uppercase transition-colors
                  ${
                    format === f
                      ? 'bg-indigo-500/20 text-indigo-400 border border-indigo-500/50'
                      : 'bg-neutral-800 text-neutral-400 border border-neutral-700 hover:text-white'
                  }
                `}
              >
                {f}
              </button>
            ))}
          </div>
        </div>

        {/* Quality (for JPG) */}
        {format === 'jpg' && (
          <div>
            <div className="flex justify-between text-sm mb-2">
              <span className="text-neutral-400">Quality</span>
              <span className="text-white">{quality}%</span>
            </div>
            <input
              type="range"
              min={10}
              max={100}
              value={quality}
              onChange={(e) => setQuality(Number(e.target.value))}
              className="w-full"
            />
          </div>
        )}

        {/* Scale */}
        <div>
          <label className="text-sm text-neutral-400 block mb-2">Scale</label>
          <div className="flex gap-2">
            {[1, 2, 3].map((s) => (
              <button
                key={s}
                onClick={() => setScale(s)}
                className={`
                  flex-1 py-2 rounded-lg text-sm transition-colors
                  ${
                    scale === s
                      ? 'bg-indigo-500/20 text-indigo-400 border border-indigo-500/50'
                      : 'bg-neutral-800 text-neutral-400 border border-neutral-700 hover:text-white'
                  }
                `}
              >
                {s}x
              </button>
            ))}
          </div>
        </div>
      </div>

      {/* Export button */}
      <button
        onClick={handleExport}
        disabled={isExporting || selectedPresets.length === 0}
        className={`
          w-full py-3 rounded-xl font-medium
          flex items-center justify-center gap-2
          transition-all
          ${
            exportComplete
              ? 'bg-green-500 text-white'
              : isExporting
                ? 'bg-indigo-500/50 text-white/50 cursor-wait'
                : selectedPresets.length === 0
                  ? 'bg-neutral-700 text-neutral-500 cursor-not-allowed'
                  : 'bg-indigo-500 hover:bg-indigo-600 text-white'
          }
        `}
      >
        {exportComplete ? (
          <>
            <Check className="w-5 h-5" />
            Exported!
          </>
        ) : isExporting ? (
          <>
            <div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
            Exporting...
          </>
        ) : (
          <>
            <Download className="w-5 h-5" />
            Export {selectedPresets.length} {selectedPresets.length === 1 ? 'Size' : 'Sizes'}
          </>
        )}
      </button>

      {/* Quick copy dimensions */}
      <div className="mt-4 p-3 rounded-lg bg-neutral-800/50 border border-neutral-700/50">
        <div className="flex items-center justify-between text-sm">
          <span className="text-neutral-400">Current canvas</span>
          <button
            onClick={() => navigator.clipboard.writeText(`${canvas.width}x${canvas.height}`)}
            className="flex items-center gap-1 text-neutral-300 hover:text-white transition-colors"
          >
            <Copy className="w-3 h-3" />
            {canvas.width}x{canvas.height}
          </button>
        </div>
      </div>
    </div>
  );
}

About

A web app to help you design things, local, offline, on device. In your browser.

0 stars
0 forks