This commit is contained in:
Kilian Hofmann 2024-07-29 03:21:29 +02:00
parent 2008888a16
commit 9deff439d7
32 changed files with 448 additions and 236 deletions

View File

@ -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

File diff suppressed because one or more lines are too long

2
exam/dist/assets/i18n-W-kxdzA-.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

5
exam/dist/assets/index-CzVZnOtD.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

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

View File

@ -1 +0,0 @@
import"./react-C_FdcE2X.js";

17
exam/dist/assets/zustand-DAXCIHlT.js vendored Normal file
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};

View File

@ -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>

View File

@ -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"
}

View File

@ -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"
}

File diff suppressed because one or more lines are too long

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": {

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

View File

@ -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"
}

View File

@ -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
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;

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>();

View 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;
/*
*/

View File

@ -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);

View File

@ -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 } });
},

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 />

View 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;

View File

@ -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" />

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,
});

View File

@ -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>

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>

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>
);
};

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}>

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',
}
)
)
);