Skip to content
Merged
11 changes: 9 additions & 2 deletions static/js/store/components/SearchInput/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button } from "@canonical/react-components";
import { useSearchParams } from "react-router-dom";
import { useSearchParams, useNavigate, useLocation } from "react-router-dom";

import type { RefObject } from "react";

Expand All @@ -13,9 +13,16 @@ export const SearchInput = ({
searchSummaryRef,
}: Props): React.JSX.Element => {
const [searchParams, setSearchParams] = useSearchParams();
const { pathname } = useLocation();
const navigate = useNavigate();

const onSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
event.preventDefault();

if (pathname !== "/store") {
navigate("/store");
}

if (searchRef?.current && searchRef.current.value) {
searchParams.delete("page");
searchParams.set("q", searchRef.current.value);
Expand Down Expand Up @@ -43,7 +50,7 @@ export const SearchInput = ({
id="search"
className="p-search-box__input"
name="q"
placeholder="Search Snapcraft"
placeholder="Search snap store"
defaultValue={searchParams.get("q") || ""}
ref={searchRef}
/>
Expand Down
3 changes: 2 additions & 1 deletion static/js/store/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import usePackages from "./usePackages";
import useCategories from "./useCategories";

export { usePackages };
export { usePackages, useCategories };
20 changes: 20 additions & 0 deletions static/js/store/hooks/useCategories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useQuery } from "react-query";

function useCategories() {
return useQuery({
queryKey: ["categories"],
queryFn: async () => {
const response = await fetch("/store.json");

if (!response.ok) {
throw new Error("There was a problem fetching categories");
}

const responseData = await response.json();

return responseData.categories;
},
});
}

export default useCategories;
5 changes: 5 additions & 0 deletions static/js/store/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from "react-query";

import Root from "./layouts/Root";
import Store from "./pages/Store";
import Explore from "./pages/Explore";

const router = createBrowserRouter([
{
Expand All @@ -14,6 +15,10 @@ const router = createBrowserRouter([
path: "/store",
element: <Store />,
},
{
path: "/explore",
element: <Explore />,
},
],
},
]);
Expand Down
28 changes: 28 additions & 0 deletions static/js/store/pages/Explore/CardsLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Row, Col } from "@canonical/react-components";
import { LoadingCard } from "@canonical/store-components";
import { v4 as uuidv4 } from "uuid";

const elementIds: string[] = Array.of(
uuidv4(),
uuidv4(),
uuidv4(),
uuidv4(),
uuidv4(),
uuidv4(),
uuidv4(),
uuidv4(),
);

function CardsLoader(): JSX.Element {
return (
<Row>
{elementIds.map((id) => (
<Col size={4} key={id}>
<LoadingCard height={157} />
</Col>
))}
</Row>
);
}

export default CardsLoader;
35 changes: 35 additions & 0 deletions static/js/store/pages/Explore/Categories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Row, Col, Strip } from "@canonical/react-components";

import { useCategories } from "../../hooks";

import type { Category } from "../../types";

function Categories(): JSX.Element {
const { data, isLoading } = useCategories();

return (
<>
<h2>Categories</h2>
{!isLoading && data && (
<Strip shallow className="u-no-padding--bottom">
<Row>
{data.map((category: Category) => (
<Col size={3} key={category.name}>
<p className="p-heading--4">
<a
className="p-link--soft"
href={`/store?categories=${category.name}&page=1`}
>
{category.display_name}
</a>
</p>
</Col>
))}
</Row>
</Strip>
)}
</>
);
}

export default Categories;
56 changes: 56 additions & 0 deletions static/js/store/pages/Explore/EditorialSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Row, Col } from "@canonical/react-components";
import { SlicesData } from "../../types";

type Props = {
isLoading: boolean;
slice: SlicesData;
gradient: "blueGreen" | "purplePink";
};

function EditorialSection({ isLoading, slice, gradient }: Props): JSX.Element {
const gradients: Record<string, string> = {
blueGreen: "linear-gradient(45deg, #251755 20%, #69e07c)",
purplePink: "linear-gradient(45deg, #19224d 20%, #c481d1)",
};

return (
<div className="u-fixed-width">
<div
style={{
background: gradient ? gradients[gradient] : gradients["purplePink"],
color: "#fff",
}}
>
{!isLoading && (
<div className="slice-banner">
<Row>
<Col size={6} className="u-vertically-center">
<div>
<h2 className="p-heading--3">{slice.slice.name}</h2>
<p className="p-heading--4">{slice.slice.description}</p>
</div>
</Col>
<Col size={6} className="slice-icons">
{slice.snaps.slice(0, 3).map((snap) => (
<div className="slice-icon" key={snap.name}>
<a href={`/${snap.name}`}>
<img
src={snap.icon}
width={70}
height={70}
alt={snap.title}
title={snap.title}
/>
</a>
</div>
))}
</Col>
</Row>
</div>
)}
</div>
</div>
);
}

export default EditorialSection;
191 changes: 191 additions & 0 deletions static/js/store/pages/Explore/Explore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { useRef } from "react";
import { useQueries } from "react-query";
import { Strip, Row, Col } from "@canonical/react-components";

import Banner from "../../components/Banner";
import RecommendationsSection from "./RecommendationsSection";
// import EditorialSection from "./EditorialSection";
import ListSection from "./ListSection";
import Categories from "./Categories";

import type { UseQueryResult } from "react-query";
import type { RecommendationData } from "../../types";

function Explore(): JSX.Element {
const searchRef = useRef<HTMLInputElement | null>(null);
const searchSummaryRef = useRef<HTMLDivElement | null>(null);
const categories: string[] = ["popular", "recent", "trending"];
// const sliceIds: string[] = ["our_picks", "must_have_snaps"];

// const slices = useQueries(
// sliceIds.map((sliceId) => ({
// queryKey: ["slices", sliceId],
// queryFn: async () => {
// const response = await fetch(
// `https://recommendations.snapcraft.io/api/slice/${sliceId}`,
// );
//
// if (!response.ok) {
// throw Error(`Unable to fetch ${sliceId} data`);
// }
//
// const responseData = await response.json();
//
// return responseData;
// },
// })),
// );

// const slicesLoading: boolean = slices.some((s) => s.isLoading);

// const slicesData: Record<string, SlicesData> = {};

// if (slices) {
// slices.forEach((slice) => {
// if (slice.data) {
// slicesData[slice.data.slice.id] = slice.data;
// }
// });
// }

const recommendations: UseQueryResult<{
name: string;
snaps: RecommendationData[];
}>[] = useQueries(
categories.map((category) => ({
queryKey: ["recommendations", category],
queryFn: async () => {
const response = await fetch(
`https://recommendations.snapcraft.io/api/category/${category}`,
);

if (!response.ok) {
throw Error(`Unable to fetch ${category} data`);
}

const responseData = await response.json();

return {
name: category,
snaps: responseData,
};
},
})),
);

const recommendationsLoading: boolean = recommendations.some(
(r) => r.isLoading,
);

const snaps: Record<string, RecommendationData[]> = {};

if (recommendations) {
recommendations.forEach((recommendation) => {
if (recommendation.data) {
snaps[recommendation.data.name] = recommendation.data.snaps.slice(0, 8);
}
});
}

return (
<>
<Banner searchRef={searchRef} searchSummaryRef={searchSummaryRef} />

{/* slices && (
<>
{slicesData.our_picks && (
<Strip className="u-no-padding--bottom">
<EditorialSection
isLoading={slicesLoading}
slice={slicesData.our_picks}
gradient="purplePink"
/>
</Strip>
)}
</>
) */}

{recommendations && (
<>
{snaps.recent && (
<Strip shallow className="u-no-padding--bottom">
<RecommendationsSection
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note, from my understanding, @0-luke wants this to be more dynamic (i.e, if I want to change the title, I don't need to open a PR). The service itself still isn't ready for this, but thought I'd mention it so that its in mind when the code is changed to be server side

snaps={snaps.recent}
title="Recently updated snaps"
isLoading={recommendationsLoading}
highlight={true}
/>
</Strip>
)}

{snaps.popular && (
<Strip shallow className="u-no-padding--bottom">
<ListSection
snaps={snaps.popular}
title="Most popular snaps"
isLoading={recommendationsLoading}
/>
</Strip>
)}
</>
)}

<Strip shallow className="u-no-padding--top">
<Categories />
</Strip>

{/* Placeholder until content is decided */}
{/* slices && (
<>
{slicesData.must_have_snaps && (
<Strip shallow className="u-no-padding--top u-no-padding--bottom">
<EditorialSection
isLoading={slicesLoading}
slice={slicesData.must_have_snaps}
gradient="blueGreen"
/>
</Strip>
)}
</>
) */}

{recommendations && snaps.trending && (
<Strip shallow>
<RecommendationsSection
snaps={snaps.trending}
title="Trending snaps"
isLoading={recommendationsLoading}
/>
</Strip>
)}

<Strip className="u-no-padding--top">
<div
style={{
backgroundImage:
"url('https://assets.ubuntu.com/v1/e888a79f-suru.png')",
backgroundPosition: "top right",
backgroundSize: "contain",
backgroundRepeat: "no-repeat",
backgroundColor: "#f3f3f3",
padding: "67px",
}}
>
<Row>
<Col size={6}>
<h2>Learn how to snap in 30 minutes</h2>
<p className="p-heading--4">
Find out how to build and publish snaps
</p>
<a className="p-button--positive" href="/docs/get-started">
Get started
</a>
</Col>
</Row>
</div>
</Strip>
</>
);
}

export default Explore;
Loading