Compare commits
No commits in common. "a14c4fca488deab4bb49edb72d4773f94775f449" and "8a8dd468bee857dcde59a3d2dca9ebb9d7bbec73" have entirely different histories.
a14c4fca48
...
8a8dd468be
25 changed files with 831 additions and 548 deletions
|
@ -1,5 +1,116 @@
|
|||
var DEBUG = document.location.search.substr(0, 6) == "?debug";
|
||||
|
||||
function petThumbnailUrl(pet_name) {
|
||||
// if first character is "@", use the hash url
|
||||
if (pet_name[0] == "@") {
|
||||
return "https://pets.neopets.com/cp/" + pet_name.substr(1) + "/1/1.png";
|
||||
}
|
||||
|
||||
return "https://pets.neopets.com/cpn/" + pet_name + "/1/1.png";
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* 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 */
|
||||
(function () {
|
||||
var form = $("#bulk-pets-form"),
|
||||
|
@ -11,15 +122,6 @@ var DEBUG = document.location.search.substr(0, 6) == "?debug";
|
|||
|
||||
$(document.body).addClass("js");
|
||||
|
||||
function petThumbnailUrl(pet_name) {
|
||||
// if first character is "@", use the hash url
|
||||
if (pet_name[0] == "@") {
|
||||
return "https://pets.neopets.com/cp/" + pet_name.substr(1) + "/1/1.png";
|
||||
}
|
||||
|
||||
return "https://pets.neopets.com/cpn/" + pet_name + "/1/1.png";
|
||||
}
|
||||
|
||||
bulk_load_queue = new (function BulkLoadQueue() {
|
||||
var RECENTLY_SENT_INTERVAL_IN_SECONDS = 30;
|
||||
var RECENTLY_SENT_MAX = 3;
|
||||
|
|
30
app/assets/stylesheets/_items.sass
Normal file
30
app/assets/stylesheets/_items.sass
Normal file
|
@ -0,0 +1,30 @@
|
|||
@import "partials/campaign-progress"
|
||||
|
||||
body.items-index, body.items-show, body.items-needed, body.item_trades
|
||||
+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
|
||||
|
|
@ -14,6 +14,10 @@
|
|||
@import closet_lists/form
|
||||
@import neopets_page_import_tasks/new
|
||||
@import contributions/index
|
||||
@import items
|
||||
@import items/index
|
||||
@import items/show
|
||||
@import item_trades/index
|
||||
@import outfits/index
|
||||
@import outfits/new
|
||||
@import pets/bulk
|
||||
|
|
29
app/assets/stylesheets/item_trades/_index.sass
Normal file
29
app/assets/stylesheets/item_trades/_index.sass
Normal file
|
@ -0,0 +1,29 @@
|
|||
@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: ", "
|
|
@ -1,28 +0,0 @@
|
|||
@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
|
||||
|
||||
.trade-list-names
|
||||
list-style: none
|
||||
|
||||
li
|
||||
display: inline
|
||||
|
||||
&:not(:last-child)::after
|
||||
content: ", "
|
25
app/assets/stylesheets/items/_index.sass
Normal file
25
app/assets/stylesheets/items/_index.sass
Normal file
|
@ -0,0 +1,25 @@
|
|||
=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
|
||||
|
401
app/assets/stylesheets/items/_show.sass
Normal file
401
app/assets/stylesheets/items/_show.sass
Normal file
|
@ -0,0 +1,401 @@
|
|||
@import "../partials/clean/constants"
|
||||
@import "../partials/clean/mixins"
|
||||
@import "../partials/item_header"
|
||||
|
||||
body.items-show
|
||||
#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
|
||||
display: block
|
||||
position: relative
|
||||
width: 300px
|
||||
height: 300px
|
||||
border: 1px solid $module-border-color
|
||||
border-radius: 1em
|
||||
overflow: hidden
|
||||
|
||||
// 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 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. We also wait for the outfit-viewer to
|
||||
// execute a `setTimeout(0)`, to make sure we always *start* in the
|
||||
// non-loading state. This is because it's sometimes possible for the page to
|
||||
// start with the web component already in `state(loading)`, and we need to
|
||||
// make sure we *start* in *non-loading* state for the transition delay to
|
||||
// happen. (This can happen when you Turbo-navigate between multiple items.)
|
||||
#item-preview[busy] outfit-viewer, outfit-viewer:has(outfit-layer:state(loading))
|
||||
cursor: wait
|
||||
|
||||
&:state(after-first-frame)
|
||||
.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
|
||||
|
||||
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
|
|
@ -1,23 +0,0 @@
|
|||
=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
|
|
@ -1,400 +0,0 @@
|
|||
@import "../partials/clean/constants"
|
||||
@import "../partials/clean/mixins"
|
||||
@import "../partials/item_header"
|
||||
|
||||
#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
|
||||
display: block
|
||||
position: relative
|
||||
width: 300px
|
||||
height: 300px
|
||||
border: 1px solid $module-border-color
|
||||
border-radius: 1em
|
||||
overflow: hidden
|
||||
|
||||
// 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 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. We also wait for the outfit-viewer to
|
||||
// execute a `setTimeout(0)`, to make sure we always *start* in the
|
||||
// non-loading state. This is because it's sometimes possible for the page to
|
||||
// start with the web component already in `state(loading)`, and we need to
|
||||
// make sure we *start* in *non-loading* state for the transition delay to
|
||||
// happen. (This can happen when you Turbo-navigate between multiple items.)
|
||||
#item-preview[busy] outfit-viewer, outfit-viewer:has(outfit-layer:state(loading))
|
||||
cursor: wait
|
||||
|
||||
&:state(after-first-frame)
|
||||
.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
|
||||
|
||||
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
|
|
@ -1,28 +0,0 @@
|
|||
@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
|
|
@ -2,8 +2,70 @@
|
|||
@import "../partials/clean/mixins"
|
||||
|
||||
body.pets-bulk
|
||||
#bulk-pets-form
|
||||
#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
|
||||
border-top: 1px solid $module-border-color
|
||||
margin-top: 12px
|
||||
padding-top: 12px
|
||||
|
||||
|
|
|
@ -112,6 +112,27 @@ class ItemsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def needed
|
||||
if params[:color] && params[:species]
|
||||
@pet_type = PetType.find_by_color_id_and_species_id(
|
||||
params[:color],
|
||||
params[:species]
|
||||
)
|
||||
end
|
||||
|
||||
unless @pet_type
|
||||
raise ActiveRecord::RecordNotFound, 'Pet type not found'
|
||||
end
|
||||
|
||||
@items = @pet_type.needed_items.order(:name)
|
||||
assign_closeted!(@items)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { @pet_name = params[:name] ; render :layout => 'application' }
|
||||
format.json { render :json => @items }
|
||||
end
|
||||
end
|
||||
|
||||
def sources
|
||||
# Load all the items, then group them by source.
|
||||
item_ids = params[:ids].split(",")
|
||||
|
|
|
@ -39,6 +39,7 @@ class PetsController < ApplicationController
|
|||
def destination
|
||||
case (params[:destination] || params[:origin])
|
||||
when 'wardrobe' then wardrobe_path
|
||||
when 'needed_items' then needed_items_path
|
||||
else root_path
|
||||
end
|
||||
end
|
||||
|
|
|
@ -79,6 +79,27 @@ class PetType < ApplicationRecord
|
|||
species_human_name: possibly_new_species.human_name)
|
||||
end
|
||||
|
||||
def needed_items
|
||||
# If I need this item on a pet type, that means that we've already seen it
|
||||
# and it's body-specific. So, there's a body-specific asset for the item,
|
||||
# but no asset that fits this pet type.
|
||||
i = Item.arel_table
|
||||
psa = ParentSwfAssetRelationship.arel_table
|
||||
sa = SwfAsset.arel_table
|
||||
|
||||
Item.where('(' + ParentSwfAssetRelationship.select('count(DISTINCT body_id)').joins(:swf_asset).
|
||||
where(
|
||||
psa[:parent_id].eq(i[:id]).and(
|
||||
psa[:parent_type].eq('Item').and(
|
||||
sa[:body_id].not_eq(self.body_id)))
|
||||
).to_sql + ') > 1').
|
||||
where(ParentSwfAssetRelationship.joins(:swf_asset).where(
|
||||
psa[:parent_id].eq(i[:id]).and(
|
||||
psa[:parent_type].eq('Item').and(
|
||||
sa[:body_id].in([self.body_id, 0])))
|
||||
).exists.not)
|
||||
end
|
||||
|
||||
def add_pet_state_from_biology!(biology)
|
||||
pet_state = PetState.from_pet_type_and_biology_info(self, biology)
|
||||
pet_state
|
||||
|
|
|
@ -34,7 +34,3 @@
|
|||
class: "not-in-a-list"
|
||||
- else
|
||||
%p= t(".no_trades_yet")
|
||||
|
||||
- content_for :stylesheets do
|
||||
= page_stylesheet_link_tag "layouts/items"
|
||||
= page_stylesheet_link_tag "item_trades/index"
|
||||
|
|
|
@ -37,6 +37,3 @@
|
|||
%h2= t '.species_search.header'
|
||||
= standard_species_search_links
|
||||
|
||||
- content_for :stylesheets do
|
||||
= page_stylesheet_link_tag "layouts/items"
|
||||
= page_stylesheet_link_tag "items/index"
|
||||
|
|
16
app/views/items/needed.html.haml
Normal file
16
app/views/items/needed.html.haml
Normal file
|
@ -0,0 +1,16 @@
|
|||
- title "Needed items for #{@pet_type.human_name}"
|
||||
%h2
|
||||
= image_tag "https://pets.neopets.com/cpn/#{@pet_name}/1/1.png",
|
||||
:class => 'inline-image'
|
||||
%span.pet-name= @pet_name
|
||||
can model…
|
||||
%ul.buttons
|
||||
%li
|
||||
= form_tag load_pet_path do
|
||||
= origin_tag 'needed_items'
|
||||
= hidden_field_tag 'name', @pet_name
|
||||
= submit_tag "I'm wearing one now!", :class => 'loud'
|
||||
%li
|
||||
= link_to 'What do I own?', 'https://www.neopets.com/closet.phtml',
|
||||
:class => 'button', :target => '_blank'
|
||||
= render @items
|
|
@ -122,8 +122,6 @@
|
|||
|
||||
- content_for :stylesheets do
|
||||
= stylesheet_link_tag "application/hanger-spinner"
|
||||
= page_stylesheet_link_tag "layouts/items"
|
||||
= page_stylesheet_link_tag "items/show"
|
||||
|
||||
- content_for :javascripts do
|
||||
= javascript_include_tag "lib/idiomorph", async: true
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
- else
|
||||
#{t 'app_name'}: #{t '.title_tagline'}
|
||||
%link{href: image_path('favicon.png'), rel: 'icon'}
|
||||
= stylesheet_link_tag "application"
|
||||
= yield :stylesheets
|
||||
= stylesheet_link_tag "application"
|
||||
- if use_responsive_design?
|
||||
%meta{name: "viewport", content: "width=device-width, initial-scale=1"}
|
||||
= yield :meta
|
||||
|
|
|
@ -1,20 +1,42 @@
|
|||
- title t('modeling_hub')
|
||||
|
||||
= form_tag load_pet_path, :id => 'needed-items-form' do
|
||||
- localized_cache :action_suffix => 'needed_items_content' do
|
||||
%h3= t '.needed_items.header'
|
||||
|
||||
%p= t '.needed_items.explanation'
|
||||
|
||||
= origin_tag bulk_pets_path
|
||||
= destination_tag 'needed_items'
|
||||
|
||||
%input#needed-items-pet-name-field{:type => "text", :name => "name"}/
|
||||
%input{:type => "submit", :value => t('.needed_items.submit')}/
|
||||
|
||||
#needed-items-alert.alert
|
||||
|
||||
#needed-items-pet.script-only
|
||||
%h4
|
||||
%img#needed-items-pet-thumbnail.inline-image
|
||||
%span#needed-items-pet-header
|
||||
%button#needed-items-reload= t '.needed_items.reload'
|
||||
|
||||
%ul#needed-items-pet-items
|
||||
|
||||
= form_tag load_pet_path, :id => 'bulk-pets-form' do
|
||||
- localized_cache :action_suffix => 'bulk_pets_content' do
|
||||
%h3= t '.header'
|
||||
%h3= t '.bulk_pets.header'
|
||||
|
||||
%p= t '.explanation'
|
||||
%p= t '.bulk_pets.explanation'
|
||||
|
||||
= origin_tag bulk_pets_path
|
||||
|
||||
%div.noscript
|
||||
%input{:name => "name", :type => "text"}/
|
||||
%input{:type => "submit", :value => t('.submit')}/
|
||||
%input{:type => "submit", :value => t('.bulk_pets.submit')}/
|
||||
%div.script-only
|
||||
%textarea
|
||||
%button#bulk-pets-form-add{:type => "button"}= t '.add'
|
||||
%button#bulk-pets-form-clear{:type => "button"}= t '.clear'
|
||||
%button#bulk-pets-form-add{:type => "button"}= t '.bulk_pets.add'
|
||||
%button#bulk-pets-form-clear{:type => "button"}= t '.bulk_pets.clear'
|
||||
%ul
|
||||
|
||||
- localized_cache :action_suffix => 'templates' do
|
||||
|
@ -48,12 +70,12 @@
|
|||
%li.waiting
|
||||
%img{:src => '${pet_thumbnail}'}
|
||||
%span.name ${pet_name}
|
||||
%span.waiting-message= t '.waiting'
|
||||
%span.loading-message= t '.loading'
|
||||
%span.waiting-message= t '.bulk_pets.waiting'
|
||||
%span.loading-message= t '.bulk_pets.loading'
|
||||
%span.response
|
||||
|
||||
%script#bulk-pets-submission-success-template{:type => 'text/x-jquery/tmpl'}
|
||||
= t '.submission_success', :points => '${points}'
|
||||
= t '.bulk_pets.submission_success', :points => '${points}'
|
||||
|
||||
- content_for :javascripts do
|
||||
= include_javascript_libraries :jquery, :jquery_tmpl
|
||||
|
|
|
@ -630,6 +630,16 @@ en-MEEP:
|
|||
|
||||
pets:
|
||||
bulk:
|
||||
needed_items:
|
||||
header: Looking for ways to contreepute?
|
||||
explanation:
|
||||
Meep your pet's name below and we'll tell you what items you can
|
||||
meep. Thanks for your meep!
|
||||
submit: Meep
|
||||
pet_header: Items %{pet_name} can meep
|
||||
reload: Remeep
|
||||
item_owned: You own this meepit
|
||||
bulk_pets:
|
||||
header: Meep pets in bulk
|
||||
explanation:
|
||||
Got a lot of pets to meep? Just keep meeping them into the box below,
|
||||
|
|
|
@ -751,6 +751,16 @@ en:
|
|||
|
||||
pets:
|
||||
bulk:
|
||||
needed_items:
|
||||
header: Looking for ways to contribute?
|
||||
explanation:
|
||||
Enter your pet's name below and we'll tell you what items you can
|
||||
model. Thanks for your help!
|
||||
submit: Submit
|
||||
pet_header: Items %{pet_name} can model
|
||||
reload: Reload
|
||||
item_owned: You own this item
|
||||
bulk_pets:
|
||||
header: Model pets in bulk
|
||||
explanation:
|
||||
Got a lot of pets to model? Just keep typing them into the box below,
|
||||
|
|
|
@ -498,6 +498,14 @@ es:
|
|||
default_human_name: (una especie nueva)
|
||||
pets:
|
||||
bulk:
|
||||
needed_items:
|
||||
header: ¿Quieres contribuir en la página?
|
||||
explanation: ¡Escribe el nombre de tu pet aquí abajo y te diremos qué objetos nos hace falta junto a esa especie de pet para actualizar la base de datos! ¡Gracias por tu ayuda!
|
||||
submit: Enviar
|
||||
pet_header: Objetos que tu pet %{pet_name} puede posar
|
||||
reload: Actualizar
|
||||
item_owned: Tú tienes este objeto
|
||||
bulk_pets:
|
||||
header: Hacer que desfilen varios pets a la vez
|
||||
explanation: "¿Tienes muchos pets para que desfilen?
|
||||
|
||||
|
|
|
@ -494,6 +494,14 @@ pt:
|
|||
default_human_name: (uma nova espécie)
|
||||
pets:
|
||||
bulk:
|
||||
needed_items:
|
||||
header: Procurando maneiras de ajudar?
|
||||
explanation: Digite o nome do seu pet e nós diremos quais itens você pode modelar. Obrigado pela ajuda
|
||||
submit: Enviar
|
||||
pet_header: Itens que %{pet_name} pode modelar
|
||||
reload: Recarregar
|
||||
item_owned: Você possui este item
|
||||
bulk_pets:
|
||||
header: Modele pets em massa
|
||||
explanation: Tem um monte de pets para modelar? Basta digitar seus nomes na caixa abaixo, ou até mesmo colar toda uma lista de nomes, um nome por linha. Obrigado por sua ajuda!
|
||||
submit: Carregar pet
|
||||
|
|
|
@ -26,6 +26,7 @@ OpenneoImpressItems::Application.routes.draw do
|
|||
resources :appearances, controller: 'item_appearances', only: [:index]
|
||||
|
||||
collection do
|
||||
get :needed
|
||||
get "sources/:ids", action: :sources
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue