- Error boundary with Macedonian fallback screen and retry button wrapping all navigation - Pull-to-refresh on all list/detail screens (home, explore, posts, chat, profile, ad detail, post detail) - Accessibility: labels on all interactive elements, roles on Pressable/Button, selectable text for user data - Deep linking: mojmajstor:// scheme configured in app.json, ad/[id] and chat/[id] routes work via Expo Router auto-routing - Pagination: cursor-based paginated queries for ads, posts, messages, chats, reviews with Load More buttons on frontend - Profile: shows customer posts with PostCard, loading/error states, create-post navigation - Auth error messages in Macedonian (Невалидни акредитиви, итн.) All UI text in Macedonian. TypeScript clean, Convex functions deployed.
102 lines
3.0 KiB
TypeScript
102 lines
3.0 KiB
TypeScript
import { query, mutation } from "./_generated/server";
|
||
import { v } from "convex/values";
|
||
import { paginationOptsValidator } from "convex/server";
|
||
|
||
export const getByAd = query({
|
||
args: { adId: v.id("ads") },
|
||
handler: async (ctx, args) => {
|
||
return await ctx.db
|
||
.query("reviews")
|
||
.withIndex("by_ad", (q) => q.eq("adId", args.adId))
|
||
.order("desc")
|
||
.collect();
|
||
},
|
||
});
|
||
|
||
export const getByAdPaginated = query({
|
||
args: { adId: v.id("ads"), paginationOpts: paginationOptsValidator },
|
||
handler: async (ctx, args) => {
|
||
return await ctx.db
|
||
.query("reviews")
|
||
.withIndex("by_ad", (q) => q.eq("adId", args.adId))
|
||
.order("desc")
|
||
.paginate(args.paginationOpts);
|
||
},
|
||
});
|
||
|
||
export const getByHandyman = query({
|
||
args: { handymanId: v.id("users") },
|
||
handler: async (ctx, args) => {
|
||
return await ctx.db
|
||
.query("reviews")
|
||
.withIndex("by_handyman", (q) => q.eq("handymanId", args.handymanId))
|
||
.order("desc")
|
||
.collect();
|
||
},
|
||
});
|
||
|
||
export const getByHandymanPaginated = query({
|
||
args: { handymanId: v.id("users"), paginationOpts: paginationOptsValidator },
|
||
handler: async (ctx, args) => {
|
||
return await ctx.db
|
||
.query("reviews")
|
||
.withIndex("by_handyman", (q) => q.eq("handymanId", args.handymanId))
|
||
.order("desc")
|
||
.paginate(args.paginationOpts);
|
||
},
|
||
});
|
||
|
||
export const create = mutation({
|
||
args: {
|
||
token: v.string(),
|
||
adId: v.id("ads"),
|
||
rating: v.number(),
|
||
comment: v.optional(v.string()),
|
||
},
|
||
handler: async (ctx, args) => {
|
||
if (args.rating < 1 || args.rating > 5) {
|
||
throw new Error("Оцената мора да биде помеѓу 1 и 5");
|
||
}
|
||
|
||
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 !== "customer") 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 reviewId = await ctx.db.insert("reviews", {
|
||
adId: args.adId,
|
||
customerId: session.userId,
|
||
handymanId: ad.handymanId,
|
||
rating: args.rating,
|
||
comment: args.comment,
|
||
createdAt: Date.now(),
|
||
});
|
||
|
||
const allReviews = await ctx.db
|
||
.query("reviews")
|
||
.withIndex("by_ad", (q) => q.eq("adId", args.adId))
|
||
.collect();
|
||
|
||
const totalRating = allReviews.reduce((sum, r) => sum + r.rating, 0);
|
||
const ratingAvg = totalRating / allReviews.length;
|
||
|
||
await ctx.db.patch(args.adId, {
|
||
ratingAvg,
|
||
reviewCount: allReviews.length,
|
||
});
|
||
|
||
return reviewId;
|
||
},
|
||
}); |