create-next-mdx-blog-app 2.1.2 → 2.1.4

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 (31) hide show
  1. package/README.md +11 -2
  2. package/package.json +1 -1
  3. package/scripts/sql/DDL/alterTable.sql +0 -2
  4. package/scripts/sql/DDL/createIncrementViewCountFunction.sql +19 -0
  5. package/scripts/sql/DDL/createViewCountsTable.sql +26 -0
  6. package/src/app/api/views/[slug]/route.ts +19 -0
  7. package/src/app/sample-blog-post-page/page.tsx +10 -2
  8. package/src/components/ArticleAuthorBio.tsx +1 -1
  9. package/src/components/ArticleCoverImage.tsx +1 -1
  10. package/src/components/ArticleHeader.tsx +8 -1
  11. package/src/components/BackToTopButton.tsx +34 -0
  12. package/src/components/CodeSandboxClient.tsx +1 -1
  13. package/src/components/CodeSandboxFeaturesSection.tsx +1 -1
  14. package/src/components/DynamicArticle.tsx +13 -6
  15. package/src/components/MDXRemoteArticle.tsx +1 -1
  16. package/src/components/ReadingProgressBar.tsx +34 -0
  17. package/src/components/SandpackEditor.tsx +2 -2
  18. package/src/components/StaticArticle.tsx +1 -1
  19. package/src/components/ViewCounter.tsx +32 -0
  20. package/src/components/chat/chat-input.tsx +3 -3
  21. package/src/components/chat/chat-interface.tsx +1 -1
  22. package/src/components/chat/chat-messages.tsx +1 -1
  23. package/src/components/chat/chat-tool-result.tsx +1 -1
  24. package/src/components/customMDXComponents/CodeBlock.tsx +2 -2
  25. package/src/components/customMDXComponents/GistCodeBlock.tsx +1 -1
  26. package/src/components/customMDXComponents/GistCopyButton.tsx +2 -2
  27. package/src/components/customMDXComponents/GitHubGist.tsx +1 -1
  28. package/src/components/customMDXComponents/MDXImage.tsx +1 -1
  29. package/src/utils/functions/index.ts +1 -0
  30. package/src/utils/functions/rpc/incrementViewCount.ts +19 -0
  31. package/src/utils/functions/rpc/index.ts +1 -0
package/README.md CHANGED
@@ -59,7 +59,7 @@ npx create-next-mdx-blog-app .
59
59
  ### Package Information
60
60
 
61
61
  - **Package Name**: `create-next-mdx-blog-app`
62
- - **Version**: `2.1.2`
62
+ - **Version**: `2.1.4`
63
63
  - **License**: MIT
64
64
  - **Homepage**: [https://www.npmjs.com/package/create-next-mdx-blog-app](https://www.npmjs.com/package/create-next-mdx-blog-app/)
65
65
 
@@ -164,6 +164,15 @@ Images are displayed inside a styled container with a **green glow border** and
164
164
  ### Article Header Actions
165
165
  A **Copy Link** button (`src/components/CopyLinkButton.tsx`) is rendered in the article header, writing the current page URL to the clipboard on click and displaying a brief "Copied!" confirmation via icon swap. **Social Share** buttons (`src/components/SocialShareButtons.tsx`) sit alongside it, opening pre-filled share dialogs for X (Twitter), LinkedIn, and Reddit in a new tab using the article title and URL.
166
166
 
167
+ ### Reading Progress Bar
168
+ A thin green bar (`src/components/ReadingProgressBar.tsx`) is fixed to the top of the viewport on all article pages, growing from left to right as the reader scrolls through the article. It uses a passive scroll listener for zero performance impact and includes `role="progressbar"` ARIA attributes for accessibility.
169
+
170
+ ### Back to Top Button
171
+ A floating circular button (`src/components/BackToTopButton.tsx`) appears in the bottom-right corner of the screen once the reader has scrolled more than 400px down the page. Clicking it smoothly scrolls back to the top. The button is hidden when not needed and matches the green colour scheme of the rest of the UI.
172
+
173
+ ### View Counter
174
+ Each article page displays a live view count stored in a dedicated `view_counts` Supabase table, incremented atomically via the `increment_view_count` RPC function. Dynamic pages increment server-side at render time; static pages use a `ViewCounter` client component that calls the `/api/views/[slug]` Route Handler after hydration.
175
+
167
176
  ## 🖥️ Code Sandbox
168
177
  The project includes an interactive in-browser code execution environment powered by **Sandpack** (<b>route</b>: `/code-sandbox`).
169
178
 
@@ -260,7 +269,7 @@ docker run -e SUPABASE_URL=your_supabase_url \ -e SUPABASE_ANON_KEY=your_supabas
260
269
  ``
261
270
 
262
271
  ## 🔄 CRUD Operations and Supabase Actions
263
- Implementation of the CRUD operation functions is stored in the `/src/utils/functions/crud` directory. This includes functions for creating, reading, updating, and deleting articles in the Supabase database.
272
+ Implementation of the CRUD operation functions is stored in the `/src/utils/functions/crud` directory. This includes functions for creating, reading, updating, and deleting articles in the Supabase database. Supabase RPC functions (such as `incrementViewCount`) are kept separately in `/src/utils/functions/rpc`.
264
273
 
265
274
  The `article-manager.ts` file utilizes these CRUD functions to successfully perform the desired actions.
266
275
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-next-mdx-blog-app",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "bin": {
5
5
  "create-next-mdx-blog-app": "./bin/create.js"
6
6
  },
@@ -1,8 +1,6 @@
1
1
  -- Alter table properties such as enabling RLS (Row-Level Security)
2
2
  ALTER TABLE Article ENABLE ROW LEVEL SECURITY;
3
3
 
4
- -- Add and use more here..
5
-
6
4
  -- Insert policy added to Article table
7
5
  CREATE POLICY "Allow insert on Article"
8
6
  ON "Article"
@@ -0,0 +1,19 @@
1
+ -- Creates the increment_view_count RPC function used to atomically upsert
2
+ -- and increment the count for a given slug in the view_counts table.
3
+ -- Works for both static and dynamic article pages.
4
+ -- Run once in the Supabase SQL editor before using the view counter feature.
5
+ CREATE OR REPLACE FUNCTION increment_view_count(article_slug TEXT)
6
+ RETURNS INTEGER
7
+ LANGUAGE plpgsql
8
+ AS $$
9
+ DECLARE
10
+ new_count INTEGER;
11
+ BEGIN
12
+ INSERT INTO view_counts (slug, count)
13
+ VALUES (article_slug, 1)
14
+ ON CONFLICT (slug) DO UPDATE
15
+ SET count = view_counts.count + 1
16
+ RETURNING count INTO new_count;
17
+ RETURN new_count;
18
+ END;
19
+ $$;
@@ -0,0 +1,26 @@
1
+ -- Creates a dedicated view_counts table for tracking article view counts.
2
+ -- Decoupled from the Article table so both static and dynamic articles
3
+ -- can have their views tracked regardless of whether a full Article record exists.
4
+ CREATE TABLE view_counts (
5
+ slug VARCHAR PRIMARY KEY,
6
+ count INTEGER DEFAULT 0
7
+ );
8
+
9
+ -- Enable Row-Level Security
10
+ ALTER TABLE view_counts ENABLE ROW LEVEL SECURITY;
11
+
12
+ -- Allow select, insert and update for view count tracking
13
+ CREATE POLICY "Allow select on view_counts"
14
+ ON view_counts
15
+ FOR SELECT
16
+ USING (true);
17
+
18
+ CREATE POLICY "Allow insert on view_counts"
19
+ ON view_counts
20
+ FOR INSERT
21
+ WITH CHECK (true);
22
+
23
+ CREATE POLICY "Allow update on view_counts"
24
+ ON view_counts
25
+ FOR UPDATE
26
+ USING (true);
@@ -0,0 +1,19 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { incrementViewCount } from "@/utils/functions";
3
+
4
+ // Route Handler — increments the view count for a given article slug and returns the new count.
5
+ // Called client-side by the ViewCounter component on SSG pages.
6
+ export async function POST(
7
+ _req: NextRequest,
8
+ { params }: { params: Promise<{ slug: string }> }
9
+ ): Promise<NextResponse> {
10
+ const { slug } = await params;
11
+
12
+ if (!slug || typeof slug !== "string" || slug.trim() === "") {
13
+ return NextResponse.json({ error: "Invalid slug" }, { status: 400 });
14
+ }
15
+
16
+ const count = await incrementViewCount(slug.trim());
17
+
18
+ return NextResponse.json({ count });
19
+ }
@@ -3,6 +3,9 @@ import ArticleAuthorBio from "@/components/ArticleAuthorBio";
3
3
  import ArticleHeader from "@/components/ArticleHeader";
4
4
  import { ArticleAuthorInfoList, ArticleHeaderInfoList } from "@/utils/constants";
5
5
  import StaticArticle from "@/components/StaticArticle";
6
+ import ReadingProgressBar from "@/components/ReadingProgressBar";
7
+ import BackToTopButton from "@/components/BackToTopButton";
8
+ import ViewCounter from "@/components/ViewCounter";
6
9
  import type { Metadata } from 'next';
7
10
 
8
11
  // Generate metadata for the Sample Blog Post page
@@ -21,10 +24,15 @@ export const metadata: Metadata = {
21
24
  // Utilizes the Static Article custom component
22
25
  const SampleBlogPostPage = () => {
23
26
  return (
24
- <div className="min-h-screen flex flex-col bg-black">
27
+ <div className="min-h-screen flex flex-col bg-black">
28
+ <ReadingProgressBar />
29
+ <BackToTopButton />
25
30
  <main className="flex-grow px-4 py-8">
26
31
  <div className="max-w-4xl mx-auto">
27
- <ArticleHeader articleHeaderInformation={ArticleHeaderInfoList} />
32
+ <ArticleHeader
33
+ articleHeaderInformation={ArticleHeaderInfoList}
34
+ viewCounterSlot={<ViewCounter slug="sample-blog-post-page" />}
35
+ />
28
36
  <StaticArticle />
29
37
  <ArticleAuthorBio authorInformation={ArticleAuthorInfoList} />
30
38
  </div>
@@ -5,7 +5,7 @@ import Image from "next/image";
5
5
  import type { ArticleAuthorInfoType } from "@/utils/types";
6
6
 
7
7
  // Article Author Bio Section component
8
- export default function ArticleAuthorBio({ authorInformation }: { authorInformation: ArticleAuthorInfoType }) {
8
+ export default function ArticleAuthorBio({ authorInformation }: { authorInformation: ArticleAuthorInfoType }): React.JSX.Element {
9
9
  return (
10
10
  <div className="glass-card p-4 sm:p-6 mb-8 sm:mb-12">
11
11
  <div className="flex flex-col sm:flex-row items-center sm:items-start space-y-4 sm:space-y-0">
@@ -2,7 +2,7 @@ import Image from "next/image";
2
2
  import type { ArticleCoverImageInfoType } from "@/utils/types";
3
3
 
4
4
  // Article Cover Image Section component
5
- export default function ArticleCoverImage(coverImageInformation: ArticleCoverImageInfoType) {
5
+ export default function ArticleCoverImage(coverImageInformation: ArticleCoverImageInfoType): React.JSX.Element {
6
6
  return (
7
7
  <div className="rounded-lg overflow-hidden mb-4">
8
8
  <Image
@@ -5,9 +5,10 @@ import CopyLinkButton from "./CopyLinkButton";
5
5
  import SocialShareButtons from "./SocialShareButtons";
6
6
  import type { ArticleHeaderInfoType } from "@/utils/types";
7
7
  import Image from "next/image";
8
+ import { Eye } from "lucide-react";
8
9
 
9
10
  // Article Header custom component
10
- export default function ArticleHeader({ articleHeaderInformation } : { articleHeaderInformation: ArticleHeaderInfoType }) {
11
+ export default function ArticleHeader({ articleHeaderInformation, viewCount, viewCounterSlot } : { articleHeaderInformation: ArticleHeaderInfoType; viewCount?: number; viewCounterSlot?: React.ReactNode }): React.JSX.Element {
11
12
  return (
12
13
  <div className="mb-8">
13
14
  <Badge className="mb-3 bg-green-900/60 text-green-100 border border-green-500/50">
@@ -33,6 +34,12 @@ export default function ArticleHeader({ articleHeaderInformation } : { articleHe
33
34
  month: 'long',
34
35
  day: 'numeric'
35
36
  })} • { articleHeaderInformation.articleReadingTime }
37
+ { viewCount !== undefined && (
38
+ <span className="inline-flex items-center gap-1 ml-2">
39
+ • <Eye className="inline h-3 w-3" /> { viewCount.toLocaleString() } views
40
+ </span>
41
+ )}
42
+ { viewCounterSlot }
36
43
  </p>
37
44
  </div>
38
45
  </div>
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { ArrowUp } from 'lucide-react';
5
+
6
+ // Floating button that appears after scrolling down and returns the user to the top of the page
7
+ export default function BackToTopButton(): React.JSX.Element | null {
8
+ const [visible, setVisible] = useState<boolean>(false);
9
+
10
+ useEffect(() => {
11
+ const onScroll = (): void => {
12
+ setVisible(window.scrollY > 400);
13
+ };
14
+
15
+ window.addEventListener('scroll', onScroll, { passive: true });
16
+ return () => window.removeEventListener('scroll', onScroll);
17
+ }, []);
18
+
19
+ const scrollToTop = (): void => {
20
+ window.scrollTo({ top: 0, behavior: 'smooth' });
21
+ };
22
+
23
+ if (!visible) return null;
24
+
25
+ return (
26
+ <button
27
+ onClick={scrollToTop}
28
+ className="fixed bottom-6 right-6 z-50 p-3 rounded-full bg-green-500 text-black shadow-lg hover:bg-green-400 transition-colors duration-200"
29
+ aria-label="Back to top"
30
+ >
31
+ <ArrowUp size={20} strokeWidth={2.5} />
32
+ </button>
33
+ );
34
+ }
@@ -11,7 +11,7 @@ import { JS_DEFAULT, TS_DEFAULT, JS_EXAMPLES, TS_EXAMPLES } from '@/utils/consta
11
11
  type Runtime = 'javascript' | 'typescript';
12
12
 
13
13
  // Client boundary for the Code Sandbox page — owns all interactive state
14
- export default function CodeSandboxClient() {
14
+ export default function CodeSandboxClient(): React.JSX.Element {
15
15
  const [runtime, setRuntime] = useState<Runtime>('javascript');
16
16
  const [sandpackKey, setSandpackKey] = useState<number>(0);
17
17
  const [sandpackCode, setSandpackCode] = useState<string>(JS_DEFAULT);
@@ -1,6 +1,6 @@
1
1
  import { Copy, Play, Download, Shield } from "lucide-react";
2
2
 
3
- export default function CodeSandboxFeaturesSection() {
3
+ export default function CodeSandboxFeaturesSection(): React.JSX.Element {
4
4
  return (
5
5
  <section className="pb-8 sm:pb-12">
6
6
  <h3 className="text-2xl sm:text-3xl font-bold mb-6 sm:mb-8 matrix-glow text-center text-green-300">Sandbox Features</h3>
@@ -3,22 +3,29 @@ import ArticleAuthorBio from "@/components/ArticleAuthorBio";
3
3
  import ArticleHeader from "@/components/ArticleHeader";
4
4
  import { ArticleAuthorInfoList, ArticleHeaderInfoList } from "@/utils/constants";
5
5
  import MDXRemoteArticle from "./MDXRemoteArticle";
6
- import { fetchArticle } from "@/utils/functions";
6
+ import ReadingProgressBar from "@/components/ReadingProgressBar";
7
+ import BackToTopButton from "@/components/BackToTopButton";
8
+ import { fetchArticle, incrementViewCount } from "@/utils/functions";
7
9
 
8
10
  // Custom Dynamic Article component encompasses loading article content stored in a Supabase database
9
- export default async function DynamicArticle({ slug } : { slug: string }) {
11
+ export default async function DynamicArticle({ slug } : { slug: string }): Promise<React.JSX.Element> {
10
12
  // Make a call to the back-end and fetch article information
11
- const articleData = await fetchArticle(slug);
12
-
13
+ const [articleData, viewCount] = await Promise.all([
14
+ fetchArticle(slug),
15
+ incrementViewCount(slug)
16
+ ]);
17
+
13
18
  if (!articleData || !articleData.content) {
14
19
  throw new Error("Invalid article");
15
20
  }
16
21
  else {
17
22
  return (
18
- <div className="min-h-screen flex flex-col bg-black">
23
+ <div className="min-h-screen flex flex-col bg-black">
24
+ <ReadingProgressBar />
25
+ <BackToTopButton />
19
26
  <main className="flex-grow px-4 py-8">
20
27
  <div className="max-w-4xl mx-auto">
21
- <ArticleHeader articleHeaderInformation={ArticleHeaderInfoList} />
28
+ <ArticleHeader articleHeaderInformation={ArticleHeaderInfoList} viewCount={viewCount} />
22
29
  <div className="glass-card p-8 mb-8">
23
30
  <MDXRemoteArticle content={articleData.content} />
24
31
  </div>
@@ -4,7 +4,7 @@ import matter from 'gray-matter';
4
4
 
5
5
  // Pass in the MDX remote article content as a string
6
6
  // Utilize the gray matter library to separate article content from article metadata (front-matter)
7
- export default function MDXRemoteArticle({ content }: { content: string }) {
7
+ export default function MDXRemoteArticle({ content }: { content: string }): React.JSX.Element {
8
8
  const { content: mdxContent } = matter(content);
9
9
 
10
10
  // MDX Remote used to capture article data from database
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ // Thin fixed progress bar at the top of the page indicating scroll depth through the article
6
+ export default function ReadingProgressBar(): React.JSX.Element {
7
+ const [progress, setProgress] = useState<number>(0);
8
+
9
+ useEffect(() => {
10
+ const updateProgress = (): void => {
11
+ const scrollTop = window.scrollY;
12
+ const docHeight = document.documentElement.scrollHeight - window.innerHeight;
13
+ const pct = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
14
+ setProgress(pct);
15
+ };
16
+
17
+ window.addEventListener('scroll', updateProgress, { passive: true });
18
+ updateProgress();
19
+
20
+ return () => window.removeEventListener('scroll', updateProgress);
21
+ }, []);
22
+
23
+ return (
24
+ <div
25
+ className="fixed top-0 left-0 z-50 h-1 bg-green-500 transition-all duration-75 ease-out"
26
+ style={{ width: `${progress}%` }}
27
+ role="progressbar"
28
+ aria-valuenow={Math.round(progress)}
29
+ aria-valuemin={0}
30
+ aria-valuemax={100}
31
+ aria-label="Reading progress"
32
+ />
33
+ );
34
+ }
@@ -43,7 +43,7 @@ const matrixTheme: SandpackTheme = {
43
43
  },
44
44
  };
45
45
 
46
- function EditorToolbar({ mainFile, template }: { mainFile: string; template: 'vanilla' | 'vanilla-ts' }) {
46
+ function EditorToolbar({ mainFile, template }: { mainFile: string; template: 'vanilla' | 'vanilla-ts' }): React.JSX.Element {
47
47
  const { sandpack, dispatch } = useSandpack();
48
48
  const isTS = template === 'vanilla-ts';
49
49
 
@@ -113,7 +113,7 @@ export interface SandpackEditorProps {
113
113
  template: 'vanilla' | 'vanilla-ts';
114
114
  }
115
115
 
116
- export default function SandpackEditor({ initialCode, template }: SandpackEditorProps) {
116
+ export default function SandpackEditor({ initialCode, template }: SandpackEditorProps): React.JSX.Element {
117
117
  const mainFile = template === 'vanilla-ts' ? '/index.ts' : '/index.js';
118
118
 
119
119
  return (
@@ -3,7 +3,7 @@ import ArticleContent from "@/markdown/ArticleContent.mdx";
3
3
 
4
4
  // Custom Static Article component to be used for loading article content stored locally
5
5
  // Article Header information will be hardcoded and stored locally, the following is simply an example
6
- const StaticArticle = () => {
6
+ const StaticArticle = (): React.JSX.Element => {
7
7
  return (
8
8
  <div className="glass-card p-8 mb-8">
9
9
  <ArticleContent />
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { Eye } from "lucide-react";
5
+
6
+ // ViewCounter — Client Component for SSG pages.
7
+ // Fires a POST to the /api/views/[slug] Route Handler on mount,
8
+ // which increments the count in Supabase and returns the new value.
9
+ export default function ViewCounter({ slug }: { slug: string }): React.JSX.Element | null {
10
+ const [count, setCount] = useState<number | null>(null);
11
+
12
+ useEffect(() => {
13
+ fetch(`/api/views/${encodeURIComponent(slug)}`, { method: "POST" })
14
+ .then((res) => res.json())
15
+ .then((data: { count?: number }) => {
16
+ if (typeof data.count === "number") {
17
+ setCount(data.count);
18
+ }
19
+ })
20
+ .catch(() => {
21
+ // Silently fail — view count is non-critical
22
+ });
23
+ }, [slug]);
24
+
25
+ if (count === null) return null;
26
+
27
+ return (
28
+ <span className="inline-flex items-center gap-1 ml-2">
29
+ • <Eye className="inline h-3 w-3" /> {count.toLocaleString()} views
30
+ </span>
31
+ );
32
+ }
@@ -7,11 +7,11 @@ import { Send } from "lucide-react";
7
7
  import type { ChatInputType } from "@/utils/types";
8
8
 
9
9
  // Chat Input — matrix themed
10
- export function ChatInput({ isLoading, onSendMessage }: ChatInputType) {
10
+ export function ChatInput({ isLoading, onSendMessage }: ChatInputType): React.JSX.Element {
11
11
  const [input, setInput] = useState("");
12
12
  const textareaRef = useRef<HTMLTextAreaElement>(null);
13
13
 
14
- function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
14
+ function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>): void {
15
15
  if (e.key === "Enter" && !e.shiftKey) {
16
16
  e.preventDefault();
17
17
  if (input.trim() && !isLoading) {
@@ -21,7 +21,7 @@ export function ChatInput({ isLoading, onSendMessage }: ChatInputType) {
21
21
  }
22
22
  }
23
23
 
24
- function handleSubmit(e: React.FormEvent) {
24
+ function handleSubmit(e: React.FormEvent): void {
25
25
  e.preventDefault();
26
26
  if (input.trim() && !isLoading) {
27
27
  onSendMessage(input);
@@ -11,7 +11,7 @@ import { Bot, RotateCcw } from "lucide-react";
11
11
  const transport = new TextStreamChatTransport({ api: "/api/chat" });
12
12
 
13
13
  // Chat Interface — no sidebar, no persistence, matrix themed
14
- export function ChatInterface() {
14
+ export function ChatInterface(): React.JSX.Element {
15
15
  const { messages, sendMessage, status, error, setMessages } = useChat({ transport });
16
16
 
17
17
  const isLoading = status === "submitted" || status === "streaming";
@@ -14,7 +14,7 @@ const SUGGESTIONS = [
14
14
  ];
15
15
 
16
16
  // Chat Messages — matrix themed, auto-scrolling
17
- export function ChatMessages({ messages, isLoading, error }: ChatMessagesType) {
17
+ export function ChatMessages({ messages, isLoading, error }: ChatMessagesType): React.JSX.Element {
18
18
  const bottomRef = useRef<HTMLDivElement>(null);
19
19
 
20
20
  useEffect(() => {
@@ -9,7 +9,7 @@ export function ChatToolResult({
9
9
  toolInvocation,
10
10
  }: {
11
11
  toolInvocation: ToolInvocationPartType;
12
- }) {
12
+ }): React.JSX.Element {
13
13
  const config = TOOL_CONFIG[toolInvocation.toolName] ?? {
14
14
  icon: Search,
15
15
  label: toolInvocation.toolName,
@@ -5,13 +5,13 @@ import { toast } from 'sonner';
5
5
 
6
6
  // Custom code block component for handling code in MDX files
7
7
  // Visual Studio Code Dark Plus theme with copy functionality
8
- const CodeBlock = ({ className = '', children }: { className?: string; children: string }) => {
8
+ const CodeBlock = ({ className = '', children }: { className?: string; children: string }): React.JSX.Element => {
9
9
  const match = /language-(\w+)/.exec(className || '');
10
10
  const language = match?.[1] || 'text';
11
11
  const code = String(children).trim();
12
12
 
13
13
  // Copy content and receive a toast message based on action
14
- const copyToClipboard = async () => {
14
+ const copyToClipboard = async (): Promise<void> => {
15
15
  try {
16
16
  await navigator.clipboard.writeText(code);
17
17
  toast.success('Code copied!', {
@@ -6,7 +6,7 @@ interface GistCodeBlockProps {
6
6
  language: string;
7
7
  }
8
8
 
9
- export default function GistCodeBlock({ content, language }: GistCodeBlockProps) {
9
+ export default function GistCodeBlock({ content, language }: GistCodeBlockProps): React.JSX.Element {
10
10
  return (
11
11
  <SyntaxHighlighter
12
12
  language={language}
@@ -6,8 +6,8 @@ interface GistCopyButtonProps {
6
6
  mdxGHGistURL: string;
7
7
  }
8
8
 
9
- export default function GistCopyButton({ content, mdxGHGistURL }: GistCopyButtonProps) {
10
- const copyToClipboard = async () => {
9
+ export default function GistCopyButton({ content, mdxGHGistURL }: GistCopyButtonProps): React.JSX.Element {
10
+ const copyToClipboard = async (): Promise<void> => {
11
11
  try {
12
12
  await navigator.clipboard.writeText(content);
13
13
  toast.success('GitHub Gist copied!', {
@@ -3,7 +3,7 @@ import GistCopyButton from './GistCopyButton';
3
3
  import GistCodeBlock from './GistCodeBlock';
4
4
  import { GITHUB_USERNAME, GITHUB_GIST_LANGUAGE_MAP, GIST_BASE_URL } from '@/utils/constants';
5
5
 
6
- export default async function GitHubGist({ id, figCaptionText }: GitHubGistType) {
6
+ export default async function GitHubGist({ id, figCaptionText }: GitHubGistType): Promise<React.JSX.Element> {
7
7
  try {
8
8
  const headers: HeadersInit = {
9
9
  'Accept': 'application/vnd.github.v3+json',
@@ -3,7 +3,7 @@ import type { MDXImageType } from "@/utils/types";
3
3
 
4
4
  // MDXImage custom component
5
5
  // Utilizes the built-in Next.js Image component as well as the figcaption element
6
- export default function MDXImage(imageProperties: MDXImageType) {
6
+ export default function MDXImage(imageProperties: MDXImageType): React.JSX.Element {
7
7
  return (
8
8
  <figure className='text-center my-6'>
9
9
  <div className="relative inline-block p-4 bg-gray-900/30 rounded-lg border-2 border-green-500 shadow-[0_0_15px_rgba(34,197,94,0.3)] transition-all duration-300 hover:shadow-[0_0_25px_rgba(34,197,94,0.5)] hover:scale-[1.02]">
@@ -1,2 +1,3 @@
1
1
  export { deleteArticle, fetchAllArticles, fetchArticle, insertArticle, updateArticle } from "./crud";
2
+ export { incrementViewCount } from "./rpc";
2
3
  export { getSupabaseClient } from "./supabase_client";
@@ -0,0 +1,19 @@
1
+ import getSupabaseClient from "../supabase_client/SupabaseClient";
2
+
3
+ // Atomically increments the view count for a given article slug and returns the new count.
4
+ // Targets the dedicated view_counts table via an upsert — works for both static and dynamic pages.
5
+ //
6
+ // Required Supabase migration — run scripts/sql/DDL/createViewCountsTable.sql and
7
+ // scripts/sql/DDL/createIncrementViewCountFunction.sql once in the Supabase SQL editor.
8
+
9
+ export default async function incrementViewCount(slug: string): Promise<number> {
10
+ const { data, error } = await getSupabaseClient()
11
+ .rpc('increment_view_count', { article_slug: slug.trim() });
12
+
13
+ if (error) {
14
+ console.error('Could not increment view count:', error.message);
15
+ return 0;
16
+ }
17
+
18
+ return (data as number) ?? 0;
19
+ }
@@ -0,0 +1 @@
1
+ export { default as incrementViewCount } from "./incrementViewCount";