diff --git a/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx index bb32da0..984d30f 100644 --- a/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx +++ b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx @@ -1,5 +1,11 @@ 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 { Metadata } from 'next' import { notFound, redirect } from 'next/navigation' diff --git a/src/app/groups/[groupId]/expenses/create/page.tsx b/src/app/groups/[groupId]/expenses/create/page.tsx index 405db26..adbdff9 100644 --- a/src/app/groups/[groupId]/expenses/create/page.tsx +++ b/src/app/groups/[groupId]/expenses/create/page.tsx @@ -1,5 +1,5 @@ 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 { Metadata } from 'next' import { notFound, redirect } from 'next/navigation' @@ -24,5 +24,11 @@ export default async function ExpensePage({ redirect(`/groups/${groupId}`) } - return + return ( + + ) } diff --git a/src/app/groups/page.tsx b/src/app/groups/page.tsx index 8aeeb59..967c6b7 100644 --- a/src/app/groups/page.tsx +++ b/src/app/groups/page.tsx @@ -11,18 +11,19 @@ export const metadata: Metadata = { export default async function GroupsPage() { return ( <> -
+

- Recently visited groups + My groups

-
- +
+ +
) } diff --git a/src/app/groups/recent-group-list-card.tsx b/src/app/groups/recent-group-list-card.tsx new file mode 100644 index 0000000..7809566 --- /dev/null +++ b/src/app/groups/recent-group-list-card.tsx @@ -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) => 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 ( +
  • + + + + + + + { + 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: ( + { + saveRecentGroup(group) + setState({ + ...state, + groups: state.groups, + }) + }} + > + Undo + + ), + }) + }} + > + Remove from recent groups + + { + event.stopPropagation() + if (isArchived) { + unarchiveGroup(group.id) + } else { + archiveGroup(group.id) + unstarGroup(group.id) + } + refreshGroupsFromStorage() + }} + > + {isArchived ? <>Unarchive group : <>Archive group} + + + + +
  • +
    + {details ? ( +
    +
    + + {details._count.participants} +
    +
    + + + {new Date(details.createdAt).toLocaleDateString('en-US', { + dateStyle: 'medium', + })} + +
    +
    + ) : ( +
    + + +
    + )} +
    + + + + + ) +} diff --git a/src/app/groups/recent-group-list.tsx b/src/app/groups/recent-group-list.tsx index 92a8067..5f41fdb 100644 --- a/src/app/groups/recent-group-list.tsx +++ b/src/app/groups/recent-group-list.tsx @@ -1,73 +1,80 @@ 'use client' import { getGroupsAction } from '@/app/groups/actions' import { - deleteRecentGroup, + RecentGroups, + getArchivedGroups, getRecentGroups, getStarredGroups, - saveRecentGroup, - starGroup, - 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 { getGroups } from '@/lib/api' -import { StarFilledIcon } from '@radix-ui/react-icons' -import { Calendar, Loader2, MoreHorizontal, Star, Users } from 'lucide-react' +import { Loader2 } from 'lucide-react' import Link from 'next/link' -import { useRouter } from 'next/navigation' -import { useEffect, useState } from 'react' -import { z } from 'zod' +import { SetStateAction, useEffect, useState } from 'react' +import { RecentGroupListCard } from './recent-group-list-card' -const recentGroupsSchema = z.array( - z.object({ - id: z.string().min(1), - name: z.string(), - }), -) -type RecentGroups = z.infer - -type State = +export type RecentGroupsState = | { status: 'pending' } - | { status: 'partial'; groups: RecentGroups; starredGroups: string[] } + | { + status: 'partial' + groups: RecentGroups + starredGroups: string[] + archivedGroups: string[] + } | { status: 'complete' groups: RecentGroups groupsDetails: Awaited> starredGroups: string[] + archivedGroups: string[] } -type Props = { - getGroupsAction: (groupIds: string[]) => ReturnType +function sortGroups( + 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() { - const [state, setState] = useState({ status: 'pending' }) + const [state, setState] = useState({ status: 'pending' }) useEffect(() => { const groupsInStorage = getRecentGroups() 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) => { setState({ status: 'complete', groups: groupsInStorage, groupsDetails, starredGroups, + archivedGroups, }) }) }, []) - const router = useRouter() - const toast = useToast() - if (state.status === 'pending') { return (

    @@ -91,139 +98,63 @@ export function RecentGroupList() { ) } + const { starredGroupInfo, groupInfo, archivedGroupInfo } = sortGroups(state) + + return ( + <> + {starredGroupInfo.length > 0 && ( + <> +

    Starred groups

    + + + )} + + {groupInfo.length > 0 && ( + <> +

    Recent groups

    + + + )} + + {archivedGroupInfo.length > 0 && ( + <> +

    Archived groups

    +
    + +
    + + )} + + ) +} + +function GroupList({ + groups, + state, + setState, +}: { + groups: RecentGroups + state: RecentGroupsState + setState: (state: SetStateAction) => void +}) { return (
      - {state.groups - .toSorted( - (first, second) => - (state.starredGroups.includes(second.id) ? 2 : 0) - - (state.starredGroups.includes(first.id) ? 1 : 0), - ) - .map((group) => { - const details = - state.status === 'complete' - ? state.groupsDetails.find((d) => d.id === group.id) - : null - return ( -
    • - - - - - - - { - 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: ( - { - saveRecentGroup(group) - setState({ - ...state, - groups: state.groups, - }) - }} - > - Undo - - ), - }) - }} - > - Remove from recent groups - - - - - -
      - {details ? ( -
      -
      - - {details._count.participants} -
      -
      - - - {new Date(details.createdAt).toLocaleDateString( - 'en-US', - { - dateStyle: 'medium', - }, - )} - -
      -
      - ) : ( -
      - - -
      - )} -
      - - - -
    • - ) - })} + {groups.map((group) => ( + + ))}
    ) } diff --git a/src/app/groups/recent-groups-helpers.ts b/src/app/groups/recent-groups-helpers.ts index 72c5d28..0d718e2 100644 --- a/src/app/groups/recent-groups-helpers.ts +++ b/src/app/groups/recent-groups-helpers.ts @@ -8,12 +8,14 @@ export const recentGroupsSchema = z.array( ) export const starredGroupsSchema = z.array(z.string()) +export const archivedGroupsSchema = z.array(z.string()) export type RecentGroups = z.infer export type RecentGroup = RecentGroups[number] const STORAGE_KEY = 'recentGroups' const STARRED_GROUPS_STORAGE_KEY = 'starredGroups' +const ARCHIVED_GROUPS_STORAGE_KEY = 'archivedGroups' export function getRecentGroups() { const groupsInStorageJson = localStorage.getItem(STORAGE_KEY) @@ -64,3 +66,28 @@ export function unstarGroup(groupId: string) { 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)), + ) +} diff --git a/src/lib/api.ts b/src/lib/api.ts index 2a03ded..c27e584 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -218,7 +218,11 @@ export async function getGroupExpenses(groupId: string) { const prisma = await getPrisma() return prisma.expense.findMany({ where: { groupId }, - include: { paidFor: { include: { participant: true } }, paidBy: true, category: true }, + include: { + paidFor: { include: { participant: true } }, + paidBy: true, + category: true, + }, orderBy: { expenseDate: 'desc' }, }) }