{"id":19574150,"url":"https://github.com/0mppula/thesis-money-mapper","last_synced_at":"2025-04-27T05:33:47.144Z","repository":{"id":196345285,"uuid":"695795635","full_name":"0mppula/thesis-money-mapper","owner":"0mppula","description":"An extensively documentated Next.js fullstack finance tracker for organizing personal finances. Covers income, taxes, assets, debt, and net worth management. Utilizes next-auth for user authentication and stores user data in a MongoDB database with prisma ORM.","archived":false,"fork":false,"pushed_at":"2024-07-29T05:15:48.000Z","size":862,"stargazers_count":4,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-07-29T06:49:05.399Z","etag":null,"topics":["axios","data-visualization","date-fns","finance-application","finance-tracker","lucide-react","money-manager","mongodb","next-auth","next-themes","nextjs","prisma-orm","react-hook-form","reactjs","recharts","shadcn-ui","tailwindcss","typescript","zod","zustand"],"latest_commit_sha":null,"homepage":"https://thesismoneymapper.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/0mppula.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-09-24T08:50:06.000Z","updated_at":"2024-07-29T05:15:51.000Z","dependencies_parsed_at":"2023-10-02T11:29:46.682Z","dependency_job_id":"70ef51df-c090-48dd-87a4-b2efbfeec20e","html_url":"https://github.com/0mppula/thesis-money-mapper","commit_stats":null,"previous_names":["0mppula/thesis-money-mapper"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0mppula%2Fthesis-money-mapper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0mppula%2Fthesis-money-mapper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0mppula%2Fthesis-money-mapper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0mppula%2Fthesis-money-mapper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/0mppula","download_url":"https://codeload.github.com/0mppula/thesis-money-mapper/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224062332,"owners_count":17249274,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["axios","data-visualization","date-fns","finance-application","finance-tracker","lucide-react","money-manager","mongodb","next-auth","next-themes","nextjs","prisma-orm","react-hook-form","reactjs","recharts","shadcn-ui","tailwindcss","typescript","zod","zustand"],"created_at":"2024-11-11T06:38:38.845Z","updated_at":"2024-11-11T06:38:39.803Z","avatar_url":"https://github.com/0mppula.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Thesis Thesis Money Mapper\n\n## Table of Contents\n\n-   [What is Thesis Money Mapper?](#what-is-thesis-money-mapper)\n-   [The Dashboard page](#the-dashboard-page)\n-   [The Money page](#the-money-page)\n-   [The Login page](#the-login-page)\n-   [How to Set Up the Project Locally](#how-to-set-up-the-project-locally)\n    -   [Prerequisites](#prerequisites)\n    -   [Cloning the repository](#cloning-the-repository)\n    -   [Install packages](#install-packages)\n    -   [.env File Configuration](#env-file-configuration)\n    -   [Setup Prisma](#setup-prisma)\n    -   [Start the app](#start-the-app)\n-   [Available commands](#available-commands)\n-   [Tech Stack](#tech-stack)\n    -   [Framework](#framework)\n    -   [UI](#ui)\n    -   [State Managment](#state-managment)\n    -   [Backend \u0026 Authentication](#backend--authentication)\n-   [Creating the Project](#creating-the-project)\n    -   [Initializing the Project](#initializing-the-project)\n    -   [Cleaning Up](#cleaning-up)\n        -   [Css](#css)\n        -   [Tailwind Config](#tailwind-config)\n        -   [Landing Page](#landing-page)\n        -   [Root Layout](#root-layout)\n        -   [.env File](#env-file)\n    -   [Version Control](#version-control)\n    -   [Creating a MongoDB Database](#creating-a-mongodb-database)\n    -   [Connecting to the Database with Prisma](#connecting-to-the-database-with-prisma)\n        -   [Database Connection String](#database-connection-string)\n        -   [Insalling Prisma](#insalling-prisma)\n        -   [Initialize the Prisma Schema](#initialize-the-prisma-schema)\n        -   [Test the Database Connection](#test-the-database-connection)\n        -   [Prisma Models](#prisma-models)\n        -   [Users model](#users-model)\n        -   [Account model](#account-model)\n        -   [FinancialRecord model](#financialrecord-model)\n        -   [Install and generate Prisma Client](#install-and-generate-prisma-client)\n    -   [shadcn/ui](#shadcnui)\n        -   [Installation](#installation)\n        -   [Installing new components](#installing-new-components)\n    -   [Login Page](#login-page)\n        -   [page.tsx](#pagetsx)\n        -   [TypographyH1.tsx](#typographyh1tsx)\n        -   [AuthForm.tsx](#authformtsx)\n        -   [ButtonWithIcon.tsx](#buttonwithicontsx)\n        -   [RootLayout.tsx](#rootlayouttsx)\n    -   [Authentication](#authentication)\n        -   [Prisma Client Instantiation](#prisma-client-instantiation)\n        -   [Next-auth Installation](#next-auth-installation)\n        -   [Next-auth Configuration](#next-auth-configuration)\n        -   [Authentication API](#authentication-api)\n        -   [Google Provider](#google-provider)\n        -   [GitHub Provider](#github-provider)\n        -   [Adding Authentication Logic to the Client](#adding-authentication-logic-to-the-client)\n    -   [Navbar](#navbar)\n        -   [Nav.tsx](#navtsx)\n        -   [auth.ts](#authts)\n        -   [Theme Toggler](#theme-toggler)\n        -   [User Account Nav](#user-account-nav)\n\n---\n\n## What is Thesis Money Mapper?\n\n[Thesis Money Mapper][Thesis Money Mapper] is a Next.js fullstack finance tracker for organizing personal finances. Covers income, taxes, assets, debt, and net worth management. Utilizes next-auth for user authentication and stores user data in a MongoDB database with prisma ORM. The app uses shadcn-ui with tailwind css, providing a visually appealing and responsive user interface. Global client state is managed with zustand, ensuring efficient and streamlined data management across the app. Emphasizing user experience, the app offers both dark and light modes to suit individual preferences. To enhance data visualization, financial information is presented through interactive charts and comprehensive tables, allowing users to gain valuable insights at a glance.\n\nI developed this application as part of my university thesis at LAB University of Applied Sciences in Lahti. You can find my thesis [here][thesis link].\n\n## The Dashboard page\n\n![Thesis Money Mapper dashboad page](/public/images/dashboard.png)\n\n## The Money page\n\n![Thesis Money Mapper money page](/public/images/money.png)\n\n## The Login page\n\n![Thesis Money Mapper login page](/public/images/login.png)\n\n## How to Set Up the Project Locally\n\n### Prerequisites\n\n**Node version 14.x**\n\n### Cloning the repository\n\n```shell\ngit clone https://github.com/0mppula/money-mapper.git\n```\n\n### Install packages\n\n```shell\nnpm i\n```\n\n### `.env` File Configuration\n\nIn the root of the project create an `.env` file and declare the following variables:\n\n```js\nDATABASE_URL=\nNEXTAUTH_SECRET=\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\nGITHUB_CLIENT_ID=\nGITHUB_CLIENT_SECRET=\n```\n\nPopulate the variables with the corresponding data.\n\n### Setup Prisma\n\n```shell\nnpx prisma db push\n```\n\n### Start the app\n\n```shell\nnpm run dev\n```\n\n## Available commands\n\nRunning commands with npm `npm run [command]`\n\n| command | description                              |\n| :------ | :--------------------------------------- |\n| `dev`   | Starts a development instance of the app |\n\n---\n\n## Tech Stack\n\n### Framework\n\n-   **Front-end Framework:** Next.js (v13.4.12)\n\n### UI\n\n-   **UI Library:** shadcn-ui\n-   **UI Styling:** tailwindcss (v3.3.3) with tailwindcss-animate (v1.0.6)\n-   **Theming:** next-themes (v0.2.1)\n-   **Data Visualization:** recharts (v2.7.2)\n-   **Icons:** @radix-ui/react-icons (v1.3.0), react-icons (v4.10.1) \u0026 lucide-react (v0.263.1)\n-   **Date Picker:** react-day-picker (v8.8.0)\n-   **Date Manipulation:** date-fns (v2.30.0)\n-   **CSS Utility:** clsx (v2.0.0)\n-   **Class Variance Management:** class-variance-authority (v0.7.0)\n\n### State Managment\n\n-   **Global State Management:** zustand (v4.4.0)\n-   **Data Fetching and Management:** @tanstack/react-query (v4.32.6) and @tanstack/react-table (v8.9.3)\n-   **Form Handling:** react-hook-form (v7.45.2) with @hookform/resolvers (v3.1.1)\n-   **State Validation:** zod (v3.21.4)\n\n### Backend \u0026 Authentication\n\n-   **Prisma ORM:** @prisma/client (v5.1.0) with prisma (v5.1.0) as a dev dependency\n-   **User Authentication:** next-auth (v4.22.3)\n-   **API Requests:** axios (v1.4.0)\n-   **TypeScript:** (v5.1.6)\n-   **Type Definitions:** @types/node (v20.4.5), @types/react (v18.2.18), @types/react-dom (v18.2.7)\n\n## Creating the Project\n\n### Initializing the Project\n\nThe easiest way to create a Next.js app is by using `create-next-app` ([Next.js 2023](https://nextjs.org/docs/pages/api-reference/create-next-app)).\n\nOpen a new terminal window and run the command below. This command will ensure that you initialize your project using the latest version of Next.js.\n\n```shell\nnpx create-next-app@latest\n```\n\nAfter running this you will be asked the following prompts:\n\n1. Name your project\n2. Select \"Yes\" to use TypeScript\n3. Select \"Yes\" to use ESLint\n4. Select \"Yes\" to use Tailwind CSS\n5. Select \"No\" to omit the usage of the `src/` directory\n6. Select \"Yes\" to use the newer App Router of Next.js\n7. Optional but recommended select \"Yes\" to customize you default import alias\n\n```shell\nWhat is your project named?  thesis-money-mapper\nWould you like to use TypeScript?  No / Yes\nWould you like to use ESLint?  No / Yes\nWould you like to use Tailwind CSS?  No / Yes\nWould you like to use `src/` directory?  No / Yes\nWould you like to use App Router? (recommended)  No / Yes\nWould you like to customize the default import alias?  No / Yes\n```\n\nWhen you are done with the prompts the required dependencies will be installed and the project will get initialized. This process typically takes only a few seconds.\n\nNext `cd` into your project and run the following command `code .` to open the project in Visual Studio Code. Alternatively you can open the project using your operating systems GUI.\n\n---\n\n### Cleaning Up\n\nWhen setting up a Next.js project, the initial codebase will typically include some boilerplate code that is not necessary for your application.\n\n#### Css\n\nStart by deleting everything except the tailwind specific configuration code from `app/globals.css`.\n\n```css\n/* app/globals.css (keep these lines) */\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n```\n\n#### Tailwind Config\n\nNext modify the tailwind config in `tailwind.config.js` by assigning the `theme` key to an empty object and remove `'./pages/**/*.{js,ts,jsx,tsx,mdx}'` from the `content` array since the app router does not use a pages directory.\n\nYour config section should look something like this.\n\n```typescript\n// tailwind.config.js\nconst config: Config = {\n\tcontent: ['./components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}'],\n\ttheme: {},\n\tplugins: [],\n};\n```\n\n#### Landing Page\n\nIn `app/page.tsx` remove all the boilerplate `jsx` inside of the `main` tag and remove its className. Additionally, eliminate all unused import statements.\n\n```tsx\n// app/page.tsx\n\u003cmain\u003e\n\t\u003cp\u003eHello World!\u003c/p\u003e\n\u003c/main\u003e\n```\n\n#### Root Layout\n\nReplace the default metadata in the root layout of the project.\n\n```tsx\n// app/layout.tsx\nexport const metadata: Metadata = {\n\ttitle: 'Thesis Money Mapper',\n\tdescription: 'Take control of your finances with Thesis Money Mapper, ...',\n};\n```\n\n#### .env File\n\nIn the root of your project create an `.env` file for storing the projects environment variables. Finally, include an exception for the `.env` file in the `.gitignore` to prevent the accidental inclusion of sensitive data in your project's version control (git).\n\n```.gitignore\n# .gitignore\n.env\n```\n\n---\n\n### Version Control\n\nWhen working on web applications or any project, it's crucial to use a version control tool like Git. Git commits should be made in concise, modular steps to facilitate easy tracking and potential future changes. You can save your changes using the following commands:\n\n```shell\n# this is optional\ngit status\ngit add .\ngit commit -m \"Initialized application\"\n```\n\n1. `git status`: This command is optional but often used to check the current status of your Git repository. It provides information about which files have been modified and are ready to be committed (staged), which files are not yet tracked by Git, and other relevant information about the repository's state.\n\n2. `git add .`: This command stages all the changes in your working directory for the next commit. The . represents the current directory, so it stages all the changes in the current directory and its subdirectories. This step prepares your changes to be included in the upcoming commit.\n\n3. `git commit -m \"Initialized application\"`: This command creates a new commit with a descriptive message. The -m flag is used to specify a commit message enclosed in double quotes. The commit message should briefly describe the changes or the purpose of the commit. In this example, the commit message is \"Initialized application,\" which suggests that this commit marks the initial setup of the project.\n\nAlternatively, you can perform these steps using the Visual Studio Code's user interface (UI). This process should be carried out whenever you believe the project needs to be saved or when a feature is complete.\n\n---\n\n### Creating a MongoDB Database\n\nThis app uses a MongoDB database. To create a database login or create an account at [account.mongodb.com/account/login](https://account.mongodb.com/account/login).\n\nNext create a new project. After that, create a new database from your projects dashboard. While configuring the database create a new database user (this is used to connect with the db Prisma later).\n\n---\n\n### Connecting to the Database with Prisma\n\n#### Database Connection String\n\nIn the root of the project create a new `.env` file (make sure that you have an entry for it in the `.gitignore` file so you dont accidentally leak sensitive data from your app).\n\nIn your MongoDB dashboard select \"Connect\" to prompt a connect modal. From the UI select \"MongoDB for VS Code\" this will generate the correct `DATABASE_URL` for your `.env` file.\n\nCopy the connection string provided to you by MongoDB and at it to your `.env` file:\n\n```\nDATABASE_URL=mongodb+srv://\u003cUSERNAME\u003e:\u003cPASSWORD\u003e@\u003cHOST\u003e/\u003cDATABASE_NAME\u003e\n```\n\nModify the connection string with your own configurations from your database.\n\n#### Insalling Prisma\n\nThis app connects to the database using Prisma ORM. To start using Prisma add the Prisma CLI as a development dependency to your project: ([Prisma docs](https://www.prisma.io/docs/getting-started/setup-prisma/add-to-existing-project/mongodb-typescript-mongodb))\n\n```shell\nnpm install prisma --save-dev\n```\n\nYou can now invoke the Prisma CLI by prefixing it with npx:\n\n```shell\nnpx prisma\n```\n\nNext, set up your Prisma project by creating your Prisma schema file template with the following command:\n\n```shell\nnpx prisma init\n```\n\nThis command will auto generate a [schema.prisma file](prisma\\schema.prisma) in in `prisma\\schema.prisma`.\n\n#### Initialize the Prisma Schema\n\nBy default, the generated `schema.prisma` file will have its `provider` set to `postgresql`. Since the app uses MongoDB as its database, replace the providers value to `mongodb`:\n\n```\ndatasource db {\n  provider = \"mongodb\" \u003c--\n  url      = env(\"DATABASE_URL\")\n}\n```\n\nThe value for the `url` is fetched from your `.env` file.\n\n#### Test the Database Connection\n\nTo test out if a connection can be successfully made with the database using Prisma run the following command:\n\n```shell\nnpx prisma db pull\n```\n\nThis command simply introspects the MongoDB database and writes its inferred schema into the `prisma/schema.prisma`. However, in this case, this should fail because the database has yet to contain any collections. If the only error you get is an introspection error, the connection was successfully made.\n\n#### Prisma Models\n\nThis app has user authentication and has the ability to record users financial data. Users and their financial records are saved to the database, so their respective Prisma models must be defined.\n\nThese models auto-generate types for our application based on the models structure. Additionally, the `User` and `Account` models include fields for `next-auth`'s Prisma adapter which will be used when authenticating users later on.\n\n#### Users model\n\n```prisma\nmodel User {\n  id             String    @id @default(auto()) @map(\"_id\") @db.ObjectId\n  name           String\n  email          String    @unique\n  emailVerified  DateTime?\n  image          String?\n  hashedPassword String?\n  createdAt      DateTime  @default(now())\n  updatedAt      DateTime  @updatedAt\n\n  accounts         Account[]\n  FinancialRecords FinancialRecord[]\n}\n```\n\n#### Account model\n\n```prisma\nmodel Account {\n  id                String  @id @default(auto()) @map(\"_id\") @db.ObjectId\n  userId            String  @db.ObjectId\n  type              String\n  provider          String\n  providerAccountId String\n  refresh_token     String? @db.String\n  access_token      String? @db.String\n  expires_at        Int?\n  token_type        String?\n  scope             String?\n  id_token          String? @db.String\n  session_state     String?\n\n  user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n  @@unique([provider, providerAccountId])\n}\n```\n\n#### FinancialRecord model\n\n```prisma\nmodel FinancialRecord {\n  id             String   @id @default(auto()) @map(\"_id\") @db.ObjectId\n  userId         String   @db.ObjectId\n  date           DateTime\n  currency       String\n  grossIncomeYtd Float\n  taxesPaidYtd   Float\n  assetsExCash   Float\n  cash           Float\n  debt           Float\n  createdAt      DateTime @default(now())\n  updatedAt      DateTime @updatedAt\n\n  User User @relation(fields: [userId], references: [id], onDelete: Cascade)\n}\n```\n\n#### Install and generate Prisma Client\n\nPrisma Client is an auto-generated and type-safe query builder that's tailored to your data. To get started with Prisma Client, you need to install the `@prisma/client` package:\n\n```shell\nnpm install @prisma/client\n```\n\nWhenever you make changes to your Prisma schema in the future, you manually need to invoke prisma generate in order to accommodate the changes in your Prisma Client API:\n\n```shell\nnpx prisma generate\n```\n\nLastly, you can push your Prisma schema to MongoDB with the following command:\n\n```shell\nnpx prisma db push\n```\n\nNow your MongoDB database should have the collections you defined in your Prisma schema file and connecting to the database is possible.\n\nIf you encouter any problems with the last step please that ensure your `.env` file has the correct connection string and that your IP address is allowed to connect to the database (you might have a dynamic IP address so allowing access from anywhere might me needed in development).\n\n---\n\n### shadcn/ui\n\n[shadcn/ui](https://ui.shadcn.com/) provides accessible and customizable components that you can copy and paste into your apps. It's free \u0026 open source.\n\nThis project will use shadcn/ui for its UI and styling is done with Tailwind CSS.\n\n#### Installation\n\nTo start using shadcn/ui run the shadcn-ui init command to setup your project:\n\n```shell\nnpx shadcn-ui@latest init\n```\n\nYou will be asked a few questions to configure `components.json`. Below is the selected answers for this project, feel free to change some of them.\n\n```shell\nWould you like to use TypeScript (recommended)? yes\nWhich style would you like to use? › New York\nWhich color would you like to use as base color? › Slate\nWhere is your global CSS file? › app/globals.css\nDo you want to use CSS variables for colors? › yes\nWhere is your tailwind.config.js located? › tailwind.config.js\nConfigure the import alias for components: › @/components\nConfigure the import alias for utils: › @/lib/utils\nAre you using React Server Components? › yes\n```\n\n#### Installing new components\n\nVarious shadcn/ui components are used throughout the app. Each component along with its dependencies needs to be installed seperately. Some of the components used are: `toast`, `button`, and `card` you can install them with the following commands:\n\n```shell\nnpx shadcn-ui@latest add toast\nnpx shadcn-ui@latest add button\nnpx shadcn-ui@latest add card\n```\n\nIts easier to install components as you find the need for them with a single command. Check out the shadcn/ui [documentation](https://ui.shadcn.com/docs/components/) on how to install each component.\n\n---\n\n### Login Page\n\nThe login page of the app is the landing page (pathname `/`), so it needs to be created in the root `page.tsx` file.\n\nFor oragnizational purposes you can create a new `(site)` directory in the root of the project and place all the landing page specific files in it like its `page.tsx` file. This will keep the original functionality of the root `page.tsx` but structures the project differently ([route groups](https://nextjs.org/docs/app/building-your-application/routing/colocation#route-groups) are completely optional in Next.js).\n\nIn `app/(site)/page.tsx` create the login page:\n\n#### page.tsx\n\nThis is the login page with a wrapper styled with Tailwind CSS. It has a header and the actual login form component for user authentication.\n\n```tsx\nimport { TypographyH1 } from '@/components/TypographyH1';\nimport AuthForm from './components/AuthForm';\n\nexport default function Home() {\n\treturn (\n\t\t\u003cdiv className=\"container flex flex-col items-center justify-center pb-[117px] py-12 min-h-[calc(100vh-69px)]\"\u003e\n\t\t\t\u003cTypographyH1 center\u003eSign in to your account\u003c/TypographyH1\u003e\n\n\t\t\t\u003cAuthForm /\u003e\n\t\t\u003c/div\u003e\n\t);\n}\n```\n\nThe login page uses the `TypographyH1` and `AuthForm` components.\n\n#### TypographyH1.tsx\n\nThis is a reusable `h1` component that is used whenever an `h1` tag needs to be rendered on a page. Additionally, it can be centered with a prop.\n\n```tsx\nimport { cn } from '@/lib/utils';\n\ninterface TypographyH1Props {\n\tchildren: React.ReactNode;\n\tcenter?: boolean;\n}\n\nexport function TypographyH1({ children, center }: TypographyH1Props) {\n\treturn (\n\t\t\u003ch1\n\t\t\tclassName={cn(\n\t\t\t\t'scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl underline decoration-lime-800 dark:decoration-lime-400',\n\t\t\t\tcenter \u0026\u0026 'text-center'\n\t\t\t)}\n\t\t\u003e\n\t\t\t{children}\n\t\t\u003c/h1\u003e\n\t);\n}\n```\n\n#### AuthForm.tsx\n\nThis component is used to authenticate users. Since user authentication is yet to be implemented all the `next-auth` logic is commented out for now.\n\n```tsx\n'use client';\n\nimport { ButtonWithIcon } from '@/components/ButtonWithIcon';\nimport { useToast } from '@/components/ui/use-toast';\n// import { signIn } from 'next-auth/react';\nimport { useState } from 'react';\nimport { FaGithub, FaGoogle } from 'react-icons/fa';\n\nconst AuthForm = () =\u003e {\n\tconst [googleIsLoading, setGoogleIsLoading] = useState(false);\n\tconst [githubIsLoading, setGithubIsLoading] = useState(false);\n\n\tconst { toast } = useToast();\n\n\tconst socialAction = async (provider: string) =\u003e {\n\t\tif (provider === 'google') {\n\t\t\tsetGoogleIsLoading(true);\n\t\t}\n\n\t\tif (provider === 'github') {\n\t\t\tsetGithubIsLoading(true);\n\t\t}\n\n\t\t// await signIn(provider, { callbackUrl: '/money' }).then((callback) =\u003e {\n\t\t// \tif (callback?.error) {\n\t\t// \t\ttoast({\n\t\t// \t\t\tdescription: 'Invalid credentials. Please try again.',\n\t\t// \t\t});\n\t\t// \t}\n\t\t// });\n\t};\n\n\treturn (\n\t\t\u003cform\n\t\t\tclassName=\"gap-4 flex flex-col mt-4 lg:mt-8 px-4 py-6 sm:px-10 w-full sm:max-w-lg max-w-md rounded-xl border bg-card text-card-foreground shadow\"\n\t\t\tonSubmit={(e) =\u003e e.preventDefault()}\n\t\t\u003e\n\t\t\t\u003cButtonWithIcon\n\t\t\t\tloading={googleIsLoading}\n\t\t\t\tdisabled={googleIsLoading || githubIsLoading}\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"w-full\"\n\t\t\t\ticon={FaGoogle}\n\t\t\t\tonClick={() =\u003e socialAction('google')}\n\t\t\t\u003e\n\t\t\t\tContinue with Google\n\t\t\t\u003c/ButtonWithIcon\u003e\n\n\t\t\t\u003cButtonWithIcon\n\t\t\t\tloading={githubIsLoading}\n\t\t\t\tdisabled={googleIsLoading || githubIsLoading}\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"w-full\"\n\t\t\t\ticon={FaGithub}\n\t\t\t\tonClick={() =\u003e socialAction('github')}\n\t\t\t\u003e\n\t\t\t\tContinue with Github\n\t\t\t\u003c/ButtonWithIcon\u003e\n\t\t\u003c/form\u003e\n\t);\n};\n\nexport default AuthForm;\n```\n\n#### ButtonWithIcon.tsx\n\nThis component renders a shadcn/ui `button` element with either a loader or another `react-icons` icon and is used throughout the app.\n\nThis component needs the `react-icons` npm package install it with:\n\n```shell\nnpm install react-icons\n```\n\n```tsx\nimport { Button, ButtonProps } from '@/components/ui/button';\nimport React from 'react';\nimport { IconType } from 'react-icons';\nimport { ImSpinner8 } from 'react-icons/im';\n\ninterface ButtonWithIconProps extends ButtonProps {\n\tchildren?: React.ReactNode;\n\ticon?: IconType;\n\tloading?: boolean;\n}\n\nexport function ButtonWithIcon({ icon: Icon, loading, children, ...props }: ButtonWithIconProps) {\n\treturn (\n\t\t\u003cButton {...props} disabled={loading || props.disabled}\u003e\n\t\t\t{children}{' '}\n\t\t\t{loading ? (\n\t\t\t\t\u003cImSpinner8 className=\"ml-2 h-4 w-4 animate-spin\" /\u003e\n\t\t\t) : Icon ? (\n\t\t\t\t\u003cIcon className=\"ml-2 h-4 w-4\" /\u003e\n\t\t\t) : (\n\t\t\t\t\u003c\u003e\u003c/\u003e\n\t\t\t)}\n\t\t\u003c/Button\u003e\n\t);\n}\n```\n\n#### RootLayout.tsx\n\nLastly, update the `RootLayout.tsx` to have some Tailwind CSS classes. Optionally, create a helper function for genereating the title for you app and store the description in a different file for easier use in other sections of the app:\n\n```tsx\nimport { mainAppDescription } from '@/constants';\nimport { cn } from '@/lib/utils';\nimport createAppTitle from '@/utils/createAppTitle';\nimport type { Metadata } from 'next';\nimport { Inter } from 'next/font/google';\n\nimport './globals.css';\nconst inter = Inter({ subsets: ['latin'] });\n\nexport const metadata: Metadata = {\n\ttitle: createAppTitle('Sign in'),\n\tdescription: mainAppDescription,\n};\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n\treturn (\n\t\t\u003chtml className={cn(inter.className, 'antialiased')} lang=\"en\" suppressHydrationWarning\u003e\n\t\t\t\u003cbody className=\"min-h-screen text-slate-900 dark:text-slate-50 bg-slate-100 dark:bg-slate-950 antialiased pt-[68px] pb-16\"\u003e\n\t\t\t\t{children}\n\t\t\t\u003c/body\u003e\n\t\t\u003c/html\u003e\n\t);\n}\n```\n\n---\n\n### Authentication\n\nSaving user data on authentication requires quering the database.\n\n#### Prisma Client Instantiation\n\nIn order to communicate with the database a `Prisma Client` needs to be instantiated. Create a `db.ts` file in the `lib` directory.\n\nThis code ensures that only a single client is instantiated (singelton).\n\n```ts\nimport { PrismaClient } from '@prisma/client';\n\ndeclare global {\n\tvar prisma: PrismaClient | undefined;\n}\n\nconst client = globalThis.prisma || new PrismaClient();\n\nif (process.env.NODE_ENV === 'development') {\n\tglobalThis.prisma = client;\n}\n\nexport default client;\n```\n\n#### Next-auth Installation\n\nNextAuth.js is a complete open-source authentication solution for Next.js applications.\n\nTo setup user authentication with `next-auth` install it with npm:\n\n```shell\nnpm install next-auth\n```\n\nAdditionally, to save user data a Prisma adapters is needed, which can be installed with:\n\n```shell\nnpm install @prisma/client @auth/prisma-adapter\nnpm install prisma --save-dev\n```\n\n#### Next-auth Configuration\n\nTo make the authenticated user session and the `JWT` from `next-auth` type safe they need to be typed. create a new `types` directory in the root of the project with a `next-auth.d.ts` file in it.\n\nDeclare a new `JWT` module and add an `id` field with a type of `string` to it. Also declare a `Session` module that extends the User type with an the same `id` field.\n\n```ts\n/* eslint-disable no-unused-vars */\nimport type { Session, User } from 'next-auth';\nimport type { JWT } from 'next-auth/jwt';\n\ntype UserId = string;\n\ndeclare module 'next-auth/jwt' {\n\tinterface JWT {\n\t\tid: UserId;\n\t}\n}\n\ndeclare module 'next-auth' {\n\tinterface Session {\n\t\tuser: User \u0026 {\n\t\t\tid: UserId;\n\t\t};\n\t}\n}\n```\n\nNow the user session and the JWT is type safe for authentication purposes.\n\n#### Authentication API\n\nUser authentication needs its own api route since it makes API calls to the database. Create a new `api/auth/[...nextauth]` directory in the `app` directory and create a `route.ts` file in it:\n\n```ts\nimport NextAuth, { AuthOptions } from 'next-auth';\nimport GoogleProvider from 'next-auth/providers/google';\nimport GitHubProvider from 'next-auth/providers/github';\nimport { PrismaAdapter } from '@auth/prisma-adapter';\nimport db from '@/lib/db';\n\nexport const authOptions: AuthOptions = {\n\tadapter: PrismaAdapter(db),\n\tproviders: [\n\t\tGoogleProvider({\n\t\t\tclientId: process.env.GOOGLE_CLIENT_ID as string,\n\t\t\tclientSecret: process.env.GOOGLE_CLIENT_SECRET as string,\n\t\t}),\n\t\tGitHubProvider({\n\t\t\tclientId: process.env.GITHUB_CLIENT_ID as string,\n\t\t\tclientSecret: process.env.GITHUB_CLIENT_SECRET as string,\n\t\t}),\n\t],\n\tdebug: process.env.NODE_ENV === 'development',\n\tsession: {\n\t\tstrategy: 'jwt',\n\t},\n\tcallbacks: {\n\t\tjwt: async ({ token }) =\u003e {\n\t\t\tconst db_user = await db.user.findFirst({\n\t\t\t\twhere: {\n\t\t\t\t\temail: token?.email as string,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tif (db_user) {\n\t\t\t\ttoken.id = db_user.id;\n\t\t\t}\n\n\t\t\treturn token;\n\t\t},\n\t\tasync session({ token, session }) {\n\t\t\tif (token) {\n\t\t\t\tsession.user.id = token.id;\n\t\t\t\tsession.user.name = token.name;\n\t\t\t\tsession.user.email = token.email;\n\t\t\t\tsession.user.image = token.picture;\n\t\t\t}\n\n\t\t\treturn session;\n\t\t},\n\t},\n\tpages: {\n\t\tsignIn: '/',\n\t},\n\tsecret: process.env.NEXTAUTH_SECRET as string,\n};\n\nconst handler = NextAuth(authOptions);\n\nexport { handler as GET, handler as POST };\n```\n\nExplain the file:\n\n-   `adapter`: By default NextAuth.js does not include an adapter. If you would like to persist user / account data, please install one of the many available adapters. More information can be found in the adapter [documentation](https://authjs.dev/reference/adapters).\n-   `providers`: An array of authentication providers for signing in (e.g. Google, GitHub, etc) in any order. See the providers [documentation](https://next-auth.js.org/configuration/providers/oauth) for a list of supported providers and how to use them.\n\n-   `debug`: Set debug to true to enable debug messages for authentication and database operations.\n-   `session`: The session object and all properties on it are optional. The default `strategy` is `\"jwt\"`, an encrypted JWT (JWE) stored in the session cookie.\n-   `callbacks`: Callbacks are asynchronous functions you can use to control what happens when an action is performed. Callbacks are extremely powerful, especially in scenarios involving JSON Web Tokens as they allow you to implement access controls without a database and to integrate with external databases or APIs. In this implementation the callback function checks if an authenticated user exists on the database and adds its id to the token.\n-   `pages`: Specify URLs to be used if you want to create custom sign in, sign out and error pages. Pages specified will override the corresponding built-in page.\n-   `secret`: The default value is a string (SHA hash of the \"options\" object) in development, no default in production. In production this is required.\n\nAlso add a new `NEXTAUTH_SECRET` entry in the `.env`. This can be a random string.\n\n#### Google Provider\n\nTo start using a Google provider you need to add entries for `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in the `.env` file.\n\nYou can get the values for these by logging into your [Google Cloud](https://console.cloud.google.com/) and creating a new project.\n\nThen open the project and navigate to the \"OAuth consent screen\" page and create an OAuth consent screen.\n\nAfterwards, create an OAuth client ID from the \"credentials\" page. Set the `Authorized JavaScript origins` to \"http://localhost:3000 and the `Authorized redirect URIs` to \"http://localhost:3000/api/auth/callback/google/\". These values must be changed to your deployment domain when in production.\n\nCopy the `Client ID` and `Client secret` values provided to you by Google, and add them to the `.env` file.\n\n```.env\nGOOGLE_CLIENT_ID=\u003cGOOGLE_CLIENT_ID\u003e\nGOOGLE_CLIENT_SECRET=\u003cGOOGLE_CLIENT_SECRET\u003e\n```\n\n#### GitHub Provider\n\nTo start using a GitHub provider you need to add entries for `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` in the `.env` file.\n\nYou can get the values for these by logging into your GitHub account and navigating to [settings/developers](https://github.com/settings/developers) and creating a new OAuth App from the \"New OAuth App\" button.\n\nSet the `Homepage URL` to \"http://localhost:3000\" and the `Authorization callback URL` to \"http://localhost:3000\". These values must be changed to your deployment domain when in production.\n\nCopy the `Client ID` and `Client secret` values provided to you by GitHub, and add them to the `.env` file.\n\n```.env\nGITHUB_CLIENT_ID=\u003cGITHUB_CLIENT_ID\u003e\nGITHUB_CLIENT_SECRET=\u003cGITHUB_CLIENT_SECRET\u003e\n```\n\n#### Adding Authentication Logic to the Client\n\nIn the `AuthForm` component uncomment the previously commented logic:\n\n```tsx\nimport { signIn } from 'next-auth/react';\n...\n\nawait signIn(provider, { callbackUrl: '/money' }).then((callback) =\u003e {\n\tif (callback?.error) {\n\t\ttoast({\n\t\t\tdescription: 'Invalid credentials. Please try again.',\n\t\t});\n\t}\n});\n...\n```\n\nLastly, to have access to the authenticated users session on the client it needs to be wrapped in a `SessionProvider` component. Additionally to have access to dark / light modes later on, install the `next-themes` package with npm and wrap the `SessionProvider` with a `ThemeProvider` component.\n\n```shell\nnpm i next-themes\n```\n\n**components/providers/NextSessionProvider.tsx**\n\n```tsx\n'use client';\n\nimport { SessionProvider } from 'next-auth/react';\nimport { ThemeProvider } from 'next-themes';\nimport React from 'react';\n\ninterface NextSessionProviderProps {\n\tchildren: React.ReactNode;\n}\n\nconst NextSessionProvider = ({ children }: NextSessionProviderProps) =\u003e {\n\treturn (\n\t\t\u003cThemeProvider attribute=\"class\" defaultTheme=\"system\" enableSystem\u003e\n\t\t\t\u003cSessionProvider\u003e{children}\u003c/SessionProvider\u003e\n\t\t\u003c/ThemeProvider\u003e\n\t);\n};\n\nexport default NextSessionProvider;\n```\n\n**app/layout.tsx**\n\n```tsx\n\u003cbody\u003e\n\t\u003cNextSessionProvider\u003e{children}\u003c/NextSessionProvider\u003e\n\u003c/body\u003e\n```\n\nNow the authentication is configured for the client and you can try logging in!\n\n---\n\n### Navbar\n\nNext its time to add the `Nav` component to the app. In `components/Nav/Nav.tsx` create `Nav.tsx`.\n\nThis component renders `ThemeToggle` and `UserAccountNav` components that will be created later on.\n\nAdditionally, it uses a server action `getAuthSession` to determine if a user is authenticated which needs created.\n\n#### Nav.tsx\n\n```tsx\nimport { getAuthSession } from '@/app/actions/auth';\nimport Link from 'next/link';\nimport ThemeToggle from './ThemeToggle';\nimport UserAccountNav from './UserAccountNav';\nimport { buttonVariants } from '../ui/button';\n\nconst Nav = async () =\u003e {\n\tconst session = await getAuthSession();\n\n\treturn (\n\t\t\u003cdiv className=\"py-4 fixed inset-x-0 top-0 bg-white/75 dark:bg-slate-950/75 z-[50] h-fit border-b-2 border-slate-200 dark:border-slate-800 backdrop-blur-sm\"\u003e\n\t\t\t\u003cdiv className=\"flex items-center justify-between h-full gap-2 px-4 sm:px-8 mx-auto max-w-7xl\"\u003e\n\t\t\t\t{/* Logo */}\n\t\t\t\t\u003cLink href=\"/\" className=\"flex items-center gap-2\"\u003e\n\t\t\t\t\t\u003cp className=\"font-bold\"\u003eThesis Money Mapper\u003c/p\u003e\n\t\t\t\t\u003c/Link\u003e\n\n\t\t\t\t\u003cdiv className=\"flex lg:gap-8 gap-4\"\u003e\n\t\t\t\t\t{session?.user \u0026\u0026 (\n\t\t\t\t\t\t\u003cdiv className=\"md:flex gap-4 hidden\"\u003e\n\t\t\t\t\t\t\t\u003cLink className={buttonVariants({ variant: 'outline' })} href=\"/money\"\u003e\n\t\t\t\t\t\t\t\tMoney\n\t\t\t\t\t\t\t\u003c/Link\u003e\n\t\t\t\t\t\t\t\u003cLink\n\t\t\t\t\t\t\t\tclassName={buttonVariants({ variant: 'outline' })}\n\t\t\t\t\t\t\t\thref=\"/dashboard\"\n\t\t\t\t\t\t\t\u003e\n\t\t\t\t\t\t\t\tDashboard\n\t\t\t\t\t\t\t\u003c/Link\u003e\n\t\t\t\t\t\t\u003c/div\u003e\n\t\t\t\t\t)}\n\n\t\t\t\t\t\u003cdiv className=\"flex items-center gap-2 lg:gap-4\"\u003e\n\t\t\t\t\t\t\u003cThemeToggle /\u003e\n\t\t\t\t\t\t{session?.user \u0026\u0026 \u003cUserAccountNav user={session.user} /\u003e}\n\t\t\t\t\t\u003c/div\u003e\n\t\t\t\t\u003c/div\u003e\n\t\t\t\u003c/div\u003e\n\t\t\u003c/div\u003e\n\t);\n};\n\nexport default Nav;\n```\n\n#### auth.ts\n\n```ts\nimport { authOptions } from '@/app/api/auth/[...nextauth]/route';\nimport { getServerSession } from 'next-auth';\n\nexport const getAuthSession = () =\u003e {\n\treturn getServerSession(authOptions);\n};\n```\n\nLastly, add the `Nav` component to `RootLayout` so that it gets rendered on every page.\n\n```jsx\n\u003cNextSessionProvider\u003e\n\t\u003cNav /\u003e\n\n\t{children}\n\u003c/NextSessionProvider\u003e\n```\n\n#### Theme Toggler\n\nIn order to rendrer the navbar create a `ThemeToggler` component for it.\n\n```jsx\n'use client';\n\nimport * as React from 'react';\nimport { Laptop, Moon, Sun } from 'lucide-react';\nimport { useTheme } from 'next-themes';\n\nimport { Button } from '@/components/ui/button';\nimport {\n\tDropdownMenu,\n\tDropdownMenuContent,\n\tDropdownMenuItem,\n\tDropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\n\nconst ThemeToggler = () =\u003e {\n\tconst { setTheme } = useTheme();\n\n\treturn (\n\t\t\u003cDropdownMenu\u003e\n\t\t\t\u003cDropdownMenuTrigger asChild\u003e\n\t\t\t\t\u003cButton variant=\"ghost\" size=\"icon\" suppressHydrationWarning\u003e\n\t\t\t\t\t\u003cSun className=\"h-[1.5rem] w-[1.5rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0\" /\u003e\n\t\t\t\t\t\u003cMoon className=\"absolute h-[1.5rem] w-[1.5rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100\" /\u003e\n\t\t\t\t\t\u003cspan className=\"sr-only\"\u003eToggle theme\u003c/span\u003e\n\t\t\t\t\u003c/Button\u003e\n\t\t\t\u003c/DropdownMenuTrigger\u003e\n\n\t\t\t\u003cDropdownMenuContent align=\"end\"\u003e\n\t\t\t\t\u003cDropdownMenuItem onClick={() =\u003e setTheme('light')}\u003e\n\t\t\t\t\t\u003cSun className=\"mr-2 h-[1.125rem] w-[1.125rem]\" /\u003e Light\n\t\t\t\t\u003c/DropdownMenuItem\u003e\n\t\t\t\t\u003cDropdownMenuItem onClick={() =\u003e setTheme('dark')}\u003e\n\t\t\t\t\t\u003cMoon className=\"mr-2 h-[1.125rem] w-[1.125rem]\" /\u003e Dark\n\t\t\t\t\u003c/DropdownMenuItem\u003e\n\t\t\t\t\u003cDropdownMenuItem onClick={() =\u003e setTheme('system')}\u003e\n\t\t\t\t\t\u003cLaptop className=\"mr-2 h-[1.125rem] w-[1.125rem]\" /\u003e System\n\t\t\t\t\u003c/DropdownMenuItem\u003e\n\t\t\t\u003c/DropdownMenuContent\u003e\n\t\t\u003c/DropdownMenu\u003e\n\t);\n};\n\nexport default ThemeToggler;\n```\n\n#### User Account Nav\n\nIn order to rendrer the navbar create a `UserAccountNav` component for it.\n\n```jsx\n'use client';\n\nimport { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';\nimport {\n\tDropdownMenu,\n\tDropdownMenuContent,\n\tDropdownMenuItem,\n\tDropdownMenuLabel,\n\tDropdownMenuSeparator,\n\tDropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport { LogOut } from 'lucide-react';\nimport type { User } from 'next-auth';\nimport { signOut } from 'next-auth/react';\nimport Link from 'next/link';\nimport { Button } from '../ui/button';\n\ninterface UserAccountNavProps {\n\tuser: Pick\u003cUser, 'name' | 'image' | 'email'\u003e;\n}\n\nconst UserAccountNav = ({ user }: UserAccountNavProps) =\u003e {\n\treturn (\n\t\t\u003cDropdownMenu\u003e\n\t\t\t\u003cDropdownMenuTrigger asChild\u003e\n\t\t\t\t\u003cButton variant=\"ghost\" size=\"icon\" suppressHydrationWarning\u003e\n\t\t\t\t\t\u003cAvatar className=\"h-[1.625rem] w-[1.625rem]\"\u003e\n\t\t\t\t\t\t\u003cAvatarImage src={user?.image ? user.image : './images/placeholder'} /\u003e\n\n\t\t\t\t\t\t\u003cAvatarFallback\u003e\n\t\t\t\t\t\t\t\u003cspan className=\"sr-only \"\u003e{user?.name}\u003c/span\u003e\n\t\t\t\t\t\t\u003c/AvatarFallback\u003e\n\t\t\t\t\t\u003c/Avatar\u003e\n\t\t\t\t\u003c/Button\u003e\n\t\t\t\u003c/DropdownMenuTrigger\u003e\n\n\t\t\t\u003cDropdownMenuContent align=\"end\"\u003e\n\t\t\t\t\u003cDropdownMenuLabel\u003e\n\t\t\t\t\t{user.name \u0026\u0026 \u003cp className=\"font-normal\"\u003e{user.name}\u003c/p\u003e}\n\t\t\t\t\u003c/DropdownMenuLabel\u003e\n\n\t\t\t\t\u003cDropdownMenuLabel\u003e\n\t\t\t\t\t{user.email \u0026\u0026 (\n\t\t\t\t\t\t\u003cp className=\"w-[200px] truncate font-normal text-slate-500 dark:text-slate-400\"\u003e\n\t\t\t\t\t\t\t{user.email}\n\t\t\t\t\t\t\u003c/p\u003e\n\t\t\t\t\t)}\n\t\t\t\t\u003c/DropdownMenuLabel\u003e\n\n\t\t\t\t\u003cDropdownMenuSeparator /\u003e\n\n\t\t\t\t\u003cDropdownMenuItem className=\"md:hidden\" asChild\u003e\n\t\t\t\t\t\u003cLink href=\"/money\"\u003eMoney\u003c/Link\u003e\n\t\t\t\t\u003c/DropdownMenuItem\u003e\n\n\t\t\t\t\u003cDropdownMenuItem className=\"md:hidden\" asChild\u003e\n\t\t\t\t\t\u003cLink href=\"/dashboard\"\u003eDashboard\u003c/Link\u003e\n\t\t\t\t\u003c/DropdownMenuItem\u003e\n\n\t\t\t\t\u003cDropdownMenuSeparator /\u003e\n\t\t\t\t\u003cDropdownMenuItem\n\t\t\t\t\tclassName=\"focus:bg-destructive/25\"\n\t\t\t\t\tonClick={() =\u003e signOut({ callbackUrl: '/' })}\n\t\t\t\t\u003e\n\t\t\t\t\t\u003cLogOut className=\"mr-2 h-[1.125rem] w-[1.125rem]\" /\u003e Sign out\n\t\t\t\t\u003c/DropdownMenuItem\u003e\n\t\t\t\u003c/DropdownMenuContent\u003e\n\t\t\u003c/DropdownMenu\u003e\n\t);\n};\n\nexport default UserAccountNav;\n```\n\n---\n\n[Thesis Money Mapper]: https://thesismoneymapper.vercel.app/\n[thesis link]: https://www.theseus.fi/handle/10024/850318\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F0mppula%2Fthesis-money-mapper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F0mppula%2Fthesis-money-mapper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F0mppula%2Fthesis-money-mapper/lists"}