use zustand
This commit is contained in:
parent
d2d358bff2
commit
5f29bc436c
15 changed files with 74 additions and 83 deletions
|
@ -20,7 +20,8 @@
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.6.0",
|
"react-router-dom": "^7.6.0",
|
||||||
"tailwindcss": "^4.1.5"
|
"tailwindcss": "^4.1.5",
|
||||||
|
"zustand": "^5.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.22.0",
|
"@eslint/js": "^9.22.0",
|
||||||
|
|
32
src/App.tsx
32
src/App.tsx
|
@ -8,13 +8,36 @@ import UnauthorizedHandler from './app/auth/components/UnauthorizedHandler.tsx'
|
||||||
import AdminPage from './app/admin/pages/AdminPage.tsx'
|
import AdminPage from './app/admin/pages/AdminPage.tsx'
|
||||||
import SignupCodesManagementPage from './app/admin/pages/subpages/SignupCodesManagementPage.tsx'
|
import SignupCodesManagementPage from './app/admin/pages/subpages/SignupCodesManagementPage.tsx'
|
||||||
import { useRefreshSessionLoop } from './useRefreshSessionLoop.ts'
|
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() {
|
export default function App() {
|
||||||
|
const setUser = useUserStore((state) => state.setUser)
|
||||||
|
|
||||||
useRefreshSessionLoop(authService)
|
useRefreshSessionLoop(authService)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const user = getUserFromCookie()
|
||||||
|
console.debug('got user cookie', user)
|
||||||
|
setUser(user)
|
||||||
|
}, [setUser])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<UnauthorizedHandler>
|
<UnauthorizedHandler>
|
||||||
|
@ -23,10 +46,7 @@ export default function App() {
|
||||||
path={'/'}
|
path={'/'}
|
||||||
element={<HomePage postsService={postService} mediaService={mediaService} />}
|
element={<HomePage postsService={postService} mediaService={mediaService} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route path={'/p/:postId'} element={<PostPage postsService={postService} />} />
|
||||||
path={'/p/:postId'}
|
|
||||||
element={<PostPage postsService={postService} />}
|
|
||||||
/>
|
|
||||||
<Route path="/login" element={<LoginPage authService={authService} />} />
|
<Route path="/login" element={<LoginPage authService={authService} />} />
|
||||||
<Route path="/logout" element={<LogoutPage authService={authService} />} />
|
<Route path="/logout" element={<LogoutPage authService={authService} />} />
|
||||||
<Route path="/signup/:code?" element={<SignupPage authService={authService} />} />
|
<Route path="/signup/:code?" element={<SignupPage authService={authService} />} />
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { paths } from './schema.ts'
|
import { paths } from './schema.ts'
|
||||||
import createClient, { Middleware } from 'openapi-fetch'
|
import createClient, { Middleware } from 'openapi-fetch'
|
||||||
import { dispatchMessage } from '../messageBus/messageBus.ts'
|
import { dispatchMessage } from '../messageBus/messageBus.ts'
|
||||||
|
import { useUserStore } from '../user/user.ts'
|
||||||
|
import { getUserFromCookie } from '../auth/getUserFromCookie.ts'
|
||||||
|
|
||||||
export const initClient = () => {
|
export const initClient = () => {
|
||||||
const client = createClient<paths>({ baseUrl: import.meta.env.VITE_API_URL })
|
const client = createClient<paths>({ baseUrl: import.meta.env.VITE_API_URL })
|
||||||
|
@ -9,6 +11,10 @@ export const initClient = () => {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
dispatchMessage('auth:unauthorized', null)
|
dispatchMessage('auth:unauthorized', null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = getUserFromCookie()
|
||||||
|
console.debug('got user cookie', user)
|
||||||
|
useUserStore.getState().setUser(user)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { useUser } from '../../user/user.ts'
|
|
||||||
import NavButton from '../../../components/buttons/NavButton.tsx'
|
import NavButton from '../../../components/buttons/NavButton.tsx'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { useTranslations } from '../../i18n/translations.ts'
|
import { useTranslations } from '../../i18n/translations.ts'
|
||||||
|
import { useUserStore } from '../../user/user.ts'
|
||||||
|
|
||||||
export default function AuthNavButtons() {
|
export default function AuthNavButtons() {
|
||||||
const { t } = useTranslations()
|
const { t } = useTranslations()
|
||||||
const user = useUser()
|
const user = useUserStore((state) => state.user)
|
||||||
|
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { useUser } from '../../user/user.ts'
|
|
||||||
import { useNavigate, Outlet } from 'react-router-dom'
|
import { useNavigate, Outlet } from 'react-router-dom'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import { useUserStore } from '../../user/user.ts'
|
||||||
|
|
||||||
export default function Protected() {
|
export default function Protected() {
|
||||||
const user = useUser()
|
const user = useUserStore((state) => state.user)
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { PropsWithChildren, useEffect, useRef } from 'react'
|
import { PropsWithChildren, useEffect, useRef } from 'react'
|
||||||
import { AuthService } from '../authService.ts'
|
import { AuthService } from '../authService.ts'
|
||||||
import { useUser } from '../../user/user.ts'
|
import { useUserStore } from '../../user/user.ts'
|
||||||
|
|
||||||
interface RefreshUserProps {
|
interface RefreshUserProps {
|
||||||
authService: AuthService
|
authService: AuthService
|
||||||
|
@ -10,7 +10,7 @@ export default function RefreshUser({
|
||||||
authService,
|
authService,
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<RefreshUserProps>) {
|
}: PropsWithChildren<RefreshUserProps>) {
|
||||||
const user = useUser()
|
const user = useUserStore((state) => state.user)
|
||||||
const didRefresh = useRef(false)
|
const didRefresh = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
11
src/app/auth/getUserFromCookie.ts
Normal file
11
src/app/auth/getUserFromCookie.ts
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import TextInput from '../../../components/inputs/TextInput.tsx'
|
||||||
import Button from '../../../components/buttons/Button.tsx'
|
import Button from '../../../components/buttons/Button.tsx'
|
||||||
import { AuthService } from '../authService.ts'
|
import { AuthService } from '../authService.ts'
|
||||||
import { useNavigate } from 'react-router-dom'
|
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 NavBar from '../../../components/NavBar.tsx'
|
||||||
import NavButton from '../../../components/buttons/NavButton.tsx'
|
import NavButton from '../../../components/buttons/NavButton.tsx'
|
||||||
import LinkButton from '../../../components/buttons/LinkButton.tsx'
|
import LinkButton from '../../../components/buttons/LinkButton.tsx'
|
||||||
|
@ -26,7 +26,7 @@ export default function LoginPage({ authService }: LoginPageProps) {
|
||||||
const passwordInputRef = useRef<HTMLInputElement | null>(null)
|
const passwordInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUserStore((state) => state.user)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { AuthService } from '../authService.ts'
|
import { AuthService } from '../authService.ts'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useUser } from '../../user/user.ts'
|
import { useUserStore } from '../../user/user.ts'
|
||||||
|
|
||||||
interface LogoutPageProps {
|
interface LogoutPageProps {
|
||||||
authService: AuthService
|
authService: AuthService
|
||||||
|
@ -9,7 +9,7 @@ interface LogoutPageProps {
|
||||||
|
|
||||||
export default function LogoutPage({ authService }: LogoutPageProps) {
|
export default function LogoutPage({ authService }: LogoutPageProps) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const user = useUser()
|
const user = useUserStore((state) => state.user)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
import { PostsService } from '../posts/postsService.ts'
|
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 { MediaService } from '../../media/mediaService.ts'
|
||||||
import NewPostWidget from '../../../components/NewPostWidget.tsx'
|
import NewPostWidget from '../../../components/NewPostWidget.tsx'
|
||||||
import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx'
|
import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx'
|
||||||
|
@ -21,7 +21,7 @@ interface HomePageProps {
|
||||||
const PageSize = 20
|
const PageSize = 20
|
||||||
|
|
||||||
export default function HomePage({ postsService, mediaService }: HomePageProps) {
|
export default function HomePage({ postsService, mediaService }: HomePageProps) {
|
||||||
const user = useUser()
|
const user = useUserStore((state) => state.user)
|
||||||
useSaveSignupCodeToLocalStorage()
|
useSaveSignupCodeToLocalStorage()
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import { addMessageListener, dispatchMessage } from '../messageBus/messageBus.ts'
|
import { create } from 'zustand'
|
||||||
import { getCookie } from '../auth/cookies.ts'
|
|
||||||
import { useMessageListener } from '../../hooks/useMessageListener.ts'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { setGlobal } from '../femtoApp.ts'
|
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string
|
id: string
|
||||||
|
@ -15,38 +11,12 @@ export enum Role {
|
||||||
SuperUser = 1,
|
SuperUser = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
let globalUser: User | null
|
interface UserState {
|
||||||
|
user: User | null
|
||||||
export function initUser() {
|
setUser: (user: User | null) => void
|
||||||
updateUser()
|
|
||||||
|
|
||||||
addMessageListener('auth:logged-in', updateUser)
|
|
||||||
addMessageListener('auth:registered', updateUser)
|
|
||||||
addMessageListener('auth:logged-out', updateUser)
|
|
||||||
addMessageListener('auth:refreshed', updateUser)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUser() {
|
export const useUserStore = create<UserState>()((set) => ({
|
||||||
globalUser = getUserFromCookie()
|
user: null,
|
||||||
setGlobal('user', globalUser)
|
setUser: (user: User | null) => set({ user }),
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { PropsWithChildren } from 'react'
|
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'
|
import NavButton from './buttons/NavButton.tsx'
|
||||||
|
|
||||||
type NavBarProps = unknown
|
type NavBarProps = unknown
|
||||||
|
|
||||||
export default function NavBar({ children }: PropsWithChildren<NavBarProps>) {
|
export default function NavBar({ children }: PropsWithChildren<NavBarProps>) {
|
||||||
const user = useUser()
|
const user = useUserStore((state) => state.user)
|
||||||
const isSuperUser = user?.roles.includes(Role.SuperUser)
|
const isSuperUser = user?.roles.includes(Role.SuperUser)
|
||||||
return (
|
return (
|
||||||
<nav className={`w-full flex flex-row justify-between px-4 md:px-8 py-3`}>
|
<nav className={`w-full flex flex-row justify-between px-4 md:px-8 py-3`}>
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { initUser } from './app/user/user.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'
|
|
||||||
|
|
||||||
export function initApp() {
|
|
||||||
setGlobal('version', import.meta.env.VITE_FEMTO_VERSION)
|
|
||||||
initUser()
|
|
||||||
|
|
||||||
const client = initClient()
|
|
||||||
|
|
||||||
const postService = new PostsService(client)
|
|
||||||
const mediaService = new MediaService(client)
|
|
||||||
const authService = new AuthService(client)
|
|
||||||
|
|
||||||
setGlobal('postsService', postService)
|
|
||||||
setGlobal('authService', authService)
|
|
||||||
|
|
||||||
return { postService, mediaService, authService }
|
|
||||||
}
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useUser } from './app/user/user.ts'
|
|
||||||
import { AuthService } from './app/auth/authService.ts'
|
import { AuthService } from './app/auth/authService.ts'
|
||||||
|
import { useUserStore } from './app/user/user.ts'
|
||||||
|
|
||||||
// Starts a loop that pings the server to keep the session alive, while also getting any updates on the user profile
|
// Starts a loop that pings the server to keep the session alive, while also getting any updates on the user profile
|
||||||
export function useRefreshSessionLoop(authService: AuthService) {
|
export function useRefreshSessionLoop(authService: AuthService) {
|
||||||
const user = useUser()
|
const user = useUserStore((state) => state.user)
|
||||||
const userId = user?.id ?? null
|
const userId = user?.id ?? null
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -2574,3 +2574,8 @@ zod@^3.23.8, zod@^3.24.2:
|
||||||
version "3.24.3"
|
version "3.24.3"
|
||||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.3.tgz#1f40f750a05e477396da64438e0e1c0995dafd87"
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.3.tgz#1f40f750a05e477396da64438e0e1c0995dafd87"
|
||||||
integrity sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==
|
integrity sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==
|
||||||
|
|
||||||
|
zustand@^5.0.7:
|
||||||
|
version "5.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.7.tgz#e325364e82c992a84bf386d8445aa7f180c450dc"
|
||||||
|
integrity sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue