Compare commits
380 commits
simpler-it
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| efd92f6367 | |||
| b257de85f2 | |||
| 4e2c99d4dd | |||
| 629090a88c | |||
| 77872311e6 | |||
| c78c0cb237 | |||
| 59da1fa04d | |||
| 3dca3fe05a | |||
| ec8d0fdbdc | |||
| 3d86231e29 | |||
| 3582229b47 | |||
| b1f06029f8 | |||
| d90e0549ca | |||
| d72d358135 | |||
| 1bfacf0340 | |||
| e0b2f6654f | |||
| d75b30ee7b | |||
| ba873e9e55 | |||
| 234c866e28 | |||
| 8eebc96b00 | |||
| a4dd680445 | |||
| 10708de615 | |||
| 0e57a76ce6 | |||
| 14be40a292 | |||
| 1cfb5129fa | |||
| 903d6a8a19 | |||
| 0e32eb5d8f | |||
| 99290235f5 | |||
| 00ec7002cb | |||
| bdd4356894 | |||
| a72d043659 | |||
| 735ca6c07b | |||
| 38bad12778 | |||
| c48b2b14aa | |||
| 475c4eb8dd | |||
| 8ba094a0be | |||
| 2c2ba1fc67 | |||
| 97ffffb67a | |||
| 667f562a88 | |||
| a4c3c31279 | |||
| b3f635c96c | |||
| 86a1875d6d | |||
| bf1c8f71fe | |||
| 39fe53c51f | |||
| 8eb3f73deb | |||
| eba5f04ec0 | |||
| e09296ef51 | |||
| b6e3079599 | |||
| a385a5b962 | |||
| 6ced72e10a | |||
| eff7f75a3a | |||
| 39e6872f59 | |||
| 3940513244 | |||
| 28cdef29d0 | |||
| 5be35591c2 | |||
| 7f62417294 | |||
| fe6d42ef67 | |||
| b8772c3aad | |||
| 8502b780c2 | |||
| 2f090f3924 | |||
| ab572b6576 | |||
| 019484b6dd | |||
| 34bf27069b | |||
| 84dec2d82e | |||
| d75492bd1d | |||
| b61526f6ad | |||
| 2b8fe68387 | |||
| 86205c5e44 | |||
| 5546d6df5d | |||
| 2be21a9238 | |||
| 1d4771ecc5 | |||
| 7eb209e206 | |||
| 30d42d29c1 | |||
| 347419aee8 | |||
| 98965ccc6f | |||
| 763f6d66be | |||
| addf2d6cc5 | |||
| b2a23b3e7b | |||
| b3f3b39aa0 | |||
| d92e3288ab | |||
| c32a495780 | |||
| ea5c315c2a | |||
| ab238ab2a6 | |||
| 0d2648d030 | |||
| d9bf4f745b | |||
| 407c4b38a5 | |||
| 6dc5aa28a4 | |||
| b656ccd982 | |||
| 02836494ae | |||
| b6e6f27fdf | |||
| aeb00f73cf | |||
| 06a301e6d7 | |||
| 1119bbb292 | |||
| fdbfa3c03f | |||
| 252f4f1df1 | |||
| 2d3d4051fe | |||
| 3cd02baa09 | |||
| 8347633a84 | |||
| 661a5385f4 | |||
| c27477fabe | |||
| c7bea666c9 | |||
| f49f9f386d | |||
| 3937ba354f | |||
| 388bb9a251 | |||
| e846a75f7a | |||
| 270b27c1d2 | |||
| 4cbac13df1 | |||
| 0261d02137 | |||
| e82c606ee8 | |||
| ed5b62e161 | |||
| 5472ccebef | |||
| f6f618c9d5 | |||
| 39bed6b157 | |||
| af5187edb6 | |||
| 21eaf7b266 | |||
| 91851bc340 | |||
| 3e7d27eaa3 | |||
| f7109e398a | |||
| f90380c4e6 | |||
| 218dc5b6f9 | |||
| bc0097850d | |||
| ec0b8d9cb9 | |||
| a57b3629db | |||
| 1d1dc15320 | |||
| b6c21dfe40 | |||
| c4a7e7916f | |||
| 217d25edab | |||
| dd213e8078 | |||
| c5995a2bd1 | |||
| 1ad3ea8f96 | |||
| b245690a60 | |||
| 3ed1c46b64 | |||
| 9e3ce74ed5 | |||
| 5f31e38428 | |||
| 8f9daf4d52 | |||
| 3242981eb2 | |||
| 54b25ef08e | |||
| e4e81f0694 | |||
| e3d196fe87 | |||
| 0b3dd02323 | |||
| 48c1a58df9 | |||
| 42e7eabdd8 | |||
| a208fca8d2 | |||
| 3ac89e830e | |||
| d82c7f817a | |||
| 5264947608 | |||
| 90407403ba | |||
| 242b85470d | |||
| 43717e2535 | |||
| bc1f7152bf | |||
| 9eaee4a2d4 | |||
| 52ca41dbff | |||
| c03e7446e3 | |||
| 6402e5abc3 | |||
| f81415d327 | |||
| 13ceec8fcc | |||
| 40765c729e | |||
| d26f3a7598 | |||
| 06721f77e9 | |||
| f9be3dceb1 | |||
| c9c080e74d | |||
| e65634d8bc | |||
| 4c5d14c591 | |||
| 28bd6ecca4 | |||
| 7a837edaf6 | |||
| f3894759d6 | |||
| 30ada0b7e1 | |||
| 8a38ce90dc | |||
| 6d25b3424f | |||
| 8902527438 | |||
| 044dface14 | |||
| b1890d4f6f | |||
| 3a5f33fd56 | |||
| a54a844e03 | |||
| c78d45a0b5 | |||
| 930bfca028 | |||
| 29aa769bda | |||
| 66438eae1a | |||
| 3b5b13c172 | |||
| 5b1d1f0695 | |||
| e92e315743 | |||
| eb2fb125b9 | |||
| d8ff99475e | |||
| 9726ecb1a5 | |||
| 540ce08caa | |||
| 881e63cfbd | |||
| 09e5a39b4c | |||
| bf20c9bb31 | |||
| 7607c2c015 | |||
| abfe1e6df7 | |||
| e36e273d50 | |||
| 83e5ad6bcc | |||
| acb52cb870 | |||
| 7ef689d658 | |||
| 23c083ff1d | |||
| 6b7c73870a | |||
| e7a0ff1234 | |||
| 50c9ba53e7 | |||
| 89c729ecbe | |||
| bb83f6fd36 | |||
| 7891acd3b1 | |||
| 16deee94e4 | |||
| 2cc0c5b031 | |||
| 381a892af8 | |||
| 1a0fb68b1c | |||
| 4f9fbc1ac0 | |||
| ad51690617 | |||
| 5648f55d2c | |||
| 6934b636fb | |||
| 83fe0d20e0 | |||
| be5ad31a1d | |||
| 1626f0706c | |||
| 7fad6abfed | |||
| c985c50a1b | |||
| bba863b94b | |||
| 7c1b3ca447 | |||
| 71f0aa4908 | |||
| 13a0362e6d | |||
| fe67211fdf | |||
| 0244653cb0 | |||
| 2c0d55edd1 | |||
| be0faaa36e | |||
| f87f4e61b3 | |||
| dfca88bed3 | |||
| bd001e643e | |||
| 1d51e28144 | |||
| fe4db1b605 | |||
| 860b8eef72 | |||
| 61e22e3943 | |||
| 03e4233f67 | |||
| b6bddb14be | |||
| e52838ba70 | |||
| 7ba68c52d4 | |||
| 26add4577c | |||
| efda6d74ab | |||
| 4a431a4ae8 | |||
| 4bcc3aaebb | |||
| 5890e52e53 | |||
| dd8426fefd | |||
| 2a9818b2d1 | |||
| 0b72b5568c | |||
| 86e1f31231 | |||
| a99fb3ec02 | |||
| d11c18129d | |||
| 0958111341 | |||
| 775baa250b | |||
| 2bd8afd486 | |||
| 1f1c6d92b1 | |||
| e4a640ccee | |||
| d465f4125e | |||
| 946a6326ac | |||
| d5a901b917 | |||
| 39e5ca59c4 | |||
| 4fa80d33cc | |||
| d66f81c96b | |||
| f8a5ce4490 | |||
| 81f0845d4a | |||
| f0257ba2d3 | |||
| d056a5e766 | |||
| 5214a14990 | |||
| 06a89689d8 | |||
| a08fb89d59 | |||
| 80307f21f7 | |||
| 75040ffbf3 | |||
| 6f45cd0485 | |||
| 4e33477c65 | |||
| b28255cafd | |||
| 99e8b46157 | |||
| 734b7fba1d | |||
| a1d6961249 | |||
| e7148ffae3 | |||
| 64b1d11faa | |||
| e63f4df25b | |||
| 535a0029f9 | |||
| c0e4291745 | |||
| d27c03606f | |||
| 40a3f5bf68 | |||
| 4bc38db5aa | |||
| 2ab1951e68 | |||
| cae2f3ca74 | |||
| 31619071af | |||
| f20a1b5398 | |||
| 3bd6f09a54 | |||
| 38474d19d7 | |||
| 73e0b3bb3c | |||
| 1f53615654 | |||
| 7f55456454 | |||
| f23bebb607 | |||
| cf2cd41531 | |||
| d45162897d | |||
| 02b510bb3f | |||
| 9ebc498888 | |||
| 5bf2ef42a0 | |||
| 0a5d369735 | |||
| ebd400369a | |||
| 81e4d16816 | |||
| 95ae669549 | |||
| 989c96fd2b | |||
| fdf1f31867 | |||
| c7b0ec71ef | |||
| 287d7af1b9 | |||
| 58d7c38523 | |||
| 68b6f46939 | |||
| cf6a19a7fc | |||
| 9e052789db | |||
| 30f211caf3 | |||
| dab865689f | |||
| 874483eacb | |||
| 2466e5971e | |||
| ce50a19d31 | |||
| d55512ad99 | |||
| 852dc74001 | |||
| 1d36e60df9 | |||
| 0f7b01bec3 | |||
| a14c4fca48 | |||
| a315282b70 | |||
| 38b2bad044 | |||
| 8a8dd468be | |||
| d84ab44771 | |||
| 903fb19d5c | |||
| 7f356cdede | |||
| 2aed7b21db | |||
| d1ac66a80f | |||
| 0e314482f7 | |||
| 71ffb7f1be | |||
| 2ffad8120e | |||
| 3f4e864a17 | |||
| 9f62d7cdbe | |||
| 27774d908f | |||
| 09572b5c05 | |||
| 5f2c454423 | |||
| 0b4d6dc7e6 | |||
| d470dde135 | |||
| 620e59f3ed | |||
| be560e4595 | |||
| c9f2d660bc | |||
| 052c02f841 | |||
| 96215c037a | |||
| 3a18820d05 | |||
| 5131ba40d8 | |||
| 65eaa031dd | |||
| 2e7bdc47d7 | |||
| d69c37089e | |||
| 5001a50a60 | |||
| c60dceb0ae | |||
| 2dbbc4bdd8 | |||
| 30eced448d | |||
| 6f08abc3aa | |||
| edcb21558a | |||
| 176ab20fd1 | |||
| 0305817cec | |||
| 4d5b583432 | |||
| 2e48376c5a | |||
| 2ea8f16e43 | |||
| de99e0236b | |||
| 6dd8e585a3 | |||
| 77ff55353c | |||
| a88fc14bd7 | |||
| 9f44fd47e4 | |||
| 4c44f8d6a4 | |||
| 2b2bffd9da | |||
| a184c75575 | |||
| c06c297174 | |||
| 36f8efadbf | |||
| e0f9a27adc | |||
| 1c36276865 | |||
| 6fdeffebf1 | |||
| 2f341cfd39 | |||
| 6312253b82 | |||
| 8ab5af1aca | |||
| bd62476722 | |||
| 5cbab5a766 | |||
| e67830642c | |||
| 1972ecf043 | |||
| 30e757b050 | |||
| af8dd42830 | |||
| 8ad0025e32 | |||
| 04ffc30e92 | |||
| acade360a8 | |||
| fdfc6c9756 |
|
|
@ -1,15 +1,3 @@
|
||||||
FROM mcr.microsoft.com/devcontainers/ruby:1-3.1-bullseye
|
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
|
||||||
|
ARG RUBY_VERSION=3.4.5
|
||||||
# Default value to allow debug server to serve content over GitHub Codespace's port forwarding service
|
FROM ghcr.io/rails/devcontainer/images/ruby:$RUBY_VERSION
|
||||||
# The value is a comma-separated list of allowed domains
|
|
||||||
ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev,.preview.app.github.dev,.app.github.dev"
|
|
||||||
|
|
||||||
# [Optional] Uncomment this section to install additional OS packages.
|
|
||||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|
||||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
|
||||||
|
|
||||||
# [Optional] Uncomment this line to install additional gems.
|
|
||||||
# RUN gem install <your-gem-names-here>
|
|
||||||
|
|
||||||
# [Optional] Uncomment this line to install global node packages.
|
|
||||||
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
version: '3'
|
name: "openneo_impress_items"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
rails-app:
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: .devcontainer/Dockerfile
|
dockerfile: .devcontainer/Dockerfile
|
||||||
|
|
@ -12,18 +12,26 @@ services:
|
||||||
# Overrides default command so things don't shut down after the process ends.
|
# Overrides default command so things don't shut down after the process ends.
|
||||||
command: sleep infinity
|
command: sleep infinity
|
||||||
|
|
||||||
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
# Uncomment the next line to use a non-root user for all processes.
|
||||||
network_mode: service:db
|
# user: vscode
|
||||||
|
|
||||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
|
||||||
db:
|
|
||||||
image: mysql:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- ./create-db.sql:/docker-entrypoint-initdb.d/create-db.sql
|
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: impress_dev
|
DB_USER: root
|
||||||
MYSQL_USER: impress_dev
|
|
||||||
MYSQL_PASSWORD: impress_dev
|
mysql:
|
||||||
|
image: mariadb:10.6
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 'true'
|
||||||
|
volumes:
|
||||||
|
- mysql-data:/var/lib/mysql
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql-data:
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
CREATE DATABASE openneo_impress;
|
|
||||||
GRANT ALL PRIVILEGES ON openneo_impress.* TO impress_dev;
|
|
||||||
|
|
||||||
CREATE DATABASE openneo_id;
|
|
||||||
GRANT ALL PRIVILEGES ON openneo_id.* TO impress_dev;
|
|
||||||
|
|
@ -1,46 +1,36 @@
|
||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
// For format details, see https://containers.dev/implementors/json_reference/.
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/ruby-rails-postgres
|
// For config options, see the README at: https://github.com/devcontainers/templates/tree/main/src/ruby
|
||||||
{
|
{
|
||||||
"name": "Dress to Impress",
|
"name": "openneo_impress_items",
|
||||||
"dockerComposeFile": "docker-compose.yml",
|
"dockerComposeFile": "compose.yaml",
|
||||||
"service": "app",
|
"service": "rails-app",
|
||||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
|
||||||
"nodeGypDependencies": true,
|
|
||||||
"version": "lts"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
// "features": {},
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {},
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
"ghcr.io/rails/devcontainer/features/mysql-client": {},
|
||||||
// This can be used to network with other containers or the host.
|
"ghcr.io/devcontainers-extra/features/ansible:2": {}
|
||||||
"forwardPorts": [3000],
|
},
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
|
||||||
"postCreateCommand": ".devcontainer/post-create.sh",
|
|
||||||
|
|
||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
// Because the database is hosted on the local network at the hostname `db`,
|
"DB_HOST": "mysql"
|
||||||
// we partially override `config/database.yml` to connect to `db`!
|
},
|
||||||
"DATABASE_URL_PRIMARY_DEV": "mysql2://db",
|
|
||||||
"DATABASE_URL_OPENNEO_ID_DEV": "mysql2://db",
|
|
||||||
"DATABASE_URL_PRIMARY_TEST": "mysql2://db",
|
|
||||||
"DATABASE_URL_OPENNEO_ID_TEST": "mysql2://db",
|
|
||||||
|
|
||||||
// HACK: Out of the box, this dev container doesn't allow installation to
|
"remoteEnv": {
|
||||||
// the default GEM_HOME, because of a weird thing going on with RVM.
|
"IMPRESS_DEPLOY_USER": "${localEnv:USER}"
|
||||||
// Instead, we set a custom GEM_HOME and GEM_PATH in our home directory!
|
},
|
||||||
// https://github.com/devcontainers/templates/issues/188
|
|
||||||
"GEM_HOME": "~/.rubygems",
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
"GEM_PATH": "~/.rubygems"
|
"forwardPorts": [3000],
|
||||||
}
|
|
||||||
|
|
||||||
// Configure tool-specific properties.
|
// Configure tool-specific properties.
|
||||||
// "customizations": {},
|
// "customizations": {},
|
||||||
|
|
||||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
// Uncomment to connect as root instead. More info: https://containers.dev/implementors/json_reference/#remoteUser.
|
||||||
// "remoteUser": "root"
|
// "remoteUser": "root",
|
||||||
|
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
"postCreateCommand": "bash .devcontainer/setup-ssh-config.sh && bin/setup --skip-server"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -e # Quit if any part of this script fails.
|
|
||||||
|
|
||||||
# Mark all git repositories as safe to execute, including cached gems.
|
|
||||||
# NOTE: This would be dangerous to run on a normal multi-user machine,
|
|
||||||
# but for a dev container that only we use, it should be fine!
|
|
||||||
git config --global safe.directory '*'
|
|
||||||
|
|
||||||
# Install the app's Ruby gem dependencies.
|
|
||||||
bundle install
|
|
||||||
|
|
||||||
# Set up the databases: create the schema, and load in some default data.
|
|
||||||
bin/rails db:schema:load db:seed
|
|
||||||
|
|
||||||
# Install the app's JS dependencies.
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
# Run a first-time build of the app's JS, in development mode.
|
|
||||||
yarn build:dev
|
|
||||||
28
.devcontainer/setup-ssh-config.sh
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Creates SSH config for devcontainer to use host's SSH identity
|
||||||
|
# This allows `ssh impress.openneo.net` to work without hardcoding usernames
|
||||||
|
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
chmod 700 ~/.ssh
|
||||||
|
|
||||||
|
# Only create SSH config if IMPRESS_DEPLOY_USER is explicitly set
|
||||||
|
if [ -z "$IMPRESS_DEPLOY_USER" ]; then
|
||||||
|
echo "⚠️ IMPRESS_DEPLOY_USER not set - skipping SSH config creation."
|
||||||
|
echo " This should be automatically set from your host \$USER environment variable."
|
||||||
|
echo " See docs/deployment-setup.md for details."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > ~/.ssh/config <<EOF
|
||||||
|
# Deployment server config
|
||||||
|
# Username: ${IMPRESS_DEPLOY_USER}
|
||||||
|
Host impress.openneo.net
|
||||||
|
User ${IMPRESS_DEPLOY_USER}
|
||||||
|
ForwardAgent yes
|
||||||
|
|
||||||
|
# Add other host configurations as needed
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 600 ~/.ssh/config
|
||||||
|
|
||||||
|
echo "✓ SSH config created. Deployment username: ${IMPRESS_DEPLOY_USER}"
|
||||||
2
.gitignore
vendored
|
|
@ -4,6 +4,8 @@ log/*.log
|
||||||
tmp/**/*
|
tmp/**/*
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
/spec/examples.txt
|
||||||
|
/.yardoc
|
||||||
|
|
||||||
/app/assets/builds/*
|
/app/assets/builds/*
|
||||||
!/app/assets/builds/.keep
|
!/app/assets/builds/.keep
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
yarn lint --max-warnings=0 --fix
|
# Run the linter, and all our tests.
|
||||||
|
yarn lint --max-warnings=0 --fix && bin/rake test spec
|
||||||
|
|
|
||||||
1
.rspec
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
--require spec_helper
|
||||||
|
|
@ -1 +1 @@
|
||||||
3.3.0
|
3.4.5
|
||||||
|
|
|
||||||
43
Gemfile
|
|
@ -1,10 +1,10 @@
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
ruby '3.3.0'
|
ruby '3.4.5'
|
||||||
|
|
||||||
gem 'rails', '~> 7.1', '>= 7.1.3.4'
|
gem 'rails', '~> 8.0', '>= 8.0.1'
|
||||||
|
|
||||||
# The HTTP server running the Rails instance.
|
# The HTTP server running the Rails instance.
|
||||||
gem 'falcon', '~> 0.43.0'
|
gem 'falcon', '~> 0.48.0'
|
||||||
|
|
||||||
# Our database is MySQL, in both development and production.
|
# Our database is MySQL, in both development and production.
|
||||||
gem 'mysql2', '~> 0.5.5'
|
gem 'mysql2', '~> 0.5.5'
|
||||||
|
|
@ -19,7 +19,7 @@ gem 'haml', '~> 6.1', '>= 6.1.1'
|
||||||
gem 'sass-rails', '~> 6.0'
|
gem 'sass-rails', '~> 6.0'
|
||||||
gem 'terser', '~> 1.1', '>= 1.1.17'
|
gem 'terser', '~> 1.1', '>= 1.1.17'
|
||||||
gem 'react-rails', '~> 2.7', '>= 2.7.1'
|
gem 'react-rails', '~> 2.7', '>= 2.7.1'
|
||||||
gem 'jsbundling-rails', '~> 1.1'
|
gem 'jsbundling-rails', '~> 1.3'
|
||||||
gem 'turbo-rails', '~> 2.0'
|
gem 'turbo-rails', '~> 2.0'
|
||||||
|
|
||||||
# For authentication.
|
# For authentication.
|
||||||
|
|
@ -33,7 +33,7 @@ gem "omniauth_openid_connect", "~> 0.7.1"
|
||||||
gem 'will_paginate', '~> 4.0'
|
gem 'will_paginate', '~> 4.0'
|
||||||
|
|
||||||
# For translation, both for the site UI and for Neopets data.
|
# For translation, both for the site UI and for Neopets data.
|
||||||
gem 'rails-i18n', '~> 7.0', '>= 7.0.7'
|
gem 'rails-i18n', '~> 8.0', '>= 8.0.1'
|
||||||
gem 'http_accept_language', '~> 2.1', '>= 2.1.1'
|
gem 'http_accept_language', '~> 2.1', '>= 2.1.1'
|
||||||
|
|
||||||
# For reading and parsing HTML from Neopets.com, like importing Closet pages.
|
# For reading and parsing HTML from Neopets.com, like importing Closet pages.
|
||||||
|
|
@ -45,7 +45,8 @@ gem 'sanitize', '~> 6.0', '>= 6.0.2'
|
||||||
|
|
||||||
# For working with Neopets APIs.
|
# For working with Neopets APIs.
|
||||||
# unstable version of RocketAMF interprets info registry as a hash instead of an array
|
# unstable version of RocketAMF interprets info registry as a hash instead of an array
|
||||||
gem 'RocketAMF', :git => 'https://github.com/rubyamf/rocketamf.git'
|
# Vendored version with Ruby 3.4 ARM compatibility fixes (see vendor/gems/README-RocketAMF.md)
|
||||||
|
gem 'RocketAMF', path: 'vendor/gems/RocketAMF-1.0.0'
|
||||||
|
|
||||||
# For preventing too many modeling attempts.
|
# For preventing too many modeling attempts.
|
||||||
gem 'rack-attack', '~> 6.7'
|
gem 'rack-attack', '~> 6.7'
|
||||||
|
|
@ -53,23 +54,19 @@ gem 'rack-attack', '~> 6.7'
|
||||||
# For testing emails in development.
|
# For testing emails in development.
|
||||||
gem 'letter_opener', '~> 1.8', '>= 1.8.1', group: :development
|
gem 'letter_opener', '~> 1.8', '>= 1.8.1', group: :development
|
||||||
|
|
||||||
# For parallel API calls.
|
|
||||||
gem 'parallel', '~> 1.23'
|
|
||||||
|
|
||||||
# For miscellaneous HTTP requests.
|
# For miscellaneous HTTP requests.
|
||||||
gem "httparty", "~> 0.21.0"
|
|
||||||
gem "addressable", "~> 2.8"
|
gem "addressable", "~> 2.8"
|
||||||
|
|
||||||
# For advanced batching of many HTTP requests.
|
# For advanced batching of many HTTP requests.
|
||||||
gem "async", "~> 2.6", require: false
|
gem "async", "~> 2.17", require: false
|
||||||
gem "async-http", "~> 0.61.0", require: false
|
gem "async-http", "~> 0.89.0", require: false
|
||||||
gem "thread-local", "~> 1.1", require: false
|
gem "thread-local", "~> 1.1", require: false
|
||||||
|
|
||||||
# For debugging.
|
# For debugging.
|
||||||
gem 'web-console', '~> 4.2', group: :development
|
group :development do
|
||||||
|
gem 'debug', '~> 1.9.2'
|
||||||
# TODO: Review our use of content_tag_for etc and uninstall this!
|
gem 'web-console', '~> 4.2'
|
||||||
gem 'record_tag_helper', '~> 1.0', '>= 1.0.1'
|
end
|
||||||
|
|
||||||
# Reduces boot times through caching; required in config/boot.rb
|
# Reduces boot times through caching; required in config/boot.rb
|
||||||
gem 'bootsnap', '~> 1.16', require: false
|
gem 'bootsnap', '~> 1.16', require: false
|
||||||
|
|
@ -87,5 +84,15 @@ gem "sentry-rails", "~> 5.12"
|
||||||
gem "shell", "~> 0.8.1"
|
gem "shell", "~> 0.8.1"
|
||||||
|
|
||||||
# For workspace autocomplete.
|
# For workspace autocomplete.
|
||||||
gem "solargraph", "~> 0.50.0", group: :development
|
group :development do
|
||||||
gem "solargraph-rails", "~> 1.1", group: :development
|
gem "solargraph", "~> 0.50.0"
|
||||||
|
gem "solargraph-rails", "~> 1.1"
|
||||||
|
end
|
||||||
|
|
||||||
|
# For automated tests.
|
||||||
|
group :development, :test do
|
||||||
|
gem "rspec-rails", "~> 7.0"
|
||||||
|
end
|
||||||
|
group :test do
|
||||||
|
gem "webmock", "~> 3.24"
|
||||||
|
end
|
||||||
|
|
|
||||||
567
Gemfile.lock
|
|
@ -1,137 +1,147 @@
|
||||||
GIT
|
PATH
|
||||||
remote: https://github.com/rubyamf/rocketamf.git
|
remote: vendor/gems/RocketAMF-1.0.0
|
||||||
revision: 796f591d002b5cf47df436dbcbd6f2ab00e869ed
|
|
||||||
specs:
|
specs:
|
||||||
RocketAMF (1.0.0)
|
RocketAMF (1.0.0.dti1)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (7.1.3.4)
|
action_text-trix (2.1.15)
|
||||||
actionpack (= 7.1.3.4)
|
railties
|
||||||
activesupport (= 7.1.3.4)
|
actioncable (8.1.1)
|
||||||
|
actionpack (= 8.1.1)
|
||||||
|
activesupport (= 8.1.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
actionmailbox (7.1.3.4)
|
actionmailbox (8.1.1)
|
||||||
actionpack (= 7.1.3.4)
|
actionpack (= 8.1.1)
|
||||||
activejob (= 7.1.3.4)
|
activejob (= 8.1.1)
|
||||||
activerecord (= 7.1.3.4)
|
activerecord (= 8.1.1)
|
||||||
activestorage (= 7.1.3.4)
|
activestorage (= 8.1.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.1.1)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.8.0)
|
||||||
net-imap
|
actionmailer (8.1.1)
|
||||||
net-pop
|
actionpack (= 8.1.1)
|
||||||
net-smtp
|
actionview (= 8.1.1)
|
||||||
actionmailer (7.1.3.4)
|
activejob (= 8.1.1)
|
||||||
actionpack (= 7.1.3.4)
|
activesupport (= 8.1.1)
|
||||||
actionview (= 7.1.3.4)
|
mail (>= 2.8.0)
|
||||||
activejob (= 7.1.3.4)
|
|
||||||
activesupport (= 7.1.3.4)
|
|
||||||
mail (~> 2.5, >= 2.5.4)
|
|
||||||
net-imap
|
|
||||||
net-pop
|
|
||||||
net-smtp
|
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (7.1.3.4)
|
actionpack (8.1.1)
|
||||||
actionview (= 7.1.3.4)
|
actionview (= 8.1.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.1.1)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
racc
|
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
rack-session (>= 1.0.1)
|
rack-session (>= 1.0.1)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
actiontext (7.1.3.4)
|
useragent (~> 0.16)
|
||||||
actionpack (= 7.1.3.4)
|
actiontext (8.1.1)
|
||||||
activerecord (= 7.1.3.4)
|
action_text-trix (~> 2.1.15)
|
||||||
activestorage (= 7.1.3.4)
|
actionpack (= 8.1.1)
|
||||||
activesupport (= 7.1.3.4)
|
activerecord (= 8.1.1)
|
||||||
|
activestorage (= 8.1.1)
|
||||||
|
activesupport (= 8.1.1)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.1.3.4)
|
actionview (8.1.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.1.1)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.11)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
activejob (7.1.3.4)
|
activejob (8.1.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.1.1)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.1.3.4)
|
activemodel (8.1.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.1.1)
|
||||||
activerecord (7.1.3.4)
|
activerecord (8.1.1)
|
||||||
activemodel (= 7.1.3.4)
|
activemodel (= 8.1.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.1.1)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (7.1.3.4)
|
activestorage (8.1.1)
|
||||||
actionpack (= 7.1.3.4)
|
actionpack (= 8.1.1)
|
||||||
activejob (= 7.1.3.4)
|
activejob (= 8.1.1)
|
||||||
activerecord (= 7.1.3.4)
|
activerecord (= 8.1.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.1.1)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (7.1.3.4)
|
activesupport (8.1.1)
|
||||||
base64
|
base64
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||||
connection_pool (>= 2.2.5)
|
connection_pool (>= 2.2.5)
|
||||||
drb
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
|
json
|
||||||
|
logger (>= 1.4.2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
mutex_m
|
securerandom (>= 0.3)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0, >= 2.0.5)
|
||||||
addressable (2.8.6)
|
uri (>= 0.13.1)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
addressable (2.8.7)
|
||||||
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
aes_key_wrap (1.1.0)
|
aes_key_wrap (1.1.0)
|
||||||
ast (2.4.2)
|
ast (2.4.3)
|
||||||
async (2.8.1)
|
async (2.34.0)
|
||||||
console (~> 1.10)
|
console (~> 1.29)
|
||||||
fiber-annotation
|
fiber-annotation
|
||||||
io-event (~> 1.1)
|
io-event (~> 1.11)
|
||||||
timers (~> 4.1)
|
metrics (~> 0.12)
|
||||||
async-container (0.16.13)
|
traces (~> 0.18)
|
||||||
async
|
async-container (0.27.7)
|
||||||
async-io
|
async (~> 2.22)
|
||||||
async-http (0.61.0)
|
async-http (0.89.0)
|
||||||
async (>= 1.25)
|
async (>= 2.10.2)
|
||||||
async-io (>= 1.28)
|
async-pool (~> 0.9)
|
||||||
async-pool (>= 0.2)
|
io-endpoint (~> 0.14)
|
||||||
protocol-http (~> 0.25.0)
|
io-stream (~> 0.6)
|
||||||
protocol-http1 (~> 0.16.0)
|
metrics (~> 0.12)
|
||||||
protocol-http2 (~> 0.15.0)
|
protocol-http (~> 0.49)
|
||||||
traces (>= 0.10.0)
|
protocol-http1 (~> 0.30)
|
||||||
async-http-cache (0.4.3)
|
protocol-http2 (~> 0.22)
|
||||||
|
traces (~> 0.10)
|
||||||
|
async-http-cache (0.4.6)
|
||||||
async-http (~> 0.56)
|
async-http (~> 0.56)
|
||||||
async-io (1.41.0)
|
async-pool (0.11.0)
|
||||||
|
async (>= 2.0)
|
||||||
|
async-service (0.14.4)
|
||||||
async
|
async
|
||||||
async-pool (0.4.0)
|
async-container (~> 0.16)
|
||||||
async (>= 1.25)
|
string-format (~> 0.2)
|
||||||
attr_required (1.0.2)
|
attr_required (1.0.2)
|
||||||
babel-source (5.8.35)
|
babel-source (5.8.35)
|
||||||
babel-transpiler (0.7.0)
|
babel-transpiler (0.7.0)
|
||||||
babel-source (>= 4.0, < 6)
|
babel-source (>= 4.0, < 6)
|
||||||
execjs (~> 2.0)
|
execjs (~> 2.0)
|
||||||
backport (1.2.0)
|
backport (1.2.0)
|
||||||
base64 (0.2.0)
|
base64 (0.3.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
benchmark (0.3.0)
|
benchmark (0.5.0)
|
||||||
bigdecimal (3.1.6)
|
bigdecimal (3.3.1)
|
||||||
bindata (2.5.0)
|
bindata (2.5.1)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.18.3)
|
bootsnap (1.18.6)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
build-environment (1.13.0)
|
builder (3.3.0)
|
||||||
builder (3.2.4)
|
childprocess (5.1.0)
|
||||||
concurrent-ruby (1.2.3)
|
logger (~> 1.5)
|
||||||
connection_pool (2.4.1)
|
concurrent-ruby (1.3.5)
|
||||||
console (1.23.4)
|
connection_pool (2.5.4)
|
||||||
|
console (1.34.2)
|
||||||
fiber-annotation
|
fiber-annotation
|
||||||
fiber-local
|
fiber-local (~> 1.1)
|
||||||
json
|
json
|
||||||
|
crack (1.0.1)
|
||||||
|
bigdecimal
|
||||||
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
date (3.3.4)
|
date (3.5.0)
|
||||||
devise (4.9.3)
|
debug (1.9.2)
|
||||||
|
irb (~> 1.10)
|
||||||
|
reline (>= 0.3.8)
|
||||||
|
devise (4.9.4)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
|
|
@ -139,123 +149,134 @@ GEM
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-encryptable (0.2.0)
|
devise-encryptable (0.2.0)
|
||||||
devise (>= 2.1.0)
|
devise (>= 2.1.0)
|
||||||
diff-lcs (1.5.1)
|
diff-lcs (1.6.2)
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
dotenv-rails (2.8.1)
|
dotenv-rails (2.8.1)
|
||||||
dotenv (= 2.8.1)
|
dotenv (= 2.8.1)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
drb (2.2.0)
|
drb (2.2.3)
|
||||||
ruby2_keywords
|
|
||||||
e2mmap (0.1.0)
|
e2mmap (0.1.0)
|
||||||
email_validator (2.2.4)
|
email_validator (2.2.4)
|
||||||
activemodel
|
activemodel
|
||||||
erubi (1.12.0)
|
erb (5.1.3)
|
||||||
execjs (2.9.1)
|
erubi (1.13.1)
|
||||||
falcon (0.43.0)
|
execjs (2.10.0)
|
||||||
|
falcon (0.48.6)
|
||||||
async
|
async
|
||||||
async-container (~> 0.16.0)
|
async-container (~> 0.18)
|
||||||
async-http (~> 0.57)
|
async-http (~> 0.75)
|
||||||
async-http-cache (~> 0.4.0)
|
async-http-cache (~> 0.4)
|
||||||
async-io (~> 1.22)
|
async-service (~> 0.10)
|
||||||
build-environment (~> 1.13)
|
|
||||||
bundler
|
bundler
|
||||||
localhost (~> 1.1)
|
localhost (~> 1.1)
|
||||||
openssl (~> 3.0)
|
openssl (~> 3.0)
|
||||||
process-metrics (~> 0.2.0)
|
process-metrics (~> 0.2)
|
||||||
protocol-rack (~> 0.1)
|
protocol-http (~> 0.31)
|
||||||
samovar (~> 2.1)
|
protocol-rack (~> 0.7)
|
||||||
faraday (2.9.0)
|
samovar (~> 2.3)
|
||||||
faraday-net_http (>= 2.0, < 3.2)
|
faraday (2.14.0)
|
||||||
faraday-follow_redirects (0.3.0)
|
faraday-net_http (>= 2.0, < 3.5)
|
||||||
|
json
|
||||||
|
logger
|
||||||
|
faraday-follow_redirects (0.4.0)
|
||||||
faraday (>= 1, < 3)
|
faraday (>= 1, < 3)
|
||||||
faraday-net_http (3.1.0)
|
faraday-net_http (3.4.2)
|
||||||
net-http
|
net-http (~> 0.5)
|
||||||
ffi (1.16.3)
|
ffi (1.17.2)
|
||||||
fiber-annotation (0.2.0)
|
fiber-annotation (0.2.0)
|
||||||
fiber-local (1.0.0)
|
fiber-local (1.1.0)
|
||||||
globalid (1.2.1)
|
fiber-storage
|
||||||
|
fiber-storage (1.0.1)
|
||||||
|
globalid (1.3.0)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
haml (6.3.0)
|
haml (6.4.0)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
thor
|
thor
|
||||||
tilt
|
tilt
|
||||||
|
hashdiff (1.2.1)
|
||||||
hashie (5.0.0)
|
hashie (5.0.0)
|
||||||
http_accept_language (2.1.1)
|
http_accept_language (2.1.1)
|
||||||
httparty (0.21.0)
|
i18n (1.14.7)
|
||||||
mini_mime (>= 1.0.0)
|
|
||||||
multi_xml (>= 0.5.2)
|
|
||||||
i18n (1.14.1)
|
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
io-console (0.7.2)
|
io-console (0.8.1)
|
||||||
io-event (1.4.4)
|
io-endpoint (0.15.2)
|
||||||
irb (1.11.2)
|
io-event (1.14.0)
|
||||||
rdoc
|
io-stream (0.11.0)
|
||||||
|
irb (1.15.3)
|
||||||
|
pp (>= 0.6.0)
|
||||||
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
jaro_winkler (1.6.0)
|
jaro_winkler (1.6.1)
|
||||||
jsbundling-rails (1.3.0)
|
jsbundling-rails (1.3.1)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
json (2.7.1)
|
json (2.16.0)
|
||||||
json-jwt (1.16.6)
|
json-jwt (1.17.0)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
aes_key_wrap
|
aes_key_wrap
|
||||||
base64
|
base64
|
||||||
bindata
|
bindata
|
||||||
faraday (~> 2.0)
|
faraday (~> 2.0)
|
||||||
faraday-follow_redirects
|
faraday-follow_redirects
|
||||||
kramdown (2.4.0)
|
kramdown (2.5.1)
|
||||||
rexml
|
rexml (>= 3.3.9)
|
||||||
kramdown-parser-gfm (1.1.0)
|
kramdown-parser-gfm (1.1.0)
|
||||||
kramdown (~> 2.0)
|
kramdown (~> 2.0)
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.5)
|
||||||
launchy (2.5.2)
|
launchy (3.1.1)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
letter_opener (1.9.0)
|
childprocess (~> 5.0)
|
||||||
launchy (>= 2.2, < 3)
|
logger (~> 1.6)
|
||||||
localhost (1.2.0)
|
letter_opener (1.10.0)
|
||||||
loofah (2.22.0)
|
launchy (>= 2.2, < 4)
|
||||||
|
lint_roller (1.1.0)
|
||||||
|
localhost (1.6.0)
|
||||||
|
logger (1.7.0)
|
||||||
|
loofah (2.24.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.9.0)
|
||||||
|
logger
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
mapping (1.1.1)
|
mapping (1.1.3)
|
||||||
marcel (1.0.4)
|
marcel (1.1.0)
|
||||||
memory_profiler (1.0.1)
|
memory_profiler (1.1.0)
|
||||||
|
metrics (0.15.0)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.5)
|
mini_portile2 (2.8.9)
|
||||||
minitest (5.22.2)
|
minitest (5.26.1)
|
||||||
msgpack (1.7.2)
|
msgpack (1.8.0)
|
||||||
multi_xml (0.6.0)
|
mysql2 (0.5.7)
|
||||||
mutex_m (0.2.0)
|
bigdecimal
|
||||||
mysql2 (0.5.6)
|
net-http (0.7.0)
|
||||||
net-http (0.4.1)
|
|
||||||
uri
|
uri
|
||||||
net-imap (0.4.10)
|
net-imap (0.5.12)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.2)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.4.0.1)
|
net-smtp (0.5.1)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.3)
|
nio4r (2.7.5)
|
||||||
nokogiri (1.16.2)
|
nokogiri (1.18.10)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
omniauth (2.1.2)
|
omniauth (2.1.4)
|
||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
|
logger
|
||||||
rack (>= 2.2.3)
|
rack (>= 2.2.3)
|
||||||
rack-protection
|
rack-protection
|
||||||
omniauth-rails_csrf_protection (1.0.1)
|
omniauth-rails_csrf_protection (1.0.2)
|
||||||
actionpack (>= 4.2)
|
actionpack (>= 4.2)
|
||||||
omniauth (~> 2.0)
|
omniauth (~> 2.0)
|
||||||
omniauth_openid_connect (0.7.1)
|
omniauth_openid_connect (0.7.1)
|
||||||
omniauth (>= 1.9, < 3)
|
omniauth (>= 1.9, < 3)
|
||||||
openid_connect (~> 2.2)
|
openid_connect (~> 2.2)
|
||||||
openid_connect (2.3.0)
|
openid_connect (2.3.1)
|
||||||
activemodel
|
activemodel
|
||||||
attr_required (>= 1.0.0)
|
attr_required (>= 1.0.0)
|
||||||
email_validator
|
email_validator
|
||||||
|
|
@ -268,126 +289,151 @@ GEM
|
||||||
tzinfo
|
tzinfo
|
||||||
validate_url
|
validate_url
|
||||||
webfinger (~> 2.0)
|
webfinger (~> 2.0)
|
||||||
openssl (3.2.0)
|
openssl (3.3.2)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
parallel (1.24.0)
|
parallel (1.27.0)
|
||||||
parser (3.3.3.0)
|
parser (3.3.10.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
process-metrics (0.2.1)
|
pp (0.6.3)
|
||||||
|
prettyprint
|
||||||
|
prettyprint (0.2.0)
|
||||||
|
prism (1.6.0)
|
||||||
|
process-metrics (0.7.0)
|
||||||
console (~> 1.8)
|
console (~> 1.8)
|
||||||
|
json (~> 2)
|
||||||
samovar (~> 2.1)
|
samovar (~> 2.1)
|
||||||
protocol-hpack (1.4.2)
|
protocol-hpack (1.5.1)
|
||||||
protocol-http (0.25.0)
|
protocol-http (0.55.0)
|
||||||
protocol-http1 (0.16.1)
|
protocol-http1 (0.35.2)
|
||||||
protocol-http (~> 0.22)
|
protocol-http (~> 0.22)
|
||||||
protocol-http2 (0.15.1)
|
protocol-http2 (0.23.0)
|
||||||
protocol-hpack (~> 1.4)
|
protocol-hpack (~> 1.4)
|
||||||
protocol-http (~> 0.18)
|
protocol-http (~> 0.47)
|
||||||
protocol-rack (0.4.1)
|
protocol-rack (0.16.0)
|
||||||
protocol-http (~> 0.23)
|
io-stream (>= 0.10)
|
||||||
|
protocol-http (~> 0.43)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
psych (5.1.2)
|
psych (5.2.6)
|
||||||
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (5.0.4)
|
public_suffix (6.0.2)
|
||||||
racc (1.7.3)
|
racc (1.8.1)
|
||||||
rack (3.0.9.1)
|
rack (3.2.4)
|
||||||
rack-attack (6.7.0)
|
rack-attack (6.8.0)
|
||||||
rack (>= 1.0, < 4)
|
rack (>= 1.0, < 4)
|
||||||
rack-mini-profiler (3.3.1)
|
rack-mini-profiler (3.3.1)
|
||||||
rack (>= 1.2.0)
|
rack (>= 1.2.0)
|
||||||
rack-oauth2 (2.2.1)
|
rack-oauth2 (2.3.0)
|
||||||
activesupport
|
activesupport
|
||||||
attr_required
|
attr_required
|
||||||
faraday (~> 2.0)
|
faraday (~> 2.0)
|
||||||
faraday-follow_redirects
|
faraday-follow_redirects
|
||||||
json-jwt (>= 1.11.0)
|
json-jwt (>= 1.11.0)
|
||||||
rack (>= 2.1.0)
|
rack (>= 2.1.0)
|
||||||
rack-protection (4.0.0)
|
rack-protection (4.2.1)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
|
logger (>= 1.6.0)
|
||||||
rack (>= 3.0.0, < 4)
|
rack (>= 3.0.0, < 4)
|
||||||
rack-session (2.0.0)
|
rack-session (2.1.1)
|
||||||
|
base64 (>= 0.1.0)
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
rack-test (2.1.0)
|
rack-test (2.2.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rackup (2.1.0)
|
rackup (2.2.1)
|
||||||
rack (>= 3)
|
rack (>= 3)
|
||||||
webrick (~> 1.8)
|
rails (8.1.1)
|
||||||
rails (7.1.3.4)
|
actioncable (= 8.1.1)
|
||||||
actioncable (= 7.1.3.4)
|
actionmailbox (= 8.1.1)
|
||||||
actionmailbox (= 7.1.3.4)
|
actionmailer (= 8.1.1)
|
||||||
actionmailer (= 7.1.3.4)
|
actionpack (= 8.1.1)
|
||||||
actionpack (= 7.1.3.4)
|
actiontext (= 8.1.1)
|
||||||
actiontext (= 7.1.3.4)
|
actionview (= 8.1.1)
|
||||||
actionview (= 7.1.3.4)
|
activejob (= 8.1.1)
|
||||||
activejob (= 7.1.3.4)
|
activemodel (= 8.1.1)
|
||||||
activemodel (= 7.1.3.4)
|
activerecord (= 8.1.1)
|
||||||
activerecord (= 7.1.3.4)
|
activestorage (= 8.1.1)
|
||||||
activestorage (= 7.1.3.4)
|
activesupport (= 8.1.1)
|
||||||
activesupport (= 7.1.3.4)
|
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.1.3.4)
|
railties (= 8.1.1)
|
||||||
rails-dom-testing (2.2.0)
|
rails-dom-testing (2.3.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.6.0)
|
rails-html-sanitizer (1.6.2)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
nokogiri (~> 1.14)
|
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 (7.0.8)
|
rails-i18n (8.0.2)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 8.0.0, < 9)
|
||||||
railties (7.1.3.4)
|
railties (8.1.1)
|
||||||
actionpack (= 7.1.3.4)
|
actionpack (= 8.1.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.1.1)
|
||||||
irb
|
irb (~> 1.13)
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0, >= 1.2.2)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
|
tsort (>= 0.2)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.1.0)
|
rake (13.3.1)
|
||||||
rbs (2.8.4)
|
rbs (2.8.4)
|
||||||
rdiscount (2.2.7.3)
|
rdiscount (2.2.7.3)
|
||||||
rdoc (6.6.2)
|
rdoc (6.15.1)
|
||||||
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
|
tsort
|
||||||
react-rails (2.7.1)
|
react-rails (2.7.1)
|
||||||
babel-transpiler (>= 0.7.0)
|
babel-transpiler (>= 0.7.0)
|
||||||
connection_pool
|
connection_pool
|
||||||
execjs
|
execjs
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
tilt
|
tilt
|
||||||
record_tag_helper (1.0.1)
|
regexp_parser (2.11.3)
|
||||||
actionview (>= 5)
|
reline (0.6.3)
|
||||||
regexp_parser (2.9.2)
|
|
||||||
reline (0.4.2)
|
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
responders (3.1.1)
|
responders (3.2.0)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 7.0)
|
||||||
railties (>= 5.2)
|
railties (>= 7.0)
|
||||||
reverse_markdown (2.1.1)
|
reverse_markdown (2.1.1)
|
||||||
nokogiri
|
nokogiri
|
||||||
rexml (3.3.1)
|
rexml (3.4.4)
|
||||||
strscan
|
rspec-core (3.13.6)
|
||||||
rubocop (1.64.1)
|
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.7)
|
||||||
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-rails (7.1.1)
|
||||||
|
actionpack (>= 7.0)
|
||||||
|
activesupport (>= 7.0)
|
||||||
|
railties (>= 7.0)
|
||||||
|
rspec-core (~> 3.13)
|
||||||
|
rspec-expectations (~> 3.13)
|
||||||
|
rspec-mocks (~> 3.13)
|
||||||
|
rspec-support (~> 3.13)
|
||||||
|
rspec-support (3.13.6)
|
||||||
|
rubocop (1.81.7)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
|
lint_roller (~> 1.1.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.3.0.2)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 2.9.3, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rubocop-ast (>= 1.47.1, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.31.3)
|
rubocop-ast (1.48.0)
|
||||||
parser (>= 3.3.1.0)
|
parser (>= 3.3.7.2)
|
||||||
|
prism (~> 1.4)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby2_keywords (0.0.5)
|
samovar (2.4.1)
|
||||||
samovar (2.2.0)
|
|
||||||
console (~> 1.0)
|
console (~> 1.0)
|
||||||
mapping (~> 1.0)
|
mapping (~> 1.0)
|
||||||
sanitize (6.1.0)
|
sanitize (6.1.3)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
sass-rails (6.0.0)
|
sass-rails (6.0.0)
|
||||||
|
|
@ -400,10 +446,12 @@ GEM
|
||||||
sprockets (> 3.0)
|
sprockets (> 3.0)
|
||||||
sprockets-rails
|
sprockets-rails
|
||||||
tilt
|
tilt
|
||||||
sentry-rails (5.16.1)
|
securerandom (0.4.1)
|
||||||
|
sentry-rails (5.28.1)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
sentry-ruby (~> 5.16.1)
|
sentry-ruby (~> 5.28.1)
|
||||||
sentry-ruby (5.16.1)
|
sentry-ruby (5.28.1)
|
||||||
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
shell (0.8.1)
|
shell (0.8.1)
|
||||||
e2mmap
|
e2mmap
|
||||||
|
|
@ -424,42 +472,45 @@ GEM
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
tilt (~> 2.0)
|
tilt (~> 2.0)
|
||||||
yard (~> 0.9, >= 0.9.24)
|
yard (~> 0.9, >= 0.9.24)
|
||||||
solargraph-rails (1.1.0)
|
solargraph-rails (1.2.4)
|
||||||
activesupport
|
activesupport
|
||||||
solargraph
|
solargraph (>= 0.48.0, <= 0.57)
|
||||||
sprockets (4.2.1)
|
sprockets (4.2.2)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
|
logger
|
||||||
rack (>= 2.2.4, < 4)
|
rack (>= 2.2.4, < 4)
|
||||||
sprockets-rails (3.4.2)
|
sprockets-rails (3.5.2)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 6.1)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 6.1)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
stackprof (0.2.26)
|
stackprof (0.2.27)
|
||||||
stringio (3.1.0)
|
string-format (0.2.0)
|
||||||
strscan (3.1.0)
|
stringio (3.1.7)
|
||||||
swd (2.0.3)
|
swd (2.0.3)
|
||||||
activesupport (>= 3)
|
activesupport (>= 3)
|
||||||
attr_required (>= 0.0.5)
|
attr_required (>= 0.0.5)
|
||||||
faraday (~> 2.0)
|
faraday (~> 2.0)
|
||||||
faraday-follow_redirects
|
faraday-follow_redirects
|
||||||
sync (0.5.0)
|
sync (0.5.0)
|
||||||
temple (0.10.3)
|
temple (0.10.4)
|
||||||
terser (1.2.0)
|
terser (1.2.6)
|
||||||
execjs (>= 0.3.0, < 3)
|
execjs (>= 0.3.0, < 3)
|
||||||
thor (1.3.0)
|
thor (1.4.0)
|
||||||
thread-local (1.1.0)
|
thread-local (1.1.0)
|
||||||
tilt (2.3.0)
|
tilt (2.6.1)
|
||||||
timeout (0.4.1)
|
timeout (0.4.4)
|
||||||
timers (4.3.5)
|
traces (0.18.2)
|
||||||
traces (0.11.1)
|
tsort (0.2.0)
|
||||||
turbo-rails (2.0.5)
|
turbo-rails (2.0.20)
|
||||||
actionpack (>= 6.0.0)
|
actionpack (>= 7.1.0)
|
||||||
activejob (>= 6.0.0)
|
railties (>= 7.1.0)
|
||||||
railties (>= 6.0.0)
|
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
unicode-display_width (2.5.0)
|
unicode-display_width (3.2.0)
|
||||||
uri (0.13.0)
|
unicode-emoji (~> 4.1)
|
||||||
|
unicode-emoji (4.1.0)
|
||||||
|
uri (1.1.1)
|
||||||
|
useragent (0.16.11)
|
||||||
validate_url (1.0.15)
|
validate_url (1.0.15)
|
||||||
activemodel (>= 3.0.0)
|
activemodel (>= 3.0.0)
|
||||||
public_suffix
|
public_suffix
|
||||||
|
|
@ -474,13 +525,17 @@ GEM
|
||||||
activesupport
|
activesupport
|
||||||
faraday (~> 2.0)
|
faraday (~> 2.0)
|
||||||
faraday-follow_redirects
|
faraday-follow_redirects
|
||||||
webrick (1.8.1)
|
webmock (3.26.1)
|
||||||
websocket-driver (0.7.6)
|
addressable (>= 2.8.0)
|
||||||
|
crack (>= 0.3.2)
|
||||||
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
|
websocket-driver (0.8.0)
|
||||||
|
base64
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
will_paginate (4.0.0)
|
will_paginate (4.0.1)
|
||||||
yard (0.9.36)
|
yard (0.9.37)
|
||||||
zeitwerk (2.6.13)
|
zeitwerk (2.7.3)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
|
@ -488,17 +543,17 @@ PLATFORMS
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
RocketAMF!
|
RocketAMF!
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
async (~> 2.6)
|
async (~> 2.17)
|
||||||
async-http (~> 0.61.0)
|
async-http (~> 0.89.0)
|
||||||
bootsnap (~> 1.16)
|
bootsnap (~> 1.16)
|
||||||
|
debug (~> 1.9.2)
|
||||||
devise (~> 4.9, >= 4.9.2)
|
devise (~> 4.9, >= 4.9.2)
|
||||||
devise-encryptable (~> 0.2.0)
|
devise-encryptable (~> 0.2.0)
|
||||||
dotenv-rails (~> 2.8, >= 2.8.1)
|
dotenv-rails (~> 2.8, >= 2.8.1)
|
||||||
falcon (~> 0.43.0)
|
falcon (~> 0.48.0)
|
||||||
haml (~> 6.1, >= 6.1.1)
|
haml (~> 6.1, >= 6.1.1)
|
||||||
http_accept_language (~> 2.1, >= 2.1.1)
|
http_accept_language (~> 2.1, >= 2.1.1)
|
||||||
httparty (~> 0.21.0)
|
jsbundling-rails (~> 1.3)
|
||||||
jsbundling-rails (~> 1.1)
|
|
||||||
letter_opener (~> 1.8, >= 1.8.1)
|
letter_opener (~> 1.8, >= 1.8.1)
|
||||||
memory_profiler (~> 1.0)
|
memory_profiler (~> 1.0)
|
||||||
mysql2 (~> 0.5.5)
|
mysql2 (~> 0.5.5)
|
||||||
|
|
@ -506,14 +561,13 @@ DEPENDENCIES
|
||||||
omniauth (~> 2.1)
|
omniauth (~> 2.1)
|
||||||
omniauth-rails_csrf_protection (~> 1.0)
|
omniauth-rails_csrf_protection (~> 1.0)
|
||||||
omniauth_openid_connect (~> 0.7.1)
|
omniauth_openid_connect (~> 0.7.1)
|
||||||
parallel (~> 1.23)
|
|
||||||
rack-attack (~> 6.7)
|
rack-attack (~> 6.7)
|
||||||
rack-mini-profiler (~> 3.1)
|
rack-mini-profiler (~> 3.1)
|
||||||
rails (~> 7.1, >= 7.1.3.4)
|
rails (~> 8.0, >= 8.0.1)
|
||||||
rails-i18n (~> 7.0, >= 7.0.7)
|
rails-i18n (~> 8.0, >= 8.0.1)
|
||||||
rdiscount (~> 2.2, >= 2.2.7.1)
|
rdiscount (~> 2.2, >= 2.2.7.1)
|
||||||
react-rails (~> 2.7, >= 2.7.1)
|
react-rails (~> 2.7, >= 2.7.1)
|
||||||
record_tag_helper (~> 1.0, >= 1.0.1)
|
rspec-rails (~> 7.0)
|
||||||
sanitize (~> 6.0, >= 6.0.2)
|
sanitize (~> 6.0, >= 6.0.2)
|
||||||
sass-rails (~> 6.0)
|
sass-rails (~> 6.0)
|
||||||
sentry-rails (~> 5.12)
|
sentry-rails (~> 5.12)
|
||||||
|
|
@ -527,10 +581,11 @@ DEPENDENCIES
|
||||||
thread-local (~> 1.1)
|
thread-local (~> 1.1)
|
||||||
turbo-rails (~> 2.0)
|
turbo-rails (~> 2.0)
|
||||||
web-console (~> 4.2)
|
web-console (~> 4.2)
|
||||||
|
webmock (~> 3.24)
|
||||||
will_paginate (~> 4.0)
|
will_paginate (~> 4.0)
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.3.0p0
|
ruby 3.4.5p51
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.5
|
2.7.1
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
web: unset PORT && env RUBY_DEBUG_OPEN=true bin/rails server
|
web: unset PORT && env RUBY_DEBUG_OPEN=true bin/rails server -b 0.0.0.0
|
||||||
js: yarn dev
|
js: yarn dev
|
||||||
|
|
|
||||||
161
README.md
|
|
@ -2,6 +2,163 @@
|
||||||
|
|
||||||
# Dress to Impress
|
# Dress to Impress
|
||||||
|
|
||||||
Oh! We've been revitalizing the Rails app! Fun!
|
Dress to Impress (DTI) is a tool for designing Neopets outfits. Load your pet, browse items, and see how they look together—all with a mobile-friendly interface!
|
||||||
|
|
||||||
There'll be more to say about it here soon :3
|
## Architecture Overview
|
||||||
|
|
||||||
|
DTI is a Rails application with a React-based outfit editor, backed by MySQL databases and a crowdsourced data collection system.
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
- **Rails backend** (Ruby 3.4, Rails 8.0): Serves web pages, API endpoints, and manages data
|
||||||
|
- **MySQL databases**: Primary database (`openneo_impress`) + legacy auth database (`openneo_id`)
|
||||||
|
- **React outfit editor**: Embedded in `app/javascript/wardrobe-2020/`, provides the main customization UI
|
||||||
|
- **Modeling system**: Crowdsources pet/item appearance data by fetching from Neopets APIs when users load their pets
|
||||||
|
|
||||||
|
### The Impress 2020 Complication
|
||||||
|
|
||||||
|
In 2020, we started a NextJS rewrite ("Impress 2020") to modernize the frontend. We've since consolidated back into Rails, but **Impress 2020 still provides essential services**:
|
||||||
|
|
||||||
|
- **GraphQL API**: Some outfit appearance data still loads via GraphQL (being migrated to Rails REST APIs)
|
||||||
|
- **Image generation**: Runs a headless browser to render outfit thumbnails and convert HTML5 assets to PNGs
|
||||||
|
|
||||||
|
See [docs/impress-2020-dependencies.md](./docs/impress-2020-dependencies.md) for migration status.
|
||||||
|
|
||||||
|
## Key Concepts
|
||||||
|
|
||||||
|
### Customization Data Model
|
||||||
|
|
||||||
|
The core data model powers outfit rendering and item compatibility. See [docs/customization-architecture.md](./docs/customization-architecture.md) for details.
|
||||||
|
|
||||||
|
**Quick summary**:
|
||||||
|
- `body_id` is the key compatibility constraint (not species or color directly)
|
||||||
|
- Items have different `swf_assets` (visual layers) for different bodies
|
||||||
|
- Restrictions are subtractive: start with all layers, hide some based on zone restrictions
|
||||||
|
- Data is crowdsourced through "modeling" (users loading pets to contribute appearance data)
|
||||||
|
|
||||||
|
### Modeling (Crowdsourced Data)
|
||||||
|
|
||||||
|
DTI doesn't pre-populate item/pet data. Instead:
|
||||||
|
|
||||||
|
1. User loads a pet (via pet name lookup)
|
||||||
|
2. DTI fetches appearance data from Neopets APIs (legacy Flash/AMF protocol)
|
||||||
|
3. New `SwfAsset` records and relationships are created
|
||||||
|
4. Over time, the database learns which items fit which pet bodies
|
||||||
|
|
||||||
|
This "self-sustaining" approach means the site stays up-to-date as Neopets releases new content, without manual data entry.
|
||||||
|
|
||||||
|
## Directory Map
|
||||||
|
|
||||||
|
### Key Application Files
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── controllers/
|
||||||
|
│ ├── outfits_controller.rb # Outfit editor + CRUD
|
||||||
|
│ ├── items_controller.rb # Item search, pages, and JSON APIs
|
||||||
|
│ ├── pets_controller.rb # Pet loading (triggers modeling)
|
||||||
|
│ └── closet_hangers_controller.rb # User item lists ("closets")
|
||||||
|
│
|
||||||
|
├── models/
|
||||||
|
│ ├── item.rb # Items + compatibility prediction logic
|
||||||
|
│ ├── pet_type.rb # Species+Color combinations (has body_id)
|
||||||
|
│ ├── pet_state.rb # Visual variants (pose/gender/mood)
|
||||||
|
│ ├── swf_asset.rb # Visual layers (biology/object)
|
||||||
|
│ ├── outfit.rb # Saved outfits + rendering logic (visible_layers)
|
||||||
|
│ ├── alt_style.rb # Alternative pet appearances (Nostalgic, etc.)
|
||||||
|
│ └── pet/
|
||||||
|
│ └── modeling_snapshot.rb # Processes Neopets API data into models
|
||||||
|
│
|
||||||
|
├── services/
|
||||||
|
│ ├── neopets/
|
||||||
|
│ │ ├── custom_pets.rb # Neopets AMF/Flash API client (pet data)
|
||||||
|
│ │ ├── nc_mall.rb # NC Mall item scraping
|
||||||
|
│ │ └── neopass.rb # NeoPass OAuth integration
|
||||||
|
│ ├── neopets_media_archive.rb # Local mirror of images.neopets.com
|
||||||
|
│ └── lebron_nc_values.rb # NC item trading values (external API)
|
||||||
|
│
|
||||||
|
├── javascript/
|
||||||
|
│ ├── wardrobe-2020/ # React outfit editor (extracted from Impress 2020)
|
||||||
|
│ │ ├── loaders/ # REST API calls (migrated from GraphQL)
|
||||||
|
│ │ ├── WardrobePage/ # Main editor UI
|
||||||
|
│ │ └── components/ # Shared React components
|
||||||
|
│ └── application.js # Rails asset pipeline entrypoint
|
||||||
|
│
|
||||||
|
└── views/
|
||||||
|
├── outfits/
|
||||||
|
│ └── edit.html.haml # Outfit editor page (loads React app)
|
||||||
|
├── items/
|
||||||
|
│ └── show.html.haml # Item detail page
|
||||||
|
└── closet_hangers/
|
||||||
|
└── index.html.haml # User closet/item lists
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration & Docs
|
||||||
|
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
├── routes.rb # All Rails routes
|
||||||
|
├── database.yml # Multi-database setup (main + openneo_id)
|
||||||
|
└── environments/
|
||||||
|
└── *.rb # Env-specific config (incl. impress_2020_origin)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- [docs/customization-architecture.md](./docs/customization-architecture.md) - Deep dive into data model & rendering
|
||||||
|
- [docs/impress-2020-dependencies.md](./docs/impress-2020-dependencies.md) - What still depends on Impress 2020 service
|
||||||
|
|
||||||
|
**Tests:**
|
||||||
|
- `test/` - Test::Unit tests (privacy features)
|
||||||
|
- `spec/` - RSpec tests (models, services, integrations)
|
||||||
|
- Coverage is focused on key areas: modeling, prediction logic, external APIs
|
||||||
|
- Not comprehensive, but thorough for critical behaviors
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Backend**: Ruby on Rails (Ruby 3.4, Rails 8.0)
|
||||||
|
- **Frontend**: Mix of Rails views (Turbo/HAML) and React (for outfit editor)
|
||||||
|
- **Database**: MySQL (two databases: `openneo_impress`, `openneo_id`)
|
||||||
|
- **Styling**: CSS, Sass (moving toward modern Rails conventions)
|
||||||
|
- **External Integrations**:
|
||||||
|
- **Neopets.com**: Legacy Flash/AMF protocol for pet appearance data (modeling)
|
||||||
|
- **Neopets NC Mall**: Web scraping for NC item availability/pricing
|
||||||
|
- **NeoPass**: OAuth integration for Neopets account linking
|
||||||
|
- **Neopets Media Archive**: Local filesystem mirror of `images.neopets.com` (never discards old files)
|
||||||
|
- **Lebron's NC Values**: Third-party API for NC item trading values ([lebron-values.netlify.app](https://lebron-values.netlify.app))
|
||||||
|
- **Impress 2020**: GraphQL for some outfit data, image generation service (being phased out)
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
|
||||||
|
### OpenNeo ID Database
|
||||||
|
|
||||||
|
The `openneo_id` database is a legacy from when authentication was a separate service ("OpenNeo ID") meant to unify auth across multiple OpenNeo projects. DTI was the only project that succeeded, so the apps were merged—but the database split remains for now.
|
||||||
|
|
||||||
|
**Implications**:
|
||||||
|
- Rails is configured for multi-database mode
|
||||||
|
- User auth models live in `auth_user.rb` and connect to `openneo_id`
|
||||||
|
- **⚠️ CRITICAL**: Impress 2020 also directly accesses both `openneo_impress` and `openneo_id` databases via SQL
|
||||||
|
- **Database migrations affecting these schemas must consider Impress 2020's direct access**
|
||||||
|
- See [docs/impress-2020-dependencies.md](./docs/impress-2020-dependencies.md) for full details on this dependency
|
||||||
|
|
||||||
|
### Rails/React Hybrid
|
||||||
|
|
||||||
|
Most pages are traditional Rails views using Turbo for interactivity. The **outfit editor** (`/outfits/new`) is a full React app that:
|
||||||
|
|
||||||
|
- Loads into a `#wardrobe-2020-root` div
|
||||||
|
- Uses React Query for data fetching
|
||||||
|
- Calls both Rails REST APIs (in `loaders/`) and Impress 2020 GraphQL (being migrated)
|
||||||
|
|
||||||
|
The goal is to simplify this over time—either consolidate into Rails+Turbo, or commit fully to React. For now, we're in a hybrid state.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
- **Main app**: VPS running Rails (Puma, MySQL)
|
||||||
|
- **Impress 2020**: Separate VPS in same datacenter (NextJS, GraphQL, headless browser for images)
|
||||||
|
- **Shared databases**: Both services directly access the same MySQL databases over the network
|
||||||
|
- `openneo_impress` - Main application data
|
||||||
|
- `openneo_id` - Authentication data
|
||||||
|
- ⚠️ **Any database schema changes must be compatible with both services**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Project maintained by [@matchu](https://github.com/matchu)** • **[OpenNeo.net](https://openneo.net)**
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
//= link_tree ../images
|
//= link_tree ../images
|
||||||
//= link_tree ../javascripts .js
|
//= link_tree ../javascripts .js
|
||||||
|
//= link_tree ../../../vendor/javascript .js
|
||||||
//= link_tree ../stylesheets .css
|
//= link_tree ../stylesheets .css
|
||||||
//= link_directory ../fonts .otf
|
//= link_directory ../fonts .otf
|
||||||
//= link_tree ../builds
|
//= link_tree ../builds
|
||||||
|
|
|
||||||
BIN
app/assets/images/about/announcement.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
app/assets/images/about/announcement@2x.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 585 B |
|
Before Width: | Height: | Size: 601 B |
|
Before Width: | Height: | Size: 206 B |
|
Before Width: | Height: | Size: 516 B |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 38 KiB |
BIN
app/assets/images/rainbow_pool.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
|
|
@ -1,20 +0,0 @@
|
||||||
(function () {
|
|
||||||
var CSRFProtection;
|
|
||||||
var token = $('meta[name="csrf-token"]').attr("content");
|
|
||||||
if (token) {
|
|
||||||
CSRFProtection = function (xhr, settings) {
|
|
||||||
var sendToken =
|
|
||||||
typeof settings.useCSRFProtection === "undefined" || // default to true
|
|
||||||
settings.useCSRFProtection;
|
|
||||||
if (sendToken) {
|
|
||||||
xhr.setRequestHeader("X-CSRF-Token", token);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
CSRFProtection = $.noop;
|
|
||||||
}
|
|
||||||
|
|
||||||
$.ajaxSetup({
|
|
||||||
beforeSend: CSRFProtection,
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
(function () {
|
|
||||||
function setChecked() {
|
|
||||||
var el = $(this);
|
|
||||||
el.closest("li").toggleClass("checked", el.is(":checked"));
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#petpage-closet-lists input").click(setChecked).each(setChecked);
|
|
||||||
})();
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
document.addEventListener("change", ({ target }) => {
|
document.addEventListener("change", ({ target }) => {
|
||||||
if (target.matches('select[name="closet_list[visibility]"]')) {
|
if (target.matches('select[name="closet_list[visibility]"]')) {
|
||||||
target
|
target.closest("form").setAttribute("data-list-visibility", target.value);
|
||||||
.closest("form")
|
|
||||||
.setAttribute("data-list-visibility", target.value);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
(function() {
|
(function () {
|
||||||
$('span.choose-outfit select').change(function(e) {
|
$("span.choose-outfit select").change(function (e) {
|
||||||
var select = $(this);
|
var select = $(this);
|
||||||
select.closest('li').find('input[type=text]').val(select.val());
|
select.closest("li").find("input[type=text]").val(select.val());
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
115
app/assets/javascripts/items/show.js
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
// When the species face picker changes, update and submit the main picker form.
|
||||||
|
document.addEventListener("change", (e) => {
|
||||||
|
if (!e.target.matches("species-face-picker")) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mainPickerForm = document.querySelector(
|
||||||
|
"#item-preview species-color-picker form",
|
||||||
|
);
|
||||||
|
const mainSpeciesField = mainPickerForm.querySelector(
|
||||||
|
"[name='preview[species_id]']",
|
||||||
|
);
|
||||||
|
mainSpeciesField.value = e.target.value;
|
||||||
|
mainPickerForm.requestSubmit(); // `submit` doesn't get captured by Turbo!
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Couldn't update species picker: ", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the preview frame fails to load, try a full pageload.
|
||||||
|
document.addEventListener("turbo:frame-missing", (e) => {
|
||||||
|
if (!e.target.matches("#item-preview")) return;
|
||||||
|
|
||||||
|
e.detail.visit(e.detail.response.url);
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
class SpeciesColorPicker 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) {
|
||||||
|
this.querySelector("form").requestSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpeciesFacePicker extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
this.addEventListener("click", this.#handleClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.querySelector("input[type=radio]:checked")?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleClick(e) {
|
||||||
|
if (e.target.matches("input[type=radio]")) {
|
||||||
|
this.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpeciesFacePickerOptions extends HTMLElement {
|
||||||
|
static observedAttributes = ["inert", "aria-hidden"];
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
// Once this component is loaded, we stop being inert and aria-hidden. We're ready!
|
||||||
|
this.#activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback() {
|
||||||
|
// If a Turbo Frame tries to morph us into being inert again, activate again!
|
||||||
|
// (It's important that the server's HTML always return `inert`, for progressive
|
||||||
|
// enhancement; and it's important to morph this element, so radio focus state
|
||||||
|
// is preserved. To thread that needle, we have to monitor and remove!)
|
||||||
|
this.#activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#activate() {
|
||||||
|
this.removeAttribute("inert");
|
||||||
|
this.removeAttribute("aria-hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: If it ever gets wide support, remove this in favor of the CSS rule
|
||||||
|
// `interpolate-size: allow-keywords`, to animate directly from `auto`.
|
||||||
|
// https://drafts.csswg.org/css-values-5/#valdef-interpolate-size-allow-keywords
|
||||||
|
class MeasuredContainer extends HTMLElement {
|
||||||
|
static observedAttributes = ["style"];
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
setTimeout(() => this.#measure(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback() {
|
||||||
|
// When `--natural-width` gets morphed away by Turbo, measure it again!
|
||||||
|
if (this.style.getPropertyValue("--natural-width") === "") {
|
||||||
|
this.#measure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#measure() {
|
||||||
|
// Find our `<measured-content>` child, and set our natural width as
|
||||||
|
// `var(--natural-width)` in the context of our CSS styles.
|
||||||
|
const content = this.querySelector("measured-content");
|
||||||
|
if (content == null) {
|
||||||
|
throw new Error(`<measured-container> must contain a <measured-content>`);
|
||||||
|
}
|
||||||
|
this.style.setProperty("--natural-width", content.offsetWidth + "px");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("species-color-picker", SpeciesColorPicker);
|
||||||
|
customElements.define("species-face-picker", SpeciesFacePicker);
|
||||||
|
customElements.define("species-face-picker-options", SpeciesFacePickerOptions);
|
||||||
|
customElements.define("measured-container", MeasuredContainer);
|
||||||
36
app/assets/javascripts/magic-magnifier.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
class MagicMagnifier extends HTMLElement {
|
||||||
|
#internals = this.attachInternals();
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
setTimeout(() => this.#attachLens(), 0);
|
||||||
|
this.addEventListener("mousemove", this.#onMouseMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
#attachLens() {
|
||||||
|
const lens = document.createElement("magic-magnifier-lens");
|
||||||
|
lens.inert = true;
|
||||||
|
lens.useContent(this.children);
|
||||||
|
this.appendChild(lens);
|
||||||
|
}
|
||||||
|
|
||||||
|
#onMouseMove(e) {
|
||||||
|
const lens = this.querySelector("magic-magnifier-lens");
|
||||||
|
const rect = this.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
this.style.setProperty("--magic-magnifier-x", x + "px");
|
||||||
|
this.style.setProperty("--magic-magnifier-y", y + "px");
|
||||||
|
this.#internals.states.add("ready");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MagicMagnifierLens extends HTMLElement {
|
||||||
|
useContent(contentNodes) {
|
||||||
|
for (const contentNode of contentNodes) {
|
||||||
|
this.appendChild(contentNode.cloneNode(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("magic-magnifier", MagicMagnifier);
|
||||||
|
customElements.define("magic-magnifier-lens", MagicMagnifierLens);
|
||||||
|
|
@ -3,10 +3,12 @@ class OutfitViewer extends HTMLElement {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.#internals = this.attachInternals();
|
this.#internals = this.attachInternals(); // for CSS `:state()`
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
// The `<outfit-layer>` is connected to the DOM right before its
|
||||||
|
// children are. So, to engage with the children, wait a tick!
|
||||||
setTimeout(() => this.#connectToChildren(), 0);
|
setTimeout(() => this.#connectToChildren(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,6 +69,8 @@ class OutfitLayer extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
|
// When this `<outfit-layer>` leaves the DOM, stop listening for iframe
|
||||||
|
// messages, if we were.
|
||||||
window.removeEventListener("message", this.#onMessage);
|
window.removeEventListener("message", this.#onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,33 +87,36 @@ class OutfitLayer extends HTMLElement {
|
||||||
const iframe = this.querySelector("iframe");
|
const iframe = this.querySelector("iframe");
|
||||||
|
|
||||||
if (image) {
|
if (image) {
|
||||||
// Initialize status based on the image's current `complete` attribute,
|
// If this is an image layer, track its loading state by listening
|
||||||
// then wait for load/error events to update it further if needed.
|
// to the load/error events, and initialize based on whether it's
|
||||||
|
// already `complete` (which it can be if it loaded from cache).
|
||||||
this.#setStatus(image.complete ? "loaded" : "loading");
|
this.#setStatus(image.complete ? "loaded" : "loading");
|
||||||
image.addEventListener("load", () => this.#setStatus("loaded"));
|
image.addEventListener("load", () => this.#setStatus("loaded"));
|
||||||
image.addEventListener("error", () => this.#setStatus("error"));
|
image.addEventListener("error", () => this.#setStatus("error"));
|
||||||
} else if (iframe) {
|
} else if (iframe) {
|
||||||
this.iframe = iframe;
|
this.iframe = iframe;
|
||||||
|
|
||||||
// Initialize status to `loading`, and asynchronously request a status
|
// Initialize status to `loading`, and asynchronously request a
|
||||||
// message from the iframe if it managed to load before this triggers
|
// status message from the iframe if it managed to load before this
|
||||||
// (impressive, but I think I've seen it happen!). Then, wait for
|
// triggers (impressive, but I think I've seen it happen!). Then,
|
||||||
// messages or error events from the iframe to update status further if
|
// wait for messages or error events from the iframe to update
|
||||||
// needed.
|
// status further if needed.
|
||||||
this.#setStatus("loading");
|
this.#setStatus("loading");
|
||||||
this.#sendMessageToIframe({ type: "requestStatus" });
|
this.#sendMessageToIframe({ type: "requestStatus" });
|
||||||
window.addEventListener("message", (m) => this.#onMessage(m));
|
window.addEventListener("message", (m) => this.#onMessage(m));
|
||||||
this.iframe.addEventListener("error", () => this.#setStatus("error"));
|
this.iframe.addEventListener("error", () => this.#setStatus("error"));
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`<outfit-layer> must contain an <img> or <iframe> tag`);
|
console.warn(`<outfit-layer> contained no image or iframe: `, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#onMessage({ source, data }) {
|
#onMessage({ source, data }) {
|
||||||
|
// Ignore messages that aren't from *our* frame.
|
||||||
if (source !== this.iframe.contentWindow) {
|
if (source !== this.iframe.contentWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate the incoming status message, then set our status to match.
|
||||||
if (data.type === "status") {
|
if (data.type === "status") {
|
||||||
if (data.status === "loaded") {
|
if (data.status === "loaded") {
|
||||||
this.#setStatus("loaded");
|
this.#setStatus("loaded");
|
||||||
|
|
@ -118,16 +125,21 @@ class OutfitLayer extends HTMLElement {
|
||||||
this.#setStatus("error");
|
this.#setStatus("error");
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`<outfit-layer> got unexpected status: ${JSON.stringify(data.status)}`,
|
`<outfit-layer> got unexpected status: ` +
|
||||||
|
JSON.stringify(data.status),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`<outfit-layer> got unexpected message: ${JSON.stringify(data)}`,
|
`<outfit-layer> got unexpected message: ` + JSON.stringify(data),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the status value that the CSS `:state()` selector will match.
|
||||||
|
* For example, when loading, `:state(loading)` matches this element.
|
||||||
|
*/
|
||||||
#setStatus(newStatus) {
|
#setStatus(newStatus) {
|
||||||
this.#internals.states.delete("loading");
|
this.#internals.states.delete("loading");
|
||||||
this.#internals.states.delete("loaded");
|
this.#internals.states.delete("loaded");
|
||||||
|
|
@ -135,6 +147,9 @@ class OutfitLayer extends HTMLElement {
|
||||||
this.#internals.states.add(newStatus);
|
this.#internals.states.add(newStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether CSS selector `:state(has-animations)` matches this element.
|
||||||
|
*/
|
||||||
#setHasAnimations(hasAnimations) {
|
#setHasAnimations(hasAnimations) {
|
||||||
if (hasAnimations) {
|
if (hasAnimations) {
|
||||||
this.#internals.states.add("has-animations");
|
this.#internals.states.add("has-animations");
|
||||||
|
|
@ -144,7 +159,16 @@ class OutfitLayer extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
#sendMessageToIframe(message) {
|
#sendMessageToIframe(message) {
|
||||||
if (this.iframe?.contentWindow == null) {
|
// If we have no frame or it hasn't loaded, ignore this message.
|
||||||
|
if (this.iframe == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.iframe.contentWindow == null) {
|
||||||
|
console.debug(
|
||||||
|
`Ignoring message, frame not loaded yet: `,
|
||||||
|
this.iframe,
|
||||||
|
message,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,27 +185,30 @@ customElements.define("outfit-layer", OutfitLayer);
|
||||||
// aggressively reusing existing <outfit-layer> nodes for entirely different
|
// aggressively reusing existing <outfit-layer> nodes for entirely different
|
||||||
// assets. (It's a lot clearer for managing the loading state, and not showing
|
// assets. (It's a lot clearer for managing the loading state, and not showing
|
||||||
// old incorrect layers!) (We also tried using `id` to enforce this… no luck.)
|
// old incorrect layers!) (We also tried using `id` to enforce this… no luck.)
|
||||||
|
function morphWithOutfitLayers(currentElement, newElement) {
|
||||||
|
Idiomorph.morph(currentElement, newElement.innerHTML, {
|
||||||
|
morphStyle: "innerHTML",
|
||||||
|
callbacks: {
|
||||||
|
beforeNodeMorphed: (currentNode, newNode) => {
|
||||||
|
// If Idiomorph wants to transform an <outfit-layer> to
|
||||||
|
// have a different data-asset-id attribute, we replace
|
||||||
|
// the node ourselves and abort the morph.
|
||||||
|
if (
|
||||||
|
newNode.tagName === "OUTFIT-LAYER" &&
|
||||||
|
newNode.getAttribute("data-asset-id") !==
|
||||||
|
currentNode.getAttribute("data-asset-id")
|
||||||
|
) {
|
||||||
|
currentNode.replaceWith(newNode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
addEventListener("turbo:before-frame-render", (event) => {
|
addEventListener("turbo:before-frame-render", (event) => {
|
||||||
|
// Rather than enforce Idiomorph must be loaded, let's just be resilient
|
||||||
|
// and only bother if we have it. (Replacing content is not *that* bad!)
|
||||||
if (typeof Idiomorph !== "undefined") {
|
if (typeof Idiomorph !== "undefined") {
|
||||||
event.detail.render = (currentElement, newElement) => {
|
event.detail.render = (a, b) => morphWithOutfitLayers(a, b);
|
||||||
Idiomorph.morph(currentElement, newElement.innerHTML, {
|
|
||||||
morphStyle: "innerHTML",
|
|
||||||
callbacks: {
|
|
||||||
beforeNodeMorphed: (currentNode, newNode) => {
|
|
||||||
// If Idiomorph wants to transform an <outfit-layer> to
|
|
||||||
// have a different data-asset-id attribute, we replace
|
|
||||||
// the node ourselves and abort the morph.
|
|
||||||
if (
|
|
||||||
newNode.tagName === "OUTFIT-LAYER" &&
|
|
||||||
newNode.getAttribute("data-asset-id") !==
|
|
||||||
currentNode.getAttribute("data-asset-id")
|
|
||||||
) {
|
|
||||||
currentNode.replaceWith(newNode);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,272 +1,253 @@
|
||||||
(function () {
|
(function () {
|
||||||
function petImage(id, size) {
|
function petImage(id, size) {
|
||||||
return "https://pets.neopets.com/" + id + "/1/" + size + ".png";
|
return "https://pets.neopets.com/" + id + "/1/" + size + ".png";
|
||||||
}
|
}
|
||||||
|
|
||||||
var PetQuery = {},
|
var PetQuery = {},
|
||||||
query_string = document.location.hash || document.location.search;
|
query_string = document.location.hash || document.location.search;
|
||||||
|
|
||||||
$.each(query_string.substr(1).split("&"), function () {
|
for (const [key, value] of new URLSearchParams(query_string).entries()) {
|
||||||
var split_piece = this.split("=");
|
PetQuery[key] = value;
|
||||||
if (split_piece.length == 2) {
|
}
|
||||||
PetQuery[split_piece[0]] = split_piece[1];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (PetQuery.name) {
|
if (PetQuery.name) {
|
||||||
if (PetQuery.species && PetQuery.color) {
|
if (PetQuery.species && PetQuery.color) {
|
||||||
$("#pet-query-notice-template")
|
var image_url = petImage("cpn/" + PetQuery.name, 1);
|
||||||
.tmpl({
|
if (PetQuery.name.startsWith("@")) {
|
||||||
pet_name: PetQuery.name,
|
image_url = petImage("cp/" + PetQuery.name.substr(1), 1);
|
||||||
pet_image_url: petImage("cpn/" + PetQuery.name, 1),
|
}
|
||||||
})
|
$("#pet-query-notice-template")
|
||||||
.prependTo("#container");
|
.tmpl({
|
||||||
}
|
pet_name: PetQuery.name,
|
||||||
}
|
pet_image_url: image_url,
|
||||||
|
})
|
||||||
|
.prependTo("#container");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var preview_el = $("#pet-preview"),
|
var preview_el = $("#pet-preview"),
|
||||||
img_el = preview_el.find("img"),
|
img_el = preview_el.find("img"),
|
||||||
response_el = preview_el.find("span");
|
response_el = preview_el.find("span");
|
||||||
|
|
||||||
var defaultPreviewUrl = img_el.attr("src");
|
var defaultPreviewUrl = img_el.attr("src");
|
||||||
|
|
||||||
preview_el.click(function () {
|
preview_el.click(function () {
|
||||||
Preview.Job.current.visit();
|
Preview.Job.current.visit();
|
||||||
});
|
});
|
||||||
|
|
||||||
var Preview = {
|
var Preview = {
|
||||||
clear: function () {
|
clear: function () {
|
||||||
if (typeof Preview.Job.fallback != "undefined")
|
if (typeof Preview.Job.fallback != "undefined")
|
||||||
Preview.Job.fallback.setAsCurrent();
|
Preview.Job.fallback.setAsCurrent();
|
||||||
},
|
},
|
||||||
displayLoading: function () {
|
displayLoading: function () {
|
||||||
preview_el.addClass("loading");
|
preview_el.addClass("loading");
|
||||||
response_el.text("Loading...");
|
response_el.text("Loading...");
|
||||||
},
|
},
|
||||||
failed: function () {
|
failed: function () {
|
||||||
preview_el.addClass("hidden");
|
preview_el.addClass("hidden");
|
||||||
},
|
},
|
||||||
notFound: function (key, options) {
|
notFound: function (key, options) {
|
||||||
Preview.failed();
|
Preview.failed();
|
||||||
response_el.empty();
|
response_el.empty();
|
||||||
$("#preview-" + key + "-template")
|
$("#preview-" + key + "-template")
|
||||||
.tmpl(options)
|
.tmpl(options)
|
||||||
.appendTo(response_el);
|
.appendTo(response_el);
|
||||||
},
|
},
|
||||||
updateWithName: function (name_el) {
|
updateWithName: function (name_el) {
|
||||||
var name = name_el.val(),
|
var name = name_el.val(),
|
||||||
job;
|
job;
|
||||||
if (name) {
|
if (name) {
|
||||||
currentName = name;
|
currentName = name;
|
||||||
if (!Preview.Job.current || name != Preview.Job.current.name) {
|
if (!Preview.Job.current || name != Preview.Job.current.name) {
|
||||||
job = new Preview.Job.Name(name);
|
job = new Preview.Job.Name(name);
|
||||||
job.setAsCurrent();
|
job.setAsCurrent();
|
||||||
Preview.displayLoading();
|
Preview.displayLoading();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Preview.clear();
|
Preview.clear();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function loadNotable() {
|
function loadFeature() {
|
||||||
// TODO: add HTTPS to notables
|
$.getJSON("/donations/features", function (features) {
|
||||||
// $.getJSON('https://notables.openneo.net/api/1/days/ago/1?callback=?', function (response) {
|
if (features.length > 0) {
|
||||||
// var notables = response.notables;
|
var feature = features[Math.floor(Math.random() * features.length)];
|
||||||
// var i = Math.floor(Math.random() * notables.length);
|
Preview.Job.fallback = new Preview.Job.Feature(feature);
|
||||||
// Preview.Job.fallback = new Preview.Job.Name(notables[i].petName);
|
if (!Preview.Job.current) {
|
||||||
// if(!Preview.Job.current) {
|
Preview.Job.fallback.setAsCurrent();
|
||||||
// Preview.Job.fallback.setAsCurrent();
|
}
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
if (!Preview.Job.current) {
|
}
|
||||||
Preview.Job.fallback.setAsCurrent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadFeature() {
|
loadFeature();
|
||||||
$.getJSON("/donations/features", function (features) {
|
|
||||||
if (features.length > 0) {
|
|
||||||
var feature = features[Math.floor(Math.random() * features.length)];
|
|
||||||
Preview.Job.fallback = new Preview.Job.Feature(feature);
|
|
||||||
if (!Preview.Job.current) {
|
|
||||||
Preview.Job.fallback.setAsCurrent();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
loadNotable();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
loadFeature();
|
Preview.Job = function (key, base) {
|
||||||
|
var job = this,
|
||||||
|
quality = 2;
|
||||||
|
job.loading = false;
|
||||||
|
|
||||||
Preview.Job = function (key, base) {
|
function getImageSrc() {
|
||||||
var job = this,
|
if (base === "cp" || base === "cpn") {
|
||||||
quality = 2;
|
return petImage(base + "/" + key, quality);
|
||||||
job.loading = false;
|
} else if (base === "url") {
|
||||||
|
return key;
|
||||||
|
} else {
|
||||||
|
throw new Error("unrecognized image base " + base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getImageSrc() {
|
function load() {
|
||||||
if (key.substr(0, 3) === "a:-") {
|
job.loading = true;
|
||||||
// lol lazy code for prank image :P
|
img_el.attr("src", getImageSrc());
|
||||||
// TODO: HTTPS?
|
}
|
||||||
return (
|
|
||||||
"https://swfimages.impress.openneo.net" +
|
|
||||||
"/biology/000/000/0-2/" +
|
|
||||||
key.substr(2) +
|
|
||||||
"/300x300.png"
|
|
||||||
);
|
|
||||||
} else if (base === "cp" || base === "cpn") {
|
|
||||||
return petImage(base + "/" + key, quality);
|
|
||||||
} else if (base === "url") {
|
|
||||||
return key;
|
|
||||||
} else {
|
|
||||||
throw new Error("unrecognized image base " + base);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function load() {
|
this.increaseQualityIfPossible = function () {
|
||||||
job.loading = true;
|
if (quality == 2) {
|
||||||
img_el.attr("src", getImageSrc());
|
quality = 4;
|
||||||
}
|
load();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.increaseQualityIfPossible = function () {
|
this.setAsCurrent = function () {
|
||||||
if (quality == 2) {
|
Preview.Job.current = job;
|
||||||
quality = 4;
|
load();
|
||||||
load();
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setAsCurrent = function () {
|
this.notFound = function () {
|
||||||
Preview.Job.current = job;
|
Preview.notFound("pet-not-found");
|
||||||
load();
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
this.notFound = function () {
|
Preview.Job.Name = function (name) {
|
||||||
Preview.notFound("pet-not-found");
|
this.name = name;
|
||||||
};
|
if (name.startsWith("@")) {
|
||||||
};
|
// This is an image hash "pet name".
|
||||||
|
Preview.Job.apply(this, [name.substr(1), "cp"]);
|
||||||
|
} else {
|
||||||
|
// This is a normal pet name.
|
||||||
|
Preview.Job.apply(this, [name, "cpn"]);
|
||||||
|
}
|
||||||
|
|
||||||
Preview.Job.Name = function (name) {
|
this.visit = function () {
|
||||||
this.name = name;
|
$(".main-pet-name").val(this.name).closest("form").submit();
|
||||||
Preview.Job.apply(this, [name, "cpn"]);
|
};
|
||||||
|
};
|
||||||
|
|
||||||
this.visit = function () {
|
Preview.Job.Hash = function (hash, form) {
|
||||||
$(".main-pet-name").val(this.name).closest("form").submit();
|
Preview.Job.apply(this, [hash, "cp"]);
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
Preview.Job.Hash = function (hash, form) {
|
this.visit = function () {
|
||||||
Preview.Job.apply(this, [hash, "cp"]);
|
window.location =
|
||||||
|
"/wardrobe?color=" +
|
||||||
|
form.find(".color").val() +
|
||||||
|
"&species=" +
|
||||||
|
form.find(".species").val();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
this.visit = function () {
|
Preview.Job.Feature = function (feature) {
|
||||||
window.location =
|
Preview.Job.apply(this, [feature.outfit_image_url, "url"]);
|
||||||
"/wardrobe?color=" +
|
this.name = "Thanks for donating, " + feature.donor_name + "!"; // TODO: i18n
|
||||||
form.find(".color").val() +
|
|
||||||
"&species=" +
|
|
||||||
form.find(".species").val();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
Preview.Job.Feature = function (feature) {
|
this.visit = function () {
|
||||||
Preview.Job.apply(this, [feature.outfit_image_url, "url"]);
|
window.location = "/donate";
|
||||||
this.name = "Thanks for donating, " + feature.donor_name + "!"; // TODO: i18n
|
};
|
||||||
|
|
||||||
this.visit = function () {
|
this.notFound = function () {
|
||||||
window.location = "/donate";
|
// The outfit thumbnail hasn't generated or is missing or something.
|
||||||
};
|
// Let's fall back to a boring image for now.
|
||||||
|
var boring = new Preview.Job.Feature({
|
||||||
|
donor_name: feature.donor_name,
|
||||||
|
outfit_image_url: defaultPreviewUrl,
|
||||||
|
});
|
||||||
|
boring.setAsCurrent();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
this.notFound = function () {
|
$(function () {
|
||||||
// The outfit thumbnail hasn't generated or is missing or something.
|
var previewWithNameTimeout;
|
||||||
// Let's fall back to a boring image for now.
|
|
||||||
var boring = new Preview.Job.Feature({
|
|
||||||
donor_name: feature.donor_name,
|
|
||||||
outfit_image_url: defaultPreviewUrl,
|
|
||||||
});
|
|
||||||
boring.setAsCurrent();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
$(function () {
|
var name_el = $(".main-pet-name");
|
||||||
var previewWithNameTimeout;
|
name_el.val(PetQuery.name);
|
||||||
|
Preview.updateWithName(name_el);
|
||||||
|
|
||||||
var name_el = $(".main-pet-name");
|
name_el.keyup(function () {
|
||||||
name_el.val(PetQuery.name);
|
if (previewWithNameTimeout && Preview.Job.current) {
|
||||||
Preview.updateWithName(name_el);
|
clearTimeout(previewWithNameTimeout);
|
||||||
|
Preview.Job.current.loading = false;
|
||||||
|
}
|
||||||
|
var name_el = $(this);
|
||||||
|
previewWithNameTimeout = setTimeout(function () {
|
||||||
|
Preview.updateWithName(name_el);
|
||||||
|
}, 250);
|
||||||
|
});
|
||||||
|
|
||||||
name_el.keyup(function () {
|
img_el
|
||||||
if (previewWithNameTimeout && Preview.Job.current) {
|
.load(function () {
|
||||||
clearTimeout(previewWithNameTimeout);
|
if (Preview.Job.current.loading) {
|
||||||
Preview.Job.current.loading = false;
|
Preview.Job.loading = false;
|
||||||
}
|
Preview.Job.current.increaseQualityIfPossible();
|
||||||
var name_el = $(this);
|
preview_el
|
||||||
previewWithNameTimeout = setTimeout(function () {
|
.removeClass("loading")
|
||||||
Preview.updateWithName(name_el);
|
.removeClass("hidden")
|
||||||
}, 250);
|
.addClass("loaded");
|
||||||
});
|
response_el.text(Preview.Job.current.name);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.error(function () {
|
||||||
|
if (Preview.Job.current.loading) {
|
||||||
|
Preview.Job.loading = false;
|
||||||
|
Preview.Job.current.notFound();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
img_el
|
$(".species, .color").change(function () {
|
||||||
.load(function () {
|
var type = {},
|
||||||
if (Preview.Job.current.loading) {
|
nameComponents = {};
|
||||||
Preview.Job.loading = false;
|
var form = $(this).closest("form");
|
||||||
Preview.Job.current.increaseQualityIfPossible();
|
form.find("select").each(function () {
|
||||||
preview_el
|
var el = $(this),
|
||||||
.removeClass("loading")
|
selectedEl = el.children(":selected"),
|
||||||
.removeClass("hidden")
|
key = el.attr("name");
|
||||||
.addClass("loaded");
|
type[key] = selectedEl.val();
|
||||||
response_el.text(Preview.Job.current.name);
|
nameComponents[key] = selectedEl.text();
|
||||||
}
|
});
|
||||||
})
|
name = nameComponents.color + " " + nameComponents.species;
|
||||||
.error(function () {
|
Preview.displayLoading();
|
||||||
if (Preview.Job.current.loading) {
|
$.ajax({
|
||||||
Preview.Job.loading = false;
|
url:
|
||||||
Preview.Job.current.notFound();
|
"/species/" +
|
||||||
}
|
type.species +
|
||||||
});
|
"/colors/" +
|
||||||
|
type.color +
|
||||||
|
"/pet_type.json",
|
||||||
|
dataType: "json",
|
||||||
|
success: function (data) {
|
||||||
|
var job;
|
||||||
|
if (data) {
|
||||||
|
job = new Preview.Job.Hash(data.image_hash, form);
|
||||||
|
job.name = name;
|
||||||
|
job.setAsCurrent();
|
||||||
|
} else {
|
||||||
|
Preview.notFound("pet-type-not-found", {
|
||||||
|
color_name: nameComponents.color,
|
||||||
|
species_name: nameComponents.species,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$(".species, .color").change(function () {
|
$(".load-pet-to-wardrobe").submit(function (e) {
|
||||||
var type = {},
|
if ($(this).find(".main-pet-name").val() === "" && Preview.Job.current) {
|
||||||
nameComponents = {};
|
e.preventDefault();
|
||||||
var form = $(this).closest("form");
|
Preview.Job.current.visit();
|
||||||
form.find("select").each(function () {
|
}
|
||||||
var el = $(this),
|
});
|
||||||
selectedEl = el.children(":selected"),
|
});
|
||||||
key = el.attr("name");
|
|
||||||
type[key] = selectedEl.val();
|
|
||||||
nameComponents[key] = selectedEl.text();
|
|
||||||
});
|
|
||||||
name = nameComponents.color + " " + nameComponents.species;
|
|
||||||
Preview.displayLoading();
|
|
||||||
$.ajax({
|
|
||||||
url:
|
|
||||||
"/species/" +
|
|
||||||
type.species +
|
|
||||||
"/colors/" +
|
|
||||||
type.color +
|
|
||||||
"/pet_type.json",
|
|
||||||
dataType: "json",
|
|
||||||
success: function (data) {
|
|
||||||
var job;
|
|
||||||
if (data) {
|
|
||||||
job = new Preview.Job.Hash(data.image_hash, form);
|
|
||||||
job.name = name;
|
|
||||||
job.setAsCurrent();
|
|
||||||
} else {
|
|
||||||
Preview.notFound("pet-type-not-found", {
|
|
||||||
color_name: nameComponents.color,
|
|
||||||
species_name: nameComponents.species,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".load-pet-to-wardrobe").submit(function (e) {
|
$("#latest-contribution-created-at").timeago();
|
||||||
if ($(this).find(".main-pet-name").val() === "" && Preview.Job.current) {
|
|
||||||
e.preventDefault();
|
|
||||||
Preview.Job.current.visit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#latest-contribution-created-at").timeago();
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
46
app/assets/javascripts/pet_states/support-outfit-viewer.js
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
class SupportOutfitViewer extends HTMLElement {
|
||||||
|
#internals = this.attachInternals();
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.addEventListener("mouseenter", this.#onMouseEnter, { capture: true });
|
||||||
|
this.addEventListener("mouseleave", this.#onMouseLeave, { capture: true });
|
||||||
|
this.addEventListener("click", this.#onClick);
|
||||||
|
this.#internals.states.add("ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a row is hovered, highlight its corresponding outfit viewer layer.
|
||||||
|
#onMouseEnter(e) {
|
||||||
|
if (!e.target.matches("tr")) return;
|
||||||
|
|
||||||
|
const id = e.target.querySelector("[data-field=id]").innerText;
|
||||||
|
const layers = this.querySelectorAll(
|
||||||
|
`outfit-viewer [data-asset-id="${CSS.escape(id)}"]`,
|
||||||
|
);
|
||||||
|
for (const layer of layers) {
|
||||||
|
layer.setAttribute("highlighted", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a row is unhovered, unhighlight its corresponding outfit viewer layer.
|
||||||
|
#onMouseLeave(e) {
|
||||||
|
if (!e.target.matches("tr")) return;
|
||||||
|
|
||||||
|
const id = e.target.querySelector("[data-field=id]").innerText;
|
||||||
|
const layers = this.querySelectorAll(
|
||||||
|
`outfit-viewer [data-asset-id="${CSS.escape(id)}"]`,
|
||||||
|
);
|
||||||
|
for (const layer of layers) {
|
||||||
|
layer.removeAttribute("highlighted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When clicking a row, redirect the click to the first link.
|
||||||
|
#onClick(e) {
|
||||||
|
const row = e.target.closest("tr");
|
||||||
|
if (row == null) return;
|
||||||
|
|
||||||
|
row.querySelector("[data-field=links] a").click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("support-outfit-viewer", SupportOutfitViewer);
|
||||||
|
|
@ -1,203 +1,110 @@
|
||||||
var DEBUG = document.location.search.substr(0, 6) == "?debug";
|
|
||||||
|
|
||||||
/* Needed items form */
|
|
||||||
(function () {
|
|
||||||
var UI = {};
|
|
||||||
UI.form = $("#needed-items-form");
|
|
||||||
UI.alert = $("#needed-items-alert");
|
|
||||||
UI.pet_name_field = $("#needed-items-pet-name-field");
|
|
||||||
UI.pet_thumbnail = $("#needed-items-pet-thumbnail");
|
|
||||||
UI.pet_header = $("#needed-items-pet-header");
|
|
||||||
UI.reload = $("#needed-items-reload");
|
|
||||||
UI.pet_items = $("#needed-items-pet-items");
|
|
||||||
UI.item_template = $("#item-template");
|
|
||||||
|
|
||||||
var current_request = { abort: function () {} };
|
|
||||||
function sendRequest(options) {
|
|
||||||
current_request = $.ajax(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelRequest() {
|
|
||||||
if (DEBUG) console.log("Canceling request", current_request);
|
|
||||||
current_request.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pet */
|
|
||||||
|
|
||||||
var last_successful_pet_name = null;
|
|
||||||
|
|
||||||
function loadPet(pet_name) {
|
|
||||||
// If there is a request in progress, kill it. Our new pet request takes
|
|
||||||
// priority, and, if I submit a name while the previous name is loading, I
|
|
||||||
// don't want to process both responses.
|
|
||||||
cancelRequest();
|
|
||||||
|
|
||||||
sendRequest({
|
|
||||||
url: UI.form.attr("action") + ".json",
|
|
||||||
dataType: "json",
|
|
||||||
data: { name: pet_name },
|
|
||||||
error: petError,
|
|
||||||
success: function (data) {
|
|
||||||
petSuccess(data, pet_name);
|
|
||||||
},
|
|
||||||
complete: petComplete,
|
|
||||||
});
|
|
||||||
|
|
||||||
UI.form.removeClass("failed").addClass("loading-pet");
|
|
||||||
}
|
|
||||||
|
|
||||||
function petComplete() {
|
|
||||||
UI.form.removeClass("loading-pet");
|
|
||||||
}
|
|
||||||
|
|
||||||
function petError(xhr) {
|
|
||||||
UI.alert.text(xhr.responseText);
|
|
||||||
UI.form.addClass("failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
function petSuccess(data, pet_name) {
|
|
||||||
last_successful_pet_name = pet_name;
|
|
||||||
UI.pet_thumbnail.attr("src", petThumbnailUrl(pet_name));
|
|
||||||
UI.pet_header.empty();
|
|
||||||
$("#needed-items-pet-header-template")
|
|
||||||
.tmpl({ pet_name: pet_name })
|
|
||||||
.appendTo(UI.pet_header);
|
|
||||||
loadItems(data.query);
|
|
||||||
}
|
|
||||||
|
|
||||||
function petThumbnailUrl(pet_name) {
|
|
||||||
return "https://pets.neopets.com/cpn/" + pet_name + "/1/1.png";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Items */
|
|
||||||
|
|
||||||
function loadItems(query) {
|
|
||||||
UI.form.addClass("loading-items");
|
|
||||||
sendRequest({
|
|
||||||
url: "/items/needed.json",
|
|
||||||
dataType: "json",
|
|
||||||
data: query,
|
|
||||||
success: itemsSuccess,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function itemsSuccess(items) {
|
|
||||||
if (DEBUG) {
|
|
||||||
// The dev server is missing lots of data, so sends me 2000+ needed
|
|
||||||
// items. We don't need that many for styling, so limit it to 100 to make
|
|
||||||
// my browser happier.
|
|
||||||
items = items.slice(0, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
UI.pet_items.empty();
|
|
||||||
UI.item_template.tmpl(items).appendTo(UI.pet_items);
|
|
||||||
|
|
||||||
UI.form.removeClass("loading-items").addClass("loaded");
|
|
||||||
}
|
|
||||||
|
|
||||||
UI.form.submit(function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
loadPet(UI.pet_name_field.val());
|
|
||||||
});
|
|
||||||
|
|
||||||
UI.reload.click(function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
loadPet(last_successful_pet_name);
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
/* Bulk pets form */
|
/* Bulk pets form */
|
||||||
(function () {
|
(function () {
|
||||||
var form = $("#bulk-pets-form"),
|
var form = $("#bulk-pets-form"),
|
||||||
queue_el = form.find("ul"),
|
queue_el = form.find("ul"),
|
||||||
names_el = form.find("textarea"),
|
names_el = form.find("textarea"),
|
||||||
add_el = $("#bulk-pets-form-add"),
|
add_el = $("#bulk-pets-form-add"),
|
||||||
clear_el = $("#bulk-pets-form-clear"),
|
clear_el = $("#bulk-pets-form-clear"),
|
||||||
bulk_load_queue;
|
bulk_load_queue;
|
||||||
|
|
||||||
$(document.body).addClass("js");
|
$(document.body).addClass("js");
|
||||||
|
|
||||||
bulk_load_queue = new (function BulkLoadQueue() {
|
function petThumbnailUrl(pet_name) {
|
||||||
var RECENTLY_SENT_INTERVAL_IN_SECONDS = 30;
|
// if first character is "@", use the hash url
|
||||||
var RECENTLY_SENT_MAX = 3;
|
if (pet_name[0] == "@") {
|
||||||
var pets = [],
|
return "https://pets.neopets.com/cp/" + pet_name.substr(1) + "/1/1.png";
|
||||||
url = form.attr("action") + ".json",
|
}
|
||||||
recently_sent_count = 0,
|
|
||||||
loading = false;
|
|
||||||
|
|
||||||
function Pet(name) {
|
return "https://pets.neopets.com/cpn/" + pet_name + "/1/1.png";
|
||||||
var el = $("#bulk-pets-submission-template")
|
}
|
||||||
.tmpl({ pet_name: name })
|
|
||||||
.appendTo(queue_el);
|
|
||||||
|
|
||||||
this.load = function () {
|
bulk_load_queue = new (function BulkLoadQueue() {
|
||||||
el.removeClass("waiting").addClass("loading");
|
var RECENTLY_SENT_INTERVAL_IN_SECONDS = 30;
|
||||||
var response_el = el.find("span.response");
|
var RECENTLY_SENT_MAX = 3;
|
||||||
pets.shift();
|
var pets = [],
|
||||||
loading = true;
|
url = form.attr("action") + ".json",
|
||||||
$.ajax({
|
recently_sent_count = 0,
|
||||||
complete: function (data) {
|
loading = false;
|
||||||
loading = false;
|
|
||||||
loadNextIfReady();
|
|
||||||
},
|
|
||||||
data: { name: name },
|
|
||||||
dataType: "json",
|
|
||||||
error: function (xhr) {
|
|
||||||
el.removeClass("loading").addClass("failed");
|
|
||||||
response_el.text(xhr.responseText);
|
|
||||||
},
|
|
||||||
success: function (data) {
|
|
||||||
var points = data.points;
|
|
||||||
el.removeClass("loading").addClass("loaded");
|
|
||||||
$("#bulk-pets-submission-success-template")
|
|
||||||
.tmpl({ points: points })
|
|
||||||
.appendTo(response_el);
|
|
||||||
},
|
|
||||||
type: "post",
|
|
||||||
url: url,
|
|
||||||
});
|
|
||||||
|
|
||||||
recently_sent_count++;
|
function Pet(name) {
|
||||||
setTimeout(function () {
|
var el = $("#bulk-pets-submission-template")
|
||||||
recently_sent_count--;
|
.tmpl({ pet_name: name, pet_thumbnail: petThumbnailUrl(name) })
|
||||||
loadNextIfReady();
|
.appendTo(queue_el);
|
||||||
}, RECENTLY_SENT_INTERVAL_IN_SECONDS * 1000);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.add = function (name) {
|
this.load = function () {
|
||||||
name = name.replace(/^\s+|\s+$/g, "");
|
el.removeClass("waiting").addClass("loading");
|
||||||
if (name.length) {
|
var response_el = el.find("span.response");
|
||||||
var pet = new Pet(name);
|
pets.shift();
|
||||||
pets.push(pet);
|
loading = true;
|
||||||
if (pets.length == 1) loadNextIfReady();
|
$.ajax({
|
||||||
}
|
beforeSend: (xhr) => {
|
||||||
};
|
const token = document
|
||||||
|
.querySelector('meta[name="csrf-token"]')
|
||||||
|
?.getAttribute("content");
|
||||||
|
xhr.setRequestHeader("X-CSRF-Token", token);
|
||||||
|
},
|
||||||
|
complete: function (data) {
|
||||||
|
loading = false;
|
||||||
|
loadNextIfReady();
|
||||||
|
},
|
||||||
|
data: { name: name },
|
||||||
|
dataType: "json",
|
||||||
|
error: function (xhr) {
|
||||||
|
el.removeClass("loading").addClass("failed");
|
||||||
|
response_el.text(xhr.responseText);
|
||||||
|
},
|
||||||
|
success: function (data) {
|
||||||
|
var points = data.points;
|
||||||
|
el.removeClass("loading").addClass("loaded");
|
||||||
|
$("#bulk-pets-submission-success-template")
|
||||||
|
.tmpl({ points: points })
|
||||||
|
.appendTo(response_el);
|
||||||
|
},
|
||||||
|
type: "post",
|
||||||
|
url: url,
|
||||||
|
});
|
||||||
|
|
||||||
function loadNextIfReady() {
|
recently_sent_count++;
|
||||||
if (!loading && recently_sent_count < RECENTLY_SENT_MAX && pets.length) {
|
setTimeout(function () {
|
||||||
pets[0].load();
|
recently_sent_count--;
|
||||||
}
|
loadNextIfReady();
|
||||||
}
|
}, RECENTLY_SENT_INTERVAL_IN_SECONDS * 1000);
|
||||||
})();
|
};
|
||||||
|
}
|
||||||
|
|
||||||
names_el.keyup(function () {
|
this.add = function (name) {
|
||||||
var names = this.value.split("\n"),
|
name = name.replace(/^\s+|\s+$/g, "");
|
||||||
x = names.length - 1,
|
if (name.length) {
|
||||||
i,
|
var pet = new Pet(name);
|
||||||
name;
|
pets.push(pet);
|
||||||
for (i = 0; i < x; i++) {
|
if (pets.length == 1) loadNextIfReady();
|
||||||
bulk_load_queue.add(names[i]);
|
}
|
||||||
}
|
};
|
||||||
this.value = x >= 0 ? names[x] : "";
|
|
||||||
});
|
|
||||||
|
|
||||||
add_el.click(function () {
|
function loadNextIfReady() {
|
||||||
bulk_load_queue.add(names_el.val());
|
if (!loading && recently_sent_count < RECENTLY_SENT_MAX && pets.length) {
|
||||||
names_el.val("");
|
pets[0].load();
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
clear_el.click(function () {
|
names_el.keyup(function () {
|
||||||
queue_el.children("li.loaded, li.failed").remove();
|
var names = this.value.split("\n"),
|
||||||
});
|
x = names.length - 1,
|
||||||
|
i,
|
||||||
|
name;
|
||||||
|
for (i = 0; i < x; i++) {
|
||||||
|
bulk_load_queue.add(names[i]);
|
||||||
|
}
|
||||||
|
this.value = x >= 0 ? names[x] : "";
|
||||||
|
});
|
||||||
|
|
||||||
|
add_el.click(function () {
|
||||||
|
bulk_load_queue.add(names_el.val());
|
||||||
|
names_el.val("");
|
||||||
|
});
|
||||||
|
|
||||||
|
clear_el.click(function () {
|
||||||
|
queue_el.children("li.loaded, li.failed").remove();
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ let numFramesSinceLastLog = 0;
|
||||||
// State for error reporting.
|
// State for error reporting.
|
||||||
let hasLoggedRenderError = false;
|
let hasLoggedRenderError = false;
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
//////// Loading the library and its assets ////////
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
|
||||||
function loadImage(src) {
|
function loadImage(src) {
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
image.crossOrigin = "anonymous";
|
image.crossOrigin = "anonymous";
|
||||||
|
|
@ -64,8 +68,8 @@ async function getLibrary() {
|
||||||
// One more loading step as part of loading this library is loading the
|
// One more loading step as part of loading this library is loading the
|
||||||
// images it uses for sprites.
|
// images it uses for sprites.
|
||||||
//
|
//
|
||||||
// TODO: I guess the manifest has these too, so we could put them in preload
|
// NOTE: We also read these from the manifest, and include them in the
|
||||||
// meta tags to get them here faster?
|
// document as preload meta tags, to get them moving faster.
|
||||||
const librarySrcDir = libraryUrl.split("/").slice(0, -1).join("/");
|
const librarySrcDir = libraryUrl.split("/").slice(0, -1).join("/");
|
||||||
const manifestImages = new Map(
|
const manifestImages = new Map(
|
||||||
library.properties.manifest.map(({ id, src }) => [
|
library.properties.manifest.map(({ id, src }) => [
|
||||||
|
|
@ -96,6 +100,10 @@ async function getLibrary() {
|
||||||
return library;
|
return library;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////
|
||||||
|
//////// Rendering the movie ////////
|
||||||
|
/////////////////////////////////////
|
||||||
|
|
||||||
function buildMovieClip(library) {
|
function buildMovieClip(library) {
|
||||||
let constructorName;
|
let constructorName;
|
||||||
try {
|
try {
|
||||||
|
|
@ -151,6 +159,22 @@ function updateCanvasDimensions() {
|
||||||
movieClip.scaleY = internalHeight / library.properties.height;
|
movieClip.scaleY = internalHeight / library.properties.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
updateCanvasDimensions();
|
||||||
|
|
||||||
|
// Redraw the stage with the new dimensions - but with `tickOnUpdate` set
|
||||||
|
// to `false`, so that we don't advance by a frame. This keeps us
|
||||||
|
// really-paused if we're paused, and avoids skipping ahead by a frame if
|
||||||
|
// we're playing.
|
||||||
|
stage.tickOnUpdate = false;
|
||||||
|
updateStage();
|
||||||
|
stage.tickOnUpdate = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
//// Monitoring and controlling animation state ////
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
|
||||||
async function startMovie() {
|
async function startMovie() {
|
||||||
// Load the movie's library (from the JS file already run), and use it to
|
// Load the movie's library (from the JS file already run), and use it to
|
||||||
// build a movie clip.
|
// build a movie clip.
|
||||||
|
|
@ -274,6 +298,10 @@ function getInitialPlayingStatus() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////
|
||||||
|
//// Syncing with the parent document ////
|
||||||
|
//////////////////////////////////////////
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively scans the given MovieClip (or child createjs node), to see if
|
* Recursively scans the given MovieClip (or child createjs node), to see if
|
||||||
* there are any animated areas.
|
* there are any animated areas.
|
||||||
|
|
@ -312,18 +340,6 @@ function sendMessage(message) {
|
||||||
parent.postMessage(message, document.location.origin);
|
parent.postMessage(message, document.location.origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
updateCanvasDimensions();
|
|
||||||
|
|
||||||
// Redraw the stage with the new dimensions - but with `tickOnUpdate` set
|
|
||||||
// to `false`, so that we don't advance by a frame. This keeps us
|
|
||||||
// really-paused if we're paused, and avoids skipping ahead by a frame if
|
|
||||||
// we're playing.
|
|
||||||
stage.tickOnUpdate = false;
|
|
||||||
updateStage();
|
|
||||||
stage.tickOnUpdate = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("message", ({ data }) => {
|
window.addEventListener("message", ({ data }) => {
|
||||||
// NOTE: For more sensitive messages, it's important for security to also
|
// NOTE: For more sensitive messages, it's important for security to also
|
||||||
// check the `origin` property of the incoming event. But in this case, I'm
|
// check the `origin` property of the incoming event. But in this case, I'm
|
||||||
|
|
@ -339,6 +355,10 @@ window.addEventListener("message", ({ data }) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/////////////////////////////////
|
||||||
|
//// The actual entry point! ////
|
||||||
|
/////////////////////////////////
|
||||||
|
|
||||||
startMovie()
|
startMovie()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
sendStatus();
|
sendStatus();
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
@import "partials/campaign-progress"
|
|
||||||
|
|
||||||
body.items-index, body.items-show, body.items-needed, body.item_trades
|
|
||||||
+campaign-progress
|
|
||||||
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
input[type=text]
|
|
||||||
font-size: 125%
|
|
||||||
width: 15em
|
|
||||||
|
|
||||||
h1
|
|
||||||
margin-bottom: 1em
|
|
||||||
img
|
|
||||||
height: 80px
|
|
||||||
margin-bottom: -0.5em
|
|
||||||
width: 80px
|
|
||||||
a
|
|
||||||
text-decoration: none
|
|
||||||
span
|
|
||||||
text-decoration: underline
|
|
||||||
&:hover span
|
|
||||||
text-decoration: none
|
|
||||||
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
@import "partials/icon"
|
@import "partials/icon"
|
||||||
@import "partials/clean/constants"
|
@import "partials/clean/constants"
|
||||||
@import "partials/clean/mixins"
|
@import "partials/clean/mixins"
|
||||||
@import fonts
|
|
||||||
@import url("https://fonts.googleapis.com/css?family=Droid+Sans:400,700")
|
|
||||||
@import url("https://fonts.googleapis.com/css?family=Droid+Serif:400,700,400italic")
|
|
||||||
@import url("https://fonts.googleapis.com/css?family=Calligraffitti")
|
|
||||||
|
|
||||||
/* Reset
|
/* Reset
|
||||||
|
|
||||||
|
|
@ -36,9 +32,6 @@ body
|
||||||
a[href]
|
a[href]
|
||||||
color: $link-color
|
color: $link-color
|
||||||
|
|
||||||
p
|
|
||||||
font-family: $text-font
|
|
||||||
|
|
||||||
input, button, select
|
input, button, select
|
||||||
font:
|
font:
|
||||||
family: inherit
|
family: inherit
|
||||||
|
|
@ -81,7 +74,7 @@ $container_width: 800px
|
||||||
input, button, select, label
|
input, button, select, label
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
input[type=text], input[type=password], input[type=search], input[type=number], input[type=email], select, textarea
|
input[type=text], input[type=password], input[type=search], input[type=number], input[type=email], input[type=url], select, textarea
|
||||||
border-radius: 3px
|
border-radius: 3px
|
||||||
background: #fff
|
background: #fff
|
||||||
border: 1px solid $input-border-color
|
border: 1px solid $input-border-color
|
||||||
|
|
@ -90,6 +83,15 @@ input[type=text], input[type=password], input[type=search], input[type=number],
|
||||||
&:focus, &:active
|
&:focus, &:active
|
||||||
color: inherit
|
color: inherit
|
||||||
|
|
||||||
|
select:has(option[value='']:checked)
|
||||||
|
color: #666
|
||||||
|
|
||||||
|
option[value='']
|
||||||
|
color: #666
|
||||||
|
|
||||||
|
option:not([value=''])
|
||||||
|
color: $text-color
|
||||||
|
|
||||||
textarea
|
textarea
|
||||||
font: inherit
|
font: inherit
|
||||||
|
|
||||||
|
|
@ -250,23 +252,3 @@ dd
|
||||||
margin: 0 .5em
|
margin: 0 .5em
|
||||||
.current
|
.current
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
|
|
||||||
/* Fonts
|
|
||||||
|
|
||||||
/* A font by Jos Buivenga (exljbris) -> www.exljbris.nl
|
|
||||||
@font-face
|
|
||||||
font-family: Delicious
|
|
||||||
src: local("Delicious"), font-url("Delicious-Roman.otf")
|
|
||||||
|
|
||||||
|
|
||||||
@font-face
|
|
||||||
font-family: Delicious
|
|
||||||
font-weight: bold
|
|
||||||
src: local("Delicious"), font-url("Delicious-Bold.otf")
|
|
||||||
|
|
||||||
|
|
||||||
@font-face
|
|
||||||
font-family: Delicious
|
|
||||||
font-style: italic
|
|
||||||
src: local("Delicious"), font-url("Delicious-Italic.otf")
|
|
||||||
|
|
||||||
|
|
|
||||||
22
app/assets/stylesheets/_responsive.sass
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
body.use-responsive-design
|
||||||
|
#container
|
||||||
|
max-width: 100%
|
||||||
|
padding-inline: 1rem
|
||||||
|
box-sizing: border-box
|
||||||
|
padding-top: 0
|
||||||
|
|
||||||
|
#main-nav
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
|
|
||||||
|
#home-link, #userbar
|
||||||
|
position: static
|
||||||
|
|
||||||
|
#home-link
|
||||||
|
padding-inline: .5rem
|
||||||
|
margin-inline: -.5rem
|
||||||
|
margin-right: auto
|
||||||
|
|
||||||
|
#userbar
|
||||||
|
margin-left: auto
|
||||||
|
text-align: right
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
body.alt_styles-index
|
|
||||||
.alt-styles-header
|
|
||||||
margin-top: 1em
|
|
||||||
margin-bottom: .5em
|
|
||||||
|
|
||||||
.alt-styles-list
|
|
||||||
list-style: none
|
|
||||||
display: flex
|
|
||||||
flex-wrap: wrap
|
|
||||||
gap: 1.5em
|
|
||||||
|
|
||||||
.alt-style
|
|
||||||
text-align: center
|
|
||||||
width: 80px
|
|
||||||
|
|
||||||
.alt-style-thumbnail
|
|
||||||
width: 80px
|
|
||||||
height: 80px
|
|
||||||
4
app/assets/stylesheets/alt_styles/edit.sass
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.alt-style-preview
|
||||||
|
width: 300px
|
||||||
|
height: 300px
|
||||||
|
margin: 0 auto
|
||||||
13
app/assets/stylesheets/alt_styles/index.sass
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
@import "../partials/clean/constants"
|
||||||
|
|
||||||
|
// Prefer to break the name at visually appealing points.
|
||||||
|
.rainbow-pool-list
|
||||||
|
.name
|
||||||
|
text-wrap: balance
|
||||||
|
|
||||||
|
// De-emphasize Prismatic styles, in browsers that support it.
|
||||||
|
.rainbow-pool-filters
|
||||||
|
select[name="series"]
|
||||||
|
option[value*=": "]
|
||||||
|
color: $soft-text-color
|
||||||
|
font-style: italic
|
||||||
|
|
@ -4,19 +4,14 @@
|
||||||
@import partials/clean/mixins
|
@import partials/clean/mixins
|
||||||
|
|
||||||
@import layout
|
@import layout
|
||||||
|
@import responsive
|
||||||
|
|
||||||
@import partials/jquery.jgrowl
|
@import partials/jquery.jgrowl
|
||||||
|
|
||||||
@import alt_styles/index
|
|
||||||
@import closet_hangers/index
|
@import closet_hangers/index
|
||||||
@import closet_hangers/petpage
|
|
||||||
@import closet_lists/form
|
@import closet_lists/form
|
||||||
@import neopets_page_import_tasks/new
|
@import neopets_page_import_tasks/new
|
||||||
@import contributions/index
|
@import contributions/index
|
||||||
@import items
|
|
||||||
@import items/index
|
|
||||||
@import items/show
|
|
||||||
@import item_trades/index
|
|
||||||
@import outfits/index
|
@import outfits/index
|
||||||
@import outfits/new
|
@import outfits/new
|
||||||
@import pets/bulk
|
@import pets/bulk
|
||||||
|
|
|
||||||
23
app/assets/stylesheets/application/breadcrumbs.sass
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#title:has(+ .breadcrumbs)
|
||||||
|
margin-bottom: .125em
|
||||||
|
|
||||||
|
.breadcrumbs
|
||||||
|
list-style-type: none
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
margin-block: .5em
|
||||||
|
font-size: .85em
|
||||||
|
|
||||||
|
li
|
||||||
|
display: flex
|
||||||
|
|
||||||
|
li:not(:first-child)
|
||||||
|
&::before
|
||||||
|
margin-inline: .35em
|
||||||
|
content: "→"
|
||||||
|
|
||||||
|
&[data-relation-to-prev=sibling]::before
|
||||||
|
content: "+"
|
||||||
|
|
||||||
|
&[data-relation-to-prev=menu]::before
|
||||||
|
content: "-"
|
||||||
50
app/assets/stylesheets/application/magic-magnifier.sass
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
magic-magnifier
|
||||||
|
display: block
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
// Only show the lens when we are hovering, and the magnifier's X and Y
|
||||||
|
// coordinates are set. (This ensures the component is running, and has
|
||||||
|
// received a mousemove event, instead of defaulting to (0, 0).)
|
||||||
|
magic-magnifier-lens
|
||||||
|
display: none
|
||||||
|
|
||||||
|
// TODO: Once container query support is broader, we can remove the CSS state
|
||||||
|
// and read for the presence of the X and Y custom properties instead.
|
||||||
|
&:hover:state(ready)
|
||||||
|
magic-magnifier-lens
|
||||||
|
display: block
|
||||||
|
|
||||||
|
magic-magnifier-lens
|
||||||
|
display: block
|
||||||
|
width: var(--magic-magnifier-lens-width, 100px)
|
||||||
|
height: var(--magic-magnifier-lens-height, 100px)
|
||||||
|
overflow: hidden
|
||||||
|
border-radius: 100%
|
||||||
|
|
||||||
|
background: white
|
||||||
|
border: 2px solid black
|
||||||
|
box-shadow: 3px 3px 3px rgba(0, 0, 0, .5)
|
||||||
|
|
||||||
|
position: absolute
|
||||||
|
left: var(--magic-magnifier-x, 0px)
|
||||||
|
top: var(--magic-magnifier-y, 0px)
|
||||||
|
|
||||||
|
> *
|
||||||
|
// Translations are applied in the opposite of the order they're specified.
|
||||||
|
// So, here's what we're doing:
|
||||||
|
//
|
||||||
|
// 1. Translate the content left by --magic-magnifier-x and up by
|
||||||
|
// --magic-magnifier-y, to align the target location with the lens's
|
||||||
|
// top-right corner.
|
||||||
|
// 2. Zoom in by --magic-magnifier-scale.
|
||||||
|
// 3. Translate the content right by half of --magic-magnifier-lens-width,
|
||||||
|
// and down by half of --magic-magnifier-lens-height, to align the
|
||||||
|
// target location with the lens's center.
|
||||||
|
//
|
||||||
|
// Note that it *is* possible to specify transforms relative to the center,
|
||||||
|
// rather than the top-left corner—this is in fact the default!—but that
|
||||||
|
// gets confusing fast with scale in play. I think this is easier to reason
|
||||||
|
// about with the top-left corner in terms of math, and center it after the
|
||||||
|
// fact.
|
||||||
|
transform: translateX(calc(var(--magic-magnifier-lens-width, 100px) / 2)) translateY(calc(var(--magic-magnifier-lens-height, 100px) / 2)) scale(var(--magic-magnifier-scale, 2)) translateX(calc(-1 * var(--magic-magnifier-x, 0px))) translateY(calc(-1 * var(--magic-magnifier-y, 0px)))
|
||||||
|
transform-origin: left top
|
||||||
125
app/assets/stylesheets/application/outfit-viewer.sass
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
@import "../partials/clean/constants"
|
||||||
|
|
||||||
|
// When loading, fade in the loading spinner after a brief delay. We only apply
|
||||||
|
// the delay here, not on the base styles, because fading *out* on load should
|
||||||
|
// be instant.
|
||||||
|
//
|
||||||
|
// This is implemented as a mixin, so that the item page can leverage the same
|
||||||
|
// loading state when loading a new preview altogether. Once CSS container
|
||||||
|
// style queries gain wider support, maybe use that instead.
|
||||||
|
=outfit-viewer-loading
|
||||||
|
cursor: wait
|
||||||
|
|
||||||
|
.loading-indicator
|
||||||
|
opacity: 1
|
||||||
|
transition-delay: 2s
|
||||||
|
|
||||||
|
// If the outfit *starts* in loading state, still delay the fade-in.
|
||||||
|
@starting-style
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
outfit-viewer
|
||||||
|
display: block
|
||||||
|
position: relative
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
// These are default widths, expected to often be overridden.
|
||||||
|
width: 300px
|
||||||
|
height: 300px
|
||||||
|
|
||||||
|
// There's no useful text in here, but double-clicking the play/pause
|
||||||
|
// button can cause a weird selection state. Disable text selection.
|
||||||
|
user-select: none
|
||||||
|
-webkit-user-select: none
|
||||||
|
|
||||||
|
outfit-layer
|
||||||
|
display: block
|
||||||
|
position: absolute
|
||||||
|
inset: 0
|
||||||
|
|
||||||
|
// We disable pointer-events most importantly for the iframes, which
|
||||||
|
// will ignore our `cursor: wait` and show a plain cursor for the
|
||||||
|
// inside of its own document. But also, the context menus for these
|
||||||
|
// elements are kinda actively misleading, too!
|
||||||
|
pointer-events: none
|
||||||
|
|
||||||
|
img, iframe
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
|
||||||
|
.loading-indicator
|
||||||
|
position: absolute
|
||||||
|
z-index: 1000
|
||||||
|
bottom: 0px
|
||||||
|
right: 4px
|
||||||
|
padding: 8px
|
||||||
|
background: radial-gradient(circle closest-side, white 45%, #ffffff00)
|
||||||
|
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
.play-pause-button
|
||||||
|
position: absolute
|
||||||
|
z-index: 1001
|
||||||
|
left: 8px
|
||||||
|
bottom: 8px
|
||||||
|
display: none
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
color: white
|
||||||
|
background: rgba(0, 0, 0, 0.64)
|
||||||
|
width: 2.5em
|
||||||
|
height: 2.5em
|
||||||
|
border-radius: 100%
|
||||||
|
border: 2px solid transparent
|
||||||
|
transition: all .25s
|
||||||
|
|
||||||
|
.playing-label, .paused-label
|
||||||
|
display: none
|
||||||
|
width: 1em
|
||||||
|
height: 1em
|
||||||
|
|
||||||
|
.play-pause-toggle
|
||||||
|
// Visually hidden
|
||||||
|
clip: rect(0 0 0 0)
|
||||||
|
clip-path: inset(50%)
|
||||||
|
height: 1px
|
||||||
|
overflow: hidden
|
||||||
|
position: absolute
|
||||||
|
white-space: nowrap
|
||||||
|
width: 1px
|
||||||
|
|
||||||
|
&:checked ~ .playing-label
|
||||||
|
display: block
|
||||||
|
|
||||||
|
&:not(:checked) ~ .paused-label
|
||||||
|
display: block
|
||||||
|
|
||||||
|
&:hover, &:has(.play-pause-toggle:focus)
|
||||||
|
border: 2px solid $module-border-color
|
||||||
|
background: $module-bg-color
|
||||||
|
color: $text-color
|
||||||
|
|
||||||
|
&:has(.play-pause-toggle:active)
|
||||||
|
transform: translateY(2px)
|
||||||
|
|
||||||
|
&:has(outfit-layer:state(has-animations))
|
||||||
|
.play-pause-button
|
||||||
|
display: flex
|
||||||
|
|
||||||
|
&:has(outfit-layer:state(loading))
|
||||||
|
+outfit-viewer-loading
|
||||||
|
|
||||||
|
// If a layer has the `[highlighted]` attribute, it's brought to the front,
|
||||||
|
// and other layers are grayed out and blurred. We use this in the support
|
||||||
|
// outfit viewer, when you hover over a layer.
|
||||||
|
&:has(outfit-layer[highlighted])
|
||||||
|
outfit-layer[highlighted]
|
||||||
|
z-index: 999
|
||||||
|
|
||||||
|
// Filter everything behind the bottom-most highlighted layer, using a
|
||||||
|
// backdrop filter. This gives us the best visual consistency by applying
|
||||||
|
// effects to the entire backdrop, instead of each layer and then
|
||||||
|
// re-compositing them.
|
||||||
|
backdrop-filter: grayscale(1) brightness(2) blur(1px)
|
||||||
|
& ~ outfit-layer[highlighted]
|
||||||
|
backdrop-filter: none
|
||||||
74
app/assets/stylesheets/application/rainbow-pool.sass
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
@import "../partials/clean/constants"
|
||||||
|
|
||||||
|
.rainbow-pool-filters
|
||||||
|
margin-block: .5em
|
||||||
|
|
||||||
|
fieldset
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
gap: .5em
|
||||||
|
|
||||||
|
legend
|
||||||
|
display: contents
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
select
|
||||||
|
width: 16ch
|
||||||
|
|
||||||
|
.rainbow-pool-list
|
||||||
|
list-style-type: none
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
|
justify-content: center
|
||||||
|
gap: .5em
|
||||||
|
|
||||||
|
--preview-base-width: 150px
|
||||||
|
|
||||||
|
> li
|
||||||
|
width: var(--preview-base-width)
|
||||||
|
max-width: calc(50% - .25em)
|
||||||
|
min-width: 150px
|
||||||
|
box-sizing: border-box
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
a
|
||||||
|
display: block
|
||||||
|
border-radius: 1em
|
||||||
|
padding: .5em
|
||||||
|
text-decoration: none
|
||||||
|
background: white
|
||||||
|
&:hover
|
||||||
|
outline: 1px solid $module-border-color
|
||||||
|
background: $module-bg-color
|
||||||
|
|
||||||
|
.preview
|
||||||
|
width: 100%
|
||||||
|
height: auto
|
||||||
|
aspect-ratio: 1 / 1
|
||||||
|
margin-bottom: -1em
|
||||||
|
|
||||||
|
.name
|
||||||
|
background: inherit
|
||||||
|
padding: .25em .5em
|
||||||
|
border-radius: .5em
|
||||||
|
margin: 0 auto
|
||||||
|
position: relative
|
||||||
|
z-index: 1
|
||||||
|
|
||||||
|
.info
|
||||||
|
font-size: .85em
|
||||||
|
p
|
||||||
|
margin-block: .25em
|
||||||
|
|
||||||
|
.rainbow-pool-pagination
|
||||||
|
margin-block: .5em
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
gap: 1em
|
||||||
|
|
||||||
|
.rainbow-pool-no-results
|
||||||
|
margin-block: 1em
|
||||||
|
text-align: center
|
||||||
|
font-style: italic
|
||||||
102
app/assets/stylesheets/application/support-form.sass
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
@import "../partials/clean/constants"
|
||||||
|
|
||||||
|
.support-form
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: 1em
|
||||||
|
align-items: flex-start
|
||||||
|
|
||||||
|
.fields
|
||||||
|
list-style-type: none
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: .75em
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
> li
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: .25em
|
||||||
|
max-width: 60ch
|
||||||
|
|
||||||
|
> label, > .field_with_errors label
|
||||||
|
display: block
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
.field_with_errors
|
||||||
|
> label
|
||||||
|
color: $error-color
|
||||||
|
|
||||||
|
input[type=text], input[type=url]
|
||||||
|
border-color: $error-border-color
|
||||||
|
color: $error-color
|
||||||
|
|
||||||
|
&[data-type=radio]
|
||||||
|
ul
|
||||||
|
list-style-type: none
|
||||||
|
|
||||||
|
&[data-type=radio-grid] // Set the `--num-columns` property to configure!
|
||||||
|
max-width: none
|
||||||
|
|
||||||
|
ul
|
||||||
|
list-style-type: none
|
||||||
|
display: grid
|
||||||
|
grid-template-columns: repeat(var(--num-columns, 1), 1fr)
|
||||||
|
gap: .25em
|
||||||
|
|
||||||
|
li
|
||||||
|
display: flex
|
||||||
|
align-items: stretch // Give the bubbles equal heights!
|
||||||
|
|
||||||
|
label
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: .5em
|
||||||
|
padding: .5em 1em
|
||||||
|
border: 1px solid $soft-border-color
|
||||||
|
border-radius: 1em
|
||||||
|
flex: 1 1 auto
|
||||||
|
|
||||||
|
input
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
&:has(:checked)
|
||||||
|
background: $module-bg-color
|
||||||
|
border-color: $module-border-color
|
||||||
|
|
||||||
|
input[type=text], input[type=url]
|
||||||
|
width: 100%
|
||||||
|
min-width: 10ch
|
||||||
|
box-sizing: border-box
|
||||||
|
|
||||||
|
.thumbnail-input
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: .25em
|
||||||
|
|
||||||
|
img
|
||||||
|
width: 40px
|
||||||
|
height: 40px
|
||||||
|
|
||||||
|
fieldset
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: .25em
|
||||||
|
|
||||||
|
legend
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
.field_with_errors
|
||||||
|
display: contents
|
||||||
|
|
||||||
|
.actions
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 1em
|
||||||
|
|
||||||
|
.go-to-next
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: .25em
|
||||||
|
font-size: .85em
|
||||||
|
font-style: italic
|
||||||
|
|
@ -31,11 +31,14 @@ body.closet_hangers-index
|
||||||
color: $soft-text-color
|
color: $soft-text-color
|
||||||
margin-bottom: 1em
|
margin-bottom: 1em
|
||||||
margin-left: 2em
|
margin-left: 2em
|
||||||
min-height: image-height("neomail.png")
|
min-height: $icon-height
|
||||||
|
|
||||||
|
display: flex
|
||||||
|
gap: .5em
|
||||||
|
align-items: center
|
||||||
|
|
||||||
a
|
a
|
||||||
color: inherit
|
color: inherit
|
||||||
margin-right: .5em
|
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
&:hover
|
&:hover
|
||||||
text-decoration: underline
|
text-decoration: underline
|
||||||
|
|
@ -44,13 +47,14 @@ body.closet_hangers-index
|
||||||
background:
|
background:
|
||||||
position: left center
|
position: left center
|
||||||
repeat: no-repeat
|
repeat: no-repeat
|
||||||
padding-left: image-width("neomail.png") + 4px
|
|
||||||
|
|
||||||
a.neomail, > form
|
a.neomail, > form
|
||||||
background-image: image-url("neomail.png")
|
background-image: image-url("neomail.png")
|
||||||
|
padding-left: $icon-width + 4px
|
||||||
|
|
||||||
a.lookup
|
a.lookup
|
||||||
background-image: image-url("lookup.png")
|
background-image: image-url("lookup.png")
|
||||||
|
padding-left: $icon-width + 4px
|
||||||
|
|
||||||
select
|
select
|
||||||
width: 10em
|
width: 10em
|
||||||
|
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
@import "../partials/clean/constants"
|
|
||||||
@import "../partials/clean/mixins"
|
|
||||||
@import "../partials/secondary_nav"
|
|
||||||
|
|
||||||
body.closet_hangers-petpage
|
|
||||||
+secondary-nav
|
|
||||||
|
|
||||||
#intro
|
|
||||||
clear: both
|
|
||||||
|
|
||||||
#petpage-closet-lists
|
|
||||||
+clearfix
|
|
||||||
border-radius: 10px
|
|
||||||
border: 1px solid $soft-border-color
|
|
||||||
margin-bottom: 1.5em
|
|
||||||
padding: .5em 1.5em
|
|
||||||
|
|
||||||
> div
|
|
||||||
margin: .25em 0
|
|
||||||
|
|
||||||
h4
|
|
||||||
display: inline-block
|
|
||||||
vertical-align: middle
|
|
||||||
|
|
||||||
&::after
|
|
||||||
content: ":"
|
|
||||||
|
|
||||||
ul
|
|
||||||
list-style: none
|
|
||||||
margin: 0
|
|
||||||
padding: 0
|
|
||||||
|
|
||||||
li
|
|
||||||
display: inline-block
|
|
||||||
font-size: 85%
|
|
||||||
margin: .25em .5em
|
|
||||||
padding: 1px
|
|
||||||
|
|
||||||
label
|
|
||||||
padding: .25em .75em .25em .25em
|
|
||||||
|
|
||||||
&.checked
|
|
||||||
background: $module-bg-color
|
|
||||||
border-radius: 3px
|
|
||||||
border: 1px solid $module-border-color
|
|
||||||
padding: 0
|
|
||||||
|
|
||||||
&.unlisted
|
|
||||||
font-style: italic
|
|
||||||
|
|
||||||
input[type=submit]
|
|
||||||
float: right
|
|
||||||
|
|
||||||
#petpage-output
|
|
||||||
display: block
|
|
||||||
height: 30em
|
|
||||||
margin: 0 auto
|
|
||||||
width: 50%
|
|
||||||
57
app/assets/stylesheets/closet_hangers/petpage.sass
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
@import "../partials/clean/constants"
|
||||||
|
@import "../partials/clean/mixins"
|
||||||
|
@import "../partials/secondary_nav"
|
||||||
|
|
||||||
|
+secondary-nav
|
||||||
|
|
||||||
|
#intro
|
||||||
|
clear: both
|
||||||
|
|
||||||
|
#petpage-closet-lists
|
||||||
|
+clearfix
|
||||||
|
border-radius: 10px
|
||||||
|
border: 1px solid $soft-border-color
|
||||||
|
margin-bottom: 1.5em
|
||||||
|
padding: .5em 1.5em
|
||||||
|
|
||||||
|
> div
|
||||||
|
margin: .25em 0
|
||||||
|
|
||||||
|
h4
|
||||||
|
display: inline-block
|
||||||
|
vertical-align: middle
|
||||||
|
|
||||||
|
&::after
|
||||||
|
content: ":"
|
||||||
|
|
||||||
|
ul
|
||||||
|
list-style: none
|
||||||
|
margin: 0
|
||||||
|
padding: 0
|
||||||
|
|
||||||
|
li
|
||||||
|
display: inline-block
|
||||||
|
font-size: 85%
|
||||||
|
margin: .25em .5em
|
||||||
|
padding: 1px
|
||||||
|
|
||||||
|
label
|
||||||
|
padding: .25em .75em .25em .25em
|
||||||
|
|
||||||
|
&:has(:checked)
|
||||||
|
background: $module-bg-color
|
||||||
|
border-radius: 3px
|
||||||
|
border: 1px solid $module-border-color
|
||||||
|
padding: 0
|
||||||
|
|
||||||
|
&.unlisted
|
||||||
|
font-style: italic
|
||||||
|
|
||||||
|
input[type=submit]
|
||||||
|
float: right
|
||||||
|
|
||||||
|
#petpage-output
|
||||||
|
display: block
|
||||||
|
height: 30em
|
||||||
|
margin: 0 auto
|
||||||
|
width: 50%
|
||||||
17
app/assets/stylesheets/fonts.css.erb
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
/* A font by Jos Buivenga (exljbris) -> www.exljbris.nl */
|
||||||
|
@font-face {
|
||||||
|
font-family: Delicious;
|
||||||
|
src: local("Delicious"), url("<%= font_path "Delicious-Roman.otf" %>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Delicious;
|
||||||
|
font-weight: bold;
|
||||||
|
src: local("Delicious"), url("<%= font_path "Delicious-Bold.otf" %>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Delicious;
|
||||||
|
font-style: italic;
|
||||||
|
src: local("Delicious"), url("<%= font_path "Delicious-Italic.otf" %>");
|
||||||
|
}
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
/* A font by Jos Buivenga (exljbris) -> www.exljbris.nl
|
|
||||||
@font-face
|
|
||||||
font-family: Delicious
|
|
||||||
src: local("Delicious"), font-url("Delicious-Roman.otf")
|
|
||||||
|
|
||||||
@font-face
|
|
||||||
font-family: Delicious
|
|
||||||
font-weight: bold
|
|
||||||
src: local("Delicious"), font-url("Delicious-Bold.otf")
|
|
||||||
|
|
||||||
@font-face
|
|
||||||
font-family: Delicious
|
|
||||||
font-style: italic
|
|
||||||
src: local("Delicious"), font-url("Delicious-Italic.otf")
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
@import "../partials/item_header"
|
|
||||||
|
|
||||||
body.item_trades-index
|
|
||||||
.item-header
|
|
||||||
+item-header
|
|
||||||
|
|
||||||
.item-subpage-title
|
|
||||||
text-align: left
|
|
||||||
margin-bottom: .5em
|
|
||||||
|
|
||||||
.trades-table
|
|
||||||
text-align: left
|
|
||||||
width: 100%
|
|
||||||
table-layout: fixed
|
|
||||||
|
|
||||||
th, td
|
|
||||||
&:nth-child(1), &:nth-child(2)
|
|
||||||
width: 15ch
|
|
||||||
overflow: hidden
|
|
||||||
text-overflow: ellipsis
|
|
||||||
|
|
||||||
.trade-list-names
|
|
||||||
list-style: none
|
|
||||||
|
|
||||||
li
|
|
||||||
display: inline
|
|
||||||
|
|
||||||
&:not(:last-child)::after
|
|
||||||
content: ", "
|
|
||||||
38
app/assets/stylesheets/item_trades/index.sass
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
@import "../partials/item_header"
|
||||||
|
|
||||||
|
.item-header
|
||||||
|
+item-header
|
||||||
|
|
||||||
|
.item-subpage-title
|
||||||
|
text-align: left
|
||||||
|
margin-bottom: .5em
|
||||||
|
|
||||||
|
.trades-table
|
||||||
|
text-align: left
|
||||||
|
width: 100%
|
||||||
|
table-layout: fixed
|
||||||
|
|
||||||
|
th, td
|
||||||
|
&:nth-child(1), &:nth-child(2)
|
||||||
|
width: 15ch
|
||||||
|
overflow: hidden
|
||||||
|
text-overflow: ellipsis
|
||||||
|
|
||||||
|
td[data-is-same-as-prev]
|
||||||
|
// Visually hidden
|
||||||
|
clip: rect(0 0 0 0)
|
||||||
|
clip-path: inset(50%)
|
||||||
|
height: 1px
|
||||||
|
overflow: hidden
|
||||||
|
position: absolute
|
||||||
|
white-space: nowrap
|
||||||
|
width: 1px
|
||||||
|
|
||||||
|
.trade-list-names
|
||||||
|
list-style: none
|
||||||
|
|
||||||
|
li
|
||||||
|
display: inline
|
||||||
|
|
||||||
|
&:not(:last-child)::after
|
||||||
|
content: ", "
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
=main_unit
|
|
||||||
float: left
|
|
||||||
width: 49%
|
|
||||||
h2
|
|
||||||
font-size: 125%
|
|
||||||
|
|
||||||
body.items-index
|
|
||||||
form
|
|
||||||
margin-bottom: 2em
|
|
||||||
|
|
||||||
#search-info
|
|
||||||
+main_unit
|
|
||||||
padding-right: 1%
|
|
||||||
dl
|
|
||||||
text-align: left
|
|
||||||
dd
|
|
||||||
margin-bottom: 1em
|
|
||||||
|
|
||||||
#species-search-links
|
|
||||||
+main_unit
|
|
||||||
padding-left: 1%
|
|
||||||
img
|
|
||||||
height: 80px
|
|
||||||
width: 80px
|
|
||||||
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
@import "../partials/clean/constants"
|
|
||||||
@import "../partials/clean/mixins"
|
|
||||||
@import "../partials/item_header"
|
|
||||||
|
|
||||||
body.items-show
|
|
||||||
.item-header
|
|
||||||
+item-header
|
|
||||||
|
|
||||||
#item-contributors
|
|
||||||
+subtle-banner
|
|
||||||
clear: both
|
|
||||||
margin:
|
|
||||||
bottom: 0
|
|
||||||
top: 2em
|
|
||||||
|
|
||||||
header
|
|
||||||
display: inline
|
|
||||||
font-weight: bold
|
|
||||||
margin-right: .25em
|
|
||||||
|
|
||||||
footer
|
|
||||||
display: inline
|
|
||||||
|
|
||||||
ul
|
|
||||||
display: inline
|
|
||||||
list-style: none
|
|
||||||
|
|
||||||
li
|
|
||||||
display: inline
|
|
||||||
|
|
||||||
&::after
|
|
||||||
content: ", "
|
|
||||||
|
|
||||||
&:last-child::after
|
|
||||||
content: "."
|
|
||||||
|
|
||||||
.nc-icon
|
|
||||||
height: 16px
|
|
||||||
width: 16px
|
|
||||||
|
|
||||||
outfit-viewer
|
|
||||||
display: block
|
|
||||||
position: relative
|
|
||||||
width: 300px
|
|
||||||
height: 300px
|
|
||||||
border: 1px solid $module-border-color
|
|
||||||
border-radius: 1em
|
|
||||||
overflow: hidden
|
|
||||||
margin: 0 auto .75em
|
|
||||||
|
|
||||||
// There's no useful text in here, but double-clicking the play/pause
|
|
||||||
// button can cause a weird selection state. Disable text selection.
|
|
||||||
user-select: none
|
|
||||||
-webkit-user-select: none
|
|
||||||
|
|
||||||
outfit-layer
|
|
||||||
display: block
|
|
||||||
position: absolute
|
|
||||||
inset: 0
|
|
||||||
|
|
||||||
// We disable pointer-events most importantly for the iframes, which
|
|
||||||
// will ignore our `cursor: wait` and show a plain cursor for the
|
|
||||||
// inside of its own document. But also, the context menus for these
|
|
||||||
// elements are kinda actively misleading, too!
|
|
||||||
pointer-events: none
|
|
||||||
|
|
||||||
img, iframe
|
|
||||||
width: 100%
|
|
||||||
height: 100%
|
|
||||||
|
|
||||||
.loading-indicator
|
|
||||||
position: absolute
|
|
||||||
z-index: 1000
|
|
||||||
bottom: 0px
|
|
||||||
right: 4px
|
|
||||||
padding: 8px
|
|
||||||
background: radial-gradient(circle closest-side, white 45%, #ffffff00)
|
|
||||||
|
|
||||||
opacity: 0
|
|
||||||
transition: opacity .5s
|
|
||||||
|
|
||||||
.play-pause-button
|
|
||||||
position: absolute
|
|
||||||
z-index: 1001
|
|
||||||
left: 8px
|
|
||||||
bottom: 8px
|
|
||||||
display: none
|
|
||||||
align-items: center
|
|
||||||
justify-content: center
|
|
||||||
color: white
|
|
||||||
background: rgba(0, 0, 0, 0.64)
|
|
||||||
width: 2.5em
|
|
||||||
height: 2.5em
|
|
||||||
border-radius: 100%
|
|
||||||
border: 2px solid transparent
|
|
||||||
transition: all .25s
|
|
||||||
|
|
||||||
.playing-label, .paused-label
|
|
||||||
display: none
|
|
||||||
width: 1em
|
|
||||||
height: 1em
|
|
||||||
|
|
||||||
.play-pause-toggle
|
|
||||||
// Visually hidden
|
|
||||||
clip: rect(0 0 0 0)
|
|
||||||
clip-path: inset(50%)
|
|
||||||
height: 1px
|
|
||||||
overflow: hidden
|
|
||||||
position: absolute
|
|
||||||
white-space: nowrap
|
|
||||||
width: 1px
|
|
||||||
|
|
||||||
&:checked ~ .playing-label
|
|
||||||
display: block
|
|
||||||
|
|
||||||
&:not(:checked) ~ .paused-label
|
|
||||||
display: block
|
|
||||||
|
|
||||||
&:hover, &:has(.play-pause-toggle:focus)
|
|
||||||
border: 2px solid $module-border-color
|
|
||||||
background: $module-bg-color
|
|
||||||
color: $text-color
|
|
||||||
|
|
||||||
&:has(.play-pause-toggle:active)
|
|
||||||
transform: translateY(2px)
|
|
||||||
|
|
||||||
&:has(outfit-layer:state(has-animations))
|
|
||||||
.play-pause-button
|
|
||||||
display: flex
|
|
||||||
|
|
||||||
.error-indicator
|
|
||||||
font-size: 85%
|
|
||||||
color: $error-color
|
|
||||||
margin-top: .25em
|
|
||||||
margin-bottom: .5em
|
|
||||||
display: none
|
|
||||||
|
|
||||||
// When loading, fade in the loading spinner after a brief delay. (We only
|
|
||||||
// apply the delay here, because fading *out* on load should be instant.)
|
|
||||||
// We are loading when the <turbo-frame> is busy, or when at least one layer
|
|
||||||
// is loading.
|
|
||||||
#item-preview[busy] outfit-viewer, outfit-viewer:has(outfit-layer:state(loading))
|
|
||||||
cursor: wait
|
|
||||||
.loading-indicator
|
|
||||||
opacity: 1
|
|
||||||
transition-delay: 2s
|
|
||||||
|
|
||||||
#item-preview:has(outfit-layer:state(error))
|
|
||||||
outfit-viewer
|
|
||||||
border: 2px solid red
|
|
||||||
.error-indicator
|
|
||||||
display: block
|
|
||||||
|
|
||||||
.species-color-picker
|
|
||||||
.error-icon
|
|
||||||
cursor: help
|
|
||||||
margin-right: .25em
|
|
||||||
|
|
||||||
&[data-is-valid="false"]
|
|
||||||
select
|
|
||||||
border-color: $error-border-color
|
|
||||||
color: $error-color
|
|
||||||
23
app/assets/stylesheets/items/index.sass
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
=main_unit
|
||||||
|
float: left
|
||||||
|
width: 49%
|
||||||
|
h2
|
||||||
|
font-size: 125%
|
||||||
|
|
||||||
|
form
|
||||||
|
margin-bottom: 2em
|
||||||
|
|
||||||
|
#search-info
|
||||||
|
+main_unit
|
||||||
|
padding-right: 1%
|
||||||
|
dl
|
||||||
|
text-align: left
|
||||||
|
dd
|
||||||
|
margin-bottom: 1em
|
||||||
|
|
||||||
|
#species-search-links
|
||||||
|
+main_unit
|
||||||
|
padding-left: 1%
|
||||||
|
img
|
||||||
|
height: 80px
|
||||||
|
width: 80px
|
||||||
309
app/assets/stylesheets/items/show.sass
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
@import "../partials/clean/constants"
|
||||||
|
@import "../partials/clean/mixins"
|
||||||
|
@import "../partials/item_header"
|
||||||
|
|
||||||
|
@import "../application/outfit-viewer"
|
||||||
|
|
||||||
|
#container
|
||||||
|
width: 900px // A bit more generous to the preview area!
|
||||||
|
|
||||||
|
.item-header
|
||||||
|
+item-header
|
||||||
|
|
||||||
|
#item-contributors
|
||||||
|
+subtle-banner
|
||||||
|
clear: both
|
||||||
|
margin:
|
||||||
|
bottom: 0
|
||||||
|
top: 2em
|
||||||
|
|
||||||
|
header
|
||||||
|
display: inline
|
||||||
|
font-weight: bold
|
||||||
|
margin-right: .25em
|
||||||
|
|
||||||
|
footer
|
||||||
|
display: inline
|
||||||
|
|
||||||
|
ul
|
||||||
|
display: inline
|
||||||
|
list-style: none
|
||||||
|
|
||||||
|
li
|
||||||
|
display: inline
|
||||||
|
|
||||||
|
&::after
|
||||||
|
content: ", "
|
||||||
|
|
||||||
|
&:last-child::after
|
||||||
|
content: "."
|
||||||
|
|
||||||
|
.nc-icon
|
||||||
|
height: 16px
|
||||||
|
width: 16px
|
||||||
|
|
||||||
|
.preview-area
|
||||||
|
margin: 0 auto
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
.customize-more
|
||||||
|
position: absolute
|
||||||
|
top: 1em
|
||||||
|
right: 1em
|
||||||
|
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
text-decoration: none
|
||||||
|
|
||||||
|
background: #EDF2F7
|
||||||
|
padding-inline: .75em
|
||||||
|
border-radius: .375em
|
||||||
|
min-height: 2rem
|
||||||
|
min-width: 2rem
|
||||||
|
box-sizing: border-box
|
||||||
|
|
||||||
|
.customize-more-label
|
||||||
|
width: 0
|
||||||
|
overflow: hidden
|
||||||
|
transition: width .25s
|
||||||
|
white-space: nowrap
|
||||||
|
--natural-width: auto
|
||||||
|
|
||||||
|
measured-content
|
||||||
|
padding-right: .5em
|
||||||
|
|
||||||
|
&:hover, &:focus
|
||||||
|
// Expand the label to its natural width. If the JS ran to tell us
|
||||||
|
// what it is in px, we can use that for a smooth transition. If not,
|
||||||
|
// okay, we just pop out to `auto`, which CSS can't make smooth.
|
||||||
|
.customize-more-label
|
||||||
|
width: var(--natural-width)
|
||||||
|
|
||||||
|
outfit-viewer
|
||||||
|
width: 300px
|
||||||
|
height: 300px
|
||||||
|
border: 1px solid $module-border-color
|
||||||
|
border-radius: 1em
|
||||||
|
|
||||||
|
.error-indicator
|
||||||
|
font-size: 85%
|
||||||
|
color: $error-color
|
||||||
|
margin-top: .25em
|
||||||
|
margin-bottom: .5em
|
||||||
|
display: none
|
||||||
|
|
||||||
|
// When loading, fade in the loading spinner after a brief delay. We are
|
||||||
|
// loading when the <turbo-frame> is busy, or when at least one layer
|
||||||
|
// is loading.
|
||||||
|
//
|
||||||
|
// We only apply the delay here, not on the base styles, because fading
|
||||||
|
// *out* on load should be instant.
|
||||||
|
#item-preview[busy] outfit-viewer
|
||||||
|
+outfit-viewer-loading
|
||||||
|
|
||||||
|
#item-preview:has(outfit-layer:state(error))
|
||||||
|
outfit-viewer
|
||||||
|
border: 2px solid red
|
||||||
|
.error-indicator
|
||||||
|
display: block
|
||||||
|
|
||||||
|
species-color-picker
|
||||||
|
.error-icon
|
||||||
|
cursor: help
|
||||||
|
margin-right: .25em
|
||||||
|
|
||||||
|
form[data-is-valid="false"]
|
||||||
|
select
|
||||||
|
border-color: $error-border-color
|
||||||
|
color: $error-color
|
||||||
|
|
||||||
|
// If JS is enabled, but auto-loading isn't ready yet (script loading or
|
||||||
|
// failed?), hide the submit button for .75sec, to give it time to load.
|
||||||
|
@media (scripting: enabled)
|
||||||
|
input[type=submit]
|
||||||
|
position: absolute
|
||||||
|
margin-left: .5em
|
||||||
|
opacity: 0
|
||||||
|
animation: fade-in .25s forwards
|
||||||
|
animation-delay: .75s
|
||||||
|
|
||||||
|
// Once the auto-loading behavior is ready, remove the submit button.
|
||||||
|
&:state(auto-loading)
|
||||||
|
input[type=submit]
|
||||||
|
display: none
|
||||||
|
|
||||||
|
species-face-picker
|
||||||
|
display: block
|
||||||
|
position: relative
|
||||||
|
margin-top: -10px
|
||||||
|
|
||||||
|
species-face-picker-options
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
flex-wrap: wrap
|
||||||
|
isolation: isolate // avoid z-index conflicts between pets and noscript
|
||||||
|
overflow: auto
|
||||||
|
max-height: 200px // 4 rows of 50px images, and padding will offer a hint of below
|
||||||
|
padding: 10px // leave enough room for the zoomed-in selected face
|
||||||
|
|
||||||
|
img
|
||||||
|
width: 54px
|
||||||
|
height: 54px
|
||||||
|
transition: all 0.2s
|
||||||
|
|
||||||
|
// Calm down the default color, just a smidge! There's a lot of color
|
||||||
|
// on this page already, y'know?
|
||||||
|
opacity: .9
|
||||||
|
filter: saturate(90%)
|
||||||
|
|
||||||
|
label
|
||||||
|
display: flex
|
||||||
|
overflow: hidden
|
||||||
|
transition: all 0.2s
|
||||||
|
position: relative
|
||||||
|
line-height: 1
|
||||||
|
|
||||||
|
// NOTE: The box-shadows here were copy-pasted from Impress 2020, which uses
|
||||||
|
// Chakra UI's styling system to generate them! (The colors are from their
|
||||||
|
// color palette, too.)
|
||||||
|
&:has(input:checked)
|
||||||
|
border-radius: 6px
|
||||||
|
z-index: 1
|
||||||
|
background: #9AE6B4
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),0 10px 10px -5px rgba(0, 0, 0, 0.04), #2F855A 0 0 2px 2px
|
||||||
|
transform: scale(1.1)
|
||||||
|
|
||||||
|
&:has(input:focus)
|
||||||
|
background: #BEE3F8
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),0 10px 10px -5px rgba(0, 0, 0, 0.04), #4299e1 0 0 0 3px
|
||||||
|
transform: scale(1.2)
|
||||||
|
|
||||||
|
input[type=radio]
|
||||||
|
position: absolute
|
||||||
|
left: -10000px
|
||||||
|
top: auto
|
||||||
|
width: 1px
|
||||||
|
height: 1px
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
&:checked + img
|
||||||
|
opacity: 1
|
||||||
|
filter: saturate(110%)
|
||||||
|
|
||||||
|
&:disabled + img
|
||||||
|
opacity: .6
|
||||||
|
filter: saturate(0%)
|
||||||
|
|
||||||
|
label:has(input[type=radio]:disabled)
|
||||||
|
cursor: not-allowed
|
||||||
|
|
||||||
|
noscript
|
||||||
|
position: absolute
|
||||||
|
inset: 0
|
||||||
|
padding: 1em
|
||||||
|
background: rgba(white, .8)
|
||||||
|
z-index: 1
|
||||||
|
cursor: auto
|
||||||
|
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
&:has(species-face-picker-options[inert])
|
||||||
|
cursor: wait
|
||||||
|
|
||||||
|
.item-preview-meta-info
|
||||||
|
display: grid
|
||||||
|
grid-template-columns: 1fr auto
|
||||||
|
gap: .5em
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
.item-zones-info
|
||||||
|
h3
|
||||||
|
display: inline
|
||||||
|
font: inherit
|
||||||
|
font-weight: bold
|
||||||
|
&:after
|
||||||
|
content: ": "
|
||||||
|
|
||||||
|
ul
|
||||||
|
list-style-type: none
|
||||||
|
display: inline
|
||||||
|
|
||||||
|
li
|
||||||
|
display: inline
|
||||||
|
&:not(:last-of-type):after
|
||||||
|
content: ", "
|
||||||
|
|
||||||
|
.no-zones
|
||||||
|
font-style: italic
|
||||||
|
opacity: .85
|
||||||
|
|
||||||
|
.zone-species-info
|
||||||
|
font-style: italic
|
||||||
|
text-decoration: underline dotted
|
||||||
|
|
||||||
|
// Many of these styles copied from Impress 2020 and its Chakra UI styles!
|
||||||
|
.item-html5-info
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
border: 1px solid
|
||||||
|
border-radius: .375em
|
||||||
|
padding: 4px 8px
|
||||||
|
min-height: 30px
|
||||||
|
box-sizing: border-box
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px
|
||||||
|
|
||||||
|
&[data-status=converted]
|
||||||
|
background: $module-bg-color
|
||||||
|
color: $text-color
|
||||||
|
|
||||||
|
svg:nth-of-type(2)
|
||||||
|
margin-right: -4px // spacing hacks!
|
||||||
|
|
||||||
|
&[data-status=unconverted]
|
||||||
|
background: $warning-bg-color
|
||||||
|
color: #975A16
|
||||||
|
gap: .25em // spacing hacks!
|
||||||
|
|
||||||
|
svg:first-of-type
|
||||||
|
width: 12px
|
||||||
|
height: 12px
|
||||||
|
|
||||||
|
svg:nth-of-type(2)
|
||||||
|
width: 20px
|
||||||
|
height: 20px
|
||||||
|
|
||||||
|
#item-preview
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: .75em
|
||||||
|
|
||||||
|
@media (min-width: 700px)
|
||||||
|
display: grid
|
||||||
|
grid-template-areas: "viewer faces" "picker meta"
|
||||||
|
gap: .5em
|
||||||
|
|
||||||
|
.preview-area
|
||||||
|
grid-area: viewer
|
||||||
|
outfit-viewer
|
||||||
|
width: 380px
|
||||||
|
height: 380px
|
||||||
|
|
||||||
|
species-color-picker
|
||||||
|
grid-area: picker
|
||||||
|
|
||||||
|
species-face-picker
|
||||||
|
grid-area: faces
|
||||||
|
species-face-picker-options
|
||||||
|
max-height: 380px
|
||||||
|
|
||||||
|
.item-preview-meta-info
|
||||||
|
grid-area: meta
|
||||||
|
|
||||||
|
@keyframes fade-in
|
||||||
|
from
|
||||||
|
opacity: 0
|
||||||
|
to
|
||||||
|
opacity: 1
|
||||||
|
|
@ -107,10 +107,10 @@
|
||||||
a
|
a
|
||||||
color: inherit
|
color: inherit
|
||||||
|
|
||||||
.owls-info-link
|
.nc-trade-guide-info-link
|
||||||
cursor: help
|
cursor: help
|
||||||
|
|
||||||
.owls-info-label
|
.nc-trade-guide-info-label
|
||||||
text-decoration-line: underline
|
text-decoration-line: underline
|
||||||
text-decoration-style: dotted
|
text-decoration-style: dotted
|
||||||
|
|
||||||
|
|
|
||||||
28
app/assets/stylesheets/layouts/items.sass
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
@import "partials/campaign-progress"
|
||||||
|
|
||||||
|
body
|
||||||
|
+campaign-progress
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
.item-search-form
|
||||||
|
display: flex
|
||||||
|
gap: .5em
|
||||||
|
justify-content: center
|
||||||
|
|
||||||
|
input[type=text]
|
||||||
|
font-size: 125%
|
||||||
|
width: 15em
|
||||||
|
flex: 0 1 auto
|
||||||
|
|
||||||
|
h1
|
||||||
|
margin-bottom: 1em
|
||||||
|
img
|
||||||
|
height: 80px
|
||||||
|
margin-bottom: -0.5em
|
||||||
|
width: 80px
|
||||||
|
a
|
||||||
|
text-decoration: none
|
||||||
|
span
|
||||||
|
text-decoration: underline
|
||||||
|
&:hover span
|
||||||
|
text-decoration: none
|
||||||
|
|
@ -7,9 +7,8 @@ body.outfits-new
|
||||||
#pet-not-found
|
#pet-not-found
|
||||||
display: none
|
display: none
|
||||||
|
|
||||||
.neopass-announcement
|
.announcement
|
||||||
border: 1px solid #cd8400
|
border: 1px solid $module-border-color
|
||||||
color: #764a00
|
|
||||||
padding: .5em
|
padding: .5em
|
||||||
display: grid
|
display: grid
|
||||||
grid-template-areas: "thumbnail content"
|
grid-template-areas: "thumbnail content"
|
||||||
|
|
@ -24,9 +23,6 @@ body.outfits-new
|
||||||
p:last-of-type
|
p:last-of-type
|
||||||
margin-bottom: 0
|
margin-bottom: 0
|
||||||
|
|
||||||
a
|
|
||||||
color: #be7a00
|
|
||||||
|
|
||||||
#outfit-forms
|
#outfit-forms
|
||||||
+clearfix
|
+clearfix
|
||||||
+module
|
+module
|
||||||
|
|
@ -82,85 +78,57 @@ body.outfits-new
|
||||||
font-size: 175%
|
font-size: 175%
|
||||||
select
|
select
|
||||||
font-size: 120%
|
font-size: 120%
|
||||||
#description, #top-contributors
|
|
||||||
float: left
|
|
||||||
#description
|
|
||||||
margin-right: 2%
|
|
||||||
width: 64%
|
|
||||||
#top-contributors
|
|
||||||
border: 1px solid $input-border-color
|
|
||||||
margin-top: 1em
|
|
||||||
padding: 1%
|
|
||||||
width: 30%
|
|
||||||
ol
|
|
||||||
margin-left: 2em
|
|
||||||
padding-left: 1em
|
|
||||||
> a
|
|
||||||
font-size: 80%
|
|
||||||
display: block
|
|
||||||
text-align: right
|
|
||||||
#how-can-i-help, #i-found-something
|
|
||||||
+module
|
|
||||||
float: left
|
|
||||||
padding: 1%
|
|
||||||
width: 46%
|
|
||||||
h2
|
|
||||||
font-style: italic
|
|
||||||
input, button
|
|
||||||
font-size: 115%
|
|
||||||
input[type=text]
|
|
||||||
border-color: $module-border-color
|
|
||||||
width: 12em
|
|
||||||
#how-can-i-help
|
|
||||||
margin-right: 1%
|
|
||||||
#i-found-something
|
|
||||||
margin-left: 1%
|
|
||||||
a
|
|
||||||
float: right
|
|
||||||
font-size: 87.5%
|
|
||||||
margin-top: 1em
|
|
||||||
$section-count: 3
|
|
||||||
$section-border-width: 1px
|
|
||||||
$section-padding: 0.5em
|
|
||||||
$section-width: 100% / $section-count
|
|
||||||
// (A - (B-1)*C) / B
|
|
||||||
#sections
|
#sections
|
||||||
+clearfix
|
display: grid
|
||||||
display: table
|
grid-template-columns: 1fr 1fr 1fr
|
||||||
list-style: none
|
list-style: none
|
||||||
margin-top: 1em
|
margin-top: 1em
|
||||||
h3
|
|
||||||
margin-bottom: .25em
|
|
||||||
li
|
li
|
||||||
border-left:
|
display: grid
|
||||||
color: $module-border-color
|
grid-template-areas: "header image" "info image" "form form"
|
||||||
style: solid
|
grid-template-rows: auto auto auto
|
||||||
width: $section-border-width
|
row-gap: .5em
|
||||||
display: table-cell
|
padding: 0.5em
|
||||||
padding: $section-padding
|
&:not(:first-child)
|
||||||
position: relative
|
border-left: 1px solid $module-border-color
|
||||||
width: $section-width
|
h3
|
||||||
&:first-child
|
grid-area: header
|
||||||
border-left: 0
|
margin-bottom: 0
|
||||||
div
|
div
|
||||||
|
grid-area: info
|
||||||
color: $soft-text-color
|
color: $soft-text-color
|
||||||
font-size: 75%
|
font-size: 75%
|
||||||
margin-left: 1em
|
margin-left: 1em
|
||||||
z-index: 2
|
z-index: 2
|
||||||
h4, input
|
strong
|
||||||
font-size: 116%
|
font-size: 116%
|
||||||
h4, input[type=text]
|
a:has(img)
|
||||||
color: inherit
|
grid-area: image
|
||||||
h4 a
|
|
||||||
background: #ffffc0
|
|
||||||
img
|
img
|
||||||
+opacity(0.75)
|
opacity: 0.75
|
||||||
float: right
|
float: right
|
||||||
margin-left: .5em
|
margin-left: .5em
|
||||||
&:hover
|
&:hover
|
||||||
+opacity(1)
|
opacity: 1
|
||||||
p
|
p
|
||||||
|
line-height: 1.5
|
||||||
min-height: 4.5em
|
min-height: 4.5em
|
||||||
|
margin-bottom: 0
|
||||||
|
form
|
||||||
|
grid-area: form
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: .5em
|
||||||
|
font-size: .85em
|
||||||
|
margin-left: 1em
|
||||||
|
margin-right: .5em
|
||||||
|
|
||||||
|
input[type=text], input[type=search]
|
||||||
|
// TODO: It doesn't make sense to me that this is the right style? I
|
||||||
|
// expected `flex: 1 0 0` to be right, but that grew *too* large, and
|
||||||
|
// forced the sections to grow wider too. I also tried `flex: 0 1 100%`,
|
||||||
|
// which I would have *thought* is the same as this, but isn't! Idk!
|
||||||
|
width: 100%
|
||||||
|
|
||||||
#whats-new
|
#whats-new
|
||||||
margin-bottom: 1em
|
margin-bottom: 1em
|
||||||
|
|
@ -329,4 +297,3 @@ body.outfits-new
|
||||||
#latest-contribution-created-at
|
#latest-contribution-created-at
|
||||||
color: $soft-text-color
|
color: $soft-text-color
|
||||||
margin-left: .5em
|
margin-left: .5em
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
// Used internally:
|
|
||||||
|
|
||||||
$background_color: #0b61a4
|
|
||||||
$module_border_color: #033e6b
|
|
||||||
$module_background_color: #66a3d2
|
|
||||||
|
|
||||||
$input_hover_border_color: #ff9200
|
|
||||||
$input_focus_border_color: #fff
|
|
||||||
|
|
||||||
$loud_button_background_color: #ff9200
|
|
||||||
$loud_button_border_color: #ffad40
|
|
||||||
$loud_button_color: #a65f00
|
|
||||||
$loud_button_focus_border_color: #000
|
|
||||||
|
|
||||||
// Used by Blueprint:
|
|
||||||
|
|
||||||
$font_color: #fff
|
|
||||||
|
|
||||||
$header_color: inherit
|
|
||||||
|
|
||||||
$link_color: inherit
|
|
||||||
$link_hover_color: inherit
|
|
||||||
$link_focus_color: inherit
|
|
||||||
$link_active_color: inherit
|
|
||||||
$link_visited_color: inherit
|
|
||||||
|
|
||||||
$error_color: inherit
|
|
||||||
$error_bg_color: #e14f1c
|
|
||||||
$error_border_color: #cd0a0a
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
@import "clean/mixins"
|
||||||
|
|
||||||
=context-button
|
=context-button
|
||||||
+awesome-button
|
+awesome-button
|
||||||
+awesome-button-color(#aaaaaa)
|
+awesome-button-color(#aaaaaa)
|
||||||
+opacity(0.9)
|
+opacity(0.9)
|
||||||
font-size: 80%
|
font-size: 80%
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
text-align: left
|
text-align: left
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
|
flex-wrap: wrap
|
||||||
gap: 1em
|
gap: 1em
|
||||||
|
|
||||||
abbr
|
abbr
|
||||||
|
|
@ -66,14 +67,21 @@
|
||||||
background: #FEEBC8
|
background: #FEEBC8
|
||||||
color: #7B341E
|
color: #7B341E
|
||||||
|
|
||||||
|
.support-form
|
||||||
|
grid-area: support
|
||||||
|
font-size: 85%
|
||||||
|
text-align: left
|
||||||
|
|
||||||
.user-lists-info
|
.user-lists-info
|
||||||
grid-area: lists
|
grid-area: lists
|
||||||
font-size: 85%
|
font-size: 85%
|
||||||
text-align: left
|
text-align: left
|
||||||
|
|
||||||
.user-lists-form-opener
|
display: flex
|
||||||
&::after
|
gap: 1em
|
||||||
content: " ›"
|
|
||||||
|
a::after
|
||||||
|
content: " ›"
|
||||||
|
|
||||||
.user-lists-form
|
.user-lists-form
|
||||||
background: $background-color
|
background: $background-color
|
||||||
|
|
@ -127,6 +135,7 @@
|
||||||
.item-subpages-nav
|
.item-subpages-nav
|
||||||
display: flex
|
display: flex
|
||||||
align-items: flex-end
|
align-items: flex-end
|
||||||
|
gap: 1em
|
||||||
|
|
||||||
.preview-link
|
.preview-link
|
||||||
margin-right: auto
|
margin-right: auto
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,8 @@ $error-color: #8a1f11
|
||||||
$error-bg-color: #fbe3e4
|
$error-bg-color: #fbe3e4
|
||||||
$error-border-color: #fbc2c4
|
$error-border-color: #fbc2c4
|
||||||
|
|
||||||
$header-font: Delicious, Helvetica, Arial, Verdana, sans-serif
|
$header-font: Delicious, system-ui, sans-serif
|
||||||
$main-font: "Droid Sans", Helvetica, Arial, Verdana, sans-serif
|
$main-font: system-ui, sans-serif
|
||||||
$text-font: "Droid Serif", Georgia, "Times New Roman", Times, serif
|
|
||||||
|
|
||||||
$object-img-size: 80px
|
$object-img-size: 80px
|
||||||
$object-width: 100px
|
$object-width: 100px
|
||||||
|
|
|
||||||
15
app/assets/stylesheets/pet_states/edit.sass
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
support-outfit-viewer
|
||||||
|
margin-block: 1em
|
||||||
|
|
||||||
|
.fields li[data-type=radio-grid]
|
||||||
|
--num-columns: 3
|
||||||
|
|
||||||
|
.reference-link
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: .5em
|
||||||
|
padding-inline: .5em
|
||||||
|
|
||||||
|
img
|
||||||
|
height: 2em
|
||||||
|
width: auto
|
||||||
85
app/assets/stylesheets/pet_states/support-outfit-viewer.sass
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
@import "../partials/clean/constants"
|
||||||
|
|
||||||
|
support-outfit-viewer
|
||||||
|
display: flex
|
||||||
|
gap: 2em
|
||||||
|
flex-wrap: wrap
|
||||||
|
justify-content: center
|
||||||
|
|
||||||
|
outfit-viewer
|
||||||
|
flex: 0 0 auto
|
||||||
|
border: 1px solid $module-border-color
|
||||||
|
border-radius: 1em
|
||||||
|
|
||||||
|
.outfit-viewer-controls
|
||||||
|
margin-block: .5em
|
||||||
|
isolation: isolate // Avoid z-index weirdness with our buttons vs the lens
|
||||||
|
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
gap: .5em
|
||||||
|
|
||||||
|
font-size: .85em
|
||||||
|
|
||||||
|
fieldset
|
||||||
|
display: contents
|
||||||
|
|
||||||
|
legend
|
||||||
|
font-weight: bold
|
||||||
|
&::after
|
||||||
|
content: ":"
|
||||||
|
|
||||||
|
label
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: .25em
|
||||||
|
|
||||||
|
input[type=radio]
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
.outfit-viewer-area
|
||||||
|
> [data-format=png]
|
||||||
|
display: none
|
||||||
|
|
||||||
|
&:has(input[value=png]:checked)
|
||||||
|
.outfit-viewer-area
|
||||||
|
> [data-format=svg]
|
||||||
|
display: none
|
||||||
|
> [data-format=png]
|
||||||
|
display: block
|
||||||
|
|
||||||
|
> table
|
||||||
|
flex: 0 0 auto
|
||||||
|
border-collapse: collapse
|
||||||
|
table-layout: fixed
|
||||||
|
border-radius: .5em
|
||||||
|
|
||||||
|
th, td
|
||||||
|
border: 1px solid $module-border-color
|
||||||
|
font-size: .85em
|
||||||
|
padding: .25em .5em
|
||||||
|
text-align: left
|
||||||
|
|
||||||
|
> tbody
|
||||||
|
[data-field=links]
|
||||||
|
ul
|
||||||
|
list-style-type: none
|
||||||
|
display: flex
|
||||||
|
gap: .5em
|
||||||
|
|
||||||
|
// Once the component is ready, add some hints about potential interactions.
|
||||||
|
&:state(ready)
|
||||||
|
> table
|
||||||
|
> tbody > tr
|
||||||
|
cursor: zoom-in
|
||||||
|
&:hover
|
||||||
|
background: $module-bg-color
|
||||||
|
|
||||||
|
magic-magnifier
|
||||||
|
--magic-magnifier-lens-width: 100px
|
||||||
|
--magic-magnifier-lens-height: 100px
|
||||||
|
--magic-magnifier-scale: 2.5
|
||||||
|
|
||||||
|
magic-magnifier-lens
|
||||||
|
z-index: 2 // Be above things by default, but not by much!
|
||||||
8
app/assets/stylesheets/pet_types/show.sass
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
@import "../partials/clean/constants"
|
||||||
|
|
||||||
|
.rainbow-pool-list
|
||||||
|
--preview-base-width: 200px
|
||||||
|
margin-bottom: 2em
|
||||||
|
|
||||||
|
.glitched
|
||||||
|
cursor: help
|
||||||
|
|
@ -2,70 +2,8 @@
|
||||||
@import "../partials/clean/mixins"
|
@import "../partials/clean/mixins"
|
||||||
|
|
||||||
body.pets-bulk
|
body.pets-bulk
|
||||||
#needed-items-form, #bulk-pets-form
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
#needed-items-form
|
|
||||||
#needed-items-pet
|
|
||||||
border-top: 1px solid $soft-border-color
|
|
||||||
display: none
|
|
||||||
margin-top: 1em
|
|
||||||
padding-top: 1em
|
|
||||||
|
|
||||||
h4
|
|
||||||
font-size: 150%
|
|
||||||
margin-bottom: .5em
|
|
||||||
|
|
||||||
#needed-items-reload
|
|
||||||
+inline-block
|
|
||||||
font-size: 12px
|
|
||||||
margin-left: 1em
|
|
||||||
vertical-align: middle
|
|
||||||
|
|
||||||
#needed-items-alert
|
|
||||||
display: none
|
|
||||||
margin-top: .5em
|
|
||||||
|
|
||||||
#needed-items-pet-thumbnail
|
|
||||||
height: 50px
|
|
||||||
width: 50px
|
|
||||||
|
|
||||||
#needed-items-pet-items
|
|
||||||
li.owned
|
|
||||||
background: $module-bg-color
|
|
||||||
border: 1px solid $module-border-color
|
|
||||||
|
|
||||||
.object-owned
|
|
||||||
color: $soft-text-color
|
|
||||||
display: block
|
|
||||||
font-size: 75%
|
|
||||||
font-style: italic
|
|
||||||
padding-bottom: .25em
|
|
||||||
|
|
||||||
&.loading-pet, &.loading-items
|
|
||||||
#needed-items-pet-name-field
|
|
||||||
background:
|
|
||||||
image: image-url("loading.gif")
|
|
||||||
position: center right
|
|
||||||
repeat: no-repeat
|
|
||||||
|
|
||||||
#needed-items-pet-items
|
|
||||||
+opacity(.50)
|
|
||||||
|
|
||||||
&.loading-pet
|
|
||||||
#needed-items-pet h4
|
|
||||||
+opacity(.50)
|
|
||||||
|
|
||||||
&.loaded
|
|
||||||
#needed-items-pet
|
|
||||||
display: block
|
|
||||||
|
|
||||||
&.failed
|
|
||||||
#needed-items-alert
|
|
||||||
display: block
|
|
||||||
|
|
||||||
#bulk-pets-form
|
#bulk-pets-form
|
||||||
border-top: 1px solid $module-border-color
|
text-align: center
|
||||||
margin-top: 12px
|
margin-top: 12px
|
||||||
padding-top: 12px
|
padding-top: 12px
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,45 @@
|
||||||
class AltStylesController < ApplicationController
|
class AltStylesController < ApplicationController
|
||||||
|
before_action :support_staff_only, except: [:index]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@alt_styles = AltStyle.includes(:species, :color, :swf_assets).
|
@all_series_names = AltStyle.all_series_names
|
||||||
order(:species_id, :color_id)
|
@all_color_names = AltStyle.all_supported_colors.map(&:human_name).sort
|
||||||
|
@all_species_names = AltStyle.all_supported_species.map(&:human_name).sort
|
||||||
|
|
||||||
if params[:species_id]
|
@series_name = params[:series]
|
||||||
@species = Species.find(params[:species_id])
|
@color = find_color
|
||||||
@alt_styles = @alt_styles.merge(@species.alt_styles)
|
@species = find_species
|
||||||
end
|
|
||||||
|
|
||||||
# We're going to link to the HTML5 image URL, so make sure we have all the
|
@alt_styles = AltStyle.includes(:color, :species, :swf_assets)
|
||||||
# manifests ready!
|
@alt_styles.where!(series_name: @series_name) if @series_name.present?
|
||||||
SwfAsset.preload_manifests @alt_styles.map(&:swf_assets).flatten
|
@alt_styles.merge!(@color.alt_styles) if @color
|
||||||
|
@alt_styles.merge!(@species.alt_styles) if @species
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { render }
|
format.html {
|
||||||
|
@alt_styles = @alt_styles.
|
||||||
|
by_creation_date.order(:color_id, :species_id, :series_name).
|
||||||
|
paginate(page: params[:page], per_page: 30)
|
||||||
|
|
||||||
|
# We're using the HTML5 image for our preview, so make sure we have all the
|
||||||
|
# manifests ready!
|
||||||
|
SwfAsset.preload_manifests @alt_styles.map(&:swf_assets).flatten
|
||||||
|
|
||||||
|
if support_staff?
|
||||||
|
@counts = {
|
||||||
|
total: AltStyle.count,
|
||||||
|
unlabeled: AltStyle.unlabeled.count,
|
||||||
|
}
|
||||||
|
@counts[:labeled] = @counts[:total] - @counts[:unlabeled]
|
||||||
|
@unlabeled_style = AltStyle.unlabeled.newest.first
|
||||||
|
end
|
||||||
|
|
||||||
|
render
|
||||||
|
}
|
||||||
format.json {
|
format.json {
|
||||||
render json: @alt_styles.includes(swf_assets: [:zone]).as_json(
|
@alt_styles = @alt_styles.includes(swf_assets: [:zone]).by_name_grouped
|
||||||
only: [:id, :species_id, :color_id, :body_id, :series_name,
|
render json: @alt_styles.as_json(
|
||||||
:adjective_name, :thumbnail_url],
|
only: [:id, :species_id, :color_id, :body_id, :thumbnail_url],
|
||||||
include: {
|
include: {
|
||||||
swf_assets: {
|
swf_assets: {
|
||||||
only: [:id, :body_id],
|
only: [:id, :body_id],
|
||||||
|
|
@ -25,9 +47,62 @@ class AltStylesController < ApplicationController
|
||||||
methods: [:urls, :known_glitches],
|
methods: [:urls, :known_glitches],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: [:series_name, :adjective_name, :thumbnail_url],
|
methods: [:series_main_name, :adjective_name],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
@alt_style = AltStyle.find params[:id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@alt_style = AltStyle.find params[:id]
|
||||||
|
|
||||||
|
if @alt_style.update(alt_style_params)
|
||||||
|
flash[:notice] = "\"#{@alt_style.full_name}\" successfully saved!"
|
||||||
|
redirect_to destination_after_save
|
||||||
|
else
|
||||||
|
render action: :edit, status: :bad_request
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def alt_style_params
|
||||||
|
params.require(:alt_style).
|
||||||
|
permit(:real_series_name, :real_full_name, :thumbnail_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_color
|
||||||
|
if params[:color]
|
||||||
|
Color.find_by(name: params[:color])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_species
|
||||||
|
if params[:species_id]
|
||||||
|
Species.find_by(id: params[:species_id])
|
||||||
|
elsif params[:species]
|
||||||
|
Species.find_by(name: params[:species])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destination_after_save
|
||||||
|
if params[:next] == "unlabeled-style"
|
||||||
|
next_unlabeled_style_path
|
||||||
|
else
|
||||||
|
alt_styles_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_unlabeled_style_path
|
||||||
|
unlabeled_style = AltStyle.unlabeled.newest.first
|
||||||
|
if unlabeled_style
|
||||||
|
edit_alt_style_path(unlabeled_style, next: "unlabeled-style")
|
||||||
|
else
|
||||||
|
alt_styles_path
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,9 @@ require 'async'
|
||||||
require 'async/container'
|
require 'async/container'
|
||||||
|
|
||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
include FragmentLocalization
|
|
||||||
|
|
||||||
protect_from_forgery
|
protect_from_forgery
|
||||||
|
|
||||||
helper_method :current_user, :user_signed_in?
|
helper_method :current_user, :support_staff?, :user_signed_in?
|
||||||
|
|
||||||
before_action :set_locale
|
before_action :set_locale
|
||||||
|
|
||||||
|
|
@ -23,9 +21,12 @@ class ApplicationController < ActionController::Base
|
||||||
|
|
||||||
class AccessDenied < StandardError; end
|
class AccessDenied < StandardError; end
|
||||||
rescue_from AccessDenied, with: :on_access_denied
|
rescue_from AccessDenied, with: :on_access_denied
|
||||||
|
|
||||||
rescue_from Async::Stop, Async::Container::Terminate,
|
rescue_from Async::Stop, Async::Container::Terminate,
|
||||||
with: :on_request_stopped
|
with: :on_request_stopped
|
||||||
|
|
||||||
|
rescue_from ActiveRecord::ConnectionTimeoutError, with: :on_db_timeout
|
||||||
|
|
||||||
def authenticate_user!
|
def authenticate_user!
|
||||||
redirect_to(new_auth_user_session_path) unless user_signed_in?
|
redirect_to(new_auth_user_session_path) unless user_signed_in?
|
||||||
end
|
end
|
||||||
|
|
@ -50,7 +51,7 @@ class ApplicationController < ActionController::Base
|
||||||
return params[:locale] if valid_locale?(params[:locale])
|
return params[:locale] if valid_locale?(params[:locale])
|
||||||
return cookies[:locale] if valid_locale?(cookies[:locale])
|
return cookies[:locale] if valid_locale?(cookies[:locale])
|
||||||
Rails.logger.debug "Preferred languages: #{http_accept_language.user_preferred_languages}"
|
Rails.logger.debug "Preferred languages: #{http_accept_language.user_preferred_languages}"
|
||||||
http_accept_language.language_region_compatible_from(I18n.public_locales.map(&:to_s)) ||
|
http_accept_language.language_region_compatible_from(I18n.available_locales.map(&:to_s)) ||
|
||||||
I18n.default_locale
|
I18n.default_locale
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -67,6 +68,11 @@ class ApplicationController < ActionController::Base
|
||||||
status: :internal_server_error
|
status: :internal_server_error
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def on_db_timeout
|
||||||
|
render file: 'public/503.html', layout: false,
|
||||||
|
status: :service_unavailable
|
||||||
|
end
|
||||||
|
|
||||||
def redirect_back!(default=:back)
|
def redirect_back!(default=:back)
|
||||||
redirect_to(params[:return_to] || default)
|
redirect_to(params[:return_to] || default)
|
||||||
end
|
end
|
||||||
|
|
@ -76,7 +82,7 @@ class ApplicationController < ActionController::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_locale?(locale)
|
def valid_locale?(locale)
|
||||||
locale && I18n.usable_locales.include?(locale.to_sym)
|
locale && I18n.available_locales.include?(locale.to_sym)
|
||||||
end
|
end
|
||||||
|
|
||||||
def configure_permitted_parameters
|
def configure_permitted_parameters
|
||||||
|
|
@ -104,5 +110,13 @@ class ApplicationController < ActionController::Base
|
||||||
Rails.logger.debug "Using return_to path: #{return_to.inspect}"
|
Rails.logger.debug "Using return_to path: #{return_to.inspect}"
|
||||||
return_to || root_path
|
return_to || root_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def support_staff?
|
||||||
|
current_user&.support_staff?
|
||||||
|
end
|
||||||
|
|
||||||
|
def support_staff_only
|
||||||
|
raise AccessDenied, "Support staff only" unless support_staff?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,7 @@ class ClosetHangersController < ApplicationController
|
||||||
def enforce_shadowban
|
def enforce_shadowban
|
||||||
# If this user is shadowbanned, and this *doesn't* seem to be a request
|
# If this user is shadowbanned, and this *doesn't* seem to be a request
|
||||||
# from that user, render the 404 page.
|
# from that user, render the 404 page.
|
||||||
if @user.shadowbanned? && !@user.likely_is?(current_user, request.remote_ip)
|
if !@user.visible_to?(current_user, request.remote_ip)
|
||||||
render file: "public/404.html", layout: false, status: :not_found
|
render file: "public/404.html", layout: false, status: :not_found
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ class ItemTradesController < ApplicationController
|
||||||
@type = type_from_params
|
@type = type_from_params
|
||||||
|
|
||||||
@item_trades = @item.closet_hangers.trading.includes(:user, :list).
|
@item_trades = @item.closet_hangers.trading.includes(:user, :list).
|
||||||
user_is_active.order('users.last_trade_activity_at DESC').to_trades
|
user_is_active.order('users.last_trade_activity_at DESC').
|
||||||
|
to_trades(current_user, request.remote_ip)
|
||||||
@trades = @item_trades[@type]
|
@trades = @item_trades[@type]
|
||||||
|
|
||||||
if user_signed_in?
|
if user_signed_in?
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
class ItemsController < ApplicationController
|
class ItemsController < ApplicationController
|
||||||
before_action :set_query
|
before_action :set_query
|
||||||
|
before_action :support_staff_only, except: [:index, :show, :sources]
|
||||||
rescue_from Item::Search::Error, :with => :search_error
|
rescue_from Item::Search::Error, :with => :search_error
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|
@ -28,6 +29,12 @@ class ItemsController < ApplicationController
|
||||||
render json: {
|
render json: {
|
||||||
items: @items.as_json(
|
items: @items.as_json(
|
||||||
methods: [:nc?, :pb?, :owned?, :wanted?],
|
methods: [:nc?, :pb?, :owned?, :wanted?],
|
||||||
|
include: {
|
||||||
|
restricted_zones: {
|
||||||
|
only: [:id, :depth, :label],
|
||||||
|
methods: [:is_commonly_used_by_items],
|
||||||
|
},
|
||||||
|
},
|
||||||
),
|
),
|
||||||
appearances: load_appearances.as_json(
|
appearances: load_appearances.as_json(
|
||||||
include: {
|
include: {
|
||||||
|
|
@ -73,7 +80,8 @@ class ItemsController < ApplicationController
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
@trades = @item.closet_hangers.trading.user_is_active.to_trades
|
@trades = @item.closet_hangers.trading.user_is_active.
|
||||||
|
to_trades(current_user, request.remote_ip)
|
||||||
|
|
||||||
@contributors_with_counts = @item.contributors_with_counts
|
@contributors_with_counts = @item.contributors_with_counts
|
||||||
|
|
||||||
|
|
@ -89,6 +97,14 @@ class ItemsController < ApplicationController
|
||||||
worn_items: [@item],
|
worn_items: [@item],
|
||||||
)
|
)
|
||||||
@preview_error = validate_preview
|
@preview_error = validate_preview
|
||||||
|
|
||||||
|
@all_appearances = @item.appearances
|
||||||
|
@appearances_by_occupied_zone_label =
|
||||||
|
@item.appearances_by_occupied_zone_label.sort_by { |l, a| l }
|
||||||
|
@selected_item_appearance = @preview_outfit.item_appearances.first
|
||||||
|
|
||||||
|
@preview_pet_type_options = PetType.where(color: @preview_outfit.color).
|
||||||
|
includes(:species).merge(Species.alphabetical)
|
||||||
end
|
end
|
||||||
|
|
||||||
format.gif do
|
format.gif do
|
||||||
|
|
@ -98,24 +114,18 @@ class ItemsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def needed
|
def edit
|
||||||
if params[:color] && params[:species]
|
@item = Item.find params[:id]
|
||||||
@pet_type = PetType.find_by_color_id_and_species_id(
|
render layout: "application"
|
||||||
params[:color],
|
end
|
||||||
params[:species]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
unless @pet_type
|
def update
|
||||||
raise ActiveRecord::RecordNotFound, 'Pet type not found'
|
@item = Item.find params[:id]
|
||||||
end
|
if @item.update(item_params)
|
||||||
|
flash[:notice] = "\"#{@item.name}\" successfully saved!"
|
||||||
@items = @pet_type.needed_items.order(:name)
|
redirect_to @item
|
||||||
assign_closeted!(@items)
|
else
|
||||||
|
render action: "edit", layout: "application", status: :bad_request
|
||||||
respond_to do |format|
|
|
||||||
format.html { @pet_name = params[:name] ; render :layout => 'application' }
|
|
||||||
format.json { render :json => @items }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -135,7 +145,7 @@ class ItemsController < ApplicationController
|
||||||
|
|
||||||
# For Dyeworks items whose base is currently in the NC Mall, preload their
|
# For Dyeworks items whose base is currently in the NC Mall, preload their
|
||||||
# trade values. We'll use this to determine which ones are fully buyable rn
|
# trade values. We'll use this to determine which ones are fully buyable rn
|
||||||
# (because Owls tracks this data and we don't).
|
# (because our NC values guide tracks this data and we don't).
|
||||||
Item.preload_nc_trade_values(@items[:dyeworks])
|
Item.preload_nc_trade_values(@items[:dyeworks])
|
||||||
|
|
||||||
# Start loading the NC trade values for the non-Mall NC items.
|
# Start loading the NC trade values for the non-Mall NC items.
|
||||||
|
|
@ -171,6 +181,15 @@ class ItemsController < ApplicationController
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
|
def item_params
|
||||||
|
params.require(:item).permit(
|
||||||
|
:name, :thumbnail_url, :description, :modeling_status_hint,
|
||||||
|
:is_manually_nc, :explicitly_body_specific,
|
||||||
|
).tap do |p|
|
||||||
|
p[:modeling_status_hint] = nil if p[:modeling_status_hint] == ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def assign_closeted!(items)
|
def assign_closeted!(items)
|
||||||
current_user.assign_closeted_to_items!(items) if user_signed_in?
|
current_user.assign_closeted_to_items!(items) if user_signed_in?
|
||||||
end
|
end
|
||||||
|
|
@ -222,7 +241,8 @@ class ItemsController < ApplicationController
|
||||||
@item.compatible_pet_types.
|
@item.compatible_pet_types.
|
||||||
preferring_species(cookies["preferred-preview-species-id"] || "<ignore>").
|
preferring_species(cookies["preferred-preview-species-id"] || "<ignore>").
|
||||||
preferring_color(cookies["preferred-preview-color-id"] || "<ignore>").
|
preferring_color(cookies["preferred-preview-color-id"] || "<ignore>").
|
||||||
preferring_simple.first
|
preferring_simple.first ||
|
||||||
|
PetType.matching_name("Blue", "Acara").first!
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_preview
|
def validate_preview
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@ class NeopetsConnectionsController < ApplicationController
|
||||||
if connection.save
|
if connection.save
|
||||||
render json: connection
|
render json: connection
|
||||||
else
|
else
|
||||||
render json: {error: 'failure'}, status: :internal_server_error
|
render json: {
|
||||||
|
errors: connection.errors,
|
||||||
|
full_error_messages: connection.errors.map(&:full_message)
|
||||||
|
}, status: :bad_request
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,29 +47,24 @@ class OutfitsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@colors = Color.funny.alphabetical
|
@colors = Color.alphabetical
|
||||||
@species = Species.alphabetical
|
@species = Species.alphabetical
|
||||||
|
|
||||||
# HACK: Skip this in development, because it's slow!
|
newest_items = Item.newest.limit(18)
|
||||||
unless Rails.env.development?
|
@newest_modeled_items, @newest_unmodeled_items =
|
||||||
newest_items = Item.newest.
|
newest_items.partition(&:predicted_fully_modeled?)
|
||||||
select(:id, :name, :updated_at, :thumbnail_url, :rarity_index, :is_manually_nc)
|
|
||||||
.limit(18)
|
|
||||||
@newest_modeled_items, @newest_unmodeled_items =
|
|
||||||
newest_items.partition(&:predicted_fully_modeled?)
|
|
||||||
|
|
||||||
@newest_unmodeled_items_predicted_missing_species_by_color = {}
|
@newest_unmodeled_items_predicted_missing_species_by_color = {}
|
||||||
@newest_unmodeled_items_predicted_modeled_ratio = {}
|
@newest_unmodeled_items_predicted_modeled_ratio = {}
|
||||||
@newest_unmodeled_items.each do |item|
|
@newest_unmodeled_items.each do |item|
|
||||||
h = item.predicted_missing_nonstandard_body_ids_by_species_by_color
|
h = item.predicted_missing_nonstandard_body_ids_by_species_by_color
|
||||||
standard_body_ids_by_species = item.
|
standard_body_ids_by_species = item.
|
||||||
predicted_missing_standard_body_ids_by_species
|
predicted_missing_standard_body_ids_by_species
|
||||||
if standard_body_ids_by_species.present?
|
if standard_body_ids_by_species.present?
|
||||||
h[:standard] = standard_body_ids_by_species
|
h[:standard] = standard_body_ids_by_species
|
||||||
end
|
|
||||||
@newest_unmodeled_items_predicted_missing_species_by_color[item] = h
|
|
||||||
@newest_unmodeled_items_predicted_modeled_ratio[item] = item.predicted_modeled_ratio
|
|
||||||
end
|
end
|
||||||
|
@newest_unmodeled_items_predicted_missing_species_by_color[item] = h
|
||||||
|
@newest_unmodeled_items_predicted_modeled_ratio[item] = item.predicted_modeled_ratio
|
||||||
end
|
end
|
||||||
|
|
||||||
@species_count = Species.count
|
@species_count = Species.count
|
||||||
|
|
|
||||||
56
app/controllers/pet_states_controller.rb
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
class PetStatesController < ApplicationController
|
||||||
|
before_action :support_staff_only
|
||||||
|
before_action :find_pet_state
|
||||||
|
before_action :preload_assets
|
||||||
|
|
||||||
|
def edit
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @pet_state.update(pet_state_params)
|
||||||
|
flash[:notice] = "Pet appearance \##{@pet_state.id} successfully saved!"
|
||||||
|
redirect_to destination_after_save
|
||||||
|
else
|
||||||
|
render action: :edit, status: :bad_request
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def find_pet_state
|
||||||
|
@pet_type = PetType.find_by_param!(params[:pet_type_name])
|
||||||
|
@pet_state = @pet_type.pet_states.find(params[:id])
|
||||||
|
@reference_pet_type = @pet_type.reference
|
||||||
|
end
|
||||||
|
|
||||||
|
def preload_assets
|
||||||
|
SwfAsset.preload_manifests @pet_state.swf_assets
|
||||||
|
end
|
||||||
|
|
||||||
|
def pet_state_params
|
||||||
|
params.require(:pet_state).permit(:pose, :glitched)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destination_after_save
|
||||||
|
if params[:next] == "unlabeled-appearance"
|
||||||
|
next_unlabeled_appearance_path
|
||||||
|
else
|
||||||
|
@pet_type
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_unlabeled_appearance_path
|
||||||
|
unlabeled_appearance =
|
||||||
|
PetState.next_unlabeled_appearance(after_id: params[:after])
|
||||||
|
|
||||||
|
if unlabeled_appearance
|
||||||
|
edit_pet_type_pet_state_path(
|
||||||
|
unlabeled_appearance.pet_type,
|
||||||
|
unlabeled_appearance,
|
||||||
|
next: "unlabeled-appearance"
|
||||||
|
)
|
||||||
|
else
|
||||||
|
@pet_type
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,10 +1,111 @@
|
||||||
class PetTypesController < ApplicationController
|
class PetTypesController < ApplicationController
|
||||||
def show
|
def index
|
||||||
@pet_type = PetType.
|
respond_to do |format|
|
||||||
where(species_id: params[:species_id]).
|
format.html {
|
||||||
where(color_id: params[:color_id]).
|
@species_names = Species.order(:name).map(&:human_name)
|
||||||
first
|
@color_names = Color.order(:name).map(&:human_name)
|
||||||
|
|
||||||
render json: @pet_type
|
if params[:species].present?
|
||||||
|
@selected_species = Species.find_by!(name: params[:species])
|
||||||
|
@selected_species_name = @selected_species.human_name
|
||||||
|
end
|
||||||
|
if params[:color].present?
|
||||||
|
@selected_color = Color.find_by!(name: params[:color])
|
||||||
|
@selected_color_name = @selected_color.human_name
|
||||||
|
end
|
||||||
|
@selected_order =
|
||||||
|
if @selected_species.present? || @selected_color.present?
|
||||||
|
:alphabetical
|
||||||
|
else
|
||||||
|
:newest
|
||||||
|
end
|
||||||
|
|
||||||
|
@pet_types = PetType.
|
||||||
|
includes(:color, :species, :pet_states).
|
||||||
|
paginate(page: params[:page], per_page: 30)
|
||||||
|
|
||||||
|
@pet_types.where!(species_id: @selected_species) if @selected_species
|
||||||
|
@pet_types.where!(color_id: @selected_color) if @selected_color
|
||||||
|
if @selected_order == :newest
|
||||||
|
@pet_types.order!(created_at: :desc)
|
||||||
|
elsif @selected_order == :alphabetical
|
||||||
|
@pet_types.merge!(Color.alphabetical).merge!(Species.alphabetical)
|
||||||
|
end
|
||||||
|
|
||||||
|
if @selected_species && @selected_color && @pet_types.size == 1
|
||||||
|
redirect_to @pet_types.first
|
||||||
|
end
|
||||||
|
|
||||||
|
if support_staff?
|
||||||
|
@counts = {
|
||||||
|
total: PetState.count,
|
||||||
|
glitched: PetState.glitched.count,
|
||||||
|
needs_labeling: PetState.needs_labeling.count,
|
||||||
|
usable: PetState.usable.count,
|
||||||
|
}
|
||||||
|
@unlabeled_appearance = PetState.next_unlabeled_appearance
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
format.json {
|
||||||
|
if stale?(etag: PetState.last_updated_key)
|
||||||
|
render json: {
|
||||||
|
species: Species.order(:name).all,
|
||||||
|
colors: Color.order(:name).all,
|
||||||
|
supported_poses: PetState.all_supported_poses,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
@pet_type = find_pet_type
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
@pet_states = group_pet_states @pet_type.pet_states
|
||||||
|
end
|
||||||
|
format.json { render json: @pet_type }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
# The API-ish route uses IDs, but the human-facing route uses names.
|
||||||
|
def find_pet_type
|
||||||
|
if params[:species_id] && params[:color_id]
|
||||||
|
PetType.find_by!(
|
||||||
|
species_id: params[:species_id],
|
||||||
|
color_id: params[:color_id],
|
||||||
|
)
|
||||||
|
elsif params[:name]
|
||||||
|
PetType.find_by_param!(params[:name])
|
||||||
|
else
|
||||||
|
raise "expected params: species_id and color_id, or name"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# The `canonical` pet states are the main ones we want to show: the most
|
||||||
|
# canonical state for each pose. The `other` pet states are, the others!
|
||||||
|
#
|
||||||
|
# If no main poses are available, then we just make all the poses
|
||||||
|
# "canonical", and show the whole mish-mash!
|
||||||
|
def group_pet_states(pet_states)
|
||||||
|
pose_groups = pet_states.emotion_order.group_by(&:pose)
|
||||||
|
main_groups =
|
||||||
|
pose_groups.select { |k| PetState::MAIN_POSES.include?(k) }.values
|
||||||
|
other_groups =
|
||||||
|
pose_groups.reject { |k| PetState::MAIN_POSES.include?(k) }.values
|
||||||
|
|
||||||
|
if main_groups.empty?
|
||||||
|
return {canonical: other_groups.flatten(1).sort_by(&:pose), other: []}
|
||||||
|
end
|
||||||
|
|
||||||
|
canonical = main_groups.map(&:first).sort_by(&:pose)
|
||||||
|
main_others = main_groups.map { |l| l.drop(1) }.flatten(1)
|
||||||
|
other = (main_others + other_groups.flatten(1)).sort_by(&:pose)
|
||||||
|
|
||||||
|
{canonical:, other:}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
class PetsController < ApplicationController
|
class PetsController < ApplicationController
|
||||||
rescue_from Pet::PetNotFound, with: :pet_not_found
|
rescue_from Neopets::CustomPets::PetNotFound, with: :pet_not_found
|
||||||
rescue_from PetType::DownloadError, SwfAsset::DownloadError, with: :asset_download_error
|
rescue_from Neopets::CustomPets::DownloadError, with: :pet_download_error
|
||||||
rescue_from Pet::DownloadError, with: :pet_download_error
|
rescue_from Pet::ModelingDisabled, with: :modeling_disabled
|
||||||
rescue_from Pet::UnexpectedDataFormat, with: :unexpected_data_format
|
rescue_from Pet::UnexpectedDataFormat, with: :unexpected_data_format
|
||||||
|
|
||||||
def load
|
def load
|
||||||
# Uncomment this to temporarily disable modeling for most users.
|
raise Neopets::CustomPets::PetNotFound unless params[:name]
|
||||||
# return modeling_disabled unless user_signed_in? && current_user.admin?
|
|
||||||
|
|
||||||
raise Pet::PetNotFound unless params[:name]
|
|
||||||
@pet = Pet.load(params[:name])
|
@pet = Pet.load(params[:name])
|
||||||
points = contribute(current_user, @pet)
|
points = contribute(current_user, @pet)
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
path = destination + @pet.wardrobe_query
|
path = destination + "?" + @pet.wardrobe_query
|
||||||
redirect_to path
|
redirect_to path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -38,9 +35,8 @@ class PetsController < ApplicationController
|
||||||
|
|
||||||
def destination
|
def destination
|
||||||
case (params[:destination] || params[:origin])
|
case (params[:destination] || params[:origin])
|
||||||
when 'wardrobe' then wardrobe_path + '?'
|
when 'wardrobe' then wardrobe_path
|
||||||
when 'needed_items' then needed_items_path + '?'
|
else root_path
|
||||||
else root_path + '#'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -49,12 +45,6 @@ class PetsController < ApplicationController
|
||||||
:status => :not_found
|
:status => :not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
def asset_download_error(e)
|
|
||||||
Rails.logger.warn e.message
|
|
||||||
pet_load_error :long_message => t('pets.load.asset_download_error'),
|
|
||||||
:status => :gateway_timeout
|
|
||||||
end
|
|
||||||
|
|
||||||
def pet_download_error(e)
|
def pet_download_error(e)
|
||||||
Rails.logger.warn e.message
|
Rails.logger.warn e.message
|
||||||
Rails.logger.warn e.backtrace.join("\n")
|
Rails.logger.warn e.backtrace.join("\n")
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,20 @@ class SwfAssetsController < ApplicationController
|
||||||
helpers.image_url("favicon.png"),
|
helpers.image_url("favicon.png"),
|
||||||
@swf_asset.image_url,
|
@swf_asset.image_url,
|
||||||
*@swf_asset.canvas_movie_sprite_urls,
|
*@swf_asset.canvas_movie_sprite_urls,
|
||||||
|
|
||||||
|
# For images, `images.neopets.com` is a generally safe host to load
|
||||||
|
# from (shouldn't be a vulnerable site or exfiltration vector), and
|
||||||
|
# doing this can help make this header a *lot* shorter, which helps
|
||||||
|
# our nginx reverse proxy (and probably some clients) handle it. (For
|
||||||
|
# example, see asset `667993` for "Engulfed in Flames Effect".)
|
||||||
|
origins: ["https://images.neopets.com"],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
policy.script_src -> {
|
policy.script_src -> {
|
||||||
src_list(
|
src_list(
|
||||||
helpers.javascript_url("lib/easeljs.min"),
|
helpers.javascript_url("easeljs.min"),
|
||||||
helpers.javascript_url("lib/tweenjs.min"),
|
helpers.javascript_url("tweenjs.min"),
|
||||||
helpers.javascript_url("swf_assets/show"),
|
helpers.javascript_url("swf_assets/show"),
|
||||||
@swf_asset.canvas_movie_library_url,
|
@swf_asset.canvas_movie_library_url,
|
||||||
)
|
)
|
||||||
|
|
@ -38,7 +45,23 @@ class SwfAssetsController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def src_list(*urls)
|
def src_list(*urls, origins: [])
|
||||||
urls.filter(&:present?).map { |url| url.sub(/\?.*\z/, "") }.join(" ")
|
clean_urls = urls.
|
||||||
|
# Ignore any `nil`s that might arise
|
||||||
|
filter(&:present?).
|
||||||
|
# Parse the URL.
|
||||||
|
map { |url| Addressable::URI.parse(url) }.
|
||||||
|
# Remove query strings from URLs (they're invalid in CSPs)
|
||||||
|
each { |url| url.query = nil }.
|
||||||
|
# For the given `origins`, remove all their specific URLs, because
|
||||||
|
# we'll just include the entire origin anyway.
|
||||||
|
reject { |url| origins.include?(url.origin) }.
|
||||||
|
# Normalize the URLs. (This fixes issues like when the canonical
|
||||||
|
# Neopets version of the URL contains plain unescaped spaces.)
|
||||||
|
each(&:normalize!).
|
||||||
|
# Convert the URLs back into strings.
|
||||||
|
map(&:to_s)
|
||||||
|
|
||||||
|
clean_urls + origins
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
class UsersController < ApplicationController
|
class UsersController < ApplicationController
|
||||||
before_action :find_and_authorize_user!, :only => [:update]
|
before_action :find_and_authorize_user!, only: [:edit, :update]
|
||||||
|
before_action :support_staff_only, only: [:edit]
|
||||||
|
|
||||||
def index # search, really
|
def index # search, really
|
||||||
name = params[:name]
|
name = params[:name]
|
||||||
|
|
@ -16,6 +17,9 @@ class UsersController < ApplicationController
|
||||||
@users = User.top_contributors.paginate :page => params[:page], :per_page => 20
|
@users = User.top_contributors.paginate :page => params[:page], :per_page => 20
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@user.attributes = user_params
|
@user.attributes = user_params
|
||||||
success = @user.save
|
success = @user.save
|
||||||
|
|
@ -42,17 +46,24 @@ class UsersController < ApplicationController
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
|
ALLOWED_ATTRS = [
|
||||||
|
:owned_closet_hangers_visibility,
|
||||||
|
:wanted_closet_hangers_visibility,
|
||||||
|
:contact_neopets_connection_id,
|
||||||
|
]
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(:owned_closet_hangers_visibility,
|
if support_staff?
|
||||||
:wanted_closet_hangers_visibility, :contact_neopets_connection_id)
|
params.require(:user).permit(
|
||||||
|
*ALLOWED_ATTRS, :name, :shadowbanned, :support_staff
|
||||||
|
)
|
||||||
|
else
|
||||||
|
params.require(:user).permit(*ALLOWED_ATTRS)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_and_authorize_user!
|
def find_and_authorize_user!
|
||||||
if current_user.id == params[:id].to_i
|
@user = User.find(params[:id])
|
||||||
@user = current_user
|
raise AccessDenied unless current_user == @user || support_staff?
|
||||||
else
|
|
||||||
raise AccessDenied
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
13
app/helpers/alt_styles_helper.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
module AltStylesHelper
|
||||||
|
def view_or_edit_alt_style_url(alt_style)
|
||||||
|
if support_staff?
|
||||||
|
edit_alt_style_path alt_style
|
||||||
|
else
|
||||||
|
wardrobe_path(
|
||||||
|
species: alt_style.species_id,
|
||||||
|
color: alt_style.color_id,
|
||||||
|
style: alt_style.id,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
module ApplicationHelper
|
module ApplicationHelper
|
||||||
include FragmentLocalization
|
|
||||||
|
|
||||||
def absolute_url(path_or_url)
|
def absolute_url(path_or_url)
|
||||||
if path_or_url.include?('://') # already an absolute URL
|
if path_or_url.include?('://') # already an absolute URL
|
||||||
path_or_url
|
path_or_url
|
||||||
|
|
@ -101,6 +99,12 @@ module ApplicationHelper
|
||||||
"matchu@openneo.net"
|
"matchu@openneo.net"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
EDIT_ICON_SVG_SOURCE = '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></g>'.html_safe
|
||||||
|
def edit_icon(alt: "Edit")
|
||||||
|
content_tag :svg, EDIT_ICON_SVG_SOURCE, alt:, class: "icon",
|
||||||
|
viewBox: "0 0 24 24", style: "width: 1em; height: 1em"
|
||||||
|
end
|
||||||
|
|
||||||
# SVG icon source from Chakra UI!
|
# SVG icon source from Chakra UI!
|
||||||
EXTERNAL_LINK_SVG_SOURCE = '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><path d="M15 3h6v6"></path><path d="M10 14L21 3"></path></g>'.html_safe
|
EXTERNAL_LINK_SVG_SOURCE = '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><path d="M15 3h6v6"></path><path d="M10 14L21 3"></path></g>'.html_safe
|
||||||
def external_link_icon
|
def external_link_icon
|
||||||
|
|
@ -123,10 +127,6 @@ module ApplicationHelper
|
||||||
!@hide_home_link
|
!@hide_home_link
|
||||||
end
|
end
|
||||||
|
|
||||||
def support_staff?
|
|
||||||
user_signed_in? && current_user.support_staff?
|
|
||||||
end
|
|
||||||
|
|
||||||
def impress_2020_meta_tags
|
def impress_2020_meta_tags
|
||||||
origin = Rails.configuration.impress_2020_origin
|
origin = Rails.configuration.impress_2020_origin
|
||||||
support_secret = Rails.application.credentials.dig(
|
support_secret = Rails.application.credentials.dig(
|
||||||
|
|
@ -142,20 +142,9 @@ module ApplicationHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
JAVASCRIPT_LIBRARIES = {
|
|
||||||
:jquery => 'https://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js',
|
|
||||||
:jquery_tmpl => 'https://ajax.microsoft.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js',
|
|
||||||
}
|
|
||||||
|
|
||||||
def include_javascript_libraries(*library_names)
|
|
||||||
raw(library_names.inject('') do |html, name|
|
|
||||||
html + javascript_include_tag(JAVASCRIPT_LIBRARIES[name], defer: true)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
def locale_options
|
def locale_options
|
||||||
current_locale_is_public = false
|
current_locale_is_public = false
|
||||||
options = I18n.public_locales.map do |available_locale|
|
options = I18n.available_locales.map do |available_locale|
|
||||||
current_locale_is_public = true if I18n.locale == available_locale
|
current_locale_is_public = true if I18n.locale == available_locale
|
||||||
# Include fallbacks data on the tag. Right now it's used in blog
|
# Include fallbacks data on the tag. Right now it's used in blog
|
||||||
# localization, but may conceivably be used for something else later.
|
# localization, but may conceivably be used for something else later.
|
||||||
|
|
@ -171,13 +160,6 @@ module ApplicationHelper
|
||||||
options
|
options
|
||||||
end
|
end
|
||||||
|
|
||||||
def localized_cache(key={}, &block)
|
|
||||||
localized_key = localize_fragment_key(key, locale)
|
|
||||||
# TODO: The digest feature is handy, but it's not compatible with how we
|
|
||||||
# check for fragments existence in the controller, so skip it for now.
|
|
||||||
cache(localized_key, skip_digest: true, &block)
|
|
||||||
end
|
|
||||||
|
|
||||||
def auth_user_sign_in_path_with_return_to
|
def auth_user_sign_in_path_with_return_to
|
||||||
new_auth_user_session_path :return_to => request.fullpath
|
new_auth_user_session_path :return_to => request.fullpath
|
||||||
end
|
end
|
||||||
|
|
@ -231,6 +213,19 @@ module ApplicationHelper
|
||||||
@hide_title_header = true
|
@hide_title_header = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hide_after(last_day, &block)
|
||||||
|
yield if Date.today <= last_day
|
||||||
|
end
|
||||||
|
|
||||||
|
def use_responsive_design
|
||||||
|
@use_responsive_design = true
|
||||||
|
add_body_class "use-responsive-design"
|
||||||
|
end
|
||||||
|
|
||||||
|
def use_responsive_design?
|
||||||
|
@use_responsive_design || false
|
||||||
|
end
|
||||||
|
|
||||||
def signed_in_meta_tag
|
def signed_in_meta_tag
|
||||||
%(<meta name="user-signed-in" content="#{user_signed_in?}">).html_safe
|
%(<meta name="user-signed-in" content="#{user_signed_in?}">).html_safe
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,28 @@
|
||||||
module ItemTradesHelper
|
module ItemTradesHelper
|
||||||
def vague_trade_timestamp(last_trade_activity_at)
|
def vague_trade_timestamp(trade)
|
||||||
if last_trade_activity_at >= 1.week.ago
|
return nil if trade.nil?
|
||||||
|
|
||||||
|
if trade.last_activity_at >= 1.week.ago
|
||||||
translate "item_trades.index.table.last_active.this_week"
|
translate "item_trades.index.table.last_active.this_week"
|
||||||
else
|
else
|
||||||
last_trade_activity_at.strftime("%b %Y")
|
trade.last_activity_at.to_date.to_fs(:month_and_year)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def same_vague_trade_timestamp?(trade1, trade2)
|
||||||
|
vague_trade_timestamp(trade1) == vague_trade_timestamp(trade2)
|
||||||
|
end
|
||||||
|
|
||||||
def sorted_vaguely_by_trade_activity(trades)
|
def sorted_vaguely_by_trade_activity(trades)
|
||||||
# First, sort the list in ascending order.
|
# First, sort the list in ascending order.
|
||||||
trades_ascending = trades.sort_by do |trade|
|
trades_ascending = trades.sort_by do |trade|
|
||||||
if trade.user.last_trade_activity_at >= 1.week.ago
|
if trade.last_activity_at >= 1.week.ago
|
||||||
# Sort recent trades in a random order, but still collectively as the
|
# Sort recent trades in a random order, but still collectively as the
|
||||||
# most recent. (This discourages spamming updates to game the system!)
|
# most recent. (This discourages spamming updates to game the system!)
|
||||||
[1, rand]
|
[1, rand]
|
||||||
else
|
else
|
||||||
# Sort older trades by last trade activity.
|
# Sort older trades by last trade activity.
|
||||||
[0, trade.user.last_trade_activity_at]
|
[0, trade.last_activity_at]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,28 +14,42 @@ module ItemsHelper
|
||||||
}
|
}
|
||||||
|
|
||||||
Sizes = {
|
Sizes = {
|
||||||
face: 1,
|
face: 1, # 50x50
|
||||||
thumb: 2,
|
face_3x: 6, # 150x150
|
||||||
zoom: 3,
|
|
||||||
full: 4,
|
thumb: 2, # 150x150
|
||||||
face_2x: 6,
|
full: 4, # 300x300
|
||||||
|
large: 5, # 500x500
|
||||||
|
xlarge: 7, # 640x640
|
||||||
|
|
||||||
|
zoom: 3, # 80x80
|
||||||
|
autocrop: 9, # <varies>
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeUpgrades = {
|
||||||
|
face: :face_3x,
|
||||||
|
thumb: :full,
|
||||||
|
full: :xlarge,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def pet_type_image_url(pet_type, emotion: :happy, size: :face)
|
def pet_type_image_url(pet_type, emotion: :happy, size: :face)
|
||||||
PetTypeImage::Template.expand(
|
PetTypeImage::Template.expand(
|
||||||
hash: pet_type.basic_image_hash || pet_type.image_hash,
|
hash: pet_type.basic_image_hash || pet_type.image_hash,
|
||||||
emotion: PetTypeImage::Emotions[emotion],
|
emotion: PetTypeImage::Emotions.fetch(emotion),
|
||||||
size: PetTypeImage::Sizes[size],
|
size: PetTypeImage::Sizes.fetch(size),
|
||||||
).to_s
|
).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def standard_species_search_links
|
def standard_species_search_links
|
||||||
build_on_pet_types(Species.alphabetical) do |pet_type|
|
all_species = Species.alphabetical.map(&:id)
|
||||||
image = pet_type_image(pet_type, :happy, :zoom)
|
PetType.random_basic_per_species(all_species).map do |pet_type|
|
||||||
|
human_name = pet_type.species.human_name
|
||||||
|
image = pet_type_image pet_type, :happy, :zoom,
|
||||||
|
alt: human_name, title: human_name
|
||||||
query = "species:#{pet_type.species.name}"
|
query = "species:#{pet_type.species.name}"
|
||||||
link_to(image, items_path(:q => query))
|
link_to(image, items_path(:q => query))
|
||||||
end
|
end.join.html_safe
|
||||||
end
|
end
|
||||||
|
|
||||||
def closet_list_verb(owned)
|
def closet_list_verb(owned)
|
||||||
|
|
@ -113,13 +127,6 @@ module ItemsHelper
|
||||||
SHOP_WIZARD_URL_TEMPLATE.expand(string: item_or_name).to_s
|
SHOP_WIZARD_URL_TEMPLATE.expand(string: item_or_name).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
SUPER_SHOP_WIZARD_URL_TEMPLATE = Addressable::Template.new(
|
|
||||||
"https://www.neopets.com/portal/supershopwiz.phtml{?string}"
|
|
||||||
)
|
|
||||||
def super_shop_wizard_url_for(item)
|
|
||||||
SUPER_SHOP_WIZARD_URL_TEMPLATE.expand(string: item.name).to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
TRADING_POST_URL_TEMPLATE = Addressable::Template.new(
|
TRADING_POST_URL_TEMPLATE = Addressable::Template.new(
|
||||||
"https://www.neopets.com/island/tradingpost.phtml?type=browse&criteria=item_exact{&search_string}"
|
"https://www.neopets.com/island/tradingpost.phtml?type=browse&criteria=item_exact{&search_string}"
|
||||||
)
|
)
|
||||||
|
|
@ -144,7 +151,7 @@ module ItemsHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def nc_trade_value_updated_at_text(nc_trade_value)
|
def nc_trade_value_updated_at_text(nc_trade_value)
|
||||||
return nil if nc_trade_value.updated_at.nil?
|
return "NC trade value" if nc_trade_value.updated_at.nil?
|
||||||
|
|
||||||
# Render both "[X] [days] ago", and also the exact date, only including the
|
# Render both "[X] [days] ago", and also the exact date, only including the
|
||||||
# year if it's not this same year.
|
# year if it's not this same year.
|
||||||
|
|
@ -153,7 +160,7 @@ module ItemsHelper
|
||||||
nc_trade_value.updated_at.strftime("%b %-d") :
|
nc_trade_value.updated_at.strftime("%b %-d") :
|
||||||
nc_trade_value.updated_at.strftime("%b %-d, %Y")
|
nc_trade_value.updated_at.strftime("%b %-d, %Y")
|
||||||
|
|
||||||
"Last updated: #{date_str} (#{time_ago_str} ago)"
|
"NC trade value—Last updated: #{date_str} (#{time_ago_str} ago)"
|
||||||
end
|
end
|
||||||
|
|
||||||
NC_TRADE_VALUE_ESTIMATE_PATTERN = %r{
|
NC_TRADE_VALUE_ESTIMATE_PATTERN = %r{
|
||||||
|
|
@ -177,7 +184,7 @@ module ItemsHelper
|
||||||
# nicely for our use case.
|
# nicely for our use case.
|
||||||
def nc_trade_value_estimate_text(nc_trade_value)
|
def nc_trade_value_estimate_text(nc_trade_value)
|
||||||
match = nc_trade_value.value_text.match(NC_TRADE_VALUE_ESTIMATE_PATTERN)
|
match = nc_trade_value.value_text.match(NC_TRADE_VALUE_ESTIMATE_PATTERN)
|
||||||
return nc_trade_value if match.nil?
|
return nc_trade_value.value_text if match.nil?
|
||||||
|
|
||||||
match => {single:, low:, high:}
|
match => {single:, low:, high:}
|
||||||
if single.present?
|
if single.present?
|
||||||
|
|
@ -185,7 +192,7 @@ module ItemsHelper
|
||||||
elsif low.present? && high.present?
|
elsif low.present? && high.present?
|
||||||
"#{low}–#{high} capsules"
|
"#{low}–#{high} capsules"
|
||||||
else
|
else
|
||||||
nc_trade_value
|
nc_trade_value.value_text
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -228,21 +235,35 @@ module ItemsHelper
|
||||||
cookies["DTIOutfitViewerIsPlaying"] == "true"
|
cookies["DTIOutfitViewerIsPlaying"] == "true"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def item_fits?(item, pet_type)
|
||||||
|
item.appearances.any? { |a| a.fits? pet_type }
|
||||||
def build_on_pet_types(species, special_color=nil, &block)
|
|
||||||
species_ids = species.map(&:id)
|
|
||||||
pet_types = special_color ?
|
|
||||||
PetType.where(:color_id => special_color.id, :species_id => species_ids).
|
|
||||||
order(:species_id) :
|
|
||||||
PetType.random_basic_per_species(species.map(&:id))
|
|
||||||
pet_types.map(&block).join.html_safe
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def pet_type_image(pet_type, emotion, size)
|
def species_face_tooltip(pet_type, item)
|
||||||
|
if item_fits?(item, pet_type)
|
||||||
|
"#{pet_type.species.human_name}"
|
||||||
|
else
|
||||||
|
"#{pet_type.species.human_name}: No data yet"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def item_zone_partial_fit?(appearances_in_zone, all_appearances)
|
||||||
|
appearances_in_zone.size < all_appearances.size
|
||||||
|
end
|
||||||
|
|
||||||
|
def item_zone_species_list(appearances_in_zone)
|
||||||
|
appearances_in_zone.map(&:species).uniq.map(&:human_name).sort.join(", ")
|
||||||
|
end
|
||||||
|
|
||||||
|
def pet_type_image(pet_type, emotion, size, **options)
|
||||||
src = pet_type_image_url(pet_type, emotion:, size:)
|
src = pet_type_image_url(pet_type, emotion:, size:)
|
||||||
human_name = pet_type.species.name.humanize
|
|
||||||
image_tag(src, :alt => human_name, :title => human_name)
|
size_2x = PetTypeImage::SizeUpgrades[size]
|
||||||
|
srcset = if size_2x
|
||||||
|
[[pet_type_image_url(pet_type, emotion:, size: size_2x), "2x"]]
|
||||||
|
end
|
||||||
|
|
||||||
|
image_tag(src, srcset:, **options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def item_header_user_lists_form_state
|
def item_header_user_lists_form_state
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
module OutfitsHelper
|
module OutfitsHelper
|
||||||
LAST_DAY_OF_NEOPASS_ANNOUNCEMENT = Date.parse("2024-05-05")
|
|
||||||
def show_neopass_announcement?
|
|
||||||
Date.today <= LAST_DAY_OF_NEOPASS_ANNOUNCEMENT
|
|
||||||
end
|
|
||||||
|
|
||||||
def destination_tag(value)
|
def destination_tag(value)
|
||||||
hidden_field_tag 'destination', value, :id => nil
|
hidden_field_tag 'destination', value, :id => nil
|
||||||
end
|
end
|
||||||
|
|
@ -69,5 +64,28 @@ module OutfitsHelper
|
||||||
options = {:spellcheck => false, :id => nil}.merge(options)
|
options = {:spellcheck => false, :id => nil}.merge(options)
|
||||||
text_field_tag 'name', nil, options
|
text_field_tag 'name', nil, options
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
|
def outfit_viewer(...)
|
||||||
|
render partial: "outfit_viewer",
|
||||||
|
locals: parse_outfit_viewer_options(...)
|
||||||
|
end
|
||||||
|
|
||||||
|
def support_outfit_viewer(...)
|
||||||
|
render partial: "support_outfit_viewer",
|
||||||
|
locals: parse_outfit_viewer_options(...)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def parse_outfit_viewer_options(
|
||||||
|
outfit=nil, pet_state: nil, preferred_image_format: :png, **html_options
|
||||||
|
)
|
||||||
|
outfit = Outfit.new(pet_state:) if outfit.nil? && pet_state.present?
|
||||||
|
|
||||||
|
if outfit.nil?
|
||||||
|
raise ArgumentError, "outfit viewer must have outfit or pet state"
|
||||||
|
end
|
||||||
|
|
||||||
|
{outfit:, preferred_image_format:, html_options:}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
||||||
41
app/helpers/pet_states_helper.rb
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
module PetStatesHelper
|
||||||
|
def pose_name(pose)
|
||||||
|
case pose
|
||||||
|
when "HAPPY_FEM"
|
||||||
|
"Happy (Feminine)"
|
||||||
|
when "HAPPY_MASC"
|
||||||
|
"Happy (Masculine)"
|
||||||
|
when "SAD_FEM"
|
||||||
|
"Sad (Feminine)"
|
||||||
|
when "SAD_MASC"
|
||||||
|
"Sad (Masculine)"
|
||||||
|
when "SICK_FEM"
|
||||||
|
"Sick (Feminine)"
|
||||||
|
when "SICK_MASC"
|
||||||
|
"Sick (Masculine)"
|
||||||
|
when "UNCONVERTED"
|
||||||
|
"Unconverted"
|
||||||
|
else
|
||||||
|
"Not labeled yet"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
POSE_OPTIONS = %w(HAPPY_FEM SAD_FEM SICK_FEM HAPPY_MASC SAD_MASC SICK_MASC
|
||||||
|
UNCONVERTED UNKNOWN)
|
||||||
|
def pose_options
|
||||||
|
POSE_OPTIONS
|
||||||
|
end
|
||||||
|
|
||||||
|
def useful_pet_state_path(pet_type, pet_state)
|
||||||
|
if support_staff?
|
||||||
|
edit_pet_type_pet_state_path(pet_type, pet_state)
|
||||||
|
else
|
||||||
|
wardrobe_path(
|
||||||
|
color: pet_type.color_id,
|
||||||
|
species: pet_type.species_id,
|
||||||
|
pose: pet_state.pose,
|
||||||
|
state: pet_state.id,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
16
app/helpers/pet_types_helper.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
module PetTypesHelper
|
||||||
|
def moon_progress(num, total)
|
||||||
|
nearest_quarter = (4.0 * num / total).round / 4.0
|
||||||
|
if nearest_quarter >= 1
|
||||||
|
"🌕️"
|
||||||
|
elsif nearest_quarter >= 0.75
|
||||||
|
"🌔"
|
||||||
|
elsif nearest_quarter >= 0.5
|
||||||
|
"🌓"
|
||||||
|
elsif nearest_quarter >= 0.25
|
||||||
|
"🌒"
|
||||||
|
else
|
||||||
|
"🌑"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
64
app/helpers/support_form_helper.rb
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
module SupportFormHelper
|
||||||
|
class SupportFormBuilder < ActionView::Helpers::FormBuilder
|
||||||
|
attr_reader :template
|
||||||
|
delegate :capture, :check_box_tag, :concat, :content_tag,
|
||||||
|
:hidden_field_tag, :params, :render,
|
||||||
|
to: :template, private: true
|
||||||
|
|
||||||
|
def errors
|
||||||
|
render partial: "application/support_form/errors", locals: {form: self}
|
||||||
|
end
|
||||||
|
|
||||||
|
def fields(&block)
|
||||||
|
content_tag(:ul, class: "fields", &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def field(**options, &block)
|
||||||
|
content_tag(:li, **options, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def radio_fieldset(legend, **options, &block)
|
||||||
|
render partial: "application/support_form/radio_fieldset",
|
||||||
|
locals: {form: self, legend:, options:, content: capture(&block)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def radio_field(**options, &block)
|
||||||
|
content_tag(:li) do
|
||||||
|
content_tag(:label, **options, &block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def radio_grid_fieldset(*args, &block)
|
||||||
|
radio_fieldset(*args, "data-type": "radio-grid", &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def thumbnail_input(method)
|
||||||
|
render partial: "application/support_form/thumbnail_input",
|
||||||
|
locals: {form: self, method:}
|
||||||
|
end
|
||||||
|
|
||||||
|
def actions(&block)
|
||||||
|
content_tag(:section, class: "actions", &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def go_to_next_field(after: nil, **options, &block)
|
||||||
|
content_tag(:label, class: "go-to-next", **options) do
|
||||||
|
concat hidden_field_tag(:after, after) if after
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def go_to_next_check_box(value)
|
||||||
|
check_box_tag "next", value, checked: params[:next] == value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def support_form_with(**options, &block)
|
||||||
|
form_with(
|
||||||
|
builder: SupportFormBuilder,
|
||||||
|
**options,
|
||||||
|
class: ["support-form", options[:class]],
|
||||||
|
&block
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||