Images Gallery

This commit is contained in:
2024-12-01 17:26:06 +00:00
parent d1d469d16d
commit 3bc3ff5ba7
4 changed files with 359 additions and 1 deletions

View File

@@ -1,9 +1,10 @@
// src/App.jsx
// Update src/App.jsx
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import Login from './components/Login';
import Dashboard from './components/Dashboard';
import Editor from './components/Editor';
import ImagesGallery from './components/ImagesGallery';
function PrivateRoute({ children }) {
const { isAuthenticated } = useAuth();
@@ -25,6 +26,14 @@ function App() {
</PrivateRoute>
}
/>
<Route
path="/images"
element={
<PrivateRoute>
<ImagesGallery />
</PrivateRoute>
}
/>
<Route
path="/new"
element={

View File

@@ -54,6 +54,7 @@ export default function Dashboard() {
<h1>My Blog Posts</h1>
<div className="actions">
<Link to="/new" className="button">New Post</Link>
<Link to="/images" className="button">Images</Link>
<button onClick={handleLogout} className="button logout">Logout</button>
</div>
</header>

View File

@@ -0,0 +1,177 @@
// src/components/ImagesGallery.jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
export default function ImagesGallery() {
const [images, setImages] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [uploading, setUploading] = useState(false);
const [selectedImage, setSelectedImage] = useState(null);
const { logout } = useAuth();
const navigate = useNavigate();
useEffect(() => {
loadImages();
}, []);
const loadImages = async () => {
try {
const response = await axios.get(`${import.meta.env.VITE_API_URL}/api/images`, {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
setImages(response.data);
setError(null);
} catch (err) {
console.error('Error loading images:', err);
setError('Failed to load images');
if (err.response?.status === 401) {
logout();
navigate('/login');
}
} finally {
setLoading(false);
}
};
const handleFileChange = async (event) => {
const file = event.target.files[0];
if (!file) return;
setUploading(true);
const formData = new FormData();
formData.append('file', file);
try {
const response = await axios.post(
`${import.meta.env.VITE_API_URL}/api/images/upload`,
formData,
{
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'multipart/form-data'
}
}
);
setImages(prev => [response.data, ...prev]);
setError(null);
} catch (err) {
console.error('Error uploading image:', err);
setError('Failed to upload image');
} finally {
setUploading(false);
}
};
const handleImageClick = (image) => {
setSelectedImage(image);
};
const copyUrlToClipboard = async (url) => {
try {
await navigator.clipboard.writeText(`${import.meta.env.VITE_API_URL}${url}`);
alert('URL copied to clipboard!');
} catch (err) {
console.error('Failed to copy URL:', err);
alert('Failed to copy URL');
}
};
if (loading) return <div className="loading">Loading...</div>;
return (
<div className="images-gallery">
<header className="gallery-header">
<h1>Image Gallery</h1>
<div className="actions">
<label className="button upload">
Upload Image
<input
type="file"
hidden
accept="image/*"
onChange={handleFileChange}
disabled={uploading}
/>
</label>
<button
onClick={() => navigate('/dashboard')}
className="button"
>
Back to Dashboard
</button>
</div>
</header>
{error && <div className="error-message">{error}</div>}
<div className="images-grid">
{images.map(image => (
<div
key={image.id}
className="image-card"
onClick={() => handleImageClick(image)}
>
<img
src={`${import.meta.env.VITE_API_URL}${image.url}`}
alt={image.filename}
loading="lazy"
/>
<div className="image-info">
<span className="filename">{image.filename}</span>
<button
className="copy-url"
onClick={(e) => {
e.stopPropagation();
copyUrlToClipboard(image.url);
}}
>
Copy URL
</button>
</div>
</div>
))}
</div>
{selectedImage && (
<div
className="image-modal"
onClick={() => setSelectedImage(null)}
>
<div
className="modal-content"
onClick={e => e.stopPropagation()}
>
<img
src={`${import.meta.env.VITE_API_URL}${selectedImage.url}`}
alt={selectedImage.filename}
/>
<div className="modal-info">
<h3>{selectedImage.filename}</h3>
<div className="url-container">
<input
type="text"
value={`${import.meta.env.VITE_API_URL}${selectedImage.url}`}
readOnly
/>
<button
onClick={() => copyUrlToClipboard(selectedImage.url)}
>
Copy
</button>
</div>
<button
className="close"
onClick={() => setSelectedImage(null)}
>
Close
</button>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -381,4 +381,175 @@ body {
// Override some MD Editor default styles
.w-md-editor {
--md-editor-box-shadow: none !important;
}
.images-gallery {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
.gallery-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
h1 {
margin: 0;
color: $primary-color;
}
.actions {
display: flex;
gap: 10px;
.button {
width: auto;
&.upload {
background-color: $secondary-color;
cursor: pointer;
&:hover {
background-color: darken($secondary-color, 10%);
}
}
}
}
}
.images-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.image-card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s;
&:hover {
transform: translateY(-2px);
}
img {
width: 100%;
height: 200px;
object-fit: cover;
}
.image-info {
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
.filename {
font-size: 0.875rem;
color: $text-color;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.copy-url {
padding: 4px 8px;
font-size: 0.875rem;
background: $secondary-color;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
background: darken($secondary-color, 10%);
}
}
}
}
.image-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 20px;
.modal-content {
background: white;
border-radius: 8px;
overflow: hidden;
max-width: 90vw;
max-height: 90vh;
img {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
}
.modal-info {
padding: 20px;
h3 {
margin: 0 0 10px;
color: $primary-color;
}
.url-container {
display: flex;
gap: 10px;
margin-bottom: 20px;
input {
flex: 1;
padding: 8px;
border: 1px solid $border-color;
border-radius: 4px;
font-size: 14px;
}
button {
padding: 8px 16px;
background: $secondary-color;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
background: darken($secondary-color, 10%);
}
}
}
.close {
width: 100%;
padding: 8px;
background: #95a5a6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
background: darken(#95a5a6, 10%);
}
}
}
}
}
}