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
+13
View File
@@ -6,6 +6,9 @@
## Tabelle `egb_benutzer` ## Tabelle `egb_benutzer`
- Neue Spalten `token` (Auth token): VarChar(36), Nullable, UNIQUE Constraint - Neue Spalten `token` (Auth token): VarChar(36), Nullable, UNIQUE Constraint
- Neue Spalten `tokenExpiry` (Auth token verfall): DateTime, Nullable
- Neue Spalten `refreshToken` (Auth refresh token): VarChar(36), Nullable, UNIQUE Constraint
- Neue Spalten `refreshExpiry` (Auth refresh token verfall): VarChar(36), DateTime
- Abänderung der Spalte `zeitstempel`: Entfernen des `ON UPDATE` (da sonst die Mitgliedszeit beim Ändern der Daten sich ändert) - Abänderung der Spalte `zeitstempel`: Entfernen des `ON UPDATE` (da sonst die Mitgliedszeit beim Ändern der Daten sich ändert)
- Abänderung der Spalte `benutzername`: Non-Nullable gemacht, UNIQUE Constraint - Abänderung der Spalte `benutzername`: Non-Nullable gemacht, UNIQUE Constraint
- Abänderung der Spalte `email`: Non-Nullable gemacht, UNIQUE Constraint - Abänderung der Spalte `email`: Non-Nullable gemacht, UNIQUE Constraint
@@ -19,6 +22,15 @@
# Notwendige Anpassung für die Verzeichnisstruktur eines anderen Hosters # Notwendige Anpassung für die Verzeichnisstruktur eines anderen Hosters
## HTACCESS
### `.htaccess`
- RewriteBase anpassen
### `react/public/.htaccess`
- RewriteBase anpassen
- **WICHTIG:** React Projekt neu bauen damit die Datei an den korrekten Platz kopiert wird
## PHP ## PHP
### `classes/Models/User.php` ### `classes/Models/User.php`
@@ -28,6 +40,7 @@
- Alle Pfade - Alle Pfade
## JS ## JS
**WICHTIG:** Nach allen Änderungen muss das React Projekt neu gebaut werden
### `react/vite.config.ts` ### `react/vite.config.ts`
- `base` Pfad - `base` Pfad
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-178
View File
File diff suppressed because one or more lines are too long
+178
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
import"./react-C_FdcE2X.js";
+17
View File
@@ -0,0 +1,17 @@
import{r as g,d as R,a as j}from"./react-C_FdcE2X.js";var V={BASE_URL:"/phpCourse/exam/dist",MODE:"production",DEV:!1,PROD:!0,SSR:!1};const y=t=>{let e;const n=new Set,o=(s,d)=>{const c=typeof s=="function"?s(e):s;if(!Object.is(c,e)){const i=e;e=d??(typeof c!="object"||c===null)?c:Object.assign({},e,c),n.forEach(a=>a(e,i))}},r=()=>e,S={setState:o,getState:r,getInitialState:()=>v,subscribe:s=>(n.add(s),()=>n.delete(s)),destroy:()=>{(V?"production":void 0)!=="production"&&console.warn("[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected."),n.clear()}},v=e=t(o,r,S);return S},$=t=>t?y(t):y;var w={exports:{}},b={},D={exports:{}},_={};/**
* @license React
* use-sync-external-store-shim.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/var f=g;function I(t,e){return t===e&&(t!==0||1/t===1/e)||t!==t&&e!==e}var A=typeof Object.is=="function"?Object.is:I,C=f.useState,P=f.useEffect,T=f.useLayoutEffect,W=f.useDebugValue;function q(t,e){var n=e(),o=C({inst:{value:n,getSnapshot:e}}),r=o[0].inst,u=o[1];return T(function(){r.value=n,r.getSnapshot=e,m(r)&&u({inst:r})},[t,n,e]),P(function(){return m(r)&&u({inst:r}),t(function(){m(r)&&u({inst:r})})},[t]),W(n),n}function m(t){var e=t.getSnapshot;t=t.value;try{var n=e();return!A(t,n)}catch{return!0}}function z(t,e){return e()}var B=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?z:q;_.useSyncExternalStore=f.useSyncExternalStore!==void 0?f.useSyncExternalStore:B;D.exports=_;var F=D.exports;/**
* @license React
* use-sync-external-store-shim/with-selector.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/var E=g,L=F;function M(t,e){return t===e&&(t!==0||1/t===1/e)||t!==t&&e!==e}var U=typeof Object.is=="function"?Object.is:M,k=L.useSyncExternalStore,G=E.useRef,H=E.useEffect,J=E.useMemo,K=E.useDebugValue;b.useSyncExternalStoreWithSelector=function(t,e,n,o,r){var u=G(null);if(u.current===null){var l={hasValue:!1,value:null};u.current=l}else l=u.current;u=J(function(){function S(i){if(!v){if(v=!0,s=i,i=o(i),r!==void 0&&l.hasValue){var a=l.value;if(r(a,i))return d=a}return d=i}if(a=d,U(s,i))return a;var h=o(i);return r!==void 0&&r(a,h)?a:(s=i,d=h)}var v=!1,s,d,c=n===void 0?null:n;return[function(){return S(e())},c===null?void 0:function(){return S(c())}]},[e,n,o,r]);var p=k(t,u[0],u[1]);return H(function(){l.hasValue=!0,l.value=p},[p]),K(p),p};w.exports=b;var N=w.exports;const Q=R(N);var O={BASE_URL:"/phpCourse/exam/dist",MODE:"production",DEV:!1,PROD:!0,SSR:!1};const{useDebugValue:X}=j,{useSyncExternalStoreWithSelector:Y}=Q;let x=!1;const Z=t=>t;function tt(t,e=Z,n){(O?"production":void 0)!=="production"&&n&&!x&&(console.warn("[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937"),x=!0);const o=Y(t.subscribe,t.getState,t.getServerState||t.getInitialState,e,n);return X(o),o}const et=t=>{(O?"production":void 0)!=="production"&&typeof t!="function"&&console.warn("[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.");const e=typeof t=="function"?$(t):t,n=(o,r)=>tt(e,o,r);return Object.assign(n,e),n},rt=t=>et;export{rt as c};
+5 -4
View File
@@ -5,11 +5,12 @@
<link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>Vite + React + TS</title>
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-CB6IjOSm.js"></script> <script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-CzVZnOtD.js"></script>
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-C_FdcE2X.js"> <link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-C_FdcE2X.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-53GMXZgr.js"> <link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-aBip8mmu.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-ZGp-Rrdw.js"> <link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-DojtBDN6.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-DyW0LrNj.js"> <link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/zustand-DAXCIHlT.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-W-kxdzA-.js">
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/mui-CKDNpdid.css"> <link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/mui-CKDNpdid.css">
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/index-D83Ey19k.css"> <link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/index-D83Ey19k.css">
</head> </head>
+5 -1
View File
@@ -82,5 +82,9 @@
"Register": "Konto anlegen", "Register": "Konto anlegen",
"Confirm header": "Fast geschafft!", "Confirm header": "Fast geschafft!",
"Confirm mail": "Prüfe dein E-Mail Postfach auf eine Bestätigungsmail.", "Confirm mail": "Prüfe dein E-Mail Postfach auf eine Bestätigungsmail.",
"Close": "Schließen" "Close": "Schließen",
"Dark": "Dunkel",
"Light": "Hell",
"System": "System"
} }
+5 -1
View File
@@ -83,5 +83,9 @@
"Register": "Create account", "Register": "Create account",
"Confirm header": "Almost there!", "Confirm header": "Almost there!",
"Confirm mail": "Check your email for a confirmation mail.", "Confirm mail": "Check your email for a confirmation mail.",
"Close": "Close" "Close": "Close",
"Dark": "Dark",
"Light": "Light",
"System": "System"
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -24,7 +24,6 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-i18next": "^15.0.0", "react-i18next": "^15.0.0",
"use-local-storage-state": "^19.3.1",
"zustand": "^4.5.4" "zustand": "^4.5.4"
}, },
"devDependencies": { "devDependencies": {
-15
View File
@@ -50,9 +50,6 @@ importers:
react-i18next: react-i18next:
specifier: ^15.0.0 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) 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: zustand:
specifier: ^4.5.4 specifier: ^4.5.4
version: 4.5.4(@types/react@18.3.3)(react@18.3.1) version: 4.5.4(@types/react@18.3.3)(react@18.3.1)
@@ -1845,13 +1842,6 @@ packages:
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 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: use-sync-external-store@1.2.0:
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies: peerDependencies:
@@ -3731,11 +3721,6 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 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): use-sync-external-store@1.2.0(react@18.3.1):
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
@@ -82,5 +82,9 @@
"Register": "Konto anlegen", "Register": "Konto anlegen",
"Confirm header": "Fast geschafft!", "Confirm header": "Fast geschafft!",
"Confirm mail": "Prüfe dein E-Mail Postfach auf eine Bestätigungsmail.", "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", "Register": "Create account",
"Confirm header": "Almost there!", "Confirm header": "Almost there!",
"Confirm mail": "Check your email for a confirmation mail.", "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 { 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 { POST_LIMIT, PROFILE_POST_LIMIT } from '../constanst';
import useGuestBookStore from '../store/store';
import { PostAuth, PostCreate, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post'; import { PostAuth, PostCreate, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post';
import { User, UserCreate, UserImageUpdate, UserUpdate } from '../types/User'; import { User, UserCreate, UserImageUpdate, UserUpdate } from '../types/User';
@@ -94,10 +94,11 @@ export const useApi = () => {
export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ children }) => { export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ children }) => {
const [hasAuth, setHasAuth] = useState(false); const [hasAuth, setHasAuth] = useState(false);
const [authenticatedUser, setAuthenticatedUser] = useState<User>(); const [authenticatedUser, setAuthenticatedUser] = useState<User>();
const [currentSession, setCurrentSession] = useLocalStorageState<[string | undefined, string | undefined]>(
'egb_session', const [currentSession, setCurrentSession] = useGuestBookStore((state) => [
{ defaultValue: [undefined, undefined] } state.currentSession,
); state.setCurrentSession,
]);
const token = useRef<string | undefined>(); 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 { Login } from '../../../types/User';
import ErrorComponent from '../../Error/ErrorComponent'; import ErrorComponent from '../../Error/ErrorComponent';
interface Props { const LoginForm: FC = () => {
handleClose: () => void;
}
const LoginForm: FC<Props> = ({ handleClose }) => {
const [error, setError] = useState(); const [error, setError] = useState();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -27,7 +23,6 @@ const LoginForm: FC<Props> = ({ handleClose }) => {
try { try {
await Api.logIn(value.email, value.password); await Api.logIn(value.email, value.password);
router.invalidate(); router.invalidate();
handleClose();
//eslint-disable-next-line @typescript-eslint/no-explicit-any //eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (_error: any) { } catch (_error: any) {
setError(_error); setError(_error);
@@ -35,6 +35,7 @@ const PostForm: FC = () => {
{ {
onSuccess: async (data) => { onSuccess: async (data) => {
form.reset(); form.reset();
setCharacterCount(0);
await queryClient.invalidateQueries({ queryKey: ['posts'] }); await queryClient.invalidateQueries({ queryKey: ['posts'] });
navigate({ to: '/', search: { page: data.pages - 1 } }); 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 { import {
AppBar, AppBar,
Avatar, Avatar,
@@ -13,7 +13,9 @@ import { Link, useRouterState } from '@tanstack/react-router';
import { cloneElement, FC, ReactElement, useState } from 'react'; import { cloneElement, FC, ReactElement, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useApi } from '../../api/Api'; import { useApi } from '../../api/Api';
import useGuestBookStore from '../../store/store';
import LanguageMenu from '../Menus/Language/LanguageMenu'; import LanguageMenu from '../Menus/Language/LanguageMenu';
import ThemeMenu from '../Menus/Theme/ThemeMenu';
import UserMenu from '../Menus/User/UserMenu'; import UserMenu from '../Menus/User/UserMenu';
const ElevationScroll = ({ children }: { children: ReactElement }) => { const ElevationScroll = ({ children }: { children: ReactElement }) => {
@@ -30,14 +32,17 @@ const ElevationScroll = ({ children }: { children: ReactElement }) => {
const Header: FC = () => { const Header: FC = () => {
const [anchorUserMenu, setAnchorUserMenu] = useState<null | HTMLElement>(null); const [anchorUserMenu, setAnchorUserMenu] = useState<null | HTMLElement>(null);
const [anchorLanguageMenu, setAnchorLanguageMenu] = useState<null | HTMLElement>(null); const [anchorLanguageMenu, setAnchorLanguageMenu] = useState<null | HTMLElement>(null);
const [anchorThemeMenu, setAnchorThemeMenu] = useState<null | HTMLElement>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const isLoading = useRouterState({ select: (s) => s.status === 'pending' }); const isLoading = useRouterState({ select: (s) => s.status === 'pending' });
const Api = useApi(); const Api = useApi();
const [theme] = useGuestBookStore((state) => [state.theme, state.setTheme]);
const handleClose = () => { const handleClose = () => {
setAnchorLanguageMenu(null);
setAnchorUserMenu(null); setAnchorUserMenu(null);
setAnchorLanguageMenu(null);
setAnchorThemeMenu(null);
}; };
return ( return (
@@ -56,6 +61,11 @@ const Header: FC = () => {
<IconButton size="large" onClick={(event) => setAnchorLanguageMenu(event.currentTarget)}> <IconButton size="large" onClick={(event) => setAnchorLanguageMenu(event.currentTarget)}>
<Translate sx={{ color: 'white' }} /> <Translate sx={{ color: 'white' }} />
</IconButton> </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 ? ( {Api.authenticatedUser ? (
<IconButton onClick={(event) => setAnchorUserMenu(event.currentTarget)} sx={{ p: 0 }}> <IconButton onClick={(event) => setAnchorUserMenu(event.currentTarget)} sx={{ p: 0 }}>
<Avatar alt={Api.authenticatedUser.username} src={`${Api.authenticatedUser.image}`}> <Avatar alt={Api.authenticatedUser.username} src={`${Api.authenticatedUser.image}`}>
@@ -70,6 +80,7 @@ const Header: FC = () => {
</Box> </Box>
<LanguageMenu anchorEl={anchorLanguageMenu} handleClose={handleClose} /> <LanguageMenu anchorEl={anchorLanguageMenu} handleClose={handleClose} />
<UserMenu anchorEl={anchorUserMenu} handleClose={handleClose} /> <UserMenu anchorEl={anchorUserMenu} handleClose={handleClose} />
<ThemeMenu anchorEl={anchorThemeMenu} handleClose={handleClose} />
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<Toolbar /> <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 () => { onClick={async () => {
await Api.logOut(); await Api.logOut();
router.invalidate(); router.invalidate();
_handleClose(); navigate({ to: ROUTES.INDEX });
}} }}
> >
{t('Log out')} {t('Log out')}
@@ -74,7 +74,7 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
<RegisterDialog open={register} onClose={() => setRegister(false)} /> <RegisterDialog open={register} onClose={() => setRegister(false)} />
) : ( ) : (
<Box> <Box>
<LoginForm handleClose={_handleClose} /> <LoginForm />
<Box sx={{ padding: 1 }}> <Box sx={{ padding: 1 }}>
<Trans i18nKey="Register prompt"> <Trans i18nKey="Register prompt">
<Typography component="span" /> <Typography component="span" />
+3 -2
View File
@@ -27,11 +27,12 @@ import PostEditDialog from '../Dialogs/PostEdit/PostEditDialog';
import ErrorComponent from '../Error/ErrorComponent'; import ErrorComponent from '../Error/ErrorComponent';
interface Props { interface Props {
page?: number;
post: PostNonAuth | PostAuth; post: PostNonAuth | PostAuth;
disableActions?: boolean; disableActions?: boolean;
} }
const Post: FC<Props> = ({ post, disableActions }) => { const Post: FC<Props> = ({ page = 0, post, disableActions }) => {
const [deleteOpen, setDeleteOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false); const [editOpen, setEditOpen] = useState(false);
//eslint-disable-next-line @typescript-eslint/no-explicit-any //eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -128,7 +129,7 @@ const Post: FC<Props> = ({ post, disableActions }) => {
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ['posts'], queryKey: ['posts'],
}); });
navigate({ to: '/', search: { page: data.pages - 1 } }); if (page >= data.pages) navigate({ to: '/', search: { page: data.pages - 1 } });
}, },
onError: setError, onError: setError,
}); });
@@ -33,7 +33,7 @@ const Profile: FC<Props> = ({ user, posts, canEdit }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Grid container sx={{ justifyContent: 'center' }} spacing={2}> <Grid container sx={{ justifyContent: 'center', marginTop: 0 }} spacing={2}>
<Grid item> <Grid item>
<Card> <Card>
<CardContent> <CardContent>
+2 -2
View File
@@ -3,7 +3,6 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import ApiProvider from './api/Api'; import ApiProvider from './api/Api';
import Router from './router';
// Import i18n // Import i18n
import './i18n'; import './i18n';
@@ -13,6 +12,7 @@ import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css'; import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css'; import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css'; import '@fontsource/roboto/700.css';
import App from './App';
// Query Client // Query Client
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -25,7 +25,7 @@ if (!rootElement.innerHTML) {
<StrictMode> <StrictMode>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ApiProvider> <ApiProvider>
<Router /> <App />
</ApiProvider> </ApiProvider>
{process.env.NODE_ENV === 'development' && <ReactQueryDevtools initialIsOpen={false} />} {process.env.NODE_ENV === 'development' && <ReactQueryDevtools initialIsOpen={false} />}
</QueryClientProvider> </QueryClientProvider>
+5 -2
View File
@@ -4,19 +4,22 @@ import { createRootRouteWithContext, ErrorRouteComponent, Outlet, useRouter } fr
import { TanStackRouterDevtools } from '@tanstack/router-devtools'; import { TanStackRouterDevtools } from '@tanstack/router-devtools';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useApi } from '../api/Api'; import { useApi } from '../api/Api';
import Footer from '../components/Footer/Footer';
import Header from '../components/Header/Header'; import Header from '../components/Header/Header';
const Root = () => { const Root = () => {
return ( return (
<> <Box sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<Header /> <Header />
<Box sx={{ display: 'flex', justifyContent: 'center' }}> <Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Box sx={{ maxWidth: '800px', flexGrow: 1 }}> <Box sx={{ maxWidth: '800px', flexGrow: 1 }}>
<Outlet /> <Outlet />
</Box> </Box>
</Box> </Box>
<Box sx={{ flexGrow: 1 }} />
<Footer />
{process.env.NODE_ENV === 'development' && <TanStackRouterDevtools />} {process.env.NODE_ENV === 'development' && <TanStackRouterDevtools />}
</> </Box>
); );
}; };
+2 -2
View File
@@ -36,10 +36,10 @@ const Home = () => {
return ( return (
<> <>
<Snackbar open={isFetching} message={t('Updating')} /> <Snackbar open={isFetching} message={t('Updating')} />
<Grid container spacing={2}> <Grid container spacing={2} sx={{ marginTop: 0 }}>
{postsQuery.data.map((post) => ( {postsQuery.data.map((post) => (
<Grid item xs={12} key={post.id}> <Grid item xs={12} key={post.id}>
<Post post={post} /> <Post page={page} post={post} />
</Grid> </Grid>
))} ))}
<Grid item xs={12}> <Grid item xs={12}>
+20 -4
View File
@@ -2,13 +2,29 @@ import type {} from '@redux-devtools/extension';
import { create } from 'zustand'; import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware'; 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>()( const useGuestBookStore = create<GuestBookState>()(
devtools( devtools(
persist(() => ({}), { persist(
name: 'guestbook-storage', (set) => ({
}) theme: undefined,
currentSession: [undefined, undefined],
setTheme: (theme: GuestBookState['theme']) => set(() => ({ theme })),
setCurrentSession: (session: GuestBookState['currentSession']) =>
set(() => ({
currentSession: session,
})),
}),
{
name: 'guestbook-storage',
}
)
) )
); );