remove search page and added search modal

This commit is contained in:
somrat sorkar
2023-12-11 15:00:43 +06:00
parent 4c3b181a46
commit 5da3079efb
12 changed files with 702 additions and 242 deletions
+3 -4
View File
@@ -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",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

+61 -25
View File
@@ -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);
}
+9 -8
View File
@@ -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;
---
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<!-- favicon -->
@@ -80,7 +80,7 @@ const { title, meta_title, description, image, noindex, canonical } =
<meta
name="description"
content={plainify(
description ? description : config.metadata.meta_description
description ? description : config.metadata.meta_description,
)}
/>
@@ -93,7 +93,7 @@ const { title, meta_title, description, image, noindex, canonical } =
<meta
property="og:title"
content={plainify(
meta_title ? meta_title : title ? title : config.site.title
meta_title ? meta_title : title ? title : config.site.title,
)}
/>
@@ -101,7 +101,7 @@ const { title, meta_title, description, image, noindex, canonical } =
<meta
property="og:description"
content={plainify(
description ? description : config.metadata.meta_description
description ? description : config.metadata.meta_description,
)}
/>
<meta property="og:type" content="website" />
@@ -114,7 +114,7 @@ const { title, meta_title, description, image, noindex, canonical } =
<meta
name="twitter:title"
content={plainify(
meta_title ? meta_title : title ? title : config.site.title
meta_title ? meta_title : title ? title : config.site.title,
)}
/>
@@ -122,7 +122,7 @@ const { title, meta_title, description, image, noindex, canonical } =
<meta
name="twitter:description"
content={plainify(
description ? description : config.metadata.meta_description
description ? description : config.metadata.meta_description,
)}
/>
@@ -146,6 +146,7 @@ const { title, meta_title, description, image, noindex, canonical } =
<body>
<TwSizeIndicator />
<Header />
<SearchModal client:load />
<main id="main-content">
<slot />
</main>
-181
View File
@@ -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<HTMLInputElement>(null);
const [inputVal, setInputVal] = useState("");
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const handleChange = (e: React.FormEvent<HTMLInputElement>) => {
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 (
<section className="section-sm">
<div className="container">
<div className="row mb-10 justify-center">
<div className="lg:col-8">
<div className="flex flex-nowrap">
<input
className="form-input rounded-r-none"
placeholder="Search posts"
type="search"
name="search"
value={inputVal}
onChange={handleChange}
autoComplete="off"
autoFocus
ref={inputRef}
/>
<button className="btn btn-primary rounded-l-none" type="submit">
<FaSearch />
</button>
</div>
</div>
</div>
{/* {inputVal.length > 1 && (
<div className="mt-8">
Found {searchResults?.length}
{searchResults?.length && searchResults?.length === 1
? " result"
: " results"}{" "}
for '{inputVal}'
</div>
)} */}
<div className="row">
{searchResults?.length < 1 ? (
<div className="mx-auto pt-5 text-center">
<img
className="mx-auto mb-6"
src="/images/no-search-found.png"
alt="no-search-found"
/>
<h1 className="h2 mb-4">
{inputVal.length < 1 ? "Search Post Here" : "No Search Found!"}
</h1>
<p>
{inputVal.length < 1
? "Search for posts by title, category, or tag."
: "We couldn't find what you searched for. Try searching again."}
</p>
</div>
) : (
searchResults?.map(({ item }, index) => (
<div className="mb-12 md:col-6 lg:col-4" key={`search-${index}`}>
<div className="bg-body dark:bg-darkmode-body">
{item.data.image && (
<img
className="mb-6 w-full rounded"
src={item.data.image}
alt={item.data.title}
width={445}
height={230}
/>
)}
<h4 className="mb-3">
<a href={`/${blog_folder}/${item.slug}`}>
{item.data.title}
</a>
</h4>
<ul className="mb-4">
<li className="mr-4 inline-block">
<a href={`/authors/${slugify(item.data.author)}`}>
<FaRegUserCircle
className={"-mt-1 mr-2 inline-block"}
/>
{humanize(item.data.author)}
</a>
</li>
<li className="mr-4 inline-block">
<FaRegFolder className={"-mt-1 mr-2 inline-block"} />
{item.data.categories.map(
(category: string, index: number) => (
<a
href={`/categories/${slugify(category)}`}
key={category}
>
{humanize(category)}
{index !== item.data.categories.length - 1 && ", "}
</a>
),
)}
</li>
</ul>
<p className="mb-6">
{plainify(item.content?.slice(0, Number(summary_length)))}
</p>
<a
className="btn btn-outline-primary btn-sm"
href={`/${blog_folder}/${item.slug}`}
>
read more
</a>
</div>
</div>
))
)}
</div>
</div>
</section>
);
};
export default SearchLayout;
+257
View File
@@ -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<HTMLInputElement>(null);
const [searchString, setSearchString] = useState("");
// handle input change
const handleSearch = (e: React.FormEvent<HTMLInputElement>) => {
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 (
<div id="searchModal" className="search-modal">
<div id="searchModalOverlay" className="search-modal-overlay" />
<div className="search-wrapper">
<div className="search-wrapper-header">
<label
htmlFor="searchInput"
className="absolute left-7 top-[calc(50%-7px)]"
>
{searchString ? (
<svg
onClick={() => setSearchString("")}
viewBox="0 0 512 512"
height="18"
width="18"
className="hover:text-red-500 cursor-pointer -mt-0.5"
>
<path
fill="currentcolor"
d="M256 512A256 256 0 10256 0a256 256 0 100 512zM175 175c9.4-9.4 24.6-9.4 33.9.0l47 47 47-47c9.4-9.4 24.6-9.4 33.9.0s9.4 24.6.0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6.0 33.9s-24.6 9.4-33.9.0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9.0s-9.4-24.6.0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6.0-33.9z"
></path>
</svg>
) : (
<svg
viewBox="0 0 512 512"
height="18"
width="18"
className="-mt-0.5"
>
<path
fill="currentcolor"
d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8.0 45.3s-32.8 12.5-45.3.0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9.0 208S93.1.0 208 0 416 93.1 416 208zM208 352a144 144 0 100-288 144 144 0 100 288z"
></path>
</svg>
)}
</label>
<input
id="searchInput"
placeholder="Search..."
className="search-wrapper-header-input"
type="input"
name="search"
value={searchString}
onChange={handleSearch}
autoFocus
autoComplete="off"
ref={searchInputRef}
/>
</div>
<SearchResult searchResult={searchResult} searchString={searchString} />
<div className="search-wrapper-footer">
<span className="flex items-center">
<kbd>
<svg
width="14"
height="14"
fill="currentcolor"
viewBox="0 0 16 16"
>
<path d="M3.204 11h9.592L8 5.519 3.204 11zm-.753-.659 4.796-5.48a1 1 0 011.506.0l4.796 5.48c.566.647.106 1.659-.753 1.659H3.204a1 1 0 01-.753-1.659z"></path>
</svg>
</kbd>
<kbd>
<svg
width="14"
height="14"
fill="currentcolor"
viewBox="0 0 16 16"
>
<path d="M3.204 5h9.592L8 10.481 3.204 5zm-.753.659 4.796 5.48a1 1 0 001.506.0l4.796-5.48c.566-.647.106-1.659-.753-1.659H3.204a1 1 0 00-.753 1.659z"></path>
</svg>
</kbd>
to navigate
</span>
<span className="flex items-center">
<kbd>
<svg
width="12"
height="12"
fill="currentcolor"
viewBox="0 0 16 16"
>
<path
fillRule="evenodd"
d="M14.5 1.5a.5.5.0 01.5.5v4.8a2.5 2.5.0 01-2.5 2.5H2.707l3.347 3.346a.5.5.0 01-.708.708l-4.2-4.2a.5.5.0 010-.708l4-4a.5.5.0 11.708.708L2.707 8.3H12.5A1.5 1.5.0 0014 6.8V2a.5.5.0 01.5-.5z"
></path>
</svg>
</kbd>
to select
</span>
{searchString && (
<span>
<strong>{searchResult.length} </strong> results - in{" "}
<strong>{totalTime} </strong> seconds
</span>
)}
<span>
<kbd>ESC</kbd> to close
</span>
</div>
</div>
</div>
);
};
export default SearchModal;
+260
View File
@@ -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() ? (
<mark key={index}>{part}</mark>
) : (
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() ? (
<span key={index} className="underline">
{part}
</span>
) : (
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 (
<div className="search-wrapper-body">
{searchString ? (
<div className="search-result">
{finalResult.length > 0 ? (
finalResult.map((result) => (
<div className="search-result-group" key={result.group}>
<p className="search-result-group-title">
{titleify(result.group)}
</p>
{result.groupItems.map((item) => (
<div
key={item.slug}
id="searchItem"
className="search-result-item"
>
{item.frontmatter.image && (
<div className="search-result-item-image">
<img
src={item.frontmatter.image}
alt={item.frontmatter.title}
/>
</div>
)}
<div className="search-result-item-body">
<a
href={`/${item.slug}`}
className="search-result-item-title search-result-item-link"
>
{matchUnderline(item.frontmatter.title, searchString)}
</a>
{item.frontmatter.description && (
<p className="search-result-item-description">
{matchUnderline(
item.frontmatter.description,
searchString,
)}
</p>
)}
{item.content && (
<p className="search-result-item-content">
{matchContent(item.content, searchString)}
</p>
)}
<div className="search-result-item-taxonomies">
{item.frontmatter.categories && (
<div className="mr-2">
<svg
width="14"
height="14"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M11 0H3a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2 2 2 0 0 0 2-2V4a2 2 0 0 0-2-2 2 2 0 0 0-2-2zm2 3a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1V3zM2 2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V2z"></path>
</svg>
{item.frontmatter.categories.map(
(category, index) => (
<span key={category}>
{matchUnderline(category, searchString)}
{item.frontmatter.categories &&
index !==
item.frontmatter.categories.length -
1 && <>, </>}
</span>
),
)}
</div>
)}
{item.frontmatter.tags && (
<div className="mr-2">
<svg
width="14"
height="14"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M3 2v4.586l7 7L14.586 9l-7-7H3zM2 2a1 1 0 0 1 1-1h4.586a1 1 0 0 1 .707.293l7 7a1 1 0 0 1 0 1.414l-4.586 4.586a1 1 0 0 1-1.414 0l-7-7A1 1 0 0 1 2 6.586V2z"></path>
<path d="M5.5 5a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0 1a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM1 7.086a1 1 0 0 0 .293.707L8.75 15.25l-.043.043a1 1 0 0 1-1.414 0l-7-7A1 1 0 0 1 0 7.586V3a1 1 0 0 1 1-1v5.086z"></path>
</svg>
{item.frontmatter.tags.map((tag, index) => (
<span key={tag}>
{matchUnderline(tag, searchString)}
{item.frontmatter.tags &&
index !==
item.frontmatter.tags.length - 1 && <>, </>}
</span>
))}
</div>
)}
</div>
</div>
</div>
))}
</div>
))
) : (
<div className="search-result-empty">
<svg
className="mx-auto"
width="42"
height="42"
viewBox="0 0 47 47"
fill="none"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.10368 33.9625C9.90104 36.2184 13.2988 37.6547 16.9158 38.0692C21.6958 38.617 26.5063 37.3401 30.3853 34.4939C30.4731 34.6109 30.5668 34.7221 30.6721 34.8304L41.9815 46.1397C42.5323 46.6909 43.2795 47.0007 44.0587 47.001C44.838 47.0013 45.5854 46.692 46.1366 46.1412C46.6878 45.5904 46.9976 44.8432 46.9979 44.064C46.9981 43.2847 46.6888 42.5373 46.138 41.9861L34.8287 30.6767C34.7236 30.5704 34.6107 30.4752 34.4909 30.3859C37.3352 26.5046 38.6092 21.6924 38.0579 16.912C37.6355 13.2498 36.1657 9.81322 33.8586 6.9977L31.7805 9.09214C34.0157 11.9274 35.2487 15.4472 35.2487 19.0942C35.2487 21.2158 34.8308 23.3167 34.0189 25.2769C33.207 27.2371 32.0169 29.0181 30.5167 30.5184C29.0164 32.0186 27.2354 33.2087 25.2752 34.0206C23.315 34.8325 21.2141 35.2504 19.0925 35.2504C16.9708 35.2504 14.8699 34.8325 12.9098 34.0206C11.5762 33.4682 10.3256 32.7409 9.18992 31.8599L7.10368 33.9625ZM28.9344 6.28152C26.1272 4.12516 22.671 2.93792 19.0925 2.93792C14.8076 2.93792 10.6982 4.64009 7.66829 7.66997C4.6384 10.6999 2.93623 14.8093 2.93623 19.0942C2.93623 21.2158 3.35413 23.3167 4.16605 25.2769C4.72475 26.6257 5.4625 27.8897 6.35716 29.0358L4.2702 31.1391C1.35261 27.548 -0.165546 23.0135 0.00974294 18.3781C0.19158 13.5695 2.18233 9.00695 5.58371 5.60313C8.98509 2.19932 13.5463 0.205307 18.3547 0.0200301C22.9447 -0.156832 27.4369 1.32691 31.0132 4.18636L28.9344 6.28152Z"
fill="currentColor"
></path>
<path
d="M3.13672 39.1367L38.3537 3.64355"
stroke="black"
strokeWidth="3"
strokeLinecap="round"
></path>
</svg>
<p className="mt-4">
No results for &quot;<strong>{searchString}</strong>&quot;
</p>
</div>
)}
</div>
) : (
<div className="py-8 text-center">Type something to search...</div>
)}
</div>
);
};
export default SearchResult;
+4 -4
View File
@@ -119,13 +119,13 @@ const { pathname } = Astro.url;
<div class="order-1 ml-auto flex items-center md:order-2 lg:ml-0">
{
settings.search && (
<a
class="mr-5 inline-block border-r border-border pr-5 text-xl text-dark hover:text-primary dark:border-darkmode-border dark:text-white"
href="/search"
<span
class="mr-5 inline-block border-r border-border pr-5 text-xl text-dark hover:text-primary dark:border-darkmode-border dark:text-white cursor-pointer"
aria-label="search"
data-search-trigger
>
<IoSearch />
</a>
</span>
)
}
<ThemeSwitcher className="mr-5" />
+11 -1
View File
@@ -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);
-19
View File
@@ -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,
}));
---
<Base title={`Search`}>
<SearchLayout client:load searchList={searchList} />
</Base>
+1
View File
@@ -10,6 +10,7 @@
@import "components";
@import "navigation";
@import "buttons";
@import "search";
}
@layer utilities {
+96
View File
@@ -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;
}
}
}
}
}
}