PlatePickerModal.tsx 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. import { Layers, X } from 'lucide-react';
  2. import { useTranslation } from 'react-i18next';
  3. import type { PlateMetadata } from '../types/plates';
  4. import { withStreamToken } from '../api/client';
  5. import { formatDuration } from '../utils/date';
  6. interface PlatePickerModalProps {
  7. plates: PlateMetadata[];
  8. onSelect: (plateIndex: number) => void;
  9. onClose: () => void;
  10. }
  11. export function PlatePickerModal({ plates, onSelect, onClose }: PlatePickerModalProps) {
  12. const { t } = useTranslation();
  13. return (
  14. <div
  15. className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
  16. onClick={onClose}
  17. >
  18. <div
  19. className="w-full max-w-3xl max-h-[85vh] flex flex-col rounded-lg bg-bambu-dark-secondary border border-bambu-dark-tertiary/60"
  20. onClick={(e) => e.stopPropagation()}
  21. >
  22. {/* Header */}
  23. <div className="flex-shrink-0 flex items-start justify-between gap-3 px-4 pt-4 pb-3 border-b border-bambu-dark-tertiary/40">
  24. <div className="min-w-0">
  25. <h3 className="text-white font-medium">{t('archives.platePicker.title')}</h3>
  26. <p className="text-xs text-bambu-gray mt-1">{t('archives.platePicker.hint')}</p>
  27. </div>
  28. <button
  29. onClick={onClose}
  30. className="flex-shrink-0 text-bambu-gray hover:text-white transition-colors"
  31. aria-label={t('common.close', 'Close')}
  32. >
  33. <X className="w-5 h-5" />
  34. </button>
  35. </div>
  36. {/* Grid */}
  37. <div className="flex-1 overflow-y-auto p-4">
  38. <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
  39. {plates.map((plate) => (
  40. <button
  41. key={plate.index}
  42. type="button"
  43. onClick={() => onSelect(plate.index)}
  44. className="flex items-center gap-2 p-2 rounded-lg border border-bambu-dark-tertiary bg-bambu-dark hover:border-bambu-gray transition-colors text-left"
  45. >
  46. {plate.has_thumbnail && plate.thumbnail_url != null ? (
  47. <img
  48. src={withStreamToken(plate.thumbnail_url)}
  49. alt={`Plate ${plate.index}`}
  50. className="w-12 h-12 rounded object-cover bg-bambu-dark-tertiary flex-shrink-0"
  51. />
  52. ) : (
  53. <div className="w-12 h-12 rounded bg-bambu-dark-tertiary flex items-center justify-center flex-shrink-0">
  54. <Layers className="w-5 h-5 text-bambu-gray" />
  55. </div>
  56. )}
  57. <div className="min-w-0 flex-1">
  58. <p className="text-sm text-white font-medium truncate">
  59. {plate.name
  60. ? `${t('archives.platePicker.plateLabel', { index: plate.index })} — ${plate.name}`
  61. : t('archives.platePicker.plateLabel', { index: plate.index })}
  62. </p>
  63. <p className="text-xs text-bambu-gray truncate">
  64. {plate.objects.length > 0
  65. ? plate.objects.slice(0, 3).join(', ') +
  66. (plate.objects.length > 3 ? '…' : '')
  67. : plate.object_count != null && plate.object_count > 0
  68. ? t('archives.platePicker.objectCount', { count: plate.object_count })
  69. : `${plate.filaments.length} filament${plate.filaments.length !== 1 ? 's' : ''}`}
  70. {plate.print_time_seconds != null ? ` • ${formatDuration(plate.print_time_seconds)}` : ''}
  71. </p>
  72. </div>
  73. </button>
  74. ))}
  75. </div>
  76. </div>
  77. </div>
  78. </div>
  79. );
  80. }