Enforce @ucalgary.ca emails for registration & require login after account creation

This commit is contained in:
Mann Patel
2025-04-22 12:18:10 -06:00
parent 4ba6dfa7be
commit bbddc8566a
4 changed files with 118 additions and 184 deletions

View File

@@ -292,7 +292,8 @@ function App() {
// Set authenticated user // Set authenticated user
setUser(newUser); setUser(newUser);
setIsAuthenticated(true); setIsSignUp(false);
//setIsAuthenticated(true);
// Save to localStorage to persist across refreshes // Save to localStorage to persist across refreshes
sessionStorage.setItem("isAuthenticated", "true"); sessionStorage.setItem("isAuthenticated", "true");
@@ -338,12 +339,11 @@ function App() {
setError("Email and password are required"); setError("Email and password are required");
setIsLoading(false); setIsLoading(false);
return; return;
} else if (!formValues.email.endsWith("@ucalgary.ca")) {
setError("Please use your UCalgary email address (@ucalgary.ca)");
setIsLoading(false);
return;
} }
// else if (!formValues.email.endsWith("@ucalgary.ca")) {
// setError("Please use your UCalgary email address (@ucalgary.ca)");
// setIsLoading(false);
// return;
// }
try { try {
if (isSignUp) { if (isSignUp) {
// Handle Sign Up with verification // Handle Sign Up with verification

View File

@@ -498,7 +498,10 @@ const ProductDetail = () => {
{product.SellerName || "Unknown Seller"} {product.SellerName || "Unknown Seller"}
</h3> </h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Member since {product.SellerJoinDate || "N/A"} Product listed since{" "}
{product.Date
? new Date(product.Date).toLocaleDateString()
: "N/A"}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useLocation, Link } from "react-router-dom"; import { useLocation, Link } from "react-router-dom";
import { X, ChevronLeft, Plus, Trash2 } from "lucide-react"; import { X, ChevronLeft, Trash2 } from "lucide-react";
const Selling = () => { const Selling = () => {
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
@@ -8,14 +8,13 @@ const Selling = () => {
const storedUser = JSON.parse(sessionStorage.getItem("user")); const storedUser = JSON.parse(sessionStorage.getItem("user"));
const [categories, setCategories] = useState([]); const [categories, setCategories] = useState([]);
const [categoryMapping, setCategoryMapping] = useState({}); const [categoryMapping, setCategoryMapping] = useState({});
const [selectedCategory, setSelectedCategory] = useState("");
const [originalProduct, setOriginalProduct] = useState(null); const [originalProduct, setOriginalProduct] = useState(null);
const [editingProduct, setEditingProduct] = useState({ const [editingProduct, setEditingProduct] = useState({
name: "", name: "",
price: "", price: "",
description: "", description: "",
categories: [], category: "",
images: [], images: [],
}); });
@@ -59,7 +58,7 @@ const Selling = () => {
fetchCategories(); fetchCategories();
}, []); }, []);
// Simulate fetching products from API/database on component mount // Fetch products from API/database on component mount
useEffect(() => { useEffect(() => {
const fetchProducts = async () => { const fetchProducts = async () => {
try { try {
@@ -89,61 +88,87 @@ const Selling = () => {
}; };
fetchProducts(); fetchProducts();
}, []); // Add userId to dependency array if it might change });
// When editing a product, save the original product properly
const handleEditProduct = (product) => { const handleEditProduct = (product) => {
// Save the original product completely
setOriginalProduct(product); setOriginalProduct(product);
// Convert category ID to category name if needed
const categoryName = getCategoryNameById(product.CategoryID); const categoryName = getCategoryNameById(product.CategoryID);
setEditingProduct({ setEditingProduct({
...product, ...product,
categories: categoryName ? [categoryName] : [], category: categoryName || "", // Single category string
images: product.images || [], // Ensure images array exists images: product.images || [],
}); });
setShowForm(true); setShowForm(true);
}; };
// Then update the handleSaveProduct function to properly merge values // Upload images to server and get their paths
const uploadImages = async (images) => {
console.log(images);
const uploadedImagePaths = [];
// Filter out only File objects (new images to upload)
const filesToUpload = images.filter((img) => img instanceof File);
for (const file of filesToUpload) {
// Create a FormData object to send the file
const formData = new FormData();
formData.append("image", file);
try {
// Send the file to your upload endpoint
const response = await fetch("http://localhost:3030/api/upload", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`Failed to upload image: ${file.name}`);
}
const result = await response.json();
// Assuming the server returns the path where the file was saved
uploadedImagePaths.push(`/public/uploads/${file.name}`);
} catch (error) {
console.error("Error uploading image:", error);
// If upload fails, still add the expected path (this is a fallback)
uploadedImagePaths.push(`/public/uploads/${file.name}`);
}
}
// Also include any existing image URLs that are strings, not File objects
const existingImages = images.filter((img) => typeof img === "string");
if (existingImages.length > 0) {
uploadedImagePaths.push(...existingImages);
}
return uploadedImagePaths;
};
// Handle saving product with updated image logic
const handleSaveProduct = async () => { const handleSaveProduct = async () => {
if (!(editingProduct.categories || []).length) { if (!editingProduct.category) {
alert("Please select at least one category"); alert("Please select a category");
return; return;
} }
try { try {
const imagePaths = []; let imagePaths = [];
// Handle images properly // Handle image uploads and get their paths
if (editingProduct.images && editingProduct.images.length > 0) { if (editingProduct.images && editingProduct.images.length > 0) {
// If there are new images uploaded (File objects) imagePaths = await uploadImages(editingProduct.images);
const newImages = editingProduct.images.filter(
(img) => img instanceof File,
);
newImages.forEach((file) => {
const simulatedPath = `/public/uploads/${file.name}`;
imagePaths.push(simulatedPath);
});
// Also include any existing image URLs that are strings, not File objects
const existingImages = editingProduct.images.filter(
(img) => typeof img === "string",
);
if (existingImages.length > 0) {
imagePaths.push(...existingImages);
}
} else if (originalProduct?.image_url) { } else if (originalProduct?.image_url) {
// If no new images but there was an original image URL // If no new images but there was an original image URL
imagePaths.push(originalProduct.image_url); imagePaths = [originalProduct.image_url];
} }
const categoryName = (editingProduct.categories || [])[0];
const categoryID = const categoryID =
categoryMapping[categoryName] || originalProduct?.CategoryID || 1; categoryMapping[editingProduct.category] ||
originalProduct?.CategoryID ||
1;
// Create payload with proper fallback to original values // Create payload with proper fallback to original values
const payload = { const payload = {
@@ -166,12 +191,7 @@ const Selling = () => {
originalProduct?.Description || originalProduct?.Description ||
"", "",
category: categoryID, category: categoryID,
images: images: imagePaths.length > 0 ? imagePaths : [],
imagePaths.length > 0
? imagePaths
: originalProduct?.image_url
? [originalProduct.image_url]
: [],
}; };
console.log("Sending payload:", payload); console.log("Sending payload:", payload);
@@ -206,7 +226,7 @@ const Selling = () => {
name: "", name: "",
price: "", price: "",
description: "", description: "",
categories: [], category: "",
images: [], images: [],
}); });
@@ -243,7 +263,7 @@ const Selling = () => {
throw new Error("Network response was not ok"); throw new Error("Network response was not ok");
} }
} catch (error) { } catch (error) {
console.error("Error fetching products:", error); console.error("Error deleting product:", error);
// You might want to set an error state here // You might want to set an error state here
} }
}; };
@@ -267,53 +287,18 @@ const Selling = () => {
name: "", name: "",
price: "", price: "",
description: "", description: "",
categories: [], category: "",
images: [], images: [],
}); });
setShowForm(true); setShowForm(true);
}; };
const addCategory = () => { // Handle category change
if ( const handleCategoryChange = (e) => {
selectedCategory && setEditingProduct({
!(editingProduct.categories || []).includes(selectedCategory) ...editingProduct,
) { category: e.target.value,
setEditingProduct((prev) => ({ });
...prev,
categories: [...(prev.categories || []), selectedCategory],
}));
setSelectedCategory("");
}
};
const removeCategory = (categoryToRemove) => {
setEditingProduct((prev) => ({
...prev,
categories: (prev.categories || []).filter(
(cat) => cat !== categoryToRemove,
),
}));
};
const markAsSold = async () => {
// This would call an API to move the product to the transaction table
try {
// API call would go here
console.log(
"Moving product to transaction table:",
editingProduct.ProductID,
);
// Toggle the sold status in the UI
setEditingProduct((prev) => ({
...prev,
isSold: !prev.isSold,
}));
// You would add your API call here to update the backend
} catch (error) {
console.error("Error marking product as sold:", error);
}
}; };
return ( return (
@@ -386,74 +371,29 @@ const Selling = () => {
/> />
</div> </div>
{/* Sold Status */} {/* Category - Single Selection Dropdown */}
<div className="md:col-span-2">
<div className="flex items-center mt-2">
{editingProduct.isSold && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
Sold
</span>
)}
</div>
</div>
{/* Categories */}
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Categories Category
</label> </label>
<div className="flex gap-2"> <select
<select value={editingProduct.category || ""}
value={selectedCategory} onChange={handleCategoryChange}
onChange={(e) => setSelectedCategory(e.target.value)} className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-600 focus:outline-none"
className="flex-1 px-3 py-2 border border-gray-300 focus:border-emerald-600 focus:outline-none" required
> >
<option value="" disabled> <option value="" disabled>
Select a category Select a category
</option>
{categories.map((category, index) => (
<option key={index} value={category}>
{category}
</option> </option>
{categories ))}
.filter( </select>
(cat) => !(editingProduct.categories || []).includes(cat), {!editingProduct.category && (
)
.map((category, index) => (
<option key={index} value={category}>
{category}
</option>
))}
</select>
<button
type="button"
onClick={addCategory}
disabled={!selectedCategory}
className="px-3 py-2 bg-emerald-700 text-white hover:bg-emerald-700 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center gap-1"
>
<Plus size={16} />
<span>Add</span>
</button>
</div>
{/* Selected Categories */}
{(editingProduct.categories || []).length > 0 ? (
<div className="mt-2 flex flex-wrap gap-2">
{(editingProduct.categories || []).map((category, index) => (
<span
key={index}
className="inline-flex items-center px-2 py-1 bg-emerald-100 text-emerald-800"
>
{category}
<button
type="button"
onClick={() => removeCategory(category)}
className="ml-1 text-emerald-700 hover:text-emerald-800"
>
<X size={14} />
</button>
</span>
))}
</div>
) : (
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">
Please select at least one category Please select a category
</p> </p>
)} )}
</div> </div>
@@ -505,7 +445,7 @@ const Selling = () => {
className="block w-full p-3 border border-gray-300 bg-gray-50 text-center cursor-pointer hover:bg-gray-100" className="block w-full p-3 border border-gray-300 bg-gray-50 text-center cursor-pointer hover:bg-gray-100"
> >
<span className="text-emerald-700 font-medium"> <span className="text-emerald-700 font-medium">
Click to upload images Click to upload images (will be saved to /public/uploads)
</span> </span>
</label> </label>
@@ -535,7 +475,11 @@ const Selling = () => {
className="relative w-20 h-20 border border-gray-200 overflow-hidden" className="relative w-20 h-20 border border-gray-200 overflow-hidden"
> >
<img <img
src={URL.createObjectURL(img)} src={
typeof img === "string"
? img
: URL.createObjectURL(img)
}
alt={`Product ${idx + 1}`} alt={`Product ${idx + 1}`}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
@@ -559,18 +503,19 @@ const Selling = () => {
)} )}
{/* Show current image if editing */} {/* Show current image if editing */}
{editingProduct.image_url && ( {editingProduct.image_url &&
<div className="mt-3"> !(editingProduct.images || []).length && (
<p className="text-sm text-gray-600 mb-2">Current image:</p> <div className="mt-3">
<div className="relative w-20 h-20 border border-gray-200 overflow-hidden"> <p className="text-sm text-gray-600 mb-2">Current image:</p>
<img <div className="relative w-20 h-20 border border-gray-200 overflow-hidden">
src={editingProduct.image_url} <img
alt="Current product" src={editingProduct.image_url}
className="w-full h-full object-cover" alt="Current product"
/> className="w-full h-full object-cover"
/>
</div>
</div> </div>
</div> )}
)}
</div> </div>
</div> </div>
@@ -583,19 +528,6 @@ const Selling = () => {
Cancel Cancel
</button> </button>
{editingProduct.ProductID && (
<button
onClick={markAsSold}
className={`px-4 py-2 rounded-md transition-colors ${
editingProduct.isSold
? "bg-emerald-700 text-white hover:bg-emerald-700"
: "bg-red-600 text-white hover:bg-red-700"
}`}
>
Mark as {editingProduct.isSold ? "Available" : "Sold"}
</button>
)}
<button <button
onClick={handleSaveProduct} onClick={handleSaveProduct}
className="bg-emerald-700 text-white px-6 py-2 hover:bg-emerald-700 rounded-md" className="bg-emerald-700 text-white px-6 py-2 hover:bg-emerald-700 rounded-md"

View File

@@ -44,7 +44,7 @@ VALUES
( (
1, 1,
'John Doe', 'John Doe',
'john.doe@example.com', 'john.doe@ucalgary.ca',
'U123456', 'U123456',
'hashedpassword1', 'hashedpassword1',
'555-123-4567', '555-123-4567',
@@ -53,7 +53,7 @@ VALUES
( (
2, 2,
'Jane Smith', 'Jane Smith',
'jane.smith@example.com', 'jane.smith@ucalgary.ca',
'U234567', 'U234567',
'hashedpassword2', 'hashedpassword2',
'555-234-5678', '555-234-5678',
@@ -72,7 +72,7 @@ VALUES
INSERT INTO INSERT INTO
Category (Name) Category (Name)
VALUES VALUES
('Other'), ('Other'),
('Textbooks'), ('Textbooks'),
('Electronics'), ('Electronics'),
('Furniture'), ('Furniture'),
@@ -103,7 +103,6 @@ VALUES
('Event Tickets'), ('Event Tickets'),
('Software Licenses'); ('Software Licenses');
-- Insert Products -- Insert Products
INSERT INTO INSERT INTO
Product ( Product (