[init] Open source from splitpro.

This commit is contained in:
kk034kk034 2025-05-01 13:25:28 +08:00
commit 18ff9fdd9b
263 changed files with 25683 additions and 0 deletions

57
.dockerignore Normal file
View File

@ -0,0 +1,57 @@
# .env
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
# next.js
/.next/
/out/
next-env.d.ts
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
# .env
.env
.env.example
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
**/public/workbox-*.js
**/public/workbox-*.js.map
**/public/sw.js
**/public/sw.js.map
**/public/worker-*.js
src/server/random.code-workspace
prisma/seed.ts
creds
package-lock.json

72
.env.example Normal file
View File

@ -0,0 +1,72 @@
# When adding additional environment variables, the schema in "/src/env.js"
# should be updated accordingly.
#********* REQUIRED ENV VARS *********
# Prisma
# DataBase ENV VARS
# You could give a DB URL or give the username, password, host, port individually
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DATABASE_URL="postgresql://splitpro:password@localhost:54321/splitpro"
# These variables are also used by docker compose in compose.yml to name the container
# and initialise postgres with default username, password
# POSTGRES_USER="postgres"
# POSTGRES_PASSWORD="strong-password"
# POSTGRES_DB="splitpro"
# DATABASE_URL="postgresql://postgres:strong-password@splitpro-db-prod:5432/splitpro"
# Next Auth
# You can generate a new secret on the command line with:
# openssl rand -base64 32
# https://next-auth.js.org/configuration/options#secret
NEXTAUTH_SECRET="secret"
NEXTAUTH_URL="http://localhost:3000"
# If provided, server-side calls will use this instead of NEXTAUTH_URL. Useful in environments when the server doesn't have access to the canonical URL of your site.
# NEXTAUTH_URL_INTERNAL="http://localhost:3000"
# Enable sending invites
ENABLE_SENDING_INVITES=false
#********* END OF REQUIRED ENV VARS *********
#********* OPTIONAL ENV VARS *********
# SMTP options
FROM_EMAIL=
EMAIL_SERVER_HOST=
EMAIL_SERVER_PORT=
EMAIL_SERVER_USER=
EMAIL_SERVER_PASSWORD=
# Google Provider : https://next-auth.js.org/providers/google
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Authentic Providder : https://next-auth.js.org/providers/authentik
# Issuer: should include the slug without a trailing slash e.g., https://my-authentik-domain.com/application/o/splitpro
AUTHENTIK_ID=
AUTHENTIK_SECRET=
AUTHENTIK_ISSUER=
# Storage: any S3 compatible storage will work, for self hosting can use minio
# If you're using minio for dev, you can generate access keys from the console http://localhost:9001/access-keys/new-account
# R2_ACCESS_KEY="access-key"
# R2_SECRET_KEY="secret-key"
# R2_BUCKET="splitpro"
# R2_URL="http://localhost:9002"
# R2_PUBLIC_URL="http://localhost:9002/splitpro"
# Push notification, Web Push: https://www.npmjs.com/package/web-push
# generate web push keys using this command: web-push generate-vapid-keys --json
WEB_PUSH_PRIVATE_KEY=
WEB_PUSH_PUBLIC_KEY=
WEB_PUSH_EMAIL=
# Email options
FEEDBACK_EMAIL=
# Discord webhook for error notifications
DISCORD_WEBHOOK_URL=
#********* END OF OPTIONAL ENV VARS *********

37
.eslintrc.cjs Normal file
View File

@ -0,0 +1,37 @@
/** @type {import("eslint").Linter.Config} */
const config = {
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
plugins: ["@typescript-eslint"],
extends: [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
],
rules: {
// These opinionated rules are enabled in stylistic-type-checked above.
// Feel free to reconfigure them to your own preference.
"@typescript-eslint/array-type": "off",
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/consistent-type-imports": [
"warn",
{
prefer: "type-imports",
fixStyle: "inline-type-imports",
},
],
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-misused-promises": [
"error",
{
checksVoidReturn: { attributes: false },
},
],
},
};
module.exports = config;

52
.gitignore vendored Normal file
View File

@ -0,0 +1,52 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
# next.js
/.next/
/out/
next-env.d.ts
# production
/build
# misc
.DS_Store
*.pem
.idea
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
**/public/workbox-*.js
**/public/workbox-*.js.map
**/public/sw.js
**/public/sw.js.map
**/public/worker-*.js
src/server/random.code-workspace
certificates

126
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,126 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of
any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
koushik@ossapps.dev.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].

52
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,52 @@
# Contributing to Splitpro
If you plan to contribute to Splitpro, please take a moment to feel awesome ✨ People like you are what open source is about ♥. Any contributions, no matter how big or small, are highly appreciated.
## Before getting started
- Before jumping into a PR be sure to search [existing PRs](https://github.com/oss-apps/split-pro/pulls) or [issues](https://github.com/oss-apps/split-pro/issues) for an open or closed item that relates to your submission.
- Select an issue from [here](https://github.com/oss-apps/split-pro/issues) or create a new one
- Consider the results from the discussion on the issue
## Taking issues
Before taking an issue, ensure that:
- The issue is clearly defined and understood
- No one has been assigned to the issue
- No one has expressed intention to work on it
You can then:
1. Comment on the issue with your intention to work on it
2. Begin work on the issue
Always feel free to ask questions or seek clarification on the issue.
## Developing
The development branch is <code>main</code>. All pull requests should be made against this branch. If you need help getting started, send an email to koushik@ossapps.dev.
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
own GitHub account and then
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
2. Create a new branch:
- Create a new branch (include the issue id and something readable):
```sh
git checkout -b feat/doc-999-somefeature-that-rocks
```
3. See the [Developer Setup](https://github.com/oss-apps/split-pro?tab=readme-ov-file#developer-setup) for more setup details.
## Building
> **Note**
> Please ensure you can make a full production build before pushing code or creating PRs.
You can build the project with:
```bash
pnpm build
```

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 OSS Apps
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

120
README.md Normal file
View File

@ -0,0 +1,120 @@
<p align="center" style="margin-top: 12px">
<a href="https://splitpro.app">
<img width="100px" style="border-radius: 50%;" src="https://splitpro.app/logo_circle.png" alt="SplitPro Logo">
</a>
<h1 align="center">SplitPro</h1>
<h2 align="center">An open source alternative to Splitwise</h2>
<p align="center">
<a href="https://splitpro.app"><strong>To our App »</strong></a>
<br />
<br />
</p>
</p>
## About
SplitPro aims to provide an open-source way to share expenses with your friends.
It's meant to be a complete replacement for Splitwise.
It currently has most of the important features.
- Add expenses with an individual or groups
- Overall balances across the groups
- Multiple currency support
- Upload expense bills
- PWA support
- Split expense unequally (share, percentage, exact amounts, adjustments)
- Push notification
- Download your data
- Import from splitwise
**More features coming every day**
---
## Why
Splitwise is one of the best apps to add expenses and bills.
I understand that every app needs to make money, After all, lots of effort has been put into Splitwise. My main problem is how they implemented this.
Monetising on pro features or ads is fine, but asking money for adding expenses (core feature) is frustrating.
I was searching for other open-source alternatives (Let's be honest, any closed-source product might do the same and I don't have any reason to believe otherwise).
I managed to find a good app [spliit.app](https://spliit.app/) by [Sebastien Castiel](https://scastiel.dev/) but it's not a complete replacement and didn't suit my workflow sadly. Check it out to see if it fits you.
_That's when I decided to work on this_
## Tech stack
- [NextJS](https://nextjs.org/)
- [Tailwind](https://tailwindcss.com/)
- [tRPC](https://trpc.io/)
- [ShadcnUI](https://ui.shadcn.com/)
- [Prisma](https://www.prisma.io/)
- [Postgres](https://www.postgresql.org/)
- [NextAuth](https://next-auth.js.org/)
## Getting started.
### Prerequisites
- Node.js (Version: >=18.x)
- PostgreSQL
- pnpm (recommended)
## Docker
We provide a Docker container for Splitpro, which is published on both DockerHub and GitHub Container Registry.
DockerHub: [https://hub.docker.com/r/ossapps/splitpro](https://hub.docker.com/r/ossapps/splitpro)
GitHub Container Registry: [https://ghcr.io/oss-apps/splitpro](https://ghcr.io/oss-apps/splitpro)
You can pull the Docker image from either of these registries and run it with your preferred container hosting provider.
Please note that you will need to provide environment variables for connecting to the database, redis, aws and so forth.
For detailed instructions on how to configure and run the Docker container, please refer to the Docker [Docker README](./docker/README.md) in the docker directory.
## Developer Setup
### Install Dependencies
```bash
corepack enable
```
```bash
pnpm i
```
### Setting up the environment
- Copy the env.example file into .env
- Setup google oauth required for auth https://next-auth.js.org/providers/google or Email provider by setting SMTP details
- Login to minio console using `splitpro` user and password `password` and [create access keys](http://localhost:9001/access-keys/new-account) and the R2 related env variables
### Run the app
```bash
pnpm d
```
## Sponsors
We are grateful for the support of our sponsors.
### Our Sponsors
<a href="https://hekuta.net/en" target="_blank">
<img src="https://avatars.githubusercontent.com/u/70084358?v=4" alt="hekuta" style="width:60px;height:60px;">
</a>
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=oss-apps/split-pro&type=Date)](https://star-history.com/#oss-apps/split-pro&Date)

17
components.json Normal file
View File

@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/styles/globals.css",
"baseColor": "gray",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "~/components",
"utils": "~/lib/utils"
}
}

51
docker/Dockerfile Normal file
View File

@ -0,0 +1,51 @@
FROM node:20.11.1-alpine AS base
ENV SKIP_ENV_VALIDATION="true"
ENV DOCKER_OUTPUT=1
ENV NEXT_TELEMETRY_DISABLED 1
RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
RUN npm i -g pnpm@8.9
RUN ls
COPY package.json pnpm-lock.yaml ./
RUN pnpm install
COPY . .
RUN pnpm generate
RUN pnpm build
FROM node:20-alpine3.19 as release
WORKDIR /app
RUN npm i -g pnpm@8.9
RUN apk add --no-cache libc6-compat
RUN apk update
COPY --from=base /app/next.config.js .
COPY --from=base /app/package.json .
COPY --from=base /app/pnpm-lock.yaml .
COPY --from=base /app/.next/standalone ./
COPY --from=base /app/.next/static ./.next/static
COPY --from=base /app/public ./public
COPY --from=base /app/prisma/schema.prisma ./prisma/schema.prisma
COPY --from=base /app/prisma/migrations ./prisma/migrations
COPY --from=base /app/node_modules/prisma ./node_modules/prisma
COPY --from=base /app/node_modules/@prisma ./node_modules/@prisma
COPY --from=base /app/node_modules/sharp ./node_modules/sharp
# Symlink the prisma binary
RUN mkdir node_modules/.bin
RUN ln -s /app/node_modules/prisma/build/index.js ./node_modules/.bin/prisma
# set this so it throws error where starting server
ENV SKIP_ENV_VALIDATION="false"
COPY ./docker/start.sh ./start.sh
CMD ["sh", "start.sh"]

65
docker/README.md Normal file
View File

@ -0,0 +1,65 @@
# Docker Setup for Splitpro
The following guide will walk you through setting up Splitpro using Docker. You can choose between a production setup using Docker Compose or a standalone container.
## Prerequisites
Before you begin, ensure that you have the following installed:
- Docker
- Docker Compose (if using the Docker Compose setup)
## Option 1: Production Docker Compose Setup
This setup includes PostgreSQL and the Splitpro application.
1. Download the Docker Compose file from the Splitpro repository: [compose.yml](https://github.com/oss-apps/split-pro/blob/main/docker/prod/compose.yml)
2. Navigate to the directory containing the `compose.yml` file.
3. Create a `.env` file in the same directory. Copy the contents of `.env.example`
4. Run the following command to start the containers:
```
docker-compose --env-file ./.env up -d
```
This will start the PostgreSQL database and the Splitpro application containers.
5. Access the Splitpro application by visiting `http://localhost:3000` in your web browser.
## Option 2: Standalone Docker Container
If you prefer to host the Splitpro application on your container provider of choice, you can use the pre-built Docker image from DockerHub or GitHub's Package Registry.
1. Pull the Splitpro Docker image:
```
docker pull ossapps/splitpro
```
Or, if using GitHub's Package Registry:
```
docker pull ghcr.io/oss-apps/splitpro
```
2. Run the Docker container, providing the necessary environment variables for your database and SMTP host:
```
docker run -d \
-p ${PORT:-3000}:${PORT:-3000} \
-e PORT=${PORT:-3000} \
-e DATABASE_URL=${DATABASE_URL:?err} \
-e NEXTAUTH_URL=${NEXTAUTH_URL:?err} \
-e NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?err} \
-e GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:?err} \
-e GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:?err}
ossapps/splitpro
```
Replace the placeholders with your actual database and aws details.
1. Access the Splitpro application by visiting the URL you provided in the `NEXTAUTH_URL` environment variable in your web browser.
## Success
You have now successfully set up Splitpro using Docker. If you encounter any issues or have further questions, please seek assistance from the community.

26
docker/build.sh Normal file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
command -v docker >/dev/null 2>&1 || {
echo "Docker is not running. Please start Docker and try again."
exit 1
}
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
GIT_SHA="$(git rev-parse HEAD)"
echo "Building docker image for monorepo at $MONOREPO_ROOT"
echo "App version: $APP_VERSION"
echo "Git SHA: $GIT_SHA"
docker build -f "$SCRIPT_DIR/Dockerfile" \
--progress=plain \
-t "ossapps/splitpro:latest" \
-t "ossapps/splitpro:$GIT_SHA" \
-t "ossapps/splitpro:$APP_VERSION" \
-t "ghcr.io/oss-apps/splitpro:latest" \
-t "ghcr.io/oss-apps/splitpro:$GIT_SHA" \
-t "ghcr.io/oss-apps/splitpro:$APP_VERSION" \
"$MONOREPO_ROOT"

33
docker/dev/compose.yml Normal file
View File

@ -0,0 +1,33 @@
name: split-pro-dev
services:
postgres:
image: postgres:16
container_name: splitpro-db-dev
restart: always
environment:
- POSTGRES_USER=splitpro
- POSTGRES_PASSWORD=password
- POSTGRES_DB=splitpro
volumes:
- database:/var/lib/postgresql/data
ports:
- '54321:5432'
minio:
image: minio/minio
container_name: splitpro-storage-dev
ports:
- 9002:9002
- 9001:9001
volumes:
- minio:/data
environment:
MINIO_ROOT_USER: splitpro
MINIO_ROOT_PASSWORD: password
entrypoint: sh
command: -c 'mkdir -p /data/splitpro && minio server /data --console-address ":9001" --address ":9002"'
volumes:
database:
minio:

60
docker/prod/compose.yml Normal file
View File

@ -0,0 +1,60 @@
name: split-pro-prod
services:
postgres:
image: postgres:16
container_name: splitpro-db-prod
restart: always
environment:
- POSTGRES_USER=${POSTGRES_USER:?err}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?err}
- POSTGRES_DB=${POSTGRES_DB:?err}
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}']
interval: 10s
timeout: 5s
retries: 5
# ports:
# - "5432:5432"
volumes:
- database:/var/lib/postgresql/data
splitpro:
image: ossapps/splitpro:latest
container_name: splitpro
restart: always
ports:
- ${PORT:-3000}:${PORT:-3000}
environment:
- PORT=${PORT:-3000}
- DATABASE_URL=${DATABASE_URL:?err}
- NEXTAUTH_URL=${NEXTAUTH_URL:?err}
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?err}
- ENABLE_SENDING_INVITES=${ENABLE_SENDING_INVITES:?err}
- FROM_EMAIL=${FROM_EMAIL}
- EMAIL_SERVER_HOST=${EMAIL_SERVER_HOST}
- EMAIL_SERVER_PORT=${EMAIL_SERVER_PORT}
- EMAIL_SERVER_USER=${EMAIL_SERVER_USER}
- EMAIL_SERVER_PASSWORD=${EMAIL_SERVER_PASSWORD}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
- AUTHENTIK_ID=${AUTHENTIK_ID}
- AUTHENTIK_SECRET=${AUTHENTIK_SECRET}
- AUTHENTIK_ISSUER=${AUTHENTIK_ISSUER}
- R2_ACCESS_KEY=${R2_ACCESS_KEY}
- R2_SECRET_KEY=${R2_SECRET_KEY}
- R2_BUCKET=${R2_BUCKET}
- R2_URL=${R2_URL}
- R2_PUBLIC_URL=${R2_PUBLIC_URL}
- WEB_PUSH_PRIVATE_KEY=${WEB_PUSH_PRIVATE_KEY}
- WEB_PUSH_PUBLIC_KEY=${WEB_PUSH_PUBLIC_KEY}
- WEB_PUSH_EMAIL=${WEB_PUSH_EMAIL}
- FEEDBACK_EMAIL=${FEEDBACK_EMAIL}
- DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL}
depends_on:
postgres:
condition: service_healthy
volumes:
database:

12
docker/start.sh Normal file
View File

@ -0,0 +1,12 @@
#!/bin/sh
set -x
echo "Deploying prisma migrations"
pnpx prisma migrate deploy --schema ./prisma/schema.prisma
echo "Starting web server"
node server.js

58
next.config.js Normal file
View File

@ -0,0 +1,58 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
* for Docker builds.
*/
await import('./src/env.js');
/** @type {import("next").NextConfig} */
import pwa from 'next-pwa';
// @ts-ignore
import nextra from 'nextra';
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const withPwa = pwa({
dest: 'public',
// disable: process.env.NODE_ENV === 'development',
});
const config = {
reactStrictMode: true,
output: process.env.DOCKER_OUTPUT ? 'standalone' : undefined,
experimental: {
instrumentationHook: true,
},
/**
* If you are using `appDir` then you must comment the below `i18n` config out.
*
* @see https://github.com/vercel/next.js/issues/41980
*/
i18n: {
locales: ['en'],
defaultLocale: 'en',
},
transpilePackages: ['geist'],
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '**',
},
{
protocol: 'http',
hostname: '**',
},
],
},
};
const withNextra = nextra({
theme: 'nextra-theme-blog',
themeConfig: './theme.config.jsx',
});
// @ts-ignore
export default withNextra(withPwa(config));

108
package.json Normal file
View File

@ -0,0 +1,108 @@
{
"name": "split",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "next build",
"just-build": "next build",
"db:push": "prisma db push",
"db:studio": "prisma studio",
"db:dev": "prisma migrate dev",
"db:seed": "prisma db seed",
"prisma:prod": "prisma migrate deploy",
"dev": "next dev",
"postinstall": "prisma generate",
"generate": "prisma generate",
"lint": "next lint",
"start": "sleep 3 && pnpm prisma:prod && next start",
"start-with-latest-migrations": "prisma migrate deploy && next start",
"d": "pnpm dx && pnpm dev",
"dx": "pnpm i && pnpm dx:up && pnpm db:dev",
"dx:up": "docker compose -f docker/dev/compose.yml up -d",
"dx:down": "docker compose -f docker/dev/compose.yml down"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.515.0",
"@aws-sdk/s3-request-presigner": "^3.515.0",
"@heroicons/react": "^2.1.1",
"@hookform/resolvers": "^3.3.4",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.9.1",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@t3-oss/env-nextjs": "^0.11.1",
"@tanstack/react-query": "^4.36.1",
"@trpc/client": "^10.43.6",
"@trpc/next": "^10.43.6",
"@trpc/react-query": "^10.43.6",
"@trpc/server": "^10.43.6",
"babel-loader": "^9.1.3",
"boring-avatars": "^1.10.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"cmdk": "^0.2.0",
"date-fns": "^3.3.1",
"framer-motion": "^11.0.3",
"geist": "^1.2.1",
"input-otp": "^1.2.3",
"lucide-react": "^0.312.0",
"nanoid": "^5.0.6",
"next": "^14.0.4",
"next-auth": "^4.24.5",
"next-pwa": "^5.6.0",
"next-themes": "^0.2.1",
"nextra": "^2.13.4",
"nextra-theme-blog": "^2.13.4",
"nodemailer": "^6.9.8",
"react": "18.2.0",
"react-day-picker": "^8.10.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.50.1",
"resend": "^3.2.0",
"sharp": "0.32.6",
"sonner": "^1.4.0",
"superjson": "^2.2.1",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.8.9",
"web-push": "^3.6.7",
"zod": "^3.22.4",
"zustand": "^4.5.0"
},
"devDependencies": {
"@types/eslint": "^8.44.7",
"@types/next-pwa": "^5.6.9",
"@types/node": "^18.17.0",
"@types/nodemailer": "^6.4.15",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/web-push": "^3.6.3",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.54.0",
"eslint-config-next": "^14.0.4",
"postcss": "^8.4.31",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7",
"prisma": "^5.9.1",
"tailwindcss": "^3.3.5",
"tsx": "^4.7.1",
"typescript": "^5.1.6"
},
"ct3aMetadata": {
"initVersion": "7.25.2"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"packageManager": "pnpm@8.9.2"
}

11198
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

8
postcss.config.cjs Normal file
View File

@ -0,0 +1,8 @@
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
module.exports = config;

10
prettier.config.js Normal file
View File

@ -0,0 +1,10 @@
/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
const config = {
plugins: ['prettier-plugin-tailwindcss'],
semi: true,
singleQuote: true,
trailingComma: 'all',
printWidth: 100,
};
export default config;

View File

@ -0,0 +1,208 @@
-- CreateEnum
CREATE TYPE "SplitType" AS ENUM ('EQUAL', 'PERCENTAGE', 'EXACT', 'SHARE', 'ADJUSTMENT', 'SETTLEMENT');
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"name" TEXT,
"email" TEXT,
"emailVerified" TIMESTAMP(3),
"image" TEXT,
"currency" TEXT NOT NULL DEFAULT 'USD',
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateTable
CREATE TABLE "Balance" (
"userId" INTEGER NOT NULL,
"currency" TEXT NOT NULL,
"friendId" INTEGER NOT NULL,
"amount" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Balance_pkey" PRIMARY KEY ("userId","currency","friendId")
);
-- CreateTable
CREATE TABLE "Group" (
"id" SERIAL NOT NULL,
"publicId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"defaultCurrency" TEXT NOT NULL DEFAULT 'USD',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Group_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GroupUser" (
"groupId" INTEGER NOT NULL,
"userId" INTEGER NOT NULL,
CONSTRAINT "GroupUser_pkey" PRIMARY KEY ("groupId","userId")
);
-- CreateTable
CREATE TABLE "GroupBalance" (
"groupId" INTEGER NOT NULL,
"currency" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"firendId" INTEGER NOT NULL,
"amount" INTEGER NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "GroupBalance_pkey" PRIMARY KEY ("groupId","currency","firendId","userId")
);
-- CreateTable
CREATE TABLE "Expense" (
"id" TEXT NOT NULL,
"paidBy" INTEGER NOT NULL,
"addedBy" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"category" TEXT NOT NULL,
"amount" INTEGER NOT NULL,
"splitType" "SplitType" NOT NULL DEFAULT 'EQUAL',
"expenseDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"currency" TEXT NOT NULL,
"fileKey" TEXT,
"groupId" INTEGER,
CONSTRAINT "Expense_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ExpenseParticipant" (
"expenseId" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"amount" INTEGER NOT NULL,
CONSTRAINT "ExpenseParticipant_pkey" PRIMARY KEY ("expenseId","userId")
);
-- CreateTable
CREATE TABLE "ExpenseNote" (
"id" TEXT NOT NULL,
"expenseId" TEXT NOT NULL,
"note" TEXT NOT NULL,
"createdById" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ExpenseNote_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
-- CreateIndex
CREATE UNIQUE INDEX "Group_publicId_key" ON "Group"("publicId");
-- CreateIndex
CREATE INDEX "Expense_groupId_idx" ON "Expense"("groupId");
-- CreateIndex
CREATE INDEX "Expense_paidBy_idx" ON "Expense"("paidBy");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Balance" ADD CONSTRAINT "Balance_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Balance" ADD CONSTRAINT "Balance_friendId_fkey" FOREIGN KEY ("friendId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Group" ADD CONSTRAINT "Group_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GroupUser" ADD CONSTRAINT "GroupUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GroupUser" ADD CONSTRAINT "GroupUser_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "Group"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GroupBalance" ADD CONSTRAINT "GroupBalance_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "Group"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GroupBalance" ADD CONSTRAINT "GroupBalance_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GroupBalance" ADD CONSTRAINT "GroupBalance_firendId_fkey" FOREIGN KEY ("firendId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "Group"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_paidBy_fkey" FOREIGN KEY ("paidBy") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_addedBy_fkey" FOREIGN KEY ("addedBy") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ExpenseParticipant" ADD CONSTRAINT "ExpenseParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ExpenseParticipant" ADD CONSTRAINT "ExpenseParticipant_expenseId_fkey" FOREIGN KEY ("expenseId") REFERENCES "Expense"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ExpenseNote" ADD CONSTRAINT "ExpenseNote_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ExpenseNote" ADD CONSTRAINT "ExpenseNote_expenseId_fkey" FOREIGN KEY ("expenseId") REFERENCES "Expense"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "Expense" ADD COLUMN "deletedAt" TIMESTAMP(3),
ADD COLUMN "deletedBy" INTEGER;
-- AddForeignKey
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_deletedBy_fkey" FOREIGN KEY ("deletedBy") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,7 @@
-- CreateTable
CREATE TABLE "PushNotification" (
"userId" INTEGER NOT NULL,
"subscription" TEXT NOT NULL,
CONSTRAINT "PushNotification_pkey" PRIMARY KEY ("userId")
);

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Balance" ADD COLUMN "importedFromSplitwise" BOOLEAN NOT NULL DEFAULT false;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Group" ADD COLUMN "splitwiseGroupId" TEXT;

View File

@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[splitwiseGroupId]` on the table `Group` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "Group_splitwiseGroupId_key" ON "Group"("splitwiseGroupId");

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Expense" ADD COLUMN "updatedBy" INTEGER;
-- AddForeignKey
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_updatedBy_fkey" FOREIGN KEY ("updatedBy") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

189
prisma/schema.prisma Normal file
View File

@ -0,0 +1,189 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
previewFeatures = ["relationJoins"]
}
datasource db {
provider = "postgresql"
// NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below
// Further reading:
// https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
// https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
url = env("DATABASE_URL")
}
// Necessary for Next auth
model Account {
id String @id @default(cuid())
userId Int
type String
provider String
providerAccountId String
refresh_token String? // @db.Text
access_token String? // @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? // @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId Int
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id Int @id @default(autoincrement())
name String?
email String? @unique
emailVerified DateTime?
image String?
currency String @default("USD")
accounts Account[]
sessions Session[]
groups Group[]
associatedGroups GroupUser[]
expenseParticipants ExpenseParticipant[]
expenseNotes ExpenseNote[]
userBalances Balance[] @relation("UserBalance")
friendBalances Balance[] @relation("FriendBalance")
groupUserBalances GroupBalance[] @relation("GroupUserBalance")
groupFriendBalances GroupBalance[] @relation("GroupFriendBalance")
paidExpenses Expense[] @relation("PaidByUser")
addedExpenses Expense[] @relation("AddedByUser")
deletedExpenses Expense[] @relation("DeletedByUser")
updatedExpenses Expense[] @relation("UpdatedByUser")
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
model Balance {
userId Int
currency String
friendId Int
amount Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
importedFromSplitwise Boolean @default(false)
user User @relation(name: "UserBalance", fields: [userId], references: [id], onDelete: Cascade)
friend User @relation(name: "FriendBalance", fields: [friendId], references: [id], onDelete: Cascade)
@@id([userId, currency, friendId])
}
model Group {
id Int @id @default(autoincrement())
publicId String @unique
name String
userId Int
defaultCurrency String @default("USD")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
splitwiseGroupId String? @unique
createdBy User @relation(fields: [userId], references: [id], onDelete: Cascade)
groupUsers GroupUser[]
expenses Expense[]
groupBalances GroupBalance[]
}
model GroupUser {
groupId Int
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
@@id([groupId, userId])
}
model GroupBalance {
groupId Int
currency String
userId Int
firendId Int
amount Int
updatedAt DateTime @updatedAt
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
user User @relation(name: "GroupUserBalance", fields: [userId], references: [id], onDelete: Cascade)
friend User @relation(name: "GroupFriendBalance", fields: [firendId], references: [id], onDelete: Cascade)
@@id([groupId, currency, firendId, userId])
}
enum SplitType {
EQUAL
PERCENTAGE
EXACT
SHARE
ADJUSTMENT
SETTLEMENT
}
model Expense {
id String @id @default(cuid())
paidBy Int
addedBy Int
name String
category String
amount Int
splitType SplitType @default(EQUAL)
expenseDate DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
currency String
fileKey String?
groupId Int?
deletedAt DateTime?
deletedBy Int?
updatedBy Int?
group Group? @relation(fields: [groupId], references: [id], onDelete: Cascade)
paidByUser User @relation(name: "PaidByUser", fields: [paidBy], references: [id], onDelete: Cascade)
addedByUser User @relation(name: "AddedByUser", fields: [addedBy], references: [id], onDelete: Cascade)
deletedByUser User? @relation(name: "DeletedByUser", fields: [deletedBy], references: [id], onDelete: Cascade)
updatedByUser User? @relation(name: "UpdatedByUser", fields: [updatedBy], references: [id], onDelete: SetNull)
expenseParticipants ExpenseParticipant[]
expenseNotes ExpenseNote[]
@@index([groupId])
@@index([paidBy])
}
model ExpenseParticipant {
expenseId String
userId Int
amount Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
expense Expense @relation(fields: [expenseId], references: [id], onDelete: Cascade)
@@id([expenseId, userId])
}
model ExpenseNote {
id String @id @default(cuid())
expenseId String
note String
createdById Int
createdAt DateTime @default(now())
createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade)
expense Expense @relation(fields: [expenseId], references: [id], onDelete: Cascade)
}
model PushNotification {
userId Int @id
subscription String
}

73
prisma/seed.ts Normal file
View File

@ -0,0 +1,73 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function createUsers() {
const users = await prisma.user.createMany({
data: [
{
name: 'Alice',
email: 'alice@example.com',
currency: 'USD',
},
{
name: 'Bob',
email: 'bob@example.com',
currency: 'EUR',
},
{
name: 'Charlie',
email: 'charlie@example.com',
currency: 'GBP',
},
{
name: 'Diana',
email: 'diana@example.com',
currency: 'JPY',
},
{
name: 'Evan',
email: 'evan@example.com',
currency: 'CNY',
},
],
});
return prisma.user.findMany();
}
async function createGroups() {
// Assuming Alice creates a group and adds Bob and Charlie
const users = await prisma.user.findMany();
if (users.length) {
const group = await prisma.group.create({
data: {
name: 'Holiday Trip',
publicId: 'holiday-trip-123',
defaultCurrency: 'USD',
createdBy: { connect: { id: users[0]?.id } },
},
});
await prisma.groupUser.createMany({
data: users.map((u) => ({ groupId: group.id, userId: u.id })),
});
console.log('Group created and users added');
}
}
async function main() {
await createUsers();
await createGroups();
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(() => {
prisma.$disconnect().catch(console.log);
});

BIN
public/Desktop.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

1
public/add_expense.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.8 KiB

1
public/empty_img.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
public/group.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/hero.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

452
public/icons/icons.json Normal file
View File

@ -0,0 +1,452 @@
{
"icons": [
{
"src": "windows11/SmallTile.scale-100.png",
"sizes": "71x71"
},
{
"src": "windows11/SmallTile.scale-125.png",
"sizes": "89x89"
},
{
"src": "windows11/SmallTile.scale-150.png",
"sizes": "107x107"
},
{
"src": "windows11/SmallTile.scale-200.png",
"sizes": "142x142"
},
{
"src": "windows11/SmallTile.scale-400.png",
"sizes": "284x284"
},
{
"src": "windows11/Square150x150Logo.scale-100.png",
"sizes": "150x150"
},
{
"src": "windows11/Square150x150Logo.scale-125.png",
"sizes": "188x188"
},
{
"src": "windows11/Square150x150Logo.scale-150.png",
"sizes": "225x225"
},
{
"src": "windows11/Square150x150Logo.scale-200.png",
"sizes": "300x300"
},
{
"src": "windows11/Square150x150Logo.scale-400.png",
"sizes": "600x600"
},
{
"src": "windows11/Wide310x150Logo.scale-100.png",
"sizes": "310x150"
},
{
"src": "windows11/Wide310x150Logo.scale-125.png",
"sizes": "388x188"
},
{
"src": "windows11/Wide310x150Logo.scale-150.png",
"sizes": "465x225"
},
{
"src": "windows11/Wide310x150Logo.scale-200.png",
"sizes": "620x300"
},
{
"src": "windows11/Wide310x150Logo.scale-400.png",
"sizes": "1240x600"
},
{
"src": "windows11/LargeTile.scale-100.png",
"sizes": "310x310"
},
{
"src": "windows11/LargeTile.scale-125.png",
"sizes": "388x388"
},
{
"src": "windows11/LargeTile.scale-150.png",
"sizes": "465x465"
},
{
"src": "windows11/LargeTile.scale-200.png",
"sizes": "620x620"
},
{
"src": "windows11/LargeTile.scale-400.png",
"sizes": "1240x1240"
},
{
"src": "windows11/Square44x44Logo.scale-100.png",
"sizes": "44x44"
},
{
"src": "windows11/Square44x44Logo.scale-125.png",
"sizes": "55x55"
},
{
"src": "windows11/Square44x44Logo.scale-150.png",
"sizes": "66x66"
},
{
"src": "windows11/Square44x44Logo.scale-200.png",
"sizes": "88x88"
},
{
"src": "windows11/Square44x44Logo.scale-400.png",
"sizes": "176x176"
},
{
"src": "windows11/StoreLogo.scale-100.png",
"sizes": "50x50"
},
{
"src": "windows11/StoreLogo.scale-125.png",
"sizes": "63x63"
},
{
"src": "windows11/StoreLogo.scale-150.png",
"sizes": "75x75"
},
{
"src": "windows11/StoreLogo.scale-200.png",
"sizes": "100x100"
},
{
"src": "windows11/StoreLogo.scale-400.png",
"sizes": "200x200"
},
{
"src": "windows11/SplashScreen.scale-100.png",
"sizes": "620x300"
},
{
"src": "windows11/SplashScreen.scale-125.png",
"sizes": "775x375"
},
{
"src": "windows11/SplashScreen.scale-150.png",
"sizes": "930x450"
},
{
"src": "windows11/SplashScreen.scale-200.png",
"sizes": "1240x600"
},
{
"src": "windows11/SplashScreen.scale-400.png",
"sizes": "2480x1200"
},
{
"src": "windows11/Square44x44Logo.targetsize-16.png",
"sizes": "16x16"
},
{
"src": "windows11/Square44x44Logo.targetsize-20.png",
"sizes": "20x20"
},
{
"src": "windows11/Square44x44Logo.targetsize-24.png",
"sizes": "24x24"
},
{
"src": "windows11/Square44x44Logo.targetsize-30.png",
"sizes": "30x30"
},
{
"src": "windows11/Square44x44Logo.targetsize-32.png",
"sizes": "32x32"
},
{
"src": "windows11/Square44x44Logo.targetsize-36.png",
"sizes": "36x36"
},
{
"src": "windows11/Square44x44Logo.targetsize-40.png",
"sizes": "40x40"
},
{
"src": "windows11/Square44x44Logo.targetsize-44.png",
"sizes": "44x44"
},
{
"src": "windows11/Square44x44Logo.targetsize-48.png",
"sizes": "48x48"
},
{
"src": "windows11/Square44x44Logo.targetsize-60.png",
"sizes": "60x60"
},
{
"src": "windows11/Square44x44Logo.targetsize-64.png",
"sizes": "64x64"
},
{
"src": "windows11/Square44x44Logo.targetsize-72.png",
"sizes": "72x72"
},
{
"src": "windows11/Square44x44Logo.targetsize-80.png",
"sizes": "80x80"
},
{
"src": "windows11/Square44x44Logo.targetsize-96.png",
"sizes": "96x96"
},
{
"src": "windows11/Square44x44Logo.targetsize-256.png",
"sizes": "256x256"
},
{
"src": "windows11/Square44x44Logo.altform-unplated_targetsize-16.png",
"sizes": "16x16"
},
{
"src": "windows11/Square44x44Logo.altform-unplated_targetsize-20.png",
"sizes": "20x20"
},
{
"src": "windows11/Square44x44Logo.altform-unplated_targetsize-24.png",
"sizes": "24x24"
},
{
"src": "windows11/Square44x44Logo.altform-unplated_targetsize-30.png",
"sizes": "30x30"
},
{
"src": "windows11/Square44x44Logo.altform-unplated_targetsize-32.png",
"sizes": "32x32"
},
{
"src": "windows11/Square44x44Logo.altform-unplated_targetsize-36.png",
"sizes": "36x36"
},
{
"src": "windows11/Square44x44Logo.altform-unplated_targetsize-40.png",
"sizes": "40x40"
},
{
"src": "windows11/Square44x44Logo.altform-unplated_targetsize-44.png",
"sizes": "44x44"
},
{
"src": "windows11/Square44x44Logo.altform-unplated_targetsize-48.png",
"sizes": "48x48"
},
{
"src": "windows11/Square44x44Logo.altform-unplated_targetsize-60.png",
"sizes": "60x60"
},
{
"src": "windows11/Square44x44Logo.altform-unplated_targetsize-64.png",
"sizes": "64x64"
},
{
"src": "windows11/Square44x44Logo.altform-unplated_targetsize-72.png",
"sizes": "72x72"
},
{
"src": "windows11/Square44x44Logo.altform-unplated_targetsize-80.png",
"sizes": "80x80"
},
{
"src": "windows11/Square44x44Logo.altform-unplated_targetsize-96.png",
"sizes": "96x96"
},
{
"src": "windows11/Square44x44Logo.altform-unplated_targetsize-256.png",
"sizes": "256x256"
},
{
"src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png",
"sizes": "16x16"
},
{
"src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png",
"sizes": "20x20"
},
{
"src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png",
"sizes": "24x24"
},
{
"src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png",
"sizes": "30x30"
},
{
"src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png",
"sizes": "32x32"
},
{
"src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png",
"sizes": "36x36"
},
{
"src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png",
"sizes": "40x40"
},
{
"src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png",
"sizes": "44x44"
},
{
"src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png",
"sizes": "48x48"
},
{
"src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png",
"sizes": "60x60"
},
{
"src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png",
"sizes": "64x64"
},
{
"src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png",
"sizes": "72x72"
},
{
"src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png",
"sizes": "80x80"
},
{
"src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png",
"sizes": "96x96"
},
{
"src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png",
"sizes": "256x256"
},
{
"src": "android/android-launchericon-512-512.png",
"sizes": "512x512"
},
{
"src": "android/android-launchericon-192-192.png",
"sizes": "192x192"
},
{
"src": "android/android-launchericon-144-144.png",
"sizes": "144x144"
},
{
"src": "android/android-launchericon-96-96.png",
"sizes": "96x96"
},
{
"src": "android/android-launchericon-72-72.png",
"sizes": "72x72"
},
{
"src": "android/android-launchericon-48-48.png",
"sizes": "48x48"
},
{
"src": "ios/16.png",
"sizes": "16x16"
},
{
"src": "ios/20.png",
"sizes": "20x20"
},
{
"src": "ios/29.png",
"sizes": "29x29"
},
{
"src": "ios/32.png",
"sizes": "32x32"
},
{
"src": "ios/40.png",
"sizes": "40x40"
},
{
"src": "ios/50.png",
"sizes": "50x50"
},
{
"src": "ios/57.png",
"sizes": "57x57"
},
{
"src": "ios/58.png",
"sizes": "58x58"
},
{
"src": "ios/60.png",
"sizes": "60x60"
},
{
"src": "ios/64.png",
"sizes": "64x64"
},
{
"src": "ios/72.png",
"sizes": "72x72"
},
{
"src": "ios/76.png",
"sizes": "76x76"
},
{
"src": "ios/80.png",
"sizes": "80x80"
},
{
"src": "ios/87.png",
"sizes": "87x87"
},
{
"src": "ios/100.png",
"sizes": "100x100"
},
{
"src": "ios/114.png",
"sizes": "114x114"
},
{
"src": "ios/120.png",
"sizes": "120x120"
},
{
"src": "ios/128.png",
"sizes": "128x128"
},
{
"src": "ios/144.png",
"sizes": "144x144"
},
{
"src": "ios/152.png",
"sizes": "152x152"
},
{
"src": "ios/167.png",
"sizes": "167x167"
},
{
"src": "ios/180.png",
"sizes": "180x180"
},
{
"src": "ios/192.png",
"sizes": "192x192"
},
{
"src": "ios/256.png",
"sizes": "256x256"
},
{
"src": "ios/512.png",
"sizes": "512x512"
},
{
"src": "ios/1024.png",
"sizes": "1024x1024"
}
]
}

BIN
public/icons/ios/100.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
public/icons/ios/1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
public/icons/ios/114.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
public/icons/ios/120.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
public/icons/ios/128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
public/icons/ios/144.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/icons/ios/152.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
public/icons/ios/16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

BIN
public/icons/ios/167.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
public/icons/ios/180.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
public/icons/ios/192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
public/icons/ios/20.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

BIN
public/icons/ios/256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
public/icons/ios/29.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 B

BIN
public/icons/ios/32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/icons/ios/40.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/icons/ios/50.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/icons/ios/512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
public/icons/ios/57.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/icons/ios/58.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/icons/ios/60.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/icons/ios/64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/icons/ios/72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/icons/ios/76.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/icons/ios/80.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/icons/ios/87.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Some files were not shown because too many files have changed in this diff Show More