diff --git a/src/app/HomePage.js b/src/app/HomePage.js
index 8714f9a..685495e 100644
--- a/src/app/HomePage.js
+++ b/src/app/HomePage.js
@@ -5,6 +5,7 @@ import {
Box,
Button,
Flex,
+ HStack,
Input,
Textarea,
useColorModeValue,
@@ -13,16 +14,21 @@ import {
VStack,
} from "@chakra-ui/react";
import { useHistory, useLocation } from "react-router-dom";
-import { useLazyQuery } from "@apollo/client";
+import { useLazyQuery, useQuery } from "@apollo/client";
import {
+ ErrorMessage,
Heading1,
+ Heading2,
useCommonStyles,
useLocalStorage,
usePageTitle,
} from "./util";
import OutfitPreview from "./components/OutfitPreview";
import SpeciesColorPicker from "./components/SpeciesColorPicker";
+import SquareItemCard, {
+ SquareItemCardSkeleton,
+} from "./components/SquareItemCard";
import WIPCallout from "./components/WIPCallout";
import HomepageSplashImg from "./images/homepage-splash.png";
@@ -81,6 +87,8 @@ function HomePage() {
+
+
@@ -269,6 +277,85 @@ function SubmitPetForm() {
);
}
+function NewItemsSection() {
+ return (
+
+ Latest items
+
+
+ );
+}
+
+function NewItemsSectionContent() {
+ const { loading, error, data } = useQuery(gql`
+ query NewItemsSection {
+ newestItems {
+ id
+ name
+ thumbnailUrl
+ }
+ }
+ `);
+
+ if (loading) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ Couldn't load new items. Check your connection and try again!
+
+ );
+ }
+
+ return (
+
+ {data.newestItems.map((item) => (
+
+ ))}
+
+ );
+}
+
+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");
diff --git a/src/app/components/ItemCard.js b/src/app/components/ItemCard.js
index b167a9e..134aff4 100644
--- a/src/app/components/ItemCard.js
+++ b/src/app/components/ItemCard.js
@@ -9,81 +9,20 @@ import {
WrapItem,
useColorModeValue,
useTheme,
- useToken,
} from "@chakra-ui/react";
import { CheckIcon, NotAllowedIcon, StarIcon } from "@chakra-ui/icons";
import { HiSparkles } from "react-icons/hi";
import { Link } from "react-router-dom";
+import SquareItemCard from "./SquareItemCard";
import { safeImageUrl, useCommonStyles } from "../util";
function ItemCard({ item, badges, variant = "list", ...props }) {
const { brightBackground } = useCommonStyles();
- const brightBackgroundValue = useToken("colors", brightBackground);
- const theme = useTheme();
switch (variant) {
case "grid":
- return (
- // ItemCard renders in large lists of 1k+ items, so we get a big perf
- // win by using Emotion directly instead of Chakra's styled-system Box.
-
- {({ css }) => (
-
-
-
-
- {item.name}
-
-
-
- )}
-
- );
+ return ;
case "list":
return (
+ {({ css }) => (
+ // SquareItemCard renders in large lists of 1k+ items, so we get a big
+ // perf win by using Emotion directly instead of Chakra's styled-system
+ // Box.
+
+
+ }
+ />
+
+ )}
+
+ );
+}
+
+function SquareItemCardLayout({ name, thumbnailImage, minHeightNumLines = 2 }) {
+ const { brightBackground } = useCommonStyles();
+ const brightBackgroundValue = useToken("colors", brightBackground);
+ const theme = useTheme();
+
+ return (
+ // SquareItemCard renders in large lists of 1k+ items, so we get a big perf
+ // win by using Emotion directly instead of Chakra's styled-system Box.
+
+ {({ css }) => (
+
+ {thumbnailImage}
+
+ {name}
+
+
+ )}
+
+ );
+}
+
+export function SquareItemCardSkeleton({ minHeightNumLines }) {
+ return (
+
+
+ {minHeightNumLines >= 3 && (
+
+ )}
+ >
+ }
+ thumbnailImage={}
+ minHeightNumLines={minHeightNumLines}
+ />
+ );
+}
+
+export default SquareItemCard;
diff --git a/src/server/loaders.js b/src/server/loaders.js
index 1257888..ea1670a 100644
--- a/src/server/loaders.js
+++ b/src/server/loaders.js
@@ -376,6 +376,29 @@ const buildItemSearchToFitLoader = (db, loaders) =>
return responses;
});
+const buildNewestItemsLoader = (db, loaders) =>
+ new DataLoader(async (keys) => {
+ // Essentially, I want to provide the loader-like API, and populate other
+ // loaders, even though there's only one query to run.
+ if (keys.length !== 1 && keys[0] !== "all-newest") {
+ throw new Error(
+ `this loader can only be loaded with the key "all-newest"`
+ );
+ }
+
+ const [rows, _] = await db.execute(
+ `SELECT * FROM items ORDER BY created_at DESC LIMIT 20;`
+ );
+
+ const entities = rows.map(normalizeRow);
+
+ for (const entity of entities) {
+ loaders.itemLoader.prime(entity.id, entity);
+ }
+
+ return [entities];
+ });
+
let lastKnownUpdate = "1970-01-01"; // start it out very old!
let lastResult = new Map();
const buildItemsThatNeedModelsLoader = (db) =>
@@ -1135,6 +1158,7 @@ function buildLoaders(db) {
loaders.itemByNameLoader = buildItemByNameLoader(db, loaders);
loaders.itemSearchLoader = buildItemSearchLoader(db, loaders);
loaders.itemSearchToFitLoader = buildItemSearchToFitLoader(db, loaders);
+ loaders.newestItemsLoader = buildNewestItemsLoader(db, loaders);
loaders.itemsThatNeedModelsLoader = buildItemsThatNeedModelsLoader(db);
loaders.itemBodiesWithAppearanceDataLoader = buildItemBodiesWithAppearanceDataLoader(
db
diff --git a/src/server/types/Item.js b/src/server/types/Item.js
index f049676..8965800 100644
--- a/src/server/types/Item.js
+++ b/src/server/types/Item.js
@@ -122,6 +122,9 @@ const typeDefs = gql`
limit: Int
): ItemSearchResult!
+ # Get the 20 items most recently added to our database. Cache for 1 hour.
+ newestItems: [Item!]! @cacheControl(maxAge: 3600)
+
# Get items that need models for the given color.
#
# NOTE: Most color IDs won't be accepted here. Either pass the ID of a
@@ -386,6 +389,10 @@ const resolvers = {
const zones = zoneIds.map((id) => ({ id }));
return { query, zones, items };
},
+ newestItems: async (_, __, { newestItemsLoader }) => {
+ const items = await newestItemsLoader.load("all-newest");
+ return items.map((item) => ({ id: item.id }));
+ },
itemsThatNeedModels: async (
_,
{ colorId = "8" }, // Defaults to Blue