Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Routes, Route } from "react-router-dom";
import BlogPostLayout from "./components/BlogPostLayout";
import blogs from "./data/blog";
import './styles/globals.css'
import { useEffect } from 'react'
import { Routes, Route, useLocation } from 'react-router-dom'
Expand Down Expand Up @@ -27,8 +30,52 @@ function ScrollToTop() {
}


export default function App() {
const BlogPage = () => {
return (
<div className="max-w-4xl mx-auto px-4 py-10">
<h1 className="text-3xl font-bold text-gray-900 mb-8">Blog</h1>
<div className="grid gap-6 md:grid-cols-2">
{blogs.map((post) => (
<a
key={post.id}
href={`/blog/${post.slug}`}
className="block p-6 bg-white rounded-xl shadow hover:shadow-md transition border border-gray-100"
>
<span className="text-xs font-semibold uppercase text-purple-600">
{post.category}
</span>
<h2 className="mt-2 text-lg font-bold text-gray-800">
{post.title}
</h2>
<p className="mt-2 text-sm text-gray-500">{post.excerpt}</p>
<p className="mt-3 text-xs text-gray-400">
{post.date} · {post.readTime}
</p>
</a>
))}
</div>
</div>
);
};

const BlogPostPage = () => {
const slug = window.location.pathname.split("/blog/")[1];
const post = blogs.find((b) => b.slug === slug);
if (!post) return <p className="text-center py-20">Post not found.</p>;
return <BlogPostLayout post={post} />;
};

function App() {
return (
<Routes>
<Route path="/" element={<BlogPage />} />
<Route path="/blog" element={<BlogPage />} />
<Route path="/blog/:slug" element={<BlogPostPage />} />
</Routes>
);
}

export default App;
<>
<Nav />
<ScrollToTop />
Expand All @@ -55,4 +102,4 @@ export default function App() {
<BackToTopButton/>
</>
)
}
}
76 changes: 76 additions & 0 deletions src/components/BlogPostLayout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import ShareButtons from "./ShareButtons";

const BlogPostLayout = ({ post }) => {
const {
title,
author,
date,
readTime,
category,
tags = [],
content,
coverImage,
} = post;

const formattedDate = new Date(date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});

return (
<article className="max-w-3xl mx-auto px-4 py-10">
{/* Category badge */}
<span className="inline-block mb-4 px-3 py-1 text-xs font-semibold uppercase tracking-wider bg-purple-100 text-purple-700 rounded-full">
{category}
</span>

{/* Title */}
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 leading-tight mb-4">
{title}
</h1>

{/* Meta row */}
<div className="flex flex-wrap items-center gap-3 text-sm text-gray-500 mb-6">
<span>{author}</span>
<span>·</span>
<span>{formattedDate}</span>
<span>·</span>
<span>{readTime}</span>
</div>

{/* Cover image */}
{coverImage && (
<img
src={coverImage}
alt={title}
className="w-full h-64 object-cover rounded-xl mb-8"
/>
)}

{/* Tags */}
{tags.length > 0 && (
<div className="flex flex-wrap gap-2 mb-6">
{tags.map((tag) => (
<span
key={tag}
className="px-3 py-1 text-xs bg-gray-100 text-gray-600 rounded-full"
>
#{tag}
</span>
))}
</div>
)}

{/* Article body */}
<div className="prose prose-gray max-w-none text-gray-700 leading-relaxed">
<p>{content}</p>
</div>

{/* Share Buttons — placed at the bottom as per issue spec */}
<ShareButtons title={title} url={window.location.href} />
</article>
);
};

export default BlogPostLayout;
148 changes: 148 additions & 0 deletions src/components/ShareButtons.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { useState } from "react";

const ShareButtons = ({ title, url }) => {
const shareUrl = url || window.location.href;
const shareTitle = title || document.title;
const [copied, setCopied] = useState(false);

const links = {
twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(
shareTitle
)}&url=${encodeURIComponent(shareUrl)}`,
linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(
shareUrl
)}`,
whatsapp: `https://api.whatsapp.com/send?text=${encodeURIComponent(
shareTitle + " " + shareUrl
)}`,
};

const handleNativeShare = async () => {
if (navigator.share) {
try {
await navigator.share({ title: shareTitle, url: shareUrl });
} catch (err) {
// User cancelled or error — do nothing
}
}
};

const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(shareUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Fallback for older browsers
const el = document.createElement("input");
el.value = shareUrl;
document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};

return (
<div className="mt-10 pt-6 border-t border-gray-200">
<p className="text-sm font-semibold text-gray-600 mb-3 uppercase tracking-wide">
Share this post
</p>
<div className="flex flex-wrap gap-3 items-center">
{/* Twitter/X */}
<a
href={links.twitter}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 rounded-full bg-black text-white text-sm font-medium hover:bg-gray-800 transition-colors"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.746l7.73-8.835L1.254 2.25H8.08l4.253 5.622zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
Twitter / X
</a>

{/* LinkedIn */}
<a
href={links.linkedin}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 rounded-full bg-[#0A66C2] text-white text-sm font-medium hover:bg-[#004182] transition-colors"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
LinkedIn
</a>

{/* WhatsApp */}
<a
href={links.whatsapp}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 rounded-full bg-[#25D366] text-white text-sm font-medium hover:bg-[#1aab52] transition-colors"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" />
</svg>
WhatsApp
</a>

{/* Copy Link */}
<button
onClick={handleCopyLink}
className="flex items-center gap-2 px-4 py-2 rounded-full bg-gray-100 text-gray-700 text-sm font-medium hover:bg-gray-200 transition-colors"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
strokeWidth="2"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
{copied ? "Link Copied!" : "Copy Link"}
</button>

{/* Native Share (mobile only) */}
{typeof navigator !== "undefined" && navigator.share && (
<button
onClick={handleNativeShare}
className="flex items-center gap-2 px-4 py-2 rounded-full bg-purple-100 text-purple-700 text-sm font-medium hover:bg-purple-200 transition-colors"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
strokeWidth="2"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"
/>
</svg>
Share
</button>
)}
</div>

{/* Toast notification */}
{copied && (
<div className="mt-3 inline-block px-4 py-2 bg-green-100 text-green-700 text-sm rounded-lg">
✅ Link copied to clipboard!
</div>
)}
</div>
);
};

export default ShareButtons;
49 changes: 49 additions & 0 deletions src/data/blog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const blogs = [
{
id: 1,
title: "Breaking Into Tech: A Beginner's Roadmap for Women",
slug: "breaking-into-tech-beginners-roadmap",
author: "HerStack Team",
date: "2025-05-10",
readTime: "5 min read",
category: "Career",
tags: ["career", "beginners", "roadmap"],
excerpt:
"Feeling overwhelmed about where to start in tech? Here's a clear, step-by-step guide tailored for women entering the industry.",
content:
"Full article content goes here. This is placeholder text for now.",
coverImage: "/assets/blog/breaking-into-tech.jpg",
},
{
id: 2,
title: "Top 10 Open Source Projects to Contribute to as a Beginner",
slug: "top-10-open-source-projects-for-beginners",
author: "HerStack Team",
date: "2025-05-15",
readTime: "7 min read",
category: "Open Source",
tags: ["open-source", "github", "beginners"],
excerpt:
"Contributing to open source is one of the best ways to grow your skills. Here are 10 beginner-friendly projects to get you started.",
content:
"Full article content goes here. This is placeholder text for now.",
coverImage: "/assets/blog/open-source.jpg",
},
{
id: 3,
title: "How to Ace Your First Technical Interview",
slug: "how-to-ace-your-first-technical-interview",
author: "HerStack Team",
date: "2025-05-20",
readTime: "6 min read",
category: "Interview Prep",
tags: ["interview", "dsa", "career"],
excerpt:
"Technical interviews can be nerve-wracking. Here's everything you need to know to walk in confident and prepared.",
content:
"Full article content goes here. This is placeholder text for now.",
coverImage: "/assets/blog/technical-interview.jpg",
},
];

export default blogs;
1 change: 1 addition & 0 deletions src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
import './styles/globals.css'

ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
Expand Down