import React from "react";
import {
Badge,
Box,
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
Button,
Center,
Flex,
HStack,
Input,
Textarea,
useToast,
Wrap,
WrapItem,
} from "@chakra-ui/react";
import {
ArrowForwardIcon,
CheckIcon,
ChevronRightIcon,
EditIcon,
EmailIcon,
} from "@chakra-ui/icons";
import { gql, useMutation, useQuery } from "@apollo/client";
import { Link, useParams } from "react-router-dom";
import { HashLink } from "react-router-hash-link";
import { Heading1, Heading3, MajorErrorMessage, usePageTitle } from "./util";
import HangerSpinner from "./components/HangerSpinner";
import MarkdownAndSafeHTML from "./components/MarkdownAndSafeHTML";
import ItemCard from "./components/ItemCard";
import useCurrentUser from "./components/useCurrentUser";
import useSupport from "./WardrobePage/support/useSupport";
import WIPCallout from "./components/WIPCallout";
function UserItemListPage() {
const { listId } = useParams();
const currentUser = useCurrentUser();
const { loading, error, data } = useQuery(
gql`
query UserItemListPage($listId: ID!) {
closetList(id: $listId) {
id
name
description
ownsOrWantsItems
creator {
id
username
contactNeopetsUsername
}
items {
id
isNc
isPb
name
thumbnailUrl
currentUserOwnsThis
currentUserWantsThis
}
}
}
`,
{ variables: { listId }, context: { sendAuth: true } }
);
const closetList = data?.closetList;
usePageTitle(closetList?.name);
if (loading) {
return (
);
}
if (error) {
return ;
}
if (!closetList) {
return ;
}
const { creator, ownsOrWantsItems } = closetList;
// NOTE: `currentUser` should have resolved by now, because the GraphQL query
// sends authorization, which requires the current user to load first!
const isCurrentUser = currentUser.id === creator.id;
let linkBackText;
let linkBackPath;
if (ownsOrWantsItems === "OWNS") {
linkBackText = `Items ${creator.username} owns`;
linkBackPath = `/user/${creator.id}/lists#owned-items`;
} else if (ownsOrWantsItems === "WANTS") {
linkBackText = `Items ${creator.username} wants`;
linkBackPath = `/user/${creator.id}/lists#wanted-items`;
} else {
throw new Error(`unexpected ownsOrWantsItems value: ${ownsOrWantsItems}`);
}
return (
}
>
{creator.username}'s lists
{linkBackText}
);
}
export function ClosetList({
closetList,
isCurrentUser,
headingVariant = "list-item",
maxNumItemsToShow = null,
}) {
const { isSupportUser, supportSecret } = useSupport();
const toast = useToast();
// When this mounts, scroll it into view if it matches the location hash.
// This works around the fact that, while the browser tries to do this
// natively on page load, the list might not be mounted yet!
const anchorId = `list-${closetList.id}`;
React.useEffect(() => {
if (document.location.hash === "#" + anchorId) {
document.getElementById(anchorId).scrollIntoView();
}
}, [anchorId]);
const [
sendSaveChangesMutation,
{ loading: loadingSaveChanges },
] = useMutation(
gql`
mutation ClosetList_Edit(
$closetListId: ID!
$name: String!
$description: String!
# Support users can edit any list, if they provide the secret. If you're
# editing your own list, this will be empty, and that's okay.
$supportSecret: String
) {
editClosetList(
closetListId: $closetListId
name: $name
description: $description
supportSecret: $supportSecret
) {
id
name
description
}
}
`,
{ context: { sendAuth: true } }
);
const [isEditing, setIsEditing] = React.useState(false);
const [editableName, setEditableName] = React.useState(closetList.name);
const [editableDescription, setEditableDescription] = React.useState(
closetList.description
);
const hasChanges =
editableName !== closetList.name ||
editableDescription !== closetList.description;
const onSaveChanges = () => {
if (!hasChanges) {
setIsEditing(false);
return;
}
sendSaveChangesMutation({
variables: {
closetListId: closetList.id,
name: editableName,
description: editableDescription,
supportSecret,
},
})
.then(() => {
setIsEditing(false);
toast({
status: "success",
title: "Changes saved!",
});
})
.catch((err) => {
console.error(err);
toast({
status: "error",
title: "Sorry, we couldn't save this list 😖",
description: "Check your connection and try again.",
});
});
};
const Heading = headingVariant === "top-level" ? Heading1 : Heading3;
return (
{headingVariant !== "hidden" &&
(isEditing ? (
setEditableName(e.target.value)}
maxWidth="20ch"
// Shift left by our own padding/border, for alignment with the
// original title
paddingX="0.75rem"
marginLeft="calc(-0.75rem - 1px)"
boxShadow="sm"
lineHeight="1.2"
// HACK: Idk, the height stuff is really getting away from me,
// this is close enough :/
height="1.2em"
/>
) : (
{closetList.isDefaultList || headingVariant === "top-level" ? (
closetList.name
) : (
{closetList.name}
)}
))}
{(isCurrentUser || isSupportUser) &&
!closetList.isDefaultList &&
(isEditing ? (
<>
WIP: Can only edit text for now!
>
) : (
))}
{headingVariant === "top-level" && (
{closetList.creator?.contactNeopetsUsername && (
{closetList.creator.contactNeopetsUsername}
)}
{closetList.creator?.contactNeopetsUsername && (
Neomail
)}
)}
{(closetList.description || isEditing) && (
{isEditing ? (
)}
);
}
export function ClosetListContents({
closetList,
isCurrentUser,
maxNumItemsToShow = null,
}) {
const isTradeMatch = (item) =>
!isCurrentUser &&
((closetList.ownsOrWantsItems === "OWNS" && item.currentUserWantsThis) ||
(closetList.ownsOrWantsItems === "WANTS" && item.currentUserOwnsThis));
const sortedItems = [...closetList.items].sort((a, b) => {
// This is a cute sort hack. We sort first by, bringing trade matches to
// the top, and then sorting by name _within_ those two groups.
const aName = `${isTradeMatch(a) ? "000" : "999"} ${a.name}`;
const bName = `${isTradeMatch(b) ? "000" : "999"} ${b.name}`;
return aName.localeCompare(bName);
});
let itemsToShow = sortedItems;
if (maxNumItemsToShow != null) {
itemsToShow = itemsToShow.slice(0, maxNumItemsToShow);
}
const numMoreItems = Math.max(sortedItems.length - itemsToShow.length, 0);
let tradeMatchingMode;
if (isCurrentUser) {
// On your own item list, it's not helpful to show your own trade matches!
tradeMatchingMode = "hide-all";
} else if (closetList.ownsOrWantsItems === "OWNS") {
tradeMatchingMode = "offering";
} else if (closetList.ownsOrWantsItems === "WANTS") {
tradeMatchingMode = "seeking";
} else {
throw new Error(
`unexpected ownsOrWantsItems value: ${closetList.ownsOrWantsItems}`
);
}
return (
{itemsToShow.length > 0 ? (
{itemsToShow.map((item) => (
))}
) : (
This list is empty!
)}
{numMoreItems > 0 && (
Show {numMoreItems} more items
)}
);
}
export function buildClosetListPath(closetList) {
let ownsOrWants;
if (closetList.ownsOrWantsItems === "OWNS") {
ownsOrWants = "owns";
} else if (closetList.ownsOrWantsItems === "WANTS") {
ownsOrWants = "wants";
} else {
throw new Error(
`unexpected ownsOrWantsItems value: ${closetList.ownsOrWantsItems}`
);
}
return `/user/${closetList.creator.id}/lists/${ownsOrWants}/${closetList.id}`;
}
export function NeopetsStarIcon(props) {
// Converted from the Neopets favicon with https://www.vectorizer.io/.
return (
);
}
export default UserItemListPage;