feat: custom branding (#112)
* add first concept * remove setup status * split config page in multiple components * add custom branding docs * add test email button * fix invalid email from header * add migration * mount images to host * update docs * remove unused endpoint * run formatter
This commit is contained in:
@@ -1,153 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Paper,
|
||||
Space,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import useConfig from "../../../hooks/config.hook";
|
||||
import configService from "../../../services/config.service";
|
||||
import {
|
||||
AdminConfigGroupedByCategory,
|
||||
UpdateConfig,
|
||||
} from "../../../types/config.type";
|
||||
import {
|
||||
capitalizeFirstLetter,
|
||||
configVariableToFriendlyName,
|
||||
} from "../../../utils/string.util";
|
||||
import toast from "../../../utils/toast.util";
|
||||
|
||||
import AdminConfigInput from "./AdminConfigInput";
|
||||
import TestEmailButton from "./TestEmailButton";
|
||||
|
||||
const AdminConfigTable = () => {
|
||||
const config = useConfig();
|
||||
const router = useRouter();
|
||||
const isMobile = useMediaQuery("(max-width: 560px)");
|
||||
|
||||
const [updatedConfigVariables, setUpdatedConfigVariables] = useState<
|
||||
UpdateConfig[]
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (config.get("SETUP_STATUS") != "FINISHED") {
|
||||
config.refresh();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateConfigVariable = (configVariable: UpdateConfig) => {
|
||||
const index = updatedConfigVariables.findIndex(
|
||||
(item) => item.key === configVariable.key
|
||||
);
|
||||
if (index > -1) {
|
||||
updatedConfigVariables[index] = configVariable;
|
||||
} else {
|
||||
setUpdatedConfigVariables([...updatedConfigVariables, configVariable]);
|
||||
}
|
||||
};
|
||||
|
||||
const [configVariablesByCategory, setCofigVariablesByCategory] =
|
||||
useState<AdminConfigGroupedByCategory>({});
|
||||
|
||||
const getConfigVariables = async () => {
|
||||
await configService.listForAdmin().then((configVariables) => {
|
||||
const configVariablesByCategory = configVariables.reduce(
|
||||
(categories: any, item) => {
|
||||
const category = categories[item.category] || [];
|
||||
category.push(item);
|
||||
categories[item.category] = category;
|
||||
return categories;
|
||||
},
|
||||
{}
|
||||
);
|
||||
setCofigVariablesByCategory(configVariablesByCategory);
|
||||
});
|
||||
};
|
||||
|
||||
const saveConfigVariables = async () => {
|
||||
if (config.get("SETUP_STATUS") == "REGISTERED") {
|
||||
await configService
|
||||
.updateMany(updatedConfigVariables)
|
||||
.then(async () => {
|
||||
await configService.finishSetup();
|
||||
router.reload();
|
||||
})
|
||||
.catch(toast.axiosError);
|
||||
} else {
|
||||
await configService
|
||||
.updateMany(updatedConfigVariables)
|
||||
.then(() => {
|
||||
setUpdatedConfigVariables([]);
|
||||
toast.success("Configurations updated successfully");
|
||||
})
|
||||
.catch(toast.axiosError);
|
||||
}
|
||||
config.refresh();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getConfigVariables();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box mb="lg">
|
||||
{Object.entries(configVariablesByCategory).map(
|
||||
([category, configVariables]) => {
|
||||
return (
|
||||
<Paper key={category} withBorder p="lg" mb="xl">
|
||||
<Title mb="xs" order={3}>
|
||||
{capitalizeFirstLetter(category)}
|
||||
</Title>
|
||||
{configVariables.map((configVariable) => (
|
||||
<>
|
||||
<Group position="apart">
|
||||
<Stack
|
||||
style={{ maxWidth: isMobile ? "100%" : "40%" }}
|
||||
spacing={0}
|
||||
>
|
||||
<Title order={6}>
|
||||
{configVariableToFriendlyName(configVariable.key)}
|
||||
</Title>
|
||||
<Text color="dimmed" size="sm" mb="xs">
|
||||
{configVariable.description}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack></Stack>
|
||||
<Box style={{ width: isMobile ? "100%" : "50%" }}>
|
||||
<AdminConfigInput
|
||||
key={configVariable.key}
|
||||
updateConfigVariable={updateConfigVariable}
|
||||
configVariable={configVariable}
|
||||
/>
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
<Space h="lg" />
|
||||
</>
|
||||
))}
|
||||
{category == "smtp" && (
|
||||
<Group position="right">
|
||||
<TestEmailButton
|
||||
configVariablesChanged={updatedConfigVariables.length != 0}
|
||||
saveConfigVariables={saveConfigVariables}
|
||||
/>
|
||||
</Group>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
)}
|
||||
<Group position="right">
|
||||
<Button onClick={saveConfigVariables}>Save</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminConfigTable;
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
Burger,
|
||||
Button,
|
||||
Group,
|
||||
Header,
|
||||
MediaQuery,
|
||||
Text,
|
||||
useMantineTheme,
|
||||
} from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import useConfig from "../../../hooks/config.hook";
|
||||
import Logo from "../../Logo";
|
||||
|
||||
const ConfigurationHeader = ({
|
||||
isMobileNavBarOpened,
|
||||
setIsMobileNavBarOpened,
|
||||
}: {
|
||||
isMobileNavBarOpened: boolean;
|
||||
setIsMobileNavBarOpened: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const config = useConfig();
|
||||
const theme = useMantineTheme();
|
||||
return (
|
||||
<Header height={60} p="md">
|
||||
<div style={{ display: "flex", alignItems: "center", height: "100%" }}>
|
||||
<MediaQuery largerThan="sm" styles={{ display: "none" }}>
|
||||
<Burger
|
||||
opened={isMobileNavBarOpened}
|
||||
onClick={() => setIsMobileNavBarOpened((o) => !o)}
|
||||
size="sm"
|
||||
color={theme.colors.gray[6]}
|
||||
mr="xl"
|
||||
/>
|
||||
</MediaQuery>
|
||||
<Group position="apart" w="100%">
|
||||
<Link href="/" passHref>
|
||||
<Group>
|
||||
<Logo height={35} width={35} />
|
||||
<Text weight={600}>{config.get("general.appName")}</Text>
|
||||
</Group>
|
||||
</Link>
|
||||
<MediaQuery smallerThan="sm" styles={{ display: "none" }}>
|
||||
<Button variant="light" component={Link} href="/admin">
|
||||
Go back
|
||||
</Button>
|
||||
</MediaQuery>
|
||||
</Group>
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigurationHeader;
|
||||
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
createStyles,
|
||||
Group,
|
||||
MediaQuery,
|
||||
Navbar,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
} from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { TbAt, TbMail, TbShare, TbSquare } from "react-icons/tb";
|
||||
|
||||
const categories = [
|
||||
{ name: "General", icon: <TbSquare /> },
|
||||
{ name: "Email", icon: <TbMail /> },
|
||||
{ name: "Share", icon: <TbShare /> },
|
||||
{ name: "SMTP", icon: <TbAt /> },
|
||||
];
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
activeLink: {
|
||||
backgroundColor: theme.fn.variant({
|
||||
variant: "light",
|
||||
color: theme.primaryColor,
|
||||
}).background,
|
||||
color: theme.fn.variant({ variant: "light", color: theme.primaryColor })
|
||||
.color,
|
||||
|
||||
borderRadius: theme.radius.sm,
|
||||
fontWeight: 600,
|
||||
},
|
||||
}));
|
||||
|
||||
const ConfigurationNavBar = ({
|
||||
categoryId,
|
||||
isMobileNavBarOpened,
|
||||
setIsMobileNavBarOpened,
|
||||
}: {
|
||||
categoryId: string;
|
||||
isMobileNavBarOpened: boolean;
|
||||
setIsMobileNavBarOpened: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const { classes } = useStyles();
|
||||
return (
|
||||
<Navbar
|
||||
p="md"
|
||||
hiddenBreakpoint="sm"
|
||||
hidden={!isMobileNavBarOpened}
|
||||
width={{ sm: 200, lg: 300 }}
|
||||
>
|
||||
<Navbar.Section>
|
||||
<Text size="xs" color="dimmed" mb="sm">
|
||||
Configuration
|
||||
</Text>
|
||||
<Stack spacing="xs">
|
||||
{categories.map((category) => (
|
||||
<Box
|
||||
p="xs"
|
||||
component={Link}
|
||||
onClick={() => setIsMobileNavBarOpened(false)}
|
||||
className={
|
||||
categoryId == category.name.toLowerCase()
|
||||
? classes.activeLink
|
||||
: undefined
|
||||
}
|
||||
key={category.name}
|
||||
href={`/admin/config/${category.name.toLowerCase()}`}
|
||||
>
|
||||
<Group>
|
||||
<ThemeIcon
|
||||
variant={
|
||||
categoryId == category.name.toLowerCase()
|
||||
? "filled"
|
||||
: "light"
|
||||
}
|
||||
>
|
||||
{category.icon}
|
||||
</ThemeIcon>
|
||||
<Text size="sm">{category.name}</Text>
|
||||
</Group>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Navbar.Section>
|
||||
<MediaQuery largerThan="sm" styles={{ display: "none" }}>
|
||||
<Button mt="xl" variant="light" component={Link} href="/admin">
|
||||
Go back
|
||||
</Button>
|
||||
</MediaQuery>
|
||||
</Navbar>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigurationNavBar;
|
||||
Reference in New Issue
Block a user