mirror of
https://github.com/stonith404/pingvin-share.git
synced 2025-02-19 01:55:48 -05:00
feat(frontend): server side rendering to improve performance
This commit is contained in:
parent
82f204e8a9
commit
38de022215
14 changed files with 137 additions and 71 deletions
|
@ -7,18 +7,20 @@ const Meta = ({
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const metaTitle = `${title} - Pingvin Share`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Head>
|
<Head>
|
||||||
{/* TODO: Doesn't work because script get only executed on client side */}
|
<title>{metaTitle}</title>
|
||||||
<title>{title} - Pingvin Share</title>
|
<meta name="og:title" content={metaTitle} />
|
||||||
<meta name="og:title" content={`${title} - Pingvin Share`} />
|
|
||||||
<meta
|
<meta
|
||||||
name="og:description"
|
name="og:description"
|
||||||
content={
|
content={
|
||||||
description ?? "An open-source and self-hosted sharing platform."
|
description ?? "An open-source and self-hosted sharing platform."
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<meta name="twitter:title" content={`${title} - Pingvin Share`} />
|
<meta property="og:image" content="/img/opengraph-default.png" />
|
||||||
|
<meta name="twitter:title" content={metaTitle} />
|
||||||
<meta name="twitter:description" content={description} />
|
<meta name="twitter:description" content={description} />
|
||||||
</Head>
|
</Head>
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,7 +18,6 @@ const ThemeSwitcher = () => {
|
||||||
);
|
);
|
||||||
const { toggleColorScheme } = useMantineColorScheme();
|
const { toggleColorScheme } = useMantineColorScheme();
|
||||||
const systemColorScheme = useColorScheme();
|
const systemColorScheme = useColorScheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
|
|
|
@ -82,6 +82,7 @@ const AdminConfigTable = () => {
|
||||||
})
|
})
|
||||||
.catch(toast.axiosError);
|
.catch(toast.axiosError);
|
||||||
}
|
}
|
||||||
|
config.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -18,13 +18,12 @@ import * as yup from "yup";
|
||||||
import useConfig from "../../hooks/config.hook";
|
import useConfig from "../../hooks/config.hook";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
import authService from "../../services/auth.service";
|
import authService from "../../services/auth.service";
|
||||||
import userService from "../../services/user.service";
|
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
|
||||||
const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { setUser } = useUser();
|
const { refreshUser } = useUser();
|
||||||
|
|
||||||
const [showTotp, setShowTotp] = React.useState(false);
|
const [showTotp, setShowTotp] = React.useState(false);
|
||||||
const [loginToken, setLoginToken] = React.useState("");
|
const [loginToken, setLoginToken] = React.useState("");
|
||||||
|
@ -64,7 +63,7 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||||
});
|
});
|
||||||
setLoginToken(response.data["loginToken"]);
|
setLoginToken(response.data["loginToken"]);
|
||||||
} else {
|
} else {
|
||||||
setUser(await userService.getCurrentUser());
|
await refreshUser();
|
||||||
router.replace(redirectPath);
|
router.replace(redirectPath);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -74,7 +73,10 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||||
const signInTotp = (email: string, password: string, totp: string) => {
|
const signInTotp = (email: string, password: string, totp: string) => {
|
||||||
authService
|
authService
|
||||||
.signInTotp(email, password, totp, loginToken)
|
.signInTotp(email, password, totp, loginToken)
|
||||||
.then(() => window.location.replace("/"))
|
.then(async () => {
|
||||||
|
await refreshUser();
|
||||||
|
router.replace(redirectPath);
|
||||||
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (error?.response?.data?.message == "Login token expired") {
|
if (error?.response?.data?.message == "Login token expired") {
|
||||||
toast.error("Login token expired");
|
toast.error("Login token expired");
|
||||||
|
|
|
@ -15,13 +15,12 @@ import * as yup from "yup";
|
||||||
import useConfig from "../../hooks/config.hook";
|
import useConfig from "../../hooks/config.hook";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
import authService from "../../services/auth.service";
|
import authService from "../../services/auth.service";
|
||||||
import userService from "../../services/user.service";
|
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
|
||||||
const SignUpForm = () => {
|
const SignUpForm = () => {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { setUser } = useUser();
|
const { refreshUser } = useUser();
|
||||||
|
|
||||||
const validationSchema = yup.object().shape({
|
const validationSchema = yup.object().shape({
|
||||||
email: yup.string().email().required(),
|
email: yup.string().email().required(),
|
||||||
|
@ -42,8 +41,8 @@ const SignUpForm = () => {
|
||||||
await authService
|
await authService
|
||||||
.signUp(email, username, password)
|
.signUp(email, username, password)
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
setUser(await userService.getCurrentUser());
|
await refreshUser();
|
||||||
router.replace("/");
|
router.replace("/upload");
|
||||||
})
|
})
|
||||||
.catch(toast.axiosError);
|
.catch(toast.axiosError);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
|
||||||
Box,
|
Box,
|
||||||
Burger,
|
Burger,
|
||||||
Container,
|
Container,
|
||||||
|
@ -14,7 +13,6 @@ import {
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ReactNode, useEffect, useState } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
import { TbPlus } from "react-icons/tb";
|
|
||||||
import useConfig from "../../hooks/config.hook";
|
import useConfig from "../../hooks/config.hook";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
import Logo from "../Logo";
|
import Logo from "../Logo";
|
||||||
|
@ -172,7 +170,9 @@ const NavBar = () => {
|
||||||
href={link.link ?? ""}
|
href={link.link ?? ""}
|
||||||
onClick={() => toggleOpened.toggle()}
|
onClick={() => toggleOpened.toggle()}
|
||||||
className={cx(classes.link, {
|
className={cx(classes.link, {
|
||||||
[classes.linkActive]: window.location.pathname == link.link,
|
[classes.linkActive]:
|
||||||
|
typeof window != "undefined" &&
|
||||||
|
window.location.pathname == link.link,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
import configService from "../services/config.service";
|
import configService from "../services/config.service";
|
||||||
import Config from "../types/config.type";
|
import { ConfigHook } from "../types/config.type";
|
||||||
|
|
||||||
export const ConfigContext = createContext<Config[] | null>(null);
|
export const ConfigContext = createContext<ConfigHook>({
|
||||||
|
configVariables: [],
|
||||||
|
refresh: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
const useConfig = () => {
|
const useConfig = () => {
|
||||||
const configVariables = useContext(ConfigContext) as Config[];
|
const configContext = useContext(ConfigContext);
|
||||||
return {
|
return {
|
||||||
get: (key: string) => configService.get(key, configVariables),
|
get: (key: string) => configService.get(key, configContext.configVariables),
|
||||||
|
refresh: () => configContext.refresh(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { UserHook } from "../types/user.type";
|
||||||
|
|
||||||
export const UserContext = createContext<UserHook>({
|
export const UserContext = createContext<UserHook>({
|
||||||
user: null,
|
user: null,
|
||||||
setUser: () => {},
|
refreshUser: async () => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const useUser = () => {
|
const useUser = () => {
|
||||||
|
|
|
@ -2,12 +2,14 @@ import {
|
||||||
ColorScheme,
|
ColorScheme,
|
||||||
ColorSchemeProvider,
|
ColorSchemeProvider,
|
||||||
Container,
|
Container,
|
||||||
LoadingOverlay,
|
|
||||||
MantineProvider,
|
MantineProvider,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useColorScheme } from "@mantine/hooks";
|
import { useColorScheme } from "@mantine/hooks";
|
||||||
import { ModalsProvider } from "@mantine/modals";
|
import { ModalsProvider } from "@mantine/modals";
|
||||||
import { NotificationsProvider } from "@mantine/notifications";
|
import { NotificationsProvider } from "@mantine/notifications";
|
||||||
|
import axios from "axios";
|
||||||
|
import { getCookie, setCookie } from "cookies-next";
|
||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Header from "../components/navBar/NavBar";
|
import Header from "../components/navBar/NavBar";
|
||||||
|
@ -21,38 +23,38 @@ import GlobalStyle from "../styles/global.style";
|
||||||
import globalStyle from "../styles/mantine.style";
|
import globalStyle from "../styles/mantine.style";
|
||||||
import Config from "../types/config.type";
|
import Config from "../types/config.type";
|
||||||
import { CurrentUser } from "../types/user.type";
|
import { CurrentUser } from "../types/user.type";
|
||||||
import { GlobalLoadingContext } from "../utils/loading.util";
|
|
||||||
|
|
||||||
function App({ Component, pageProps }: AppProps) {
|
function App({ Component, pageProps }: AppProps) {
|
||||||
const systemTheme = useColorScheme();
|
const systemTheme = useColorScheme(pageProps.colorScheme);
|
||||||
|
const [colorScheme, setColorScheme] = useState<ColorScheme>(systemTheme);
|
||||||
const preferences = usePreferences();
|
const preferences = usePreferences();
|
||||||
const [colorScheme, setColorScheme] = useState<ColorScheme>("light");
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [user, setUser] = useState<CurrentUser | null>(null);
|
|
||||||
const [configVariables, setConfigVariables] = useState<Config[] | null>(null);
|
|
||||||
|
|
||||||
const getInitalData = async () => {
|
const [user, setUser] = useState<CurrentUser | null>(pageProps.user);
|
||||||
setIsLoading(true);
|
|
||||||
setConfigVariables(await configService.list());
|
const [configVariables, setConfigVariables] = useState<Config[]>(
|
||||||
await authService.refreshAccessToken();
|
pageProps.configVariables
|
||||||
setUser(await userService.getCurrentUser());
|
);
|
||||||
setIsLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInterval(async () => await authService.refreshAccessToken(), 30 * 1000);
|
setInterval(async () => await authService.refreshAccessToken(), 30 * 1000);
|
||||||
getInitalData();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColorScheme(
|
const colorScheme =
|
||||||
preferences.get("colorScheme") == "system"
|
preferences.get("colorScheme") == "system"
|
||||||
? systemTheme
|
? systemTheme
|
||||||
: preferences.get("colorScheme")
|
: preferences.get("colorScheme");
|
||||||
);
|
|
||||||
|
toggleColorScheme(colorScheme);
|
||||||
}, [systemTheme]);
|
}, [systemTheme]);
|
||||||
|
|
||||||
|
const toggleColorScheme = (value: ColorScheme) => {
|
||||||
|
setColorScheme(value ?? "light");
|
||||||
|
setCookie("mantine-color-scheme", value ?? "light", {
|
||||||
|
sameSite: "lax",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineProvider
|
<MantineProvider
|
||||||
withGlobalStyles
|
withGlobalStyles
|
||||||
|
@ -61,26 +63,35 @@ function App({ Component, pageProps }: AppProps) {
|
||||||
>
|
>
|
||||||
<ColorSchemeProvider
|
<ColorSchemeProvider
|
||||||
colorScheme={colorScheme}
|
colorScheme={colorScheme}
|
||||||
toggleColorScheme={(value) => setColorScheme(value ?? "light")}
|
toggleColorScheme={toggleColorScheme}
|
||||||
>
|
>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
||||||
<NotificationsProvider>
|
<NotificationsProvider>
|
||||||
<ModalsProvider>
|
<ModalsProvider>
|
||||||
<GlobalLoadingContext.Provider value={{ isLoading, setIsLoading }}>
|
<ConfigContext.Provider
|
||||||
{isLoading ? (
|
value={{
|
||||||
<LoadingOverlay visible overlayOpacity={1} />
|
configVariables,
|
||||||
) : (
|
refresh: async () => {
|
||||||
<ConfigContext.Provider value={configVariables}>
|
setConfigVariables(await configService.list());
|
||||||
<UserContext.Provider value={{ user, setUser }}>
|
},
|
||||||
<LoadingOverlay visible={isLoading} overlayOpacity={1} />
|
}}
|
||||||
<Header />
|
>
|
||||||
<Container>
|
<UserContext.Provider
|
||||||
<Component {...pageProps} />
|
value={{
|
||||||
</Container>
|
user,
|
||||||
</UserContext.Provider>
|
refreshUser: async () => {
|
||||||
</ConfigContext.Provider>
|
const user = await userService.getCurrentUser();
|
||||||
)}
|
setUser(user);
|
||||||
</GlobalLoadingContext.Provider>
|
return user;
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Header />
|
||||||
|
<Container>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</Container>
|
||||||
|
</UserContext.Provider>
|
||||||
|
</ConfigContext.Provider>
|
||||||
</ModalsProvider>
|
</ModalsProvider>
|
||||||
</NotificationsProvider>
|
</NotificationsProvider>
|
||||||
</ColorSchemeProvider>
|
</ColorSchemeProvider>
|
||||||
|
@ -88,4 +99,33 @@ function App({ Component, pageProps }: AppProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch user and config variables on server side when the first request is made
|
||||||
|
// These will get passed as a page prop to the App component and stored in the contexts
|
||||||
|
App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||||
|
let pageProps: {
|
||||||
|
user?: CurrentUser;
|
||||||
|
configVariables?: Config[];
|
||||||
|
colorScheme: ColorScheme;
|
||||||
|
} = {
|
||||||
|
colorScheme:
|
||||||
|
(getCookie("mantine-color-scheme", ctx) as ColorScheme) ?? "light",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ctx.req) {
|
||||||
|
const cookieHeader = ctx.req.headers.cookie;
|
||||||
|
|
||||||
|
pageProps.user = await axios(`http://localhost:8080/api/users/me`, {
|
||||||
|
headers: { cookie: cookieHeader },
|
||||||
|
})
|
||||||
|
.then((res) => res.data)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
pageProps.configVariables = (
|
||||||
|
await axios(`http://localhost:8080/api/configs`)
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { pageProps };
|
||||||
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -24,7 +24,7 @@ import userService from "../../services/user.service";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
|
||||||
const Account = () => {
|
const Account = () => {
|
||||||
const { user, setUser } = useUser();
|
const { user, refreshUser } = useUser();
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
|
||||||
const accountForm = useForm({
|
const accountForm = useForm({
|
||||||
|
@ -81,8 +81,6 @@ const Account = () => {
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const refreshUser = async () => setUser(await userService.getCurrentUser());
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="My account" />
|
<Meta title="My account" />
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
Button,
|
Button,
|
||||||
Center,
|
Center,
|
||||||
Group,
|
Group,
|
||||||
LoadingOverlay,
|
|
||||||
Space,
|
Space,
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
|
@ -18,6 +17,7 @@ import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { TbLink, TbTrash } from "react-icons/tb";
|
import { TbLink, TbTrash } from "react-icons/tb";
|
||||||
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
||||||
|
import CenterLoader from "../../components/core/CenterLoader";
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
import useConfig from "../../hooks/config.hook";
|
import useConfig from "../../hooks/config.hook";
|
||||||
import shareService from "../../services/share.service";
|
import shareService from "../../services/share.service";
|
||||||
|
@ -35,7 +35,8 @@ const MyShares = () => {
|
||||||
shareService.getMyShares().then((shares) => setShares(shares));
|
shareService.getMyShares().then((shares) => setShares(shares));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!shares) return <LoadingOverlay visible />;
|
if (!shares) return <CenterLoader />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="My shares" />
|
<Meta title="My shares" />
|
||||||
|
|
|
@ -1,26 +1,41 @@
|
||||||
import { LoadingOverlay } from "@mantine/core";
|
import { LoadingOverlay } from "@mantine/core";
|
||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import SignInForm from "../../components/auth/SignInForm";
|
import SignInForm from "../../components/auth/SignInForm";
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
|
|
||||||
const SignIn = () => {
|
export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const { user } = useUser();
|
return {
|
||||||
|
props: { redirectPath: context.query.redirect ?? null },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const SignIn = ({ redirectPath }: { redirectPath?: string }) => {
|
||||||
|
const { refreshUser } = useUser();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const redirectPath = (router.query.redirect as string) ?? "/upload";
|
const [isLoading, setIsLoading] = useState(redirectPath ? true : false);
|
||||||
|
|
||||||
// If the access token is expired, the middleware redirects to this page.
|
// If the access token is expired, the middleware redirects to this page.
|
||||||
// If the refresh token is still valid, the user will be redirected to the home page.
|
// If the refresh token is still valid, the user will be redirected to the last page.
|
||||||
if (user) {
|
useEffect(() => {
|
||||||
router.replace(redirectPath);
|
refreshUser().then((user) => {
|
||||||
return <LoadingOverlay overlayOpacity={1} visible />;
|
if (user) {
|
||||||
}
|
router.replace(redirectPath ?? "/upload");
|
||||||
|
} else {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isLoading) return <LoadingOverlay overlayOpacity={1} visible />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Sign In" />
|
<Meta title="Sign In" />
|
||||||
<SignInForm redirectPath={redirectPath} />
|
<SignInForm redirectPath={redirectPath ?? "/upload"} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,4 +29,9 @@ export type AdminConfigGroupedByCategory = {
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ConfigHook = {
|
||||||
|
configVariables: Config[];
|
||||||
|
refresh: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
export default Config;
|
export default Config;
|
||||||
|
|
|
@ -29,7 +29,7 @@ export type CurrentUser = User & {};
|
||||||
|
|
||||||
export type UserHook = {
|
export type UserHook = {
|
||||||
user: CurrentUser | null;
|
user: CurrentUser | null;
|
||||||
setUser: (user: CurrentUser | null) => void;
|
refreshUser: () => Promise<CurrentUser | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default User;
|
export default User;
|
||||||
|
|
Loading…
Add table
Reference in a new issue