mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3d2b1ef96 |
95
.github/workflows/docker-publish.yml
vendored
Normal file
95
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
name: Docker Build and Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- github-actions
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
EXISTS: ${{ steps.check-version.outputs.EXISTS }}
|
||||
VERSION: ${{ steps.package-version.outputs.VERSION }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Get version from package.json
|
||||
id: package-version
|
||||
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Check if version exists
|
||||
id: check-version
|
||||
run: |
|
||||
if docker pull dohsimpson/habittrove:v${{ steps.package-version.outputs.VERSION }} 2>/dev/null; then
|
||||
echo "EXISTS=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "EXISTS=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.check-version.outputs.EXISTS == 'false' && format('dohsimpson/habittrove:v{0}', steps.package-version.outputs.VERSION) || '' }}
|
||||
dohsimpson/habittrove:demo
|
||||
dohsimpson/habittrove:latest
|
||||
|
||||
deploy-demo:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-push
|
||||
# demo tracks the demo tag
|
||||
if: needs.build-and-push.outputs.EXISTS == 'false'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-hub/kubectl@master
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
|
||||
with:
|
||||
args: rollout restart -n ${{ secrets.KUBE_NAMESPACE }} deploy/${{ secrets.KUBE_DEPLOYMENT }}
|
||||
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-push
|
||||
if: needs.build-and-push.outputs.EXISTS == 'false'
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create GitHub release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
VERSION: ${{ needs.build-and-push.outputs.VERSION }}
|
||||
run: |
|
||||
# Extract release notes from CHANGELOG.md
|
||||
notes=$(awk -v version="$VERSION" '
|
||||
$0 ~ "## Version " version {flag=1;next}
|
||||
$0 ~ "## Version " && flag {exit}
|
||||
flag' CHANGELOG.md)
|
||||
|
||||
gh release create "v$VERSION" \
|
||||
--repo="$GITHUB_REPOSITORY" \
|
||||
--title="v$VERSION" \
|
||||
--notes="$notes"
|
||||
40
.github/workflows/release.yml
vendored
40
.github/workflows/release.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Create and publish a Docker image to Github Container Registry
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
create-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
28
.github/workflows/test.yml
vendored
Normal file
28
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Unit Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Run lint
|
||||
run: bun run lint
|
||||
|
||||
- name: Run unit tests
|
||||
run: bun test
|
||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": [
|
||||
"i18n",
|
||||
"messages"
|
||||
]
|
||||
}
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
## Version 0.2.13
|
||||
|
||||
### Added
|
||||
|
||||
* support deleting user (#93)
|
||||
|
||||
## Version 0.2.13
|
||||
|
||||
### Fixed
|
||||
|
||||
* fix responsive design on mobile (#134)
|
||||
|
||||
@@ -23,8 +23,11 @@ Want to try HabitTrove before installing? Visit the public [demo instance](https
|
||||
## Usage
|
||||
|
||||
1. **Creating Habits**: Click the "Add Habit" button to create a new habit. Set a name, description, and coin reward.
|
||||
|
||||
2. **Tracking Habits**: Mark habits as complete on your dashboard. Each completion earns you the specified coins.
|
||||
|
||||
3. **Wishlist**: Add rewards to your wishlist that you can redeem with earned coins.
|
||||
|
||||
4. **Statistics**: View your progress through the heatmap and streak counters.
|
||||
|
||||
## Docker Deployment
|
||||
@@ -61,7 +64,7 @@ docker run -d \
|
||||
-v ./data:/app/data \
|
||||
-v ./backups:/app/backups \ # Add this line to map the backups directory
|
||||
-e AUTH_SECRET=$AUTH_SECRET \
|
||||
ghcr.io/manindark/habittrove
|
||||
dohsimpson/habittrove
|
||||
```
|
||||
|
||||
Available image tags:
|
||||
@@ -108,7 +111,7 @@ To contribute to HabitTrove, you'll need to set up a development environment. He
|
||||
1. Clone the repository and navigate to the project directory:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ManInDark/HabitTrove.git
|
||||
git clone https://github.com/dohsimpson/habittrove.git
|
||||
cd habittrove
|
||||
```
|
||||
|
||||
@@ -161,7 +164,7 @@ Run these commands regularly during development to catch issues early.
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome feature requests and bug reports! Please [open an issue](https://github.com/ManInDark/habittrove/issues/new).
|
||||
We welcome feature requests and bug reports! Please [open an issue](https://github.com/dohsimpson/habittrove/issues/new). We do not accept pull request at the moment.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
'use server'
|
||||
|
||||
import { getCurrentUser, saltAndHashPassword, verifyPassword } from "@/lib/server-helpers";
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import {
|
||||
HabitsData,
|
||||
CoinsData,
|
||||
CoinTransaction,
|
||||
DATA_DEFAULTS,
|
||||
DataType,
|
||||
getDefaultCoinsData,
|
||||
getDefaultHabitsData,
|
||||
getDefaultSettings,
|
||||
getDefaultUsersData,
|
||||
getDefaultWishlistData,
|
||||
HabitsData,
|
||||
Permission,
|
||||
ServerSettings,
|
||||
Settings,
|
||||
TransactionType,
|
||||
User,
|
||||
UserData,
|
||||
WishlistItemType,
|
||||
WishlistData,
|
||||
WishlistItemType
|
||||
} from '@/lib/types';
|
||||
import { d2t, getNow, uuid } from '@/lib/utils';
|
||||
Settings,
|
||||
DataType,
|
||||
DATA_DEFAULTS,
|
||||
getDefaultSettings,
|
||||
UserData,
|
||||
getDefaultUsersData,
|
||||
User,
|
||||
getDefaultWishlistData,
|
||||
getDefaultHabitsData,
|
||||
getDefaultCoinsData,
|
||||
Permission,
|
||||
ServerSettings
|
||||
} from '@/lib/types'
|
||||
import { d2t, deepMerge, getNow, checkPermission, uuid } from '@/lib/utils';
|
||||
import { verifyPassword } from "@/lib/server-helpers";
|
||||
import { saltAndHashPassword } from "@/lib/server-helpers";
|
||||
import { signInSchema } from '@/lib/zod';
|
||||
import fs from 'fs/promises';
|
||||
import { auth } from '@/auth';
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import { getCurrentUser, getCurrentUserId } from '@/lib/server-helpers'
|
||||
|
||||
import { PermissionError } from '@/lib/exceptions'
|
||||
|
||||
type ResourceType = 'habit' | 'wishlist' | 'coins'
|
||||
type ActionType = 'write' | 'interact'
|
||||
@@ -485,21 +489,80 @@ export async function updateUserPassword(userId: string, newPassword?: string):
|
||||
}
|
||||
|
||||
export async function deleteUser(userId: string): Promise<void> {
|
||||
const data = await loadUsersData()
|
||||
const userIndex = data.users.findIndex(user => user.id === userId)
|
||||
// Load all necessary data
|
||||
const wishlistData = await loadData<WishlistData>('wishlist')
|
||||
const habitsData = await loadData<HabitsData>('habits')
|
||||
const coinsData = await loadData<CoinsData>('coins')
|
||||
const authData = await loadUsersData()
|
||||
|
||||
// Process Wishlist Data
|
||||
const updatedWishlistItems = wishlistData.items.reduce((acc, item) => {
|
||||
if (item.userIds?.includes(userId)) {
|
||||
if (item.userIds.length === 1) {
|
||||
// Remove item if this is the only user
|
||||
return acc
|
||||
} else {
|
||||
// Remove userId from item's userIds
|
||||
acc.push({
|
||||
...item,
|
||||
userIds: item.userIds.filter(id => id !== userId)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Keep item as is
|
||||
acc.push(item)
|
||||
}
|
||||
return acc
|
||||
}, [] as WishlistItemType[])
|
||||
wishlistData.items = updatedWishlistItems
|
||||
await saveData('wishlist', wishlistData)
|
||||
|
||||
// Process Habits Data
|
||||
const updatedHabits = habitsData.habits.reduce((acc, habit) => {
|
||||
if (habit.userIds?.includes(userId)) {
|
||||
if (habit.userIds.length === 1) {
|
||||
// Remove habit if this is the only user
|
||||
return acc
|
||||
} else {
|
||||
// Remove userId from habit's userIds
|
||||
acc.push({
|
||||
...habit,
|
||||
userIds: habit.userIds.filter(id => id !== userId)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Keep habit as is
|
||||
acc.push(habit)
|
||||
}
|
||||
return acc
|
||||
}, [] as HabitsData['habits'])
|
||||
habitsData.habits = updatedHabits
|
||||
await saveData('habits', habitsData)
|
||||
|
||||
// Process Coins Data
|
||||
coinsData.transactions = coinsData.transactions.filter(
|
||||
transaction => transaction.userId !== userId
|
||||
)
|
||||
// Recalculate balance
|
||||
coinsData.balance = coinsData.transactions.reduce(
|
||||
(sum, transaction) => sum + transaction.amount,
|
||||
0
|
||||
)
|
||||
await saveData('coins', coinsData)
|
||||
|
||||
// Delete User from Auth Data
|
||||
const userIndex = authData.users.findIndex(user => user.id === userId)
|
||||
|
||||
if (userIndex === -1) {
|
||||
throw new Error('User not found')
|
||||
}
|
||||
|
||||
const newData: UserData = {
|
||||
users: [
|
||||
...data.users.slice(0, userIndex),
|
||||
...data.users.slice(userIndex + 1)
|
||||
]
|
||||
}
|
||||
authData.users = [
|
||||
...authData.users.slice(0, userIndex),
|
||||
...authData.users.slice(userIndex + 1)
|
||||
]
|
||||
|
||||
await saveUsersData(newData)
|
||||
await saveUsersData(authData)
|
||||
}
|
||||
|
||||
export async function updateLastNotificationReadTimestamp(userId: string, timestamp: string): Promise<void> {
|
||||
|
||||
50
app/api/user/delete/route.ts
Normal file
50
app/api/user/delete/route.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { deleteUser } from '@/app/actions/data'
|
||||
import { getCurrentUser } from '@/lib/server-helpers'
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const currentUserId = session.user.id
|
||||
const currentUser = await getCurrentUser()
|
||||
|
||||
if (!currentUser) {
|
||||
// This case should ideally not happen if session.user.id exists,
|
||||
// but as a safeguard:
|
||||
return NextResponse.json({ error: 'Unauthorized: User not found in system' }, { status: 401 })
|
||||
}
|
||||
|
||||
let userIdToDelete: string
|
||||
try {
|
||||
const body = await req.json()
|
||||
userIdToDelete = body.userId
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Invalid request body: Could not parse JSON.' }, { status: 400 })
|
||||
}
|
||||
|
||||
|
||||
if (!userIdToDelete) {
|
||||
return NextResponse.json({ error: 'Bad Request: userId is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Security Check: Users can only delete their own account unless they are an admin.
|
||||
if (!currentUser.isAdmin && userIdToDelete !== currentUserId) {
|
||||
return NextResponse.json({ error: 'Forbidden: You do not have permission to delete this user.' }, { status: 403 })
|
||||
}
|
||||
|
||||
await deleteUser(userIdToDelete)
|
||||
|
||||
return NextResponse.json({ message: 'User deleted successfully' }, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error('Error deleting user:', error)
|
||||
if (error instanceof Error && error.message === 'User not found') {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
import Layout from '@/components/Layout'
|
||||
import HabitCalendar from '@/components/HabitCalendar'
|
||||
import { ViewToggle } from '@/components/ViewToggle'
|
||||
import CompletionCountBadge from '@/components/CompletionCountBadge'
|
||||
|
||||
export default function CalendarPage() {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-end">
|
||||
{/* <ViewToggle /> */}
|
||||
</div>
|
||||
<HabitCalendar />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Layout from '@/components/Layout'
|
||||
import CoinsManager from '@/components/CoinsManager'
|
||||
|
||||
export default function CoinsPage() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useHabits } from "@/hooks/useHabits";
|
||||
import { habitsAtom, settingsAtom } from "@/lib/atoms";
|
||||
import { Habit } from "@/lib/types";
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import Layout from '@/components/Layout'
|
||||
import HabitList from '@/components/HabitList'
|
||||
import { ViewToggle } from '@/components/ViewToggle'
|
||||
|
||||
export default function HabitsPage() {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<HabitList isTasksView={false} />
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-end">
|
||||
{/* <ViewToggle /> */}
|
||||
</div>
|
||||
<HabitList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { JotaiHydrate } from '@/components/jotai-hydrate'
|
||||
import { JotaiProvider } from '@/components/jotai-providers'
|
||||
import Layout from '@/components/Layout'
|
||||
import { ThemeProvider } from "@/components/theme-provider"
|
||||
import { Toaster } from '@/components/ui/toaster'
|
||||
import { SessionProvider } from 'next-auth/react'
|
||||
import { DM_Sans } from 'next/font/google'
|
||||
import { Suspense } from 'react'
|
||||
import { loadCoinsData, loadHabitsData, loadServerSettings, loadSettings, loadUsersData, loadWishlistData } from './actions/data'
|
||||
import './globals.css'
|
||||
import { Inter } from 'next/font/google'
|
||||
import { DM_Sans } from 'next/font/google'
|
||||
import { JotaiProvider } from '@/components/jotai-providers'
|
||||
import { Suspense } from 'react'
|
||||
import { JotaiHydrate } from '@/components/jotai-hydrate'
|
||||
import { loadSettings, loadHabitsData, loadCoinsData, loadWishlistData, loadUsersData, loadServerSettings } from './actions/data'
|
||||
import Layout from '@/components/Layout'
|
||||
import { Toaster } from '@/components/ui/toaster'
|
||||
import { ThemeProvider } from "@/components/theme-provider"
|
||||
import { SessionProvider } from 'next-auth/react'
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getLocale, getMessages } from 'next-intl/server';
|
||||
|
||||
|
||||
// Inter (clean, modern, excellent readability)
|
||||
// const inter = Inter({
|
||||
// subsets: ['latin'],
|
||||
|
||||
@@ -1,33 +1,44 @@
|
||||
'use client'
|
||||
|
||||
import { DynamicTimeNoSSR } from '@/components/DynamicTimeNoSSR';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { serverSettingsAtom, settingsAtom } from '@/lib/atoms';
|
||||
import { Settings, WeekDay } from '@/lib/types';
|
||||
import { DynamicTimeNoSSR } from '@/components/DynamicTimeNoSSR';
|
||||
import { useAtom } from 'jotai';
|
||||
import { Info } from 'lucide-react'; // Import Info icon
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { saveSettings } from '../actions/data';
|
||||
import { settingsAtom, serverSettingsAtom } from '@/lib/atoms';
|
||||
import { Settings, WeekDay } from '@/lib/types'
|
||||
import { saveSettings, uploadAvatar } from '../actions/data'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { User, Info } from 'lucide-react'; // Import Info icon
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { useSession } from 'next-auth/react'; // signOut removed
|
||||
import { useRouter } from 'next/navigation';
|
||||
// AlertDialog components and useState removed
|
||||
// Trash2 icon removed
|
||||
|
||||
export default function SettingsPage() {
|
||||
const t = useTranslations('SettingsPage');
|
||||
// tWarning removed
|
||||
const [settings, setSettings] = useAtom(settingsAtom);
|
||||
const [serverSettings] = useAtom(serverSettingsAtom);
|
||||
const { data: session } = useSession();
|
||||
const router = useRouter();
|
||||
// showConfirmDialog and isDeleting states removed
|
||||
|
||||
const updateSettings = async (newSettings: Settings) => {
|
||||
await saveSettings(newSettings)
|
||||
setSettings(newSettings)
|
||||
}
|
||||
|
||||
// handleDeleteAccount function removed
|
||||
|
||||
if (!settings) return null
|
||||
|
||||
@@ -228,6 +239,8 @@ export default function SettingsPage() {
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Danger Zone Card Removed */}
|
||||
</div >
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import HabitList from '@/components/HabitList'
|
||||
|
||||
export default function TasksPage() {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<HabitList isTasksView={true} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Layout from '@/components/Layout'
|
||||
import WishlistManager from '@/components/WishlistManager'
|
||||
|
||||
export default function WishlistPage() {
|
||||
|
||||
@@ -58,13 +58,11 @@ export default function AboutModal({ isOpen, onClose }: AboutModalProps) {
|
||||
>
|
||||
@dohsimpson
|
||||
</a>
|
||||
<br/>
|
||||
Fork by <a href="https://github.com/ManInDark" target="_blank" rel="noopener noreferrer" className="font-medium hover:underline">@ManInDark</a>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<a
|
||||
href="https://github.com/ManInDark/HabitTrove"
|
||||
href="https://github.com/dohsimpson/habittrove"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { useState } from 'react'
|
||||
import { RRule, RRuleSet, rrulestr } from 'rrule'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { settingsAtom, browserSettingsAtom, usersAtom } from '@/lib/atoms'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { settingsAtom, usersAtom } from '@/lib/atoms'
|
||||
import { useHelpers } from '@/lib/client-helpers'
|
||||
import { INITIAL_DUE, INITIAL_RECURRENCE_RULE, QUICK_DATES } from '@/lib/constants'
|
||||
import { Habit } from '@/lib/types'
|
||||
import { convertHumanReadableFrequencyToMachineReadable, convertMachineReadableFrequencyToHumanReadable, d2t, serializeRRule } from '@/lib/utils'
|
||||
import { Info, SmilePlus, Zap } from 'lucide-react'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import data from '@emoji-mart/data'
|
||||
import Picker from '@emoji-mart/react'
|
||||
import { useAtom } from 'jotai'
|
||||
import { SmilePlus, Zap } from 'lucide-react'
|
||||
import { Habit, SafeUser } from '@/lib/types'
|
||||
import { convertHumanReadableFrequencyToMachineReadable, convertMachineReadableFrequencyToHumanReadable, d2s, d2t, serializeRRule } from '@/lib/utils'
|
||||
import { INITIAL_DUE, INITIAL_RECURRENCE_RULE, QUICK_DATES, RECURRENCE_RULE_MAP } from '@/lib/constants'
|
||||
import * as chrono from 'chrono-node';
|
||||
import { DateTime } from 'luxon'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useState } from 'react'
|
||||
import { RRule } from 'rrule'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { useHelpers } from '@/lib/client-helpers'
|
||||
|
||||
interface AddEditHabitModalProps {
|
||||
onClose: () => void
|
||||
@@ -45,6 +54,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
const [ruleText, setRuleText] = useState<string>(initialRuleText)
|
||||
const { currentUser } = useHelpers()
|
||||
const [isQuickDatesOpen, setIsQuickDatesOpen] = useState(false)
|
||||
const [ruleError, setRuleError] = useState<string | null>(null); // State for validation message
|
||||
const [selectedUserIds, setSelectedUserIds] = useState<string[]>((habit?.userIds || []).filter(id => id !== currentUser?.id))
|
||||
const [usersData] = useAtom(usersAtom)
|
||||
const users = usersData.users
|
||||
@@ -86,8 +96,6 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
})
|
||||
}
|
||||
|
||||
const { result, message: errorMessage } = convertHumanReadableFrequencyToMachineReadable({ text: ruleText, timezone: settings.system.timezone, isRecurring: isRecurRule });
|
||||
|
||||
return (
|
||||
<Dialog open={true} onOpenChange={onClose}>
|
||||
<DialogContent>
|
||||
@@ -201,9 +209,24 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
</div>
|
||||
{/* rrule input (habit) */}
|
||||
<div className="col-start-2 col-span-3 text-sm">
|
||||
<span className={errorMessage ? 'text-destructive' : 'text-muted-foreground'}>
|
||||
{errorMessage ? errorMessage : convertMachineReadableFrequencyToHumanReadable({ frequency: result, isRecurRule, timezone: settings.system.timezone })}
|
||||
</span>
|
||||
{(() => {
|
||||
let displayText = '';
|
||||
let errorMessage: string | null = null;
|
||||
const { result, message } = convertHumanReadableFrequencyToMachineReadable({ text: ruleText, timezone: settings.system.timezone, isRecurring: isRecurRule });
|
||||
errorMessage = message;
|
||||
displayText = convertMachineReadableFrequencyToHumanReadable({ frequency: result, isRecurRule, timezone: settings.system.timezone })
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={errorMessage ? 'text-destructive' : 'text-muted-foreground'}>
|
||||
{displayText}
|
||||
</span>
|
||||
{errorMessage && (
|
||||
<p className="text-destructive text-xs mt-1">{errorMessage}</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { usersAtom } from '@/lib/atoms'
|
||||
import { useHelpers } from '@/lib/client-helpers'
|
||||
import { WishlistItemType } from '@/lib/types'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { SmilePlus, Info } from 'lucide-react'
|
||||
import data from '@emoji-mart/data'
|
||||
import Picker from '@emoji-mart/react'
|
||||
import { useAtom } from 'jotai'
|
||||
import { SmilePlus } from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
|
||||
import { WishlistItemType } from '@/lib/types'
|
||||
|
||||
interface AddEditWishlistItemModalProps {
|
||||
isOpen: boolean
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function ClientWrapper({ children }: { children: ReactNode }) {
|
||||
if (!currentUserId && !userSelect) {
|
||||
setUserSelect(true)
|
||||
}
|
||||
}, [currentUserId, status, userSelect, setUserSelect])
|
||||
}, [currentUserId, status, userSelect])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
0
components/CoinBalance.test.tsx
Normal file
0
components/CoinBalance.test.tsx
Normal file
@@ -1,23 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { FormattedNumber } from '@/components/FormattedNumber'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
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 { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { FormattedNumber } from '@/components/FormattedNumber'
|
||||
import { History, Pencil } from 'lucide-react'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import EmptyState from './EmptyState'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useCoins } from '@/hooks/useCoins'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { settingsAtom, usersAtom } from '@/lib/atoms'
|
||||
import Link from 'next/link'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useCoins } from '@/hooks/useCoins'
|
||||
import { TransactionNoteEditor } from './TransactionNoteEditor'
|
||||
import { useHelpers } from '@/lib/client-helpers'
|
||||
import { TransactionType } from '@/lib/types'
|
||||
import { d2s, t2d } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { History } from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import Link from 'next/link'
|
||||
import { useSearchParams } from 'next/navigation'; // Import useSearchParams
|
||||
import { useEffect, useRef, useState } from 'react'; // Import useEffect, useRef
|
||||
import EmptyState from './EmptyState'
|
||||
import { TransactionNoteEditor } from './TransactionNoteEditor'
|
||||
|
||||
export default function CoinsManager() {
|
||||
const t = useTranslations('CoinsManager')
|
||||
@@ -46,7 +46,6 @@ export default function CoinsManager() {
|
||||
const highlightId = searchParams.get('highlight')
|
||||
const userIdFromQuery = searchParams.get('user') // Get user ID from query
|
||||
const transactionRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
const PAGE_ENTRY_COUNTS = [10, 50, 100, 500];
|
||||
|
||||
// Effect to set selected user from query param if admin
|
||||
useEffect(() => {
|
||||
@@ -57,7 +56,7 @@ export default function CoinsManager() {
|
||||
}
|
||||
}
|
||||
// Only run when userIdFromQuery or currentUser changes, avoid re-running on selectedUser change within this effect
|
||||
}, [userIdFromQuery, currentUser, usersData.users, selectedUser]);
|
||||
}, [userIdFromQuery, currentUser, usersData.users]);
|
||||
|
||||
// Effect to scroll to highlighted transaction
|
||||
useEffect(() => {
|
||||
@@ -107,7 +106,7 @@ export default function CoinsManager() {
|
||||
<h1 className="text-xl xs:text-3xl font-bold mr-6">{t('title')}</h1>
|
||||
{currentUser?.isAdmin && (
|
||||
<select
|
||||
className="border rounded p-2"
|
||||
className="w-[110px] xs:w-[200px] rounded-md border border-input bg-background px-3 py-2"
|
||||
value={selectedUser}
|
||||
onChange={(e) => setSelectedUser(e.target.value)}
|
||||
>
|
||||
@@ -252,7 +251,9 @@ export default function CoinsManager() {
|
||||
setCurrentPage(1) // Reset to first page when changing page size
|
||||
}}
|
||||
>
|
||||
{PAGE_ENTRY_COUNTS.map(n => <option key={n} value={n}>{n}</option>)}
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
<option value={500}>500</option>
|
||||
</select>
|
||||
<span className="text-sm text-muted-foreground">{t('entriesSuffix')}</span>
|
||||
</div>
|
||||
@@ -288,7 +289,6 @@ export default function CoinsManager() {
|
||||
}
|
||||
|
||||
const isHighlighted = transaction.id === highlightId;
|
||||
const transactionUser = usersData.users.find(u => u.id === transaction.userId);
|
||||
return (
|
||||
<div
|
||||
key={transaction.id}
|
||||
@@ -317,12 +317,12 @@ export default function CoinsManager() {
|
||||
{transaction.userId && currentUser?.isAdmin && (
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage
|
||||
src={transactionUser?.avatarPath ?
|
||||
`/api/avatars/${transactionUser?.avatarPath?.split('/').pop()}` : undefined}
|
||||
alt={transactionUser?.username}
|
||||
src={usersData.users.find(u => u.id === transaction.userId)?.avatarPath ?
|
||||
`/api/avatars/${usersData.users.find(u => u.id === transaction.userId)?.avatarPath?.split('/').pop()}` : undefined}
|
||||
alt={usersData.users.find(u => u.id === transaction.userId)?.username}
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{transactionUser?.username?.[0] || '?'}
|
||||
{usersData.users.find(u => u.id === transaction.userId)?.username?.[0] || '?'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { completedHabitsMapAtom, habitsByDateFamily, settingsAtom } from '@/lib/atoms'
|
||||
import { getTodayInTimezone } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { completedHabitsMapAtom, habitsAtom, habitsByDateFamily } from '@/lib/atoms'
|
||||
import { getTodayInTimezone } from '@/lib/utils'
|
||||
// import { useHabits } from '@/hooks/useHabits' // Not used
|
||||
import { settingsAtom } from '@/lib/atoms'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
interface CompletionCountBadgeProps {
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Circle, Coins, ArrowRight, CircleCheck, ChevronDown, ChevronUp, Plus, Pin, AlertTriangle } from 'lucide-react' // Removed unused icons
|
||||
import CompletionCountBadge from './CompletionCountBadge'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuTrigger
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu"
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { cn } from '@/lib/utils'
|
||||
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 { 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'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { useHabits } from '@/hooks/useHabits'
|
||||
import { browserSettingsAtom, completedHabitsMapAtom, hasTasksAtom, pomodoroAtom, settingsAtom } from '@/lib/atoms'
|
||||
import { Habit, WishlistItemType } from '@/lib/types'
|
||||
import { cn, d2t, getNow, getTodayInTimezone, isHabitDue, isSameDate, isTaskOverdue, t2d } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { AlertTriangle, ArrowRight, ChevronDown, ChevronUp, Circle, CircleCheck, Coins, Pin, Plus } from 'lucide-react'; // Removed unused icons
|
||||
import { useTranslations } from 'next-intl'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import AddEditHabitModal from './AddEditHabitModal'
|
||||
import CompletionCountBadge from './CompletionCountBadge'
|
||||
import ConfirmDialog from './ConfirmDialog'
|
||||
import { HabitContextMenuItems } from './HabitContextMenuItems'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Settings, WishlistItemType } from '@/lib/types'
|
||||
import { Habit } from '@/lib/types'
|
||||
import Linkify from './linkify'
|
||||
import { useHabits } from '@/hooks/useHabits'
|
||||
import AddEditHabitModal from './AddEditHabitModal'
|
||||
import ConfirmDialog from './ConfirmDialog'
|
||||
import { Button } from './ui/button'
|
||||
import { HabitContextMenuItems } from './HabitContextMenuItems'
|
||||
|
||||
interface UpcomingItemsProps {
|
||||
habits: Habit[]
|
||||
@@ -222,6 +226,12 @@ const ItemSection = ({
|
||||
<Link
|
||||
href={`/habits?highlight=${habit.id}`}
|
||||
className="flex items-center gap-1 hover:text-primary transition-colors"
|
||||
onClick={() => {
|
||||
const newViewType = isTask ? 'tasks' : 'habits';
|
||||
if (browserSettings.viewType !== newViewType) {
|
||||
setBrowserSettings(prev => ({ ...prev, viewType: newViewType }));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isTask && isTaskOverdue(habit, settings.system.timezone) && !isCompleted && (
|
||||
<TooltipProvider>
|
||||
@@ -310,6 +320,12 @@ const ItemSection = ({
|
||||
<Link
|
||||
href={viewLink}
|
||||
className="text-sm text-muted-foreground hover:text-primary flex items-center gap-1"
|
||||
onClick={() => {
|
||||
const newViewType = isTask ? 'tasks' : 'habits';
|
||||
if (browserSettings.viewType !== newViewType) {
|
||||
setBrowserSettings(prev => ({ ...prev, viewType: newViewType }));
|
||||
}
|
||||
}}
|
||||
>
|
||||
View
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useCoins } from '@/hooks/useCoins'
|
||||
import { habitsAtom, wishlistAtom } from '@/lib/atoms'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import CoinBalance from './CoinBalance'
|
||||
import { wishlistAtom, habitsAtom, settingsAtom } from '@/lib/atoms'
|
||||
import DailyOverview from './DailyOverview'
|
||||
import HabitStreak from './HabitStreak'
|
||||
import CoinBalance from './CoinBalance'
|
||||
// import { useHabits } from '@/hooks/useHabits' // useHabits is not used
|
||||
import { useCoins } from '@/hooks/useCoins'
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function Dashboard() {
|
||||
const t = useTranslations('Dashboard');
|
||||
const [habitsData] = useAtom(habitsAtom)
|
||||
const habits = habitsData.habits
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const { balance } = useCoins()
|
||||
const [wishlist] = useAtom(wishlistAtom)
|
||||
const wishlistItems = wishlist.items
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import CompletionCountBadge from '@/components/CompletionCountBadge'
|
||||
import { useState, useMemo, useCallback } from 'react'
|
||||
import { Calendar } from '@/components/ui/calendar'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { useHabits } from '@/hooks/useHabits'
|
||||
import { completedHabitsMapAtom, habitsAtom, hasTasksAtom, settingsAtom } from '@/lib/atoms'
|
||||
import { Habit } from '@/lib/types'
|
||||
import { d2s, getCompletionsForDate, getISODate, getNow, isHabitDue } from '@/lib/utils'
|
||||
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 { useAtom } from 'jotai'
|
||||
import { Circle, CircleCheck } from 'lucide-react'
|
||||
import { DateTime } from 'luxon'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useHabits } from '@/hooks/useHabits'
|
||||
import { habitsAtom, settingsAtom, completedHabitsMapAtom, hasTasksAtom } from '@/lib/atoms'
|
||||
import { DateTime } from 'luxon'
|
||||
import Linkify from './linkify'
|
||||
import { Habit } from '@/lib/types'
|
||||
|
||||
export default function HabitCalendar() {
|
||||
const t = useTranslations('HabitCalendar')
|
||||
@@ -43,7 +44,7 @@ export default function HabitCalendar() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-xl xs:text-3xl font-bold mb-6">{t('title')}</h1>
|
||||
<h1 className="text-xl xs:text-3xl font-semibold mb-6">{t('title')}</h1>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Habit, SafeUser, User, Permission } from '@/lib/types'
|
||||
import { useAtom } from 'jotai'
|
||||
import { settingsAtom, pomodoroAtom, browserSettingsAtom, usersAtom } from '@/lib/atoms'
|
||||
import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, d2s, 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' // Removed unused icons
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useHabits } from '@/hooks/useHabits'
|
||||
import { settingsAtom, usersAtom } from '@/lib/atoms'
|
||||
import { useHelpers } from '@/lib/client-helpers'
|
||||
import { Habit, User } from '@/lib/types'
|
||||
import { convertMachineReadableFrequencyToHumanReadable, getCompletionsForToday, isTaskOverdue } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { Check, Coins, Edit, MoreVertical, Pin, Undo2 } from 'lucide-react'; // Removed unused icons
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { HabitContextMenuItems } from './HabitContextMenuItems'
|
||||
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 { useHelpers } from '@/lib/client-helpers'
|
||||
import { HabitContextMenuItems } from './HabitContextMenuItems'
|
||||
|
||||
interface HabitItemProps {
|
||||
habit: Habit
|
||||
@@ -47,6 +50,7 @@ const renderUserAvatars = (habit: Habit, currentUser: User | null, usersData: {
|
||||
export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||
const { completeHabit, undoComplete, archiveHabit, unarchiveHabit, saveHabit } = useHabits()
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const [_, setPomo] = useAtom(pomodoroAtom)
|
||||
const completionsToday = getCompletionsForToday({ habit, timezone: settings.system.timezone })
|
||||
const target = habit.targetCompletions || 1
|
||||
const isCompletedToday = completionsToday >= target
|
||||
@@ -56,7 +60,9 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||
const { currentUser, hasPermission } = useHelpers()
|
||||
const canWrite = hasPermission('habit', 'write')
|
||||
const canInteract = hasPermission('habit', 'interact')
|
||||
const pathname = usePathname();
|
||||
const [browserSettings] = useAtom(browserSettingsAtom)
|
||||
const isTasksView = browserSettings.viewType === 'tasks'
|
||||
const isRecurRule = !isTasksView
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
@@ -84,7 +90,7 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||
>
|
||||
<CardHeader className="flex-none">
|
||||
<div className="flex justify-between items-start">
|
||||
<CardTitle className={`line-clamp-1 ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''} flex items-center ${pathname.includes("tasks") ? 'w-full' : ''} justify-between`}>
|
||||
<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">
|
||||
{habit.pinned && (
|
||||
<Pin className="h-4 w-4 text-yellow-500" />
|
||||
@@ -109,7 +115,7 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||
<p className={`text-sm ${habit.archived ? 'text-gray-400 dark:text-gray-500' : 'text-gray-500'}`}>
|
||||
{t('whenLabel', { frequency: convertMachineReadableFrequencyToHumanReadable({
|
||||
frequency: habit.frequency,
|
||||
isRecurRule: pathname.includes("habits"),
|
||||
isRecurRule,
|
||||
timezone: settings.system.timezone
|
||||
})})}
|
||||
</p>
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'; // Added
|
||||
import { Label } from '@/components/ui/label'; // Added
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; // Added
|
||||
import { useHabits } from '@/hooks/useHabits'
|
||||
import { habitsAtom } from '@/lib/atoms'
|
||||
import { HabitIcon, TaskIcon } from '@/lib/constants'
|
||||
import { Habit } from '@/lib/types'
|
||||
import { getHabitFreq } from '@/lib/utils'; // Added
|
||||
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 { ArrowDownWideNarrow, ArrowUpNarrowWide, Plus, Search } from 'lucide-react'; // Added sort icons, Search icon
|
||||
import { DateTime } from 'luxon'; // Added
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useEffect, useMemo, useState } from 'react'; // Added useMemo, useEffect
|
||||
import { habitsAtom, settingsAtom, browserSettingsAtom } from '@/lib/atoms'
|
||||
import EmptyState from './EmptyState'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import HabitItem from './HabitItem'
|
||||
import AddEditHabitModal from './AddEditHabitModal'
|
||||
import ConfirmDialog from './ConfirmDialog'
|
||||
import EmptyState from './EmptyState'
|
||||
import HabitItem from './HabitItem'
|
||||
import { Habit } from '@/lib/types'
|
||||
import { useHabits } from '@/hooks/useHabits'
|
||||
import { HabitIcon, TaskIcon } from '@/lib/constants'
|
||||
import { ViewToggle } from './ViewToggle'
|
||||
import { Input } from '@/components/ui/input' // Added
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' // Added
|
||||
import { Label } from '@/components/ui/label' // Added
|
||||
import { DateTime } from 'luxon' // Added
|
||||
import { getHabitFreq } from '@/lib/utils' // Added
|
||||
|
||||
export default function HabitList({ isTasksView }: { isTasksView: boolean}) {
|
||||
export default function HabitList() {
|
||||
const t = useTranslations('HabitList');
|
||||
const { saveHabit, deleteHabit } = useHabits()
|
||||
const [habitsData] = useAtom(habitsAtom) // setHabitsData removed as it's not used
|
||||
const [browserSettings] = useAtom(browserSettingsAtom)
|
||||
const isTasksView = browserSettings.viewType === 'tasks'
|
||||
// const [settings] = useAtom(settingsAtom); // settingsAtom is not directly used in HabitList itself.
|
||||
|
||||
type SortableField = 'name' | 'coinReward' | 'dueDate' | 'frequency';
|
||||
type SortOrder = 'asc' | 'desc';
|
||||
@@ -126,11 +130,17 @@ export default function HabitList({ isTasksView }: { isTasksView: boolean}) {
|
||||
{t(isTasksView ? 'myTasks' : 'myHabits')}
|
||||
</h1>
|
||||
<span>
|
||||
<Button onClick={() => setModalConfig({ isOpen: true, isTask: isTasksView })}>
|
||||
<Plus className='mr-2 h-4 w-4' />{isTasksView ? t("addTaskButton") : t("addHabitButton")}
|
||||
<Button className="mr-2" onClick={() => setModalConfig({ isOpen: true, isTask: true })}>
|
||||
<Plus className="mr-2 h-4 w-4" /> {t('addTaskButton')}
|
||||
</Button>
|
||||
<Button onClick={() => setModalConfig({ isOpen: true, isTask: false })}>
|
||||
<Plus className="mr-2 h-4 w-4" /> {t('addHabitButton')}
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
<div className='py-4'>
|
||||
<ViewToggle />
|
||||
</div>
|
||||
|
||||
{/* Search and Sort Controls */}
|
||||
<div className="flex flex-col sm:flex-row items-center gap-4 my-4">
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { completedHabitsMapAtom, hasTasksAtom, settingsAtom } from '@/lib/atoms'; // Added completedHabitsMapAtom
|
||||
import { Habit } from '@/lib/types';
|
||||
import { d2s, getNow } from '@/lib/utils'; // Removed getCompletedHabitsForDate
|
||||
import { useAtom } from 'jotai';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
import { Habit } from '@/lib/types'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { d2s, getNow, t2d } from '@/lib/utils' // Removed getCompletedHabitsForDate
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { settingsAtom, hasTasksAtom, completedHabitsMapAtom } from '@/lib/atoms' // Added completedHabitsMapAtom
|
||||
|
||||
interface HabitStreakProps {
|
||||
habits: Habit[]
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import { FormattedNumber } from '@/components/FormattedNumber'
|
||||
import { Logo } from '@/components/Logo'
|
||||
import { useCoins } from '@/hooks/useCoins'
|
||||
import { settingsAtom } from '@/lib/atoms'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useAtom } from 'jotai'
|
||||
import { Coins } from 'lucide-react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import Link from 'next/link'
|
||||
import { coinsAtom, settingsAtom, browserSettingsAtom } from '@/lib/atoms'
|
||||
import { useCoins } from '@/hooks/useCoins'
|
||||
import { FormattedNumber } from '@/components/FormattedNumber'
|
||||
import { Menu, Settings, User, Info, Coins } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Logo } from '@/components/Logo'
|
||||
import NotificationBell from './NotificationBell'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import AboutModal from './AboutModal'
|
||||
import Link from 'next/link'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Profile } from './Profile'
|
||||
import { useHelpers } from '@/lib/client-helpers'
|
||||
|
||||
interface HeaderProps {
|
||||
className?: string
|
||||
@@ -19,6 +30,7 @@ const TodayEarnedCoins = dynamic(() => import('./TodayEarnedCoins'), { ssr: fals
|
||||
|
||||
export default function Header({ className }: HeaderProps) {
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const [browserSettings] = useAtom(browserSettingsAtom)
|
||||
const { balance } = useCoins()
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Sparkles } from "lucide-react"
|
||||
|
||||
export function Logo() {
|
||||
return (
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
'use client'
|
||||
|
||||
import { useHelpers } from '@/lib/client-helpers'
|
||||
import { HabitIcon, TaskIcon } from '@/lib/constants'
|
||||
import { Calendar, Coins, Gift, Home } from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Home, Calendar, List, Gift, Coins, Settings, Info, CheckSquare } from 'lucide-react'
|
||||
import { useAtom } from 'jotai'
|
||||
import { browserSettingsAtom } from '@/lib/atoms'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import AboutModal from './AboutModal'
|
||||
import { HabitIcon, TaskIcon } from '@/lib/constants'
|
||||
import { useHelpers } from '@/lib/client-helpers'
|
||||
|
||||
type ViewPort = 'main' | 'mobile'
|
||||
|
||||
interface NavigationProps {
|
||||
className?: string
|
||||
viewPort: ViewPort
|
||||
}
|
||||
|
||||
export default function Navigation({ viewPort }: NavigationProps) {
|
||||
export default function Navigation({ className, viewPort }: NavigationProps) {
|
||||
const t = useTranslations('Navigation')
|
||||
const [showAbout, setShowAbout] = useState(false)
|
||||
const [isMobileView, setIsMobileView] = useState(false)
|
||||
const [browserSettings] = useAtom(browserSettingsAtom)
|
||||
const isTasksView = browserSettings.viewType === 'tasks'
|
||||
const { isIOS } = useHelpers()
|
||||
const pathname = usePathname();
|
||||
|
||||
const navItems = () => [
|
||||
const navItems = (isTasksView: boolean) => [
|
||||
{ icon: Home, label: t('dashboard'), href: '/', position: 'main' },
|
||||
{ icon: HabitIcon, label: t('habits'), href: '/habits', position: 'main' },
|
||||
{ icon: TaskIcon, label: t('tasks'), href: '/tasks', position: 'main' },
|
||||
{
|
||||
icon: isTasksView ? TaskIcon : HabitIcon,
|
||||
label: isTasksView ? t('tasks') : t('habits'),
|
||||
href: '/habits',
|
||||
position: 'main'
|
||||
},
|
||||
{ icon: Calendar, label: t('calendar'), href: '/calendar', position: 'main' },
|
||||
{ icon: Gift, label: t('wishlist'), href: '/wishlist', position: 'main' },
|
||||
{ icon: Coins, label: t('coins'), href: '/coins', position: 'main' },
|
||||
@@ -51,16 +58,12 @@ export default function Navigation({ viewPort }: NavigationProps) {
|
||||
<>
|
||||
<div className={isIOS ? "pb-20" : "pb-16"} /> {/* Add padding at the bottom to prevent content from being hidden */}
|
||||
<nav className={`lg:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 shadow-lg ${isIOS ? "pb-4" : ""}`}>
|
||||
<div className="grid grid-cols-6 w-full">
|
||||
{...navItems().map((item) => (
|
||||
<div className="grid grid-cols-5 w-full">
|
||||
{[...navItems(isTasksView).filter(item => item.position === 'main'), ...navItems(isTasksView).filter(item => item.position === 'bottom')].map((item) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className={"flex flex-col items-center py-2 hover:text-blue-600 dark:hover:text-blue-300 " +
|
||||
(pathname === (item.href) ?
|
||||
"text-blue-500 dark:text-blue-500" :
|
||||
"text-gray-300 dark:text-gray-300")
|
||||
}
|
||||
className="flex flex-col items-center justify-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>
|
||||
@@ -80,16 +83,13 @@ export default function Navigation({ viewPort }: NavigationProps) {
|
||||
<div className="flex flex-col h-0 flex-1 bg-gray-800">
|
||||
<div className="flex-1 flex flex-col pt-5 pb-4 overflow-y-auto">
|
||||
<nav className="mt-5 flex-1 px-2 space-y-1">
|
||||
{navItems().filter(item => item.position === 'main').map((item) => (
|
||||
{navItems(isTasksView).filter(item => item.position === 'main').map((item) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className={"flex items-center px-2 py-2 font-medium rounded-md " +
|
||||
(pathname === (item.href) ?
|
||||
"text-blue-500 hover:text-blue-600 hover:bg-gray-700" :
|
||||
"text-gray-300 hover:text-white hover:bg-gray-700")}
|
||||
className="group flex items-center px-2 py-2 text-sm leading-6 font-medium rounded-md text-gray-300 hover:text-white hover:bg-gray-700"
|
||||
>
|
||||
<item.icon className="mr-4 flex-shrink-0 h-6 w-6" aria-hidden="true" />
|
||||
<item.icon className="mr-4 flex-shrink-0 h-6 w-6 text-gray-400" aria-hidden="true" />
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||
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 { t2d } from '@/lib/utils';
|
||||
import Link from 'next/link';
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||
import { Info } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { CoinTransaction, HabitsData, User, UserData, WishlistData } from '@/lib/types';
|
||||
import { t2d } from '@/lib/utils';
|
||||
import { Info } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface NotificationDropdownProps {
|
||||
currentUser: User | null;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { User } from '@/lib/types';
|
||||
import { User as UserIcon } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
import { Button } from './ui/button';
|
||||
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 { toast } from '@/hooks/use-toast';
|
||||
import { useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface PasswordEntryFormProps {
|
||||
user: User;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { useHabits } from '@/hooks/useHabits'
|
||||
import { habitsAtom, pomodoroAtom, pomodoroTodayCompletionsAtom } from '@/lib/atoms'
|
||||
import { Play, Pause, RotateCw, Minus, X, Clock, SkipForward } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { Clock, Minus, Pause, Play, RotateCw, SkipForward, X } from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { settingsAtom, pomodoroAtom, habitsAtom, pomodoroTodayCompletionsAtom } from '@/lib/atoms'
|
||||
// import { getCompletionsForDate, getTodayInTimezone } from '@/lib/utils' // Not used after pomodoroTodayCompletionsAtom
|
||||
import { useHabits } from '@/hooks/useHabits'
|
||||
|
||||
interface PomoConfig {
|
||||
getLabels: () => string[]
|
||||
@@ -38,6 +39,7 @@ export default function PomodoroTimer() {
|
||||
},
|
||||
}
|
||||
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const [pomo, setPomo] = useAtom(pomodoroAtom)
|
||||
const { show, selectedHabitId, autoStart, minimized } = pomo
|
||||
const [habitsData] = useAtom(habitsAtom)
|
||||
@@ -112,28 +114,28 @@ export default function PomodoroTimer() {
|
||||
|
||||
// Timer logic
|
||||
useEffect(() => {
|
||||
let interval: ReturnType<typeof setInterval> | null = null;
|
||||
let interval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
if (state === "started") {
|
||||
if (state === 'started') {
|
||||
// Calculate the target end time based on current timeLeft
|
||||
const targetEndTime = Date.now() + timeLeft * 1000;
|
||||
const targetEndTime = Date.now() + timeLeft * 1000
|
||||
|
||||
interval = setInterval(() => {
|
||||
const remaining = Math.floor((targetEndTime - Date.now()) / 1000);
|
||||
const remaining = Math.floor((targetEndTime - Date.now()) / 1000)
|
||||
|
||||
if (remaining <= 0) {
|
||||
handleTimerEnd();
|
||||
handleTimerEnd()
|
||||
} else {
|
||||
setTimeLeft(remaining);
|
||||
setTimeLeft(remaining)
|
||||
}
|
||||
}, 1000);
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// return handles any other states
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [state]);
|
||||
if (interval) clearInterval(interval)
|
||||
}
|
||||
}, [state])
|
||||
|
||||
const handleTimerEnd = async () => {
|
||||
setState("stopped")
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import { signOut } from "@/app/actions/user"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { settingsAtom, userSelectAtom } from "@/lib/atoms"
|
||||
import { useHelpers } from "@/lib/client-helpers"
|
||||
import { useAtom } from "jotai"
|
||||
import { ArrowRightLeft, Crown, Info, LogOut, Moon, Palette, Settings, Sun, User } from "lucide-react"
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useTheme } from "next-themes"
|
||||
import Link from "next/link"
|
||||
import { useState } from "react"
|
||||
import AboutModal from "./AboutModal"
|
||||
import { Settings, Info, User, Moon, Sun, Palette, ArrowRightLeft, LogOut, Crown } from "lucide-react"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'
|
||||
import UserForm from './UserForm'
|
||||
import Link from "next/link"
|
||||
import { useAtom } from "jotai"
|
||||
import { settingsAtom, userSelectAtom } from "@/lib/atoms"
|
||||
import AboutModal from "./AboutModal"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { signOut } from "@/app/actions/user"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { useHelpers } from "@/lib/client-helpers"
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
export function Profile() {
|
||||
const t = useTranslations('Profile');
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { createUser, updateUser, updateUserPassword, uploadAvatar } from '@/app/actions/data';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { serverSettingsAtom, usersAtom } from '@/lib/atoms';
|
||||
import { useHelpers } from '@/lib/client-helpers';
|
||||
import { Permission } from '@/lib/types';
|
||||
import { passwordSchema, usernameSchema } from '@/lib/zod';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import _ from 'lodash';
|
||||
import { User as UserIcon } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { PermissionSelector } from './PermissionSelector';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
import { Button } from './ui/button';
|
||||
import { passwordSchema, usernameSchema } from '@/lib/zod';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Input } from './ui/input';
|
||||
import { Button } from './ui/button';
|
||||
import { Label } from './ui/label';
|
||||
import { Switch } from './ui/switch';
|
||||
import { Permission } from '@/lib/types';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { serverSettingsAtom, usersAtom } 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';
|
||||
import { PermissionSelector } from './PermissionSelector';
|
||||
import { useHelpers } from '@/lib/client-helpers';
|
||||
|
||||
interface UserFormProps {
|
||||
userId?: string; // if provided, we're editing; if not, we're creating
|
||||
|
||||
@@ -1,38 +1,97 @@
|
||||
'use client';
|
||||
|
||||
import { signIn } from '@/app/actions/user';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { usersAtom } from '@/lib/atoms';
|
||||
import { useHelpers } from '@/lib/client-helpers';
|
||||
import { SafeUser, User } from '@/lib/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Description } from '@radix-ui/react-dialog';
|
||||
import { useAtom } from 'jotai';
|
||||
import { Crown, Plus, User as UserIcon, UserRoundPen } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import PasswordEntryForm from './PasswordEntryForm';
|
||||
import UserForm from './UserForm';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
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 { useAtom } from 'jotai';
|
||||
import { usersAtom } 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';
|
||||
import { SafeUser, User } from '@/lib/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useHelpers } from '@/lib/client-helpers';
|
||||
|
||||
function UserCard({
|
||||
user,
|
||||
onSelect,
|
||||
onEdit,
|
||||
showEdit,
|
||||
isCurrentUser
|
||||
isCurrentUser,
|
||||
currentLoggedInUserId, // For "don't delete self" check
|
||||
onUserDeleted // Callback to update usersAtom
|
||||
}: {
|
||||
user: User,
|
||||
onSelect: () => void,
|
||||
onEdit: () => void,
|
||||
showEdit: boolean,
|
||||
isCurrentUser: boolean
|
||||
isCurrentUser: boolean,
|
||||
currentLoggedInUserId?: string,
|
||||
onUserDeleted: (userId: string) => void,
|
||||
}) {
|
||||
const t = useTranslations('UserSelectModal');
|
||||
const tWarning = useTranslations('Warning');
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const handleDeleteUser = async () => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const response = await fetch('/api/user/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userId: user.id }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast({
|
||||
title: t('deleteUserSuccessTitle'),
|
||||
description: t('deleteUserSuccessDescription', { username: user.username }),
|
||||
});
|
||||
onUserDeleted(user.id);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
toast({
|
||||
title: t('deleteUserErrorTitle'),
|
||||
description: errorData.error || t('genericError'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t('deleteUserErrorTitle'),
|
||||
description: t('networkError'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setShowDeleteConfirm(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={user.id} className="relative group">
|
||||
<button
|
||||
onClick={onSelect}
|
||||
disabled={isDeleting} // Disable main button while deleting this user
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-2 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors w-full",
|
||||
isCurrentUser && "ring-2 ring-primary"
|
||||
@@ -53,15 +112,56 @@ function UserCard({
|
||||
</span>
|
||||
</button>
|
||||
{showEdit && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
className="absolute top-0 right-0 p-1 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<UserRoundPen className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="absolute top-0 right-0 flex space-x-1">
|
||||
{showEdit && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevent card selection
|
||||
onEdit();
|
||||
}}
|
||||
disabled={isDeleting}
|
||||
className="p-1 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 transition-colors"
|
||||
title={t('editUserTooltip')}
|
||||
>
|
||||
<UserRoundPen className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
{showEdit && user.id !== currentLoggedInUserId && (
|
||||
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevent card selection
|
||||
setShowDeleteConfirm(true);
|
||||
}}
|
||||
disabled={isDeleting}
|
||||
className="p-1 rounded-full bg-red-200 hover:bg-red-300 dark:bg-red-700 dark:hover:bg-red-600 transition-colors text-red-600 dark:text-red-300"
|
||||
title={t('deleteUserTooltip')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{tWarning('areYouSure')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t('deleteUserConfirmation', { username: user.username })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={(e) => { e.stopPropagation(); setShowDeleteConfirm(false);}} disabled={isDeleting}>{tWarning('cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteUser();}}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? t('deletingButtonText') : t('confirmDeleteButtonText')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -86,32 +186,36 @@ function AddUserButton({ onClick }: { onClick: () => void }) {
|
||||
|
||||
function UserSelectionView({
|
||||
users,
|
||||
currentUser,
|
||||
currentUserFromHook, // Renamed to avoid confusion with map variable
|
||||
onUserSelect,
|
||||
onEditUser,
|
||||
onCreateUser,
|
||||
onUserDeleted, // Pass through the delete handler
|
||||
}: {
|
||||
users: User[],
|
||||
currentUser?: SafeUser,
|
||||
currentUserFromHook?: SafeUser,
|
||||
onUserSelect: (userId: string) => void,
|
||||
onEditUser: (userId: string) => void,
|
||||
onCreateUser: () => void,
|
||||
onUserDeleted: (userId: string) => void,
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-4 p-2 max-h-80 overflow-y-auto">
|
||||
{users
|
||||
.filter(user => user.id !== currentUser?.id)
|
||||
.filter(user => user.id !== currentUserFromHook?.id) // Show other users
|
||||
.map((user) => (
|
||||
<UserCard
|
||||
key={user.id}
|
||||
user={user}
|
||||
onSelect={() => onUserSelect(user.id)}
|
||||
onEdit={() => onEditUser(user.id)}
|
||||
showEdit={!!currentUser?.isAdmin}
|
||||
isCurrentUser={false}
|
||||
showEdit={!!currentUserFromHook?.isAdmin}
|
||||
isCurrentUser={false} // This card isn't the currently logged-in user for switching TO
|
||||
currentLoggedInUserId={currentUserFromHook?.id} // For the "don't delete self" check
|
||||
onUserDeleted={onUserDeleted}
|
||||
/>
|
||||
))}
|
||||
{currentUser?.isAdmin && <AddUserButton onClick={onCreateUser} />}
|
||||
{currentUserFromHook?.isAdmin && <AddUserButton onClick={onCreateUser} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -122,10 +226,17 @@ export default function UserSelectModal({ onClose }: { onClose: () => void }) {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [usersData] = useAtom(usersAtom);
|
||||
const [usersData, setUsersData] = useAtom(usersAtom);
|
||||
const users = usersData.users;
|
||||
const { currentUser } = useHelpers();
|
||||
|
||||
const handleUserDeleted = (userIdToDelete: string) => {
|
||||
setUsersData(prevData => ({
|
||||
...prevData,
|
||||
users: prevData.users.filter(u => u.id !== userIdToDelete)
|
||||
}));
|
||||
};
|
||||
|
||||
const handleUserSelect = (userId: string) => {
|
||||
setSelectedUser(userId);
|
||||
setError('');
|
||||
@@ -166,10 +277,11 @@ export default function UserSelectModal({ onClose }: { onClose: () => void }) {
|
||||
{!selectedUser && !isCreating && !isEditing ? (
|
||||
<UserSelectionView
|
||||
users={users}
|
||||
currentUser={currentUser}
|
||||
currentUserFromHook={currentUser}
|
||||
onUserSelect={handleUserSelect}
|
||||
onEditUser={handleEditUser}
|
||||
onCreateUser={handleCreateUser}
|
||||
onUserDeleted={handleUserDeleted}
|
||||
/>
|
||||
) : isCreating || isEditing ? (
|
||||
<UserForm
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import { browserSettingsAtom, habitsAtom, settingsAtom } from '@/lib/atoms'
|
||||
import { HabitIcon, TaskIcon } from '@/lib/constants'
|
||||
import { cn, isHabitDueToday } from '@/lib/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { browserSettingsAtom, habitsAtom, settingsAtom } from '@/lib/atoms'
|
||||
import type { ViewType } from '@/lib/types'
|
||||
import { HabitIcon, TaskIcon } from '@/lib/constants'
|
||||
import { isHabitDueToday } from '@/lib/utils'
|
||||
import { NotificationBadge } from './ui/notification-badge'
|
||||
|
||||
interface ViewToggleProps {
|
||||
defaultView?: ViewType
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ViewToggle({
|
||||
defaultView = 'habits',
|
||||
className
|
||||
}: ViewToggleProps) {
|
||||
const t = useTranslations('ViewToggle')
|
||||
const [browserSettings, setBrowserSettings] = useAtom(browserSettingsAtom)
|
||||
const [habits] = useAtom(habitsAtom)
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const handleViewChange = () => {
|
||||
router.push(pathname.includes("habits") ? "/tasks" : "/habits");
|
||||
const handleViewChange = (checked: boolean) => {
|
||||
const newView = checked ? 'tasks' : 'habits'
|
||||
setBrowserSettings({
|
||||
...browserSettings,
|
||||
viewType: newView,
|
||||
})
|
||||
}
|
||||
|
||||
// Calculate due tasks count
|
||||
@@ -35,10 +40,10 @@ export function ViewToggle({
|
||||
<div className={cn('inline-flex rounded-full bg-muted/50 h-8', className)}>
|
||||
<div className="relative flex gap-0.5 rounded-full bg-background p-0.5 h-full">
|
||||
<button
|
||||
onClick={handleViewChange}
|
||||
onClick={() => handleViewChange(false)}
|
||||
className={cn(
|
||||
'relative z-10 rounded-full px-4 py-2 text-sm font-medium transition-colors flex items-center gap-2',
|
||||
pathname.includes('habits') ? 'text-primary-foreground' : 'text-muted-foreground hover:text-foreground'
|
||||
browserSettings.viewType === 'habits' ? 'text-primary-foreground' : 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<HabitIcon className="h-4 w-4" />
|
||||
@@ -47,14 +52,14 @@ export function ViewToggle({
|
||||
<NotificationBadge
|
||||
label={dueTasksCount}
|
||||
show={dueTasksCount > 0}
|
||||
variant={pathname.includes('tasks') ? 'secondary' : 'default'}
|
||||
variant={browserSettings.viewType === 'tasks' ? 'secondary' : 'default'}
|
||||
className="shadow-md"
|
||||
>
|
||||
<button
|
||||
onClick={handleViewChange}
|
||||
onClick={() => handleViewChange(true)}
|
||||
className={cn(
|
||||
'relative z-10 rounded-full px-4 py-2 text-sm font-medium transition-colors flex items-center gap-2',
|
||||
pathname.includes('tasks') ? 'text-primary-foreground' : 'text-muted-foreground hover:text-foreground'
|
||||
browserSettings.viewType === 'tasks' ? 'text-primary-foreground' : 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<TaskIcon className="h-4 w-4" />
|
||||
@@ -64,7 +69,7 @@ export function ViewToggle({
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-0.5 top-0.5 h-[calc(100%-0.25rem)] rounded-full bg-primary transition-transform',
|
||||
pathname.includes('habits') ? 'w-[calc(50%-0.125rem)]' : 'w-[calc(50%-0.125rem)] translate-x-[calc(100%+0.125rem)]'
|
||||
browserSettings.viewType === 'habits' ? 'w-[calc(50%-0.125rem)]' : 'w-[calc(50%-0.125rem)] translate-x-[calc(100%+0.125rem)]'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { WishlistItemType, User } from '@/lib/types'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { usersAtom } from '@/lib/atoms'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
|
||||
import { useHelpers } from '@/lib/client-helpers'
|
||||
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 {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -7,13 +15,6 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { usersAtom } from '@/lib/atoms'
|
||||
import { useHelpers } from '@/lib/client-helpers'
|
||||
import { User, WishlistItemType } from '@/lib/types'
|
||||
import { useAtom } from 'jotai'
|
||||
import { Archive, ArchiveRestore, Coins, Edit, Gift, MoreVertical, Trash2 } from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
|
||||
|
||||
interface WishlistItemProps {
|
||||
item: WishlistItemType
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import * as React from "react"
|
||||
import { Moon, MoonIcon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
141
components/ui/alert-dialog.tsx
Normal file
141
components/ui/alert-dialog.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
services:
|
||||
habittrove:
|
||||
image: ghcr.io/manindark/habittrove
|
||||
container_name: habittrove
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- "./data:/app/data"
|
||||
- "./backups:/app/backups"
|
||||
image: dohsimpson/habittrove
|
||||
environment:
|
||||
- AUTH_SECRET=your-secret-key-here # Replace with your actual secret
|
||||
|
||||
40
lib/atoms.ts
40
lib/atoms.ts
@@ -1,37 +1,45 @@
|
||||
import { atom } from "jotai";
|
||||
import {
|
||||
getDefaultSettings,
|
||||
getDefaultHabitsData,
|
||||
getDefaultCoinsData,
|
||||
getDefaultWishlistData,
|
||||
Habit,
|
||||
ViewType,
|
||||
getDefaultUsersData,
|
||||
CompletionCache,
|
||||
getDefaultServerSettings,
|
||||
User,
|
||||
} from "./types";
|
||||
import {
|
||||
getTodayInTimezone,
|
||||
isSameDate,
|
||||
t2d,
|
||||
calculateCoinsEarnedToday,
|
||||
calculateCoinsSpentToday,
|
||||
calculateTotalEarned,
|
||||
calculateTotalSpent,
|
||||
calculateCoinsSpentToday,
|
||||
calculateTransactionsToday,
|
||||
getCompletionsForToday,
|
||||
getHabitFreq,
|
||||
getTodayInTimezone,
|
||||
getISODate,
|
||||
isHabitDueToday,
|
||||
getNow,
|
||||
isHabitDue,
|
||||
t2d
|
||||
getHabitFreq
|
||||
} from "@/lib/utils";
|
||||
import { atom } from "jotai";
|
||||
import { atomFamily, atomWithStorage } from "jotai/utils";
|
||||
import { DateTime } from "luxon";
|
||||
import {
|
||||
CompletionCache,
|
||||
Freq,
|
||||
getDefaultCoinsData,
|
||||
getDefaultHabitsData,
|
||||
getDefaultServerSettings,
|
||||
getDefaultSettings,
|
||||
getDefaultUsersData,
|
||||
getDefaultWishlistData,
|
||||
Habit
|
||||
} from "./types";
|
||||
import { Freq } from "./types";
|
||||
|
||||
export interface BrowserSettings {
|
||||
viewType: ViewType
|
||||
expandedHabits: boolean
|
||||
expandedTasks: boolean
|
||||
expandedWishlist: boolean
|
||||
}
|
||||
|
||||
export const browserSettingsAtom = atomWithStorage('browserSettings', {
|
||||
viewType: 'habits',
|
||||
expandedHabits: false,
|
||||
expandedTasks: false,
|
||||
expandedWishlist: false
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// client helpers
|
||||
'use-client'
|
||||
|
||||
import { useAtom } from 'jotai'
|
||||
import { useSession } from "next-auth/react"
|
||||
import { User, UserId } from './types'
|
||||
import { useAtom } from 'jotai'
|
||||
import { usersAtom } from './atoms'
|
||||
import { checkPermission } from './utils'
|
||||
|
||||
@@ -13,7 +14,7 @@ export function useHelpers() {
|
||||
const currentUser = usersData.users.find((u) => u.id === currentUserId)
|
||||
// detect iOS: https://stackoverflow.com/a/9039885
|
||||
function iOS() {
|
||||
return typeof navigator !== "undefined" && ([
|
||||
return [
|
||||
'iPad Simulator',
|
||||
'iPhone Simulator',
|
||||
'iPod Simulator',
|
||||
@@ -22,7 +23,7 @@ export function useHelpers() {
|
||||
'iPod',
|
||||
].includes(navigator.platform)
|
||||
// iPad on iOS 13 detection
|
||||
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document))
|
||||
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -183,6 +183,8 @@ export type CompletionCache = {
|
||||
}
|
||||
}
|
||||
|
||||
export type ViewType = 'habits' | 'tasks'
|
||||
|
||||
export interface JotaiHydrateInitialValues {
|
||||
settings: Settings;
|
||||
coins: CoinsData;
|
||||
|
||||
@@ -150,7 +150,17 @@
|
||||
"selectUserTitle": "Benutzer auswählen",
|
||||
"signInSuccessTitle": "Erfolgreich angemeldet",
|
||||
"signInSuccessDescription": "Willkommen zurück, {username}!",
|
||||
"errorInvalidPassword": "Ungültiges Passwort"
|
||||
"errorInvalidPassword": "Ungültiges Passwort",
|
||||
"deleteUserConfirmation": "Sind Sie sicher, dass Sie Benutzer {username} löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"confirmDeleteButtonText": "Löschen",
|
||||
"deletingButtonText": "Wird gelöscht...",
|
||||
"deleteUserSuccessTitle": "Benutzer gelöscht",
|
||||
"deleteUserSuccessDescription": "Benutzer {username} wurde erfolgreich gelöscht.",
|
||||
"deleteUserErrorTitle": "Löschen fehlgeschlagen",
|
||||
"genericError": "Ein unerwarteter Fehler ist aufgetreten.",
|
||||
"networkError": "Ein Netzwerkfehler ist aufgetreten. Bitte versuchen Sie es erneut.",
|
||||
"editUserTooltip": "Benutzer bearbeiten",
|
||||
"deleteUserTooltip": "Benutzer löschen"
|
||||
},
|
||||
"CoinsManager": {
|
||||
"title": "Münzverwaltung",
|
||||
@@ -403,5 +413,9 @@
|
||||
"addedCoinsDescription": "{amount} Münzen hinzugefügt",
|
||||
"removedCoinsDescription": "{amount} Münzen entfernt",
|
||||
"transactionNotFoundDescription": "Transaktion nicht gefunden"
|
||||
},
|
||||
"Warning": {
|
||||
"areYouSure": "Sind Sie sicher?",
|
||||
"cancel": "Abbrechen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,17 @@
|
||||
"selectUserTitle": "Select User",
|
||||
"signInSuccessTitle": "Signed in successfully",
|
||||
"signInSuccessDescription": "Welcome back, {username}!",
|
||||
"errorInvalidPassword": "invalid password"
|
||||
"errorInvalidPassword": "invalid password",
|
||||
"deleteUserConfirmation": "Are you sure you want to delete user {username}? This action cannot be undone.",
|
||||
"confirmDeleteButtonText": "Delete",
|
||||
"deletingButtonText": "Deleting...",
|
||||
"deleteUserSuccessTitle": "User Deleted",
|
||||
"deleteUserSuccessDescription": "User {username} has been successfully deleted.",
|
||||
"deleteUserErrorTitle": "Deletion Failed",
|
||||
"genericError": "An unexpected error occurred.",
|
||||
"networkError": "A network error occurred. Please try again.",
|
||||
"deleteUserTooltip": "Delete user",
|
||||
"editUserTooltip": "Edit user"
|
||||
},
|
||||
"CoinsManager": {
|
||||
"title": "Coins Management",
|
||||
@@ -403,5 +413,9 @@
|
||||
"addedCoinsDescription": "Added {amount} coins",
|
||||
"removedCoinsDescription": "Removed {amount} coins",
|
||||
"transactionNotFoundDescription": "Transaction not found"
|
||||
},
|
||||
"Warning": {
|
||||
"areYouSure": "Are you sure?",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
},
|
||||
"HabitList": {
|
||||
"myTasks": "Mis tareas",
|
||||
"myHabits": "Mis hábitos",
|
||||
"myHabits": "Mis hábitos",
|
||||
"addTaskButton": "Añadir tarea",
|
||||
"addHabitButton": "Añadir hábito",
|
||||
"searchTasksPlaceholder": "Buscar tareas...",
|
||||
@@ -150,7 +150,17 @@
|
||||
"selectUserTitle": "Seleccionar usuario",
|
||||
"signInSuccessTitle": "Inicio de sesión exitoso",
|
||||
"signInSuccessDescription": "¡Bienvenido de nuevo, {username}!",
|
||||
"errorInvalidPassword": "contraseña inválida"
|
||||
"errorInvalidPassword": "contraseña inválida",
|
||||
"deleteUserConfirmation": "¿Estás seguro de que quieres eliminar al usuario {username}? Esta acción no se puede deshacer.",
|
||||
"confirmDeleteButtonText": "Eliminar",
|
||||
"deletingButtonText": "Eliminando...",
|
||||
"deleteUserSuccessTitle": "Usuario eliminado",
|
||||
"deleteUserSuccessDescription": "El usuario {username} ha sido eliminado correctamente.",
|
||||
"deleteUserErrorTitle": "Error al eliminar",
|
||||
"genericError": "Ocurrió un error inesperado.",
|
||||
"networkError": "Ocurrió un error de red. Por favor, inténtalo de nuevo.",
|
||||
"editUserTooltip": "Editar usuario",
|
||||
"deleteUserTooltip": "Eliminar usuario"
|
||||
},
|
||||
"CoinsManager": {
|
||||
"title": "Gestión de monedas",
|
||||
@@ -190,7 +200,7 @@
|
||||
"focusLabel2": "Tú puedes",
|
||||
"focusLabel3": "Sigue adelante",
|
||||
"focusLabel4": "Hazlo",
|
||||
"focusLabel5": "Haz que suceda",
|
||||
"focusLabel5": "Haz que suceda",
|
||||
"focusLabel6": "Mantente fuerte",
|
||||
"focusLabel7": "Esfuérzate",
|
||||
"focusLabel8": "Un paso a la vez",
|
||||
@@ -403,5 +413,9 @@
|
||||
"addedCoinsDescription": "Añadidas {amount} monedas",
|
||||
"removedCoinsDescription": "Quitadas {amount} monedas",
|
||||
"transactionNotFoundDescription": "Transacción no encontrada"
|
||||
},
|
||||
"Warning": {
|
||||
"areYouSure": "¿Estás seguro?",
|
||||
"cancel": "Cancelar"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,17 @@
|
||||
"selectUserTitle": "Sélectionner un utilisateur",
|
||||
"signInSuccessTitle": "Connecté avec succès",
|
||||
"signInSuccessDescription": "Bienvenue, {username} !",
|
||||
"errorInvalidPassword": "mot de passe invalide"
|
||||
"errorInvalidPassword": "mot de passe invalide",
|
||||
"deleteUserConfirmation": "Êtes-vous sûr de vouloir supprimer l'utilisateur {username} ? Cette action est irréversible.",
|
||||
"confirmDeleteButtonText": "Supprimer",
|
||||
"deletingButtonText": "Suppression en cours...",
|
||||
"deleteUserSuccessTitle": "Utilisateur supprimé",
|
||||
"deleteUserSuccessDescription": "L'utilisateur {username} a été supprimé avec succès.",
|
||||
"deleteUserErrorTitle": "Échec de la suppression",
|
||||
"genericError": "Une erreur inattendue s'est produite.",
|
||||
"networkError": "Une erreur réseau s'est produite. Veuillez réessayer.",
|
||||
"editUserTooltip": "Modifier l'utilisateur",
|
||||
"deleteUserTooltip": "Supprimer l'utilisateur"
|
||||
},
|
||||
"CoinsManager": {
|
||||
"title": "Gestion des pièces",
|
||||
@@ -403,5 +413,9 @@
|
||||
"addedCoinsDescription": "Ajouté {amount} pièces",
|
||||
"removedCoinsDescription": "Retiré {amount} pièces",
|
||||
"transactionNotFoundDescription": "Transaction non trouvée"
|
||||
},
|
||||
"Warning": {
|
||||
"areYouSure": "Êtes-vous sûr ?",
|
||||
"cancel": "Annuler"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
"HabitContextMenuItems": {
|
||||
"startPomodoro": "ポモドーロを開始",
|
||||
"moveToToday": "今日に移動",
|
||||
"moveToTomorrow": "明日に移動",
|
||||
"moveToTomorrow": "明日に移動",
|
||||
"unpin": "ピン留めを解除",
|
||||
"pin": "ピン留めする",
|
||||
"edit": "編集",
|
||||
@@ -81,7 +81,7 @@
|
||||
"completeLabel": "完了",
|
||||
"timesSuffix": "回",
|
||||
"rewardLabel": "報酬",
|
||||
"coinsSuffix": "コイン",
|
||||
"coinsSuffix": "コイン",
|
||||
"shareLabel": "共有",
|
||||
"saveChangesButton": "変更を保存",
|
||||
"addTaskButton": "タスクを追加",
|
||||
@@ -110,11 +110,11 @@
|
||||
"addButton": "報酬を追加"
|
||||
},
|
||||
"Navigation": {
|
||||
"dashboard": "ダッシュボード",
|
||||
"dashboard": "ダッシュボード",
|
||||
"tasks": "タスク",
|
||||
"habits": "習慣",
|
||||
"calendar": "カレンダー",
|
||||
"wishlist": "ウィッシュリスト",
|
||||
"wishlist": "ウィッシュリスト",
|
||||
"coins": "コイン"
|
||||
},
|
||||
"TodayEarnedCoins": {
|
||||
@@ -150,7 +150,17 @@
|
||||
"selectUserTitle": "ユーザーを選択",
|
||||
"signInSuccessTitle": "サインインに成功しました",
|
||||
"signInSuccessDescription": "おかえりなさい、{username}さん!",
|
||||
"errorInvalidPassword": "パスワードが無効です"
|
||||
"errorInvalidPassword": "パスワードが無効です",
|
||||
"deleteUserConfirmation": "ユーザー {username} を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"confirmDeleteButtonText": "削除",
|
||||
"deletingButtonText": "削除中...",
|
||||
"deleteUserSuccessTitle": "ユーザーが削除されました",
|
||||
"deleteUserSuccessDescription": "ユーザー {username} は正常に削除されました。",
|
||||
"deleteUserErrorTitle": "削除に失敗しました",
|
||||
"genericError": "予期しないエラーが発生しました。",
|
||||
"networkError": "ネットワークエラーが発生しました。もう一度お試しください。",
|
||||
"editUserTooltip": "ユーザーを編集",
|
||||
"deleteUserTooltip": "ユーザーを削除"
|
||||
},
|
||||
"CoinsManager": {
|
||||
"title": "コイン管理",
|
||||
@@ -167,7 +177,7 @@
|
||||
"todaysTransactionsLabel": "今日の取引数",
|
||||
"transactionHistoryTitle": "取引履歴",
|
||||
"showLabel": "表示:",
|
||||
"entriesSuffix": "件",
|
||||
"entriesSuffix": "件",
|
||||
"showingEntries": "{from} から {to} 件(全 {total} 件)",
|
||||
"noTransactionsTitle": "取引履歴がありません",
|
||||
"noTransactionsDescription": "コインを獲得または使用すると、ここに取引履歴が表示されます",
|
||||
@@ -215,7 +225,7 @@
|
||||
"wakeLockNotSupported": "ブラウザがWake Lockをサポートしていません",
|
||||
"wakeLockInUse": "Wake Lockは既に使用中です",
|
||||
"wakeLockRequestError": "Wake Lockのリクエストエラー:",
|
||||
"wakeLockReleaseError": "Wake Lockの解放エラー:"
|
||||
"wakeLockReleaseError": "Wake Lockの解放エラー:"
|
||||
},
|
||||
"HabitCalendar": {
|
||||
"title": "習慣カレンダー",
|
||||
@@ -304,7 +314,7 @@
|
||||
"pleaseTryAgainDescription": "再度お試しください",
|
||||
"addNotePlaceholder": "メモを追加...",
|
||||
"saveNoteTitle": "メモを保存",
|
||||
"cancelButtonTitle": "キャンセル",
|
||||
"cancelButtonTitle": "キャンセル",
|
||||
"deleteNoteTitle": "メモを削除",
|
||||
"editNoteAriaLabel": "メモを編集"
|
||||
},
|
||||
@@ -323,12 +333,12 @@
|
||||
},
|
||||
"PasswordEntryForm": {
|
||||
"notYouButton": "違うユーザー?",
|
||||
"passwordLabel": "パスワード",
|
||||
"passwordLabel": "パスワード",
|
||||
"passwordPlaceholder": "パスワードを入力",
|
||||
"loginErrorToastTitle": "エラー",
|
||||
"loginFailedErrorToastDescription": "ログインに失敗しました",
|
||||
"cancelButton": "キャンセル",
|
||||
"loginButton": "ログイン"
|
||||
"loginButton": "ログイン"
|
||||
},
|
||||
"CompletionCountBadge": {
|
||||
"countCompleted": "完了 {completedCount}/{totalCount}"
|
||||
@@ -403,5 +413,9 @@
|
||||
"addedCoinsDescription": "{amount}コインを追加しました",
|
||||
"removedCoinsDescription": "{amount}コインを削除しました",
|
||||
"transactionNotFoundDescription": "取引が見つかりません"
|
||||
},
|
||||
"Warning": {
|
||||
"areYouSure": "本当によろしいですか?",
|
||||
"cancel": "キャンセル"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,17 @@
|
||||
"selectUserTitle": "Выбрать пользователя",
|
||||
"signInSuccessTitle": "Успешный вход",
|
||||
"signInSuccessDescription": "Добро пожаловать, {username}!",
|
||||
"errorInvalidPassword": "Неверный пароль"
|
||||
"errorInvalidPassword": "Неверный пароль",
|
||||
"deleteUserConfirmation": "Вы уверены, что хотите удалить пользователя {username}? Это действие нельзя отменить.",
|
||||
"confirmDeleteButtonText": "Удалить",
|
||||
"deletingButtonText": "Удаление...",
|
||||
"deleteUserSuccessTitle": "Пользователь удален",
|
||||
"deleteUserSuccessDescription": "Пользователь {username} успешно удален.",
|
||||
"deleteUserErrorTitle": "Ошибка удаления",
|
||||
"genericError": "Произошла непредвиденная ошибка.",
|
||||
"networkError": "Произошла сетевая ошибка. Пожалуйста, попробуйте еще раз.",
|
||||
"editUserTooltip": "Редактировать пользователя",
|
||||
"deleteUserTooltip": "Удалить пользователя"
|
||||
},
|
||||
"CoinsManager": {
|
||||
"title": "Управление монетами",
|
||||
@@ -403,5 +413,9 @@
|
||||
"addedCoinsDescription": "Добавлено {amount} монет",
|
||||
"removedCoinsDescription": "Удалено {amount} монет",
|
||||
"transactionNotFoundDescription": "Транзакция не найдена"
|
||||
},
|
||||
"Warning": {
|
||||
"areYouSure": "Вы уверены?",
|
||||
"cancel": "Отмена"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,17 @@
|
||||
"selectUserTitle": "选择用户",
|
||||
"signInSuccessTitle": "登录成功",
|
||||
"signInSuccessDescription": "欢迎回来,{username}!",
|
||||
"errorInvalidPassword": "密码错误"
|
||||
"errorInvalidPassword": "密码错误",
|
||||
"deleteUserConfirmation": "您确定要删除用户 {username} 吗?此操作无法撤销。",
|
||||
"confirmDeleteButtonText": "删除",
|
||||
"deletingButtonText": "正在删除...",
|
||||
"deleteUserSuccessTitle": "用户已删除",
|
||||
"deleteUserSuccessDescription": "用户 {username} 已成功删除。",
|
||||
"deleteUserErrorTitle": "删除失败",
|
||||
"genericError": "发生意外错误。",
|
||||
"networkError": "发生网络错误。请再试一次。",
|
||||
"editUserTooltip": "编辑用户",
|
||||
"deleteUserTooltip": "删除用户"
|
||||
},
|
||||
"CoinsManager": {
|
||||
"title": "金币管理",
|
||||
@@ -403,5 +413,9 @@
|
||||
"addedCoinsDescription": "添加了{amount}金币",
|
||||
"removedCoinsDescription": "移除了{amount}金币",
|
||||
"transactionNotFoundDescription": "未找到交易记录"
|
||||
},
|
||||
"Warning": {
|
||||
"areYouSure": "您确定吗?",
|
||||
"cancel": "取消"
|
||||
}
|
||||
}
|
||||
|
||||
564
package-lock.json
generated
564
package-lock.json
generated
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"name": "habittrove",
|
||||
"version": "0.2.11",
|
||||
"version": "0.2.13",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "habittrove",
|
||||
"version": "0.2.11",
|
||||
"version": "0.2.13",
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@next/font": "^14.2.15",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-context-menu": "^2.2.4",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
@@ -21,7 +22,7 @@
|
||||
"@radix-ui/react-scroll-area": "^1.2.4",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.3",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
@@ -1060,6 +1061,93 @@
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.14.tgz",
|
||||
"integrity": "sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-dialog": "1.1.14",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"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-alert-dialog/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"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-arrow": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
|
||||
@@ -1132,6 +1220,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||
@@ -1188,24 +1294,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz",
|
||||
"integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==",
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
|
||||
"integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
|
||||
"license": "MIT",
|
||||
"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-dismissable-layer": "1.1.3",
|
||||
"@radix-ui/react-focus-guards": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-portal": "1.1.3",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "^2.6.1"
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.10",
|
||||
"@radix-ui/react-focus-guards": "1.1.2",
|
||||
"@radix-ui/react-focus-scope": "1.1.7",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-presence": "1.1.4",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -1222,6 +1329,265 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
|
||||
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||
},
|
||||
"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-dialog/node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
|
||||
"integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
|
||||
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||
},
|
||||
"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-dialog/node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"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-dialog/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
|
||||
"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"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-dialog/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"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-dialog/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||
@@ -1406,6 +1772,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.4.tgz",
|
||||
@@ -1442,6 +1826,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
|
||||
@@ -1541,6 +1943,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-progress": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.1.tgz",
|
||||
@@ -1819,6 +2239,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.3.tgz",
|
||||
@@ -1899,11 +2337,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -1915,6 +2354,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -2009,6 +2463,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||
@@ -2040,6 +2512,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-effect-event": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
||||
@@ -7987,15 +8492,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz",
|
||||
"integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.0.tgz",
|
||||
"integrity": "sha512-sGsQtcjMqdQyijAHytfGEELB8FufGbfXIsvUTe+NLx1GDRJCXtCFLBLUI1eyZCKXXvbEU2C6gai0PZKoIE9Vbg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.7",
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"react-style-singleton": "^2.2.3",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.3",
|
||||
"use-sidecar": "^1.1.2"
|
||||
"use-sidecar": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "habittrove",
|
||||
"version": "0.2.13",
|
||||
"version": "0.2.14",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
@@ -18,6 +18,7 @@
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@next/font": "^14.2.15",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-context-menu": "^2.2.4",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
@@ -28,7 +29,7 @@
|
||||
"@radix-ui/react-scroll-area": "^1.2.4",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.3",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
@@ -44,7 +45,6 @@
|
||||
"js-confetti": "^0.12.0",
|
||||
"linkify": "^0.2.1",
|
||||
"linkify-react": "^4.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.469.0",
|
||||
"luxon": "^3.5.0",
|
||||
"next": "15.2.3",
|
||||
|
||||
Reference in New Issue
Block a user