ExternalLinksSettings.tsx 6.9 KB

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