levrops-contracts 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -0
- package/contracts/content/README.md +108 -0
- package/contracts/content/example.ts +142 -0
- package/contracts/content/heirloom/product.ts +98 -0
- package/contracts/content/index.ts +70 -0
- package/contracts/content/shared/index.ts +13 -0
- package/contracts/content/studioops/artist.ts +102 -0
- package/contracts/content/types.ts +180 -0
- package/contracts/content/utils.ts +148 -0
- package/index.ts +20 -0
- package/package.json +51 -0
- package/sanity/generated/schema-heirloom.ts +223 -0
- package/sanity/generated/schema-studioops.ts +213 -0
- package/sanity/generated/schema.ts +293 -0
- package/sanity/utils.ts +99 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Contract Types
|
|
3
|
+
*
|
|
4
|
+
* Defines the structure for content contracts that describe Sanity schema.
|
|
5
|
+
* LevrOps is the source of truth for content models; Sanity Studio consumes
|
|
6
|
+
* generated schema from these contracts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Supported field types in content contracts.
|
|
11
|
+
*/
|
|
12
|
+
export type ContentFieldType =
|
|
13
|
+
| "string"
|
|
14
|
+
| "text"
|
|
15
|
+
| "number"
|
|
16
|
+
| "boolean"
|
|
17
|
+
| "date"
|
|
18
|
+
| "datetime"
|
|
19
|
+
| "url"
|
|
20
|
+
| "email"
|
|
21
|
+
| "slug"
|
|
22
|
+
| "richText"
|
|
23
|
+
| "image"
|
|
24
|
+
| "file"
|
|
25
|
+
| "array"
|
|
26
|
+
| "object"
|
|
27
|
+
| "reference";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Options for field configuration.
|
|
31
|
+
* Field type-specific options are provided here.
|
|
32
|
+
*/
|
|
33
|
+
export interface ContentFieldOptions {
|
|
34
|
+
/**
|
|
35
|
+
* For string fields: max length
|
|
36
|
+
* For array fields: max items
|
|
37
|
+
*/
|
|
38
|
+
maxLength?: number;
|
|
39
|
+
/**
|
|
40
|
+
* For string fields: min length
|
|
41
|
+
* For array fields: min items
|
|
42
|
+
*/
|
|
43
|
+
minLength?: number;
|
|
44
|
+
/**
|
|
45
|
+
* For string fields: pattern/regex validation
|
|
46
|
+
*/
|
|
47
|
+
pattern?: string;
|
|
48
|
+
/**
|
|
49
|
+
* For number fields: min value
|
|
50
|
+
*/
|
|
51
|
+
min?: number;
|
|
52
|
+
/**
|
|
53
|
+
* For number fields: max value
|
|
54
|
+
*/
|
|
55
|
+
max?: number;
|
|
56
|
+
/**
|
|
57
|
+
* For string fields: enum of allowed values
|
|
58
|
+
*/
|
|
59
|
+
options?: string[];
|
|
60
|
+
/**
|
|
61
|
+
* For array fields: the type of items in the array
|
|
62
|
+
*/
|
|
63
|
+
of?: ContentFieldType;
|
|
64
|
+
/**
|
|
65
|
+
* For reference fields: array of document type names this can reference
|
|
66
|
+
*/
|
|
67
|
+
to?: string[];
|
|
68
|
+
/**
|
|
69
|
+
* For slug fields: the field name to use as source (default: "title")
|
|
70
|
+
*/
|
|
71
|
+
source?: string;
|
|
72
|
+
/**
|
|
73
|
+
* For richText fields: allowed annotation types (e.g., ["link", "internalLink"])
|
|
74
|
+
*/
|
|
75
|
+
annotations?: string[];
|
|
76
|
+
/**
|
|
77
|
+
* For richText fields: allowed decorators (e.g., ["strong", "em", "code"])
|
|
78
|
+
*/
|
|
79
|
+
decorators?: string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A field definition within a content contract.
|
|
84
|
+
*/
|
|
85
|
+
export interface ContentField {
|
|
86
|
+
/**
|
|
87
|
+
* Field name (camelCase)
|
|
88
|
+
*/
|
|
89
|
+
name: string;
|
|
90
|
+
/**
|
|
91
|
+
* Field type
|
|
92
|
+
*/
|
|
93
|
+
type: ContentFieldType;
|
|
94
|
+
/**
|
|
95
|
+
* Field title (human-readable, used in Sanity Studio)
|
|
96
|
+
*/
|
|
97
|
+
title?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Field description
|
|
100
|
+
*/
|
|
101
|
+
description?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Whether the field is required
|
|
104
|
+
*/
|
|
105
|
+
required?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Type-specific field options
|
|
108
|
+
*/
|
|
109
|
+
options?: ContentFieldOptions;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Multi-tenant scope configuration for content contracts.
|
|
114
|
+
*/
|
|
115
|
+
export interface ContentContractScope {
|
|
116
|
+
/**
|
|
117
|
+
* Tenant slugs that can use this contract.
|
|
118
|
+
* If empty or undefined, the contract is available to all tenants.
|
|
119
|
+
* Examples: ["heirloom", "studioops"]
|
|
120
|
+
*/
|
|
121
|
+
tenants?: string[];
|
|
122
|
+
/**
|
|
123
|
+
* Property slugs that can use this contract.
|
|
124
|
+
* If empty or undefined, the contract is available to all properties.
|
|
125
|
+
* Examples: ["store1", "store2"]
|
|
126
|
+
*/
|
|
127
|
+
properties?: string[];
|
|
128
|
+
/**
|
|
129
|
+
* Whether this contract is required for specific tenants/properties.
|
|
130
|
+
*/
|
|
131
|
+
requiredFor?: {
|
|
132
|
+
/**
|
|
133
|
+
* Tenant slugs where this contract is required.
|
|
134
|
+
*/
|
|
135
|
+
tenants?: string[];
|
|
136
|
+
/**
|
|
137
|
+
* Property slugs where this contract is required.
|
|
138
|
+
*/
|
|
139
|
+
properties?: string[];
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A content contract defining a document or object type.
|
|
145
|
+
*/
|
|
146
|
+
export interface ContentContract {
|
|
147
|
+
/**
|
|
148
|
+
* Unique identifier for the contract (kebab-case, e.g., "blog-post", "hero-section")
|
|
149
|
+
*/
|
|
150
|
+
id: string;
|
|
151
|
+
/**
|
|
152
|
+
* Human-readable title (used in Sanity Studio UI)
|
|
153
|
+
*/
|
|
154
|
+
title: string;
|
|
155
|
+
/**
|
|
156
|
+
* Whether this is a document (can be queried standalone) or object (nested type)
|
|
157
|
+
*/
|
|
158
|
+
type: "document" | "object";
|
|
159
|
+
/**
|
|
160
|
+
* Description of the content type
|
|
161
|
+
*/
|
|
162
|
+
description?: string;
|
|
163
|
+
/**
|
|
164
|
+
* Fields that make up this content type
|
|
165
|
+
*/
|
|
166
|
+
fields: ContentField[];
|
|
167
|
+
/**
|
|
168
|
+
* For document types: preview configuration
|
|
169
|
+
*/
|
|
170
|
+
preview?: {
|
|
171
|
+
select: Record<string, string>;
|
|
172
|
+
prepare?: (selection: Record<string, unknown>) => { title?: string; subtitle?: string };
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Multi-tenant/property scope configuration.
|
|
176
|
+
* If omitted, the contract is available to all tenants and properties.
|
|
177
|
+
*/
|
|
178
|
+
scope?: ContentContractScope;
|
|
179
|
+
}
|
|
180
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Contract Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for filtering and managing content contracts by tenant and property.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ContentContract } from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Filter content contracts by tenant.
|
|
11
|
+
*
|
|
12
|
+
* @param contracts - Array of content contracts to filter
|
|
13
|
+
* @param tenant - Tenant slug to filter by
|
|
14
|
+
* @returns Filtered contracts that are available to the specified tenant
|
|
15
|
+
*/
|
|
16
|
+
export function filterContractsByTenant(
|
|
17
|
+
contracts: ContentContract[],
|
|
18
|
+
tenant: string
|
|
19
|
+
): ContentContract[] {
|
|
20
|
+
return contracts.filter((contract) => {
|
|
21
|
+
const scope = contract.scope;
|
|
22
|
+
|
|
23
|
+
// If no scope, available to all tenants
|
|
24
|
+
if (!scope || !scope.tenants || scope.tenants.length === 0) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if tenant is in the allowed list
|
|
29
|
+
return scope.tenants.includes(tenant);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Filter content contracts by property.
|
|
35
|
+
*
|
|
36
|
+
* @param contracts - Array of content contracts to filter
|
|
37
|
+
* @param property - Property slug to filter by
|
|
38
|
+
* @returns Filtered contracts that are available to the specified property
|
|
39
|
+
*/
|
|
40
|
+
export function filterContractsByProperty(
|
|
41
|
+
contracts: ContentContract[],
|
|
42
|
+
property: string
|
|
43
|
+
): ContentContract[] {
|
|
44
|
+
return contracts.filter((contract) => {
|
|
45
|
+
const scope = contract.scope;
|
|
46
|
+
|
|
47
|
+
// If no scope, available to all properties
|
|
48
|
+
if (!scope || !scope.properties || scope.properties.length === 0) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if property is in the allowed list
|
|
53
|
+
return scope.properties.includes(property);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Filter content contracts by tenant and property.
|
|
59
|
+
*
|
|
60
|
+
* @param contracts - Array of content contracts to filter
|
|
61
|
+
* @param tenant - Tenant slug to filter by
|
|
62
|
+
* @param property - Optional property slug to filter by
|
|
63
|
+
* @returns Filtered contracts that are available to the specified tenant (and property if provided)
|
|
64
|
+
*/
|
|
65
|
+
export function filterContracts(
|
|
66
|
+
contracts: ContentContract[],
|
|
67
|
+
tenant: string,
|
|
68
|
+
property?: string
|
|
69
|
+
): ContentContract[] {
|
|
70
|
+
let filtered = filterContractsByTenant(contracts, tenant);
|
|
71
|
+
|
|
72
|
+
if (property) {
|
|
73
|
+
filtered = filterContractsByProperty(filtered, property);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return filtered;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get contracts required for a specific tenant.
|
|
81
|
+
*
|
|
82
|
+
* @param contracts - Array of content contracts
|
|
83
|
+
* @param tenant - Tenant slug
|
|
84
|
+
* @returns Contracts that are required for the specified tenant
|
|
85
|
+
*/
|
|
86
|
+
export function getRequiredContractsForTenant(
|
|
87
|
+
contracts: ContentContract[],
|
|
88
|
+
tenant: string
|
|
89
|
+
): ContentContract[] {
|
|
90
|
+
return contracts.filter((contract) => {
|
|
91
|
+
const scope = contract.scope;
|
|
92
|
+
return scope?.requiredFor?.tenants?.includes(tenant) ?? false;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get contracts required for a specific property.
|
|
98
|
+
*
|
|
99
|
+
* @param contracts - Array of content contracts
|
|
100
|
+
* @param property - Property slug
|
|
101
|
+
* @returns Contracts that are required for the specified property
|
|
102
|
+
*/
|
|
103
|
+
export function getRequiredContractsForProperty(
|
|
104
|
+
contracts: ContentContract[],
|
|
105
|
+
property: string
|
|
106
|
+
): ContentContract[] {
|
|
107
|
+
return contracts.filter((contract) => {
|
|
108
|
+
const scope = contract.scope;
|
|
109
|
+
return scope?.requiredFor?.properties?.includes(property) ?? false;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Group contracts by tenant.
|
|
115
|
+
*
|
|
116
|
+
* @param contracts - Array of content contracts
|
|
117
|
+
* @returns Object mapping tenant slugs to their available contracts
|
|
118
|
+
*/
|
|
119
|
+
export function groupContractsByTenant(
|
|
120
|
+
contracts: ContentContract[]
|
|
121
|
+
): Record<string, ContentContract[]> {
|
|
122
|
+
const grouped: Record<string, ContentContract[]> = {};
|
|
123
|
+
|
|
124
|
+
contracts.forEach((contract) => {
|
|
125
|
+
const scope = contract.scope;
|
|
126
|
+
|
|
127
|
+
// If no scope, add to all tenants (we'll handle this separately)
|
|
128
|
+
if (!scope || !scope.tenants || scope.tenants.length === 0) {
|
|
129
|
+
// Shared contracts - add to a special key
|
|
130
|
+
if (!grouped["_shared"]) {
|
|
131
|
+
grouped["_shared"] = [];
|
|
132
|
+
}
|
|
133
|
+
grouped["_shared"].push(contract);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Add to each tenant's list
|
|
138
|
+
scope.tenants.forEach((tenant) => {
|
|
139
|
+
if (!grouped[tenant]) {
|
|
140
|
+
grouped[tenant] = [];
|
|
141
|
+
}
|
|
142
|
+
grouped[tenant].push(contract);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return grouped;
|
|
147
|
+
}
|
|
148
|
+
|
package/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LevrOps Contracts
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for consuming content contracts and generated Sanity schemas.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Export content contracts
|
|
8
|
+
export * from "./contracts/content";
|
|
9
|
+
|
|
10
|
+
// Export Sanity utilities
|
|
11
|
+
export * from "./sanity/utils";
|
|
12
|
+
|
|
13
|
+
// Re-export commonly used types
|
|
14
|
+
export type {
|
|
15
|
+
ContentContract,
|
|
16
|
+
ContentField,
|
|
17
|
+
ContentFieldOptions,
|
|
18
|
+
ContentContractScope,
|
|
19
|
+
} from "./contracts/content/types";
|
|
20
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "levrops-contracts",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "LevrOps API contracts, schemas, code generators, and Sanity content contracts",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/jackmckracken/levrops-contracts.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/jackmckracken/levrops-contracts#readme",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./index.ts",
|
|
13
|
+
"types": "./index.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./index.ts",
|
|
17
|
+
"import": "./index.ts"
|
|
18
|
+
},
|
|
19
|
+
"./sanity/generated/schema": "./sanity/generated/schema.ts",
|
|
20
|
+
"./sanity/generated/schema-*": "./sanity/generated/schema-*.ts",
|
|
21
|
+
"./sanity/utils": "./sanity/utils.ts",
|
|
22
|
+
"./contracts/content": "./contracts/content/index.ts"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"contracts/",
|
|
26
|
+
"sanity/",
|
|
27
|
+
"index.ts",
|
|
28
|
+
"README.md",
|
|
29
|
+
"package.json",
|
|
30
|
+
"tsconfig.json"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"sanity:codegen": "tsx tools/levrops-sanity-codegen.ts",
|
|
34
|
+
"sanity:check": "tsx tools/sanity-check.ts"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"sanity": "^3.56.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^20.11.24",
|
|
41
|
+
"tsx": "^4.7.0",
|
|
42
|
+
"typescript": "^5.4.0"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { defineType, defineField, string, text, number, boolean, date, datetime, url, email, slug, image, file, array, object, reference } from "sanity";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generated Sanity Schema
|
|
5
|
+
*
|
|
6
|
+
* This file is auto-generated from content contracts in contracts/content/.
|
|
7
|
+
* DO NOT EDIT THIS FILE DIRECTLY.
|
|
8
|
+
*
|
|
9
|
+
* Content contracts for heirloom tenant
|
|
10
|
+
*
|
|
11
|
+
* To update the schema:
|
|
12
|
+
* 1. Update or add contracts in contracts/content/
|
|
13
|
+
* 2. Run: npm run sanity:codegen
|
|
14
|
+
* (or: npm run sanity:codegen -- --tenant=<tenant> --property=<property>)
|
|
15
|
+
*
|
|
16
|
+
* Generated: 2025-11-14T19:30:16.455Z
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Blog Post
|
|
20
|
+
// A blog post article
|
|
21
|
+
export const BlogPost = defineType({
|
|
22
|
+
name: "blog-post",
|
|
23
|
+
title: "Blog Post",
|
|
24
|
+
type: "document",
|
|
25
|
+
description: "A blog post article",
|
|
26
|
+
fields: [
|
|
27
|
+
defineField({
|
|
28
|
+
name: "title",
|
|
29
|
+
type: string(),
|
|
30
|
+
title: "Title",
|
|
31
|
+
validation: (Rule) => Rule.required().and(Rule.max(200)),
|
|
32
|
+
|
|
33
|
+
}),
|
|
34
|
+
defineField({
|
|
35
|
+
name: "slug",
|
|
36
|
+
type: slug({ source: "title" }),
|
|
37
|
+
title: "Slug",
|
|
38
|
+
validation: (Rule) => Rule.required(),
|
|
39
|
+
|
|
40
|
+
}),
|
|
41
|
+
defineField({
|
|
42
|
+
name: "author",
|
|
43
|
+
type: reference({ to: [{ type: "author" }] }),
|
|
44
|
+
title: "Author",
|
|
45
|
+
validation: (Rule) => Rule.required(),
|
|
46
|
+
|
|
47
|
+
}),
|
|
48
|
+
defineField({
|
|
49
|
+
name: "publishedAt",
|
|
50
|
+
type: datetime(),
|
|
51
|
+
title: "Published At",
|
|
52
|
+
validation: (Rule) => Rule.required(),
|
|
53
|
+
|
|
54
|
+
}),
|
|
55
|
+
defineField({
|
|
56
|
+
name: "excerpt",
|
|
57
|
+
type: text(),
|
|
58
|
+
title: "Excerpt",
|
|
59
|
+
description: "Short summary for previews",
|
|
60
|
+
|
|
61
|
+
}),
|
|
62
|
+
defineField({
|
|
63
|
+
name: "content",
|
|
64
|
+
type: array({
|
|
65
|
+
of: [{ type: "block" }],
|
|
66
|
+
}),
|
|
67
|
+
title: "Content",
|
|
68
|
+
validation: (Rule) => Rule.required(),
|
|
69
|
+
|
|
70
|
+
}),
|
|
71
|
+
defineField({
|
|
72
|
+
name: "tags",
|
|
73
|
+
type: array({
|
|
74
|
+
of: [string()],
|
|
75
|
+
}),
|
|
76
|
+
title: "Tags",
|
|
77
|
+
|
|
78
|
+
}),
|
|
79
|
+
defineField({
|
|
80
|
+
name: "featuredImage",
|
|
81
|
+
type: image(),
|
|
82
|
+
title: "Featured Image",
|
|
83
|
+
|
|
84
|
+
}),
|
|
85
|
+
],
|
|
86
|
+
preview: {
|
|
87
|
+
select: {
|
|
88
|
+
"title": "title",
|
|
89
|
+
"author": "author.name",
|
|
90
|
+
"publishedAt": "publishedAt"
|
|
91
|
+
},
|
|
92
|
+
// prepare: (selection) => { ... } // Custom prepare function
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Hero Section
|
|
97
|
+
// Hero banner section for landing pages
|
|
98
|
+
export const HeroSection = defineType({
|
|
99
|
+
name: "hero-section",
|
|
100
|
+
title: "Hero Section",
|
|
101
|
+
type: "object",
|
|
102
|
+
description: "Hero banner section for landing pages",
|
|
103
|
+
fields: [
|
|
104
|
+
defineField({
|
|
105
|
+
name: "headline",
|
|
106
|
+
type: string(),
|
|
107
|
+
title: "Headline",
|
|
108
|
+
validation: (Rule) => Rule.required().and(Rule.max(100)),
|
|
109
|
+
|
|
110
|
+
}),
|
|
111
|
+
defineField({
|
|
112
|
+
name: "subheadline",
|
|
113
|
+
type: text(),
|
|
114
|
+
title: "Subheadline",
|
|
115
|
+
|
|
116
|
+
}),
|
|
117
|
+
defineField({
|
|
118
|
+
name: "image",
|
|
119
|
+
type: image(),
|
|
120
|
+
title: "Background Image",
|
|
121
|
+
validation: (Rule) => Rule.required(),
|
|
122
|
+
|
|
123
|
+
}),
|
|
124
|
+
defineField({
|
|
125
|
+
name: "ctaText",
|
|
126
|
+
type: string(),
|
|
127
|
+
title: "CTA Button Text",
|
|
128
|
+
|
|
129
|
+
}),
|
|
130
|
+
defineField({
|
|
131
|
+
name: "ctaLink",
|
|
132
|
+
type: url(),
|
|
133
|
+
title: "CTA Link",
|
|
134
|
+
|
|
135
|
+
}),
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Product
|
|
140
|
+
// Product for Heirloom Supply catalog
|
|
141
|
+
export const Product = defineType({
|
|
142
|
+
name: "product",
|
|
143
|
+
title: "Product",
|
|
144
|
+
type: "document",
|
|
145
|
+
description: "Product for Heirloom Supply catalog",
|
|
146
|
+
fields: [
|
|
147
|
+
defineField({
|
|
148
|
+
name: "title",
|
|
149
|
+
type: string(),
|
|
150
|
+
title: "Product Name",
|
|
151
|
+
validation: (Rule) => Rule.required().and(Rule.max(200)),
|
|
152
|
+
|
|
153
|
+
}),
|
|
154
|
+
defineField({
|
|
155
|
+
name: "slug",
|
|
156
|
+
type: slug({ source: "title" }),
|
|
157
|
+
title: "Slug",
|
|
158
|
+
validation: (Rule) => Rule.required(),
|
|
159
|
+
|
|
160
|
+
}),
|
|
161
|
+
defineField({
|
|
162
|
+
name: "description",
|
|
163
|
+
type: array({
|
|
164
|
+
of: [{ type: "block" }],
|
|
165
|
+
}),
|
|
166
|
+
title: "Description",
|
|
167
|
+
validation: (Rule) => Rule.required(),
|
|
168
|
+
|
|
169
|
+
}),
|
|
170
|
+
defineField({
|
|
171
|
+
name: "price",
|
|
172
|
+
type: number(),
|
|
173
|
+
title: "Price",
|
|
174
|
+
validation: (Rule) => Rule.required().and(Rule.min(0)),
|
|
175
|
+
|
|
176
|
+
}),
|
|
177
|
+
defineField({
|
|
178
|
+
name: "images",
|
|
179
|
+
type: array({
|
|
180
|
+
of: [image()],
|
|
181
|
+
validation: (Rule) => Rule.min(1),
|
|
182
|
+
}),
|
|
183
|
+
title: "Product Images",
|
|
184
|
+
validation: (Rule) => Rule.required(),
|
|
185
|
+
|
|
186
|
+
}),
|
|
187
|
+
defineField({
|
|
188
|
+
name: "category",
|
|
189
|
+
type: reference({ to: [{ type: "category" }] }),
|
|
190
|
+
title: "Category",
|
|
191
|
+
|
|
192
|
+
}),
|
|
193
|
+
defineField({
|
|
194
|
+
name: "tags",
|
|
195
|
+
type: array({
|
|
196
|
+
of: [string()],
|
|
197
|
+
}),
|
|
198
|
+
title: "Tags",
|
|
199
|
+
|
|
200
|
+
}),
|
|
201
|
+
defineField({
|
|
202
|
+
name: "inStock",
|
|
203
|
+
type: boolean(),
|
|
204
|
+
title: "In Stock",
|
|
205
|
+
|
|
206
|
+
}),
|
|
207
|
+
],
|
|
208
|
+
preview: {
|
|
209
|
+
select: {
|
|
210
|
+
"title": "title",
|
|
211
|
+
"price": "price",
|
|
212
|
+
"inStock": "inStock"
|
|
213
|
+
},
|
|
214
|
+
// prepare: (selection) => { ... } // Custom prepare function
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Export all schemas
|
|
219
|
+
export const allSchemas = [
|
|
220
|
+
BlogPost,
|
|
221
|
+
HeroSection,
|
|
222
|
+
Product,
|
|
223
|
+
];
|