Compare commits
37 Commits
v0.1.0-alp
...
v0.1.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52d3528361 | ||
|
|
d3d9af8bce | ||
|
|
0149ca4ea1 | ||
|
|
30f7c120e1 | ||
|
|
949d3d80fa | ||
|
|
c28dd8f940 | ||
|
|
277e4476d9 | ||
|
|
b9341ac302 | ||
|
|
86741401c3 | ||
|
|
edf44bec03 | ||
|
|
5178928b68 | ||
|
|
ac0ff35360 | ||
|
|
04037b8943 | ||
|
|
cb13fd2245 | ||
|
|
eebc07d75e | ||
|
|
c30c1b9698 | ||
|
|
d3971f9cee | ||
|
|
9e4b931612 | ||
|
|
b44da70836 | ||
|
|
0db75a019b | ||
|
|
ee572d8d1f | ||
|
|
33d007a07b | ||
|
|
3673ab8f03 | ||
|
|
fb42c2ad43 | ||
|
|
9172eb931b | ||
|
|
0c8cf7e217 | ||
|
|
0bbf7f82b7 | ||
|
|
c05ee9b572 | ||
|
|
38c2b4670c | ||
|
|
f82ce59dad | ||
|
|
166ed4b1ea | ||
|
|
0c0db44b7f | ||
|
|
cd254fd19b | ||
|
|
0d20be4905 | ||
|
|
cf861ccff9 | ||
|
|
525439e44d | ||
|
|
e1efe97e6f |
64
.ai/cursorrules.md
Normal file
64
.ai/cursorrules.md
Normal 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 user’s 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 todo’s, 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
1
.gitignore
vendored
@@ -59,3 +59,4 @@ compose-dev.yaml
|
||||
gcp-storage-keyfile.json
|
||||
|
||||
coverage
|
||||
.cursorrules
|
||||
2
Gemfile
2
Gemfile
@@ -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"
|
||||
|
||||
155
Gemfile.lock
155
Gemfile.lock
@@ -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
|
||||
|
||||
BIN
app/assets/images/discord-icon.png
Normal file
BIN
app/assets/images/discord-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 932 B |
BIN
app/assets/images/github-icon.png
Normal file
BIN
app/assets/images/github-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 548 B |
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
10
app/controllers/invite_codes_controller.rb
Normal file
10
app/controllers/invite_codes_controller.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
class Settings::BillingsController < SettingsController
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
class Settings::NotificationsController < SettingsController
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
class Settings::SecuritiesController < SettingsController
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
end
|
||||
end
|
||||
@@ -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)
|
||||
|
||||
@@ -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 != "/")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
28
app/javascript/controllers/clipboard_controller.js
Normal file
28
app/javascript/controllers/clipboard_controller.js
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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`,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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" %>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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" %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
21
app/views/categories/_category.html.erb
Normal file
21
app/views/categories/_category.html.erb
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
3
app/views/categories/_ruler.html.erb
Normal file
3
app/views/categories/_ruler.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="bg-white">
|
||||
<div class="h-px bg-alpha-black-50 ml-4 mr-6"></div>
|
||||
</div>
|
||||
@@ -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">·</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
16
app/views/invite_codes/_invite_code.html.erb
Normal file
16
app/views/invite_codes/_invite_code.html.erb
Normal 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>
|
||||
12
app/views/invite_codes/index.html.erb
Normal file
12
app/views/invite_codes/index.html.erb
Normal 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 %>
|
||||
@@ -91,8 +91,9 @@
|
||||
<%= t(".portfolio") %>
|
||||
<% end %>
|
||||
<span class="font-bold tracking-wide">•</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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %>
|
||||
26
app/views/merchants/_merchant.html.erb
Normal file
26
app/views/merchants/_merchant.html.erb
Normal 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>
|
||||
3
app/views/merchants/_ruler.html.erb
Normal file
3
app/views/merchants/_ruler.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="bg-white">
|
||||
<div class="h-px bg-alpha-black-50 ml-14 mr-6"></div>
|
||||
</div>
|
||||
@@ -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">·</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">·</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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, " ".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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<%= next_setting("Preferences", settings_preferences_path) %>
|
||||
</div>
|
||||
|
||||
<%= settings_nav_footer %>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
11
app/views/shared/_color_avatar.html.erb
Normal file
11
app/views/shared/_color_avatar.html.erb
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
3
app/views/tags/_ruler.html.erb
Normal file
3
app/views/tags/_ruler.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="bg-white">
|
||||
<div class="h-px bg-alpha-black-50 ml-4 mr-6"></div>
|
||||
</div>
|
||||
@@ -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>
|
||||
|
||||
@@ -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">·</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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 } %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,7 +10,7 @@ module Maybe
|
||||
|
||||
private
|
||||
def semver
|
||||
"0.1.0-alpha.16"
|
||||
"0.1.0-alpha.17"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,5 +11,7 @@ en:
|
||||
name: Financial institution name
|
||||
new:
|
||||
new_institution: New financial institution
|
||||
sync:
|
||||
success: Institution sync started
|
||||
update:
|
||||
success: Institution updated
|
||||
|
||||
7
config/locales/views/invite_codes/en.yml
Normal file
7
config/locales/views/invite_codes/en.yml
Normal 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
Reference in New Issue
Block a user