One Man's Treasure is a platform for unwanted items where a user can display an item they don't want to dispose of but would like nothing in return for it. Depending on another users claims and criteria, they may receive the item.
Table of Contents
- One Man's Treasure - Project Overview - Bugs:
- Overview
- Tech Stack
- Project Structure
- API Endpoints
- Database Schema
- Key Features
- Heroku Deployment Setup
- Required Versions
- Environment Variables
- Getting Started (Development)
- Notable Implementation Details
- Recommended Improvements
- Testing
- Redux Store Architecture
- Developers
- Currently you can't givaway your item to the first user who claimed it.
- Longer comments do not word-wrap.
- Listings do not automatically populate the profile. (Must log out and log back in).
Author: Erwin
One Man's Treasure is a platform for sharing unwanted items where users can display items they no longer need but don't want to dispose of. Other users can express interest and potentially receive these items based on their claims and a karma-based system.
- Node.js (v8.11.1) - Runtime environment
- Express.js (v4.16.3) - Web framework
- MongoDB with Mongoose (v5.1.2) - Database and ODM
- bcrypt (v1.0.3) - Password hashing
- express-session - Session management
- dotenv - Environment variable management
- morgan - HTTP request logging
- cors - Cross-origin resource sharing
- body-parser - Request body parsing
- React (v16.4.0) - UI framework
- Redux (v4.0.0) with Redux-Promise - State management
- React-Redux (v5.0.7) - React-Redux bindings
- Axios (v0.18.0) - HTTP client
- jQuery (v3.3.1) - DOM manipulation and AJAX
- Moment.js (v2.22.2) - Date/time handling
- Webpack (v2.2.1) - Module bundler
- Babel - JavaScript transpiler (ES2015, React presets)
- nodemon - Development server auto-restart
- Google Maps API - Location services and map display
- Imgur API - Image hosting
one-mans-treasure/
├── client/
│ ├── dist/ # Built files and static assets
│ │ ├── index.html # Main HTML template
│ │ └── bundle.js # Webpack compiled bundle
│ └── src/ # React source code
│ ├── components/ # React components
│ ├── actions/ # Redux actions
│ ├── reducers/ # Redux reducers
│ └── services/ # API service functions
├── server/
│ ├── app.js # Express server entry point
│ ├── routes.js # API route definitions
│ ├── controllers/ # Route handlers
│ └── services/ # Business logic
├── database/
│ ├── index.js # Database connection
│ ├── Users.js # User model and methods
│ ├── Listings.js # Listing model and methods
│ └── Comments.js # Comment model
├── package.json # Dependencies and scripts
├── Procfile # Heroku deployment config
└── webpack.config.js # Webpack configuration
Purpose: Create new user account Request Body:
{
"user": "string", // Username
"pw": "string", // Password (will be hashed)
"created_at": "Date" // Account creation date
}Response: Returns user session data with user object
Purpose: User authentication Request Body:
{
"user": "string", // Username
"pw": "string" // Password
}Response: Returns session data with user object or false if authentication fails
Purpose: Update user information URL Parameters:
userId- User ID to update Request Body:
{
"user": "string", // New username
"pw": "string", // New password
"originalPw": "string" // Original password for verification
}Response: Returns updated user information
Purpose: Get users interested in a listing Request Body:
{
"users": ["userId1", "userId2"] // Array of user IDs
}Response: Returns array of user objects
Purpose: Update user karma Request Body:
{
"userId": "string", // User ID
"claimed": "boolean" // true to decrease karma, false to increase
}Response: Returns updated user data
Purpose: Get all available listings (with optional search query) Query Parameters:
query(optional) - Search term to filter listings by title, description, or location Response: Returns array of listing objects (limited to 12 results)
Purpose: Create a new listing Request Body:
{
"title": "string", // Listing title
"desc": "string", // Description
"loc": "string", // Location
"userId": "string", // User ID of the lister
"image": "string", // Imgur URL for photo
"username": "string", // Username of the lister
"comments": [] // Array of comment IDs
}Response: Returns saved listing object
Purpose: Update an existing listing URL Parameters:
listingId- Listing ID to update Request Body:
{
"title": "string", // Updated title
"desc": "string", // Updated description
"image": "string", // Updated image URL
"loc": "string" // Updated location
}Response: Returns updated listing object
Purpose: Delete a listing URL Parameters:
listingId- Listing ID to delete Response: Returns deleted listing object
Purpose: Get a specific listing with comments URL Parameters:
listingId- Listing ID to fetch Response: Returns listing object populated with comments
Purpose: Get user's claimed listings Request Body:
{
"listingsId": ["listingId1", "listingId2"] // Array of listing IDs
}Response: Returns array of claimed listing objects
Purpose: Give away a listing to a specific user Request Body:
{
"listingId": "string", // Listing ID to give away
"userId": "string" // User ID to give the listing to
}Response: Returns success/failure status
Purpose: Express/withdraw interest in a listing Request Body:
{
"id": "string", // Listing ID
"userId": "string", // User ID expressing interest
"claimed": "string" // "true" to withdraw interest, "false" to express interest
}Response: Returns updated listing object
Purpose: Add a comment to a listing Request Body:
{
"listingId": "string", // Listing ID to comment on
"userId": "string", // User ID of commenter
"content": "string", // Comment content
"timestamp": "Date" // Comment timestamp
}Response: Returns saved comment object
{
username: String (required, unique),
password: String (hashed, required),
created_at: Date,
my_listings: [ObjectId], // References to Listing documents
claimed: Array, // Array of claimed listing IDs
karma: Number (default: 3),
tokenCount: Number,
isAdmin: Boolean
}{
title: String,
location: String,
listedBy: String, // User ID who created the listing
isAvailable: Boolean,
interested_users: Array, // Array of user IDs interested in the item
description: String,
photo: String, // Imgur URL
username: String, // Username of the lister
comments: [ObjectId], // References to Comment documents
createdAt: Date, // Auto-generated timestamp
updatedAt: Date // Auto-generated timestamp
}{
// Schema not fully visible in codebase, but referenced in listings
}- Users start with 3 karma points
- Karma increases when:
- Creating a new listing (+1)
- Withdrawing interest from a listing (+1)
- Karma decreases when:
- Expressing interest in a listing (-1)
- Karma affects user credibility in the platform
- Images are uploaded to Imgur via their API
- Imgur Client ID required in configuration
- Images are processed before listing creation/update
- Search across listing titles, descriptions, and locations
- Uses MongoDB regex queries with escaping to prevent injection
- Limited to 12 results per query
- Express sessions with cookie-based authentication
- Sessions configured with security settings
- User authentication state managed in React components
- Heroku CLI installed
- Git repository initialized
- MongoDB Atlas account (for production database)
- Google Maps API key
- Imgur API key
Set these in Heroku config vars:
# Database
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/dbname
# Port (automatically set by Heroku)
PORT=process.env.PORT
# API Keys (add these to your config)
GOOGLE_MAPS_API_KEY=your_google_maps_api_key
IMGUR_API_ID=your_imgur_client_id-
Create Heroku App
heroku create your-app-name
-
Set Environment Variables
heroku config:set MONGODB_URI=your_mongodb_connection_string heroku config:set IMGUR_API_ID=your_imgur_client_id
-
Update API Keys in Code
- Update
client/dist/index.htmlline 14 with your Google Maps API key - Update
client/src/services/config.jswith your Imgur API ID
- Update
-
Deploy to Heroku
git add . git commit -m "Prepare for Heroku deployment" git push heroku main
-
Scale the Application
heroku ps:scale web=1
- The
Procfileis already configured:web: node server/app.js postinstallscript automatically runs webpack build- Node.js version is pinned to 8.11.1 (consider updating)
- Static files are served from
client/dist - MongoDB connection uses
process.env.MONGODB_URIor falls back to localhost
- Node.js: 8.11.1 (exact version specified in package.json)
- npm: 6.0.0 (exact version specified in package.json)
| Variable | Description | Usage | Required |
|---|---|---|---|
MONGODB_URI |
MongoDB connection string | Database connection. Falls back to 'mongodb://localhost/greenfield' if not set | Yes (Production) |
PORT |
Server port number | Express server port. Defaults to 1128 if not set | No |
IMGUR_API_ID |
Imgur API Client ID | Image upload functionality via Imgur API | Yes |
IMGUR_API_SECRET |
Imgur API Secret | Image upload authentication (currently hardcoded in config.js) | Yes |
GOOGLE_MAPS_API_KEY |
Google Maps API Key | Map display and location services (set in index.html) | Yes |
SESSION_SECRET |
Express session secret | Session management ( |
Recommended |
- Node.js 8.11.1 (exact version)
- MongoDB (local installation or MongoDB Atlas)
- npm 6.0.0 (exact version)
-
Clone the repository
git clone https://github.com/PantryPatron/one-mans-treasure.git cd one-mans-treasure -
Install dependencies
npm install
-
Set up environment variables Create a
.envfile in the root directory:# MongoDB connection string MONGODB_URI=mongodb://localhost/greenfield # Server port number PORT=port # Imgur API credentials IMGUR_API_ID=your_imgur_api_id IMGUR_API_SECRET=your_imgur_api_secret # Google Maps API Key GOOGLE_MAPS_API_KEY=your_google_maps_api_key # Express session secret SESSION_SECRET=secret_key_here
-
Update API keys
- Add Google Maps API key to
client/dist/index.html - Add Imgur API ID to
client/src/services/config.js
- Add Google Maps API key to
-
Start development servers
# Terminal 1: Start the Express server npm start # Terminal 2: Start webpack in watch mode npm run react-dev
-
Access the application
- Open browser to
http://localhost:1128
- Open browser to
- Uses Redux-Promise middleware for simplified async actions
- State includes: listings, claimed listings, and users who claimed items
- Actions are promise-based, reducing boilerplate code
- Passwords are hashed using bcrypt with salt rounds of 10
- Session secret is hardcoded (
⚠️ should be moved to environment variables) - MongoDB queries use regex escaping to prevent injection attacks
- CORS is enabled for cross-origin requests
- Listings are limited to 12 results per query
- MongoDB indexes on username field for faster lookups
- Webpack bundling for optimized client-side code
- Static file serving through Express
As documented in the README:
- Cannot give away items to the first user who claimed it
- Long comments don't word-wrap properly
- Listings don't automatically populate in profile (requires logout/login)
- Uses nodemon for server auto-restart during development
- Webpack in watch mode for automatic client-side rebuilds
- Babel transpiles ES6+ and JSX to browser-compatible JavaScript
- ESLint configuration present for code quality
-
Security Enhancements
- Move session secret to environment variables
- Add input validation and sanitization
- Implement rate limiting
- Add HTTPS enforcement
-
Performance
- Implement pagination for listings
- Add database indexes for search queries
- Optimize image loading and caching
-
User Experience
- Fix known bugs listed in README
- Add real-time notifications
- Implement user profiles with avatars
- Add listing categories and filters
-
Technical Debt
- Update Node.js version (currently pinned to 8.11.1)
- Update React and other dependencies
- Add comprehensive testing suite
- Implement proper error handling and logging
-
Features
- Add messaging system between users
- Implement geolocation-based search
- Add admin panel for moderation
- Create mobile-responsive design improvements
Currently, the application has no test suite implemented. Consider adding:
- Unit tests for database methods
- Integration tests for API endpoints
- React component testing
- End-to-end testing with tools like Cypress
Entry Point: client/src/index.jsx
// Store setup with middleware
const createStoreWithMiddleware = applyMiddleware(ReduxPromise)(createStore);
const store = createStoreWithMiddleware(reducers);// Root State Shape (RootReducer.js)
{
listings: {
listings: Array<ListingObject>, // All available listings
query: String // Current search query
},
interestedUsers: Array<UserObject>, // Users interested in specific listing
claimedListings: Array<ListingObject> // User's claimed/received listings
}State Shape: { listings: Array, query: String }
Actions & Behaviors:
FETCH_LISTINGS: Updates listings array from API response- Preserves existing query state
- Used when loading initial listings or search results
SET_QUERY: Updates search query while preserving listings- Used when user types in search bar
- Maintains current listings display
SET_LISTINGS: Direct local update of listings array- Used for client-side sorting/filtering
- No API call, immediate state update
Action Creators:
// ListingActions.js
fetchListings(query?) // GET /listing?query=${query}
setQuery(query) // Local state update
setListings(listings) // Local array replacement
fetchClaimedListings(ids) // POST /listing/claimedConnected Components:
App.jsx- Main consumer, passes to Listings componentSearchEnhancer.jsx- Reads directly from store for sortingNavBar.jsx- Accesses query state for search display
State Shape: Array<UserObject> (interested users)
Actions & Behaviors:
FETCH_INTERESTED_USERS: Replaces entire array with users interested in a specific listing- Used when viewing "Give Away" options in user's own listings
- Shows list of users who expressed interest
Action Creators:
// UserActions.js
fetchInterestedUsers(userIds); // PUT /interestedUsersConnected Components:
MyListingEntry.jsx- Shows interested users for giveaway selection
State Shape: Array<ListingObject> (user's claimed items)
Actions & Behaviors:
FETCH_CLAIMED_LISTINGS: Replaces array with user's claimed listings- Shows items user has received/claimed
- Used in profile modal
Action Creators:
// ListingActions.js (exports claimed listings action)
fetchClaimedListings(listingIds); // POST /listing/claimedConnected Components:
ClaimListings.jsx- Profile modal showing claimed items
// Components accessing store directly via import
import store from "../index.jsx";
// Examples:
store.getState().listings.query; // NavBar.jsx, SearchEnhancer.jsx
store.dispatch({ type, payload }); // App.jsx, NavBar.jsx, SearchEnhancer.jsx// App.jsx
mapStateToProps = ({ listings }) => ({ listings });
// ClaimListings.jsx
mapStateToProps = ({ claimedListings }) => ({ claimedListings });
// MyListingEntry.jsx
mapStateToProps = ({ interestedUsers }) => ({ interestedUsers });User Action → Component → Action Creator → API Call → Reducer → Component Re-render
Example: Search Listings
User types → NavBar → fetchListings(query) → GET /listing → FETCH_LISTINGS → Listings updates
User Interest → Service Call → Store Update → UI Refresh
Example: Express Interest
Click "Interested" → listingInterestService() → fetchListings() → Store update → Button state change
Profile Modal → Fetch Claims → Store Update → Modal Content
Example: View Claimed Items
Open modal → fetchClaimedListings() → FETCH_CLAIMED_LISTINGS → ClaimListings renders list
- Purpose: Automatically handles promise-based actions
- Behavior:
- Action creators return
{type, payload: Promise} - Middleware resolves promises
- Reducers receive
action.payload.data(resolved value)
- Action creators return
- Benefits: Reduces boilerplate for async operations
- API-driven updates: Most state changes come from server responses
- Local updates: SearchEnhancer sorting, query updates
- Hybrid approach: Interest actions update server then refresh from API
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ App.jsx │◄──►│ listings │◄──►│ Listings.jsx │
│ (main consumer) │ │ {listings,query}│ │ (display only) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ ▲
│ │
▼ ┌────────┴──────────┐
┌─────────────────┐ │ SearchEnhancer │
│ NavBar.jsx │────►│ (direct access) │
│ (search & home) │ │ store.getState() │
└─────────────────┘ └───────────────────┘
┌─────────────────┐ ┌──────────────────┐
│ MyListingEntry │◄──►│ interestedUsers │
│ (giveaway UI) │ │ Array<User> │
└─────────────────┘ └──────────────────┘
┌─────────────────┐ ┌──────────────────┐
│ ClaimListings │◄──►│ claimedListings │
│ (profile modal) │ │ Array<Listing> │
└─────────────────┘ └──────────────────┘
- Limited API calls: Store caches listings between searches
- Local sorting: SearchEnhancer sorts in-memory vs. new API calls
- Selective updates: Only interested users fetched when needed
- Component-level state: Session data kept in App.jsx state, not Redux
- Redux for: Shareable data (listings, search results, user lists)
- Local state for: UI state, session data, temporary form data
- Direct store access: Used where connect() would be overkill
- Promise-based actions with redux-promise middleware
- Combining API updates with local state updates (App.jsx karma)
- Using store.getState() for read-only access in utility components
- Mixed patterns: Some components use connect(), others direct access
- Session management in component state vs. Redux
- Karma tracking duplicated between local state and user object
- John Webb
- Mealear Khiev
- Erwin Carrasquilla
- Heshie London