mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
685 lines
19 KiB
Markdown
685 lines
19 KiB
Markdown
# Product & Service Discovery: Quick Start Implementation Guide
|
|
|
|
## Overview
|
|
|
|
This guide provides step-by-step instructions to implement the first phase of the Product & Service Discovery feature.
|
|
|
|
---
|
|
|
|
## Phase 1: Foundation (Weeks 1-3)
|
|
|
|
### Step 1: Database Migrations
|
|
|
|
Create migration file: `bugulma/backend/migrations/postgres/019_create_discovery_tables.up.sql`
|
|
|
|
```sql
|
|
-- +migrate Up
|
|
-- Product Listings (Business Products)
|
|
CREATE TABLE product_listings (
|
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
organization_id TEXT REFERENCES organizations(id) ON DELETE CASCADE,
|
|
site_id TEXT REFERENCES sites(id),
|
|
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
category VARCHAR(100) NOT NULL,
|
|
subcategory VARCHAR(100),
|
|
brand VARCHAR(100),
|
|
model VARCHAR(100),
|
|
|
|
price DECIMAL(10,2),
|
|
price_unit VARCHAR(50),
|
|
currency VARCHAR(3) DEFAULT 'EUR',
|
|
negotiable BOOLEAN DEFAULT false,
|
|
|
|
quantity_available INTEGER,
|
|
availability_status VARCHAR(20) DEFAULT 'available',
|
|
availability_schedule JSONB,
|
|
|
|
pickup_location POINT,
|
|
delivery_available BOOLEAN DEFAULT false,
|
|
delivery_radius_km DECIMAL(5,2),
|
|
delivery_cost DECIMAL(10,2),
|
|
|
|
images TEXT[],
|
|
specifications JSONB,
|
|
tags TEXT[],
|
|
search_keywords TEXT,
|
|
verified BOOLEAN DEFAULT false,
|
|
|
|
status VARCHAR(20) DEFAULT 'active',
|
|
featured BOOLEAN DEFAULT false,
|
|
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
expires_at TIMESTAMP WITH TIME ZONE
|
|
);
|
|
|
|
-- Service Listings (Business Services)
|
|
CREATE TABLE service_listings (
|
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
organization_id TEXT REFERENCES organizations(id) ON DELETE CASCADE,
|
|
site_id TEXT REFERENCES sites(id),
|
|
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
category VARCHAR(100) NOT NULL,
|
|
subcategory VARCHAR(100),
|
|
|
|
price DECIMAL(10,2),
|
|
price_type VARCHAR(50),
|
|
currency VARCHAR(3) DEFAULT 'EUR',
|
|
negotiable BOOLEAN DEFAULT false,
|
|
|
|
availability_status VARCHAR(20) DEFAULT 'available',
|
|
availability_schedule JSONB,
|
|
booking_required BOOLEAN DEFAULT false,
|
|
min_advance_booking_hours INTEGER,
|
|
|
|
service_location POINT,
|
|
service_radius_km DECIMAL(5,2),
|
|
on_site_service BOOLEAN DEFAULT true,
|
|
remote_service BOOLEAN DEFAULT false,
|
|
|
|
requirements TEXT,
|
|
duration_estimate VARCHAR(100),
|
|
|
|
images TEXT[],
|
|
portfolio_urls TEXT[],
|
|
tags TEXT[],
|
|
search_keywords TEXT,
|
|
certifications TEXT[],
|
|
verified BOOLEAN DEFAULT false,
|
|
|
|
status VARCHAR(20) DEFAULT 'active',
|
|
featured BOOLEAN DEFAULT false,
|
|
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
-- Community Listings
|
|
CREATE TABLE community_listings (
|
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
title VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
listing_type VARCHAR(50) NOT NULL,
|
|
category VARCHAR(100) NOT NULL,
|
|
subcategory VARCHAR(100),
|
|
|
|
condition VARCHAR(50),
|
|
price DECIMAL(10,2),
|
|
price_type VARCHAR(50),
|
|
|
|
service_type VARCHAR(50),
|
|
rate DECIMAL(10,2),
|
|
rate_type VARCHAR(50),
|
|
|
|
availability_status VARCHAR(20) DEFAULT 'available',
|
|
availability_schedule JSONB,
|
|
quantity_available INTEGER,
|
|
|
|
location POINT,
|
|
pickup_available BOOLEAN DEFAULT true,
|
|
delivery_available BOOLEAN DEFAULT false,
|
|
delivery_radius_km DECIMAL(5,2),
|
|
|
|
images TEXT[],
|
|
tags TEXT[],
|
|
search_keywords TEXT,
|
|
|
|
user_rating DECIMAL(3,2),
|
|
review_count INTEGER DEFAULT 0,
|
|
verified BOOLEAN DEFAULT false,
|
|
|
|
status VARCHAR(20) DEFAULT 'active',
|
|
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
expires_at TIMESTAMP WITH TIME ZONE
|
|
);
|
|
|
|
-- Indexes
|
|
CREATE INDEX idx_product_listings_org ON product_listings(organization_id);
|
|
CREATE INDEX idx_product_listings_category ON product_listings(category);
|
|
CREATE INDEX idx_product_listings_status ON product_listings(status);
|
|
CREATE INDEX idx_product_listings_location ON product_listings USING GIST(pickup_location);
|
|
CREATE INDEX idx_product_listings_search ON product_listings USING GIN(to_tsvector('russian', name || ' ' || COALESCE(description, '') || ' ' || COALESCE(search_keywords, '')));
|
|
|
|
CREATE INDEX idx_service_listings_org ON service_listings(organization_id);
|
|
CREATE INDEX idx_service_listings_category ON service_listings(category);
|
|
CREATE INDEX idx_service_listings_status ON service_listings(status);
|
|
CREATE INDEX idx_service_listings_location ON service_listings USING GIST(service_location);
|
|
CREATE INDEX idx_service_listings_search ON service_listings USING GIN(to_tsvector('russian', name || ' ' || COALESCE(description, '') || ' ' || COALESCE(search_keywords, '')));
|
|
|
|
CREATE INDEX idx_community_listings_user ON community_listings(user_id);
|
|
CREATE INDEX idx_community_listings_type ON community_listings(listing_type);
|
|
CREATE INDEX idx_community_listings_category ON community_listings(category);
|
|
CREATE INDEX idx_community_listings_status ON community_listings(status);
|
|
CREATE INDEX idx_community_listings_location ON community_listings USING GIST(location);
|
|
CREATE INDEX idx_community_listings_search ON community_listings USING GIN(to_tsvector('russian', title || ' ' || COALESCE(description, '') || ' ' || COALESCE(search_keywords, '')));
|
|
```
|
|
|
|
Create down migration: `019_create_discovery_tables.down.sql`
|
|
|
|
```sql
|
|
-- +migrate Down
|
|
DROP TABLE IF EXISTS community_listings;
|
|
DROP TABLE IF EXISTS service_listings;
|
|
DROP TABLE IF EXISTS product_listings;
|
|
```
|
|
|
|
---
|
|
|
|
### Step 2: Domain Models
|
|
|
|
Create: `bugulma/backend/internal/domain/product_listing.go`
|
|
|
|
```go
|
|
package domain
|
|
|
|
import (
|
|
"time"
|
|
"github.com/lib/pq"
|
|
)
|
|
|
|
type ProductListing struct {
|
|
ID string `gorm:"primaryKey;type:text"`
|
|
OrganizationID string `gorm:"type:text;index"`
|
|
SiteID *string `gorm:"type:text"`
|
|
|
|
Name string `gorm:"type:varchar(255);not null"`
|
|
Description *string `gorm:"type:text"`
|
|
Category string `gorm:"type:varchar(100);not null;index"`
|
|
Subcategory *string `gorm:"type:varchar(100)"`
|
|
Brand *string `gorm:"type:varchar(100)"`
|
|
Model *string `gorm:"type:varchar(100)"`
|
|
|
|
Price *float64 `gorm:"type:decimal(10,2)"`
|
|
PriceUnit *string `gorm:"type:varchar(50)"`
|
|
Currency string `gorm:"type:varchar(3);default:'EUR'"`
|
|
Negotiable bool `gorm:"default:false"`
|
|
|
|
QuantityAvailable *int
|
|
AvailabilityStatus string `gorm:"type:varchar(20);default:'available'"`
|
|
AvailabilitySchedule *string `gorm:"type:jsonb"`
|
|
|
|
PickupLocation *string `gorm:"type:point"` // PostGIS point
|
|
DeliveryAvailable bool `gorm:"default:false"`
|
|
DeliveryRadiusKm *float64 `gorm:"type:decimal(5,2)"`
|
|
DeliveryCost *float64 `gorm:"type:decimal(10,2)"`
|
|
|
|
Images pq.StringArray `gorm:"type:text[]"`
|
|
Specifications *string `gorm:"type:jsonb"`
|
|
Tags pq.StringArray `gorm:"type:text[]"`
|
|
SearchKeywords *string `gorm:"type:text"`
|
|
Verified bool `gorm:"default:false"`
|
|
|
|
Status string `gorm:"type:varchar(20);default:'active';index"`
|
|
Featured bool `gorm:"default:false"`
|
|
|
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
ExpiresAt *time.Time
|
|
|
|
// Relationships
|
|
Organization *Organization `gorm:"foreignKey:OrganizationID"`
|
|
Site *Site `gorm:"foreignKey:SiteID"`
|
|
}
|
|
```
|
|
|
|
Create similar models for `ServiceListing` and `CommunityListing`.
|
|
|
|
---
|
|
|
|
### Step 3: Repository Layer
|
|
|
|
Create: `bugulma/backend/internal/repository/product_listing_repository.go`
|
|
|
|
```go
|
|
package repository
|
|
|
|
import (
|
|
"context"
|
|
"bugulma/backend/internal/domain"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type ProductListingRepository interface {
|
|
Create(ctx context.Context, listing *domain.ProductListing) error
|
|
GetByID(ctx context.Context, id string) (*domain.ProductListing, error)
|
|
Update(ctx context.Context, listing *domain.ProductListing) error
|
|
Delete(ctx context.Context, id string) error
|
|
Search(ctx context.Context, query SearchQuery) ([]*domain.ProductListing, int64, error)
|
|
GetByOrganization(ctx context.Context, orgID string) ([]*domain.ProductListing, error)
|
|
}
|
|
|
|
type productListingRepository struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewProductListingRepository(db *gorm.DB) ProductListingRepository {
|
|
return &productListingRepository{db: db}
|
|
}
|
|
|
|
func (r *productListingRepository) Create(ctx context.Context, listing *domain.ProductListing) error {
|
|
return r.db.WithContext(ctx).Create(listing).Error
|
|
}
|
|
|
|
func (r *productListingRepository) GetByID(ctx context.Context, id string) (*domain.ProductListing, error) {
|
|
var listing domain.ProductListing
|
|
err := r.db.WithContext(ctx).
|
|
Preload("Organization").
|
|
Preload("Site").
|
|
First(&listing, "id = ?", id).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &listing, nil
|
|
}
|
|
|
|
func (r *productListingRepository) Search(ctx context.Context, query SearchQuery) ([]*domain.ProductListing, int64, error) {
|
|
var listings []*domain.ProductListing
|
|
var total int64
|
|
|
|
db := r.db.WithContext(ctx).Model(&domain.ProductListing{}).
|
|
Where("status = ?", "active")
|
|
|
|
// Text search
|
|
if query.Text != "" {
|
|
db = db.Where(
|
|
"to_tsvector('russian', name || ' ' || COALESCE(description, '') || ' ' || COALESCE(search_keywords, '')) @@ plainto_tsquery('russian', ?)",
|
|
query.Text,
|
|
)
|
|
}
|
|
|
|
// Category filter
|
|
if query.Category != "" {
|
|
db = db.Where("category = ?", query.Category)
|
|
}
|
|
|
|
// Location filter (if provided)
|
|
if query.Location != nil {
|
|
// Use PostGIS ST_DWithin for distance filtering
|
|
db = db.Where(
|
|
"ST_DWithin(pickup_location::geography, ST_MakePoint(?, ?)::geography, ?)",
|
|
query.Location.Lng,
|
|
query.Location.Lat,
|
|
query.MaxDistanceKm*1000, // Convert km to meters
|
|
)
|
|
}
|
|
|
|
// Price filter
|
|
if query.PriceMin != nil {
|
|
db = db.Where("price >= ?", *query.PriceMin)
|
|
}
|
|
if query.PriceMax != nil {
|
|
db = db.Where("price <= ?", *query.PriceMax)
|
|
}
|
|
|
|
// Count total
|
|
if err := db.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Apply pagination and ordering
|
|
offset := (query.Page - 1) * query.Limit
|
|
err := db.
|
|
Preload("Organization").
|
|
Offset(offset).
|
|
Limit(query.Limit).
|
|
Order("featured DESC, created_at DESC").
|
|
Find(&listings).Error
|
|
|
|
return listings, total, err
|
|
}
|
|
|
|
type SearchQuery struct {
|
|
Text string
|
|
Category string
|
|
Location *struct {
|
|
Lat float64
|
|
Lng float64
|
|
}
|
|
MaxDistanceKm float64
|
|
PriceMin *float64
|
|
PriceMax *float64
|
|
Page int
|
|
Limit int
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Step 4: Service Layer
|
|
|
|
Create: `bugulma/backend/internal/service/discovery_service.go`
|
|
|
|
```go
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"bugulma/backend/internal/domain"
|
|
"bugulma/backend/internal/repository"
|
|
)
|
|
|
|
type DiscoveryService interface {
|
|
SearchProducts(ctx context.Context, query SearchQuery) (*SearchResults, error)
|
|
SearchServices(ctx context.Context, query SearchQuery) (*SearchResults, error)
|
|
SearchCommunity(ctx context.Context, query SearchQuery) (*SearchResults, error)
|
|
UniversalSearch(ctx context.Context, query SearchQuery) (*UniversalSearchResults, error)
|
|
}
|
|
|
|
type discoveryService struct {
|
|
productRepo repository.ProductListingRepository
|
|
serviceRepo repository.ServiceListingRepository
|
|
communityRepo repository.CommunityListingRepository
|
|
}
|
|
|
|
func NewDiscoveryService(
|
|
productRepo repository.ProductListingRepository,
|
|
serviceRepo repository.ServiceListingRepository,
|
|
communityRepo repository.CommunityListingRepository,
|
|
) DiscoveryService {
|
|
return &discoveryService{
|
|
productRepo: productRepo,
|
|
serviceRepo: serviceRepo,
|
|
communityRepo: communityRepo,
|
|
}
|
|
}
|
|
|
|
func (s *discoveryService) UniversalSearch(ctx context.Context, query SearchQuery) (*UniversalSearchResults, error) {
|
|
// Search all types in parallel
|
|
products, productsTotal, _ := s.productRepo.Search(ctx, query)
|
|
services, servicesTotal, _ := s.serviceRepo.Search(ctx, query)
|
|
community, communityTotal, _ := s.communityRepo.Search(ctx, query)
|
|
|
|
// Combine and rank results
|
|
results := &UniversalSearchResults{
|
|
Products: products,
|
|
Services: services,
|
|
Community: community,
|
|
Total: productsTotal + servicesTotal + communityTotal,
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Step 5: Handler Layer
|
|
|
|
Create: `bugulma/backend/internal/handler/discovery_handler.go`
|
|
|
|
```go
|
|
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"bugulma/backend/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type DiscoveryHandler struct {
|
|
discoveryService service.DiscoveryService
|
|
}
|
|
|
|
func NewDiscoveryHandler(discoveryService service.DiscoveryService) *DiscoveryHandler {
|
|
return &DiscoveryHandler{
|
|
discoveryService: discoveryService,
|
|
}
|
|
}
|
|
|
|
// GET /api/v1/discovery/search
|
|
func (h *DiscoveryHandler) Search(c *gin.Context) {
|
|
var query service.SearchQuery
|
|
if err := c.ShouldBindQuery(&query); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Set defaults
|
|
if query.Page == 0 {
|
|
query.Page = 1
|
|
}
|
|
if query.Limit == 0 {
|
|
query.Limit = 20
|
|
}
|
|
if query.MaxDistanceKm == 0 {
|
|
query.MaxDistanceKm = 25.0
|
|
}
|
|
|
|
results, err := h.discoveryService.UniversalSearch(c.Request.Context(), query)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, results)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Step 6: Routes
|
|
|
|
Update: `bugulma/backend/internal/routes/routes.go`
|
|
|
|
Add discovery routes:
|
|
|
|
```go
|
|
func RegisterDiscoveryRoutes(public, protected *gin.RouterGroup, discoveryHandler *handler.DiscoveryHandler) {
|
|
discovery := public.Group("/discovery")
|
|
{
|
|
discovery.GET("/search", discoveryHandler.Search)
|
|
discovery.GET("/products", discoveryHandler.GetProducts)
|
|
discovery.GET("/services", discoveryHandler.GetServices)
|
|
discovery.GET("/community", discoveryHandler.GetCommunity)
|
|
discovery.GET("/listings/:type/:id", discoveryHandler.GetListing)
|
|
}
|
|
|
|
discoveryProtected := protected.Group("/discovery")
|
|
{
|
|
discoveryProtected.POST("/products", discoveryHandler.CreateProduct)
|
|
discoveryProtected.POST("/services", discoveryHandler.CreateService)
|
|
discoveryProtected.POST("/community", discoveryHandler.CreateCommunity)
|
|
discoveryProtected.PUT("/listings/:type/:id", discoveryHandler.UpdateListing)
|
|
discoveryProtected.DELETE("/listings/:type/:id", discoveryHandler.DeleteListing)
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Step 7: Frontend - Search Component
|
|
|
|
Create: `bugulma/frontend/components/discovery/DiscoverySearchBar.tsx`
|
|
|
|
```typescript
|
|
import React, { useState } from 'react';
|
|
import { Search } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
|
|
interface DiscoverySearchBarProps {
|
|
onSearch: (query: string) => void;
|
|
}
|
|
|
|
export const DiscoverySearchBar: React.FC<DiscoverySearchBarProps> = ({ onSearch }) => {
|
|
const [query, setQuery] = useState('');
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
onSearch(query);
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="w-full max-w-2xl mx-auto">
|
|
<div className="flex gap-2">
|
|
<Input
|
|
type="text"
|
|
placeholder="I need a van, who fixes computers, looking for..."
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
className="flex-1"
|
|
/>
|
|
<Button type="submit">
|
|
<Search className="w-4 h-4 mr-2" />
|
|
Search
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
);
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### Step 8: Frontend - Results Page
|
|
|
|
Create: `bugulma/frontend/pages/DiscoveryPage.tsx`
|
|
|
|
```typescript
|
|
import React, { useState, useEffect } from 'react';
|
|
import { DiscoverySearchBar } from '@/components/discovery/DiscoverySearchBar';
|
|
import { DiscoveryResults } from '@/components/discovery/DiscoveryResults';
|
|
import { discoveryAPI } from '@/services/discovery-api';
|
|
|
|
export const DiscoveryPage: React.FC = () => {
|
|
const [query, setQuery] = useState('');
|
|
const [results, setResults] = useState(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const handleSearch = async (searchQuery: string) => {
|
|
setQuery(searchQuery);
|
|
setLoading(true);
|
|
|
|
try {
|
|
const data = await discoveryAPI.search({
|
|
q: searchQuery,
|
|
page: 1,
|
|
limit: 20,
|
|
});
|
|
setResults(data);
|
|
} catch (error) {
|
|
console.error('Search error:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="container mx-auto py-8">
|
|
<h1 className="text-3xl font-bold mb-8">Find Products & Services</h1>
|
|
<DiscoverySearchBar onSearch={handleSearch} />
|
|
|
|
{loading && <div>Searching...</div>}
|
|
{results && <DiscoveryResults results={results} />}
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### Step 9: Frontend - API Service
|
|
|
|
Create: `bugulma/frontend/services/discovery-api.ts`
|
|
|
|
```typescript
|
|
import { BaseService } from './base-service';
|
|
|
|
export interface SearchQuery {
|
|
q?: string;
|
|
category?: string;
|
|
listing_type?: string;
|
|
location?: { lat: number; lng: number };
|
|
max_distance_km?: number;
|
|
price_min?: number;
|
|
price_max?: number;
|
|
page?: number;
|
|
limit?: number;
|
|
}
|
|
|
|
export interface SearchResult {
|
|
id: string;
|
|
type: 'product' | 'service' | 'community';
|
|
title: string;
|
|
description: string;
|
|
category: string;
|
|
price?: number;
|
|
location: { lat: number; lng: number };
|
|
distance_km?: number;
|
|
organization_name?: string;
|
|
user_name?: string;
|
|
rating?: number;
|
|
verified: boolean;
|
|
images: string[];
|
|
}
|
|
|
|
export interface SearchResponse {
|
|
results: SearchResult[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
}
|
|
|
|
class DiscoveryService extends BaseService {
|
|
constructor() {
|
|
super('/api/v1/discovery');
|
|
}
|
|
|
|
async search(query: SearchQuery): Promise<SearchResponse> {
|
|
const params = new URLSearchParams();
|
|
if (query.q) params.append('q', query.q);
|
|
if (query.category) params.append('category', query.category);
|
|
if (query.location) {
|
|
params.append('lat', query.location.lat.toString());
|
|
params.append('lng', query.location.lng.toString());
|
|
}
|
|
if (query.max_distance_km) params.append('max_distance_km', query.max_distance_km.toString());
|
|
if (query.page) params.append('page', query.page.toString());
|
|
if (query.limit) params.append('limit', query.limit.toString());
|
|
|
|
return this.get<SearchResponse>(`/search?${params.toString()}`);
|
|
}
|
|
}
|
|
|
|
export const discoveryAPI = new DiscoveryService();
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Checklist
|
|
|
|
- [ ] Database migrations run successfully
|
|
- [ ] Can create product listing (business)
|
|
- [ ] Can create service listing (business)
|
|
- [ ] Can create community listing (user)
|
|
- [ ] Search returns results
|
|
- [ ] Location filtering works
|
|
- [ ] Category filtering works
|
|
- [ ] Text search works
|
|
- [ ] Frontend search bar works
|
|
- [ ] Results display correctly
|
|
- [ ] Listing detail page works
|
|
|
|
---
|
|
|
|
## Next Steps After Phase 1
|
|
|
|
1. **Phase 2**: Enhanced search with relevance scoring
|
|
2. **Phase 3**: User dashboard and listing management
|
|
3. **Phase 4**: Trust system (ratings, reviews)
|
|
4. **Phase 5**: Advanced features (booking, matching)
|
|
|
|
---
|
|
|
|
**Last Updated**: 2025-01-27
|
|
**Status**: Implementation Guide
|
|
|