First version

This commit is contained in:
Sebastien Castiel
2023-12-05 17:39:05 -05:00
parent 1fd6e48807
commit ed55c696cd
41 changed files with 8305 additions and 468 deletions

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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
}

View File

@@ -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>
)
}

14
src/app/groups/page.tsx Normal file
View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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)]),
)
}