diff options
| author | Sebastien Castiel <sebastien@castiel.me> | 2023-12-05 17:39:05 -0500 |
|---|---|---|
| committer | Sebastien Castiel <sebastien@castiel.me> | 2023-12-05 17:39:05 -0500 |
| commit | ed55c696cd083bfaa5429082a9c5edd4ebde0cd8 (patch) | |
| tree | 9f387881f85716bcb4a05d6fce13bfe59fbc9c17 /src/app/groups | |
| parent | 1fd6e48807d455f66d9cd79db64cbf9e9f947c6c (diff) | |
First version
Diffstat (limited to 'src/app/groups')
| -rw-r--r-- | src/app/groups/[groupId]/edit/page.tsx | 36 | ||||
| -rw-r--r-- | src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx | 42 | ||||
| -rw-r--r-- | src/app/groups/[groupId]/expenses/create/page.tsx | 37 | ||||
| -rw-r--r-- | src/app/groups/[groupId]/page.tsx | 141 | ||||
| -rw-r--r-- | src/app/groups/[groupId]/save-recent-group.tsx | 18 | ||||
| -rw-r--r-- | src/app/groups/create/page.tsx | 29 | ||||
| -rw-r--r-- | src/app/groups/page.tsx | 14 | ||||
| -rw-r--r-- | src/app/groups/recent-group-list.tsx | 43 | ||||
| -rw-r--r-- | src/app/groups/recent-groups-helpers.ts | 30 |
9 files changed, 390 insertions, 0 deletions
diff --git a/src/app/groups/[groupId]/edit/page.tsx b/src/app/groups/[groupId]/edit/page.tsx new file mode 100644 index 0000000..23bd865 --- /dev/null +++ b/src/app/groups/[groupId]/edit/page.tsx @@ -0,0 +1,36 @@ +import { GroupForm } from '@/components/group-form' +import { Button } from '@/components/ui/button' +import { getGroup, updateGroup } from '@/lib/api' +import { groupFormSchema } from '@/lib/schemas' +import { ChevronLeft } from 'lucide-react' +import Link from 'next/link' +import { notFound, redirect } from 'next/navigation' + +export default async function EditGroupPage({ + params: { groupId }, +}: { + params: { groupId: string } +}) { + const group = await getGroup(groupId) + if (!group) notFound() + + async function updateGroupAction(values: unknown) { + 'use server' + const groupFormValues = groupFormSchema.parse(values) + const group = await updateGroup(groupId, groupFormValues) + redirect(`/groups/${group.id}`) + } + + return ( + <main> + <div className="mb-4"> + <Button variant="ghost" asChild> + <Link href={`/groups/${groupId}`}> + <ChevronLeft className="w-4 h-4 mr-2" /> Back to group + </Link> + </Button> + </div> + <GroupForm group={group} onSubmit={updateGroupAction} /> + </main> + ) +} diff --git a/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx new file mode 100644 index 0000000..f22ac08 --- /dev/null +++ b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx @@ -0,0 +1,42 @@ +import { ExpenseForm } from '@/components/expense-form' +import { Button } from '@/components/ui/button' +import { getExpense, getGroup, updateExpense } from '@/lib/api' +import { expenseFormSchema } from '@/lib/schemas' +import { ChevronLeft } from 'lucide-react' +import Link from 'next/link' +import { notFound, redirect } from 'next/navigation' + +export default async function EditExpensePage({ + params: { groupId, expenseId }, +}: { + params: { groupId: string; expenseId: string } +}) { + const group = await getGroup(groupId) + if (!group) notFound() + const expense = await getExpense(groupId, expenseId) + if (!expense) notFound() + + async function updateExpenseAction(values: unknown) { + 'use server' + const expenseFormValues = expenseFormSchema.parse(values) + await updateExpense(groupId, expenseId, expenseFormValues) + redirect(`/groups/${groupId}`) + } + + return ( + <main> + <div className="mb-4"> + <Button variant="ghost" asChild> + <Link href={`/groups/${groupId}`}> + <ChevronLeft className="w-4 h-4 mr-2" /> Back to group + </Link> + </Button> + </div> + <ExpenseForm + group={group} + expense={expense} + onSubmit={updateExpenseAction} + /> + </main> + ) +} diff --git a/src/app/groups/[groupId]/expenses/create/page.tsx b/src/app/groups/[groupId]/expenses/create/page.tsx new file mode 100644 index 0000000..f8d0ff9 --- /dev/null +++ b/src/app/groups/[groupId]/expenses/create/page.tsx @@ -0,0 +1,37 @@ +import { ExpenseForm } from '@/components/expense-form' +import { Button } from '@/components/ui/button' +import { createExpense, getGroup } from '@/lib/api' +import { expenseFormSchema } from '@/lib/schemas' +import { ChevronLeft } from 'lucide-react' +import Link from 'next/link' +import { notFound, redirect } from 'next/navigation' + +export default async function ExpensePage({ + params: { groupId }, +}: { + params: { groupId: string } +}) { + const group = await getGroup(groupId) + if (!group) notFound() + + async function createExpenseAction(values: unknown) { + 'use server' + const expenseFormValues = expenseFormSchema.parse(values) + await createExpense(expenseFormValues, groupId) + redirect(`/groups/${groupId}`) + } + + return ( + <main> + <div className="mb-4"> + <Button variant="ghost" asChild> + <Link href={`/groups/${groupId}`}> + <ChevronLeft className="w-4 h-4 mr-2" /> Back to group + </Link> + </Button> + </div> + + <ExpenseForm group={group} onSubmit={createExpenseAction} /> + </main> + ) +} diff --git a/src/app/groups/[groupId]/page.tsx b/src/app/groups/[groupId]/page.tsx new file mode 100644 index 0000000..1577d7a --- /dev/null +++ b/src/app/groups/[groupId]/page.tsx @@ -0,0 +1,141 @@ +import { SaveGroupLocally } from '@/app/groups/[groupId]/save-recent-group' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { getGroup, getGroupExpenses } from '@/lib/api' +import { ChevronLeft, ChevronRight, Edit, Plus } from 'lucide-react' +import Link from 'next/link' +import { notFound } from 'next/navigation' + +export default async function GroupPage({ + params: { groupId }, +}: { + params: { groupId: string } +}) { + const group = await getGroup(groupId) + if (!group) notFound() + + const expenses = await getGroupExpenses(groupId) + + return ( + <main> + <div className="mb-4 flex justify-between"> + <Button variant="ghost" asChild> + <Link href="/groups"> + <ChevronLeft className="w-4 h-4 mr-2" /> Back to recent groups + </Link> + </Button> + <Button variant="secondary" asChild> + <Link href={`/groups/${groupId}/edit`}> + <Edit className="w-4 h-4 mr-2" /> Edit group settings + </Link> + </Button> + </div> + + <h1 className="font-bold mb-4">{group.name}</h1> + + <Card className="mb-4"> + <div className="flex flex-1"> + <CardHeader className="flex-1"> + <CardTitle>Expenses</CardTitle> + <CardDescription> + Here are the expenses that you created for your group. + </CardDescription> + </CardHeader> + <CardHeader> + <Button asChild size="icon"> + <Link href={`/groups/${groupId}/expenses/create`}> + <Plus /> + </Link> + </Button> + </CardHeader> + </div> + <CardContent className="p-0"> + <Table className=""> + <TableHeader> + <TableRow> + <TableHead>Title</TableHead> + <TableHead>Paid by</TableHead> + <TableHead>Paid for</TableHead> + <TableHead className="text-right">Amount</TableHead> + <TableHead className="w-0"></TableHead> + </TableRow> + </TableHeader> + <TableBody> + {expenses.map((expense) => ( + <TableRow key={expense.id}> + <TableCell>{expense.title}</TableCell> + <TableCell> + <Badge variant="secondary"> + { + group.participants.find( + (p) => p.id === expense.paidById, + )?.name + } + </Badge> + </TableCell> + <TableCell className="flex flex-wrap gap-1"> + {expense.paidFor.map((paidFor, index) => ( + <Badge variant="secondary" key={index}> + { + group.participants.find( + (p) => p.id === paidFor.participantId, + )?.name + } + </Badge> + ))} + </TableCell> + <TableCell className="text-right tabular-nums"> + $ {expense.amount.toFixed(2)} + </TableCell> + <TableCell> + <Button + size="icon" + variant="link" + className="-my-2" + asChild + > + <Link + href={`/groups/${groupId}/expenses/${expense.id}/edit`} + > + <ChevronRight className="w-4 h-4" /> + </Link> + </Button> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </CardContent> + </Card> + + <Card className="mb-4"> + <CardHeader> + <CardTitle>Participants</CardTitle> + </CardHeader> + <CardContent> + <ul> + {group.participants.map((participant) => ( + <li key={participant.id}>{participant.name}</li> + ))} + </ul> + </CardContent> + </Card> + <SaveGroupLocally group={{ id: group.id, name: group.name }} /> + </main> + ) +} diff --git a/src/app/groups/[groupId]/save-recent-group.tsx b/src/app/groups/[groupId]/save-recent-group.tsx new file mode 100644 index 0000000..e55d7c8 --- /dev/null +++ b/src/app/groups/[groupId]/save-recent-group.tsx @@ -0,0 +1,18 @@ +'use client' +import { + RecentGroup, + saveRecentGroup, +} from '@/app/groups/recent-groups-helpers' +import { useEffect } from 'react' + +type Props = { + group: RecentGroup +} + +export function SaveGroupLocally({ group }: Props) { + useEffect(() => { + saveRecentGroup(group) + }, []) + + return null +} diff --git a/src/app/groups/create/page.tsx b/src/app/groups/create/page.tsx new file mode 100644 index 0000000..4f220d2 --- /dev/null +++ b/src/app/groups/create/page.tsx @@ -0,0 +1,29 @@ +import { GroupForm } from '@/components/group-form' +import { Button } from '@/components/ui/button' +import { createGroup } from '@/lib/api' +import { groupFormSchema } from '@/lib/schemas' +import { ChevronLeft } from 'lucide-react' +import Link from 'next/link' +import { redirect } from 'next/navigation' + +export default function CreateGroupPage() { + async function createGroupAction(values: unknown) { + 'use server' + const groupFormValues = groupFormSchema.parse(values) + const group = await createGroup(groupFormValues) + redirect(`/groups/${group.id}`) + } + + return ( + <main> + <div className="mb-4"> + <Button variant="ghost" asChild> + <Link href="/groups"> + <ChevronLeft className="w-4 h-4 mr-2" /> Back to recent groups + </Link> + </Button> + </div> + <GroupForm onSubmit={createGroupAction} /> + </main> + ) +} diff --git a/src/app/groups/page.tsx b/src/app/groups/page.tsx new file mode 100644 index 0000000..7e4f484 --- /dev/null +++ b/src/app/groups/page.tsx @@ -0,0 +1,14 @@ +import { RecentGroupList } from '@/app/groups/recent-group-list' +import { Button } from '@/components/ui/button' +import Link from 'next/link' + +export default async function GroupsPage() { + return ( + <main> + <Button asChild> + <Link href="/groups/create">New group</Link> + </Button> + <RecentGroupList /> + </main> + ) +} diff --git a/src/app/groups/recent-group-list.tsx b/src/app/groups/recent-group-list.tsx new file mode 100644 index 0000000..e728193 --- /dev/null +++ b/src/app/groups/recent-group-list.tsx @@ -0,0 +1,43 @@ +'use client' +import { getRecentGroups } from '@/app/groups/recent-groups-helpers' +import { Button } from '@/components/ui/button' +import Link from 'next/link' +import { useEffect, useState } from 'react' +import { z } from 'zod' + +const recentGroupsSchema = z.array( + z.object({ + id: z.string().min(1), + name: z.string(), + }), +) +type RecentGroups = z.infer<typeof recentGroupsSchema> + +type State = { status: 'pending' } | { status: 'success'; groups: RecentGroups } + +export function RecentGroupList() { + const [state, setState] = useState<State>({ status: 'pending' }) + + useEffect(() => { + const groupsInStorage = getRecentGroups() + setState({ status: 'success', groups: groupsInStorage }) + }, []) + + return ( + <ul className="flex flex-col gap-2 mt-2"> + {state.status === 'pending' ? ( + <li> + <em>Loading recent groups…</em> + </li> + ) : ( + state.groups.map(({ id, name }) => ( + <li key={id}> + <Button asChild variant="outline"> + <Link href={`/groups/${id}`}>{name}</Link> + </Button> + </li> + )) + )} + </ul> + ) +} diff --git a/src/app/groups/recent-groups-helpers.ts b/src/app/groups/recent-groups-helpers.ts new file mode 100644 index 0000000..5d52141 --- /dev/null +++ b/src/app/groups/recent-groups-helpers.ts @@ -0,0 +1,30 @@ +import { z } from 'zod' + +export const recentGroupsSchema = z.array( + z.object({ + id: z.string().min(1), + name: z.string(), + }), +) + +export type RecentGroups = z.infer<typeof recentGroupsSchema> +export type RecentGroup = RecentGroups[number] + +const STORAGE_KEY = 'recentGroups' + +export function getRecentGroups() { + const groupsInStorageJson = localStorage.getItem(STORAGE_KEY) + const groupsInStorageRaw = groupsInStorageJson + ? JSON.parse(groupsInStorageJson) + : [] + const parseResult = recentGroupsSchema.safeParse(groupsInStorageRaw) + return parseResult.success ? parseResult.data : [] +} + +export function saveRecentGroup(group: RecentGroup) { + const recentGroups = getRecentGroups() + localStorage.setItem( + STORAGE_KEY, + JSON.stringify([group, ...recentGroups.filter((rg) => rg.id !== group.id)]), + ) +} |
