Compare commits
18 commits
ab46d90d6a
...
812e8226bb
| Author | SHA1 | Date | |
|---|---|---|---|
| 812e8226bb | |||
| 955aeb984e | |||
| 74386b45d7 | |||
| ba0612b694 | |||
| b36b1577b5 | |||
| fb8fb4b27e | |||
| 02a64ef639 | |||
| 18a7e8fd9e | |||
| 9c4a0cd7a3 | |||
| d23f16c217 | |||
| 63f8768cc3 | |||
| 7459037c8a | |||
| 6eace54c34 | |||
| 76496f8a6d | |||
| 78931ddb47 | |||
| 811bb3e036 | |||
| efd92f6367 | |||
| b257de85f2 |
236 changed files with 1841 additions and 1122 deletions
|
|
@ -7,7 +7,7 @@ services:
|
|||
dockerfile: .devcontainer/Dockerfile
|
||||
|
||||
volumes:
|
||||
- ../..:/workspaces:cached
|
||||
- ../..:/workspaces:cached
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
|
|
@ -18,17 +18,20 @@ services:
|
|||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
depends_on:
|
||||
- mysql
|
||||
- mysql
|
||||
|
||||
environment:
|
||||
DB_USER: root
|
||||
|
||||
mysql:
|
||||
image: mariadb:10.6
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
|
||||
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 'true'
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
- mysql-data:/var/lib/mysql
|
||||
networks:
|
||||
- default
|
||||
- default
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
|
|
|
|||
1
Gemfile
1
Gemfile
|
|
@ -18,7 +18,6 @@ gem 'sprockets', '~> 4.2'
|
|||
gem 'haml', '~> 6.1', '>= 6.1.1'
|
||||
gem 'sass-rails', '~> 6.0'
|
||||
gem 'terser', '~> 1.1', '>= 1.1.17'
|
||||
gem 'react-rails', '~> 2.7', '>= 2.7.1'
|
||||
gem 'jsbundling-rails', '~> 1.3'
|
||||
gem 'turbo-rails', '~> 2.0'
|
||||
|
||||
|
|
|
|||
332
Gemfile.lock
332
Gemfile.lock
|
|
@ -6,29 +6,31 @@ PATH
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
action_text-trix (2.1.15)
|
||||
railties
|
||||
actioncable (8.1.1)
|
||||
actionpack (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activejob (= 8.0.2)
|
||||
activerecord (= 8.0.2)
|
||||
activestorage (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
actionmailbox (8.1.1)
|
||||
actionpack (= 8.1.1)
|
||||
activejob (= 8.1.1)
|
||||
activerecord (= 8.1.1)
|
||||
activestorage (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
actionview (= 8.0.2)
|
||||
activejob (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
actionmailer (8.1.1)
|
||||
actionpack (= 8.1.1)
|
||||
actionview (= 8.1.1)
|
||||
activejob (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.0.2)
|
||||
actionview (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
actionpack (8.1.1)
|
||||
actionview (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
|
|
@ -36,58 +38,59 @@ GEM
|
|||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activerecord (= 8.0.2)
|
||||
activestorage (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
actiontext (8.1.1)
|
||||
action_text-trix (~> 2.1.15)
|
||||
actionpack (= 8.1.1)
|
||||
activerecord (= 8.1.1)
|
||||
activestorage (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
actionview (8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
activejob (8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
activerecord (8.0.2)
|
||||
activemodel (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
activemodel (8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
activerecord (8.1.1)
|
||||
activemodel (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activejob (= 8.0.2)
|
||||
activerecord (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
activestorage (8.1.1)
|
||||
actionpack (= 8.1.1)
|
||||
activejob (= 8.1.1)
|
||||
activerecord (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.0.2)
|
||||
activesupport (8.1.1)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
json
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
addressable (2.8.8)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
ast (2.4.3)
|
||||
async (2.27.0)
|
||||
async (2.35.0)
|
||||
console (~> 1.29)
|
||||
fiber-annotation
|
||||
io-event (~> 1.11)
|
||||
metrics (~> 0.12)
|
||||
traces (~> 0.15)
|
||||
async-container (0.24.0)
|
||||
traces (~> 0.18)
|
||||
async-container (0.27.7)
|
||||
async (~> 2.22)
|
||||
async-http (0.89.0)
|
||||
async (>= 2.10.2)
|
||||
|
|
@ -99,41 +102,38 @@ GEM
|
|||
protocol-http1 (~> 0.30)
|
||||
protocol-http2 (~> 0.22)
|
||||
traces (~> 0.10)
|
||||
async-http-cache (0.4.5)
|
||||
async-http-cache (0.4.6)
|
||||
async-http (~> 0.56)
|
||||
async-pool (0.11.0)
|
||||
async-pool (0.11.1)
|
||||
async (>= 2.0)
|
||||
async-service (0.13.0)
|
||||
async-service (0.16.0)
|
||||
async
|
||||
async-container (~> 0.16)
|
||||
string-format (~> 0.2)
|
||||
attr_required (1.0.2)
|
||||
babel-source (5.8.35)
|
||||
babel-transpiler (0.7.0)
|
||||
babel-source (>= 4.0, < 6)
|
||||
execjs (~> 2.0)
|
||||
backport (1.2.0)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
benchmark (0.4.1)
|
||||
bigdecimal (3.2.2)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (4.0.1)
|
||||
bindata (2.5.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.18.6)
|
||||
bootsnap (1.20.1)
|
||||
msgpack (~> 1.2)
|
||||
builder (3.3.0)
|
||||
childprocess (5.1.0)
|
||||
logger (~> 1.5)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.3)
|
||||
console (1.32.0)
|
||||
concurrent-ruby (1.3.6)
|
||||
connection_pool (3.0.2)
|
||||
console (1.34.2)
|
||||
fiber-annotation
|
||||
fiber-local (~> 1.1)
|
||||
json
|
||||
crack (1.0.0)
|
||||
crack (1.0.1)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
date (3.4.1)
|
||||
date (3.5.1)
|
||||
debug (1.9.2)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
|
|
@ -154,7 +154,7 @@ GEM
|
|||
e2mmap (0.1.0)
|
||||
email_validator (2.2.4)
|
||||
activemodel
|
||||
erb (5.0.2)
|
||||
erb (6.0.1)
|
||||
erubi (1.13.1)
|
||||
execjs (2.10.0)
|
||||
falcon (0.48.6)
|
||||
|
|
@ -170,43 +170,47 @@ GEM
|
|||
protocol-http (~> 0.31)
|
||||
protocol-rack (~> 0.7)
|
||||
samovar (~> 2.3)
|
||||
faraday (2.13.4)
|
||||
faraday (2.14.0)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-follow_redirects (0.3.0)
|
||||
faraday-follow_redirects (0.4.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-net_http (3.4.1)
|
||||
net-http (>= 0.5.0)
|
||||
faraday-net_http (3.4.2)
|
||||
net-http (~> 0.5)
|
||||
ffi (1.17.2)
|
||||
ffi (1.17.2-aarch64-linux-gnu)
|
||||
ffi (1.17.2-arm64-darwin)
|
||||
ffi (1.17.2-x86_64-linux-gnu)
|
||||
fiber-annotation (0.2.0)
|
||||
fiber-local (1.1.0)
|
||||
fiber-storage
|
||||
fiber-storage (1.0.1)
|
||||
globalid (1.2.1)
|
||||
globalid (1.3.0)
|
||||
activesupport (>= 6.1)
|
||||
haml (6.3.0)
|
||||
haml (6.4.0)
|
||||
temple (>= 0.8.2)
|
||||
thor
|
||||
tilt
|
||||
hashdiff (1.2.0)
|
||||
hashie (5.0.0)
|
||||
hashdiff (1.2.1)
|
||||
hashie (5.1.0)
|
||||
logger
|
||||
http_accept_language (2.1.1)
|
||||
i18n (1.14.7)
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
io-console (0.8.1)
|
||||
io-endpoint (0.15.2)
|
||||
io-event (1.12.1)
|
||||
io-stream (0.10.0)
|
||||
irb (1.15.2)
|
||||
io-console (0.8.2)
|
||||
io-endpoint (0.16.0)
|
||||
io-event (1.14.2)
|
||||
io-stream (0.11.1)
|
||||
irb (1.16.0)
|
||||
pp (>= 0.6.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jaro_winkler (1.6.1)
|
||||
jsbundling-rails (1.3.1)
|
||||
railties (>= 6.0.0)
|
||||
json (2.13.1)
|
||||
json-jwt (1.16.7)
|
||||
json (2.18.0)
|
||||
json-jwt (1.17.0)
|
||||
activesupport (>= 4.2)
|
||||
aes_key_wrap
|
||||
base64
|
||||
|
|
@ -225,28 +229,31 @@ GEM
|
|||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
lint_roller (1.1.0)
|
||||
localhost (1.5.0)
|
||||
localhost (1.6.0)
|
||||
logger (1.7.0)
|
||||
loofah (2.24.1)
|
||||
loofah (2.25.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
mail (2.9.0)
|
||||
logger
|
||||
mini_mime (>= 0.1.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
mapping (1.1.3)
|
||||
marcel (1.0.4)
|
||||
marcel (1.1.0)
|
||||
memory_profiler (1.1.0)
|
||||
metrics (0.12.2)
|
||||
metrics (0.15.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (5.25.5)
|
||||
minitest (6.0.1)
|
||||
prism (~> 1.5)
|
||||
msgpack (1.8.0)
|
||||
mysql2 (0.5.6)
|
||||
net-http (0.6.0)
|
||||
uri
|
||||
net-imap (0.5.9)
|
||||
mysql2 (0.5.7)
|
||||
bigdecimal
|
||||
net-http (0.9.1)
|
||||
uri (>= 0.11.1)
|
||||
net-imap (0.6.2)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
|
|
@ -255,12 +262,19 @@ GEM
|
|||
timeout
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.9)
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.18.10)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
omniauth (2.1.3)
|
||||
nokogiri (1.18.10-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
omniauth (2.1.4)
|
||||
hashie (>= 3.4.6)
|
||||
logger
|
||||
rack (>= 2.2.3)
|
||||
rack-protection
|
||||
omniauth-rails_csrf_protection (1.0.2)
|
||||
|
|
@ -282,49 +296,49 @@ GEM
|
|||
tzinfo
|
||||
validate_url
|
||||
webfinger (~> 2.0)
|
||||
openssl (3.3.0)
|
||||
openssl (3.3.2)
|
||||
orm_adapter (0.5.0)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.9.0)
|
||||
parser (3.3.10.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pp (0.6.2)
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.4.0)
|
||||
process-metrics (0.5.1)
|
||||
prism (1.7.0)
|
||||
process-metrics (0.8.0)
|
||||
console (~> 1.8)
|
||||
json (~> 2)
|
||||
samovar (~> 2.1)
|
||||
protocol-hpack (1.5.1)
|
||||
protocol-http (0.51.0)
|
||||
protocol-http1 (0.34.1)
|
||||
protocol-http (0.56.1)
|
||||
protocol-http1 (0.35.2)
|
||||
protocol-http (~> 0.22)
|
||||
protocol-http2 (0.22.1)
|
||||
protocol-http2 (0.23.0)
|
||||
protocol-hpack (~> 1.4)
|
||||
protocol-http (~> 0.47)
|
||||
protocol-rack (0.15.0)
|
||||
protocol-rack (0.19.0)
|
||||
io-stream (>= 0.10)
|
||||
protocol-http (~> 0.43)
|
||||
rack (>= 1.0)
|
||||
psych (5.2.6)
|
||||
psych (5.3.1)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.2)
|
||||
public_suffix (7.0.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.16)
|
||||
rack-attack (6.7.0)
|
||||
rack (3.2.4)
|
||||
rack-attack (6.8.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-mini-profiler (3.3.1)
|
||||
rack (>= 1.2.0)
|
||||
rack-oauth2 (2.2.1)
|
||||
rack-oauth2 (2.3.0)
|
||||
activesupport
|
||||
attr_required
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
json-jwt (>= 1.11.0)
|
||||
rack (>= 2.1.0)
|
||||
rack-protection (4.1.1)
|
||||
rack-protection (4.2.1)
|
||||
base64 (>= 0.1.0)
|
||||
logger (>= 1.6.0)
|
||||
rack (>= 3.0.0, < 4)
|
||||
|
|
@ -333,22 +347,22 @@ GEM
|
|||
rack (>= 3.0.0)
|
||||
rack-test (2.2.0)
|
||||
rack (>= 1.3)
|
||||
rackup (2.2.1)
|
||||
rackup (2.3.1)
|
||||
rack (>= 3)
|
||||
rails (8.0.2)
|
||||
actioncable (= 8.0.2)
|
||||
actionmailbox (= 8.0.2)
|
||||
actionmailer (= 8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
actiontext (= 8.0.2)
|
||||
actionview (= 8.0.2)
|
||||
activejob (= 8.0.2)
|
||||
activemodel (= 8.0.2)
|
||||
activerecord (= 8.0.2)
|
||||
activestorage (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
rails (8.1.1)
|
||||
actioncable (= 8.1.1)
|
||||
actionmailbox (= 8.1.1)
|
||||
actionmailer (= 8.1.1)
|
||||
actionpack (= 8.1.1)
|
||||
actiontext (= 8.1.1)
|
||||
actionview (= 8.1.1)
|
||||
activejob (= 8.1.1)
|
||||
activemodel (= 8.1.1)
|
||||
activerecord (= 8.1.1)
|
||||
activestorage (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.0.2)
|
||||
railties (= 8.1.1)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
|
|
@ -356,45 +370,41 @@ GEM
|
|||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rails-i18n (8.0.1)
|
||||
rails-i18n (8.1.0)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 8.0.0, < 9)
|
||||
railties (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
railties (8.1.1)
|
||||
actionpack (= 8.1.1)
|
||||
activesupport (= 8.1.1)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.0)
|
||||
rake (13.3.1)
|
||||
rbs (2.8.4)
|
||||
rdiscount (2.2.7.3)
|
||||
rdoc (6.14.2)
|
||||
rdoc (7.0.3)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
react-rails (2.7.1)
|
||||
babel-transpiler (>= 0.7.0)
|
||||
connection_pool
|
||||
execjs
|
||||
railties (>= 3.2)
|
||||
tilt
|
||||
regexp_parser (2.10.0)
|
||||
reline (0.6.2)
|
||||
tsort
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
responders (3.2.0)
|
||||
actionpack (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
reverse_markdown (2.1.1)
|
||||
nokogiri
|
||||
rexml (3.4.1)
|
||||
rspec-core (3.13.5)
|
||||
rexml (3.4.4)
|
||||
rspec-core (3.13.6)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.5)
|
||||
rspec-mocks (3.13.7)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (7.1.1)
|
||||
|
|
@ -405,8 +415,8 @@ GEM
|
|||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.4)
|
||||
rubocop (1.79.0)
|
||||
rspec-support (3.13.6)
|
||||
rubocop (1.82.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
|
|
@ -414,15 +424,14 @@ GEM
|
|||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.46.0, < 2.0)
|
||||
rubocop-ast (>= 1.48.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
tsort (>= 0.2.0)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.46.0)
|
||||
rubocop-ast (1.48.0)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
ruby-progressbar (1.13.0)
|
||||
samovar (2.3.0)
|
||||
samovar (2.4.1)
|
||||
console (~> 1.0)
|
||||
mapping (~> 1.0)
|
||||
sanitize (6.1.3)
|
||||
|
|
@ -439,10 +448,10 @@ GEM
|
|||
sprockets-rails
|
||||
tilt
|
||||
securerandom (0.4.1)
|
||||
sentry-rails (5.26.0)
|
||||
sentry-rails (5.28.1)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.26.0)
|
||||
sentry-ruby (5.26.0)
|
||||
sentry-ruby (~> 5.28.1)
|
||||
sentry-ruby (5.28.1)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
shell (0.8.1)
|
||||
|
|
@ -464,9 +473,9 @@ GEM
|
|||
thor (~> 1.0)
|
||||
tilt (~> 2.0)
|
||||
yard (~> 0.9, >= 0.9.24)
|
||||
solargraph-rails (1.1.2)
|
||||
solargraph-rails (1.2.4)
|
||||
activesupport
|
||||
solargraph (>= 0.48.0, < 0.53.0)
|
||||
solargraph (>= 0.48.0, <= 0.57)
|
||||
sprockets (4.2.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
logger
|
||||
|
|
@ -476,7 +485,8 @@ GEM
|
|||
activesupport (>= 6.1)
|
||||
sprockets (>= 3.0.0)
|
||||
stackprof (0.2.27)
|
||||
stringio (3.1.7)
|
||||
string-format (0.2.0)
|
||||
stringio (3.2.0)
|
||||
swd (2.0.3)
|
||||
activesupport (>= 3)
|
||||
attr_required (>= 0.0.5)
|
||||
|
|
@ -489,18 +499,18 @@ GEM
|
|||
thor (1.4.0)
|
||||
thread-local (1.1.0)
|
||||
tilt (2.6.1)
|
||||
timeout (0.4.3)
|
||||
traces (0.15.2)
|
||||
timeout (0.6.0)
|
||||
traces (0.18.2)
|
||||
tsort (0.2.0)
|
||||
turbo-rails (2.0.16)
|
||||
turbo-rails (2.0.20)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unicode-display_width (3.1.4)
|
||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||
unicode-emoji (4.0.4)
|
||||
uri (1.0.3)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.2.0)
|
||||
uri (1.1.1)
|
||||
useragent (0.16.11)
|
||||
validate_url (1.0.15)
|
||||
activemodel (>= 3.0.0)
|
||||
|
|
@ -516,7 +526,7 @@ GEM
|
|||
activesupport
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
webmock (3.25.1)
|
||||
webmock (3.26.1)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
|
|
@ -525,11 +535,14 @@ GEM
|
|||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
will_paginate (4.0.1)
|
||||
yard (0.9.37)
|
||||
zeitwerk (2.7.3)
|
||||
yard (0.9.38)
|
||||
zeitwerk (2.7.4)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
arm64-darwin
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
RocketAMF!
|
||||
|
|
@ -557,7 +570,6 @@ DEPENDENCIES
|
|||
rails (~> 8.0, >= 8.0.1)
|
||||
rails-i18n (~> 8.0, >= 8.0.1)
|
||||
rdiscount (~> 2.2, >= 2.2.7.1)
|
||||
react-rails (~> 2.7, >= 2.7.1)
|
||||
rspec-rails (~> 7.0)
|
||||
sanitize (~> 6.0, >= 6.0.2)
|
||||
sass-rails (~> 6.0)
|
||||
|
|
|
|||
31
app/assets/javascripts/pose-picker.js
Normal file
31
app/assets/javascripts/pose-picker.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* PosePicker web component
|
||||
*
|
||||
* Progressive enhancement for pose picker forms:
|
||||
* - Auto-submits the form when a pose is selected (if JS is enabled)
|
||||
* - Shows a submit button as fallback (if JS is disabled or slow to load)
|
||||
* - Uses Custom Element internals API to communicate state to CSS
|
||||
*/
|
||||
class PosePickerPopover extends HTMLElement {
|
||||
#internals;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#internals = this.attachInternals();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// Listen for changes to auto-submit the form, then tell CSS about it!
|
||||
this.addEventListener("change", this.#handleChange);
|
||||
this.#internals.states.add("auto-loading");
|
||||
}
|
||||
|
||||
#handleChange(e) {
|
||||
// Only auto-submit if a radio button was changed
|
||||
if (e.target.type === "radio") {
|
||||
this.querySelector("form").requestSubmit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("pose-picker-popover", PosePickerPopover);
|
||||
|
|
@ -73,6 +73,7 @@ outfit-viewer
|
|||
border-radius: 100%
|
||||
border: 2px solid transparent
|
||||
transition: all .25s
|
||||
cursor: pointer
|
||||
|
||||
.playing-label, .paused-label
|
||||
display: none
|
||||
|
|
|
|||
|
|
@ -1,545 +0,0 @@
|
|||
@import "../application/item-badges.css";
|
||||
|
||||
body.wardrobe-v2 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.wardrobe-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background: #000;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.outfit-preview-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #000;
|
||||
position: relative;
|
||||
min-height: 400px;
|
||||
container-type: size;
|
||||
|
||||
/* The outfit viewer is a square filling the space, to at most 600px. */
|
||||
outfit-viewer {
|
||||
width: min(100cqw, 100cqh, 600px);
|
||||
height: min(100cqw, 100cqh, 600px);
|
||||
}
|
||||
|
||||
.no-preview-message {
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* Species/color picker floats over the preview at the bottom */
|
||||
species-color-picker {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
pointer-events: none;
|
||||
/* Allow clicks through when hidden */
|
||||
|
||||
/* Start hidden, reveal on hover or focus */
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
form {
|
||||
pointer-events: auto;
|
||||
/* Re-enable clicks on the form itself */
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 0.5rem 2rem 0.5rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 0.375rem;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
|
||||
background-position: right 0.5rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.5em 1.5em;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
option {
|
||||
background: #2D3748;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
/* Submit button: progressive enhancement pattern */
|
||||
/* If JS is disabled, the button is always visible */
|
||||
/* If JS is enabled but slow to load, fade in after 0.75s */
|
||||
/* Once the web component loads, hide the button completely */
|
||||
input[type="submit"] {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 0.375rem;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* If JS is enabled, hide the submit button initially with a delay */
|
||||
@media (scripting: enabled) {
|
||||
input[type="submit"] {
|
||||
position: absolute;
|
||||
margin-left: 0.5em;
|
||||
opacity: 0;
|
||||
animation: fade-in 0.25s forwards;
|
||||
animation-delay: 0.75s;
|
||||
}
|
||||
}
|
||||
|
||||
/* Once auto-loading is ready, hide the submit button completely */
|
||||
&:state(auto-loading) {
|
||||
input[type="submit"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Show picker on hover (real hover only, not simulated touch hover) */
|
||||
@media (hover: hover) {
|
||||
&:hover species-color-picker {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Show picker when it has focus */
|
||||
&:has(species-color-picker:focus-within) species-color-picker {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.outfit-controls-section {
|
||||
width: 400px;
|
||||
background: #fff;
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.3);
|
||||
|
||||
@media (max-width: 800px) {
|
||||
width: 100%;
|
||||
max-height: 40vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
font-size: 1.75rem;
|
||||
color: #448844;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
color: #448844;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.current-selection {
|
||||
padding: 1rem;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
margin: 1rem 0;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.worn-items {
|
||||
margin-top: 2rem;
|
||||
|
||||
.items-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: #f9f9f9;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 8px;
|
||||
color: #333;
|
||||
transition: background 0.2s, box-shadow 0.2s;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:hover .item-remove-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.item-thumbnail {
|
||||
flex-shrink: 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
/* Allow text to truncate */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-weight: 500;
|
||||
color: #2D3748;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-badges {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.item-remove-button {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, background 0.2s, transform 0.1s;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
outline: 2px solid #448844;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-search-form {
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
font-size: 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #448844;
|
||||
box-shadow: 0 0 0 3px rgba(68, 136, 68, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: #448844;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #357535;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(68, 136, 68, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-results {
|
||||
.search-results-header {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.back-button {
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0 0 1rem 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
color: #448844;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f9f9f9;
|
||||
border-color: #448844;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #448844;
|
||||
box-shadow: 0 0 0 3px rgba(68, 136, 68, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-results-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 1rem 0;
|
||||
|
||||
.item-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: #f9f9f9;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 8px;
|
||||
color: #333;
|
||||
transition: background 0.2s, box-shadow 0.2s;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:hover .item-add-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.item-thumbnail {
|
||||
flex-shrink: 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-weight: 500;
|
||||
color: #2D3748;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-badges {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.item-add-button {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
background: rgba(68, 136, 68, 0.9);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, background 0.2s, transform 0.1s;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: rgba(68, 136, 68, 1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
outline: 2px solid #448844;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin: 1.5rem 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
|
||||
a,
|
||||
span,
|
||||
em {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
color: #448844;
|
||||
background: white;
|
||||
|
||||
&:hover {
|
||||
background: #f9f9f9;
|
||||
border-color: #448844;
|
||||
}
|
||||
}
|
||||
|
||||
.current,
|
||||
em {
|
||||
background: #448844;
|
||||
color: white;
|
||||
border-color: #448844;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
border-color: #eee;
|
||||
|
||||
&:hover {
|
||||
background: white;
|
||||
border-color: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
713
app/assets/stylesheets/wardrobe/show.css
Normal file
713
app/assets/stylesheets/wardrobe/show.css
Normal file
|
|
@ -0,0 +1,713 @@
|
|||
@import "../application/item-badges.css";
|
||||
|
||||
/* Base button defaults - applied to all interactive controls */
|
||||
button,
|
||||
input[type="submit"],
|
||||
select {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
color: #448844;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f9f9f9;
|
||||
border-color: #448844;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #448844;
|
||||
box-shadow: 0 0 0 3px rgba(68, 136, 68, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Overlay variant - dark translucent buttons on preview background */
|
||||
.outfit-preview-section {
|
||||
button,
|
||||
select,
|
||||
input[type="submit"] {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Primary action variant - solid green for main actions */
|
||||
.search-form input[type="submit"] {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
background: #448844;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background: #357535;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 3px rgba(68, 136, 68, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
/* Icon button pattern - small action buttons with hover reveals */
|
||||
.item-remove-button,
|
||||
.item-add-button {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, background 0.2s, transform 0.1s;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
outline: 2px solid #448844;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-remove-button {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.item-add-button {
|
||||
background: rgba(68, 136, 68, 0.9);
|
||||
|
||||
&:hover {
|
||||
background: rgba(68, 136, 68, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Pagination links - treated as buttons for consistency */
|
||||
.pagination {
|
||||
a,
|
||||
span,
|
||||
em {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
color: #448844;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f9f9f9;
|
||||
border-color: #448844;
|
||||
}
|
||||
}
|
||||
|
||||
.current,
|
||||
em {
|
||||
background: #448844;
|
||||
color: white;
|
||||
border-color: #448844;
|
||||
font-style: normal;
|
||||
|
||||
&:hover {
|
||||
background: #448844;
|
||||
border-color: #448844;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: #ccc;
|
||||
border-color: #eee;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background: white;
|
||||
border-color: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.wardrobe-v2 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.wardrobe-container {
|
||||
display: grid;
|
||||
height: 100vh;
|
||||
background: #000;
|
||||
|
||||
/* Mobile: vertical stack with preview on top, controls below */
|
||||
grid-template-areas:
|
||||
"preview"
|
||||
"controls";
|
||||
grid-template-rows: minmax(100px, 45%) minmax(300px, 55%);
|
||||
grid-template-columns: 100%;
|
||||
|
||||
/* Desktop: side-by-side layout */
|
||||
@media (min-width: 801px) {
|
||||
grid-template-areas: "preview controls";
|
||||
grid-template-rows: 100%;
|
||||
grid-template-columns: 50% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.outfit-preview-section {
|
||||
grid-area: preview;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #000;
|
||||
position: relative;
|
||||
container-type: size;
|
||||
|
||||
/* The outfit viewer is a square filling the space, to at most 600px. */
|
||||
outfit-viewer {
|
||||
width: min(100cqw, 100cqh, 600px);
|
||||
height: min(100cqw, 100cqh, 600px);
|
||||
}
|
||||
|
||||
.no-preview-message {
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* Preview controls container - groups species/color picker and pose picker */
|
||||
.preview-controls {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
pointer-events: none;
|
||||
|
||||
/* Start hidden, reveal on hover or focus */
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
> * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pose picker button */
|
||||
.pose-picker-button {
|
||||
anchor-name: --pose-picker-anchor;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
.pose-emoji {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.pose-label {
|
||||
font-weight: normal;
|
||||
min-width: 3.5rem;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&[popovertargetopen] {
|
||||
border-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
/* Pose picker popover */
|
||||
pose-picker-popover {
|
||||
position: absolute;
|
||||
position-anchor: --pose-picker-anchor;
|
||||
margin: 0;
|
||||
inset: auto;
|
||||
position-area: top;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
|
||||
|
||||
.pose-picker-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pose-picker-table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0.5rem;
|
||||
|
||||
th {
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 0.25rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-icon {
|
||||
font-size: 1.25rem;
|
||||
display: inline-block;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pose-option {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
|
||||
input[type="radio"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.pose-thumbnail {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pose-thumbnail-viewer {
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.pose-unavailable {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.4;
|
||||
|
||||
.question-mark {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Selected state */
|
||||
input[type="radio"]:checked + .pose-thumbnail {
|
||||
border-color: #48BB78;
|
||||
box-shadow: 0 0 0 3px rgba(72, 187, 120, 0.4);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Hover state (only for available poses) */
|
||||
&.available:hover .pose-thumbnail {
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Focus state */
|
||||
input[type="radio"]:focus + .pose-thumbnail {
|
||||
border-color: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Unavailable state */
|
||||
&.unavailable {
|
||||
cursor: not-allowed;
|
||||
|
||||
.pose-thumbnail {
|
||||
opacity: 0.5;
|
||||
background: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Submit button: progressive enhancement pattern */
|
||||
.pose-submit-button {
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* If JS is enabled, hide the submit button initially with a delay */
|
||||
@media (scripting: enabled) {
|
||||
.pose-submit-button {
|
||||
opacity: 0;
|
||||
animation: fade-in 0.25s forwards;
|
||||
animation-delay: 0.75s;
|
||||
}
|
||||
}
|
||||
|
||||
/* Once auto-submit is enabled, hide the submit button completely */
|
||||
&:state(auto-loading) .pose-submit-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Species/color picker */
|
||||
species-color-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
select {
|
||||
padding: 0.5rem 2rem 0.5rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
|
||||
background-position: right 0.5rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.5em 1.5em;
|
||||
|
||||
option {
|
||||
background: #2D3748;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
/* Submit button: progressive enhancement pattern */
|
||||
/* If JS is disabled, the button is always visible */
|
||||
/* If JS is enabled but slow to load, fade in after 0.75s */
|
||||
/* Once the web component loads, hide the button completely */
|
||||
input[type="submit"] {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* If JS is enabled, hide the submit button initially with a delay */
|
||||
@media (scripting: enabled) {
|
||||
input[type="submit"] {
|
||||
position: absolute;
|
||||
margin-left: 0.5em;
|
||||
opacity: 0;
|
||||
animation: fade-in 0.25s forwards;
|
||||
animation-delay: 0.75s;
|
||||
}
|
||||
}
|
||||
|
||||
/* Once auto-loading is ready, hide the submit button completely */
|
||||
&:state(auto-loading) {
|
||||
input[type="submit"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Show controls on hover (real hover only, not simulated touch hover) */
|
||||
@media (hover: hover) {
|
||||
&:hover .preview-controls {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Show controls when they have focus or when popover is open */
|
||||
&:has(.preview-controls:focus-within) .preview-controls,
|
||||
&:has(.pose-picker-button[popovertargetopen]) .preview-controls {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.outfit-controls-section {
|
||||
grid-area: controls;
|
||||
background: #fff;
|
||||
padding: 2rem;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.3);
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
font-size: 1.75rem;
|
||||
color: #448844;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
color: #448844;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.current-selection {
|
||||
padding: 1rem;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
margin: 1rem 0;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.worn-items {
|
||||
margin-top: 2rem;
|
||||
|
||||
.items-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: #f9f9f9;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 8px;
|
||||
color: #333;
|
||||
transition: background 0.2s, box-shadow 0.2s;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:hover .item-remove-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.item-thumbnail {
|
||||
flex-shrink: 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
/* Allow text to truncate */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-weight: 500;
|
||||
color: #2D3748;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-badges {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* .item-remove-button styles are defined in button system above */
|
||||
}
|
||||
|
||||
.item-search-form {
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: stretch;
|
||||
|
||||
> form {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #448844;
|
||||
box-shadow: 0 0 0 3px rgba(68, 136, 68, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* input[type="submit"] styles are defined in button system above */
|
||||
}
|
||||
}
|
||||
|
||||
.search-results {
|
||||
.search-results-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 1rem 0;
|
||||
|
||||
.item-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: #f9f9f9;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 8px;
|
||||
color: #333;
|
||||
transition: background 0.2s, box-shadow 0.2s;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:hover .item-add-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.item-thumbnail {
|
||||
flex-shrink: 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-weight: 500;
|
||||
color: #2D3748;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-badges {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* .item-add-button styles are defined in button system above */
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin: 1.5rem 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
/* Pagination link styles are defined in button system above */
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -77,53 +77,6 @@ class OutfitsController < ApplicationController
|
|||
@campaign = Fundraising::Campaign.current rescue nil
|
||||
end
|
||||
|
||||
def new_v2
|
||||
# Get selected species and color from params, or default to Blue Acara
|
||||
@selected_species = params[:species] ? Species.find_by_id(params[:species]) : Species.find_by_name("Acara")
|
||||
@selected_color = params[:color] ? Color.find_by_id(params[:color]) : Color.find_by_name("Blue")
|
||||
|
||||
# Load valid colors for the selected species (colors that have existing pet types)
|
||||
@species = Species.alphabetical
|
||||
@colors = @selected_species.compatible_colors
|
||||
|
||||
# Find the best pet type for this species+color combo
|
||||
# If the exact combo doesn't exist, this will fall back to a simple color
|
||||
@pet_type = PetType.for_species_and_color(
|
||||
species_id: @selected_species.id,
|
||||
color_id: @selected_color.id
|
||||
)
|
||||
|
||||
# Use the pet type's actual color as the selected color
|
||||
# (might differ from requested color if we fell back to a simple color)
|
||||
@selected_color = @pet_type&.color
|
||||
|
||||
# Load items from the objects[] parameter
|
||||
item_ids = params[:objects] || []
|
||||
items = Item.where(id: item_ids)
|
||||
|
||||
# Build the outfit
|
||||
@outfit = Outfit.new(
|
||||
pet_state: @pet_type&.canonical_pet_state,
|
||||
worn_items: items,
|
||||
)
|
||||
|
||||
# Preload the manifests for all visible layers, so they load efficiently
|
||||
# in parallel rather than sequentially when rendering
|
||||
SwfAsset.preload_manifests(@outfit.visible_layers)
|
||||
|
||||
# Handle search mode
|
||||
@search_mode = params[:q].present?
|
||||
if @search_mode
|
||||
search_filters = build_search_filters(params[:q], @outfit)
|
||||
query_params = ActionController::Parameters.new(
|
||||
search_filters.each_with_index.map { |filter, i| [i.to_s, filter] }.to_h
|
||||
)
|
||||
@query = Item::Search::Query.from_params(query_params, current_user)
|
||||
@search_results = @query.results.paginate(page: params.dig(:q, :page), per_page: 30)
|
||||
end
|
||||
|
||||
render layout: false
|
||||
end
|
||||
|
||||
def show
|
||||
@outfit = Outfit.find(params[:id])
|
||||
|
|
@ -176,51 +129,5 @@ class OutfitsController < ApplicationController
|
|||
:status => :bad_request
|
||||
end
|
||||
|
||||
def build_search_filters(query_params, outfit)
|
||||
filters = []
|
||||
|
||||
# Add name filter if present
|
||||
if query_params[:name].present?
|
||||
filters << { key: "name", value: query_params[:name] }
|
||||
end
|
||||
|
||||
# Add item kind filter if present
|
||||
if query_params[:item_kind].present?
|
||||
case query_params[:item_kind]
|
||||
when "nc"
|
||||
filters << { key: "is_nc", value: "true" }
|
||||
when "np"
|
||||
filters << { key: "is_np", value: "true" }
|
||||
when "pb"
|
||||
filters << { key: "is_pb", value: "true" }
|
||||
end
|
||||
end
|
||||
|
||||
# Add zone filter if present
|
||||
if query_params[:zone].present?
|
||||
filters << { key: "occupied_zone_set_name", value: query_params[:zone] }
|
||||
end
|
||||
|
||||
# Always add auto-filter for items that fit the current pet
|
||||
pet_type = outfit.pet_type
|
||||
if pet_type
|
||||
fit_filter = {
|
||||
key: "fits",
|
||||
value: {
|
||||
species_id: pet_type.species_id.to_s,
|
||||
color_id: pet_type.color_id.to_s
|
||||
}
|
||||
}
|
||||
|
||||
# Include alt_style_id if present
|
||||
if outfit.alt_style_id.present?
|
||||
fit_filter[:value][:alt_style_id] = outfit.alt_style_id.to_s
|
||||
end
|
||||
|
||||
filters << fit_filter
|
||||
end
|
||||
|
||||
filters
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
144
app/controllers/wardrobe_controller.rb
Normal file
144
app/controllers/wardrobe_controller.rb
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
class WardrobeController < ApplicationController
|
||||
def show
|
||||
# Get selected species and color from params, or default to Blue Acara
|
||||
@selected_species = params[:species] ? Species.find_by_id(params[:species]) : Species.find_by_name("Acara")
|
||||
@selected_color = params[:color] ? Color.find_by_id(params[:color]) : Color.find_by_name("Blue")
|
||||
|
||||
# Load valid colors for the selected species (colors that have existing pet types)
|
||||
@species = Species.alphabetical
|
||||
@colors = @selected_species.compatible_colors
|
||||
|
||||
# Find the best pet type for this species+color combo
|
||||
# If the exact combo doesn't exist, this will fall back to a simple color
|
||||
@pet_type = PetType.for_species_and_color(
|
||||
species_id: @selected_species.id,
|
||||
color_id: @selected_color.id
|
||||
)
|
||||
|
||||
# Use the pet type's actual color as the selected color
|
||||
# (might differ from requested color if we fell back to a simple color)
|
||||
@selected_color = @pet_type&.color
|
||||
|
||||
# Get the selected pose from params, or default to nil (will use canonical)
|
||||
@selected_pose = params[:pose]
|
||||
|
||||
# Find the pet state for the selected pose, or use canonical
|
||||
@pet_state = if @pet_type && @selected_pose.present?
|
||||
@pet_type.pet_states.with_pose(@selected_pose).first || @pet_type.canonical_pet_state
|
||||
else
|
||||
@pet_type&.canonical_pet_state
|
||||
end
|
||||
|
||||
# If we found a pet_state, use its actual pose as the selected pose
|
||||
@selected_pose = @pet_state&.pose
|
||||
|
||||
# Load all available poses for this pet type (for the pose picker)
|
||||
@available_poses = @pet_type ? available_poses_for(@pet_type) : {}
|
||||
|
||||
# Preload the layers for all available poses so the thumbnails render efficiently
|
||||
if @pet_type
|
||||
pose_pet_states = @available_poses.values.compact
|
||||
SwfAsset.preload_manifests(pose_pet_states.flat_map(&:swf_assets))
|
||||
end
|
||||
|
||||
# Load items from the objects[] parameter
|
||||
item_ids = params[:objects] || []
|
||||
items = Item.where(id: item_ids)
|
||||
|
||||
# Build the outfit
|
||||
@outfit = Outfit.new(
|
||||
pet_state: @pet_state,
|
||||
worn_items: items,
|
||||
)
|
||||
|
||||
# Preload the manifests for all visible layers, so they load efficiently
|
||||
# in parallel rather than sequentially when rendering
|
||||
SwfAsset.preload_manifests(@outfit.visible_layers)
|
||||
|
||||
# Handle search mode
|
||||
@search_mode = params[:q].present?
|
||||
if @search_mode
|
||||
search_filters = build_search_filters(params[:q], @outfit)
|
||||
query_params = ActionController::Parameters.new(
|
||||
search_filters.each_with_index.map { |filter, i| [i.to_s, filter] }.to_h
|
||||
)
|
||||
@query = Item::Search::Query.from_params(query_params, current_user)
|
||||
@search_results = @query.results.paginate(page: params.dig(:q, :page), per_page: 30)
|
||||
end
|
||||
|
||||
render layout: false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Returns a hash of pose => pet_state for all the main poses,
|
||||
# indicating which poses are available for this pet type.
|
||||
# Uses the same logic as the Rainbow Pool to pick the "canonical" pet state
|
||||
# for each pose when multiple states exist.
|
||||
def available_poses_for(pet_type)
|
||||
poses_hash = {}
|
||||
|
||||
# Group all pet states by pose, then pick the best one for each pose
|
||||
# using emotion_order (same logic as Rainbow Pool)
|
||||
pet_type.pet_states.emotion_order.group_by(&:pose).each do |pose, states|
|
||||
# Only include the main poses (skip UNKNOWN, UNCONVERTED, etc.)
|
||||
if PetState::MAIN_POSES.include?(pose)
|
||||
poses_hash[pose] = states.first
|
||||
end
|
||||
end
|
||||
|
||||
# Ensure all main poses are in the hash, even if nil
|
||||
PetState::MAIN_POSES.each do |pose|
|
||||
poses_hash[pose] ||= nil
|
||||
end
|
||||
|
||||
poses_hash
|
||||
end
|
||||
|
||||
def build_search_filters(query_params, outfit)
|
||||
filters = []
|
||||
|
||||
# Add name filter if present
|
||||
if query_params[:name].present?
|
||||
filters << { key: "name", value: query_params[:name] }
|
||||
end
|
||||
|
||||
# Add item kind filter if present
|
||||
if query_params[:item_kind].present?
|
||||
case query_params[:item_kind]
|
||||
when "nc"
|
||||
filters << { key: "is_nc", value: "true" }
|
||||
when "np"
|
||||
filters << { key: "is_np", value: "true" }
|
||||
when "pb"
|
||||
filters << { key: "is_pb", value: "true" }
|
||||
end
|
||||
end
|
||||
|
||||
# Add zone filter if present
|
||||
if query_params[:zone].present?
|
||||
filters << { key: "occupied_zone_set_name", value: query_params[:zone] }
|
||||
end
|
||||
|
||||
# Always add auto-filter for items that fit the current pet
|
||||
pet_type = outfit.pet_type
|
||||
if pet_type
|
||||
fit_filter = {
|
||||
key: "fits",
|
||||
value: {
|
||||
species_id: pet_type.species_id.to_s,
|
||||
color_id: pet_type.color_id.to_s
|
||||
}
|
||||
}
|
||||
|
||||
# Include alt_style_id if present
|
||||
if outfit.alt_style_id.present?
|
||||
fit_filter[:value][:alt_style_id] = outfit.alt_style_id.to_s
|
||||
end
|
||||
|
||||
filters << fit_filter
|
||||
end
|
||||
|
||||
filters
|
||||
end
|
||||
end
|
||||
|
|
@ -65,30 +65,6 @@ module OutfitsHelper
|
|||
text_field_tag 'name', nil, options
|
||||
end
|
||||
|
||||
# Generate hidden fields to preserve outfit state in URL params.
|
||||
# Use the `except` parameter to skip certain fields, e.g. to override
|
||||
# them with specific values, like in the species/color picker.
|
||||
def outfit_state_params(outfit = @outfit, except: [])
|
||||
fields = []
|
||||
|
||||
fields << hidden_field_tag(:species, @outfit.species_id) unless except.include?(:species)
|
||||
fields << hidden_field_tag(:color, @outfit.color_id) unless except.include?(:color)
|
||||
|
||||
unless except.include?(:worn_items)
|
||||
outfit.worn_items.each do |item|
|
||||
fields << hidden_field_tag('objects[]', item.id)
|
||||
end
|
||||
end
|
||||
|
||||
unless except.include?(:q)
|
||||
(params[:q] || {}).each do |key, value|
|
||||
fields << hidden_field_tag("q[#{key}]", value) if value.present?
|
||||
end
|
||||
end
|
||||
|
||||
safe_join fields
|
||||
end
|
||||
|
||||
def outfit_viewer(...)
|
||||
render partial: "outfit_viewer",
|
||||
locals: parse_outfit_viewer_options(...)
|
||||
|
|
@ -99,133 +75,8 @@ module OutfitsHelper
|
|||
locals: parse_outfit_viewer_options(...)
|
||||
end
|
||||
|
||||
# Group outfit items by zone, applying smart multi-zone simplification.
|
||||
# Returns an array of hashes: {zone:, items:}
|
||||
# This matches the logic from wardrobe-2020's getZonesAndItems function.
|
||||
def outfit_items_by_zone(outfit)
|
||||
return [] if outfit.pet_type.nil?
|
||||
|
||||
# Get item appearances for this outfit
|
||||
item_appearances = Item.appearances_for(
|
||||
outfit.worn_items,
|
||||
outfit.pet_type,
|
||||
swf_asset_includes: [:zone]
|
||||
)
|
||||
|
||||
# Separate incompatible items (no layers for this pet)
|
||||
compatible_items = []
|
||||
incompatible_items = []
|
||||
|
||||
outfit.worn_items.each do |item|
|
||||
appearance = item_appearances[item.id]
|
||||
if appearance&.present?
|
||||
compatible_items << {item: item, appearance: appearance}
|
||||
else
|
||||
incompatible_items << item
|
||||
end
|
||||
end
|
||||
|
||||
# Group items by zone - multi-zone items appear in each zone
|
||||
items_by_zone = Hash.new { |h, k| h[k] = [] }
|
||||
zones_by_id = {}
|
||||
|
||||
compatible_items.each do |item_with_appearance|
|
||||
item = item_with_appearance[:item]
|
||||
appearance = item_with_appearance[:appearance]
|
||||
|
||||
# Get unique zones for this item (an item may have multiple assets per zone)
|
||||
appearance.swf_assets.map(&:zone).uniq.each do |zone|
|
||||
zones_by_id[zone.id] = zone
|
||||
items_by_zone[zone.id] << item
|
||||
end
|
||||
end
|
||||
|
||||
# Create zone groups with sorted items
|
||||
zones_and_items = items_by_zone.map do |zone_id, items|
|
||||
{
|
||||
zone_id: zone_id,
|
||||
zone_label: zones_by_id[zone_id].label,
|
||||
items: items.sort_by { |item| item.name.downcase }
|
||||
}
|
||||
end
|
||||
|
||||
# Sort zone groups alphabetically by label, then by ID for tiebreaking
|
||||
zones_and_items.sort_by! do |group|
|
||||
[group[:zone_label].downcase, group[:zone_id]]
|
||||
end
|
||||
|
||||
# Apply multi-zone simplification: remove redundant single-item groups
|
||||
zones_and_items = simplify_multi_zone_groups(zones_and_items)
|
||||
|
||||
# Add zone ID disambiguation for duplicate labels
|
||||
zones_and_items = disambiguate_zone_labels(zones_and_items)
|
||||
|
||||
# Add incompatible items section if any
|
||||
if incompatible_items.any?
|
||||
zones_and_items << {
|
||||
zone_id: nil,
|
||||
zone_label: "Incompatible",
|
||||
items: incompatible_items.sort_by { |item| item.name.downcase }
|
||||
}
|
||||
end
|
||||
|
||||
zones_and_items
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Simplify zone groups by removing redundant single-item groups.
|
||||
# Keep groups with multiple items (conflicts). For single-item groups,
|
||||
# only keep them if the item doesn't appear in a multi-item group.
|
||||
def simplify_multi_zone_groups(zones_and_items)
|
||||
# Find groups with conflicts (multiple items)
|
||||
groups_with_conflicts = zones_and_items.select { |g| g[:items].length > 1 }
|
||||
|
||||
# Track which items appear in conflict groups
|
||||
items_with_conflicts = Set.new(
|
||||
groups_with_conflicts.flat_map { |g| g[:items].map(&:id) }
|
||||
)
|
||||
|
||||
# Track which items we've already shown
|
||||
items_we_have_seen = Set.new
|
||||
|
||||
# Filter groups
|
||||
zones_and_items.select do |group|
|
||||
# Always keep groups with multiple items
|
||||
if group[:items].length > 1
|
||||
group[:items].each { |item| items_we_have_seen.add(item.id) }
|
||||
true
|
||||
else
|
||||
# For single-item groups, only keep if:
|
||||
# - Item hasn't been seen yet AND
|
||||
# - Item won't appear in a conflict group
|
||||
item = group[:items].first
|
||||
item_id = item.id
|
||||
|
||||
if items_we_have_seen.include?(item_id) || items_with_conflicts.include?(item_id)
|
||||
false
|
||||
else
|
||||
items_we_have_seen.add(item_id)
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Add zone IDs to labels when there are duplicates
|
||||
def disambiguate_zone_labels(zones_and_items)
|
||||
label_counts = zones_and_items.group_by { |g| g[:zone_label] }
|
||||
.transform_values(&:count)
|
||||
|
||||
zones_and_items.each do |group|
|
||||
if label_counts[group[:zone_label]] > 1
|
||||
group[:zone_label] = "#{group[:zone_label]} (##{group[:zone_id]})"
|
||||
end
|
||||
end
|
||||
|
||||
zones_and_items
|
||||
end
|
||||
|
||||
def parse_outfit_viewer_options(
|
||||
outfit=nil, pet_state: nil, preferred_image_format: :png, **html_options
|
||||
)
|
||||
|
|
|
|||
167
app/helpers/wardrobe_helper.rb
Normal file
167
app/helpers/wardrobe_helper.rb
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
module WardrobeHelper
|
||||
# Generate hidden fields to preserve outfit state in URL params.
|
||||
# Use the `except` parameter to skip certain fields, e.g. to override
|
||||
# them with specific values, like in the species/color picker.
|
||||
def outfit_state_params(outfit = @outfit, except: [])
|
||||
fields = []
|
||||
|
||||
fields << hidden_field_tag(:species, @outfit.species_id) unless except.include?(:species)
|
||||
fields << hidden_field_tag(:color, @outfit.color_id) unless except.include?(:color)
|
||||
fields << hidden_field_tag(:pose, @selected_pose) if @selected_pose && !except.include?(:pose)
|
||||
|
||||
unless except.include?(:worn_items)
|
||||
outfit.worn_items.each do |item|
|
||||
fields << hidden_field_tag('objects[]', item.id)
|
||||
end
|
||||
end
|
||||
|
||||
unless except.include?(:q)
|
||||
(params[:q] || {}).each do |key, value|
|
||||
fields << hidden_field_tag("q[#{key}]", value) if value.present?
|
||||
end
|
||||
end
|
||||
|
||||
safe_join fields
|
||||
end
|
||||
|
||||
# Get the emoji and label for a pose, for display in the pose picker button
|
||||
def pose_emoji_and_label(pose)
|
||||
case pose
|
||||
when "HAPPY_MASC", "HAPPY_FEM"
|
||||
{ emoji: "😀", label: "Happy" }
|
||||
when "SAD_MASC", "SAD_FEM"
|
||||
{ emoji: "😢", label: "Sad" }
|
||||
when "SICK_MASC", "SICK_FEM"
|
||||
{ emoji: "🤢", label: "Sick" }
|
||||
else
|
||||
{ emoji: "😀", label: "Default" }
|
||||
end
|
||||
end
|
||||
|
||||
# Group outfit items by zone, applying smart multi-zone simplification.
|
||||
# Returns an array of hashes: {zone:, items:}
|
||||
# This matches the logic from wardrobe-2020's getZonesAndItems function.
|
||||
def outfit_items_by_zone(outfit)
|
||||
return [] if outfit.pet_type.nil?
|
||||
|
||||
# Get item appearances for this outfit
|
||||
item_appearances = Item.appearances_for(
|
||||
outfit.worn_items,
|
||||
outfit.pet_type,
|
||||
swf_asset_includes: [:zone]
|
||||
)
|
||||
|
||||
# Separate incompatible items (no layers for this pet)
|
||||
compatible_items = []
|
||||
incompatible_items = []
|
||||
|
||||
outfit.worn_items.each do |item|
|
||||
appearance = item_appearances[item.id]
|
||||
if appearance&.present?
|
||||
compatible_items << {item: item, appearance: appearance}
|
||||
else
|
||||
incompatible_items << item
|
||||
end
|
||||
end
|
||||
|
||||
# Group items by zone - multi-zone items appear in each zone
|
||||
items_by_zone = Hash.new { |h, k| h[k] = [] }
|
||||
zones_by_id = {}
|
||||
|
||||
compatible_items.each do |item_with_appearance|
|
||||
item = item_with_appearance[:item]
|
||||
appearance = item_with_appearance[:appearance]
|
||||
|
||||
# Get unique zones for this item (an item may have multiple assets per zone)
|
||||
appearance.swf_assets.map(&:zone).uniq.each do |zone|
|
||||
zones_by_id[zone.id] = zone
|
||||
items_by_zone[zone.id] << item
|
||||
end
|
||||
end
|
||||
|
||||
# Create zone groups with sorted items
|
||||
zones_and_items = items_by_zone.map do |zone_id, items|
|
||||
{
|
||||
zone_id: zone_id,
|
||||
zone_label: zones_by_id[zone_id].label,
|
||||
items: items.sort_by { |item| item.name.downcase }
|
||||
}
|
||||
end
|
||||
|
||||
# Sort zone groups alphabetically by label, then by ID for tiebreaking
|
||||
zones_and_items.sort_by! do |group|
|
||||
[group[:zone_label].downcase, group[:zone_id]]
|
||||
end
|
||||
|
||||
# Apply multi-zone simplification: remove redundant single-item groups
|
||||
zones_and_items = simplify_multi_zone_groups(zones_and_items)
|
||||
|
||||
# Add zone ID disambiguation for duplicate labels
|
||||
zones_and_items = disambiguate_zone_labels(zones_and_items)
|
||||
|
||||
# Add incompatible items section if any
|
||||
if incompatible_items.any?
|
||||
zones_and_items << {
|
||||
zone_id: nil,
|
||||
zone_label: "Incompatible",
|
||||
items: incompatible_items.sort_by { |item| item.name.downcase }
|
||||
}
|
||||
end
|
||||
|
||||
zones_and_items
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Simplify zone groups by removing redundant single-item groups.
|
||||
# Keep groups with multiple items (conflicts). For single-item groups,
|
||||
# only keep them if the item doesn't appear in a multi-item group.
|
||||
def simplify_multi_zone_groups(zones_and_items)
|
||||
# Find groups with conflicts (multiple items)
|
||||
groups_with_conflicts = zones_and_items.select { |g| g[:items].length > 1 }
|
||||
|
||||
# Track which items appear in conflict groups
|
||||
items_with_conflicts = Set.new(
|
||||
groups_with_conflicts.flat_map { |g| g[:items].map(&:id) }
|
||||
)
|
||||
|
||||
# Track which items we've already shown
|
||||
items_we_have_seen = Set.new
|
||||
|
||||
# Filter groups
|
||||
zones_and_items.select do |group|
|
||||
# Always keep groups with multiple items
|
||||
if group[:items].length > 1
|
||||
group[:items].each { |item| items_we_have_seen.add(item.id) }
|
||||
true
|
||||
else
|
||||
# For single-item groups, only keep if:
|
||||
# - Item hasn't been seen yet AND
|
||||
# - Item won't appear in a conflict group
|
||||
item = group[:items].first
|
||||
item_id = item.id
|
||||
|
||||
if items_we_have_seen.include?(item_id) || items_with_conflicts.include?(item_id)
|
||||
false
|
||||
else
|
||||
items_we_have_seen.add(item_id)
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Add zone IDs to labels when there are duplicates
|
||||
def disambiguate_zone_labels(zones_and_items)
|
||||
label_counts = zones_and_items.group_by { |g| g[:zone_label] }
|
||||
.transform_values(&:count)
|
||||
|
||||
zones_and_items.each do |group|
|
||||
if label_counts[group[:zone_label]] > 1
|
||||
group[:zone_label] = "#{group[:zone_label]} (##{group[:zone_id]})"
|
||||
end
|
||||
end
|
||||
|
||||
zones_and_items
|
||||
end
|
||||
end
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
- title "Wardrobe v2"
|
||||
|
||||
!!! 5
|
||||
%html
|
||||
%head
|
||||
%meta{charset: 'utf-8'}
|
||||
%meta{name: 'viewport', content: 'width=device-width, initial-scale=1'}
|
||||
%title= yield :title
|
||||
%link{href: image_path('favicon.png'), rel: 'icon'}
|
||||
= stylesheet_link_tag "application/hanger-spinner"
|
||||
= stylesheet_link_tag "application/outfit-viewer"
|
||||
= page_stylesheet_link_tag "outfits/new_v2"
|
||||
= javascript_include_tag "application", async: true
|
||||
= javascript_include_tag "idiomorph", async: true
|
||||
= javascript_include_tag "outfit-viewer", async: true
|
||||
= javascript_include_tag "species-color-picker", async: true
|
||||
= javascript_include_tag "outfits/new_v2", async: true
|
||||
= csrf_meta_tags
|
||||
%meta{name: 'outfit-viewer-morph-mode', value: 'full-page'}
|
||||
%body.wardrobe-v2
|
||||
.wardrobe-container
|
||||
.outfit-preview-section
|
||||
- if @pet_type.nil?
|
||||
.no-preview-message
|
||||
%p
|
||||
We haven't seen this kind of pet before! Try a different species/color
|
||||
combination.
|
||||
- else
|
||||
= outfit_viewer @outfit
|
||||
|
||||
%species-color-picker
|
||||
= form_with url: wardrobe_v2_path, method: :get do |f|
|
||||
= outfit_state_params except: [:color, :species]
|
||||
= select_tag :color,
|
||||
options_from_collection_for_select(@colors, "id", "human_name",
|
||||
@selected_color&.id),
|
||||
"aria-label": "Pet color"
|
||||
= select_tag :species,
|
||||
options_from_collection_for_select(@species, "id", "human_name",
|
||||
@selected_species&.id),
|
||||
"aria-label": "Pet species"
|
||||
= submit_tag "Go", name: nil
|
||||
|
||||
.outfit-controls-section
|
||||
%h1 Customize your pet
|
||||
|
||||
= form_with url: wardrobe_v2_path, method: :get, class: "item-search-form" do |f|
|
||||
= outfit_state_params
|
||||
= f.text_field "q[name]", placeholder: "Search for items...", value: params.dig(:q, :name), "aria-label": "Search for items"
|
||||
= f.submit "Search"
|
||||
|
||||
- if @search_mode
|
||||
= render "search_results"
|
||||
- elsif @outfit.worn_items.any?
|
||||
.worn-items
|
||||
- outfit_items_by_zone(@outfit).each do |zone_group|
|
||||
.zone-group
|
||||
%h3.zone-label= zone_group[:zone_label]
|
||||
%ul.items-list
|
||||
- zone_group[:items].each do |item|
|
||||
%li.item-card
|
||||
.item-thumbnail
|
||||
= image_tag item.thumbnail_url, alt: item.name, loading: "lazy"
|
||||
.item-info
|
||||
.item-name= item.name
|
||||
.item-badges
|
||||
= render "items/badges/kind", item: item
|
||||
= render "items/badges/first_seen", item: item
|
||||
= button_to wardrobe_v2_path, method: :get, class: "item-remove-button", title: "Remove #{item.name}", "aria-label": "Remove #{item.name}" do
|
||||
❌
|
||||
= outfit_state_params @outfit.without_item(item)
|
||||
21
app/views/wardrobe/_pose_option.html.haml
Normal file
21
app/views/wardrobe/_pose_option.html.haml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
-# Renders a single pose option in the pose picker grid
|
||||
-# @param pose [String] The pose name (e.g., "HAPPY_MASC")
|
||||
-# @param pet_state [PetState, nil] The pet state for this pose, or nil if unavailable
|
||||
-# @param selected [Boolean] Whether this pose is currently selected
|
||||
|
||||
- is_available = pet_state.present?
|
||||
- pose_label = pose.split('_').map(&:capitalize).join(' ')
|
||||
|
||||
%label.pose-option{class: [is_available ? 'available' : 'unavailable', selected ? 'selected' : nil]}
|
||||
= radio_button_tag :pose, pose, selected,
|
||||
disabled: !is_available,
|
||||
"aria-label": pose_label + (is_available ? "" : " (not available)")
|
||||
|
||||
.pose-thumbnail
|
||||
- if is_available
|
||||
-# Create a minimal outfit with just this pet state for the thumbnail
|
||||
- thumbnail_outfit = Outfit.new(pet_state: pet_state, worn_items: [])
|
||||
= outfit_viewer thumbnail_outfit, class: "pose-thumbnail-viewer"
|
||||
- else
|
||||
.pose-unavailable
|
||||
%span.question-mark{title: "Not available"} ❓
|
||||
|
|
@ -1,11 +1,6 @@
|
|||
.search-results
|
||||
.search-results-header
|
||||
= button_to wardrobe_v2_path, method: :get, class: "back-button" do
|
||||
← Back to outfit
|
||||
= outfit_state_params except: [:q]
|
||||
|
||||
- if @search_results.any?
|
||||
= will_paginate @search_results, param_name: "q[page]", params: { q: params[:q], species: @outfit.species_id, color: @outfit.color_id, objects: params[:objects] }
|
||||
= will_paginate @search_results, page_links: false, param_name: "q[page]", params: { q: params[:q], species: @outfit.species_id, color: @outfit.color_id, objects: params[:objects] }
|
||||
|
||||
%ul.search-results-list
|
||||
- @search_results.each do |item|
|
||||
119
app/views/wardrobe/show.html.haml
Normal file
119
app/views/wardrobe/show.html.haml
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
- title "Wardrobe v2"
|
||||
|
||||
!!! 5
|
||||
%html
|
||||
%head
|
||||
%meta{charset: 'utf-8'}
|
||||
%meta{name: 'viewport', content: 'width=device-width, initial-scale=1'}
|
||||
%title= yield :title
|
||||
%link{href: image_path('favicon.png'), rel: 'icon'}
|
||||
= stylesheet_link_tag "application/hanger-spinner"
|
||||
= stylesheet_link_tag "application/outfit-viewer"
|
||||
= page_stylesheet_link_tag "wardrobe/show"
|
||||
= javascript_include_tag "application", async: true
|
||||
= javascript_include_tag "idiomorph", async: true
|
||||
= javascript_include_tag "outfit-viewer", async: true
|
||||
= javascript_include_tag "species-color-picker", async: true
|
||||
= javascript_include_tag "pose-picker", async: true
|
||||
= javascript_include_tag "wardrobe/show", async: true
|
||||
= csrf_meta_tags
|
||||
%meta{name: 'outfit-viewer-morph-mode', value: 'full-page'}
|
||||
%body.wardrobe-v2
|
||||
.wardrobe-container
|
||||
.outfit-preview-section
|
||||
- if @pet_type.nil?
|
||||
.no-preview-message
|
||||
%p
|
||||
We haven't seen this kind of pet before! Try a different species/color
|
||||
combination.
|
||||
- else
|
||||
= outfit_viewer @outfit
|
||||
|
||||
.preview-controls
|
||||
%species-color-picker
|
||||
= form_with url: wardrobe_v2_path, method: :get do |f|
|
||||
= outfit_state_params except: [:color, :species]
|
||||
= select_tag :color,
|
||||
options_from_collection_for_select(@colors, "id", "human_name",
|
||||
@selected_color&.id),
|
||||
"aria-label": "Pet color"
|
||||
= select_tag :species,
|
||||
options_from_collection_for_select(@species, "id", "human_name",
|
||||
@selected_species&.id),
|
||||
"aria-label": "Pet species"
|
||||
= submit_tag "Go", name: nil
|
||||
|
||||
- if @pet_type
|
||||
- pose_info = pose_emoji_and_label(@selected_pose)
|
||||
%button#pose-picker-button.pose-picker-button{type: "button", popovertarget: "pose-picker-popover"}
|
||||
%span.pose-emoji= pose_info[:emoji]
|
||||
%span.pose-label= pose_info[:label]
|
||||
%span.chevron ▾
|
||||
|
||||
%pose-picker-popover#pose-picker-popover{popover: "auto"}
|
||||
= form_with url: wardrobe_v2_path, method: :get, class: "pose-picker-form" do |f|
|
||||
= outfit_state_params except: [:pose]
|
||||
%table.pose-picker-table
|
||||
%thead
|
||||
%tr
|
||||
%th
|
||||
%th
|
||||
%span.emoji-icon{title: "Happy"} 😀
|
||||
%th
|
||||
%span.emoji-icon{title: "Sad"} 😢
|
||||
%th
|
||||
%span.emoji-icon{title: "Sick"} 🤢
|
||||
%tbody
|
||||
%tr
|
||||
%th
|
||||
%span.emoji-icon{title: "Masculine"} 💁♂️
|
||||
%td
|
||||
= render "pose_option", pose: "HAPPY_MASC", pet_state: @available_poses["HAPPY_MASC"], selected: @selected_pose == "HAPPY_MASC"
|
||||
%td
|
||||
= render "pose_option", pose: "SAD_MASC", pet_state: @available_poses["SAD_MASC"], selected: @selected_pose == "SAD_MASC"
|
||||
%td
|
||||
= render "pose_option", pose: "SICK_MASC", pet_state: @available_poses["SICK_MASC"], selected: @selected_pose == "SICK_MASC"
|
||||
%tr
|
||||
%th
|
||||
%span.emoji-icon{title: "Feminine"} 💁♀️
|
||||
%td
|
||||
= render "pose_option", pose: "HAPPY_FEM", pet_state: @available_poses["HAPPY_FEM"], selected: @selected_pose == "HAPPY_FEM"
|
||||
%td
|
||||
= render "pose_option", pose: "SAD_FEM", pet_state: @available_poses["SAD_FEM"], selected: @selected_pose == "SAD_FEM"
|
||||
%td
|
||||
= render "pose_option", pose: "SICK_FEM", pet_state: @available_poses["SICK_FEM"], selected: @selected_pose == "SICK_FEM"
|
||||
= submit_tag "Change pose", name: nil, class: "pose-submit-button"
|
||||
|
||||
.outfit-controls-section
|
||||
.item-search-form
|
||||
- if @search_mode
|
||||
= button_to wardrobe_v2_path, method: :get, class: "back-button" do
|
||||
←
|
||||
= outfit_state_params except: [:q]
|
||||
= form_with url: wardrobe_v2_path, method: :get, class: "search-form" do |f|
|
||||
= outfit_state_params
|
||||
= f.text_field "q[name]", placeholder: "Search for items...", value: params.dig(:q, :name), "aria-label": "Search for items"
|
||||
= f.submit "Search"
|
||||
|
||||
- if @search_mode
|
||||
= render "search_results"
|
||||
- else
|
||||
%h1 Untitled outfit
|
||||
- if @outfit.worn_items.any?
|
||||
.worn-items
|
||||
- outfit_items_by_zone(@outfit).each do |zone_group|
|
||||
.zone-group
|
||||
%h3.zone-label= zone_group[:zone_label]
|
||||
%ul.items-list
|
||||
- zone_group[:items].each do |item|
|
||||
%li.item-card
|
||||
.item-thumbnail
|
||||
= image_tag item.thumbnail_url, alt: item.name, loading: "lazy"
|
||||
.item-info
|
||||
.item-name= item.name
|
||||
.item-badges
|
||||
= render "items/badges/kind", item: item
|
||||
= render "items/badges/first_seen", item: item
|
||||
= button_to wardrobe_v2_path, method: :get, class: "item-remove-button", title: "Remove #{item.name}", "aria-label": "Remove #{item.name}" do
|
||||
❌
|
||||
= outfit_state_params @outfit.without_item(item)
|
||||
6
bin/ci
Executable file
6
bin/ci
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env ruby
|
||||
require_relative "../config/boot"
|
||||
require "active_support/continuous_integration"
|
||||
|
||||
CI = ActiveSupport::ContinuousIntegration
|
||||
require_relative "../config/ci.rb"
|
||||
8
bin/rubocop
Executable file
8
bin/rubocop
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env ruby
|
||||
require "rubygems"
|
||||
require "bundler/setup"
|
||||
|
||||
# Explicit RuboCop config increases performance slightly while avoiding config confusion.
|
||||
ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
|
||||
|
||||
load Gem.bin_path("rubocop", "rubocop")
|
||||
|
|
@ -23,6 +23,8 @@ FileUtils.chdir APP_ROOT do
|
|||
puts "\n== Preparing database =="
|
||||
system! "bin/rails db:prepare"
|
||||
|
||||
system! "bin/rails db:reset" if ARGV.include?("--reset")
|
||||
|
||||
puts "\n== Importing public modeling data =="
|
||||
system! "bin/rails public_data:pull"
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Bundler.require(*Rails.groups)
|
|||
module OpenneoImpressItems
|
||||
class Application < Rails::Application
|
||||
# Initialize configuration defaults for originally generated Rails version.
|
||||
config.load_defaults 8.0
|
||||
config.load_defaults 8.1
|
||||
|
||||
# Please, add to the `ignore` list any other `lib` subdirectories that do
|
||||
# not contain `.rb` files, or that should not be reloaded or eager loaded.
|
||||
|
|
|
|||
21
config/ci.rb
Normal file
21
config/ci.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Run using bin/ci
|
||||
|
||||
CI.run do
|
||||
step "Setup", "bin/setup --skip-server"
|
||||
|
||||
step "Style: Ruby", "bin/rubocop"
|
||||
|
||||
step "Security: Importmap vulnerability audit", "bin/importmap audit"
|
||||
|
||||
step "Tests: Rails", "bin/rails test"
|
||||
step "Tests: System", "bin/rails test:system"
|
||||
step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant"
|
||||
|
||||
# Optional: set a green GitHub commit status to unblock PR merge.
|
||||
# Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
|
||||
# if success?
|
||||
# step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
|
||||
# else
|
||||
# failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
|
||||
# end
|
||||
end
|
||||
|
|
@ -3,7 +3,7 @@ development:
|
|||
adapter: mysql2
|
||||
host: <%= ENV.fetch("DB_HOST", "localhost") %>
|
||||
database: openneo_impress
|
||||
username: root
|
||||
username: <%= ENV.fetch("DB_USER", ENV.fetch("USER", "root")) %>
|
||||
pool: 5
|
||||
encoding: utf8mb4
|
||||
collation: utf8mb4_unicode_520_ci
|
||||
|
|
@ -14,7 +14,7 @@ development:
|
|||
adapter: mysql2
|
||||
host: <%= ENV.fetch("DB_HOST", "localhost") %>
|
||||
database: openneo_id
|
||||
username: root
|
||||
username: <%= ENV.fetch("DB_USER", ENV.fetch("USER", "root")) %>
|
||||
pool: 2
|
||||
variables:
|
||||
sql_mode: TRADITIONAL
|
||||
|
|
@ -25,7 +25,7 @@ test:
|
|||
adapter: mysql2
|
||||
host: <%= ENV.fetch("DB_HOST", "localhost") %>
|
||||
database: openneo_impress_test
|
||||
username: root
|
||||
username: <%= ENV.fetch("DB_USER", ENV.fetch("USER", "root")) %>
|
||||
pool: 5
|
||||
encoding: utf8mb4
|
||||
collation: utf8mb4_unicode_520_ci
|
||||
|
|
@ -36,7 +36,7 @@ test:
|
|||
adapter: mysql2
|
||||
host: <%= ENV.fetch("DB_HOST", "localhost") %>
|
||||
database: openneo_id_test
|
||||
username: root
|
||||
username: <%= ENV.fetch("DB_USER", ENV.fetch("USER", "root")) %>
|
||||
pool: 2
|
||||
variables:
|
||||
sql_mode: TRADITIONAL
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ Rails.application.configure do
|
|||
# Highlight code that enqueued background job in logs.
|
||||
config.active_job.verbose_enqueue_logs = true
|
||||
|
||||
config.react.variant = :development
|
||||
# Highlight code that triggered redirect in logs.
|
||||
config.action_dispatch.verbose_redirect_logs = true
|
||||
|
||||
# Raises error for missing translations.
|
||||
# config.i18n.raise_on_missing_translations = true
|
||||
|
|
|
|||
|
|
@ -18,9 +18,6 @@ Rails.application.configure do
|
|||
# Cache assets for far-future expiry since they are all digest stamped.
|
||||
config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" }
|
||||
|
||||
# Enable static file serving from the `/public` folder (turn off if using NGINX/Apache for it).
|
||||
config.public_file_server.enabled = false
|
||||
|
||||
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
|
||||
# config.asset_host = "http://assets.example.com"
|
||||
|
||||
|
|
@ -37,9 +34,7 @@ Rails.application.configure do
|
|||
config.log_tags = [ :request_id ]
|
||||
config.logger = ActiveSupport::TaggedLogging.logger(STDOUT)
|
||||
|
||||
config.react.variant = :production
|
||||
|
||||
# Change to "debug" to log everything (including potentially personally-identifiable information!)
|
||||
# Change to "debug" to log everything (including potentially personally-identifiable information!).
|
||||
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
|
||||
|
||||
# Prevent health checks from clogging up the logs.
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@
|
|||
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
|
||||
# config.content_security_policy_nonce_directives = %w(script-src style-src)
|
||||
#
|
||||
# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
|
||||
# # if the corresponding directives are specified in `content_security_policy_nonce_directives`.
|
||||
# # config.content_security_policy_nonce_auto = true
|
||||
#
|
||||
# # Report violations without enforcing the policy.
|
||||
# # config.content_security_policy_report_only = true
|
||||
# end
|
||||
|
|
|
|||
74
config/initializers/new_framework_defaults_8_1.rb
Normal file
74
config/initializers/new_framework_defaults_8_1.rb
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# Be sure to restart your server when you modify this file.
|
||||
#
|
||||
# This file eases your Rails 8.1 framework defaults upgrade.
|
||||
#
|
||||
# Uncomment each configuration one by one to switch to the new default.
|
||||
# Once your application is ready to run with all new defaults, you can remove
|
||||
# this file and set the `config.load_defaults` to `8.1`.
|
||||
#
|
||||
# Read the Guide for Upgrading Ruby on Rails for more info on each option.
|
||||
# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html
|
||||
|
||||
###
|
||||
# Skips escaping HTML entities and line separators. When set to `false`, the
|
||||
# JSON renderer no longer escapes these to improve performance.
|
||||
#
|
||||
# Example:
|
||||
# class PostsController < ApplicationController
|
||||
# def index
|
||||
# render json: { key: "\u2028\u2029<>&" }
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Renders `{"key":"\u2028\u2029\u003c\u003e\u0026"}` with the previous default, but `{"key":"
<>&"}` with the config
|
||||
# set to `false`.
|
||||
#
|
||||
# Applications that want to keep the escaping behavior can set the config to `true`.
|
||||
#++
|
||||
# Rails.configuration.action_controller.escape_json_responses = false
|
||||
|
||||
###
|
||||
# Skips escaping LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR (U+2029) in JSON.
|
||||
#
|
||||
# Historically these characters were not valid inside JavaScript literal strings but that changed in ECMAScript 2019.
|
||||
# As such it's no longer a concern in modern browsers: https://caniuse.com/mdn-javascript_builtins_json_json_superset.
|
||||
#++
|
||||
# Rails.configuration.active_support.escape_js_separators_in_json = false
|
||||
|
||||
###
|
||||
# Raises an error when order dependent finder methods (e.g. `#first`, `#second`) are called without `order` values
|
||||
# on the relation, and the model does not have any order columns (`implicit_order_column`, `query_constraints`, or
|
||||
# `primary_key`) to fall back on.
|
||||
#
|
||||
# The current behavior of not raising an error has been deprecated, and this configuration option will be removed in
|
||||
# Rails 8.2.
|
||||
#++
|
||||
# Rails.configuration.active_record.raise_on_missing_required_finder_order_columns = true
|
||||
|
||||
###
|
||||
# Controls how Rails handles path relative URL redirects.
|
||||
# When set to `:raise`, Rails will raise an `ActionController::Redirecting::UnsafeRedirectError`
|
||||
# for relative URLs without a leading slash, which can help prevent open redirect vulnerabilities.
|
||||
#
|
||||
# Example:
|
||||
# redirect_to "example.com" # Raises UnsafeRedirectError
|
||||
# redirect_to "@attacker.com" # Raises UnsafeRedirectError
|
||||
# redirect_to "/safe/path" # Works correctly
|
||||
#
|
||||
# Applications that want to allow these redirects can set the config to `:log` (previous default)
|
||||
# to only log warnings, or `:notify` to send ActiveSupport notifications.
|
||||
#++
|
||||
# Rails.configuration.action_controller.action_on_path_relative_redirect = :raise
|
||||
|
||||
###
|
||||
# Use a Ruby parser to track dependencies between Action View templates
|
||||
#++
|
||||
# Rails.configuration.action_view.render_tracker = :ruby
|
||||
|
||||
###
|
||||
# When enabled, hidden inputs generated by `form_tag`, `token_tag`, `method_tag`, and the hidden parameter fields
|
||||
# included in `button_to` forms will omit the `autocomplete="off"` attribute.
|
||||
#
|
||||
# Applications that want to keep generating the `autocomplete` attribute for those tags can set it to `false`.
|
||||
#++
|
||||
# Rails.configuration.action_view.remove_hidden_field_autocomplete = true
|
||||
|
|
@ -10,8 +10,8 @@ OpenneoImpressItems::Application.routes.draw do
|
|||
# TODO: It's a bit silly that outfits/new points to outfits#edit.
|
||||
# Should we refactor the controller/view structure here?
|
||||
get '/outfits/new', to: 'outfits#edit', as: :wardrobe
|
||||
get '/outfits/new/v2', to: 'outfits#new_v2', as: :wardrobe_v2
|
||||
get '/wardrobe' => redirect('/outfits/new')
|
||||
get '/wardrobe/v2', to: 'wardrobe#show', as: :wardrobe_v2
|
||||
get '/start/:color_name/:species_name' => 'outfits#start'
|
||||
|
||||
# The outfits users have created!
|
||||
|
|
|
|||
|
|
@ -13,34 +13,34 @@ Replace the complex React outfit editor (`app/javascript/wardrobe-2020/`) with a
|
|||
|
||||
## Current Status
|
||||
|
||||
**Wardrobe V2 is in early prototype/proof-of-concept stage.** It's accessible at `/outfits/new/v2` but is not yet linked from the main UI.
|
||||
**Wardrobe V2 is in early prototype/proof-of-concept stage.** It's accessible at `/wardrobe/v2` but is not yet linked from the main UI.
|
||||
|
||||
### What's Implemented
|
||||
|
||||
#### Core Infrastructure
|
||||
|
||||
**Route & Controller** ([outfits_controller.rb:80-115](app/controllers/outfits_controller.rb#L80-L115))
|
||||
- `GET /outfits/new/v2` - Main wardrobe endpoint
|
||||
**Route & Controller** ([wardrobe_controller.rb](app/controllers/wardrobe_controller.rb))
|
||||
- `GET /wardrobe/v2` - Main wardrobe endpoint
|
||||
- Takes URL params: `species`, `color`, `objects[]` (item IDs)
|
||||
- Returns full HTML page (no layout, designed to work standalone)
|
||||
- Defaults to Blue Acara if no pet specified
|
||||
|
||||
**View Layer** ([new_v2.html.haml](app/views/outfits/new_v2.html.haml))
|
||||
**View Layer** ([show.html.haml](app/views/wardrobe/show.html.haml))
|
||||
- Full-page layout with preview (left) and controls (right)
|
||||
- Responsive: stacks vertically on mobile (< 800px)
|
||||
- Uses existing `outfit_viewer` partial for rendering
|
||||
- Custom CSS in [outfits/new_v2.css](app/assets/stylesheets/outfits/new_v2.css)
|
||||
- Minimal JavaScript in [outfits/new_v2.js](app/assets/javascripts/outfits/new_v2.js)
|
||||
- Custom CSS in [wardrobe/show.css](app/assets/stylesheets/wardrobe/show.css)
|
||||
- Minimal JavaScript in [wardrobe/show.js](app/assets/javascripts/wardrobe/show.js)
|
||||
|
||||
**Pet Selection** ([new_v2.html.haml:31-42](app/views/outfits/new_v2.html.haml#L31-L42))
|
||||
**Pet Selection** ([show.html.haml:31-42](app/views/wardrobe/show.html.haml#L31-L42))
|
||||
- Species/color picker using `<species-color-picker>` web component
|
||||
- Floats over preview area (bottom), reveals on hover/focus
|
||||
- Progressive enhancement: submit button appears if JS slow/disabled
|
||||
- Auto-submits form on change when JS loaded
|
||||
- Filters colors to only those compatible with selected species
|
||||
- Advanced fallback: if species+color combo doesn't exist, falls back to simple color ([outfits_controller.rb:89-98](app/controllers/outfits_controller.rb#L89-L98))
|
||||
- Advanced fallback: if species+color combo doesn't exist, falls back to simple color ([wardrobe_controller.rb:13-16](app/controllers/wardrobe_controller.rb#L13-L16))
|
||||
|
||||
**Item Display** ([new_v2.html.haml:47-64](app/views/outfits/new_v2.html.haml#L47-L64))
|
||||
**Item Display** ([show.html.haml:47-64](app/views/wardrobe/show.html.haml#L47-L64))
|
||||
- Groups worn items by zone (Hat, Jacket, Wings, etc.)
|
||||
- Smart multi-zone simplification: hides redundant single-item zones
|
||||
- Shows items that occupy multiple zones in conflict zones only
|
||||
|
|
@ -49,7 +49,7 @@ Replace the complex React outfit editor (`app/javascript/wardrobe-2020/`) with a
|
|||
- Displays item thumbnails, names, and badges (NC/NP, first seen date)
|
||||
- Alphabetical sorting within zones
|
||||
|
||||
**Item Search** ([new_v2.html.haml:47-50](app/views/outfits/new_v2.html.haml#L47-L50))
|
||||
**Item Search** ([show.html.haml:47-50](app/views/wardrobe/show.html.haml#L47-L50))
|
||||
- Search form at top of controls section
|
||||
- Auto-filters to items that fit current pet (species + color + alt_style)
|
||||
- Uses `Item::Search::Query.from_params` for structured search
|
||||
|
|
@ -58,20 +58,20 @@ Replace the complex React outfit editor (`app/javascript/wardrobe-2020/`) with a
|
|||
- All search state scoped under `q[...]` params (name, page, etc.)
|
||||
- "Back to outfit" button to exit search
|
||||
|
||||
**Item Addition** ([_search_results.html.haml](app/views/outfits/_search_results.html.haml))
|
||||
**Item Addition** ([_search_results.html.haml](app/views/wardrobe/_search_results.html.haml))
|
||||
- Add button (➕) on each search result item
|
||||
- Adds item to outfit via GET request with updated `objects[]` params
|
||||
- Button hidden by default, appears on hover/focus
|
||||
- Preserves search state when adding items
|
||||
- Uses `outfit.with_item(item)` helper to generate new state
|
||||
|
||||
**Item Removal** ([new_v2.html.haml:70-72](app/views/outfits/new_v2.html.haml#L70-L72))
|
||||
**Item Removal** ([show.html.haml:70-72](app/views/wardrobe/show.html.haml#L70-L72))
|
||||
- Remove button (❌) on each worn item
|
||||
- Removes item from outfit via GET request with updated `objects[]` params
|
||||
- Button hidden by default, appears on hover/focus
|
||||
- Uses `outfit.without_item(item)` helper to generate new state
|
||||
|
||||
**State Management** ([outfits_helper.rb:68-90](app/helpers/outfits_helper.rb#L68-L90))
|
||||
**State Management** ([wardrobe_helper.rb:68-90](app/helpers/wardrobe_helper.rb#L68-L90))
|
||||
- All state lives in URL params (no client-side state)
|
||||
- `outfit_state_params` helper generates hidden fields for outfit state
|
||||
- Preserves: species, color, worn items (`objects[]`), search query (`q[...]`)
|
||||
|
|
@ -80,11 +80,11 @@ Replace the complex React outfit editor (`app/javascript/wardrobe-2020/`) with a
|
|||
|
||||
#### Supporting Helpers
|
||||
|
||||
**`outfit_items_by_zone`** ([outfits_helper.rb:96-167](app/helpers/outfits_helper.rb#L96-L167))
|
||||
**`outfit_items_by_zone`** ([wardrobe_helper.rb:96-167](app/helpers/wardrobe_helper.rb#L96-L167))
|
||||
- Core grouping logic for items by zone
|
||||
- Matches wardrobe-2020's `getZonesAndItems` behavior
|
||||
- Returns array of `{zone_id:, zone_label:, items:}` hashes
|
||||
- Extensively tested in [outfits_helper_spec.rb](spec/helpers/outfits_helper_spec.rb)
|
||||
- Extensively tested in [wardrobe_helper_spec.rb](spec/helpers/wardrobe_helper_spec.rb)
|
||||
|
||||
**`outfit_viewer`** ([outfits_helper.rb:86-89](app/helpers/outfits_helper.rb#L86-L89))
|
||||
- Renders `<outfit-viewer>` web component
|
||||
|
|
@ -969,12 +969,13 @@ This section documents ALL features in the React-based Wardrobe 2020 for referen
|
|||
- [Customization Architecture](./customization-architecture.md) - Data model deep dive
|
||||
|
||||
**Wardrobe V2 Files:**
|
||||
- Controller: [app/controllers/outfits_controller.rb](../app/controllers/outfits_controller.rb)
|
||||
- View: [app/views/outfits/new_v2.html.haml](../app/views/outfits/new_v2.html.haml)
|
||||
- Helpers: [app/helpers/outfits_helper.rb](../app/helpers/outfits_helper.rb)
|
||||
- Tests: [spec/helpers/outfits_helper_spec.rb](../spec/helpers/outfits_helper_spec.rb)
|
||||
- Styles: [app/assets/stylesheets/outfits/new_v2.css](../app/assets/stylesheets/outfits/new_v2.css)
|
||||
- JavaScript: [app/assets/javascripts/outfits/new_v2.js](../app/assets/javascripts/outfits/new_v2.js)
|
||||
- Controller: [app/controllers/wardrobe_controller.rb](../app/controllers/wardrobe_controller.rb)
|
||||
- View: [app/views/wardrobe/show.html.haml](../app/views/wardrobe/show.html.haml)
|
||||
- Search results partial: [app/views/wardrobe/_search_results.html.haml](../app/views/wardrobe/_search_results.html.haml)
|
||||
- Helpers: [app/helpers/wardrobe_helper.rb](../app/helpers/wardrobe_helper.rb)
|
||||
- Tests: [spec/helpers/wardrobe_helper_spec.rb](../spec/helpers/wardrobe_helper_spec.rb)
|
||||
- Styles: [app/assets/stylesheets/wardrobe/show.css](../app/assets/stylesheets/wardrobe/show.css)
|
||||
- JavaScript: [app/assets/javascripts/wardrobe/show.js](../app/assets/javascripts/wardrobe/show.js)
|
||||
- Web Components:
|
||||
- [app/assets/javascripts/species-color-picker.js](../app/assets/javascripts/species-color-picker.js)
|
||||
- [app/assets/javascripts/outfit-viewer.js](../app/assets/javascripts/outfit-viewer.js)
|
||||
|
|
|
|||
|
|
@ -35,12 +35,35 @@
|
|||
font-weight: 400;
|
||||
letter-spacing: -0.0025em;
|
||||
line-height: 1.4;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
place-items: center;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
#error-description {
|
||||
fill: #d30001;
|
||||
}
|
||||
|
||||
#error-id {
|
||||
fill: #f0eff0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #101010;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
#error-description {
|
||||
fill: #FF6161;
|
||||
}
|
||||
|
||||
#error-id {
|
||||
fill: #2c2c2c;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
font-weight: 700;
|
||||
|
|
@ -83,13 +106,11 @@
|
|||
}
|
||||
|
||||
main article br {
|
||||
|
||||
display: none;
|
||||
|
||||
@media(min-width: 48em) {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -102,10 +123,10 @@
|
|||
|
||||
<main>
|
||||
<header>
|
||||
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm140.456 133.2831c-40.823 0-64.884-35.146-64.884-85.7015 0-50.5554 24.061-85.700907 64.884-85.700907 40.822 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.062 85.7015-64.884 85.7015zm0-133.2831c-17.573 0-22.71 21.8984-22.71 47.5816 0 25.6835 5.137 47.5815 22.71 47.5815 17.302 0 22.709-21.898 22.709-47.5815 0-25.6832-5.407-47.5816-22.709-47.5816z" fill="#f0eff0"/><path d="m123.606 85.4445c3.212 1.0523 5.538 4.2089 5.538 8.0301 0 6.1472-4.209 9.5254-11.298 9.5254h-15.617v-34.0033h14.565c7.089 0 11.353 3.1566 11.353 9.2484 0 3.6551-2.049 6.3134-4.541 7.1994zm-12.904-2.9905h5.095c2.603 0 3.988-.9968 3.988-3.1013 0-2.1044-1.385-3.0459-3.988-3.0459h-5.095zm0 6.6456v6.5902h5.981c2.492 0 3.877-1.3291 3.877-3.2674 0-2.049-1.385-3.3228-3.877-3.3228zm43.786 13.9004h-8.362v-1.274c-.831.831-3.323 1.717-5.981 1.717-4.929 0-9.083-2.769-9.083-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.483 1.3845v-1.606c0-1.606-.942-2.9905-3.046-2.9905-1.606 0-2.548.7199-2.935 1.8275h-8.197c.72-4.8181 4.985-8.6393 11.409-8.6393 7.088 0 11.131 3.7659 11.131 10.2453zm-8.362-6.9779v-1.4399c-.554-1.0522-2.049-1.7167-3.655-1.7167-1.717 0-3.434.7199-3.434 2.3813 0 1.7168 1.717 2.4367 3.434 2.4367 1.606 0 3.101-.6645 3.655-1.6614zm27.996 6.9779v-1.994c-1.163 1.329-3.599 2.548-6.147 2.548-7.199 0-11.131-5.8151-11.131-13.0145s3.932-13.0143 11.131-13.0143c2.548 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.664-1.3291-2.159-2.326-3.821-2.326-2.99 0-4.763 2.4368-4.763 5.6488s1.773 5.5934 4.763 5.5934c1.717 0 3.157-.9415 3.821-2.326zm35.471-2.049h-3.101v11.2421h-8.806v-34.0033h15.285c7.31 0 12.35 4.1535 12.35 11.5744 0 5.1503-2.603 8.6947-6.757 10.2453l7.975 12.1836h-9.858zm-3.101-15.2849v8.1962h5.538c3.156 0 4.596-1.606 4.596-4.0981s-1.44-4.0981-4.596-4.0981zm36.957 17.8323h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm30.98 27.5234v-10.799c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.9259-11.132-13.0145 0-7.144 3.932-13.0143 11.132-13.0143 2.547 0 4.984 1.2184 6.147 2.5475v-1.9937h8.695v33.726zm0-17.9981v-6.5902c-.665-1.3291-2.105-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.661 0 3.156-.9415 3.821-2.326zm36.789-15.7279v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.996 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm19.084 16.2263h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.963 5.095 11.963 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm13.428 11.0206h8.474c.387 1.3845 1.606 2.1598 3.156 2.1598 1.44 0 2.548-.5538 2.548-1.7168 0-.9414-.72-1.2737-1.939-1.5506l-4.873-.9969c-4.154-.886-6.867-2.8797-6.867-7.2547 0-5.3165 4.762-8.4178 10.633-8.4178 6.812 0 10.522 3.1567 11.297 8.0855h-8.03c-.277-1.0522-1.052-1.9937-3.046-1.9937-1.273 0-2.326.5538-2.326 1.6614 0 .7753.554 1.163 1.717 1.3845l4.929 1.163c4.541 1.0522 6.978 3.4335 6.978 7.4763 0 5.3168-4.818 8.2518-10.91 8.2518-6.369 0-10.965-2.88-11.741-8.2518zm27.538-.8861v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.993-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.871 0-9.193-2.769-9.193-9.0819z" fill="#d30001"/></svg>
|
||||
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm140.456 133.2831c-40.823 0-64.884-35.146-64.884-85.7015 0-50.5554 24.061-85.700907 64.884-85.700907 40.822 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.062 85.7015-64.884 85.7015zm0-133.2831c-17.573 0-22.71 21.8984-22.71 47.5816 0 25.6835 5.137 47.5815 22.71 47.5815 17.302 0 22.709-21.898 22.709-47.5815 0-25.6832-5.407-47.5816-22.709-47.5816z" id="error-id"/><path d="m123.606 85.4445c3.212 1.0523 5.538 4.2089 5.538 8.0301 0 6.1472-4.209 9.5254-11.298 9.5254h-15.617v-34.0033h14.565c7.089 0 11.353 3.1566 11.353 9.2484 0 3.6551-2.049 6.3134-4.541 7.1994zm-12.904-2.9905h5.095c2.603 0 3.988-.9968 3.988-3.1013 0-2.1044-1.385-3.0459-3.988-3.0459h-5.095zm0 6.6456v6.5902h5.981c2.492 0 3.877-1.3291 3.877-3.2674 0-2.049-1.385-3.3228-3.877-3.3228zm43.786 13.9004h-8.362v-1.274c-.831.831-3.323 1.717-5.981 1.717-4.929 0-9.083-2.769-9.083-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.483 1.3845v-1.606c0-1.606-.942-2.9905-3.046-2.9905-1.606 0-2.548.7199-2.935 1.8275h-8.197c.72-4.8181 4.985-8.6393 11.409-8.6393 7.088 0 11.131 3.7659 11.131 10.2453zm-8.362-6.9779v-1.4399c-.554-1.0522-2.049-1.7167-3.655-1.7167-1.717 0-3.434.7199-3.434 2.3813 0 1.7168 1.717 2.4367 3.434 2.4367 1.606 0 3.101-.6645 3.655-1.6614zm27.996 6.9779v-1.994c-1.163 1.329-3.599 2.548-6.147 2.548-7.199 0-11.131-5.8151-11.131-13.0145s3.932-13.0143 11.131-13.0143c2.548 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.664-1.3291-2.159-2.326-3.821-2.326-2.99 0-4.763 2.4368-4.763 5.6488s1.773 5.5934 4.763 5.5934c1.717 0 3.157-.9415 3.821-2.326zm35.471-2.049h-3.101v11.2421h-8.806v-34.0033h15.285c7.31 0 12.35 4.1535 12.35 11.5744 0 5.1503-2.603 8.6947-6.757 10.2453l7.975 12.1836h-9.858zm-3.101-15.2849v8.1962h5.538c3.156 0 4.596-1.606 4.596-4.0981s-1.44-4.0981-4.596-4.0981zm36.957 17.8323h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm30.98 27.5234v-10.799c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.9259-11.132-13.0145 0-7.144 3.932-13.0143 11.132-13.0143 2.547 0 4.984 1.2184 6.147 2.5475v-1.9937h8.695v33.726zm0-17.9981v-6.5902c-.665-1.3291-2.105-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.661 0 3.156-.9415 3.821-2.326zm36.789-15.7279v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.996 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm19.084 16.2263h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.963 5.095 11.963 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm13.428 11.0206h8.474c.387 1.3845 1.606 2.1598 3.156 2.1598 1.44 0 2.548-.5538 2.548-1.7168 0-.9414-.72-1.2737-1.939-1.5506l-4.873-.9969c-4.154-.886-6.867-2.8797-6.867-7.2547 0-5.3165 4.762-8.4178 10.633-8.4178 6.812 0 10.522 3.1567 11.297 8.0855h-8.03c-.277-1.0522-1.052-1.9937-3.046-1.9937-1.273 0-2.326.5538-2.326 1.6614 0 .7753.554 1.163 1.717 1.3845l4.929 1.163c4.541 1.0522 6.978 3.4335 6.978 7.4763 0 5.3168-4.818 8.2518-10.91 8.2518-6.369 0-10.965-2.88-11.741-8.2518zm27.538-.8861v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.993-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.871 0-9.193-2.769-9.193-9.0819z" id="error-description"/></svg>
|
||||
</header>
|
||||
<article>
|
||||
<p><strong>The server cannot process the request due to a client error.</strong> Please check the request and try again. If you’re the application owner check the logs for more information.</p>
|
||||
<p><strong>The server cannot process the request due to a client error.</strong> Please check the request and try again. If you're the application owner check the logs for more information.</p>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
|
|
|
|||
|
|
@ -35,12 +35,35 @@
|
|||
font-weight: 400;
|
||||
letter-spacing: -0.0025em;
|
||||
line-height: 1.4;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
place-items: center;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
#error-description {
|
||||
fill: #d30001;
|
||||
}
|
||||
|
||||
#error-id {
|
||||
fill: #f0eff0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #101010;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
#error-description {
|
||||
fill: #FF6161;
|
||||
}
|
||||
|
||||
#error-id {
|
||||
fill: #2c2c2c;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
font-weight: 700;
|
||||
|
|
@ -83,13 +106,11 @@
|
|||
}
|
||||
|
||||
main article br {
|
||||
|
||||
display: none;
|
||||
|
||||
@media(min-width: 48em) {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -102,7 +123,7 @@
|
|||
|
||||
<main>
|
||||
<header>
|
||||
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm202.906 9.7326h-41.093c-2.433-7.2994-7.84-12.4361-17.302-12.4361-16.221 0-25.413 17.5728-25.954 34.8752v1.3517c5.137-7.0291 16.221-12.4361 30.82-12.4361 33.524 0 54.881 24.0612 54.881 53.7998 0 33.253-23.791 58.396-61.64 58.396-21.628 0-39.741-10.003-50.825-27.576-9.733-14.599-13.788-32.442-13.788-54.3406 0-51.9072 24.331-89.485807 66.236-89.485807 32.712 0 53.258 18.654107 58.665 47.851907zm-82.727 66.2355c0 13.247 9.463 22.439 22.71 22.439 12.977 0 22.439-9.192 22.439-22.439 0-13.517-9.462-22.7091-22.439-22.7091-13.247 0-22.71 9.1921-22.71 22.7091z" fill="#f0eff0"/><path d="m100.761 68.9967v34.0033h-7.1991l-14.2326-19.8814v19.8814h-8.5839v-34.0033h8.307l13.125 18.7184v-18.7184zm28.454 21.5428c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm13.185 3.8766v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.02-25.4194h9.083l12.958 34.0033h-9.027l-2.436-6.5902h-12.35l-2.381 6.5902h-8.806zm4.431 10.5222-3.489 9.5807h6.978zm17.44 11.0206c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm25.676 0c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm42.013 3.7658h8.031c-.887 5.7597-5.206 9.2487-11.686 9.2487-7.642 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.317-13.0143 12.516-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.319 4.5965 1.773 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm23.4 16.7244v10.799h-8.694v-33.726h8.694v1.9937c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8703 11.131 13.0143 0 7.0886-3.932 13.0145-11.131 13.0145-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.16 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.717 0-3.157.9969-3.822 2.326zm21.892 7.1994v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.206v6.7564h-5.206v8.307c0 1.9383.941 2.769 2.658 2.769.942 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.458 8.5839h-8.363v-1.274c-.83.831-3.322 1.717-5.981 1.717-4.928 0-9.082-2.769-9.082-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.482 1.3845v-1.606c0-1.606-.941-2.9905-3.045-2.9905-1.606 0-2.548.7199-2.936 1.8275h-8.196c.72-4.8181 4.984-8.6393 11.408-8.6393 7.089 0 11.132 3.7659 11.132 10.2453zm-8.363-6.9779v-1.4399c-.553-1.0522-2.049-1.7167-3.655-1.7167-1.716 0-3.433.7199-3.433 2.3813 0 1.7168 1.717 2.4367 3.433 2.4367 1.606 0 3.102-.6645 3.655-1.6614zm20.742 4.9839v1.994h-8.694v-35.997h8.694v13.0697c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8149 11.131 13.0143s-3.932 13.0145-11.131 13.0145c-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.105 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.662 0-3.157.9969-3.822 2.326zm28.759-20.2137v35.997h-8.695v-35.997zm19.172 27.3023h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.516-13.0143 7.642 0 11.962 5.095 11.962 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.434 1.2737-3.988 3.5997h7.532c-.111-2.0491-1.384-3.5997-3.544-3.5997z" fill="#d30001"/></svg>
|
||||
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm202.906 9.7326h-41.093c-2.433-7.2994-7.84-12.4361-17.302-12.4361-16.221 0-25.413 17.5728-25.954 34.8752v1.3517c5.137-7.0291 16.221-12.4361 30.82-12.4361 33.524 0 54.881 24.0612 54.881 53.7998 0 33.253-23.791 58.396-61.64 58.396-21.628 0-39.741-10.003-50.825-27.576-9.733-14.599-13.788-32.442-13.788-54.3406 0-51.9072 24.331-89.485807 66.236-89.485807 32.712 0 53.258 18.654107 58.665 47.851907zm-82.727 66.2355c0 13.247 9.463 22.439 22.71 22.439 12.977 0 22.439-9.192 22.439-22.439 0-13.517-9.462-22.7091-22.439-22.7091-13.247 0-22.71 9.1921-22.71 22.7091z" id="error-id"/><path d="m100.761 68.9967v34.0033h-7.1991l-14.2326-19.8814v19.8814h-8.5839v-34.0033h8.307l13.125 18.7184v-18.7184zm28.454 21.5428c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm13.185 3.8766v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.02-25.4194h9.083l12.958 34.0033h-9.027l-2.436-6.5902h-12.35l-2.381 6.5902h-8.806zm4.431 10.5222-3.489 9.5807h6.978zm17.44 11.0206c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm25.676 0c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm42.013 3.7658h8.031c-.887 5.7597-5.206 9.2487-11.686 9.2487-7.642 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.317-13.0143 12.516-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.319 4.5965 1.773 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm23.4 16.7244v10.799h-8.694v-33.726h8.694v1.9937c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8703 11.131 13.0143 0 7.0886-3.932 13.0145-11.131 13.0145-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.16 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.717 0-3.157.9969-3.822 2.326zm21.892 7.1994v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.206v6.7564h-5.206v8.307c0 1.9383.941 2.769 2.658 2.769.942 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.458 8.5839h-8.363v-1.274c-.83.831-3.322 1.717-5.981 1.717-4.928 0-9.082-2.769-9.082-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.482 1.3845v-1.606c0-1.606-.941-2.9905-3.045-2.9905-1.606 0-2.548.7199-2.936 1.8275h-8.196c.72-4.8181 4.984-8.6393 11.408-8.6393 7.089 0 11.132 3.7659 11.132 10.2453zm-8.363-6.9779v-1.4399c-.553-1.0522-2.049-1.7167-3.655-1.7167-1.716 0-3.433.7199-3.433 2.3813 0 1.7168 1.717 2.4367 3.433 2.4367 1.606 0 3.102-.6645 3.655-1.6614zm20.742 4.9839v1.994h-8.694v-35.997h8.694v13.0697c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8149 11.131 13.0143s-3.932 13.0145-11.131 13.0145c-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.105 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.662 0-3.157.9969-3.822 2.326zm28.759-20.2137v35.997h-8.695v-35.997zm19.172 27.3023h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.516-13.0143 7.642 0 11.962 5.095 11.962 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.434 1.2737-3.988 3.5997h7.532c-.111-2.0491-1.384-3.5997-3.544-3.5997z" id="error-description"/></svg>
|
||||
</header>
|
||||
<article>
|
||||
<p><strong>Your browser is not supported.</strong><br> Please upgrade your browser to continue.</p>
|
||||
|
|
|
|||
157
public/422.html
157
public/422.html
File diff suppressed because one or more lines are too long
157
public/500.html
157
public/500.html
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
require_relative '../rails_helper'
|
||||
|
||||
RSpec.describe OutfitsHelper, type: :helper do
|
||||
RSpec.describe WardrobeHelper, type: :helper do
|
||||
fixtures :zones, :colors, :species, :pet_types
|
||||
|
||||
# Use the Blue Acara's body_id throughout tests
|
||||
BIN
vendor/cache/action_text-trix-2.1.15.gem
vendored
Normal file
BIN
vendor/cache/action_text-trix-2.1.15.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/actioncable-8.0.2.gem
vendored
BIN
vendor/cache/actioncable-8.0.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/actioncable-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/actioncable-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/actionmailbox-8.0.2.gem
vendored
BIN
vendor/cache/actionmailbox-8.0.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/actionmailbox-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/actionmailbox-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/actionmailer-8.0.2.gem
vendored
BIN
vendor/cache/actionmailer-8.0.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/actionmailer-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/actionmailer-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/actionpack-8.0.2.gem
vendored
BIN
vendor/cache/actionpack-8.0.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/actionpack-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/actionpack-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/actiontext-8.0.2.gem
vendored
BIN
vendor/cache/actiontext-8.0.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/actiontext-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/actiontext-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/actionview-8.0.2.gem
vendored
BIN
vendor/cache/actionview-8.0.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/actionview-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/actionview-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/activejob-8.0.2.gem
vendored
BIN
vendor/cache/activejob-8.0.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/activejob-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/activejob-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/activemodel-8.0.2.gem
vendored
BIN
vendor/cache/activemodel-8.0.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/activemodel-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/activemodel-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/activerecord-8.0.2.gem
vendored
BIN
vendor/cache/activerecord-8.0.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/activerecord-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/activerecord-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/activestorage-8.0.2.gem
vendored
BIN
vendor/cache/activestorage-8.0.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/activestorage-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/activestorage-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/activesupport-8.0.2.gem
vendored
BIN
vendor/cache/activesupport-8.0.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/activesupport-8.1.1.gem
vendored
Normal file
BIN
vendor/cache/activesupport-8.1.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/addressable-2.8.7.gem
vendored
BIN
vendor/cache/addressable-2.8.7.gem
vendored
Binary file not shown.
BIN
vendor/cache/addressable-2.8.8.gem
vendored
Normal file
BIN
vendor/cache/addressable-2.8.8.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/async-2.27.0.gem
vendored
BIN
vendor/cache/async-2.27.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/async-2.35.0.gem
vendored
Normal file
BIN
vendor/cache/async-2.35.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/async-container-0.24.0.gem
vendored
BIN
vendor/cache/async-container-0.24.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/async-container-0.27.7.gem
vendored
Normal file
BIN
vendor/cache/async-container-0.27.7.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/async-http-cache-0.4.5.gem
vendored
BIN
vendor/cache/async-http-cache-0.4.5.gem
vendored
Binary file not shown.
BIN
vendor/cache/async-http-cache-0.4.6.gem
vendored
Normal file
BIN
vendor/cache/async-http-cache-0.4.6.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/async-pool-0.11.0.gem
vendored
BIN
vendor/cache/async-pool-0.11.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/async-pool-0.11.1.gem
vendored
Normal file
BIN
vendor/cache/async-pool-0.11.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/async-service-0.13.0.gem
vendored
BIN
vendor/cache/async-service-0.13.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/async-service-0.16.0.gem
vendored
Normal file
BIN
vendor/cache/async-service-0.16.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/babel-source-5.8.35.gem
vendored
BIN
vendor/cache/babel-source-5.8.35.gem
vendored
Binary file not shown.
BIN
vendor/cache/babel-transpiler-0.7.0.gem
vendored
BIN
vendor/cache/babel-transpiler-0.7.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/benchmark-0.4.1.gem
vendored
BIN
vendor/cache/benchmark-0.4.1.gem
vendored
Binary file not shown.
BIN
vendor/cache/benchmark-0.5.0.gem
vendored
Normal file
BIN
vendor/cache/benchmark-0.5.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/bigdecimal-3.2.2.gem
vendored
BIN
vendor/cache/bigdecimal-3.2.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/bigdecimal-4.0.1.gem
vendored
Normal file
BIN
vendor/cache/bigdecimal-4.0.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/bootsnap-1.18.6.gem
vendored
BIN
vendor/cache/bootsnap-1.18.6.gem
vendored
Binary file not shown.
BIN
vendor/cache/bootsnap-1.20.1.gem
vendored
Normal file
BIN
vendor/cache/bootsnap-1.20.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/concurrent-ruby-1.3.5.gem
vendored
BIN
vendor/cache/concurrent-ruby-1.3.5.gem
vendored
Binary file not shown.
BIN
vendor/cache/concurrent-ruby-1.3.6.gem
vendored
Normal file
BIN
vendor/cache/concurrent-ruby-1.3.6.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/connection_pool-2.5.3.gem
vendored
BIN
vendor/cache/connection_pool-2.5.3.gem
vendored
Binary file not shown.
BIN
vendor/cache/connection_pool-3.0.2.gem
vendored
Normal file
BIN
vendor/cache/connection_pool-3.0.2.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/console-1.32.0.gem
vendored
BIN
vendor/cache/console-1.32.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/console-1.34.2.gem
vendored
Normal file
BIN
vendor/cache/console-1.34.2.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/crack-1.0.0.gem
vendored
BIN
vendor/cache/crack-1.0.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/crack-1.0.1.gem
vendored
Normal file
BIN
vendor/cache/crack-1.0.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/date-3.4.1.gem
vendored
BIN
vendor/cache/date-3.4.1.gem
vendored
Binary file not shown.
BIN
vendor/cache/date-3.5.1.gem
vendored
Normal file
BIN
vendor/cache/date-3.5.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/erb-5.0.2.gem
vendored
BIN
vendor/cache/erb-5.0.2.gem
vendored
Binary file not shown.
BIN
vendor/cache/erb-6.0.1.gem
vendored
Normal file
BIN
vendor/cache/erb-6.0.1.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/faraday-2.13.4.gem
vendored
BIN
vendor/cache/faraday-2.13.4.gem
vendored
Binary file not shown.
BIN
vendor/cache/faraday-2.14.0.gem
vendored
Normal file
BIN
vendor/cache/faraday-2.14.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/faraday-follow_redirects-0.3.0.gem
vendored
BIN
vendor/cache/faraday-follow_redirects-0.3.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/faraday-follow_redirects-0.4.0.gem
vendored
Normal file
BIN
vendor/cache/faraday-follow_redirects-0.4.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/faraday-net_http-3.4.1.gem
vendored
BIN
vendor/cache/faraday-net_http-3.4.1.gem
vendored
Binary file not shown.
BIN
vendor/cache/faraday-net_http-3.4.2.gem
vendored
Normal file
BIN
vendor/cache/faraday-net_http-3.4.2.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/ffi-1.17.2.gem
vendored
Normal file
BIN
vendor/cache/ffi-1.17.2.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/globalid-1.2.1.gem
vendored
BIN
vendor/cache/globalid-1.2.1.gem
vendored
Binary file not shown.
BIN
vendor/cache/globalid-1.3.0.gem
vendored
Normal file
BIN
vendor/cache/globalid-1.3.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/haml-6.3.0.gem
vendored
BIN
vendor/cache/haml-6.3.0.gem
vendored
Binary file not shown.
BIN
vendor/cache/haml-6.4.0.gem
vendored
Normal file
BIN
vendor/cache/haml-6.4.0.gem
vendored
Normal file
Binary file not shown.
BIN
vendor/cache/hashdiff-1.2.0.gem
vendored
BIN
vendor/cache/hashdiff-1.2.0.gem
vendored
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue