From 340966e22a699269ead59a213048aa38b866e0f0 Mon Sep 17 00:00:00 2001 From: darkruby Date: Fri, 10 Apr 2026 22:44:11 +0100 Subject: [PATCH 1/9] login css update --- TODO.md | 5 +++++ packages/web/src/components/Tx/TickerLookup.scss | 4 +++- packages/web/src/screens/Login.scss | 13 +++++++++++++ packages/web/src/screens/Login.tsx | 15 +++++++++------ packages/web/src/screens/Users.tsx | 1 + 5 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 packages/web/src/screens/Login.scss diff --git a/TODO.md b/TODO.md index 9fd0a80e..9f10c517 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,10 @@ # todo +## 1.9 + - [ ] portfolio reports, cap gains + - [ ] asset also in.. + - [ ] tx modal diff view for buy/sell (cap gains, limit on sales) + ## 1.8.1 - [x] multi asset layered chart for portfolios - [x] multi portfollio layered chart for home screen diff --git a/packages/web/src/components/Tx/TickerLookup.scss b/packages/web/src/components/Tx/TickerLookup.scss index ac689fa5..7defff0e 100644 --- a/packages/web/src/components/Tx/TickerLookup.scss +++ b/packages/web/src/components/Tx/TickerLookup.scss @@ -21,11 +21,13 @@ } background-color: $dropdown-dark-bg; } + .ticker-lookup__option { background-color: $dropdown-dark-bg; + color: $dropdown-dark-color; + &:hover { background-color: lighten($dropdown-dark-bg, 15%); } - color: $dropdown-dark-color; } } diff --git a/packages/web/src/screens/Login.scss b/packages/web/src/screens/Login.scss new file mode 100644 index 00000000..fad72fda --- /dev/null +++ b/packages/web/src/screens/Login.scss @@ -0,0 +1,13 @@ +.login-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: calc(100vh - 2rem); + padding: 1rem; + + @media (max-width: 768px) { + padding: 0.5rem; + min-height: auto; + } +} diff --git a/packages/web/src/screens/Login.tsx b/packages/web/src/screens/Login.tsx index 5014f5a8..3287d655 100644 --- a/packages/web/src/screens/Login.tsx +++ b/packages/web/src/screens/Login.tsx @@ -8,6 +8,7 @@ import { Login } from "../components/Auth/Login"; import { routes } from "../components/Router"; import { Error } from "../decorators/errors"; import { useStore } from "../hooks/store"; +import "./Login.scss"; const RawLoginScreen: React.FC = () => { useSignals(); @@ -27,16 +28,18 @@ const RawLoginScreen: React.FC = () => { useHead({ title: `Assets - Login` }); return ( - <> - - +
+ + - - - +
); }; diff --git a/packages/web/src/screens/Users.tsx b/packages/web/src/screens/Users.tsx index bf5f22c4..07f1efbd 100644 --- a/packages/web/src/screens/Users.tsx +++ b/packages/web/src/screens/Users.tsx @@ -28,6 +28,7 @@ const RawUsersScreen: React.FC = () => { onDelete={users.delete} users={users.data.value} error={users.error.value} + onErrorDismiss={users.load} fetching={users.fetching.value} /> ); From 6a1749502e4aef3502573c9fddf30e7ffcbfc84f Mon Sep 17 00:00:00 2001 From: darkruby Date: Sun, 12 Apr 2026 16:35:45 +0100 Subject: [PATCH 2/9] tx modal updates --- .github/workflows/build-docker-container.yaml | 1 + TODO.md | 2 +- packages/backend/src/enrichment/tx.ts | 4 +- packages/core/src/domain/tx.ts | 6 +- packages/core/src/validation/tx.ts | 7 +- packages/core/src/validation/util.ts | 4 + .../web/src/components/Charts/AssetChart.tsx | 8 +- .../web/src/components/Form/MoneyEdit.tsx | 32 +++++ packages/web/src/components/Form/Tabs.tsx | 29 +++- packages/web/src/components/Tx/TxFields.tsx | 128 ++++++++++++------ packages/web/src/screens/Test.tsx | 6 +- 11 files changed, 166 insertions(+), 61 deletions(-) create mode 100644 packages/web/src/components/Form/MoneyEdit.tsx diff --git a/.github/workflows/build-docker-container.yaml b/.github/workflows/build-docker-container.yaml index 7982d275..58f00e32 100644 --- a/.github/workflows/build-docker-container.yaml +++ b/.github/workflows/build-docker-container.yaml @@ -50,6 +50,7 @@ jobs: with: push: true tags: | + ghcr.io/venil7/assets:nightly ghcr.io/venil7/assets:${{ env.BUILD_TAG }} # builds release image, pushes under version and latest tags diff --git a/TODO.md b/TODO.md index 9f10c517..9d9326e1 100644 --- a/TODO.md +++ b/TODO.md @@ -1,9 +1,9 @@ # todo ## 1.9 + - [x] tx modal diff view for buy/sell (cap gains, limit on sales) - [ ] portfolio reports, cap gains - [ ] asset also in.. - - [ ] tx modal diff view for buy/sell (cap gains, limit on sales) ## 1.8.1 - [x] multi asset layered chart for portfolios diff --git a/packages/backend/src/enrichment/tx.ts b/packages/backend/src/enrichment/tx.ts index 8f4741ce..74e4fabc 100644 --- a/packages/backend/src/enrichment/tx.ts +++ b/packages/backend/src/enrichment/tx.ts @@ -1,6 +1,6 @@ import { calcPnl, - isBuy, + txBuy, type Action, type EnrichedTx, type GetTx @@ -19,7 +19,7 @@ const getTxEnricher = ), TE.map(({ meta }) => { // if meta is present, TX is of last stretch, and needs enrichment - const buy = isBuy(tx); + const buy = txBuy(tx); if (meta && buy) { const value = tx.quantity_ext * meta.regularMarketPrice; const [pnl, pnlPct] = calcPnl({ before: tx.cost, after: value }); diff --git a/packages/core/src/domain/tx.ts b/packages/core/src/domain/tx.ts index 423154e5..2bcbc3a1 100644 --- a/packages/core/src/domain/tx.ts +++ b/packages/core/src/domain/tx.ts @@ -36,9 +36,11 @@ export const byDateAsc = pipe( DateOrd, contramap((tx) => tx.date) ); +export const isBuy = (type: TxType) => type === "buy"; +export const isSell = (type: TxType) => !isBuy(type); -export const isBuy = ({ type }: T) => type == "buy"; -export const isSell = (tx: T) => !isBuy(tx); +export const txBuy = ({ type }: T) => isBuy(type); +export const txSell = (tx: T) => !txBuy(tx); export const toKey = (tx: T) => `tx-${tx.id}-${tx.modified.getTime()}`; diff --git a/packages/core/src/validation/tx.ts b/packages/core/src/validation/tx.ts index 180e523b..f4bce694 100644 --- a/packages/core/src/validation/tx.ts +++ b/packages/core/src/validation/tx.ts @@ -3,22 +3,21 @@ import { pipe } from "fp-ts/lib/function"; import { nonEmptyArray } from "io-ts-types"; import { BooleanDecoder, - nonNegative, PostTxDecoder, PostTxsUploadDecoder } from "../decoders"; import { nonFuture } from "../decoders/date"; import { chainDecoder } from "../decoders/util"; import type { PostTxsUpload } from "../domain"; -import { createValidator } from "./util"; +import { createValidator, nonNegativeField } from "./util"; export const txValidator = pipe( PostTxDecoder, chainDecoder(({ price, quantity, date }) => pipe( E.Do, - E.apS("price", nonNegative.decode(price)), - E.apS("quantity", nonNegative.decode(quantity)), + E.apS("price", nonNegativeField("Price").decode(price)), + E.apS("quantity", nonNegativeField("Quantity").decode(quantity)), E.apS("date", nonFuture.decode(date)) ) ), diff --git a/packages/core/src/validation/util.ts b/packages/core/src/validation/util.ts index 625cbaae..a7c941e6 100644 --- a/packages/core/src/validation/util.ts +++ b/packages/core/src/validation/util.ts @@ -1,6 +1,7 @@ import * as E from "fp-ts/lib/Either"; import { pipe } from "fp-ts/lib/function"; import * as t from "io-ts"; +import { nonNegative } from "../decoders"; import { nonEmptyString } from "../decoders/string"; import { validationErr, withErrorMessage } from "../decoders/util"; @@ -50,3 +51,6 @@ export const alphaNumOnly = (str: string) => export const nonEmptyField = (fieldName: string) => pipe(nonEmptyString, withErrorMessage(`${fieldName} can't be empty`)); + +export const nonNegativeField = (fieldName: string) => + pipe(nonNegative, withErrorMessage(`${fieldName} can't be zero or less`)); diff --git a/packages/web/src/components/Charts/AssetChart.tsx b/packages/web/src/components/Charts/AssetChart.tsx index 392f57b0..15b5347b 100644 --- a/packages/web/src/components/Charts/AssetChart.tsx +++ b/packages/web/src/components/Charts/AssetChart.tsx @@ -1,7 +1,7 @@ import { defined, - isBuy, - isSell, + txBuy, + txSell, type ChartData, type ChartDataPoint } from "@darkruby/assets-core"; @@ -130,13 +130,13 @@ const EventDot = ({ cx, cy, payload }: DotItemDotProps) => { const size = 2; switch (true) { - case isBuy(tx): + case txBuy(tx): return ( ); - case isSell(tx): + case txSell(tx): return ( diff --git a/packages/web/src/components/Form/MoneyEdit.tsx b/packages/web/src/components/Form/MoneyEdit.tsx new file mode 100644 index 00000000..539e0fb9 --- /dev/null +++ b/packages/web/src/components/Form/MoneyEdit.tsx @@ -0,0 +1,32 @@ +import { maybe, type Ccy } from "@darkruby/assets-core"; +import { InputGroup } from "react-bootstrap"; +import { useFormatters, usePrefs } from "../../hooks/prefs"; +import type { PropsOf } from "../../util/props"; +import { FormNumber } from "./NumberEdit"; + +type MoneyFieldProps = PropsOf & { + currency: Ccy; + toBase: (n: number) => number; +}; + +export const MoneyField: React.FC = ({ + currency, + toBase, + ...props +}) => { + const { money } = useFormatters(); + const { base_ccy } = usePrefs(); + const sameCcy = currency == base_ccy; + + const toBaseMaybe = maybe(toBase); + + return ( + + {currency} + + + + ); +}; diff --git a/packages/web/src/components/Form/Tabs.tsx b/packages/web/src/components/Form/Tabs.tsx index aba832ed..c3f3f42c 100644 --- a/packages/web/src/components/Form/Tabs.tsx +++ b/packages/web/src/components/Form/Tabs.tsx @@ -1,7 +1,10 @@ +import type { Optional } from "@darkruby/assets-core"; import { pipe, type Lazy } from "fp-ts/lib/function"; import { createContext, useContext, + useEffect, + useRef, useState, type PropsWithChildren } from "react"; @@ -10,14 +13,34 @@ import { withVisibility } from "../../decorators/nodata"; export type TabsProps = PropsWithChildren<{ tabs: readonly string[]; + onTabChange?: (idx: number) => void; + init?: number; hidden?: boolean; }>; const TabContext = createContext({ tab: 0 }); -export const Tabs: React.FC = ({ tabs, hidden, children }) => { - const [tab, setTab] = useState(0); - const handleTabClick = (idx: number) => () => setTab(idx); +export const Tabs: React.FC = ({ + tabs, + hidden, + onTabChange, + init = 0, + children +}) => { + const [tab, setTab] = useState(init >= 0 && init < tabs.length ? init : 0); + const lastCallbackTabRef = useRef>(undefined); + + const handleTabClick = (idx: number) => () => { + setTab(idx); + }; + + useEffect(() => { + if (lastCallbackTabRef.current !== tab) { + lastCallbackTabRef.current = tab; + onTabChange?.(tab); + } + }, [tab, onTabChange]); + return ( <>