diff --git a/frontend/src/components/Meta.tsx b/frontend/src/components/Meta.tsx index c755ac27b..da5c8907b 100644 --- a/frontend/src/components/Meta.tsx +++ b/frontend/src/components/Meta.tsx @@ -7,18 +7,20 @@ const Meta = ({ title: string; description?: string; }) => { + const metaTitle = `${title} - Pingvin Share`; + return ( - {/* TODO: Doesn't work because script get only executed on client side */} - {title} - Pingvin Share - + {metaTitle} + - + + ); diff --git a/frontend/src/components/account/ThemeSwitcher.tsx b/frontend/src/components/account/ThemeSwitcher.tsx index 9bd778281..75eea9215 100644 --- a/frontend/src/components/account/ThemeSwitcher.tsx +++ b/frontend/src/components/account/ThemeSwitcher.tsx @@ -18,7 +18,6 @@ const ThemeSwitcher = () => { ); const { toggleColorScheme } = useMantineColorScheme(); const systemColorScheme = useColorScheme(); - return ( { }) .catch(toast.axiosError); } + config.refresh(); }; useEffect(() => { diff --git a/frontend/src/components/auth/SignInForm.tsx b/frontend/src/components/auth/SignInForm.tsx index d0d483ddb..9d1fb4944 100644 --- a/frontend/src/components/auth/SignInForm.tsx +++ b/frontend/src/components/auth/SignInForm.tsx @@ -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(""); @@ -64,7 +63,7 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => { }); setLoginToken(response.data["loginToken"]); } else { - setUser(await userService.getCurrentUser()); + await refreshUser(); router.replace(redirectPath); } }) @@ -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"); diff --git a/frontend/src/components/auth/SignUpForm.tsx b/frontend/src/components/auth/SignUpForm.tsx index 02bd63640..f1fe3306e 100644 --- a/frontend/src/components/auth/SignUpForm.tsx +++ b/frontend/src/components/auth/SignUpForm.tsx @@ -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(), @@ -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); }; diff --git a/frontend/src/components/navBar/NavBar.tsx b/frontend/src/components/navBar/NavBar.tsx index d80dd547f..020ff7a39 100644 --- a/frontend/src/components/navBar/NavBar.tsx +++ b/frontend/src/components/navBar/NavBar.tsx @@ -1,5 +1,4 @@ import { - ActionIcon, Box, Burger, Container, @@ -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"; @@ -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} diff --git a/frontend/src/hooks/config.hook.ts b/frontend/src/hooks/config.hook.ts index 8f3c271ce..24ab4894a 100644 --- a/frontend/src/hooks/config.hook.ts +++ b/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(null); +export const ConfigContext = createContext({ + 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(), }; }; diff --git a/frontend/src/hooks/user.hook.ts b/frontend/src/hooks/user.hook.ts index 08be50c05..47dd2c87d 100644 --- a/frontend/src/hooks/user.hook.ts +++ b/frontend/src/hooks/user.hook.ts @@ -3,7 +3,7 @@ import { UserHook } from "../types/user.type"; export const UserContext = createContext({ user: null, - setUser: () => {}, + refreshUser: async () => null, }); const useUser = () => { diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 17d0df9fb..8a4c551ca 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -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"; @@ -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(systemTheme); const preferences = usePreferences(); - const [colorScheme, setColorScheme] = useState("light"); - const [isLoading, setIsLoading] = useState(true); - const [user, setUser] = useState(null); - const [configVariables, setConfigVariables] = useState(null); - const getInitalData = async () => { - setIsLoading(true); - setConfigVariables(await configService.list()); - await authService.refreshAccessToken(); - setUser(await userService.getCurrentUser()); - setIsLoading(false); - }; + const [user, setUser] = useState(pageProps.user); + + const [configVariables, setConfigVariables] = useState( + 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 ( setColorScheme(value ?? "light")} + toggleColorScheme={toggleColorScheme} > - - {isLoading ? ( - - ) : ( - - - -
- - - - - - )} - + { + setConfigVariables(await configService.list()); + }, + }} + > + { + const user = await userService.getCurrentUser(); + setUser(user); + return user; + }, + }} + > +
+ + + + + @@ -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; diff --git a/frontend/src/pages/account/index.tsx b/frontend/src/pages/account/index.tsx index 796871a49..8819613ad 100644 --- a/frontend/src/pages/account/index.tsx +++ b/frontend/src/pages/account/index.tsx @@ -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({ @@ -81,8 +81,6 @@ const Account = () => { ), }); - const refreshUser = async () => setUser(await userService.getCurrentUser()); - return ( <> diff --git a/frontend/src/pages/account/shares.tsx b/frontend/src/pages/account/shares.tsx index 834f4b5a7..8a00807e8 100644 --- a/frontend/src/pages/account/shares.tsx +++ b/frontend/src/pages/account/shares.tsx @@ -4,7 +4,6 @@ import { Button, Center, Group, - LoadingOverlay, Space, Stack, Table, @@ -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"; @@ -35,7 +35,8 @@ const MyShares = () => { shareService.getMyShares().then((shares) => setShares(shares)); }, []); - if (!shares) return ; + if (!shares) return ; + return ( <> diff --git a/frontend/src/pages/auth/signIn.tsx b/frontend/src/pages/auth/signIn.tsx index 317c05bcc..08d28986b 100644 --- a/frontend/src/pages/auth/signIn.tsx +++ b/frontend/src/pages/auth/signIn.tsx @@ -1,26 +1,41 @@ import { LoadingOverlay } from "@mantine/core"; +import { GetServerSidePropsContext } from "next"; import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; import SignInForm from "../../components/auth/SignInForm"; import Meta from "../../components/Meta"; import useUser from "../../hooks/user.hook"; -const SignIn = () => { - const { user } = useUser(); +export function getServerSideProps(context: GetServerSidePropsContext) { + return { + props: { redirectPath: context.query.redirect ?? null }, + }; +} + +const SignIn = ({ redirectPath }: { redirectPath?: string }) => { + const { refreshUser } = useUser(); 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 refresh token is still valid, the user will be redirected to the home page. - if (user) { - router.replace(redirectPath); - return ; - } + // If the refresh token is still valid, the user will be redirected to the last page. + useEffect(() => { + refreshUser().then((user) => { + if (user) { + router.replace(redirectPath ?? "/upload"); + } else { + setIsLoading(false); + } + }); + }, []); + + if (isLoading) return ; return ( <> - + ); }; diff --git a/frontend/src/types/config.type.ts b/frontend/src/types/config.type.ts index 44fa3ff9e..12bc935ee 100644 --- a/frontend/src/types/config.type.ts +++ b/frontend/src/types/config.type.ts @@ -29,4 +29,9 @@ export type AdminConfigGroupedByCategory = { ]; }; +export type ConfigHook = { + configVariables: Config[]; + refresh: () => void; +}; + export default Config; diff --git a/frontend/src/types/user.type.ts b/frontend/src/types/user.type.ts index dcf8ad982..1761d4a23 100644 --- a/frontend/src/types/user.type.ts +++ b/frontend/src/types/user.type.ts @@ -29,7 +29,7 @@ export type CurrentUser = User & {}; export type UserHook = { user: CurrentUser | null; - setUser: (user: CurrentUser | null) => void; + refreshUser: () => Promise; }; export default User;