From 66dc187295bbc68ec7909e2be1df26573fc5dd0a Mon Sep 17 00:00:00 2001 From: jackiettran <41605212+jackiettran@users.noreply.github.com> Date: Tue, 19 Aug 2025 17:28:22 -0400 Subject: [PATCH] simplified create item. Restructured profile. Simplified availability --- backend/routes/items.js | 88 +- frontend/src/App.css | 7 - .../src/components/AvailabilitySettings.tsx | 165 ++++ frontend/src/components/Navbar.tsx | 301 ++++--- frontend/src/pages/CreateItem.tsx | 190 ++--- frontend/src/pages/EditItem.tsx | 14 +- frontend/src/pages/ItemList.tsx | 60 +- frontend/src/pages/MyListings.tsx | 361 ++++---- frontend/src/pages/MyRentals.tsx | 208 ++--- frontend/src/pages/Profile.tsx | 794 ++++++++++-------- frontend/src/types/index.ts | 1 - 11 files changed, 1317 insertions(+), 872 deletions(-) create mode 100644 frontend/src/components/AvailabilitySettings.tsx diff --git a/backend/routes/items.js b/backend/routes/items.js index 1fdba04..0364c5b 100644 --- a/backend/routes/items.js +++ b/backend/routes/items.js @@ -1,34 +1,36 @@ -const express = require('express'); -const { Op } = require('sequelize'); -const { Item, User, Rental } = require('../models'); // Import from models/index.js to get models with associations -const { authenticateToken } = require('../middleware/auth'); +const express = require("express"); +const { Op } = require("sequelize"); +const { Item, User, Rental } = require("../models"); // Import from models/index.js to get models with associations +const { authenticateToken } = require("../middleware/auth"); const router = express.Router(); -router.get('/', async (req, res) => { +router.get("/", async (req, res) => { try { const { - isPortable, minPrice, maxPrice, location, + city, + zipCode, search, page = 1, - limit = 20 + limit = 20, } = req.query; const where = {}; - - if (isPortable !== undefined) where.isPortable = isPortable === 'true'; + if (minPrice || maxPrice) { where.pricePerDay = {}; if (minPrice) where.pricePerDay[Op.gte] = minPrice; if (maxPrice) where.pricePerDay[Op.lte] = maxPrice; } if (location) where.location = { [Op.iLike]: `%${location}%` }; + if (city) where.city = { [Op.iLike]: `%${city}%` }; + if (zipCode) where.zipCode = { [Op.iLike]: `%${zipCode}%` }; if (search) { where[Op.or] = [ { name: { [Op.iLike]: `%${search}%` } }, - { description: { [Op.iLike]: `%${search}%` } } + { description: { [Op.iLike]: `%${search}%` } }, ]; } @@ -36,37 +38,43 @@ router.get('/', async (req, res) => { const { count, rows } = await Item.findAndCountAll({ where, - include: [{ model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] }], + include: [ + { + model: User, + as: "owner", + attributes: ["id", "username", "firstName", "lastName"], + }, + ], limit: parseInt(limit), offset: parseInt(offset), - order: [['createdAt', 'DESC']] + order: [["createdAt", "DESC"]], }); res.json({ items: rows, totalPages: Math.ceil(count / limit), currentPage: parseInt(page), - totalItems: count + totalItems: count, }); } catch (error) { res.status(500).json({ error: error.message }); } }); -router.get('/recommendations', authenticateToken, async (req, res) => { +router.get("/recommendations", authenticateToken, async (req, res) => { try { const userRentals = await Rental.findAll({ where: { renterId: req.user.id }, - include: [{ model: Item, as: 'item' }] + include: [{ model: Item, as: "item" }], }); // For now, just return random available items as recommendations const recommendations = await Item.findAll({ where: { - availability: true + availability: true, }, limit: 10, - order: [['createdAt', 'DESC']] + order: [["createdAt", "DESC"]], }); res.json(recommendations); @@ -75,14 +83,20 @@ router.get('/recommendations', authenticateToken, async (req, res) => { } }); -router.get('/:id', async (req, res) => { +router.get("/:id", async (req, res) => { try { const item = await Item.findByPk(req.params.id, { - include: [{ model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] }] + include: [ + { + model: User, + as: "owner", + attributes: ["id", "username", "firstName", "lastName"], + }, + ], }); if (!item) { - return res.status(404).json({ error: 'Item not found' }); + return res.status(404).json({ error: "Item not found" }); } res.json(item); @@ -91,15 +105,21 @@ router.get('/:id', async (req, res) => { } }); -router.post('/', authenticateToken, async (req, res) => { +router.post("/", authenticateToken, async (req, res) => { try { const item = await Item.create({ ...req.body, - ownerId: req.user.id + ownerId: req.user.id, }); const itemWithOwner = await Item.findByPk(item.id, { - include: [{ model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] }] + include: [ + { + model: User, + as: "owner", + attributes: ["id", "username", "firstName", "lastName"], + }, + ], }); res.status(201).json(itemWithOwner); @@ -108,22 +128,28 @@ router.post('/', authenticateToken, async (req, res) => { } }); -router.put('/:id', authenticateToken, async (req, res) => { +router.put("/:id", authenticateToken, async (req, res) => { try { const item = await Item.findByPk(req.params.id); if (!item) { - return res.status(404).json({ error: 'Item not found' }); + return res.status(404).json({ error: "Item not found" }); } if (item.ownerId !== req.user.id) { - return res.status(403).json({ error: 'Unauthorized' }); + return res.status(403).json({ error: "Unauthorized" }); } await item.update(req.body); const updatedItem = await Item.findByPk(item.id, { - include: [{ model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] }] + include: [ + { + model: User, + as: "owner", + attributes: ["id", "username", "firstName", "lastName"], + }, + ], }); res.json(updatedItem); @@ -132,16 +158,16 @@ router.put('/:id', authenticateToken, async (req, res) => { } }); -router.delete('/:id', authenticateToken, async (req, res) => { +router.delete("/:id", authenticateToken, async (req, res) => { try { const item = await Item.findByPk(req.params.id); if (!item) { - return res.status(404).json({ error: 'Item not found' }); + return res.status(404).json({ error: "Item not found" }); } if (item.ownerId !== req.user.id) { - return res.status(403).json({ error: 'Unauthorized' }); + return res.status(403).json({ error: "Unauthorized" }); } await item.destroy(); @@ -151,4 +177,4 @@ router.delete('/:id', authenticateToken, async (req, res) => { } }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/frontend/src/App.css b/frontend/src/App.css index 2851728..af72036 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -12,13 +12,6 @@ main { font-size: 1.5rem; } -.card { - transition: transform 0.2s; -} - -.card:hover { - transform: translateY(-5px); -} .dropdown-toggle::after { display: none; diff --git a/frontend/src/components/AvailabilitySettings.tsx b/frontend/src/components/AvailabilitySettings.tsx new file mode 100644 index 0000000..e5f1332 --- /dev/null +++ b/frontend/src/components/AvailabilitySettings.tsx @@ -0,0 +1,165 @@ +import React from 'react'; + +interface AvailabilityData { + generalAvailableAfter: string; + generalAvailableBefore: string; + specifyTimesPerDay: boolean; + weeklyTimes: { + sunday: { availableAfter: string; availableBefore: string }; + monday: { availableAfter: string; availableBefore: string }; + tuesday: { availableAfter: string; availableBefore: string }; + wednesday: { availableAfter: string; availableBefore: string }; + thursday: { availableAfter: string; availableBefore: string }; + friday: { availableAfter: string; availableBefore: string }; + saturday: { availableAfter: string; availableBefore: string }; + }; +} + +interface AvailabilitySettingsProps { + data: AvailabilityData; + onChange: (field: string, value: string | boolean) => void; + onWeeklyTimeChange: (day: string, field: 'availableAfter' | 'availableBefore', value: string) => void; + showTitle?: boolean; +} + +const AvailabilitySettings: React.FC = ({ + data, + onChange, + onWeeklyTimeChange, + showTitle = true +}) => { + const generateTimeOptions = () => { + const options = []; + for (let hour = 0; hour < 24; hour++) { + const time24 = `${hour.toString().padStart(2, '0')}:00`; + const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; + const period = hour < 12 ? 'AM' : 'PM'; + const time12 = `${hour12}:00 ${period}`; + options.push({ value: time24, label: time12 }); + } + return options; + }; + + const handleGeneralChange = (e: React.ChangeEvent) => { + const { name, value, type } = e.target; + if (type === 'checkbox') { + const checked = (e.target as HTMLInputElement).checked; + onChange(name, checked); + } else { + onChange(name, value); + } + }; + + return ( +
+ {showTitle &&
Availability
} + + {/* General Times */} +
+
+ + +
+
+ + +
+
+ + {/* Checkbox for day-specific times */} +
+ + +
+ + {/* Weekly Times */} + {data.specifyTimesPerDay && ( +
+
Weekly Schedule
+ {Object.entries(data.weeklyTimes).map(([day, times]) => ( +
+
+ +
+
+ +
+
+ +
+
+ ))} +
+ )} +
+ ); +}; + +export default AvailabilitySettings; \ No newline at end of file diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 7806549..d0c9457 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,138 +1,211 @@ -import React, { useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import { useAuth } from '../contexts/AuthContext'; -import AuthModal from './AuthModal'; +import React, { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { useAuth } from "../contexts/AuthContext"; +import AuthModal from "./AuthModal"; const Navbar: React.FC = () => { const { user, logout } = useAuth(); const navigate = useNavigate(); const [showAuthModal, setShowAuthModal] = useState(false); - const [authModalMode, setAuthModalMode] = useState<'login' | 'signup'>('login'); + const [authModalMode, setAuthModalMode] = useState<"login" | "signup">( + "login" + ); + const [searchFilters, setSearchFilters] = useState({ + search: "", + location: "", + }); const handleLogout = () => { logout(); - navigate('/'); + navigate("/"); }; - const openAuthModal = (mode: 'login' | 'signup') => { + const openAuthModal = (mode: "login" | "signup") => { setAuthModalMode(mode); setShowAuthModal(true); }; + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + const params = new URLSearchParams(); + + if (searchFilters.search.trim()) { + params.append("search", searchFilters.search.trim()); + } + if (searchFilters.location.trim()) { + // Check if location looks like a zip code (5 digits) or city name + const location = searchFilters.location.trim(); + if (/^\d{5}(-\d{4})?$/.test(location)) { + params.append("zipCode", location); + } else { + params.append("city", location); + } + } + + const queryString = params.toString(); + navigate(`/items${queryString ? `?${queryString}` : ""}`); + + // Clear search after navigating + setSearchFilters({ search: "", location: "" }); + }; + + const handleSearchInputChange = ( + field: "search" | "location", + value: string + ) => { + setSearchFilters((prev) => ({ ...prev, [field]: value })); + }; + return ( <> - - - setShowAuthModal(false)} - initialMode={authModalMode} - /> - + + + setShowAuthModal(false)} + initialMode={authModalMode} + /> + ); }; -export default Navbar; \ No newline at end of file +export default Navbar; diff --git a/frontend/src/pages/CreateItem.tsx b/frontend/src/pages/CreateItem.tsx index 081a7d5..4fe6258 100644 --- a/frontend/src/pages/CreateItem.tsx +++ b/frontend/src/pages/CreateItem.tsx @@ -2,15 +2,12 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import { useAuth } from "../contexts/AuthContext"; import api from "../services/api"; -import AvailabilityCalendar from "../components/AvailabilityCalendar"; +import AvailabilitySettings from "../components/AvailabilitySettings"; interface ItemFormData { name: string; description: string; pickUpAvailable: boolean; - localDeliveryAvailable: boolean; - localDeliveryRadius?: number; - shippingAvailable: boolean; inPlaceUseAvailable: boolean; pricePerHour?: number; pricePerDay?: number; @@ -27,13 +24,18 @@ interface ItemFormData { rules?: string; minimumRentalDays: number; needsTraining: boolean; - unavailablePeriods?: Array<{ - id: string; - startDate: Date; - endDate: Date; - startTime?: string; - endTime?: string; - }>; + generalAvailableAfter: string; + generalAvailableBefore: string; + specifyTimesPerDay: boolean; + weeklyTimes: { + sunday: { availableAfter: string; availableBefore: string }; + monday: { availableAfter: string; availableBefore: string }; + tuesday: { availableAfter: string; availableBefore: string }; + wednesday: { availableAfter: string; availableBefore: string }; + thursday: { availableAfter: string; availableBefore: string }; + friday: { availableAfter: string; availableBefore: string }; + saturday: { availableAfter: string; availableBefore: string }; + }; } const CreateItem: React.FC = () => { @@ -45,9 +47,6 @@ const CreateItem: React.FC = () => { name: "", description: "", pickUpAvailable: false, - localDeliveryAvailable: false, - localDeliveryRadius: 25, - shippingAvailable: false, inPlaceUseAvailable: false, pricePerDay: undefined, replacementCost: 0, @@ -57,10 +56,21 @@ const CreateItem: React.FC = () => { city: "", state: "", zipCode: "", - country: "", + country: "US", minimumRentalDays: 1, needsTraining: false, - unavailablePeriods: [], + generalAvailableAfter: "09:00", + generalAvailableBefore: "17:00", + specifyTimesPerDay: false, + weeklyTimes: { + sunday: { availableAfter: "09:00", availableBefore: "17:00" }, + monday: { availableAfter: "09:00", availableBefore: "17:00" }, + tuesday: { availableAfter: "09:00", availableBefore: "17:00" }, + wednesday: { availableAfter: "09:00", availableBefore: "17:00" }, + thursday: { availableAfter: "09:00", availableBefore: "17:00" }, + friday: { availableAfter: "09:00", availableBefore: "17:00" }, + saturday: { availableAfter: "09:00", availableBefore: "17:00" }, + }, }); const [imageFiles, setImageFiles] = useState([]); const [imagePreviews, setImagePreviews] = useState([]); @@ -126,6 +136,24 @@ const CreateItem: React.FC = () => { } }; + const handleWeeklyTimeChange = ( + day: string, + field: "availableAfter" | "availableBefore", + value: string + ) => { + setFormData((prev) => ({ + ...prev, + weeklyTimes: { + ...prev.weeklyTimes, + [day]: { + ...prev.weeklyTimes[day as keyof typeof prev.weeklyTimes], + [field]: value, + }, + }, + })); + }; + + const handleImageChange = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); @@ -170,7 +198,12 @@ const CreateItem: React.FC = () => {
- + +
+ Have pictures of everything that's included +
{ multiple disabled={imageFiles.length >= 5} /> -
- Upload up to 5 images of your item -
{imagePreviews.length > 0 && ( @@ -252,6 +282,13 @@ const CreateItem: React.FC = () => { {/* Location Card */}
+
+ + + Your address is private. This will only be used to show + renters a general area. + +
-
- - -
-
- - -
{
+ {/* Availability Card */} +
+
+ { + setFormData(prev => ({ ...prev, [field]: value })); + }} + onWeeklyTimeChange={handleWeeklyTimeChange} + /> +
+
+ {/* Pricing Card */}
@@ -518,9 +515,12 @@ const CreateItem: React.FC = () => {
-
- {/* Availability Schedule Card */} -
-
-

- Select dates when the item is NOT available for rent -

- - setFormData((prev) => ({ - ...prev, - unavailablePeriods: periods, - })) - } - mode="owner" - /> -
-
- {/* Rules & Guidelines Card */}
@@ -578,7 +556,7 @@ const CreateItem: React.FC = () => {