Skip to content

Commit 9009aa4

Browse files
committed
Minor UI improvements
1 parent ebf5341 commit 9009aa4

5 files changed

Lines changed: 231 additions & 354 deletions

File tree

currency-converter/app.py

Lines changed: 88 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,123 @@
1-
from flask import Flask, render_template, request
2-
import requests
1+
from flask import Flask, render_template, request, jsonify, make_response
2+
import requests, os, time
33
import pycountry
4-
import os
4+
from collections import defaultdict
55
from dotenv import load_dotenv
66

77
load_dotenv()
8-
98
app = Flask(__name__)
109

11-
# =========================
12-
# API CONFIG
13-
# =========================
1410
API_KEY = os.getenv("API_KEY")
15-
if not API_KEY:
16-
raise ValueError("API key not found")
17-
1811
API_URL = f"https://v6.exchangerate-api.com/v6/{API_KEY}/pair"
1912

20-
# =========================
21-
# CURRENCIES LIST
22-
# =========================
13+
# ======================
14+
# DATA
15+
# ======================
2316
CURRENCIES = sorted([
24-
(c.alpha_3, c.name) for c in pycountry.currencies
25-
if hasattr(c, 'alpha_3') and hasattr(c, 'name')
17+
(c.alpha_3, c.name)
18+
for c in pycountry.currencies
19+
if hasattr(c, "alpha_3")
2620
])
2721

28-
# =========================
29-
# FLAG + COUNTRY MAPPINGS
30-
# =========================
3122
CURRENCY_TO_FLAG = {
32-
"USD": "us",
33-
"EUR": "eu",
34-
"GBP": "gb",
35-
"JPY": "jp",
36-
"CHF": "ch",
37-
"CAD": "ca",
38-
"AUD": "au",
39-
"CNY": "cn",
40-
"INR": "in",
23+
"USD": "us", "EUR": "eu", "GBP": "gb", "JPY": "jp",
24+
"CHF": "ch", "CAD": "ca", "AUD": "au", "CNY": "cn", "INR": "in"
4125
}
4226

43-
COUNTRY_TO_CURRENCY = {
44-
"US": "USD",
45-
"GB": "GBP",
46-
"JP": "JPY",
47-
"CH": "CHF",
48-
"CA": "CAD",
49-
"AU": "AUD",
50-
"CN": "CNY",
51-
"IN": "INR",
52-
}
53-
54-
EU_COUNTRIES = {
27+
EU = {
5528
"GR","DE","FR","IT","ES","NL","BE","PT","AT","FI","IE",
5629
"CY","EE","LV","LT","LU","MT","SI","SK","HR"
5730
}
5831

59-
# =========================
60-
# HELPERS
61-
# =========================
62-
def detect_user_currency():
63-
try:
64-
response = requests.get(
65-
"https://ipapi.co/json/",
66-
timeout=3,
67-
headers={"User-Agent": "CurrencyConverter"}
68-
)
69-
data = response.json()
32+
# ======================
33+
# CACHE & RATE LIMIT
34+
# ======================
35+
RATE_CACHE = {}
36+
CACHE_TTL = 600 # 10 minutes
7037

71-
country = data.get("country_code")
38+
REQUESTS = defaultdict(list)
39+
LIMIT = 30 # per IP / minute
7240

73-
if country in EU_COUNTRIES:
41+
# ======================
42+
# HELPERS
43+
# ======================
44+
def rate_limited(ip):
45+
now = time.time()
46+
REQUESTS[ip] = [t for t in REQUESTS[ip] if now - t < 60]
47+
if len(REQUESTS[ip]) >= LIMIT:
48+
return True
49+
REQUESTS[ip].append(now)
50+
return False
51+
52+
def detect_currency():
53+
try:
54+
data = requests.get("https://ipapi.co/json/", timeout=3).json()
55+
cc = data.get("country_code")
56+
if cc in EU:
7457
return "EUR"
58+
return data.get("currency", "USD")
59+
except:
60+
return "USD"
7561

76-
return COUNTRY_TO_CURRENCY.get(country, "USD")
62+
def get_rate(frm, to):
63+
key = (frm, to)
64+
now = time.time()
7765

78-
except Exception:
79-
return "USD"
66+
if key in RATE_CACHE and now - RATE_CACHE[key]["time"] < CACHE_TTL:
67+
return RATE_CACHE[key]["rate"]
8068

81-
def currency_to_flag(currency):
82-
return CURRENCY_TO_FLAG.get(currency, "generic")
69+
r = requests.get(f"{API_URL}/{frm}/{to}", timeout=5).json()
70+
if r.get("result") != "success":
71+
raise ValueError("API error")
8372

84-
# =========================
73+
RATE_CACHE[key] = {
74+
"rate": r["conversion_rate"],
75+
"time": now
76+
}
77+
return r["conversion_rate"]
78+
79+
# ======================
8580
# ROUTES
86-
# =========================
87-
@app.route('/')
81+
# ======================
82+
@app.route("/")
8883
def index():
89-
user_currency = detect_user_currency()
84+
from_cur = request.cookies.get("from") or detect_currency()
85+
to_cur = request.cookies.get("to") or ("USD" if from_cur == "EUR" else "EUR")
9086

9187
return render_template(
9288
"index.html",
9389
currencies=CURRENCIES,
94-
from_currency=user_currency,
95-
to_currency="EUR" if user_currency != "EUR" else "USD",
96-
from_currency_flag=currency_to_flag(user_currency),
97-
to_currency_flag=currency_to_flag(
98-
"EUR" if user_currency != "EUR" else "USD"
99-
)
90+
from_currency=from_cur,
91+
to_currency=to_cur,
92+
from_flag=CURRENCY_TO_FLAG.get(from_cur, "generic"),
93+
to_flag=CURRENCY_TO_FLAG.get(to_cur, "generic")
10094
)
10195

102-
@app.route('/convert', methods=['POST'])
96+
@app.route("/convert", methods=["POST"])
10397
def convert():
104-
try:
105-
amount = float(request.form['amount'])
106-
from_currency = request.form['from_currency']
107-
to_currency = request.form['to_currency']
108-
109-
response = requests.get(
110-
f"{API_URL}/{from_currency}/{to_currency}",
111-
timeout=5
112-
)
113-
data = response.json()
114-
115-
if data.get('result') == 'success':
116-
converted = round(amount * data['conversion_rate'], 2)
117-
result = f"{amount} {from_currency} = {converted} {to_currency}"
118-
else:
119-
result = "Conversion failed."
120-
121-
except Exception as e:
122-
result = f"Error: {e}"
123-
124-
return render_template(
125-
"index.html",
126-
currencies=CURRENCIES,
127-
result=result,
128-
from_currency=from_currency,
129-
to_currency=to_currency,
130-
from_currency_flag=currency_to_flag(from_currency),
131-
to_currency_flag=currency_to_flag(to_currency)
132-
)
98+
ip = request.remote_addr
99+
if rate_limited(ip):
100+
return jsonify({"error": "Too many requests"}), 429
101+
102+
data = request.json
103+
amount = float(data["amount"])
104+
frm = data["from"]
105+
to = data["to"]
106+
107+
rate = get_rate(frm, to)
108+
result = round(amount * rate, 2)
109+
110+
resp = make_response(jsonify({
111+
"result": result,
112+
"rate": rate,
113+
"from_flag": CURRENCY_TO_FLAG.get(frm, "generic"),
114+
"to_flag": CURRENCY_TO_FLAG.get(to, "generic")
115+
}))
116+
117+
resp.set_cookie("from", frm, max_age=86400 * 30)
118+
resp.set_cookie("to", to, max_age=86400 * 30)
119+
return resp
120+
121+
@app.route("/health")
122+
def health():
123+
return jsonify({"status": "ok"})
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const amount = document.getElementById("amount");
2+
const from = document.getElementById("from");
3+
const to = document.getElementById("to");
4+
const result = document.getElementById("result");
5+
const rateBox = document.getElementById("rate");
6+
7+
document.getElementById("convert").onclick = convert;
8+
document.getElementById("swap").onclick = () => {
9+
[from.value, to.value] = [to.value, from.value];
10+
convert();
11+
};
12+
13+
amount.addEventListener("keydown", e => {
14+
if (e.key === "Enter") convert();
15+
});
16+
17+
async function convert() {
18+
if (!amount.value) return;
19+
20+
const res = await fetch("/convert", {
21+
method:"POST",
22+
headers:{ "Content-Type":"application/json" },
23+
body:JSON.stringify({
24+
amount: amount.value,
25+
from: from.value,
26+
to: to.value
27+
})
28+
});
29+
30+
if (!res.ok) {
31+
result.textContent = "Rate limit reached.";
32+
return;
33+
}
34+
35+
const data = await res.json();
36+
result.textContent = `${amount.value} ${from.value} = ${data.result} ${to.value}`;
37+
rateBox.textContent = `1 ${from.value} = ${data.rate} ${to.value}`;
38+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
body {
2+
background: linear-gradient(135deg,#020617,#0f172a);
3+
font-family: system-ui;
4+
color: #e5e7eb;
5+
display:flex;
6+
justify-content:center;
7+
align-items:center;
8+
height:100vh;
9+
}
10+
11+
.container {
12+
background:#020617cc;
13+
padding:2rem;
14+
border-radius:16px;
15+
width:380px;
16+
box-shadow:0 20px 50px #000;
17+
}
18+
19+
h1 { text-align:center; margin-bottom:1rem; }
20+
21+
input, select, button {
22+
width:100%;
23+
padding:0.7rem;
24+
border-radius:10px;
25+
background:#020617;
26+
color:#e5e7eb;
27+
border:1px solid #1e293b;
28+
}
29+
30+
.row {
31+
display:flex;
32+
align-items:center;
33+
gap:0.5rem;
34+
margin:1rem 0;
35+
}
36+
37+
button {
38+
background:linear-gradient(135deg,#38bdf8,#2563eb);
39+
border:none;
40+
cursor:pointer;
41+
}
42+
43+
#swap {
44+
width:42px;
45+
font-size:1.2rem;
46+
}
47+
48+
.flag {
49+
position:relative;
50+
flex:1;
51+
}
52+
53+
.flag::before {
54+
content:"";
55+
position:absolute;
56+
left:10px;
57+
top:50%;
58+
transform:translateY(-50%);
59+
width:20px;
60+
height:14px;
61+
background-size:cover;
62+
}
63+
64+
.flag select { padding-left:40px; }
65+
66+
.flag-us::before{background-image:url("https://flagcdn.com/w40/us.png")}
67+
.flag-eu::before{background-image:url("https://flagcdn.com/w40/eu.png")}
68+
.flag-gb::before{background-image:url("https://flagcdn.com/w40/gb.png")}
69+
.flag-jp::before{background-image:url("https://flagcdn.com/w40/jp.png")}
70+
.flag-generic::before{background-image:url("https://flagcdn.com/w40/un.png")}
71+
72+
#result,#rate { text-align:center; margin-top:1rem; }
73+
footer { text-align:center; font-size:.7rem; opacity:.6; margin-top:1rem; }

0 commit comments

Comments
 (0)