@stackshift-ui/blog 6.0.5 → 6.0.7-beta.0
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/blog_a.js +1 -1
- package/dist/blog_a.mjs +1 -1
- package/dist/blog_b.js +1 -1
- package/dist/blog_b.mjs +1 -1
- package/dist/blog_c.js +1 -1
- package/dist/blog_c.mjs +1 -1
- package/dist/blog_d.js +1 -1
- package/dist/blog_d.mjs +1 -1
- package/dist/chunk-4JEFLTN2.mjs +1 -0
- package/dist/chunk-7K4AC7BM.mjs +1 -0
- package/dist/chunk-BYE6WLYT.mjs +1 -0
- package/dist/chunk-HRT2UV7A.mjs +1 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/types.d.ts +2 -0
- package/package.json +15 -14
- 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 +160 -0
- package/src/blog_c.tsx +142 -0
- package/src/blog_d.tsx +350 -0
- package/src/hooks/useMediaQuery.ts +27 -0
- package/src/index.ts +8 -0
- package/src/types.ts +414 -0
- package/dist/chunk-2USNC6WZ.mjs +0 -1
- package/dist/chunk-OV72XAML.mjs +0 -1
- package/dist/chunk-XEJIKOLC.mjs +0 -1
- package/dist/chunk-YM6QOMEL.mjs +0 -1
- /package/dist/{chunk-VDHKVAJM.mjs → chunk-N5BQFWOH.mjs} +0 -0
package/src/blog_b.tsx
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Button } from "@stackshift-ui/button";
|
|
2
|
+
import { Container } from "@stackshift-ui/container";
|
|
3
|
+
import { Flex } from "@stackshift-ui/flex";
|
|
4
|
+
import { Heading } from "@stackshift-ui/heading";
|
|
5
|
+
import { Image } from "@stackshift-ui/image";
|
|
6
|
+
import { Link } from "@stackshift-ui/link";
|
|
7
|
+
import { Section } from "@stackshift-ui/section";
|
|
8
|
+
import { Text } from "@stackshift-ui/text";
|
|
9
|
+
import { format } from "date-fns";
|
|
10
|
+
import { BlogProps } from ".";
|
|
11
|
+
import { useMediaQuery } from "./hooks/useMediaQuery";
|
|
12
|
+
import { BlogPost, LabeledRoute } from "./types";
|
|
13
|
+
|
|
14
|
+
export default function Blog_B({ subtitle, title, posts, primaryButton }: BlogProps) {
|
|
15
|
+
const blogsPerPage = 5;
|
|
16
|
+
const count = 0;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Section className="py-20 bg-background">
|
|
20
|
+
<Container maxWidth={1280}>
|
|
21
|
+
<SubtitleAndTitle subtitle={subtitle} title={title} />
|
|
22
|
+
<BlogPosts posts={posts} count={count} blogsPerPage={blogsPerPage} />
|
|
23
|
+
<PrimaryButton primaryButton={primaryButton} />
|
|
24
|
+
</Container>
|
|
25
|
+
</Section>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function SubtitleAndTitle({ subtitle, title }: { subtitle?: string; title?: string }) {
|
|
30
|
+
return (
|
|
31
|
+
<div className="w-full mb-16 text-center">
|
|
32
|
+
{subtitle ? (
|
|
33
|
+
<Text weight="bold" className="text-secondary">
|
|
34
|
+
{subtitle}
|
|
35
|
+
</Text>
|
|
36
|
+
) : null}
|
|
37
|
+
{title ? <Heading fontSize="3xl">{title}</Heading> : null}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function BlogPosts({
|
|
43
|
+
posts,
|
|
44
|
+
count,
|
|
45
|
+
blogsPerPage,
|
|
46
|
+
}: {
|
|
47
|
+
posts?: BlogPost[];
|
|
48
|
+
count: number;
|
|
49
|
+
blogsPerPage: number;
|
|
50
|
+
}) {
|
|
51
|
+
return (
|
|
52
|
+
<Flex wrap justify="center" className="mb-16" gap={4}>
|
|
53
|
+
<div className="w-full lg:w-[45%]">
|
|
54
|
+
{posts
|
|
55
|
+
?.slice(count, count + 1)
|
|
56
|
+
.map((post, key) => <BlogItem size="lg" post={post} key={key} />)}
|
|
57
|
+
</div>
|
|
58
|
+
<Flex wrap className="w-full lg:w-[45%]" gap={4}>
|
|
59
|
+
{posts?.slice(count + 1, blogsPerPage).map((post, key) => (
|
|
60
|
+
<div className="w-full lg:basis-[45%]" key={key}>
|
|
61
|
+
<BlogItem post={post} size="sm" key={key} />
|
|
62
|
+
</div>
|
|
63
|
+
))}
|
|
64
|
+
</Flex>
|
|
65
|
+
</Flex>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function BlogItem({ post, size, key }: { post: BlogPost; size?: string; key: number }) {
|
|
70
|
+
const breakpoints = useMediaQuery("1024");
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="overflow-hidden rounded-md shadow">
|
|
74
|
+
{post?.mainImage ? (
|
|
75
|
+
<ImageContainer post={post} size={size} breakpoints={breakpoints} key={key} />
|
|
76
|
+
) : null}
|
|
77
|
+
<div className="p-6 bg-white flex flex-col justify-between" style={{ height: "295px" }}>
|
|
78
|
+
<div>
|
|
79
|
+
{post?.publishedAt ? (
|
|
80
|
+
<Text muted className="text-sm">
|
|
81
|
+
{format(new Date(post.publishedAt), " dd MMM, yyyy")}
|
|
82
|
+
</Text>
|
|
83
|
+
) : null}
|
|
84
|
+
{post?.title ? (
|
|
85
|
+
<Heading type="h4" className="my-2">
|
|
86
|
+
{post.title.length > 25 ? `${post.title.substring(0, 25)}...` : post.title}
|
|
87
|
+
</Heading>
|
|
88
|
+
) : null}
|
|
89
|
+
{post?.excerpt ? (
|
|
90
|
+
<Text muted className="mb-6 text-justify">
|
|
91
|
+
{post.excerpt.length > 41 ? `${post.excerpt.substring(0, 41)}...` : post.excerpt}
|
|
92
|
+
</Text>
|
|
93
|
+
) : null}
|
|
94
|
+
</div>
|
|
95
|
+
{post?.link ? (
|
|
96
|
+
<Link
|
|
97
|
+
aria-label="View Blog Post"
|
|
98
|
+
className="font-bold text-primary hover:text-secondary"
|
|
99
|
+
href={`/${post?.link}`}>
|
|
100
|
+
View Blog Post
|
|
101
|
+
</Link>
|
|
102
|
+
) : null}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function ImageContainer({
|
|
109
|
+
post,
|
|
110
|
+
size,
|
|
111
|
+
breakpoints,
|
|
112
|
+
key,
|
|
113
|
+
}: {
|
|
114
|
+
post: BlogPost;
|
|
115
|
+
size?: string;
|
|
116
|
+
breakpoints: boolean;
|
|
117
|
+
key: number;
|
|
118
|
+
}) {
|
|
119
|
+
return (
|
|
120
|
+
<>
|
|
121
|
+
{breakpoints ? (
|
|
122
|
+
<Image
|
|
123
|
+
className="object-cover w-full overflow-hidden"
|
|
124
|
+
src={`${post.mainImage}`}
|
|
125
|
+
sizes="100vw"
|
|
126
|
+
style={{ width: "100%", height: "auto", objectFit: "cover" }}
|
|
127
|
+
width={271}
|
|
128
|
+
height={248}
|
|
129
|
+
alt={post?.mainImage ?? `blog-variantB-image-${key}`}
|
|
130
|
+
/>
|
|
131
|
+
) : (
|
|
132
|
+
<div className={`${size === "lg" ? "h-[44.5rem]" : "h-[12.5rem]"}`}>
|
|
133
|
+
<Image
|
|
134
|
+
className="object-cover w-full overflow-hidden rounded-t-md"
|
|
135
|
+
src={`${post.mainImage}`}
|
|
136
|
+
sizes="100vw"
|
|
137
|
+
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
|
138
|
+
width={271}
|
|
139
|
+
height={248}
|
|
140
|
+
alt={`blog-variantB-image-${post.title}`}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function PrimaryButton({ primaryButton }: { primaryButton?: LabeledRoute }) {
|
|
149
|
+
if (!primaryButton?.label) return null;
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div className="text-center">
|
|
153
|
+
<Button as="link" link={primaryButton} ariaLabel={primaryButton?.label}>
|
|
154
|
+
{primaryButton?.label}
|
|
155
|
+
</Button>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export { Blog_B };
|
package/src/blog_c.tsx
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Badge } from "@stackshift-ui/badge";
|
|
2
|
+
import { Button } from "@stackshift-ui/button";
|
|
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 { Link } from "@stackshift-ui/link";
|
|
8
|
+
import { Section } from "@stackshift-ui/section";
|
|
9
|
+
import { Text } from "@stackshift-ui/text";
|
|
10
|
+
import { format } from "date-fns";
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { BlogProps } from ".";
|
|
13
|
+
import { useMediaQuery } from "./hooks/useMediaQuery";
|
|
14
|
+
import { BlogPost, LabeledRoute } from "./types";
|
|
15
|
+
|
|
16
|
+
export default function Blog_C({ subtitle, title, posts, primaryButton }: BlogProps) {
|
|
17
|
+
let blogsPerPage = 3;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Section className="py-20 bg-background">
|
|
21
|
+
<Container maxWidth={1280}>
|
|
22
|
+
<Flex align="center" justify="between" className="flex-col mb-16 md:flex-row" gap={4}>
|
|
23
|
+
<SubtitleAndTitleText subtitle={subtitle} title={title} />
|
|
24
|
+
<PrimaryButton primaryButton={primaryButton} />
|
|
25
|
+
</Flex>
|
|
26
|
+
<BlogPosts posts={posts} blogsPerPage={blogsPerPage} />
|
|
27
|
+
<PrimaryButton primaryButton={primaryButton} />
|
|
28
|
+
</Container>
|
|
29
|
+
</Section>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function SubtitleAndTitleText({ subtitle, title }: { subtitle?: string; title?: string }) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="text-center md:text-left">
|
|
36
|
+
{subtitle ? (
|
|
37
|
+
<Text weight="bold" className="text-primary">
|
|
38
|
+
{subtitle}
|
|
39
|
+
</Text>
|
|
40
|
+
) : null}
|
|
41
|
+
{title ? <Heading fontSize="3xl">{title}</Heading> : null}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function BlogPosts({ posts, blogsPerPage }: { posts?: BlogPost[]; blogsPerPage?: number }) {
|
|
47
|
+
if (!posts) return null;
|
|
48
|
+
return (
|
|
49
|
+
<div>
|
|
50
|
+
{posts?.slice(0, blogsPerPage)?.map((post, key) => (
|
|
51
|
+
<div className="flex flex-wrap mb-8 overflow-hidden rounded-md shadow" key={key}>
|
|
52
|
+
<BlogItem
|
|
53
|
+
post={post}
|
|
54
|
+
className={`${key % 2 === 0 ? "flex-row" : "flex-row-reverse"}`}
|
|
55
|
+
key={key}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function BlogItem({ post, className, key }: { key: number; post: BlogPost; className?: string }) {
|
|
64
|
+
const breakpoints = useMediaQuery("1100");
|
|
65
|
+
const maxExcerptLength = breakpoints ? 70 : 200;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Flex wrap className={`bg-white overflow-hidden rounded-lg shadow w-full ${className}`}>
|
|
69
|
+
{post?.mainImage && (
|
|
70
|
+
<Image
|
|
71
|
+
className="object-cover w-full h-auto rounded-l lg:w-1/2"
|
|
72
|
+
src={`${post?.mainImage}`}
|
|
73
|
+
sizes="100vw"
|
|
74
|
+
width={554}
|
|
75
|
+
height={416}
|
|
76
|
+
alt={post?.alt ?? `blog-variantC-image-${key}`}
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
79
|
+
<div className="w-full px-6 py-6 rounded-r lg:w-1/2 lg:pt-10">
|
|
80
|
+
<Flex gap={2}>
|
|
81
|
+
{post?.categories &&
|
|
82
|
+
post?.categories?.map((category, index) => (
|
|
83
|
+
<Badge className=" bg-secondary-foreground text-primary" key={index}>
|
|
84
|
+
{category?.title}
|
|
85
|
+
</Badge>
|
|
86
|
+
))}
|
|
87
|
+
</Flex>
|
|
88
|
+
|
|
89
|
+
{post?.publishedAt && (
|
|
90
|
+
<Text muted className="m-1">
|
|
91
|
+
{format(new Date(post?.publishedAt), " dd MMM, yyyy")}
|
|
92
|
+
</Text>
|
|
93
|
+
)}
|
|
94
|
+
{post?.title && (
|
|
95
|
+
<Heading className="my-4" type="h3">
|
|
96
|
+
{post?.title?.length > 40 ? post?.title?.substring(0, 40) + "..." : post?.title}
|
|
97
|
+
</Heading>
|
|
98
|
+
)}
|
|
99
|
+
{post?.authors && (
|
|
100
|
+
<div className="flex mb-10 flex-wrap">
|
|
101
|
+
<span className="italic text-primary">By </span>
|
|
102
|
+
{post?.authors?.map((author, index, { length }) => (
|
|
103
|
+
<>
|
|
104
|
+
<Text className="italic text-primary">{author?.name}</Text>
|
|
105
|
+
{index + 1 !== length ? <span> , </span> : null}
|
|
106
|
+
</>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
{post?.excerpt && (
|
|
111
|
+
<Text muted className="mb-6 leading-loose text-justify">
|
|
112
|
+
{post?.excerpt?.length > maxExcerptLength
|
|
113
|
+
? post?.excerpt?.substring(0, maxExcerptLength) + "..."
|
|
114
|
+
: post?.excerpt}
|
|
115
|
+
</Text>
|
|
116
|
+
)}
|
|
117
|
+
{post?.link && (
|
|
118
|
+
<Link
|
|
119
|
+
aria-label="View Blog Post"
|
|
120
|
+
className="font-bold text-primary hover:text-primary-foreground"
|
|
121
|
+
href={`/${post?.link}`}>
|
|
122
|
+
View Blog Post
|
|
123
|
+
</Link>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
</Flex>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function PrimaryButton({ primaryButton }: { primaryButton?: LabeledRoute }) {
|
|
131
|
+
if (!primaryButton?.label) return null;
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<React.Fragment>
|
|
135
|
+
<Button as="link" link={primaryButton} ariaLabel={primaryButton?.label}>
|
|
136
|
+
{primaryButton?.label}
|
|
137
|
+
</Button>
|
|
138
|
+
</React.Fragment>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { Blog_C };
|
package/src/blog_d.tsx
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
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 border 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 border-r 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="md">
|
|
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
|
+
|
|
265
|
+
return (
|
|
266
|
+
<Flex wrap className="mb-8 lg:mb-6 bg-white shadow rounded-lg">
|
|
267
|
+
<div className="w-full h-full mb-4 lg:mb-0 lg:w-1/4">
|
|
268
|
+
<Image
|
|
269
|
+
className="object-cover w-full h-full overflow-hidden rounded"
|
|
270
|
+
src={`${post?.mainImage}`}
|
|
271
|
+
sizes="100vw"
|
|
272
|
+
width={188}
|
|
273
|
+
height={129}
|
|
274
|
+
alt={post?.alt ?? `blog-variantD-image-${post?.title}`}
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
<div className="w-full px-3 py-2 lg:w-3/4">
|
|
278
|
+
{post?.title && (
|
|
279
|
+
<Link
|
|
280
|
+
aria-label={post?.title}
|
|
281
|
+
className="mb-1 text-2xl font-bold hover:text-secondary font-heading"
|
|
282
|
+
href={`/${post?.link ?? "page-not-added"}`}>
|
|
283
|
+
{post?.title.length > 25 ? post?.title?.substring(0, 25) + "..." : post?.title}
|
|
284
|
+
</Link>
|
|
285
|
+
)}
|
|
286
|
+
<Flex wrap align="center" gap={1} className="mb-2 text-sm">
|
|
287
|
+
{post?.authors
|
|
288
|
+
? post?.authors?.map((author, index, { length }) => (
|
|
289
|
+
<Flex key={index}>
|
|
290
|
+
<Text className="text-primary">{author?.name}</Text>
|
|
291
|
+
{index + 1 !== length ? <span> , </span> : null}
|
|
292
|
+
</Flex>
|
|
293
|
+
))
|
|
294
|
+
: null}
|
|
295
|
+
{post?.publishedAt && post?.authors ? (
|
|
296
|
+
<span className="mx-2 text-gray-500">•</span>
|
|
297
|
+
) : null}
|
|
298
|
+
{post?.publishedAt ? (
|
|
299
|
+
<Text muted>{format(new Date(post?.publishedAt), " dd MMM, yyyy")}</Text>
|
|
300
|
+
) : null}
|
|
301
|
+
</Flex>
|
|
302
|
+
{post?.excerpt ? (
|
|
303
|
+
<Text muted>
|
|
304
|
+
{post?.excerpt.length > 60 ? post?.excerpt.substring(0, 60) + "..." : post?.excerpt}
|
|
305
|
+
</Text>
|
|
306
|
+
) : null}
|
|
307
|
+
</div>
|
|
308
|
+
</Flex>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
interface PaginationProps {
|
|
313
|
+
blogsPerPage: number;
|
|
314
|
+
totalBlogs: number;
|
|
315
|
+
paginate: (pageNumber: number) => void;
|
|
316
|
+
currentPage: number;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function Pagination({ blogsPerPage, totalBlogs, paginate, currentPage }: PaginationProps) {
|
|
320
|
+
if (!blogsPerPage) return null;
|
|
321
|
+
const pageNumber = [];
|
|
322
|
+
|
|
323
|
+
for (let i = 1; i <= Math.ceil(totalBlogs / blogsPerPage); i++) {
|
|
324
|
+
pageNumber.push(i);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return (
|
|
328
|
+
<nav className="mt-4" aria-label="Pagination">
|
|
329
|
+
<ul className="flex space-x-2 justify-end mr-5">
|
|
330
|
+
{pageNumber.map(number => (
|
|
331
|
+
<Button
|
|
332
|
+
variant="unstyled"
|
|
333
|
+
as="button"
|
|
334
|
+
ariaLabel={`Page ${number}`}
|
|
335
|
+
key={number}
|
|
336
|
+
className={`${
|
|
337
|
+
currentPage === number
|
|
338
|
+
? "bg-secondary-foreground text-gray-500"
|
|
339
|
+
: "bg-white hover:bg-secondary-foreground hover:text-gray-500"
|
|
340
|
+
} text-primary font-medium py-2 px-4 border border-primary rounded-global focus:outline-none`}
|
|
341
|
+
onClick={() => paginate(number)}>
|
|
342
|
+
{number}
|
|
343
|
+
</Button>
|
|
344
|
+
))}
|
|
345
|
+
</ul>
|
|
346
|
+
</nav>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
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
|
+
};
|