Skip to content

Commit

Permalink
feat(frontend): server side rendering to improve performance
Browse files Browse the repository at this point in the history
  • Loading branch information
stonith404 committed Feb 7, 2023
1 parent 82f204e commit 38de022
Show file tree
Hide file tree
Showing 14 changed files with 137 additions and 71 deletions.
10 changes: 6 additions & 4 deletions frontend/src/components/Meta.tsx
Expand Up @@ -7,18 +7,20 @@ const Meta = ({
title: string;
description?: string;
}) => {
const metaTitle = `${title} - Pingvin Share`;

return (
<Head>
{/* TODO: Doesn't work because script get only executed on client side */}
<title>{title} - Pingvin Share</title>
<meta name="og:title" content={`${title} - Pingvin Share`} />
<title>{metaTitle}</title>
<meta name="og:title" content={metaTitle} />
<meta
name="og:description"
content={
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} />
</Head>
);
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/account/ThemeSwitcher.tsx
Expand Up @@ -18,7 +18,6 @@ const ThemeSwitcher = () => {
);
const { toggleColorScheme } = useMantineColorScheme();
const systemColorScheme = useColorScheme();

return (
<Stack>
<SegmentedControl
Expand Down
Expand Up @@ -82,6 +82,7 @@ const AdminConfigTable = () => {
})
.catch(toast.axiosError);
}
config.refresh();
};

useEffect(() => {
Expand Down
10 changes: 6 additions & 4 deletions frontend/src/components/auth/SignInForm.tsx
Expand Up @@ -18,13 +18,12 @@ import * as yup from "yup";
import useConfig from "../../hooks/config.hook";
import useUser from "../../hooks/user.hook";
import authService from "../../services/auth.service";
import userService from "../../services/user.service";
import toast from "../../utils/toast.util";

const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
const config = useConfig();
const router = useRouter();
const { setUser } = useUser();
const { refreshUser } = useUser();

const [showTotp, setShowTotp] = React.useState(false);
const [loginToken, setLoginToken] = React.useState("");
Expand Down Expand Up @@ -64,7 +63,7 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
});
setLoginToken(response.data["loginToken"]);
} else {
setUser(await userService.getCurrentUser());
await refreshUser();
router.replace(redirectPath);
}
})
Expand All @@ -74,7 +73,10 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
const signInTotp = (email: string, password: string, totp: string) => {
authService
.signInTotp(email, password, totp, loginToken)
.then(() => window.location.replace("/"))
.then(async () => {
await refreshUser();
router.replace(redirectPath);
})
.catch((error) => {
if (error?.response?.data?.message == "Login token expired") {
toast.error("Login token expired");
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/components/auth/SignUpForm.tsx
Expand Up @@ -15,13 +15,12 @@ import * as yup from "yup";
import useConfig from "../../hooks/config.hook";
import useUser from "../../hooks/user.hook";
import authService from "../../services/auth.service";
import userService from "../../services/user.service";
import toast from "../../utils/toast.util";

const SignUpForm = () => {
const config = useConfig();
const router = useRouter();
const { setUser } = useUser();
const { refreshUser } = useUser();

const validationSchema = yup.object().shape({
email: yup.string().email().required(),
Expand All @@ -42,8 +41,8 @@ const SignUpForm = () => {
await authService
.signUp(email, username, password)
.then(async () => {
setUser(await userService.getCurrentUser());
router.replace("/");
await refreshUser();
router.replace("/upload");
})
.catch(toast.axiosError);
};
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/navBar/NavBar.tsx
@@ -1,5 +1,4 @@
import {
ActionIcon,
Box,
Burger,
Container,
Expand All @@ -14,7 +13,6 @@ import {
import { useDisclosure } from "@mantine/hooks";
import Link from "next/link";
import { ReactNode, useEffect, useState } from "react";
import { TbPlus } from "react-icons/tb";
import useConfig from "../../hooks/config.hook";
import useUser from "../../hooks/user.hook";
import Logo from "../Logo";
Expand Down Expand Up @@ -172,7 +170,9 @@ const NavBar = () => {
href={link.link ?? ""}
onClick={() => toggleOpened.toggle()}
className={cx(classes.link, {
[classes.linkActive]: window.location.pathname == link.link,
[classes.linkActive]:
typeof window != "undefined" &&
window.location.pathname == link.link,
})}
>
{link.label}
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/hooks/config.hook.ts
@@ -1,13 +1,17 @@
import { createContext, useContext } from "react";
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 configVariables = useContext(ConfigContext) as Config[];
const configContext = useContext(ConfigContext);
return {
get: (key: string) => configService.get(key, configVariables),
get: (key: string) => configService.get(key, configContext.configVariables),
refresh: () => configContext.refresh(),
};
};

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/user.hook.ts
Expand Up @@ -3,7 +3,7 @@ import { UserHook } from "../types/user.type";

export const UserContext = createContext<UserHook>({
user: null,
setUser: () => {},
refreshUser: async () => null,
});

const useUser = () => {
Expand Down
110 changes: 75 additions & 35 deletions frontend/src/pages/_app.tsx
Expand Up @@ -2,12 +2,14 @@ import {
ColorScheme,
ColorSchemeProvider,
Container,
LoadingOverlay,
MantineProvider,
} from "@mantine/core";
import { useColorScheme } from "@mantine/hooks";
import { ModalsProvider } from "@mantine/modals";
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 { useEffect, useState } from "react";
import Header from "../components/navBar/NavBar";
Expand All @@ -21,38 +23,38 @@ import GlobalStyle from "../styles/global.style";
import globalStyle from "../styles/mantine.style";
import Config from "../types/config.type";
import { CurrentUser } from "../types/user.type";
import { GlobalLoadingContext } from "../utils/loading.util";

function App({ Component, pageProps }: AppProps) {
const systemTheme = useColorScheme();

const systemTheme = useColorScheme(pageProps.colorScheme);
const [colorScheme, setColorScheme] = useState<ColorScheme>(systemTheme);
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 () => {
setIsLoading(true);
setConfigVariables(await configService.list());
await authService.refreshAccessToken();
setUser(await userService.getCurrentUser());
setIsLoading(false);
};
const [user, setUser] = useState<CurrentUser | null>(pageProps.user);

const [configVariables, setConfigVariables] = useState<Config[]>(
pageProps.configVariables
);

useEffect(() => {
setInterval(async () => await authService.refreshAccessToken(), 30 * 1000);
getInitalData();
}, []);

useEffect(() => {
setColorScheme(
const colorScheme =
preferences.get("colorScheme") == "system"
? systemTheme
: preferences.get("colorScheme")
);
: preferences.get("colorScheme");

toggleColorScheme(colorScheme);
}, [systemTheme]);

const toggleColorScheme = (value: ColorScheme) => {
setColorScheme(value ?? "light");
setCookie("mantine-color-scheme", value ?? "light", {
sameSite: "lax",
});
};

return (
<MantineProvider
withGlobalStyles
Expand All @@ -61,31 +63,69 @@ function App({ Component, pageProps }: AppProps) {
>
<ColorSchemeProvider
colorScheme={colorScheme}
toggleColorScheme={(value) => setColorScheme(value ?? "light")}
toggleColorScheme={toggleColorScheme}
>
<GlobalStyle />
<NotificationsProvider>
<ModalsProvider>
<GlobalLoadingContext.Provider value={{ isLoading, setIsLoading }}>
{isLoading ? (
<LoadingOverlay visible overlayOpacity={1} />
) : (
<ConfigContext.Provider value={configVariables}>
<UserContext.Provider value={{ user, setUser }}>
<LoadingOverlay visible={isLoading} overlayOpacity={1} />
<Header />
<Container>
<Component {...pageProps} />
</Container>
</UserContext.Provider>
</ConfigContext.Provider>
)}
</GlobalLoadingContext.Provider>
<ConfigContext.Provider
value={{
configVariables,
refresh: async () => {
setConfigVariables(await configService.list());
},
}}
>
<UserContext.Provider
value={{
user,
refreshUser: async () => {
const user = await userService.getCurrentUser();
setUser(user);
return user;
},
}}
>
<Header />
<Container>
<Component {...pageProps} />
</Container>
</UserContext.Provider>
</ConfigContext.Provider>
</ModalsProvider>
</NotificationsProvider>
</ColorSchemeProvider>
</MantineProvider>
);
}

// 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;
4 changes: 1 addition & 3 deletions frontend/src/pages/account/index.tsx
Expand Up @@ -24,7 +24,7 @@ import userService from "../../services/user.service";
import toast from "../../utils/toast.util";

const Account = () => {
const { user, setUser } = useUser();
const { user, refreshUser } = useUser();
const modals = useModals();

const accountForm = useForm({
Expand Down Expand Up @@ -81,8 +81,6 @@ const Account = () => {
),
});

const refreshUser = async () => setUser(await userService.getCurrentUser());

return (
<>
<Meta title="My account" />
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/pages/account/shares.tsx
Expand Up @@ -4,7 +4,6 @@ import {
Button,
Center,
Group,
LoadingOverlay,
Space,
Stack,
Table,
Expand All @@ -18,6 +17,7 @@ import Link from "next/link";
import { useEffect, useState } from "react";
import { TbLink, TbTrash } from "react-icons/tb";
import showShareLinkModal from "../../components/account/showShareLinkModal";
import CenterLoader from "../../components/core/CenterLoader";
import Meta from "../../components/Meta";
import useConfig from "../../hooks/config.hook";
import shareService from "../../services/share.service";
Expand All @@ -35,7 +35,8 @@ const MyShares = () => {
shareService.getMyShares().then((shares) => setShares(shares));
}, []);

if (!shares) return <LoadingOverlay visible />;
if (!shares) return <CenterLoader />;

return (
<>
<Meta title="My shares" />
Expand Down

0 comments on commit 38de022

Please sign in to comment.