[init] Open source from splitpro.
57
.dockerignore
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
|
||||||
|
|
||||||
|
[](https://star-history.com/#oss-apps/split-pro&Date)
|
||||||
17
components.json
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
8
postcss.config.cjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
10
prettier.config.js
Normal 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;
|
||||||
208
prisma/migrations/20240224010302_init/migration.sql
Normal 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;
|
||||||
@ -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;
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "PushNotification" (
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"subscription" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "PushNotification_pkey" PRIMARY KEY ("userId")
|
||||||
|
);
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Balance" ADD COLUMN "importedFromSplitwise" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Group" ADD COLUMN "splitwiseGroupId" TEXT;
|
||||||
@ -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");
|
||||||
@ -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;
|
||||||
3
prisma/migrations/migration_lock.toml
Normal 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
@ -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
@ -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
|
After Width: | Height: | Size: 77 KiB |
1
public/add_expense.svg
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
1
public/empty_img.svg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
public/group.svg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/hero.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
public/icons/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
public/icons/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/icons/android/android-launchericon-144-144.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/icons/android/android-launchericon-192-192.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
public/icons/android/android-launchericon-48-48.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/icons/android/android-launchericon-512-512.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
public/icons/android/android-launchericon-72-72.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/icons/android/android-launchericon-96-96.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
public/icons/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 592 B |
BIN
public/icons/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
452
public/icons/icons.json
Normal 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
|
After Width: | Height: | Size: 3.6 KiB |
BIN
public/icons/ios/1024.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
public/icons/ios/114.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
public/icons/ios/120.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
public/icons/ios/128.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/icons/ios/144.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/icons/ios/152.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/icons/ios/16.png
Normal file
|
After Width: | Height: | Size: 579 B |
BIN
public/icons/ios/167.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
public/icons/ios/180.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
public/icons/ios/192.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
public/icons/ios/20.png
Normal file
|
After Width: | Height: | Size: 677 B |
BIN
public/icons/ios/256.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
public/icons/ios/29.png
Normal file
|
After Width: | Height: | Size: 1000 B |
BIN
public/icons/ios/32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/icons/ios/40.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icons/ios/50.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/icons/ios/512.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
public/icons/ios/57.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/icons/ios/58.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/icons/ios/60.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/icons/ios/64.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/icons/ios/72.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/icons/ios/76.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/icons/ios/80.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/icons/ios/87.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
public/icons/windows11/LargeTile.scale-100.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/icons/windows11/LargeTile.scale-125.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/icons/windows11/LargeTile.scale-150.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/icons/windows11/LargeTile.scale-200.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/icons/windows11/LargeTile.scale-400.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
public/icons/windows11/SmallTile.scale-100.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/icons/windows11/SmallTile.scale-125.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/icons/windows11/SmallTile.scale-150.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/icons/windows11/SmallTile.scale-200.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/icons/windows11/SmallTile.scale-400.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
public/icons/windows11/SplashScreen.scale-100.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/icons/windows11/SplashScreen.scale-125.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/icons/windows11/SplashScreen.scale-150.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/icons/windows11/SplashScreen.scale-200.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
public/icons/windows11/SplashScreen.scale-400.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
public/icons/windows11/Square150x150Logo.scale-100.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/icons/windows11/Square150x150Logo.scale-125.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
public/icons/windows11/Square150x150Logo.scale-150.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
public/icons/windows11/Square150x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
public/icons/windows11/Square150x150Logo.scale-400.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 579 B |
|
After Width: | Height: | Size: 677 B |
|
After Width: | Height: | Size: 819 B |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 1015 B |
|
After Width: | Height: | Size: 1.1 KiB |