From ed55c696cd083bfaa5429082a9c5edd4ebde0cd8 Mon Sep 17 00:00:00 2001 From: Sebastien Castiel Date: Tue, 5 Dec 2023 17:39:05 -0500 Subject: First version --- src/app/globals.css | 85 ++++++++++--- src/app/groups/[groupId]/edit/page.tsx | 36 ++++++ .../[groupId]/expenses/[expenseId]/edit/page.tsx | 42 ++++++ src/app/groups/[groupId]/expenses/create/page.tsx | 37 ++++++ src/app/groups/[groupId]/page.tsx | 141 +++++++++++++++++++++ src/app/groups/[groupId]/save-recent-group.tsx | 18 +++ src/app/groups/create/page.tsx | 29 +++++ src/app/groups/page.tsx | 14 ++ src/app/groups/recent-group-list.tsx | 43 +++++++ src/app/groups/recent-groups-helpers.ts | 30 +++++ src/app/layout.tsx | 7 +- src/app/page.tsx | 115 +---------------- 12 files changed, 467 insertions(+), 130 deletions(-) create mode 100644 src/app/groups/[groupId]/edit/page.tsx create mode 100644 src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx create mode 100644 src/app/groups/[groupId]/expenses/create/page.tsx create mode 100644 src/app/groups/[groupId]/page.tsx create mode 100644 src/app/groups/[groupId]/save-recent-group.tsx create mode 100644 src/app/groups/create/page.tsx create mode 100644 src/app/groups/page.tsx create mode 100644 src/app/groups/recent-group-list.tsx create mode 100644 src/app/groups/recent-groups-helpers.ts (limited to 'src/app') diff --git a/src/app/globals.css b/src/app/globals.css index fd81e88..8abdb15 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,26 +2,75 @@ @tailwind components; @tailwind utilities; -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { +@layer base { :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; } } -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } } 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 ( +
+
+ +
+ +
+ ) +} 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 ( +
+
+ +
+ +
+ ) +} 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 ( +
+
+ +
+ + +
+ ) +} 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 ( +
+
+ + +
+ +

{group.name}

+ + +
+ + Expenses + + Here are the expenses that you created for your group. + + + + + +
+ + + + + Title + Paid by + Paid for + Amount + + + + + {expenses.map((expense) => ( + + {expense.title} + + + { + group.participants.find( + (p) => p.id === expense.paidById, + )?.name + } + + + + {expense.paidFor.map((paidFor, index) => ( + + { + group.participants.find( + (p) => p.id === paidFor.participantId, + )?.name + } + + ))} + + + $ {expense.amount.toFixed(2)} + + + + + + ))} + +
+
+
+ + + + Participants + + +
    + {group.participants.map((participant) => ( +
  • {participant.name}
  • + ))} +
+
+
+ +
+ ) +} 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 ( +
+
+ +
+ +
+ ) +} 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 ( +
+ + +
+ ) +} 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 + +type State = { status: 'pending' } | { status: 'success'; groups: RecentGroups } + +export function RecentGroupList() { + const [state, setState] = useState({ status: 'pending' }) + + useEffect(() => { + const groupsInStorage = getRecentGroups() + setState({ status: 'success', groups: groupsInStorage }) + }, []) + + return ( +
    + {state.status === 'pending' ? ( +
  • + Loading recent groups… +
  • + ) : ( + state.groups.map(({ id, name }) => ( +
  • + +
  • + )) + )} +
+ ) +} 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 +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)]), + ) +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 40e027f..4a83b57 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,9 +1,6 @@ import type { Metadata } from 'next' -import { Inter } from 'next/font/google' import './globals.css' -const inter = Inter({ subsets: ['latin'] }) - export const metadata: Metadata = { title: 'Create Next App', description: 'Generated by create next app', @@ -16,7 +13,9 @@ export default function RootLayout({ }) { return ( - {children} + +
{children}
+ ) } diff --git a/src/app/page.tsx b/src/app/page.tsx index e38c626..a94e2d5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,113 +1,12 @@ -import Image from 'next/image' +import { Button } from '@/components/ui/button' +import Link from 'next/link' -export default function Home() { +export default function HomePage() { return ( -
-
-

- Get started by editing  - src/app/page.tsx -

- -
- -
- Next.js Logo -
- - +
+
) } -- cgit v1.3