├── app
├── favicon.ico
├── page.tsx
├── actions
│ ├── getUserBalance.ts
│ ├── getTransactions.tsx
│ ├── deleteTransaction.ts
│ ├── getIncomeExpense.ts
│ └── addTransaction.ts
├── layout.tsx
└── globals.css
├── public
├── screenshot.png
├── vercel.svg
└── next.svg
├── next.config.mjs
├── lib
├── utils.ts
├── db.ts
└── checkUser.ts
├── .env-example
├── types
└── Transaction.ts
├── prisma
├── migrations
│ ├── migration_lock.toml
│ └── 20240620171129_user_transaction_create
│ │ └── migration.sql
└── schema.prisma
├── middleware.ts
├── components
├── Guest.tsx
├── Balance.tsx
├── Header.tsx
├── IncomeExpense.tsx
├── TransactionList.tsx
├── TransactionItem.tsx
└── AddTransaction.tsx
├── .gitignore
├── tsconfig.json
├── package.json
└── README.md
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/expense-tracker-nextjs/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/public/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/expense-tracker-nextjs/HEAD/public/screenshot.png
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | export function addCommas(x: number): string {
2 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
3 | }
4 |
--------------------------------------------------------------------------------
/.env-example:
--------------------------------------------------------------------------------
1 | DATABASE_URL=REPLACE_WITH_YOURS
2 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=REPLACE_WITH_YOURS
3 | CLERK_SECRET_KEY=REPLACE_WITH_YOURS
--------------------------------------------------------------------------------
/types/Transaction.ts:
--------------------------------------------------------------------------------
1 | export interface Transaction {
2 | id: string;
3 | text: string;
4 | amount: number;
5 | userId: string;
6 | createdAt: Date;
7 | }
8 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware } from '@clerk/nextjs/server';
2 |
3 | export default clerkMiddleware();
4 |
5 | export const config = {
6 | matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
7 | };
8 |
--------------------------------------------------------------------------------
/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client';
2 |
3 | declare global {
4 | var prisma: PrismaClient | undefined;
5 | }
6 |
7 | export const db = globalThis.prisma || new PrismaClient();
8 |
9 | if (process.env.NODE_ENV !== 'production') {
10 | globalThis.prisma = db;
11 | }
12 |
--------------------------------------------------------------------------------
/components/Guest.tsx:
--------------------------------------------------------------------------------
1 | import { SignInButton } from '@clerk/nextjs';
2 |
3 | const Guest = () => {
4 | return (
5 |
6 |
Welcome
7 |
Please sign in to manage your transactions
8 |
9 |
10 | );
11 | };
12 |
13 | export default Guest;
14 |
--------------------------------------------------------------------------------
/components/Balance.tsx:
--------------------------------------------------------------------------------
1 | import getUserBalance from '@/app/actions/getUserBalance';
2 | import { addCommas } from '@/lib/utils';
3 |
4 | const Balance = async () => {
5 | const { balance } = await getUserBalance();
6 |
7 | return (
8 | <>
9 | Your Balance
10 | ${addCommas(Number(balance?.toFixed(2) ?? 0))}
11 | >
12 | );
13 | };
14 |
15 | export default Balance;
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs';
2 | import { checkUser } from '@/lib/checkUser';
3 |
4 | const Header = async () => {
5 | const user = await checkUser();
6 |
7 | return (
8 |
21 | );
22 | };
23 |
24 | export default Header;
25 |
--------------------------------------------------------------------------------
/components/IncomeExpense.tsx:
--------------------------------------------------------------------------------
1 | import getIncomeExpense from '@/app/actions/getIncomeExpense';
2 | import { addCommas } from '@/lib/utils';
3 |
4 | const IncomeExpense = async () => {
5 | const { income, expense } = await getIncomeExpense();
6 |
7 | return (
8 |
9 |
10 |
Income
11 |
${addCommas(Number(income?.toFixed(2)))}
12 |
13 |
14 |
Expense
15 |
${addCommas(Number(expense?.toFixed(2)))}
16 |
17 |
18 | );
19 | };
20 |
21 | export default IncomeExpense;
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "expense-tracker-nextjs",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "postinstall": "prisma generate"
11 | },
12 | "dependencies": {
13 | "@clerk/nextjs": "^5.1.6",
14 | "@prisma/client": "^5.15.1",
15 | "next": "14.2.4",
16 | "react": "^18",
17 | "react-dom": "^18",
18 | "react-toastify": "^10.0.5"
19 | },
20 | "devDependencies": {
21 | "@types/node": "^20",
22 | "@types/react": "^18",
23 | "@types/react-dom": "^18",
24 | "prisma": "^5.15.1",
25 | "typescript": "^5"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { currentUser } from '@clerk/nextjs/server';
2 | import Guest from '@/components/Guest';
3 | import AddTransaction from '@/components/AddTransaction';
4 | import Balance from '@/components/Balance';
5 | import IncomeExpense from '@/components/IncomeExpense';
6 | import TransactionList from '@/components/TransactionList';
7 |
8 | const HomePage = async () => {
9 | const user = await currentUser();
10 |
11 | if (!user) {
12 | return ;
13 | }
14 |
15 | return (
16 |
17 | Welcome, {user.firstName}
18 |
19 |
20 |
21 |
22 |
23 | );
24 | };
25 |
26 | export default HomePage;
27 |
--------------------------------------------------------------------------------
/app/actions/getUserBalance.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 | import { db } from '@/lib/db';
3 | import { auth } from '@clerk/nextjs/server';
4 |
5 | async function getUserBalance(): Promise<{
6 | balance?: number;
7 | error?: string;
8 | }> {
9 | const { userId } = auth();
10 |
11 | if (!userId) {
12 | return { error: 'User not found' };
13 | }
14 |
15 | try {
16 | const transactions = await db.transaction.findMany({
17 | where: { userId },
18 | });
19 |
20 | const balance = transactions.reduce(
21 | (sum, transaction) => sum + transaction.amount,
22 | 0
23 | );
24 |
25 | return { balance };
26 | } catch (error) {
27 | return { error: 'Database error' };
28 | }
29 | }
30 |
31 | export default getUserBalance;
32 |
--------------------------------------------------------------------------------
/components/TransactionList.tsx:
--------------------------------------------------------------------------------
1 | import getTransactions from '@/app/actions/getTransactions';
2 | import TransactionItem from './TransactionItem';
3 | import { Transaction } from '@/types/Transaction';
4 |
5 | const TransactionList = async () => {
6 | const { transactions, error } = await getTransactions();
7 |
8 | if (error) {
9 | return {error}
;
10 | }
11 |
12 | return (
13 | <>
14 | History
15 |
16 | {transactions &&
17 | transactions.map((transaction: Transaction) => (
18 |
19 | ))}
20 |
21 | >
22 | );
23 | };
24 |
25 | export default TransactionList;
26 |
--------------------------------------------------------------------------------
/app/actions/getTransactions.tsx:
--------------------------------------------------------------------------------
1 | 'use server';
2 | import { db } from '@/lib/db';
3 | import { auth } from '@clerk/nextjs/server';
4 | import { Transaction } from '@/types/Transaction';
5 |
6 | async function getTransactions(): Promise<{
7 | transactions?: Transaction[];
8 | error?: string;
9 | }> {
10 | const { userId } = auth();
11 |
12 | if (!userId) {
13 | return { error: 'User not found' };
14 | }
15 |
16 | try {
17 | const transactions = await db.transaction.findMany({
18 | where: { userId },
19 | orderBy: {
20 | createdAt: 'desc',
21 | },
22 | });
23 |
24 | return { transactions };
25 | } catch (error) {
26 | return { error: 'Database error' };
27 | }
28 | }
29 |
30 | export default getTransactions;
31 |
--------------------------------------------------------------------------------
/app/actions/deleteTransaction.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 | import { db } from '@/lib/db';
3 | import { auth } from '@clerk/nextjs/server';
4 | import { revalidatePath } from 'next/cache';
5 |
6 | async function deleteTransaction(transactionId: string): Promise<{
7 | message?: string;
8 | error?: string;
9 | }> {
10 | const { userId } = auth();
11 |
12 | if (!userId) {
13 | return { error: 'User not found' };
14 | }
15 |
16 | try {
17 | await db.transaction.delete({
18 | where: {
19 | id: transactionId,
20 | userId,
21 | },
22 | });
23 |
24 | revalidatePath('/');
25 |
26 | return { message: 'Transaction deleted' };
27 | } catch (error) {
28 | return { error: 'Database error' };
29 | }
30 | }
31 |
32 | export default deleteTransaction;
33 |
--------------------------------------------------------------------------------
/lib/checkUser.ts:
--------------------------------------------------------------------------------
1 | import { currentUser } from '@clerk/nextjs/server';
2 | import { db } from '@/lib/db';
3 |
4 | export const checkUser = async () => {
5 | const user = await currentUser();
6 |
7 | // Check for current logged in clerk user
8 | if (!user) {
9 | return null;
10 | }
11 |
12 | // Check if the user is already in the database
13 | const loggedInUser = await db.user.findUnique({
14 | where: {
15 | clerkUserId: user.id,
16 | },
17 | });
18 |
19 | // If user is in database, return user
20 | if (loggedInUser) {
21 | return loggedInUser;
22 | }
23 |
24 | // If not in database, create new user
25 | const newUser = await db.user.create({
26 | data: {
27 | clerkUserId: user.id,
28 | name: `${user.firstName} ${user.lastName}`,
29 | imageUrl: user.imageUrl,
30 | email: user.emailAddresses[0].emailAddress,
31 | },
32 | });
33 |
34 | return newUser;
35 | };
36 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 | import { Roboto } from 'next/font/google';
3 | import './globals.css';
4 | import { ClerkProvider } from '@clerk/nextjs';
5 | import Header from '@/components/Header';
6 | import { ToastContainer } from 'react-toastify';
7 | import 'react-toastify/dist/ReactToastify.css';
8 |
9 | const roboto = Roboto({ weight: '400', subsets: ['latin'] });
10 |
11 | export const metadata: Metadata = {
12 | title: 'Expense Tracker',
13 | description: 'Track your expenses and create a budget',
14 | };
15 |
16 | export default function RootLayout({
17 | children,
18 | }: Readonly<{
19 | children: React.ReactNode;
20 | }>) {
21 | return (
22 |
23 |
24 |
25 |
26 | {children}
27 |
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/app/actions/getIncomeExpense.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 | import { db } from '@/lib/db';
3 | import { auth } from '@clerk/nextjs/server';
4 |
5 | async function getIncomeExpense(): Promise<{
6 | income?: number;
7 | expense?: number;
8 | error?: string;
9 | }> {
10 | const { userId } = auth();
11 |
12 | if (!userId) {
13 | return { error: 'User not found' };
14 | }
15 |
16 | try {
17 | const transactions = await db.transaction.findMany({
18 | where: { userId },
19 | });
20 |
21 | const amounts = transactions.map((transaction) => transaction.amount);
22 |
23 | const income = amounts
24 | .filter((item) => item > 0)
25 | .reduce((acc, item) => acc + item, 0);
26 |
27 | const expense = amounts
28 | .filter((item) => item < 0)
29 | .reduce((acc, item) => acc + item, 0);
30 |
31 | return { income, expense: Math.abs(expense) };
32 | } catch (error) {
33 | return { error: 'Database error' };
34 | }
35 | }
36 |
37 | export default getIncomeExpense;
38 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6 |
7 | generator client {
8 | provider = "prisma-client-js"
9 | }
10 |
11 | datasource db {
12 | provider = "postgresql"
13 | url = env("DATABASE_URL")
14 | }
15 |
16 | model User {
17 | id String @id @default(uuid())
18 | clerkUserId String @unique
19 | email String @unique
20 | name String?
21 | imageUrl String?
22 | createdAt DateTime @default(now())
23 | updatedAt DateTime @updatedAt
24 | transactions Transaction[]
25 | }
26 |
27 | model Transaction {
28 | id String @id @default(uuid())
29 | text String
30 | amount Float
31 | // Relation to user
32 | userId String
33 | user User @relation(fields: [userId], references: [clerkUserId], onDelete: Cascade)
34 | createdAt DateTime @default(now())
35 | @@index([userId])
36 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Expense Tracker (Next.js, TypeScript, Neon & Clerk)
2 |
3 | Application for tracking income and expenses. It uses Next.js with [Neon](https://fyi.neon.tech/traversy) to persist data and [Clerk](https://go.clerk.com/BsG2XQJ) for authentication.
4 |
5 | [Watch The Tutorial](https://www.youtube.com/watch?v=I6DCo5RwHBE)
6 |
7 | [Try Demo](https://traversydemos.dev)
8 |
9 |
10 |

11 |
12 |
13 | ## Usage
14 |
15 | ### Install dependencies:
16 |
17 | ```bash
18 | npm install
19 | ```
20 |
21 | ### Add Environment Variables:
22 |
23 | Rename the `.env.example` file to `.env.local` and add the following values:
24 |
25 | - `DATABASE_URL`: Your db string from https://neon.tech
26 | - `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`: Your Clerk public frontend API key from https://dashboard.clerk.dev/settings/api-keys
27 | - `CLERK_SECRET_KEY`: Your Clerk secret key from https://dashboard.clerk.dev/settings/api-keys
28 |
29 | Run the development server:
30 |
31 | ```bash
32 | npm run dev
33 | ```
34 |
35 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
36 |
--------------------------------------------------------------------------------
/prisma/migrations/20240620171129_user_transaction_create/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" TEXT NOT NULL,
4 | "clerkUserId" TEXT NOT NULL,
5 | "email" TEXT NOT NULL,
6 | "name" TEXT,
7 | "imageUrl" TEXT,
8 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
9 | "updatedAt" TIMESTAMP(3) NOT NULL,
10 |
11 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
12 | );
13 |
14 | -- CreateTable
15 | CREATE TABLE "Transaction" (
16 | "id" TEXT NOT NULL,
17 | "text" TEXT NOT NULL,
18 | "amount" DOUBLE PRECISION NOT NULL,
19 | "userId" TEXT NOT NULL,
20 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
21 |
22 | CONSTRAINT "Transaction_pkey" PRIMARY KEY ("id")
23 | );
24 |
25 | -- CreateIndex
26 | CREATE UNIQUE INDEX "User_clerkUserId_key" ON "User"("clerkUserId");
27 |
28 | -- CreateIndex
29 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
30 |
31 | -- CreateIndex
32 | CREATE INDEX "Transaction_userId_idx" ON "Transaction"("userId");
33 |
34 | -- AddForeignKey
35 | ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("clerkUserId") ON DELETE CASCADE ON UPDATE CASCADE;
36 |
--------------------------------------------------------------------------------
/components/TransactionItem.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Transaction } from '@/types/Transaction';
3 | import { addCommas } from '@/lib/utils';
4 | import { toast } from 'react-toastify';
5 | import deleteTransaction from '@/app/actions/deleteTransaction';
6 |
7 | const TransactionItem = ({ transaction }: { transaction: Transaction }) => {
8 | const sign = transaction.amount < 0 ? '-' : '+';
9 |
10 | const handleDeleteTransaction = async (transactionId: string) => {
11 | const confirmed = window.confirm(
12 | 'Are you sure you want to delete this transaction?'
13 | );
14 |
15 | if (!confirmed) return;
16 |
17 | const { message, error } = await deleteTransaction(transactionId);
18 |
19 | if (error) {
20 | toast.error(error);
21 | }
22 |
23 | toast.success(message);
24 | };
25 |
26 | return (
27 |
28 | {transaction.text}
29 |
30 | {sign}${addCommas(Math.abs(transaction.amount))}
31 |
32 |
38 |
39 | );
40 | };
41 |
42 | export default TransactionItem;
43 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/actions/addTransaction.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 | import { auth } from '@clerk/nextjs/server';
3 | import { db } from '@/lib/db';
4 | import { revalidatePath } from 'next/cache';
5 |
6 | interface TransactionData {
7 | text: string;
8 | amount: number;
9 | }
10 |
11 | interface TransactionResult {
12 | data?: TransactionData;
13 | error?: string;
14 | }
15 |
16 | async function addTransaction(formData: FormData): Promise {
17 | const textValue = formData.get('text');
18 | const amountValue = formData.get('amount');
19 |
20 | // Check for input values
21 | if (!textValue || textValue === '' || !amountValue) {
22 | return { error: 'Text or amount is missing' };
23 | }
24 |
25 | const text: string = textValue.toString(); // Ensure text is a string
26 | const amount: number = parseFloat(amountValue.toString()); // Parse amount as number
27 |
28 | // Get logged in user
29 | const { userId } = auth();
30 |
31 | // Check for user
32 | if (!userId) {
33 | return { error: 'User not found' };
34 | }
35 |
36 | try {
37 | const transactionData: TransactionData = await db.transaction.create({
38 | data: {
39 | text,
40 | amount,
41 | userId,
42 | },
43 | });
44 |
45 | revalidatePath('/');
46 |
47 | return { data: transactionData };
48 | } catch (error) {
49 | return { error: 'Transaction not added' };
50 | }
51 | }
52 |
53 | export default addTransaction;
54 |
--------------------------------------------------------------------------------
/components/AddTransaction.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useRef } from 'react';
3 | import addTransaction from '@/app/actions/addTransaction';
4 | import { toast } from 'react-toastify';
5 |
6 | const AddTransaction = () => {
7 | const formRef = useRef(null);
8 |
9 | const clientAction = async (formData: FormData) => {
10 | const { data, error } = await addTransaction(formData);
11 |
12 | if (error) {
13 | toast.error(error);
14 | } else {
15 | toast.success('Transaction added');
16 | formRef.current?.reset();
17 | }
18 | };
19 |
20 | return (
21 | <>
22 | Add transaction
23 |
47 | >
48 | );
49 | };
50 |
51 | export default AddTransaction;
52 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
3 | }
4 |
5 | * {
6 | box-sizing: border-box;
7 | }
8 |
9 | body {
10 | background-color: #f7f7f7;
11 | min-height: 100vh;
12 | margin: 0;
13 | font-family: 'Lato', sans-serif;
14 | }
15 |
16 | .container {
17 | margin: 30px auto;
18 | width: 350px;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | }
24 |
25 | .navbar {
26 | background: #000;
27 | color: #fff;
28 | }
29 |
30 | .navbar a {
31 | color: #fff;
32 | }
33 |
34 | .navbar button,
35 | .guest button {
36 | border: 0;
37 | border-radius: 5px;
38 | background: rebeccapurple;
39 | color: #fff;
40 | padding: 0.5rem 2rem;
41 | cursor: pointer;
42 | }
43 |
44 | .navbar button:hover {
45 | opacity: 0.9;
46 | }
47 |
48 | .navbar-container {
49 | display: flex;
50 | justify-content: space-between;
51 | align-items: center;
52 | max-width: 900px;
53 | margin: 0 auto;
54 | padding: 0 2rem;
55 | }
56 |
57 | .navbar .cl-button {
58 | background: transparent;
59 | border: 0;
60 | }
61 |
62 | h1 {
63 | letter-spacing: 1px;
64 | margin: 0;
65 | }
66 |
67 | h3 {
68 | border-bottom: 1px solid #bbb;
69 | padding-bottom: 10px;
70 | margin: 40px 0 10px;
71 | }
72 |
73 | h4 {
74 | margin: 0;
75 | text-transform: uppercase;
76 | }
77 |
78 | .guest {
79 | text-align: center;
80 | }
81 |
82 | .error {
83 | background: red;
84 | color: #fff;
85 | padding: 3px;
86 | }
87 |
88 | .inc-exp-container {
89 | background-color: #fff;
90 | box-shadow: var(--box-shadow);
91 | padding: 20px;
92 | display: flex;
93 | justify-content: space-between;
94 | margin: 20px 0;
95 | }
96 |
97 | .inc-exp-container > div {
98 | flex: 1;
99 | text-align: center;
100 | }
101 |
102 | .inc-exp-container > div:first-of-type {
103 | border-right: 1px solid #dedede;
104 | }
105 |
106 | .money {
107 | font-size: 20px;
108 | letter-spacing: 1px;
109 | margin: 5px 0;
110 | }
111 |
112 | .money.plus {
113 | color: #2ecc71;
114 | }
115 |
116 | .money.minus {
117 | color: #c0392b;
118 | }
119 |
120 | label {
121 | display: inline-block;
122 | margin: 10px 0;
123 | }
124 |
125 | input[type='text'],
126 | input[type='number'] {
127 | border: 1px solid #dedede;
128 | border-radius: 2px;
129 | display: block;
130 | font-size: 16px;
131 | padding: 10px;
132 | width: 100%;
133 | }
134 |
135 | .btn {
136 | cursor: pointer;
137 | background-color: #9c88ff;
138 | box-shadow: var(--box-shadow);
139 | color: #fff;
140 | border: 0;
141 | display: block;
142 | font-size: 16px;
143 | margin: 10px 0 30px;
144 | padding: 10px;
145 | width: 100%;
146 | }
147 |
148 | .btn:focus,
149 | .delete-btn:focus {
150 | outline: 0;
151 | }
152 |
153 | .list {
154 | list-style-type: none;
155 | padding: 0;
156 | margin-bottom: 40px;
157 | }
158 |
159 | .list li {
160 | background-color: #fff;
161 | box-shadow: var(--box-shadow);
162 | color: #333;
163 | display: flex;
164 | justify-content: space-between;
165 | position: relative;
166 | padding: 10px;
167 | margin: 10px 0;
168 | }
169 |
170 | .list li.plus {
171 | border-right: 5px solid #2ecc71;
172 | }
173 |
174 | .list li.minus {
175 | border-right: 5px solid #c0392b;
176 | }
177 |
178 | .delete-btn {
179 | cursor: pointer;
180 | background-color: #e74c3c;
181 | border: 0;
182 | color: #fff;
183 | font-size: 20px;
184 | line-height: 20px;
185 | padding: 2px 5px;
186 | position: absolute;
187 | top: 50%;
188 | left: 0;
189 | transform: translate(-100%, -50%);
190 | opacity: 0;
191 | transition: opacity 0.3s ease;
192 | }
193 |
194 | .list li:hover .delete-btn {
195 | opacity: 1;
196 | }
197 |
--------------------------------------------------------------------------------