mojmajstor/convex/posts.ts
echo c85ea09d4b feat: Phase 4 - customer posts feed, create, detail, respond
- Add create/close post mutations with auth & ownership verification
- Add getOrCreate chat mutation for handyman response flow
- PostCard component: title, description preview, category badge,
  location, budget, status badge (open=green, closed=red)
- Post detail screen: full description, category, location, budget,
  status, time-ago. Owner: close button. Handyman: respond button
  creates/opens chat with customer
- Create post screen: title, description, category picker, location,
  budget, auth-gated with redirect
- Posts feed: open/all filter tabs, new-post button for customers

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

91 lines
2.6 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: { status: v.optional(v.union(v.literal("open"), v.literal("closed"))) },
handler: async (ctx, args) => {
if (args.status) {
return await ctx.db
.query("posts")
.withIndex("by_status", (q) => q.eq("status", args.status!))
.order("desc")
.collect();
}
return await ctx.db.query("posts").order("desc").collect();
},
});
export const getByCustomer = query({
args: { customerId: v.id("users") },
handler: async (ctx, args) => {
return await ctx.db
.query("posts")
.withIndex("by_customer", (q) => q.eq("customerId", args.customerId))
.order("desc")
.collect();
},
});
export const getById = query({
args: { id: v.id("posts") },
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
},
});
export const create = mutation({
args: {
token: v.string(),
title: v.string(),
description: v.string(),
category: v.optional(v.string()),
location: v.optional(v.string()),
budget: v.optional(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("Корисникот не е пронајден");
const now = Date.now();
const postId = await ctx.db.insert("posts", {
customerId: session.userId,
title: args.title,
description: args.description,
category: args.category,
location: args.location,
budget: args.budget,
status: "open",
createdAt: now,
updatedAt: now,
});
return postId;
},
});
export const close = mutation({
args: {
token: v.string(),
postId: v.id("posts"),
},
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 post = await ctx.db.get(args.postId);
if (!post) throw new Error("Побарувањето не е пронајдено");
if (post.customerId !== session.userId) throw new Error("Само авторот може да го затвори побарувањето");
await ctx.db.patch(args.postId, { status: "closed", updatedAt: Date.now() });
return true;
},
});