commit ecdd81d185df7c2520f85bc28c3f982e2a99932a
parent 44a7b24af5654585091f2a020fa7b102c6f4452f
Author: Tomas Nemec <nemi@skaut.cz>
Date: Sun, 1 Aug 2021 00:38:00 +0200
feat: multi acounts
cli takes -tokens file with tokens listed one by line. each line has to
be in format:
`<token> <name> [<config>]`
`config` is comma separated string without spaces
currently available config is `onlyIncome` that will filter all outcome
from balance page
Diffstat:
13 files changed, 651 insertions(+), 183 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,3 +1,4 @@
token
+tokens
t.json
gofio
diff --git a/fio.go b/fio.go
@@ -0,0 +1,95 @@
+package main
+
+import (
+ "errors"
+ "fio/fio"
+ "fmt"
+ "log"
+ "strings"
+ "time"
+
+ "github.com/leekchan/accounting"
+)
+
+type account struct {
+ name string
+ config config
+ fio *fio.Fio
+ root *fio.Root
+}
+
+func (acc *account) needsReload() bool {
+ return acc.root == nil || acc.hasNew()
+}
+
+func (acc *account) reload(force bool) error {
+ if force || acc.needsReload() {
+ log.Printf("Reloading '%s'", acc.name)
+ if !acc.loadRoot() {
+ return errors.New("Failed to load root for '" + acc.name + "'")
+ } else {
+ reverse(acc.root.AccountStatement.TransactionList.Transactions)
+ }
+ }
+ return nil
+}
+
+func (acc *account) hasNew() bool {
+ last := acc.fio.Last()
+ if last == nil {
+ return false
+ }
+ return len(last.AccountStatement.TransactionList.Transactions) > 0
+}
+
+func (acc *account) loadCard(template []byte, isActive bool) (string, error) {
+ err := acc.reload(false)
+ if err != nil {
+ return "", err
+ }
+ ac := accounting.Accounting{Symbol: acc.root.AccountStatement.Info.Currency, Precision: 2, Thousand: " ", Format: "%v %s"}
+ // templating
+ card := string(template)
+ card = strings.Replace(card, "{{LINK}}", "/"+acc.name, -1)
+ var active string
+ if isActive {
+ active = "active"
+ }
+ card = strings.Replace(card, "{{ACTIVE}}", active, -1)
+ card = strings.Replace(card, "{{NAME}}", acc.name, -1)
+ card = strings.Replace(card, "{{ACC_ID}}", formatAccId(acc.root), -1)
+ closingBal := ac.FormatMoney(acc.root.AccountStatement.Info.ClosingBalance)
+ card = strings.Replace(card, "{{BALANCE}}", fmt.Sprintf("%v", closingBal), -1)
+
+ return card, nil
+}
+
+func (acc *account) loadBalance(template []byte, cards string) (string, error) {
+ err := acc.reload(false)
+ if err != nil {
+ return "", err
+ }
+ // templating
+ file := string(template)
+ file = strings.Replace(file, "{{CARDS}}", cards, -1)
+ file = strings.Replace(file, "{{NAME}}", acc.name, -1)
+ file = strings.Replace(file, "{{LIST}}", formatList(acc.root, acc.config), -1)
+
+ return string(file), nil
+}
+
+func (acc *account) loadRoot() bool {
+ from := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
+ to := time.Date(2021, time.December, 31, 0, 0, 0, 0, time.UTC)
+
+ acc.root = acc.fio.Period(from, to)
+ return acc.root != nil
+}
+
+func reverse(numbers []*fio.Transaction) []*fio.Transaction {
+ for i := 0; i < len(numbers)/2; i++ {
+ j := len(numbers) - i - 1
+ numbers[i], numbers[j] = numbers[j], numbers[i]
+ }
+ return numbers
+}
diff --git a/fio/api.go b/fio/api.go
@@ -2,16 +2,11 @@ package fio
import (
"encoding/json"
- "fmt"
"io"
"log"
"net/http"
- "time"
)
-func fmtDate(time time.Time) string {
- return fmt.Sprintf("%d-%d-%d", time.Year(), time.Month(), time.Day())
-}
func get(url string) *Root {
resp, err := http.Get(url)
diff --git a/fio/fio.go b/fio/fio.go
@@ -10,6 +10,10 @@ type Fio struct {
Token string
}
+func fmtDate(time time.Time) string {
+ return fmt.Sprintf("%d-%d-%d", time.Year(), time.Month(), time.Day())
+}
+
func (f *Fio) Period(from time.Time, to time.Time) *Root {
return get(fmt.Sprintf("%s/periods/%s/%s/%s/transactions.json", f.Base_url, f.Token, fmtDate(from), fmtDate(to)))
}
@@ -17,6 +21,7 @@ func (f *Fio) Period(from time.Time, to time.Time) *Root {
func (f *Fio) Statement(year int, id int) *Root {
return get(fmt.Sprintf("%s/by_id/%s/%d/%d/transactions.json", f.Base_url, f.Token, year, id))
}
+
func (f *Fio) Last() *Root {
return get(fmt.Sprintf("%s/last/%s/transactions.json", f.Base_url, f.Token))
}
diff --git a/fio/model.go b/fio/model.go
@@ -41,6 +41,7 @@ type Transaction struct {
BeneficiaryAccount *Column `json:"column2"`
BeneficiaryName *Column `json:"column10"`
BeneficiaryMessage *Column `json:"column16"`
+ BeneficiaryComment *Column `json:"column25"`
User *Column `json:"column7"`
Type *Column `json:"column8"`
KS *Column `json:"column4,omitempty"`
diff --git a/main.go b/main.go
@@ -9,146 +9,101 @@ import (
"net/http"
"os"
"strings"
- "time"
-
- "github.com/leekchan/accounting"
)
const base_url = "https://www.fio.cz/ib_api/rest/"
-var tokenFile = flag.String("t", "", "fio api token")
-var token string
+var (
+ tokensFile = flag.String("t", "tokens", "fio api token")
+ port = flag.String("port", "3000", "serving port")
+
+ templates map[string][]byte
+ accs []*account
+ lastUri string = "/"
+)
-var page string
+func checkErr(err error, msg ...interface{}) {
+ if err != nil {
+ log.Fatal(msg...)
+ }
+}
func main() {
flag.Parse()
- if *tokenFile == "" {
+ // Tokens
+ if *tokensFile == "" {
log.Fatal("Token file was not provided")
- os.Exit(1)
}
+ file, err := os.Open(*tokensFile)
+ checkErr(err, "Cannot read token file")
+ tokens, err := parseTokens(file)
+ checkErr(err, "Cannot parse token file")
- data, err := os.ReadFile(*tokenFile)
- token = strings.TrimSuffix(string(data), "\n")
- if err != nil {
- log.Fatal("Cannot read token file")
- os.Exit(1)
- }
-
- http.HandleFunc("/", serveFiles)
+ // templates
+ templates = loadTemplates([]string{"dashboard", "balance", "card"})
- log.Println("Listening on :3000...")
- err = http.ListenAndServe(":3000", nil)
- if err != nil {
- log.Fatal(err)
- os.Exit(1)
- }
-}
-func serveFiles(w http.ResponseWriter, r *http.Request) {
- index, _ := os.ReadFile("./web/index.html")
- fio := &fio.Fio{Base_url: base_url, Token: token}
-
- if page != "" && !hasNew(fio) {
- log.Println("No new updates")
- fmt.Fprint(w, page)
- return
+ // accounts
+ for _, token := range tokens {
+ accs = append(accs, &account{name: token.name, config: token.config, fio: &fio.Fio{Base_url: base_url, Token: token.token}})
}
- root := data(fio)
- if root == nil {
- log.Println("Nil root")
- fmt.Fprint(w, page)
- return
+ http.HandleFunc("/", serveDashboard())
+ for _, acc := range accs {
+ http.HandleFunc("/"+acc.name, serveBalance(acc.name))
}
- log.Println("Serving new")
- ac := accounting.Accounting{Symbol: root.AccountStatement.Info.Currency, Precision: 2, Thousand: " ", Format: "%v %s"}
- // Templating
- file := string(index)
- file = strings.Replace(file, "{{ACC_ID}}", formatAccId(root), -1)
- file = strings.Replace(file, "{{ACC_IBAN}}", root.AccountStatement.Info.Iban, -1)
- file = strings.Replace(file, "{{ACC_BAL}}", fmt.Sprintf("%v", ac.FormatMoney(root.AccountStatement.Info.ClosingBalance)), -1)
- file = strings.Replace(file, "{{LIST}}", formatList(root), -1)
- // serving
- page = string(file)
- fmt.Fprint(w, page)
-}
-func formatAccId(root *fio.Root) string {
- return fmt.Sprintf("%s / %s", root.AccountStatement.Info.AccountId, root.AccountStatement.Info.BankId)
+ log.Printf("Listening on :%s...\n", *port)
+ err = http.ListenAndServe(":"+*port, nil)
+ checkErr(err, err)
}
-func formatList(root *fio.Root) string {
- ac := accounting.Accounting{Precision: 2, Thousand: " ", Format: "%v %s"}
- headers := make(map[int]string)
- var head bytes.Buffer
- var rows bytes.Buffer
- for _, v := range reverse(root.AccountStatement.TransactionList.Transactions) {
- columns := []*fio.Column{
- v.Date,
- v.Money,
- v.BeneficiaryAccount,
- v.BeneficiaryMessage,
- v.Type,
- }
+// DASHBOARD
+func serveDashboard() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ file := loadDashboard(templates["dashboard"])
+ fmt.Fprint(w, file)
+ }
+}
- rows.WriteString("<tr>")
- for _, c := range columns {
- if c == nil {
- rows.WriteString("<td></td>")
- continue
- }
- if _, in := headers[c.Id]; !in {
- headers[c.Id] = c.Name
- head.WriteString(fmt.Sprintf("<th>%s</th>", c.Name))
- }
- switch c.Id {
- case 1:
- moneyClass := "positive"
- if c.Value.(float64) < 0 {
- moneyClass = "negative"
- }
- ac.Symbol = v.Currency.Value.(string)
- rows.WriteString(fmt.Sprintf("<td class='%s %s'>%v</td>", "money", moneyClass, ac.FormatMoney(c.Value)))
- case 2:
- rows.WriteString(fmt.Sprintf("<td><span>%v</span> / <span>%v</span></td>", c.Value, v.BankCode.Value))
- default:
- rows.WriteString(fmt.Sprintf("<td>%v</td>", c.Value))
- }
+// BALANCE
+func serveBalance(name string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ acc := getAcc(name)
+ cards := accCards(acc.name)
+ balance, err := acc.loadBalance(templates["balance"], cards)
+ if err != nil {
+ log.Print(err)
+ http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
+ return
}
- rows.WriteString("</tr>")
-
+ fmt.Fprint(w, balance)
}
-
- return fmt.Sprintf(`
- <thead>
- %s
- </thead>
- <tbody>
- %s
- </tbody>
-`, head.String(), rows.String())
}
-func data(fio *fio.Fio) *fio.Root {
- from := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
- to := time.Date(2021, time.December, 31, 0, 0, 0, 0, time.UTC)
-
- return fio.Period(from, to)
+func loadDashboard(template []byte) string {
+ cards := accCards("")
+ return strings.Replace(string(template), "{{CARDS}}", cards, -1)
}
-func hasNew(fio *fio.Fio) bool {
- last := fio.Last()
- if last == nil {
- return false
+func accCards(activeAcc string) string {
+ var cardBuf bytes.Buffer
+ for _, acc := range accs {
+ card, err := acc.loadCard(templates["card"], activeAcc == acc.name)
+ if err != nil {
+ log.Print(err)
+ continue
+ }
+ cardBuf.WriteString(card)
}
- return len(last.AccountStatement.TransactionList.Transactions) > 0
+ return cardBuf.String()
}
-func reverse(numbers []*fio.Transaction) []*fio.Transaction {
- for i := 0; i < len(numbers)/2; i++ {
- j := len(numbers) - i - 1
- numbers[i], numbers[j] = numbers[j], numbers[i]
+func getAcc(name string) *account {
+ for _, acc := range accs {
+ if acc.name == name {
+ return acc
+ }
}
- return numbers
+ return nil
}
diff --git a/template.go b/template.go
@@ -0,0 +1,112 @@
+package main
+
+import (
+ "bytes"
+ "fio/fio"
+ "fmt"
+ "os"
+ "sort"
+
+ "github.com/leekchan/accounting"
+)
+
+func loadTemplates(list []string) (templates map[string][]byte) {
+ templates = make(map[string][]byte, len(list))
+ for _, filename := range list {
+ file, err := os.ReadFile("./web/" + filename + ".template.html")
+ checkErr(err, "Template '"+filename+"' failed to load")
+ templates[filename] = file
+ }
+ return
+}
+
+func formatAccId(root *fio.Root) string {
+ return fmt.Sprintf("%s / %s", root.AccountStatement.Info.AccountId, root.AccountStatement.Info.BankId)
+}
+
+func formatList(root *fio.Root, config config) string {
+ ac := accounting.Accounting{Precision: 2, Thousand: " ", Format: "%v %s"}
+ headers := make(map[int]string)
+ var rows bytes.Buffer
+ for _, v := range root.AccountStatement.TransactionList.Transactions {
+ columns := []*fio.Column{
+ v.Date,
+ v.Money,
+ v.BeneficiaryAccount,
+ v.BeneficiaryMessage,
+ v.BeneficiaryComment,
+ v.Type,
+ }
+
+ var row bytes.Buffer
+ var skipRow bool
+ for i, c := range columns {
+ if c == nil {
+ row.WriteString("<td></td>")
+ // skip header
+ if _, in := headers[i]; !in {
+ headers[i] = ""
+ }
+ continue
+ }
+
+ // header
+ if n, in := headers[i]; !in || in && n == "" {
+ headers[i] = c.Name
+ }
+
+ // Row
+ switch c.Id {
+ case 1:
+ moneyClass := "positive"
+ if c.Value.(float64) < 0 {
+ if config.onlyIncome {
+ skipRow = true
+ }
+ moneyClass = "negative"
+ }
+ ac.Symbol = v.Currency.Value.(string)
+ row.WriteString(fmt.Sprintf("<td class='%s %s'>%v</td>", "money", moneyClass, ac.FormatMoney(c.Value)))
+ case 2:
+ row.WriteString(fmt.Sprintf("<td><span>%v</span> / <span>%v</span></td>", c.Value, v.BankCode.Value))
+ default:
+ row.WriteString(fmt.Sprintf("<td>%v</td>", c.Value))
+ }
+ }
+
+ if !skipRow {
+ rows.WriteString("<tr>")
+ rows.WriteString(row.String())
+ rows.WriteString("</tr>")
+ }
+ }
+
+ var head bytes.Buffer
+ indexes := make([]int, 0, len(headers))
+ for i := range headers {
+ indexes = append(indexes, i)
+ }
+ sort.Ints(indexes)
+ for _, i := range indexes {
+ name := headers[i]
+ head.WriteString(fmt.Sprintf("<th>%s</th>", name))
+ }
+
+ return fmt.Sprintf(`
+ <thead>
+ %s
+ </thead>
+ <tbody>
+ %s
+ </tbody>
+ `, head.String(), rows.String())
+}
+
+func contains(s []int, e int) bool {
+ for _, a := range s {
+ if a == e {
+ return true
+ }
+ }
+ return false
+}
diff --git a/token.go b/token.go
@@ -0,0 +1,45 @@
+package main
+
+import (
+ "bufio"
+ "io"
+ "strings"
+)
+
+type token struct {
+ name string
+ token string
+ config config
+}
+
+type config struct {
+ onlyIncome bool
+}
+
+func parseTokens(tokens io.Reader) ([]token, error) {
+ var o []token
+ scanner := bufio.NewScanner(tokens)
+ for scanner.Scan() {
+ text := scanner.Text()
+ if strings.HasPrefix(text, "#") {
+ continue
+ }
+ split := strings.Split(text, " ")
+ token := token{name: split[1], token: split[0]}
+ // config
+ if len(split) > 2 {
+ configs := strings.Split(split[2], ",")
+ for _, c := range configs {
+ switch c {
+ case "onlyIncome":
+ token.config = config{onlyIncome: true}
+ }
+ }
+ }
+ o = append(o, token)
+ }
+ if err := scanner.Err(); err != nil {
+ return []token{}, err
+ }
+ return o, nil
+}
diff --git a/token_test.go b/token_test.go
@@ -0,0 +1,73 @@
+package main
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+)
+
+func TestTokenParse(t *testing.T) {
+ tokensRaw := `token1 A
+token2 B
+token3 C`
+ expected := []token{{name: "A", token: "token1"}, {name: "B", token: "token2"}, {name: "C", token: "token3"}}
+
+ actual, err := parseTokens(strings.NewReader(tokensRaw))
+ if err != nil {
+ t.Error(err)
+ }
+
+ if len(actual) != 3 {
+ t.Errorf("Expected 3 outputs, got %d", len(actual))
+ }
+ if fmt.Sprint(actual) != fmt.Sprint(expected) {
+ t.Errorf("Maps mismatch, \nexpected %v,\ngot %v", expected, actual)
+ }
+}
+
+func TestTokenParseWithComment(t *testing.T) {
+ tokensRaw := `token1 A
+token2 B
+#token3 C`
+ expected := []token{{name: "A", token: "token1"}, {name: "B", token: "token2"}}
+
+ actual, err := parseTokens(strings.NewReader(tokensRaw))
+ if err != nil {
+ t.Error(err)
+ }
+
+ if len(actual) != 2 {
+ t.Errorf("Expected 2 outputs, got %d", len(actual))
+ }
+ if fmt.Sprint(actual) != fmt.Sprint(expected) {
+ t.Errorf("Maps mismatch, \nexpected %v,\ngot %v", expected, actual)
+ }
+}
+
+func TestTokenParseWithConfig(t *testing.T) {
+ tokensRaw := `token1 A onlyIncome`
+ expected := []token{{name: "A", token: "token1", config: config{onlyIncome: true}}}
+
+ actual, err := parseTokens(strings.NewReader(tokensRaw))
+ if err != nil {
+ t.Error(err)
+ }
+
+ if fmt.Sprint(actual) != fmt.Sprint(expected) {
+ t.Errorf("Maps mismatch, \nexpected %v,\ngot %v", expected, actual)
+ }
+}
+
+func TestTokenParseWithDifferentConfig(t *testing.T) {
+ tokensRaw := `token1 A banana`
+ expected := []token{{name: "A", token: "token1", config: config{onlyIncome: false}}}
+
+ actual, err := parseTokens(strings.NewReader(tokensRaw))
+ if err != nil {
+ t.Error(err)
+ }
+
+ if fmt.Sprint(actual) != fmt.Sprint(expected) {
+ t.Errorf("Maps mismatch, \nexpected %v,\ngot %v", expected, actual)
+ }
+}
diff --git a/web/balance.template.html b/web/balance.template.html
@@ -0,0 +1,158 @@
+<!doctype html>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Milion</title>
+ <link rel="stylesheet" type="text/css" href="tabs.css" />
+ <style>
+ body {
+ font-family: monospace;
+ margin: 0;
+ font-size: 12px;
+ }
+
+ h1 {
+ text-align: center;
+ text-transform: uppercase;
+ }
+
+ table {
+ border-collapse: collapse;
+ width: 100%;
+ }
+
+ table tr:hover td {
+ background-color: #e6e6e6;
+ }
+
+ table.head tr td:first-child {
+ text-align: right
+ }
+
+ table.head tr td:last-child {
+ text-align: left
+ }
+
+ th,
+ td {
+ padding: 8px;
+ text-align: left;
+ border-bottom: 1px solid #ddd;
+ white-space: nowrap;
+ }
+
+ td.positive {
+ color: green;
+ }
+
+ td.negative {
+ color: red;
+ }
+
+ td.money {
+ text-align: right;
+ }
+
+ @import url("https://fonts.googleapis.com/css2?family=Open+Sans:wght@600;700&display=swap");
+
+ * {
+ box-sizing: border-box;
+ }
+
+ body {
+ margin: 0;
+ font-family: monospace;
+ }
+
+ .mono {
+ font-family: monospace;
+ }
+
+ .page-contain {
+ height: auto;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ background: #627084;
+ font-family: "Open Sans", sans-serif;
+ }
+
+ .data-card {
+ display: flex;
+ flex-direction: column;
+ max-width: 20em;
+ overflow: hidden;
+ border-radius: 0.5em;
+ text-decoration: none;
+ background: white;
+ margin: 1em;
+ padding: 1em;
+ box-shadow: 0 1.5em 2.5em -0.5em rgba(0, 0, 0, 0.1);
+ transition: transform 0.45s ease;
+ }
+
+ .data-card h3 {
+ color: #2e3c40;
+ font-size: 2em;
+ font-weight: 600;
+ line-height: 1;
+ padding-bottom: 0.5em;
+ margin: 0 0 0.142857143em;
+ border-bottom: 2px solid #363e49;
+ transition: border 0.45s ease;
+ }
+
+ .data-card h4 {
+ color: #627084;
+ text-transform: uppercase;
+ font-size: 1.125em;
+ font-weight: 700;
+ line-height: 1;
+ letter-spacing: 0.1em;
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+
+ .data-card p {
+ color: #627084;
+ margin: 0;
+ }
+
+ .data-card:hover {
+ transform: scale(1.02);
+ }
+
+ .data-card.active {
+ background: #363e49;
+ }
+
+ .data-card:hover h3 {
+ border-bottom-color: darkorange;
+ }
+
+ .data-card.active h3 {
+ color: #ffffff;
+ border-bottom-color: darkorange;
+ }
+
+ .data-card.active h4 {
+ color: #ffffff;
+ }
+
+ .data-card.active p {
+ color: #ffffff;
+ }
+ </style>
+</head>
+
+<body>
+ <section class="page-contain">
+ {{CARDS}}
+ </section>
+ <h1>{{NAME}}</h1>
+ <table>{{LIST}}</table>
+</body>
+
+</html>
diff --git a/web/card.template.html b/web/card.template.html
@@ -0,0 +1,5 @@
+<a href="{{LINK}}" class="data-card {{ACTIVE}}">
+ <h3 class="mono">{{BALANCE}}</h3>
+ <h4>{{NAME}}</h4>
+ <p>{{ACC_ID}}</p>
+</a>
diff --git a/web/dashboard.template.html b/web/dashboard.template.html
@@ -0,0 +1,89 @@
+<!doctype html>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Milion</title>
+ <style>
+ @import url("https://fonts.googleapis.com/css2?family=Open+Sans:wght@600;700&display=swap");
+
+ * {
+ box-sizing: border-box;
+ }
+
+ body {
+ margin: 0;
+ font-family: monospace;
+ }
+
+ .mono {
+ font-family: monospace;
+ }
+
+ .page-contain {
+ display: flex;
+ min-height: 100vh;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ background: #627084;
+ font-family: "Open Sans", sans-serif;
+ }
+
+ .data-card {
+ display: flex;
+ flex-direction: column;
+ max-width: 20em;
+ overflow: hidden;
+ border-radius: 0.5em;
+ text-decoration: none;
+ background: white;
+ margin: 1em;
+ padding: 2em 2.5em;
+ box-shadow: 0 1.5em 2.5em -0.5em rgba(0, 0, 0, 0.1);
+ transition: transform 0.45s ease;
+ }
+
+ .data-card h3 {
+ color: #2e3c40;
+ font-size: 2em;
+ font-weight: 600;
+ line-height: 1;
+ padding-bottom: 0.5em;
+ margin: 0 0 0.142857143em;
+ border-bottom: 2px solid #363e49;
+ transition: border 0.45s ease;
+ }
+
+ .data-card h4 {
+ color: #627084;
+ text-transform: uppercase;
+ font-size: 1.125em;
+ font-weight: 700;
+ line-height: 1;
+ letter-spacing: 0.1em;
+ margin-top: 0;
+ }
+
+ .data-card p {
+ color: #627084;
+ margin: 0;
+ }
+
+ .data-card:hover {
+ transform: scale(1.02);
+ }
+
+ .data-card:hover h3 {
+ border-bottom-color: darkorange;
+ }
+ </style>
+</head>
+
+<body>
+ <section class="page-contain">
+ {{CARDS}}
+ </section>
+</body>
+
+</html>
diff --git a/web/index.html b/web/index.html
@@ -1,66 +0,0 @@
-<!doctype html>
-<html>
-
-<head>
- <meta charset="utf-8">
- <title>Milion</title>
- <style>
- table {
- border-collapse: collapse;
- width: 100%;
- }
-
- table tr:hover td {
- background-color: #e6e6e6;
- }
-
- table.head tr td:first-child {
- text-align: right
- }
-
- table.head tr td:last-child {
- text-align: left
- }
-
- th,
- td {
- padding: 8px;
- text-align: left;
- border-bottom: 1px solid #ddd;
- }
-
- td.positive {
- color: green;
- }
-
- td.negative {
- color: red;
- }
-
- td.money {
- text-align: right;
- }
- </style>
-</head>
-
-<body>
- <h1>Milion</h1>
- <table class="head">
- <tbody>
- <tr>
- <td>Číslo účtu:</td>
- <td>{{ACC_ID}}</td>
- </tr>
- <tr>
- <td>IBAN:</td>
- <td>{{ACC_IBAN}}</td>
- </tr>
- <tr>
- <td>Disponibilní zůstatek:</td>
- <td>{{ACC_BAL}}</td>
- </tr>
- </table>
- <table>{{LIST}}</table>
-</body>
-
-</html>