Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 19 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@ Tàwá is a Nigerian online marketplace platform that connects buyers and seller

## Features

- **Buy & Sell**: Create listings to sell items or browse available products
- **Categories**: Electronics, Fashion, Home & Garden, Vehicles, Real Estate, and more
- **Messaging**: Direct communication between buyers and sellers
- **Search & Filter**: Find items by keyword, category, location, and price range
- **User Profiles**: Manage your listings and track your activity
- **Featured Listings**: Highlight your most important items
- **Admin Moderation**: Secure platform with listing approval system
- **Rent & List**: Create listings to rent out items or browse available rentals.
- **Categories**: Electronics, Fashion, Home & Garden, Vehicles, Tools & Equipment, and more.
- **Messaging**: Direct communication between renters and listers.
- **Search & Filter**: Find rentals by keyword, category, location, or price range.
- **Featured Listings**: Highlight your most important rentals.
- **Admin Moderation**: Secure platform with listing approval system.
- **User Profiles**: Manage your listings and track your rental activity.

---

## Getting Started

Run the development server:
### Prerequisites

```bash
npm run dev
```
- Node.js installed (v18+ recommended)
- Git installed
- Convex account for backend

### Installation

Visit the application in your browser to start buying and selling.
1. Clone the repository:

```bash
git clone https://github.com/funmsss/tawa.git
4 changes: 2 additions & 2 deletions convex/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export const getPendingListings = query({

const listingsWithDetails = await Promise.all(
result.page.map(async (listing) => {
const seller = await ctx.db.get(listing.sellerId);
const Lister = await ctx.db.get(listing.ListerId);
const category = await ctx.db.get(listing.categoryId);
const imageUrls = await Promise.all(
listing.images.slice(0, 1).map(async (imageId) => {
Expand All @@ -206,7 +206,7 @@ export const getPendingListings = query({

return {
...listing,
seller: seller && 'name' in seller ? { name: seller.name, email: seller.email } : null,
Lister: Lister && 'name' in Lister ? { name: Lister.name, email: Lister.email } : null,
category: category && 'name' in category ? category.name : "Unknown",
imageUrls: imageUrls.filter(Boolean),
};
Expand Down
30 changes: 15 additions & 15 deletions convex/listings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ export const getListings = query({
});
}

// Get seller info and images for each listing
// Get Lister info and images for each listing
const listingsWithDetails = await Promise.all(
listings.map(async (listing) => {
const seller = await ctx.db.get(listing.sellerId);
const lister = await ctx.db.get(listing.listerId);
const category = await ctx.db.get(listing.categoryId);
const imageUrls = await Promise.all(
listing.images.map(async (imageId) => {
Expand All @@ -72,7 +72,7 @@ export const getListings = query({

return {
...listing,
seller: seller ? { name: seller.name, email: seller.email, _id: seller._id } : null,
lister: lister ? { name: lister.name, email: lister.email, _id: lister._id } : null,
category: category && 'name' in category ? category.name : "Unknown",
imageUrls: imageUrls.filter(Boolean),
};
Expand All @@ -95,7 +95,7 @@ export const getFeaturedListings = query({

return Promise.all(
listings.map(async (listing) => {
const seller = await ctx.db.get(listing.sellerId);
const lister = await ctx.db.get(listing.listerId);
const category = await ctx.db.get(listing.categoryId);
const imageUrls = await Promise.all(
listing.images.map(async (imageId) => {
Expand All @@ -106,7 +106,7 @@ export const getFeaturedListings = query({

return {
...listing,
seller: seller ? { name: seller.name, email: seller.email, _id: seller._id } : null,
lister: listerer ? { name: lister.name, email: lister.email, _id: lister._id } : null,
category: category && 'name' in category ? category.name : "Unknown",
imageUrls: imageUrls.filter(Boolean),
};
Expand All @@ -121,7 +121,7 @@ export const getListingById = query({
const listing = await ctx.db.get(args.id);
if (!listing) return null;

const seller = await ctx.db.get(listing.sellerId);
const listerer = await ctx.db.get(listing.listerId);
const category = await ctx.db.get(listing.categoryId);
const imageUrls = await Promise.all(
listing.images.map(async (imageId) => {
Expand All @@ -132,7 +132,7 @@ export const getListingById = query({

return {
...listing,
seller: seller ? { name: seller.name, email: seller.email, _id: seller._id } : null,
listererer: lister ? { name: lister.name, email: lister.email, _id: lister._id } : null,
category: category && 'name' in category ? category.name : "Unknown",
imageUrls: imageUrls.filter(Boolean),
};
Expand Down Expand Up @@ -169,7 +169,7 @@ export const createListing = mutation({

return await ctx.db.insert("listings", {
...args,
sellerId: userId,
ListerId: userId,
status: "pending",
views: 0,
});
Expand All @@ -184,7 +184,7 @@ export const getUserListings = query({

const listings = await ctx.db
.query("listings")
.withIndex("by_seller", (q) => q.eq("sellerId", userId))
.withIndex("by_Lister", (q) => q.eq("ListerId", userId))
.order("desc")
.collect();

Expand Down Expand Up @@ -233,19 +233,19 @@ export const updateListingStatus = mutation({

// Business logic for who can change what status:
if (args.status === "sold") {
// Only the seller can mark their own listing as sold
if (listing.sellerId !== userId) {
throw new Error("Only the seller can mark their listing as sold");
// Only the Lister can mark their own listing as sold
if (listing.ListerId !== userId) {
throw new Error("Only the Lister can mark their listing as sold");
}
} else if (args.status === "approved" || args.status === "rejected") {
// Only admins can approve or reject listings
if (!isAdmin) {
throw new Error("Only admins can approve or reject listings");
}
} else if (args.status === "pending") {
// Sellers can resubmit rejected listings, admins can change any status
if (listing.sellerId !== userId && !isAdmin) {
throw new Error("Only the seller or admin can change listing to pending");
// Listers can resubmit rejected listings, admins can change any status
if (listing.ListerId !== userId && !isAdmin) {
throw new Error("Only the Lister or admin can change listing to pending");
}
}

Expand Down
4 changes: 2 additions & 2 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const applicationTables = {
categoryId: v.id("categories"),
condition: v.union(v.literal("new"), v.literal("used")),
location: v.string(),
sellerId: v.id("users"),
ListerId: v.id("users"),
images: v.array(v.id("_storage")),
status: v.union(
v.literal("pending"),
Expand All @@ -28,7 +28,7 @@ const applicationTables = {
featured: v.optional(v.boolean()),
views: v.optional(v.number()),
})
.index("by_seller", ["sellerId"])
.index("by_Lister", ["ListerId"])
.index("by_category", ["categoryId"])
.index("by_status", ["status"])
.index("by_location", ["location"])
Expand Down
8 changes: 4 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default function App() {
: "text-gray-600 hover:text-gray-900 hover:bg-gray-50"
}`}
>
Sell
List
</button>
<button
onClick={() => setCurrentPage("dashboard")}
Expand Down Expand Up @@ -139,7 +139,7 @@ export default function App() {
</Authenticated>
<Unauthenticated>
<div className="text-xs sm:text-sm text-gray-600">
Sign in to start selling
Sign in to start listing
</div>
</Unauthenticated>
</div>
Expand Down Expand Up @@ -178,7 +178,7 @@ export default function App() {
: "text-gray-600 hover:text-gray-900 hover:bg-gray-50"
}`}
>
Sell
List
</button>
<button
onClick={() => handleNavigation("dashboard")}
Expand Down Expand Up @@ -247,7 +247,7 @@ export default function App() {
<div className="max-w-md mx-auto mt-16 px-4">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Buy & Sell Anything
Rent & List Anything
</h1>
<p className="text-xl text-gray-600">
Fast, Simple, Secure marketplace for Nigeria
Expand Down
2 changes: 1 addition & 1 deletion src/components/AdminDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export function AdminDashboard() {
</div>

<div className="text-xs text-gray-400 mb-4">
Seller: {listing.seller?.name || listing.seller?.email}
Lister: {listing.Lister?.name || listing.Lister?.email}
</div>

<div className="space-y-2">
Expand Down
2 changes: 1 addition & 1 deletion src/components/CreateListing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export function CreateListing({ onSuccess }: CreateListingProps) {
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="What are you selling?"
placeholder="What are you listing?"
className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-rose-500 focus:border-transparent"
required
/>
Expand Down
24 changes: 12 additions & 12 deletions src/components/ListingDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ export function ListingDetail({ listingId, onBack }: ListingDetailProps) {
return;
}

if (!listing?.seller?._id) {
toast.error("Seller information unavailable. Please refresh the page.");
if (!listing?.Lister?._id) {
toast.error("Lister information unavailable. Please refresh the page.");
return;
}

try {
await sendMessage({
listingId: listingId,
receiverId: listing.seller._id,
receiverId: listing.Lister._id,
content: message.trim(),
});

Expand Down Expand Up @@ -79,7 +79,7 @@ export function ListingDetail({ listingId, onBack }: ListingDetailProps) {
);
}

const isOwner = loggedInUser?._id === listing.seller?._id;
const isOwner = loggedInUser?._id === listing.Lister?._id;

return (
<div className="max-w-4xl mx-auto px-4 py-8">
Expand Down Expand Up @@ -165,19 +165,19 @@ export function ListingDetail({ listingId, onBack }: ListingDetailProps) {
<p className="text-gray-700 whitespace-pre-wrap">{listing.description}</p>
</div>

{/* Seller Info */}
{/* Lister Info */}
<div className="bg-gray-50 rounded-xl p-4">
<h3 className="text-lg font-semibold text-gray-900 mb-3">Seller Information</h3>
<h3 className="text-lg font-semibold text-gray-900 mb-3">Lister Information</h3>
<div className="flex items-center space-x-3">
<div className="w-12 h-12 bg-rose-500 rounded-full flex items-center justify-center text-white font-semibold">
{listing.seller?.name?.[0] || listing.seller?.email?.[0] || "?"}
{listing.Lister?.name?.[0] || listing.Lister?.email?.[0] || "?"}
</div>
<div>
<div className="font-medium text-gray-900">
{listing.seller?.name || "Anonymous"}
{listing.Lister?.name || "Anonymous"}
</div>
<div className="text-sm text-gray-600">
{listing.seller?.email}
{listing.Lister?.email}
</div>
</div>
</div>
Expand All @@ -186,10 +186,10 @@ export function ListingDetail({ listingId, onBack }: ListingDetailProps) {
{/* Contact Form */}
{!isOwner && listing.status === "approved" && (
<div className="bg-white border rounded-xl p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Contact Seller</h3>
{!listing?.seller?._id ? (
<h3 className="text-lg font-semibold text-gray-900 mb-4">Contact Lister</h3>
{!listing?.Lister?._id ? (
<div className="text-center py-6 text-gray-500">
<p className="mb-2">Seller information is currently unavailable.</p>
<p className="mb-2">Lister information is currently unavailable.</p>
<button
onClick={() => window.location.reload()}
className="text-rose-500 hover:text-rose-600 font-medium"
Expand Down
2 changes: 1 addition & 1 deletion src/components/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function Messages() {
<div className="max-w-6xl mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Messages</h1>
<p className="text-gray-600">Communicate with buyers and sellers</p>
<p className="text-gray-600">Communicate with renters and listers</p>
</div>

<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 h-[600px]">
Expand Down
2 changes: 1 addition & 1 deletion src/components/UserDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export function UserDashboard({ onViewListing }: UserDashboardProps) {
<div className="text-center py-12">
<div className="text-6xl mb-4">📦</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">No listings yet</h3>
<p className="text-gray-600 mb-6">Start selling by creating your first listing</p>
<p className="text-gray-600 mb-6">Start listing by creating your first listing</p>
</div>
)
) : (
Expand Down