├── amplify
├── package.json
├── backend.ts
├── auth
│ └── resource.ts
├── tsconfig.json
└── data
│ └── resource.ts
├── src
├── app
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ ├── app.css
│ ├── login
│ │ └── page.tsx
│ ├── dashboard
│ │ ├── page.tsx
│ │ └── setup
│ │ │ └── page.tsx
│ ├── _actions
│ │ └── actions.ts
│ ├── page.module.css
│ └── page.tsx
├── components
│ ├── SubmitButton.tsx
│ ├── ConfigureAmplify.tsx
│ ├── Logout.tsx
│ ├── CardItem.tsx
│ ├── RightColumn.tsx
│ ├── Navbar.tsx
│ ├── BarChartComponent.tsx
│ ├── ScoreList.tsx
│ ├── LeftColumn.tsx
│ ├── TableComponent.tsx
│ ├── SavingsModal.tsx
│ ├── Sidebar.tsx
│ ├── ExpenseModal.tsx
│ ├── SalesItem.tsx
│ └── WebAnalytics.tsx
├── utils
│ ├── utils.ts
│ └── amplify-utils.ts
└── middleware.ts
├── postcss.config.js
├── next.config.js
├── next-env.d.ts
├── CODE_OF_CONDUCT.md
├── amplify.yml
├── .gitignore
├── tsconfig.json
├── LICENSE
├── package.json
├── public
├── amplify.svg
└── next.svg
├── README.md
├── CONTRIBUTING.md
└── tailwind.config.ts
/amplify/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module"
3 | }
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TRamos5/spotme-app/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | experimental: {
4 | serverActions: true,
5 | },
6 | };
7 |
8 | module.exports = nextConfig;
9 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | * {
6 | scroll-behavior: smooth;
7 | }
8 |
9 | body {
10 | background-color: #1e293b;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/SubmitButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@aws-amplify/ui-react";
4 |
5 | export default function SubmitButton() {
6 | return Submit ;
7 | }
8 |
--------------------------------------------------------------------------------
/amplify/backend.ts:
--------------------------------------------------------------------------------
1 | import { defineBackend } from "@aws-amplify/backend";
2 | import { auth } from "./auth/resource.js";
3 | import { data } from "./data/resource.js";
4 |
5 | defineBackend({
6 | auth,
7 | data,
8 | });
9 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/src/components/ConfigureAmplify.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Amplify } from "aws-amplify";
4 |
5 | import outputs from "@/amplify_outputs.json";
6 |
7 | Amplify.configure(outputs, { ssr: true });
8 |
9 | export default function ConfigureAmplifyClientSide() {
10 | return null;
11 | }
12 |
--------------------------------------------------------------------------------
/amplify/auth/resource.ts:
--------------------------------------------------------------------------------
1 | import { defineAuth } from "@aws-amplify/backend";
2 |
3 | /**
4 | * Define and configure your auth resource
5 | * @see https://docs.amplify.aws/gen2/build-a-backend/auth
6 | */
7 | export const auth = defineAuth({
8 | loginWith: {
9 | email: true,
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
4 | opensource-codeofconduct@amazon.com with any additional questions or comments.
5 |
--------------------------------------------------------------------------------
/amplify/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2022",
4 | "module": "es2022",
5 | "moduleResolution": "bundler",
6 | "resolveJsonModule": true,
7 | "esModuleInterop": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "strict": true,
10 | "skipLibCheck": true,
11 | "paths": {
12 | "$amplify/*": [
13 | "../.amplify/generated/*"
14 | ]
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | export const dataFormatter = (number: number) => {
2 | return "$ " + Intl.NumberFormat("us").format(number).toString();
3 | };
4 |
5 | export const months = [
6 | "January",
7 | "February",
8 | "March",
9 | "April",
10 | "May",
11 | "June",
12 | "July",
13 | "August",
14 | "September",
15 | "October",
16 | "November",
17 | "December",
18 | ];
19 |
20 | export const getCurrentMonth = () => {
21 | const d = new Date();
22 | return months[d.getMonth()];
23 | };
24 |
--------------------------------------------------------------------------------
/amplify.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | backend:
3 | phases:
4 | build:
5 | commands:
6 | - npm ci --cache .npm --prefer-offline
7 | - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
8 | frontend:
9 | phases:
10 | build:
11 | commands:
12 | - npm run build
13 | artifacts:
14 | baseDirectory: .next
15 | files:
16 | - '**/*'
17 | cache:
18 | paths:
19 | - .next/cache/**/*
20 | - .npm/**/*
21 | - node_modules/**/*
--------------------------------------------------------------------------------
/src/components/Logout.tsx:
--------------------------------------------------------------------------------
1 | // components/Logout.tsx
2 |
3 | "use client";
4 |
5 | import { signOut } from "aws-amplify/auth";
6 | import { useRouter } from "next/navigation";
7 | import { Button } from "@aws-amplify/ui-react";
8 |
9 | export default function Logout() {
10 | const router = useRouter();
11 |
12 | return (
13 | {
16 | await signOut();
17 | router.push("/login");
18 | }}
19 | className="px-2 bg-white text-black"
20 | >
21 | Sign out
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/CardItem.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Card, Metric, Text, Flex, BadgeDelta } from "@tremor/react";
4 |
5 | export default function CardItem({
6 | name,
7 | amount,
8 | }: {
9 | name: string;
10 | amount: string;
11 | }) {
12 | return (
13 |
14 |
15 | {name}
16 | {/* +12.5% */}
17 |
18 | {amount}
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/RightColumn.tsx:
--------------------------------------------------------------------------------
1 | import SalesItem from "./SalesItem";
2 | import WebAnalytics from "./WebAnalytics";
3 | import ScoreList from "./ScoreList";
4 | import { getCurrentMonth } from "../utils/utils";
5 | import type { Schema } from "@/amplify/data/resource";
6 |
7 | type Expenses = Schema["Expenses"]["type"];
8 |
9 | export default function RightColumn({ expenses }: { expenses: Expenses[] }) {
10 | return (
11 |
12 |
13 | {/* */}
14 | {/* */}
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { TextInput } from "@tremor/react";
4 | import { WifiIcon } from "@heroicons/react/20/solid";
5 | import Logout from "./Logout";
6 |
7 | export default function Navbar({ username }: { username: string }) {
8 | return (
9 |
13 |
Welcome, {username}
14 |
15 | {/* */}
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | # amplify
39 | .amplify
40 | amplify_outputs*
41 | amplifyconfiguration*
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 | import ConfigureAmplifyClientSide from "@/src/components/ConfigureAmplify";
5 |
6 | const inter = Inter({ subsets: ["latin"] });
7 |
8 | export const metadata: Metadata = {
9 | title: "SpotMe",
10 | description: "Your personal finance tracker.",
11 | };
12 |
13 | export default function RootLayout({
14 | children,
15 | }: {
16 | children: React.ReactNode;
17 | }) {
18 | return (
19 |
20 |
21 |
22 | {children}
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/BarChartComponent.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Card, Title, BarChart } from "@tremor/react";
4 | import { dataFormatter } from "../utils/utils";
5 | import type { Schema } from "@/amplify/data/resource";
6 |
7 | type AmountSaved = Schema["AmountSaved"]["type"];
8 |
9 | export default function BarChartComponent({
10 | amountToSave,
11 | amountSaved,
12 | }: {
13 | amountToSave: number;
14 | amountSaved: AmountSaved[];
15 | }) {
16 | return (
17 |
18 | Saved amount in 2024 (USD)
19 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/ScoreList.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Card, List, ListItem, Title } from "@tremor/react";
4 |
5 | const cities = [
6 | {
7 | city: "Athens",
8 | rating: "2 open PR",
9 | },
10 | {
11 | city: "Luzern",
12 | rating: "1 open PR",
13 | },
14 | {
15 | city: "Zürich",
16 | rating: "0 open PR",
17 | },
18 | {
19 | city: "Vienna",
20 | rating: "1 open PR",
21 | },
22 | {
23 | city: "Ermatingen",
24 | rating: "0 open PR",
25 | },
26 | {
27 | city: "Lisbon",
28 | rating: "0 open PR",
29 | },
30 | ];
31 |
32 | export default function ScoreList() {
33 | return (
34 |
35 | Tremor's Hometowns
36 |
37 | {cities.map((item) => (
38 |
39 | {item.city}
40 | {item.rating}
41 |
42 | ))}
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT No Attribution
2 |
3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17 |
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spot-me",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@aws-amplify/adapter-nextjs": "^1.1.8",
13 | "@aws-amplify/ui-react": "^6.1.11",
14 | "@headlessui/react": "^2.0.3",
15 | "@headlessui/tailwindcss": "^0.2.0",
16 | "@heroicons/react": "^2.1.3",
17 | "@remixicon/react": "^4.2.0",
18 | "@tremor/react": "^3.16.3",
19 | "aws-amplify": "^6.3.2",
20 | "next": "^14.2.3",
21 | "react": "^18.3.1",
22 | "react-dom": "^18.3.1"
23 | },
24 | "devDependencies": {
25 | "@aws-amplify/backend": "^1.0.0",
26 | "@aws-amplify/backend-cli": "^1.0.1",
27 | "@tailwindcss/forms": "^0.5.7",
28 | "@types/node": "^20",
29 | "@types/react": "^18.3.2",
30 | "@types/react-dom": "^18.3.0",
31 | "autoprefixer": "^10.4.19",
32 | "postcss": "^8.4.38",
33 | "tailwindcss": "^3.4.3",
34 | "typescript": "^5.3.2"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/public/amplify.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/amplify/data/resource.ts:
--------------------------------------------------------------------------------
1 | import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
2 |
3 | const schema = a.schema({
4 | SavingStrategy: a
5 | .model({
6 | savingsModel: a.string().required(),
7 | monthlyIncome: a.float().required(),
8 | amountToSave: a.float().required(),
9 | yearlyIncome: a.float().required(),
10 | })
11 | .authorization((allow) => [allow.owner()]),
12 | Expenses: a
13 | .model({
14 | expenseName: a.string().required(),
15 | expenseAmount: a.float().required(),
16 | expenseCategory: a.string().required(),
17 | month: a.string().required(),
18 | year: a.string().required(),
19 | })
20 | .authorization((allow) => [allow.owner()]),
21 | AmountSaved: a
22 | .model({
23 | amountSaved: a.float().required(),
24 | month: a.string().required(),
25 | year: a.string().required(),
26 | })
27 | .authorization((allow) => [allow.owner()]),
28 | });
29 |
30 | export type Schema = ClientSchema;
31 |
32 | export const data = defineData({
33 | schema,
34 | authorizationModes: {
35 | defaultAuthorizationMode: "userPool",
36 | apiKeyAuthorizationMode: {
37 | expiresInDays: 30,
38 | },
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## AWS Amplify Next.js (App Router) Starter Template
2 |
3 | This repository provides a starter template for creating applications using Next.js (App Router) and AWS Amplify, emphasizing easy setup for authentication, API, and database capabilities.
4 |
5 | ## Overview
6 |
7 | This template equips you with a foundational Next.js application integrated with AWS Amplify, streamlined for scalability and performance. It is ideal for developers looking to jumpstart their project with pre-configured AWS services like Cognito, AppSync, and DynamoDB.
8 |
9 | ## Features
10 |
11 | - **Authentication**: Setup with Amazon Cognito for secure user authentication.
12 | - **API**: Ready-to-use GraphQL endpoint with AWS AppSync.
13 | - **Database**: Real-time database powered by Amazon DynamoDB.
14 |
15 | ## Deploying to AWS
16 |
17 | For detailed instructions on deploying your application, refer to the [deployment section](https://docs.amplify.aws/nextjs/start/quickstart/nextjs-app-router-client-components/#deploy-a-fullstack-app-to-aws) of our documentation.
18 |
19 | ## Security
20 |
21 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.
22 |
23 | ## License
24 |
25 | This library is licensed under the MIT-0 License. See the LICENSE file.
--------------------------------------------------------------------------------
/src/app/app.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | display: flex;
4 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
5 | height: 100vh;
6 | width: 100vw;
7 | justify-content: center;
8 | align-items: center;
9 | }
10 |
11 | main {
12 | display: flex;
13 | flex-direction: column;
14 | align-items: stretch;
15 | }
16 |
17 | button {
18 | border-radius: 8px;
19 | border: 1px solid transparent;
20 | padding: 0.6em 1.2em;
21 | font-size: 1em;
22 | font-weight: 500;
23 | font-family: inherit;
24 | background-color: #1a1a1a;
25 | cursor: pointer;
26 | transition: border-color 0.25s;
27 | color: white;
28 | }
29 | button:hover {
30 | border-color: #646cff;
31 | }
32 | button:focus,
33 | button:focus-visible {
34 | outline: 4px auto -webkit-focus-ring-color;
35 | }
36 |
37 | ul {
38 | padding-inline-start: 0;
39 | margin-block-start: 0;
40 | margin-block-end: 0;
41 | list-style-type: none;
42 | display: flex;
43 | flex-direction: column;
44 | margin: 8px 0;
45 | border: 1px solid black;
46 | gap: 1px;
47 | background-color: black;
48 | border-radius: 8px;
49 | overflow: auto;
50 | }
51 |
52 | li {
53 | background-color: white;
54 | padding: 8px;
55 | }
56 |
57 | li:hover {
58 | background: #dadbf9;
59 | }
60 |
61 | a {
62 | font-weight: 800;
63 | text-decoration: none;
64 | }
65 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/login/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Authenticator,
5 | Text,
6 | View,
7 | useAuthenticator,
8 | } from "@aws-amplify/ui-react";
9 | import { redirect } from "next/navigation";
10 | import { useEffect } from "react";
11 | import { signUp, SignUpInput } from "aws-amplify/auth";
12 | import "@aws-amplify/ui-react/styles.css";
13 |
14 | function CustomAuthenticator() {
15 | const { user } = useAuthenticator((context) => [context.user]);
16 |
17 | const services = {
18 | async handleSignUp(input: SignUpInput) {
19 | const { username, password } = input;
20 | return signUp({
21 | username,
22 | password,
23 | options: {
24 | ...input.options,
25 | userAttributes: {
26 | ...input.options?.userAttributes,
27 | "custom:isSetup": "0",
28 | },
29 | },
30 | });
31 | },
32 | };
33 |
34 | useEffect(() => {
35 | if (user) {
36 | redirect("/dashboard");
37 | }
38 | }, [user]);
39 |
40 | return (
41 |
47 | );
48 | }
49 |
50 | export default function LoginPage() {
51 | return (
52 |
53 |
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/src/utils/amplify-utils.ts:
--------------------------------------------------------------------------------
1 | // utils/amplify-utils.ts
2 | import { cookies } from "next/headers";
3 |
4 | import { createServerRunner } from "@aws-amplify/adapter-nextjs";
5 | import { generateServerClientUsingCookies } from "@aws-amplify/adapter-nextjs/api";
6 | import { getCurrentUser, fetchUserAttributes } from "aws-amplify/auth/server";
7 |
8 | import { type Schema } from "@/amplify/data/resource";
9 | import outputs from "@/amplify_outputs.json";
10 |
11 | export const { runWithAmplifyServerContext } = createServerRunner({
12 | config: outputs,
13 | });
14 |
15 | export const cookiesClient = generateServerClientUsingCookies({
16 | config: outputs,
17 | cookies,
18 | });
19 |
20 | export async function AuthGetCurrentUserServer() {
21 | try {
22 | const currentUser = await runWithAmplifyServerContext({
23 | nextServerContext: { cookies },
24 | operation: (contextSpec) => getCurrentUser(contextSpec),
25 | });
26 | return currentUser;
27 | } catch (error) {
28 | console.error(error);
29 | }
30 | }
31 |
32 | export async function AuthGetCurrentUserAttributesServer() {
33 | try {
34 | const currentUser = await runWithAmplifyServerContext({
35 | nextServerContext: { cookies },
36 | operation: (contextSpec) => fetchUserAttributes(contextSpec),
37 | });
38 | return currentUser;
39 | } catch (error) {
40 | console.error(error);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/LeftColumn.tsx:
--------------------------------------------------------------------------------
1 | import CardItem from "./CardItem";
2 | import BarChartComponent from "./BarChartComponent";
3 | import TableComponent from "./TableComponent";
4 | import { dataFormatter, getCurrentMonth } from "../utils/utils";
5 | import type { Schema } from "@/amplify/data/resource";
6 |
7 | type SavingStrategy = Schema["SavingStrategy"]["type"];
8 | type Expenses = Schema["Expenses"]["type"];
9 | type AmountSaved = Schema["AmountSaved"]["type"];
10 |
11 | export default function LeftColumn({
12 | savingStrategy,
13 | expenses,
14 | amountSaved,
15 | }: {
16 | savingStrategy: SavingStrategy[];
17 | expenses: Expenses[];
18 | amountSaved: AmountSaved[];
19 | }) {
20 | const strategy = savingStrategy[0];
21 | return (
22 |
23 |
24 |
28 |
32 |
36 |
37 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/TableComponent.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { CreditCardIcon } from "@heroicons/react/16/solid";
4 | import {
5 | Card,
6 | Table,
7 | TableHead,
8 | TableRow,
9 | TableHeaderCell,
10 | TableBody,
11 | TableCell,
12 | Text,
13 | Title,
14 | Badge,
15 | } from "@tremor/react";
16 | import type { Schema } from "@/amplify/data/resource";
17 | import { dataFormatter } from "../utils/utils";
18 |
19 | type Expenses = Schema["Expenses"]["type"];
20 |
21 | const TableComponent = ({
22 | expenses,
23 | month,
24 | }: {
25 | expenses: Expenses[];
26 | month: string;
27 | }) => {
28 | return (
29 |
30 | List of Expenses for {month}
31 |
32 |
33 |
34 | Name
35 | Amount
36 | Category
37 |
38 |
39 |
40 | {expenses.map((expense) => (
41 |
42 | {expense.expenseName}
43 |
44 | {dataFormatter(expense.expenseAmount)}
45 |
46 |
47 |
48 | {expense.expenseCategory}
49 |
50 |
51 |
52 | ))}
53 |
54 |
55 |
56 | );
57 | };
58 |
59 | export default TableComponent;
60 |
--------------------------------------------------------------------------------
/src/app/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import "@aws-amplify/ui-react/styles.css";
2 | import Sidebar from "@/src/components/Sidebar";
3 | import Navbar from "@/src/components/Navbar";
4 | import LeftColumn from "@/src/components/LeftColumn";
5 | import RightColumn from "@/src/components/RightColumn";
6 | import {
7 | AuthGetCurrentUserAttributesServer,
8 | cookiesClient,
9 | } from "@/src/utils/amplify-utils";
10 | import { months, getCurrentMonth } from "@/src/utils/utils";
11 |
12 | export default async function App() {
13 | const user = await AuthGetCurrentUserAttributesServer();
14 | const { data: expenses, errors: expensesErrors } =
15 | await cookiesClient.models.Expenses.list();
16 | const { data: savingStrategy, errors: savingStrategyErrors } =
17 | await cookiesClient.models.SavingStrategy.list();
18 | const { data: amountSaved, errors: amountSavedErros } =
19 | await cookiesClient.models.AmountSaved.list();
20 |
21 | // Sort the amountSaved by month
22 | amountSaved.sort((a, b) => {
23 | const monthIndexA = months.indexOf(a.month);
24 | const monthIndexB = months.indexOf(b.month);
25 | return monthIndexA - monthIndexB;
26 | });
27 |
28 | const currentMonth = getCurrentMonth();
29 | const filteredExpenses = expenses.filter((expense) => {
30 | return expense.month === currentMonth;
31 | });
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
39 |
40 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | // middleware.ts
2 | import { NextRequest, NextResponse } from "next/server";
3 |
4 | import { fetchAuthSession, fetchUserAttributes } from "aws-amplify/auth/server";
5 |
6 | import { runWithAmplifyServerContext } from "@/src/utils/amplify-utils";
7 | import { a } from "@aws-amplify/backend";
8 |
9 | export async function middleware(request: NextRequest) {
10 | const response = NextResponse.next();
11 |
12 | const authenticated = await runWithAmplifyServerContext({
13 | nextServerContext: { request, response },
14 | operation: async (contextSpec) => {
15 | try {
16 | const session = await fetchAuthSession(contextSpec, {});
17 | return session.tokens !== undefined;
18 | } catch (error) {
19 | console.log(error);
20 | return false;
21 | }
22 | },
23 | });
24 |
25 | const isSetup = await runWithAmplifyServerContext({
26 | nextServerContext: { request, response },
27 | operation: async (contextSpec) => {
28 | try {
29 | const attributes = await fetchUserAttributes(contextSpec);
30 | console.log(attributes);
31 | return attributes["custom:isSetup"] === "1";
32 | } catch (error) {
33 | console.log(error);
34 | return false;
35 | }
36 | },
37 | });
38 |
39 | const { pathname } = request.nextUrl;
40 |
41 | if (pathname === "/") {
42 | return response;
43 | }
44 |
45 | if (authenticated) {
46 | if (!isSetup && pathname !== "/dashboard/setup") {
47 | const newUrl = new URL("/dashboard/setup", request.url);
48 | return NextResponse.redirect(newUrl);
49 | }
50 | return response;
51 | }
52 |
53 | return NextResponse.redirect(new URL("/login", request.url));
54 | }
55 |
56 | export const config = {
57 | matcher: [
58 | /*
59 | * Match all request paths except for the ones starting with:
60 | * - api (API routes)
61 | * - _next/static (static files)
62 | * - _next/image (image optimization files)
63 | * - favicon.ico (favicon file)
64 | * - login
65 | */
66 | "/((?!api|_next/static|_next/image|favicon.ico|login|dashboard/setup).*)",
67 | ],
68 | };
69 |
--------------------------------------------------------------------------------
/src/components/SavingsModal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Flex, Label, Input, SelectField, Button } from "@aws-amplify/ui-react";
4 | import { createAmountSaved } from "@/src/app/_actions/actions";
5 | import { useRef } from "react";
6 | import "@aws-amplify/ui-react/styles.css";
7 | import { useFormStatus } from "react-dom";
8 | import { XMarkIcon } from "@heroicons/react/16/solid";
9 |
10 | function Submit() {
11 | const { pending } = useFormStatus();
12 | return (
13 |
14 | {pending ? "Submitting..." : "Submit"}
15 |
16 | );
17 | }
18 |
19 | function Form({ action }: { action: (formData: FormData) => void }) {
20 | const ref = useRef(null);
21 |
22 | return (
23 |
59 | );
60 | }
61 |
62 | export default function SavingsModal({
63 | isOpen,
64 | onClose,
65 | }: {
66 | isOpen: boolean;
67 | onClose: () => void;
68 | }) {
69 | if (!isOpen) return null;
70 |
71 | return (
72 |
73 |
74 |
75 |
76 | Let's add how much you've saved!
77 |
78 |
83 | Close
84 |
85 |
86 |
87 |
88 |
89 | );
90 | }
91 |
--------------------------------------------------------------------------------
/src/app/_actions/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { cookiesClient } from "@/src/utils/amplify-utils";
4 | import { redirect } from "next/navigation";
5 | import { revalidatePath } from "next/cache";
6 |
7 | export async function createExpense(formData: FormData) {
8 | try {
9 | const { data } = await cookiesClient.models.Expenses.create({
10 | expenseName: formData.get("expenseName") as string,
11 | expenseAmount: parseFloat(formData.get("expenseAmount") as string),
12 | expenseCategory: formData.get("expenseCategory") as string,
13 | month: formData.get("month") as string,
14 | year: "2024",
15 | });
16 | console.log("Created expense", data);
17 | revalidatePath("/dashboard");
18 | } catch (error) {
19 | console.error("Error creating expense", error);
20 | redirect("/login");
21 | }
22 | }
23 |
24 | export async function createAmountSaved(formData: FormData) {
25 | try {
26 | const { data } = await cookiesClient.models.AmountSaved.create({
27 | amountSaved: parseFloat(formData.get("amountSaved") as string),
28 | month: formData.get("month") as string,
29 | year: "2024",
30 | });
31 | console.log("Created amount saved", data);
32 | revalidatePath("/dashboard");
33 | } catch (error) {
34 | console.error("Error creating amount saved", error);
35 | redirect("/login");
36 | }
37 | }
38 |
39 | export async function createSavingStrategy(formData: FormData) {
40 | try {
41 | let savingPercentage;
42 | const savingModel = formData.get("savingsModel") as string;
43 | const monthlyIncome = parseFloat(formData.get("monthlyIncome") as string);
44 |
45 | switch (savingModel) {
46 | case "50% - Aggressive":
47 | savingPercentage = 0.5;
48 | break;
49 | case "30% - Moderate":
50 | savingPercentage = 0.3;
51 | break;
52 | case "20% - Conservative":
53 | savingPercentage = 0.2;
54 | break;
55 | case "10% - Essential":
56 | savingPercentage = 0.1;
57 | break;
58 | default:
59 | throw "Invalid saving model";
60 | }
61 |
62 | const { data } = await cookiesClient.models.SavingStrategy.create({
63 | savingsModel: savingModel,
64 | monthlyIncome: parseFloat(formData.get("monthlyIncome") as string),
65 | amountToSave: monthlyIncome * savingPercentage,
66 | yearlyIncome: monthlyIncome * 12,
67 | });
68 |
69 | console.log("Created saving strategy", data);
70 | revalidatePath("/dashboard");
71 | redirect("/dashboard");
72 | } catch (error) {
73 | console.error("Error creating saving strategy", error);
74 | redirect("/login");
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | BellIcon,
5 | ChartBarIcon,
6 | CreditCardIcon,
7 | HomeIcon,
8 | ClipboardDocumentListIcon,
9 | EnvelopeIcon,
10 | ArrowUpIcon,
11 | LinkIcon,
12 | BanknotesIcon,
13 | } from "@heroicons/react/16/solid";
14 | import ExpenseModal from "./ExpenseModal";
15 | import SavingsModal from "./SavingsModal";
16 | import { useState } from "react";
17 |
18 | export default function Sidebar() {
19 | const [isExpenseModalOpen, setIsExpenseModalOpen] = useState(false);
20 | const [isSavingsModalOpen, setIsSavingsModalOpen] = useState(false);
21 |
22 | const openExpenseModal = () => setIsExpenseModalOpen(true);
23 | const closeExpenseModal = () => setIsExpenseModalOpen(false);
24 | const openSavingsModal = () => setIsSavingsModalOpen(true);
25 | const closeSavingsModal = () => setIsSavingsModalOpen(false);
26 |
27 | return (
28 | <>
29 |
30 |
38 |
39 |
40 |
44 |
45 |
50 |
55 | {/*
59 |
*/}
63 |
64 |
76 |
77 |
78 |
79 | >
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/src/app/dashboard/setup/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Flex, Label, Input, SelectField } from "@aws-amplify/ui-react";
4 | import { createSavingStrategy } from "@/src/app/_actions/actions";
5 | import { Button } from "@aws-amplify/ui-react";
6 | import { updateUserAttributes, fetchUserAttributes } from "aws-amplify/auth";
7 | import { useEffect } from "react";
8 | import { useRouter } from "next/navigation";
9 | import { useRef } from "react";
10 | import "@aws-amplify/ui-react/styles.css";
11 | import { useFormStatus } from "react-dom";
12 |
13 | function Submit() {
14 | const { pending } = useFormStatus();
15 |
16 | async function updateUser() {
17 | await updateUserAttributes({
18 | userAttributes: {
19 | "custom:isSetup": "1",
20 | },
21 | });
22 | }
23 |
24 | return (
25 |
26 | {pending ? "Submitting..." : "Submit"}
27 |
28 | );
29 | }
30 |
31 | function Form({ action }: { action: (formData: FormData) => void }) {
32 | const ref = useRef(null);
33 |
34 | return (
35 |
70 | );
71 | }
72 |
73 | export default function Setup() {
74 | const router = useRouter();
75 |
76 | useEffect(() => {
77 | async function getUserAttributes() {
78 | try {
79 | const userAttributes = await fetchUserAttributes();
80 | if (userAttributes && userAttributes["custom:isSetup"] === "1") {
81 | router.replace("/dashboard");
82 | }
83 | } catch (error) {
84 | console.error("Error fetching user attributes", error);
85 | }
86 | }
87 | getUserAttributes();
88 | }, [router]);
89 |
90 | return (
91 |
92 |
93 |
Hi, let's setup your account!
94 |
95 |
96 |
97 | );
98 | }
99 |
--------------------------------------------------------------------------------
/src/components/ExpenseModal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRef } from "react";
4 | import { Flex, Label, Input, SelectField, Button } from "@aws-amplify/ui-react";
5 | import { createExpense } from "@/src/app/_actions/actions";
6 | import { useFormStatus } from "react-dom";
7 | import "@aws-amplify/ui-react/styles.css";
8 | import { XMarkIcon } from "@heroicons/react/16/solid";
9 |
10 | function Submit() {
11 | const { pending } = useFormStatus();
12 | return (
13 |
14 | {pending ? "Submitting..." : "Submit"}
15 |
16 | );
17 | }
18 |
19 | function Form({ action }: { action: (formData: FormData) => void }) {
20 | const ref = useRef(null);
21 |
22 | return (
23 |
82 | );
83 | }
84 |
85 | export default function ExpenseModal({
86 | isOpen,
87 | onClose,
88 | }: {
89 | isOpen: boolean;
90 | onClose: () => void;
91 | }) {
92 | if (!isOpen) return null;
93 |
94 | return (
95 |
96 |
97 |
98 |
Let's add your expenses!
99 |
104 | Close
105 |
106 |
107 |
108 |
109 |
110 | );
111 | }
112 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
4 | documentation, we greatly value feedback and contributions from our community.
5 |
6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
7 | information to effectively respond to your bug report or contribution.
8 |
9 |
10 | ## Reporting Bugs/Feature Requests
11 |
12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
13 |
14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
16 |
17 | * A reproducible test case or series of steps
18 | * The version of our code being used
19 | * Any modifications you've made relevant to the bug
20 | * Anything unusual about your environment or deployment
21 |
22 |
23 | ## Contributing via Pull Requests
24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
25 |
26 | 1. You are working against the latest source on the *main* branch.
27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
29 |
30 | To send us a pull request, please:
31 |
32 | 1. Fork the repository.
33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
34 | 3. Ensure local tests pass.
35 | 4. Commit to your fork using clear commit messages.
36 | 5. Send us a pull request, answering any default questions in the pull request interface.
37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
38 |
39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
41 |
42 |
43 | ## Finding contributions to work on
44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.
45 |
46 |
47 | ## Code of Conduct
48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
50 | opensource-codeofconduct@amazon.com with any additional questions or comments.
51 |
52 |
53 | ## Security issue notifications
54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
55 |
56 |
57 | ## Licensing
58 |
59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
60 |
--------------------------------------------------------------------------------
/src/components/SalesItem.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import {
5 | BadgeDelta,
6 | Button,
7 | Card,
8 | DonutChart,
9 | Flex,
10 | TabGroup,
11 | Tab,
12 | TabList,
13 | Bold,
14 | Divider,
15 | List,
16 | ListItem,
17 | Metric,
18 | Text,
19 | Title,
20 | } from "@tremor/react";
21 | import {
22 | ArrowRightIcon,
23 | ChartPieIcon,
24 | ListBulletIcon,
25 | } from "@heroicons/react/16/solid";
26 | import { dataFormatter } from "../utils/utils";
27 | import type { Schema } from "@/amplify/data/resource";
28 |
29 | type Expenses = Schema["Expenses"]["type"];
30 |
31 | export default function SalesItem({
32 | expenses,
33 | month,
34 | }: {
35 | expenses: Expenses[];
36 | month: string;
37 | }) {
38 | console.log(expenses);
39 | const [selectedIndex, setSelectedIndex] = useState(0);
40 | const expenseTotal = expenses.reduce(
41 | (acc, expense) => acc + expense.expenseAmount,
42 | 0
43 | );
44 | const categories = expenses.map((expense) => expense.expenseCategory);
45 | const uniqueCategoriesSet = new Set(categories);
46 | const uniqueCategories = Array.from(uniqueCategoriesSet);
47 |
48 | const groupedAndSummedExpenses = Object.values(
49 | expenses.reduce((acc, expense) => {
50 | const { expenseCategory, expenseAmount } = expense;
51 | if (!acc[expenseCategory]) {
52 | acc[expenseCategory] = { ...expense, expenseAmount: 0 };
53 | }
54 | acc[expenseCategory].expenseAmount += expenseAmount;
55 | return acc;
56 | }, {} as Record)
57 | );
58 |
59 | console.log(groupedAndSummedExpenses);
60 |
61 | return (
62 |
63 |
64 | Overview
65 |
66 |
67 | Chart
68 | List
69 |
70 |
71 |
72 | Total Expenses Value for 2024
73 | {dataFormatter(expenseTotal)}
74 |
75 |
76 | Expense Allocation
77 |
78 | {uniqueCategories.length} Categories
79 | {selectedIndex === 0 ? (
80 |
88 | ) : (
89 | <>
90 |
91 |
92 | Expenses
93 |
94 | Amount
95 |
96 |
97 | {expenses.map((expense) => (
98 |
99 | {expense.expenseName}
100 |
101 |
102 | {/* $ {Intl.NumberFormat("us").format(stock.value).toString()} */}
103 | {expense.expenseAmount}
104 |
105 |
106 |
107 | ))}
108 |
109 | >
110 | )}
111 | {/*
112 |
118 | View more
119 |
120 | */}
121 |
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/src/app/page.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | align-items: center;
6 | padding: 6rem;
7 | min-height: 100vh;
8 | }
9 |
10 | .description {
11 | display: inherit;
12 | justify-content: inherit;
13 | align-items: inherit;
14 | font-size: 0.85rem;
15 | max-width: var(--max-width);
16 | width: 100%;
17 | z-index: 2;
18 | font-family: var(--font-mono);
19 | }
20 |
21 | .description a {
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 | gap: 0.5rem;
26 | }
27 |
28 | .description p {
29 | position: relative;
30 | margin: 0;
31 | padding: 1rem;
32 | background-color: rgba(var(--callout-rgb), 0.5);
33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3);
34 | border-radius: var(--border-radius);
35 | }
36 |
37 | .code {
38 | font-weight: 700;
39 | font-family: var(--font-mono);
40 | }
41 |
42 | .grid {
43 | display: grid;
44 | grid-template-columns: repeat(4, minmax(25%, auto));
45 | max-width: 100%;
46 | width: var(--max-width);
47 | }
48 |
49 | .card {
50 | padding: 1rem 1.2rem;
51 | border-radius: var(--border-radius);
52 | background: rgba(var(--card-rgb), 0);
53 | border: 1px solid rgba(var(--card-border-rgb), 0);
54 | transition: background 200ms, border 200ms, box-shadow 200ms;
55 | }
56 |
57 | .card span {
58 | display: inline-block;
59 | transition: transform 200ms;
60 | }
61 |
62 | .card h2 {
63 | font-weight: 600;
64 | margin-bottom: 0.7rem;
65 | }
66 |
67 | .card p {
68 | margin: 0;
69 | opacity: 0.6;
70 | font-size: 0.9rem;
71 | line-height: 1.5;
72 | max-width: 30ch;
73 | }
74 |
75 | .center {
76 | display: flex;
77 | justify-content: center;
78 | align-items: center;
79 | position: relative;
80 | padding: 4rem 0;
81 | gap: 4rem;
82 | }
83 |
84 | /* Enable hover only on non-touch devices */
85 | @media (hover: hover) and (pointer: fine) {
86 | .card:hover {
87 | background: rgba(var(--card-rgb), 0.1);
88 | border: 1px solid rgba(var(--card-border-rgb), 0.15);
89 | box-shadow: 0px 4px 12px 0px #CBBEFF;
90 | }
91 |
92 | .card:hover span {
93 | transform: translateX(4px);
94 | }
95 |
96 | @media (prefers-color-scheme: dark) {
97 | .card:hover {
98 | box-shadow: none;
99 | }
100 | }
101 | }
102 |
103 | @media (prefers-reduced-motion) {
104 | .card:hover span {
105 | transform: none;
106 | }
107 | }
108 |
109 | /* Mobile */
110 | @media (max-width: 700px) {
111 | .content {
112 | padding: 4rem;
113 | }
114 |
115 | .grid {
116 | grid-template-columns: 1fr;
117 | margin-bottom: 120px;
118 | max-width: 320px;
119 | text-align: center;
120 | }
121 |
122 | .card {
123 | padding: 1rem 2.5rem;
124 | }
125 |
126 | .card h2 {
127 | margin-bottom: 0.5rem;
128 | }
129 |
130 | .center {
131 | padding: 8rem 0 6rem;
132 | }
133 |
134 | .center::before {
135 | transform: none;
136 | height: 300px;
137 | }
138 |
139 | .description {
140 | font-size: 0.8rem;
141 | }
142 |
143 | .description a {
144 | padding: 1rem;
145 | }
146 |
147 | .description p,
148 | .description div {
149 | display: flex;
150 | justify-content: center;
151 | position: fixed;
152 | width: 100%;
153 | }
154 |
155 | .description p {
156 | align-items: center;
157 | inset: 0 0 auto;
158 | padding: 2rem 1rem 1.4rem;
159 | border-radius: 0;
160 | border: none;
161 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
162 | background: linear-gradient(
163 | to bottom,
164 | rgba(var(--background-start-rgb), 1),
165 | rgba(var(--callout-rgb), 0.5)
166 | );
167 | background-clip: padding-box;
168 | backdrop-filter: blur(24px);
169 | }
170 |
171 | .description div {
172 | align-items: flex-end;
173 | pointer-events: none;
174 | inset: auto 0 0;
175 | padding: 2rem;
176 | height: 200px;
177 | background: linear-gradient(
178 | to bottom,
179 | transparent 0%,
180 | rgb(var(--background-end-rgb)) 40%
181 | );
182 | z-index: 1;
183 | }
184 | }
185 |
186 | /* Tablet and Smaller Desktop */
187 | @media (min-width: 701px) and (max-width: 1120px) {
188 | .grid {
189 | grid-template-columns: repeat(2, 50%);
190 | }
191 | }
192 |
193 | @media (prefers-color-scheme: dark) {
194 | .logo {
195 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
196 | }
197 | }
198 |
199 |
200 | @keyframes rotate {
201 | from {
202 | transform: rotate(360deg);
203 | }
204 | to {
205 | transform: rotate(0deg);
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 | import colors from "tailwindcss/colors";
3 |
4 | const config: Config = {
5 | content: [
6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
9 |
10 | // Path to Tremor module
11 | "./node_modules/@tremor/**/*.{js,ts,jsx,tsx}",
12 | ],
13 | theme: {
14 | transparent: "transparent",
15 | current: "currentColor",
16 | extend: {
17 | colors: {
18 | // light mode
19 | tremor: {
20 | brand: {
21 | faint: colors.blue[50],
22 | muted: colors.blue[200],
23 | subtle: colors.blue[400],
24 | DEFAULT: colors.blue[500],
25 | emphasis: colors.blue[700],
26 | inverted: colors.white,
27 | },
28 | background: {
29 | muted: colors.gray[50],
30 | subtle: colors.gray[100],
31 | DEFAULT: colors.white,
32 | emphasis: colors.gray[700],
33 | },
34 | border: {
35 | DEFAULT: colors.gray[200],
36 | },
37 | ring: {
38 | DEFAULT: colors.gray[200],
39 | },
40 | content: {
41 | subtle: colors.gray[400],
42 | DEFAULT: colors.gray[500],
43 | emphasis: colors.gray[700],
44 | strong: colors.gray[900],
45 | inverted: colors.white,
46 | },
47 | },
48 | // dark mode
49 | "dark-tremor": {
50 | brand: {
51 | faint: "#0B1229",
52 | muted: colors.blue[950],
53 | subtle: colors.blue[800],
54 | DEFAULT: colors.blue[500],
55 | emphasis: colors.blue[400],
56 | inverted: colors.blue[950],
57 | },
58 | background: {
59 | muted: "#131A2B",
60 | subtle: colors.gray[800],
61 | DEFAULT: colors.gray[900],
62 | emphasis: colors.gray[300],
63 | },
64 | border: {
65 | DEFAULT: colors.gray[800],
66 | },
67 | ring: {
68 | DEFAULT: colors.gray[800],
69 | },
70 | content: {
71 | subtle: colors.gray[600],
72 | DEFAULT: colors.gray[500],
73 | emphasis: colors.gray[200],
74 | strong: colors.gray[50],
75 | inverted: colors.gray[950],
76 | },
77 | },
78 | },
79 | boxShadow: {
80 | // light
81 | "tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
82 | "tremor-card":
83 | "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
84 | "tremor-dropdown":
85 | "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
86 | // dark
87 | "dark-tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
88 | "dark-tremor-card":
89 | "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
90 | "dark-tremor-dropdown":
91 | "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
92 | },
93 | borderRadius: {
94 | "tremor-small": "0.375rem",
95 | "tremor-default": "0.5rem",
96 | "tremor-full": "9999px",
97 | },
98 | fontSize: {
99 | "tremor-label": ["0.75rem", { lineHeight: "1rem" }],
100 | "tremor-default": ["0.875rem", { lineHeight: "1.25rem" }],
101 | "tremor-title": ["1.125rem", { lineHeight: "1.75rem" }],
102 | "tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }],
103 | },
104 | },
105 | },
106 | safelist: [
107 | {
108 | pattern:
109 | /^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
110 | variants: ["hover", "ui-selected"],
111 | },
112 | {
113 | pattern:
114 | /^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
115 | variants: ["hover", "ui-selected"],
116 | },
117 | {
118 | pattern:
119 | /^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
120 | variants: ["hover", "ui-selected"],
121 | },
122 | {
123 | pattern:
124 | /^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
125 | },
126 | {
127 | pattern:
128 | /^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
129 | },
130 | {
131 | pattern:
132 | /^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
133 | },
134 | ],
135 | plugins: [require("@headlessui/tailwindcss"), require("@tailwindcss/forms")],
136 | };
137 |
138 | export default config;
139 |
--------------------------------------------------------------------------------
/src/components/WebAnalytics.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { BarList, Card, Title, Bold, Flex, Text } from "@tremor/react";
4 |
5 | const data = [
6 | {
7 | name: "Twitter",
8 | value: 456,
9 | href: "https://twitter.com/tremorlabs",
10 | icon: function TwitterIcon() {
11 | return (
12 |
19 |
20 |
21 |
22 | );
23 | },
24 | },
25 | {
26 | name: "Google",
27 | value: 351,
28 | href: "https://google.com",
29 | icon: function GoogleIcon() {
30 | return (
31 |
38 |
39 |
40 |
41 | );
42 | },
43 | },
44 | {
45 | name: "GitHub",
46 | value: 271,
47 | href: "https://github.com/tremorlabs/tremor",
48 | icon: function GitHubIcon() {
49 | return (
50 |
57 |
58 |
59 |
60 | );
61 | },
62 | },
63 | {
64 | name: "Reddit",
65 | value: 191,
66 | href: "https://reddit.com",
67 | icon: function RedditIcon() {
68 | return (
69 |
76 |
77 |
78 |
79 | );
80 | },
81 | },
82 | {
83 | name: "Youtube",
84 | value: 91,
85 | href: "https://www.youtube.com/@tremorlabs3079",
86 | icon: function YouTubeIcon() {
87 | return (
88 |
95 |
96 |
97 |
98 | );
99 | },
100 | },
101 | ];
102 |
103 | const WebAnalytics = () => {
104 | return (
105 |
106 | Website Analytics
107 |
108 |
109 | Source
110 |
111 |
112 | Visits
113 |
114 |
115 |
116 |
117 | );
118 | };
119 |
120 | export default WebAnalytics;
121 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Dialog } from "@headlessui/react";
4 | import { useState } from "react";
5 | import { XMarkIcon, Bars3Icon } from "@heroicons/react/16/solid";
6 |
7 | export default function App() {
8 | const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
9 |
10 | return (
11 |
12 |
80 |
81 |
82 |
94 |
95 |
96 |
97 | Where you go to manage your money.
98 |
99 |
100 |
101 |
102 | Personal Finance Tracking Made Easy
103 |
104 |
105 | Budgeting and tracking your expenses has never been easier. With
106 | Spot Me, you can easily manage your finances and get a clear view
107 | of your spending habits.
108 |
109 |
117 |
118 |
119 |
131 |
132 |
133 | );
134 | }
135 |
--------------------------------------------------------------------------------