mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
Merge Tag v0.2.22
This commit is contained in:
@@ -1,27 +1,29 @@
|
||||
'use client'
|
||||
|
||||
import { aboutOpenAtom, currentUserIdAtom, pomodoroAtom, userSelectAtom } from '@/lib/atoms';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import AboutModal from './AboutModal'
|
||||
import LoadingSpinner from './LoadingSpinner'
|
||||
import PomodoroTimer from './PomodoroTimer'
|
||||
import UserSelectModal from './UserSelectModal'
|
||||
import { checkDataFreshness as checkServerDataFreshness } from '@/app/actions/data';
|
||||
import { aboutOpenAtom, clientFreshnessTokenAtom, currentUserIdAtom, pomodoroAtom, userSelectAtom } from '@/lib/atoms';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { ReactNode, Suspense, useCallback, useEffect, useState } from 'react';
|
||||
import AboutModal from './AboutModal';
|
||||
import LoadingSpinner from './LoadingSpinner';
|
||||
import PomodoroTimer from './PomodoroTimer';
|
||||
import RefreshBanner from './RefreshBanner';
|
||||
import UserSelectModal from './UserSelectModal';
|
||||
|
||||
export default function ClientWrapper({ children }: { children: ReactNode }) {
|
||||
function ClientWrapperContent({ children }: { children: ReactNode }) {
|
||||
const [pomo] = useAtom(pomodoroAtom)
|
||||
const [userSelect, setUserSelect] = useAtom(userSelectAtom)
|
||||
const [aboutOpen, setAboutOpen] = useAtom(aboutOpenAtom)
|
||||
const setCurrentUserIdAtom = useSetAtom(currentUserIdAtom)
|
||||
const { data: session, status } = useSession()
|
||||
const currentUserId = session?.user.id
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [showRefreshBanner, setShowRefreshBanner] = useState(false);
|
||||
|
||||
// clientFreshnessTokenAtom is async, useAtomValue will suspend until it's resolved.
|
||||
// Suspense boundary is in app/layout.tsx or could be added here if needed more locally.
|
||||
const clientToken = useAtomValue(clientFreshnessTokenAtom);
|
||||
|
||||
// block client-side hydration until mounted (this is crucial to wait for all jotai atoms to load), to prevent SSR hydration errors in the children components
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'loading') return
|
||||
@@ -34,21 +36,62 @@ export default function ClientWrapper({ children }: { children: ReactNode }) {
|
||||
setCurrentUserIdAtom(currentUserId)
|
||||
}, [currentUserId, setCurrentUserIdAtom])
|
||||
|
||||
if (!isMounted) {
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
const performFreshnessCheck = useCallback(async () => {
|
||||
if (!clientToken || status !== 'authenticated') return;
|
||||
|
||||
try {
|
||||
const result = await checkServerDataFreshness(clientToken);
|
||||
if (!result.isFresh) {
|
||||
setShowRefreshBanner(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check data freshness with server:", error);
|
||||
}
|
||||
}, [clientToken, status]);
|
||||
|
||||
useEffect(() => {
|
||||
// Interval for polling data freshness
|
||||
if (clientToken && !showRefreshBanner && status === 'authenticated') {
|
||||
const intervalId = setInterval(() => {
|
||||
performFreshnessCheck();
|
||||
}, 30000); // Check every 30 seconds
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}
|
||||
}, [clientToken, performFreshnessCheck, showRefreshBanner, status]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
setShowRefreshBanner(false);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{pomo.show && (
|
||||
<PomodoroTimer />
|
||||
)}
|
||||
{userSelect && (
|
||||
<UserSelectModal onClose={() => setUserSelect(false)} />
|
||||
)}
|
||||
{aboutOpen && (
|
||||
<AboutModal onClose={() => setAboutOpen(false)} />
|
||||
)}
|
||||
{pomo.show && <PomodoroTimer />}
|
||||
{userSelect && <UserSelectModal onClose={() => setUserSelect(false)} />}
|
||||
{aboutOpen && <AboutModal onClose={() => setAboutOpen(false)} />}
|
||||
{showRefreshBanner && <RefreshBanner onRefresh={handleRefresh} />}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default function ClientWrapper({ children }: { children: ReactNode }) {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
// block client-side hydration until mounted (this is crucial to wait for all jotai atoms to load),
|
||||
// to prevent SSR hydration errors in the children components
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!isMounted) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<LoadingSpinner />}>
|
||||
<ClientWrapperContent>{children}</ClientWrapperContent>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export function Profile() {
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col mr-4">
|
||||
<span className="text-sm font-semibold flex items-center gap-1">
|
||||
<span className="text-sm font-semibold flex items-center gap-1 break-all">
|
||||
{user?.username || t('guestUsername')}
|
||||
{user?.isAdmin && <Crown className="h-3 w-3 text-yellow-500" />}
|
||||
</span>
|
||||
|
||||
27
components/RefreshBanner.tsx
Normal file
27
components/RefreshBanner.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { AlertTriangle } from "lucide-react"
|
||||
|
||||
interface RefreshBannerProps {
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
export default function RefreshBanner({ onRefresh }: RefreshBannerProps) {
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-[100] bg-yellow-400 dark:bg-yellow-500 text-black dark:text-gray-900 p-4 rounded-lg shadow-lg flex items-center gap-3">
|
||||
<AlertTriangle className="h-6 w-6 text-yellow-800 dark:text-yellow-900" />
|
||||
<div>
|
||||
<p className="font-semibold">Data out of sync</p>
|
||||
<p className="text-sm">New data is available. Please refresh to see the latest updates.</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onRefresh}
|
||||
variant="outline"
|
||||
className="ml-auto bg-yellow-500 hover:bg-yellow-600 dark:bg-yellow-600 dark:hover:bg-yellow-700 border-yellow-600 dark:border-yellow-700 text-white dark:text-gray-900"
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export function JotaiHydrate({
|
||||
[coinsAtom, initialValues.coins],
|
||||
[wishlistAtom, initialValues.wishlist],
|
||||
[usersAtom, initialValues.users],
|
||||
[serverSettingsAtom, initialValues.serverSettings]
|
||||
[serverSettingsAtom, initialValues.serverSettings],
|
||||
])
|
||||
return children
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user