Compare commits

...

37 Commits

Author SHA1 Message Date
Zach Gollwitzer
52d3528361 Bump to v0.1.0-alpha.17
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2024-09-13 17:24:46 -04:00
Zach Gollwitzer
d3d9af8bce Add basic self hosted onboarding (#1177)
* Add basic self hosted onboarding

* Lint fix

* Normalize translations
2024-09-13 17:24:19 -04:00
Zach Gollwitzer
0149ca4ea1 Transaction page design fixes (#1176)
* Fix transaction summary spacing

* Fix search input padding

* Search and transfer design fixes
2024-09-13 15:43:16 -04:00
Zach Gollwitzer
30f7c120e1 Allow partial investment quantities (#1174) 2024-09-13 11:45:27 -04:00
Zach Gollwitzer
949d3d80fa Support multi-currency transfers (#1175) 2024-09-13 11:45:19 -04:00
Zach Gollwitzer
c28dd8f940 Omit trend if zero in sidebar (#1173)
* Omit trend if zero in sidebar

* Lint fix
2024-09-13 11:28:47 -04:00
Tony Vincent
277e4476d9 Fix missing sync_all_button partial (#1172)
* Fix missing sync_all_button partial

* Add missing translation

* Bring back partial

* Unify button text translation

* Add test
2024-09-13 11:19:20 -04:00
Zach Gollwitzer
b9341ac302 Add sync status and errors to account settings page (#1169) 2024-09-11 17:24:01 -04:00
Valentin Zwerschke
86741401c3 Fix text (#1168)
Signed-off-by: Valentin Zwerschke <vallezw@gmail.com>
2024-09-11 13:40:29 -04:00
Tony Vincent
edf44bec03 Add setting to disable new user registration on self-hosted instances (#1163)
* Add clipboard stimulus controller

* Add invite codes controller

* Setting to force invite code for new signups

* Fix erb linter

* Normalize keys

* Add POST /invite_codes

* Cleanup clipboard_controller.js

* Create invite codes on-demand

* Design changes

* Style alignment

* Update app/views/invite_codes/_invite_code.html.erb

Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
Signed-off-by: Tony Vincent <tonyvince7@gmail.com>

* Update app/views/invite_codes/_invite_code.html.erb

Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
Signed-off-by: Tony Vincent <tonyvince7@gmail.com>

* Split into individual forms

* Fix missing styles

* Update app/javascript/controllers/clipboard_controller.js

Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
Signed-off-by: Tony Vincent <tonyvince7@gmail.com>

* Fix test

---------

Signed-off-by: Tony Vincent <tonyvince7@gmail.com>
Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
2024-09-11 13:04:39 -04:00
Jestin Palamuttam
5178928b68 fix: html dialog not closing (#1167) 2024-09-11 09:24:38 -04:00
Zach Gollwitzer
ac0ff35360 Update empty account states on dashboard (#1166)
* Update empty account states on dashboard

* Translations
2024-09-10 17:17:10 -04:00
Zach Gollwitzer
04037b8943 Consolidate transaction menu items (#1164)
* Fix valuation frame issue

* Consolidate transactions menu items

* Translations
2024-09-10 14:45:08 -04:00
Zach Gollwitzer
cb13fd2245 Fix valuation frame issue (#1162) 2024-09-09 17:13:52 -04:00
Zach Gollwitzer
eebc07d75e Feedback page (#1160)
* Add feedback page

* Only show latest release on changelog

* Constrain changelog height

* Ignore sanitization warning for Github content

* Add cassette for Github release notes

* Lint fix
2024-09-09 16:54:56 -04:00
dependabot[bot]
c30c1b9698 Bump hotwire-livereload from 1.4.0 to 1.4.1 (#1157)
Bumps [hotwire-livereload](https://github.com/kirillplatonov/hotwire-livereload) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/kirillplatonov/hotwire-livereload/releases)
- [Commits](https://github.com/kirillplatonov/hotwire-livereload/compare/v1.4.0...v1.4.1)

---
updated-dependencies:
- dependency-name: hotwire-livereload
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-09 07:57:30 -04:00
dependabot[bot]
d3971f9cee Bump inline_svg from 1.9.0 to 1.10.0 (#1156)
Bumps [inline_svg](https://github.com/jamesmartin/inline_svg) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/jamesmartin/inline_svg/releases)
- [Changelog](https://github.com/jamesmartin/inline_svg/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jamesmartin/inline_svg/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: inline_svg
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-09 07:57:15 -04:00
dependabot[bot]
9e4b931612 Bump pg from 1.5.7 to 1.5.8 (#1158)
Bumps [pg](https://github.com/ged/ruby-pg) from 1.5.7 to 1.5.8.
- [Changelog](https://github.com/ged/ruby-pg/blob/master/History.md)
- [Commits](https://github.com/ged/ruby-pg/compare/v1.5.7...v1.5.8)

---
updated-dependencies:
- dependency-name: pg
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-09 07:56:05 -04:00
dependabot[bot]
b44da70836 Bump aws-sdk-s3 from 1.159.0 to 1.160.0 (#1159)
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.159.0 to 1.160.0.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-09 07:55:51 -04:00
Zach Gollwitzer
0db75a019b Revert "Do not show registation link when REQUIRE_INVITE_CODE=true (#1148)" (#1155)
This reverts commit 9172eb931b.
2024-09-06 11:37:25 -04:00
dependabot[bot]
ee572d8d1f Bump selenium-webdriver from 4.23.0 to 4.24.0 (#1146)
Bumps [selenium-webdriver](https://github.com/SeleniumHQ/selenium) from 4.23.0 to 4.24.0.
- [Release notes](https://github.com/SeleniumHQ/selenium/releases)
- [Changelog](https://github.com/SeleniumHQ/selenium/blob/trunk/rb/CHANGES)
- [Commits](https://github.com/SeleniumHQ/selenium/compare/selenium-4.23.0...selenium-4.24.0)

---
updated-dependencies:
- dependency-name: selenium-webdriver
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 09:25:22 -04:00
dependabot[bot]
33d007a07b Bump good_job from 4.2.0 to 4.2.1 (#1144)
Bumps [good_job](https://github.com/bensheldon/good_job) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/bensheldon/good_job/releases)
- [Changelog](https://github.com/bensheldon/good_job/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bensheldon/good_job/compare/v4.2.0...v4.2.1)

---
updated-dependencies:
- dependency-name: good_job
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 09:25:11 -04:00
dependabot[bot]
3673ab8f03 Bump faraday from 2.10.1 to 2.11.0 (#1145)
Bumps [faraday](https://github.com/lostisland/faraday) from 2.10.1 to 2.11.0.
- [Release notes](https://github.com/lostisland/faraday/releases)
- [Changelog](https://github.com/lostisland/faraday/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lostisland/faraday/compare/v2.10.1...v2.11.0)

---
updated-dependencies:
- dependency-name: faraday
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 09:25:02 -04:00
dependabot[bot]
fb42c2ad43 Bump pagy from 9.0.6 to 9.0.8 (#1147)
Bumps [pagy](https://github.com/ddnexus/pagy) from 9.0.6 to 9.0.8.
- [Release notes](https://github.com/ddnexus/pagy/releases)
- [Changelog](https://github.com/ddnexus/pagy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ddnexus/pagy/compare/9.0.6...9.0.8)

---
updated-dependencies:
- dependency-name: pagy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 09:24:44 -04:00
Tony Vincent
9172eb931b Do not show registation link when REQUIRE_INVITE_CODE=true (#1148) 2024-09-06 08:52:26 -04:00
Josh Pigford
0c8cf7e217 Update .gitignore 2024-09-03 12:22:54 -04:00
Josh Pigford
0bbf7f82b7 .ai directory 2024-09-03 09:54:01 -04:00
Zach Gollwitzer
c05ee9b572 Remove unused settings temporarily (#1136) 2024-08-27 17:10:31 -04:00
Zach Gollwitzer
38c2b4670c Categories, tags, merchants, and menus improvements (#1135) 2024-08-27 17:06:41 -04:00
Zach Gollwitzer
f82ce59dad Fix merchants color picker (#1134)
* Fix merchants color picker

* Lint fixes
2024-08-26 19:18:27 -04:00
Zach Gollwitzer
166ed4b1ea Fix account transaction form resetting amount to 0 (#1133) 2024-08-26 19:10:17 -04:00
dependabot[bot]
0c0db44b7f Bump rails from 7.2.0 to 7.2.1 (#1130)
Bumps [rails](https://github.com/rails/rails) from 7.2.0 to 7.2.1.
- [Release notes](https://github.com/rails/rails/releases)
- [Commits](https://github.com/rails/rails/compare/v7.2.0...v7.2.1)

---
updated-dependencies:
- dependency-name: rails
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 10:38:46 -04:00
dependabot[bot]
cd254fd19b Bump aws-sdk-s3 from 1.158.0 to 1.159.0 (#1129)
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.158.0 to 1.159.0.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 09:37:08 -04:00
dependabot[bot]
0d20be4905 Bump pagy from 9.0.5 to 9.0.6 (#1128)
Bumps [pagy](https://github.com/ddnexus/pagy) from 9.0.5 to 9.0.6.
- [Release notes](https://github.com/ddnexus/pagy/releases)
- [Changelog](https://github.com/ddnexus/pagy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ddnexus/pagy/compare/9.0.5...9.0.6)

---
updated-dependencies:
- dependency-name: pagy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 09:36:59 -04:00
Tony Vincent
cf861ccff9 Fix account sync when prices missing (#1127) 2024-08-26 09:36:27 -04:00
dependabot[bot]
525439e44d Bump vcr from 6.2.0 to 6.3.1 (#1131)
Bumps [vcr](https://github.com/vcr/vcr) from 6.2.0 to 6.3.1.
- [Release notes](https://github.com/vcr/vcr/releases)
- [Changelog](https://github.com/vcr/vcr/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vcr/vcr/compare/v6.2.0...v6.3.1)

---
updated-dependencies:
- dependency-name: vcr
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 09:35:04 -04:00
Tony Vincent
e1efe97e6f Fix unable to create Deposit entries in investment portfolio (#1125)
* Fix unable to create Deposit entries in investment portfolio

* Add system test for deposit transaction
2024-08-25 17:48:46 -04:00
121 changed files with 1442 additions and 877 deletions

64
.ai/cursorrules.md Normal file
View File

@@ -0,0 +1,64 @@
<!-- Copy this file to .cursorrules in the root of the project on your local machine if you'd like to use these rules with Cursor. -->
You are an expert in Ruby, Ruby on Rails, Postgres, Tailwind, Stimulus, Hotwire and Turbo and always use the latest stable versions of those technologies.
**Code Style and Structure**
- Write concise, technical Ruby code with accurate examples.
- Prefer iteration and modularization over code duplication.
- Use descriptive variable names with auxiliary verbs (e.g., is_loading, has_error).
- Structure files: models, controllers, views, helpers, services, jobs, mailers.
**Naming Conventions**
- Use snake_case for file names and directories (e.g., app/models/user_profile.rb).
- Use CamelCase for classes and modules (e.g., UserProfile).
**Ruby on Rails Usage**
- Use Rails conventions for MVC structure.
- Favor scopes over class methods for queries.
- Use strong parameters for mass assignment protection.
- Use partials to DRY up views.
**Syntax and Formatting**
- Use two spaces for indentation.
- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
- Use descriptive method names and keep methods short.
**Commenting Code**
- Write clear, concise comments to explain the purpose of individual functions and methods.
- Use comments to describe the intent and functionality of complex logic.
- Avoid redundant comments that state the obvious.
**UI and Styling**
- Use Tailwind CSS for styling.
- Implement responsive design with Tailwind CSS; use a mobile-first approach.
- Use Stimulus for JavaScript behavior.
- Use Turbo for asynchronous actions and updates.
**Performance Optimization**
- Use eager loading to avoid N+1 queries.
- Cache expensive queries and partials where appropriate.
- Use background jobs for long-running tasks.
- Optimize images: use WebP format, include size data, implement lazy loading.
**Database Querying & Data Model Creation**
- Use ActiveRecord for data querying and model creation.
- Favor database constraints and indexes for data integrity and performance.
- Use migrations to manage schema changes.
**Key Conventions**
- Follow Rails best practices for RESTful routing.
- Optimize for performance and security.
- Use environment variables for configuration.
- Write tests for models, controllers, and features.
**AI Guidelines**
- Follow the users requirements carefully & to the letter.
- Confirm, then write code!
- Suggest solutions that I didn't think about—anticipate my needs
- Focus on readability over being performant.
- Fully implement all requested functionality.
- Leave NO todos, placeholders or missing pieces.
- Don't say things like "additional logic can be added here" — instead, add the logic.
- Be concise. Minimize any other prose.
- Consider new technologies and contrarian ideas, not just the conventional wisdom
- If I ask for adjustments to code, do not repeat all of my code unnecessarily. Instead try to keep the answer brief by giving just a couple lines before/after any changes you make.

1
.gitignore vendored
View File

@@ -59,3 +59,4 @@ compose-dev.yaml
gcp-storage-keyfile.json
coverage
.cursorrules

View File

@@ -3,7 +3,7 @@ source "https://rubygems.org"
ruby file: ".ruby-version"
# Rails
gem "rails", "~> 7.2.0"
gem "rails", "~> 7.2.1"
# Drivers
gem "pg", "~> 1.5"

View File

@@ -8,29 +8,29 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (7.2.0)
actionpack (= 7.2.0)
activesupport (= 7.2.0)
actioncable (7.2.1)
actionpack (= 7.2.1)
activesupport (= 7.2.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.2.0)
actionpack (= 7.2.0)
activejob (= 7.2.0)
activerecord (= 7.2.0)
activestorage (= 7.2.0)
activesupport (= 7.2.0)
actionmailbox (7.2.1)
actionpack (= 7.2.1)
activejob (= 7.2.1)
activerecord (= 7.2.1)
activestorage (= 7.2.1)
activesupport (= 7.2.1)
mail (>= 2.8.0)
actionmailer (7.2.0)
actionpack (= 7.2.0)
actionview (= 7.2.0)
activejob (= 7.2.0)
activesupport (= 7.2.0)
actionmailer (7.2.1)
actionpack (= 7.2.1)
actionview (= 7.2.1)
activejob (= 7.2.1)
activesupport (= 7.2.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.2.0)
actionview (= 7.2.0)
activesupport (= 7.2.0)
actionpack (7.2.1)
actionview (= 7.2.1)
activesupport (= 7.2.1)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4, < 3.2)
@@ -39,35 +39,35 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (7.2.0)
actionpack (= 7.2.0)
activerecord (= 7.2.0)
activestorage (= 7.2.0)
activesupport (= 7.2.0)
actiontext (7.2.1)
actionpack (= 7.2.1)
activerecord (= 7.2.1)
activestorage (= 7.2.1)
activesupport (= 7.2.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.2.0)
activesupport (= 7.2.0)
actionview (7.2.1)
activesupport (= 7.2.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.2.0)
activesupport (= 7.2.0)
activejob (7.2.1)
activesupport (= 7.2.1)
globalid (>= 0.3.6)
activemodel (7.2.0)
activesupport (= 7.2.0)
activerecord (7.2.0)
activemodel (= 7.2.0)
activesupport (= 7.2.0)
activemodel (7.2.1)
activesupport (= 7.2.1)
activerecord (7.2.1)
activemodel (= 7.2.1)
activesupport (= 7.2.1)
timeout (>= 0.4.0)
activestorage (7.2.0)
actionpack (= 7.2.0)
activejob (= 7.2.0)
activerecord (= 7.2.0)
activesupport (= 7.2.0)
activestorage (7.2.1)
actionpack (= 7.2.1)
activejob (= 7.2.1)
activerecord (= 7.2.1)
activesupport (= 7.2.1)
marcel (~> 1.0)
activesupport (7.2.0)
activesupport (7.2.1)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
@@ -82,17 +82,17 @@ GEM
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
aws-eventstream (1.3.0)
aws-partitions (1.965.0)
aws-sdk-core (3.201.5)
aws-partitions (1.971.0)
aws-sdk-core (3.203.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.88.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-kms (1.89.0)
aws-sdk-core (~> 3, >= 3.203.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.158.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-s3 (1.160.0)
aws-sdk-core (~> 3, >= 3.203.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.9.1)
@@ -153,10 +153,10 @@ GEM
tzinfo
faker (3.4.2)
i18n (>= 1.8.11, < 2)
faraday (2.10.1)
faraday-net_http (>= 2.0, < 3.2)
faraday (2.11.0)
faraday-net_http (>= 2.0, < 3.4)
logger
faraday-net_http (3.1.1)
faraday-net_http (3.3.0)
net-http
faraday-retry (2.2.1)
faraday (~> 2.0)
@@ -171,7 +171,7 @@ GEM
raabro (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
good_job (4.2.0)
good_job (4.2.1)
activejob (>= 6.1.0)
activerecord (>= 6.1.0)
concurrent-ruby (>= 1.3.1)
@@ -180,7 +180,7 @@ GEM
thor (>= 1.0.0)
hashdiff (1.1.0)
highline (3.0.1)
hotwire-livereload (1.4.0)
hotwire-livereload (1.4.1)
actioncable (>= 6.0.0)
listen (>= 3.0.0)
railties (>= 6.0.0)
@@ -203,7 +203,7 @@ GEM
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0)
inline_svg (1.9.0)
inline_svg (1.10.0)
activesupport (>= 3.0)
nokogiri (>= 1.6)
io-console (0.7.2)
@@ -221,7 +221,7 @@ GEM
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.6.0)
logger (1.6.1)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
@@ -265,12 +265,12 @@ GEM
octokit (9.1.0)
faraday (>= 1, < 3)
sawyer (~> 0.9)
pagy (9.0.5)
pagy (9.0.8)
parallel (1.25.1)
parser (3.3.4.0)
ast (~> 2.4.1)
racc
pg (1.5.7)
pg (1.5.8)
prism (0.30.0)
propshaft (0.9.1)
actionpack (>= 7.0.0)
@@ -292,20 +292,20 @@ GEM
rackup (2.1.0)
rack (>= 3)
webrick (~> 1.8)
rails (7.2.0)
actioncable (= 7.2.0)
actionmailbox (= 7.2.0)
actionmailer (= 7.2.0)
actionpack (= 7.2.0)
actiontext (= 7.2.0)
actionview (= 7.2.0)
activejob (= 7.2.0)
activemodel (= 7.2.0)
activerecord (= 7.2.0)
activestorage (= 7.2.0)
activesupport (= 7.2.0)
rails (7.2.1)
actioncable (= 7.2.1)
actionmailbox (= 7.2.1)
actionmailer (= 7.2.1)
actionpack (= 7.2.1)
actiontext (= 7.2.1)
actionview (= 7.2.1)
activejob (= 7.2.1)
activemodel (= 7.2.1)
activerecord (= 7.2.1)
activestorage (= 7.2.1)
activesupport (= 7.2.1)
bundler (>= 1.15.0)
railties (= 7.2.0)
railties (= 7.2.1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
@@ -319,9 +319,9 @@ GEM
rails-settings-cached (2.9.4)
activerecord (>= 5.0.0)
railties (>= 5.0.0)
railties (7.2.0)
actionpack (= 7.2.0)
activesupport (= 7.2.0)
railties (7.2.1)
actionpack (= 7.2.1)
activesupport (= 7.2.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -330,7 +330,7 @@ GEM
rainbow (3.1.1)
rake (13.2.1)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
rb-inotify (0.11.1)
ffi (~> 1.0)
rbs (3.5.2)
logger
@@ -338,9 +338,9 @@ GEM
psych (>= 4.0.0)
redcarpet (3.6.0)
regexp_parser (2.9.2)
reline (0.5.9)
reline (0.5.10)
io-console (~> 0.5)
rexml (3.3.4)
rexml (3.3.6)
strscan
rubocop (1.65.1)
json (~> 2.3)
@@ -388,7 +388,7 @@ GEM
addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3)
securerandom (0.3.1)
selenium-webdriver (4.23.0)
selenium-webdriver (4.24.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
@@ -427,7 +427,7 @@ GEM
railties (>= 7.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
thor (1.3.1)
thor (1.3.2)
timeout (0.4.1)
turbo-rails (2.0.6)
actionpack (>= 6.0.0)
@@ -436,9 +436,10 @@ GEM
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
uri (0.13.0)
uri (0.13.1)
useragent (0.16.10)
vcr (6.2.0)
vcr (6.3.1)
base64
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
@@ -455,7 +456,7 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.17)
zeitwerk (2.6.18)
PLATFORMS
aarch64-linux
@@ -493,7 +494,7 @@ DEPENDENCIES
pg (~> 1.5)
propshaft
puma (>= 5.0)
rails (~> 7.2.0)
rails (~> 7.2.1)
rails-settings-cached
redcarpet
rubocop-rails-omakase

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

View File

@@ -94,6 +94,18 @@
.tooltip {
@apply hidden absolute;
}
.btn {
@apply px-3 py-2 rounded-lg text-sm font-medium;
}
.btn--primary {
@apply bg-gray-900 text-white hover:bg-gray-700;
}
.btn--light {
@apply bg-gray-25 border border-alpha-black-200 text-gray-900 hover:bg-gray-50;
}
}
/* Small, single purpose classes that should take precedence over other styles */
@@ -110,4 +122,4 @@
.scrollbar::-webkit-scrollbar-thumb:hover {
background: #a6a6a6;
}
}
}

View File

@@ -12,8 +12,7 @@ class Account::TransactionsController < ApplicationController
end
def update
@entry.update!(entry_params.merge(amount: amount))
@entry.sync_account_later
@entry.update!(entry_params)
respond_to do |format|
format.html { redirect_to account_entry_path(@account, @entry), notice: t(".success") }
@@ -34,7 +33,7 @@ class Account::TransactionsController < ApplicationController
def entry_params
params.require(:account_entry)
.permit(
:name, :date, :amount, :currency, :entryable_type,
:name, :date, :amount, :currency, :entryable_type, :nature,
entryable_attributes: [
:id,
:notes,
@@ -43,14 +42,18 @@ class Account::TransactionsController < ApplicationController
:merchant_id,
{ tag_ids: [] }
]
)
end
).tap do |permitted_params|
nature = permitted_params.delete(:nature)
def amount
if params[:account_entry][:nature] == "income"
entry_params[:amount].to_d * -1
else
entry_params[:amount].to_d
end
if permitted_params[:amount]
amount_value = permitted_params[:amount].to_d
if nature == "income"
amount_value *= -1
end
permitted_params[:amount] = amount_value
end
end
end
end

View File

@@ -40,6 +40,6 @@ class Account::TransfersController < ApplicationController
end
def transfer_params
params.require(:account_transfer).permit(:from_account_id, :to_account_id, :amount, :currency, :date, :name)
params.require(:account_transfer).permit(:from_account_id, :to_account_id, :amount, :date, :name)
end
end

View File

@@ -12,7 +12,7 @@ class Account::ValuationsController < ApplicationController
if @entry.save
@entry.sync_account_later
redirect_to account_valuations_path(@account), notice: t(".success")
redirect_back_or_to account_valuations_path(@account), notice: t(".success")
else
flash[:alert] = @entry.errors.full_messages.to_sentence
redirect_to account_path(@account)

View File

@@ -17,7 +17,11 @@ module Authentication
if user = User.find_by(id: session[:user_id])
Current.user = user
else
redirect_to new_session_url
if self_hosted_first_login?
redirect_to new_registration_url
else
redirect_to new_session_url
end
end
end
@@ -36,4 +40,8 @@ module Authentication
def set_last_login_at
Current.user.update(last_login_at: DateTime.now)
end
def self_hosted_first_login?
Rails.application.config.app_mode.self_hosted? && User.count.zero?
end
end

View File

@@ -7,6 +7,10 @@ module Invitable
private
def invite_code_required?
ENV["REQUIRE_INVITE_CODE"] == "true"
self_hosted? ? Setting.require_invite_for_signup : ENV["REQUIRE_INVITE_CODE"] == "true"
end
def self_hosted?
Rails.application.config.app_mode.self_hosted?
end
end

View File

@@ -2,11 +2,15 @@ module SelfHostable
extend ActiveSupport::Concern
included do
helper_method :self_hosted?
helper_method :self_hosted?, :self_hosted_first_login?
end
private
def self_hosted?
Rails.configuration.app_mode.self_hosted?
end
def self_hosted_first_login?
self_hosted? && User.count.zero?
end
end

View File

@@ -23,6 +23,11 @@ class InstitutionsController < ApplicationController
redirect_to accounts_path, notice: t(".success")
end
def sync
@institution.sync
redirect_back_or_to accounts_path, notice: t(".success")
end
private
def institution_params

View File

@@ -0,0 +1,10 @@
class InviteCodesController < ApplicationController
def index
@invite_codes = InviteCode.all
end
def create
InviteCode.generate!
redirect_back_or_to invite_codes_path, notice: "Code generated"
end
end

View File

@@ -31,7 +31,7 @@ class PagesController < ApplicationController
end
def changelog
@releases_notes = Provider::Github.new.fetch_latest_releases_notes
@release_notes = Provider::Github.new.fetch_latest_release_notes
end
def feedback

View File

@@ -1,7 +0,0 @@
class Settings::BillingsController < SettingsController
def edit
end
def update
end
end

View File

@@ -55,13 +55,13 @@ class Settings::HostingsController < SettingsController
end
def hosting_params
permitted_params = params.require(:setting).permit(:render_deploy_hook, :upgrades_mode, :email_sender, :app_domain, :smtp_host, :smtp_port, :smtp_username, :smtp_password)
permitted_params = params.require(:setting).permit(:render_deploy_hook, :upgrades_mode, :email_sender, :app_domain, :smtp_host, :smtp_port, :smtp_username, :smtp_password, :require_invite_for_signup)
result = {}
result[:upgrades_mode] = permitted_params[:upgrades_mode] == "manual" ? "manual" : "auto" if permitted_params.key?(:upgrades_mode)
result[:render_deploy_hook] = permitted_params[:render_deploy_hook] if permitted_params.key?(:render_deploy_hook)
result[:upgrades_target] = permitted_params[:upgrades_mode] unless permitted_params[:upgrades_mode] == "manual" if permitted_params.key?(:upgrades_mode)
result.merge!(permitted_params.slice(:email_sender, :app_domain, :smtp_host, :smtp_port, :smtp_username, :smtp_password))
result.merge!(permitted_params.slice(:email_sender, :app_domain, :smtp_host, :smtp_port, :smtp_username, :smtp_password, :require_invite_for_signup))
result
end

View File

@@ -1,7 +0,0 @@
class Settings::NotificationsController < SettingsController
def edit
end
def update
end
end

View File

@@ -1,7 +0,0 @@
class Settings::SecuritiesController < SettingsController
def edit
end
def update
end
end

View File

@@ -69,6 +69,11 @@ module AccountsHelper
tab || available_tabs.first
end
def account_groups(period: nil)
assets, liabilities = Current.family.accounts.by_group(currency: Current.family.currency, period: period || Period.last_30_days).values_at(:assets, :liabilities)
[ assets.children, liabilities.children ].flatten
end
private
def class_mapping(accountable_type)

View File

@@ -57,11 +57,6 @@ module ApplicationHelper
render partial: "shared/drawer", locals: { content: content }
end
def account_groups(period: nil)
assets, liabilities = Current.family.accounts.by_group(currency: Current.family.currency, period: period || Period.last_30_days).values_at(:assets, :liabilities)
[ assets.children, liabilities.children ].flatten
end
def sidebar_link_to(name, path, options = {})
is_current = current_page?(path) || (request.path.start_with?(path) && path != "/")

View File

@@ -1,12 +1,12 @@
module MenusHelper
def contextual_menu(&block)
tag.div class: "relative cursor-pointer", data: { controller: "menu" } do
tag.div data: { controller: "menu" } do
concat contextual_menu_icon
concat contextual_menu_content(&block)
end
end
def contextual_menu_modal_action_item(label, url, icon: "pencil-line", turbo_frame: nil)
def contextual_menu_modal_action_item(label, url, icon: "pencil-line", turbo_frame: :modal)
link_to url, class: "flex items-center rounded-lg text-gray-900 hover:bg-gray-50 py-2 px-3 gap-2", data: { turbo_frame: } do
concat(lucide_icon(icon, class: "shrink-0 w-5 h-5 text-gray-500"))
concat(tag.span(label, class: "text-sm"))
@@ -25,13 +25,14 @@ module MenusHelper
private
def contextual_menu_icon
tag.button class: "flex hover:bg-gray-100 p-2 rounded", data: { menu_target: "button" } do
tag.button class: "flex hover:bg-gray-100 p-2 rounded cursor-pointer", data: { menu_target: "button" } do
lucide_icon "more-horizontal", class: "w-5 h-5 text-gray-500"
end
end
def contextual_menu_content(&block)
tag.div class: "absolute z-10 top-10 right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs hidden", data: { menu_target: "content" } do
tag.div class: "z-50 border border-alpha-black-25 bg-white rounded-lg shadow-xs hidden",
data: { menu_target: "content" } do
capture(&block)
end
end

View File

@@ -1,14 +1,46 @@
module SettingsHelper
def next_setting(title, path)
render partial: "settings/nav_link_large", locals: { path: path, direction: "next", title: title }
end
SETTINGS_ORDER = [
{ name: I18n.t("settings.nav.profile_label"), path: :settings_profile_path },
{ name: I18n.t("settings.nav.preferences_label"), path: :settings_preferences_path },
{ name: I18n.t("settings.nav.self_hosting_label"), path: :settings_hosting_path, condition: :self_hosted? },
{ name: I18n.t("settings.nav.accounts_label"), path: :accounts_path },
{ name: I18n.t("settings.nav.tags_label"), path: :tags_path },
{ name: I18n.t("settings.nav.categories_label"), path: :categories_path },
{ name: I18n.t("settings.nav.merchants_label"), path: :merchants_path },
{ name: I18n.t("settings.nav.imports_label"), path: :imports_path },
{ name: I18n.t("settings.nav.whats_new_label"), path: :changelog_path },
{ name: I18n.t("settings.nav.feedback_label"), path: :feedback_path }
]
def previous_setting(title, path)
render partial: "settings/nav_link_large", locals: { path: path, direction: "previous", title: title }
def adjacent_setting(current_path, offset)
visible_settings = SETTINGS_ORDER.select { |setting| setting[:condition].nil? || send(setting[:condition]) }
current_index = visible_settings.index { |setting| send(setting[:path]) == current_path }
return nil unless current_index
adjacent_index = current_index + offset
return nil if adjacent_index < 0 || adjacent_index >= visible_settings.size
adjacent = visible_settings[adjacent_index]
render partial: "settings/nav_link_large", locals: {
path: send(adjacent[:path]),
direction: offset > 0 ? "next" : "previous",
title: adjacent[:name]
}
end
def settings_section(title:, subtitle: nil, &block)
content = capture(&block)
render partial: "settings/section", locals: { title: title, subtitle: subtitle, content: content }
end
def settings_nav_footer
previous_setting = adjacent_setting(request.path, -1)
next_setting = adjacent_setting(request.path, 1)
content_tag :div, class: "flex justify-between gap-4" do
concat(previous_setting)
concat(next_setting)
end
end
end

View File

@@ -0,0 +1,28 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["source", "iconDefault", "iconSuccess"]
copy(event) {
event.preventDefault();
if (this.sourceTarget && this.sourceTarget.textContent) {
navigator.clipboard.writeText(this.sourceTarget.textContent)
.then(() => {
this.showSuccess();
})
.catch((error) => {
console.error('Failed to copy text: ', error);
});
}
}
showSuccess() {
this.iconDefaultTarget.classList.add('hidden');
this.iconSuccessTarget.classList.remove('hidden');
setTimeout(() => {
this.iconDefaultTarget.classList.remove('hidden');
this.iconSuccessTarget.classList.add('hidden');
}, 3000);
}
}

View File

@@ -1,32 +1,29 @@
import {Controller} from "@hotwired/stimulus";
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="merchant-avatar"
// Connects to data-controller="color-avatar"
// Used by the transaction merchant form to show a preview of what the avatar will look like
export default class extends Controller {
static targets = [
"name",
"color",
"avatar"
];
connect() {
this.nameTarget.addEventListener("input", this.handleNameChange);
this.colorTarget.addEventListener("input", this.handleColorChange);
}
disconnect() {
this.nameTarget.removeEventListener("input", this.handleNameChange);
this.colorTarget.removeEventListener("input", this.handleColorChange);
}
handleNameChange = (e) => {
this.avatarTarget.textContent = (e.currentTarget.value?.[0] || "?").toUpperCase();
}
handleColorChange = (e) => {
handleColorChange(e) {
const color = e.currentTarget.value;
this.avatarTarget.style.backgroundColor = `color-mix(in srgb, ${color} 5%, white)`;
this.avatarTarget.style.borderColor = `color-mix(in srgb, ${color} 10%, white)`;
this.avatarTarget.style.color = color;
}
}
}

View File

@@ -1,61 +1,57 @@
import { Controller } from "@hotwired/stimulus";
import { computePosition, flip, shift, offset, autoUpdate } from '@floating-ui/dom';
/**
* A "menu" can contain arbitrary content including non-clickable items, links, buttons, and forms.
*
* - If you need a form-enabled "select" element, use the "listbox" controller instead.
*/
export default class extends Controller {
static targets = [
"button",
"content",
"submenu",
"submenuButton",
"submenuContent",
];
static targets = ["button", "content"];
static values = {
show: { type: Boolean, default: false },
showSubmenu: { type: Boolean, default: false },
show: Boolean,
placement: { type: String, default: "bottom-end" },
offset: { type: Number, default: 6 },
};
initialize() {
connect() {
this.show = this.showValue;
this.showSubmenu = this.showSubmenuValue;
this.boundUpdate = this.update.bind(this);
this.addEventListeners();
this.startAutoUpdate();
}
connect() {
disconnect() {
this.removeEventListeners();
this.stopAutoUpdate();
this.close();
}
addEventListeners() {
this.buttonTarget.addEventListener("click", this.toggle);
this.element.addEventListener("keydown", this.handleKeydown);
document.addEventListener("click", this.handleOutsideClick);
document.addEventListener("turbo:load", this.handleTurboLoad);
}
disconnect() {
this.element.removeEventListener("keydown", this.handleKeydown);
removeEventListeners() {
this.buttonTarget.removeEventListener("click", this.toggle);
this.element.removeEventListener("keydown", this.handleKeydown);
document.removeEventListener("click", this.handleOutsideClick);
document.removeEventListener("turbo:load", this.handleTurboLoad);
this.close();
}
// If turbo reloads, we maintain the state of the menu
handleTurboLoad = () => {
if (!this.show) this.close();
};
handleOutsideClick = (event) => {
if (this.show && !this.element.contains(event.target)) {
this.close();
}
if (this.show && !this.element.contains(event.target)) this.close();
};
handleKeydown = (event) => {
switch (event.key) {
case "Escape":
this.close();
this.buttonTarget.focus(); // Bring focus back to the button
break;
if (event.key === "Escape") {
this.close();
this.buttonTarget.focus();
}
};
@@ -63,6 +59,7 @@ export default class extends Controller {
this.show = !this.show;
this.contentTarget.classList.toggle("hidden", !this.show);
if (this.show) {
this.update();
this.focusFirstElement();
}
};
@@ -73,12 +70,40 @@ export default class extends Controller {
}
focusFirstElement() {
const focusableElements =
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
const firstFocusableElement =
this.contentTarget.querySelectorAll(focusableElements)[0];
const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
const firstFocusableElement = this.contentTarget.querySelectorAll(focusableElements)[0];
if (firstFocusableElement) {
firstFocusableElement.focus();
}
}
startAutoUpdate() {
if (!this._cleanup) {
this._cleanup = autoUpdate(this.buttonTarget, this.contentTarget, this.boundUpdate);
}
}
stopAutoUpdate() {
if (this._cleanup) {
this._cleanup();
this._cleanup = null;
}
}
update() {
computePosition(this.buttonTarget, this.contentTarget, {
placement: this.placementValue,
middleware: [
offset(this.offsetValue),
flip(),
shift({ padding: 5 })
],
}).then(({ x, y }) => {
Object.assign(this.contentTarget.style, {
position: 'fixed',
left: `${x}px`,
top: `${y}px`,
});
});
}
}

View File

@@ -204,7 +204,6 @@ class Account::Entry < ApplicationRecord
current_qty = account.holding_qty(account_trade.security)
if current_qty < account_trade.qty.abs
# i18n-tasks-use t('activerecord.errors.models.account/entry.attributes.base.invalid_sell_quantity')
errors.add(
:base,
:invalid_sell_quantity,

View File

@@ -21,7 +21,7 @@ class Account::TransactionBuilder
end
def create_transfer
return create_unlinked_transfer(account.id, signed_amount) unless transfer_account_id
return create_unlinked_transfer(account.id, signed_amount) if transfer_account_id.blank?
from_account_id = type == "transfer_in" ? transfer_account_id : account.id
to_account_id = type == "transfer_in" ? account.id : transfer_account_id

View File

@@ -46,7 +46,7 @@ class Account::Transfer < ApplicationRecord
def build_from_accounts(from_account, to_account, date:, amount:, currency:, name:)
outflow = from_account.entries.build \
amount: amount.abs,
currency: currency,
currency: from_account.currency,
date: date,
name: name,
marked_as_transfer: true,
@@ -54,7 +54,7 @@ class Account::Transfer < ApplicationRecord
inflow = to_account.entries.build \
amount: amount.abs * -1,
currency: currency,
currency: from_account.currency,
date: date,
name: name,
marked_as_transfer: true,
@@ -72,27 +72,23 @@ class Account::Transfer < ApplicationRecord
def transaction_count
unless entries.size == 2
# i18n-tasks-use t('activerecord.errors.models.account/transfer.attributes.entries.must_have_exactly_2_entries')
errors.add :entries, :must_have_exactly_2_entries
end
end
def from_different_accounts
accounts = entries.map { |e| e.account_id }.uniq
# i18n-tasks-use t('activerecord.errors.models.account/transfer.attributes.entries.must_be_from_different_accounts')
errors.add :entries, :must_be_from_different_accounts if accounts.size < entries.size
end
def net_zero_flows
unless entries.sum(&:amount).zero?
# i18n-tasks-use t('activerecord.errors.models.account/transfer.attributes.entries.must_have_an_inflow_and_outflow_that_net_to_zero')
errors.add :entries, :must_have_an_inflow_and_outflow_that_net_to_zero
end
end
def all_transactions_marked
unless entries.all?(&:marked_as_transfer)
# i18n-tasks-use t('activerecord.errors.models.account/transfer.attributes.entries.must_be_marked_as_transfer')
errors.add :entries, :must_be_marked_as_transfer
end
end

View File

@@ -6,6 +6,7 @@ class Demo::Generator
end
def reset_and_clear_data!
reset_settings!
clear_data!
create_user!
@@ -14,6 +15,7 @@ class Demo::Generator
def reset_data!
Family.transaction do
reset_settings!
clear_data!
create_user!
@@ -52,12 +54,17 @@ class Demo::Generator
end
def clear_data!
InviteCode.destroy_all
User.find_by_email("user@maybe.local")&.destroy
ExchangeRate.destroy_all
Security.destroy_all
Security::Price.destroy_all
end
def reset_settings!
Setting.destroy_all
end
def create_user!
family.users.create! \
email: "user@maybe.local",

View File

@@ -179,7 +179,6 @@ class Import < ApplicationRecord
begin
CSV.parse(raw_file_str || "", col_sep:)
rescue CSV::MalformedCSVError
# i18n-tasks-use t('activerecord.errors.models.import.attributes.raw_file_str.invalid_csv_format')
errors.add(:raw_file_str, :invalid_csv_format)
end
end

View File

@@ -4,4 +4,22 @@ class Institution < ApplicationRecord
has_one_attached :logo
scope :alphabetically, -> { order(name: :asc) }
def sync
accounts.active.each do |account|
if account.needs_sync?
account.sync
end
end
update! last_synced_at: Time.now
end
def syncing?
accounts.active.any? { |account| account.syncing? }
end
def has_issues?
accounts.active.any? { |account| account.has_issues? }
end
end

View File

@@ -16,7 +16,7 @@ class Issue::PricesMissing < Issue
missing_prices.each do |ticker, dates|
next unless issuable.owns_ticker?(ticker)
oldest_date = dates.min
oldest_date = dates.min.to_date
expected_price_count = (oldest_date..Date.current).count
prices = Security::Price.find_prices(ticker: ticker, start_date: oldest_date)
stale = false if prices.count < expected_price_count

View File

@@ -40,23 +40,24 @@ class Provider::Github
end
end
def fetch_latest_releases_notes
def fetch_latest_release_notes
begin
Rails.cache.fetch("latest_github_releases_notes", expires_in: 2.hours) do
releases = Octokit.releases(repo)
releases.map do |release|
Rails.cache.fetch("latest_github_release_notes", expires_in: 2.hours) do
release = Octokit.releases(repo).first
if release
{
avatar: release.author.avatar_url,
name: release.name,
published_at: release.published_at,
body: Octokit.markdown(release.body, mode: "gfm", context: repo)
}
else
nil
end
end
rescue => e
Rails.logger.error "Failed to fetch latest GitHub releases notes: #{e.message}"
[]
Rails.logger.error "Failed to fetch latest GitHub release notes: #{e.message}"
nil
end
end

View File

@@ -22,6 +22,8 @@ class Setting < RailsSettings::Base
field :synth_api_key, type: :string, default: ENV["SYNTH_API_KEY"]
field :require_invite_for_signup, type: :boolean, default: false
scope :smtp_settings do
field :smtp_host, type: :string, read_only: true, default: ENV["SMTP_ADDRESS"]
field :smtp_port, type: :string, read_only: true, default: ENV["SMTP_PORT"]

View File

@@ -83,21 +83,17 @@ class TimeSeries::Trend
def values_must_be_of_same_type
unless current.class == previous.class || [ previous, current ].any?(&:nil?)
# i18n-tasks-use t('activemodel.errors.models.time_series/trend.attributes.current.must_be_of_the_same_type_as_previous')
errors.add :current, :must_be_of_the_same_type_as_previous
# i18n-tasks-use t('activemodel.errors.models.time_series/trend.attributes.previous.must_be_of_the_same_type_as_current')
errors.add :previous, :must_be_of_the_same_type_as_current
end
end
def values_must_be_of_known_type
unless current.is_a?(Money) || current.is_a?(Numeric) || current.nil?
# i18n-tasks-use t('activemodel.errors.models.time_series/trend.attributes.current.must_be_of_type_money_numeric_or_nil')
errors.add :current, :must_be_of_type_money_numeric_or_nil
end
unless previous.is_a?(Money) || previous.is_a?(Numeric) || previous.nil?
# i18n-tasks-use t('activemodel.errors.models.time_series/trend.attributes.previous.must_be_of_type_money_numeric_or_nil')
errors.add :previous, :must_be_of_type_money_numeric_or_nil
end
end

View File

@@ -40,7 +40,6 @@ class TimeSeries::Value
def value_must_be_of_known_type
unless value.is_a?(Money) || value.is_a?(Numeric)
# i18n-tasks-use t('activemodel.errors.models.time_series/value.attributes.value.must_be_a_money_or_numeric')
errors.add :value, :must_be_a_money_or_numeric
end
end

View File

@@ -55,7 +55,6 @@ class User < ApplicationRecord
def can_deactivate
if admin? && family.users.count > 1
# i18n-tasks-use t('activerecord.errors.models.user.attributes.base.cannot_deactivate_admin_with_other_users')
errors.add(:base, :cannot_deactivate_admin_with_other_users)
end
end
@@ -84,7 +83,6 @@ class User < ApplicationRecord
def profile_image_size
if profile_image.attached? && profile_image.byte_size > 5.megabytes
# i18n-tasks-use t('activerecord.errors.models.user.attributes.profile_image.invalid_file_size')
errors.add(:profile_image, :invalid_file_size, max_megabytes: 5)
end
end

View File

@@ -21,7 +21,7 @@
</div>
<div data-trade-form-target="qtyInput">
<%= form.number_field :qty, label: t(".qty"), placeholder: "10", min: 0 %>
<%= form.number_field :qty, label: t(".qty"), placeholder: "10", min: 0.000000000000000001, step: "any" %>
</div>
<div data-trade-form-target="priceInput">

View File

@@ -30,7 +30,6 @@
<%= f.collection_select :from_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".from") }, required: true %>
<%= f.collection_select :to_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".to") }, required: true %>
<%= money_field f, :amount_money, label: t(".amount"), required: true %>
<%= f.hidden_field :currency, value: Current.family.currency %>
<%= f.date_field :date, value: transfer.date, label: t(".date"), required: true, max: Date.current %>
</section>

View File

@@ -10,7 +10,7 @@
<% end %>
<%= tag.div class: short ? "max-w-[250px]" : "max-w-[325px]" do %>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 <%= selectable ? "" : "pl-8" %>">
<%= circle_logo(transfer.from_name[0].upcase) %>
<%= tag.p transfer.name, class: "truncate text-gray-900" %>

View File

@@ -33,7 +33,7 @@
<div class="col-span-1 justify-self-end">
<%= contextual_menu do %>
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= contextual_menu_modal_action_item t(".edit_entry"), edit_account_entry_path(account, entry) %>
<%= contextual_menu_modal_action_item t(".edit_entry"), edit_account_entry_path(account, entry), turbo_frame: dom_id(entry) %>
<%= contextual_menu_destructive_item t(".delete_entry"),
account_entry_path(account, entry),

View File

@@ -4,7 +4,17 @@
<div class="w-8 h-8 flex items-center justify-center rounded-full text-xs font-medium <%= account.is_active ? "bg-blue-500/10 text-blue-500" : "bg-gray-500/10 text-gray-500" %>">
<%= account.name[0].upcase %>
</div>
<%= link_to account.name, account, class: [(account.is_active ? "text-gray-900" : "text-gray-400"), "text-sm font-medium hover:underline"], data: { turbo_frame: "_top" } %>
<div>
<%= link_to account.name, account, class: [(account.is_active ? "text-gray-900" : "text-gray-400"), "text-sm font-medium hover:underline"], data: { turbo_frame: "_top" } %>
<% if account.has_issues? %>
<div class="text-sm flex items-center gap-1 text-error">
<%= lucide_icon "alert-octagon", class: "shrink-0 w-4 h-4" %>
<%= tag.span t(".has_issues") %>
<%= link_to t(".troubleshoot"), issue_path(account.issues.first), class: "underline", data: { turbo_frame: :drawer } %>
</div>
<% end %>
</div>
<%= link_to edit_account_path(account), data: { turbo_frame: :modal }, class: "group-hover/account:flex hidden hover:opacity-80 items-center justify-center" do %>
<%= lucide_icon "pencil-line", class: "w-4 h-4 text-gray-500" %>

View File

@@ -1,6 +1,6 @@
<%# locals: (group:) -%>
<% type = Accountable.from_type(group.name) %>
<% if group %>
<% if group && group.children.any? %>
<details class="mb-1 text-sm group" data-controller="account-collapse" data-account-collapse-type-value="<%= type %>">
<summary class="flex gap-4 px-3 py-2 items-center w-full rounded-[10px] font-medium hover:bg-gray-100 cursor-pointer">
<%= lucide_icon("chevron-down", class: "hidden group-open:block text-gray-500 w-5 h-5") %>
@@ -8,8 +8,8 @@
<div class="text-left"><%= type.model_name.human %></div>
<div class="ml-auto flex flex-col items-end">
<p class="text-right"><%= format_money group.sum %></p>
<div class="flex items-center gap-1">
<%=
<div class="flex items-center gap-1">
<%=
tag.div(
id: "#{group.name}_sparkline",
class: "h-3 w-8 ml-auto",
@@ -21,10 +21,10 @@
"time-series-chart-use-tooltip-value": false
}
)
%>
<% styles = trend_styles(group.series.trend) %>
<span class="text-xs <%= styles[:text_class] %>"><%= sprintf("%+.2f", group.series.trend.percent) %>%</span>
</div>
%>
<% styles = trend_styles(group.series.trend) %>
<span class="text-xs <%= styles[:text_class] %>"><%= sprintf("%+.2f", group.series.trend.percent) %>%</span>
</div>
</div>
</summary>
<% group.children.sort_by(&:name).each do |account_value_node| %>
@@ -39,8 +39,9 @@
</div>
<div class="flex flex-col ml-auto font-medium text-right">
<p><%= format_money account.balance_money %></p>
<div class="flex items-center gap-1">
<%=
<% unless account_value_node.series.trend.direction.flat? %>
<div class="flex items-center gap-1">
<%=
tag.div(
id: dom_id(account, :list_sparkline),
class: "h-3 w-8 ml-auto",
@@ -52,10 +53,11 @@
"time-series-chart-use-tooltip-value": false
}
)
%>
<% styles = trend_styles(account_value_node.series.trend) %>
<span class="text-xs <%= styles[:text_class] %>"><%= sprintf("%+.2f", account_value_node.series.trend.percent) %>%</span>
</div>
%>
<% styles = trend_styles(account_value_node.series.trend) %>
<span class="text-xs <%= styles[:text_class] %>"><%= sprintf("%+.2f", account_value_node.series.trend.percent) %>%</span>
</div>
<% end %>
</div>
<% end %>
<% end %>

View File

@@ -13,11 +13,11 @@
<% end %>
<%= render "sync_all_button" %>
<%= link_to new_account_path, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<p class="text-sm font-medium"><%= t(".new") %></p>
<% end %>
<%= render "sync_all_button" %>
</div>
</header>

View File

@@ -1,40 +1,62 @@
<%# locals: (institution:) %>
<details open class="group bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<summary class="flex items-center gap-2 focus-visible:outline-none">
<%= lucide_icon "chevron-right", class: "group-open:transform group-open:rotate-90 text-gray-500 w-5" %>
<summary class="flex items-center justify-between gap-2 focus-visible:outline-none">
<div class="flex items-center gap-2">
<%= lucide_icon "chevron-right", class: "group-open:transform group-open:rotate-90 text-gray-500 w-5" %>
<div class="flex items-center justify-center h-8 w-8 bg-blue-600/10 rounded-full bg-black/5">
<% if institution_logo(institution) %>
<%= image_tag institution_logo(institution), class: "rounded-full h-full w-full" %>
<% else %>
<div class="flex items-center justify-center">
<%= tag.p institution.name.first.upcase, class: "text-blue-600 text-xs font-medium" %>
</div>
<% end %>
<div class="flex items-center justify-center h-8 w-8 bg-blue-600/10 rounded-full bg-black/5">
<% if institution_logo(institution) %>
<%= image_tag institution_logo(institution), class: "rounded-full h-full w-full" %>
<% else %>
<div class="flex items-center justify-center">
<%= tag.p institution.name.first.upcase, class: "text-blue-600 text-xs font-medium" %>
</div>
<% end %>
</div>
<div class="pl-1 text-sm">
<%= link_to institution.name, edit_institution_path(institution), data: { turbo_frame: :modal }, class: "font-medium text-gray-900 hover:underline" %>
<% if institution.has_issues? %>
<div class="flex items-center gap-1 text-error">
<%= lucide_icon "alert-octagon", class: "shrink-0 w-4 h-4" %>
<%= tag.span t(".has_issues") %>
</div>
<% elsif institution.syncing? %>
<div class="text-gray-500 flex items-center gap-1">
<%= lucide_icon "loader", class: "w-4 h-4 animate-pulse" %>
<%= tag.span t(".syncing") %>
</div>
<% else %>
<p class="text-gray-500"><%= institution.last_synced_at ? t(".status", last_synced_at: time_ago_in_words(institution.last_synced_at)) : t(".status_never") %></p>
<% end %>
</div>
</div>
<%= link_to institution.name, edit_institution_path(institution), data: { turbo_frame: :modal }, class: "text-sm font-medium text-gray-900 ml-1 mr-auto hover:underline" %>
<div class="flex items-center gap-2">
<%= button_to sync_institution_path(institution), method: :post, class: "text-gray-900 flex hover:text-gray-800 items-center text-sm font-medium hover:underline" do %>
<%= lucide_icon "refresh-cw", class: "w-4 h-4" %>
<% end %>
<%= contextual_menu do %>
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= link_to new_account_path(institution_id: institution.id),
<%= contextual_menu do %>
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= link_to new_account_path(institution_id: institution.id),
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg",
data: { turbo_frame: :modal } do %>
<%= lucide_icon "plus", class: "w-5 h-5 text-gray-500" %>
<%= lucide_icon "plus", class: "w-5 h-5 text-gray-500" %>
<span><%= t(".add_account_to_institution") %></span>
<% end %>
<span><%= t(".add_account_to_institution") %></span>
<% end %>
<%= link_to edit_institution_path(institution),
<%= link_to edit_institution_path(institution),
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg",
data: { turbo_frame: :modal } do %>
<%= lucide_icon "pencil-line", class: "w-5 h-5 text-gray-500" %>
<%= lucide_icon "pencil-line", class: "w-5 h-5 text-gray-500" %>
<span><%= t(".edit") %></span>
<% end %>
<span><%= t(".edit") %></span>
<% end %>
<%= button_to institution_path(institution),
<%= button_to institution_path(institution),
method: :delete,
class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg",
data: {
@@ -44,13 +66,13 @@
accept: t(".confirm_accept")
}
} do %>
<%= lucide_icon "trash-2", class: "w-5 h-5" %>
<%= lucide_icon "trash-2", class: "w-5 h-5" %>
<span><%= t(".delete") %></span>
<% end %>
</div>
<% end %>
<span><%= t(".delete") %></span>
<% end %>
</div>
<% end %>
</div>
</summary>
<div class="space-y-4 mt-4">

View File

@@ -1,3 +1,4 @@
<%= button_to sync_all_accounts_path, method: :post, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", title: "Sync All" do %>
<%= button_to sync_all_accounts_path, class: "btn btn--light flex items-center gap-2", title: "Sync All" do %>
<%= lucide_icon "refresh-cw", class: "w-5 h-5" %>
<span><%= t("accounts.sync_all.button_text") %></span>
<% end %>

View File

@@ -18,14 +18,14 @@
</div>
<% end %>
<%= render "sync_all_button" %>
<%= link_to new_account_path,
data: { turbo_frame: "modal" },
class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2" do %>
class: "btn btn--primary flex items-center gap-1" do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<p class="text-sm font-medium"><%= t(".new_account") %></p>
<% end %>
<%= render "sync_all_button" %>
</div>
</div>
</header>
@@ -38,16 +38,11 @@
<%= render "institution_accounts", institution: %>
<% end %>
<%= render "institutionless_accounts", accounts: @accounts %>
<% if @accounts.any? %>
<%= render "institutionless_accounts", accounts: @accounts %>
<% end %>
</div>
<% end %>
<div class="flex justify-between gap-4">
<% if self_hosted? %>
<%= previous_setting("Self-Hosting", settings_hosting_path) %>
<% else %>
<%= previous_setting("Billing", settings_billing_path) %>
<% end %>
<%= next_setting("Tags", tags_path) %>
</div>
<%= settings_nav_footer %>
</div>

View File

@@ -1,6 +1,6 @@
<h1 class="text-3xl font-semibold font-display"><%= t(".title") %></h1>
<%= modal do %>
<div class="flex flex-col min-h-[530px] w-screen max-w-xl" data-controller="list-keyboard-navigation">
<div class="flex flex-col w-screen max-w-xl" data-controller="list-keyboard-navigation">
<% if @account.accountable.blank? %>
<div class="border-b border-alpha-black-25 p-4 text-gray-400">
<%= t ".select_accountable_type" %>

View File

@@ -1,10 +1,11 @@
<%# locals: (category:) %>
<% category ||= null_category %>
<span class="border text-sm font-medium px-2.5 py-1 rounded-full content-center"
style="
background-color: color-mix(in srgb, <%= category.color %> 5%, white);
border-color: color-mix(in srgb, <%= category.color %> 10%, white);
color: <%= category.color %>;">
<%= category.name %>
</span>
<div>
<span class="flex items-center gap-1 text-sm font-medium rounded-full px-1.5 py-1 border border-alpha-black-25"
style="
background-color: color-mix(in srgb, <%= category.color %> 5%, white);
color: <%= category.color %>;">
<%= category.name %>
</span>
</div>

View File

@@ -0,0 +1,21 @@
<%# locals: (category:) %>
<div id="<%= dom_id(category) %>" class="flex justify-between items-center p-4 bg-white">
<div class="flex w-full items-center gap-2.5">
<%= render partial: "categories/badge", locals: { category: category } %>
</div>
<div class="justify-self-end">
<%= contextual_menu do %>
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= contextual_menu_modal_action_item t(".edit"), edit_category_path(category) %>
<%= link_to new_category_deletion_path(category),
class: "flex items-center w-full rounded-lg text-red-600 hover:bg-red-50 py-2 px-3 gap-2",
data: { turbo_frame: :modal } do %>
<%= lucide_icon "trash-2", class: "shrink-0 w-5 h-5" %>
<span class="text-sm"><%= t(".delete") %></span>
<% end %>
</div>
<% end %>
</div>
</div>

View File

@@ -1,38 +1,25 @@
<%= styled_form_with model: category, data: { turbo: false } do |form| %>
<div class="flex flex-col space-y-4 w-96" data-controller="color-select" data-color-select-selection-value="<%= category.color %>">
<fieldset class="relative">
<span data-color-select-target="decoration" class="pointer-events-none absolute inset-y-3.5 left-3 flex items-center pl-1 block w-1 rounded-lg"></span>
<%= form.text_field :name,
value: category.name,
autofocus: "",
required: true,
placeholder: "Enter Category name",
class: "rounded-lg w-full focus:ring-black focus:border-transparent placeholder:text-gray-500 pl-6" %>
</fieldset>
<fieldset>
<%= form.hidden_field :color, data: { color_select_target: "input" } %>
<ul role="radiogroup" class="flex justify-between items-center py-2">
<div data-controller="color-avatar">
<%= styled_form_with model: category, class: "space-y-4", data: { turbo: false } do |f| %>
<section class="space-y-4">
<div class="w-fit m-auto">
<%= render partial: "shared/color_avatar", locals: { name: category.name, color: category.color } %>
</div>
<div class="flex gap-2 items-center justify-center">
<% Category::COLORS.each do |color| %>
<li tabindex="0"
role="radio"
data-action="click->color-select#select keydown.enter->color-select#select keydown.space->color-select#select"
data-value="<%= color %>"
class="flex shrink-0 justify-center items-center w-5 h-5 cursor-pointer hover:bg-gray-200 rounded-full">
</li>
<label class="relative">
<%= f.radio_button :color, color, class: "sr-only peer", data: { action: "change->color-avatar#handleColorChange" } %>
<div class="w-6 h-6 rounded-full cursor-pointer peer-checked:ring-2 peer-checked:ring-offset-2 peer-checked:ring-blue-500" style="background-color: <%= color %>"></div>
</label>
<% end %>
</ul>
</fieldset>
</div>
<div class="relative flex items-center border border-gray-200 rounded-lg">
<%= f.text_field :name, placeholder: t(".placeholder"), class: "text-sm font-normal placeholder:text-gray-500 h-10 relative pl-3 w-full border-none rounded-lg", required: true, data: { color_avatar_target: "name" } %>
</div>
</section>
<section>
<%= hidden_field_tag :transaction_id, params[:transaction_id] %>
<% if category.persisted? %>
<%= form.submit t(".update") %>
<% else %>
<%= form.submit t(".create") %>
<% end %>
<%= f.submit %>
</section>
</div>
<% end %>
<% end %>
</div>

View File

@@ -1,23 +0,0 @@
<div class="flex justify-between mx-4 py-5 border-b last:border-b-0 border-alpha-black-50">
<%= render partial: "categories/badge", locals: { category: row } %>
<%= contextual_menu do %>
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= link_to edit_category_path(row),
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg",
data: { turbo_frame: :modal } do %>
<%= lucide_icon "pencil-line", class: "w-5 h-5 text-gray-500" %>
<span><%= t(".edit") %></span>
<% end %>
<%= link_to new_category_deletion_path(row),
class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg",
data: { turbo_frame: :modal } do %>
<%= lucide_icon "trash-2", class: "w-5 h-5" %>
<span><%= t(".delete") %></span>
<% end %>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,3 @@
<div class="bg-white">
<div class="h-px bg-alpha-black-50 ml-4 mr-6"></div>
</div>

View File

@@ -1,28 +1,44 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<section class="space-y-4">
<header class="flex items-center justify-between">
<h1 class="text-gray-900 text-xl font-medium"><%= t(".categories") %></h1>
<%= link_to new_category_path, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %>
<%= link_to new_category_path, class: "btn btn--primary flex items-center gap-1 justify-center", data: { turbo_frame: :modal } do %>
<%= lucide_icon "plus", class: "w-5 h-5" %>
<p><%= t(".new") %></p>
<% end %>
</header>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="rounded-xl bg-gray-25 p-1">
<h2 class="uppercase px-4 py-2 text-gray-500 text-xs"><%= t(".categories") %> · <%= @categories.size %></h2>
<% if @categories.any? %>
<div class="rounded-xl bg-gray-25 space-y-1 p-1">
<div class="flex items-center gap-1.5 px-4 py-2 text-xs font-medium text-gray-500 uppercase">
<p><%= t(".categories") %></p>
<span class="text-gray-400">&middot;</span>
<p><%= @categories.count %></p>
</div>
<div class="border border-alpha-gray-100 rounded-lg bg-white shadow-xs">
<%= render collection: @categories, partial: "categories/row" %>
<div class="border border-alpha-black-25 rounded-md bg-white shadow-xs">
<div class="overflow-hidden rounded-md">
<%= render partial: @categories, spacer_template: "categories/ruler" %>
</div>
</div>
</div>
</div>
<% else %>
<div class="flex justify-center items-center py-20">
<div class="text-center flex flex-col items-center max-w-[300px]">
<p class="text-gray-900 mb-1 font-medium text-sm"><%= t(".empty") %></p>
<%= link_to new_category_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new") %></span>
<% end %>
</div>
</div>
<% end %>
</div>
<footer class="flex justify-between gap-4">
<%= previous_setting("Tags", tags_path) %>
<%= next_setting("Merchants", merchants_path) %>
</footer>
<%= settings_nav_footer %>
</section>

View File

@@ -24,8 +24,6 @@
</div>
<% end %>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Rules", rules_transactions_path) %>
<%= next_setting("What's new", changelog_path) %>
</div>
<%= settings_nav_footer %>
</div>

View File

@@ -0,0 +1,16 @@
<%# app/views/invite_codes/_invite_code.html.erb %>
<div class="invite_code pt-2">
<div class="flex items-center justify-between p-2 w-1/2 bg-gray-25 rounded-md" data-controller="clipboard">
<div>
<span data-clipboard-target="source" class="text-sm font-medium"><%= invite_code.token %></span>
</div>
<button data-action="clipboard#copy" class="flex-shrink-0 z-10 inline-flex items-center px-1 text-sm text-gray-500 font-sm text-center" type="button">
<span data-clipboard-target="iconDefault">
<%= lucide_icon "copy", class: "w-5 h-5" %>
</span>
<span class="hidden inline-flex items-center" data-clipboard-target="iconSuccess">
<%= lucide_icon "check", class: "w-5 h-4" %>
</span>
</button>
</div>
</div>

View File

@@ -0,0 +1,12 @@
<%# app/views/invite_codes/index.html.erb %>
<%= turbo_frame_tag "invite_codes" do %>
<% if @invite_codes.present? %>
<%= render @invite_codes %>
<% else %>
<div class="flex flex-col items-center w-full h-64 bg-white text-center justify-center">
<%= lucide_icon "binary", class: "w-6 h-6 text-sm text-gray-500" %>
<p class="text-base pt-4"><%= t(".no_invite_codes") %></p>
<p class="text-sm text-gray-500 pt-2 w-2/3"><%= t(".invite_code_description") %></p>
</div>
<% end %>
<% end %>

View File

@@ -91,8 +91,9 @@
<%= t(".portfolio") %>
<% end %>
<span class="font-bold tracking-wide">&bull;</span>
<%= form_with url: list_accounts_path, method: :get, data: { controller: "auto-submit-form", turbo_frame: "account-list" } do |form| %>
<%= period_select form: form, selected: "last_7_days", classes: "w-full border-none pl-2 pr-7 text-xs bg-transparent gap-1 cursor-pointer font-semibold tracking-wide focus:outline-none focus:ring-0" %>
<%= form_with url: list_accounts_path, method: :get, data: { controller: Current.family.accounts.any? ? "auto-submit-form" : nil, turbo_frame: "account-list" } do |form| %>
<%= period_select form: form, selected: "last_30_days", classes: "w-full border-none pl-2 pr-7 text-xs bg-transparent gap-1 cursor-pointer font-semibold tracking-wide focus:outline-none focus:ring-0" %>
<% end %>
</div>
<%= link_to new_account_path, id: "sidebar-new-account", class: "block hover:bg-gray-100 font-semibold text-gray-900 flex items-center rounded", title: t(".new_account"), data: { turbo_frame: "modal" } do %>
@@ -101,8 +102,15 @@
</div>
<%= turbo_frame_tag "account-list", target: "_top" do %>
<% account_groups.each do |group| %>
<%= render "accounts/account_list", group: group %>
<% if Current.family.accounts.any? %>
<% account_groups.each do |group| %>
<%= render "accounts/account_list", group: group %>
<% end %>
<% else %>
<%= link_to new_account_path, class: "flex items-center min-h-10 gap-4 px-3 py-2 mb-1 text-gray-500 text-sm font-medium rounded-[10px] hover:bg-gray-100", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<%= tag.p t(".new_account") %>
<% end %>
<% end %>
<% end %>
</div>

View File

@@ -1,7 +0,0 @@
<%# locals: (merchant:) %>
<% name = merchant.name || "?" %>
<% background_color = "color-mix(in srgb, #{merchant.color} 5%, white)" %>
<% border_color = "color-mix(in srgb, #{merchant.color} 10%, white)" %>
<span data-merchant-avatar-target="avatar" class="w-8 h-8 flex items-center justify-center rounded-full" style="background-color: <%= background_color %>; border-color: <%= border_color %>; color: <%= merchant.color %>">
<%= name[0].upcase %>
</span>

View File

@@ -1,27 +1,24 @@
<% is_editing = @merchant.id.present? %>
<div data-controller="merchant-avatar">
<%= styled_form_with model: @merchant, url: is_editing ? merchant_path(@merchant) : merchants_path, method: is_editing ? :patch : :post, scope: :merchant, class: "space-y-4", data: { turbo: false } do |f| %>
<div data-controller="color-avatar">
<%= styled_form_with model: @merchant, class: "space-y-4", data: { turbo: false } do |f| %>
<section class="space-y-4">
<div class="w-fit m-auto">
<%= render partial: "merchants/avatar", locals: { merchant: } %>
<%= render partial: "shared/color_avatar", locals: { name: @merchant.name, color: @merchant.color } %>
</div>
<div data-controller="select" data-select-active-class="bg-gray-200" data-select-selected-value="<%= @merchant&.color || Merchant::COLORS[0] %>">
<%= f.hidden_field :color, data: { select_target: "input", merchant_avatar_target: "color" } %>
<ul data-select-target="list" class="flex gap-2 items-center">
<% Merchant::COLORS.each do |color| %>
<li tabindex="0" data-select-target="option" data-action="click->select#selectOption" data-value="<%= color %>" class="flex shrink-0 justify-center items-center w-6 h-6 cursor-pointer hover:bg-gray-200 rounded-full">
<div style="background-color: <%= color %>" class="shrink-0 w-4 h-4 rounded-full"></div>
</li>
<% end %>
</ul>
<div class="flex gap-2 items-center justify-center">
<% Merchant::COLORS.each do |color| %>
<label class="relative">
<%= f.radio_button :color, color, class: "sr-only peer", data: { action: "change->color-avatar#handleColorChange" } %>
<div class="w-6 h-6 rounded-full cursor-pointer peer-checked:ring-2 peer-checked:ring-offset-2 peer-checked:ring-blue-500" style="background-color: <%= color %>"></div>
</label>
<% end %>
</div>
<div class="relative flex items-center border border-gray-200 rounded-lg">
<%= f.text_field :name, placeholder: t(".name_placeholder"), class: "text-sm font-normal placeholder:text-gray-500 h-10 relative pl-3 w-full border-none rounded-lg", required: true, data: { merchant_avatar_target: "name" } %>
<%= f.text_field :name, placeholder: t(".name_placeholder"), class: "text-sm font-normal placeholder:text-gray-500 h-10 relative pl-3 w-full border-none rounded-lg", required: true, data: { color_avatar_target: "name" } %>
</div>
</section>
<section>
<%= f.submit(is_editing ? t(".submit_edit") : t(".submit_create")) %>
<%= f.submit %>
</section>
<% end %>
</div>

View File

@@ -1,41 +0,0 @@
<%# locals: (merchants:) %>
<% merchants.each.with_index do |merchant, index| %>
<div class="flex justify-between items-center p-4 bg-white">
<div class="flex w-full items-center gap-2.5">
<%= render partial: "merchants/avatar", locals: { merchant: } %>
<p class="text-gray-900 text-sm truncate">
<%= merchant.name %>
</p>
</div>
<div class="relative cursor-pointer" data-controller="menu">
<button data-menu-target="button" class="flex hover:bg-gray-100 p-2 rounded">
<%= lucide_icon("more-horizontal", class: "w-5 h-5 text-gray-500") %>
</button>
<div data-menu-target="content" class="absolute z-10 top-10 right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs w-48 hidden">
<div class="border-t border-b border-alpha-black-100 p-1">
<%= button_to edit_merchant_path(merchant),
method: :get,
class: "flex w-full gap-1 items-center text-sm hover:bg-gray-50 rounded-lg px-3 py-2",
data: { turbo_frame: "modal" } do %>
<%= lucide_icon("pencil-line", class: "w-5 h-5 mr-2") %> <%= t(".edit") %>
<% end %>
<%= button_to merchant_path(merchant),
method: :delete,
class: "flex w-full gap-1 items-center text-sm text-red-600 hover:text-red-800 hover:bg-gray-50 rounded-lg px-3 py-2",
data: {
turbo_confirm: {
title: t(".confirm_title"),
body: t(".confirm_body"),
accept: t(".confirm_accept")
}
} do %>
<%= lucide_icon("trash-2", class: "w-5 h-5 mr-1") %> <%= t(".delete") %>
<% end %>
</div>
</div>
</div>
</div>
<% unless index == merchants.size - 1 %>
<div class="h-px bg-alpha-black-50 ml-14 mr-6"></div>
<% end %>
<% end %>

View File

@@ -0,0 +1,26 @@
<%# locals: (merchant:) %>
<div class="flex justify-between items-center p-4 bg-white">
<div class="flex w-full items-center gap-2.5">
<%= render partial: "shared/color_avatar", locals: { name: merchant.name, color: merchant.color } %>
<p class="text-gray-900 text-sm truncate">
<%= merchant.name %>
</p>
</div>
<div class="justify-self-end">
<%= contextual_menu do %>
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= contextual_menu_modal_action_item t(".edit"), edit_merchant_path(merchant) %>
<%= contextual_menu_destructive_item t(".delete"),
merchant_path(merchant),
turbo_frame: "_top",
turbo_confirm: {
title: t(".confirm_title"),
body: t(".confirm_body"),
accept: t(".confirm_accept")
} %>
</div>
<% end %>
</div>
</div>

View File

@@ -0,0 +1,3 @@
<div class="bg-white">
<div class="h-px bg-alpha-black-50 ml-14 mr-6"></div>
</div>

View File

@@ -2,39 +2,43 @@
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<div class="flex items-center justify-between">
<section class="space-y-4">
<header class="flex items-center justify-between">
<h1 class="text-gray-900 text-xl font-medium"><%= t(".title") %></h1>
<%= link_to new_merchant_path, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new_short") %></span>
<%= link_to new_merchant_path, class: "btn btn--primary flex items-center gap-1 justify-center", data: { turbo_frame: :modal } do %>
<%= lucide_icon "plus", class: "w-5 h-5" %>
<p><%= t(".new") %></p>
<% end %>
</div>
</header>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<% if @merchants.empty? %>
<% if @merchants.any? %>
<div class="rounded-xl bg-gray-25 space-y-1 p-1">
<div class="flex items-center gap-1.5 px-4 py-2 text-xs font-medium text-gray-500 uppercase">
<p><%= t(".title") %></p>
<span class="text-gray-400">&middot;</span>
<p><%= @merchants.count %></p>
</div>
<div class="border border-alpha-black-25 rounded-md bg-white shadow-xs">
<div class="overflow-hidden rounded-md">
<%= render partial: @merchants, spacer_template: "merchants/ruler" %>
</div>
</div>
</div>
<% else %>
<div class="flex justify-center items-center py-20">
<div class="text-center flex flex-col items-center max-w-[300px]">
<p class="text-gray-900 mb-1 font-medium text-sm"><%= t(".empty") %></p>
<%= link_to new_merchant_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new_long") %></span>
<span><%= t(".new") %></span>
<% end %>
</div>
</div>
<% else %>
<div class="bg-gray-25 p-1 rounded-xl">
<div class="flex items-center px-4 py-2 text-xs font-medium text-gray-500">
<p><%= t(".title") %></p>
<span class="text-gray-400 mx-2">&middot;</span>
<p><%= @merchants.count %></p>
</div>
<%= render partial: "merchants/list", locals: { merchants: @merchants } %>
</div>
<% end %>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Categories", categories_path) %>
<%= next_setting("Rules", rules_transactions_path) %>
</div>
<%= settings_nav_footer %>
</div>

View File

@@ -1,31 +1,30 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<div class="space-y-4 flex flex-col h-full">
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".title") %></h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<% @releases_notes.each do |release_notes| %>
<div class="flex justify-between gap-4 mb-12 last:mb-0">
<div class="w-1/3">
<div class="px-3 flex items-center gap-3">
<div class="text-white shrink-0 w-9 h-9">
<%= image_tag release_notes[:avatar], class: "rounded-full w-full h-full object-cover" %>
</div>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4 flex-grow overflow-y-auto">
<div class="flex justify-between gap-4 mb-12 last:mb-0">
<div class="w-1/3">
<div class="px-3 flex items-center gap-3">
<div class="text-white shrink-0 w-9 h-9">
<%= image_tag @release_notes[:avatar], class: "rounded-full w-full h-full object-cover" %>
</div>
<div>
<div class="text-gray-900 font-medium text-sm"><%= release_notes[:name] %></div>
<div class="text-gray-500 text-sm"><%= release_notes[:published_at].strftime("%B %d, %Y") %></div>
<div class="text-gray-900 font-medium text-sm"><%= @release_notes[:name] %></div>
<div class="text-gray-500 text-sm"><%= @release_notes[:published_at].strftime("%B %d, %Y") %></div>
</div>
</div>
</div>
<div class="w-2/3 text-gray-500 text-sm prose prose--github-release-notes">
<h2 class="mb-5 text-xl text-gray-900"><%= release_notes[:name] %></h2>
<%= release_notes[:body].html_safe %>
</div>
</div>
<% end %>
<div class="w-2/3 text-gray-500 text-sm prose prose--github-release-notes">
<h2 class="mb-5 text-xl text-gray-900"><%= @release_notes[:name] %></h2>
<%= @release_notes[:body].html_safe %>
</div>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Imports", imports_path) %>
<%= next_setting("Feedback", feedback_path) %>
<div class="mt-auto">
<%= settings_nav_footer %>
</div>
</div>

View File

@@ -1,189 +1,177 @@
<div class="space-y-4">
<header class="flex items-center justify-between">
<div>
<h1 class="sr-only">Dashboard</h1>
<h1 class="sr-only"><%= t(".title") %></h1>
<p class="text-xl font-medium text-gray-900 mb-1"><%= t(".greeting", name: Current.user.first_name ) %></p>
<% unless @accounts.blank? %>
<p class="text-gray-500 text-sm"><%= t(".subtitle") %></p>
<% end %>
</div>
<%= link_to new_account_path, class: "flex text-white text-sm font-medium items-center gap-1 bg-gray-900 hover:bg-gray-700 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %>
<%= link_to new_account_path, class: "flex items-center gap-1 btn btn--primary", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new") %></span>
<% end %>
</header>
<% if @accounts.empty? %>
<%= render "shared/no_account_empty_state" %>
<% else %>
<section class="flex gap-4">
<div class="bg-white border border-alpha-black-25 shadow-xs rounded-xl w-3/4 min-h-48 flex flex-col">
<div class="flex justify-between p-4">
<div>
<%= render partial: "shared/value_heading", locals: {
<section class="flex gap-4">
<div class="bg-white border border-alpha-black-25 shadow-xs rounded-xl w-3/4 min-h-48 flex flex-col">
<div class="flex justify-between p-4">
<div>
<%= render partial: "shared/value_heading", locals: {
label: t(".net_worth"),
period: @period,
value: Current.family.net_worth,
trend: @net_worth_series.trend
} %>
</div>
<%= form_with url: root_path, method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do |form| %>
<%= period_select form: form, selected: @period.name %>
<% end %>
</div>
<%= form_with url: root_path, method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do |form| %>
<%= period_select form: form, selected: @period.name %>
<% end %>
<%= render partial: "pages/dashboard/net_worth_chart", locals: { series: @net_worth_series } %>
</div>
<%= render partial: "pages/dashboard/net_worth_chart", locals: { series: @net_worth_series } %>
</div>
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl w-1/4">
<%= render partial: "pages/dashboard/allocation_chart", locals: { account_groups: @account_groups } %>
</div>
</section>
<section class="grid grid-cols-2 gap-4">
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<div class="flex flex-col gap-4 h-full">
<div class="flex gap-4">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl w-1/4">
<%= render partial: "pages/dashboard/allocation_chart", locals: { account_groups: @account_groups } %>
</div>
</section>
<section class="grid grid-cols-2 gap-4">
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<div class="flex flex-col gap-4 h-full">
<div class="flex gap-4">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
label: t(".income"),
period: Period.last_30_days,
value: @income_series.last&.value,
trend: @income_series.trend
} %>
</div>
<div
</div>
<div
id="incomeChart"
class="h-full w-2/5"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= @income_series.to_json %>"
data-time-series-chart-use-labels-value="false"
data-time-series-chart-use-tooltip-value="false"></div>
</div>
<div class="flex gap-1.5">
<% @top_earners.first(3).each do |account| %>
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-gray-900 font-medium hover:bg-gray-25" do %>
<%= image_tag account_logo_url(account), class: "w-5 h-5" %>
<span>+<%= Money.new(account.income, account.currency) %></span>
</div>
<div class="flex gap-1.5">
<% @top_earners.first(3).each do |account| %>
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-gray-900 font-medium hover:bg-gray-25" do %>
<%= image_tag account_logo_url(account), class: "w-5 h-5" %>
<span>+<%= Money.new(account.income, account.currency) %></span>
<% end %>
<% end %>
<% end %>
<% if @top_earners.count > 3 %>
<div class="bg-gray-25 rounded-full flex h-full aspect-1 items-center justify-center text-xs font-medium text-gray-500">+<%= @top_earners.count - 3 %></div>
<% end %>
<% if @top_earners.count > 3 %>
<div class="bg-gray-25 rounded-full flex h-full aspect-1 items-center justify-center text-xs font-medium text-gray-500">+<%= @top_earners.count - 3 %></div>
<% end %>
</div>
</div>
</div>
</div>
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<div class="flex flex-col gap-4 h-full">
<div class="flex gap-4">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<div class="flex flex-col gap-4 h-full">
<div class="flex gap-4">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
label: t(".spending"),
period: Period.last_30_days,
value: @spending_series.last&.value,
trend: @spending_series.trend
} %>
</div>
<div
</div>
<div
id="spendingChart"
class="h-full w-2/5"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= @spending_series.to_json %>"
data-time-series-chart-use-labels-value="false"
data-time-series-chart-use-tooltip-value="false"></div>
</div>
<div class="flex gap-1.5">
<% @top_spenders.first(3).each do |account| %>
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-gray-900 font-medium hover:bg-gray-25" do %>
<%= image_tag account_logo_url(account), class: "w-5 h-5" %>
-<%= Money.new(account.spending, account.currency) %>
</div>
<div class="flex gap-1.5">
<% @top_spenders.first(3).each do |account| %>
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-gray-900 font-medium hover:bg-gray-25" do %>
<%= image_tag account_logo_url(account), class: "w-5 h-5" %>
-<%= Money.new(account.spending, account.currency) %>
<% end %>
<% end %>
<% end %>
<% if @top_spenders.count > 3 %>
<div class="bg-gray-25 rounded-full flex h-full aspect-1 items-center justify-center text-xs font-medium text-gray-500">+<%= @top_spenders.count - 3 %></div>
<% end %>
<% if @top_spenders.count > 3 %>
<div class="bg-gray-25 rounded-full flex h-full aspect-1 items-center justify-center text-xs font-medium text-gray-500">+<%= @top_spenders.count - 3 %></div>
<% end %>
</div>
</div>
</div>
</div>
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<div class="flex flex-col gap-4 h-full">
<div class="flex gap-4">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<div class="flex flex-col gap-4 h-full">
<div class="flex gap-4">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
label: t(".savings_rate"),
period: Period.last_30_days,
value: @savings_rate_series.last&.value,
trend: @savings_rate_series.trend,
is_percentage: true
} %>
</div>
<div
</div>
<div
id="savingsRateChart"
class="h-full w-2/5"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= @savings_rate_series.to_json %>"
data-time-series-chart-use-labels-value="false"
data-time-series-chart-use-tooltip-value="false"></div>
</div>
<div class="flex gap-1.5">
<% @top_savers.first(3).each do |account| %>
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-gray-900 font-medium hover:bg-gray-25" do %>
<%= image_tag account_logo_url(account), class: "w-5 h-5" %>
<span><%= account.savings_rate > 0 ? "+" : "-" %><%= number_to_percentage(account.savings_rate.abs * 100, precision: 2) %></span>
</div>
<div class="flex gap-1.5">
<% @top_savers.first(3).each do |account| %>
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-gray-900 font-medium hover:bg-gray-25" do %>
<%= image_tag account_logo_url(account), class: "w-5 h-5" %>
<span><%= account.savings_rate > 0 ? "+" : "-" %><%= number_to_percentage(account.savings_rate.abs * 100, precision: 2) %></span>
<% end %>
<% end %>
<% end %>
<% if @top_savers.count > 3 %>
<div class="bg-gray-25 rounded-full flex h-full aspect-1 items-center justify-center text-xs font-medium text-gray-500">+<%= @top_savers.count - 3 %></div>
<% end %>
<% if @top_savers.count > 3 %>
<div class="bg-gray-25 rounded-full flex h-full aspect-1 items-center justify-center text-xs font-medium text-gray-500">+<%= @top_savers.count - 3 %></div>
<% end %>
</div>
</div>
</div>
</div>
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<div class="flex gap-4 h-full">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<div class="flex gap-4 h-full">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
label: t(".investing"),
period: @period,
value: @investing_series.last.value,
trend: @investing_series.trend
} %>
</div>
<div
</div>
<div
id="investingChart"
class="h-full w-2/5"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= @investing_series.to_json %>"
data-time-series-chart-use-labels-value="false"></div>
</div>
</div>
</section>
<section class="grid grid-cols-2 gap-4 items-baseline">
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl space-y-4">
<h2 class="text-lg font-medium text-gray-900"><%= t(".transactions") %></h2>
<% if @transaction_entries.empty? %>
<div class="text-gray-500 flex items-center justify-center py-12">
<p><%= t(".no_transactions") %></p>
</div>
<% else %>
<div class="text-gray-500 p-1 space-y-1 bg-gray-25 rounded-xl">
<%= entries_by_date(@transaction_entries, selectable: false) do |entries| %>
<%= render entries, selectable: false, editable: false, short: true %>
<% end %>
</div>
</section>
<section class="w-full">
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl space-y-4">
<h2 class="text-lg font-medium text-gray-900"><%= t(".transactions") %></h2>
<% if @transaction_entries.empty? %>
<div class="text-gray-500 flex items-center justify-center py-12">
<p><%= t(".no_transactions") %></p>
</div>
<% else %>
<div class="text-gray-500 p-1 space-y-1 bg-gray-25 rounded-xl">
<%= entries_by_date(@transaction_entries, selectable: false) do |entries| %>
<%= render entries, selectable: false, editable: false %>
<% end %>
<p class="py-2 text-sm text-center"><%= link_to t(".view_all"), transactions_path %></p>
</div>
<% end %>
</div>
<div class="space-y-4">
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl space-y-4">
<h2 class="text-lg font-medium text-gray-900"><%= t(".recurring") %></h2>
<div class="text-gray-500 flex items-center justify-center py-12">
<p>Coming soon...</p>
</div>
<p class="py-2 text-sm text-center"><%= link_to t(".view_all"), transactions_path %></p>
</div>
<% end %>
</div>
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl space-y-4">
<h2 class="text-lg font-medium text-gray-900"><%= t(".categories") %></h2>
<div class="text-gray-500 flex items-center justify-center py-12">
<p>Coming soon...</p>
</div>
</div>
</div>
</section>
</section>
<% end %>
</div>

View File

@@ -1,15 +1,29 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">Feedback</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="flex justify-center items-center py-20">
<p class="text-gray-500">Feedback coming soon...</p>
<h2 class="text-lg font-medium text-gray-900 mb-1">Leave feedback</h2>
<p class="text-sm text-gray-500 mb-4">Let us know if you have any specific feedback. Feel free to include links to videos or screenshots.</p>
<div class="flex gap-2">
<%= link_to "https://github.com/maybe-finance/maybe/discussions/categories/feature-requests", target: "_blank", rel: "noopener noreferrer", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50" do %>
<%= image_tag "github-icon.png", class: "w-8 h-8 mb-2" %>
<span class="text-sm font-medium text-gray-900">Write a feature request</span>
<% end %>
<%= link_to "https://github.com/maybe-finance/maybe/issues/new?assignees=&labels=bug&template=bug_report.md&title=", target: "_blank", rel: "noopener noreferrer", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50" do %>
<%= image_tag "github-icon.png", class: "w-8 h-8 mb-2" %>
<span class="text-sm font-medium text-gray-900">File a bug report</span>
<% end %>
<%= link_to "https://link.maybe.co/discord", target: "_blank", rel: "noopener noreferrer", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50" do %>
<%= image_tag "discord-icon.png", class: "w-8 h-8 mb-2" %>
<span class="text-sm font-medium text-gray-900">Discuss Maybe with others</span>
<% end %>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("What's New", changelog_path) %>
<%= next_setting("Invite friends", invites_path) %>
</div>
<%= settings_nav_footer %>
</div>

View File

@@ -1,14 +0,0 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">Invite friends</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="flex justify-center items-center py-20">
<p class="text-gray-500">Invite friends coming soon...</p>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Feedback", feedback_path) %>
</div>
</div>

View File

@@ -1,6 +1,14 @@
<%
header_title t(".title")
%>
<% if self_hosted_first_login? %>
<div class="fixed inset-0 w-full h-fit bg-gray-25 p-5 border-b border-alpha-black-200 flex flex-col gap-3 items-center text-center mb-12">
<h2 class="font-bold text-xl"><%= t(".welcome_title") %></h2>
<p class="text-gray-500 text-sm"><%= t(".welcome_body") %></p>
</div>
<% end %>
<%= styled_form_with model: @user, url: registration_path, class: "space-y-4" do |form| %>
<%= auth_messages form %>
<%= form.email_field :email, autofocus: false, autocomplete: "email", required: "required", placeholder: "you@example.com", label: true %>

View File

@@ -20,15 +20,6 @@
<li>
<%= sidebar_link_to t(".preferences_label"), settings_preferences_path, icon: "bolt" %>
</li>
<li>
<%= sidebar_link_to t(".notifications_label"), settings_notifications_path, icon: "bell-dot" %>
</li>
<li>
<%= sidebar_link_to t(".security_label"), settings_security_path, icon: "shield-check" %>
</li>
<li>
<%= sidebar_link_to t(".billing_label"), settings_billing_path, icon: "circle-dollar-sign" %>
</li>
<% if self_hosted? %>
<li>
<%= sidebar_link_to t(".self_hosting_label"), settings_hosting_path, icon: "database" %>
@@ -50,14 +41,11 @@
<%= sidebar_link_to t(".tags_label"), tags_path, icon: "tags" %>
</li>
<li>
<%= sidebar_link_to t(".categories_label"), categories_path, icon: "tags" %>
<%= sidebar_link_to t(".categories_label"), categories_path, icon: "shapes" %>
</li>
<li>
<%= sidebar_link_to t(".merchants_label"), merchants_path, icon: "store" %>
</li>
<li>
<%= sidebar_link_to t(".rules_label"), rules_transactions_path, icon: "list-checks" %>
</li>
<li>
<%= sidebar_link_to t(".imports_label"), imports_path, icon: "download" %>
</li>
@@ -72,7 +60,6 @@
<li>
<%= sidebar_link_to t(".whats_new_label"), changelog_path, icon: "box" %>
<%= sidebar_link_to t(".feedback_label"), feedback_path, icon: "megaphone" %>
<%= sidebar_link_to t(".invite_label"), invites_path, icon: "gift" %>
</li>
</ul>
</section>

View File

@@ -1,19 +0,0 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">Billing</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="flex justify-center items-center py-20">
<p class="text-gray-500">Billing settings coming soon...</p>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Security", settings_security_path) %>
<% if self_hosted? %>
<%= next_setting("Self-Hosting", settings_hosting_path) %>
<% else %>
<%= next_setting("Accounts", accounts_path) %>
<% end %>
</div>
</div>

View File

@@ -1,46 +1,46 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
<%= settings_section title: t(".general_settings_title") do %>
<%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, local: true, class: "space-y-6", data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %>
<% if ENV["HOSTING_PLATFORM"] == "render" %>
<div>
<h2 class="font-medium mb-1"><%= t(".upgrades.title") %></h2>
<p class="text-gray-500 text-sm mb-4"><%= t(".upgrades.description") %></p>
<div class="space-y-4">
<div class="flex items-center gap-4">
<%= form.radio_button :upgrades_mode, "manual", checked: Setting.upgrades_mode == "manual", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
<%= form.label :upgrades_mode_manual, t(".upgrades.manual.title"), class: "text-gray-900 text-sm" do %>
<span class="font-medium"><%= t(".upgrades.manual.title") %></span>
<br>
<span class="text-gray-500">
<div class="space-y-4 pb-32">
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
<% if ENV["HOSTING_PLATFORM"] == "render" %>
<%= settings_section title: t(".general_settings_title") do %>
<%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %>
<h2 class="font-medium mb-1"><%= t(".upgrades.title") %></h2>
<p class="text-gray-500 text-sm mb-4"><%= t(".upgrades.description") %></p>
<div class="space-y-4">
<div class="flex items-center gap-4">
<%= form.radio_button :upgrades_mode, "manual", checked: Setting.upgrades_mode == "manual", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
<%= form.label :upgrades_mode_manual, t(".upgrades.manual.title"), class: "text-gray-900 text-sm" do %>
<span class="font-medium"><%= t(".upgrades.manual.title") %></span>
<br>
<span class="text-gray-500">
<%= t(".upgrades.manual.description") %>
</span>
<% end %>
</div>
<div class="flex items-center gap-4">
<%= form.radio_button :upgrades_mode, "release", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "release", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
<%= form.label :upgrades_mode_release, t(".upgrades.latest_release.title"), class: "text-gray-900 text-sm" do %>
<span class="font-medium"><%= t(".upgrades.latest_release.title") %></span>
<br>
<span class="text-gray-500">
<% end %>
</div>
<div class="flex items-center gap-4">
<%= form.radio_button :upgrades_mode, "release", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "release", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
<%= form.label :upgrades_mode_release, t(".upgrades.latest_release.title"), class: "text-gray-900 text-sm" do %>
<span class="font-medium"><%= t(".upgrades.latest_release.title") %></span>
<br>
<span class="text-gray-500">
<%= t(".upgrades.latest_release.description") %>
</span>
<% end %>
</div>
<div class="flex items-center gap-4">
<%= form.radio_button :upgrades_mode, "commit", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "commit", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
<%= form.label :upgrades_mode_commit, t(".upgrades.latest_commit.title"), class: "text-gray-900 text-sm" do %>
<span class="font-medium"><%= t(".upgrades.latest_commit.title") %></span>
<br>
<span class="text-gray-500">
<% end %>
</div>
<div class="flex items-center gap-4">
<%= form.radio_button :upgrades_mode, "commit", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "commit", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
<%= form.label :upgrades_mode_commit, t(".upgrades.latest_commit.title"), class: "text-gray-900 text-sm" do %>
<span class="font-medium"><%= t(".upgrades.latest_commit.title") %></span>
<br>
<span class="text-gray-500">
<%= t(".upgrades.latest_commit.description") %>
</span>
<% end %>
</div>
<% end %>
</div>
</div>
@@ -50,40 +50,75 @@
<%= form.url_field :render_deploy_hook, label: t(".render_deploy_hook_label"), placeholder: t(".render_deploy_hook_placeholder"), value: Setting.render_deploy_hook, data: { "auto-submit-form-target" => "auto" } %>
</div>
<% end %>
<% end %>
<% end %>
<div>
<h2 class="font-medium mb-1"><%= t(".smtp_settings.title") %></h2>
<p class="text-gray-500 text-sm mb-4"><%= t(".smtp_settings.description") %></p>
<div class="space-y-4">
<div class="space-y-3">
<%= form.text_field :email_sender, label: t(".email_sender"), placeholder: t(".email_sender_placeholder"), value: Setting.email_sender, data: { "auto-submit-form-target" => "auto" } %>
<%= form.text_field :app_domain, label: t(".domain"), placeholder: t(".domain_placeholder"), value: Setting.app_domain, data: { "auto-submit-form-target" => "auto" } %>
<%= form.text_field :smtp_host, label: t(".smtp_settings.host"), placeholder: t(".smtp_settings.host_placeholder"), value: Setting.smtp_host, data: { "auto-submit-form-target" => "auto" } %>
<%= form.number_field :smtp_port, label: t(".smtp_settings.port"), placeholder: t(".smtp_settings.port_placeholder"), value: Setting.smtp_port, data: { "auto-submit-form-target" => "auto" } %>
<%= form.text_field :smtp_username, label: t(".smtp_settings.username"), placeholder: t(".smtp_settings.username_placeholder"), value: Setting.smtp_username, data: { "auto-submit-form-target" => "auto" } %>
<%= form.password_field :smtp_password, label: t(".smtp_settings.password"), placeholder: t(".smtp_settings.password_placeholder"), value: Setting.smtp_password, data: { "auto-submit-form-target" => "auto" } %>
</div>
<div class="flex items-center justify-between bg-white border border-alpha-black-100 p-4 rounded-lg">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-gray-25 flex items-center justify-center">
<%= lucide_icon "mails", class: "w-6 h-6 text-gray-500" %>
</div>
<div>
<p class="text-gray-900 font-medium text-sm"><%= t(".smtp_settings.send_test_email") %></p>
<p class="text-gray-500 text-sm"><%= t(".smtp_settings.send_test_email_description") %></p>
</div>
<%= settings_section title: t(".smtp_settings.title") do %>
<%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %>
<p class="text-gray-500 text-sm mb-4"><%= t(".smtp_settings.description") %></p>
<div class="space-y-4">
<div class="space-y-3">
<%= form.text_field :email_sender, label: t(".email_sender"), placeholder: t(".email_sender_placeholder"), value: Setting.email_sender, data: { "auto-submit-form-target" => "auto" } %>
<%= form.text_field :app_domain, label: t(".domain"), placeholder: t(".domain_placeholder"), value: Setting.app_domain, data: { "auto-submit-form-target" => "auto" } %>
<%= form.text_field :smtp_host, label: t(".smtp_settings.host"), placeholder: t(".smtp_settings.host_placeholder"), value: Setting.smtp_host, data: { "auto-submit-form-target" => "auto" } %>
<%= form.number_field :smtp_port, label: t(".smtp_settings.port"), placeholder: t(".smtp_settings.port_placeholder"), value: Setting.smtp_port, data: { "auto-submit-form-target" => "auto" } %>
<%= form.text_field :smtp_username, label: t(".smtp_settings.username"), placeholder: t(".smtp_settings.username_placeholder"), value: Setting.smtp_username, data: { "auto-submit-form-target" => "auto" } %>
<%= form.password_field :smtp_password, label: t(".smtp_settings.password"), placeholder: t(".smtp_settings.password_placeholder"), value: Setting.smtp_password, data: { "auto-submit-form-target" => "auto" } %>
</div>
<div class="flex items-center justify-between bg-white border border-alpha-black-100 p-4 rounded-lg">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-gray-25 flex items-center justify-center">
<%= lucide_icon "mails", class: "w-6 h-6 text-gray-500" %>
</div>
<div>
<%= link_to t(".smtp_settings.send_test_email_button"), send_test_email_settings_hosting_path, data: { turbo_method: :post }, class: "bg-gray-50 text-gray-900 text-sm font-medium rounded-lg px-3 py-2" %>
<p class="text-gray-900 font-medium text-sm"><%= t(".smtp_settings.send_test_email") %></p>
<p class="text-gray-500 text-sm"><%= t(".smtp_settings.send_test_email_description") %></p>
</div>
</div>
<div>
<%= link_to t(".smtp_settings.send_test_email_button"), send_test_email_settings_hosting_path, data: { turbo_method: :post }, class: "bg-gray-50 text-gray-900 text-sm font-medium rounded-lg px-3 py-2" %>
</div>
</div>
</div>
<% end %>
<% end %>
<div class="flex justify-between gap-4">
<%= previous_setting("Billing", settings_billing_path) %>
<%= next_setting("Accounts", accounts_path) %>
</div>
<%= settings_section title: t(".invite_settings.title") do %>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="space-y-1">
<p class="text-sm"><%= t(".invite_settings.require_invite_for_signup") %></p>
<p class="text-gray-500 text-sm"><%= t(".invite_settings.invite_code_description") %></p>
</div>
<%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %>
<div class="relative inline-block select-none">
<%= form.check_box :require_invite_for_signup, class: "sr-only peer", "data-auto-submit-form-target": "auto", "data-autosubmit-trigger-event": "input" %>
<%= form.label :require_invite_for_signup, "&nbsp;".html_safe, class: "maybe-switch" %>
</div>
<% end %>
</div>
<% if Setting.require_invite_for_signup %>
<div class="flex items-center justify-between mb-4">
<div>
<span class="text-gray-900 text-base font-medium"><%= t(".invite_settings.generated_tokens") %></span>
</div>
<div>
<%= button_to invite_codes_path,
method: :post,
class: "flex gap-1 bg-gray-50 text-gray-900 text-sm rounded-lg px-3 py-2" do %>
<span><%= t(".invite_settings.generate_tokens") %></span>
<% end %>
</div>
</div>
<div>
<%= turbo_frame_tag :invite_codes, src: invite_codes_path %>
</div>
<% end %>
</div>
<% end %>
<%= settings_nav_footer %>
</div>

View File

@@ -1,15 +0,0 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">Notifications</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="flex justify-center items-center py-20">
<p class="text-gray-500">Notifications coming soon...</p>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Preferences", settings_preferences_path) %>
<%= next_setting("Security", settings_security_path) %>
</div>
</div>

View File

@@ -1,6 +1,7 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
<%= settings_section title: t(".general_title"), subtitle: t(".general_subtitle") do %>
@@ -39,8 +40,6 @@
<% end %>
</div>
<% end %>
<div class="flex justify-between gap-4">
<%= previous_setting("Account", settings_profile_path) %>
<%= next_setting("Notifications", settings_notifications_path) %>
</div>
<%= settings_nav_footer %>
</div>

View File

@@ -89,7 +89,6 @@
</div>
<% end %>
</div>
<div class="flex gap-4">
<%= next_setting("Preferences", settings_preferences_path) %>
</div>
<%= settings_nav_footer %>
</div>

View File

@@ -1,15 +0,0 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4">Security</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<div class="flex justify-center items-center py-20">
<p class="text-gray-500">Security settings coming soon...</p>
</div>
</div>
<div class="flex justify-between gap-4">
<%= previous_setting("Notifications", settings_notifications_path) %>
<%= next_setting("Billing", settings_billing_path) %>
</div>
</div>

View File

@@ -0,0 +1,11 @@
<%# locals: (name: nil, color: "#000") %>
<% letter = name&.first || "?" %>
<% background_color = "color-mix(in srgb, #{color} 5%, white)" %>
<% border_color = "color-mix(in srgb, #{color} 10%, white)" %>
<span data-color-avatar-target="avatar"
class="w-8 h-8 flex items-center justify-center rounded-full"
style="background-color: <%= background_color %>; border-color: <%= border_color %>; color: <%= color %>">
<%= letter.upcase %>
</span>

View File

@@ -1,5 +1,5 @@
<%= turbo_frame_tag "drawer" do %>
<dialog class="bg-white border border-alpha-black-25 rounded-2xl max-w-[480px] w-full shadow-xs h-full mt-4 mr-4 focus-visible:outline-none flex flex-col" data-controller="modal" data-action="click->modal#clickOutside">
<dialog class="bg-white border border-alpha-black-25 rounded-2xl max-w-[480px] w-full shadow-xs h-full mt-4 mr-4 focus-visible:outline-none" data-controller="modal" data-action="click->modal#clickOutside">
<div class="flex flex-col h-full">
<div class="flex justify-end items-center p-4">
<div data-action="click->modal#close" class="cursor-pointer p-2">

View File

@@ -1,8 +1,13 @@
<div class="flex justify-center items-center h-[800px] text-sm">
<div class="text-center flex flex-col items-center max-w-[300px]">
<p class="text-gray-900 mb-1 font-medium"><%= t(".no_account_title") %></p>
<p class="text-gray-500 mb-4"><%= t(".no_account_subtitle") %></p>
<%= link_to new_account_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %>
<div class="flex justify-center items-center h-[800px]">
<div class="text-center flex flex-col gap-4 items-center max-w-[300px]">
<%= lucide_icon "layers", class: "w-6 h-6 text-gray-500" %>
<div class="space-y-1 text-sm">
<p class="text-gray-900 font-medium"><%= t(".no_account_title") %></p>
<p class="text-gray-500"><%= t(".no_account_subtitle") %></p>
</div>
<%= link_to new_account_path, class: "btn btn--primary flex items-center gap-1", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new_account") %></span>
<% end %>

View File

@@ -1,38 +1,25 @@
<%= styled_form_with model: tag, data: { turbo: false } do |form| %>
<div class="flex flex-col space-y-4 w-96" data-controller="color-select" data-color-select-selection-value="<%= tag.color %>">
<fieldset class="relative">
<span data-color-select-target="decoration" class="pointer-events-none absolute inset-y-3.5 left-3 flex items-center pl-1 block w-1 rounded-lg"></span>
<%= form.text_field :name,
value: tag.name,
autofocus: "",
required: true,
placeholder: "Enter tag name",
class: "rounded-lg w-full focus:ring-black focus:border-transparent placeholder:text-gray-500 pl-6" %>
</fieldset>
<fieldset>
<%= form.hidden_field :color, data: { color_select_target: "input" } %>
<ul role="radiogroup" class="flex justify-between items-center py-2">
<div data-controller="color-avatar">
<%= styled_form_with model: tag, class: "space-y-4", data: { turbo: false } do |f| %>
<section class="space-y-4">
<div class="w-fit m-auto">
<%= render partial: "shared/color_avatar", locals: { name: tag.name, color: tag.color } %>
</div>
<div class="flex gap-2 items-center justify-center">
<% Tag::COLORS.each do |color| %>
<li tabindex="0"
role="radio"
data-action="click->color-select#select keydown.enter->color-select#select keydown.space->color-select#select"
data-value="<%= color %>"
class="flex shrink-0 justify-center items-center w-5 h-5 cursor-pointer hover:bg-gray-200 rounded-full">
</li>
<label class="relative">
<%= f.radio_button :color, color, class: "sr-only peer", data: { action: "change->color-avatar#handleColorChange" } %>
<div class="w-6 h-6 rounded-full cursor-pointer peer-checked:ring-2 peer-checked:ring-offset-2 peer-checked:ring-blue-500" style="background-color: <%= color %>"></div>
</label>
<% end %>
</ul>
</fieldset>
</div>
<div class="relative flex items-center border border-gray-200 rounded-lg">
<%= f.text_field :name, placeholder: t(".placeholder"), class: "text-sm font-normal placeholder:text-gray-500 h-10 relative pl-3 w-full border-none rounded-lg", required: true, data: { color_avatar_target: "name" } %>
</div>
</section>
<section>
<%= hidden_field_tag :tag_id, params[:tag_id] %>
<% if tag.persisted? %>
<%= form.submit t(".update") %>
<% else %>
<%= form.submit t(".create") %>
<% end %>
<%= f.submit %>
</section>
</div>
<% end %>
<% end %>
</div>

View File

@@ -0,0 +1,3 @@
<div class="bg-white">
<div class="h-px bg-alpha-black-50 ml-4 mr-6"></div>
</div>

View File

@@ -1,23 +1,24 @@
<div id="<%= dom_id(tag) %>" class="flex justify-between mx-4 py-5 border-b last:border-b-0 border-alpha-black-50">
<%= render "badge", tag: tag %>
<%# locals: (tag:) %>
<%= contextual_menu do %>
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= link_to edit_tag_path(tag),
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg",
data: { turbo_frame: :modal } do %>
<%= lucide_icon "pencil-line", class: "w-5 h-5 text-gray-500" %>
<div id="<%= dom_id(tag) %>" class="flex justify-between items-center p-4 bg-white">
<div class="flex w-full items-center gap-2.5">
<%= render partial: "shared/color_avatar", locals: { name: tag.name, color: tag.color } %>
<p class="text-gray-900 text-sm truncate">
<%= tag.name %>
</p>
</div>
<div class="justify-self-end">
<%= contextual_menu do %>
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= contextual_menu_modal_action_item t(".edit"), edit_tag_path(tag) %>
<span><%= t(".edit") %></span>
<% end %>
<%= link_to new_tag_deletion_path(tag),
class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg",
data: { turbo_frame: :modal } do %>
<%= lucide_icon "trash-2", class: "w-5 h-5" %>
<span><%= t(".delete") %></span>
<% end %>
</div>
<% end %>
<%= link_to new_tag_deletion_path(tag),
class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg",
data: { turbo_frame: :modal } do %>
<%= lucide_icon "trash-2", class: "w-5 h-5" %>
<span><%= t(".delete") %></span>
<% end %>
</div>
<% end %>
</div>
</div>

View File

@@ -6,28 +6,28 @@
<header class="flex items-center justify-between">
<h1 class="text-gray-900 text-xl font-medium"><%= t(".tags") %></h1>
<%= link_to new_tag_path, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %>
<%= link_to new_tag_path, class: "btn btn--primary flex items-center gap-1 justify-center", data: { turbo_frame: :modal } do %>
<%= lucide_icon "plus", class: "w-5 h-5" %>
<p><%= t(".new") %></p>
<% end %>
</header>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<% if @tags.any? %>
<div class="rounded-xl bg-gray-25 space-y-1 p-1">
<div class="flex items-center gap-1.5 px-4 py-2 text-xs font-medium text-gray-500 uppercase">
<p><%= t(".tags") %></p>
<span class="text-gray-400">&middot;</span>
<p><%= @tags.count %></p>
</div>
<div class="rounded-xl bg-gray-25 p-1">
<h2 class="uppercase px-4 py-2 text-gray-500 text-xs"><%= t(".tags") %> · <%= @tags.size %></h2>
<div class="border border-alpha-gray-100 rounded-lg bg-white shadow-xs">
<%= render @tags %>
<div class="border border-alpha-black-25 rounded-md bg-white shadow-xs">
<div class="overflow-hidden rounded-md">
<%= render partial: @tags, spacer_template: "tags/ruler" %>
</div>
</div>
</div>
<% else %>
<div class="flex justify-center items-center py-20">
<div class="text-center flex flex-col items-center max-w-[300px]">
<p class="text-gray-900 mb-1 font-medium text-sm"><%= t(".empty") %></p>
@@ -37,13 +37,8 @@
<% end %>
</div>
</div>
<% end %>
</div>
<footer class="flex justify-between gap-4">
<%= previous_setting("Accounts", accounts_path) %>
<%= next_setting("Categories", categories_path) %>
</footer>
<%= settings_nav_footer %>
</section>

View File

@@ -4,19 +4,11 @@
<div class="flex items-center gap-2">
<%= contextual_menu do %>
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= link_to categories_path,
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg font-normal" do %>
<%= lucide_icon "tags", class: "w-5 h-5 text-gray-500" %>
<span class="text-black"><%= t(".edit_categories") %></span>
<% end %>
<%= link_to imports_path,
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg font-normal" do %>
<%= lucide_icon "hard-drive-upload", class: "w-5 h-5 text-gray-500" %>
<span class="text-black"><%= t(".edit_imports") %></span>
<% end %>
<%= contextual_menu_modal_action_item t(".edit_categories"), categories_path, icon: "shapes", turbo_frame: :_top %>
<%= contextual_menu_modal_action_item t(".edit_tags"), tags_path, icon: "tags", turbo_frame: :_top %>
<%= contextual_menu_modal_action_item t(".edit_merchants"), merchants_path, icon: "store", turbo_frame: :_top %>
<%= contextual_menu_modal_action_item t(".edit_imports"), imports_path, icon: "hard-drive-upload", turbo_frame: :_top %>
</div>
<% end %>
<%= link_to new_import_path(enable_type_selector: true), class: "rounded-lg bg-gray-50 border border-gray-200 flex items-center gap-1 justify-center px-3 py-2", data: { turbo_frame: "modal" } do %>

View File

@@ -1,5 +1,5 @@
<%# locals: (totals:) %>
<div class="grid grid-cols-3 bg-white rounded-xl border border-alpha-black-25 shadow-xs px-4 divide-x divide-alpha-black-100">
<div class="grid grid-cols-3 bg-white rounded-xl border border-alpha-black-25 shadow-xs divide-x divide-alpha-black-100">
<div class="p-4 space-y-2">
<p class="text-sm text-gray-500">Total transactions</p>
<p class="text-gray-900 font-medium text-xl" id="total-transactions"><%= totals[:count] %></p>

View File

@@ -5,13 +5,13 @@
data: { controller: "auto-submit-form" } do |form| %>
<div class="flex gap-2 mb-4">
<div class="grow">
<div class="relative flex items-center bg-white border border-alpha-black-200 rounded-lg focus-within:border-alpha-black-500">
<div class="flex items-center px-3 py-2 gap-2 border border-gray-200 rounded-lg focus-within:ring-gray-100 focus-within:border-gray-900">
<%= lucide_icon("search", class: "w-5 h-5 text-gray-500") %>
<%= form.text_field :search,
placeholder: "Search transactions by name",
value: @q[:search],
class: "placeholder:text-sm placeholder:text-gray-500 relative pl-10 w-full border-none rounded-lg focus:outline-none focus:ring-0",
class: "form-field__input placeholder:text-sm placeholder:text-gray-500",
"data-auto-submit-form-target": "auto" %>
<%= lucide_icon("search", class: "w-5 h-5 text-gray-500 ml-2 absolute inset-0 transform top-1/2 -translate-y-1/2") %>
</div>
</div>
<div data-controller="menu" class="relative">

View File

@@ -4,8 +4,8 @@
data-controller="tabs"
data-tabs-active-class="bg-gray-25 text-gray-900"
data-tabs-default-tab-value="<%= get_default_transaction_search_filter[:key] %>"
class="hidden absolute flex z-10 h-80 w-[540px] top-12 right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs">
<div class="flex w-44 flex-col items-start p-3 text-sm font-medium text-gray-500 border-r border-r-alpha-black-25">
class="hidden absolute flex z-10 h-80 w-[540px] top-12 right-0 border border-alpha-black-100 bg-white rounded-lg shadow-xs">
<div class="flex w-44 flex-col items-start p-3 text-sm font-medium text-gray-500 border-r border-r-alpha-black-100">
<% transaction_search_filters.each do |filter| %>
<button
class="flex text-gray-500 hover:bg-gray-25 items-center gap-2 px-3 rounded-md py-2 w-full"
@@ -20,7 +20,7 @@
</div>
<div class="flex flex-col grow">
<div class="grow p-2 border-b border-b-alpha-black-25 overflow-y-auto">
<div class="grow p-2 border-b border-b-alpha-black-100 overflow-y-auto">
<% transaction_search_filters.each do |filter| %>
<div id="<%= filter[:key] %>" data-tabs-target="tab">
<%= render partial: get_transaction_search_filter_partial_path(filter), locals: { form: form } %>

View File

@@ -1,13 +1,11 @@
<%= render "transactions/searches/form" %>
<ul id="transaction-search-filters" class="flex items-center flex-wrap gap-2">
<ul id="transaction-search-filters" class="flex items-center flex-wrap gap-2 mb-4">
<% @q.each do |param_key, param_value| %>
<% unless param_value.blank? %>
<div class="pb-4">
<% Array(param_value).each do |value| %>
<%= render partial: "transactions/searches/filters/badge", locals: { param_key: param_key, param_value: value } %>
<% end %>
</div>
<% Array(param_value).each do |value| %>
<%= render partial: "transactions/searches/filters/badge", locals: { param_key: param_key, param_value: value } %>
<% end %>
<% end %>
<% end %>
</ul>

View File

@@ -11,7 +11,7 @@
{
multiple: true,
checked: @q[:accounts]&.include?(account.name),
class: "rounded-sm border-gray-300 text-indigo-600 shadow-xs focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
class: "maybe-checkbox maybe-checkbox--light"
},
account.name,
nil %>

View File

@@ -11,7 +11,7 @@
{
multiple: true,
checked: @q[:categories]&.include?(category.name),
class: "rounded-sm border-gray-300 text-indigo-600 shadow-xs focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
class: "maybe-checkbox maybe-checkbox--light"
},
category.name,
nil %>

View File

@@ -11,11 +11,14 @@
{
multiple: true,
checked: @q[:merchants]&.include?(merchant.name),
class: "rounded-sm border-gray-300 text-indigo-600 shadow-xs focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
class: "maybe-checkbox maybe-checkbox--light"
},
merchant.name,
nil %>
<%= form.label :merchants, merchant.name, value: merchant.name, class: "text-sm text-gray-900" %>
<%= form.label :merchants, value: merchant.name, class: "text-sm text-gray-900 flex items-center gap-2" do %>
<%= circle_logo(merchant.name, hex: merchant.color, size: "sm") %>
<%= merchant.name %>
<% end %>
</div>
<% end %>
</div>

View File

@@ -23,6 +23,40 @@
],
"note": ""
},
{
"warning_type": "Cross-Site Scripting",
"warning_code": 2,
"fingerprint": "b1f821a5c03b8aa348fb21b9297081a3bf9e954244290e7e511c67213d35f3dc",
"check_name": "CrossSiteScripting",
"message": "Unescaped model attribute",
"file": "app/views/pages/changelog.html.erb",
"line": 22,
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
"code": "Provider::Github.new.fetch_latest_release_notes[:body]",
"render_path": [
{
"type": "controller",
"class": "PagesController",
"method": "changelog",
"line": 35,
"file": "app/controllers/pages_controller.rb",
"rendered": {
"name": "pages/changelog",
"file": "app/views/pages/changelog.html.erb"
}
}
],
"location": {
"type": "template",
"template": "pages/changelog"
},
"user_input": null,
"confidence": "High",
"cwe_id": [
79
],
"note": ""
},
{
"warning_type": "Dynamic Render Path",
"warning_code": 15,
@@ -58,6 +92,6 @@
"note": ""
}
],
"updated": "2024-08-23 08:29:05 -0400",
"updated": "2024-09-09 14:56:48 -0400",
"brakeman_version": "6.2.1"
}

View File

@@ -26,6 +26,8 @@ search:
ignore_unused:
- 'activerecord.attributes.*' # i18n-tasks does not detect these on forms, forms validations (https://github.com/glebm/i18n-tasks/blob/0b4b483c82664f26c5696fb0f6aa1297356e4683/templates/config/i18n-tasks.yml#L146)
- 'activerecord.models.*' # i18n-tasks does not detect use in dynamic model names (e.g. object.model_name.human)
- 'activerecord.errors.models*'
- 'activemodel.errors.models.*'
- 'helpers.submit.*' # i18n-tasks does not detect used at forms
- 'helpers.label.*' # i18n-tasks does not detect used at forms
- 'accounts.show.sync_message_*' # messages generated in the sync ActiveJob

View File

@@ -10,7 +10,7 @@ module Maybe
private
def semver
"0.1.0-alpha.16"
"0.1.0-alpha.17"
end
end
end

View File

@@ -1,6 +1,9 @@
---
en:
accounts:
account:
has_issues: Issue detected.
troubleshoot: Troubleshoot
accountables:
property:
area_unit: Area unit
@@ -72,7 +75,11 @@ en:
confirm_title: Delete financial institution?
delete: Delete institution
edit: Edit institution
has_issues: Issue detected, see accounts
new_account: Add account
status: Last synced %{last_synced_at} ago
status_never: Requires data sync
syncing: Syncing...
institutionless_accounts:
other_accounts: Other accounts
new:
@@ -102,6 +109,7 @@ en:
summary:
new: New account
sync_all:
button_text: Sync all
success: Successfully queued accounts for syncing.
tooltip:
cash: Cash

View File

@@ -1,22 +1,22 @@
---
en:
categories:
category:
delete: Delete category
edit: Edit category
create:
success: New transaction category created successfully
edit:
edit: Edit category
form:
create: Create category
update: Update
placeholder: Category name
index:
categories: Categories
new: New
empty: No categories found
new: New category
menu:
loading: Loading...
new:
new_category: New category
row:
delete: Delete category
edit: Edit category
update:
success: Transaction category updated successfully

View File

@@ -11,5 +11,7 @@ en:
name: Financial institution name
new:
new_institution: New financial institution
sync:
success: Institution sync started
update:
success: Institution updated

View File

@@ -0,0 +1,7 @@
---
en:
invite_codes:
index:
invite_code_description: Generate a new code to see it displayed here. Generated
codes that have been used will no longer be shown.
no_invite_codes: No codes to show

Some files were not shown because too many files have changed in this diff Show More