mirror of
https://github.com/spliit-app/spliit.git
synced 2026-03-04 12:06:11 +01:00
Ability to archive groups when they’re settled up (#45)
* Settled up icon on group card * remove logs * archived groups * remove settled up * remove more settled up * recent-group-list-card * sortGroups * archiveGroup * unarchiveGroup * clean up * more clean up * Prettier, fix TS errors, add titles --------- Co-authored-by: Sebastien Castiel <sebastien@castiel.me>
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
import { ExpenseForm } from '@/components/expense-form'
|
import { ExpenseForm } from '@/components/expense-form'
|
||||||
import { deleteExpense, getExpense, getCategories, getGroup, updateExpense } from '@/lib/api'
|
import {
|
||||||
|
deleteExpense,
|
||||||
|
getCategories,
|
||||||
|
getExpense,
|
||||||
|
getGroup,
|
||||||
|
updateExpense,
|
||||||
|
} from '@/lib/api'
|
||||||
import { expenseFormSchema } from '@/lib/schemas'
|
import { expenseFormSchema } from '@/lib/schemas'
|
||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
import { notFound, redirect } from 'next/navigation'
|
import { notFound, redirect } from 'next/navigation'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ExpenseForm } from '@/components/expense-form'
|
import { ExpenseForm } from '@/components/expense-form'
|
||||||
import { createExpense, getGroup, getCategories } from '@/lib/api'
|
import { createExpense, getCategories, getGroup } from '@/lib/api'
|
||||||
import { expenseFormSchema } from '@/lib/schemas'
|
import { expenseFormSchema } from '@/lib/schemas'
|
||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
import { notFound, redirect } from 'next/navigation'
|
import { notFound, redirect } from 'next/navigation'
|
||||||
@@ -24,5 +24,11 @@ export default async function ExpensePage({
|
|||||||
redirect(`/groups/${groupId}`)
|
redirect(`/groups/${groupId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ExpenseForm group={group} categories={categories} onSubmit={createExpenseAction} />
|
return (
|
||||||
|
<ExpenseForm
|
||||||
|
group={group}
|
||||||
|
categories={categories}
|
||||||
|
onSubmit={createExpenseAction}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,18 +11,19 @@ export const metadata: Metadata = {
|
|||||||
export default async function GroupsPage() {
|
export default async function GroupsPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 items-start">
|
<div className="flex justify-between items-center gap-4">
|
||||||
<h1 className="font-bold text-2xl">
|
<h1 className="font-bold text-2xl">
|
||||||
<Link href="/groups">Recently visited groups</Link>
|
<Link href="/groups">My groups</Link>
|
||||||
</h1>
|
</h1>
|
||||||
<Button asChild>
|
<Button asChild size="icon">
|
||||||
<Link href="/groups/create">
|
<Link href="/groups/create">
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4" />
|
||||||
Create group
|
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<RecentGroupList />
|
<div>
|
||||||
|
<RecentGroupList />
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
185
src/app/groups/recent-group-list-card.tsx
Normal file
185
src/app/groups/recent-group-list-card.tsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
'use client'
|
||||||
|
import { RecentGroupsState } from '@/app/groups/recent-group-list'
|
||||||
|
import {
|
||||||
|
RecentGroup,
|
||||||
|
archiveGroup,
|
||||||
|
deleteRecentGroup,
|
||||||
|
getArchivedGroups,
|
||||||
|
getStarredGroups,
|
||||||
|
saveRecentGroup,
|
||||||
|
starGroup,
|
||||||
|
unarchiveGroup,
|
||||||
|
unstarGroup,
|
||||||
|
} from '@/app/groups/recent-groups-helpers'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
import { ToastAction } from '@/components/ui/toast'
|
||||||
|
import { useToast } from '@/components/ui/use-toast'
|
||||||
|
import { StarFilledIcon } from '@radix-ui/react-icons'
|
||||||
|
import { Calendar, MoreHorizontal, Star, Users } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { SetStateAction } from 'react'
|
||||||
|
|
||||||
|
export function RecentGroupListCard({
|
||||||
|
group,
|
||||||
|
state,
|
||||||
|
setState,
|
||||||
|
}: {
|
||||||
|
group: RecentGroup
|
||||||
|
state: RecentGroupsState
|
||||||
|
setState: (state: SetStateAction<RecentGroupsState>) => void
|
||||||
|
}) {
|
||||||
|
const router = useRouter()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const details =
|
||||||
|
state.status === 'complete'
|
||||||
|
? state.groupsDetails.find((d) => d.id === group.id)
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (state.status === 'pending') return null
|
||||||
|
|
||||||
|
const refreshGroupsFromStorage = () =>
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
starredGroups: getStarredGroups(),
|
||||||
|
archivedGroups: getArchivedGroups(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const isStarred = state.starredGroups.includes(group.id)
|
||||||
|
const isArchived = state.archivedGroups.includes(group.id)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={group.id}>
|
||||||
|
<Button variant="outline" className="h-fit w-full py-3" asChild>
|
||||||
|
<div
|
||||||
|
className="text-base"
|
||||||
|
onClick={() => router.push(`/groups/${group.id}`)}
|
||||||
|
>
|
||||||
|
<div className="w-full flex flex-col gap-1">
|
||||||
|
<div className="text-base flex gap-2 justify-between">
|
||||||
|
<Link
|
||||||
|
href={`/groups/${group.id}`}
|
||||||
|
className="flex-1 overflow-hidden text-ellipsis"
|
||||||
|
>
|
||||||
|
{group.name}
|
||||||
|
</Link>
|
||||||
|
<span className="flex-shrink-0">
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="-my-3 -ml-3 -mr-1.5"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
if (isStarred) {
|
||||||
|
unstarGroup(group.id)
|
||||||
|
} else {
|
||||||
|
starGroup(group.id)
|
||||||
|
unarchiveGroup(group.id)
|
||||||
|
}
|
||||||
|
refreshGroupsFromStorage()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isStarred ? (
|
||||||
|
<StarFilledIcon className="w-4 h-4 text-orange-400" />
|
||||||
|
) : (
|
||||||
|
<Star className="w-4 h-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="-my-3 -mr-3 -ml-1.5"
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-destructive"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
deleteRecentGroup(group)
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
groups: state.groups.filter((g) => g.id !== group.id),
|
||||||
|
})
|
||||||
|
toast.toast({
|
||||||
|
title: 'Group has been removed',
|
||||||
|
description:
|
||||||
|
'The group was removed from your recent groups list.',
|
||||||
|
action: (
|
||||||
|
<ToastAction
|
||||||
|
altText="Undo group removal"
|
||||||
|
onClick={() => {
|
||||||
|
saveRecentGroup(group)
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
groups: state.groups,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Undo
|
||||||
|
</ToastAction>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove from recent groups
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
if (isArchived) {
|
||||||
|
unarchiveGroup(group.id)
|
||||||
|
} else {
|
||||||
|
archiveGroup(group.id)
|
||||||
|
unstarGroup(group.id)
|
||||||
|
}
|
||||||
|
refreshGroupsFromStorage()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isArchived ? <>Unarchive group</> : <>Archive group</>}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground font-normal text-xs">
|
||||||
|
{details ? (
|
||||||
|
<div className="w-full flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Users className="w-3 h-3 inline mr-1" />
|
||||||
|
<span>{details._count.participants}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Calendar className="w-3 h-3 inline mx-1" />
|
||||||
|
<span>
|
||||||
|
{new Date(details.createdAt).toLocaleDateString('en-US', {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Skeleton className="h-4 w-6 rounded-full" />
|
||||||
|
<Skeleton className="h-4 w-24 rounded-full" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,73 +1,80 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { getGroupsAction } from '@/app/groups/actions'
|
import { getGroupsAction } from '@/app/groups/actions'
|
||||||
import {
|
import {
|
||||||
deleteRecentGroup,
|
RecentGroups,
|
||||||
|
getArchivedGroups,
|
||||||
getRecentGroups,
|
getRecentGroups,
|
||||||
getStarredGroups,
|
getStarredGroups,
|
||||||
saveRecentGroup,
|
|
||||||
starGroup,
|
|
||||||
unstarGroup,
|
|
||||||
} from '@/app/groups/recent-groups-helpers'
|
} from '@/app/groups/recent-groups-helpers'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu'
|
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
|
||||||
import { ToastAction } from '@/components/ui/toast'
|
|
||||||
import { useToast } from '@/components/ui/use-toast'
|
|
||||||
import { getGroups } from '@/lib/api'
|
import { getGroups } from '@/lib/api'
|
||||||
import { StarFilledIcon } from '@radix-ui/react-icons'
|
import { Loader2 } from 'lucide-react'
|
||||||
import { Calendar, Loader2, MoreHorizontal, Star, Users } from 'lucide-react'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/navigation'
|
import { SetStateAction, useEffect, useState } from 'react'
|
||||||
import { useEffect, useState } from 'react'
|
import { RecentGroupListCard } from './recent-group-list-card'
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
const recentGroupsSchema = z.array(
|
export type RecentGroupsState =
|
||||||
z.object({
|
|
||||||
id: z.string().min(1),
|
|
||||||
name: z.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
type RecentGroups = z.infer<typeof recentGroupsSchema>
|
|
||||||
|
|
||||||
type State =
|
|
||||||
| { status: 'pending' }
|
| { status: 'pending' }
|
||||||
| { status: 'partial'; groups: RecentGroups; starredGroups: string[] }
|
| {
|
||||||
|
status: 'partial'
|
||||||
|
groups: RecentGroups
|
||||||
|
starredGroups: string[]
|
||||||
|
archivedGroups: string[]
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
status: 'complete'
|
status: 'complete'
|
||||||
groups: RecentGroups
|
groups: RecentGroups
|
||||||
groupsDetails: Awaited<ReturnType<typeof getGroups>>
|
groupsDetails: Awaited<ReturnType<typeof getGroups>>
|
||||||
starredGroups: string[]
|
starredGroups: string[]
|
||||||
|
archivedGroups: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
function sortGroups(
|
||||||
getGroupsAction: (groupIds: string[]) => ReturnType<typeof getGroups>
|
state: RecentGroupsState & { status: 'complete' | 'partial' },
|
||||||
|
) {
|
||||||
|
const starredGroupInfo = []
|
||||||
|
const groupInfo = []
|
||||||
|
const archivedGroupInfo = []
|
||||||
|
for (const group of state.groups) {
|
||||||
|
if (state.starredGroups.includes(group.id)) {
|
||||||
|
starredGroupInfo.push(group)
|
||||||
|
} else if (state.archivedGroups.includes(group.id)) {
|
||||||
|
archivedGroupInfo.push(group)
|
||||||
|
} else {
|
||||||
|
groupInfo.push(group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
starredGroupInfo,
|
||||||
|
groupInfo,
|
||||||
|
archivedGroupInfo,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RecentGroupList() {
|
export function RecentGroupList() {
|
||||||
const [state, setState] = useState<State>({ status: 'pending' })
|
const [state, setState] = useState<RecentGroupsState>({ status: 'pending' })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const groupsInStorage = getRecentGroups()
|
const groupsInStorage = getRecentGroups()
|
||||||
const starredGroups = getStarredGroups()
|
const starredGroups = getStarredGroups()
|
||||||
setState({ status: 'partial', groups: groupsInStorage, starredGroups })
|
const archivedGroups = getArchivedGroups()
|
||||||
|
setState({
|
||||||
|
status: 'partial',
|
||||||
|
groups: groupsInStorage,
|
||||||
|
starredGroups,
|
||||||
|
archivedGroups,
|
||||||
|
})
|
||||||
getGroupsAction(groupsInStorage.map((g) => g.id)).then((groupsDetails) => {
|
getGroupsAction(groupsInStorage.map((g) => g.id)).then((groupsDetails) => {
|
||||||
setState({
|
setState({
|
||||||
status: 'complete',
|
status: 'complete',
|
||||||
groups: groupsInStorage,
|
groups: groupsInStorage,
|
||||||
groupsDetails,
|
groupsDetails,
|
||||||
starredGroups,
|
starredGroups,
|
||||||
|
archivedGroups,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
if (state.status === 'pending') {
|
if (state.status === 'pending') {
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
@@ -91,139 +98,63 @@ export function RecentGroupList() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { starredGroupInfo, groupInfo, archivedGroupInfo } = sortGroups(state)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{starredGroupInfo.length > 0 && (
|
||||||
|
<>
|
||||||
|
<h2 className="mb-2">Starred groups</h2>
|
||||||
|
<GroupList
|
||||||
|
groups={starredGroupInfo}
|
||||||
|
state={state}
|
||||||
|
setState={setState}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{groupInfo.length > 0 && (
|
||||||
|
<>
|
||||||
|
<h2 className="mt-6 mb-2">Recent groups</h2>
|
||||||
|
<GroupList groups={groupInfo} state={state} setState={setState} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{archivedGroupInfo.length > 0 && (
|
||||||
|
<>
|
||||||
|
<h2 className="mt-6 mb-2 opacity-50">Archived groups</h2>
|
||||||
|
<div className="opacity-50">
|
||||||
|
<GroupList
|
||||||
|
groups={archivedGroupInfo}
|
||||||
|
state={state}
|
||||||
|
setState={setState}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function GroupList({
|
||||||
|
groups,
|
||||||
|
state,
|
||||||
|
setState,
|
||||||
|
}: {
|
||||||
|
groups: RecentGroups
|
||||||
|
state: RecentGroupsState
|
||||||
|
setState: (state: SetStateAction<RecentGroupsState>) => void
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<ul className="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
<ul className="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||||
{state.groups
|
{groups.map((group) => (
|
||||||
.toSorted(
|
<RecentGroupListCard
|
||||||
(first, second) =>
|
key={group.id}
|
||||||
(state.starredGroups.includes(second.id) ? 2 : 0) -
|
group={group}
|
||||||
(state.starredGroups.includes(first.id) ? 1 : 0),
|
state={state}
|
||||||
)
|
setState={setState}
|
||||||
.map((group) => {
|
/>
|
||||||
const details =
|
))}
|
||||||
state.status === 'complete'
|
|
||||||
? state.groupsDetails.find((d) => d.id === group.id)
|
|
||||||
: null
|
|
||||||
return (
|
|
||||||
<li key={group.id}>
|
|
||||||
<Button variant="outline" className="h-fit w-full py-3" asChild>
|
|
||||||
<div
|
|
||||||
className="text-base"
|
|
||||||
onClick={() => router.push(`/groups/${group.id}`)}
|
|
||||||
>
|
|
||||||
<div className="w-full flex flex-col gap-1">
|
|
||||||
<div className="text-base flex gap-2 justify-between">
|
|
||||||
<Link
|
|
||||||
href={`/groups/${group.id}`}
|
|
||||||
className="flex-1 overflow-hidden text-ellipsis"
|
|
||||||
>
|
|
||||||
{group.name}
|
|
||||||
</Link>
|
|
||||||
<span className="flex-shrink-0">
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="-my-3 -ml-3 -mr-1.5"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation()
|
|
||||||
if (state.starredGroups.includes(group.id)) {
|
|
||||||
unstarGroup(group.id)
|
|
||||||
} else {
|
|
||||||
starGroup(group.id)
|
|
||||||
}
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
starredGroups: getStarredGroups(),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{state.starredGroups.includes(group.id) ? (
|
|
||||||
<StarFilledIcon className="w-4 h-4 text-orange-400" />
|
|
||||||
) : (
|
|
||||||
<Star className="w-4 h-4 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="-my-3 -mr-3 -ml-1.5"
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="text-destructive"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation()
|
|
||||||
deleteRecentGroup(group)
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
groups: state.groups.filter(
|
|
||||||
(g) => g.id !== group.id,
|
|
||||||
),
|
|
||||||
})
|
|
||||||
toast.toast({
|
|
||||||
title: 'Group has been removed',
|
|
||||||
description:
|
|
||||||
'The group was removed from your recent groups list.',
|
|
||||||
action: (
|
|
||||||
<ToastAction
|
|
||||||
altText="Undo group removal"
|
|
||||||
onClick={() => {
|
|
||||||
saveRecentGroup(group)
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
groups: state.groups,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Undo
|
|
||||||
</ToastAction>
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove from recent groups
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground font-normal text-xs">
|
|
||||||
{details ? (
|
|
||||||
<div className="w-full flex items-center justify-between">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Users className="w-3 h-3 inline mr-1" />
|
|
||||||
<span>{details._count.participants}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Calendar className="w-3 h-3 inline mx-1" />
|
|
||||||
<span>
|
|
||||||
{new Date(details.createdAt).toLocaleDateString(
|
|
||||||
'en-US',
|
|
||||||
{
|
|
||||||
dateStyle: 'medium',
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<Skeleton className="h-4 w-6 rounded-full" />
|
|
||||||
<Skeleton className="h-4 w-24 rounded-full" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ul>
|
</ul>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ export const recentGroupsSchema = z.array(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const starredGroupsSchema = z.array(z.string())
|
export const starredGroupsSchema = z.array(z.string())
|
||||||
|
export const archivedGroupsSchema = z.array(z.string())
|
||||||
|
|
||||||
export type RecentGroups = z.infer<typeof recentGroupsSchema>
|
export type RecentGroups = z.infer<typeof recentGroupsSchema>
|
||||||
export type RecentGroup = RecentGroups[number]
|
export type RecentGroup = RecentGroups[number]
|
||||||
|
|
||||||
const STORAGE_KEY = 'recentGroups'
|
const STORAGE_KEY = 'recentGroups'
|
||||||
const STARRED_GROUPS_STORAGE_KEY = 'starredGroups'
|
const STARRED_GROUPS_STORAGE_KEY = 'starredGroups'
|
||||||
|
const ARCHIVED_GROUPS_STORAGE_KEY = 'archivedGroups'
|
||||||
|
|
||||||
export function getRecentGroups() {
|
export function getRecentGroups() {
|
||||||
const groupsInStorageJson = localStorage.getItem(STORAGE_KEY)
|
const groupsInStorageJson = localStorage.getItem(STORAGE_KEY)
|
||||||
@@ -64,3 +66,28 @@ export function unstarGroup(groupId: string) {
|
|||||||
JSON.stringify(starredGroups.filter((g) => g !== groupId)),
|
JSON.stringify(starredGroups.filter((g) => g !== groupId)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getArchivedGroups() {
|
||||||
|
const archivedGroupsJson = localStorage.getItem(ARCHIVED_GROUPS_STORAGE_KEY)
|
||||||
|
const archivedGroupsRaw = archivedGroupsJson
|
||||||
|
? JSON.parse(archivedGroupsJson)
|
||||||
|
: []
|
||||||
|
const parseResult = archivedGroupsSchema.safeParse(archivedGroupsRaw)
|
||||||
|
return parseResult.success ? parseResult.data : []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function archiveGroup(groupId: string) {
|
||||||
|
const archivedGroups = getArchivedGroups()
|
||||||
|
localStorage.setItem(
|
||||||
|
ARCHIVED_GROUPS_STORAGE_KEY,
|
||||||
|
JSON.stringify([...archivedGroups, groupId]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unarchiveGroup(groupId: string) {
|
||||||
|
const archivedGroups = getArchivedGroups()
|
||||||
|
localStorage.setItem(
|
||||||
|
ARCHIVED_GROUPS_STORAGE_KEY,
|
||||||
|
JSON.stringify(archivedGroups.filter((g) => g !== groupId)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -218,7 +218,11 @@ export async function getGroupExpenses(groupId: string) {
|
|||||||
const prisma = await getPrisma()
|
const prisma = await getPrisma()
|
||||||
return prisma.expense.findMany({
|
return prisma.expense.findMany({
|
||||||
where: { groupId },
|
where: { groupId },
|
||||||
include: { paidFor: { include: { participant: true } }, paidBy: true, category: true },
|
include: {
|
||||||
|
paidFor: { include: { participant: true } },
|
||||||
|
paidBy: true,
|
||||||
|
category: true,
|
||||||
|
},
|
||||||
orderBy: { expenseDate: 'desc' },
|
orderBy: { expenseDate: 'desc' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user