feat: add finance widget and CSS/template updates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
47
internal/glance/finance.go
Normal file
47
internal/glance/finance.go
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user