diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..94a9a76 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,7 @@ +# Example .env.local file for MySQL Database credentials + +MYSQL_HOST=localhost +MYSQL_DATABASE=mammon_manager +MYSQL_USERNAME=mm_admin +MYSQL_PASSWORD=god_not_mammon +MYSQL_PORT=3306 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..1437c53 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..fe4879d --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,33 @@ +module.exports = { + env: { + browser: true, + es2021: true, + }, + extends: [ + 'plugin:react/recommended', + 'airbnb-typescript', + 'prettier', + 'next', + ], + // parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 12, + sourceType: 'module', + }, + plugins: [ + 'react', + '@typescript-eslint', + ], + rules: { + 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx', '.ts', '.tsx'] }], + 'jsx-a11y/label-has-associated-control': ['error', { + required: { + some: ['nesting', 'id'], + }, + }], + }, +}; diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..52c3c67 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ dev ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ dev ] + schedule: + - cron: '16 0 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..36af219 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1437c53 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..1696f9f --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,6 @@ +module.exports = { + singleQuote: true, + tabWidth: 2, + semi: true, + trailingComma: 'es5', +}; diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..eacb132 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.formatOnPaste": true, + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/README.md b/README.md index d8d75d5..997c516 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ -# MySQL Example +# Mammon Manager -This is an example of using [MySQL](https://www.mysql.com/) in a Next.js project. - -## Demo - -### [https://next-mysql.vercel.app](https://next-mysql.vercel.app/) +Manage your mammon, and let not your mammon manage you. ## Deploy your own @@ -14,69 +10,18 @@ Once you have access to [the environment variables you'll need](#step-5-set-up-e ## How to use -Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: - -```bash -npx create-next-app --example with-mysql next-mysql-app -# or -yarn create next-app --example with-mysql next-mysql-app -``` - -## Configuration - -### Step 1. Set up a MySQL database - -Set up a MySQL server either locally or any cloud provider. - -### Step 2. Set up environment variables - -Copy the `env.local.example` file in this directory to `.env.local` (which will be ignored by Git): - -```bash -cp .env.local.example .env.local -``` - -Set each variable on `.env.local`: - -- `MYSQL_HOST` - Your MySQL host URL. -- `MYSQL_DATABASE` - The name of the MySQL database you want to use. -- `MYSQL_USERNAME` - The name of the MySQL user with access to database. -- `MYSQL_PASSWORD` - The passowrd of the MySQL user. - -### Step 3. Run migration script +This project is based on the [Next.js example with MySQL](https://github.com/vercel/next.js/tree/canary/examples/with-mysql). Refer to its README to see how to perform intitial setup. This may also be included in the Wiki when the project becomes mature enough. -You'll need to run a migration to create the necessary table for the example. +## Resources -```bash -npm run migrate -# or -yarn migrate -``` +### Component Libraries +- [Shards React](https://designrevision.com/docs/shards-react/) +- [Gestalt](https://gestalt.netlify.app/) +- [Grommet](https://v2.grommet.io/) +- [Blueprint](https://blueprintjs.com/) +- [Evergreen](https://evergreen.segment.com/) -### Step 4. Run Next.js in development mode - -```bash -npm install -npm run dev -# or -yarn install -yarn dev -``` - -Your app should be up and running on [http://localhost:3000](http://localhost:3000)! If it doesn't work, post on [GitHub discussions](https://github.com/vercel/next.js/discussions). - -## Deploy on Vercel - -You can deploy this app to the cloud with [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). - -#### Deploy Your Local Project - -To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example). - -**Important**: When you import your project on Vercel, make sure to click on **Environment Variables** and set them to match your `.env.local` file. - -#### Deploy from Our Template - -Alternatively, you can deploy using our template by clicking on the Deploy button below. - -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-mysql&project-name=nextjs-mysql&repository-name=nextjs-mysql&env=MYSQL_HOST,MYSQL_DATABASE,MYSQL_USERNAME,MYSQL_PASSWORD&envDescription=Required%20to%20connect%20the%20app%20with%20MySQL&envLink=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-mysql%23step-2-set-up-environment-variables&demo-title=Next.js%20%2B%20MySQL%20Demo&demo-description=A%20simple%20app%20demonstrating%20Next.js%20and%20MySQL%20&demo-url=https%3A%2F%2Fnext-mysql.vercel.app%2F) +### Dashboard Templates +- https://material-ui.com/store/collections/free-react-dashboard/ +- https://www.codeinwp.com/blog/react-ui-component-libraries-frameworks/ +- https://github.com/DesignRevision/shards-dashboard-react \ No newline at end of file diff --git a/components/button-link/index.tsx b/components/button-link/index.tsx deleted file mode 100644 index 88e4658..0000000 --- a/components/button-link/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import Link from 'next/link' -import cn from 'clsx' - -function ButtonLink({ href = '/', className = '', children }) { - return ( - - - {children} - - - ) -} - -export default ButtonLink diff --git a/components/button/index.tsx b/components/button/index.tsx deleted file mode 100644 index ca8714a..0000000 --- a/components/button/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import cn from 'clsx' - -function Button({ - onClick = console.log, - className = '', - children = null, - type = null, - disabled = false, -}) { - return ( - - ) -} - -export default Button diff --git a/components/category-form/index.tsx b/components/category-form/index.tsx new file mode 100644 index 0000000..5f8809c --- /dev/null +++ b/components/category-form/index.tsx @@ -0,0 +1,114 @@ +import { useState } from 'react'; +import { + Pane, + Alert, + Heading, + TextInputField, + Button, + ConfirmIcon, + RefreshIcon, + toaster, + majorScale, +} from 'evergreen-ui'; +import { HexColorPicker } from 'react-colorful'; + +import colors from '@/constants/colors'; + +export default function EntryForm() { + const randomColor = colors[Math.floor(Math.random() * (colors.length - 1))]; + + const [showError, setShowError] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + const [name, setName] = useState(''); + const [color, setColor] = useState(randomColor); + const [submitting, setSubmitting] = useState(false); + + async function submitHandler(e) { + setSubmitting(true); + e.preventDefault(); + try { + const res = await fetch('/api/categories', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name, + color, + }), + }); + setSubmitting(false); + const json = await res.json(); + if (!res.ok) { + setShowError(true); + setErrorMessage(json.message); + } else { + setShowError(false); + setName(''); + toaster.success('Category created!'); + } + } catch (err) { + throw Error(err.message); + } + } + + return ( + + {showError && ( + setShowError(false)} + > + {errorMessage} + + )} + + Add a category +
+ + setName(e.target.value)} + /> + + + + + setColor(e.target.value)} + /> + + + + +
+
+ ); +} diff --git a/components/charts/all-txns/index.tsx b/components/charts/all-txns/index.tsx index e69de29..b5bf9d9 100644 --- a/components/charts/all-txns/index.tsx +++ b/components/charts/all-txns/index.tsx @@ -0,0 +1,36 @@ +import dynamic from 'next/dynamic'; + +const Chart = dynamic(() => import('react-apexcharts'), { + ssr: false, +}); + +export default function AllTxns({ data }) { + return ( + t.name), + }, + }} + series={[ + { + name: 'AED', + data: data.map((t) => +(Math.round(t.amount * 100) / 100).toFixed(2)), + }, + ]} + /> + ); +} diff --git a/components/charts/pie-by-category/index.tsx b/components/charts/pie-by-category/index.tsx new file mode 100644 index 0000000..237cfc2 --- /dev/null +++ b/components/charts/pie-by-category/index.tsx @@ -0,0 +1,19 @@ +// import Chart from 'react-apexcharts'; + +import dynamic from 'next/dynamic'; + +const Chart = dynamic(() => import('react-apexcharts'), { + ssr: false, +}); + +export default function PieByCategory({ data }) { + return ( + t.category), + }} + series={data.map((t) => +(Math.round(t.total * 100) / 100).toFixed(2))} + /> + ); +} diff --git a/components/charts/txn-by-category/index.tsx b/components/charts/txn-by-category/index.tsx deleted file mode 100644 index e4295ff..0000000 --- a/components/charts/txn-by-category/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import Chart from 'react-apexcharts' -import PuffLoader from 'react-spinners/PuffLoader' - -import { useTxnByCategory } from '@/lib/swr-hooks' - -export default function TxnByCategory() { - const { transactions, isLoading } = useTxnByCategory(); - - if (isLoading) return - return ( - t.category) - }} - series={transactions.map(t => +(Math.round(t.total * 100) / 100).toFixed(2))} - /> - ) -} diff --git a/components/container/index.tsx b/components/container/index.tsx index 4c5b421..7222257 100644 --- a/components/container/index.tsx +++ b/components/container/index.tsx @@ -1,5 +1,5 @@ function Container({ className = '', children }) { - return
{children}
+ return
{children}
} export default Container diff --git a/components/dashboard/day/index.tsx b/components/dashboard/day/index.tsx new file mode 100644 index 0000000..7b36ea7 --- /dev/null +++ b/components/dashboard/day/index.tsx @@ -0,0 +1,36 @@ +import PuffLoader from 'react-spinners/PuffLoader'; + +import { useCategoryTotals, useTransactionsByDay } from '@/lib/swr-hooks'; +import PieByCategory from '@/components/charts/pie-by-category'; + +export default function DayDashboard({ date }) { + const { categories, isLoading: isCatLoading } = useCategoryTotals(date); + const { transactions, isLoading: isTxnLoading } = useTransactionsByDay(date); + + if (isCatLoading || isTxnLoading) return ; + return transactions.length > 0 ? ( +
+ + + + + + + + + + + {transactions.map((txn) => ( + + + + + + ))} + +
NameAmountCategory
{txn.name}{txn.amount}{txn.category}
+
+ ) : ( +

No Transactions!

+ ); +} diff --git a/components/dashboard/index.ts b/components/dashboard/index.ts new file mode 100644 index 0000000..e26fd6c --- /dev/null +++ b/components/dashboard/index.ts @@ -0,0 +1,6 @@ +import DayDashboard from './day'; +import WeekDashboard from './week'; +import MonthDashboard from './month'; +import YearDashboard from './year'; + +export { DayDashboard, WeekDashboard, MonthDashboard, YearDashboard }; diff --git a/components/dashboard/month/index.tsx b/components/dashboard/month/index.tsx new file mode 100644 index 0000000..e171409 --- /dev/null +++ b/components/dashboard/month/index.tsx @@ -0,0 +1,40 @@ +import PuffLoader from 'react-spinners/PuffLoader'; +import { format } from 'date-fns'; + +import { useTransactionsByMonth } from '@/lib/swr-hooks'; +import AllTxns from '@/components/charts/all-txns'; + +export default function MonthDashboard({ month }) { + const { transactions, isLoading } = useTransactionsByMonth(month); + + if (isLoading) return ; + return transactions.length > 0 ? ( +
+ + + + + + + + + + + + {transactions.map((txn) => ( + + + + + + + ))} + +
NameAmountCategoryDate
{txn.name}{txn.amount}{txn.category} + {format(new Date(txn.date), 'MMMM do, yyyy')} +
+
+ ) : ( +

No Transactions!

+ ); +} diff --git a/components/dashboard/week/index.tsx b/components/dashboard/week/index.tsx new file mode 100644 index 0000000..7351d22 --- /dev/null +++ b/components/dashboard/week/index.tsx @@ -0,0 +1,3 @@ +export default function WeekDashboard() { + return

Week Dashboard

; +} diff --git a/components/dashboard/year/index.tsx b/components/dashboard/year/index.tsx new file mode 100644 index 0000000..ba8f273 --- /dev/null +++ b/components/dashboard/year/index.tsx @@ -0,0 +1,3 @@ +export default function YearDashboard() { + return

Year Dashboard

; +} diff --git a/components/edit-entry-form/index.tsx b/components/edit-entry-form/index.tsx deleted file mode 100644 index f84307d..0000000 --- a/components/edit-entry-form/index.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useState, useEffect } from 'react' -import Router, { useRouter } from 'next/router' - -import Button from '../button' - -export default function EntryForm() { - const [_title, setTitle] = useState('') - const [_content, setContent] = useState('') - const [submitting, setSubmitting] = useState(false) - const router = useRouter() - const { id, title, content } = router.query - - useEffect(() => { - if (typeof title === 'string') { - setTitle(title) - } - if (typeof content === 'string') { - setContent(content) - } - }, [title, content]) - - async function submitHandler(e) { - e.preventDefault() - setSubmitting(true) - try { - const res = await fetch('/api/edit-entry', { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - id, - title: _title, - content: _content, - }), - }) - const json = await res.json() - setSubmitting(false) - if (!res.ok) throw Error(json.message) - Router.push('/') - } catch (e) { - throw Error(e.message) - } - } - - return ( -
-
- - setTitle(e.target.value)} - /> -
-
- -