LinkSpoolModal.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import { useState, useEffect, useCallback } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { X } from 'lucide-react';
  4. import type { InventorySpool } from '../../api/client';
  5. import { SpoolIcon } from './SpoolIcon';
  6. interface LinkSpoolModalProps {
  7. isOpen: boolean;
  8. onClose: () => void;
  9. tagId: string;
  10. untaggedSpools: InventorySpool[];
  11. onLink: (spool: InventorySpool) => void;
  12. }
  13. export function LinkSpoolModal({
  14. isOpen,
  15. onClose,
  16. tagId,
  17. untaggedSpools,
  18. onLink,
  19. }: LinkSpoolModalProps) {
  20. const { t } = useTranslation();
  21. const [selectedSpool, setSelectedSpool] = useState<InventorySpool | null>(null);
  22. const handleClose = useCallback(() => {
  23. setSelectedSpool(null);
  24. onClose();
  25. }, [onClose]);
  26. // Handle escape key
  27. const handleKeyDown = useCallback((e: KeyboardEvent) => {
  28. if (e.key === 'Escape') {
  29. handleClose();
  30. }
  31. }, [handleClose]);
  32. useEffect(() => {
  33. if (isOpen) {
  34. document.addEventListener('keydown', handleKeyDown);
  35. document.body.style.overflow = 'hidden';
  36. }
  37. return () => {
  38. document.removeEventListener('keydown', handleKeyDown);
  39. document.body.style.overflow = '';
  40. };
  41. }, [isOpen, handleKeyDown]);
  42. if (!isOpen) return null;
  43. const handleConfirm = () => {
  44. if (selectedSpool) {
  45. onLink(selectedSpool);
  46. setSelectedSpool(null);
  47. }
  48. };
  49. return (
  50. <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 animate-fade-in" onClick={handleClose}>
  51. <div
  52. className="bg-zinc-800 rounded-xl shadow-2xl w-full max-w-lg mx-4 animate-slide-up"
  53. onClick={(e) => e.stopPropagation()}
  54. >
  55. {/* Header */}
  56. <div className="flex items-center justify-between px-5 py-4 border-b border-zinc-700">
  57. <div>
  58. <h2 className="text-base font-semibold text-zinc-100">
  59. {t('spoolbuddy.dashboard.linkTagTitle', 'Link Tag to Spool')}
  60. </h2>
  61. <p className="text-sm text-zinc-500 font-mono">{tagId}</p>
  62. </div>
  63. <button
  64. onClick={handleClose}
  65. className="p-2 rounded-lg text-zinc-500 hover:text-zinc-300 hover:bg-zinc-700 transition-colors"
  66. >
  67. <X className="w-5 h-5" />
  68. </button>
  69. </div>
  70. {/* Content */}
  71. <div className="px-5 py-4 space-y-3 max-h-[400px] overflow-y-auto">
  72. <p className="text-sm text-zinc-400">
  73. {t('spoolbuddy.dashboard.selectSpool', 'Select a spool to link this tag to:')}
  74. </p>
  75. {untaggedSpools.length === 0 ? (
  76. <div className="text-center py-8 text-zinc-500">
  77. {t('spoolbuddy.dashboard.noUntagged', 'No spools without tags found')}
  78. </div>
  79. ) : (
  80. <div className="space-y-2">
  81. {untaggedSpools.map((spool) => (
  82. <button
  83. key={spool.id}
  84. type="button"
  85. onClick={() => setSelectedSpool(spool)}
  86. className={`w-full flex items-center gap-3 p-3 rounded-lg border-2 transition-all text-left ${
  87. selectedSpool?.id === spool.id
  88. ? 'border-green-500 bg-green-500/10'
  89. : 'border-zinc-700 hover:border-green-500/50 hover:bg-zinc-700/50'
  90. }`}
  91. >
  92. <SpoolIcon
  93. color={spool.rgba ? `#${spool.rgba.slice(0, 6)}` : '#808080'}
  94. isEmpty={false}
  95. size={40}
  96. />
  97. <div className="flex-1 min-w-0">
  98. <div className="flex items-center gap-2">
  99. <div className="font-medium text-zinc-100 truncate">
  100. {spool.color_name || 'Unknown color'}
  101. </div>
  102. <span className="text-[10px] font-mono text-zinc-500 shrink-0">#{spool.id}</span>
  103. </div>
  104. <div className="text-sm text-zinc-400 truncate">
  105. {spool.brand} &bull; {spool.material}
  106. {spool.subtype && ` ${spool.subtype}`}
  107. </div>
  108. </div>
  109. <div className="text-sm font-mono text-zinc-500">
  110. {Math.max(0, spool.label_weight - spool.weight_used)}g
  111. </div>
  112. </button>
  113. ))}
  114. </div>
  115. )}
  116. </div>
  117. {/* Footer */}
  118. <div className="flex justify-end gap-2 px-5 py-4 border-t border-zinc-700">
  119. <button
  120. onClick={handleClose}
  121. className="px-4 py-2.5 rounded-lg text-sm font-medium bg-zinc-700 text-zinc-300 hover:bg-zinc-600 transition-colors min-h-[44px]"
  122. >
  123. {t('common.cancel', 'Cancel')}
  124. </button>
  125. <button
  126. onClick={handleConfirm}
  127. disabled={!selectedSpool}
  128. className="px-4 py-2.5 rounded-lg text-sm font-medium bg-green-600 text-white hover:bg-green-700 disabled:opacity-40 disabled:cursor-not-allowed transition-colors min-h-[44px]"
  129. >
  130. {t('spoolbuddy.dashboard.linkTag', 'Link Tag')}
  131. </button>
  132. </div>
  133. </div>
  134. </div>
  135. );
  136. }