From 5da3079efb4609de50c3375738ef32b56cc4fc8d Mon Sep 17 00:00:00 2001 From: somrat sorkar Date: Mon, 11 Dec 2023 15:00:43 +0600 Subject: [PATCH] remove search page and added search modal --- package.json | 7 +- public/images/no-search-found.png | Bin 8267 -> 0 bytes scripts/jsonGenerator.js | 86 ++++++--- src/layouts/Base.astro | 17 +- src/layouts/Search.tsx | 181 ------------------- src/layouts/helpers/SearchModal.tsx | 257 ++++++++++++++++++++++++++ src/layouts/helpers/SearchResult.tsx | 260 +++++++++++++++++++++++++++ src/layouts/partials/Header.astro | 8 +- src/lib/utils/textConverter.ts | 12 +- src/pages/search.astro | 19 -- src/styles/main.scss | 1 + src/styles/search.scss | 96 ++++++++++ 12 files changed, 702 insertions(+), 242 deletions(-) delete mode 100755 public/images/no-search-found.png delete mode 100755 src/layouts/Search.tsx create mode 100644 src/layouts/helpers/SearchModal.tsx create mode 100755 src/layouts/helpers/SearchResult.tsx delete mode 100755 src/pages/search.astro create mode 100644 src/styles/search.scss diff --git a/package.json b/package.json index 310f9bd..cc37fff 100755 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "astroplate", - "version": "2.4.0", + "version": "2.5.0", "description": "Astro and Tailwindcss boilerplate", "author": "zeon.studio", "license": "MIT", "packageManager": "yarn@1.22.19", "scripts": { - "dev": "astro dev", - "build": "astro build", + "dev": "yarn generate-json && astro dev", + "build": "yarn generate-json && astro build", "preview": "astro preview", "format": "prettier -w ./src", "generate-json": "node scripts/jsonGenerator.js", @@ -24,7 +24,6 @@ "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", "disqus-react": "^1.1.5", - "fuse.js": "^7.0.0", "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "marked": "^11.0.0", diff --git a/public/images/no-search-found.png b/public/images/no-search-found.png deleted file mode 100755 index 1e1e6e16149baa3d78d891a3b10fc7d87b942abf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8267 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 diff --git a/scripts/jsonGenerator.js b/scripts/jsonGenerator.js index 6c2d20b..5a9f995 100644 --- a/scripts/jsonGenerator.js +++ b/scripts/jsonGenerator.js @@ -1,44 +1,80 @@ const fs = require("fs"); const path = require("path"); const matter = require("gray-matter"); -const config = require("../src/config/config.json"); -const { blog_folder } = config.settings; -const jsonDir = "./.json"; + +const CONTENT_DEPTH = 2; +const JSON_FOLDER = "./.json"; +const BLOG_FOLDER = "src/content/blog"; // get data from markdown -const getData = (folder) => { +const getData = (folder, groupDepth) => { const getPath = fs.readdirSync(path.join(folder)); - const sanitizeData = getPath.filter((item) => item.includes(".md")); - const filterData = sanitizeData.filter((item) => item.match(/^(?!_)/)); - const getData = filterData.map((filename) => { - const file = fs.readFileSync(path.join(folder, filename), "utf-8"); - const { data } = matter(file); - const content = matter(file).content; - const slug = data.slug ? data.slug : filename.replace(".md", ""); + const removeIndex = getPath.filter((item) => item.match(/^(?!-)/)); - return { - frontmatter: data, - content: content, - slug: slug, - }; + const getPaths = removeIndex.map((filename) => { + const filepath = path.join(folder, filename); + const stats = fs.statSync(filepath); + const isFolder = stats.isDirectory(); + + if (isFolder) { + return getData(filepath, groupDepth); + } else if (filename.endsWith(".md") || filename.endsWith(".mdx")) { + const file = fs.readFileSync(path.join(folder, filename), "utf-8"); + const { data } = matter(file); + const content = matter(file).content; + const removeExtension = filepath.replace(/\.[^/.]+$/, ""); + const slug = data.slug + ? data.slug + : removeExtension + .split("/") + .slice(CONTENT_DEPTH, removeExtension.split("/").length) + .join("/"); + + const group = removeExtension.split("/")[Number(groupDepth)]; + + return { + group: group, + slug: slug, + frontmatter: data, + content: content, + }; + } }); - const publishedPages = getData.filter( - (page) => !page.frontmatter?.draft && page + + const publishedPages = getPaths.filter( + (page) => !page.frontmatter?.draft && page, ); return publishedPages; }; -// get post data -const posts = getData(`src/content/${blog_folder}`); +// flatten nested arrays +const flatten = (arr) => { + return arr.reduce((result, element) => { + if (Array.isArray(element)) { + result.push(...flatten(element)); + } else { + result.push(element); + } + return result; + }, []); +}; try { - // creare folder if it doesn't exist - if (!fs.existsSync(jsonDir)) { - fs.mkdirSync(jsonDir); + // create folder if it doesn't exist + if (!fs.existsSync(JSON_FOLDER)) { + fs.mkdirSync(JSON_FOLDER); } - // create posts.json file - fs.writeFileSync(`${jsonDir}/posts.json`, JSON.stringify(posts)); + // create json files + fs.writeFileSync( + `${JSON_FOLDER}/posts.json`, + JSON.stringify(flatten(getData(BLOG_FOLDER, 2))), + ); + + // merger json files for search + const posts = require(`../${JSON_FOLDER}/posts.json`); + const search = [...posts]; + fs.writeFileSync(`${JSON_FOLDER}/search.json`, JSON.stringify(search)); } catch (err) { console.error(err); } diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index f0f7fe8..52d113f 100755 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -6,8 +6,8 @@ import { plainify } from "@/lib/utils/textConverter"; import Footer from "@/partials/Footer.astro"; import Header from "@/partials/Header.astro"; import "@/styles/main.scss"; -import { ViewTransitions } from 'astro:transitions'; - +import { ViewTransitions } from "astro:transitions"; +import SearchModal from "./helpers/SearchModal"; // font families const pf = theme.fonts.font_family.primary; @@ -28,7 +28,7 @@ const { title, meta_title, description, image, noindex, canonical } = Astro.props; --- - + @@ -80,7 +80,7 @@ const { title, meta_title, description, image, noindex, canonical } = @@ -93,7 +93,7 @@ const { title, meta_title, description, image, noindex, canonical } = @@ -101,7 +101,7 @@ const { title, meta_title, description, image, noindex, canonical } = @@ -114,7 +114,7 @@ const { title, meta_title, description, image, noindex, canonical } = @@ -122,7 +122,7 @@ const { title, meta_title, description, image, noindex, canonical } = @@ -146,6 +146,7 @@ const { title, meta_title, description, image, noindex, canonical } =
+
diff --git a/src/layouts/Search.tsx b/src/layouts/Search.tsx deleted file mode 100755 index 56976e7..0000000 --- a/src/layouts/Search.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import config from "@/config/config.json"; -import { humanize, plainify, slugify } from "@/lib/utils/textConverter"; -import Fuse from "fuse.js"; -import React, { useEffect, useRef, useState } from "react"; -import { - FaRegFolder, - FaRegUserCircle, - FaSearch, -} from "react-icons/fa/index.js"; - -const { summary_length, blog_folder } = config.settings; - -export type SearchItem = { - slug: string; - data: any; - content: any; -}; - -interface Props { - searchList: SearchItem[]; -} - -interface SearchResult { - item: SearchItem; - refIndex: number; -} - -const SearchLayout = ({ searchList }: Props) => { - const inputRef = useRef(null); - const [inputVal, setInputVal] = useState(""); - const [searchResults, setSearchResults] = useState([]); - - const handleChange = (e: React.FormEvent) => { - setInputVal(e.currentTarget.value); - }; - - const fuse = new Fuse(searchList, { - keys: ["data.title", "data.categories", "data.tags"], - includeMatches: true, - minMatchCharLength: 3, - threshold: 0.5, - }); - - useEffect(() => { - const searchUrl = new URLSearchParams(window.location.search); - const searchStr = searchUrl.get("q"); - if (searchStr) setInputVal(searchStr); - - setTimeout(function () { - inputRef.current!.selectionStart = inputRef.current!.selectionEnd = - searchStr?.length || 0; - }, 50); - }, []); - - useEffect(() => { - let inputResult = inputVal.length > 2 ? fuse.search(inputVal) : []; - setSearchResults(inputResult); - - if (inputVal.length > 0) { - const searchParams = new URLSearchParams(window.location.search); - searchParams.set("q", inputVal); - const newRelativePathQuery = - window.location.pathname + "?" + searchParams.toString(); - history.pushState(null, "", newRelativePathQuery); - } else { - history.pushState(null, "", window.location.pathname); - } - }, [inputVal]); - - return ( -
-
-
-
-
- - -
-
-
- - {/* {inputVal.length > 1 && ( -
- Found {searchResults?.length} - {searchResults?.length && searchResults?.length === 1 - ? " result" - : " results"}{" "} - for '{inputVal}' -
- )} */} -
- {searchResults?.length < 1 ? ( -
- no-search-found -

- {inputVal.length < 1 ? "Search Post Here" : "No Search Found!"} -

-

- {inputVal.length < 1 - ? "Search for posts by title, category, or tag." - : "We couldn't find what you searched for. Try searching again."} -

-
- ) : ( - searchResults?.map(({ item }, index) => ( -
-
- {item.data.image && ( - {item.data.title} - )} -

- - {item.data.title} - -

- -

- {plainify(item.content?.slice(0, Number(summary_length)))} -

- - read more - -
-
- )) - )} -
-
-
- ); -}; - -export default SearchLayout; diff --git a/src/layouts/helpers/SearchModal.tsx b/src/layouts/helpers/SearchModal.tsx new file mode 100644 index 0000000..142f242 --- /dev/null +++ b/src/layouts/helpers/SearchModal.tsx @@ -0,0 +1,257 @@ +import searchData from ".json/search.json"; +import React, { useEffect, useRef, useState } from "react"; +import SearchResult, { type ISearchItem } from "./SearchResult"; + +const SearchModal = () => { + const searchInputRef = useRef(null); + const [searchString, setSearchString] = useState(""); + + // handle input change + const handleSearch = (e: React.FormEvent) => { + setSearchString(e.currentTarget.value.toLowerCase()); + }; + + // set input value from url + useEffect(() => { + const searchUrl = new URLSearchParams(window.location.search); + const searchStr = searchUrl.get("q"); + searchStr && setSearchString(searchStr.toLowerCase()); + + // set cursor position + setTimeout(function () { + searchInputRef.current!.selectionStart = + searchInputRef.current!.selectionEnd = searchStr?.length || 0; + }, 50); + }, []); + + // update url + useEffect(() => { + if (searchString.length > 0) { + const searchParams = new URLSearchParams(window.location.search); + searchParams.set("s", searchString); + const newRelativePathQuery = + window.location.pathname + "?" + searchParams.toString(); + history.pushState(null, "", newRelativePathQuery); + } else { + history.pushState(null, "", window.location.pathname); + } + }, [searchString]); + + // generate search result + const doSearch = (searchData: ISearchItem[]) => { + const regex = new RegExp(`${searchString}`, "gi"); + if (searchString === "") { + return []; + } else { + const searchResult = searchData.filter((item) => { + const title = item.frontmatter.title.toLowerCase().match(regex); + const description = item.frontmatter.description + ?.toLowerCase() + .match(regex); + const categories = item.frontmatter.categories + ?.join(" ") + .toLowerCase() + .match(regex); + const tags = item.frontmatter.tags + ?.join(" ") + .toLowerCase() + .match(regex); + const content = item.content.toLowerCase().match(regex); + + if (title || content || description || categories || tags) { + return item; + } + }); + return searchResult; + } + }; + + // get search result + const startTime = performance.now(); + const searchResult = doSearch(searchData); + const endTime = performance.now(); + const totalTime = ((endTime - startTime) / 1000).toFixed(3); + + // search dom manipulation + useEffect(() => { + const searchModal = document.getElementById("searchModal"); + const searchInput = document.getElementById("searchInput"); + const searchModalOverlay = document.getElementById("searchModalOverlay"); + const searchResultItems = document.querySelectorAll("#searchItem"); + const searchModalTriggers = document.querySelectorAll( + "[data-search-trigger]", + ); + + // search modal open + searchModalTriggers.forEach((button) => { + button.addEventListener("click", function () { + const searchModal = document.getElementById("searchModal"); + searchModal!.classList.add("show"); + searchInput!.focus(); + }); + }); + + // search modal close + searchModalOverlay!.addEventListener("click", function () { + searchModal!.classList.remove("show"); + }); + + // keyboard navigation + let selectedIndex = -1; + + const updateSelection = () => { + searchResultItems.forEach((item, index) => { + if (index === selectedIndex) { + item.classList.add("search-result-item-active"); + } else { + item.classList.remove("search-result-item-active"); + } + }); + + searchResultItems[selectedIndex]?.scrollIntoView({ + behavior: "smooth", + block: "nearest", + }); + }; + + document.addEventListener("keydown", function (event) { + if ((event.metaKey || event.ctrlKey) && event.key === "k") { + searchModal!.classList.add("show"); + searchInput!.focus(); + updateSelection(); + } + + if (event.key === "ArrowUp" || event.key === "ArrowDown") { + event.preventDefault(); + } + + if (event.key === "Escape") { + searchModal!.classList.remove("show"); + } + + if (event.key === "ArrowUp" && selectedIndex > 0) { + selectedIndex--; + } else if ( + event.key === "ArrowDown" && + selectedIndex < searchResultItems.length - 1 + ) { + selectedIndex++; + } else if (event.key === "Enter") { + const activeLink = document.querySelector( + ".search-result-item-active a", + ) as HTMLAnchorElement; + if (activeLink) { + activeLink?.click(); + } + } + + updateSelection(); + }); + }, [searchString]); + + return ( +
+
+
+
+ + +
+ +
+ + + + + + + + + + + + to navigate + + + + + + + + to select + + {searchString && ( + + {searchResult.length} results - in{" "} + {totalTime} seconds + + )} + + ESC to close + +
+
+
+ ); +}; + +export default SearchModal; diff --git a/src/layouts/helpers/SearchResult.tsx b/src/layouts/helpers/SearchResult.tsx new file mode 100755 index 0000000..6c0efae --- /dev/null +++ b/src/layouts/helpers/SearchResult.tsx @@ -0,0 +1,260 @@ +import { plainify, titleify } from "@/lib/utils/textConverter"; +import React from "react"; + +export interface ISearchItem { + group: string; + slug: string; + frontmatter: { + title: string; + image?: string; + description?: string; + categories?: string[]; + tags?: string[]; + }; + content: string; +} + +export interface ISearchGroup { + group: string; + groupItems: { + slug: string; + frontmatter: { + title: string; + image?: string; + description?: string; + categories?: string[]; + tags?: string[]; + }; + content: string; + }[]; +} + +// search result component +const SearchResult = ({ + searchResult, + searchString, +}: { + searchResult: ISearchItem[]; + searchString: string; +}) => { + // generate search result group + const generateSearchGroup = (searchResult: ISearchItem[]) => { + const joinDataByGroup: ISearchGroup[] = searchResult.reduce( + (groupItems: ISearchGroup[], item: ISearchItem) => { + const groupIndex = groupItems.findIndex( + (group) => group.group === item.group, + ); + if (groupIndex === -1) { + groupItems.push({ + group: item.group, + groupItems: [ + { + frontmatter: { ...item.frontmatter }, + slug: item.slug, + content: item.content, + }, + ], + }); + } else { + groupItems[groupIndex].groupItems.push({ + frontmatter: { ...item.frontmatter }, + slug: item.slug, + content: item.content, + }); + } + + return groupItems; + }, + [], + ); + return joinDataByGroup; + }; + const finalResult = generateSearchGroup(searchResult); + + // match marker + const matchMarker = (text: string, substring: string) => { + const parts = text.split(new RegExp(`(${substring})`, "gi")); + return parts.map((part, index) => + part.toLowerCase() === substring.toLowerCase() ? ( + {part} + ) : ( + part + ), + ); + }; + + // match underline + const matchUnderline = (text: string, substring: string) => { + const parts = text?.split(new RegExp(`(${substring})`, "gi")); + return parts?.map((part, index) => + part.toLowerCase() === substring.toLowerCase() ? ( + + {part} + + ) : ( + part + ), + ); + }; + + // match content + const matchContent = (content: string, substring: string) => { + const plainContent = plainify(content); + const position = plainContent + .toLowerCase() + .indexOf(substring.toLowerCase()); + + // Find the start of the word containing the substring + let wordStart = position; + while (wordStart > 0 && plainContent[wordStart - 1] !== " ") { + wordStart--; + } + + const matches = plainContent.substring( + wordStart, + substring.length + position, + ); + const matchesAfter = plainContent.substring( + substring.length + position, + substring.length + position + 80, + ); + return ( + <> + {matchMarker(matches, substring)} + {matchesAfter} + + ); + }; + + return ( +
+ {searchString ? ( +
+ {finalResult.length > 0 ? ( + finalResult.map((result) => ( +
+

+ {titleify(result.group)} +

+ + {result.groupItems.map((item) => ( +
+ {item.frontmatter.image && ( +
+ {item.frontmatter.title} +
+ )} +
+ + {matchUnderline(item.frontmatter.title, searchString)} + + {item.frontmatter.description && ( +

+ {matchUnderline( + item.frontmatter.description, + searchString, + )} +

+ )} + {item.content && ( +

+ {matchContent(item.content, searchString)} +

+ )} +
+ {item.frontmatter.categories && ( +
+ + + + {item.frontmatter.categories.map( + (category, index) => ( + + {matchUnderline(category, searchString)} + {item.frontmatter.categories && + index !== + item.frontmatter.categories.length - + 1 && <>, } + + ), + )} +
+ )} + {item.frontmatter.tags && ( +
+ + + + + {item.frontmatter.tags.map((tag, index) => ( + + {matchUnderline(tag, searchString)} + {item.frontmatter.tags && + index !== + item.frontmatter.tags.length - 1 && <>, } + + ))} +
+ )} +
+
+
+ ))} +
+ )) + ) : ( +
+ + + + +

+ No results for "{searchString}" +

+
+ )} +
+ ) : ( +
Type something to search...
+ )} +
+ ); +}; + +export default SearchResult; diff --git a/src/layouts/partials/Header.astro b/src/layouts/partials/Header.astro index ebb33e4..1f70aad 100755 --- a/src/layouts/partials/Header.astro +++ b/src/layouts/partials/Header.astro @@ -119,13 +119,13 @@ const { pathname } = Astro.url;
{ settings.search && ( - - + ) } diff --git a/src/lib/utils/textConverter.ts b/src/lib/utils/textConverter.ts index ebf060e..8200bf3 100644 --- a/src/lib/utils/textConverter.ts +++ b/src/lib/utils/textConverter.ts @@ -16,14 +16,24 @@ export const humanize = (content: string) => { return content .replace(/^[\s_]+|[\s_]+$/g, "") .replace(/[_\s]+/g, " ") + .replace(/[-\s]+/g, " ") .replace(/^[a-z]/, function (m) { return m.toUpperCase(); }); }; +// titleify +export const titleify = (content: string) => { + const humanized = humanize(content); + return humanized + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +}; + // plainify export const plainify = (content: string) => { - const parseMarkdown = marked.parse(content); + const parseMarkdown: any = marked.parse(content); const filterBrackets = parseMarkdown.replace(/<\/?[^>]+(>|$)/gm, ""); const filterSpaces = filterBrackets.replace(/[\r\n]\s*[\r\n]/gm, ""); const stripHTML = htmlEntityDecoder(filterSpaces); diff --git a/src/pages/search.astro b/src/pages/search.astro deleted file mode 100755 index 0c15b9a..0000000 --- a/src/pages/search.astro +++ /dev/null @@ -1,19 +0,0 @@ ---- -import Base from "@/layouts/Base.astro"; -import SearchLayout from "@/layouts/Search"; -import { getSinglePage } from "@/lib/contentParser.astro"; - -const BLOG_FOLDER = "blog"; -const posts = await getSinglePage(BLOG_FOLDER); - -// List of items to search in -const searchList = posts.map((item) => ({ - slug: item.slug, - data: item.data, - content: item.body, -})); ---- - - - - diff --git a/src/styles/main.scss b/src/styles/main.scss index 1875eba..9a3d421 100755 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -10,6 +10,7 @@ @import "components"; @import "navigation"; @import "buttons"; + @import "search"; } @layer utilities { diff --git a/src/styles/search.scss b/src/styles/search.scss new file mode 100644 index 0000000..ab22252 --- /dev/null +++ b/src/styles/search.scss @@ -0,0 +1,96 @@ +.search { + &-modal { + @apply z-50 fixed top-0 left-0 w-full h-full flex items-start justify-center invisible opacity-0; + &.show { + @apply visible opacity-100; + } + &-overlay { + @apply fixed top-0 left-0 w-full h-full bg-black opacity-50; + } + } + &-wrapper { + @apply bg-white dark:bg-darkmode-body w-[660px] max-w-[96%] mt-24 rounded shadow-lg relative z-10; + &-header { + @apply p-4 relative; + &-input { + @apply border border-solid w-full focus:ring-0 focus:border-dark border-border rounded-[4px] h-12 pr-4 pl-10 transition duration-200 outline-none dark:bg-darkmode-theme-light dark:text-darkmode-text dark:border-darkmode-border dark:focus:border-darkmode-primary; + } + } + &-body { + @apply dark:bg-darkmode-theme-light dark:shadow-none max-h-[calc(100vh-350px)] overflow-y-auto bg-theme-light shadow-[inset_0_2px_18px_#ddd] p-4 rounded; + } + &-footer { + @apply text-xs select-none leading-none md:flex items-center px-3.5 py-2 hidden; + kbd { + @apply bg-theme-light dark:bg-darkmode-theme-light text-xs leading-none text-center mr-[3px] px-1 py-0.5 rounded-[3px]; + } + span:not(:last-child) { + @apply mr-4; + } + span:last-child { + @apply ml-auto; + } + } + } + &-result { + &-empty { + @apply text-center cursor-text select-none px-0 py-8; + } + &-group { + @apply mb-4; + &-title { + @apply text-lg text-dark dark:text-darkmode-dark mb-[5px] px-3; + } + } + &-item { + @apply rounded border bg-white dark:bg-darkmode-body dark:border-darkmode-border flex items-start mb-1 p-4 scroll-my-[30px] border-solid border-border relative; + mark { + @apply bg-yellow-200 rounded-[2px]; + } + &-title { + @apply text-lg font-bold text-dark dark:text-darkmode-dark leading-none; + } + &-link::after { + @apply absolute top-0 right-0 bottom-0 left-0 z-10 content-[""]; + } + &-image { + @apply shrink-0 mr-3.5; + img { + @apply w-[60px] h-[60px] md:w-[100px] md:h-[100px] rounded-[4px] object-cover; + } + } + &-description { + @apply text-sm line-clamp-1 mt-1; + } + &-content { + @apply mx-0 my-1.5 empty:hidden line-clamp-1; + } + &-taxonomies { + @apply text-sm flex flex-wrap items-center text-light dark:text-darkmode-light; + svg { + @apply inline-block mr-1; + } + } + + &-active, + &:focus, + &:hover { + @apply bg-dark dark:bg-dark; + .search-result-item { + &-title { + @apply text-white; + } + &-description { + @apply text-white/80; + } + &-content { + @apply text-white/90; + } + &-taxonomies { + @apply text-white/90; + } + } + } + } + } +}