Compare commits

..

No commits in common. "c7cf1d21116ea110a4619f56973f224c8e773463" and "b06149cf220fe06042ca2acd0df720dacf1eb87c" have entirely different histories.

11 changed files with 55 additions and 117 deletions

View file

@ -115,7 +115,7 @@ class OutfitsController < ApplicationController
def outfit_params def outfit_params
params.require(:outfit).permit( params.require(:outfit).permit(
:name, :starred, :alt_style_id, item_ids: {worn: [], closeted: []}, :name, :starred, item_ids: {worn: [], closeted: []},
biology: [:species_id, :color_id, :pose]) biology: [:species_id, :color_id, :pose])
end end

View file

@ -23,7 +23,7 @@ import {
useToast, useToast,
useToken, useToken,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { ChevronDownIcon, WarningTwoIcon } from "@chakra-ui/icons"; import { ChevronDownIcon } from "@chakra-ui/icons";
import { loadable } from "../util"; import { loadable } from "../util";
import { petAppearanceFragment } from "../components/useOutfitAppearance"; import { petAppearanceFragment } from "../components/useOutfitAppearance";
@ -42,7 +42,6 @@ import twemojiSunglasses from "../images/twemoji/sunglasses.svg";
import twemojiQuestion from "../images/twemoji/question.svg"; import twemojiQuestion from "../images/twemoji/question.svg";
import twemojiMasc from "../images/twemoji/masc.svg"; import twemojiMasc from "../images/twemoji/masc.svg";
import twemojiFem from "../images/twemoji/fem.svg"; import twemojiFem from "../images/twemoji/fem.svg";
import twemojiHourglass from "../images/twemoji/hourglass.svg";
const PosePickerSupport = loadable(() => import("./support/PosePickerSupport")); const PosePickerSupport = loadable(() => import("./support/PosePickerSupport"));
@ -219,11 +218,18 @@ function PosePicker({
gap="2" gap="2"
index={tabIndex} index={tabIndex}
onChange={setTabIndex} onChange={setTabIndex}
// HACK: To only apply `initialFocusRef` to the selected input
// in the *active* tab, we just use `isLazy` to only *render*
// the active tab. We could also watch the tab state and set
// the ref accordingly!
isLazy
> >
<SupportOnly>
<TabList paddingX="2" paddingY="0"> <TabList paddingX="2" paddingY="0">
<Tab width="50%">Expressions</Tab> <Tab width="50%">Expressions</Tab>
<Tab width="50%">Styles</Tab> <Tab width="50%">Styles</Tab>
</TabList> </TabList>
</SupportOnly>
<TabPanels position="relative"> <TabPanels position="relative">
<TabPanel paddingX="4" paddingY="0"> <TabPanel paddingX="4" paddingY="0">
{isInSupportMode ? ( {isInSupportMode ? (
@ -232,9 +238,7 @@ function PosePicker({
colorId={colorId} colorId={colorId}
pose={pose} pose={pose}
appearanceId={appearanceId} appearanceId={appearanceId}
initialFocusRef={ initialFocusRef={initialFocusRef}
tabIndex === 0 ? initialFocusRef : null
}
dispatchToOutfit={dispatchToOutfit} dispatchToOutfit={dispatchToOutfit}
/> />
) : ( ) : (
@ -242,9 +246,7 @@ function PosePicker({
<PosePickerTable <PosePickerTable
poseInfos={poseInfos} poseInfos={poseInfos}
onChange={onChangePose} onChange={onChangePose}
initialFocusRef={ initialFocusRef={initialFocusRef}
tabIndex === 0 ? initialFocusRef : null
}
/> />
{numStandardPoses == 0 && ( {numStandardPoses == 0 && (
<PosePickerEmptyExplanation /> <PosePickerEmptyExplanation />
@ -265,7 +267,7 @@ function PosePicker({
selectedStyleId={altStyleId} selectedStyleId={altStyleId}
altStyles={altStyles} altStyles={altStyles}
onChange={onChangeStyle} onChange={onChangeStyle}
initialFocusRef={tabIndex === 1 ? initialFocusRef : null} initialFocusRef={initialFocusRef}
/> />
<StyleExplanation /> <StyleExplanation />
</TabPanel> </TabPanel>
@ -422,22 +424,14 @@ function PosePickerTable({ poseInfos, onChange, initialFocusRef }) {
</tbody> </tbody>
</table> </table>
{poseInfos.unconverted.isAvailable && ( {poseInfos.unconverted.isAvailable && (
<Flex
align="center"
justify="center"
gap="1"
marginTop="2"
marginBottom="2"
>
<PoseOption <PoseOption
poseInfo={poseInfos.unconverted} poseInfo={poseInfos.unconverted}
onChange={onChange} onChange={onChange}
inputRef={poseInfos.unconverted.isSelected && initialFocusRef} inputRef={poseInfos.unconverted.isSelected && initialFocusRef}
size="sm" size="sm"
label="Retired UC" label="Unconverted"
marginTop="2"
/> />
<RetiredUCWarning isSelected={poseInfos.unconverted.isSelected} />
</Flex>
)} )}
</Box> </Box>
); );
@ -630,40 +624,6 @@ function PosePickerEmptyExplanation() {
); );
} }
function RetiredUCWarning({ isSelected }) {
return (
<Popover placement="right" trigger="hover">
<PopoverTrigger>
<Box
as="button"
tabIndex="0"
aria-label="Warning"
cursor="help"
lineHeight="1"
opacity={isSelected ? "1" : "0.75"}
transform={isSelected ? "scale(1)" : "scale(0.8)"}
color={isSelected ? "yellow.500" : "inherit"}
transition="all 0.2s"
padding="1"
>
<WarningTwoIcon />
</Box>
</PopoverTrigger>
<PopoverContent
background="blackAlpha.800"
borderColor="blackAlpha.900"
color="white"
padding="2"
fontSize="sm"
>
"Unconverted" pets are no longer available on Neopets.com, and have been
replaced with the very similar Styles feature. We're just keeping this
as an archive!
</PopoverContent>
</Popover>
);
}
function StyleSelect({ function StyleSelect({
selectedStyleId, selectedStyleId,
altStyles, altStyles,
@ -788,6 +748,10 @@ function StyleExplanation() {
Styling Chamber Styling Chamber
</Box> </Box>
. Not all items fit Alt Style pets. The pet's color doesn't have to match. . Not all items fit Alt Style pets. The pet's color doesn't have to match.
<SupportOnly>
<br />
WIP: Only Support staff see this tab for now! 💖
</SupportOnly>
</Box> </Box>
); );
} }
@ -925,7 +889,7 @@ function getIcon(pose) {
} else if (["SICK_MASC", "SICK_FEM"].includes(pose)) { } else if (["SICK_MASC", "SICK_FEM"].includes(pose)) {
return twemojiSick; return twemojiSick;
} else if (pose === "UNCONVERTED") { } else if (pose === "UNCONVERTED") {
return twemojiHourglass; return twemojiSunglasses;
} else { } else {
return twemojiSmile; return twemojiSmile;
} }
@ -939,7 +903,7 @@ function getLabel(pose) {
} else if (pose === "SICK_MASC" || pose === "SICK_FEM") { } else if (pose === "SICK_MASC" || pose === "SICK_FEM") {
return "Sick"; return "Sick";
} else if (pose === "UNCONVERTED") { } else if (pose === "UNCONVERTED") {
return "Retired UC"; return "Classic UC";
} else { } else {
return "Default"; return "Default";
} }

View file

@ -69,7 +69,6 @@ function useOutfitSaving(outfitState, dispatchToOutfit) {
speciesId: outfitState.speciesId, speciesId: outfitState.speciesId,
colorId: outfitState.colorId, colorId: outfitState.colorId,
pose: outfitState.pose, pose: outfitState.pose,
altStyleId: outfitState.altStyleId,
wornItemIds: [...outfitState.wornItemIds], wornItemIds: [...outfitState.wornItemIds],
closetedItemIds: [...outfitState.closetedItemIds], closetedItemIds: [...outfitState.closetedItemIds],
}) })

View file

@ -447,7 +447,6 @@ function getOutfitStateFromOutfitData(outfit) {
speciesId: outfit.speciesId, speciesId: outfit.speciesId,
colorId: outfit.colorId, colorId: outfit.colorId,
pose: outfit.pose, pose: outfit.pose,
altStyleId: outfit.altStyleId,
wornItemIds: new Set(outfit.wornItemIds), wornItemIds: new Set(outfit.wornItemIds),
closetedItemIds: new Set(outfit.closetedItemIds), closetedItemIds: new Set(outfit.closetedItemIds),
}; };
@ -704,6 +703,12 @@ function buildOutfitQueryString(outfitState) {
color: colorId || "", color: colorId || "",
pose: pose || "", pose: pose || "",
}); });
for (const itemId of wornItemIds) {
params.append("objects[]", itemId);
}
for (const itemId of closetedItemIds) {
params.append("closet[]", itemId);
}
if (altStyleId != null) { if (altStyleId != null) {
params.append("style", altStyleId); params.append("style", altStyleId);
} }
@ -712,12 +717,6 @@ function buildOutfitQueryString(outfitState) {
// refers to "PetState", the database table name for pet appearances. // refers to "PetState", the database table name for pet appearances.
params.append("state", appearanceId); params.append("state", appearanceId);
} }
for (const itemId of wornItemIds) {
params.append("objects[]", itemId);
}
for (const itemId of closetedItemIds) {
params.append("closet[]", itemId);
}
return params.toString(); return params.toString();
} }

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#FFE8B6" d="M21 18c0-2.001 3.246-3.369 5-6 2-3 2-10 2-10H8s0 7 2 10c1.754 2.631 5 3.999 5 6s-3.246 3.369-5 6c-2 3-2 10-2 10h20s0-7-2-10c-1.754-2.631-5-3.999-5-6z"/><path fill="#FFAC33" d="M20.999 24c-.999 0-2.057-1-2.057-2C19 20.287 19 19.154 19 18c0-3.22 3.034-4.561 4.9-7H12.1c1.865 2.439 4.9 3.78 4.9 7 0 1.155 0 2.289.058 4 0 1-1.058 2-2.058 2-2 0-3.595 1.784-4 3-1 3-1 7-1 7h16s0-4-1-7c-.405-1.216-2.001-3-4.001-3z"/><path fill="#3B88C3" d="M30 34c0 1.104-.896 2-2 2H8c-1.104 0-2-.896-2-2s.896-2 2-2h20c1.104 0 2 .896 2 2zm0-32c0 1.104-.896 2-2 2H8c-1.104 0-2-.896-2-2s.896-2 2-2h20c1.104 0 2 .896 2 2z"/></svg>

Before

Width:  |  Height:  |  Size: 688 B

View file

@ -30,9 +30,7 @@ export function useDeleteOutfitMutation(options = {}) {
...options, ...options,
mutationFn: deleteOutfit, mutationFn: deleteOutfit,
onSuccess: (emptyData, id, context) => { onSuccess: (emptyData, id, context) => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({ queryKey: ["outfits", String(id)] });
queryKey: ["outfits", String(id)],
});
if (options.onSuccess) { if (options.onSuccess) {
options.onSuccess(emptyData, id, context); options.onSuccess(emptyData, id, context);
} }
@ -44,9 +42,7 @@ async function loadSavedOutfit(id) {
const res = await fetch(`/outfits/${encodeURIComponent(id)}.json`); const res = await fetch(`/outfits/${encodeURIComponent(id)}.json`);
if (!res.ok) { if (!res.ok) {
throw new Error( throw new Error(`loading outfit failed: ${res.status} ${res.statusText}`);
`loading outfit failed: ${res.status} ${res.statusText}`,
);
} }
return res.json().then(normalizeOutfit); return res.json().then(normalizeOutfit);
@ -58,7 +54,6 @@ async function saveOutfit({
speciesId, speciesId,
colorId, colorId,
pose, pose,
altStyleId,
wornItemIds, wornItemIds,
closetedItemIds, closetedItemIds,
}) { }) {
@ -70,7 +65,6 @@ async function saveOutfit({
color_id: colorId, color_id: colorId,
pose: pose, pose: pose,
}, },
alt_style_id: altStyleId,
item_ids: { worn: wornItemIds, closeted: closetedItemIds }, item_ids: { worn: wornItemIds, closeted: closetedItemIds },
}, },
}; };
@ -97,9 +91,7 @@ async function saveOutfit({
} }
if (!res.ok) { if (!res.ok) {
throw new Error( throw new Error(`saving outfit failed: ${res.status} ${res.statusText}`);
`saving outfit failed: ${res.status} ${res.statusText}`,
);
} }
return res.json().then(normalizeOutfit); return res.json().then(normalizeOutfit);
@ -114,9 +106,7 @@ async function deleteOutfit(id) {
}); });
if (!res.ok) { if (!res.ok) {
throw new Error( throw new Error(`deleting outfit failed: ${res.status} ${res.statusText}`);
`deleting outfit failed: ${res.status} ${res.statusText}`,
);
} }
} }
@ -127,11 +117,8 @@ function normalizeOutfit(outfit) {
speciesId: String(outfit.species_id), speciesId: String(outfit.species_id),
colorId: String(outfit.color_id), colorId: String(outfit.color_id),
pose: outfit.pose, pose: outfit.pose,
altStyleId: outfit.alt_style_id ? String(outfit.alt_style_id) : null,
wornItemIds: (outfit.item_ids?.worn || []).map((id) => String(id)), wornItemIds: (outfit.item_ids?.worn || []).map((id) => String(id)),
closetedItemIds: (outfit.item_ids?.closeted || []).map((id) => closetedItemIds: (outfit.item_ids?.closeted || []).map((id) => String(id)),
String(id),
),
creator: outfit.user ? { id: String(outfit.user.id) } : null, creator: outfit.user ? { id: String(outfit.user.id) } : null,
createdAt: outfit.created_at, createdAt: outfit.created_at,
updatedAt: outfit.updated_at, updatedAt: outfit.updated_at,

View file

@ -4,7 +4,6 @@ class Outfit < ApplicationRecord
class_name: 'ItemOutfitRelationship' class_name: 'ItemOutfitRelationship'
has_many :worn_items, through: :worn_item_outfit_relationships, source: :item has_many :worn_items, through: :worn_item_outfit_relationships, source: :item
belongs_to :alt_style, optional: true
belongs_to :pet_state, optional: true # We validate presence below! belongs_to :pet_state, optional: true # We validate presence below!
belongs_to :user, optional: true belongs_to :user, optional: true
@ -83,8 +82,7 @@ class Outfit < ApplicationRecord
def as_json(more_options={}) def as_json(more_options={})
serializable_hash( serializable_hash(
only: [:id, :name, :pet_state_id, :starred, :created_at, :updated_at, only: [:id, :name, :pet_state_id, :starred, :created_at, :updated_at],
:alt_style_id],
methods: [:color_id, :species_id, :pose, :item_ids, :user] methods: [:color_id, :species_id, :pose, :item_ids, :user]
) )
end end

View file

@ -1,12 +1,12 @@
- title "Styling Studio" - title "Styling Studio"
%p %p
Here's all the new NC Pet Styles we have! They're available in the app too, We're getting set up with the new NC Pet Styles! They're not ready in the app
by opening the emotion picker and clicking the "Styles" tab. yet, but here's what we have so far!
%p %p
If you have an Alt Style we don't, please model it by entering your pet's If you have one we don't, please model it by entering your pet's name on the
name on the homepage! Thank you! 💖 homepage! Thank you! 💖
%p %p
Also, heads-up: Style tokens are pretty different from normal wearables, so Also, heads-up: Style tokens are pretty different from normal wearables, so

View file

@ -3,11 +3,11 @@
= advertise_campaign_progress @campaign = advertise_campaign_progress @campaign
.notice .notice
%strong Alt styles are ready now! %strong Happy NC UC day!
You can find them by opening the emotion picker, then clicking "Styles". We're working on Styling Studio support,
= link_to("here's what we have so far", alt_styles_path) + "!"
%br %br
= link_to("Here's our reference page, too!", alt_styles_path) Thank you for helping us model the new styles, we appreciate it lots!!! 💖
Thanks again for all your help, let us know how it works for you! 💖
%p#pet-not-found.alert= t 'pets.load.not_found' %p#pet-not-found.alert= t 'pets.load.not_found'

View file

@ -1,5 +0,0 @@
class AddAltStyleIdToOutfits < ActiveRecord::Migration[7.1]
def change
add_reference :outfits, :alt_style, null: true, foreign_key: true
end
end

View file

@ -10,13 +10,13 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_02_01_134440) do ActiveRecord::Schema[7.1].define(version: 2024_01_29_114639) do
create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.integer "species_id", null: false t.integer "species_id", null: false
t.integer "color_id", null: false t.integer "color_id", null: false
t.integer "body_id", null: false t.integer "body_id", null: false
t.datetime "created_at", precision: nil, null: false t.datetime "created_at", null: false
t.datetime "updated_at", precision: nil, null: false t.datetime "updated_at", null: false
t.index ["color_id"], name: "index_alt_styles_on_color_id" t.index ["color_id"], name: "index_alt_styles_on_color_id"
t.index ["species_id"], name: "index_alt_styles_on_species_id" t.index ["species_id"], name: "index_alt_styles_on_species_id"
end end
@ -125,11 +125,11 @@ ActiveRecord::Schema[7.1].define(version: 2024_02_01_134440) do
t.index ["outfit_id", "is_worn"], name: "index_item_outfit_relationships_on_outfit_id_and_is_worn" t.index ["outfit_id", "is_worn"], name: "index_item_outfit_relationships_on_outfit_id_and_is_worn"
end end
create_table "item_translations", id: :integer, charset: "latin1", collation: "latin1_swedish_ci", force: :cascade do |t| create_table "item_translations", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.integer "item_id" t.integer "item_id"
t.string "locale" t.string "locale"
t.string "name" t.string "name"
t.text "description" t.text "description", size: :medium
t.string "rarity" t.string "rarity"
t.datetime "created_at", precision: nil t.datetime "created_at", precision: nil
t.datetime "updated_at", precision: nil t.datetime "updated_at", precision: nil
@ -191,8 +191,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_02_01_134440) do
t.string "image" t.string "image"
t.string "image_layers_hash" t.string "image_layers_hash"
t.boolean "image_enqueued", default: false, null: false t.boolean "image_enqueued", default: false, null: false
t.bigint "alt_style_id"
t.index ["alt_style_id"], name: "index_outfits_on_alt_style_id"
t.index ["pet_state_id"], name: "index_outfits_on_pet_state_id" t.index ["pet_state_id"], name: "index_outfits_on_pet_state_id"
t.index ["user_id"], name: "index_outfits_on_user_id" t.index ["user_id"], name: "index_outfits_on_user_id"
end end
@ -316,5 +314,4 @@ ActiveRecord::Schema[7.1].define(version: 2024_02_01_134440) do
add_foreign_key "alt_styles", "colors" add_foreign_key "alt_styles", "colors"
add_foreign_key "alt_styles", "species" add_foreign_key "alt_styles", "species"
add_foreign_key "outfits", "alt_styles"
end end