Use our custom table for displaying logos

This commit is contained in:
SergeantPanda 2025-07-15 20:02:21 -05:00
parent cea078f6ef
commit 2bba31940d
2 changed files with 363 additions and 197 deletions

View 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;

View file

@ -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>
);
};