add react components and pages
This commit is contained in:
parent
b786c86cf4
commit
259005c833
9 changed files with 423 additions and 35 deletions
|
@ -1,35 +1,29 @@
|
|||
import { useState } from 'react'
|
||||
import reactLogo from './assets/react.svg'
|
||||
import viteLogo from '/vite.svg'
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import Header from './components/Header'
|
||||
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'
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
</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>
|
||||
</>
|
||||
<div className="app">
|
||||
<Header />
|
||||
<main className="content">
|
||||
<Routes>
|
||||
<Route path="/" element={<About />} />
|
||||
<Route path="/blog" element={<Blog />} />
|
||||
<Route path="/blog/:slug" element={<Post />} />
|
||||
<Route path="/projects" element={<Projects />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App
|
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 { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</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