@stackshift-ui/blog 6.0.5 → 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/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>&nbsp;,&nbsp;</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
+ };
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ "use client";
2
+
3
+ // component exports
4
+ export * from "./blog";
5
+ export * from "./blog_a";
6
+ export * from "./blog_b";
7
+ export * from "./blog_c";
8
+ export * from "./blog_d";