create-landing-app 0.2.8 → 0.3.1

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.
Files changed (57) hide show
  1. package/dist/prompts.js +7 -3
  2. package/dist/scaffold.js +5 -2
  3. package/package.json +1 -1
  4. package/templates/nextjs/base/.dockerignore +6 -0
  5. package/templates/nextjs/base/.editorconfig +15 -0
  6. package/templates/nextjs/base/.env.example +8 -2
  7. package/templates/nextjs/base/.husky/pre-push +8 -10
  8. package/templates/nextjs/base/CLAUDE.md +169 -0
  9. package/templates/nextjs/base/Dockerfile +3 -9
  10. package/templates/nextjs/base/Makefile +25 -0
  11. package/templates/nextjs/base/app/layout.tsx +6 -9
  12. package/templates/nextjs/base/app/sitemap.ts +15 -0
  13. package/templates/nextjs/base/commitlint.config.mjs +6 -22
  14. package/templates/nextjs/base/components/navs/navbar-mobile.tsx +60 -27
  15. package/templates/nextjs/base/components/navs/navbar.tsx +9 -2
  16. package/templates/nextjs/base/components/ui/checkbox.tsx +26 -0
  17. package/templates/nextjs/base/components/ui/input.tsx +21 -0
  18. package/templates/nextjs/base/components/ui/radio-group.tsx +36 -0
  19. package/templates/nextjs/base/components/ui/select.tsx +139 -0
  20. package/templates/nextjs/base/components/ui/sheet.tsx +139 -0
  21. package/templates/nextjs/base/components/ui/tabs.tsx +53 -0
  22. package/templates/nextjs/base/components/ui/textarea.tsx +20 -0
  23. package/templates/nextjs/base/docker-compose.yml +9 -0
  24. package/templates/nextjs/base/eslint.config.mjs +5 -9
  25. package/templates/nextjs/base/next.config.ts +4 -0
  26. package/templates/nextjs/base/package.json +7 -4
  27. package/templates/nextjs/base/styles/theme.css +2 -0
  28. package/templates/nextjs/base/tsconfig.json +2 -2
  29. package/templates/nextjs/optional/analytics/files/components/analytics.tsx +16 -0
  30. package/templates/nextjs/optional/analytics/files/components/web-vitals.tsx +16 -0
  31. package/templates/nextjs/optional/analytics/inject/app__layout.tsx +7 -0
  32. package/templates/nextjs/optional/analytics/pkg.json +5 -0
  33. package/templates/nextjs/optional/dark-mode/files/components/theme-toggle.tsx +21 -0
  34. package/templates/nextjs/optional/dark-mode/inject/app__layout.tsx +8 -0
  35. package/templates/nextjs/optional/dark-mode/pkg.json +5 -0
  36. package/templates/nextjs/optional/i18n-dict/files/components/navs/navbar-mobile.tsx +60 -26
  37. package/templates/nextjs/optional/i18n-dict/files/components/navs/navbar.tsx +8 -2
  38. package/templates/nextjs/optional/i18n-dict/files/{middleware.ts → proxy.ts} +8 -2
  39. package/templates/nextjs/optional/i18n-dict/inject/app__layout.tsx +34 -0
  40. package/templates/nextjs/optional/sections/blog/files/app/[lang]/blogs/(list)/[category]/main-page.tsx +15 -0
  41. package/templates/nextjs/optional/sections/blog/files/app/[lang]/blogs/(list)/[category]/page.tsx +38 -0
  42. package/templates/nextjs/optional/sections/blog/files/app/[lang]/blogs/(list)/layout.tsx +28 -0
  43. package/templates/nextjs/optional/sections/blog/files/app/[lang]/blogs/detail/[slugNews]/blog-detail-view.tsx +122 -0
  44. package/templates/nextjs/optional/sections/blog/files/app/[lang]/blogs/detail/[slugNews]/page.tsx +73 -0
  45. package/templates/nextjs/optional/sections/blog/files/app/api/blogs/route.ts +14 -0
  46. package/templates/nextjs/optional/sections/blog/files/components/blogs/blog-component.tsx +67 -0
  47. package/templates/nextjs/optional/sections/blog/files/components/blogs/blog-view-desktop.tsx +121 -0
  48. package/templates/nextjs/optional/sections/blog/files/components/blogs/blog-view-mobile.tsx +90 -0
  49. package/templates/nextjs/optional/sections/blog/files/components/navs/layout-blogs.tsx +51 -0
  50. package/templates/nextjs/optional/sections/blog/files/components/sections/blog-section-view.tsx +171 -0
  51. package/templates/nextjs/optional/sections/blog/files/components/sections/blog-section.tsx +13 -174
  52. package/templates/nextjs/optional/sections/blog/files/hooks/use-mobile.ts +19 -0
  53. package/templates/nextjs/optional/sections/blog/files/lib/blog-api.ts +336 -0
  54. package/templates/nextjs/optional/sections/blog/files/lib/sanitize.ts +25 -0
  55. package/templates/nextjs/optional/sections/blog/files/styles/prose.css +40 -0
  56. package/templates/nextjs/optional/sections/blog/inject/constants__common.ts +1 -1
  57. package/templates/nextjs/optional/sections/blog/pkg.json +10 -0
@@ -0,0 +1,51 @@
1
+ "use client";
2
+ import { useRouter, usePathname } from "next/navigation";
3
+ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
4
+
5
+ type Category = { slug: string; name: string };
6
+
7
+ export default function LayoutBlogs({
8
+ categories,
9
+ children,
10
+ }: {
11
+ categories: Category[];
12
+ children: React.ReactNode;
13
+ }) {
14
+ const router = useRouter();
15
+ const pathName = usePathname();
16
+
17
+ // Extract current category from URL — last path segment after /blogs/
18
+ const currentCategory = pathName.split("/blogs/")[1]?.split("/")[0] ?? "all";
19
+
20
+ const handleTabChange = (category: string) => {
21
+ // Replace only the category segment, preserving the rest of the path (e.g. /en/blogs/all)
22
+ const base = pathName.substring(0, pathName.lastIndexOf("/blogs/"));
23
+ router.push(`${base}/blogs/${category}`);
24
+ };
25
+
26
+ return (
27
+ <div className="py-20 px-5">
28
+ <div className="mx-auto max-w-[1200px]">
29
+ {/* Page header */}
30
+ <div className="mb-12">
31
+ <h1 className="mb-4 text-4xl font-bold text-primary">Blog</h1>
32
+ <p className="text-lg text-muted-foreground">Insights, tutorials, and updates from our team.</p>
33
+ </div>
34
+
35
+ {/* Category tabs */}
36
+ <Tabs value={currentCategory} onValueChange={handleTabChange} className="mb-8">
37
+ <TabsList className="w-full justify-start overflow-x-auto">
38
+ <TabsTrigger value="all">All</TabsTrigger>
39
+ {categories.map((cat) => (
40
+ <TabsTrigger key={cat.slug} value={cat.slug}>
41
+ {cat.name}
42
+ </TabsTrigger>
43
+ ))}
44
+ </TabsList>
45
+ </Tabs>
46
+
47
+ {children}
48
+ </div>
49
+ </div>
50
+ );
51
+ }
@@ -0,0 +1,171 @@
1
+ "use client";
2
+ import Link from "next/link";
3
+ import { motion } from "motion/react";
4
+ import { Button } from "@/components/ui/button";
5
+ import type { Blog, BlogCategory } from "@/lib/blog-api";
6
+
7
+ function formatDate(date: string) {
8
+ return new Date(date).toLocaleDateString("en-GB"); // DD/MM/YYYY
9
+ }
10
+
11
+ function CategoryBadges({ categoryIds, categories }: { categoryIds?: string[]; categories: BlogCategory[] }) {
12
+ if (!categoryIds?.length) return null;
13
+ return (
14
+ <div className="flex flex-wrap gap-1">
15
+ {categoryIds.map((id) => (
16
+ <Button key={id} className="h-[22px] w-fit rounded-[4px] bg-primary px-2 text-xs leading-[18px] tracking-[-0.24px] text-primary-foreground hover:bg-primary/80">
17
+ {categories.find((c) => c.id === id)?.name ?? id}
18
+ </Button>
19
+ ))}
20
+ </div>
21
+ );
22
+ }
23
+
24
+ function Thumbnail({ src, alt, className }: { src?: string; alt?: string; className: string }) {
25
+ return src ? (
26
+ <img src={src} className={`h-full w-full object-cover ${className}`} alt={alt ?? ""} loading="lazy" />
27
+ ) : (
28
+ <div className={`h-full w-full bg-muted ${className}`} />
29
+ );
30
+ }
31
+
32
+ // Desktop: featured left + 3 secondary right + optional related grid
33
+ function BlogViewDesktop({ blogs, categories }: { blogs: Blog[]; categories: BlogCategory[] }) {
34
+ if (!blogs.length) return null;
35
+ const [featured, ...rest] = blogs;
36
+ return (
37
+ <motion.div
38
+ viewport={{ once: true }}
39
+ initial={{ opacity: 0, y: 40 }}
40
+ whileInView={{ opacity: 1, y: 0 }}
41
+ transition={{ duration: 1.1 }}
42
+ className="flex flex-col gap-[128px] pb-20"
43
+ >
44
+ <div className="flex justify-between gap-[29px] max-lg:gap-5">
45
+ {/* Featured blog */}
46
+ <div className={`flex flex-col gap-2 ${rest.length === 0 ? "w-full" : "w-1/2"}`}>
47
+ <Link href={`/blogs/detail/${featured.slug}`} className={`w-full ${rest.length === 0 ? "h-[500px]" : "h-[320px]"}`}>
48
+ <Thumbnail src={featured.thumbnail} className="rounded-[16px]" />
49
+ </Link>
50
+ <div className="mt-1">
51
+ <CategoryBadges categoryIds={featured.categoryIds} categories={categories} />
52
+ </div>
53
+ <Link href={`/blogs/detail/${featured.slug}`} className="line-clamp-2 text-xl font-semibold leading-[24px] text-primary hover:underline">
54
+ {featured.name}
55
+ </Link>
56
+ <time dateTime={featured.publishedAt} className="text-sm leading-5 text-primary">
57
+ {formatDate(featured.publishedAt)}
58
+ </time>
59
+ <span className="mt-2 text-sm leading-5 text-primary">{featured.description}</span>
60
+ </div>
61
+
62
+ {/* Secondary blogs */}
63
+ {rest.length > 0 && (
64
+ <div className="flex w-1/2 flex-col gap-[30px]">
65
+ {rest.slice(0, 3).map((blog) => (
66
+ <Link key={blog.slug} href={`/blogs/detail/${blog.slug}`} className="flex gap-[30px] max-xl:gap-6 max-lg:gap-4">
67
+ <div className="h-[147px] w-[191px] flex-shrink-0 max-lg:h-[130px] max-lg:w-[169px]">
68
+ <Thumbnail src={blog.thumbnail} className="rounded-[10px]" />
69
+ </div>
70
+ <div className="flex flex-col gap-2">
71
+ <CategoryBadges categoryIds={blog.categoryIds} categories={categories} />
72
+ <span className="text-xl font-semibold leading-[30px] text-primary hover:underline max-xl:text-lg max-lg:text-base">
73
+ {blog.name}
74
+ </span>
75
+ <time dateTime={blog.publishedAt} className="text-sm leading-5 text-primary">
76
+ {formatDate(blog.publishedAt)}
77
+ </time>
78
+ </div>
79
+ </Link>
80
+ ))}
81
+ </div>
82
+ )}
83
+ </div>
84
+
85
+ {/* Related: 4-col grid for blogs beyond index 4 */}
86
+ {blogs.length > 4 && (
87
+ <div className="flex flex-col gap-[32px]">
88
+ <span className="text-2xl font-semibold leading-8 text-primary">Related blogs</span>
89
+ <div className="grid grid-cols-4 gap-8">
90
+ {blogs.slice(4).map((blog) => (
91
+ <Link key={blog.slug} href={`/blogs/detail/${blog.slug}`} className="flex flex-col gap-[30px]">
92
+ <div className="h-[220px] w-full max-lg:h-[130px]">
93
+ <Thumbnail src={blog.thumbnail} alt={blog.name} className="rounded-[16px]" />
94
+ </div>
95
+ <div className="flex flex-col gap-2">
96
+ <CategoryBadges categoryIds={blog.categoryIds} categories={categories} />
97
+ <span className="text-xl font-semibold leading-[30px] text-primary max-xl:text-lg max-lg:text-base">
98
+ {blog.name}
99
+ </span>
100
+ <time dateTime={blog.publishedAt} className="text-sm leading-5 text-primary">
101
+ {formatDate(blog.publishedAt)}
102
+ </time>
103
+ </div>
104
+ </Link>
105
+ ))}
106
+ </div>
107
+ </div>
108
+ )}
109
+ </motion.div>
110
+ );
111
+ }
112
+
113
+ // Mobile: full-width featured + list of remaining
114
+ function BlogViewMobile({ blogs, categories }: { blogs: Blog[]; categories: BlogCategory[] }) {
115
+ if (!blogs.length) return null;
116
+ const [featured, ...rest] = blogs;
117
+ return (
118
+ <motion.div
119
+ viewport={{ once: true }}
120
+ initial={{ opacity: 0, y: 40 }}
121
+ whileInView={{ opacity: 1, y: 0 }}
122
+ transition={{ duration: 1.1 }}
123
+ className="flex flex-col gap-6 pb-20"
124
+ >
125
+ <div className="flex flex-col gap-2">
126
+ <Link href={`/blogs/detail/${featured.slug}`} className="h-[230px] w-full">
127
+ <Thumbnail src={featured.thumbnail} className="rounded-[16px]" />
128
+ </Link>
129
+ <div className="mt-1">
130
+ <CategoryBadges categoryIds={featured.categoryIds} categories={categories} />
131
+ </div>
132
+ <Link href={`/blogs/detail/${featured.slug}`} className="text-xl font-semibold leading-[30px] text-primary hover:underline max-lg:text-lg">
133
+ {featured.name}
134
+ </Link>
135
+ <time dateTime={featured.publishedAt} className="text-sm leading-5 text-primary">
136
+ {formatDate(featured.publishedAt)}
137
+ </time>
138
+ <span className="mt-2 text-sm leading-5 text-primary">{featured.description}</span>
139
+ </div>
140
+ <div className="flex flex-col gap-6">
141
+ {rest.map((blog) => (
142
+ <Link key={blog.slug} href={`/blogs/detail/${blog.slug}`} className="flex items-start gap-4">
143
+ <div className="h-[91px] w-[118px] flex-shrink-0">
144
+ <Thumbnail src={blog.thumbnail} className="rounded-[10px]" />
145
+ </div>
146
+ <div className="flex flex-col gap-2">
147
+ <CategoryBadges categoryIds={blog.categoryIds} categories={categories} />
148
+ <span className="text-sm font-semibold leading-6 text-primary">{blog.name}</span>
149
+ <time dateTime={blog.publishedAt} className="text-sm leading-5 text-primary">
150
+ {formatDate(blog.publishedAt)}
151
+ </time>
152
+ </div>
153
+ </Link>
154
+ ))}
155
+ </div>
156
+ </motion.div>
157
+ );
158
+ }
159
+
160
+ export function BlogSectionView({ blogs, categories = [] }: { blogs: Blog[]; categories?: BlogCategory[] }) {
161
+ return (
162
+ <>
163
+ <div className="hidden md:block">
164
+ <BlogViewDesktop blogs={blogs} categories={categories} />
165
+ </div>
166
+ <div className="block md:hidden">
167
+ <BlogViewMobile blogs={blogs} categories={categories} />
168
+ </div>
169
+ </>
170
+ );
171
+ }
@@ -1,175 +1,11 @@
1
- "use client";
2
1
  import Link from "next/link";
3
- import { motion } from "motion/react";
4
- import { Button } from "@/components/ui/button";
2
+ import { getBlogs, getBlogCategories } from "@/lib/blog-api";
3
+ import { BlogSectionView } from "./blog-section-view";
5
4
 
6
- type BlogPost = {
7
- slug: string;
8
- title: string;
9
- description: string;
10
- date: string;
11
- category: string;
12
- thumbnail?: string;
13
- };
5
+ // Server component — fetches latest 4 blogs at render time (SSR/SSG)
6
+ export default async function BlogSection() {
7
+ const [blogs, categories] = await Promise.all([getBlogs(4), getBlogCategories()]);
14
8
 
15
- // Replace with your CMS/API data
16
- const BLOG_POSTS: BlogPost[] = [
17
- { slug: "post-1", title: "Getting started with our platform", description: "Learn how to set up your account and launch your first project in minutes.", date: "2024-01-15", category: "Product" },
18
- { slug: "post-2", title: "Best practices for modern web apps", description: "A guide to building scalable, maintainable applications.", date: "2024-02-01", category: "Engineering" },
19
- { slug: "post-3", title: "How we scaled to 10k users", description: "A behind-the-scenes look at the challenges and solutions.", date: "2024-02-20", category: "Growth" },
20
- { slug: "post-4", title: "Designing for accessibility", description: "Why inclusive design benefits everyone and how to do it.", date: "2024-03-05", category: "Design" },
21
- { slug: "post-5", title: "Our open-source journey", description: "What we learned shipping our first open-source library.", date: "2024-03-18", category: "Engineering" },
22
- { slug: "post-6", title: "The future of edge computing", description: "How edge functions are changing how we think about latency.", date: "2024-04-01", category: "Product" },
23
- { slug: "post-7", title: "Team rituals that actually work", description: "Async-first habits that helped our remote team stay aligned.", date: "2024-04-10", category: "Culture" },
24
- ];
25
-
26
- function formatDate(date: string) {
27
- return new Date(date).toLocaleDateString("en-GB"); // DD/MM/YYYY
28
- }
29
-
30
- function CategoryBadge({ label }: { label: string }) {
31
- return (
32
- <Button className="h-[22px] w-fit rounded-[4px] bg-primary text-xs leading-[18px] tracking-[-0.24px] text-primary-foreground hover:bg-primary/80">
33
- {label}
34
- </Button>
35
- );
36
- }
37
-
38
- function Thumbnail({ src, alt, className }: { src?: string; alt?: string; className: string }) {
39
- return src ? (
40
- <img src={src} className={`h-full w-full object-cover ${className}`} alt={alt ?? ""} loading="lazy" />
41
- ) : (
42
- <div className={`h-full w-full bg-muted ${className}`} />
43
- );
44
- }
45
-
46
- // Desktop: featured left + 3 secondary right + optional related grid
47
- function BlogViewDesktop({ posts }: { posts: BlogPost[] }) {
48
- const [featured, ...rest] = posts;
49
- return (
50
- <motion.div
51
- viewport={{ once: true }}
52
- initial={{ opacity: 0, y: 40 }}
53
- whileInView={{ opacity: 1, y: 0 }}
54
- transition={{ duration: 1.1 }}
55
- className="flex flex-col gap-[128px] pb-20"
56
- >
57
- <div className="flex justify-between gap-[29px] max-lg:gap-5">
58
- {/* Featured post */}
59
- <div className={`flex flex-col gap-2 ${rest.length === 0 ? "w-full" : "w-1/2"}`}>
60
- <Link href={`/blog/${featured.slug}`} className={`w-full ${rest.length === 0 ? "h-[500px]" : "h-[320px]"}`}>
61
- <Thumbnail src={featured.thumbnail} className="rounded-[16px]" />
62
- </Link>
63
- <div className="mt-1">
64
- <CategoryBadge label={featured.category} />
65
- </div>
66
- <Link href={`/blog/${featured.slug}`} className="line-clamp-2 text-xl font-semibold leading-[24px] text-primary hover:underline">
67
- {featured.title}
68
- </Link>
69
- <time dateTime={featured.date} className="text-sm leading-5 text-primary">
70
- {formatDate(featured.date)}
71
- </time>
72
- <span className="mt-2 text-sm leading-5 text-primary">{featured.description}</span>
73
- </div>
74
-
75
- {/* Secondary posts */}
76
- {rest.length > 0 && (
77
- <div className="flex w-1/2 flex-col gap-[30px]">
78
- {rest.slice(0, 3).map((post) => (
79
- <Link key={post.slug} href={`/blog/${post.slug}`} className="flex gap-[30px] max-xl:gap-6 max-lg:gap-4">
80
- <div className="h-[147px] w-[191px] flex-shrink-0 max-lg:h-[130px] max-lg:w-[169px]">
81
- <Thumbnail src={post.thumbnail} className="rounded-[10px]" />
82
- </div>
83
- <div className="flex flex-col gap-2">
84
- <CategoryBadge label={post.category} />
85
- <span className="text-xl font-semibold leading-[30px] text-primary hover:underline max-xl:text-lg max-lg:text-base">
86
- {post.title}
87
- </span>
88
- <time dateTime={post.date} className="text-sm leading-5 text-primary">
89
- {formatDate(post.date)}
90
- </time>
91
- </div>
92
- </Link>
93
- ))}
94
- </div>
95
- )}
96
- </div>
97
-
98
- {/* Related: 4-col grid for posts beyond index 4 */}
99
- {posts.length > 4 && (
100
- <div className="flex flex-col gap-[32px]">
101
- <span className="text-2xl font-semibold leading-8 text-primary">Related posts</span>
102
- <div className="grid grid-cols-4 gap-8">
103
- {posts.slice(4).map((post) => (
104
- <Link key={post.slug} href={`/blog/${post.slug}`} className="flex flex-col gap-[30px]">
105
- <div className="h-[220px] w-full max-lg:h-[130px]">
106
- <Thumbnail src={post.thumbnail} alt={post.title} className="rounded-[16px]" />
107
- </div>
108
- <div className="flex flex-col gap-2">
109
- <CategoryBadge label={post.category} />
110
- <span className="text-xl font-semibold leading-[30px] text-primary max-xl:text-lg max-lg:text-base">
111
- {post.title}
112
- </span>
113
- <time dateTime={post.date} className="text-sm leading-5 text-primary">
114
- {formatDate(post.date)}
115
- </time>
116
- </div>
117
- </Link>
118
- ))}
119
- </div>
120
- </div>
121
- )}
122
- </motion.div>
123
- );
124
- }
125
-
126
- // Mobile: full-width featured + horizontal list of remaining
127
- function BlogViewMobile({ posts }: { posts: BlogPost[] }) {
128
- const [featured, ...rest] = posts;
129
- return (
130
- <motion.div
131
- viewport={{ once: true }}
132
- initial={{ opacity: 0, y: 40 }}
133
- whileInView={{ opacity: 1, y: 0 }}
134
- transition={{ duration: 1.1 }}
135
- className="flex flex-col gap-6 pb-20"
136
- >
137
- <div className="flex flex-col gap-2">
138
- <Link href={`/blog/${featured.slug}`} className="h-[230px] w-full">
139
- <Thumbnail src={featured.thumbnail} className="rounded-[16px]" />
140
- </Link>
141
- <div className="mt-1">
142
- <CategoryBadge label={featured.category} />
143
- </div>
144
- <Link href={`/blog/${featured.slug}`} className="text-xl font-semibold leading-[30px] text-primary hover:underline max-lg:text-lg">
145
- {featured.title}
146
- </Link>
147
- <time dateTime={featured.date} className="text-sm leading-5 text-primary">
148
- {formatDate(featured.date)}
149
- </time>
150
- <span className="mt-2 text-sm leading-5 text-primary">{featured.description}</span>
151
- </div>
152
- <div className="flex flex-col gap-6">
153
- {rest.map((post) => (
154
- <Link key={post.slug} href={`/blog/${post.slug}`} className="flex items-start gap-4">
155
- <div className="h-[91px] w-[118px] flex-shrink-0">
156
- <Thumbnail src={post.thumbnail} className="rounded-[10px]" />
157
- </div>
158
- <div className="flex flex-col gap-2">
159
- <CategoryBadge label={post.category} />
160
- <span className="text-sm font-semibold leading-6 text-primary">{post.title}</span>
161
- <time dateTime={post.date} className="text-sm leading-5 text-primary">
162
- {formatDate(post.date)}
163
- </time>
164
- </div>
165
- </Link>
166
- ))}
167
- </div>
168
- </motion.div>
169
- );
170
- }
171
-
172
- export default function BlogSection() {
173
9
  return (
174
10
  <section id="blog" className="py-24">
175
11
  <div className="content-container">
@@ -179,11 +15,14 @@ export default function BlogSection() {
179
15
  Insights, tutorials, and updates from our team.
180
16
  </p>
181
17
  </div>
182
- <div className="hidden md:block">
183
- <BlogViewDesktop posts={BLOG_POSTS} />
184
- </div>
185
- <div className="block md:hidden">
186
- <BlogViewMobile posts={BLOG_POSTS} />
18
+ <BlogSectionView blogs={blogs} categories={categories} />
19
+ <div className="mt-10 text-center">
20
+ <Link
21
+ href="/blogs/all"
22
+ className="inline-flex items-center gap-1 text-sm font-medium text-primary underline-offset-4 hover:underline"
23
+ >
24
+ View all blogs →
25
+ </Link>
187
26
  </div>
188
27
  </div>
189
28
  </section>
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+
4
+ const MOBILE_BREAKPOINT = 768;
5
+
6
+ export function useIsMobile() {
7
+ const [isMobile, setIsMobile] = useState(
8
+ () => typeof window !== "undefined" && window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches
9
+ );
10
+
11
+ useEffect(() => {
12
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
13
+ const onChange = () => setIsMobile(mql.matches);
14
+ mql.addEventListener("change", onChange);
15
+ return () => mql.removeEventListener("change", onChange);
16
+ }, []);
17
+
18
+ return isMobile;
19
+ }