@stackshift-ui/blog 6.0.4 → 6.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blog.d.ts +1 -1
- package/dist/blog.js +1 -1
- package/dist/blog.mjs +1 -1
- package/dist/chunk-25V6I4CL.mjs +1 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/types.d.ts +1 -0
- package/package.json +18 -17
- package/src/blog.test.tsx +13 -0
- package/src/blog.tsx +32 -0
- package/src/blog_a.tsx +121 -0
- package/src/blog_b.tsx +158 -0
- package/src/blog_c.tsx +142 -0
- package/src/blog_d.tsx +349 -0
- package/src/hooks/useMediaQuery.ts +27 -0
- package/src/index.ts +8 -0
- package/src/types.ts +413 -0
- package/dist/chunk-VDHKVAJM.mjs +0 -1
package/src/blog_d.tsx
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { Button } from "@stackshift-ui/button";
|
|
2
|
+
import { Card } from "@stackshift-ui/card";
|
|
3
|
+
import { Container } from "@stackshift-ui/container";
|
|
4
|
+
import { Flex } from "@stackshift-ui/flex";
|
|
5
|
+
import { Heading } from "@stackshift-ui/heading";
|
|
6
|
+
import { Image } from "@stackshift-ui/image";
|
|
7
|
+
import { Input } from "@stackshift-ui/input";
|
|
8
|
+
import { Link } from "@stackshift-ui/link";
|
|
9
|
+
import { Section } from "@stackshift-ui/section";
|
|
10
|
+
import { Text } from "@stackshift-ui/text";
|
|
11
|
+
import { format } from "date-fns";
|
|
12
|
+
import React from "react";
|
|
13
|
+
import { BlogProps } from ".";
|
|
14
|
+
import { Author, BlogPost, SanityBody } from "./types";
|
|
15
|
+
|
|
16
|
+
interface BlogPostProps extends SanityBody {
|
|
17
|
+
category?: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
slug?: {
|
|
20
|
+
_type: "slug";
|
|
21
|
+
current: string;
|
|
22
|
+
};
|
|
23
|
+
excerpt?: string;
|
|
24
|
+
publishedAt?: string;
|
|
25
|
+
mainImage?: string;
|
|
26
|
+
authors?: Author[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function Blog_D({ subtitle, title, posts }: BlogProps) {
|
|
30
|
+
const [activeTab, setActiveTab] = React.useState<string>("All");
|
|
31
|
+
const [currentPage, setCurrentPage] = React.useState<number>(1);
|
|
32
|
+
const [searchQuery, setSearchQuery] = React.useState<string>("");
|
|
33
|
+
let blogsPerPage = 6;
|
|
34
|
+
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
setCurrentPage(1);
|
|
37
|
+
}, [activeTab]);
|
|
38
|
+
|
|
39
|
+
const transformedPosts: BlogPostProps[] = (posts ?? []).flatMap(post =>
|
|
40
|
+
(post?.categories ?? []).map(
|
|
41
|
+
category =>
|
|
42
|
+
({
|
|
43
|
+
category: category?.title,
|
|
44
|
+
title: post?.title,
|
|
45
|
+
slug: post?.slug,
|
|
46
|
+
excerpt: post?.excerpt,
|
|
47
|
+
publishedAt: post?.publishedAt,
|
|
48
|
+
mainImage: post?.mainImage,
|
|
49
|
+
authors: post?.authors,
|
|
50
|
+
}) as BlogPostProps,
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// get all categories
|
|
55
|
+
const categories: string[] = transformedPosts?.reduce((newArr: any[], items: BlogPostProps) => {
|
|
56
|
+
const titles = items?.category;
|
|
57
|
+
|
|
58
|
+
if (newArr.indexOf(titles) === -1) {
|
|
59
|
+
newArr.push(titles);
|
|
60
|
+
}
|
|
61
|
+
return newArr;
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
// filtered posts per category
|
|
65
|
+
const filteredPosts =
|
|
66
|
+
activeTab === "All"
|
|
67
|
+
? posts?.filter(post => post?.title?.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
68
|
+
: transformedPosts.filter(
|
|
69
|
+
item =>
|
|
70
|
+
item?.category === activeTab &&
|
|
71
|
+
item?.title?.toLowerCase().includes(searchQuery.toLowerCase()),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
//Pagination
|
|
75
|
+
const indexOfLastPost = currentPage * blogsPerPage;
|
|
76
|
+
const indexOfFirstPost = indexOfLastPost - blogsPerPage;
|
|
77
|
+
const currentPosts = filteredPosts?.slice(indexOfFirstPost, indexOfLastPost);
|
|
78
|
+
|
|
79
|
+
//Change page
|
|
80
|
+
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
|
|
81
|
+
|
|
82
|
+
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
83
|
+
setSearchQuery(e.target.value);
|
|
84
|
+
setActiveTab("All");
|
|
85
|
+
setCurrentPage(1);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Section className="py-20 bg-background">
|
|
90
|
+
<Container maxWidth={1280}>
|
|
91
|
+
<SubtitleAndTitleText subtitle={subtitle} title={title} />
|
|
92
|
+
<SearchInput handleSearchChange={handleSearchChange} />
|
|
93
|
+
<Flex wrap>
|
|
94
|
+
<CategoryTab categories={categories} activeTab={activeTab} setActiveTab={setActiveTab} />
|
|
95
|
+
{filteredPosts?.length === 0 ? (
|
|
96
|
+
<NoPostsMessage />
|
|
97
|
+
) : (
|
|
98
|
+
<PostItems
|
|
99
|
+
currentPosts={currentPosts}
|
|
100
|
+
activeTab={activeTab}
|
|
101
|
+
blogsPerPage={blogsPerPage}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
</Flex>
|
|
105
|
+
<Pagination
|
|
106
|
+
blogsPerPage={blogsPerPage}
|
|
107
|
+
totalBlogs={filteredPosts?.length as number}
|
|
108
|
+
paginate={paginate}
|
|
109
|
+
currentPage={currentPage}
|
|
110
|
+
/>
|
|
111
|
+
</Container>
|
|
112
|
+
</Section>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function NoPostsMessage({ message = "No post available." }) {
|
|
117
|
+
return <div className="w-full px-3 lg:w-3/4 font-medium text-lg">{message}</div>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function SubtitleAndTitleText({ subtitle, title }: { subtitle?: string; title?: string }) {
|
|
121
|
+
return (
|
|
122
|
+
<div className="w-full mb-16">
|
|
123
|
+
{subtitle ? (
|
|
124
|
+
<Text weight={"bold"} className="text-secondary">
|
|
125
|
+
{subtitle}
|
|
126
|
+
</Text>
|
|
127
|
+
) : null}
|
|
128
|
+
{title ? <Heading fontSize="3xl">{title}</Heading> : null}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function SearchInput({
|
|
134
|
+
handleSearchChange,
|
|
135
|
+
}: {
|
|
136
|
+
handleSearchChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
137
|
+
}) {
|
|
138
|
+
return (
|
|
139
|
+
<div className="relative mb-5 w-full lg:w-1/4">
|
|
140
|
+
<Input
|
|
141
|
+
type="text"
|
|
142
|
+
aria-label="Search, find any question you want to ask..."
|
|
143
|
+
className="w-full bg-white rounded-global font-heading focus:border-gray-500 focus:outline-none"
|
|
144
|
+
placeholder="Search posts..."
|
|
145
|
+
onChange={handleSearchChange}
|
|
146
|
+
/>
|
|
147
|
+
<Button
|
|
148
|
+
as="button"
|
|
149
|
+
variant="unstyled"
|
|
150
|
+
ariaLabel="Search button"
|
|
151
|
+
className="absolute right-0 top-0 h-full px-3 bg-white rounded-global text-primary flex items-center">
|
|
152
|
+
<svg
|
|
153
|
+
className="w-6 h-6"
|
|
154
|
+
fill="none"
|
|
155
|
+
stroke="currentColor"
|
|
156
|
+
viewBox="0 0 24 24"
|
|
157
|
+
xmlns="http://www.w3.org/2000/svg">
|
|
158
|
+
<path
|
|
159
|
+
strokeLinecap="round"
|
|
160
|
+
strokeLinejoin="round"
|
|
161
|
+
strokeWidth={2}
|
|
162
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
163
|
+
/>
|
|
164
|
+
</svg>
|
|
165
|
+
</Button>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function CategoryTab({
|
|
171
|
+
categories,
|
|
172
|
+
activeTab,
|
|
173
|
+
setActiveTab,
|
|
174
|
+
}: {
|
|
175
|
+
categories?: string[];
|
|
176
|
+
activeTab: string;
|
|
177
|
+
setActiveTab: (category: string) => void;
|
|
178
|
+
}) {
|
|
179
|
+
return (
|
|
180
|
+
<Card className="w-full px-3 mb-8 bg-white lg:mb-0 lg:w-1/4" borderRadius="global">
|
|
181
|
+
{categories && (
|
|
182
|
+
<React.Fragment>
|
|
183
|
+
<Heading
|
|
184
|
+
type="h3"
|
|
185
|
+
muted
|
|
186
|
+
weight={"bold"}
|
|
187
|
+
className="mb-4 text-base uppercase lg:text-base">
|
|
188
|
+
Topics
|
|
189
|
+
</Heading>
|
|
190
|
+
<ul>
|
|
191
|
+
{categories?.length > 1 && (
|
|
192
|
+
<CategoryItem activeTab={activeTab} setActiveTab={setActiveTab} category={"All"} />
|
|
193
|
+
)}
|
|
194
|
+
{categories?.map((category, index) => (
|
|
195
|
+
<CategoryItem
|
|
196
|
+
key={index}
|
|
197
|
+
activeTab={activeTab}
|
|
198
|
+
setActiveTab={setActiveTab}
|
|
199
|
+
category={category}
|
|
200
|
+
/>
|
|
201
|
+
))}
|
|
202
|
+
</ul>
|
|
203
|
+
</React.Fragment>
|
|
204
|
+
)}
|
|
205
|
+
</Card>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function CategoryItem({
|
|
210
|
+
key,
|
|
211
|
+
activeTab,
|
|
212
|
+
setActiveTab,
|
|
213
|
+
category,
|
|
214
|
+
}: {
|
|
215
|
+
key?: number;
|
|
216
|
+
activeTab: string;
|
|
217
|
+
setActiveTab: (category: string) => void;
|
|
218
|
+
category: string;
|
|
219
|
+
}) {
|
|
220
|
+
return (
|
|
221
|
+
<li key={key}>
|
|
222
|
+
<Button
|
|
223
|
+
as="button"
|
|
224
|
+
variant="unstyled"
|
|
225
|
+
ariaLabel="Show all blog posts"
|
|
226
|
+
className={`mb-4 block ${
|
|
227
|
+
!category ? "hidden" : "block"
|
|
228
|
+
} px-3 py-2 hover:bg-secondary-foreground focus:outline-none w-full text-left rounded ${
|
|
229
|
+
activeTab === category
|
|
230
|
+
? "font-bold text-primary focus:outline-none bg-secondary-foreground"
|
|
231
|
+
: null
|
|
232
|
+
}`}
|
|
233
|
+
onClick={() => setActiveTab(category)}>
|
|
234
|
+
{category}
|
|
235
|
+
</Button>
|
|
236
|
+
</li>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function PostItems({
|
|
241
|
+
currentPosts,
|
|
242
|
+
activeTab,
|
|
243
|
+
blogsPerPage,
|
|
244
|
+
}: {
|
|
245
|
+
currentPosts?: BlogPost[];
|
|
246
|
+
activeTab: string;
|
|
247
|
+
blogsPerPage: number;
|
|
248
|
+
}) {
|
|
249
|
+
if (!currentPosts) return null;
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<div className="w-full px-3 lg:w-3/4">
|
|
253
|
+
{activeTab === "All"
|
|
254
|
+
? currentPosts?.map((post, index) => <PostItem post={post} key={index} />)
|
|
255
|
+
: currentPosts
|
|
256
|
+
?.slice(0, blogsPerPage)
|
|
257
|
+
?.map((post, index) => <PostItem post={post} key={index} />)}
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function PostItem({ post }: { post?: BlogPost }) {
|
|
263
|
+
if (!post) return null;
|
|
264
|
+
return (
|
|
265
|
+
<Flex wrap className="mb-8 lg:mb-6 bg-white shadow rounded-lg">
|
|
266
|
+
<div className="w-full h-full mb-4 lg:mb-0 lg:w-1/4">
|
|
267
|
+
<Image
|
|
268
|
+
className="object-cover w-full h-full overflow-hidden rounded"
|
|
269
|
+
src={`${post?.mainImage}`}
|
|
270
|
+
sizes="100vw"
|
|
271
|
+
width={188}
|
|
272
|
+
height={129}
|
|
273
|
+
alt={`blog-variantD-image-`}
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
<div className="w-full px-3 py-2 lg:w-3/4">
|
|
277
|
+
{post?.title && (
|
|
278
|
+
<Link
|
|
279
|
+
aria-label={post?.title}
|
|
280
|
+
className="mb-1 text-2xl font-bold hover:text-secondary font-heading"
|
|
281
|
+
href={`/${post?.link ?? "page-not-added"}`}>
|
|
282
|
+
{post?.title.length > 25 ? post?.title?.substring(0, 25) + "..." : post?.title}
|
|
283
|
+
</Link>
|
|
284
|
+
)}
|
|
285
|
+
<Flex wrap align="center" gap={1} className="mb-2 text-sm">
|
|
286
|
+
{post?.authors
|
|
287
|
+
? post?.authors?.map((author, index, { length }) => (
|
|
288
|
+
<Flex key={index}>
|
|
289
|
+
<Text className="text-primary">{author?.name}</Text>
|
|
290
|
+
{index + 1 !== length ? <span> , </span> : null}
|
|
291
|
+
</Flex>
|
|
292
|
+
))
|
|
293
|
+
: null}
|
|
294
|
+
{post?.publishedAt && post?.authors ? (
|
|
295
|
+
<span className="mx-2 text-gray-500">•</span>
|
|
296
|
+
) : null}
|
|
297
|
+
{post?.publishedAt ? (
|
|
298
|
+
<Text muted>{format(new Date(post?.publishedAt), " dd MMM, yyyy")}</Text>
|
|
299
|
+
) : null}
|
|
300
|
+
</Flex>
|
|
301
|
+
{post?.excerpt ? (
|
|
302
|
+
<Text muted>
|
|
303
|
+
{post?.excerpt.length > 60 ? post?.excerpt.substring(0, 60) + "..." : post?.excerpt}
|
|
304
|
+
</Text>
|
|
305
|
+
) : null}
|
|
306
|
+
</div>
|
|
307
|
+
</Flex>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
interface PaginationProps {
|
|
312
|
+
blogsPerPage: number;
|
|
313
|
+
totalBlogs: number;
|
|
314
|
+
paginate: (pageNumber: number) => void;
|
|
315
|
+
currentPage: number;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function Pagination({ blogsPerPage, totalBlogs, paginate, currentPage }: PaginationProps) {
|
|
319
|
+
if (!blogsPerPage) return null;
|
|
320
|
+
const pageNumber = [];
|
|
321
|
+
|
|
322
|
+
for (let i = 1; i <= Math.ceil(totalBlogs / blogsPerPage); i++) {
|
|
323
|
+
pageNumber.push(i);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<nav className="mt-4" aria-label="Pagination">
|
|
328
|
+
<ul className="flex space-x-2 justify-end mr-5">
|
|
329
|
+
{pageNumber.map(number => (
|
|
330
|
+
<Button
|
|
331
|
+
variant="unstyled"
|
|
332
|
+
as="button"
|
|
333
|
+
ariaLabel={`Page ${number}`}
|
|
334
|
+
key={number}
|
|
335
|
+
className={`${
|
|
336
|
+
currentPage === number
|
|
337
|
+
? "bg-secondary-foreground text-gray-500"
|
|
338
|
+
: "bg-white hover:bg-secondary-foreground hover:text-gray-500"
|
|
339
|
+
} text-primary font-medium py-2 px-4 border border-primary rounded focus:outline-none`}
|
|
340
|
+
onClick={() => paginate(number)}>
|
|
341
|
+
{number}
|
|
342
|
+
</Button>
|
|
343
|
+
))}
|
|
344
|
+
</ul>
|
|
345
|
+
</nav>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export { Blog_D };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export const useMediaQuery = (width: string) => {
|
|
4
|
+
const [targetReached, setTargetReached] = React.useState(false);
|
|
5
|
+
|
|
6
|
+
const updateTarget = React.useCallback((e: any) => {
|
|
7
|
+
if (e.matches) {
|
|
8
|
+
setTargetReached(true);
|
|
9
|
+
} else {
|
|
10
|
+
setTargetReached(false);
|
|
11
|
+
}
|
|
12
|
+
}, []);
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
const media = window.matchMedia(`(max-width: ${width}px)`);
|
|
16
|
+
media.addEventListener("change", updateTarget);
|
|
17
|
+
|
|
18
|
+
// Check on mount (callback is not called until a change occurs)
|
|
19
|
+
if (media.matches) {
|
|
20
|
+
setTargetReached(true);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return () => media.removeEventListener("change", updateTarget);
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
return targetReached;
|
|
27
|
+
};
|