mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-08-04 07:42:47 +00:00
Use our custom table for displaying logos
This commit is contained in:
parent
cea078f6ef
commit
2bba31940d
2 changed files with 363 additions and 197 deletions
356
frontend/src/components/tables/LogosTable.jsx
Normal file
356
frontend/src/components/tables/LogosTable.jsx
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
import React, { useMemo, useCallback, useState } from 'react';
|
||||
import API from '../../api';
|
||||
import LogoForm from '../forms/Logo';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||
import {
|
||||
SquarePlus,
|
||||
SquareMinus,
|
||||
SquarePen,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Text,
|
||||
Paper,
|
||||
Button,
|
||||
Flex,
|
||||
Group,
|
||||
useMantineTheme,
|
||||
LoadingOverlay,
|
||||
Stack,
|
||||
Image,
|
||||
Center,
|
||||
} from '@mantine/core';
|
||||
import { CustomTable, useTable } from './CustomTable';
|
||||
import ConfirmationDialog from '../ConfirmationDialog';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
||||
const LogoRowActions = ({ theme, row, editLogo, deleteLogo }) => {
|
||||
const [tableSize, _] = useLocalStorage('table-size', 'default');
|
||||
|
||||
const onEdit = useCallback(() => {
|
||||
editLogo(row.original);
|
||||
}, [row.original, editLogo]);
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
deleteLogo(row.original.id);
|
||||
}, [row.original.id, deleteLogo]);
|
||||
|
||||
const iconSize =
|
||||
tableSize == 'default' ? 'sm' : tableSize == 'compact' ? 'xs' : 'md';
|
||||
|
||||
return (
|
||||
<Box style={{ width: '100%', justifyContent: 'left' }}>
|
||||
<Group gap={2} justify="center">
|
||||
<ActionIcon
|
||||
size={iconSize}
|
||||
variant="transparent"
|
||||
color={theme.tailwind.yellow[3]}
|
||||
onClick={onEdit}
|
||||
>
|
||||
<SquarePen size="18" />
|
||||
</ActionIcon>
|
||||
|
||||
<ActionIcon
|
||||
size={iconSize}
|
||||
variant="transparent"
|
||||
color={theme.tailwind.red[6]}
|
||||
onClick={onDelete}
|
||||
>
|
||||
<SquareMinus size="18" />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const LogosTable = () => {
|
||||
const theme = useMantineTheme();
|
||||
|
||||
/**
|
||||
* STORES
|
||||
*/
|
||||
const { logos, fetchLogos } = useChannelsStore();
|
||||
|
||||
/**
|
||||
* useState
|
||||
*/
|
||||
const [selectedLogo, setSelectedLogo] = useState(null);
|
||||
const [logoModalOpen, setLogoModalOpen] = useState(false);
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState(null);
|
||||
const [logoToDelete, setLogoToDelete] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
/**
|
||||
* Functions
|
||||
*/
|
||||
const executeDeleteLogo = useCallback(async (id) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await API.deleteLogo(id);
|
||||
await fetchLogos();
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'Logo deleted successfully',
|
||||
color: 'green',
|
||||
});
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to delete logo',
|
||||
color: 'red',
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setConfirmDeleteOpen(false);
|
||||
}
|
||||
}, [fetchLogos]);
|
||||
|
||||
const editLogo = useCallback(async (logo = null) => {
|
||||
setSelectedLogo(logo);
|
||||
setLogoModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const deleteLogo = useCallback(async (id) => {
|
||||
const logosArray = Object.values(logos || {});
|
||||
const logo = logosArray.find((l) => l.id === id);
|
||||
setLogoToDelete(logo);
|
||||
setDeleteTarget(id);
|
||||
setConfirmDeleteOpen(true);
|
||||
}, [logos]);
|
||||
|
||||
/**
|
||||
* useMemo
|
||||
*/
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
header: 'Preview',
|
||||
accessorKey: 'cache_url',
|
||||
size: 80,
|
||||
enableSorting: false,
|
||||
cell: ({ getValue, row }) => (
|
||||
<Center>
|
||||
<Image
|
||||
src={getValue()}
|
||||
alt={row.original.name}
|
||||
width={40}
|
||||
height={30}
|
||||
fit="contain"
|
||||
fallbackSrc="/logo.png"
|
||||
/>
|
||||
</Center>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
size: 200,
|
||||
cell: ({ getValue }) => (
|
||||
<Text fw={500} size="sm">
|
||||
{getValue()}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'URL',
|
||||
accessorKey: 'url',
|
||||
cell: ({ getValue }) => (
|
||||
<Group gap={4} style={{ alignItems: 'center' }}>
|
||||
<Box
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: 300,
|
||||
}}
|
||||
>
|
||||
<Text size="sm" c="dimmed">
|
||||
{getValue()}
|
||||
</Text>
|
||||
</Box>
|
||||
{getValue()?.startsWith('http') && (
|
||||
<ActionIcon
|
||||
size="xs"
|
||||
variant="transparent"
|
||||
color="gray"
|
||||
onClick={() => window.open(getValue(), '_blank')}
|
||||
>
|
||||
<ExternalLink size={12} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
size: 80,
|
||||
header: 'Actions',
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => (
|
||||
<LogoRowActions
|
||||
theme={theme}
|
||||
row={row}
|
||||
editLogo={editLogo}
|
||||
deleteLogo={deleteLogo}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[theme, editLogo, deleteLogo]
|
||||
);
|
||||
|
||||
const closeLogoForm = () => {
|
||||
setSelectedLogo(null);
|
||||
setLogoModalOpen(false);
|
||||
fetchLogos(); // Refresh the logos list
|
||||
};
|
||||
|
||||
const data = useMemo(() => {
|
||||
const logosArray = Object.values(logos || {});
|
||||
return logosArray.sort((a, b) => a.id - b.id);
|
||||
}, [logos]);
|
||||
|
||||
const renderHeaderCell = (header) => {
|
||||
return (
|
||||
<Text size="sm" name={header.id}>
|
||||
{header.column.columnDef.header}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const table = useTable({
|
||||
columns,
|
||||
data,
|
||||
allRowIds: data.map((logo) => logo.id),
|
||||
enablePagination: false,
|
||||
enableRowSelection: false,
|
||||
enableRowVirtualization: false,
|
||||
renderTopToolbar: false,
|
||||
manualSorting: false,
|
||||
manualFiltering: false,
|
||||
manualPagination: false,
|
||||
headerCellRenderFns: {
|
||||
actions: renderHeaderCell,
|
||||
cache_url: renderHeaderCell,
|
||||
name: renderHeaderCell,
|
||||
url: renderHeaderCell,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
padding: '0px',
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<Stack gap="md" style={{ maxWidth: '1200px', width: '100%' }}>
|
||||
<Flex style={{ alignItems: 'center', paddingBottom: 10 }} gap={15}>
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
fontWeight: 500,
|
||||
fontSize: '20px',
|
||||
lineHeight: 1,
|
||||
letterSpacing: '-0.3px',
|
||||
color: 'gray.6',
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
Logos
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
({data.length} logo{data.length !== 1 ? 's' : ''})
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Paper
|
||||
style={{
|
||||
backgroundColor: '#27272A',
|
||||
border: '1px solid #3f3f46',
|
||||
borderRadius: 'var(--mantine-radius-md)',
|
||||
}}
|
||||
>
|
||||
{/* Top toolbar */}
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
padding: '16px',
|
||||
borderBottom: '1px solid #3f3f46',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
leftSection={<SquarePlus size={18} />}
|
||||
variant="light"
|
||||
size="xs"
|
||||
onClick={() => editLogo()}
|
||||
p={5}
|
||||
color={theme.tailwind.green[5]}
|
||||
style={{
|
||||
borderWidth: '1px',
|
||||
borderColor: theme.tailwind.green[5],
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
Add Logo
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Table container */}
|
||||
<Box
|
||||
style={{
|
||||
position: 'relative',
|
||||
overflow: 'auto',
|
||||
borderRadius: '0 0 var(--mantine-radius-md) var(--mantine-radius-md)',
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: '700px' }}>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
<CustomTable table={table} />
|
||||
</div>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<LogoForm
|
||||
logo={selectedLogo}
|
||||
isOpen={logoModalOpen}
|
||||
onClose={closeLogoForm}
|
||||
/>
|
||||
|
||||
<ConfirmationDialog
|
||||
opened={confirmDeleteOpen}
|
||||
onClose={() => setConfirmDeleteOpen(false)}
|
||||
onConfirm={() => executeDeleteLogo(deleteTarget)}
|
||||
title="Delete Logo"
|
||||
message={
|
||||
logoToDelete ? (
|
||||
<div>
|
||||
Are you sure you want to delete the logo "{logoToDelete.name}"?
|
||||
<br />
|
||||
<Text size="sm" c="dimmed" mt="xs">
|
||||
This action cannot be undone.
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
'Are you sure you want to delete this logo?'
|
||||
)
|
||||
}
|
||||
confirmLabel="Delete"
|
||||
cancelLabel="Cancel"
|
||||
size="md"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogosTable;
|
||||
|
|
@ -1,39 +1,17 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Container,
|
||||
Title,
|
||||
Button,
|
||||
Table,
|
||||
Group,
|
||||
ActionIcon,
|
||||
Text,
|
||||
Image,
|
||||
Box,
|
||||
Center,
|
||||
Stack,
|
||||
Badge,
|
||||
} from '@mantine/core';
|
||||
import { SquarePen, Trash2, Plus, ExternalLink } from 'lucide-react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Box } from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import useChannelsStore from '../store/channels';
|
||||
import API from '../api';
|
||||
import LogoForm from '../components/forms/Logo';
|
||||
import ConfirmationDialog from '../components/ConfirmationDialog';
|
||||
import LogosTable from '../components/tables/LogosTable';
|
||||
|
||||
const LogosPage = () => {
|
||||
const { logos, fetchLogos } = useChannelsStore();
|
||||
const [logoFormOpen, setLogoFormOpen] = useState(false);
|
||||
const [editingLogo, setEditingLogo] = useState(null);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [logoToDelete, setLogoToDelete] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { fetchLogos } = useChannelsStore();
|
||||
|
||||
useEffect(() => {
|
||||
loadLogos();
|
||||
}, []);
|
||||
|
||||
const loadLogos = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await fetchLogos();
|
||||
} catch (error) {
|
||||
|
|
@ -42,181 +20,13 @@ const LogosPage = () => {
|
|||
message: 'Failed to load logos',
|
||||
color: 'red',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateLogo = () => {
|
||||
setEditingLogo(null);
|
||||
setLogoFormOpen(true);
|
||||
};
|
||||
|
||||
const handleEditLogo = (logo) => {
|
||||
setEditingLogo(logo);
|
||||
setLogoFormOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteLogo = (logo) => {
|
||||
setLogoToDelete(logo);
|
||||
setDeleteConfirmOpen(true);
|
||||
};
|
||||
|
||||
const confirmDeleteLogo = async () => {
|
||||
if (!logoToDelete) return;
|
||||
|
||||
try {
|
||||
await API.deleteLogo(logoToDelete.id);
|
||||
await fetchLogos();
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'Logo deleted successfully',
|
||||
color: 'green',
|
||||
});
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to delete logo',
|
||||
color: 'red',
|
||||
});
|
||||
} finally {
|
||||
setDeleteConfirmOpen(false);
|
||||
setLogoToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormClose = () => {
|
||||
setLogoFormOpen(false);
|
||||
setEditingLogo(null);
|
||||
loadLogos(); // Refresh the logos list
|
||||
};
|
||||
|
||||
const logosArray = Object.values(logos || {});
|
||||
|
||||
const rows = logosArray.map((logo) => (
|
||||
<Table.Tr key={logo.id}>
|
||||
<Table.Td>
|
||||
<Center>
|
||||
<Image
|
||||
src={logo.cache_url}
|
||||
alt={logo.name}
|
||||
width={40}
|
||||
height={30}
|
||||
fit="contain"
|
||||
fallbackSrc="/logo.png"
|
||||
/>
|
||||
</Center>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fw={500}>{logo.name}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group spacing="xs" align="center">
|
||||
<Text size="sm" color="dimmed" style={{ wordBreak: 'break-all', maxWidth: 300 }}>
|
||||
{logo.url}
|
||||
</Text>
|
||||
{logo.url.startsWith('http') && (
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
onClick={() => window.open(logo.url, '_blank')}
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group spacing="xs">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
onClick={() => handleEditLogo(logo)}
|
||||
color="blue"
|
||||
>
|
||||
<SquarePen size={16} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
onClick={() => handleDeleteLogo(logo)}
|
||||
color="red"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container size="xl" py="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={2}>Logos</Title>
|
||||
<Button leftSection={<Plus size={16} />} onClick={handleCreateLogo}>
|
||||
Add Logo
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{loading ? (
|
||||
<Center py="xl">
|
||||
<Text>Loading logos...</Text>
|
||||
</Center>
|
||||
) : logosArray.length === 0 ? (
|
||||
<Center py="xl">
|
||||
<Stack align="center" spacing="md">
|
||||
<Text size="lg" color="dimmed">No logos found</Text>
|
||||
<Text size="sm" color="dimmed">Click "Add Logo" to create your first logo</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
<Box>
|
||||
<Text size="sm" color="dimmed" mb="sm">
|
||||
Total: {logosArray.length} logo{logosArray.length !== 1 ? 's' : ''}
|
||||
</Text>
|
||||
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Preview</Table.Th>
|
||||
<Table.Th>Name</Table.Th>
|
||||
<Table.Th>URL</Table.Th>
|
||||
<Table.Th>Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>{rows}</Table.Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
<LogoForm
|
||||
logo={editingLogo}
|
||||
isOpen={logoFormOpen}
|
||||
onClose={handleFormClose}
|
||||
/>
|
||||
|
||||
<ConfirmationDialog
|
||||
opened={deleteConfirmOpen}
|
||||
onClose={() => setDeleteConfirmOpen(false)}
|
||||
onConfirm={confirmDeleteLogo}
|
||||
title="Delete Logo"
|
||||
message={
|
||||
logoToDelete ? (
|
||||
<div>
|
||||
Are you sure you want to delete the logo "{logoToDelete.name}"?
|
||||
<br />
|
||||
<Text size="sm" color="dimmed" mt="xs">
|
||||
This action cannot be undone.
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
'Are you sure you want to delete this logo?'
|
||||
)
|
||||
}
|
||||
confirmLabel="Delete"
|
||||
cancelLabel="Cancel"
|
||||
/>
|
||||
</>
|
||||
<Box style={{ padding: 10 }}>
|
||||
<LogosTable />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue