- 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.
170 lines
5.2 KiB
TypeScript
170 lines
5.2 KiB
TypeScript
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;
|
||
},
|
||
}); |