diff --git a/package.json b/package.json index 28aaca5..d2f825e 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.6.0", - "tailwindcss": "^4.1.5" + "tailwindcss": "^4.1.5", + "zustand": "^5.0.7" }, "devDependencies": { "@eslint/js": "^9.22.0", diff --git a/src/App.tsx b/src/App.tsx index 2e407da..9063592 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,13 +8,36 @@ import UnauthorizedHandler from './app/auth/components/UnauthorizedHandler.tsx' import AdminPage from './app/admin/pages/AdminPage.tsx' import SignupCodesManagementPage from './app/admin/pages/subpages/SignupCodesManagementPage.tsx' import { useRefreshSessionLoop } from './useRefreshSessionLoop.ts' -import { initApp } from './initApp.ts' +import { setGlobal } from './app/femtoApp.ts' +import { PostsService } from './app/feed/posts/postsService.ts' +import { MediaService } from './app/media/mediaService.ts' +import { AuthService } from './app/auth/authService.ts' +import { initClient } from './app/api/client.ts' +import { useEffect } from 'react' +import { useUserStore } from './app/user/user.ts' +import { getUserFromCookie } from './app/auth/getUserFromCookie.ts' -const { postService, mediaService, authService } = initApp() +setGlobal('version', import.meta.env.VITE_FEMTO_VERSION) + +const client = initClient() +const postService = new PostsService(client) +const mediaService = new MediaService(client) +const authService = new AuthService(client) + +setGlobal('postsService', postService) +setGlobal('authService', authService) export default function App() { + const setUser = useUserStore((state) => state.setUser) + useRefreshSessionLoop(authService) + useEffect(() => { + const user = getUserFromCookie() + console.debug('got user cookie', user) + setUser(user) + }, [setUser]) + return ( @@ -23,10 +46,7 @@ export default function App() { path={'/'} element={} /> - } - /> + } /> } /> } /> } /> diff --git a/src/app/api/client.ts b/src/app/api/client.ts index 63cce0c..eebacdf 100644 --- a/src/app/api/client.ts +++ b/src/app/api/client.ts @@ -1,6 +1,8 @@ import { paths } from './schema.ts' import createClient, { Middleware } from 'openapi-fetch' import { dispatchMessage } from '../messageBus/messageBus.ts' +import { useUserStore } from '../user/user.ts' +import { getUserFromCookie } from '../auth/getUserFromCookie.ts' export const initClient = () => { const client = createClient({ baseUrl: import.meta.env.VITE_API_URL }) @@ -9,6 +11,10 @@ export const initClient = () => { if (response.status === 401) { dispatchMessage('auth:unauthorized', null) } + + const user = getUserFromCookie() + console.debug('got user cookie', user) + useUserStore.getState().setUser(user) }, } diff --git a/src/app/auth/components/AuthNavButtons.tsx b/src/app/auth/components/AuthNavButtons.tsx index c646888..0c5141b 100644 --- a/src/app/auth/components/AuthNavButtons.tsx +++ b/src/app/auth/components/AuthNavButtons.tsx @@ -1,11 +1,11 @@ -import { useUser } from '../../user/user.ts' import NavButton from '../../../components/buttons/NavButton.tsx' import { useLocation } from 'react-router-dom' import { useTranslations } from '../../i18n/translations.ts' +import { useUserStore } from '../../user/user.ts' export default function AuthNavButtons() { const { t } = useTranslations() - const user = useUser() + const user = useUserStore((state) => state.user) const { pathname } = useLocation() diff --git a/src/app/auth/components/Protected.tsx b/src/app/auth/components/Protected.tsx index f5ea238..19d71bb 100644 --- a/src/app/auth/components/Protected.tsx +++ b/src/app/auth/components/Protected.tsx @@ -1,9 +1,9 @@ -import { useUser } from '../../user/user.ts' import { useNavigate, Outlet } from 'react-router-dom' import { useEffect } from 'react' +import { useUserStore } from '../../user/user.ts' export default function Protected() { - const user = useUser() + const user = useUserStore((state) => state.user) const navigate = useNavigate() diff --git a/src/app/auth/components/RefreshUser.tsx b/src/app/auth/components/RefreshUser.tsx index 8796381..7d1db08 100644 --- a/src/app/auth/components/RefreshUser.tsx +++ b/src/app/auth/components/RefreshUser.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren, useEffect, useRef } from 'react' import { AuthService } from '../authService.ts' -import { useUser } from '../../user/user.ts' +import { useUserStore } from '../../user/user.ts' interface RefreshUserProps { authService: AuthService @@ -10,7 +10,7 @@ export default function RefreshUser({ authService, children, }: PropsWithChildren) { - const user = useUser() + const user = useUserStore((state) => state.user) const didRefresh = useRef(false) useEffect(() => { diff --git a/src/app/auth/getUserFromCookie.ts b/src/app/auth/getUserFromCookie.ts new file mode 100644 index 0000000..00bd891 --- /dev/null +++ b/src/app/auth/getUserFromCookie.ts @@ -0,0 +1,11 @@ +import { User } from '../user/user.ts' +import { getCookie } from './cookies.ts' + +export function getUserFromCookie(): User | null { + const userCookie = getCookie('user') + + if (!userCookie) return null + + // TODO validate but it should be fine + return JSON.parse(decodeURIComponent(userCookie)) as User +} diff --git a/src/app/auth/pages/LoginPage.tsx b/src/app/auth/pages/LoginPage.tsx index 6c072d7..8b914ca 100644 --- a/src/app/auth/pages/LoginPage.tsx +++ b/src/app/auth/pages/LoginPage.tsx @@ -4,7 +4,7 @@ import TextInput from '../../../components/inputs/TextInput.tsx' import Button from '../../../components/buttons/Button.tsx' import { AuthService } from '../authService.ts' import { useNavigate } from 'react-router-dom' -import { useUser } from '../../user/user.ts' +import { useUserStore } from '../../user/user.ts' import NavBar from '../../../components/NavBar.tsx' import NavButton from '../../../components/buttons/NavButton.tsx' import LinkButton from '../../../components/buttons/LinkButton.tsx' @@ -26,7 +26,7 @@ export default function LoginPage({ authService }: LoginPageProps) { const passwordInputRef = useRef(null) const navigate = useNavigate() - const user = useUser() + const user = useUserStore((state) => state.user) useEffect(() => { if (user) { diff --git a/src/app/auth/pages/LogoutPage.tsx b/src/app/auth/pages/LogoutPage.tsx index dea8a72..dfceb5c 100644 --- a/src/app/auth/pages/LogoutPage.tsx +++ b/src/app/auth/pages/LogoutPage.tsx @@ -1,7 +1,7 @@ import { useNavigate } from 'react-router-dom' import { AuthService } from '../authService.ts' import { useEffect } from 'react' -import { useUser } from '../../user/user.ts' +import { useUserStore } from '../../user/user.ts' interface LogoutPageProps { authService: AuthService @@ -9,7 +9,7 @@ interface LogoutPageProps { export default function LogoutPage({ authService }: LogoutPageProps) { const navigate = useNavigate() - const user = useUser() + const user = useUserStore((state) => state.user) useEffect(() => { if (!user) { diff --git a/src/app/feed/pages/HomePage.tsx b/src/app/feed/pages/HomePage.tsx index c470d39..35d994d 100644 --- a/src/app/feed/pages/HomePage.tsx +++ b/src/app/feed/pages/HomePage.tsx @@ -1,6 +1,6 @@ import { useRef, useState } from 'react' import { PostsService } from '../posts/postsService.ts' -import { useUser } from '../../user/user.ts' +import { useUserStore } from '../../user/user.ts' import { MediaService } from '../../media/mediaService.ts' import NewPostWidget from '../../../components/NewPostWidget.tsx' import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx' @@ -21,7 +21,7 @@ interface HomePageProps { const PageSize = 20 export default function HomePage({ postsService, mediaService }: HomePageProps) { - const user = useUser() + const user = useUserStore((state) => state.user) useSaveSignupCodeToLocalStorage() const [isSubmitting, setIsSubmitting] = useState(false) diff --git a/src/app/user/user.ts b/src/app/user/user.ts index fb5b520..8ce3446 100644 --- a/src/app/user/user.ts +++ b/src/app/user/user.ts @@ -1,8 +1,4 @@ -import { addMessageListener, dispatchMessage } from '../messageBus/messageBus.ts' -import { getCookie } from '../auth/cookies.ts' -import { useMessageListener } from '../../hooks/useMessageListener.ts' -import { useState } from 'react' -import { setGlobal } from '../femtoApp.ts' +import { create } from 'zustand' export interface User { id: string @@ -15,38 +11,12 @@ export enum Role { SuperUser = 1, } -let globalUser: User | null - -export function initUser() { - updateUser() - - addMessageListener('auth:logged-in', updateUser) - addMessageListener('auth:registered', updateUser) - addMessageListener('auth:logged-out', updateUser) - addMessageListener('auth:refreshed', updateUser) +interface UserState { + user: User | null + setUser: (user: User | null) => void } -function updateUser() { - globalUser = getUserFromCookie() - setGlobal('user', globalUser) - dispatchMessage('user:updated', globalUser) -} - -export function useUser(): User | null { - const [user, setUser] = useState(globalUser) - - useMessageListener('user:updated', (u) => { - setUser(u) - }) - - return user -} - -function getUserFromCookie(): User | null { - const userCookie = getCookie('user') - - if (!userCookie) return null - - // TODO validate but it should be fine - return JSON.parse(decodeURIComponent(userCookie)) as User -} +export const useUserStore = create()((set) => ({ + user: null, + setUser: (user: User | null) => set({ user }), +})) diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx index 330c7aa..d8fb4f7 100644 --- a/src/components/NavBar.tsx +++ b/src/components/NavBar.tsx @@ -1,11 +1,11 @@ import { PropsWithChildren } from 'react' -import { Role, useUser } from '../app/user/user.ts' +import { Role, useUserStore } from '../app/user/user.ts' import NavButton from './buttons/NavButton.tsx' type NavBarProps = unknown export default function NavBar({ children }: PropsWithChildren) { - const user = useUser() + const user = useUserStore((state) => state.user) const isSuperUser = user?.roles.includes(Role.SuperUser) return (