mirror of
https://codeberg.org/MarkusThielker/finances.git
synced 2025-04-12 05:08:43 +00:00
Release v1.2.0 (#64)
This commit is contained in:
commit
c16ac9c2fa
45 changed files with 1899 additions and 194 deletions
8
.env.example
Normal file
8
.env.example
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
#
|
||||||
|
# This environment file sets variables for the development
|
||||||
|
# Database specifics are set in the docker-compose file and the ORIGIN is not required for local development
|
||||||
|
#
|
||||||
|
|
||||||
|
# prisma database url
|
||||||
|
DATABASE_URL="postgresql://prisma:prisma@localhost:5432/finances?schema=public"
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -38,3 +38,6 @@ next-env.d.ts
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
/.idea
|
/.idea
|
||||||
|
|
||||||
|
# serwist
|
||||||
|
public/sw.js
|
||||||
|
|
67
README.md
67
README.md
|
@ -1,3 +1,70 @@
|
||||||
# Next-Finances
|
# Next-Finances
|
||||||
|
|
||||||
This is my simple finances tracker that I use to keep track of my spending.
|
This is my simple finances tracker that I use to keep track of my spending.
|
||||||
|
|
||||||
|
## Using the app
|
||||||
|
|
||||||
|
### Understanding the Basics
|
||||||
|
|
||||||
|
- **Entities**: The core building blocks of your finances.
|
||||||
|
- Accounts: Where you hold money (e.g., bank accounts, PayPal account, cash)
|
||||||
|
- Entities: Where you spend money (e.g., Walmart, Spotify, Netflix)
|
||||||
|
- **Payments**: Record money movement.
|
||||||
|
- Expenses: Money leaving an Account. (Account -> Entity)
|
||||||
|
- Income: Money entering an Account. (Entity -> Account)
|
||||||
|
- **Categories** *(optional)*: Add labels to Payments for better tracking.
|
||||||
|
|
||||||
|
### Your First Steps
|
||||||
|
|
||||||
|
- Set up: Create Entities and Accounts that reflect your finances.
|
||||||
|
- Record a Payment:
|
||||||
|
- Enter the amount and date.
|
||||||
|
- Select payor and payee
|
||||||
|
- *(optional)* Assign a category or enter a note.
|
||||||
|
- Explore: View your payment history and view your statics at the dashboard
|
||||||
|
|
||||||
|
### Tips
|
||||||
|
|
||||||
|
- Install the website as a PWA for easy access.
|
||||||
|
- Get in the habit of recording Payments as they happen for accurate tracking.
|
||||||
|
- Use categories to understand your spending patterns.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Clone this repository and run the following commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
|
||||||
|
## create .env file
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
## start the database
|
||||||
|
docker compose -f docker/finances-dev/docker-compose.yml up -d
|
||||||
|
|
||||||
|
## generate prisma client
|
||||||
|
npx prisma generate
|
||||||
|
|
||||||
|
## apply database migrations
|
||||||
|
npx prisma migrate deploy
|
||||||
|
|
||||||
|
## start the development server
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open [http://localhost:3000](http://localhost:3000) with your browser and create an account.
|
||||||
|
While in development mode, you can generate sample data from the [Account page](http://localhost:3000/account).
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Copy the [docker-compose.yaml](./docker/finances-prod/docker-compose.yaml) file and
|
||||||
|
the [.env.example](./docker/finances-prod/.env.example) from 'docker/finances-prod' to your server.
|
||||||
|
|
||||||
|
Rename the `.env.example` file to `.env` and adjust the required environment variables.
|
||||||
|
|
||||||
|
The docker setup expects you to run a Traefik reverse proxy. It will then register itself automatically.
|
||||||
|
If your setup is different, you will need to adjust the `docker-compose.yaml` file accordingly.
|
||||||
|
|
||||||
|
The finances containers will automatically register themselves to a running watchtower container if it is present.
|
||||||
|
|
||||||
|
Finally run `docker-compose up -d` on your server to start the application.
|
||||||
|
|
11
docker/finances-prod/.env.example
Normal file
11
docker/finances-prod/.env.example
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
# database configuration
|
||||||
|
DB_USER="db_user"
|
||||||
|
DB_PASSWORD="db_password"
|
||||||
|
|
||||||
|
# prisma database url
|
||||||
|
DATABASE_URL="postgresql://$DB_USER:$DB_PASSWORD@postgres:5432/finances?schema=public"
|
||||||
|
|
||||||
|
APPLICATION_DOMAIN="finances.thielker.xyz"
|
||||||
|
COOKIE_DOMAIN="$APPLICATION_DOMAIN"
|
||||||
|
ORIGIN="https://$APPLICATION_DOMAIN"
|
|
@ -25,7 +25,7 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.xyz-next-finances.rule=Host(`finances.thielker.xyz`)"
|
- "traefik.http.routers.xyz-next-finances.rule=Host(`${APPLICATION_DOMAIN}`)"
|
||||||
- "traefik.http.routers.xyz-next-finances.entrypoints=web, websecure"
|
- "traefik.http.routers.xyz-next-finances.entrypoints=web, websecure"
|
||||||
- "traefik.http.routers.xyz-next-finances.tls=true"
|
- "traefik.http.routers.xyz-next-finances.tls=true"
|
||||||
- "traefik.http.routers.xyz-next-finances.tls.certresolver=lets-encrypt"
|
- "traefik.http.routers.xyz-next-finances.tls.certresolver=lets-encrypt"
|
||||||
|
@ -46,7 +46,7 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.xyz-next-finances-studio.rule=Host(`studio.finances.thielker.xyz`)"
|
- "traefik.http.routers.xyz-next-finances-studio.rule=Host(`studio.${APPLICATION_DOMAIN}`)"
|
||||||
- "traefik.http.routers.xyz-next-finances-studio.entrypoints=web, websecure"
|
- "traefik.http.routers.xyz-next-finances-studio.entrypoints=web, websecure"
|
||||||
- "traefik.http.services.xyz-next-finances-studio.loadbalancer.server.port=5555"
|
- "traefik.http.services.xyz-next-finances-studio.loadbalancer.server.port=5555"
|
||||||
- "traefik.http.routers.xyz-next-finances-studio.tls=true"
|
- "traefik.http.routers.xyz-next-finances-studio.tls=true"
|
||||||
|
|
13
docker/finances-prod/traefik.toml
Normal file
13
docker/finances-prod/traefik.toml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":80"
|
||||||
|
# [entryPoints.web.http.redirections.entryPoint]
|
||||||
|
# to = "websecure"
|
||||||
|
# scheme = "https"
|
||||||
|
|
||||||
|
[entryPoints.websecure]
|
||||||
|
address = ":443"
|
||||||
|
|
||||||
|
[providers.docker]
|
||||||
|
watch = true
|
||||||
|
network = "web"
|
13
docker/finances-prod/traefik_setup.sh
Normal file
13
docker/finances-prod/traefik_setup.sh
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
#
|
||||||
|
# run this container on your server to use traefik as a reverse proxy
|
||||||
|
#
|
||||||
|
docker run -d \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v $PWD/traefik.toml:/traefik.toml \
|
||||||
|
-p 80:80 \
|
||||||
|
-p 443:443 \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--network web \
|
||||||
|
--name traefik \
|
||||||
|
traefik:v2.10
|
13
docker/finances-prod/watchtower_setup.sh
Normal file
13
docker/finances-prod/watchtower_setup.sh
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
#
|
||||||
|
# run this container on your server to keep the labeled containers up to date
|
||||||
|
#
|
||||||
|
# run 'docker login' to authenticate with your docker hub account
|
||||||
|
# label your containers with 'com.centurylinklabs.watchtower.enable=true' to enable watchtower
|
||||||
|
#
|
||||||
|
docker run -d \
|
||||||
|
--name watchtower \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-v $HOME/.docker/config.json:/config.json \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
containrrr/watchtower -s "*/30 * * * * *" --label-enable
|
|
@ -1,5 +1,11 @@
|
||||||
/** @type {import('next').NextConfig} */
|
import withSerwistInit from '@serwist/next';
|
||||||
const nextConfig = {
|
|
||||||
|
const withSerwist = withSerwistInit({
|
||||||
|
swSrc: 'src/app/service-worker.ts',
|
||||||
|
swDest: 'public/sw.js',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withSerwist({
|
||||||
webpack: (config) => {
|
webpack: (config) => {
|
||||||
config.externals.push(
|
config.externals.push(
|
||||||
'@node-rs/argon2',
|
'@node-rs/argon2',
|
||||||
|
@ -11,6 +17,4 @@ const nextConfig = {
|
||||||
env: {
|
env: {
|
||||||
appVersion: process.env.npm_package_version,
|
appVersion: process.env.npm_package_version,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
|
|
1323
package-lock.json
generated
1323
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -2,8 +2,7 @@
|
||||||
"name": "next-finances",
|
"name": "next-finances",
|
||||||
"description": "A finances application to keep track of my personal spendings",
|
"description": "A finances application to keep track of my personal spendings",
|
||||||
"homepage": "https://github.com/MarkusThielker/next-finances",
|
"homepage": "https://github.com/MarkusThielker/next-finances",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"license": "MIT",
|
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Markus Thielker"
|
"name": "Markus Thielker"
|
||||||
},
|
},
|
||||||
|
@ -32,6 +31,9 @@
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@serwist/next": "^8.4.4",
|
||||||
|
"@serwist/precaching": "^8.4.4",
|
||||||
|
"@serwist/sw": "^8.4.4",
|
||||||
"@tanstack/react-table": "^8.13.2",
|
"@tanstack/react-table": "^8.13.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "entities"
|
||||||
|
ADD COLUMN "default_category_id" INTEGER;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "entities"
|
||||||
|
ADD CONSTRAINT "entities_default_category_id_fkey" FOREIGN KEY ("default_category_id") REFERENCES "categories" ("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@ -34,13 +34,15 @@ model Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Entity {
|
model Entity {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
userId String @map("user_id")
|
userId String @map("user_id")
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
name String
|
name String
|
||||||
type EntityType
|
type EntityType
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
defaultCategory Category? @relation(fields: [defaultCategoryId], references: [id])
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
defaultCategoryId Int? @map("default_category_id")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
paymentsAsPayor Payment[] @relation("PayorEntity")
|
paymentsAsPayor Payment[] @relation("PayorEntity")
|
||||||
paymentsAsPayee Payment[] @relation("PayeeEntity")
|
paymentsAsPayee Payment[] @relation("PayeeEntity")
|
||||||
|
@ -84,6 +86,7 @@ model Category {
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
payments Payment[]
|
payments Payment[]
|
||||||
|
Entity Entity[]
|
||||||
|
|
||||||
@@unique(fields: [userId, name])
|
@@unique(fields: [userId, name])
|
||||||
@@map("categories")
|
@@map("categories")
|
||||||
|
|
16
public/manifest.json
Normal file
16
public/manifest.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "Finances",
|
||||||
|
"short_name": "Finances",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/logo_white.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#0B0908",
|
||||||
|
"background_color": "#0B0908",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "portrait"
|
||||||
|
}
|
|
@ -5,11 +5,11 @@ import { redirect } from 'next/navigation';
|
||||||
import signOut from '@/lib/actions/signOut';
|
import signOut from '@/lib/actions/signOut';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import SignOutForm from '@/components/form/signOutForm';
|
|
||||||
import { URL_SIGN_IN } from '@/lib/constants';
|
import { URL_SIGN_IN } from '@/lib/constants';
|
||||||
import GenerateSampleDataForm from '@/components/form/generateSampleDataForm';
|
|
||||||
import generateSampleData from '@/lib/actions/generateSampleData';
|
import generateSampleData from '@/lib/actions/generateSampleData';
|
||||||
import { prismaClient } from '@/prisma';
|
import prisma from '@/prisma';
|
||||||
|
import { ServerActionTrigger } from '@/components/form/serverActionTrigger';
|
||||||
|
import accountDelete from '@/lib/actions/accountDelete';
|
||||||
|
|
||||||
export default async function AccountPage() {
|
export default async function AccountPage() {
|
||||||
|
|
||||||
|
@ -20,21 +20,21 @@ export default async function AccountPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let paymentCount = 0;
|
let paymentCount = 0;
|
||||||
paymentCount = await prismaClient.payment.count({
|
paymentCount = await prisma.payment.count({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let entityCount = 0;
|
let entityCount = 0;
|
||||||
entityCount = await prismaClient.entity.count({
|
entityCount = await prisma.entity.count({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let categoryCount = 0;
|
let categoryCount = 0;
|
||||||
categoryCount = await prismaClient.category.count({
|
categoryCount = await prisma.category.count({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
|
@ -81,13 +81,31 @@ export default async function AccountPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="grid gap-4 grid-cols-1 md:grid-cols-2">
|
<CardFooter className="w-full grid gap-4 grid-cols-1 md:grid-cols-2">
|
||||||
|
<ServerActionTrigger
|
||||||
|
action={accountDelete}
|
||||||
|
dialog={{
|
||||||
|
title: 'Delete Account',
|
||||||
|
description: 'Are you sure you want to delete your account? This action is irreversible.',
|
||||||
|
actionText: 'Delete Account',
|
||||||
|
}}
|
||||||
|
variant="outline">
|
||||||
|
Delete Account
|
||||||
|
</ServerActionTrigger>
|
||||||
|
<ServerActionTrigger
|
||||||
|
action={signOut}>
|
||||||
|
Sign Out
|
||||||
|
</ServerActionTrigger>
|
||||||
{
|
{
|
||||||
process.env.NODE_ENV === 'development' && (
|
process.env.NODE_ENV === 'development' && (
|
||||||
<GenerateSampleDataForm onSubmit={generateSampleData}/>
|
<ServerActionTrigger
|
||||||
|
variant="outline"
|
||||||
|
className="col-span-2"
|
||||||
|
action={generateSampleData}>
|
||||||
|
Generate sample data
|
||||||
|
</ServerActionTrigger>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<SignOutForm onSubmit={signOut}/>
|
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="flex w-full items-center justify-between max-w-md mt-2 text-neutral-600">
|
<div className="flex w-full items-center justify-between max-w-md mt-2 text-neutral-600">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getUser } from '@/auth';
|
import { getUser } from '@/auth';
|
||||||
import { prismaClient } from '@/prisma';
|
import prisma from '@/prisma';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import CategoryPageClientContent from '@/components/categoryPageClientComponents';
|
import CategoryPageClientContent from '@/components/categoryPageClientComponents';
|
||||||
import categoryCreateUpdate from '@/lib/actions/categoryCreateUpdate';
|
import categoryCreateUpdate from '@/lib/actions/categoryCreateUpdate';
|
||||||
|
@ -9,7 +9,7 @@ export default async function CategoriesPage() {
|
||||||
|
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
|
|
||||||
const categories = await prismaClient.category.findMany({
|
const categories = await prisma.category.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { Entity } from '@prisma/client';
|
import { Category, Entity } from '@prisma/client';
|
||||||
import { CellContext, ColumnDefTemplate } from '@tanstack/table-core';
|
import { CellContext, ColumnDefTemplate } from '@tanstack/table-core';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
export const columns = (
|
export const columns = (
|
||||||
actionCell: ColumnDefTemplate<CellContext<Entity, unknown>>,
|
actionCell: ColumnDefTemplate<CellContext<Entity, unknown>>,
|
||||||
|
categories: Category[],
|
||||||
) => {
|
) => {
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -19,6 +20,30 @@ export const columns = (
|
||||||
header: 'Type',
|
header: 'Type',
|
||||||
size: 100,
|
size: 100,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'defaultCategoryId',
|
||||||
|
header: 'Default Category',
|
||||||
|
cell: ({row}) => {
|
||||||
|
const category = categories.find((category) => category.id === row.original.defaultCategoryId);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
category && (
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<svg className="h-5" fill={category?.color} viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="10" cy="10" r="10"/>
|
||||||
|
</svg>
|
||||||
|
<p>{category?.name ?? '-'}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
size: 200,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'createdAt',
|
accessorKey: 'createdAt',
|
||||||
header: 'Created at',
|
header: 'Created at',
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { prismaClient } from '@/prisma';
|
import prisma from '@/prisma';
|
||||||
import { getUser } from '@/auth';
|
import { getUser } from '@/auth';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import EntityPageClientContent from '@/components/entityPageClientComponents';
|
import EntityPageClientContent from '@/components/entityPageClientComponents';
|
||||||
|
@ -9,7 +9,7 @@ export default async function EntitiesPage() {
|
||||||
|
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
|
|
||||||
const entities = await prismaClient.entity.findMany({
|
const entities = await prisma.entity.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
},
|
},
|
||||||
|
@ -23,9 +23,24 @@ export default async function EntitiesPage() {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const categories = await prisma.category.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user?.id,
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
name: 'asc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'asc',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityPageClientContent
|
<EntityPageClientContent
|
||||||
entities={entities}
|
entities={entities}
|
||||||
|
categories={categories}
|
||||||
onSubmit={entityCreateUpdate}
|
onSubmit={entityCreateUpdate}
|
||||||
onDelete={entityDelete}
|
onDelete={entityDelete}
|
||||||
className="flex flex-col justify-center space-y-4"/>
|
className="flex flex-col justify-center space-y-4"/>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Metadata } from 'next';
|
import type { Viewport } from 'next';
|
||||||
import { Inter } from 'next/font/google';
|
import { Inter } from 'next/font/google';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
@ -8,9 +8,31 @@ import Navigation from '@/components/navigation';
|
||||||
|
|
||||||
const inter = Inter({subsets: ['latin']});
|
const inter = Inter({subsets: ['latin']});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
const APP_NAME = 'Finances';
|
||||||
title: 'Finances',
|
const APP_DEFAULT_TITLE = 'Finances';
|
||||||
description: 'Track your finances with ease',
|
const APP_TITLE_TEMPLATE = `%s | ${APP_DEFAULT_TITLE}`;
|
||||||
|
const APP_DESCRIPTION = 'Track your finances with ease';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
applicationName: APP_NAME,
|
||||||
|
title: {
|
||||||
|
default: APP_DEFAULT_TITLE,
|
||||||
|
template: APP_TITLE_TEMPLATE,
|
||||||
|
},
|
||||||
|
description: APP_DESCRIPTION,
|
||||||
|
appleWebApp: {
|
||||||
|
capable: true,
|
||||||
|
statusBarStyle: 'default',
|
||||||
|
title: APP_DEFAULT_TITLE,
|
||||||
|
},
|
||||||
|
formatDetection: {
|
||||||
|
telephone: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
themeColor: '#0B0908',
|
||||||
|
width: 'device-width',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
@ -20,6 +42,13 @@ export default function RootLayout({
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<link crossOrigin="use-credentials" rel="manifest" href="/manifest.json"/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
href="/logo_white.png"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
<body className={cn('dark', inter.className)}>
|
<body className={cn('dark', inter.className)}>
|
||||||
<Navigation/>
|
<Navigation/>
|
||||||
<main className="p-4 sm:p-8">
|
<main className="p-4 sm:p-8">
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Category, Entity, EntityType } from '@prisma/client';
|
import { Category, Entity, EntityType } from '@prisma/client';
|
||||||
import { Scope, ScopeType } from '@/lib/types/scope';
|
import { Scope, ScopeType } from '@/lib/types/scope';
|
||||||
import { prismaClient } from '@/prisma';
|
import prisma from '@/prisma';
|
||||||
import { getUser } from '@/auth';
|
import { getUser } from '@/auth';
|
||||||
import DashboardPageClient from '@/components/dashboardPageClientComponents';
|
import DashboardPageClient from '@/components/dashboardPageClientComponents';
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ export default async function DashboardPage(props: { searchParams?: { scope: Sco
|
||||||
const scope = Scope.of(props.searchParams?.scope || ScopeType.ThisMonth);
|
const scope = Scope.of(props.searchParams?.scope || ScopeType.ThisMonth);
|
||||||
|
|
||||||
// get all payments in the current scope
|
// get all payments in the current scope
|
||||||
const payments = await prismaClient.payment.findMany({
|
const payments = await prisma.payment.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
date: {
|
date: {
|
||||||
|
@ -108,6 +108,7 @@ export default async function DashboardPage(props: { searchParams?: { scope: Sco
|
||||||
userId: '',
|
userId: '',
|
||||||
name: 'Other',
|
name: 'Other',
|
||||||
type: EntityType.Entity,
|
type: EntityType.Entity,
|
||||||
|
defaultCategoryId: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
},
|
||||||
|
|
|
@ -55,13 +55,19 @@ export const columns = (
|
||||||
cell: ({row}) => {
|
cell: ({row}) => {
|
||||||
const category = categories.find((category) => category.id === row.original.categoryId);
|
const category = categories.find((category) => category.id === row.original.categoryId);
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-4">
|
<>
|
||||||
<svg className="h-5" fill={category?.color} viewBox="0 0 20 20"
|
{
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
category && (
|
||||||
<circle cx="10" cy="10" r="10"/>
|
<div className="flex items-center space-x-4">
|
||||||
</svg>
|
<svg className="h-5" fill={category?.color} viewBox="0 0 20 20"
|
||||||
<p>{category?.name ?? '-'}</p>
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
</div>
|
<circle cx="10" cy="10" r="10"/>
|
||||||
|
</svg>
|
||||||
|
<p>{category?.name ?? '-'}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
size: 200,
|
size: 200,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getUser } from '@/auth';
|
import { getUser } from '@/auth';
|
||||||
import { prismaClient } from '@/prisma';
|
import prisma from '@/prisma';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PaymentPageClientContent from '@/components/paymentPageClientComponents';
|
import PaymentPageClientContent from '@/components/paymentPageClientComponents';
|
||||||
import paymentCreateUpdate from '@/lib/actions/paymentCreateUpdate';
|
import paymentCreateUpdate from '@/lib/actions/paymentCreateUpdate';
|
||||||
|
@ -9,7 +9,7 @@ export default async function PaymentsPage() {
|
||||||
|
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
|
|
||||||
const payments = await prismaClient.payment.findMany({
|
const payments = await prisma.payment.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
},
|
},
|
||||||
|
@ -23,7 +23,7 @@ export default async function PaymentsPage() {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const entities = await prismaClient.entity.findMany({
|
const entities = await prisma.entity.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
},
|
},
|
||||||
|
@ -37,7 +37,7 @@ export default async function PaymentsPage() {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const categories = await prismaClient.category.findMany({
|
const categories = await prisma.category.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
},
|
},
|
||||||
|
|
15
src/app/service-worker.ts
Normal file
15
src/app/service-worker.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { defaultCache } from '@serwist/next/browser';
|
||||||
|
import type { PrecacheEntry } from '@serwist/precaching';
|
||||||
|
import { installSerwist } from '@serwist/sw';
|
||||||
|
|
||||||
|
declare const self: ServiceWorkerGlobalScope & {
|
||||||
|
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
installSerwist({
|
||||||
|
precacheEntries: self.__SW_MANIFEST,
|
||||||
|
skipWaiting: true,
|
||||||
|
clientsClaim: true,
|
||||||
|
navigationPreload: true,
|
||||||
|
runtimeCaching: defaultCache,
|
||||||
|
});
|
|
@ -1,9 +1,9 @@
|
||||||
import { Lucia } from 'lucia';
|
import { Lucia } from 'lucia';
|
||||||
import { PrismaAdapter } from '@lucia-auth/adapter-prisma';
|
import { PrismaAdapter } from '@lucia-auth/adapter-prisma';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import { prismaClient } from '@/prisma';
|
import prisma from '@/prisma';
|
||||||
|
|
||||||
const adapter = new PrismaAdapter(prismaClient.session, prismaClient.user);
|
const adapter = new PrismaAdapter(prisma.session, prisma.user);
|
||||||
|
|
||||||
export const lucia = new Lucia(adapter, {
|
export const lucia = new Lucia(adapter, {
|
||||||
sessionCookie: {
|
sessionCookie: {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Entity } from '@prisma/client';
|
import { Category, Entity } from '@prisma/client';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { CellContext } from '@tanstack/table-core';
|
import { CellContext } from '@tanstack/table-core';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
@ -27,8 +27,9 @@ import {
|
||||||
import { useMediaQuery } from '@/lib/hooks/useMediaQuery';
|
import { useMediaQuery } from '@/lib/hooks/useMediaQuery';
|
||||||
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer';
|
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer';
|
||||||
|
|
||||||
export default function EntityPageClientContent({entities, onSubmit, onDelete, className}: {
|
export default function EntityPageClientContent({entities, categories, onSubmit, onDelete, className}: {
|
||||||
entities: Entity[],
|
entities: Entity[],
|
||||||
|
categories: Category[],
|
||||||
onSubmit: (data: z.infer<typeof entityFormSchema>) => Promise<ActionResponse>,
|
onSubmit: (data: z.infer<typeof entityFormSchema>) => Promise<ActionResponse>,
|
||||||
onDelete: (id: number) => Promise<ActionResponse>,
|
onDelete: (id: number) => Promise<ActionResponse>,
|
||||||
className: string,
|
className: string,
|
||||||
|
@ -146,6 +147,7 @@ export default function EntityPageClientContent({entities, onSubmit, onDelete, c
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<EntityForm
|
<EntityForm
|
||||||
value={selectedEntity}
|
value={selectedEntity}
|
||||||
|
categories={categories}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="grid grid-cols-1 md:grid-cols-2 gap-4 py-4"/>
|
className="grid grid-cols-1 md:grid-cols-2 gap-4 py-4"/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
@ -167,6 +169,7 @@ export default function EntityPageClientContent({entities, onSubmit, onDelete, c
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<EntityForm
|
<EntityForm
|
||||||
value={selectedEntity}
|
value={selectedEntity}
|
||||||
|
categories={categories}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="grid grid-cols-1 md:grid-cols-2 gap-4 py-4"/>
|
className="grid grid-cols-1 md:grid-cols-2 gap-4 py-4"/>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
|
@ -184,7 +187,7 @@ export default function EntityPageClientContent({entities, onSubmit, onDelete, c
|
||||||
{/* Data Table */}
|
{/* Data Table */}
|
||||||
<DataTable
|
<DataTable
|
||||||
className="w-full"
|
className="w-full"
|
||||||
columns={columns(actionCell)}
|
columns={columns(actionCell, categories)}
|
||||||
data={filterEntities(entities, filter)}
|
data={filterEntities(entities, filter)}
|
||||||
pagination/>
|
pagination/>
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,13 @@ import { useRouter } from 'next/navigation';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { sonnerContent } from '@/components/ui/sonner';
|
import { sonnerContent } from '@/components/ui/sonner';
|
||||||
import { entityFormSchema } from '@/lib/form-schemas/entityFormSchema';
|
import { entityFormSchema } from '@/lib/form-schemas/entityFormSchema';
|
||||||
import { Entity, EntityType } from '@prisma/client';
|
import { Category, Entity, EntityType } from '@prisma/client';
|
||||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { AutoCompleteInput } from '@/components/ui/auto-complete-input';
|
||||||
|
|
||||||
export default function EntityForm({value, onSubmit, className}: {
|
export default function EntityForm({value, categories, onSubmit, className}: {
|
||||||
value: Entity | undefined,
|
value: Entity | undefined,
|
||||||
|
categories: Category[],
|
||||||
onSubmit: (data: z.infer<typeof entityFormSchema>) => Promise<ActionResponse>
|
onSubmit: (data: z.infer<typeof entityFormSchema>) => Promise<ActionResponse>
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
|
@ -29,6 +31,7 @@ export default function EntityForm({value, onSubmit, className}: {
|
||||||
id: value?.id ?? undefined,
|
id: value?.id ?? undefined,
|
||||||
name: value?.name ?? '',
|
name: value?.name ?? '',
|
||||||
type: value?.type ?? EntityType.Entity,
|
type: value?.type ?? EntityType.Entity,
|
||||||
|
defaultCategoryId: value?.defaultCategoryId ?? undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -40,6 +43,13 @@ export default function EntityForm({value, onSubmit, className}: {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const categoriesMapped = categories?.map((category) => {
|
||||||
|
return {
|
||||||
|
label: category.name,
|
||||||
|
value: category.id,
|
||||||
|
};
|
||||||
|
}) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form autoComplete="off" onSubmit={form.handleSubmit(handleSubmit)}>
|
<form autoComplete="off" onSubmit={form.handleSubmit(handleSubmit)}>
|
||||||
|
@ -94,6 +104,22 @@ export default function EntityForm({value, onSubmit, className}: {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="defaultCategoryId"
|
||||||
|
render={({field}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Category</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<AutoCompleteInput
|
||||||
|
placeholder="Select category"
|
||||||
|
items={categoriesMapped}
|
||||||
|
{...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" className="w-full">{value?.id ? 'Update Entity' : 'Create Entity'}</Button>
|
<Button type="submit" className="w-full">{value?.id ? 'Update Entity' : 'Create Entity'}</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import React from 'react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { sonnerContent } from '@/components/ui/sonner';
|
|
||||||
import { ActionResponse } from '@/lib/types/actionResponse';
|
|
||||||
|
|
||||||
export default function GenerateSampleDataForm({onSubmit}: { onSubmit: () => Promise<ActionResponse> }) {
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
const response = await onSubmit();
|
|
||||||
toast(sonnerContent(response));
|
|
||||||
router.refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button className="w-full" variant="outline" onClick={handleSubmit}>Generate sample data</Button>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -166,7 +166,17 @@ export default function PaymentForm({value, entities, categories, onSubmit, clas
|
||||||
placeholder="Select payee"
|
placeholder="Select payee"
|
||||||
items={entitiesMapped}
|
items={entitiesMapped}
|
||||||
next={categoryRef}
|
next={categoryRef}
|
||||||
{...field} />
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
field.onChange(e);
|
||||||
|
if (e && e.target.value) {
|
||||||
|
const entity = entities.find((entity) => entity.id === Number(e.target.value));
|
||||||
|
console.log(entity?.defaultCategoryId);
|
||||||
|
if (entity?.defaultCategoryId !== null) {
|
||||||
|
form.setValue('categoryId', entity?.defaultCategoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
97
src/components/form/serverActionTrigger.tsx
Normal file
97
src/components/form/serverActionTrigger.tsx
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { buttonVariants } from '@/components/ui/button';
|
||||||
|
import React from 'react';
|
||||||
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { sonnerContent } from '@/components/ui/sonner';
|
||||||
|
import type { VariantProps } from 'class-variance-authority';
|
||||||
|
import { ActionResponse } from '@/lib/types/actionResponse';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
|
||||||
|
export interface ConfirmationDialogProps {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
actionText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButtonWithActionProps<T = any>
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean;
|
||||||
|
dialog?: ConfirmationDialogProps;
|
||||||
|
action: () => Promise<ActionResponse<T>>;
|
||||||
|
callback?: (data: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ServerActionTrigger = React.forwardRef<HTMLButtonElement, ButtonWithActionProps>(
|
||||||
|
({className, variant, size, asChild = false, ...props}, ref) => {
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const Comp = asChild ? Slot : 'button';
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const response = await props.action();
|
||||||
|
toast(sonnerContent(response));
|
||||||
|
if (props.callback) {
|
||||||
|
props.callback(response);
|
||||||
|
}
|
||||||
|
if (response.redirect) {
|
||||||
|
router.push(response.redirect);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return props.dialog ? (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({variant, size, className}))}
|
||||||
|
{...{...props, dialog: undefined, action: undefined, callback: undefined}}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{props.dialog.title}</AlertDialogTitle>
|
||||||
|
{props.dialog?.description && (
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{props.dialog.description}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
)}
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>
|
||||||
|
Cancel
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleSubmit}>
|
||||||
|
{props.dialog.actionText || 'Confirm'}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
) : (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({variant, size, className}))}
|
||||||
|
ref={ref}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
{...{...props, dialog: undefined, action: undefined, callback: undefined}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ServerActionTrigger.displayName = 'ServerActionTrigger';
|
||||||
|
|
||||||
|
export { ServerActionTrigger };
|
|
@ -1,25 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { ActionResponse } from '@/lib/types/actionResponse';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import React from 'react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { sonnerContent } from '@/components/ui/sonner';
|
|
||||||
|
|
||||||
export default function SignOutForm({onSubmit}: { onSubmit: () => Promise<ActionResponse> }) {
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
|
||||||
const response = await onSubmit();
|
|
||||||
toast(sonnerContent(response));
|
|
||||||
if (response.redirect) {
|
|
||||||
router.push(response.redirect);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button className="w-full" onClick={handleSignOut}>Sign out</Button>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -3,6 +3,8 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
export interface AutoCompleteInputProps
|
export interface AutoCompleteInputProps
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
@ -13,12 +15,12 @@ export interface AutoCompleteInputProps
|
||||||
const AutoCompleteInput = React.forwardRef<HTMLInputElement, AutoCompleteInputProps>(
|
const AutoCompleteInput = React.forwardRef<HTMLInputElement, AutoCompleteInputProps>(
|
||||||
({className, type, ...props}, ref) => {
|
({className, type, ...props}, ref) => {
|
||||||
|
|
||||||
const [value, setValue] = useState(getInitialValue());
|
const [value, setValue] = useState(getNameOfPropValue());
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [lastKey, setLastKey] = useState('');
|
const [lastKey, setLastKey] = useState('');
|
||||||
const [filteredItems, setFilteredItems] = useState(props.items);
|
const [filteredItems, setFilteredItems] = useState(props.items);
|
||||||
|
|
||||||
function getInitialValue() {
|
function getNameOfPropValue() {
|
||||||
|
|
||||||
if (!props.items) {
|
if (!props.items) {
|
||||||
return '';
|
return '';
|
||||||
|
@ -50,6 +52,15 @@ const AutoCompleteInput = React.forwardRef<HTMLInputElement, AutoCompleteInputPr
|
||||||
}
|
}
|
||||||
}, [filteredItems]);
|
}, [filteredItems]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('Prop value changed', value, props.value);
|
||||||
|
if (props.value) {
|
||||||
|
setValue(getNameOfPropValue());
|
||||||
|
} else {
|
||||||
|
setValue('');
|
||||||
|
}
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
|
@ -71,6 +82,20 @@ const AutoCompleteInput = React.forwardRef<HTMLInputElement, AutoCompleteInputPr
|
||||||
props.onKeyDown?.(e);
|
props.onKeyDown?.(e);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
value.length > 0 && (
|
||||||
|
<Button
|
||||||
|
className="absolute end-0 top-0 z-10"
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
handleChange({target: {value: ''}} as any);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4"/>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
{
|
{
|
||||||
open && (
|
open && (
|
||||||
<div
|
<div
|
||||||
|
|
58
src/lib/actions/accountDelete.ts
Normal file
58
src/lib/actions/accountDelete.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { ActionResponse } from '@/lib/types/actionResponse';
|
||||||
|
import { URL_SIGN_IN } from '@/lib/constants';
|
||||||
|
import { getUser, lucia } from '@/auth';
|
||||||
|
import prisma from '@/prisma';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
|
export default async function accountDelete(): Promise<ActionResponse> {
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
const user = await getUser();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
type: 'error',
|
||||||
|
message: 'You aren\'t signed in.',
|
||||||
|
redirect: URL_SIGN_IN,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.payment.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.entity.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.category.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.session.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.user.delete({
|
||||||
|
where: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionCookie = lucia.createBlankSessionCookie();
|
||||||
|
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'success',
|
||||||
|
message: 'Your account was removed.',
|
||||||
|
redirect: URL_SIGN_IN,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ActionResponse } from '@/lib/types/actionResponse';
|
import { ActionResponse } from '@/lib/types/actionResponse';
|
||||||
import { prismaClient } from '@/prisma';
|
import prisma from '@/prisma';
|
||||||
import { getUser } from '@/auth';
|
import { getUser } from '@/auth';
|
||||||
import { URL_SIGN_IN } from '@/lib/constants';
|
import { URL_SIGN_IN } from '@/lib/constants';
|
||||||
import { categoryFormSchema } from '@/lib/form-schemas/categoryFormSchema';
|
import { categoryFormSchema } from '@/lib/form-schemas/categoryFormSchema';
|
||||||
|
@ -25,7 +25,7 @@ export default async function categoryCreateUpdate({
|
||||||
// create/update category
|
// create/update category
|
||||||
try {
|
try {
|
||||||
if (id) {
|
if (id) {
|
||||||
await prismaClient.category.update({
|
await prisma.category.update({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
},
|
},
|
||||||
|
@ -42,7 +42,7 @@ export default async function categoryCreateUpdate({
|
||||||
message: `'${name}' updated`,
|
message: `'${name}' updated`,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
await prismaClient.category.create({
|
await prisma.category.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: name,
|
name: name,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ActionResponse } from '@/lib/types/actionResponse';
|
import { ActionResponse } from '@/lib/types/actionResponse';
|
||||||
import { prismaClient } from '@/prisma';
|
import prisma from '@/prisma';
|
||||||
import { getUser } from '@/auth';
|
import { getUser } from '@/auth';
|
||||||
import { URL_SIGN_IN } from '@/lib/constants';
|
import { URL_SIGN_IN } from '@/lib/constants';
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ export default async function categoryDelete(id: number): Promise<ActionResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that category is associated with user
|
// check that category is associated with user
|
||||||
const category = await prismaClient.category.findFirst({
|
const category = await prisma.category.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
@ -40,7 +40,7 @@ export default async function categoryDelete(id: number): Promise<ActionResponse
|
||||||
|
|
||||||
// delete category
|
// delete category
|
||||||
try {
|
try {
|
||||||
await prismaClient.category.delete({
|
await prisma.category.delete({
|
||||||
where: {
|
where: {
|
||||||
id: category.id,
|
id: category.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ActionResponse } from '@/lib/types/actionResponse';
|
import { ActionResponse } from '@/lib/types/actionResponse';
|
||||||
import { entityFormSchema } from '@/lib/form-schemas/entityFormSchema';
|
import { entityFormSchema } from '@/lib/form-schemas/entityFormSchema';
|
||||||
import { prismaClient } from '@/prisma';
|
import prisma from '@/prisma';
|
||||||
import { getUser } from '@/auth';
|
import { getUser } from '@/auth';
|
||||||
import { URL_SIGN_IN } from '@/lib/constants';
|
import { URL_SIGN_IN } from '@/lib/constants';
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ export default async function entityCreateUpdate({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
|
defaultCategoryId,
|
||||||
}: z.infer<typeof entityFormSchema>): Promise<ActionResponse> {
|
}: z.infer<typeof entityFormSchema>): Promise<ActionResponse> {
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
|
@ -25,13 +26,14 @@ export default async function entityCreateUpdate({
|
||||||
// create/update entity
|
// create/update entity
|
||||||
try {
|
try {
|
||||||
if (id) {
|
if (id) {
|
||||||
await prismaClient.entity.update({
|
await prisma.entity.update({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
name: name,
|
name: name,
|
||||||
type: type,
|
type: type,
|
||||||
|
defaultCategoryId: defaultCategoryId ?? null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -42,11 +44,12 @@ export default async function entityCreateUpdate({
|
||||||
message: `${type} '${name}' updated`,
|
message: `${type} '${name}' updated`,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
await prismaClient.entity.create({
|
await prisma.entity.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: name,
|
name: name,
|
||||||
type: type,
|
type: type,
|
||||||
|
defaultCategoryId: defaultCategoryId ?? null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ActionResponse } from '@/lib/types/actionResponse';
|
import { ActionResponse } from '@/lib/types/actionResponse';
|
||||||
import { prismaClient } from '@/prisma';
|
import prisma from '@/prisma';
|
||||||
import { getUser } from '@/auth';
|
import { getUser } from '@/auth';
|
||||||
import { URL_SIGN_IN } from '@/lib/constants';
|
import { URL_SIGN_IN } from '@/lib/constants';
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ export default async function entityDelete(id: number): Promise<ActionResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that entity is associated with user
|
// check that entity is associated with user
|
||||||
const entity = await prismaClient.entity.findFirst({
|
const entity = await prisma.entity.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
@ -40,7 +40,7 @@ export default async function entityDelete(id: number): Promise<ActionResponse>
|
||||||
|
|
||||||
// delete entity
|
// delete entity
|
||||||
try {
|
try {
|
||||||
await prismaClient.entity.delete({
|
await prisma.entity.delete({
|
||||||
where: {
|
where: {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { prismaClient } from '@/prisma';
|
import prisma from '@/prisma';
|
||||||
import type { Category, Entity } from '@prisma/client';
|
import type { Category, Entity } from '@prisma/client';
|
||||||
import { EntityType } from '@prisma/client';
|
import { EntityType } from '@prisma/client';
|
||||||
import { getUser } from '@/auth';
|
import { getUser } from '@/auth';
|
||||||
|
@ -19,12 +19,12 @@ export default async function generateSampleData(): Promise<ActionResponse> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Categories: create sample data
|
// Categories: create sample data
|
||||||
const categories: Category[] = await prismaClient.category.findMany({where: {userId: user.id}});
|
const categories: Category[] = await prisma.category.findMany({where: {userId: user.id}});
|
||||||
if (await prismaClient.category.count({where: {userId: user.id}}) == 0) {
|
if (await prisma.category.count({where: {userId: user.id}}) == 0) {
|
||||||
|
|
||||||
console.log('Creating sample categories...');
|
console.log('Creating sample categories...');
|
||||||
|
|
||||||
categories.push(await prismaClient.category.create({
|
categories.push(await prisma.category.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: 'Groceries',
|
name: 'Groceries',
|
||||||
|
@ -32,7 +32,7 @@ export default async function generateSampleData(): Promise<ActionResponse> {
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
categories.push(await prismaClient.category.create({
|
categories.push(await prisma.category.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: 'Drugstore items',
|
name: 'Drugstore items',
|
||||||
|
@ -40,7 +40,7 @@ export default async function generateSampleData(): Promise<ActionResponse> {
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
categories.push(await prismaClient.category.create({
|
categories.push(await prisma.category.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: 'Going out',
|
name: 'Going out',
|
||||||
|
@ -48,7 +48,7 @@ export default async function generateSampleData(): Promise<ActionResponse> {
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
categories.push(await prismaClient.category.create({
|
categories.push(await prisma.category.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: 'Random stuff',
|
name: 'Random stuff',
|
||||||
|
@ -56,7 +56,7 @@ export default async function generateSampleData(): Promise<ActionResponse> {
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
categories.push(await prismaClient.category.create({
|
categories.push(await prisma.category.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: 'Salary',
|
name: 'Salary',
|
||||||
|
@ -69,12 +69,12 @@ export default async function generateSampleData(): Promise<ActionResponse> {
|
||||||
console.log(categories);
|
console.log(categories);
|
||||||
|
|
||||||
// Entities: create sample data
|
// Entities: create sample data
|
||||||
const entities: Entity[] = await prismaClient.entity.findMany({where: {userId: user.id}});
|
const entities: Entity[] = await prisma.entity.findMany({where: {userId: user.id}});
|
||||||
if (await prismaClient.entity.count({where: {userId: user.id}}) == 0) {
|
if (await prisma.entity.count({where: {userId: user.id}}) == 0) {
|
||||||
|
|
||||||
console.log('Creating sample entities...');
|
console.log('Creating sample entities...');
|
||||||
|
|
||||||
entities.push(await prismaClient.entity.create({
|
entities.push(await prisma.entity.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: 'Main Account',
|
name: 'Main Account',
|
||||||
|
@ -82,7 +82,7 @@ export default async function generateSampleData(): Promise<ActionResponse> {
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
entities.push(await prismaClient.entity.create({
|
entities.push(await prisma.entity.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: 'Company',
|
name: 'Company',
|
||||||
|
@ -90,7 +90,7 @@ export default async function generateSampleData(): Promise<ActionResponse> {
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
entities.push(await prismaClient.entity.create({
|
entities.push(await prisma.entity.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: 'Supermarket 1',
|
name: 'Supermarket 1',
|
||||||
|
@ -98,7 +98,7 @@ export default async function generateSampleData(): Promise<ActionResponse> {
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
entities.push(await prismaClient.entity.create({
|
entities.push(await prisma.entity.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: 'Supermarket 2',
|
name: 'Supermarket 2',
|
||||||
|
@ -106,7 +106,7 @@ export default async function generateSampleData(): Promise<ActionResponse> {
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
entities.push(await prismaClient.entity.create({
|
entities.push(await prisma.entity.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: 'Supermarket 3',
|
name: 'Supermarket 3',
|
||||||
|
@ -114,7 +114,7 @@ export default async function generateSampleData(): Promise<ActionResponse> {
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
entities.push(await prismaClient.entity.create({
|
entities.push(await prisma.entity.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: 'Supermarket 4',
|
name: 'Supermarket 4',
|
||||||
|
@ -129,14 +129,14 @@ export default async function generateSampleData(): Promise<ActionResponse> {
|
||||||
// Payments: create sample data
|
// Payments: create sample data
|
||||||
console.log('Creating sample payments...');
|
console.log('Creating sample payments...');
|
||||||
|
|
||||||
if (await prismaClient.payment.count({where: {userId: user.id}}) == 0) {
|
if (await prisma.payment.count({where: {userId: user.id}}) == 0) {
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
|
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
date.setDate(1);
|
date.setDate(1);
|
||||||
date.setMonth(date.getMonth() - i);
|
date.setMonth(date.getMonth() - i);
|
||||||
|
|
||||||
await prismaClient.payment.create({
|
await prisma.payment.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
amount: 200000,
|
amount: 200000,
|
||||||
|
@ -164,7 +164,7 @@ export default async function generateSampleData(): Promise<ActionResponse> {
|
||||||
const date = new Date(
|
const date = new Date(
|
||||||
new Date().getTime() - Math.floor(Math.random() * 10000000000));
|
new Date().getTime() - Math.floor(Math.random() * 10000000000));
|
||||||
|
|
||||||
await prismaClient.payment.create({
|
await prisma.payment.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
amount: Math.floor(
|
amount: Math.floor(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ActionResponse } from '@/lib/types/actionResponse';
|
import { ActionResponse } from '@/lib/types/actionResponse';
|
||||||
import { prismaClient } from '@/prisma';
|
import prisma from '@/prisma';
|
||||||
import { getUser } from '@/auth';
|
import { getUser } from '@/auth';
|
||||||
import { URL_SIGN_IN } from '@/lib/constants';
|
import { URL_SIGN_IN } from '@/lib/constants';
|
||||||
import { paymentFormSchema } from '@/lib/form-schemas/paymentFormSchema';
|
import { paymentFormSchema } from '@/lib/form-schemas/paymentFormSchema';
|
||||||
|
@ -29,7 +29,7 @@ export default async function paymentCreateUpdate({
|
||||||
// create/update payment
|
// create/update payment
|
||||||
try {
|
try {
|
||||||
if (id) {
|
if (id) {
|
||||||
await prismaClient.payment.update({
|
await prisma.payment.update({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
},
|
},
|
||||||
|
@ -38,7 +38,7 @@ export default async function paymentCreateUpdate({
|
||||||
date: date,
|
date: date,
|
||||||
payorId: payorId,
|
payorId: payorId,
|
||||||
payeeId: payeeId,
|
payeeId: payeeId,
|
||||||
categoryId: categoryId,
|
categoryId: categoryId ?? null,
|
||||||
note: note,
|
note: note,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -50,14 +50,14 @@ export default async function paymentCreateUpdate({
|
||||||
message: `Payment updated`,
|
message: `Payment updated`,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
await prismaClient.payment.create({
|
await prisma.payment.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
date: date,
|
date: date,
|
||||||
payorId: payorId,
|
payorId: payorId,
|
||||||
payeeId: payeeId,
|
payeeId: payeeId,
|
||||||
categoryId: categoryId,
|
categoryId: categoryId ?? null,
|
||||||
note: note,
|
note: note,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ActionResponse } from '@/lib/types/actionResponse';
|
import { ActionResponse } from '@/lib/types/actionResponse';
|
||||||
import { prismaClient } from '@/prisma';
|
import prisma from '@/prisma';
|
||||||
import { getUser } from '@/auth';
|
import { getUser } from '@/auth';
|
||||||
import { URL_SIGN_IN } from '@/lib/constants';
|
import { URL_SIGN_IN } from '@/lib/constants';
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ export default async function paymentDelete(id: number): Promise<ActionResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that payment is associated with user
|
// check that payment is associated with user
|
||||||
const payment = await prismaClient.payment.findFirst({
|
const payment = await prisma.payment.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
@ -40,7 +40,7 @@ export default async function paymentDelete(id: number): Promise<ActionResponse>
|
||||||
|
|
||||||
// delete payment
|
// delete payment
|
||||||
try {
|
try {
|
||||||
await prismaClient.payment.delete({
|
await prisma.payment.delete({
|
||||||
where: {
|
where: {
|
||||||
id: payment.id,
|
id: payment.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|
|
@ -5,12 +5,12 @@ import { cookies } from 'next/headers';
|
||||||
import { signInFormSchema } from '@/lib/form-schemas/signInFormSchema';
|
import { signInFormSchema } from '@/lib/form-schemas/signInFormSchema';
|
||||||
import { ActionResponse } from '@/lib/types/actionResponse';
|
import { ActionResponse } from '@/lib/types/actionResponse';
|
||||||
import { URL_HOME } from '@/lib/constants';
|
import { URL_HOME } from '@/lib/constants';
|
||||||
import { prismaClient } from '@/prisma';
|
import prisma from '@/prisma';
|
||||||
|
|
||||||
export default async function signIn({username, password}: z.infer<typeof signInFormSchema>): Promise<ActionResponse> {
|
export default async function signIn({username, password}: z.infer<typeof signInFormSchema>): Promise<ActionResponse> {
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
const existingUser = await prismaClient.user.findFirst({
|
const existingUser = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
username: username.toLowerCase(),
|
username: username.toLowerCase(),
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { cookies } from 'next/headers';
|
||||||
import { signUpFormSchema } from '@/lib/form-schemas/signUpFormSchema';
|
import { signUpFormSchema } from '@/lib/form-schemas/signUpFormSchema';
|
||||||
import { ActionResponse } from '@/lib/types/actionResponse';
|
import { ActionResponse } from '@/lib/types/actionResponse';
|
||||||
import { URL_HOME } from '@/lib/constants';
|
import { URL_HOME } from '@/lib/constants';
|
||||||
import { prismaClient } from '@/prisma';
|
import prisma from '@/prisma';
|
||||||
|
|
||||||
export default async function signUp({username, password}: z.infer<typeof signUpFormSchema>): Promise<ActionResponse> {
|
export default async function signUp({username, password}: z.infer<typeof signUpFormSchema>): Promise<ActionResponse> {
|
||||||
'use server';
|
'use server';
|
||||||
|
@ -14,7 +14,7 @@ export default async function signUp({username, password}: z.infer<typeof signUp
|
||||||
const hashedPassword = await new Argon2id().hash(password);
|
const hashedPassword = await new Argon2id().hash(password);
|
||||||
const userId = generateId(15);
|
const userId = generateId(15);
|
||||||
|
|
||||||
const existingUser = await prismaClient.user.findFirst({
|
const existingUser = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
username: username.toLowerCase(),
|
username: username.toLowerCase(),
|
||||||
},
|
},
|
||||||
|
@ -27,7 +27,7 @@ export default async function signUp({username, password}: z.infer<typeof signUp
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await prismaClient.user.create({
|
await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
id: userId,
|
id: userId,
|
||||||
username: username,
|
username: username,
|
||||||
|
|
|
@ -5,4 +5,5 @@ export const entityFormSchema = z.object({
|
||||||
id: z.number().positive().optional(),
|
id: z.number().positive().optional(),
|
||||||
name: z.string().min(1).max(32),
|
name: z.string().min(1).max(32),
|
||||||
type: z.nativeEnum(EntityType),
|
type: z.nativeEnum(EntityType),
|
||||||
|
defaultCategoryId: z.number().positive().optional(),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export interface ActionResponse {
|
export interface ActionResponse<T = any> {
|
||||||
type: 'success' | 'info' | 'warning' | 'error';
|
type: 'success' | 'info' | 'warning' | 'error';
|
||||||
message: string;
|
message: string;
|
||||||
redirect?: string;
|
redirect?: string;
|
||||||
|
data?: T;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,18 @@
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
export const prismaClient = new PrismaClient();
|
const prismaClientSingleton = () => {
|
||||||
|
return new PrismaClient();
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// noinspection ES6ConvertVarToLetConst
|
||||||
|
var prismaGlobal: undefined | ReturnType<typeof prismaClientSingleton>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
|
||||||
|
|
||||||
|
export default prisma;
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
globalThis.prismaGlobal = prisma
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"esnext"
|
"esnext",
|
||||||
|
"webworker"
|
||||||
],
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
@ -25,7 +26,10 @@
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./src/*"
|
"./src/*"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"types": [
|
||||||
|
"@serwist/next/typings"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
|
|
Loading…
Add table
Reference in a new issue