Files
setrip/features/trip/schemas.ts
T

182 lines
5.0 KiB
TypeScript

import { z } from "zod/v4";
import { LIMITS } from "@/lib/limits";
import { ACTIVITY_CATEGORIES } from "@/lib/activity-category";
import { VIBES } from "@/lib/vibe";
import type { ActivityCategory } from "@/app/generated/prisma/enums";
import {
isTripDepartureDayPast,
tripStoredInstantFromYmd,
} from "@/lib/trip-dates";
export const tripImageUrlsSchema = z
.array(
z
.string()
.trim()
.max(LIMITS.MAX_URL_LENGTH, "URL terlalu panjang")
.url("Setiap URL gambar harus valid (http/https)")
)
.max(LIMITS.MAX_IMAGE_URLS, `Maksimal ${LIMITS.MAX_IMAGE_URLS} foto`);
export const createTripSchema = z
.object({
category: z.enum(
ACTIVITY_CATEGORIES as [ActivityCategory, ...ActivityCategory[]],
{ message: "Kategori aktivitas tidak valid" }
),
title: z
.string()
.trim()
.min(3, "Judul minimal 3 karakter")
.max(
LIMITS.MAX_TITLE_LENGTH,
`Judul maksimal ${LIMITS.MAX_TITLE_LENGTH} karakter`
),
description: z.preprocess(
(val) => {
if (val == null) return undefined;
const s = String(val).trim();
return s === "" ? undefined : s;
},
z
.string()
.max(
LIMITS.MAX_DESCRIPTION_LENGTH,
`Deskripsi maksimal ${LIMITS.MAX_DESCRIPTION_LENGTH} karakter`
)
.optional()
),
destination: z
.string()
.trim()
.min(2, "Destinasi harus diisi")
.max(
LIMITS.MAX_DESTINATION_LENGTH,
`Destinasi maksimal ${LIMITS.MAX_DESTINATION_LENGTH} karakter`
),
location: z
.string()
.trim()
.min(2, "Lokasi harus diisi")
.max(
LIMITS.MAX_LOCATION_LENGTH,
`Lokasi maksimal ${LIMITS.MAX_LOCATION_LENGTH} karakter`
),
date: z
.string()
.refine((val) => !Number.isNaN(Date.parse(val)), "Tanggal berangkat tidak valid"),
endDate: z
.string()
.optional()
.refine(
(val) => !val || !Number.isNaN(Date.parse(val)),
"Tanggal pulang tidak valid"
),
maxParticipants: z.coerce
.number()
.int("Jumlah peserta harus bilangan bulat")
.min(
LIMITS.MIN_PARTICIPANTS,
`Minimal ${LIMITS.MIN_PARTICIPANTS} peserta`
)
.max(
LIMITS.MAX_PARTICIPANTS,
`Maksimal ${LIMITS.MAX_PARTICIPANTS} peserta`
),
price: z.coerce
.number()
.int("Harga harus bilangan bulat (tanpa desimal)")
.min(0, "Harga tidak valid")
.max(
LIMITS.MAX_PRICE_IDR,
`Harga maksimal Rp ${LIMITS.MAX_PRICE_IDR.toLocaleString("id-ID")}`
),
meetingPoint: z.preprocess(
(val) => {
if (val == null) return undefined;
const s = String(val).trim();
return s === "" ? undefined : s;
},
z
.string()
.max(
LIMITS.MAX_MEETING_POINT_LENGTH,
`Meeting point maksimal ${LIMITS.MAX_MEETING_POINT_LENGTH} karakter`
)
.optional()
),
itinerary: z.preprocess(
(val) => {
if (val == null) return undefined;
const s = String(val).trim();
return s === "" ? undefined : s;
},
z
.string()
.max(
LIMITS.MAX_TRIP_ITINERARY_LENGTH,
`Itinerary maksimal ${LIMITS.MAX_TRIP_ITINERARY_LENGTH} karakter`
)
.optional()
),
whatsIncluded: z.preprocess(
(val) => {
if (val == null) return undefined;
const s = String(val).trim();
return s === "" ? undefined : s;
},
z
.string()
.max(
LIMITS.MAX_TRIP_BULLET_SECTION_LENGTH,
`Bagian 'Termasuk' maksimal ${LIMITS.MAX_TRIP_BULLET_SECTION_LENGTH} karakter`
)
.optional()
),
whatsExcluded: z.preprocess(
(val) => {
if (val == null) return undefined;
const s = String(val).trim();
return s === "" ? undefined : s;
},
z
.string()
.max(
LIMITS.MAX_TRIP_BULLET_SECTION_LENGTH,
`Bagian 'Tidak termasuk' maksimal ${LIMITS.MAX_TRIP_BULLET_SECTION_LENGTH} karakter`
)
.optional()
),
vibe: z.preprocess(
(val) => {
if (val == null) return undefined;
const s = String(val).trim();
return s === "" ? undefined : s;
},
z.enum([...VIBES]).optional()
),
})
.superRefine((data, ctx) => {
const dep = tripStoredInstantFromYmd(data.date);
if (!Number.isNaN(dep.getTime()) && isTripDepartureDayPast(dep)) {
ctx.addIssue({
code: "custom",
message: "Tanggal berangkat tidak boleh di masa lalu",
path: ["date"],
});
}
if (data.endDate) {
const startY = data.date.slice(0, 10);
const endY = data.endDate.slice(0, 10);
if (endY < startY) {
ctx.addIssue({
code: "custom",
message: "Tanggal pulang tidak boleh sebelum tanggal berangkat",
path: ["endDate"],
});
}
}
});
export type CreateTripInput = z.infer<typeof createTripSchema>;