ExternalLinksSettings.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import { useState } from 'react';
  2. import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
  3. import { Link2, Plus, Pencil, Trash2, GripVertical, Loader2, ExternalLink as ExternalLinkIcon } from 'lucide-react';
  4. import { api } from '../api/client';
  5. import type { ExternalLink } from '../api/client';
  6. import { Card, CardContent, CardHeader } from './Card';
  7. import { Button } from './Button';
  8. import { AddExternalLinkModal } from './AddExternalLinkModal';
  9. import { ConfirmModal } from './ConfirmModal';
  10. import { getIconByName } from './IconPicker';
  11. export function ExternalLinksSettings() {
  12. const queryClient = useQueryClient();
  13. const [showAddModal, setShowAddModal] = useState(false);
  14. const [editingLink, setEditingLink] = useState<ExternalLink | null>(null);
  15. const [deletingLink, setDeletingLink] = useState<ExternalLink | null>(null);
  16. const [draggedId, setDraggedId] = useState<number | null>(null);
  17. // Fetch external links
  18. const { data: links, isLoading } = useQuery({
  19. queryKey: ['external-links'],
  20. queryFn: api.getExternalLinks,
  21. });
  22. // Delete mutation
  23. const deleteMutation = useMutation({
  24. mutationFn: (id: number) => api.deleteExternalLink(id),
  25. onSuccess: () => {
  26. queryClient.invalidateQueries({ queryKey: ['external-links'] });
  27. },
  28. });
  29. // Reorder mutation
  30. const reorderMutation = useMutation({
  31. mutationFn: (ids: number[]) => api.reorderExternalLinks(ids),
  32. onSuccess: () => {
  33. queryClient.invalidateQueries({ queryKey: ['external-links'] });
  34. },
  35. });
  36. const handleDragStart = (e: React.DragEvent, id: number) => {
  37. setDraggedId(id);
  38. e.dataTransfer.effectAllowed = 'move';
  39. };
  40. const handleDragOver = (e: React.DragEvent) => {
  41. e.preventDefault();
  42. e.dataTransfer.dropEffect = 'move';
  43. };
  44. const handleDrop = (e: React.DragEvent, targetId: number) => {
  45. e.preventDefault();
  46. if (draggedId === null || draggedId === targetId || !links) return;
  47. const currentIds = links.map((l) => l.id);
  48. const draggedIndex = currentIds.indexOf(draggedId);
  49. const targetIndex = currentIds.indexOf(targetId);
  50. if (draggedIndex === -1 || targetIndex === -1) return;
  51. // Reorder
  52. const newIds = [...currentIds];
  53. newIds.splice(draggedIndex, 1);
  54. newIds.splice(targetIndex, 0, draggedId);
  55. reorderMutation.mutate(newIds);
  56. setDraggedId(null);
  57. };
  58. const handleDelete = (link: ExternalLink) => {
  59. setDeletingLink(link);
  60. };
  61. const confirmDelete = () => {
  62. if (deletingLink) {
  63. deleteMutation.mutate(deletingLink.id);
  64. setDeletingLink(null);
  65. }
  66. };
  67. return (
  68. <>
  69. <Card>
  70. <CardHeader>
  71. <div className="flex items-center justify-between">
  72. <div className="flex items-center gap-2">
  73. <Link2 className="w-5 h-5 text-bambu-green" />
  74. <h2 className="text-lg font-semibold text-white">Sidebar Links</h2>
  75. </div>
  76. <Button size="sm" onClick={() => setShowAddModal(true)}>
  77. <Plus className="w-4 h-4" />
  78. Add Link
  79. </Button>
  80. </div>
  81. </CardHeader>
  82. <CardContent>
  83. <p className="text-sm text-bambu-gray mb-4">
  84. Add external links to the sidebar navigation. Drag to reorder.
  85. </p>
  86. {isLoading ? (
  87. <div className="flex justify-center py-8">
  88. <Loader2 className="w-6 h-6 text-bambu-green animate-spin" />
  89. </div>
  90. ) : links && links.length > 0 ? (
  91. <div className="space-y-2">
  92. {links.map((link) => {
  93. const Icon = getIconByName(link.icon);
  94. return (
  95. <div
  96. key={link.id}
  97. draggable
  98. onDragStart={(e) => handleDragStart(e, link.id)}
  99. onDragOver={handleDragOver}
  100. onDrop={(e) => handleDrop(e, link.id)}
  101. className={`flex items-center gap-3 p-3 rounded-lg bg-bambu-dark border border-bambu-dark-tertiary transition-colors ${
  102. draggedId === link.id ? 'opacity-50' : ''
  103. }`}
  104. >
  105. <GripVertical className="w-4 h-4 text-bambu-gray cursor-grab flex-shrink-0" />
  106. <div className="p-2 rounded-lg bg-bambu-dark-tertiary text-bambu-gray">
  107. <Icon className="w-4 h-4" />
  108. </div>
  109. <div className="flex-1 min-w-0">
  110. <div className="flex items-center gap-2">
  111. <span className="text-white font-medium truncate">{link.name}</span>
  112. <ExternalLinkIcon className="w-3 h-3 text-bambu-gray flex-shrink-0" />
  113. </div>
  114. <span className="text-sm text-bambu-gray truncate block">{link.url}</span>
  115. </div>
  116. <div className="flex items-center gap-1 flex-shrink-0">
  117. <button
  118. onClick={() => setEditingLink(link)}
  119. className="p-2 rounded-lg hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white transition-colors"
  120. title="Edit"
  121. >
  122. <Pencil className="w-4 h-4" />
  123. </button>
  124. <button
  125. onClick={() => handleDelete(link)}
  126. disabled={deleteMutation.isPending}
  127. className="p-2 rounded-lg hover:bg-red-500/20 text-bambu-gray hover:text-red-400 transition-colors disabled:opacity-50"
  128. title="Delete"
  129. >
  130. <Trash2 className="w-4 h-4" />
  131. </button>
  132. </div>
  133. </div>
  134. );
  135. })}
  136. </div>
  137. ) : (
  138. <div className="text-center py-8 text-bambu-gray">
  139. <Link2 className="w-8 h-8 mx-auto mb-2 opacity-50" />
  140. <p>No external links configured</p>
  141. <p className="text-sm">Click "Add Link" to add one</p>
  142. </div>
  143. )}
  144. </CardContent>
  145. </Card>
  146. {/* Add/Edit Modal */}
  147. {(showAddModal || editingLink) && (
  148. <AddExternalLinkModal
  149. link={editingLink}
  150. onClose={() => {
  151. setShowAddModal(false);
  152. setEditingLink(null);
  153. }}
  154. />
  155. )}
  156. {/* Delete Confirmation Modal */}
  157. {deletingLink && (
  158. <ConfirmModal
  159. title="Delete Link"
  160. message={`Are you sure you want to delete "${deletingLink.name}"? This action cannot be undone.`}
  161. confirmText="Delete"
  162. cancelText="Cancel"
  163. variant="danger"
  164. onConfirm={confirmDelete}
  165. onCancel={() => setDeletingLink(null)}
  166. />
  167. )}
  168. </>
  169. );
  170. }