mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-03-09 03:59:50 +01:00
feat: freehand drawing capability and card layout improvements and v0.2.29 release (#180)
This commit is contained in:
@@ -1,23 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { RRule, RRuleSet, rrulestr } from 'rrule'
|
||||
import { RRule } from 'rrule'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { settingsAtom, browserSettingsAtom, usersAtom, currentUserAtom } from '@/lib/atoms'
|
||||
import { settingsAtom, usersAtom, currentUserAtom } from '@/lib/atoms'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Zap } from 'lucide-react'
|
||||
import { Zap, Brush } from 'lucide-react'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Habit, SafeUser } from '@/lib/types'
|
||||
import { Habit } from '@/lib/types'
|
||||
import EmojiPickerButton from './EmojiPickerButton'
|
||||
import ModalOverlay from './ModalOverlay' // Import the new component
|
||||
import { convertHumanReadableFrequencyToMachineReadable, convertMachineReadableFrequencyToHumanReadable, d2s, d2t, serializeRRule } from '@/lib/utils'
|
||||
import { INITIAL_DUE, INITIAL_RECURRENCE_RULE, QUICK_DATES, RECURRENCE_RULE_MAP, MAX_COIN_LIMIT } from '@/lib/constants'
|
||||
import DrawingModal from './DrawingModal'
|
||||
import DrawingDisplay from './DrawingDisplay'
|
||||
import { convertHumanReadableFrequencyToMachineReadable, convertMachineReadableFrequencyToHumanReadable, d2t, serializeRRule } from '@/lib/utils'
|
||||
import { INITIAL_DUE, INITIAL_RECURRENCE_RULE, QUICK_DATES, MAX_COIN_LIMIT } from '@/lib/constants'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
|
||||
@@ -49,6 +51,8 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
const [selectedUserIds, setSelectedUserIds] = useState<string[]>((habit?.userIds || []).filter(id => id !== currentUser?.id))
|
||||
const [usersData] = useAtom(usersAtom)
|
||||
const users = usersData.users
|
||||
const [drawing, setDrawing] = useState<string>(habit?.drawing || '')
|
||||
const [isDrawingModalOpen, setIsDrawingModalOpen] = useState(false)
|
||||
|
||||
function getFrequencyUpdate() {
|
||||
if (ruleText === initialRuleText && habit?.frequency) {
|
||||
@@ -83,14 +87,19 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
targetCompletions: targetCompletions > 1 ? targetCompletions : undefined,
|
||||
completions: habit?.completions || [],
|
||||
frequency: getFrequencyUpdate(),
|
||||
userIds: selectedUserIds.length > 0 ? selectedUserIds.concat(currentUser?.id || []) : (currentUser && [currentUser.id])
|
||||
userIds: selectedUserIds.length > 0 ? selectedUserIds.concat(currentUser?.id || []) : (currentUser && [currentUser.id]),
|
||||
drawing: drawing && drawing !== '[]' ? drawing : undefined
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalOverlay />
|
||||
<Dialog open={true} onOpenChange={onClose} modal={false}>
|
||||
<Dialog open={true} onOpenChange={(open) => {
|
||||
if (!open && !isDrawingModalOpen) {
|
||||
onClose()
|
||||
}
|
||||
}} modal={false}>
|
||||
<DialogContent> {/* DialogContent from shadcn/ui is typically z-50, ModalOverlay is z-40 */}
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
@@ -290,6 +299,38 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">
|
||||
{t('drawingLabel')}
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<div className="flex gap-4 items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsDrawingModalOpen(true)
|
||||
}}
|
||||
className="flex-1 justify-start"
|
||||
>
|
||||
<Brush className="h-4 w-4 mr-2" />
|
||||
{drawing ? t('editDrawing') : t('addDrawing')}
|
||||
</Button>
|
||||
{drawing && (
|
||||
<div className="flex-shrink-0">
|
||||
<DrawingDisplay
|
||||
drawingData={drawing}
|
||||
width={80}
|
||||
height={53}
|
||||
className=""
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{users && users.length > 1 && (
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
@@ -333,6 +374,13 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<DrawingModal
|
||||
isOpen={isDrawingModalOpen}
|
||||
onClose={() => setIsDrawingModalOpen(false)}
|
||||
onSave={(drawingData) => setDrawing(drawingData)}
|
||||
initialDrawing={drawing}
|
||||
title={name}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ import { Textarea } from '@/components/ui/textarea'
|
||||
import { WishlistItemType } from '@/lib/types'
|
||||
import EmojiPickerButton from './EmojiPickerButton'
|
||||
import ModalOverlay from './ModalOverlay'
|
||||
import DrawingModal from './DrawingModal'
|
||||
import DrawingDisplay from './DrawingDisplay'
|
||||
import { Brush } from 'lucide-react'
|
||||
import { MAX_COIN_LIMIT } from '@/lib/constants'
|
||||
|
||||
interface AddEditWishlistItemModalProps {
|
||||
@@ -40,6 +43,8 @@ export default function AddEditWishlistItemModal({
|
||||
const [selectedUserIds, setSelectedUserIds] = useState<string[]>((editingItem?.userIds || []).filter(id => id !== currentUser?.id))
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({})
|
||||
const [usersData] = useAtom(usersAtom)
|
||||
const [drawing, setDrawing] = useState<string>(editingItem?.drawing || '')
|
||||
const [isDrawingModalOpen, setIsDrawingModalOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (editingItem) {
|
||||
@@ -48,12 +53,14 @@ export default function AddEditWishlistItemModal({
|
||||
setCoinCost(editingItem.coinCost)
|
||||
setTargetCompletions(editingItem.targetCompletions)
|
||||
setLink(editingItem.link || '')
|
||||
setDrawing(editingItem.drawing || '')
|
||||
} else {
|
||||
setName('')
|
||||
setDescription('')
|
||||
setCoinCost(1)
|
||||
setTargetCompletions(undefined)
|
||||
setLink('')
|
||||
setDrawing('')
|
||||
}
|
||||
setErrors({})
|
||||
}, [editingItem])
|
||||
@@ -102,7 +109,8 @@ export default function AddEditWishlistItemModal({
|
||||
coinCost,
|
||||
targetCompletions: targetCompletions || undefined,
|
||||
link: link.trim() || undefined,
|
||||
userIds: selectedUserIds.length > 0 ? selectedUserIds.concat(currentUser?.id || []) : (currentUser && [currentUser.id])
|
||||
userIds: selectedUserIds.length > 0 ? selectedUserIds.concat(currentUser?.id || []) : (currentUser && [currentUser.id]),
|
||||
drawing: drawing && drawing !== '[]' ? drawing : undefined
|
||||
}
|
||||
|
||||
if (editingItem) {
|
||||
@@ -118,7 +126,11 @@ export default function AddEditWishlistItemModal({
|
||||
return (
|
||||
<>
|
||||
<ModalOverlay />
|
||||
<Dialog open={true} onOpenChange={handleClose} modal={false}>
|
||||
<Dialog open={true} onOpenChange={(open) => {
|
||||
if (!open && !isDrawingModalOpen) {
|
||||
handleClose()
|
||||
}
|
||||
}} modal={false}>
|
||||
<DialogContent> {/* DialogContent from shadcn/ui is typically z-50, ModalOverlay is z-40 */}
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingItem ? t('editTitle') : t('addTitle')}</DialogTitle>
|
||||
@@ -269,6 +281,38 @@ export default function AddEditWishlistItemModal({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">
|
||||
{t('drawingLabel')}
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<div className="flex gap-4 items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsDrawingModalOpen(true)
|
||||
}}
|
||||
className="flex-1 justify-start"
|
||||
>
|
||||
<Brush className="h-4 w-4 mr-2" />
|
||||
{drawing ? t('editDrawing') : t('addDrawing')}
|
||||
</Button>
|
||||
{drawing && (
|
||||
<div className="flex-shrink-0">
|
||||
<DrawingDisplay
|
||||
drawingData={drawing}
|
||||
width={80}
|
||||
height={53}
|
||||
className=""
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{usersData.users && usersData.users.length > 1 && (
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
@@ -308,6 +352,13 @@ export default function AddEditWishlistItemModal({
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<DrawingModal
|
||||
isOpen={isDrawingModalOpen}
|
||||
onClose={() => setIsDrawingModalOpen(false)}
|
||||
onSave={(drawingData) => setDrawing(drawingData)}
|
||||
initialDrawing={drawing}
|
||||
title={name}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import { useState, useEffect, useRef } from 'react' // Import useEffect, useRef
|
||||
import { useSearchParams } from 'next/navigation' // Import useSearchParams
|
||||
import { t2d, d2s, getNow, isSameDate } from '@/lib/utils'
|
||||
import { t2d, d2s } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { FormattedNumber } from '@/components/FormattedNumber'
|
||||
import { History, Pencil } from 'lucide-react'
|
||||
import { History } from 'lucide-react'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import EmptyState from './EmptyState'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { useAtom } from 'jotai'
|
||||
import { completedHabitsMapAtom, habitsAtom, habitsByDateFamily } from '@/lib/atoms'
|
||||
import { completedHabitsMapAtom, habitsByDateFamily } from '@/lib/atoms'
|
||||
import { getTodayInTimezone } from '@/lib/utils'
|
||||
// import { useHabits } from '@/hooks/useHabits' // Not used
|
||||
import { settingsAtom } from '@/lib/atoms'
|
||||
|
||||
@@ -3,8 +3,6 @@ import CompletionCountBadge from './CompletionCountBadge'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu"
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -12,7 +10,7 @@ import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { pomodoroAtom, settingsAtom, completedHabitsMapAtom, browserSettingsAtom, BrowserSettings, hasTasksAtom } from '@/lib/atoms'
|
||||
import { pomodoroAtom, settingsAtom, completedHabitsMapAtom, browserSettingsAtom, hasTasksAtom } from '@/lib/atoms'
|
||||
import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, isHabitDue, isTaskOverdue } from '@/lib/utils'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -23,7 +21,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Settings, WishlistItemType } from '@/lib/types'
|
||||
import { WishlistItemType } from '@/lib/types'
|
||||
import { Habit } from '@/lib/types'
|
||||
import Linkify from './linkify'
|
||||
import { useHabits } from '@/hooks/useHabits'
|
||||
@@ -31,6 +29,7 @@ import AddEditHabitModal from './AddEditHabitModal'
|
||||
import ConfirmDialog from './ConfirmDialog'
|
||||
import { Button } from './ui/button'
|
||||
import { HabitContextMenuItems } from './HabitContextMenuItems'
|
||||
import DrawingDisplay from './DrawingDisplay'
|
||||
|
||||
interface UpcomingItemsProps {
|
||||
habits: Habit[]
|
||||
@@ -255,6 +254,16 @@ const ItemSection = ({
|
||||
{habit.name}
|
||||
</span>
|
||||
</Link>
|
||||
{habit.drawing && (
|
||||
<div className="ml-2 pr-2">
|
||||
<DrawingDisplay
|
||||
drawingData={habit.drawing}
|
||||
width={40}
|
||||
height={26}
|
||||
className="border-0"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
@@ -473,9 +482,19 @@ export default function DailyOverview({
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm">
|
||||
<Linkify>{item.name}</Linkify>
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">
|
||||
<Linkify>{item.name}</Linkify>
|
||||
</span>
|
||||
{item.drawing && (
|
||||
<DrawingDisplay
|
||||
drawingData={item.drawing}
|
||||
width={40}
|
||||
height={26}
|
||||
className="border-0"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs flex items-center">
|
||||
<Coins className={cn(
|
||||
"h-3 w-3 mr-1 transition-all",
|
||||
|
||||
241
components/DrawingCanvas.tsx
Normal file
241
components/DrawingCanvas.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Undo2, Trash2, Palette } from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
interface DrawingCanvasProps {
|
||||
initialDrawing?: string
|
||||
onSave: (drawingData: string) => void
|
||||
onClear?: () => void
|
||||
}
|
||||
|
||||
export default function DrawingCanvas({ initialDrawing, onSave, onClear }: DrawingCanvasProps) {
|
||||
const t = useTranslations('DrawingModal')
|
||||
const [drawingHistory, setDrawingHistory] = useState<Array<{
|
||||
color: string
|
||||
thickness: number
|
||||
points: Array<{ x: number; y: number }>
|
||||
}>>([])
|
||||
const [isDrawing, setIsDrawing] = useState(false)
|
||||
const [color, setColor] = useState('#000000')
|
||||
const [thickness, setThickness] = useState(4)
|
||||
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const contextRef = useRef<CanvasRenderingContext2D | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
|
||||
const context = canvas.getContext('2d')
|
||||
if (!context) return
|
||||
|
||||
context.lineCap = 'round'
|
||||
context.lineJoin = 'round'
|
||||
contextRef.current = context
|
||||
|
||||
const resizeCanvas = () => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
canvas.width = rect.width
|
||||
canvas.height = rect.height
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resizeCanvas)
|
||||
resizeCanvas()
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resizeCanvas)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (initialDrawing) {
|
||||
try {
|
||||
const loadedData = JSON.parse(initialDrawing)
|
||||
if (Array.isArray(loadedData)) {
|
||||
setDrawingHistory(loadedData)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load initial drawing data')
|
||||
}
|
||||
}
|
||||
}, [initialDrawing])
|
||||
|
||||
useEffect(() => {
|
||||
redrawCanvas()
|
||||
}, [drawingHistory])
|
||||
|
||||
const getMousePos = (event: React.MouseEvent) => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return { x: 0, y: 0 }
|
||||
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const scaleX = canvas.width / rect.width
|
||||
const scaleY = canvas.height / rect.height
|
||||
return {
|
||||
x: (event.clientX - rect.left) * scaleX,
|
||||
y: (event.clientY - rect.top) * scaleY
|
||||
}
|
||||
}
|
||||
|
||||
const startDrawing = (event: React.MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const { x, y } = getMousePos(event)
|
||||
setIsDrawing(true)
|
||||
contextRef.current?.beginPath()
|
||||
contextRef.current?.moveTo(x, y)
|
||||
|
||||
setDrawingHistory(prevHistory => [
|
||||
...prevHistory,
|
||||
{ color, thickness, points: [{ x, y }] }
|
||||
])
|
||||
}
|
||||
|
||||
const draw = (event: React.MouseEvent) => {
|
||||
if (!isDrawing || !contextRef.current) return
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const { x, y } = getMousePos(event)
|
||||
contextRef.current.lineTo(x, y)
|
||||
contextRef.current.strokeStyle = color
|
||||
contextRef.current.lineWidth = thickness
|
||||
contextRef.current.stroke()
|
||||
|
||||
setDrawingHistory(prevHistory => {
|
||||
const lastStroke = prevHistory[prevHistory.length - 1]
|
||||
if (lastStroke) {
|
||||
lastStroke.points.push({ x, y })
|
||||
}
|
||||
return [...prevHistory]
|
||||
})
|
||||
}
|
||||
|
||||
const stopDrawing = (event?: React.MouseEvent) => {
|
||||
if (event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
setIsDrawing(false)
|
||||
contextRef.current?.closePath()
|
||||
}
|
||||
|
||||
const redrawCanvas = () => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas || !contextRef.current) return
|
||||
|
||||
const context = contextRef.current
|
||||
context.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
drawingHistory.forEach(stroke => {
|
||||
if (stroke.points.length === 0) return
|
||||
context.beginPath()
|
||||
context.strokeStyle = stroke.color
|
||||
context.lineWidth = stroke.thickness
|
||||
context.moveTo(stroke.points[0].x, stroke.points[0].y)
|
||||
stroke.points.forEach(point => {
|
||||
context.lineTo(point.x, point.y)
|
||||
})
|
||||
context.stroke()
|
||||
})
|
||||
}
|
||||
|
||||
const handleUndo = () => {
|
||||
setDrawingHistory(prevHistory => {
|
||||
const newHistory = [...prevHistory]
|
||||
newHistory.pop()
|
||||
return newHistory
|
||||
})
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
setDrawingHistory([])
|
||||
onClear?.()
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
const jsonString = drawingHistory.length > 0 ? JSON.stringify(drawingHistory) : ''
|
||||
onSave(jsonString)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
onMouseDown={startDrawing}
|
||||
onMouseMove={draw}
|
||||
onMouseUp={(e) => stopDrawing(e)}
|
||||
onMouseLeave={(e) => stopDrawing(e)}
|
||||
className="border border-gray-300 rounded-lg bg-white touch-none w-full h-80 cursor-crosshair"
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="colorPicker" className="text-sm font-medium">
|
||||
{t('colorLabel')}
|
||||
</Label>
|
||||
<div className="flex items-center gap-1">
|
||||
<Palette className="h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="color"
|
||||
id="colorPicker"
|
||||
value={color}
|
||||
onChange={(e) => setColor(e.target.value)}
|
||||
className="w-8 h-8 border-2 border-gray-300 rounded cursor-pointer p-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="lineThickness" className="text-sm font-medium">
|
||||
{t('thicknessLabel')}
|
||||
</Label>
|
||||
<Input
|
||||
type="range"
|
||||
id="lineThickness"
|
||||
min="1"
|
||||
max="20"
|
||||
value={thickness}
|
||||
onChange={(e) => setThickness(Number(e.target.value))}
|
||||
className="w-20"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground w-6">{thickness}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 ml-auto">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleUndo}
|
||||
disabled={drawingHistory.length === 0}
|
||||
>
|
||||
<Undo2 className="h-4 w-4 mr-1" />
|
||||
{t('undoButton')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleClear}
|
||||
disabled={drawingHistory.length === 0}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
{t('clearButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSave}>
|
||||
{t('saveDrawingButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
113
components/DrawingDisplay.tsx
Normal file
113
components/DrawingDisplay.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
interface DrawingDisplayProps {
|
||||
drawingData?: string
|
||||
width?: number
|
||||
height?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
interface DrawingStroke {
|
||||
color: string
|
||||
thickness: number
|
||||
points: Array<{ x: number; y: number }>
|
||||
}
|
||||
|
||||
export default function DrawingDisplay({
|
||||
drawingData,
|
||||
width = 120,
|
||||
height = 80,
|
||||
className = ''
|
||||
}: DrawingDisplayProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas || !drawingData) return
|
||||
|
||||
const context = canvas.getContext('2d')
|
||||
if (!context) return
|
||||
|
||||
try {
|
||||
const strokes: DrawingStroke[] = JSON.parse(drawingData)
|
||||
|
||||
// Clear canvas
|
||||
context.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// Set up context for drawing
|
||||
context.lineCap = 'round'
|
||||
context.lineJoin = 'round'
|
||||
|
||||
// Calculate scaling to fit the drawing in the small canvas
|
||||
if (strokes.length === 0) return
|
||||
|
||||
// Find bounds of the drawing
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
|
||||
|
||||
strokes.forEach(stroke => {
|
||||
stroke.points.forEach(point => {
|
||||
minX = Math.min(minX, point.x)
|
||||
minY = Math.min(minY, point.y)
|
||||
maxX = Math.max(maxX, point.x)
|
||||
maxY = Math.max(maxY, point.y)
|
||||
})
|
||||
})
|
||||
|
||||
// Add padding
|
||||
const padding = 10
|
||||
const drawingWidth = maxX - minX + padding * 2
|
||||
const drawingHeight = maxY - minY + padding * 2
|
||||
|
||||
// Calculate scale to fit in canvas
|
||||
const scaleX = canvas.width / drawingWidth
|
||||
const scaleY = canvas.height / drawingHeight
|
||||
const scale = Math.min(scaleX, scaleY, 1) // Don't scale up
|
||||
|
||||
// Center the drawing
|
||||
const offsetX = (canvas.width - drawingWidth * scale) / 2 - (minX - padding) * scale
|
||||
const offsetY = (canvas.height - drawingHeight * scale) / 2 - (minY - padding) * scale
|
||||
|
||||
// Draw each stroke
|
||||
strokes.forEach(stroke => {
|
||||
if (stroke.points.length === 0) return
|
||||
|
||||
context.beginPath()
|
||||
context.strokeStyle = stroke.color
|
||||
context.lineWidth = Math.max(1, stroke.thickness * scale) // Ensure minimum line width
|
||||
|
||||
const firstPoint = stroke.points[0]
|
||||
context.moveTo(
|
||||
firstPoint.x * scale + offsetX,
|
||||
firstPoint.y * scale + offsetY
|
||||
)
|
||||
|
||||
stroke.points.forEach(point => {
|
||||
context.lineTo(
|
||||
point.x * scale + offsetX,
|
||||
point.y * scale + offsetY
|
||||
)
|
||||
})
|
||||
|
||||
context.stroke()
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('Failed to render drawing:', error)
|
||||
}
|
||||
}, [drawingData, width, height])
|
||||
|
||||
if (!drawingData) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={width}
|
||||
height={height}
|
||||
className={`border-2 border-muted-foreground rounded bg-white ${className}`}
|
||||
style={{ width: `${width}px`, height: `${height}px` }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
83
components/DrawingModal.tsx
Normal file
83
components/DrawingModal.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { X } from 'lucide-react'
|
||||
import DrawingCanvas from './DrawingCanvas'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
interface DrawingModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSave: (drawingData: string) => void
|
||||
initialDrawing?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export default function DrawingModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
initialDrawing,
|
||||
title = 'Drawing'
|
||||
}: DrawingModalProps) {
|
||||
const t = useTranslations('DrawingModal')
|
||||
const [currentDrawing, setCurrentDrawing] = useState<string>(initialDrawing || '')
|
||||
|
||||
const handleSave = (drawingData: string) => {
|
||||
setCurrentDrawing(drawingData)
|
||||
onSave(drawingData)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
setCurrentDrawing('')
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div
|
||||
className="relative bg-white dark:bg-gray-900 rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b">
|
||||
<h2 className="text-lg font-semibold">{title}</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="h-6 w-6"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6" onClick={(e) => e.stopPropagation()}>
|
||||
<DrawingCanvas
|
||||
initialDrawing={currentDrawing}
|
||||
onSave={handleSave}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end gap-2 p-6 border-t">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
{t('cancelButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,9 +4,8 @@ import { useState, useMemo, useCallback } from 'react'
|
||||
import { Calendar } from '@/components/ui/calendar'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import CompletionCountBadge from '@/components/CompletionCountBadge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Check, Circle, CircleCheck } from 'lucide-react'
|
||||
import { d2s, getNow, t2d, isHabitDue, getISODate, getCompletionsForDate } from '@/lib/utils'
|
||||
import { Circle, CircleCheck } from 'lucide-react'
|
||||
import { d2s, getNow, isHabitDue, getISODate, getCompletionsForDate } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useHabits } from '@/hooks/useHabits'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Habit, User } from '@/lib/types';
|
||||
import { Habit } from '@/lib/types';
|
||||
import { useHabits } from '@/hooks/useHabits';
|
||||
import { useAtom } from 'jotai';
|
||||
import { pomodoroAtom, settingsAtom, currentUserAtom } from '@/lib/atoms';
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import { Habit, SafeUser, User, Permission } from '@/lib/types'
|
||||
import { Habit, User } from '@/lib/types'
|
||||
import { useAtom } from 'jotai'
|
||||
import { settingsAtom, pomodoroAtom, browserSettingsAtom, usersAtom, currentUserAtom } from '@/lib/atoms'
|
||||
import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, d2s, getCompletionsForToday, isTaskOverdue, convertMachineReadableFrequencyToHumanReadable } from '@/lib/utils'
|
||||
import { getCompletionsForToday, isTaskOverdue, convertMachineReadableFrequencyToHumanReadable } from '@/lib/utils'
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Coins, Edit, Check, Undo2, MoreVertical, Pin } from 'lucide-react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useHabits } from '@/hooks/useHabits'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { INITIAL_RECURRENCE_RULE, RECURRENCE_RULE_MAP } from '@/lib/constants'
|
||||
import { DateTime } from 'luxon'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
|
||||
import { hasPermission } from '@/lib/utils'
|
||||
import { HabitContextMenuItems } from './HabitContextMenuItems'
|
||||
import DrawingDisplay from './DrawingDisplay'
|
||||
|
||||
interface HabitItemProps {
|
||||
habit: Habit
|
||||
@@ -88,7 +85,7 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||
id={`habit-${habit.id}`}
|
||||
className={`h-full flex flex-col transition-all duration-500 ${isHighlighted ? 'bg-yellow-100 dark:bg-yellow-900' : ''} ${habit.archived ? 'opacity-75' : ''}`}
|
||||
>
|
||||
<CardHeader className="flex-none">
|
||||
<CardHeader className="flex-shrink-0">
|
||||
<div className="flex justify-between items-start">
|
||||
<CardTitle className={`line-clamp-1 ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''} flex items-center ${isTasksView ? 'w-full' : ''} justify-between`}>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -105,28 +102,44 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||
</CardTitle>
|
||||
{renderUserAvatars(habit, currentUser as User, usersData)}
|
||||
</div>
|
||||
{habit.description && (
|
||||
<CardDescription className={`whitespace-pre-line mt-2 ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>
|
||||
{habit.description}
|
||||
</CardDescription>
|
||||
{(habit.description || habit.drawing) && (
|
||||
<div className={`flex gap-4 mt-2 ${!habit.description ? 'justify-end' : ''}`}>
|
||||
{habit.description && (
|
||||
<CardDescription className={`whitespace-pre-line flex-1 min-w-0 break-words ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>
|
||||
{habit.description}
|
||||
</CardDescription>
|
||||
)}
|
||||
{habit.drawing && (
|
||||
<div className="flex-shrink-0">
|
||||
<DrawingDisplay
|
||||
drawingData={habit.drawing}
|
||||
width={120}
|
||||
height={80}
|
||||
className=""
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1">
|
||||
<p className={`text-sm ${habit.archived ? 'text-gray-400 dark:text-gray-500' : 'text-gray-500'}`}>
|
||||
{t('whenLabel', {
|
||||
frequency: convertMachineReadableFrequencyToHumanReadable({
|
||||
frequency: habit.frequency,
|
||||
isRecurRule,
|
||||
timezone: settings.system.timezone
|
||||
})
|
||||
})}
|
||||
</p>
|
||||
<div className="flex items-center mt-2">
|
||||
<Coins className={`h-4 w-4 mr-1 ${habit.archived ? 'text-gray-400 dark:text-gray-500' : 'text-yellow-400'}`} />
|
||||
<span className={`text-sm font-medium ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>{t('coinsPerCompletion', { count: habit.coinReward })}</span>
|
||||
<CardContent className="flex-grow flex flex-col justify-end">
|
||||
<div className="mt-auto">
|
||||
<p className={`text-sm ${habit.archived ? 'text-gray-400 dark:text-gray-500' : 'text-gray-500'}`}>
|
||||
{t('whenLabel', {
|
||||
frequency: convertMachineReadableFrequencyToHumanReadable({
|
||||
frequency: habit.frequency,
|
||||
isRecurRule,
|
||||
timezone: settings.system.timezone
|
||||
})
|
||||
})}
|
||||
</p>
|
||||
<div className="flex items-center mt-2">
|
||||
<Coins className={`h-4 w-4 mr-1 ${habit.archived ? 'text-gray-400 dark:text-gray-500' : 'text-yellow-400'}`} />
|
||||
<span className={`text-sm font-medium ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>{t('coinsPerCompletion', { count: habit.coinReward })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between gap-2">
|
||||
<CardFooter className="flex-shrink-0 flex justify-between gap-2">
|
||||
<div className="flex gap-2">
|
||||
<div className="relative">
|
||||
<Button
|
||||
@@ -212,4 +225,3 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState, useMemo, useEffect } from 'react' // Added useMemo, useEffect
|
||||
import { Plus, ArrowUpNarrowWide, ArrowDownWideNarrow, Search } from 'lucide-react' // Added sort icons, Search icon
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { habitsAtom, settingsAtom, browserSettingsAtom } from '@/lib/atoms'
|
||||
import { habitsAtom, browserSettingsAtom } from '@/lib/atoms'
|
||||
import EmptyState from './EmptyState'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import HabitItem from './HabitItem'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Habit } from '@/lib/types'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { d2s, getNow, t2d } from '@/lib/utils' // Removed getCompletedHabitsForDate
|
||||
import { d2s, getNow } from '@/lib/utils' // Removed getCompletedHabitsForDate
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { CoinsData, HabitsData, WishlistData, UserData, User, CoinTransaction } from '@/lib/types';
|
||||
import { HabitsData, WishlistData, UserData, User, CoinTransaction } from '@/lib/types';
|
||||
import { t2d } from '@/lib/utils';
|
||||
import Link from 'next/link';
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Input } from './ui/input';
|
||||
import { Button } from './ui/button';
|
||||
import { Label } from './ui/label';
|
||||
import { User as UserIcon } from 'lucide-react';
|
||||
import { Permission, User } from '@/lib/types';
|
||||
import { User } from '@/lib/types';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Play, Pause, RotateCw, Minus, X, Clock, SkipForward } from 'lucide-react'
|
||||
|
||||
@@ -9,7 +9,7 @@ import UserForm from './UserForm'
|
||||
import Link from "next/link"
|
||||
import { useAtom } from "jotai"
|
||||
import { aboutOpenAtom, settingsAtom, userSelectAtom, currentUserAtom } from "@/lib/atoms"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useState } from "react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { signOut } from "@/app/actions/user"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
|
||||
@@ -23,7 +23,6 @@ import { toast } from '@/hooks/use-toast';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { serverSettingsAtom, usersAtom, currentUserAtom } from '@/lib/atoms';
|
||||
import { createUser, updateUser, updateUserPassword, uploadAvatar } from '@/app/actions/data';
|
||||
import { SafeUser, User } from '@/lib/types';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
import { User as UserIcon } from 'lucide-react';
|
||||
import _ from 'lodash';
|
||||
|
||||
@@ -5,24 +5,12 @@ import PasswordEntryForm from './PasswordEntryForm';
|
||||
import UserForm from './UserForm';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
import { Crown, Pencil, Plus, User as UserIcon, UserRoundPen, Trash2 } from 'lucide-react';
|
||||
import { Input } from './ui/input';
|
||||
import { Button } from './ui/button';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Crown, Plus, User as UserIcon, UserRoundPen } from 'lucide-react';
|
||||
|
||||
|
||||
import { useAtom } from 'jotai';
|
||||
import { usersAtom, currentUserAtom } from '@/lib/atoms';
|
||||
import { signIn } from '@/app/actions/user';
|
||||
import { createUser } from '@/app/actions/data';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { Description } from '@radix-ui/react-dialog';
|
||||
|
||||
@@ -5,7 +5,6 @@ import { usersAtom, currentUserAtom } from '@/lib/atoms'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
|
||||
import { hasPermission } from '@/lib/utils'
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Coins, Edit, Trash2, Gift, MoreVertical, Archive, ArchiveRestore } from 'lucide-react'
|
||||
import {
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import DrawingDisplay from './DrawingDisplay'
|
||||
|
||||
interface WishlistItemProps {
|
||||
item: WishlistItemType
|
||||
@@ -73,37 +73,51 @@ export default function WishlistItem({
|
||||
} ${isRecentlyRedeemed ? 'animate-[celebrate_1s_ease-in-out] shadow-lg ring-2 ring-primary' : ''
|
||||
} ${item.archived ? 'opacity-75' : ''}`}
|
||||
>
|
||||
<CardHeader className="flex-none">
|
||||
<div className="flex items-center gap-2">
|
||||
<CardTitle className={`line-clamp-1 ${item.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>
|
||||
{item.name}
|
||||
</CardTitle>
|
||||
{item.targetCompletions && (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
({item.targetCompletions === 1 ? t('usesLeftSingular') : t('usesLeftPlural', { count: item.targetCompletions })})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
{item.description && (
|
||||
<CardDescription className={`whitespace-pre-line ${item.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>
|
||||
{item.description}
|
||||
</CardDescription>
|
||||
<CardHeader className="flex-shrink-0">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<CardTitle className={`line-clamp-1 ${item.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>
|
||||
{item.name}
|
||||
</CardTitle>
|
||||
{item.targetCompletions && (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 flex-shrink-0">
|
||||
({item.targetCompletions === 1 ? t('usesLeftSingular') : t('usesLeftPlural', { count: item.targetCompletions })})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{renderUserAvatars(item, currentUser as User, usersData)}
|
||||
</div>
|
||||
{(item.description || item.drawing) && (
|
||||
<div className={`flex gap-4 mt-2 ${!item.description ? 'justify-end' : ''}`}>
|
||||
{item.description && (
|
||||
<CardDescription className={`whitespace-pre-line flex-1 min-w-0 break-words ${item.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>
|
||||
{item.description}
|
||||
</CardDescription>
|
||||
)}
|
||||
{item.drawing && (
|
||||
<div className="flex-shrink-0">
|
||||
<DrawingDisplay
|
||||
drawingData={item.drawing}
|
||||
width={120}
|
||||
height={80}
|
||||
className=""
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Coins className={`h-4 w-4 ${item.archived ? 'text-gray-400 dark:text-gray-500' : 'text-yellow-400'}`} />
|
||||
<span className={`text-sm font-medium ${item.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>
|
||||
{item.coinCost} {t('coinsSuffix')}
|
||||
</span>
|
||||
<CardContent className="flex-grow flex flex-col justify-end">
|
||||
<div className="mt-auto">
|
||||
<div className="flex items-center gap-2">
|
||||
<Coins className={`h-4 w-4 ${item.archived ? 'text-gray-400 dark:text-gray-500' : 'text-yellow-400'}`} />
|
||||
<span className={`text-sm font-medium ${item.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>
|
||||
{item.coinCost} {t('coinsSuffix')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between gap-2">
|
||||
<CardFooter className="flex-shrink-0 flex justify-between gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={canRedeem ? "default" : "secondary"}
|
||||
@@ -180,4 +194,3 @@ export default function WishlistItem({
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Moon, MoonIcon, Sun } from "lucide-react"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
Reference in New Issue
Block a user