feat: add finance widget and CSS/template updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tanmay Karande
2026-03-17 00:36:43 -04:00
parent c8b7322f13
commit 492e0ec47c
11 changed files with 326 additions and 14 deletions

View File

@@ -0,0 +1,47 @@
package glance
import (
"encoding/json"
"io"
"net/http"
"os"
)
func handleFinanceGet(w http.ResponseWriter, r *http.Request) {
f, err := os.Open("finance.json")
if err != nil {
w.Header().Set("Content-Type", "application/json")
if os.IsNotExist(err) {
w.Write([]byte(`{"accounts":[],"monthly":{"labels":[],"income":[],"expenses":[],"spend":[]}}`))
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer f.Close()
w.Header().Set("Content-Type", "application/json")
io.Copy(w, f)
}
func handleFinancePost(w http.ResponseWriter, r *http.Request) {
var data interface{}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
f, err := os.Create("finance.json")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer f.Close()
if err := json.NewEncoder(f).Encode(data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

View File

@@ -450,6 +450,9 @@ func (a *application) server() (func() error, func() error) {
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("GET /api/finance", handleFinanceGet)
mux.HandleFunc("POST /api/finance", handleFinancePost)
if a.RequiresAuth {
mux.HandleFunc("GET /login", a.handleLoginPageRequest)
mux.HandleFunc("GET /logout", a.handleLogoutRequest)

View File

@@ -10,9 +10,9 @@
font-size: 10px;
--scheme: ;
--bgh: 240;
--bgs: 8%;
--bgl: 9%;
--bgh: 220;
--bgs: 12%;
--bgl: 10%;
--bghs: var(--bgh), var(--bgs);
--cm: 1;
--tsm: 1;
@@ -25,9 +25,12 @@
--border-radius: 5px;
--mobile-navigation-height: 50px;
--color-primary: hsl(43, 50%, 70%);
--color-positive: var(--color-primary);
--color-negative: hsl(0, 70%, 70%);
--primary-h: 12;
--primary-s: 70%;
--primary-l: 60%;
--color-primary: hsl(var(--primary-h), var(--primary-s), var(--primary-l));
--color-positive: hsl(150, 60%, 50%);
--color-negative: hsl(0, 70%, 65%);
--color-background: hsl(var(--bghs), var(--bgl));
--color-widget-background-hsl-values: var(--bghs), calc(var(--bgl) + 1%);
--color-widget-background: hsl(var(--color-widget-background-hsl-values));

View File

@@ -43,6 +43,11 @@ button {
cursor: pointer;
background: none;
color: inherit;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s;
}
button:active {
transform: scale(0.95);
}
::selection {
@@ -110,6 +115,13 @@ html, body, .body-content {
h1, h2, h3, h4, h5 {
font: inherit;
font-weight: 600;
letter-spacing: -0.02em;
}
h3, h4, h5 {
font-weight: 500;
letter-spacing: 0;
}
a {
@@ -124,7 +136,7 @@ ul {
body {
font-size: 1.3rem;
font-family: 'JetBrains Mono', monospace;
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-variant-ligatures: none;
line-height: 1.6;
color: var(--color-text-base);
@@ -132,6 +144,18 @@ body {
overflow-y: scroll;
}
body::after {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: -1;
background: radial-gradient(circle at 10% 0%, hsla(var(--primary-h), var(--primary-s), var(--primary-l), 0.07), transparent 50%),
radial-gradient(circle at 90% 100%, hsla(var(--primary-h), var(--primary-s), calc(var(--primary-l) + 10%), 0.06), transparent 50%);
}
.page-column-small {
width: 300px;
flex-shrink: 0;
@@ -301,11 +325,15 @@ kbd:active {
display: block;
height: 100%;
border-bottom: 2px solid transparent;
transition: color .3s, border-color .3s;
transition: color .3s, border-color .3s, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
font-size: var(--font-size-h3);
flex-shrink: 0;
}
.nav-item:active {
transform: scale(0.95);
}
.nav-item:not(.nav-item-current):hover {
border-bottom-color: var(--color-text-subdue);
color: var(--color-text-highlight);

View File

@@ -59,19 +59,29 @@
}
.widget-content:not(.widget-content-frameless), .widget-content-frame {
background: var(--color-widget-background);
border-radius: var(--border-radius);
border: 1px solid var(--color-widget-content-border);
box-shadow: 0px 3px 0px 0px hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl)) - 0.5%));
background: linear-gradient(180deg, hsl(var(--color-widget-background-hsl-values)) 0%, hsla(var(--bghs), calc(var(--bgl) - 2%), 0.95) 100%);
backdrop-filter: blur(12px);
border-radius: calc(var(--border-radius) * 1.5);
border: none;
box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.05) inset, 0 0 0 1px rgba(255, 255, 255, 0.02) inset, 0 8px 16px -4px hsla(var(--bghs), calc(var(--bgl) - 5%), 0.7);
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease;
}
.widget-content:not(.widget-content-frameless):hover, .widget-content-frame:hover {
box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.08) inset, 0 0 0 1px rgba(255, 255, 255, 0.04) inset, 0 12px 24px -8px hsla(var(--bghs), calc(var(--bgl) - 5%), 0.9), 0 0 40px -10px hsla(var(--primary-h), var(--primary-s), var(--primary-l), 0.12);
}
.widget-header {
padding: 0 calc(var(--widget-content-horizontal-padding) + 1px);
font-size: var(--font-size-h4);
margin-bottom: 0.9rem;
margin-bottom: 1.2rem;
display: flex;
align-items: center;
gap: 1rem;
font-size: 1.15rem;
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 600;
color: var(--color-text-subdue);
}
.widget-beta-icon {
@@ -91,3 +101,7 @@
.widget + .widget {
margin-top: var(--widget-gap);
}
.widget-content:active {
transform: scale(0.99);
}

View File

@@ -2,6 +2,9 @@
<html lang="en" id="top" data-theme="{{ .Request.Theme.Key }}" data-scheme="{{ if .Request.Theme.Light }}light{{ else }}dark{{ end }}">
<head>
{{ block "document-head-before" . }}{{ end }}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600&display=swap" rel="stylesheet">
<script>
if (navigator.platform === 'iPhone') document.documentElement.classList.add('ios');
const pageData = {