diff --git a/app/a/[slug]/BlogPostClient.tsx b/app/a/[slug]/BlogPostClient.tsx index 1cc923c..27d06f4 100644 --- a/app/a/[slug]/BlogPostClient.tsx +++ b/app/a/[slug]/BlogPostClient.tsx @@ -5,27 +5,35 @@ import { ArrowLeft, Calendar, User } from "lucide-react" import Image from "next/image" import { MarkdownRenderer } from "@/lib/markdown-renderer" import Footer from "@/components/footer" +import ThemeToggle from "@/components/theme-toggle" import { BlogPost } from "@/lib/blog" +/** + * Render a complete blog post page with header, optional hero image, metadata, content, and footer. + * + * @param post - BlogPost data used to render the page. Expected properties: `title`, `author`, `date`, `content`, and optional `image`. + * @returns The React element representing the full blog post page. + */ export default function BlogPostPage({ post }: { post: BlogPost }) { return ( -
+
{/* Header */} -
+
Back to Stable Viewpoints +
{/* Article */}
-
+
{/* Hero Image */} {post.image && (
@@ -50,7 +58,7 @@ export default function BlogPostPage({ post }: { post: BlogPost }) { {post.title} -
+
{post.author} @@ -67,12 +75,12 @@ export default function BlogPostPage({ post }: { post: BlogPost }) {
-
+
-
-

+

+

© {new Date(post.date).getFullYear()} {post.author}. All rights reserved.

@@ -83,4 +91,4 @@ export default function BlogPostPage({ post }: { post: BlogPost }) {
) -} +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 192fb19..03e9af5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,6 +3,7 @@ import type { Metadata } from "next" import { Inter, Playfair_Display } from "next/font/google" import "./globals.css" import { getBaseUrl, getAbsoluteUrl, getOgImageUrl } from "@/lib/metadata" +import { ThemeProvider } from "@/components/theme-provider" const inter = Inter({ subsets: ["latin"] }) const playfair = Playfair_Display({ @@ -70,14 +71,22 @@ export const metadata: Metadata = { }, } +/** + * Root application layout that sets the HTML language, global fonts, and theme provider. + * + * @param children - The content to render inside the layout (application pages or components). + * @returns A JSX element representing the root HTML document with configured fonts and a ThemeProvider wrapping `children`. + */ export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( - - {children} + + + {children} + ) -} +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 2548a71..82e61fd 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -7,6 +7,7 @@ import BlogCard from "@/components/blog-card" import Pagination from "@/components/pagination" import Link from "next/link" import Footer from "@/components/footer" +import ThemeToggle from "@/components/theme-toggle" interface BlogPost { slug: string @@ -25,6 +26,16 @@ interface PaginatedData { hasPrevPage: boolean } +/** + * Renders the blog homepage with paginated posts, header controls, and pagination UI. + * + * Reads the "page" query parameter to initialize and synchronize the current page, loads + * the corresponding paginated posts, and displays loading or error states as needed. + * The header includes site branding, a theme toggle, and a "Submit an Article" link; + * the main area renders post cards and a pagination control that updates both state and URL. + * + * @returns The React element for the homepage containing the header, post grid, pagination, and footer + */ export default function HomePage() { const router = useRouter() const searchParams = useSearchParams() @@ -76,10 +87,10 @@ export default function HomePage() { if (loading) { return ( -
+
-
-

Loading articles...

+
+

Loading articles...

) @@ -87,9 +98,9 @@ export default function HomePage() { if (!paginatedData) { return ( -
+
-

Failed to load articles.

+

Failed to load articles.

) @@ -98,22 +109,25 @@ export default function HomePage() { const { posts, totalPages, hasNextPage, hasPrevPage } = paginatedData return ( -
+
{/* Header */} -
+

Stable Viewpoints

-

Independent Articles about Stability

+

Independent Articles about Stability

+
+
+ + + Submit an Article +
- - Submit an Article -
@@ -138,4 +152,4 @@ export default function HomePage() {
) -} +} \ No newline at end of file diff --git a/app/submit/page.tsx b/app/submit/page.tsx index 638f35b..a3b37a6 100644 --- a/app/submit/page.tsx +++ b/app/submit/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link" import { ArrowLeft, Upload, FileText, ImageIcon, CheckCircle, Users, MessageCircle } from "lucide-react" import Footer from "@/components/footer" +import ThemeToggle from "@/components/theme-toggle" export const metadata = { title: "Submit an Article | Stable Viewpoints", @@ -8,32 +9,42 @@ export const metadata = { "Learn how to submit your article to Stable Viewpoints and contribute to thoughtful dialogue about stability.", } +/** + * Render the article submission guide page for Stable Viewpoints. + * + * The component returns the full submission UI, including a sticky header with navigation and theme toggle, + * a main content card detailing topics, step-by-step submission instructions, image and submission guidelines, + * contact links, and the site footer. The layout includes responsive styling and dark-mode aware color variants. + * + * @returns A JSX element containing the complete submission guide page layout. + */ export default function SubmitPage() { return ( -
+
{/* Header */} -
+
Back to Stable Viewpoints +
{/* Main Content */}
-
+
{/* Title */}

Submit an Article

-

+

Stable Viewpoints is a digital publication focused on thoughtful perspectives about stability in our rapidly changing world. We explore how emerging technologies can be used to bring greater stability to the world. Our mission is to provide well-researched, balanced viewpoints on issues that matter for creating a @@ -49,37 +60,37 @@ export default function SubmitPage() {

Blockchain & Cryptocurrencies

-

+

Digital assets, blockchain technology, cryptocurrency adoption, regulatory frameworks

Artificial Intelligence

-

+

AI governance, machine learning applications, ethical AI development, automation impact

Economic & Financial Stability

-

+

Monetary policy, financial markets, economic resilience, inflation dynamics

Decentralized Finance (DeFi)

-

+

Protocol analysis, yield farming, liquidity provision, DeFi security

Technology & Society

-

+

Digital transformation, cybersecurity, privacy rights, technological disruption

Monetary Systems

-

+

Central bank digital currencies (CBDCs), stablecoins, alternative monetary frameworks

@@ -89,7 +100,7 @@ export default function SubmitPage() { {/* How to Submit */}

How to Submit an Article

-

+

We welcome contributions from writers, researchers, and experts who share our commitment to thoughtful, well-researched content. Here's how to submit your article:

@@ -102,10 +113,10 @@ export default function SubmitPage() { Prepare Your Article -

+

Your article should be written in Markdown format (.md file) and include:

-
    +
    • Title: Clear and descriptive
    • @@ -135,7 +146,7 @@ export default function SubmitPage() { Format Your Article -

      Create a file with this structure:

      +

      Create a file with this structure:

      {`---
       title: "Your Article Title Here"
      @@ -171,19 +182,19 @@ Remember to cite your sources and provide value to our readers!`}
      Submit your Article to the GitHub Repo -

      +

      Don't worry if you're new to GitHub - here's a simple step-by-step guide:

      {/* Sub-step 3.1 */} -
      +

      1 Go to our repository

      -

      +

      Visit{" "}

      {/* Sub-step 3.2 */} -
      +

      2 Fork the repository

      -

      +

      Click the "Fork" button in the top-right corner. This creates your own copy of the project.

      {/* Sub-step 3.3 */} -
      +

      3 Add your images

      -
        +
        • In your forked repository, click on public{" "} folder @@ -237,14 +248,14 @@ Remember to cite your sources and provide value to our readers!`}
      {/* Sub-step 3.4 */} -
      +

      4 Add your article

      -
        +
        • Navigate to public →{" "} articles folder @@ -259,14 +270,14 @@ Remember to cite your sources and provide value to our readers!`}
      {/* Sub-step 3.5 */} -
      +

      5 Use images in your article

      -
        +
        • Header image: Set in the frontmatter as{" "} image: "/images/your-header-image.jpg" @@ -284,14 +295,14 @@ Remember to cite your sources and provide value to our readers!`}
      {/* Sub-step 3.6 */} -
      +

      6 Commit your changes

      -
        +
        • Scroll down to "Commit new file"
        • Add a commit message like "Add article: Your Article Title"
        • Click "Commit new file"
        • @@ -309,14 +320,14 @@ Remember to cite your sources and provide value to our readers!`} {/* Sub-step 4.1 */} -
          +

          1 Navigate to the articles index

          -

          +

          Go to public →{" "} articles folder, then click on{" "} articles-index.json @@ -324,14 +335,14 @@ Remember to cite your sources and provide value to our readers!`}

          {/* Sub-step 4.2 */} -
          +

          2 Edit the file

          -

          +

          Click the pencil icon to edit the file, then add your article information to the{" "} articles array at the top (it will appear first on the website) @@ -339,7 +350,7 @@ Remember to cite your sources and provide value to our readers!`}

          {/* Sub-step 4.3 */} -
          +

          3 @@ -360,14 +371,14 @@ Remember to cite your sources and provide value to our readers!`}

          {/* Sub-step 4.4 */} -
          +

          4 Important formatting notes

          -
            +
            • Add a comma after the previous entry and ensure proper JSON formatting
            • Featured articles: Set{" "} @@ -389,41 +400,41 @@ Remember to cite your sources and provide value to our readers!`} {/* Sub-step 5.1 */} -
              +

              1 Start the pull request

              -

              +

              You'll see a banner saying "This branch is ahead". Click "Contribute" → "Open pull request"

              {/* Sub-step 5.2 */} -
              +

              2 Add details

              -

              +

              Add a title and description for your submission. Include: List of images you've added and their purpose

              {/* Sub-step 5.3 */} -
              +

              3 Submit

              -

              Click "Create pull request" to submit your article for review

              +

              Click "Create pull request" to submit your article for review

              @@ -438,25 +449,25 @@ Remember to cite your sources and provide value to our readers!`}
              - + Initial Review: We'll review your submission as soon as possible
              - + Feedback: If changes are needed, we'll provide constructive feedback
              - + Publication: Once approved, your article will be published on the site
              - + Promotion: We'll share your article on our social media channels
              @@ -474,7 +485,7 @@ Remember to cite your sources and provide value to our readers!`}

              Technical Requirements

              -
                +
                • Formats: JPG, PNG, or WebP
                • @@ -492,7 +503,7 @@ Remember to cite your sources and provide value to our readers!`}

                  Content Guidelines

                  -
                    +
                    • Copyright: Only use images you own or have permission to use
                    • @@ -511,7 +522,7 @@ Remember to cite your sources and provide value to our readers!`}

                      Naming Convention

                      -
                        +
                        • Use descriptive, lowercase names with hyphens
                        • Examples:{" "} @@ -533,7 +544,7 @@ Remember to cite your sources and provide value to our readers!`}

                          Content Standards

                          -
                            +
                            • Original work only - no plagiarism or previously published content
                            • @@ -551,7 +562,7 @@ Remember to cite your sources and provide value to our readers!`}

                              Technical Requirements

                              -
                                +
                                • Markdown format (.md file)
                                • @@ -569,7 +580,7 @@ Remember to cite your sources and provide value to our readers!`} {/* Contact */}

                                  Questions?

                                  -

                                  +

                                  If you have questions about the submission process or want to discuss a potential article idea, contact us via:

                                  @@ -596,7 +607,7 @@ Remember to cite your sources and provide value to our readers!`} {/* Footer Message */}
                                  -

                                  +

                                  Stable Viewpoints is committed to fostering thoughtful dialogue about the challenges and opportunities of our time. We believe that through careful analysis and open discussion, we can work together toward a more stable future. @@ -609,4 +620,4 @@ Remember to cite your sources and provide value to our readers!`}

                                  ) -} +} \ No newline at end of file diff --git a/components/blog-card.tsx b/components/blog-card.tsx index 47d6438..dfc7b39 100644 --- a/components/blog-card.tsx +++ b/components/blog-card.tsx @@ -16,10 +16,18 @@ interface BlogCardProps { post: BlogPost } +/** + * Render a clickable card UI for a blog post. + * + * Displays the post's image, title, excerpt, and author, and shows a featured badge when `post.featured` is true. + * + * @param post - The blog post data to display (expects fields like `slug`, `title`, `author`, `date`, `image`, `excerpt`, `featured`). + * @returns The rendered blog post card as a React element. + */ export default function BlogCard({ post }: BlogCardProps) { return ( -
                                  +
                                  {post.featured && (
                                  @@ -44,14 +52,14 @@ export default function BlogCard({ post }: BlogCardProps) {
                                  -

                                  +

                                  {post.title}

                                  -

                                  {post.excerpt}

                                  +

                                  {post.excerpt}

                                  -
                                  -
                                  +
                                  +
                                  {post.author}
                                  @@ -60,4 +68,4 @@ export default function BlogCard({ post }: BlogCardProps) {
                                  ) -} +} \ No newline at end of file diff --git a/components/footer.tsx b/components/footer.tsx index 5401ecb..9fe8b2f 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -22,20 +22,28 @@ const LinkedInIcon = () => ( ) +/** + * Render the site footer containing branding, a short description, and social links. + * + * The footer includes a headline with gradient text, a descriptive paragraph, and a row + * of social buttons (Telegram, Discord, LinkedIn, Twitter) that open in new tabs. + * + * @returns A React element representing the page footer with branding, descriptive text, and social link buttons. + */ export default function Footer() { return ( -
                                  +

                                  Stable Viewpoints

                                  -

                                  Fostering thoughtful dialogue toward a more stable future.

                                  +

                                  Fostering thoughtful dialogue toward a more stable future.

                                  - Connect with us: + Connect with us:
                                  ) -} +} \ No newline at end of file diff --git a/components/pagination.tsx b/components/pagination.tsx index d3da3ec..8f2ce73 100644 --- a/components/pagination.tsx +++ b/components/pagination.tsx @@ -11,6 +11,20 @@ interface PaginationProps { onPageChange?: (page: number) => void } +/** + * Render pagination controls that support either client-side callbacks or server-side links. + * + * Renders previous/next controls and a centered window of up to five page numbers; uses the optional + * `onPageChange` callback for client-side navigation when provided, otherwise renders Next.js `Link`s + * for server-side navigation. Does not render anything when `totalPages` is 1 or less. + * + * @param currentPage - The currently active page (1-based). + * @param totalPages - The total number of available pages. + * @param hasNextPage - Whether a next page exists. + * @param hasPrevPage - Whether a previous page exists. + * @param onPageChange - Optional callback invoked with the target page number for client-side navigation. + * @returns The pagination controls element, or `null` when no pagination is needed. + */ export default function Pagination({ currentPage, totalPages, @@ -49,8 +63,8 @@ export default function Pagination({ disabled={!hasPrevPage || currentPage <= 1} className={`flex items-center gap-1 px-4 py-2 transition-colors shadow-sm ${ hasPrevPage && currentPage > 1 - ? "bg-white border border-[#228B22]/20 text-[#228B22] hover:bg-[#228B22] hover:text-white cursor-pointer" - : "bg-gray-100 text-gray-400 cursor-not-allowed" + ? "bg-white dark:bg-slate-900 border border-[#228B22]/20 dark:border-white/20 text-[#228B22] dark:text-amber-200 hover:bg-[#228B22] hover:text-white dark:hover:bg-[#3E921E] dark:hover:text-white cursor-pointer" + : "bg-gray-100 dark:bg-slate-800 text-gray-400 dark:text-gray-500 cursor-not-allowed" }`} > @@ -66,7 +80,7 @@ export default function Pagination({ className={`px-4 py-2 transition-colors ${ pageNum === currentPage ? "bg-gradient-to-r from-[#228B22] to-[#91A511] text-white shadow-md cursor-default" - : "bg-white border border-[#228B22]/20 text-[#228B22] hover:bg-[#228B22]/10 cursor-pointer" + : "bg-white dark:bg-slate-900 border border-[#228B22]/20 dark:border-white/20 text-[#228B22] dark:text-amber-200 hover:bg-[#228B22]/10 dark:hover:bg-slate-800 cursor-pointer" }`} > {pageNum} @@ -80,8 +94,8 @@ export default function Pagination({ disabled={!hasNextPage || currentPage >= totalPages} className={`flex items-center gap-1 px-4 py-2 transition-colors shadow-sm ${ hasNextPage && currentPage < totalPages - ? "bg-white border border-[#228B22]/20 text-[#228B22] hover:bg-[#228B22] hover:text-white cursor-pointer" - : "bg-gray-100 text-gray-400 cursor-not-allowed" + ? "bg-white dark:bg-slate-900 border border-[#228B22]/20 dark:border-white/20 text-[#228B22] dark:text-amber-200 hover:bg-[#228B22] hover:text-white dark:hover:bg-[#3E921E] dark:hover:text-white cursor-pointer" + : "bg-gray-100 dark:bg-slate-800 text-gray-400 dark:text-gray-500 cursor-not-allowed" }`} > Next @@ -98,7 +112,7 @@ export default function Pagination({ {hasPrevPage && currentPage > 1 ? ( Previous @@ -106,7 +120,7 @@ export default function Pagination({ ) : (
                              ) -} +} \ No newline at end of file diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx index 55c2f6e..aaad64a 100644 --- a/components/theme-provider.tsx +++ b/components/theme-provider.tsx @@ -1,11 +1,72 @@ -'use client' +"use client" -import * as React from 'react' -import { - ThemeProvider as NextThemesProvider, - type ThemeProviderProps, -} from 'next-themes' +import { createContext, useContext, useEffect, useState, type ReactNode } from "react" -export function ThemeProvider({ children, ...props }: ThemeProviderProps) { - return {children} +type Theme = "light" | "dark" + +interface ThemeContextValue { + theme: Theme + toggleTheme: () => void + setTheme: (theme: Theme) => void + isReady: boolean } + +const ThemeContext = createContext(null) + +/** + * Provides theme state, persistence, and controls to descendant components via ThemeContext. + * + * Initializes the theme from localStorage (key "sv-theme") or the user's system preference, applies the "dark" + * class to the document root when the active theme is "dark", and persists theme changes to localStorage. + * + * @param children - React nodes rendered inside the provider + * @returns The ThemeContext provider element that supplies `{ theme, toggleTheme, setTheme, isReady }` to descendants + */ +export function ThemeProvider({ children }: { children: ReactNode }) { + const [theme, setThemeState] = useState("light") + const [isReady, setIsReady] = useState(false) + + useEffect(() => { + const storedTheme = localStorage.getItem("sv-theme") as Theme | null + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches + const initialTheme: Theme = storedTheme ?? (prefersDark ? "dark" : "light") + setThemeState(initialTheme) + setIsReady(true) + }, []) + + useEffect(() => { + if (!isReady) return + document.documentElement.classList.toggle("dark", theme === "dark") + localStorage.setItem("sv-theme", theme) + }, [theme, isReady]) + + const setTheme = (value: Theme) => setThemeState(value) + const toggleTheme = () => setThemeState((prev) => (prev === "dark" ? "light" : "dark")) + + return ( + + {children} + + ) +} + +/** + * Accesses the current theme context value. + * + * @returns The current ThemeContextValue with `theme`, `toggleTheme`, `setTheme`, and `isReady`. + * @throws Error if called outside of a `ThemeProvider`. + */ +export function useTheme() { + const context = useContext(ThemeContext) + if (!context) { + throw new Error("useTheme must be used within a ThemeProvider") + } + return context +} \ No newline at end of file diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx new file mode 100644 index 0000000..235721b --- /dev/null +++ b/components/theme-toggle.tsx @@ -0,0 +1,28 @@ +"use client" + +import { Moon, Sun } from "lucide-react" +import { useTheme } from "@/components/theme-provider" + +/** + * Render a button that toggles the application's color theme. + * + * The button toggles the theme when activated, exposes an accessible `aria-label` that describes the target mode, is disabled until the theme system is ready, and displays a Sun or Moon icon that reflects the current theme. + * + * @returns The button element that toggles the theme, with accessible labeling and an icon indicating the current theme + */ +export default function ThemeToggle() { + const { theme, toggleTheme, isReady } = useTheme() + const isDark = theme === "dark" + + return ( + + ) +} \ No newline at end of file diff --git a/lib/markdown-renderer.tsx b/lib/markdown-renderer.tsx index 5a1d63a..e2b3503 100644 --- a/lib/markdown-renderer.tsx +++ b/lib/markdown-renderer.tsx @@ -5,15 +5,32 @@ interface MarkdownRendererProps { // Conditionally set BASE_PATH based on environment const BASE_PATH = process.env.NODE_ENV === "production" ? "/StableViewpoints" : "" +/** + * Render Markdown-like `content` as styled HTML inside a prose container. + * + * Supports headers, bold/italic emphasis, code blocks and inline code, images (root-relative `src` are prefixed with `BASE_PATH`), links, blockquotes, horizontal rules, ordered and unordered lists, and automatic paragraph wrapping. + * + * @param content - Markdown-like text to convert to HTML and render + * @returns A React element containing the converted HTML (injected via `dangerouslySetInnerHTML`) + */ export function MarkdownRenderer({ content }: MarkdownRendererProps) { // Enhanced markdown to HTML conversion const renderMarkdown = (text: string) => { let html = text // Headers - html = html.replace(/^### (.*$)/gim, '

                              $1

                              ') - html = html.replace(/^## (.*$)/gim, '

                              $1

                              ') - html = html.replace(/^# (.*$)/gim, '

                              $1

                              ') + html = html.replace( + /^### (.*$)/gim, + '

                              $1

                              ', + ) + html = html.replace( + /^## (.*$)/gim, + '

                              $1

                              ', + ) + html = html.replace( + /^# (.*$)/gim, + '

                              $1

                              ', + ) // Bold and italic html = html.replace(/\*\*(.*?)\*\*/g, "$1") @@ -22,11 +39,14 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) { // Code blocks html = html.replace( /```([\s\S]*?)```/g, - '
                              $1
                              ' + '
                              $1
                              ', ) // Inline code - html = html.replace(/`([^`]+)`/g, '$1') + html = html.replace( + /`([^`]+)`/g, + '$1', + ) // Images - handle base path for GitHub Pages html = html.replace( @@ -47,7 +67,7 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) { // Blockquotes html = html.replace( /^> (.*$)/gim, - '
                              $1
                              ', + '
                              $1
                              ', ) // Horizontal rules @@ -65,13 +85,13 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) { if (trimmed.startsWith("- ")) { if (!inList) { - processedLines.push('
                                ') + processedLines.push('
                                  ') inList = true } processedLines.push(`
                                • ${trimmed.substring(2)}
                                • `) } else if (trimmed.match(/^\d+\. /)) { if (!inOrderedList) { - processedLines.push('
                                    ') + processedLines.push('
                                      ') inOrderedList = true } processedLines.push(`
                                    1. ${trimmed.replace(/^\d+\. /, '')}
                                    2. `) @@ -118,11 +138,16 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) { ) { return trimmed } - return `

                                      ${trimmed}

                                      ` + return `

                                      ${trimmed}

                                      ` }) return finalProcessedLines.join("\n") } - return
                                      -} + return ( +
                                      + ) +} \ No newline at end of file