Skip to content

Commit 175c5ed

Browse files
committed
🌟feat: Added currency settings, live conversion & app customization (#v2.2.0)
- Introduced currency selection in settings with persistent storage - Added live currency conversion for accurate financial tracking - Implemented ENV-based default currency for flexibility - Enabled custom app width selection (Compact / Normal mode) - Assigned predefined colors to categories for better transaction visuals - Styled transactions with colored backgrounds for improved readability - Fixed date picker crash when clearing the date (resets to first of the month) - Optimized color assignment and transaction rendering performance
1 parent 83f4c71 commit 175c5ed

19 files changed

Lines changed: 671 additions & 136 deletions

File tree

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ OopsBudgeter is a personal finance management app designed to help users track t
88
###### Please use it for test purposes **only**, check the UI, features and abilities given to the web app or pwa. The PIN code is 696969
99
[budget.oopsapps.tech](https://budget.oopsapps.tech/)
1010

11-
##### A Simple Roadmap for this Project: https://trello.com/b/rprhjhY9
11+
##### A Simple Roadmap for this Project: [Roadmap via Github](https://github.com/orgs/OopsApps/projects/3)
1212

1313
## Features
1414

@@ -45,8 +45,6 @@ OopsBudgeter is a personal finance management app designed to help users track t
4545
## Installation
4646

4747
### Deploy via Vercel (easiest)
48-
1. Afer deploying with the button, change the build command to `npx drizzle-kit push && next build`
49-
2. There, it will update the database with the correct schema.
5048

5149
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FOopsApps%2FOopsBudgeter%2F&env=NEXT_PUBLIC_CURRENCY,PASSCODE,JWT_SECRET,DATABASE_URL&project-name=oopsbudgeter&repository-name=oopsbudgeter&redirect-url=https%3A%2F%2Fgithub.com%2FOopsApps%2FOopsBudgeter&production-deploy-hook=Github)
5250

bun.lockb

0 Bytes
Binary file not shown.

changelog.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
# OopsBudgeter Changelog
22

3+
## v2.2.0 - Smart Settings & Dynamic Currency 💰🌍
4+
**Released: March 17, 2025**
5+
6+
**Take Control of Your Budget, Your Way!**
7+
This update introduces powerful new settings, including custom currency selection with live conversion, dynamic app width customization, and vibrant transaction visuals—making your budget smarter and more intuitive than ever before!
8+
9+
## ✨ New & Improved
10+
### 💰 Dynamic Currency Selection & Live Conversion
11+
- Set your preferred currency in the new Settings Menu, and it will persist per device.
12+
- If a default currency is needed, ENV configuration allows for a global default.
13+
- Live currency conversion ensures that your amounts stay accurate across different currencies!
14+
- Example: If you add 10 USD on USD currency option and switch to EUR, it correctly converts and shows 9.20 EUR.
15+
- And, when adding 10 EUR on EUR currency option and switched back to USD, it shows $10.87 USD. Aka, live conversion with the help of something API I forgot T-T
16+
17+
### ⚙️ Fully Customizable App Width
18+
- Compact or Normal mode—choose the best fit for your screen from Settings.
19+
- Your preference persists per device, ensuring a seamless experience.
20+
21+
### 🎨 Colorful Categories & Transaction Backgrounds
22+
- Categories of each type now has a predefined, unique color, making them instantly recognizable (Red for expense and Green of income).
23+
- Transactions' colored backgrounds are now optional in the settings menu, but for now giving your financial history a vibrant, clean look with dark and minimal color.
24+
25+
### 🛠️ Bug Fixes & Improvements
26+
- Fixed Date Picker Crash: Clearing the date no longer results in an "Invalid Date" error or a full page crash. Now, it properly resets to the first day of the current month.
27+
- Optimized Color & Data Handling: Transactions load faster and smoother without flickering or mismatched colors... I guess, joking of course!!!!!!!!!!!
28+
29+
---
30+
331
## v2.1.0 - Balance Toggle ⚖️
432
**Released: March 15, 2025**
533

@@ -16,6 +44,18 @@ This update brings a brand-new Balance Mode Toggle, giving you full control over
1644
- Improved UI transitions when switching balance views.
1745
- Fixed a state reset issue when rapidly switching modes.
1846

47+
## v2.0.7 - More Automated 🚀
48+
**Released: March 15, 2025**
49+
50+
This update fine-tunes Docker handling and an even smoother self-hosting process. Now, deploying is **effortless**, and keeping your app up-to-date is a breeze 😏
51+
52+
### **New Features**
53+
- **💡 Automatic Docker Versioning**
54+
- Docker images now use the **version from `package.json`** for better tracking.
55+
- Every release is now **tagged properly**, no more overwriting images!
56+
57+
---
58+
1959
## v2.0.5 - Docker Optimization 🐋
2060
**Released: March 15, 2025**
2161

@@ -31,7 +71,6 @@ This update brings seamless Docker versioning, ensuring self-hosters and deploym
3171
- Forks & self-hosted deployments now work **out of the box**.
3272
- No more manual fixes—just deploy and enjoy!
3373

34-
3574
---
3675

3776
## v2.0.0 - The Ultimate Overhaul 🚀🔥

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "oops-budgeter",
3-
"version": "2.1.0",
3+
"version": "2.2.0",
44
"private": true,
55
"scripts": {
66
"dev": "next dev --turbopack -p 3031",

src/app/api/auth/itsme/route.ts

Lines changed: 0 additions & 49 deletions
This file was deleted.

src/app/api/auth/login/route.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,38 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { NextResponse } from "next/server";
18+
import { NextRequest, NextResponse } from "next/server";
1919
import jwt from "jsonwebtoken";
2020
import { cookies } from "next/headers";
2121

2222
const SECRET = process.env.JWT_SECRET as string;
2323
const PASSCODE = process.env.PASSCODE as string;
2424

25+
export async function verifyToken(req: NextRequest) {
26+
const cookieStore = await cookies();
27+
const tokenFromCookie = cookieStore.get("authToken")?.value;
28+
29+
const authHeader = req.headers.get("Authorization");
30+
const tokenFromHeader =
31+
authHeader && authHeader.startsWith("Bearer ")
32+
? authHeader.split(" ")[1]
33+
: null;
34+
35+
const token = tokenFromHeader || tokenFromCookie;
36+
37+
if (!token) return { authorized: false, error: "Unauthorized" };
38+
39+
try {
40+
const decoded = jwt.verify(token, SECRET);
41+
return { authorized: true, user: decoded };
42+
} catch (err) {
43+
return {
44+
authorized: false,
45+
error: `Invalid or expired token, ${(err as Error).message}`,
46+
};
47+
}
48+
}
49+
2550
export async function POST(request: Request) {
2651
try {
2752
const cookieStore = await cookies();

src/app/api/transactions/route.ts

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,37 +18,9 @@
1818
import { db } from "@/lib/db";
1919
import { transactions } from "@/schema/dbSchema";
2020
import { eq } from "drizzle-orm";
21-
import { cookies } from "next/headers";
2221
import { NextRequest, NextResponse } from "next/server";
23-
import jwt from "jsonwebtoken";
2422
import { expenseCategories, incomeCategories } from "@/lib/categories";
25-
26-
const SECRET = process.env.JWT_SECRET as string;
27-
28-
async function verifyToken(req: NextRequest) {
29-
const cookieStore = await cookies();
30-
const tokenFromCookie = cookieStore.get("authToken")?.value;
31-
32-
const authHeader = req.headers.get("Authorization");
33-
const tokenFromHeader =
34-
authHeader && authHeader.startsWith("Bearer ")
35-
? authHeader.split(" ")[1]
36-
: null;
37-
38-
const token = tokenFromHeader || tokenFromCookie;
39-
40-
if (!token) return { authorized: false, error: "Unauthorized" };
41-
42-
try {
43-
const decoded = jwt.verify(token, SECRET);
44-
return { authorized: true, user: decoded };
45-
} catch (err) {
46-
return {
47-
authorized: false,
48-
error: `Invalid or expired token, ${(err as Error).message}`,
49-
};
50-
}
51-
}
23+
import { verifyToken } from "../auth/login/route";
5224

5325
export async function GET(req: NextRequest) {
5426
const { authorized, user, error } = await verifyToken(req);

src/app/layout.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import Toaster from "@/components/effects/Sonner";
2525
import { ThemeToggle } from "@/components/common/ThemeToggle";
2626
import Logo from "@/components/common/Logo";
2727
import { generateMetadata } from "@/lib/head";
28+
import { Settings } from "@/components/common/Settings";
29+
import PageLayout from "@/components/helpers/PageLayout";
2830

2931
const geistSans = Geist({
3032
variable: "--font-geist-sans",
@@ -70,11 +72,12 @@ export default function RootLayout({
7072
<PasscodeWrapper>
7173
<BudgetProvider>
7274
<main className="p-0 md:p-6">
73-
<div className="relative flex flex-col justify-center items-center gap-4 bg-secondary p-6 max-w-2xl min-w-svw md:min-w-2xl md:rounded-lg">
75+
<PageLayout>
7476
<Logo />
77+
<Settings />
7578
<ThemeToggle />
7679
{children}
77-
</div>
80+
</PageLayout>
7881
</main>
7982
<GoToTop />
8083
<Toaster />

src/components/common/Analytics.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,21 @@ import {
2929
predictNextMonthSpending,
3030
} from "@/lib/my1DollarAI";
3131

32+
export const chartColors = [
33+
"#2DAC64",
34+
"#E24444",
35+
"#FFB02E",
36+
"#0088FE",
37+
"#FF6347",
38+
"#A155B9",
39+
"#45C4B0",
40+
"#E4D374",
41+
];
42+
3243
export default function AnalyticsWrapper() {
3344
const { transactions, filteredTransactions } = useBudget();
3445
const monthlyTrend = getMonthlyTrends(transactions);
3546

36-
const COLORS = [
37-
"#2DAC64",
38-
"#E24444",
39-
"#FFB02E",
40-
"#0088FE",
41-
"#FF6347",
42-
"#A155B9",
43-
"#45C4B0",
44-
"#E4D374",
45-
];
46-
4747
const incomeData = transactions.filter((trx) => trx.type === "income");
4848
const expenseData = transactions.filter((trx) => trx.type === "expense");
4949

@@ -65,7 +65,7 @@ export default function AnalyticsWrapper() {
6565
([category, amount], index) => ({
6666
name: category,
6767
value: amount,
68-
color: COLORS[index % COLORS.length],
68+
color: chartColors[index % chartColors.length],
6969
})
7070
);
7171

@@ -400,8 +400,8 @@ export default function AnalyticsWrapper() {
400400
key={cat}
401401
dataKey={cat}
402402
stackId="1"
403-
stroke={COLORS[idx]}
404-
fill={COLORS[idx]}
403+
stroke={chartColors[idx]}
404+
fill={chartColors[idx]}
405405
/>
406406
))}
407407
</Recharts.AreaChart>

src/components/common/Currency.tsx

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,23 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17-
const currencyCode = process.env.NEXT_PUBLIC_CURRENCY || "USD";
17+
"use client";
18+
19+
import { useBudget } from "@/contexts/BudgetContext";
20+
import { useEffect, useState } from "react";
21+
22+
export const fetchExchangeRates = async (baseCurrency: string) => {
23+
try {
24+
const response = await fetch(
25+
`https://api.exchangerate-api.com/v4/latest/${baseCurrency}`
26+
);
27+
const data = await response.json();
28+
return data.rates || {};
29+
} catch (error) {
30+
console.error("Failed to fetch exchange rates:", error);
31+
return {};
32+
}
33+
};
1834

1935
export const formatCurrency = (amount: number, currency: string) => {
2036
return new Intl.NumberFormat("en-US", {
@@ -30,7 +46,48 @@ export default function PriceDisplay({
3046
amount: number;
3147
className?: string;
3248
}) {
33-
const currency = currencyCode;
49+
const { currency } = useBudget();
50+
const [exchangeRates, setExchangeRates] = useState<{ [key: string]: number }>(
51+
{}
52+
);
53+
const [convertedAmount, setConvertedAmount] = useState(amount);
54+
const correctCurrency =
55+
currency.length !== 3 ? "USD" : currency.toUpperCase();
56+
57+
useEffect(() => {
58+
const fetchRates = async () => {
59+
const rates = await fetchExchangeRates("USD");
60+
setExchangeRates(rates);
61+
};
62+
63+
fetchRates();
64+
}, []);
65+
66+
useEffect(() => {
67+
if (!exchangeRates || Object.keys(exchangeRates).length === 0) {
68+
return;
69+
}
70+
71+
console.log(exchangeRates[correctCurrency]);
72+
if (exchangeRates[correctCurrency]) {
73+
const baseRate = exchangeRates["USD"] || 1;
74+
const targetRate = exchangeRates[correctCurrency] || 1;
75+
76+
if (!baseRate || !targetRate) {
77+
console.warn("Missing exchange rate for:", correctCurrency);
78+
setConvertedAmount(amount);
79+
return;
80+
}
81+
82+
setConvertedAmount((amount / baseRate) * targetRate);
83+
} else {
84+
setConvertedAmount(amount);
85+
}
86+
}, [currency, exchangeRates, amount, correctCurrency]);
3487

35-
return <span className={className}>{formatCurrency(amount, currency)}</span>;
88+
return (
89+
<span className={className}>
90+
{formatCurrency(convertedAmount, correctCurrency)}
91+
</span>
92+
);
3693
}

0 commit comments

Comments
 (0)