diff --git a/client/src/app/App.module.scss b/client/src/app/App.module.scss new file mode 100644 index 00000000..d24c2b4f --- /dev/null +++ b/client/src/app/App.module.scss @@ -0,0 +1,3 @@ +.dark { + background-color: #121212; +} diff --git a/client/src/app/App.scss b/client/src/app/App.scss index a191d3b9..95341d62 100644 --- a/client/src/app/App.scss +++ b/client/src/app/App.scss @@ -2,6 +2,10 @@ box-sizing: border-box; } +.dark { + background-color: #121212; +} + body { margin: 0; } diff --git a/client/src/app/App.tsx b/client/src/app/App.tsx index 497d6f08..bfcb046b 100644 --- a/client/src/app/App.tsx +++ b/client/src/app/App.tsx @@ -1,5 +1,6 @@ import React from "react"; import "./App.scss"; +import styles from "./App.module.scss"; import { createBrowserRouter, createRoutesFromElements, @@ -45,6 +46,7 @@ import { action as registrationAction, } from "pages/registration"; import { FavoritePage, loader as favoriteLoader } from "pages/favorite"; +import { useDarkMode } from "shared/dark-mode/use-dark-mode"; const routes = createRoutesFromElements( @@ -85,10 +87,14 @@ const routes = createRoutesFromElements( const router = createBrowserRouter(routes); -const App = () => ( -
- -
-); +const App = () => { + const { darkMode } = useDarkMode(); + + return ( +
+ +
+ ); +}; export default App; diff --git a/client/src/index.tsx b/client/src/index.tsx index 204987c1..b0e50e06 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -1,18 +1,16 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import reportWebVitals from "./reportWebVitals"; import App from "./app/App"; +import { ThemeProvider } from "shared/dark-mode/use-dark-mode"; const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement, ); root.render( - - - , + + + + + , + , ); - -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); diff --git a/client/src/pages/catalogue/ui/Catalogue.page.tsx b/client/src/pages/catalogue/ui/Catalogue.page.tsx index 28bff164..b8c9644c 100644 --- a/client/src/pages/catalogue/ui/Catalogue.page.tsx +++ b/client/src/pages/catalogue/ui/Catalogue.page.tsx @@ -11,11 +11,14 @@ import { GetInstrumentsByCriteriaPaginatedApi } from "generated/api"; import { CatalogueLoader } from "pages/catalogue"; import { NavigationBarWidget } from "widgets/catalogue-navbar"; import { SearchBarInputField } from "pages/catalogue/ui/SearchBarInput.field"; +import { useDarkMode } from "shared/dark-mode/use-dark-mode"; const getInstrumentsByCriteriaPaginated = new GetInstrumentsByCriteriaPaginatedApi(); export function CataloguePage() { + const { darkMode } = useDarkMode(); + const loader = useLoaderData() as CatalogueLoader; // https://github.com/remix-run/react-router/discussions/9792 const [instruments, setInstruments] = useState( @@ -57,11 +60,21 @@ export function CataloguePage() { <> -
+
-
+
diff --git a/client/src/pages/catalogue/ui/SearchBarInput.field.tsx b/client/src/pages/catalogue/ui/SearchBarInput.field.tsx index 9ff23483..495ba543 100644 --- a/client/src/pages/catalogue/ui/SearchBarInput.field.tsx +++ b/client/src/pages/catalogue/ui/SearchBarInput.field.tsx @@ -2,6 +2,7 @@ import React from "react"; import styles from "./styles/SearchBarInput.field.module.scss"; import { InstrumentName } from "generated/model"; import { Filters } from "widgets/catalogue-filter"; +import { useDarkMode } from "shared/dark-mode/use-dark-mode"; interface Props { filters: Filters; @@ -9,10 +10,12 @@ interface Props { } export const SearchBarInputField = (props: Props) => { + const { darkMode } = useDarkMode(); + return ( { props.setFilters({ ...props.filters, @@ -21,7 +24,10 @@ export const SearchBarInputField = (props: Props) => { } as InstrumentName, }); }} - className={styles.search_bar__input} + className={` + ${styles.search_bar__input} + ${darkMode ? styles.dark : styles.light} + `} /> ); }; diff --git a/client/src/pages/catalogue/ui/styles/Catalogue.page.module.scss b/client/src/pages/catalogue/ui/styles/Catalogue.page.module.scss index 8e11bdc1..81972ab4 100644 --- a/client/src/pages/catalogue/ui/styles/Catalogue.page.module.scss +++ b/client/src/pages/catalogue/ui/styles/Catalogue.page.module.scss @@ -6,15 +6,28 @@ color: #212529; } +.catalogue__wrapper__dark { + background-color: #000000; +} + +.catalogue__wrapper__light { +} + .catalogue__filters__serp__navbar__wrapper { display: flex; } .catalogue__filters__wrapper { flex: 1; +} +.catalogue__filters__wrapper__light { background-color: #e0e0e0; } +.catalogue__filters__wrapper__dark { + background-color: #212529; +} + .catalogue__serp__navbar__wrapper { flex: 3; display: flex; diff --git a/client/src/pages/catalogue/ui/styles/SearchBarInput.field.module.scss b/client/src/pages/catalogue/ui/styles/SearchBarInput.field.module.scss index 363be26f..b49b59e0 100644 --- a/client/src/pages/catalogue/ui/styles/SearchBarInput.field.module.scss +++ b/client/src/pages/catalogue/ui/styles/SearchBarInput.field.module.scss @@ -6,6 +6,19 @@ border: 1px #dee2e6 solid; } +.light { + background-color: #ffffff; +} + +.dark { + background-color: #333333; + color: #ffd700; + + &::placeholder { + color: #ffd700; + } +} + @media (max-width: vars.$media_threshold) { .search_bar__input { font-size: 1.5em; diff --git a/client/src/pages/edit-instrument/ui/styles/EditInstrument.page.module.css b/client/src/pages/edit-instrument/ui/styles/EditInstrument.page.module.css index 15262eb0..fbe22696 100644 --- a/client/src/pages/edit-instrument/ui/styles/EditInstrument.page.module.css +++ b/client/src/pages/edit-instrument/ui/styles/EditInstrument.page.module.css @@ -30,7 +30,7 @@ flex-basis: 70%; } -.edit_instrument__button { +.btn__light { flex: 1; cursor: pointer; background-color: #28a745; @@ -38,6 +38,6 @@ border: 1px #cccccc solid; } -.edit_instrument__button:hover { +.btn__light:hover { background-color: #007bff; } diff --git a/client/src/pages/profile/ui/Profile.page.tsx b/client/src/pages/profile/ui/Profile.page.tsx index f683dfe5..61f873b4 100644 --- a/client/src/pages/profile/ui/Profile.page.tsx +++ b/client/src/pages/profile/ui/Profile.page.tsx @@ -10,11 +10,13 @@ import Jwt from "domain/model/jwt"; import { LOGIN } from "shared/config/paths"; import { deleteCookie } from "shared/cookie/cookie"; import { COOKIE_JWT_KEY, COOKIE_SESSIONID } from "shared/config/frontend"; +import { useDarkMode } from "shared/dark-mode/use-dark-mode"; const logout = new LogoutApi(); export function ProfilePage() { useJwt(); + const { toggleTheme } = useDarkMode(); const navigate = useNavigate(); const profile = useLoaderData() as ProfileDetails; @@ -52,6 +54,9 @@ export function ProfilePage() {
Role: {profile?.role}
+ diff --git a/client/src/pages/profile/ui/styles/Profile.page.module.css b/client/src/pages/profile/ui/styles/Profile.page.module.css index a6eeb53d..4bc78550 100644 --- a/client/src/pages/profile/ui/styles/Profile.page.module.css +++ b/client/src/pages/profile/ui/styles/Profile.page.module.css @@ -5,6 +5,10 @@ margin: 2em auto; } +.dark_mode__button { + background-color: darkgray; +} + .logout__button { background-color: #ff0000; } diff --git a/client/src/shared/config/frontend.ts b/client/src/shared/config/frontend.ts index e5372e78..3c1f9d88 100644 --- a/client/src/shared/config/frontend.ts +++ b/client/src/shared/config/frontend.ts @@ -4,3 +4,5 @@ export const MINIMAL_PASSWORD_LENGTH = 2; export const COOKIE_SESSIONID = "SESSIONID"; export const COOKIE_JWT_KEY = "jwt"; + +export const DARK_MODE = "muse_dark_mode"; diff --git a/client/src/shared/dark-mode/use-dark-mode.tsx b/client/src/shared/dark-mode/use-dark-mode.tsx new file mode 100644 index 00000000..f0c1d6de --- /dev/null +++ b/client/src/shared/dark-mode/use-dark-mode.tsx @@ -0,0 +1,76 @@ +import { + createContext, + useContext, + useState, + ReactNode, + FC, + useEffect, +} from "react"; +import { DARK_MODE } from "shared/config/frontend"; + +interface ThemeContextProps { + darkMode: boolean; + toggleTheme: () => void; +} + +const ThemeContext = createContext(undefined); + +export const ThemeProvider: FC<{ children: ReactNode }> = ({ children }) => { + const [darkMode, setDarkMode] = useState( + window.localStorage.getItem(DARK_MODE) === "true", + ); + + useEffect(() => { + if (darkMode) { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } + }, [darkMode]); + + const toggleTheme = () => { + setDarkMode((prevMode) => { + const newMode = !prevMode; + window.localStorage.setItem(DARK_MODE, newMode ? "true" : "false"); + document.documentElement.classList.toggle("dark", newMode); + return newMode; + }); + }; + + return ( + + {children} + + ); +}; + +export const useDarkMode = (): ThemeContextProps => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error("useDarkMode must be used within a ThemeProvider"); + } + return context; +}; + +// export const useDarkMode = () => { +// const [darkMode, setDarkMode] = useState( +// window.localStorage.getItem(DARK_MODE) == "true" ? true : false, +// ); +// +// const toggleDarkMode = () => { +// setDarkMode((prevMode) => { +// const newMode = !prevMode; +// window.localStorage.setItem(DARK_MODE, newMode ? "true" : "false"); +// document.documentElement.classList.toggle("dark", newMode); +// window.location.reload(); // need to rerender header and other components +// return newMode; +// }); +// }; +// +// useEffect(() => { +// window.localStorage.setItem(DARK_MODE, darkMode ? "true" : "false"); +// document.documentElement.classList.toggle("dark", darkMode); +// }, [darkMode]); +// +// return { darkMode, toggleDarkMode }; +// }; diff --git a/client/src/shared/instrument-card-actions/ui/EditInstrument.button.tsx b/client/src/shared/instrument-card-actions/ui/EditInstrument.button.tsx index 6adb8ab4..8d990688 100644 --- a/client/src/shared/instrument-card-actions/ui/EditInstrument.button.tsx +++ b/client/src/shared/instrument-card-actions/ui/EditInstrument.button.tsx @@ -3,19 +3,21 @@ import styles from "./styles/EditInstrument.button.module.css"; import actionBtnStyle from "./styles/Action.button.module.css"; import { useNavigate } from "react-router-dom"; import { InstrumentDetail } from "generated/model"; +import { useDarkMode } from "shared/dark-mode/use-dark-mode"; interface Props { instrument: InstrumentDetail; } export const EditInstrumentButton = (props: Props) => { + const { darkMode } = useDarkMode(); const navigate = useNavigate(); return ( diff --git a/client/src/shared/instrument-card-actions/ui/ShowInstrument.button.tsx b/client/src/shared/instrument-card-actions/ui/ShowInstrument.button.tsx index 57ed60c7..70171b7b 100644 --- a/client/src/shared/instrument-card-actions/ui/ShowInstrument.button.tsx +++ b/client/src/shared/instrument-card-actions/ui/ShowInstrument.button.tsx @@ -4,12 +4,14 @@ import actionBtnStyle from "./styles/Action.button.module.css"; import { useNavigate } from "react-router-dom"; import { InstrumentDetail } from "generated/model"; +import { useDarkMode } from "shared/dark-mode/use-dark-mode"; interface Props { instrument: InstrumentDetail; } export const ShowInstrumentButton = (props: Props) => { + const { darkMode } = useDarkMode(); const navigate = useNavigate(); const handleOnClick = () => { @@ -21,7 +23,7 @@ export const ShowInstrumentButton = (props: Props) => { - {/* prettier-ignore */} - {/* eslint-disable-next-line */} - +
+
diff --git a/client/src/widgets/header/ui/styles/Header.widget.module.scss b/client/src/widgets/header/ui/styles/Header.widget.module.scss new file mode 100644 index 00000000..29fc60c0 --- /dev/null +++ b/client/src/widgets/header/ui/styles/Header.widget.module.scss @@ -0,0 +1,40 @@ +.nav { + display: flex; + font-weight: bold; +} + +.nav__button { + flex: 1; + padding: 1em 0; + text-align: center; + color: #ffffff; + border: 1px #cccccc solid; + + &:hover { + } +} + +.btn__bg__dark { + background-color: #1a1a1a; + color: #ffffff; + + &:hover { + color: #ffd700; + } + + &:active { + color: #ffd700; + } +} + +.btn__bg__light { + background-color: #1e90ff; + + &:hover { + background-color: #175cc4; + } +} + +.btn_txt { + color: #ffffff; +} diff --git a/client/src/widgets/header/ui/styles/HeaderWidget.css b/client/src/widgets/header/ui/styles/HeaderWidget.css deleted file mode 100644 index fe72b7a9..00000000 --- a/client/src/widgets/header/ui/styles/HeaderWidget.css +++ /dev/null @@ -1,20 +0,0 @@ -header { - nav { - display: flex; - font-weight: bold; - } - - nav > button { - flex: 1; - padding: 1em 0; - text-align: center; - /*background-color: #55c4fa;*/ - background-color: #1e90ff; - color: #ffffff; - border: 1px #cccccc solid; - } - - nav > button:hover { - background-color: #175cc4; - } -} diff --git a/server/app/src/main/kotlin/mu/muse/application/muse/SecurityConfiguration.kt b/server/app/src/main/kotlin/mu/muse/application/muse/SecurityConfiguration.kt index e988e713..6f01b70c 100644 --- a/server/app/src/main/kotlin/mu/muse/application/muse/SecurityConfiguration.kt +++ b/server/app/src/main/kotlin/mu/muse/application/muse/SecurityConfiguration.kt @@ -5,18 +5,15 @@ import io.jsonwebtoken.Jwts import io.jsonwebtoken.security.Keys import jakarta.servlet.http.HttpServletResponse import mu.muse.rest.API_COUNTRIES -import mu.muse.rest.API_FAVORITE_ADD -import mu.muse.rest.API_FAVORITE_LIST -import mu.muse.rest.API_FAVORITE_REMOVE import mu.muse.rest.API_INSTRUMENTS import mu.muse.rest.API_INSTRUMENTS_PAGINATED import mu.muse.rest.API_INSTRUMENT_BY_ID import mu.muse.rest.API_INSTRUMENT_MATERIALS import mu.muse.rest.API_INSTRUMENT_PHOTO import mu.muse.rest.API_INSTRUMENT_TYPES +import mu.muse.rest.API_LOGIN import mu.muse.rest.API_MANUFACTURERS import mu.muse.rest.API_REGISTRATION -import mu.muse.rest.API_LOGIN import mu.muse.usecase.access.user.UserExtractor import mu.muse.usecase.scenario.user.BasicLoginUseCase import org.springframework.beans.factory.annotation.Value @@ -77,7 +74,6 @@ class SecurityConfiguration { userDetailsService: UserDetailsService, jwtFilter: JwtFilter, corsConfigurationSource: CorsConfigurationSource, - sessionRegistry: SessionRegistry, ): SecurityFilterChain { // @formatter:off var http = httpSecurity .csrf { cors -> cors.disable() } @@ -86,9 +82,7 @@ class SecurityConfiguration { http = http.sessionManagement { session -> session .sessionFixation { it.none() } - .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) - .maximumSessions(1) - .sessionRegistry(sessionRegistry) + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) } http = http.userDetailsService(userDetailsService)