Theming
This commit is contained in:
parent
2008888a16
commit
9deff439d7
@ -6,6 +6,9 @@
|
||||
|
||||
## Tabelle `egb_benutzer`
|
||||
- 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 `benutzername`: 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
|
||||
|
||||
## HTACCESS
|
||||
|
||||
### `.htaccess`
|
||||
- RewriteBase anpassen
|
||||
|
||||
### `react/public/.htaccess`
|
||||
- RewriteBase anpassen
|
||||
- **WICHTIG:** React Projekt neu bauen damit die Datei an den korrekten Platz kopiert wird
|
||||
|
||||
## PHP
|
||||
|
||||
### `classes/Models/User.php`
|
||||
@ -28,6 +40,7 @@
|
||||
- Alle Pfade
|
||||
|
||||
## JS
|
||||
**WICHTIG:** Nach allen Änderungen muss das React Projekt neu gebaut werden
|
||||
|
||||
### `react/vite.config.ts`
|
||||
- `base` Pfad
|
||||
|
||||
2
exam/dist/assets/i18n-DyW0LrNj.js
vendored
2
exam/dist/assets/i18n-DyW0LrNj.js
vendored
File diff suppressed because one or more lines are too long
2
exam/dist/assets/i18n-W-kxdzA-.js
vendored
Normal file
2
exam/dist/assets/i18n-W-kxdzA-.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
exam/dist/assets/index-CB6IjOSm.js
vendored
1
exam/dist/assets/index-CB6IjOSm.js
vendored
File diff suppressed because one or more lines are too long
5
exam/dist/assets/index-CzVZnOtD.js
vendored
Normal file
5
exam/dist/assets/index-CzVZnOtD.js
vendored
Normal file
File diff suppressed because one or more lines are too long
178
exam/dist/assets/mui-53GMXZgr.js
vendored
178
exam/dist/assets/mui-53GMXZgr.js
vendored
File diff suppressed because one or more lines are too long
178
exam/dist/assets/mui-aBip8mmu.js
vendored
Normal file
178
exam/dist/assets/mui-aBip8mmu.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
exam/dist/assets/zustand-DA8f69qW.js
vendored
1
exam/dist/assets/zustand-DA8f69qW.js
vendored
@ -1 +0,0 @@
|
||||
import"./react-C_FdcE2X.js";
|
||||
17
exam/dist/assets/zustand-DAXCIHlT.js
vendored
Normal file
17
exam/dist/assets/zustand-DAXCIHlT.js
vendored
Normal 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};
|
||||
9
exam/dist/index.html
vendored
9
exam/dist/index.html
vendored
@ -5,11 +5,12 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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/mui-53GMXZgr.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-ZGp-Rrdw.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-DyW0LrNj.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-aBip8mmu.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-DojtBDN6.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/index-D83Ey19k.css">
|
||||
</head>
|
||||
|
||||
6
exam/dist/locales/de/translation.json
vendored
6
exam/dist/locales/de/translation.json
vendored
@ -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"
|
||||
}
|
||||
|
||||
6
exam/dist/locales/en/translation.json
vendored
6
exam/dist/locales/en/translation.json
vendored
@ -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"
|
||||
}
|
||||
|
||||
2
exam/dist/stats.html
vendored
2
exam/dist/stats.html
vendored
File diff suppressed because one or more lines are too long
@ -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
exam/react/pnpm-lock.yaml
generated
15
exam/react/pnpm-lock.yaml
generated
@ -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
exam/react/src/App.tsx
Normal file
28
exam/react/src/App.tsx
Normal 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;
|
||||
@ -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>();
|
||||
|
||||
|
||||
34
exam/react/src/components/Footer/Footer.tsx
Normal file
34
exam/react/src/components/Footer/Footer.tsx
Normal file
@ -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 } });
|
||||
},
|
||||
|
||||
@ -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 />
|
||||
|
||||
88
exam/react/src/components/Menus/Theme/ThemeMenu.tsx
Normal file
88
exam/react/src/components/Menus/Theme/ThemeMenu.tsx
Normal file
@ -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" />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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',
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user