├── 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 ; 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 | 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 |
42 | 46 |
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 |
38 | 42 | 43 |
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 | 16 | ); 17 | } 18 | 19 | function Form({ action }: { action: (formData: FormData) => void }) { 20 | const ref = useRef(null); 21 | 22 | return ( 23 |
{ 26 | await action(formData); 27 | ref.current?.reset(); 28 | }} 29 | > 30 | 31 | 32 | 33 | 34 | 35 | 36 | 55 | 56 | 57 | 58 |
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 |
31 | 32 | 36 | 37 |
38 |
39 | 40 | 44 | 45 | 50 | 55 | {/* 59 | */} 63 |
64 |
65 | 66 | 70 | 71 | {/* */} 75 |
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 | 28 | ); 29 | } 30 | 31 | function Form({ action }: { action: (formData: FormData) => void }) { 32 | const ref = useRef(null); 33 | 34 | return ( 35 | { 38 | await action(formData); 39 | ref.current?.reset(); 40 | }} 41 | > 42 | 43 | 44 | 47 | 53 | 54 | 55 | 66 | 67 | 68 | 69 | 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 | 16 | ); 17 | } 18 | 19 | function Form({ action }: { action: (formData: FormData) => void }) { 20 | const ref = useRef(null); 21 | 22 | return ( 23 | { 26 | await action(formData); 27 | ref.current?.reset(); 28 | }} 29 | > 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 43 | 44 | 45 | 57 | 58 | 59 | 78 | 79 | 80 | 81 | 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 | 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 |
13 | 43 | 48 |
49 | 50 |
51 | 52 | 53 | spot me. 54 | 55 | 56 | 64 |
65 |
66 |
67 | 75 |
76 |
77 |
78 |
79 |
80 | 81 |
82 |
133 | ); 134 | } 135 | --------------------------------------------------------------------------------