import React from "react";
import { ClassNames } from "@emotion/react";
import gql from "graphql-tag";
import {
Box,
Button,
Center,
Flex,
FormControl,
FormHelperText,
FormLabel,
HStack,
IconButton,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
Link as ChakraLink,
ListItem,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverTrigger,
Skeleton,
Switch,
Textarea,
Tooltip,
UnorderedList,
useColorModeValue,
useTheme,
useToast,
VStack,
} from "@chakra-ui/react";
import { ArrowForwardIcon, SearchIcon } from "@chakra-ui/icons";
import { useLazyQuery, useQuery } from "@apollo/client";
import Link from "next/link";
import { useRouter } from "next/router";
import Image from "next/image";
import {
Delay,
ErrorMessage,
Heading1,
Heading2,
TestErrorSender,
useCommonStyles,
useLocalStorage,
} from "./util";
import OutfitPreview from "./components/OutfitPreview";
import SpeciesColorPicker from "./components/SpeciesColorPicker";
import SquareItemCard, {
SquareItemCardSkeleton,
} from "./components/SquareItemCard";
import WIPCallout from "./components/WIPCallout";
import { useAuthModeFeatureFlag } from "./components/useCurrentUser";
import HomepageSplashImg from "./images/homepage-splash.png";
import FeedbackKikoImg from "./images/feedback-kiko.png";
function HomePage() {
useSupportSetup();
const [previewState, setPreviewState] = React.useState(null);
return (
Here's a little update on the state of DTI !
}
/>
Dress to Impress
Design and share your Neopets outfits!
or
);
}
function StartOutfitForm({ onChange }) {
const { push: pushHistory } = useRouter();
const idealPose = React.useMemo(
() => (Math.random() > 0.5 ? "HAPPY_FEM" : "HAPPY_MASC"),
[]
);
const [speciesId, setSpeciesId] = React.useState("1");
const [colorId, setColorId] = React.useState("8");
const [isValid, setIsValid] = React.useState(true);
const [closestPose, setClosestPose] = React.useState(idealPose);
const onSubmit = (e) => {
e.preventDefault();
if (!isValid) {
return;
}
const params = new URLSearchParams({
species: speciesId,
color: colorId,
pose: closestPose,
});
pushHistory(`/outfits/new?${params}`);
};
const buttonBgColor = useColorModeValue("green.600", "green.300");
const buttonBgColorHover = useColorModeValue("green.700", "green.200");
return (
);
}
function SubmitPetForm() {
const { query, push: pushHistory } = useRouter();
const theme = useTheme();
const toast = useToast();
const [petName, setPetName] = React.useState("");
const [loadPetQuery, { loading }] = useLazyQuery(
gql`
query SubmitPetForm($petName: String!) {
petOnNeopetsDotCom(petName: $petName) {
petAppearance {
color {
id
}
species {
id
}
pose
}
wornItems {
id
}
}
}
`,
{
fetchPolicy: "network-only",
onCompleted: (data) => {
if (!data) return;
const { petAppearance, wornItems } = data.petOnNeopetsDotCom;
if (petAppearance == null) {
toast({
title: "This pet exists, but is in a glitchy state on Neopets.com.",
description:
"Hopefully it gets fixed soon! If this doesn't sound right to you, contact us and let us know!",
status: "error",
});
return;
}
const { species, color, pose } = petAppearance;
const params = new URLSearchParams({
name: petName,
species: species.id,
color: color.id,
pose,
});
for (const item of wornItems) {
params.append("objects[]", item.id);
}
pushHistory(`/outfits/new?${params}`);
},
onError: () => {
toast({
title: "We couldn't load that pet, sorry 😓",
description: "Is it spelled correctly?",
status: "error",
});
},
}
);
const loadPet = React.useCallback(
(petName) => {
loadPetQuery({ variables: { petName } });
// Start preloading the WardrobePage, too!
// eslint-disable-next-line no-unused-expressions
import("./WardrobePage").catch((e) => {
// Let's just let this slide, because it's a preload error. Critical
// failures will happen elsewhere, and trigger reloads!
console.error(e);
});
},
[loadPetQuery]
);
// If the ?loadPet= query param is provided, auto-load the pet immediately.
// This isn't used in-app, but is a helpful hook for things like link-ins and
// custom search engines. (I feel like a route or a different UX would be
// better, but I don't really know enough to commit to one… let's just keep
// this simple for now, I think. We might change this someday!)
const autoLoadPetName = query.loadPet;
React.useEffect(() => {
if (autoLoadPetName != null) {
setPetName(autoLoadPetName);
loadPet(autoLoadPetName);
}
}, [autoLoadPetName, loadPet]);
const { brightBackground } = useCommonStyles();
const inputBorderColor = useColorModeValue("green.600", "green.500");
const inputBorderColorHover = useColorModeValue("green.400", "green.300");
const buttonBgColor = useColorModeValue("green.600", "green.300");
const buttonBgColorHover = useColorModeValue("green.700", "green.200");
return (
{({ css }) => (
)}
);
}
function NewItemsSection() {
return (
Latest items
);
}
function ItemsSearchField() {
const [query, setQuery] = React.useState("");
const { brightBackground } = useCommonStyles();
const { push: pushHistory } = useRouter();
return (
);
}
function NewItemsSectionContent() {
const { loading, error, data } = useQuery(
gql`
query NewItemsSection {
newestItems {
id
name
thumbnailUrl
isNc
isPb
speciesThatNeedModels {
id
name
}
babySpeciesThatNeedModels: speciesThatNeedModels(colorId: "6") {
id
name
}
maraquanSpeciesThatNeedModels: speciesThatNeedModels(colorId: "44") {
id
name
}
mutantSpeciesThatNeedModels: speciesThatNeedModels(colorId: "46") {
id
name
}
compatibleBodiesAndTheirZones {
body {
id
representsAllBodies
species {
id
name
}
canonicalAppearance {
id
color {
id
name
isStandard
}
}
}
}
}
}
`
);
const { data: userData } = useQuery(
gql`
query NewItemsSection_UserData {
newestItems {
id
currentUserOwnsThis
currentUserWantsThis
}
}
`,
{
context: { sendAuth: true },
onError: (e) =>
console.error("Error loading NewItemsSection_UserData, skipping:", e),
}
);
if (loading) {
const footer = (
);
return (
);
}
if (error) {
return (
Couldn't load new items. Check your connection and try again!
);
}
// Merge in the results from the user data query, if available.
const newestItems = data.newestItems.map((item) => {
const itemUserData =
(userData?.newestItems || []).find((i) => i.id === item.id) || {};
return { ...item, ...itemUserData };
});
return (
{newestItems.map((item) => (
}
/>
))}
);
}
function ItemModelingSummary({ item }) {
// NOTE: To test this logic, I like to swap out `newestItems` in the query:
// `newestItems: items(ids: ["81546", "35082", "75149", "81797", "58741", "78953", "82427", "82727", "82726"])`
const numModelsNeeded =
item.speciesThatNeedModels.length +
item.babySpeciesThatNeedModels.length +
item.maraquanSpeciesThatNeedModels.length +
item.mutantSpeciesThatNeedModels.length;
if (numModelsNeeded > 0) {
return (
Need {numModelsNeeded} models
);
}
const bodies = item.compatibleBodiesAndTheirZones.map((bz) => bz.body);
const fitsAllPets = bodies.some((b) => b.representsAllBodies);
if (fitsAllPets) {
return (
Fits all pets
);
}
// HACK: The Maraquan Mynci and the Blue Mynci have the same body, so to test
// whether something is *meant* for standard colors, we check for more
// than
const standardBodies = bodies.filter(
(b) => b.canonicalAppearance.color.isStandard
);
const isMeantForStandardBodies = standardBodies.length >= 2;
const colors = bodies.map((b) => b.canonicalAppearance.color);
const specialColor = colors.find((c) => !c.isStandard);
const hasSpecialColorOnly = !isMeantForStandardBodies && specialColor != null;
if (hasSpecialColorOnly && bodies.length === 1) {
return (
{specialColor.name} {bodies[0].species.name} only
);
}
if (bodies.length === 1) {
return (
{bodies[0].species.name} only
);
}
if (hasSpecialColorOnly) {
return (
{specialColor.name} only
);
}
return (
Fits all{" "}
Not special colors like Baby, Maraquan, or Mutant.
}
>
basic
{" "}
pets
);
}
function ItemCardHStack({ children }) {
return (
// HACK: I wanted to just have an HStack with overflow:auto and internal
// paddingX, but the right-hand-side padding didn't seem to work
// during overflow. This was the best I could come up with...
{children}
);
}
function FeedbackFormSection() {
const { brightBackground } = useCommonStyles();
const pitchBorderColor = useColorModeValue("gray.300", "green.400");
const formBorderColor = useColorModeValue("gray.300", "blue.400");
return (
);
}
export function FeedbackFormContainer({ background, borderColor, children }) {
return (
{children}
);
}
function FeedbackFormPitch() {
const [authMode, setAuthMode] = useAuthModeFeatureFlag();
return (
Hi friends! Welcome to DTI 2020!
This is the newer Dress to Impress! It supports the new HTML5
animations, and it works great on mobile! Some features are still on
Classic DTI though.{" "}
Here's what's up.
New updates (Oct 14)
Paginated item search (bye infinite scroll!)Automatic modeling! :0
See more on Twitter!
Coming soon
Experimental login mode
Should be faster and easier—help us try it out!
After turning this on, try logging in.
setAuthMode(e.target.checked ? "db" : "auth0")
}
/>
Making sure we're ready for the long-term
…a lot of little things{" "}
😅
↓ Got ideas? Send them to us, please!{" "}
💖
);
}
export function FeedbackForm({ contentPlaceholder }) {
const [content, setContent] = React.useState("");
const [email, setEmail] = useLocalStorage("DTIFeedbackFormEmail", "");
const [isSending, setIsSending] = React.useState(false);
const toast = useToast();
const onSubmit = React.useCallback(
(e) => {
e.preventDefault();
fetch("/api/sendFeedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content, email }),
})
.then((res) => {
if (!res.ok) {
throw new Error(`/api/sendFeedback returned status ${res.status}`);
}
setIsSending(false);
setContent("");
toast({
status: "success",
title: "Got it! We'll take a look soon.",
description:
"Thanks for helping us get better! Best wishes to you and your " +
"pets!!",
});
})
.catch((e) => {
setIsSending(false);
console.error(e);
toast({
status: "warning",
title: "Oops, we had an error sending this, sorry!",
description:
"We'd still love to hear from you! Please reach out to " +
"matchu@openneo.net with whatever's on your mind. Thanks and " +
"enjoy the site!",
duration: null,
isClosable: true,
});
});
setIsSending(true);
},
[content, email, toast]
);
const { brightBackground } = useCommonStyles();
return (
setEmail(e.target.value)}
background={brightBackground}
/>
);
}
/**
* useSupportSetup helps our support staff get set up with special access.
* If you provide ?supportSecret=... in the URL, we'll save it in a cookie and
* pop up a toast!
*
* This doesn't guarantee the secret is correct, of course! We don't bother to
* check that here; the server will reject requests from bad support secrets.
* And there's nothing especially secret in the support UI, so it's okay if
* other people know about the tools and poke around a powerless interface!
*/
function useSupportSetup() {
const { query } = useRouter();
const toast = useToast();
const supportSecret = query.supportSecret;
React.useEffect(() => {
const existingSupportSecret = localStorage.getItem("supportSecret");
if (supportSecret && supportSecret !== existingSupportSecret) {
localStorage.setItem("supportSecret", supportSecret);
toast({
title: "Support secret saved!",
description:
`You should now see special Support UI across the site. ` +
`Thanks for your help! 💖`,
status: "success",
duration: 10000,
isClosable: true,
});
} else if (supportSecret === "") {
localStorage.removeItem("supportSecret");
toast({
title: "Support secret deleted.",
description: `The Support UI will now stop appearing on this device.`,
status: "success",
duration: 10000,
isClosable: true,
});
}
}, [supportSecret, toast]);
}
export default HomePage;