320 lines
8.8 KiB
TypeScript
320 lines
8.8 KiB
TypeScript
import {
|
|
Alert,
|
|
Box,
|
|
Button,
|
|
Card,
|
|
Flex,
|
|
Group,
|
|
Image,
|
|
ScrollArea,
|
|
Stack,
|
|
Switch,
|
|
Text,
|
|
TextInput,
|
|
Title,
|
|
createStyles,
|
|
useMantineTheme,
|
|
} from '@mantine/core';
|
|
import { useForm } from '@mantine/form';
|
|
import {
|
|
IconAlertTriangle,
|
|
IconBookmark,
|
|
IconLink,
|
|
IconPlaylistX,
|
|
IconTrash,
|
|
IconTypography,
|
|
} from '@tabler/icons-react';
|
|
import { useTranslation } from 'next-i18next';
|
|
import { useEffect } from 'react';
|
|
import { v4 } from 'uuid';
|
|
import { z } from 'zod';
|
|
|
|
import { useEditModeStore } from '../../components/Dashboard/Views/useEditModeStore';
|
|
import { IconSelector } from '../../components/IconSelector/IconSelector';
|
|
import { defineWidget } from '../helper';
|
|
import { IDraggableEditableListInputValue, IWidget } from '../widgets';
|
|
|
|
interface BookmarkItem {
|
|
id: string;
|
|
name: string;
|
|
href: string;
|
|
iconUrl: string;
|
|
openNewTab: boolean;
|
|
hideLink: boolean;
|
|
}
|
|
|
|
const definition = defineWidget({
|
|
id: 'bookmark',
|
|
icon: IconBookmark,
|
|
options: {
|
|
items: {
|
|
type: 'draggable-editable-list',
|
|
defaultValue: [],
|
|
getLabel(data) {
|
|
return data.name;
|
|
},
|
|
create() {
|
|
return {
|
|
id: v4(),
|
|
name: 'Homarr Documentation',
|
|
href: 'https://homarr.dev',
|
|
iconUrl: '/imgs/logo/logo.png',
|
|
openNewTab: false,
|
|
hideLink: false,
|
|
};
|
|
},
|
|
itemComponent({ data, onChange, delete: deleteData }) {
|
|
const form = useForm({
|
|
initialValues: data,
|
|
validate: {
|
|
name: (value) => {
|
|
const validation = z.string().min(1).max(100).safeParse(value);
|
|
if (validation.success) {
|
|
return undefined;
|
|
}
|
|
|
|
return 'Length must be between 1 and 100';
|
|
},
|
|
href: (value) => {
|
|
if (!z.string().min(1).max(200).safeParse(value).success) {
|
|
return 'Length must be between 1 and 200';
|
|
}
|
|
|
|
if (!z.string().url().safeParse(value).success) {
|
|
return 'Not a valid link';
|
|
}
|
|
|
|
return undefined;
|
|
},
|
|
iconUrl: (value) => {
|
|
if (z.string().min(1).max(400).safeParse(value).success) {
|
|
return undefined;
|
|
}
|
|
|
|
return 'Length must be between 1 and 100';
|
|
},
|
|
},
|
|
validateInputOnChange: true,
|
|
validateInputOnBlur: true,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!form.isValid()) {
|
|
return;
|
|
}
|
|
|
|
onChange({ ...form.values, openNewTab: form.values.openNewTab });
|
|
}, [form.values]);
|
|
|
|
return (
|
|
<form>
|
|
<Stack>
|
|
<TextInput
|
|
icon={<IconTypography size="1rem" />}
|
|
{...form.getInputProps('name')}
|
|
label="Name"
|
|
withAsterisk
|
|
/>
|
|
<TextInput
|
|
icon={<IconLink size="1rem" />}
|
|
{...form.getInputProps('href')}
|
|
label="URL"
|
|
withAsterisk
|
|
/>
|
|
<IconSelector
|
|
defaultValue={data.iconUrl}
|
|
value={form.values.iconUrl}
|
|
onChange={(value) => {
|
|
form.setFieldValue('iconUrl', value ?? '');
|
|
}}
|
|
/>
|
|
<Switch
|
|
{...form.getInputProps('openNewTab')}
|
|
label="Open in new tab"
|
|
checked={form.values.openNewTab}
|
|
/>
|
|
<Switch
|
|
{...form.getInputProps('hideLink')}
|
|
label="Hide link"
|
|
checked={form.values.hideLink}
|
|
/>
|
|
<Button
|
|
onClick={() => deleteData()}
|
|
leftIcon={<IconTrash size="1rem" />}
|
|
variant="light"
|
|
type="button"
|
|
>
|
|
Delete
|
|
</Button>
|
|
{!form.isValid() && (
|
|
<Alert color="red" icon={<IconAlertTriangle size="1rem" />}>
|
|
Did not save, because there were validation errors. Please adust your inputs
|
|
</Alert>
|
|
)}
|
|
</Stack>
|
|
</form>
|
|
);
|
|
},
|
|
} satisfies IDraggableEditableListInputValue<BookmarkItem>,
|
|
layout: {
|
|
type: 'select',
|
|
data: [
|
|
{
|
|
label: 'Auto Grid',
|
|
value: 'autoGrid',
|
|
},
|
|
{
|
|
label: 'Horizontal',
|
|
value: 'horizontal',
|
|
},
|
|
{
|
|
label: 'Vertical',
|
|
value: 'vertical',
|
|
},
|
|
],
|
|
defaultValue: 'autoGrid',
|
|
},
|
|
},
|
|
gridstack: {
|
|
minWidth: 1,
|
|
minHeight: 1,
|
|
maxWidth: 24,
|
|
maxHeight: 24,
|
|
},
|
|
component: BookmarkWidgetTile,
|
|
});
|
|
|
|
export type IBookmarkWidget = IWidget<(typeof definition)['id'], typeof definition>;
|
|
|
|
interface BookmarkWidgetTileProps {
|
|
widget: IBookmarkWidget;
|
|
}
|
|
|
|
function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
|
|
const { t } = useTranslation('modules/bookmark');
|
|
const { classes } = useStyles();
|
|
const { enabled: isEditModeEnabled } = useEditModeStore();
|
|
const { fn, colors, colorScheme } = useMantineTheme();
|
|
|
|
if (widget.properties.items.length === 0) {
|
|
return (
|
|
<Stack align="center">
|
|
<IconPlaylistX />
|
|
<Stack spacing={0}>
|
|
<Title order={5} align="center">
|
|
{t('card.noneFound.title')}
|
|
</Title>
|
|
<Text align="center" size="sm">
|
|
{t('card.noneFound.text')}
|
|
</Text>
|
|
</Stack>
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
switch (widget.properties.layout) {
|
|
case 'autoGrid':
|
|
return (
|
|
<Box className={classes.grid} mr={isEditModeEnabled ? 'xl' : undefined} h="100%">
|
|
{widget.properties.items.map((item: BookmarkItem, index) => (
|
|
<Card
|
|
className={classes.autoGridItem}
|
|
key={index}
|
|
px="xl"
|
|
component="a"
|
|
href={item.href}
|
|
target={item.openNewTab ? '_blank' : undefined}
|
|
withBorder
|
|
sx={{
|
|
backgroundColor: colorScheme === 'dark' ? colors.dark[5].concat('80') : colors.blue[0].concat('80'),
|
|
'&:hover': { backgroundColor: fn.primaryColor().concat('80'),}, //'40' = 25% opacity
|
|
}}
|
|
>
|
|
<BookmarkItemContent item={item} />
|
|
</Card>
|
|
))}
|
|
</Box>
|
|
);
|
|
case 'horizontal':
|
|
case 'vertical':
|
|
return (
|
|
<ScrollArea
|
|
scrollbarSize={8}
|
|
type="auto"
|
|
h="100%"
|
|
mr={isEditModeEnabled ? 'xl' : undefined}
|
|
styles={{
|
|
viewport:{
|
|
//mantine being mantine again... this might break
|
|
'& div[style="min-width: 100%; display: table;"]':{
|
|
height:'100%',
|
|
},
|
|
},
|
|
}}
|
|
>
|
|
<Flex
|
|
style={{ flexDirection: widget.properties.layout === 'vertical' ? 'column' : 'row' }}
|
|
gap="0"
|
|
h="100%"
|
|
>
|
|
{widget.properties.items.map((item: BookmarkItem, index) => (
|
|
<Card
|
|
key={index}
|
|
px="md"
|
|
py="0"
|
|
component="a"
|
|
href={item.href}
|
|
target={item.openNewTab ? '_blank' : undefined}
|
|
sx={{
|
|
border:'0.1rem solid transparent',
|
|
borderRadius:'0',
|
|
borderBottomColor:(widget.properties.layout === 'vertical' && index < widget.properties.items.length - 1) ? '#343740' : 'transparent',
|
|
borderRightColor:(widget.properties.layout === 'horizontal' && index < widget.properties.items.length - 1) ? '#343740' : 'transparent',
|
|
backgroundColor: 'transparent',
|
|
'&:hover': { backgroundColor: fn.primaryColor().concat('40'),}, //'40' = 25% opacity
|
|
flex:'1 1 auto'
|
|
}}
|
|
display="flex"
|
|
>
|
|
<BookmarkItemContent item={item}/>
|
|
</Card>
|
|
))}
|
|
</Flex>
|
|
</ScrollArea>
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const BookmarkItemContent = ({ item }: { item: BookmarkItem }) => {
|
|
const { colorScheme } = useMantineTheme();
|
|
return (
|
|
<Group spacing="0rem 1rem">
|
|
<Image src={item.iconUrl} width={47} height={47} fit="contain" withPlaceholder />
|
|
<Stack spacing={0}>
|
|
<Text size="md">{item.name}</Text>
|
|
<Text
|
|
color={colorScheme === 'dark' ? "gray.6" : "gray.7"}
|
|
size="sm"
|
|
hidden={item.hideLink}
|
|
>
|
|
{new URL(item.href).hostname}
|
|
</Text>
|
|
</Stack>
|
|
</Group>
|
|
)};
|
|
|
|
const useStyles = createStyles(() => ({
|
|
grid: {
|
|
display: 'grid',
|
|
gap: 10,
|
|
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
|
|
},
|
|
autoGridItem: {
|
|
flex: '1 1 auto',
|
|
},
|
|
}));
|
|
|
|
export default definition;
|