add react components and pages
This commit is contained in:
parent
b786c86cf4
commit
259005c833
9 changed files with 423 additions and 35 deletions
|
@ -1,34 +1,28 @@
|
||||||
import { useState } from 'react'
|
import { Routes, Route } from 'react-router-dom'
|
||||||
import reactLogo from './assets/react.svg'
|
import Header from './components/Header'
|
||||||
import viteLogo from '/vite.svg'
|
import Footer from './components/Footer'
|
||||||
|
import About from './pages/About'
|
||||||
|
import Blog from './pages/Blog'
|
||||||
|
import Post from './pages/Post'
|
||||||
|
import Projects from './pages/Projects'
|
||||||
|
import NotFound from './pages/NotFound'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [count, setCount] = useState(0)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="app">
|
||||||
<div>
|
<Header />
|
||||||
<a href="https://vite.dev" target="_blank">
|
<main className="content">
|
||||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
<Routes>
|
||||||
</a>
|
<Route path="/" element={<About />} />
|
||||||
<a href="https://react.dev" target="_blank">
|
<Route path="/blog" element={<Blog />} />
|
||||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
<Route path="/blog/:slug" element={<Post />} />
|
||||||
</a>
|
<Route path="/projects" element={<Projects />} />
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
<h1>Vite + React</h1>
|
|
||||||
<div className="card">
|
|
||||||
<button onClick={() => setCount((count) => count + 1)}>
|
|
||||||
count is {count}
|
|
||||||
</button>
|
|
||||||
<p>
|
|
||||||
Edit <code>src/App.jsx</code> and save to test HMR
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p className="read-the-docs">
|
|
||||||
Click on the Vite and React logos to learn more
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
20
client/src/components/Footer.jsx
Normal file
20
client/src/components/Footer.jsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import './Footer.css'
|
||||||
|
|
||||||
|
function Footer() {
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="footer">
|
||||||
|
<div className="container">
|
||||||
|
<p>© {currentYear} Your Name. All rights reserved.</p>
|
||||||
|
<div className="social-links">
|
||||||
|
<a href="https://github.com/yourusername" target="_blank" rel="noopener noreferrer">GitHub</a>
|
||||||
|
<a href="https://linkedin.com/in/yourusername" target="_blank" rel="noopener noreferrer">LinkedIn</a>
|
||||||
|
<a href="https://twitter.com/yourusername" target="_blank" rel="noopener noreferrer">Twitter</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Footer
|
21
client/src/components/Headers.jsx
Normal file
21
client/src/components/Headers.jsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { NavLink } from 'react-router-dom'
|
||||||
|
import './Header.css'
|
||||||
|
|
||||||
|
function Header() {
|
||||||
|
return (
|
||||||
|
<header className="header">
|
||||||
|
<div className="container">
|
||||||
|
<h1 className="logo">Your Name</h1>
|
||||||
|
<nav className="nav">
|
||||||
|
<ul>
|
||||||
|
<li><NavLink to="/" end>About</NavLink></li>
|
||||||
|
<li><NavLink to="/blog">Blog</NavLink></li>
|
||||||
|
<li><NavLink to="/projects">Projects</NavLink></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header
|
|
@ -1,10 +1,13 @@
|
||||||
import { StrictMode } from 'react'
|
import React from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import './index.css'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<React.StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>,
|
</BrowserRouter>
|
||||||
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
|
|
44
client/src/pages/About.jsx
Normal file
44
client/src/pages/About.jsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import './About.css'
|
||||||
|
|
||||||
|
function About() {
|
||||||
|
return (
|
||||||
|
<div className="about-page">
|
||||||
|
<div className="container">
|
||||||
|
<h1>About Me</h1>
|
||||||
|
<div className="about-content">
|
||||||
|
<div className="about-image">
|
||||||
|
<img src="/profile-picture.jpg" alt="Your Name" />
|
||||||
|
</div>
|
||||||
|
<div className="about-text">
|
||||||
|
<p>
|
||||||
|
Hello, I'm [Your Name]. I'm a [Your Profession] based in [Your Location].
|
||||||
|
I specialize in [Your Skills/Specialties].
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I've worked on various projects, from [Type of Project] to [Type of Project].
|
||||||
|
My goal is to [Your Professional Goal].
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
When I'm not coding, I enjoy [Your Hobbies/Interests].
|
||||||
|
</p>
|
||||||
|
<div className="skills">
|
||||||
|
<h2>Skills</h2>
|
||||||
|
<ul>
|
||||||
|
<li>JavaScript (React, Node.js)</li>
|
||||||
|
<li>HTML & CSS</li>
|
||||||
|
<li>Python</li>
|
||||||
|
{/* Add more skills as needed */}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="contact">
|
||||||
|
<h2>Contact</h2>
|
||||||
|
<p>Feel free to reach out to me at <a href="mailto:your.email@example.com">your.email@example.com</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default About
|
109
client/src/pages/Blog.jsx
Normal file
109
client/src/pages/Blog.jsx
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { format } from 'date-fns'
|
||||||
|
import './Blog.css'
|
||||||
|
|
||||||
|
function Blog() {
|
||||||
|
const [posts, setPosts] = useState([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
const [selectedTag, setSelectedTag] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPosts = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('http://localhost:5000/api/posts')
|
||||||
|
setPosts(response.data)
|
||||||
|
setLoading(false)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching posts:', err)
|
||||||
|
setError('Failed to load blog posts. Please try again later.')
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPosts()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Extract all unique tags from posts
|
||||||
|
const allTags = posts.reduce((tags, post) => {
|
||||||
|
if (post.tags) {
|
||||||
|
post.tags.forEach(tag => {
|
||||||
|
if (!tags.includes(tag)) {
|
||||||
|
tags.push(tag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return tags
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Filter posts by selected tag
|
||||||
|
const filteredPosts = selectedTag
|
||||||
|
? posts.filter(post => post.tags && post.tags.includes(selectedTag))
|
||||||
|
: posts
|
||||||
|
|
||||||
|
if (loading) return <div className="loading">Loading posts...</div>
|
||||||
|
if (error) return <div className="error">{error}</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="blog-page">
|
||||||
|
<div className="container">
|
||||||
|
<h1>Blog</h1>
|
||||||
|
|
||||||
|
{allTags.length > 0 && (
|
||||||
|
<div className="tags-filter">
|
||||||
|
<span>Filter by tag: </span>
|
||||||
|
<button
|
||||||
|
className={selectedTag === null ? 'active' : ''}
|
||||||
|
onClick={() => setSelectedTag(null)}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
{allTags.map(tag => (
|
||||||
|
<button
|
||||||
|
key={tag}
|
||||||
|
className={selectedTag === tag ? 'active' : ''}
|
||||||
|
onClick={() => setSelectedTag(tag)}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="posts-list">
|
||||||
|
{filteredPosts.length === 0 ? (
|
||||||
|
<p>No posts found.</p>
|
||||||
|
) : (
|
||||||
|
filteredPosts.map(post => (
|
||||||
|
<article key={post.slug} className="post-card">
|
||||||
|
<h2>
|
||||||
|
<Link to={`/blog/${post.slug}`}>{post.title}</Link>
|
||||||
|
</h2>
|
||||||
|
<div className="post-meta">
|
||||||
|
<time>{format(new Date(post.date), 'MMMM d, yyyy')}</time>
|
||||||
|
{post.tags && (
|
||||||
|
<div className="post-tags">
|
||||||
|
{post.tags.map(tag => (
|
||||||
|
<span key={tag} className="tag" onClick={() => setSelectedTag(tag)}>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="post-excerpt">{post.excerpt}</p>
|
||||||
|
<Link to={`/blog/${post.slug}`} className="read-more">
|
||||||
|
Read more →
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Blog
|
21
client/src/pages/NotFound.jsx
Normal file
21
client/src/pages/NotFound.jsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import './NotFound.css'
|
||||||
|
|
||||||
|
function NotFound() {
|
||||||
|
return (
|
||||||
|
<div className="not-found-page">
|
||||||
|
<div className="container">
|
||||||
|
<h1>404</h1>
|
||||||
|
<h2>Page Not Found</h2>
|
||||||
|
<p>
|
||||||
|
Oops! The page you're looking for doesn't exist or has been moved.
|
||||||
|
</p>
|
||||||
|
<Link to="/" className="back-home">
|
||||||
|
Go back to homepage
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotFound
|
72
client/src/pages/Post.jsx
Normal file
72
client/src/pages/Post.jsx
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useParams, Link, useNavigate } from 'react-router-dom'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { format } from 'date-fns'
|
||||||
|
import './Post.css'
|
||||||
|
|
||||||
|
function Post() {
|
||||||
|
const { slug } = useParams()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [post, setPost] = useState(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPost = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`http://localhost:5000/api/posts/${slug}`)
|
||||||
|
setPost(response.data)
|
||||||
|
setLoading(false)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error fetching post with slug ${slug}:`, err)
|
||||||
|
setError('Post not found or unable to load.')
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPost()
|
||||||
|
}, [slug])
|
||||||
|
|
||||||
|
if (loading) return <div className="loading">Loading post...</div>
|
||||||
|
if (error) return <div className="error">{error}</div>
|
||||||
|
if (!post) return <div className="error">Post not found</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="post-page">
|
||||||
|
<div className="container">
|
||||||
|
<article className="post">
|
||||||
|
<header className="post-header">
|
||||||
|
<h1>{post.title}</h1>
|
||||||
|
<div className="post-meta">
|
||||||
|
<time>{format(new Date(post.date), 'MMMM d, yyyy')}</time>
|
||||||
|
{post.tags && (
|
||||||
|
<div className="post-tags">
|
||||||
|
{post.tags.map(tag => (
|
||||||
|
<Link key={tag} to={`/blog?tag=${tag}`} className="tag">
|
||||||
|
{tag}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="post-content"
|
||||||
|
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<footer className="post-footer">
|
||||||
|
<div className="post-navigation">
|
||||||
|
<Link to="/blog" className="back-to-blog">
|
||||||
|
← Back to Blog
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Post
|
104
client/src/pages/Projects.jsx
Normal file
104
client/src/pages/Projects.jsx
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import './Projects.css'
|
||||||
|
|
||||||
|
function ProjectCard({ project }) {
|
||||||
|
return (
|
||||||
|
<div className={`project-card ${project.featured ? 'featured' : ''}`}>
|
||||||
|
{project.image && (
|
||||||
|
<div className="project-image">
|
||||||
|
<img src={`/projects/${project.image}`} alt={project.title} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="project-content">
|
||||||
|
<h3>{project.title}</h3>
|
||||||
|
<p>{project.description}</p>
|
||||||
|
<div className="project-tech">
|
||||||
|
{project.technologies.map(tech => (
|
||||||
|
<span key={tech} className="tech-tag">{tech}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="project-links">
|
||||||
|
{project.githubUrl && (
|
||||||
|
<a
|
||||||
|
href={project.githubUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="project-link github"
|
||||||
|
>
|
||||||
|
GitHub Repo
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{project.liveUrl && (
|
||||||
|
<a
|
||||||
|
href={project.liveUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="project-link live"
|
||||||
|
>
|
||||||
|
Live Demo
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Projects() {
|
||||||
|
const [projects, setProjects] = useState([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchProjects = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('http://localhost:5000/api/projects')
|
||||||
|
setProjects(response.data)
|
||||||
|
setLoading(false)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching projects:', err)
|
||||||
|
setError('Failed to load projects. Please try again later.')
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchProjects()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Sort projects to show featured ones first
|
||||||
|
const sortedProjects = [...projects].sort((a, b) => {
|
||||||
|
if (a.featured && !b.featured) return -1
|
||||||
|
if (!a.featured && b.featured) return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loading) return <div className="loading">Loading projects...</div>
|
||||||
|
if (error) return <div className="error">{error}</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="projects-page">
|
||||||
|
<div className="container">
|
||||||
|
<h1>Projects</h1>
|
||||||
|
<p className="projects-intro">
|
||||||
|
Here are some of the projects I've worked on. Check out my
|
||||||
|
<a href="https://github.com/yourusername" target="_blank" rel="noopener noreferrer">
|
||||||
|
GitHub profile
|
||||||
|
</a> for more.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="projects-grid">
|
||||||
|
{sortedProjects.length === 0 ? (
|
||||||
|
<p>No projects found.</p>
|
||||||
|
) : (
|
||||||
|
sortedProjects.map(project => (
|
||||||
|
<ProjectCard key={project.id} project={project} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Projects
|
Loading…
Reference in a new issue