Skip to content

Commit

Permalink
Merge pull request #4 from Kaleb-Rupe/main
Browse files Browse the repository at this point in the history
Added Admin Portal
  • Loading branch information
Kaleb-Rupe authored Aug 20, 2024
2 parents 4db0181 + 8e69378 commit 7c761fb
Show file tree
Hide file tree
Showing 16 changed files with 2,394 additions and 53 deletions.
758 changes: 754 additions & 4 deletions node_modules/.package-lock.json

Large diffs are not rendered by default.

761 changes: 757 additions & 4 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
"@emailjs/browser": "^4.3.3",
"@hookform/resolvers": "^3.9.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.7.4",
"bcryptjs": "^2.4.3",
"dotenv": "^16.4.5",
"firebase": "^10.13.0",
"jsonwebtoken": "^9.0.2",
"mobile-detect": "^1.4.5",
"nodemon": "^3.1.0",
Expand Down
6 changes: 6 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import Home from "./pages/Home.js";
import Services from "./pages/services.js";
import About from "./pages/about.js";
import ContactForm from "./pages/contact.js";
import AdminPage from "./components/AdminPage";
import ForgotPassword from "./components/ForgotPassword";
import VerifyEmail from "./components/VerifyEmail";
import "./css/app.css";

const App = () => (
Expand All @@ -15,6 +18,9 @@ const App = () => (
<Route path="/services" element={<Services />} key="services" />
<Route path="/about" element={<About />} key="about" />
<Route path="/contact" element={<ContactForm />} key="contact" />
<Route path="/admin" element={<AdminPage />} key="admin" />
<Route path="/forgot-password" element={<ForgotPassword />} key="forgot-password" />
<Route path="/verify-email" element={<VerifyEmail />} key="verify-email" />
<Route path="*" element={<ErrorPage />} key="error" />
</Routes>
</Layout>
Expand Down
232 changes: 232 additions & 0 deletions src/components/AdminDashboard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import React, { useState, useRef, useEffect } from 'react';
import { Toast } from 'primereact/toast';
import { FileUpload } from 'primereact/fileupload';
import { ProgressBar } from 'primereact/progressbar';
import { Button } from 'primereact/button';
import { Tooltip } from 'primereact/tooltip';
import { Tag } from 'primereact/tag';
import { Paginator } from 'primereact/paginator';
import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
import { storage } from '../firebaseConfig';
import { ref, uploadBytesResumable, getDownloadURL, listAll, deleteObject } from 'firebase/storage';
import '../css/admin.css';

const AdminDashboard = () => {
const [uploadedImages, setUploadedImages] = useState([]);
const [totalSize, setTotalSize] = useState(0);
const toast = useRef(null);
const fileUploadRef = useRef(null);
const [first, setFirst] = useState(0);
const [rows, setRows] = useState(12);
const [uploadProgress, setUploadProgress] = useState({});

useEffect(() => {
loadImages();
}, []);

const loadImages = async () => {
try {
const imagesRef = ref(storage, 'images');
const imageList = await listAll(imagesRef);
const imageUrls = await Promise.all(
imageList.items.map(async (item) => {
const url = await getDownloadURL(item);
return { name: item.name, url };
})
);
setUploadedImages(imageUrls);
} catch (error) {
console.error("Error loading images:", error);
toast.current.show({ severity: 'error', summary: 'Error', detail: 'Failed to load images' });
}
};

const onTemplateSelect = (e) => {
let _totalSize = totalSize;
let files = e.files;

Object.keys(files).forEach((key) => {
_totalSize += files[key].size || 0;
});

setTotalSize(_totalSize);
};

const onTemplateUpload = async (e) => {
let _totalSize = 0;

for (let file of e.files) {
_totalSize += file.size || 0;
await uploadImage(file);
}

setTotalSize(_totalSize);
toast.current.show({ severity: 'info', summary: 'Success', detail: 'File Uploaded' });
};

const uploadImage = async (file) => {
const storageRef = ref(storage, `images/${file.name}`);
const uploadTask = uploadBytesResumable(storageRef, file);

uploadTask.on('state_changed',
(snapshot) => {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
setUploadProgress(prev => ({...prev, [file.name]: progress}));
},
(error) => {
toast.current.show({ severity: 'error', summary: 'Error', detail: 'Failed to upload image' });
setUploadProgress(prev => ({...prev, [file.name]: 0}));
},
async () => {
const downloadURL = await getDownloadURL(uploadTask.snapshot.ref);
setUploadedImages(prevImages => [...prevImages, { name: file.name, url: downloadURL }]);
setUploadProgress(prev => ({...prev, [file.name]: 100}));

// Clear the file from the FileUpload component
if (fileUploadRef.current) {
fileUploadRef.current.clear();
}
}
);
};

const onTemplateRemove = (file, callback) => {
setTotalSize(totalSize - file.size);
callback();
};

const onTemplateClear = () => {
setTotalSize(0);
};

const headerTemplate = (options) => {
const { className, chooseButton, uploadButton, cancelButton } = options;
const value = totalSize / 10000;
const formatedValue = fileUploadRef && fileUploadRef.current ? fileUploadRef.current.formatSize(totalSize) : '0 B';

return (
<div className={`${className} admin-dashboard-header`}>
<div className="admin-dashboard-header-buttons">
{chooseButton}
{uploadButton}
{cancelButton}
</div>
<div className="admin-dashboard-header-info">
<span>{formatedValue} / 1 MB</span>
<ProgressBar value={value} showValue={false} className="admin-dashboard-progress-bar"></ProgressBar>
</div>
</div>
);
};

const itemTemplate = (file, props) => {
return (
<div className="admin-dashboard-item">
<div className="admin-dashboard-item-info">
<img alt={file.name} role="presentation" src={file.objectURL} width={100} />
<span className="admin-dashboard-item-details">
<span className="admin-dashboard-item-name">{file.name}</span>
<small>{new Date().toLocaleDateString()}</small>
</span>
</div>
<Tag value={props.formatSize} severity="warning" className="admin-dashboard-item-tag" />
<ProgressBar value={uploadProgress[file.name] || 0} className="admin-dashboard-item-progress" />
<Button type="button" icon="pi pi-times" className="p-button-outlined p-button-rounded p-button-danger admin-dashboard-item-button" onClick={() => onTemplateRemove(file, props.onRemove)} />
</div>
);
};

const emptyTemplate = () => {
return (
<div className="admin-dashboard-empty">
<i className="pi pi-image admin-dashboard-empty-icon"></i>
<span className="admin-dashboard-empty-text">
Drag and Drop Image Here
</span>
</div>
);
};

const chooseOptions = { icon: 'pi pi-fw pi-images', iconOnly: true, className: 'custom-choose-btn p-button-rounded p-button-outlined' };
const uploadOptions = { icon: 'pi pi-fw pi-cloud-upload', iconOnly: true, className: 'custom-upload-btn p-button-success p-button-rounded p-button-outlined' };
const cancelOptions = { icon: 'pi pi-fw pi-times', iconOnly: true, className: 'custom-cancel-btn p-button-danger p-button-rounded p-button-outlined' };

const handleDelete = (imageName) => {
confirmDialog({
message: 'Are you sure you want to delete this image?',
header: 'Confirm Delete',
icon: 'pi pi-exclamation-triangle',
acceptClassName: 'p-button-danger',
accept: () => deleteImage(imageName),
reject: () => {}
});
};

const deleteImage = async (imageName) => {
try {
const imageRef = ref(storage, `images/${imageName}`);
await deleteObject(imageRef);
setUploadedImages(uploadedImages.filter(img => img.name !== imageName));
toast.current.show({ severity: 'success', summary: 'Success', detail: 'Image deleted successfully' });
} catch (error) {
toast.current.show({ severity: 'error', summary: 'Error', detail: 'Failed to delete image' });
}
};

const onPageChange = (event) => {
setFirst(event.first);
setRows(event.rows);
};

return (
<div className="admin-dashboard">
<h2>Admin Dashboard</h2>
<Toast ref={toast}></Toast>
<ConfirmDialog />

<Tooltip target=".custom-choose-btn" content="Choose" position="bottom" />
<Tooltip target=".custom-upload-btn" content="Upload" position="bottom" />
<Tooltip target=".custom-cancel-btn" content="Clear" position="bottom" />

<FileUpload
ref={fileUploadRef}
name="demo[]"
multiple
accept="image/*"
maxFileSize={1000000}
customUpload
uploadHandler={onTemplateUpload}
onSelect={onTemplateSelect}
onError={onTemplateClear}
onClear={onTemplateClear}
headerTemplate={headerTemplate}
itemTemplate={itemTemplate}
emptyTemplate={emptyTemplate}
chooseOptions={chooseOptions}
uploadOptions={uploadOptions}
cancelOptions={cancelOptions}
className="admin-dashboard-file-upload"
/>

<div className="image-list">
{uploadedImages.slice(first, first + rows).map((image, index) => (
<div key={index} className="image-item">
<img src={image.url} alt={image.name} />
<Button
icon="pi pi-trash"
onClick={() => handleDelete(image.name)}
/>
</div>
))}
</div>
<Paginator
first={first}
rows={rows}
totalRecords={uploadedImages.length}
onPageChange={onPageChange}
/>
</div>
);
};

export default AdminDashboard;
66 changes: 66 additions & 0 deletions src/components/AdminLogin.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useState, useRef } from 'react';
import { InputText } from 'primereact/inputtext';
import { Button } from 'primereact/button';
import { Password } from 'primereact/password';
import { getAuth, signInWithEmailAndPassword, sendEmailVerification } from 'firebase/auth';
import { Link } from 'react-router-dom';
import { Toast } from 'primereact/toast';
import '../css/admin.css';

const AdminLogin = ({ onLogin }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const toast = useRef(null);

const handleLogin = async (e) => {
e.preventDefault();
setLoading(true);
try {
const auth = getAuth();
const userCredential = await signInWithEmailAndPassword(auth, email, password);
if (!userCredential.user.emailVerified) {
await sendEmailVerification(userCredential.user);
toast.current.show({ severity: 'warn', summary: 'Email not verified', detail: 'Please check your email to verify your account.' });
} else {
onLogin();
}
} catch (error) {
let errorMessage = 'An error occurred during login. Please try again.';
if (error.code === 'auth/user-not-found' || error.code === 'auth/wrong-password') {
errorMessage = 'Invalid email or password.';
} else if (error.code === 'auth/too-many-requests') {
errorMessage = 'Too many failed login attempts. Please try again later.';
}
if (toast.current) {
toast.current.show({ severity: 'error', summary: 'Login Error', detail: errorMessage });
}
} finally {
setLoading(false);
}
};

return (
<form onSubmit={handleLogin} className="admin-login">
<Toast ref={toast} />
<h2>Admin Login</h2>
<div className="p-fluid">
<div className="p-field">
<label htmlFor="email">Email</label>
<InputText id="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<div className="p-field">
<label htmlFor="password">Password</label>
<Password id="password" value={password} onChange={(e) => setPassword(e.target.value)} feedback={false} />
</div>
<Button type="submit" label="Login" loading={loading} />
<div className="auth-links">
<Link to="/forgot-password">Forgot Password?</Link>
<Link to="/verify-email">Resend Verification Email</Link>
</div>
</div>
</form>
);
};

export default AdminLogin;
51 changes: 51 additions & 0 deletions src/components/AdminPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { useState, useEffect } from "react";
import { getAuth, onAuthStateChanged, signOut } from "firebase/auth";
import { analytics } from '../firebaseConfig';
import { logEvent } from 'firebase/analytics';
import AdminLogin from "./AdminLogin";
import AdminDashboard from "./AdminDashboard";
import Header from "../shared/Header";
import "../css/admin.css";

const AdminPage = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const auth = getAuth();

useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setIsLoggedIn(!!user);
});

return () => unsubscribe();
}, [auth]);

const handleLogin = () => {
setIsLoggedIn(true);
logEvent(analytics, 'login');
};

const handleLogout = async () => {
try {
await signOut(auth);
setIsLoggedIn(false);
logEvent(analytics, 'logout');
} catch (error) {
console.error("Error signing out:", error);
}
};

return (
<div className="admin-page">
<Header isLoggedIn={isLoggedIn} onLogin={handleLogin} onLogout={handleLogout} />
{isLoggedIn ? (
<>
<AdminDashboard />
</>
) : (
<AdminLogin onLogin={handleLogin} />
)}
</div>
);
};

export default AdminPage;
Loading

0 comments on commit 7c761fb

Please sign in to comment.