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:
@@ -11,8 +11,9 @@ import axios from "axios";
|
||||
import { getCookie, setCookie } from "cookies-next";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import type { AppProps } from "next/app";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import Header from "../components/navBar/NavBar";
|
||||
import Header from "../components/header/Header";
|
||||
import { ConfigContext } from "../hooks/config.hook";
|
||||
import usePreferences from "../hooks/usePreferences";
|
||||
import { UserContext } from "../hooks/user.hook";
|
||||
@@ -24,17 +25,26 @@ import globalStyle from "../styles/mantine.style";
|
||||
import Config from "../types/config.type";
|
||||
import { CurrentUser } from "../types/user.type";
|
||||
|
||||
const excludeDefaultLayoutRoutes = ["/admin/config/[category]"];
|
||||
|
||||
function App({ Component, pageProps }: AppProps) {
|
||||
const systemTheme = useColorScheme(pageProps.colorScheme);
|
||||
const router = useRouter();
|
||||
|
||||
const [colorScheme, setColorScheme] = useState<ColorScheme>(systemTheme);
|
||||
const preferences = usePreferences();
|
||||
|
||||
const [user, setUser] = useState<CurrentUser | null>(pageProps.user);
|
||||
const [route, setRoute] = useState<string>(pageProps.route);
|
||||
|
||||
const [configVariables, setConfigVariables] = useState<Config[]>(
|
||||
pageProps.configVariables
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setRoute(router.pathname);
|
||||
}, [router.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
setInterval(async () => await authService.refreshAccessToken(), 30 * 1000);
|
||||
}, []);
|
||||
@@ -86,10 +96,16 @@ function App({ Component, pageProps }: AppProps) {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Header />
|
||||
<Container>
|
||||
{excludeDefaultLayoutRoutes.includes(route) ? (
|
||||
<Component {...pageProps} />
|
||||
</Container>
|
||||
) : (
|
||||
<>
|
||||
<Header />
|
||||
<Container>
|
||||
<Component {...pageProps} />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
</UserContext.Provider>
|
||||
</ConfigContext.Provider>
|
||||
</ModalsProvider>
|
||||
@@ -105,12 +121,13 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||
let pageProps: {
|
||||
user?: CurrentUser;
|
||||
configVariables?: Config[];
|
||||
route?: string;
|
||||
colorScheme: ColorScheme;
|
||||
} = {
|
||||
route: ctx.resolvedUrl,
|
||||
colorScheme:
|
||||
(getCookie("mantine-color-scheme", ctx) as ColorScheme) ?? "light",
|
||||
};
|
||||
|
||||
if (ctx.req) {
|
||||
const cookieHeader = ctx.req.headers.cookie;
|
||||
|
||||
@@ -123,6 +140,8 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||
pageProps.configVariables = (
|
||||
await axios(`http://localhost:8080/api/configs`)
|
||||
).data;
|
||||
|
||||
pageProps.route = ctx.req.url;
|
||||
}
|
||||
|
||||
return { pageProps };
|
||||
|
||||
@@ -11,11 +11,15 @@ export default class _Document extends Document {
|
||||
<Html>
|
||||
<Head>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" href="/icons/icon-white-128x128.png" />
|
||||
<link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
href="/img/icons/icon-white-128x128.png"
|
||||
/>
|
||||
|
||||
<meta property="og:image" content="/img/opengraph-default.png" />
|
||||
<meta property="og:image" content="/img/opengraph.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:image" content="/img/opengraph-default.png" />
|
||||
<meta name="twitter:image" content="/img/opengraph.png" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="theme-color" content="#46509e" />
|
||||
</Head>
|
||||
|
||||
@@ -67,7 +67,7 @@ const MyShares = () => {
|
||||
onClick={() =>
|
||||
showCreateReverseShareModal(
|
||||
modals,
|
||||
config.get("SMTP_ENABLED"),
|
||||
config.get("smtp.enabled"),
|
||||
getReverseShares
|
||||
)
|
||||
}
|
||||
@@ -129,9 +129,9 @@ const MyShares = () => {
|
||||
onClick={() => {
|
||||
if (window.isSecureContext) {
|
||||
clipboard.copy(
|
||||
`${config.get("APP_URL")}/share/${
|
||||
share.id
|
||||
}`
|
||||
`${config.get(
|
||||
"general.appUrl"
|
||||
)}/share/${share.id}`
|
||||
);
|
||||
toast.success(
|
||||
"The share link was copied to the keyboard."
|
||||
@@ -140,7 +140,7 @@ const MyShares = () => {
|
||||
showShareLinkModal(
|
||||
modals,
|
||||
share.id,
|
||||
config.get("APP_URL")
|
||||
config.get("general.appUrl")
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -84,7 +84,9 @@ const MyShares = () => {
|
||||
onClick={() => {
|
||||
if (window.isSecureContext) {
|
||||
clipboard.copy(
|
||||
`${config.get("APP_URL")}/share/${share.id}`
|
||||
`${config.get("general.appUrl")}/share/${
|
||||
share.id
|
||||
}`
|
||||
);
|
||||
toast.success(
|
||||
"Your link was copied to the keyboard."
|
||||
@@ -93,7 +95,7 @@ const MyShares = () => {
|
||||
showShareLinkModal(
|
||||
modals,
|
||||
share.id,
|
||||
config.get("APP_URL")
|
||||
config.get("general.appUrl")
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Space, Title } from "@mantine/core";
|
||||
import AdminConfigTable from "../../components/admin/configuration/AdminConfigTable";
|
||||
import Meta from "../../components/Meta";
|
||||
|
||||
const AdminConfig = () => {
|
||||
return (
|
||||
<>
|
||||
<Meta title="Configuration" />
|
||||
<Title mb={30} order={3}>
|
||||
Configuration
|
||||
</Title>
|
||||
<AdminConfigTable />
|
||||
<Space h="xl" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminConfig;
|
||||
148
frontend/src/pages/admin/config/[category].tsx
Normal file
148
frontend/src/pages/admin/config/[category].tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import {
|
||||
AppShell,
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useMantineTheme,
|
||||
} from "@mantine/core";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput";
|
||||
import ConfigurationHeader from "../../../components/admin/configuration/ConfigurationHeader";
|
||||
import ConfigurationNavBar from "../../../components/admin/configuration/ConfigurationNavBar";
|
||||
import TestEmailButton from "../../../components/admin/configuration/TestEmailButton";
|
||||
import CenterLoader from "../../../components/core/CenterLoader";
|
||||
import Meta from "../../../components/Meta";
|
||||
import useConfig from "../../../hooks/config.hook";
|
||||
import configService from "../../../services/config.service";
|
||||
import { AdminConfig, UpdateConfig } from "../../../types/config.type";
|
||||
import {
|
||||
capitalizeFirstLetter,
|
||||
configVariableToFriendlyName,
|
||||
} from "../../../utils/string.util";
|
||||
import toast from "../../../utils/toast.util";
|
||||
|
||||
export default function AppShellDemo() {
|
||||
const theme = useMantineTheme();
|
||||
const router = useRouter();
|
||||
|
||||
const [isMobileNavBarOpened, setIsMobileNavBarOpened] = useState(false);
|
||||
const isMobile = useMediaQuery("(max-width: 560px)");
|
||||
const config = useConfig();
|
||||
|
||||
const categoryId = router.query.category as string;
|
||||
|
||||
const [configVariables, setConfigVariables] = useState<AdminConfig[]>();
|
||||
const [updatedConfigVariables, setUpdatedConfigVariables] = useState<
|
||||
UpdateConfig[]
|
||||
>([]);
|
||||
|
||||
const saveConfigVariables = async () => {
|
||||
await configService
|
||||
.updateMany(updatedConfigVariables)
|
||||
.then(() => {
|
||||
setUpdatedConfigVariables([]);
|
||||
toast.success("Configurations updated successfully");
|
||||
})
|
||||
.catch(toast.axiosError);
|
||||
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]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
configService.getByCategory(categoryId).then((configVariables) => {
|
||||
setConfigVariables(configVariables);
|
||||
});
|
||||
}, [categoryId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title="Configuration" />
|
||||
<AppShell
|
||||
styles={{
|
||||
main: {
|
||||
background:
|
||||
theme.colorScheme === "dark"
|
||||
? theme.colors.dark[8]
|
||||
: theme.colors.gray[0],
|
||||
},
|
||||
}}
|
||||
navbar={
|
||||
<ConfigurationNavBar
|
||||
categoryId={categoryId}
|
||||
isMobileNavBarOpened={isMobileNavBarOpened}
|
||||
setIsMobileNavBarOpened={setIsMobileNavBarOpened}
|
||||
/>
|
||||
}
|
||||
header={
|
||||
<ConfigurationHeader
|
||||
isMobileNavBarOpened={isMobileNavBarOpened}
|
||||
setIsMobileNavBarOpened={setIsMobileNavBarOpened}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Container size="lg">
|
||||
{!configVariables ? (
|
||||
<CenterLoader />
|
||||
) : (
|
||||
<>
|
||||
<Stack>
|
||||
<Title mb="md" order={3}>
|
||||
{capitalizeFirstLetter(categoryId)}
|
||||
</Title>
|
||||
{configVariables.map((configVariable) => (
|
||||
<Group key={configVariable.key} position="apart">
|
||||
<Stack
|
||||
style={{ maxWidth: isMobile ? "100%" : "40%" }}
|
||||
spacing={0}
|
||||
>
|
||||
<Title order={6}>
|
||||
{configVariableToFriendlyName(configVariable.name)}
|
||||
</Title>
|
||||
<Text color="dimmed" size="sm" mb="xs">
|
||||
{configVariable.description}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack></Stack>
|
||||
<Box style={{ width: isMobile ? "100%" : "50%" }}>
|
||||
<AdminConfigInput
|
||||
key={configVariable.key}
|
||||
configVariable={configVariable}
|
||||
updateConfigVariable={updateConfigVariable}
|
||||
/>
|
||||
</Box>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
<Group mt="lg" position="right">
|
||||
{categoryId == "smtp" && (
|
||||
<TestEmailButton
|
||||
configVariablesChanged={updatedConfigVariables.length != 0}
|
||||
saveConfigVariables={saveConfigVariables}
|
||||
/>
|
||||
)}
|
||||
<Button onClick={saveConfigVariables}>Save</Button>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
</AppShell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
15
frontend/src/pages/admin/config/index.tsx
Normal file
15
frontend/src/pages/admin/config/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
export function getServerSideProps() {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/admin/config/general",
|
||||
},
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
|
||||
const Config = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Config;
|
||||
59
frontend/src/pages/admin/intro.tsx
Normal file
59
frontend/src/pages/admin/intro.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
Anchor,
|
||||
Button,
|
||||
Center,
|
||||
Container,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import Logo from "../../components/Logo";
|
||||
import Meta from "../../components/Meta";
|
||||
|
||||
const Intro = () => {
|
||||
return (
|
||||
<>
|
||||
<Meta title="Intro" />
|
||||
<Container size="xs">
|
||||
<Stack>
|
||||
<Center>
|
||||
<Logo height={80} width={80} />
|
||||
</Center>
|
||||
<Center>
|
||||
<Title order={2}>Welcome to Pingvin Share</Title>
|
||||
</Center>
|
||||
<Text>
|
||||
If you enjoy Pingvin Share please ⭐️ it on{" "}
|
||||
<Anchor
|
||||
target="_blank"
|
||||
href="https://github.com/stonith404/pingvin-share"
|
||||
>
|
||||
GitHub
|
||||
</Anchor>{" "}
|
||||
or{" "}
|
||||
<Anchor
|
||||
target="_blank"
|
||||
href="https://github.com/sponsors/stonith404"
|
||||
>
|
||||
buy me a coffee
|
||||
</Anchor>{" "}
|
||||
if you want to support my work.
|
||||
</Text>
|
||||
<Text>Enough talked, have fun with Pingvin Share!</Text>
|
||||
<Text mt="lg">How to you want to continue?</Text>
|
||||
<Stack>
|
||||
<Button href="/admin/config" component={Link}>
|
||||
Customize configuration
|
||||
</Button>
|
||||
<Button href="/" component={Link} variant="light">
|
||||
Explore Pingvin Share
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Intro;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Box, Stack, Text, Title } from "@mantine/core";
|
||||
import AdminConfigTable from "../../components/admin/configuration/AdminConfigTable";
|
||||
|
||||
import Logo from "../../components/Logo";
|
||||
import Meta from "../../components/Meta";
|
||||
|
||||
const Setup = () => {
|
||||
return (
|
||||
<>
|
||||
<Meta title="Setup" />
|
||||
<Stack align="center">
|
||||
<Logo height={80} width={80} />
|
||||
<Title order={2}>Welcome to Pingvin Share</Title>
|
||||
<Text>Let's customize Pingvin Share for you! </Text>
|
||||
<Box style={{ width: "100%" }}>
|
||||
<AdminConfigTable />
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Setup;
|
||||
@@ -58,7 +58,7 @@ const Users = () => {
|
||||
</Title>
|
||||
<Button
|
||||
onClick={() =>
|
||||
showCreateUserModal(modals, config.get("SMTP_ENABLED"), getUsers)
|
||||
showCreateUserModal(modals, config.get("smtp.enabled"), getUsers)
|
||||
}
|
||||
leftIcon={<TbPlus size={20} />}
|
||||
>
|
||||
|
||||
@@ -11,7 +11,7 @@ export const config = {
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => {
|
||||
return httpProxyMiddleware(req, res, {
|
||||
headers: {
|
||||
"X-Forwarded-For": req.socket.remoteAddress ?? "",
|
||||
"X-Forwarded-For": req.socket?.remoteAddress ?? "",
|
||||
},
|
||||
target: "http://localhost:8080",
|
||||
});
|
||||
|
||||
@@ -51,7 +51,6 @@ const ResetPassword = () => {
|
||||
<Paper withBorder shadow="md" p={30} radius="md" mt="xl">
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
console.log(resetPasswordToken);
|
||||
authService
|
||||
.resetPassword(resetPasswordToken, values.password)
|
||||
.then(() => {
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { TbCheck } from "react-icons/tb";
|
||||
import Logo from "../components/Logo";
|
||||
import Meta from "../components/Meta";
|
||||
import useUser from "../hooks/user.hook";
|
||||
|
||||
@@ -150,12 +150,7 @@ export default function Home() {
|
||||
</Group>
|
||||
</div>
|
||||
<Group className={classes.image} align="center">
|
||||
<Image
|
||||
src="/img/logo.svg"
|
||||
alt="Pingvin Share Logo"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
<Logo width={200} height={200} />
|
||||
</Group>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -35,7 +35,7 @@ const Upload = ({
|
||||
const [files, setFiles] = useState<FileUpload[]>([]);
|
||||
const [isUploading, setisUploading] = useState(false);
|
||||
|
||||
maxShareSize ??= parseInt(config.get("MAX_SHARE_SIZE"));
|
||||
maxShareSize ??= parseInt(config.get("share.maxSize"));
|
||||
|
||||
const uploadFiles = async (share: CreateShare) => {
|
||||
setisUploading(true);
|
||||
@@ -146,7 +146,7 @@ const Upload = ({
|
||||
.completeShare(createdShare.id)
|
||||
.then((share) => {
|
||||
setisUploading(false);
|
||||
showCompletedUploadModal(modals, share, config.get("APP_URL"));
|
||||
showCompletedUploadModal(modals, share, config.get("general.appUrl"));
|
||||
setFiles([]);
|
||||
})
|
||||
.catch(() =>
|
||||
@@ -168,9 +168,9 @@ const Upload = ({
|
||||
{
|
||||
isUserSignedIn: user ? true : false,
|
||||
isReverseShare,
|
||||
appUrl: config.get("APP_URL"),
|
||||
appUrl: config.get("general.appUrl"),
|
||||
allowUnauthenticatedShares: config.get(
|
||||
"ALLOW_UNAUTHENTICATED_SHARES"
|
||||
"share.allowUnauthenticatedShares"
|
||||
),
|
||||
enableEmailRecepients: config.get(
|
||||
"ENABLE_SHARE_EMAIL_RECIPIENTS"
|
||||
|
||||
Reference in New Issue
Block a user