Skip to content

Commit ade832a

Browse files
authored
v0.4.9 - New Currencies & UX Improvements (#70)
* Add CHF (Swiss Franc) and BRL (Brazilian Real) currency support - Add CHF and BRL to SupportedCurrencies in currency service - Add currency options to settings page - Add currency options to subscription form dropdown Closes #63, Closes #66 * Fix subscription form issues and add CHF symbol - Fix #68: Status changes no longer reset renewal date - Fix #67: Add inline category creation from subscription form - Add Fr. symbol to CHF currency display Closes #67 Closes #68
1 parent a5b568c commit ade832a

File tree

3 files changed

+145
-14
lines changed

3 files changed

+145
-14
lines changed

internal/service/currency.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
)
1616

1717
// SupportedCurrencies defines the list of currencies supported for exchange rates and settings
18-
var SupportedCurrencies = []string{"USD", "EUR", "GBP", "JPY", "RUB", "SEK", "PLN", "INR"}
18+
var SupportedCurrencies = []string{"USD", "EUR", "GBP", "JPY", "RUB", "SEK", "PLN", "INR", "CHF", "BRL"}
1919

2020
// supportedCurrencySymbols returns the currencies as a comma-separated string for API calls
2121
func supportedCurrencySymbols() string {

templates/settings.html

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,8 +482,32 @@ <h3 class="text-base font-medium text-gray-900 dark:text-white mb-4">Currency</h
482482
class="mr-2 text-primary focus:ring-primary">
483483
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">₹ INR (Indian Rupee)</span>
484484
</label>
485+
486+
<label class="flex items-center">
487+
<input type="radio"
488+
name="currency"
489+
value="CHF"
490+
{{if eq .Currency "CHF"}}checked{{end}}
491+
hx-post="/api/settings/currency"
492+
hx-trigger="change"
493+
hx-vals='{"currency": "CHF"}'
494+
class="mr-2 text-primary focus:ring-primary">
495+
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">Fr. CHF (Swiss Franc)</span>
496+
</label>
497+
498+
<label class="flex items-center">
499+
<input type="radio"
500+
name="currency"
501+
value="BRL"
502+
{{if eq .Currency "BRL"}}checked{{end}}
503+
hx-post="/api/settings/currency"
504+
hx-trigger="change"
505+
hx-vals='{"currency": "BRL"}'
506+
class="mr-2 text-primary focus:ring-primary">
507+
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">R$ BRL (Brazilian Real)</span>
508+
</label>
485509
</div>
486-
510+
487511
<div id="currency-message" class="mt-2"></div>
488512
</div>
489513

templates/subscription-form.html

Lines changed: 119 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,40 @@ <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
2828
<!-- Category -->
2929
<div>
3030
<label for="category_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Category *</label>
31-
<select id="category_id" name="category_id" required
32-
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 transition-colors duration-150">
33-
<option value="">Select category</option>
34-
{{range .Categories}}
35-
<option value="{{.ID}}" {{if $.Subscription}}{{if eq $.Subscription.CategoryID .ID}}selected{{end}}{{end}}>{{.Name}}</option>
36-
{{end}}
37-
</select>
31+
<div class="flex gap-2">
32+
<div class="flex-1" id="category-select-container">
33+
<select id="category_id" name="category_id" required
34+
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 transition-colors duration-150">
35+
<option value="">Select category</option>
36+
{{range .Categories}}
37+
<option value="{{.ID}}" {{if $.Subscription}}{{if eq $.Subscription.CategoryID .ID}}selected{{end}}{{end}}>{{.Name}}</option>
38+
{{end}}
39+
</select>
40+
</div>
41+
<button type="button" id="add-category-btn" onclick="showNewCategoryInput()"
42+
class="px-3 py-2 text-sm font-medium text-primary bg-primary/10 border border-primary/30 rounded-lg hover:bg-primary/20 transition-colors duration-150"
43+
title="Add new category">
44+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
45+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
46+
</svg>
47+
</button>
48+
</div>
49+
<!-- Inline new category input (hidden by default) -->
50+
<div id="new-category-container" class="hidden mt-2">
51+
<div class="flex gap-2">
52+
<input type="text" id="new-category-name" placeholder="New category name"
53+
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 transition-colors duration-150">
54+
<button type="button" onclick="createNewCategory()"
55+
class="px-3 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors duration-150">
56+
Add
57+
</button>
58+
<button type="button" onclick="hideNewCategoryInput()"
59+
class="px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors duration-150">
60+
Cancel
61+
</button>
62+
</div>
63+
<div id="new-category-error" class="text-sm text-danger mt-1 hidden"></div>
64+
</div>
3865
</div>
3966

4067
<!-- Cost -->
@@ -61,6 +88,8 @@ <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
6188
<option value="SEK" {{if .Subscription}}{{if eq .Subscription.OriginalCurrency "SEK"}}selected{{end}}{{end}}>kr SEK</option>
6289
<option value="PLN" {{if .Subscription}}{{if eq .Subscription.OriginalCurrency "PLN"}}selected{{end}}{{end}}>zł PLN</option>
6390
<option value="INR" {{if .Subscription}}{{if eq .Subscription.OriginalCurrency "INR"}}selected{{end}}{{end}}>₹ INR</option>
91+
<option value="CHF" {{if .Subscription}}{{if eq .Subscription.OriginalCurrency "CHF"}}selected{{end}}{{end}}>Fr. CHF</option>
92+
<option value="BRL" {{if .Subscription}}{{if eq .Subscription.OriginalCurrency "BRL"}}selected{{end}}{{end}}>R$ BRL</option>
6493
</select>
6594
</div>
6695

@@ -195,6 +224,88 @@ <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
195224
</div>
196225

197226
<script>
227+
// Inline category creation functions
228+
function showNewCategoryInput() {
229+
document.getElementById('new-category-container').classList.remove('hidden');
230+
document.getElementById('add-category-btn').classList.add('hidden');
231+
document.getElementById('new-category-name').focus();
232+
}
233+
234+
function hideNewCategoryInput() {
235+
document.getElementById('new-category-container').classList.add('hidden');
236+
document.getElementById('add-category-btn').classList.remove('hidden');
237+
document.getElementById('new-category-name').value = '';
238+
document.getElementById('new-category-error').classList.add('hidden');
239+
}
240+
241+
let isCreatingCategory = false;
242+
243+
async function createNewCategory() {
244+
// Prevent double-submission
245+
if (isCreatingCategory) return;
246+
247+
const nameInput = document.getElementById('new-category-name');
248+
const errorDiv = document.getElementById('new-category-error');
249+
const addBtn = document.querySelector('#new-category-container button');
250+
const name = nameInput.value.trim();
251+
252+
if (!name) {
253+
errorDiv.textContent = 'Please enter a category name';
254+
errorDiv.classList.remove('hidden');
255+
return;
256+
}
257+
258+
isCreatingCategory = true;
259+
if (addBtn) addBtn.disabled = true;
260+
261+
try {
262+
const response = await fetch('/api/categories', {
263+
method: 'POST',
264+
headers: {
265+
'Content-Type': 'application/json',
266+
},
267+
body: JSON.stringify({ name: name }),
268+
});
269+
270+
if (!response.ok) {
271+
const data = await response.json();
272+
throw new Error(data.error || 'Failed to create category');
273+
}
274+
275+
const newCategory = await response.json();
276+
277+
// Validate response before adding
278+
if (!newCategory.id || !newCategory.name) {
279+
throw new Error('Invalid response from server');
280+
}
281+
282+
// Add new option to dropdown and select it
283+
const select = document.getElementById('category_id');
284+
const option = document.createElement('option');
285+
option.value = newCategory.id;
286+
option.textContent = newCategory.name;
287+
option.selected = true;
288+
select.appendChild(option);
289+
290+
// Hide the input and reset
291+
hideNewCategoryInput();
292+
} catch (error) {
293+
errorDiv.textContent = error.message;
294+
errorDiv.classList.remove('hidden');
295+
} finally {
296+
isCreatingCategory = false;
297+
if (addBtn) addBtn.disabled = false;
298+
}
299+
}
300+
301+
// Allow Enter key to submit new category
302+
document.addEventListener('keydown', function(e) {
303+
if (e.key === 'Enter' && document.activeElement.id === 'new-category-name') {
304+
e.preventDefault();
305+
createNewCategory();
306+
}
307+
});
308+
198309
function calculateRenewalDate() {
199310
const scheduleSelect = document.getElementById('schedule');
200311
const statusSelect = document.getElementById('status');
@@ -255,15 +366,11 @@ <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
255366
// Add event listeners when modal content loads
256367
function initRenewalCalculator() {
257368
const scheduleSelect = document.getElementById('schedule');
258-
const statusSelect = document.getElementById('status');
259369

260370
if (scheduleSelect) {
261371
scheduleSelect.addEventListener('change', calculateRenewalDate);
262372
}
263-
264-
if (statusSelect) {
265-
statusSelect.addEventListener('change', calculateRenewalDate);
266-
}
373+
// Note: Status changes should NOT trigger renewal date recalculation (fixes #68)
267374
}
268375

269376
// Initialize immediately since this script loads with the form

0 commit comments

Comments
 (0)