Initial commit

This commit is contained in:
Doh
2024-12-30 13:20:12 -05:00
committed by dohsimpson
commit c4f0db329b
71 changed files with 11302 additions and 0 deletions

194
hooks/use-toast.ts Normal file
View File

@@ -0,0 +1,194 @@
"use client"
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

67
hooks/useCoins.tsx Normal file
View File

@@ -0,0 +1,67 @@
import { useState, useEffect } from 'react'
import { loadCoinsData, addCoins, removeCoins } from '@/app/actions/data'
import { toast } from '@/hooks/use-toast'
import { CoinTransaction, TransactionType } from '@/lib/types'
export function useCoins() {
const [balance, setBalance] = useState(0)
const [transactions, setTransactions] = useState<CoinTransaction[]>([])
useEffect(() => {
fetchCoins()
}, [])
const fetchCoins = async () => {
const data = await loadCoinsData()
setBalance(data.balance)
setTransactions(data.transactions || [])
}
const addAmount = async (
amount: number,
description: string,
type: TransactionType = 'MANUAL_ADJUSTMENT',
relatedItemId?: string
) => {
if (isNaN(amount) || amount <= 0) {
toast({
title: "Invalid amount",
description: "Please enter a valid positive number"
})
return null
}
const data = await addCoins(amount, description, type, relatedItemId)
setBalance(data.balance)
setTransactions(data.transactions)
return data
}
const removeAmount = async (
amount: number,
description: string,
type: TransactionType = 'MANUAL_ADJUSTMENT',
relatedItemId?: string
) => {
if (isNaN(amount) || amount <= 0) {
toast({
title: "Invalid amount",
description: "Please enter a valid positive number"
})
return null
}
const data = await removeCoins(amount, description, type, relatedItemId)
setBalance(data.balance)
setTransactions(data.transactions)
return data
}
return {
balance,
transactions,
addAmount,
removeAmount,
fetchCoins
}
}

104
hooks/useHabits.tsx Normal file
View File

@@ -0,0 +1,104 @@
import { useState, useEffect } from 'react'
import { loadHabitsData, saveHabitsData, addCoins, removeCoins } from '@/app/actions/data'
import { toast } from '@/hooks/use-toast'
import { ToastAction } from '@/components/ui/toast'
import { Undo2 } from 'lucide-react'
import { Habit } from '@/lib/types'
export function useHabits() {
const [habits, setHabits] = useState<Habit[]>([])
useEffect(() => {
fetchHabits()
}, [])
const fetchHabits = async () => {
const data = await loadHabitsData()
setHabits(data.habits)
}
const addHabit = async (habit: Omit<Habit, 'id'>) => {
const newHabit = { ...habit, id: Date.now().toString() }
const newHabits = [...habits, newHabit]
setHabits(newHabits)
await saveHabitsData({ habits: newHabits })
}
const editHabit = async (updatedHabit: Habit) => {
const newHabits = habits.map(habit =>
habit.id === updatedHabit.id ? updatedHabit : habit
)
setHabits(newHabits)
await saveHabitsData({ habits: newHabits })
}
const deleteHabit = async (id: string) => {
const newHabits = habits.filter(habit => habit.id !== id)
setHabits(newHabits)
await saveHabitsData({ habits: newHabits })
}
const completeHabit = async (habit: Habit) => {
const today = new Date().toISOString().split('T')[0]
if (!habit.completions.includes(today)) {
const updatedHabit = {
...habit,
completions: [...habit.completions, today]
}
const updatedHabits = habits.map(h =>
h.id === habit.id ? updatedHabit : h
)
await saveHabitsData({ habits: updatedHabits })
const coinsData = await addCoins(habit.coinReward, `Completed habit: ${habit.name}`, 'HABIT_COMPLETION', habit.id)
setHabits(updatedHabits)
toast({
title: "Habit completed!",
description: `You earned ${habit.coinReward} coins.`,
action: <ToastAction altText="Undo" className="gap-2" onClick={() => undoComplete(habit)}><Undo2 className="h-4 w-4" />Undo</ToastAction>
})
return coinsData.balance
} else {
toast({
title: "Habit already completed",
description: "You've already completed this habit today.",
variant: "destructive",
})
return null
}
}
const undoComplete = async (habit: Habit) => {
const today = new Date().toISOString().split('T')[0]
const updatedHabit = {
...habit,
completions: habit.completions.filter(date => date !== today)
}
const updatedHabits = habits.map(h =>
h.id === habit.id ? updatedHabit : h
)
await saveHabitsData({ habits: updatedHabits })
const coinsData = await removeCoins(habit.coinReward, `Undid habit completion: ${habit.name}`, 'HABIT_UNDO', habit.id)
setHabits(updatedHabits)
toast({
title: "Completion undone",
description: `${habit.coinReward} coins have been deducted.`,
action: <ToastAction altText="Redo" onClick={() => completeHabit(habit)}><Undo2 className="h-4 w-4" />Undo</ToastAction>
})
return coinsData.balance
}
return {
habits,
addHabit,
editHabit,
deleteHabit,
completeHabit,
undoComplete
}
}

84
hooks/useWishlist.tsx Normal file
View File

@@ -0,0 +1,84 @@
import { useState, useEffect } from 'react'
import { loadWishlistItems, saveWishlistItems } from '@/app/actions/data'
import { toast } from '@/hooks/use-toast'
import { WishlistItemType } from '@/lib/types'
import { useCoins } from '@/hooks/useCoins'
import { celebrations } from '@/utils/celebrations'
export function useWishlist() {
const [wishlistItems, setWishlistItems] = useState<WishlistItemType[]>([])
const { balance, removeAmount } = useCoins()
useEffect(() => {
fetchWishlistItems()
}, [])
const fetchWishlistItems = async () => {
const items = await loadWishlistItems()
setWishlistItems(items)
}
const addWishlistItem = async (item: Omit<WishlistItemType, 'id'>) => {
const newItem = { ...item, id: Date.now().toString() }
const newItems = [...wishlistItems, newItem]
setWishlistItems(newItems)
await saveWishlistItems(newItems)
}
const editWishlistItem = async (updatedItem: WishlistItemType) => {
const newItems = wishlistItems.map(item =>
item.id === updatedItem.id ? updatedItem : item
)
setWishlistItems(newItems)
await saveWishlistItems(newItems)
}
const deleteWishlistItem = async (id: string) => {
const newItems = wishlistItems.filter(item => item.id !== id)
setWishlistItems(newItems)
await saveWishlistItems(newItems)
}
const redeemWishlistItem = async (item: WishlistItemType) => {
if (balance >= item.coinCost) {
await removeAmount(
item.coinCost,
`Redeemed reward: ${item.name}`,
'WISH_REDEMPTION',
item.id
)
// Randomly choose a celebration effect
const celebrationEffects = [
celebrations.basic,
celebrations.fireworks,
celebrations.shower
]
const randomEffect = celebrationEffects[Math.floor(Math.random() * celebrationEffects.length)]
randomEffect()
toast({
title: "🎉 Reward Redeemed!",
description: `You've redeemed "${item.name}" for ${item.coinCost} coins.`,
})
return true
} else {
toast({
title: "Not enough coins",
description: `You need ${item.coinCost - balance} more coins to redeem this reward.`,
variant: "destructive",
})
return false
}
}
return {
wishlistItems,
addWishlistItem,
editWishlistItem,
deleteWishlistItem,
redeemWishlistItem,
canRedeem: (cost: number) => balance >= cost
}
}