LinkSpoolModal.tsx 5.2 KB

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