76 lines
2.6 KiB
TypeScript
76 lines
2.6 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
import { LOCALES } from '@sport/types';
|
|
|
|
import { anyIdSchema, slugSchema } from './common';
|
|
import { offsetPageQuerySchema } from './pagination';
|
|
|
|
/**
|
|
* Per-locale copy for a taxonomy row.
|
|
*
|
|
* A partial record, like products and content: publishing an English-only
|
|
* brand is legitimate, and demanding every locale would mean inventing
|
|
* Vietnamese copy before anything could be saved.
|
|
*/
|
|
const translationSchema = z.object({
|
|
name: z.string().trim().min(1).max(160),
|
|
slug: slugSchema,
|
|
description: z.string().trim().max(2000).nullish(),
|
|
metaTitle: z.string().trim().max(255).nullish(),
|
|
metaDescription: z.string().trim().max(500).nullish(),
|
|
});
|
|
|
|
const baseSchema = {
|
|
/** The fallback name, used when a locale has no translation. */
|
|
name: z.string().trim().min(1).max(160),
|
|
slug: slugSchema,
|
|
description: z.string().trim().max(2000).nullish(),
|
|
isActive: z.boolean().default(true),
|
|
metaTitle: z.string().trim().max(255).nullish(),
|
|
metaDescription: z.string().trim().max(500).nullish(),
|
|
translations: z.partialRecord(z.enum(LOCALES), translationSchema).default({}),
|
|
};
|
|
|
|
export const brandInputSchema = z.object({
|
|
...baseSchema,
|
|
logoId: anyIdSchema.nullish(),
|
|
});
|
|
|
|
export const categoryInputSchema = z.object({
|
|
...baseSchema,
|
|
/**
|
|
* Null means a root category. The service rejects a parent that is the
|
|
* category itself or one of its own descendants — that would detach the
|
|
* subtree from the tree and orphan every product under it.
|
|
*/
|
|
parentId: anyIdSchema.nullish(),
|
|
position: z.int().min(0).max(9999).default(0),
|
|
imageId: anyIdSchema.nullish(),
|
|
});
|
|
|
|
export const collectionInputSchema = z
|
|
.object({
|
|
...baseSchema,
|
|
type: z.enum(['MANUAL', 'AUTOMATED']).default('MANUAL'),
|
|
bannerId: anyIdSchema.nullish(),
|
|
startsAt: z.iso.datetime().nullish(),
|
|
endsAt: z.iso.datetime().nullish(),
|
|
position: z.int().min(0).max(9999).default(0),
|
|
})
|
|
.refine(
|
|
(value) =>
|
|
!value.startsAt || !value.endsAt || new Date(value.startsAt) < new Date(value.endsAt),
|
|
{ message: 'The end date must be after the start date', path: ['endsAt'] },
|
|
);
|
|
|
|
export const taxonomyListQuerySchema = offsetPageQuerySchema.extend({
|
|
q: z.string().trim().max(120).optional(),
|
|
/** Admin default is "everything"; the storefront only ever sees active rows. */
|
|
activeOnly: z.coerce.boolean().optional(),
|
|
});
|
|
|
|
export type BrandInput = z.output<typeof brandInputSchema>;
|
|
export type CategoryInput = z.output<typeof categoryInputSchema>;
|
|
export type CollectionInput = z.output<typeof collectionInputSchema>;
|
|
export type TaxonomyListQuery = z.output<typeof taxonomyListQuerySchema>;
|