mojmajstor/convex/ads.ts
echo ff464b0f9b feat: Phase 3 - ad creation, ad detail, profile my-ads
- Add create/update/remove ad mutations with auth & ownership checks
- Ad detail screen: gallery placeholder, title, description, category
  badge, location, price range, availability, rating, handyman info
  card, start-chat and write-review buttons for non-owners,
  edit/delete for ad owner, auth redirect for anonymous
- Create ad screen: 4-step form (category → title/description →
  location → price/availability/submit), handyman-only guard,
  edit mode via editId param
- Profile screen: user avatar/name/role/email/phone, my-ads
  section for handymen with edit/delete, new-ad button, placeholder
  my-posts section for customers, proper logout mutation

All UI text in Macedonian. TypeScript clean, Convex functions deployed.
2026-05-29 18:28:01 +02:00

170 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query("ads").order("desc").collect();
},
});
export const getByCategory = query({
args: { category: v.string() },
handler: async (ctx, args) => {
return await ctx.db
.query("ads")
.withIndex("by_category", (q) => q.eq("category", args.category))
.order("desc")
.collect();
},
});
export const getById = query({
args: { id: v.id("ads") },
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
},
});
export const search = query({
args: { query: v.string(), category: v.optional(v.string()) },
handler: async (ctx, args) => {
const searchLower = args.query.toLowerCase();
let ads;
if (args.category) {
ads = await ctx.db
.query("ads")
.withIndex("by_category", (q) => q.eq("category", args.category!))
.collect();
} else {
ads = await ctx.db.query("ads").order("desc").collect();
}
return ads.filter(
(ad) =>
ad.title.toLowerCase().includes(searchLower) ||
ad.description.toLowerCase().includes(searchLower) ||
ad.location.toLowerCase().includes(searchLower)
);
},
});
export const getByHandyman = query({
args: { handymanId: v.id("users") },
handler: async (ctx, args) => {
return await ctx.db
.query("ads")
.withIndex("by_handyman", (q) => q.eq("handymanId", args.handymanId))
.order("desc")
.collect();
},
});
export const create = mutation({
args: {
token: v.string(),
title: v.string(),
description: v.string(),
category: v.string(),
location: v.string(),
lat: v.optional(v.number()),
lng: v.optional(v.number()),
priceRange: v.optional(v.string()),
availability: v.optional(v.string()),
imageIds: v.optional(v.array(v.string())),
},
handler: async (ctx, args) => {
const session = await ctx.db
.query("sessions")
.withIndex("by_token", (q) => q.eq("token", args.token))
.first();
if (!session) throw new Error("Неавторизиран");
const user = await ctx.db.get(session.userId);
if (!user) throw new Error("Корисникот не е пронајден");
if (user.role !== "handyman") throw new Error("Само мајстори можат да креираат огласи");
const now = Date.now();
const adId = await ctx.db.insert("ads", {
handymanId: session.userId,
title: args.title,
description: args.description,
category: args.category,
location: args.location,
lat: args.lat,
lng: args.lng,
priceRange: args.priceRange,
availability: args.availability,
imageIds: args.imageIds,
ratingAvg: undefined,
reviewCount: 0,
createdAt: now,
updatedAt: now,
});
return adId;
},
});
export const update = mutation({
args: {
token: v.string(),
adId: v.id("ads"),
title: v.optional(v.string()),
description: v.optional(v.string()),
category: v.optional(v.string()),
location: v.optional(v.string()),
lat: v.optional(v.number()),
lng: v.optional(v.number()),
priceRange: v.optional(v.string()),
availability: v.optional(v.string()),
imageIds: v.optional(v.array(v.string())),
},
handler: async (ctx, args) => {
const session = await ctx.db
.query("sessions")
.withIndex("by_token", (q) => q.eq("token", args.token))
.first();
if (!session) throw new Error("Неавторизиран");
const ad = await ctx.db.get(args.adId);
if (!ad) throw new Error("Огласот не е пронајден");
if (ad.handymanId !== session.userId) throw new Error("Немате дозвола да го уредите овој оглас");
const updates: Record<string, any> = { updatedAt: Date.now() };
if (args.title !== undefined) updates.title = args.title;
if (args.description !== undefined) updates.description = args.description;
if (args.category !== undefined) updates.category = args.category;
if (args.location !== undefined) updates.location = args.location;
if (args.lat !== undefined) updates.lat = args.lat;
if (args.lng !== undefined) updates.lng = args.lng;
if (args.priceRange !== undefined) updates.priceRange = args.priceRange;
if (args.availability !== undefined) updates.availability = args.availability;
if (args.imageIds !== undefined) updates.imageIds = args.imageIds;
await ctx.db.patch(args.adId, updates);
return args.adId;
},
});
export const remove = mutation({
args: {
token: v.string(),
adId: v.id("ads"),
},
handler: async (ctx, args) => {
const session = await ctx.db
.query("sessions")
.withIndex("by_token", (q) => q.eq("token", args.token))
.first();
if (!session) throw new Error("Неавторизиран");
const ad = await ctx.db.get(args.adId);
if (!ad) throw new Error("Огласот не е пронајден");
if (ad.handymanId !== session.userId) throw new Error("Немате дозвола да го избришете овој оглас");
await ctx.db.delete(args.adId);
return true;
},
});