This commit is contained in:
2024-07-29 03:21:29 +02:00
parent 2008888a16
commit 9deff439d7
32 changed files with 448 additions and 236 deletions
-1
View File
@@ -24,7 +24,6 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.0.0",
"use-local-storage-state": "^19.3.1",
"zustand": "^4.5.4"
},
"devDependencies": {
-15
View File
@@ -50,9 +50,6 @@ importers:
react-i18next:
specifier: ^15.0.0
version: 15.0.0(i18next@23.12.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
use-local-storage-state:
specifier: ^19.3.1
version: 19.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
zustand:
specifier: ^4.5.4
version: 4.5.4(@types/react@18.3.3)(react@18.3.1)
@@ -1845,13 +1842,6 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
use-local-storage-state@19.3.1:
resolution: {integrity: sha512-y3Z1dODXvZXZB4qtLDNN8iuXbsYD6TAxz61K58GWB9/yKwrNG9ynI0GzCTHi/Je1rMiyOwMimz0oyFsZn+Kj7Q==}
engines: {node: '>=14'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
use-sync-external-store@1.2.0:
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies:
@@ -3731,11 +3721,6 @@ snapshots:
dependencies:
punycode: 2.3.1
use-local-storage-state@19.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
use-sync-external-store@1.2.0(react@18.3.1):
dependencies:
react: 18.3.1
@@ -82,5 +82,9 @@
"Register": "Konto anlegen",
"Confirm header": "Fast geschafft!",
"Confirm mail": "Prüfe dein E-Mail Postfach auf eine Bestätigungsmail.",
"Close": "Schließen"
"Close": "Schließen",
"Dark": "Dunkel",
"Light": "Hell",
"System": "System"
}
@@ -83,5 +83,9 @@
"Register": "Create account",
"Confirm header": "Almost there!",
"Confirm mail": "Check your email for a confirmation mail.",
"Close": "Close"
"Close": "Close",
"Dark": "Dark",
"Light": "Light",
"System": "System"
}
+28
View File
@@ -0,0 +1,28 @@
import { createTheme, CssBaseline, ThemeProvider, useMediaQuery } from '@mui/material';
import { FC, useMemo } from 'react';
import Router from './router';
import useGuestBookStore from './store/store';
const App: FC = () => {
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
const theme = useGuestBookStore((state) => state.theme);
const themePreset = useMemo(
() =>
createTheme({
palette: {
mode: theme ?? (prefersDarkMode ? 'dark' : 'light'),
},
}),
[theme, prefersDarkMode]
);
return (
<ThemeProvider theme={themePreset}>
<CssBaseline />
<Router />
</ThemeProvider>
);
};
export default App;
+6 -5
View File
@@ -1,6 +1,6 @@
import { createContext, FC, PropsWithChildren, useContext, useEffect, useRef, useState } from 'react';
import useLocalStorageState from 'use-local-storage-state';
import { POST_LIMIT, PROFILE_POST_LIMIT } from '../constanst';
import useGuestBookStore from '../store/store';
import { PostAuth, PostCreate, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post';
import { User, UserCreate, UserImageUpdate, UserUpdate } from '../types/User';
@@ -94,10 +94,11 @@ export const useApi = () => {
export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ children }) => {
const [hasAuth, setHasAuth] = useState(false);
const [authenticatedUser, setAuthenticatedUser] = useState<User>();
const [currentSession, setCurrentSession] = useLocalStorageState<[string | undefined, string | undefined]>(
'egb_session',
{ defaultValue: [undefined, undefined] }
);
const [currentSession, setCurrentSession] = useGuestBookStore((state) => [
state.currentSession,
state.setCurrentSession,
]);
const token = useRef<string | undefined>();
@@ -0,0 +1,34 @@
import { Box, Divider, Grid, Typography } from '@mui/material';
import { FC } from 'react';
const Footer: FC = () => {
return (
<Box
sx={{
marginTop: 2,
display: 'flex',
justifyContent: 'center',
}}
>
<Box sx={{ maxWidth: '800px', flexGrow: 1 }}>
<Grid container spacing={2}>
<Grid item xs={12} sx={{ height: '50px' }} />
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
<Typography variant="caption">© 2024 Kilian Kurt Hofmann</Typography>
</Grid>
</Grid>
</Box>
</Box>
);
};
export default Footer;
/*
*/
@@ -7,11 +7,7 @@ import { useApi } from '../../../api/Api';
import { Login } from '../../../types/User';
import ErrorComponent from '../../Error/ErrorComponent';
interface Props {
handleClose: () => void;
}
const LoginForm: FC<Props> = ({ handleClose }) => {
const LoginForm: FC = () => {
const [error, setError] = useState();
const { t } = useTranslation();
@@ -27,7 +23,6 @@ const LoginForm: FC<Props> = ({ handleClose }) => {
try {
await Api.logIn(value.email, value.password);
router.invalidate();
handleClose();
//eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (_error: any) {
setError(_error);
@@ -35,6 +35,7 @@ const PostForm: FC = () => {
{
onSuccess: async (data) => {
form.reset();
setCharacterCount(0);
await queryClient.invalidateQueries({ queryKey: ['posts'] });
navigate({ to: '/', search: { page: data.pages - 1 } });
},
+13 -2
View File
@@ -1,4 +1,4 @@
import { AccountCircle, Person, Translate } from '@mui/icons-material';
import { AccountCircle, DarkModeOutlined, LightMode, Person, SettingsBrightness, Translate } from '@mui/icons-material';
import {
AppBar,
Avatar,
@@ -13,7 +13,9 @@ import { Link, useRouterState } from '@tanstack/react-router';
import { cloneElement, FC, ReactElement, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useApi } from '../../api/Api';
import useGuestBookStore from '../../store/store';
import LanguageMenu from '../Menus/Language/LanguageMenu';
import ThemeMenu from '../Menus/Theme/ThemeMenu';
import UserMenu from '../Menus/User/UserMenu';
const ElevationScroll = ({ children }: { children: ReactElement }) => {
@@ -30,14 +32,17 @@ const ElevationScroll = ({ children }: { children: ReactElement }) => {
const Header: FC = () => {
const [anchorUserMenu, setAnchorUserMenu] = useState<null | HTMLElement>(null);
const [anchorLanguageMenu, setAnchorLanguageMenu] = useState<null | HTMLElement>(null);
const [anchorThemeMenu, setAnchorThemeMenu] = useState<null | HTMLElement>(null);
const { t } = useTranslation();
const isLoading = useRouterState({ select: (s) => s.status === 'pending' });
const Api = useApi();
const [theme] = useGuestBookStore((state) => [state.theme, state.setTheme]);
const handleClose = () => {
setAnchorLanguageMenu(null);
setAnchorUserMenu(null);
setAnchorLanguageMenu(null);
setAnchorThemeMenu(null);
};
return (
@@ -56,6 +61,11 @@ const Header: FC = () => {
<IconButton size="large" onClick={(event) => setAnchorLanguageMenu(event.currentTarget)}>
<Translate sx={{ color: 'white' }} />
</IconButton>
<IconButton size="large" onClick={(event) => setAnchorThemeMenu(event.currentTarget)}>
{theme === 'dark' && <DarkModeOutlined />}
{theme === 'light' && <LightMode sx={{ color: 'white' }} />}
{!theme && <SettingsBrightness sx={{ color: 'white' }} />}
</IconButton>
{Api.authenticatedUser ? (
<IconButton onClick={(event) => setAnchorUserMenu(event.currentTarget)} sx={{ p: 0 }}>
<Avatar alt={Api.authenticatedUser.username} src={`${Api.authenticatedUser.image}`}>
@@ -70,6 +80,7 @@ const Header: FC = () => {
</Box>
<LanguageMenu anchorEl={anchorLanguageMenu} handleClose={handleClose} />
<UserMenu anchorEl={anchorUserMenu} handleClose={handleClose} />
<ThemeMenu anchorEl={anchorThemeMenu} handleClose={handleClose} />
</Toolbar>
</AppBar>
<Toolbar />
@@ -0,0 +1,88 @@
import { DarkModeOutlined, LightMode, SettingsBrightness } from '@mui/icons-material';
import { Grid, Menu, MenuItem, Typography } from '@mui/material';
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import useGuestBookStore from '../../../store/store';
interface Props {
anchorEl: HTMLElement | null;
handleClose: () => void;
}
const ThemeMenu: FC<Props> = ({ anchorEl, handleClose }) => {
const [theme, setTheme] = useGuestBookStore((state) => [state.theme, state.setTheme]);
const { t } = useTranslation();
return (
<Menu
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={Boolean(anchorEl)}
onClose={handleClose}
sx={{
'& .MuiMenu-paper': {
minWidth: '240px',
},
}}
>
<MenuItem
key="dark"
selected={theme === 'dark'}
onClick={() => {
setTheme('dark');
}}
>
<Grid container spacing={2}>
<Grid item xs={2}>
<DarkModeOutlined />
</Grid>
<Grid item xs={10}>
<Typography>{t('Dark')}</Typography>
</Grid>
</Grid>
</MenuItem>
<MenuItem
key="light"
selected={theme === 'light'}
onClick={() => {
setTheme('light');
}}
>
<Grid container spacing={2}>
<Grid item xs={2}>
<LightMode />
</Grid>
<Grid item xs={10}>
<Typography>{t('Light')}</Typography>
</Grid>
</Grid>
</MenuItem>
<MenuItem
key="system"
selected={!theme}
onClick={() => {
setTheme(undefined);
}}
>
<Grid container spacing={2}>
<Grid item xs={2}>
<SettingsBrightness />
</Grid>
<Grid item xs={10}>
<Typography>{t('System')}</Typography>
</Grid>
</Grid>
</MenuItem>
</Menu>
);
};
export default ThemeMenu;
@@ -64,7 +64,7 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
onClick={async () => {
await Api.logOut();
router.invalidate();
_handleClose();
navigate({ to: ROUTES.INDEX });
}}
>
{t('Log out')}
@@ -74,7 +74,7 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
<RegisterDialog open={register} onClose={() => setRegister(false)} />
) : (
<Box>
<LoginForm handleClose={_handleClose} />
<LoginForm />
<Box sx={{ padding: 1 }}>
<Trans i18nKey="Register prompt">
<Typography component="span" />
+3 -2
View File
@@ -27,11 +27,12 @@ import PostEditDialog from '../Dialogs/PostEdit/PostEditDialog';
import ErrorComponent from '../Error/ErrorComponent';
interface Props {
page?: number;
post: PostNonAuth | PostAuth;
disableActions?: boolean;
}
const Post: FC<Props> = ({ post, disableActions }) => {
const Post: FC<Props> = ({ page = 0, post, disableActions }) => {
const [deleteOpen, setDeleteOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false);
//eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -128,7 +129,7 @@ const Post: FC<Props> = ({ post, disableActions }) => {
await queryClient.invalidateQueries({
queryKey: ['posts'],
});
navigate({ to: '/', search: { page: data.pages - 1 } });
if (page >= data.pages) navigate({ to: '/', search: { page: data.pages - 1 } });
},
onError: setError,
});
@@ -33,7 +33,7 @@ const Profile: FC<Props> = ({ user, posts, canEdit }) => {
const { t } = useTranslation();
return (
<Grid container sx={{ justifyContent: 'center' }} spacing={2}>
<Grid container sx={{ justifyContent: 'center', marginTop: 0 }} spacing={2}>
<Grid item>
<Card>
<CardContent>
+2 -2
View File
@@ -3,7 +3,6 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import ApiProvider from './api/Api';
import Router from './router';
// Import i18n
import './i18n';
@@ -13,6 +12,7 @@ import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import App from './App';
// Query Client
const queryClient = new QueryClient();
@@ -25,7 +25,7 @@ if (!rootElement.innerHTML) {
<StrictMode>
<QueryClientProvider client={queryClient}>
<ApiProvider>
<Router />
<App />
</ApiProvider>
{process.env.NODE_ENV === 'development' && <ReactQueryDevtools initialIsOpen={false} />}
</QueryClientProvider>
+5 -2
View File
@@ -4,19 +4,22 @@ import { createRootRouteWithContext, ErrorRouteComponent, Outlet, useRouter } fr
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
import { useEffect } from 'react';
import { useApi } from '../api/Api';
import Footer from '../components/Footer/Footer';
import Header from '../components/Header/Header';
const Root = () => {
return (
<>
<Box sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<Header />
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Box sx={{ maxWidth: '800px', flexGrow: 1 }}>
<Outlet />
</Box>
</Box>
<Box sx={{ flexGrow: 1 }} />
<Footer />
{process.env.NODE_ENV === 'development' && <TanStackRouterDevtools />}
</>
</Box>
);
};
+2 -2
View File
@@ -36,10 +36,10 @@ const Home = () => {
return (
<>
<Snackbar open={isFetching} message={t('Updating')} />
<Grid container spacing={2}>
<Grid container spacing={2} sx={{ marginTop: 0 }}>
{postsQuery.data.map((post) => (
<Grid item xs={12} key={post.id}>
<Post post={post} />
<Post page={page} post={post} />
</Grid>
))}
<Grid item xs={12}>
+20 -4
View File
@@ -2,13 +2,29 @@ import type {} from '@redux-devtools/extension';
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
interface GuestBookState {}
interface GuestBookState {
theme: 'dark' | 'light' | undefined;
currentSession: [string | undefined, string | undefined];
setTheme: (theme: GuestBookState['theme']) => void;
setCurrentSession: (session: GuestBookState['currentSession']) => void;
}
const useGuestBookStore = create<GuestBookState>()(
devtools(
persist(() => ({}), {
name: 'guestbook-storage',
})
persist(
(set) => ({
theme: undefined,
currentSession: [undefined, undefined],
setTheme: (theme: GuestBookState['theme']) => set(() => ({ theme })),
setCurrentSession: (session: GuestBookState['currentSession']) =>
set(() => ({
currentSession: session,
})),
}),
{
name: 'guestbook-storage',
}
)
)
);