Skip to content

Commit a615a45

Browse files
committed
fix demo bugs
1 parent dea2b30 commit a615a45

15 files changed

+135
-52
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ yarn-error.log*
4141
next-env.d.ts
4242

4343
# customize
44-
data/*
44+
/data/*
45+
/data.*/*
4546
Budfile
4647
certificates

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## Version 0.2.3
4+
5+
### Fixed
6+
7+
* gracefully handle invalid rrule (#76)
8+
* fix long habit name overflow in daily (#75)
9+
* disable password in demo instance (#74)
10+
311
## Version 0.2.2
412

513
### Changed

app/actions/data.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import {
1919
getDefaultWishlistData,
2020
getDefaultHabitsData,
2121
getDefaultCoinsData,
22-
Permission
22+
Permission,
23+
ServerSettings
2324
} from '@/lib/types'
2425
import { d2t, deepMerge, getNow, checkPermission, uuid } from '@/lib/utils';
2526
import { verifyPassword } from "@/lib/server-helpers";
@@ -474,3 +475,9 @@ export async function deleteUser(userId: string): Promise<void> {
474475

475476
await saveUsersData(newData)
476477
}
478+
479+
export async function loadServerSettings(): Promise<ServerSettings> {
480+
return {
481+
isDemo: !!process.env.NEXT_PUBLIC_DEMO,
482+
}
483+
}

app/layout.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { DM_Sans } from 'next/font/google'
44
import { JotaiProvider } from '@/components/jotai-providers'
55
import { Suspense } from 'react'
66
import { JotaiHydrate } from '@/components/jotai-hydrate'
7-
import { loadSettings, loadHabitsData, loadCoinsData, loadWishlistData, loadUsersData } from './actions/data'
7+
import { loadSettings, loadHabitsData, loadCoinsData, loadWishlistData, loadUsersData, loadServerSettings } from './actions/data'
88
import Layout from '@/components/Layout'
99
import { Toaster } from '@/components/ui/toaster'
1010
import { ThemeProvider } from "@/components/theme-provider"
@@ -37,12 +37,13 @@ export default async function RootLayout({
3737
}: {
3838
children: React.ReactNode
3939
}) {
40-
const [initialSettings, initialHabits, initialCoins, initialWishlist, initialUsers] = await Promise.all([
40+
const [initialSettings, initialHabits, initialCoins, initialWishlist, initialUsers, initialServerSettings] = await Promise.all([
4141
loadSettings(),
4242
loadHabitsData(),
4343
loadCoinsData(),
4444
loadWishlistData(),
4545
loadUsersData(),
46+
loadServerSettings(),
4647
])
4748

4849
return (
@@ -74,7 +75,8 @@ export default async function RootLayout({
7475
habits: initialHabits,
7576
coins: initialCoins,
7677
wishlist: initialWishlist,
77-
users: initialUsers
78+
users: initialUsers,
79+
serverSettings: initialServerSettings,
7880
}}
7981
>
8082
<ThemeProvider

components/AddEditHabitModal.tsx

+26-9
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
1616
import data from '@emoji-mart/data'
1717
import Picker from '@emoji-mart/react'
1818
import { Habit, SafeUser } from '@/lib/types'
19-
import { d2s, d2t, getISODate, getNow, parseNaturalLanguageDate, parseNaturalLanguageRRule, parseRRule, serializeRRule } from '@/lib/utils'
20-
import { INITIAL_DUE, INITIAL_RECURRENCE_RULE, QUICK_DATES } from '@/lib/constants'
19+
import { d2s, d2t, getFrequencyDisplayText, getISODate, getNow, parseNaturalLanguageDate, parseNaturalLanguageRRule, parseRRule, serializeRRule } from '@/lib/utils'
20+
import { INITIAL_DUE, INITIAL_RECURRENCE_RULE, QUICK_DATES, RECURRENCE_RULE_MAP } from '@/lib/constants'
2121
import * as chrono from 'chrono-node';
2222
import { DateTime } from 'luxon'
2323
import {
@@ -43,15 +43,33 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
4343
const [coinReward, setCoinReward] = useState(habit?.coinReward || 1)
4444
const [targetCompletions, setTargetCompletions] = useState(habit?.targetCompletions || 1)
4545
const isRecurRule = !isTask
46-
const origRuleText = isRecurRule ? parseRRule(habit?.frequency || INITIAL_RECURRENCE_RULE).toText() : habit?.frequency || INITIAL_DUE
46+
const origRuleText = getFrequencyDisplayText(habit?.frequency, isRecurRule, settings.system.timezone)
4747
const [ruleText, setRuleText] = useState<string>(origRuleText)
48-
const now = getNow({ timezone: settings.system.timezone })
4948
const { currentUser } = useHelpers()
5049
const [isQuickDatesOpen, setIsQuickDatesOpen] = useState(false)
5150
const [selectedUserIds, setSelectedUserIds] = useState<string[]>((habit?.userIds || []).filter(id => id !== currentUser?.id))
5251
const [usersData] = useAtom(usersAtom)
5352
const users = usersData.users
5453

54+
function getFrequencyUpdate() {
55+
if (ruleText === origRuleText && habit?.frequency) {
56+
return habit.frequency
57+
}
58+
if (isRecurRule) {
59+
const parsedRule = parseNaturalLanguageRRule(ruleText)
60+
return serializeRRule(parsedRule)
61+
} else {
62+
const parsedDate = parseNaturalLanguageDate({
63+
text: ruleText,
64+
timezone: settings.system.timezone
65+
})
66+
return d2t({
67+
dateTime: parsedDate,
68+
timezone: settings.system.timezone
69+
})
70+
}
71+
}
72+
5573
const handleSubmit = async (e: React.FormEvent) => {
5674
e.preventDefault()
5775
await onSave({
@@ -60,8 +78,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
6078
coinReward,
6179
targetCompletions: targetCompletions > 1 ? targetCompletions : undefined,
6280
completions: habit?.completions || [],
63-
frequency: isRecurRule ? serializeRRule(parseNaturalLanguageRRule(ruleText)) : d2t({ dateTime: parseNaturalLanguageDate({ text: ruleText, timezone: settings.system.timezone }) }),
64-
isTask: isTask || undefined,
81+
frequency: getFrequencyUpdate(),
6582
userIds: selectedUserIds.length > 0 ? selectedUserIds.concat(currentUser?.id || []) : (currentUser && [currentUser.id])
6683
})
6784
}
@@ -276,13 +293,13 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
276293
<Avatar
277294
key={user.id}
278295
className={`h-8 w-8 border-2 cursor-pointer
279-
${selectedUserIds.includes(user.id)
280-
? 'border-primary'
296+
${selectedUserIds.includes(user.id)
297+
? 'border-primary'
281298
: 'border-muted'
282299
}`}
283300
title={user.username}
284301
onClick={() => {
285-
setSelectedUserIds(prev =>
302+
setSelectedUserIds(prev =>
286303
prev.includes(user.id)
287304
? prev.filter(id => id !== user.id)
288305
: [...prev, user.id]

components/DailyOverview.tsx

+8-8
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,10 @@ export default function DailyOverview({
168168
${isCompleted ? 'bg-secondary/50' : 'bg-secondary/20'}`}
169169
key={habit.id}
170170
>
171-
<span className="flex items-center gap-2">
171+
<span className="flex items-center gap-2 flex-1 min-w-0">
172172
<ContextMenu>
173173
<ContextMenuTrigger asChild>
174-
<div className="flex-none">
174+
<div className="flex-shrink-0">
175175
<button
176176
onClick={(e) => {
177177
e.preventDefault();
@@ -204,7 +204,7 @@ export default function DailyOverview({
204204
</button>
205205
</div>
206206
</ContextMenuTrigger>
207-
<span className={isCompleted ? 'line-through' : ''}>
207+
<span className={cn(isCompleted ? 'line-through' : '', 'break-all')}>
208208
<Linkify>
209209
{habit.name}
210210
</Linkify>
@@ -223,7 +223,7 @@ export default function DailyOverview({
223223
</ContextMenuContent>
224224
</ContextMenu>
225225
</span>
226-
<span className="flex items-center gap-2 text-xs text-muted-foreground">
226+
<span className="flex items-center gap-2 text-xs text-muted-foreground flex-shrink-0">
227227
{habit.targetCompletions && (
228228
<span className="bg-secondary px-1.5 py-0.5 rounded-full">
229229
{completionsToday}/{target}
@@ -373,10 +373,10 @@ export default function DailyOverview({
373373
${isCompleted ? 'bg-secondary/50' : 'bg-secondary/20'}`}
374374
key={habit.id}
375375
>
376-
<span className="flex items-center gap-2">
376+
<span className="flex items-center gap-2 flex-1 min-w-0">
377377
<ContextMenu>
378378
<ContextMenuTrigger asChild>
379-
<div className="flex-none">
379+
<div className="flex-shrink-0">
380380
<button
381381
onClick={(e) => {
382382
e.preventDefault();
@@ -409,7 +409,7 @@ export default function DailyOverview({
409409
</button>
410410
</div>
411411
</ContextMenuTrigger>
412-
<span className={isCompleted ? 'line-through' : ''}>
412+
<span className={cn(isCompleted ? 'line-through' : '', 'break-all')}>
413413
<Linkify>
414414
{habit.name}
415415
</Linkify>
@@ -428,7 +428,7 @@ export default function DailyOverview({
428428
</ContextMenuContent>
429429
</ContextMenu>
430430
</span>
431-
<span className="flex items-center gap-2 text-xs text-muted-foreground">
431+
<span className="flex items-center gap-2 text-xs text-muted-foreground flex-shrink-0">
432432
{habit.targetCompletions && (
433433
<span className="bg-secondary px-1.5 py-0.5 rounded-full">
434434
{completionsToday}/{target}

components/HabitItem.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Habit, SafeUser, User, Permission } from '@/lib/types'
22
import { useAtom } from 'jotai'
33
import { settingsAtom, pomodoroAtom, browserSettingsAtom, usersAtom } from '@/lib/atoms'
4-
import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, parseNaturalLanguageRRule, parseRRule, d2s, getCompletionsForToday, isTaskOverdue } from '@/lib/utils'
4+
import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, parseRRule, d2s, getCompletionsForToday, isTaskOverdue, getFrequencyDisplayText } from '@/lib/utils'
55
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
66
import { Button } from '@/components/ui/button'
77
import { Coins, Edit, Trash2, Check, Undo2, MoreVertical, Timer, Archive, ArchiveRestore, Calendar } from 'lucide-react'
@@ -14,7 +14,7 @@ import {
1414
} from '@/components/ui/dropdown-menu'
1515
import { useEffect, useState } from 'react'
1616
import { useHabits } from '@/hooks/useHabits'
17-
import { INITIAL_RECURRENCE_RULE } from '@/lib/constants'
17+
import { INITIAL_RECURRENCE_RULE, RECURRENCE_RULE_MAP } from '@/lib/constants'
1818
import { DateTime } from 'luxon'
1919
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
2020
import { useHelpers } from '@/lib/client-helpers'
@@ -104,7 +104,9 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
104104
)}
105105
</CardHeader>
106106
<CardContent className="flex-1">
107-
<p className={`text-sm ${habit.archived ? 'text-gray-400 dark:text-gray-500' : 'text-gray-500'}`}>When: {isRecurRule ? parseRRule(habit.frequency || INITIAL_RECURRENCE_RULE).toText() : d2s({ dateTime: t2d({ timestamp: habit.frequency, timezone: settings.system.timezone }), timezone: settings.system.timezone, format: DateTime.DATE_MED_WITH_WEEKDAY })}</p>
107+
<p className={`text-sm ${habit.archived ? 'text-gray-400 dark:text-gray-500' : 'text-gray-500'}`}>
108+
When: {getFrequencyDisplayText(habit.frequency, isRecurRule, settings.system.timezone)}
109+
</p>
108110
<div className="flex items-center mt-2">
109111
<Coins className={`h-4 w-4 mr-1 ${habit.archived ? 'text-gray-400 dark:text-gray-500' : 'text-yellow-400'}`} />
110112
<span className={`text-sm font-medium ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>{habit.coinReward} coins per completion</span>

components/UserForm.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { Label } from './ui/label';
88
import { Switch } from './ui/switch';
99
import { Permission } from '@/lib/types';
1010
import { toast } from '@/hooks/use-toast';
11-
import { useAtom } from 'jotai';
12-
import { usersAtom } from '@/lib/atoms';
11+
import { useAtom, useAtomValue } from 'jotai';
12+
import { serverSettingsAtom, usersAtom } from '@/lib/atoms';
1313
import { createUser, updateUser, updateUserPassword, uploadAvatar } from '@/app/actions/data';
1414
import { SafeUser, User } from '@/lib/types';
1515
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
@@ -26,6 +26,7 @@ interface UserFormProps {
2626

2727
export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps) {
2828
const [users, setUsersData] = useAtom(usersAtom);
29+
const serverSettings = useAtomValue(serverSettingsAtom)
2930
const user = userId ? users.users.find(u => u.id === userId) : undefined;
3031
const { currentUser } = useHelpers()
3132
const getDefaultPermissions = (): Permission[] => [{
@@ -46,7 +47,7 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
4647
const [avatarPath, setAvatarPath] = useState(user?.avatarPath)
4748
const [username, setUsername] = useState(user?.username || '');
4849
const [password, setPassword] = useState<string | undefined>('');
49-
const [disablePassword, setDisablePassword] = useState(user?.password === '' || process.env.NEXT_PUBLIC_DEMO === 'true');
50+
const [disablePassword, setDisablePassword] = useState(user?.password === '' || serverSettings.isDemo);
5051
const [error, setError] = useState('');
5152
const [avatarFile, setAvatarFile] = useState<File | null>(null);
5253
const [isAdmin, setIsAdmin] = useState(user?.isAdmin || false);
@@ -240,7 +241,7 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
240241
className={error ? 'border-red-500' : ''}
241242
disabled={disablePassword}
242243
/>
243-
{process.env.NEXT_PUBLIC_DEMO === 'true' && (
244+
{serverSettings.isDemo && (
244245
<p className="text-sm text-red-500">Password is automatically disabled in demo instance</p>
245246
)}
246247
</div>

components/jotai-hydrate.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { settingsAtom, habitsAtom, coinsAtom, wishlistAtom, usersAtom } from "@/lib/atoms"
3+
import { settingsAtom, habitsAtom, coinsAtom, wishlistAtom, usersAtom, serverSettingsAtom } from "@/lib/atoms"
44
import { useHydrateAtoms } from "jotai/utils"
55
import { JotaiHydrateInitialValues } from "@/lib/types"
66

@@ -13,7 +13,8 @@ export function JotaiHydrate({
1313
[habitsAtom, initialValues.habits],
1414
[coinsAtom, initialValues.coins],
1515
[wishlistAtom, initialValues.wishlist],
16-
[usersAtom, initialValues.users]
16+
[usersAtom, initialValues.users],
17+
[serverSettingsAtom, initialValues.serverSettings]
1718
])
1819
return children
1920
}

lib/atoms.ts

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ViewType,
99
getDefaultUsersData,
1010
CompletionCache,
11+
getDefaultServerSettings,
1112
} from "./types";
1213
import {
1314
getTodayInTimezone,
@@ -46,6 +47,7 @@ export const settingsAtom = atom(getDefaultSettings());
4647
export const habitsAtom = atom(getDefaultHabitsData());
4748
export const coinsAtom = atom(getDefaultCoinsData());
4849
export const wishlistAtom = atom(getDefaultWishlistData());
50+
export const serverSettingsAtom = atom(getDefaultServerSettings());
4951

5052
// Derived atom for coins earned today
5153
export const coinsEarnedTodayAtom = atom((get) => {

lib/env.server.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,9 @@ export function init() {
2424
)
2525
.join("\n ")
2626

27-
console.error(
27+
throw new Error(
2828
`Missing environment variables:\n ${errorMessage}`,
2929
)
30-
process.exit(1)
3130
}
3231
}
3332
}

lib/types.ts

+9
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ export const getDefaultSettings = (): Settings => ({
130130
profile: {}
131131
});
132132

133+
export const getDefaultServerSettings = (): ServerSettings => ({
134+
isDemo: false
135+
})
136+
133137
// Map of data types to their default values
134138
export const DATA_DEFAULTS = {
135139
wishlist: getDefaultWishlistData,
@@ -178,4 +182,9 @@ export interface JotaiHydrateInitialValues {
178182
habits: HabitsData;
179183
wishlist: WishlistData;
180184
users: UserData;
185+
serverSettings: ServerSettings;
181186
}
187+
188+
export interface ServerSettings {
189+
isDemo: boolean
190+
}

lib/utils.test.ts

+4-10
Original file line numberDiff line numberDiff line change
@@ -535,13 +535,8 @@ describe('isHabitDueToday', () => {
535535

536536
test('should return false for invalid recurrence rule', () => {
537537
const habit = testHabit('INVALID_RRULE')
538-
// Mock console.error to prevent test output pollution
539-
const consoleSpy = spyOn(console, 'error').mockImplementation(() => { })
540-
541-
// Expect the function to throw an error
542-
expect(() => isHabitDueToday({ habit, timezone: 'UTC' })).toThrow()
543-
544-
consoleSpy.mockRestore()
538+
const consoleSpy = spyOn(console, 'error').mockImplementation(() => {})
539+
expect(isHabitDueToday({ habit, timezone: 'UTC' })).toBe(false)
545540
})
546541
})
547542

@@ -653,8 +648,7 @@ describe('isHabitDue', () => {
653648
test('should return false for invalid recurrence rule', () => {
654649
const habit = testHabit('INVALID_RRULE')
655650
const date = DateTime.fromISO('2024-01-01T00:00:00Z')
656-
const consoleSpy = spyOn(console, 'error').mockImplementation(() => { })
657-
expect(() => isHabitDue({ habit, timezone: 'UTC', date })).toThrow()
658-
consoleSpy.mockRestore()
651+
const consoleSpy = spyOn(console, 'error').mockImplementation(() => {})
652+
expect(isHabitDue({ habit, timezone: 'UTC', date })).toBe(false)
659653
})
660654
})

0 commit comments

Comments
 (0)