From a198cb3ca570a5fc38ef07bc19163896865025ce Mon Sep 17 00:00:00 2001 From: krckyboy Date: Sun, 25 Feb 2024 10:52:00 +0100 Subject: [PATCH 1/7] #24 Tweaking margin. --- next/src/styles/about-and-blog-content.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next/src/styles/about-and-blog-content.module.scss b/next/src/styles/about-and-blog-content.module.scss index e1a5c5c..d07bb7c 100644 --- a/next/src/styles/about-and-blog-content.module.scss +++ b/next/src/styles/about-and-blog-content.module.scss @@ -1,4 +1,4 @@ -$margin-top-value: 1rem; +$margin-top-value: 1.5rem; /** Typography, spacings and other styles for /about and /blog/[slug] From d86738d273ddfeae7344130679925da9db61304d Mon Sep 17 00:00:00 2001 From: krckyboy Date: Sun, 25 Feb 2024 11:49:49 +0100 Subject: [PATCH 2/7] #24 Add additional styles for selected tags and visual cue that you can unselect it. --- next/public/images/category/x.svg | 4 ++++ .../categories/category-link/CategoryLink.tsx | 16 ++++++++++++++-- .../category-link/styles.module.scss | 19 +++++++++++++++++-- .../components/categories/styles.module.scss | 1 + 4 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 next/public/images/category/x.svg diff --git a/next/public/images/category/x.svg b/next/public/images/category/x.svg new file mode 100644 index 0000000..097e40d --- /dev/null +++ b/next/public/images/category/x.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/next/src/components/categories/category-link/CategoryLink.tsx b/next/src/components/categories/category-link/CategoryLink.tsx index 18d09bd..f291f0e 100644 --- a/next/src/components/categories/category-link/CategoryLink.tsx +++ b/next/src/components/categories/category-link/CategoryLink.tsx @@ -1,4 +1,5 @@ import Link from 'next/link'; +import Image from 'next/image'; import React, { FunctionComponent } from 'react'; import styles from './styles.module.scss'; import { Category } from '@/components/categories/types'; @@ -11,11 +12,22 @@ interface Props { const CategoryLink: FunctionComponent = ({ activeCategory, name }) => { const isActiveCategory = name === activeCategory; - const setActiveCategory = isActiveCategory ? styles.active : ''; + const setActiveClassName = isActiveCategory ? styles.active : ''; const setHref = isActiveCategory ? '/blog' : `/blog?category=${name}`; return ( - #{name} + + #{name} + {isActiveCategory && ( + {'Unselect + )} + ); }; diff --git a/next/src/components/categories/category-link/styles.module.scss b/next/src/components/categories/category-link/styles.module.scss index 8329331..761656e 100644 --- a/next/src/components/categories/category-link/styles.module.scss +++ b/next/src/components/categories/category-link/styles.module.scss @@ -1,9 +1,24 @@ @import '@/styles/vars'; .link { - color: darken($color-text, 40%); + display: flex; + align-items: center; + + .text { + color: darken($color-text, 40%); + } + + .cross { + margin-left: .2rem; + } &.active { - color: $color-accent; + border-radius: .35rem; + padding: .5rem; + background-color: darken($color-text, 90%); + + .text { + color: $color-accent; + } } } \ No newline at end of file diff --git a/next/src/components/categories/styles.module.scss b/next/src/components/categories/styles.module.scss index e150921..9675e5c 100644 --- a/next/src/components/categories/styles.module.scss +++ b/next/src/components/categories/styles.module.scss @@ -3,6 +3,7 @@ .categories { margin-bottom: 3rem; justify-content: flex-start; + align-items: center; display: flex; gap: .5rem; } \ No newline at end of file From a98cacae4800ee07e9e9b4f961228a07509ff025 Mon Sep 17 00:00:00 2001 From: krckyboy Date: Sun, 25 Feb 2024 16:33:53 +0100 Subject: [PATCH 3/7] #24 Tweaking category link colors. --- next/public/images/category/x.svg | 2 +- .../components/categories/category-link/styles.module.scss | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/next/public/images/category/x.svg b/next/public/images/category/x.svg index 097e40d..c62467d 100644 --- a/next/public/images/category/x.svg +++ b/next/public/images/category/x.svg @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/next/src/components/categories/category-link/styles.module.scss b/next/src/components/categories/category-link/styles.module.scss index 761656e..57868ce 100644 --- a/next/src/components/categories/category-link/styles.module.scss +++ b/next/src/components/categories/category-link/styles.module.scss @@ -15,10 +15,10 @@ &.active { border-radius: .35rem; padding: .5rem; - background-color: darken($color-text, 90%); + background-color: darken($color-accent, 10%); .text { - color: $color-accent; + color: $color-text; } } } \ No newline at end of file From ef6bbb64af7631c873e61c556ece69e9ddf4cd71 Mon Sep 17 00:00:00 2001 From: krckyboy Date: Sun, 25 Feb 2024 16:34:58 +0100 Subject: [PATCH 4/7] #24 Uploading assets --- next/public/images/category/x.svg | 2 +- next/public/images/search/icon.svg | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 next/public/images/search/icon.svg diff --git a/next/public/images/category/x.svg b/next/public/images/category/x.svg index c62467d..524c6b3 100644 --- a/next/public/images/category/x.svg +++ b/next/public/images/category/x.svg @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/next/public/images/search/icon.svg b/next/public/images/search/icon.svg new file mode 100644 index 0000000..4f492ad --- /dev/null +++ b/next/public/images/search/icon.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file From a79e4d043b56b7e92c52980288998156991aafd8 Mon Sep 17 00:00:00 2001 From: krckyboy Date: Sun, 25 Feb 2024 19:50:33 +0100 Subject: [PATCH 5/7] #24 Added first version of search. Selecting by keys to be added. --- next/src/app/blog/(search-bar)/SearchBar.tsx | 89 +++++++++++++++++++ .../app/blog/(search-bar)/styles.module.scss | 64 +++++++++++++ .../src/app/blog/(search-bar)/useDebounce.tsx | 17 ++++ next/src/app/blog/page.tsx | 2 + next/src/scripts/fetch.ts | 12 +++ next/src/styles/global.scss | 1 + 6 files changed, 185 insertions(+) create mode 100644 next/src/app/blog/(search-bar)/SearchBar.tsx create mode 100644 next/src/app/blog/(search-bar)/styles.module.scss create mode 100644 next/src/app/blog/(search-bar)/useDebounce.tsx diff --git a/next/src/app/blog/(search-bar)/SearchBar.tsx b/next/src/app/blog/(search-bar)/SearchBar.tsx new file mode 100644 index 0000000..0d0aada --- /dev/null +++ b/next/src/app/blog/(search-bar)/SearchBar.tsx @@ -0,0 +1,89 @@ +'use client'; +import React, { FunctionComponent, useEffect, useRef, useState } from 'react'; +import styles from './styles.module.scss'; +import Image from 'next/image'; +import type { PostsFetchResponse } from '@/components/blog-post-item/types'; +import { db } from '@/scripts/fetch'; +import Link from 'next/link'; +import { useDebounce } from '@/app/blog/(search-bar)/useDebounce'; + +const SearchBar: FunctionComponent = () => { + const [isActive, setFormActive] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState(null); + + const searchInputRef = useRef(null); + const debouncedSearchQuery = useDebounce(searchQuery, 500); + + const fetchPosts = async () => { + const posts = await db.getPostsBySearchTerm(debouncedSearchQuery); + setSearchResults(posts.data); + }; + + const handleBlur = (event: React.FocusEvent) => { + const { currentTarget, relatedTarget } = event; + + if (currentTarget.contains(relatedTarget)) { + return; + } + + setFormActive(false); + }; + + const handleEscKey = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + searchInputRef.current?.blur(); + setSearchQuery(''); + } + }; + + useEffect(() => { + document.addEventListener('keydown', handleEscKey); + + return () => { + document.removeEventListener('keydown', handleEscKey); + }; + }, []); + + useEffect(() => { + if (!debouncedSearchQuery || debouncedSearchQuery.length < 2) { + setSearchResults([]); + return; + } + + fetchPosts(); + }, [debouncedSearchQuery]); + + return ( +
+ + {Boolean(searchResults?.length && isActive) && ( +
    + {searchResults?.map((post) => ( +
  • + {post.attributes.title} +
  • + ))} +
+ )} + + ); +}; + +export default SearchBar; \ No newline at end of file diff --git a/next/src/app/blog/(search-bar)/styles.module.scss b/next/src/app/blog/(search-bar)/styles.module.scss new file mode 100644 index 0000000..4a547f1 --- /dev/null +++ b/next/src/app/blog/(search-bar)/styles.module.scss @@ -0,0 +1,64 @@ +@import '@/styles/vars'; + +.container { + display: flex; + width: 100%; + background-color: $color-background; + margin-bottom: 2rem; + position: relative; + + .label { + height: 36px; + border-radius: .35rem; + border: 1px solid darken($color-text, 40%); + width: 100%; + display: flex; + align-items: center; + padding-inline: .5rem; + + .input { + background-color: inherit; + outline: none; + border: none; + height: 100%; + width: 100%; + padding-left: .3rem; + color: darken($color-text, 40%); + font-weight: 300; + + &::placeholder { + color: darken($color-text, 60%); + } + } + } + + .results { + overflow: hidden; + display: flex; + flex-direction: column; + background-color: $color-background; + border-radius: .35rem; + border: 1px solid darken($color-text, 40%); + width: 100%; + position: absolute; + translate: 0 100%; + bottom: -1rem; + left: 0; + padding-block: .5rem; + + li { + width: 100%; + + a { + display: flex; + width: 100%; + padding-inline: 1rem; + padding-block: .8rem; + } + + &:hover { + background-color: lighten($color-background, 10%); + } + } + } +} \ No newline at end of file diff --git a/next/src/app/blog/(search-bar)/useDebounce.tsx b/next/src/app/blog/(search-bar)/useDebounce.tsx new file mode 100644 index 0000000..b1c2493 --- /dev/null +++ b/next/src/app/blog/(search-bar)/useDebounce.tsx @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react'; + +export const useDebounce = (value: T, delay: number): T => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); // Only re-call effect if value or delay changes + + return debouncedValue; +}; diff --git a/next/src/app/blog/page.tsx b/next/src/app/blog/page.tsx index bb103b5..a75f8cf 100644 --- a/next/src/app/blog/page.tsx +++ b/next/src/app/blog/page.tsx @@ -5,6 +5,7 @@ import BlogPostItem from '@/components/blog-post-item/BlogPostItem'; import React from 'react'; import { db } from '@/scripts/fetch'; import Categories from '@/components/categories/Categories'; +import SearchBar from '@/app/blog/(search-bar)/SearchBar'; interface Props { searchParams: { @@ -20,6 +21,7 @@ const Page: NextPage = async ({ searchParams: { category } }) => {

Blog

+
{posts.data.map((post) => )} diff --git a/next/src/scripts/fetch.ts b/next/src/scripts/fetch.ts index 72113e1..2ac825e 100644 --- a/next/src/scripts/fetch.ts +++ b/next/src/scripts/fetch.ts @@ -66,6 +66,18 @@ export const db = { const queryString = qs.stringify(queryParams); return await fetchWrapper(`/posts?${queryString}`); }, + getPostsBySearchTerm: async (searchValue: string) => { + const queryParams = { + filters: { + title: { + $containsi: searchValue + } + } + }; + + const queryString = qs.stringify(queryParams); + return await fetchWrapper(`/posts?${queryString}`); + }, getPostSlugs: async () => { const queryParams = { fields: ['slug'] diff --git a/next/src/styles/global.scss b/next/src/styles/global.scss index cc2ae57..3922e6d 100644 --- a/next/src/styles/global.scss +++ b/next/src/styles/global.scss @@ -4,6 +4,7 @@ font-family: 'League Spartan', sans-serif; color: $color-text; text-decoration: none; + box-sizing: border-box; } body { From 8223dece6530a540edf4c5840e5a44896441c77c Mon Sep 17 00:00:00 2001 From: krckyboy Date: Sun, 25 Feb 2024 20:15:38 +0100 Subject: [PATCH 6/7] #24 Fixed a bug with form staying inactive when switching between browser and other apps + minor tweaks. --- next/src/app/blog/(search-bar)/SearchBar.tsx | 51 ++++++++++++++++--- .../app/blog/(search-bar)/styles.module.scss | 2 +- next/src/scripts/fetch.ts | 4 ++ 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/next/src/app/blog/(search-bar)/SearchBar.tsx b/next/src/app/blog/(search-bar)/SearchBar.tsx index 0d0aada..2cce1a5 100644 --- a/next/src/app/blog/(search-bar)/SearchBar.tsx +++ b/next/src/app/blog/(search-bar)/SearchBar.tsx @@ -6,11 +6,15 @@ import type { PostsFetchResponse } from '@/components/blog-post-item/types'; import { db } from '@/scripts/fetch'; import Link from 'next/link'; import { useDebounce } from '@/app/blog/(search-bar)/useDebounce'; +import { useRouter } from 'next/navigation'; const SearchBar: FunctionComponent = () => { + const router = useRouter(); + const [isActive, setFormActive] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState(null); + const [focusedIndex, setFocusedIndex] = useState(-1); const searchInputRef = useRef(null); const debouncedSearchQuery = useDebounce(searchQuery, 500); @@ -20,8 +24,8 @@ const SearchBar: FunctionComponent = () => { setSearchResults(posts.data); }; - const handleBlur = (event: React.FocusEvent) => { - const { currentTarget, relatedTarget } = event; + const handleBlur = (e: React.FocusEvent) => { + const { currentTarget, relatedTarget } = e; if (currentTarget.contains(relatedTarget)) { return; @@ -30,13 +34,43 @@ const SearchBar: FunctionComponent = () => { setFormActive(false); }; - const handleEscKey = (event: KeyboardEvent) => { - if (event.key === 'Escape') { + const handleEscKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') { searchInputRef.current?.blur(); setSearchQuery(''); } }; + const handleChange = (e: React.ChangeEvent) => { + setSearchQuery(e.target.value); + setFormActive(true); + }; + + // Keyboard navigation + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + + if (!searchResults) { + return; + } + + setFocusedIndex((prevIndex) => Math.min(prevIndex + 1, searchResults?.length - 1 ?? 0)); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + + setFocusedIndex((prevIndex) => Math.max(prevIndex - 1, 0)); + } else if (e.key === 'Enter' && focusedIndex !== -1) { + const postToGoTo = searchResults?.find((post, index) => focusedIndex === index); + + if (!postToGoTo) { + return; + } + + router.push(`/blog/${postToGoTo.attributes.slug}`); + } + }; + useEffect(() => { document.addEventListener('keydown', handleEscKey); @@ -55,7 +89,7 @@ const SearchBar: FunctionComponent = () => { }, [debouncedSearchQuery]); return ( -
+
{Boolean(searchResults?.length && isActive) && (
    - {searchResults?.map((post) => ( -
  • + {searchResults?.map((post, index) => ( +
  • {post.attributes.title}
  • ))} diff --git a/next/src/app/blog/(search-bar)/styles.module.scss b/next/src/app/blog/(search-bar)/styles.module.scss index 4a547f1..75ac984 100644 --- a/next/src/app/blog/(search-bar)/styles.module.scss +++ b/next/src/app/blog/(search-bar)/styles.module.scss @@ -56,7 +56,7 @@ padding-block: .8rem; } - &:hover { + &.focused, &:hover { background-color: lighten($color-background, 10%); } } diff --git a/next/src/scripts/fetch.ts b/next/src/scripts/fetch.ts index 2ac825e..b7f9032 100644 --- a/next/src/scripts/fetch.ts +++ b/next/src/scripts/fetch.ts @@ -72,6 +72,10 @@ export const db = { title: { $containsi: searchValue } + }, + pagination: { + pageSize: 10, + page: 1 } }; From 27c16ad442c8e61dc1e4dd70da60ff3d0fa9d885 Mon Sep 17 00:00:00 2001 From: krckyboy Date: Sun, 25 Feb 2024 21:00:02 +0100 Subject: [PATCH 7/7] #24 Formatting. --- next/src/app/blog/(search-bar)/SearchBar.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/next/src/app/blog/(search-bar)/SearchBar.tsx b/next/src/app/blog/(search-bar)/SearchBar.tsx index 2cce1a5..5ef6920 100644 --- a/next/src/app/blog/(search-bar)/SearchBar.tsx +++ b/next/src/app/blog/(search-bar)/SearchBar.tsx @@ -1,12 +1,12 @@ 'use client'; import React, { FunctionComponent, useEffect, useRef, useState } from 'react'; -import styles from './styles.module.scss'; import Image from 'next/image'; -import type { PostsFetchResponse } from '@/components/blog-post-item/types'; -import { db } from '@/scripts/fetch'; import Link from 'next/link'; -import { useDebounce } from '@/app/blog/(search-bar)/useDebounce'; import { useRouter } from 'next/navigation'; +import { useDebounce } from '@/app/blog/(search-bar)/useDebounce'; +import type { PostsFetchResponse } from '@/components/blog-post-item/types'; +import { db } from '@/scripts/fetch'; +import styles from './styles.module.scss'; const SearchBar: FunctionComponent = () => { const router = useRouter();