Compare commits

..

3 Commits

8 changed files with 54 additions and 34 deletions

View File

@@ -181,3 +181,26 @@ This project is licensed under the GNU Affero General Public License v3.0 - see
## Support ## Support
If you encounter any issues or have questions, please file an issue on the GitHub repository. If you encounter any issues or have questions, please file an issue on the GitHub repository.
## Issues
### Missing Permissions
Especially when updating from older versions, it may be that the permissions used in the newer versions have never been set. This causes numerous `missing permissions` errors to appear. The solution is to update the `auth.json` in the `data` directory for each user to include the following json:
```json
"permissions": [{
"habit": {
"write": true,
"interact": true
},
"wishlist": {
"write": true,
"interact": true
},
"coins": {
"write": true,
"interact": true
}
}
```

View File

@@ -10,6 +10,7 @@ import LoadingSpinner from './LoadingSpinner';
import PomodoroTimer from './PomodoroTimer'; import PomodoroTimer from './PomodoroTimer';
import RefreshBanner from './RefreshBanner'; import RefreshBanner from './RefreshBanner';
import UserSelectModal from './UserSelectModal'; import UserSelectModal from './UserSelectModal';
import { DATA_FRESHNESS_INTERVAL } from '@/lib/constants';
function ClientWrapperContent({ children }: { children: ReactNode }) { function ClientWrapperContent({ children }: { children: ReactNode }) {
const [pomo] = useAtom(pomodoroAtom) const [pomo] = useAtom(pomodoroAtom)
@@ -52,9 +53,7 @@ function ClientWrapperContent({ children }: { children: ReactNode }) {
useEffect(() => { useEffect(() => {
// Interval for polling data freshness // Interval for polling data freshness
if (clientToken && !showRefreshBanner && status === 'authenticated') { if (clientToken && !showRefreshBanner && status === 'authenticated') {
const intervalId = setInterval(() => { const intervalId = setInterval(performFreshnessCheck, DATA_FRESHNESS_INTERVAL);
performFreshnessCheck();
}, 30000); // Check every 30 seconds
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
} }

View File

@@ -1,15 +1,11 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { settingsAtom } from '@/lib/atoms' import { Coins } from 'lucide-react';
import { useAtom } from 'jotai' import { useTranslations } from 'next-intl';
import { Coins } from 'lucide-react' import TodayEarnedCoins from './TodayEarnedCoins';
import { useTranslations } from 'next-intl'
import dynamic from 'next/dynamic'
const TodayEarnedCoins = dynamic(() => import('./TodayEarnedCoins'), { ssr: false }) export default function CoinBalance({ coinBalance }: { coinBalance: number | undefined }) {
export default function CoinBalance({ coinBalance }: { coinBalance: number }) {
const t = useTranslations('CoinBalance'); const t = useTranslations('CoinBalance');
const [settings] = useAtom(settingsAtom)
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
@@ -20,7 +16,7 @@ export default function CoinBalance({ coinBalance }: { coinBalance: number }) {
<Coins className="h-12 w-12 text-yellow-400 mr-4" /> <Coins className="h-12 w-12 text-yellow-400 mr-4" />
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-4xl font-bold">{coinBalance}</span> <span className="text-4xl font-bold">{coinBalance ? coinBalance : "…"}</span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<TodayEarnedCoins longFormat={true} /> <TodayEarnedCoins longFormat={true} />
</div> </div>

View File

@@ -357,12 +357,8 @@ export default function DailyOverview({
coinBalance, coinBalance,
}: UpcomingItemsProps) { }: UpcomingItemsProps) {
const t = useTranslations('DailyOverview'); const t = useTranslations('DailyOverview');
const { completeHabit, undoComplete } = useHabits()
const [settings] = useAtom(settingsAtom) const [settings] = useAtom(settingsAtom)
const [completedHabitsMap] = useAtom(completedHabitsMapAtom)
const [browserSettings, setBrowserSettings] = useAtom(browserSettingsAtom) const [browserSettings, setBrowserSettings] = useAtom(browserSettingsAtom)
const today = getTodayInTimezone(settings.system.timezone)
const todayCompletions = completedHabitsMap.get(today) || []
const { saveHabit } = useHabits() const { saveHabit } = useHabits()
const timezone = settings.system.timezone const timezone = settings.system.timezone

View File

@@ -1,7 +1,6 @@
'use client' 'use client'
import { useCoins } from '@/hooks/useCoins' import { coinsAtom, currentUserIdAtom, habitsAtom, wishlistAtom } from '@/lib/atoms'
import { habitsAtom, wishlistAtom } from '@/lib/atoms'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { useTranslations } from 'next-intl' import { useTranslations } from 'next-intl'
import CoinBalance from './CoinBalance' import CoinBalance from './CoinBalance'
@@ -10,11 +9,12 @@ import HabitStreak from './HabitStreak'
export default function Dashboard() { export default function Dashboard() {
const t = useTranslations('Dashboard'); const t = useTranslations('Dashboard');
const [habitsData] = useAtom(habitsAtom) const [{ habits }] = useAtom(habitsAtom);
const habits = habitsData.habits const [loggedInUserId] = useAtom(currentUserIdAtom);
const { balance } = useCoins() const [{ transactions }] = useAtom(coinsAtom);
const [wishlist] = useAtom(wishlistAtom) const [{ items }] = useAtom(wishlistAtom);
const wishlistItems = wishlist.items
const loggedInUserBalance = loggedInUserId ? transactions.filter(transaction => transaction.userId === loggedInUserId).reduce((sum, transaction) => sum + transaction.amount, 0) : 0;
return ( return (
<div> <div>
@@ -22,15 +22,13 @@ export default function Dashboard() {
<h1 className="text-xl xs:text-3xl font-bold">{t('title')}</h1> <h1 className="text-xl xs:text-3xl font-bold">{t('title')}</h1>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<CoinBalance coinBalance={balance} /> <CoinBalance coinBalance={loggedInUserId ? loggedInUserBalance : undefined} />
<HabitStreak habits={habits} /> <HabitStreak habits={habits} />
<DailyOverview <DailyOverview
wishlistItems={wishlistItems} wishlistItems={items}
habits={habits} habits={habits}
coinBalance={balance} coinBalance={loggedInUserBalance}
/> />
{/* <HabitHeatmap habits={habits} /> */}
</div> </div>
</div> </div>
) )

View File

@@ -128,7 +128,7 @@ export default function UserSelectModal({ onClose }: { onClose: () => void }) {
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [usersData, setUsersData] = useAtom(usersAtom); const [usersData] = useAtom(usersAtom);
const users = usersData.users; const users = usersData.users;
const [currentUser] = useAtom(currentUserAtom); const [currentUser] = useAtom(currentUserAtom);

View File

@@ -22,6 +22,7 @@ import {
getDefaultWishlistData, getDefaultWishlistData,
Habit, Habit,
PomodoroAtom, PomodoroAtom,
User,
UserId UserId
} from "./types"; } from "./types";
@@ -77,6 +78,10 @@ export const currentUserAtom = atom((get) => {
return users.users.find(user => user.id === currentUserId); return users.users.find(user => user.id === currentUserId);
}) })
function removeHasPassword(users: Omit<User, 'password'>[]): Omit<User, 'password'>[] {
return users.map(user => { delete user.hasPassword; return user });
}
/** /**
* Asynchronous atom that calculates a freshness token (hash) based on the current client-side data. * Asynchronous atom that calculates a freshness token (hash) based on the current client-side data.
* This token can be compared with a server-generated token to detect data discrepancies. * This token can be compared with a server-generated token to detect data discrepancies.
@@ -86,9 +91,10 @@ export const clientFreshnessTokenAtom = atom(async (get) => {
const habits = get(habitsAtom); const habits = get(habitsAtom);
const coins = get(coinsAtom); const coins = get(coinsAtom);
const wishlist = get(wishlistAtom); const wishlist = get(wishlistAtom);
const users = get(usersAtom); const users = structuredClone(get(usersAtom));
const usersWithoutHasPassword = removeHasPassword(users.users);
const dataString = prepareDataForHashing(settings, habits, coins, wishlist, users); const dataString = prepareDataForHashing(settings, habits, coins, wishlist, { users: usersWithoutHasPassword });
const hash = await generateCryptoHash(dataString); const hash = await generateCryptoHash(dataString);
return hash; return hash;
}); });

View File

@@ -34,3 +34,5 @@ export const QUICK_DATES = [
export const MAX_COIN_LIMIT = 9999 export const MAX_COIN_LIMIT = 9999
export const DESKTOP_DISPLAY_ITEM_COUNT = 4 export const DESKTOP_DISPLAY_ITEM_COUNT = 4
export const DATA_FRESHNESS_INTERVAL = 30000;