Compare commits

..

4 Commits

18 changed files with 73 additions and 57 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

@@ -135,6 +135,7 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
<span className={`text-sm font-medium ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>{t('coinsPerCompletion', { count: habit.coinReward })}</span> <span className={`text-sm font-medium ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>{t('coinsPerCompletion', { count: habit.coinReward })}</span>
</div> </div>
</div> </div>
<div className='mt-2 text-sm font-medium'>{t("completionCount", { completions: habit.completions.length })}</div>
</CardContent> </CardContent>
<CardFooter className="flex-shrink-0 flex justify-between gap-2"> <CardFooter className="flex-shrink-0 flex justify-between gap-2">
<div className="flex gap-2"> <div className="flex gap-2">

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;

View File

@@ -322,7 +322,8 @@
"completeButtonCount": "Completa ({completed}/{target})", "completeButtonCount": "Completa ({completed}/{target})",
"completeButtonCountMobile": "{completed}/{target}", "completeButtonCountMobile": "{completed}/{target}",
"undoButton": "Desfés", "undoButton": "Desfés",
"editButton": "Edita" "editButton": "Edita",
"completionCount": "completat {completions} vegades"
}, },
"TransactionNoteEditor": { "TransactionNoteEditor": {
"noteTooLongTitle": "Nota massa llarga", "noteTooLongTitle": "Nota massa llarga",

View File

@@ -322,7 +322,8 @@
"completeButtonCount": "Abschließen ({completed}/{target})", "completeButtonCount": "Abschließen ({completed}/{target})",
"completeButtonCountMobile": "{completed}/{target}", "completeButtonCountMobile": "{completed}/{target}",
"undoButton": "Rückgängig", "undoButton": "Rückgängig",
"editButton": "Bearbeiten" "editButton": "Bearbeiten",
"completionCount": "{completions} mal abgeschlossen"
}, },
"TransactionNoteEditor": { "TransactionNoteEditor": {
"noteTooLongTitle": "Notiz zu lang", "noteTooLongTitle": "Notiz zu lang",
@@ -434,8 +435,6 @@
"invalidAmountDescription": "Bitte geben Sie eine gültige positive Zahl ein", "invalidAmountDescription": "Bitte geben Sie eine gültige positive Zahl ein",
"successTitle": "Erfolg", "successTitle": "Erfolg",
"transactionNotFoundDescription": "Transaktion nicht gefunden", "transactionNotFoundDescription": "Transaktion nicht gefunden",
"maxAmountExceededDescription": "Der Betrag darf {max} nicht überschreiten.",
"transactionNotFoundDescription": "Transaktion nicht gefunden",
"maxAmountExceededDescription": "Der Betrag darf {max} nicht überschreiten." "maxAmountExceededDescription": "Der Betrag darf {max} nicht überschreiten."
}, },
"DrawingModal": { "DrawingModal": {

View File

@@ -322,7 +322,8 @@
"completeButtonCount": "Complete ({completed}/{target})", "completeButtonCount": "Complete ({completed}/{target})",
"completeButtonCountMobile": "{completed}/{target}", "completeButtonCountMobile": "{completed}/{target}",
"undoButton": "Undo", "undoButton": "Undo",
"editButton": "Edit" "editButton": "Edit",
"completionCount": "Completed {completions} times"
}, },
"TransactionNoteEditor": { "TransactionNoteEditor": {
"noteTooLongTitle": "Note too long", "noteTooLongTitle": "Note too long",

View File

@@ -322,7 +322,8 @@
"completeButtonCount": "Completar ({completed}/{target})", "completeButtonCount": "Completar ({completed}/{target})",
"completeButtonCountMobile": "{completed}/{target}", "completeButtonCountMobile": "{completed}/{target}",
"undoButton": "Deshacer", "undoButton": "Deshacer",
"editButton": "Editar" "editButton": "Editar",
"completionCount": "{completions} veces completadas"
}, },
"TransactionNoteEditor": { "TransactionNoteEditor": {
"noteTooLongTitle": "Nota demasiado larga", "noteTooLongTitle": "Nota demasiado larga",
@@ -442,8 +443,6 @@
"invalidAmountDescription": "Por favor ingresa un número positivo válido", "invalidAmountDescription": "Por favor ingresa un número positivo válido",
"successTitle": "Éxito", "successTitle": "Éxito",
"transactionNotFoundDescription": "Transacción no encontrada", "transactionNotFoundDescription": "Transacción no encontrada",
"maxAmountExceededDescription": "La cantidad no puede exceder {max}.",
"transactionNotFoundDescription": "Transacción no encontrada",
"maxAmountExceededDescription": "La cantidad no puede exceder {max}." "maxAmountExceededDescription": "La cantidad no puede exceder {max}."
} }
} }

View File

@@ -322,7 +322,8 @@
"completeButtonCount": "Compléter ({completed}/{target})", "completeButtonCount": "Compléter ({completed}/{target})",
"completeButtonCountMobile": "{completed}/{target}", "completeButtonCountMobile": "{completed}/{target}",
"undoButton": "Annuler", "undoButton": "Annuler",
"editButton": "Modifier" "editButton": "Modifier",
"completionCount": "complété {completions} fois"
}, },
"TransactionNoteEditor": { "TransactionNoteEditor": {
"noteTooLongTitle": "Note trop longue", "noteTooLongTitle": "Note trop longue",
@@ -434,8 +435,6 @@
"invalidAmountDescription": "Veuillez entrer un nombre positif valide", "invalidAmountDescription": "Veuillez entrer un nombre positif valide",
"successTitle": "Succès", "successTitle": "Succès",
"transactionNotFoundDescription": "Transaction non trouvée", "transactionNotFoundDescription": "Transaction non trouvée",
"maxAmountExceededDescription": "Le montant ne peut pas dépasser {max}.",
"transactionNotFoundDescription": "Transaction non trouvée",
"maxAmountExceededDescription": "Le montant ne peut pas dépasser {max}." "maxAmountExceededDescription": "Le montant ne peut pas dépasser {max}."
}, },
"DrawingModal": { "DrawingModal": {

View File

@@ -322,7 +322,8 @@
"completeButtonCount": "完了({completed}/{target}", "completeButtonCount": "完了({completed}/{target}",
"completeButtonCountMobile": "{completed}/{target}", "completeButtonCountMobile": "{completed}/{target}",
"undoButton": "取り消し", "undoButton": "取り消し",
"editButton": "編集" "editButton": "編集",
"completionCount": "{completions} 回完了しました"
}, },
"TransactionNoteEditor": { "TransactionNoteEditor": {
"noteTooLongTitle": "メモが長すぎます", "noteTooLongTitle": "メモが長すぎます",
@@ -434,8 +435,6 @@
"invalidAmountDescription": "有効な正の数を入力してください", "invalidAmountDescription": "有効な正の数を入力してください",
"successTitle": "成功しました", "successTitle": "成功しました",
"transactionNotFoundDescription": "取引が見つかりません", "transactionNotFoundDescription": "取引が見つかりません",
"maxAmountExceededDescription": "金額は{max}を超えることはできません。",
"transactionNotFoundDescription": "取引が見つかりません",
"maxAmountExceededDescription": "金額は{max}を超えることはできません。" "maxAmountExceededDescription": "金額は{max}を超えることはできません。"
}, },
"DrawingModal": { "DrawingModal": {

View File

@@ -322,7 +322,8 @@
"completeButtonCount": "완료 ({completed}/{target})", "completeButtonCount": "완료 ({completed}/{target})",
"completeButtonCountMobile": "{completed}/{target}", "completeButtonCountMobile": "{completed}/{target}",
"undoButton": "실행 취소", "undoButton": "실행 취소",
"editButton": "수정" "editButton": "수정",
"completionCount": "{completions}번 완료됨"
}, },
"TransactionNoteEditor": { "TransactionNoteEditor": {
"noteTooLongTitle": "메모가 너무 깁니다", "noteTooLongTitle": "메모가 너무 깁니다",
@@ -430,8 +431,6 @@
"invalidAmountDescription": "유효한 양의 숫자를 입력하세요", "invalidAmountDescription": "유효한 양의 숫자를 입력하세요",
"successTitle": "성공", "successTitle": "성공",
"transactionNotFoundDescription": "거래를 찾을 수 없습니다", "transactionNotFoundDescription": "거래를 찾을 수 없습니다",
"maxAmountExceededDescription": "금액은 {max}을(를) 초과할 수 없습니다.",
"transactionNotFoundDescription": "거래를 찾을 수 없습니다",
"maxAmountExceededDescription": "금액은 {max}을(를) 초과할 수 없습니다." "maxAmountExceededDescription": "금액은 {max}을(를) 초과할 수 없습니다."
}, },
"Warning": { "Warning": {

View File

@@ -322,7 +322,8 @@
"completeButtonCount": "Выполнить ({completed}/{target})", "completeButtonCount": "Выполнить ({completed}/{target})",
"completeButtonCountMobile": "{completed}/{target}", "completeButtonCountMobile": "{completed}/{target}",
"undoButton": "Отменить", "undoButton": "Отменить",
"editButton": "Редактировать" "editButton": "Редактировать",
"completionCount": "завершено {completions} раз"
}, },
"TransactionNoteEditor": { "TransactionNoteEditor": {
"noteTooLongTitle": "Слишком длинная заметка", "noteTooLongTitle": "Слишком длинная заметка",
@@ -434,8 +435,6 @@
"invalidAmountDescription": "Пожалуйста, введите положительное число", "invalidAmountDescription": "Пожалуйста, введите положительное число",
"successTitle": "Успех", "successTitle": "Успех",
"transactionNotFoundDescription": "Транзакция не найдена", "transactionNotFoundDescription": "Транзакция не найдена",
"maxAmountExceededDescription": "Сумма не может превышать {max}.",
"transactionNotFoundDescription": "Транзакция не найдена",
"maxAmountExceededDescription": "Сумма не может превышать {max}." "maxAmountExceededDescription": "Сумма не может превышать {max}."
}, },
"DrawingModal": { "DrawingModal": {

View File

@@ -322,7 +322,8 @@
"completeButtonCount": "完成 ({completed}/{target})", "completeButtonCount": "完成 ({completed}/{target})",
"completeButtonCountMobile": "{completed}/{target}", "completeButtonCountMobile": "{completed}/{target}",
"undoButton": "撤销", "undoButton": "撤销",
"editButton": "编辑" "editButton": "编辑",
"completionCount": "完成 {completions} 次"
}, },
"TransactionNoteEditor": { "TransactionNoteEditor": {
"noteTooLongTitle": "备注太长", "noteTooLongTitle": "备注太长",
@@ -434,8 +435,6 @@
"invalidAmountDescription": "请输入有效的正数", "invalidAmountDescription": "请输入有效的正数",
"successTitle": "成功", "successTitle": "成功",
"transactionNotFoundDescription": "未找到交易记录", "transactionNotFoundDescription": "未找到交易记录",
"maxAmountExceededDescription": "金额不能超过 {max}。",
"transactionNotFoundDescription": "未找到交易记录",
"maxAmountExceededDescription": "金额不能超过 {max}。" "maxAmountExceededDescription": "金额不能超过 {max}。"
}, },
"DrawingModal": { "DrawingModal": {