mirror of
https://github.com/LukeHagar/unicorn-utterances.git
synced 2025-12-07 12:57:45 +00:00
add unicorn data to search API response, fix type errors
This commit is contained in:
@@ -23,7 +23,6 @@ export const MockCollection: ExtendedCollectionInfo = {
|
|||||||
},
|
},
|
||||||
locales: ["en"],
|
locales: ["en"],
|
||||||
locale: "en",
|
locale: "en",
|
||||||
posts: [MockPost],
|
|
||||||
slug: "this-collection-name-here",
|
slug: "this-collection-name-here",
|
||||||
title: "Collection title",
|
title: "Collection title",
|
||||||
description: "This is a short description dunno why this would be this short",
|
description: "This is a short description dunno why this would be this short",
|
||||||
|
|||||||
@@ -1,33 +1,25 @@
|
|||||||
import { MockUnicorn, MockUnicornTwo } from "./mock-unicorn";
|
import { MockUnicorn, MockUnicornTwo } from "./mock-unicorn";
|
||||||
import { MockLicense } from "./mock-license";
|
import { MockLicense } from "./mock-license";
|
||||||
import { ExtendedPostInfo } from "types/index";
|
import { PostInfo } from "types/index";
|
||||||
|
|
||||||
export const MockPost: ExtendedPostInfo = {
|
export const MockPost: PostInfo = {
|
||||||
excerpt: "This would be an auto generated excerpt of the post in particular",
|
|
||||||
title: "Post title",
|
title: "Post title",
|
||||||
published: "10-10-2010T00:00:00.000Z",
|
published: "10-10-2010T00:00:00.000Z",
|
||||||
publishedMeta: "October 10, 2010",
|
publishedMeta: "October 10, 2010",
|
||||||
tags: ["item1"],
|
tags: ["item1"],
|
||||||
description: "This is a short description dunno why this would be this short",
|
description: "This is a short description dunno why this would be this short",
|
||||||
authors: [MockUnicorn.id],
|
authors: [MockUnicorn.id],
|
||||||
authorsMeta: [MockUnicorn],
|
|
||||||
license: MockLicense.id,
|
license: MockLicense.id,
|
||||||
licenseMeta: MockLicense,
|
|
||||||
locale: "en",
|
locale: "en",
|
||||||
locales: ["en", "es"],
|
locales: ["en", "es"],
|
||||||
slug: "this-post-name-here",
|
slug: "this-post-name-here",
|
||||||
headingsWithId: [],
|
path: "path",
|
||||||
wordCount: 10000,
|
wordCount: 10000,
|
||||||
contentMeta: "",
|
|
||||||
Content: {} as never,
|
|
||||||
suggestedArticles: [] as never,
|
|
||||||
attached: [],
|
attached: [],
|
||||||
socialImg: "img.png",
|
socialImg: "img.png",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MockMultiAuthorPost: ExtendedPostInfo = {
|
export const MockMultiAuthorPost: PostInfo = {
|
||||||
excerpt:
|
|
||||||
"This would be a second auto generated excerpt of the post in particular",
|
|
||||||
title: "Another post title",
|
title: "Another post title",
|
||||||
published: "10-20-2010T00:00:00.000Z",
|
published: "10-20-2010T00:00:00.000Z",
|
||||||
publishedMeta: "October 20, 2010",
|
publishedMeta: "October 20, 2010",
|
||||||
@@ -35,24 +27,17 @@ export const MockMultiAuthorPost: ExtendedPostInfo = {
|
|||||||
description:
|
description:
|
||||||
"This is another short description dunno why this would be this short",
|
"This is another short description dunno why this would be this short",
|
||||||
authors: [MockUnicornTwo.id, MockUnicorn.id],
|
authors: [MockUnicornTwo.id, MockUnicorn.id],
|
||||||
authorsMeta: [MockUnicornTwo, MockUnicorn],
|
|
||||||
license: MockLicense.id,
|
license: MockLicense.id,
|
||||||
licenseMeta: MockLicense,
|
|
||||||
locale: "en",
|
locale: "en",
|
||||||
locales: ["en", "es"],
|
locales: ["en", "es"],
|
||||||
slug: "this-other-post-name-here",
|
slug: "this-other-post-name-here",
|
||||||
headingsWithId: [],
|
path: "path",
|
||||||
wordCount: 100000,
|
wordCount: 100000,
|
||||||
contentMeta: "",
|
|
||||||
Content: {} as never,
|
|
||||||
suggestedArticles: [] as never,
|
|
||||||
attached: [],
|
attached: [],
|
||||||
socialImg: "img.png",
|
socialImg: "img.png",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MockMuliLanguagePost: ExtendedPostInfo = {
|
export const MockMuliLanguagePost: PostInfo = {
|
||||||
excerpt:
|
|
||||||
"This would be a second auto generated excerpt of the post in particular",
|
|
||||||
title: "Another post title",
|
title: "Another post title",
|
||||||
published: "10-20-2010T00:00:00.000Z",
|
published: "10-20-2010T00:00:00.000Z",
|
||||||
publishedMeta: "October 20, 2010",
|
publishedMeta: "October 20, 2010",
|
||||||
@@ -60,24 +45,17 @@ export const MockMuliLanguagePost: ExtendedPostInfo = {
|
|||||||
description:
|
description:
|
||||||
"This is another short description dunno why this would be this short",
|
"This is another short description dunno why this would be this short",
|
||||||
authors: [MockUnicornTwo.id, MockUnicorn.id],
|
authors: [MockUnicornTwo.id, MockUnicorn.id],
|
||||||
authorsMeta: [MockUnicornTwo, MockUnicorn],
|
|
||||||
license: MockLicense.id,
|
license: MockLicense.id,
|
||||||
licenseMeta: MockLicense,
|
|
||||||
locale: "en",
|
locale: "en",
|
||||||
locales: ["en", "es"],
|
locales: ["en", "es"],
|
||||||
slug: "this-other-post-name-here",
|
slug: "this-other-post-name-here",
|
||||||
headingsWithId: [],
|
path: "path",
|
||||||
wordCount: 100000,
|
wordCount: 100000,
|
||||||
contentMeta: "",
|
|
||||||
Content: {} as never,
|
|
||||||
suggestedArticles: [] as never,
|
|
||||||
attached: [],
|
attached: [],
|
||||||
socialImg: "img.png",
|
socialImg: "img.png",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MockCanonicalPost: ExtendedPostInfo = {
|
export const MockCanonicalPost: PostInfo = {
|
||||||
excerpt:
|
|
||||||
"This would be a second auto generated excerpt of the post in particular",
|
|
||||||
title: "Another post title",
|
title: "Another post title",
|
||||||
published: "10-20-2010T00:00:00.000Z",
|
published: "10-20-2010T00:00:00.000Z",
|
||||||
publishedMeta: "October 20, 2010",
|
publishedMeta: "October 20, 2010",
|
||||||
@@ -86,17 +64,12 @@ export const MockCanonicalPost: ExtendedPostInfo = {
|
|||||||
description:
|
description:
|
||||||
"This is another short description dunno why this would be this short",
|
"This is another short description dunno why this would be this short",
|
||||||
authors: [MockUnicornTwo.id, MockUnicorn.id],
|
authors: [MockUnicornTwo.id, MockUnicorn.id],
|
||||||
authorsMeta: [MockUnicornTwo, MockUnicorn],
|
|
||||||
license: MockLicense.id,
|
license: MockLicense.id,
|
||||||
licenseMeta: MockLicense,
|
|
||||||
locale: "en",
|
locale: "en",
|
||||||
locales: ["en", "es"],
|
locales: ["en", "es"],
|
||||||
slug: "this-other-post-name-here",
|
slug: "this-other-post-name-here",
|
||||||
headingsWithId: [],
|
path: "path",
|
||||||
wordCount: 100000,
|
wordCount: 100000,
|
||||||
contentMeta: "",
|
|
||||||
Content: {} as never,
|
|
||||||
suggestedArticles: [] as never,
|
|
||||||
attached: [],
|
attached: [],
|
||||||
socialImg: "img.png",
|
socialImg: "img.png",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { RolesEnum } from "types/RolesInfo";
|
import { RolesInfo } from "types/RolesInfo";
|
||||||
|
|
||||||
export const MockRole: RolesEnum = {
|
export const MockRole: RolesInfo = {
|
||||||
id: "developer",
|
id: "developer",
|
||||||
prettyname: "Developer",
|
prettyname: "Developer",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ export const MockUnicorn: UnicornInfo = {
|
|||||||
firstName: "Joe",
|
firstName: "Joe",
|
||||||
lastName: "Other",
|
lastName: "Other",
|
||||||
id: "joe",
|
id: "joe",
|
||||||
|
locale: "en",
|
||||||
|
locales: ["en"],
|
||||||
description: "Exists",
|
description: "Exists",
|
||||||
color: "red",
|
color: "red",
|
||||||
roles: [MockRole.id],
|
roles: [MockRole.id],
|
||||||
rolesMeta: [MockRole],
|
|
||||||
socials: {
|
socials: {
|
||||||
twitter: "twtrusrname",
|
twitter: "twtrusrname",
|
||||||
github: "ghusrname",
|
github: "ghusrname",
|
||||||
@@ -36,10 +37,11 @@ export const MockUnicornTwo: UnicornInfo = {
|
|||||||
firstName: "Diane",
|
firstName: "Diane",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
id: "diane",
|
id: "diane",
|
||||||
|
locale: "en",
|
||||||
|
locales: ["en"],
|
||||||
description: "Is a human",
|
description: "Is a human",
|
||||||
color: "blue",
|
color: "blue",
|
||||||
roles: [MockRole.id],
|
roles: [MockRole.id],
|
||||||
rolesMeta: [MockRole],
|
|
||||||
socials: {
|
socials: {
|
||||||
twitter: "twtrusrname2",
|
twitter: "twtrusrname2",
|
||||||
github: "ghusrname2",
|
github: "ghusrname2",
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import type { VercelRequest, VercelResponse } from "@vercel/node";
|
|||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
import type { ExtendedPostInfo } from "types/index";
|
import { CollectionInfo, UnicornInfo, type PostInfo } from "types/index";
|
||||||
|
import type { ServerReturnType } from "../src/views/search/types";
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const searchIndex = require("./searchIndex.json");
|
const searchIndex = require("./searchIndex.json");
|
||||||
@@ -12,7 +13,7 @@ const collectionIndex = Fuse.parseIndex(searchIndex.collectionIndex);
|
|||||||
const posts = searchIndex.posts;
|
const posts = searchIndex.posts;
|
||||||
const collections = searchIndex.collections;
|
const collections = searchIndex.collections;
|
||||||
|
|
||||||
const postFuse = new Fuse<ExtendedPostInfo>(
|
const postFuse = new Fuse<PostInfo>(
|
||||||
posts,
|
posts,
|
||||||
{
|
{
|
||||||
threshold: 0.3,
|
threshold: 0.3,
|
||||||
@@ -23,7 +24,7 @@ const postFuse = new Fuse<ExtendedPostInfo>(
|
|||||||
postIndex,
|
postIndex,
|
||||||
);
|
);
|
||||||
|
|
||||||
const collectionFuse = new Fuse(
|
const collectionFuse = new Fuse<CollectionInfo>(
|
||||||
collections,
|
collections,
|
||||||
{
|
{
|
||||||
threshold: 0.3,
|
threshold: 0.3,
|
||||||
@@ -34,35 +35,53 @@ const collectionFuse = new Fuse(
|
|||||||
collectionIndex,
|
collectionIndex,
|
||||||
);
|
);
|
||||||
|
|
||||||
export default async (req: VercelRequest, res: VercelResponse) => {
|
const unicorns: Record<string, UnicornInfo> = searchIndex.unicorns;
|
||||||
|
|
||||||
|
function runQuery(req: VercelRequest): ServerReturnType {
|
||||||
// TODO: `pickdeep` only required fields
|
// TODO: `pickdeep` only required fields
|
||||||
const searchStr = req?.query?.query as string;
|
const searchStr = req?.query?.query as string;
|
||||||
if (!searchStr) {
|
if (!searchStr) {
|
||||||
res.send({
|
return {
|
||||||
|
unicorns: {},
|
||||||
posts: [],
|
posts: [],
|
||||||
totalPosts: 0,
|
totalPosts: 0,
|
||||||
collections: [],
|
collections: [],
|
||||||
totalCollections: 0,
|
totalCollections: 0,
|
||||||
});
|
};
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (searchStr === "*") {
|
if (searchStr === "*") {
|
||||||
res.send({
|
return {
|
||||||
|
unicorns,
|
||||||
posts,
|
posts,
|
||||||
totalPosts: posts.length,
|
totalPosts: posts.length,
|
||||||
collections,
|
collections,
|
||||||
totalCollections: collections.length,
|
totalCollections: collections.length,
|
||||||
});
|
};
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchedPosts = postFuse.search(searchStr).map((item) => item.item);
|
const searchedPosts = postFuse.search(searchStr).map((item) => item.item);
|
||||||
const searchedCollections = collectionFuse
|
const searchedCollections = collectionFuse
|
||||||
.search(searchStr)
|
.search(searchStr)
|
||||||
.map((item) => item.item);
|
.map((item) => item.item);
|
||||||
res.send({
|
|
||||||
|
const searchedUnicorns: Record<string, UnicornInfo> = {};
|
||||||
|
for (const post of searchedPosts) {
|
||||||
|
for (const id of post.authors) searchedUnicorns[id] = unicorns[id];
|
||||||
|
}
|
||||||
|
for (const collection of searchedCollections) {
|
||||||
|
for (const id of collection.authors) searchedUnicorns[id] = unicorns[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
unicorns: searchedUnicorns,
|
||||||
posts: searchedPosts,
|
posts: searchedPosts,
|
||||||
totalPosts: searchedPosts.length,
|
totalPosts: searchedPosts.length,
|
||||||
collections: searchedCollections,
|
collections: searchedCollections,
|
||||||
totalCollections: searchedCollections.length,
|
totalCollections: searchedCollections.length,
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (req: VercelRequest, res: VercelResponse) => {
|
||||||
|
const response = runQuery(req);
|
||||||
|
res.send(response);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Fuse from "fuse.js";
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as api from "utils/api";
|
import * as api from "utils/api";
|
||||||
import { PostInfo, CollectionInfo } from "types/index";
|
import { PostInfo, CollectionInfo, UnicornInfo } from "types/index";
|
||||||
|
|
||||||
const posts = api.getPostsByLang("en");
|
const posts = api.getPostsByLang("en");
|
||||||
const collections = api.getCollectionsByLang("en");
|
const collections = api.getCollectionsByLang("en");
|
||||||
@@ -87,5 +87,18 @@ const createCollectionIndex = () => {
|
|||||||
const postIndex = createPostIndex();
|
const postIndex = createPostIndex();
|
||||||
const collectionIndex = createCollectionIndex();
|
const collectionIndex = createCollectionIndex();
|
||||||
|
|
||||||
const json = JSON.stringify({ postIndex, posts, collectionIndex, collections });
|
const unicorns: Record<string, UnicornInfo> = api
|
||||||
|
.getUnicornsByLang("en")
|
||||||
|
.reduce((obj, unicorn) => {
|
||||||
|
obj[unicorn.id] = unicorn;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const json = JSON.stringify({
|
||||||
|
postIndex,
|
||||||
|
posts,
|
||||||
|
collectionIndex,
|
||||||
|
collections,
|
||||||
|
unicorns,
|
||||||
|
});
|
||||||
fs.writeFileSync(path.resolve(process.cwd(), "./api/searchIndex.json"), json);
|
fs.writeFileSync(path.resolve(process.cwd(), "./api/searchIndex.json"), json);
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ import YourGuide from "src/views/collections/framework-field-guide/segments/your
|
|||||||
import CodeBlock from "src/views/collections/framework-field-guide/segments/code-block.astro";
|
import CodeBlock from "src/views/collections/framework-field-guide/segments/code-block.astro";
|
||||||
import TheRestContainer from "src/views/collections/framework-field-guide/segments/the-rest-container.astro";
|
import TheRestContainer from "src/views/collections/framework-field-guide/segments/the-rest-container.astro";
|
||||||
import Pricing from "src/views/collections/framework-field-guide/segments/pricing.astro";
|
import Pricing from "src/views/collections/framework-field-guide/segments/pricing.astro";
|
||||||
import { unicorns } from "utils/data";
|
|
||||||
import SignUp from "src/views/collections/framework-field-guide/segments/sign-up.astro";
|
import SignUp from "src/views/collections/framework-field-guide/segments/sign-up.astro";
|
||||||
import WhyLearnAllThree from "src/views/collections/framework-field-guide/segments/why-learn-all-three.astro";
|
import WhyLearnAllThree from "src/views/collections/framework-field-guide/segments/why-learn-all-three.astro";
|
||||||
import WhatDoIGet from "src/views/collections/framework-field-guide/segments/what-do-i-get.astro";
|
import WhatDoIGet from "src/views/collections/framework-field-guide/segments/what-do-i-get.astro";
|
||||||
|
import { getUnicornById } from "utils/api";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Barebones>
|
<Barebones>
|
||||||
@@ -45,7 +45,7 @@ import WhatDoIGet from "src/views/collections/framework-field-guide/segments/wha
|
|||||||
pathName={`/collections/framework-field-guide`}
|
pathName={`/collections/framework-field-guide`}
|
||||||
title={"The Framework Field Guide"}
|
title={"The Framework Field Guide"}
|
||||||
description={"A practical and free way to teach Angular, React, and Vue all at once, so you can choose the right tool for the job and learn the underlying concepts in depth."}
|
description={"A practical and free way to teach Angular, React, and Vue all at once, so you can choose the right tool for the job and learn the underlying concepts in depth."}
|
||||||
unicornsData={[unicorns.find((uni) => uni.id === "crutchcorn")]}
|
unicornsData={[getUnicornById("crutchcorn", "en")]}
|
||||||
publishedTime={"2022-12-01T13:45:00.284Z"}
|
publishedTime={"2022-12-01T13:45:00.284Z"}
|
||||||
type={"book"}
|
type={"book"}
|
||||||
shareImage={"/custom-content/collections/framework-field-guide/framework_field_guide_social.png"}
|
shareImage={"/custom-content/collections/framework-field-guide/framework_field_guide_social.png"}
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
---
|
---
|
||||||
import { PostInfo } from "types/index";
|
import { Languages, PostInfo } from "types/index";
|
||||||
import { PostCardGrid } from "src/components/post-card/post-card-grid";
|
import { PostCardGrid } from "src/components/post-card/post-card-grid";
|
||||||
import { getUnicornProfilePicMap } from "utils/get-unicorn-profile-pic-map";
|
import { getUnicornProfilePicMap } from "utils/get-unicorn-profile-pic-map";
|
||||||
import { Pagination } from "components/pagination/pagination";
|
import { Pagination } from "components/pagination/pagination";
|
||||||
import { Page } from "astro";
|
import { Page } from "astro";
|
||||||
|
import * as api from "utils/api";
|
||||||
|
|
||||||
export interface PageProps {
|
export interface PageProps {
|
||||||
posts: PostInfo[];
|
posts: PostInfo[];
|
||||||
page: Pick<Page<any>, "currentPage" | "lastPage">;
|
page: Pick<Page<any>, "currentPage" | "lastPage">;
|
||||||
|
locale: Languages;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { posts, page } = Astro.props as PageProps;
|
const { posts, page, locale } = Astro.props as PageProps;
|
||||||
const unicornProfilePicMap = await getUnicornProfilePicMap();
|
const unicornProfilePicMap = await getUnicornProfilePicMap();
|
||||||
|
|
||||||
|
const postAuthors = new Map(
|
||||||
|
[...new Set(posts.flatMap((p) => p.authors))].map((id) => [
|
||||||
|
id,
|
||||||
|
api.getUnicornById(id, locale),
|
||||||
|
]),
|
||||||
|
);
|
||||||
---
|
---
|
||||||
|
|
||||||
<h1 class="visually-hidden">Posts</h1>
|
<h1 class="visually-hidden">Posts</h1>
|
||||||
@@ -20,6 +29,7 @@ const unicornProfilePicMap = await getUnicornProfilePicMap();
|
|||||||
expanded={true}
|
expanded={true}
|
||||||
aria-label="List of posts"
|
aria-label="List of posts"
|
||||||
postsToDisplay={posts}
|
postsToDisplay={posts}
|
||||||
|
postAuthors={postAuthors}
|
||||||
unicornProfilePicMap={unicornProfilePicMap}
|
unicornProfilePicMap={unicornProfilePicMap}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ProfilePictureMap } from "utils/get-unicorn-profile-pic-map";
|
import { ProfilePictureMap } from "utils/get-unicorn-profile-pic-map";
|
||||||
import { PostInfo } from "types/PostInfo";
|
import { PostInfo } from "types/PostInfo";
|
||||||
import { ExtendedCollectionInfo } from "types/CollectionInfo";
|
import { CollectionInfo } from "types/CollectionInfo";
|
||||||
import { useMemo } from "preact/hooks";
|
import { useMemo } from "preact/hooks";
|
||||||
import { UnicornInfo } from "types/UnicornInfo";
|
import { UnicornInfo } from "types/UnicornInfo";
|
||||||
import { CSSProperties } from "preact/compat";
|
import { CSSProperties } from "preact/compat";
|
||||||
@@ -15,7 +15,8 @@ interface FilterDisplayProps {
|
|||||||
unicornProfilePicMap: ProfilePictureMap;
|
unicornProfilePicMap: ProfilePictureMap;
|
||||||
posts: PostInfo[];
|
posts: PostInfo[];
|
||||||
|
|
||||||
collections: ExtendedCollectionInfo[];
|
collections: CollectionInfo[];
|
||||||
|
unicornsMap: Map<string, UnicornInfo>,
|
||||||
selectedTags: string[];
|
selectedTags: string[];
|
||||||
setSelectedTags: (tags: string[]) => void;
|
setSelectedTags: (tags: string[]) => void;
|
||||||
selectedAuthorIds: string[];
|
selectedAuthorIds: string[];
|
||||||
@@ -33,6 +34,7 @@ interface FilterDisplayProps {
|
|||||||
export const FilterDisplay = ({
|
export const FilterDisplay = ({
|
||||||
unicornProfilePicMap,
|
unicornProfilePicMap,
|
||||||
collections,
|
collections,
|
||||||
|
unicornsMap,
|
||||||
posts,
|
posts,
|
||||||
sort,
|
sort,
|
||||||
setSort,
|
setSort,
|
||||||
@@ -80,27 +82,14 @@ export const FilterDisplay = ({
|
|||||||
const authors = useMemo(() => {
|
const authors = useMemo(() => {
|
||||||
const postAuthorIdToPostNumMap = new Map<string, number>();
|
const postAuthorIdToPostNumMap = new Map<string, number>();
|
||||||
|
|
||||||
const authors: UnicornInfo[] = [];
|
|
||||||
posts.forEach((post) => {
|
posts.forEach((post) => {
|
||||||
post.authorsMeta.forEach((author) => {
|
post.authors.forEach((author) => {
|
||||||
authors.push(author);
|
const numPosts = postAuthorIdToPostNumMap.get(author) || 0;
|
||||||
|
postAuthorIdToPostNumMap.set(author, numPosts + 1);
|
||||||
const numPosts = postAuthorIdToPostNumMap.get(author.id) || 0;
|
|
||||||
postAuthorIdToPostNumMap.set(author.id, numPosts + 1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
collections.forEach((collection) => {
|
return Array.from(unicornsMap.values())
|
||||||
collection.authorsMeta.forEach((author) => {
|
|
||||||
authors.push(author);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const uniqueAuthors = new Map<string, UnicornInfo>();
|
|
||||||
authors.forEach((author) => {
|
|
||||||
uniqueAuthors.set(author.id, author);
|
|
||||||
});
|
|
||||||
return Array.from(uniqueAuthors.values())
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map((author) => ({
|
.map((author) => ({
|
||||||
...author,
|
...author,
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
queryByText,
|
queryByText,
|
||||||
getByTestId,
|
getByTestId,
|
||||||
} from "@testing-library/preact";
|
} from "@testing-library/preact";
|
||||||
import SearchPage, { ServerReturnType } from "./search-page";
|
import SearchPage from "./search-page";
|
||||||
|
import type { ServerReturnType } from "./types";
|
||||||
import { rest } from "msw";
|
import { rest } from "msw";
|
||||||
import { setupServer } from "msw/node";
|
import { setupServer } from "msw/node";
|
||||||
import { MockCanonicalPost, MockPost } from "../../../__mocks__/data/mock-post";
|
import { MockCanonicalPost, MockPost } from "../../../__mocks__/data/mock-post";
|
||||||
@@ -67,6 +68,7 @@ describe("Search page", () => {
|
|||||||
|
|
||||||
test("Should show search results for posts", async () => {
|
test("Should show search results for posts", async () => {
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [MockPost],
|
posts: [MockPost],
|
||||||
totalPosts: 1,
|
totalPosts: 1,
|
||||||
totalCollections: 0,
|
totalCollections: 0,
|
||||||
@@ -84,6 +86,7 @@ describe("Search page", () => {
|
|||||||
|
|
||||||
test("Should show search results for collections", async () => {
|
test("Should show search results for collections", async () => {
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [],
|
posts: [],
|
||||||
totalPosts: 0,
|
totalPosts: 0,
|
||||||
totalCollections: 1,
|
totalCollections: 1,
|
||||||
@@ -120,6 +123,7 @@ describe("Search page", () => {
|
|||||||
|
|
||||||
test("Should show 'nothing found'", async () => {
|
test("Should show 'nothing found'", async () => {
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [],
|
posts: [],
|
||||||
totalPosts: 0,
|
totalPosts: 0,
|
||||||
totalCollections: 0,
|
totalCollections: 0,
|
||||||
@@ -139,6 +143,7 @@ describe("Search page", () => {
|
|||||||
|
|
||||||
test("Remove collections header when none found", async () => {
|
test("Remove collections header when none found", async () => {
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [MockPost],
|
posts: [MockPost],
|
||||||
totalPosts: 1,
|
totalPosts: 1,
|
||||||
totalCollections: 0,
|
totalCollections: 0,
|
||||||
@@ -159,6 +164,7 @@ describe("Search page", () => {
|
|||||||
|
|
||||||
test("Remove posts header when none found", async () => {
|
test("Remove posts header when none found", async () => {
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [],
|
posts: [],
|
||||||
totalPosts: 0,
|
totalPosts: 0,
|
||||||
totalCollections: 1,
|
totalCollections: 1,
|
||||||
@@ -179,6 +185,7 @@ describe("Search page", () => {
|
|||||||
|
|
||||||
test("Filter by tag works on desktop sidebar", async () => {
|
test("Filter by tag works on desktop sidebar", async () => {
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [
|
posts: [
|
||||||
{ ...MockPost, tags: ["Angular"], title: "One blog post" },
|
{ ...MockPost, tags: ["Angular"], title: "One blog post" },
|
||||||
{ ...MockCanonicalPost, tags: [], title: "Two blog post" },
|
{ ...MockCanonicalPost, tags: [], title: "Two blog post" },
|
||||||
@@ -210,6 +217,7 @@ describe("Search page", () => {
|
|||||||
|
|
||||||
test("Filter by author works on desktop sidebar", async () => {
|
test("Filter by author works on desktop sidebar", async () => {
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [
|
posts: [
|
||||||
{
|
{
|
||||||
...MockPost,
|
...MockPost,
|
||||||
@@ -251,6 +259,7 @@ describe("Search page", () => {
|
|||||||
|
|
||||||
test("Filter by content type work on radio group buttons", async () => {
|
test("Filter by content type work on radio group buttons", async () => {
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [{ ...MockPost, title: "One blog post" }],
|
posts: [{ ...MockPost, title: "One blog post" }],
|
||||||
totalPosts: 1,
|
totalPosts: 1,
|
||||||
totalCollections: 1,
|
totalCollections: 1,
|
||||||
@@ -293,6 +302,7 @@ describe("Search page", () => {
|
|||||||
global.innerWidth = 2000;
|
global.innerWidth = 2000;
|
||||||
|
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [
|
posts: [
|
||||||
{
|
{
|
||||||
...MockPost,
|
...MockPost,
|
||||||
@@ -352,6 +362,7 @@ describe("Search page", () => {
|
|||||||
test("Sort by date works on mobile radio group buttons", async () => {
|
test("Sort by date works on mobile radio group buttons", async () => {
|
||||||
global.innerWidth = 500;
|
global.innerWidth = 500;
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [
|
posts: [
|
||||||
{
|
{
|
||||||
...MockPost,
|
...MockPost,
|
||||||
@@ -411,6 +422,7 @@ describe("Search page", () => {
|
|||||||
test("Pagination - Changing pages to page 2 shows second page of results", async () => {
|
test("Pagination - Changing pages to page 2 shows second page of results", async () => {
|
||||||
// 6 posts per page
|
// 6 posts per page
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [
|
posts: [
|
||||||
{ ...MockPost, slug: `blog-post-1`, title: "One blog post" },
|
{ ...MockPost, slug: `blog-post-1`, title: "One blog post" },
|
||||||
{ ...MockPost, slug: `blog-post-2`, title: "Two blog post" },
|
{ ...MockPost, slug: `blog-post-2`, title: "Two blog post" },
|
||||||
@@ -473,6 +485,7 @@ describe("Search page", () => {
|
|||||||
global.innerWidth = 2000;
|
global.innerWidth = 2000;
|
||||||
// 6 posts per page
|
// 6 posts per page
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [
|
posts: [
|
||||||
{
|
{
|
||||||
...MockPost,
|
...MockPost,
|
||||||
@@ -603,6 +616,7 @@ describe("Search page", () => {
|
|||||||
// Search page, sort order, etc
|
// Search page, sort order, etc
|
||||||
test("Make sure that initial search props are not thrown away", async () => {
|
test("Make sure that initial search props are not thrown away", async () => {
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [
|
posts: [
|
||||||
{
|
{
|
||||||
...MockPost,
|
...MockPost,
|
||||||
@@ -762,6 +776,7 @@ describe("Search page", () => {
|
|||||||
global.innerWidth = 2000;
|
global.innerWidth = 2000;
|
||||||
|
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [
|
posts: [
|
||||||
{
|
{
|
||||||
...MockPost,
|
...MockPost,
|
||||||
@@ -830,6 +845,7 @@ describe("Search page", () => {
|
|||||||
|
|
||||||
test("Make sure that re-searches reset page to 1 and preserve tags, authors, etc", async () => {
|
test("Make sure that re-searches reset page to 1 and preserve tags, authors, etc", async () => {
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [
|
posts: [
|
||||||
{
|
{
|
||||||
...MockPost,
|
...MockPost,
|
||||||
@@ -981,6 +997,7 @@ describe("Search page", () => {
|
|||||||
|
|
||||||
test("Make sure that re-searches to empty string reset page, tags, authors, etc", async () => {
|
test("Make sure that re-searches to empty string reset page, tags, authors, etc", async () => {
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [
|
posts: [
|
||||||
{
|
{
|
||||||
...MockPost,
|
...MockPost,
|
||||||
@@ -1132,6 +1149,7 @@ describe("Search page", () => {
|
|||||||
|
|
||||||
test("Back button should show last query", async () => {
|
test("Back button should show last query", async () => {
|
||||||
mockFetch(() => ({
|
mockFetch(() => ({
|
||||||
|
unicorns: {},
|
||||||
posts: [],
|
posts: [],
|
||||||
totalPosts: 0,
|
totalPosts: 0,
|
||||||
totalCollections: 0,
|
totalCollections: 0,
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import style from "./search-page.module.scss";
|
|||||||
import { PostCardGrid } from "components/post-card/post-card-grid";
|
import { PostCardGrid } from "components/post-card/post-card-grid";
|
||||||
import { SubHeader } from "components/subheader/subheader";
|
import { SubHeader } from "components/subheader/subheader";
|
||||||
import { Fragment } from "preact";
|
import { Fragment } from "preact";
|
||||||
import { ExtendedCollectionInfo } from "types/CollectionInfo";
|
|
||||||
import { CollectionCard } from "components/collection-card/collection-card";
|
import { CollectionCard } from "components/collection-card/collection-card";
|
||||||
import { FilterDisplay } from "./components/filter-display";
|
import { FilterDisplay } from "./components/filter-display";
|
||||||
import { useElementSize } from "../../hooks/use-element-size";
|
import { useElementSize } from "../../hooks/use-element-size";
|
||||||
@@ -43,6 +42,8 @@ import {
|
|||||||
import { debounce } from "utils/debounce";
|
import { debounce } from "utils/debounce";
|
||||||
import { SortType } from "./components/types";
|
import { SortType } from "./components/types";
|
||||||
import { SearchResultCount } from "./components/search-result-count";
|
import { SearchResultCount } from "./components/search-result-count";
|
||||||
|
import { ServerReturnType } from "./types";
|
||||||
|
import { CollectionInfo } from "types/CollectionInfo";
|
||||||
|
|
||||||
const DEFAULT_SORT = "relevance";
|
const DEFAULT_SORT = "relevance";
|
||||||
const DEFAULT_CONTENT_TO_DISPLAY = "all";
|
const DEFAULT_CONTENT_TO_DISPLAY = "all";
|
||||||
@@ -53,13 +54,6 @@ interface SearchPageProps {
|
|||||||
|
|
||||||
const MAX_POSTS_PER_PAGE = 6;
|
const MAX_POSTS_PER_PAGE = 6;
|
||||||
|
|
||||||
export interface ServerReturnType {
|
|
||||||
posts: PostInfo[];
|
|
||||||
totalPosts: number;
|
|
||||||
collections: ExtendedCollectionInfo[];
|
|
||||||
totalCollections: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
|
function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
|
||||||
const { urlParams, pushState } = useSearchParams();
|
const { urlParams, pushState } = useSearchParams();
|
||||||
|
|
||||||
@@ -141,11 +135,12 @@ function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
|
|||||||
},
|
},
|
||||||
queryKey: ["search", debouncedSearch],
|
queryKey: ["search", debouncedSearch],
|
||||||
initialData: {
|
initialData: {
|
||||||
|
unicorns: {},
|
||||||
posts: [],
|
posts: [],
|
||||||
totalPosts: 0,
|
totalPosts: 0,
|
||||||
collections: [],
|
collections: [],
|
||||||
totalCollections: 0,
|
totalCollections: 0,
|
||||||
} as ServerReturnType,
|
},
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
retry: false,
|
retry: false,
|
||||||
enabled,
|
enabled,
|
||||||
@@ -271,7 +266,7 @@ function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
|
|||||||
});
|
});
|
||||||
}, [data, page, sort, selectedUnicorns, selectedTags]);
|
}, [data, page, sort, selectedUnicorns, selectedTags]);
|
||||||
|
|
||||||
const filteredAndSortedCollections = useMemo(() => {
|
const filteredAndSortedCollections: CollectionInfo[] = useMemo(() => {
|
||||||
const collections = [...data.collections];
|
const collections = [...data.collections];
|
||||||
|
|
||||||
if (sort && sort !== "relevance") {
|
if (sort && sort !== "relevance") {
|
||||||
@@ -365,6 +360,7 @@ function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
|
|||||||
unicornProfilePicMap={unicornProfilePicMap}
|
unicornProfilePicMap={unicornProfilePicMap}
|
||||||
collections={data.collections}
|
collections={data.collections}
|
||||||
posts={data.posts}
|
posts={data.posts}
|
||||||
|
unicornsMap={new Map(Object.entries(data.unicorns))}
|
||||||
selectedTags={selectedTags}
|
selectedTags={selectedTags}
|
||||||
setSelectedTags={setSelectedTags}
|
setSelectedTags={setSelectedTags}
|
||||||
selectedAuthorIds={selectedUnicorns}
|
selectedAuthorIds={selectedUnicorns}
|
||||||
@@ -488,6 +484,9 @@ function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
|
|||||||
<CollectionCard
|
<CollectionCard
|
||||||
unicornProfilePicMap={unicornProfilePicMap}
|
unicornProfilePicMap={unicornProfilePicMap}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
|
authors={collection.authors.map(id => data.unicorns[id])}
|
||||||
|
// TODO: post count should be sourced from the collection info
|
||||||
|
posts={[]}
|
||||||
headingTag="h3"
|
headingTag="h3"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
@@ -509,6 +508,7 @@ function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
|
|||||||
<PostCardGrid
|
<PostCardGrid
|
||||||
aria-labelledby={"articles-header"}
|
aria-labelledby={"articles-header"}
|
||||||
postsToDisplay={posts}
|
postsToDisplay={posts}
|
||||||
|
postAuthors={new Map(Object.entries(data.unicorns))}
|
||||||
postHeadingTag="h3"
|
postHeadingTag="h3"
|
||||||
unicornProfilePicMap={unicornProfilePicMap}
|
unicornProfilePicMap={unicornProfilePicMap}
|
||||||
/>
|
/>
|
||||||
|
|||||||
11
src/views/search/types.ts
Normal file
11
src/views/search/types.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { CollectionInfo } from "types/CollectionInfo";
|
||||||
|
import { PostInfo } from "types/PostInfo";
|
||||||
|
import { UnicornInfo } from "types/UnicornInfo";
|
||||||
|
|
||||||
|
export interface ServerReturnType {
|
||||||
|
unicorns: Record<string, UnicornInfo>;
|
||||||
|
posts: PostInfo[];
|
||||||
|
totalPosts: number;
|
||||||
|
collections: CollectionInfo[];
|
||||||
|
totalCollections: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user