mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
Added settings, enabled calendar
This commit is contained in:
56
Budfile
Executable file
56
Budfile
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
bump_version() {
|
||||||
|
echo "Which version part would you like to bump? ([M]ajor/[m]inor/[p]atch)"
|
||||||
|
read -r version_part
|
||||||
|
|
||||||
|
# Get current version
|
||||||
|
current_version=$(node -p "require('./package.json').version")
|
||||||
|
IFS='.' read -r major minor patch <<<"$current_version"
|
||||||
|
|
||||||
|
# Calculate new version
|
||||||
|
if [[ "$version_part" =~ ^M$ ]]; then
|
||||||
|
new_version="$((major + 1)).0.0"
|
||||||
|
elif [[ "$version_part" =~ ^m$ ]]; then
|
||||||
|
new_version="$major.$((minor + 1)).0"
|
||||||
|
elif [[ "$version_part" =~ ^p$ ]]; then
|
||||||
|
new_version="$major.$minor.$((patch + 1))"
|
||||||
|
else
|
||||||
|
echo "Invalid version part. Please use M, m, or p"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update package.json with new version
|
||||||
|
sed -i "s/\"version\": \"$current_version\"/\"version\": \"$new_version\"/" package.json
|
||||||
|
echo "Version bumped from $current_version to $new_version"
|
||||||
|
}
|
||||||
|
|
||||||
|
commit() {
|
||||||
|
# Check if package.json is staged
|
||||||
|
if git diff --cached --name-only | grep -q "package.json"; then
|
||||||
|
# Get the new version from package.json
|
||||||
|
new_version=$(node -p "require('./package.json').version")
|
||||||
|
|
||||||
|
echo "package.json has been modified. Would you like to tag this release as v$new_version? (y/n)"
|
||||||
|
read -r response
|
||||||
|
|
||||||
|
if [[ "$response" =~ ^[Yy]$ ]]; then
|
||||||
|
git commit
|
||||||
|
git tag -a "v$new_version" -m "Release version $new_version"
|
||||||
|
echo "Created tag v$new_version"
|
||||||
|
else
|
||||||
|
git commit
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
git commit
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
docker_push() {
|
||||||
|
local version=$(node -p "require('./package.json').version")
|
||||||
|
docker tag habittrove dohsimpson/habittrove:latest
|
||||||
|
docker tag habittrove "dohsimpson/habittrove:v$version"
|
||||||
|
docker push dohsimpson/habittrove:latest
|
||||||
|
docker push "dohsimpson/habittrove:v$version"
|
||||||
|
echo "Pushed Docker images with tags: latest and v$version"
|
||||||
|
}
|
||||||
28
CHANGELOG.md
Normal file
28
CHANGELOG.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## Version 0.1.1
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Settings:
|
||||||
|
- added button to show settings
|
||||||
|
- coin display settings
|
||||||
|
- Features:
|
||||||
|
- Enabled calendar in large viewport
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- format big coin number
|
||||||
|
|
||||||
|
## Version 0.1.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Features:
|
||||||
|
- dashboard
|
||||||
|
- habits
|
||||||
|
- coins
|
||||||
|
- wishlist
|
||||||
|
- Demo
|
||||||
|
- README
|
||||||
|
- License
|
||||||
@@ -2,9 +2,21 @@
|
|||||||
|
|
||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { HabitsData, CoinsData, CoinTransaction, TransactionType, WishlistItemType } from '@/lib/types'
|
import {
|
||||||
|
HabitsData,
|
||||||
|
CoinsData,
|
||||||
|
CoinTransaction,
|
||||||
|
TransactionType,
|
||||||
|
WishlistItemType,
|
||||||
|
WishlistData,
|
||||||
|
Settings,
|
||||||
|
DataType,
|
||||||
|
DATA_DEFAULTS
|
||||||
|
} from '@/lib/types'
|
||||||
|
|
||||||
type DataType = 'wishlist' | 'habits' | 'coins'
|
function getDefaultData<T>(type: DataType): T {
|
||||||
|
return DATA_DEFAULTS[type]() as T;
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureDataDir() {
|
async function ensureDataDir() {
|
||||||
const dataDir = path.join(process.cwd(), 'data')
|
const dataDir = path.join(process.cwd(), 'data')
|
||||||
@@ -23,13 +35,8 @@ async function loadData<T>(type: DataType): Promise<T> {
|
|||||||
try {
|
try {
|
||||||
await fs.access(filePath)
|
await fs.access(filePath)
|
||||||
} catch {
|
} catch {
|
||||||
// File doesn't exist, create it with initial data
|
// File doesn't exist, create it with default data
|
||||||
const initialData = type === 'wishlist'
|
const initialData = getDefaultData(type)
|
||||||
? { items: [] }
|
|
||||||
: type === 'habits'
|
|
||||||
? { habits: [] }
|
|
||||||
: { balance: 0, transactions: [] }
|
|
||||||
|
|
||||||
await fs.writeFile(filePath, JSON.stringify(initialData, null, 2))
|
await fs.writeFile(filePath, JSON.stringify(initialData, null, 2))
|
||||||
return initialData as T
|
return initialData as T
|
||||||
}
|
}
|
||||||
@@ -37,13 +44,10 @@ async function loadData<T>(type: DataType): Promise<T> {
|
|||||||
// File exists, read and return its contents
|
// File exists, read and return its contents
|
||||||
const data = await fs.readFile(filePath, 'utf8')
|
const data = await fs.readFile(filePath, 'utf8')
|
||||||
const jsonData = JSON.parse(data)
|
const jsonData = JSON.parse(data)
|
||||||
return type === 'wishlist' ? jsonData.items : jsonData
|
return jsonData
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error loading ${type} data:`, error)
|
console.error(`Error loading ${type} data:`, error)
|
||||||
if (type === 'wishlist') return [] as T
|
return getDefaultData<T>(type)
|
||||||
if (type === 'habits') return { habits: [] } as T
|
|
||||||
if (type === 'coins') return { balance: 0, transactions: [] } as T
|
|
||||||
return {} as T
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +55,7 @@ async function saveData<T>(type: DataType, data: T): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
await ensureDataDir()
|
await ensureDataDir()
|
||||||
const filePath = path.join(process.cwd(), 'data', `${type}.json`)
|
const filePath = path.join(process.cwd(), 'data', `${type}.json`)
|
||||||
const saveData = type === 'wishlist' ? { items: data } : data
|
const saveData = data
|
||||||
await fs.writeFile(filePath, JSON.stringify(saveData, null, 2))
|
await fs.writeFile(filePath, JSON.stringify(saveData, null, 2))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error saving ${type} data:`, error)
|
console.error(`Error saving ${type} data:`, error)
|
||||||
@@ -60,11 +64,12 @@ async function saveData<T>(type: DataType, data: T): Promise<void> {
|
|||||||
|
|
||||||
// Wishlist specific functions
|
// Wishlist specific functions
|
||||||
export async function loadWishlistItems(): Promise<WishlistItemType[]> {
|
export async function loadWishlistItems(): Promise<WishlistItemType[]> {
|
||||||
return loadData<WishlistItemType[]>('wishlist')
|
const data = await loadData<WishlistData>('wishlist')
|
||||||
|
return data.items
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveWishlistItems(items: WishlistItemType[]): Promise<void> {
|
export async function saveWishlistItems(items: WishlistItemType[]): Promise<void> {
|
||||||
return saveData('wishlist', items)
|
return saveData('wishlist', { items })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Habits specific functions
|
// Habits specific functions
|
||||||
@@ -114,6 +119,26 @@ export async function addCoins(
|
|||||||
return newData
|
return newData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadSettings(): Promise<Settings> {
|
||||||
|
const defaultSettings: Settings = {
|
||||||
|
ui: {
|
||||||
|
useNumberFormatting: true,
|
||||||
|
useGrouping: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await loadData<Settings>('settings')
|
||||||
|
return { ...defaultSettings, ...data }
|
||||||
|
} catch {
|
||||||
|
return defaultSettings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveSettings(settings: Settings): Promise<void> {
|
||||||
|
return saveData('settings', settings)
|
||||||
|
}
|
||||||
|
|
||||||
export async function removeCoins(
|
export async function removeCoins(
|
||||||
amount: number,
|
amount: number,
|
||||||
description: string,
|
description: string,
|
||||||
|
|||||||
9
app/settings/layout.tsx
Normal file
9
app/settings/layout.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import Layout from '@/components/Layout'
|
||||||
|
|
||||||
|
export default function SettingsLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return <Layout>{children}</Layout>
|
||||||
|
}
|
||||||
63
app/settings/page.tsx
Normal file
63
app/settings/page.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { useSettings } from '@/hooks/useSettings'
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
const { settings, updateSettings } = useSettings()
|
||||||
|
|
||||||
|
if (!settings) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<h1 className="text-3xl font-bold mb-6">Settings</h1>
|
||||||
|
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>UI Settings</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="number-formatting">Number Formatting</Label>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Format large numbers (e.g., 1K, 1M, 1B)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="number-formatting"
|
||||||
|
checked={settings.ui.useNumberFormatting}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
updateSettings({
|
||||||
|
...settings,
|
||||||
|
ui: { ...settings.ui, useNumberFormatting: checked }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="number-grouping">Number Grouping</Label>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Use thousand separators (e.g., 1,000 vs 1000)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="number-grouping"
|
||||||
|
checked={settings.ui.useGrouping}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
updateSettings({
|
||||||
|
...settings,
|
||||||
|
ui: { ...settings.ui, useGrouping: checked }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Coins } from 'lucide-react'
|
import { Coins } from 'lucide-react'
|
||||||
|
import { formatNumber } from '@/lib/utils/formatNumber'
|
||||||
|
import { useSettings } from '@/hooks/useSettings'
|
||||||
|
|
||||||
export default function CoinBalance({ coinBalance }: { coinBalance: number }) {
|
export default function CoinBalance({ coinBalance }: { coinBalance: number }) {
|
||||||
|
const { settings } = useSettings()
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -10,7 +13,9 @@ export default function CoinBalance({ coinBalance }: { coinBalance: number }) {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<Coins className="h-12 w-12 text-yellow-400 mr-4" />
|
<Coins className="h-12 w-12 text-yellow-400 mr-4" />
|
||||||
<span className="text-4xl font-bold">{coinBalance}</span>
|
<span className="text-4xl font-bold">
|
||||||
|
{formatNumber({ amount: coinBalance, settings })}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { useSettings } from '@/hooks/useSettings'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { formatNumber } from '@/lib/utils/formatNumber'
|
||||||
import { History } from 'lucide-react'
|
import { History } from 'lucide-react'
|
||||||
import EmptyState from './EmptyState'
|
import EmptyState from './EmptyState'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
@@ -13,6 +15,7 @@ import Link from 'next/link'
|
|||||||
|
|
||||||
export default function CoinsManager() {
|
export default function CoinsManager() {
|
||||||
const { balance, transactions, addAmount, removeAmount } = useCoins()
|
const { balance, transactions, addAmount, removeAmount } = useCoins()
|
||||||
|
const { settings } = useSettings()
|
||||||
const DEFAULT_AMOUNT = '0'
|
const DEFAULT_AMOUNT = '0'
|
||||||
const [amount, setAmount] = useState(DEFAULT_AMOUNT)
|
const [amount, setAmount] = useState(DEFAULT_AMOUNT)
|
||||||
|
|
||||||
@@ -43,7 +46,7 @@ export default function CoinsManager() {
|
|||||||
<span className="text-2xl animate-bounce hover:animate-none cursor-default">🪙</span>
|
<span className="text-2xl animate-bounce hover:animate-none cursor-default">🪙</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-normal text-muted-foreground">Current Balance</div>
|
<div className="text-sm font-normal text-muted-foreground">Current Balance</div>
|
||||||
<div className="text-3xl font-bold">{balance} coins</div>
|
<div className="text-3xl font-bold">{formatNumber({ amount: balance, settings })} coins</div>
|
||||||
</div>
|
</div>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -108,7 +111,8 @@ export default function CoinsManager() {
|
|||||||
<div className="p-4 rounded-lg bg-green-100 dark:bg-green-900">
|
<div className="p-4 rounded-lg bg-green-100 dark:bg-green-900">
|
||||||
<div className="text-sm text-green-800 dark:text-green-100 mb-1">Total Earned</div>
|
<div className="text-sm text-green-800 dark:text-green-100 mb-1">Total Earned</div>
|
||||||
<div className="text-2xl font-bold text-green-900 dark:text-green-50">
|
<div className="text-2xl font-bold text-green-900 dark:text-green-50">
|
||||||
{transactions
|
{formatNumber({
|
||||||
|
amount: transactions
|
||||||
.filter(t => {
|
.filter(t => {
|
||||||
if (t.type === 'HABIT_COMPLETION' && t.relatedItemId) {
|
if (t.type === 'HABIT_COMPLETION' && t.relatedItemId) {
|
||||||
return !transactions.some(undoT =>
|
return !transactions.some(undoT =>
|
||||||
@@ -119,18 +123,21 @@ export default function CoinsManager() {
|
|||||||
return t.amount > 0 && t.type !== 'HABIT_UNDO'
|
return t.amount > 0 && t.type !== 'HABIT_UNDO'
|
||||||
})
|
})
|
||||||
.reduce((sum, t) => sum + t.amount, 0)
|
.reduce((sum, t) => sum + t.amount, 0)
|
||||||
} 🪙
|
, settings
|
||||||
|
})} 🪙
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 rounded-lg bg-red-100 dark:bg-red-900">
|
<div className="p-4 rounded-lg bg-red-100 dark:bg-red-900">
|
||||||
<div className="text-sm text-red-800 dark:text-red-100 mb-1">Total Spent</div>
|
<div className="text-sm text-red-800 dark:text-red-100 mb-1">Total Spent</div>
|
||||||
<div className="text-2xl font-bold text-red-900 dark:text-red-50">
|
<div className="text-2xl font-bold text-red-900 dark:text-red-50">
|
||||||
{Math.abs(
|
{formatNumber({
|
||||||
|
amount: Math.abs(
|
||||||
transactions
|
transactions
|
||||||
.filter(t => t.type === 'WISH_REDEMPTION' || t.type === 'MANUAL_ADJUSTMENT')
|
.filter(t => t.type === 'WISH_REDEMPTION' || t.type === 'MANUAL_ADJUSTMENT')
|
||||||
.reduce((sum, t) => sum + (t.amount < 0 ? t.amount : 0), 0)
|
.reduce((sum, t) => sum + (t.amount < 0 ? t.amount : 0), 0)
|
||||||
)} 🪙
|
), settings
|
||||||
|
})} 🪙
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default function HabitList() {
|
|||||||
}}
|
}}
|
||||||
onSave={async (habit) => {
|
onSave={async (habit) => {
|
||||||
if (editingHabit) {
|
if (editingHabit) {
|
||||||
await editHabit(editingHabit)
|
await editHabit({ ...habit, id: editingHabit.id })
|
||||||
} else {
|
} else {
|
||||||
await addHabit(habit)
|
await addHabit(habit)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Bell, Settings } from 'lucide-react'
|
import { Bell, Settings } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Logo } from '@/components/Logo'
|
import { Logo } from '@/components/Logo'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
className?: string
|
className?: string
|
||||||
@@ -12,14 +13,19 @@ export default function Header({ className }: HeaderProps) {
|
|||||||
<div className="mx-auto py-4 px-4 sm:px-6 lg:px-8">
|
<div className="mx-auto py-4 px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Logo />
|
<Logo />
|
||||||
{/* <div className="flex items-center"> */}
|
<div className="flex items-center">
|
||||||
{/* <Button variant="ghost" size="icon" className="mr-2"> */}
|
{/* <Button variant="ghost" size="icon" className="mr-2"> */}
|
||||||
{/* <Bell className="h-5 w-5" /> */}
|
{/* <Bell className="h-5 w-5" /> */}
|
||||||
{/* </Button> */}
|
{/* </Button> */}
|
||||||
{/* <Button variant="ghost" size="icon"> */}
|
<Link
|
||||||
{/* <Settings className="h-5 w-5" /> */}
|
href="/settings"
|
||||||
{/* </Button> */}
|
aria-label='settings'
|
||||||
{/* </div> */}
|
>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<Settings className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header >
|
</header >
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
import Header from './Header'
|
import Header from './Header'
|
||||||
import Sidebar from './Sidebar'
|
import Navigation from './Navigation'
|
||||||
import MobileNavigation from './MobileNavigation'
|
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen bg-gray-100 dark:bg-gray-900">
|
<div className="flex flex-col h-screen bg-gray-100 dark:bg-gray-900">
|
||||||
<Header className="sticky top-0 z-50" />
|
<Header className="sticky top-0 z-50" />
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
<Sidebar />
|
<Navigation />
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-100 dark:bg-gray-900">
|
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-100 dark:bg-gray-900">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
<MobileNavigation />
|
<Navigation isMobile />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import Link from 'next/link'
|
|
||||||
import { Home, Calendar, List, Gift, Coins } from 'lucide-react'
|
|
||||||
|
|
||||||
const navItems = [
|
|
||||||
{ icon: Home, label: 'Dashboard', href: '/' },
|
|
||||||
{ icon: List, label: 'Habits', href: '/habits' },
|
|
||||||
{ icon: Calendar, label: 'Calendar', href: '/calendar' },
|
|
||||||
{ icon: Gift, label: 'Wishlist', href: '/wishlist' },
|
|
||||||
{ icon: Coins, label: 'Coins', href: '/coins' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function MobileNavigation() {
|
|
||||||
return (
|
|
||||||
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 shadow-lg">
|
|
||||||
<div className="flex justify-around">
|
|
||||||
{navItems.map((item) => (
|
|
||||||
<Link
|
|
||||||
key={item.label}
|
|
||||||
href={item.href}
|
|
||||||
className="flex flex-col items-center py-2 text-gray-600 dark:text-gray-300 hover:text-blue-500 dark:hover:text-blue-400"
|
|
||||||
>
|
|
||||||
<item.icon className="h-6 w-6" />
|
|
||||||
<span className="text-xs mt-1">{item.label}</span>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,15 +1,39 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { Home, Calendar, List, Gift, Coins } from 'lucide-react'
|
import { Home, Calendar, List, Gift, Coins, Settings } from 'lucide-react'
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ icon: Home, label: 'Dashboard', href: '/' },
|
{ icon: Home, label: 'Dashboard', href: '/' },
|
||||||
{ icon: List, label: 'Habits', href: '/habits' },
|
{ icon: List, label: 'Habits', href: '/habits' },
|
||||||
// { icon: Calendar, label: 'Calendar', href: '/calendar' },
|
{ icon: Calendar, label: 'Calendar', href: '/calendar' },
|
||||||
{ icon: Gift, label: 'Wishlist', href: '/wishlist' },
|
{ icon: Gift, label: 'Wishlist', href: '/wishlist' },
|
||||||
{ icon: Coins, label: 'Coins', href: '/coins' },
|
{ icon: Coins, label: 'Coins', href: '/coins' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function Sidebar() {
|
interface NavigationProps {
|
||||||
|
className?: string
|
||||||
|
isMobile?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Navigation({ className, isMobile = false }: NavigationProps) {
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 shadow-lg">
|
||||||
|
<div className="flex justify-around">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.label}
|
||||||
|
href={item.href}
|
||||||
|
className="flex flex-col items-center py-2 text-gray-600 dark:text-gray-300 hover:text-blue-500 dark:hover:text-blue-400"
|
||||||
|
>
|
||||||
|
<item.icon className="h-6 w-6" />
|
||||||
|
<span className="text-xs mt-1">{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hidden lg:flex lg:flex-shrink-0">
|
<div className="hidden lg:flex lg:flex-shrink-0">
|
||||||
<div className="flex flex-col w-64">
|
<div className="flex flex-col w-64">
|
||||||
@@ -33,4 +57,3 @@ export default function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
29
components/ui/switch.tsx
Normal file
29
components/ui/switch.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Switch = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SwitchPrimitives.Root
|
||||||
|
className={cn(
|
||||||
|
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SwitchPrimitives.Thumb
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitives.Root>
|
||||||
|
))
|
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
23
hooks/useSettings.ts
Normal file
23
hooks/useSettings.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { getDefaultSettings, Settings } from '@/lib/types'
|
||||||
|
import { loadSettings, saveSettings } from '@/app/actions/data'
|
||||||
|
|
||||||
|
export function useSettings() {
|
||||||
|
const [settings, setSettings] = useState<Settings>(getDefaultSettings()) // TODO: do we need to initialize the settings here?
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadSettings().then(setSettings)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const updateSettings = async (newSettings: Settings) => {
|
||||||
|
await saveSettings(newSettings)
|
||||||
|
setSettings(newSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
settings,
|
||||||
|
updateSettings,
|
||||||
|
}
|
||||||
|
}
|
||||||
47
lib/types.ts
47
lib/types.ts
@@ -33,3 +33,50 @@ export interface CoinsData {
|
|||||||
balance: number;
|
balance: number;
|
||||||
transactions: CoinTransaction[];
|
transactions: CoinTransaction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default value functions
|
||||||
|
// Data container types
|
||||||
|
export interface WishlistData {
|
||||||
|
items: WishlistItemType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default value functions
|
||||||
|
export const getDefaultHabitsData = (): HabitsData => ({
|
||||||
|
habits: []
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getDefaultCoinsData = (): CoinsData => ({
|
||||||
|
balance: 0,
|
||||||
|
transactions: []
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getDefaultWishlistData = (): WishlistData => ({
|
||||||
|
items: []
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getDefaultSettings = (): Settings => ({
|
||||||
|
ui: {
|
||||||
|
useNumberFormatting: true,
|
||||||
|
useGrouping: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Map of data types to their default values
|
||||||
|
export const DATA_DEFAULTS = {
|
||||||
|
wishlist: getDefaultWishlistData,
|
||||||
|
habits: getDefaultHabitsData,
|
||||||
|
coins: getDefaultCoinsData,
|
||||||
|
settings: getDefaultSettings,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Type for all possible data types
|
||||||
|
export type DataType = keyof typeof DATA_DEFAULTS;
|
||||||
|
|
||||||
|
export interface UISettings {
|
||||||
|
useNumberFormatting: boolean;
|
||||||
|
useGrouping: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
ui: UISettings;
|
||||||
|
}
|
||||||
|
|||||||
37
lib/utils/formatNumber.ts
Normal file
37
lib/utils/formatNumber.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Settings } from "../types";
|
||||||
|
|
||||||
|
function formatWithLocale(amount: number, useGrouping: boolean, maximumFractionDigits?: number): string {
|
||||||
|
return amount.toLocaleString(undefined, {
|
||||||
|
maximumFractionDigits,
|
||||||
|
useGrouping,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatNumber({ amount, settings }: { amount: number, settings: Settings }): string {
|
||||||
|
const useFormatting = settings?.ui.useNumberFormatting ?? true;
|
||||||
|
const useGrouping = settings?.ui.useGrouping ?? true;
|
||||||
|
|
||||||
|
if (!useFormatting) {
|
||||||
|
return useGrouping ? formatWithLocale(amount, true) : amount.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const absNum = Math.abs(amount);
|
||||||
|
|
||||||
|
if (absNum >= 1e12) {
|
||||||
|
return amount.toExponential(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absNum >= 1e9) {
|
||||||
|
return formatWithLocale(amount / 1e9, useGrouping, 1) + 'B';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absNum >= 1e6) {
|
||||||
|
return formatWithLocale(amount / 1e6, useGrouping, 1) + 'M';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absNum >= 1e3) {
|
||||||
|
return formatWithLocale(amount / 1e3, useGrouping, 1) + 'K';
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatWithLocale(amount, useGrouping);
|
||||||
|
}
|
||||||
29
package-lock.json
generated
29
package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"@radix-ui/react-progress": "^1.1.1",
|
"@radix-ui/react-progress": "^1.1.1",
|
||||||
"@radix-ui/react-select": "^2.1.4",
|
"@radix-ui/react-select": "^2.1.4",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
"@radix-ui/react-toast": "^1.2.4",
|
"@radix-ui/react-toast": "^1.2.4",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@uiw/react-heat-map": "^2.3.2",
|
"@uiw/react-heat-map": "^2.3.2",
|
||||||
@@ -1299,6 +1300,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-switch": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.0",
|
||||||
|
"@radix-ui/react-use-size": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-toast": {
|
"node_modules/@radix-ui/react-toast": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.4.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "habittrove",
|
"name": "habittrove",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
@@ -8,8 +8,7 @@
|
|||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"docker-build": "docker build -t habittrove .",
|
"docker-build": "docker build -t habittrove .",
|
||||||
"docker-run": "docker run -p 3000:3000 -v $(pwd)/data:/app/data habittrove",
|
"docker-run": "docker run -p 3000:3000 -v $(pwd)/data:/app/data habittrove"
|
||||||
"docker-push": "docker tag habittrove dohsimpson/habittrove && docker push dohsimpson/habittrove"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/font": "^14.2.15",
|
"@next/font": "^14.2.15",
|
||||||
@@ -18,6 +17,7 @@
|
|||||||
"@radix-ui/react-progress": "^1.1.1",
|
"@radix-ui/react-progress": "^1.1.1",
|
||||||
"@radix-ui/react-select": "^2.1.4",
|
"@radix-ui/react-select": "^2.1.4",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
"@radix-ui/react-toast": "^1.2.4",
|
"@radix-ui/react-toast": "^1.2.4",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@uiw/react-heat-map": "^2.3.2",
|
"@uiw/react-heat-map": "^2.3.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user