Initial commit
This commit is contained in:
commit
61f6d7ce21
26 changed files with 23763 additions and 0 deletions
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# Dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# Compiled Sanity Studio
|
||||
/dist
|
||||
|
||||
# Temporary Sanity runtime, generated by the CLI on every dev server start
|
||||
/.sanity
|
||||
|
||||
# Logs
|
||||
/logs
|
||||
*.log
|
||||
|
||||
# Coverage directory used by testing tools
|
||||
/coverage
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# Typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Dotenv and similar local-only files
|
||||
.env
|
||||
.env.*
|
||||
*.local
|
||||
9
README.md
Normal file
9
README.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Sanity Clean Content Studio
|
||||
|
||||
Congratulations, you have now installed the Sanity Content Studio, an open-source real-time content editing environment connected to the Sanity backend.
|
||||
|
||||
Now you can do the following things:
|
||||
|
||||
- [Read “getting started” in the docs](https://www.sanity.io/docs/introduction/getting-started?utm_source=readme)
|
||||
- [Join the Sanity community](https://www.sanity.io/community/join?utm_source=readme)
|
||||
- [Extend and build plugins](https://www.sanity.io/docs/content-studio/extending?utm_source=readme)
|
||||
54
components/CountryInput.tsx
Normal file
54
components/CountryInput.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import React, {useCallback, useMemo, useState} from 'react'
|
||||
import {Autocomplete, Card, Text} from '@sanity/ui'
|
||||
import {set, unset, StringInputProps} from 'sanity'
|
||||
import {countryList} from '../schemaTypes/country-list'
|
||||
|
||||
const options = countryList.map((c) => ({value: c.value, payload: c}))
|
||||
|
||||
export function CountryInput(props: StringInputProps) {
|
||||
const {value, onChange, readOnly} = props
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!query) return options
|
||||
const q = query.toLowerCase()
|
||||
return options.filter((o) => o.payload.title.toLowerCase().includes(q))
|
||||
}, [query])
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(val: string) => {
|
||||
onChange(val ? set(val) : unset())
|
||||
},
|
||||
[onChange],
|
||||
)
|
||||
|
||||
const renderOption = useCallback(
|
||||
(option: (typeof options)[number]) => (
|
||||
<Card as="button" padding={3}>
|
||||
<Text size={1}>{option.payload.title}</Text>
|
||||
</Card>
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
const selectedTitle = useMemo(() => {
|
||||
if (!value) return undefined
|
||||
return countryList.find((c) => c.value === value)?.title
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
id="country-input"
|
||||
options={filtered}
|
||||
onSelect={handleSelect}
|
||||
onQueryChange={(q) => setQuery(q ?? '')}
|
||||
placeholder="Search for a country…"
|
||||
renderOption={renderOption}
|
||||
value={value || ''}
|
||||
readOnly={readOnly}
|
||||
openButton
|
||||
filterOption={() => true}
|
||||
renderValue={() => selectedTitle || value || ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
125
components/TrackDisplayTitleInput.tsx
Normal file
125
components/TrackDisplayTitleInput.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import React, {useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {Button, Card, Flex, Stack, Text, TextInput} from '@sanity/ui'
|
||||
import {set, unset, StringInputProps, useClient, useFormValue} from 'sanity'
|
||||
|
||||
type WorkDoc = {
|
||||
_id: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
function computeTitle(workTitle: string | undefined, movement: string | undefined) {
|
||||
const wt = (workTitle || '').trim()
|
||||
const mv = (movement || '').trim()
|
||||
|
||||
if (wt && mv) return `${wt}: ${mv}`
|
||||
if (wt) return wt
|
||||
if (mv) return mv
|
||||
return ''
|
||||
}
|
||||
|
||||
export function TrackDisplayTitleInput(props: StringInputProps) {
|
||||
const {value, onChange, readOnly, elementProps} = props
|
||||
|
||||
const workRef = useFormValue(['work', '_ref']) as string | undefined
|
||||
const movement = useFormValue(['movement']) as string | undefined
|
||||
|
||||
const client = useClient({apiVersion: '2025-01-01'})
|
||||
const [workTitle, setWorkTitle] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
async function run() {
|
||||
if (!workRef) {
|
||||
setWorkTitle('')
|
||||
return
|
||||
}
|
||||
|
||||
const doc = await client.fetch<WorkDoc | null>(`*[_type == "work" && _id == $id][0]{title}`, {
|
||||
id: workRef,
|
||||
})
|
||||
|
||||
if (!cancelled) setWorkTitle((doc?.title || '').trim())
|
||||
}
|
||||
|
||||
run()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [client, workRef])
|
||||
|
||||
const computed = useMemo(() => computeTitle(workTitle, movement), [workTitle, movement])
|
||||
|
||||
const didAutofill = useRef(false)
|
||||
useEffect(() => {
|
||||
if (didAutofill.current) return
|
||||
if (!workRef) return
|
||||
if (value && String(value).trim()) return
|
||||
if (!computed) return
|
||||
if (readOnly) return
|
||||
|
||||
didAutofill.current = true
|
||||
onChange(set(computed))
|
||||
}, [workRef, value, computed, readOnly, onChange])
|
||||
|
||||
const hasStored = Boolean(value && String(value).trim())
|
||||
const inWorkMode = Boolean(workRef)
|
||||
|
||||
if (inWorkMode) {
|
||||
return (
|
||||
<Card padding={3} radius={2} border>
|
||||
<Stack space={3}>
|
||||
<Flex direction="column" gap={2}>
|
||||
<Text size={1} weight="semibold">
|
||||
Display title (stored)
|
||||
</Text>
|
||||
<Text size={0} muted>
|
||||
Generated once from Work/Movement and saved. It will not change automatically if the
|
||||
Work changes.
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<TextInput
|
||||
{...elementProps}
|
||||
value={(value as string) || ''}
|
||||
readOnly={true}
|
||||
placeholder={computed || 'Select a Work to generate a title'}
|
||||
/>
|
||||
|
||||
<Flex gap={2} wrap="wrap">
|
||||
<Button
|
||||
text={hasStored ? 'Overwrite from Work/Movement' : 'Generate from Work/Movement'}
|
||||
mode="default"
|
||||
disabled={readOnly || !computed}
|
||||
onClick={() => onChange(set(computed))}
|
||||
/>
|
||||
<Button
|
||||
text="Clear"
|
||||
mode="ghost"
|
||||
disabled={readOnly || !hasStored}
|
||||
onClick={() => onChange(unset())}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{!computed && (
|
||||
<Text size={0} muted>
|
||||
No computable title yet. Select a Work (and optionally add Movement).
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
{...elementProps}
|
||||
value={(value as string) || ''}
|
||||
readOnly={readOnly}
|
||||
onChange={(e) => {
|
||||
const next = e.currentTarget.value
|
||||
onChange(next && next.trim() ? set(next) : unset())
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
144
components/WorkSlugInput.tsx
Normal file
144
components/WorkSlugInput.tsx
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {Stack, Button, Inline} from '@sanity/ui'
|
||||
import {PatchEvent, set, unset} from 'sanity'
|
||||
import {SlugInput, type SlugInputProps} from 'sanity'
|
||||
import {useClient, useFormValue} from 'sanity'
|
||||
|
||||
type PersonDoc = {name?: string}
|
||||
|
||||
function slugify(input: string) {
|
||||
const folded = input
|
||||
.normalize('NFKD')
|
||||
.replace(/\p{M}+/gu, '')
|
||||
.replace(/ß/g, 'ss')
|
||||
.replace(/ø/g, 'o')
|
||||
.replace(/đ/g, 'd')
|
||||
.replace(/ł/g, 'l')
|
||||
|
||||
return folded
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
||||
.replace(/(^-|-$)+/g, '')
|
||||
.slice(0, 96)
|
||||
}
|
||||
|
||||
export function WorkSlugInput(props: SlugInputProps) {
|
||||
const client = useClient({apiVersion: '2026-01-01'})
|
||||
|
||||
const title = (useFormValue(['title']) as string) || ''
|
||||
const composerRef = (useFormValue(['composer', '_ref']) as string) || ''
|
||||
const arrangerRef = (useFormValue(['arranger', '_ref']) as string) || ''
|
||||
|
||||
const [composerName, setComposerName] = useState<string>('')
|
||||
const lastFetchedRef = useRef<string>('')
|
||||
|
||||
const [arrangerName, setArrangerName] = useState<string>('')
|
||||
const lastFetchedArrangerRef = useRef<string>('')
|
||||
|
||||
const lastAutoSlugRef = useRef<string>('')
|
||||
|
||||
const desiredSource = useMemo(() => {
|
||||
const parts = [composerName, title, arrangerName ? `arr ${arrangerName}` : null].filter(Boolean)
|
||||
return parts.join(' ')
|
||||
}, [composerName, title, arrangerName])
|
||||
|
||||
const generateSlug = useCallback(() => {
|
||||
if (!desiredSource) return
|
||||
|
||||
const nextSlug = slugify(desiredSource)
|
||||
lastAutoSlugRef.current = nextSlug
|
||||
|
||||
props.onChange(PatchEvent.from(nextSlug ? set({_type: 'slug', current: nextSlug}) : unset()))
|
||||
}, [desiredSource, props.onChange])
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
|
||||
async function run() {
|
||||
if (!composerRef) {
|
||||
setComposerName('')
|
||||
lastFetchedRef.current = ''
|
||||
return
|
||||
}
|
||||
if (lastFetchedRef.current === composerRef) return
|
||||
|
||||
lastFetchedRef.current = composerRef
|
||||
|
||||
const doc = await client.fetch<PersonDoc>(`*[_type == "composer" && _id == $id][0]{name}`, {
|
||||
id: composerRef,
|
||||
})
|
||||
|
||||
if (!alive) return
|
||||
setComposerName(doc?.name || '')
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
return () => {
|
||||
alive = false
|
||||
}
|
||||
}, [composerRef, client])
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
|
||||
async function run() {
|
||||
if (!arrangerRef) {
|
||||
setArrangerName('')
|
||||
lastFetchedArrangerRef.current = ''
|
||||
return
|
||||
}
|
||||
if (lastFetchedArrangerRef.current === arrangerRef) return
|
||||
|
||||
lastFetchedArrangerRef.current = arrangerRef
|
||||
|
||||
const doc = await client.fetch<PersonDoc>(`*[_type == "composer" && _id == $id][0]{name}`, {
|
||||
id: arrangerRef,
|
||||
})
|
||||
|
||||
if (!alive) return
|
||||
setArrangerName(doc?.name || '')
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
return () => {
|
||||
alive = false
|
||||
}
|
||||
}, [arrangerRef, client])
|
||||
|
||||
useEffect(() => {
|
||||
if (!composerName || !title) return
|
||||
|
||||
const current = props.value?.current || ''
|
||||
const nextSlug = slugify(desiredSource)
|
||||
|
||||
if (!lastAutoSlugRef.current && current && current === nextSlug) {
|
||||
lastAutoSlugRef.current = current
|
||||
return
|
||||
}
|
||||
|
||||
if (!current || current === lastAutoSlugRef.current) {
|
||||
lastAutoSlugRef.current = nextSlug
|
||||
props.onChange(PatchEvent.from(set({_type: 'slug', current: nextSlug})))
|
||||
}
|
||||
}, [composerName, arrangerName, title, desiredSource, props.value?.current, props.onChange])
|
||||
|
||||
return (
|
||||
<Stack space={3}>
|
||||
<SlugInput {...props} />
|
||||
|
||||
<Stack space={2}>
|
||||
<Inline space={2}>
|
||||
<Button
|
||||
text="Generate"
|
||||
mode="default"
|
||||
onClick={generateSlug}
|
||||
disabled={!composerName || !title}
|
||||
/>
|
||||
</Inline>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
3
eslint.config.mjs
Normal file
3
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import studio from '@sanity/eslint-config-studio'
|
||||
|
||||
export default [...studio]
|
||||
16984
package-lock.json
generated
Normal file
16984
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
39
package.json
Normal file
39
package.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "trptk-sanity",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"main": "package.json",
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"dev": "sanity dev",
|
||||
"start": "sanity start",
|
||||
"build": "sanity build",
|
||||
"deploy": "sanity deploy",
|
||||
"deploy-graphql": "sanity graphql deploy",
|
||||
"schema:extract": "sanity schema extract"
|
||||
},
|
||||
"keywords": [
|
||||
"sanity"
|
||||
],
|
||||
"dependencies": {
|
||||
"@sanity/vision": "^5.11.0",
|
||||
"react": "^19.1",
|
||||
"react-dom": "^19.1",
|
||||
"sanity": "^5.11.0",
|
||||
"sanity-plugin-media": "^4.1.1",
|
||||
"styled-components": "^6.1.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sanity/eslint-config-studio": "^6.0.0",
|
||||
"@types/react": "^19.1",
|
||||
"eslint": "^9.28",
|
||||
"prettier": "^3.5",
|
||||
"typescript": "^5.8"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"printWidth": 100,
|
||||
"bracketSpacing": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
}
|
||||
5
sanity-typegen.json
Normal file
5
sanity-typegen.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"path": "./schemaTypes/**/*.ts",
|
||||
"schema": "./schema.json",
|
||||
"generates": "./sanity.types.ts"
|
||||
}
|
||||
15
sanity.cli.ts
Normal file
15
sanity.cli.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import {defineCliConfig} from 'sanity/cli'
|
||||
|
||||
export default defineCliConfig({
|
||||
api: {
|
||||
projectId: 'e0x723bq',
|
||||
dataset: 'production',
|
||||
},
|
||||
deployment: {
|
||||
/**
|
||||
* Enable auto-updates for studios. <
|
||||
* Learn more at https://www.sanity.io/docs/cli#auto-updates
|
||||
*/
|
||||
autoUpdates: true,
|
||||
},
|
||||
})
|
||||
38
sanity.config.ts
Normal file
38
sanity.config.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import {defineConfig} from 'sanity'
|
||||
import {structureTool} from 'sanity/structure'
|
||||
import {visionTool} from '@sanity/vision'
|
||||
import {schemaTypes} from './schemaTypes'
|
||||
import {media} from 'sanity-plugin-media'
|
||||
import {CogIcon} from '@sanity/icons'
|
||||
|
||||
export default defineConfig({
|
||||
name: 'default',
|
||||
title: 'TRPTK',
|
||||
|
||||
projectId: 'e0x723bq',
|
||||
dataset: 'production',
|
||||
|
||||
plugins: [
|
||||
structureTool({
|
||||
structure: (S) =>
|
||||
S.list()
|
||||
.title('Content')
|
||||
.items([
|
||||
S.listItem()
|
||||
.title('Settings')
|
||||
.icon(CogIcon)
|
||||
.child(S.document().schemaType('settings').documentId('settings')),
|
||||
S.divider(),
|
||||
...S.documentTypeListItems().filter(
|
||||
(listItem) => !['settings'].includes(listItem.getId()!),
|
||||
),
|
||||
]),
|
||||
}),
|
||||
visionTool(),
|
||||
media(),
|
||||
],
|
||||
|
||||
schema: {
|
||||
types: schemaTypes,
|
||||
},
|
||||
})
|
||||
735
sanity.types.ts
Normal file
735
sanity.types.ts
Normal file
|
|
@ -0,0 +1,735 @@
|
|||
/**
|
||||
* ---------------------------------------------------------------------------------
|
||||
* This file has been generated by Sanity TypeGen.
|
||||
* Command: `sanity typegen generate`
|
||||
*
|
||||
* Any modifications made directly to this file will be overwritten the next time
|
||||
* the TypeScript definitions are generated. Please make changes to the Sanity
|
||||
* schema definitions and/or GROQ queries if you need to update these types.
|
||||
*
|
||||
* For more information on how to use Sanity TypeGen, visit the official documentation:
|
||||
* https://www.sanity.io/docs/sanity-typegen
|
||||
* ---------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// Source: schema.json
|
||||
export type ReleaseReference = {
|
||||
_ref: string
|
||||
_type: 'reference'
|
||||
_weak?: boolean
|
||||
[internalGroqTypeReferenceTo]?: 'release'
|
||||
}
|
||||
|
||||
export type ArtistReference = {
|
||||
_ref: string
|
||||
_type: 'reference'
|
||||
_weak?: boolean
|
||||
[internalGroqTypeReferenceTo]?: 'artist'
|
||||
}
|
||||
|
||||
export type ComposerReference = {
|
||||
_ref: string
|
||||
_type: 'reference'
|
||||
_weak?: boolean
|
||||
[internalGroqTypeReferenceTo]?: 'composer'
|
||||
}
|
||||
|
||||
export type Settings = {
|
||||
_id: string
|
||||
_type: 'settings'
|
||||
_createdAt: string
|
||||
_updatedAt: string
|
||||
_rev: string
|
||||
title?: string
|
||||
featuredAlbum?: ReleaseReference
|
||||
featuredArtist?: ArtistReference
|
||||
featuredComposer?: ComposerReference
|
||||
}
|
||||
|
||||
export type SanityImageAssetReference = {
|
||||
_ref: string
|
||||
_type: 'reference'
|
||||
_weak?: boolean
|
||||
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset'
|
||||
}
|
||||
|
||||
export type SanityFileAssetReference = {
|
||||
_ref: string
|
||||
_type: 'reference'
|
||||
_weak?: boolean
|
||||
[internalGroqTypeReferenceTo]?: 'sanity.fileAsset'
|
||||
}
|
||||
|
||||
export type WorkReference = {
|
||||
_ref: string
|
||||
_type: 'reference'
|
||||
_weak?: boolean
|
||||
[internalGroqTypeReferenceTo]?: 'work'
|
||||
}
|
||||
|
||||
export type Release = {
|
||||
_id: string
|
||||
_type: 'release'
|
||||
_createdAt: string
|
||||
_updatedAt: string
|
||||
_rev: string
|
||||
name?: string
|
||||
albumArtist?: string
|
||||
catalogNo?: string
|
||||
slug?: Slug
|
||||
upc?: string
|
||||
releaseDate?: string
|
||||
format?: 'single' | 'ep' | 'album' | 'boxset'
|
||||
label?: 'TRPTK' | 'other'
|
||||
shortDescription?: string
|
||||
description?: Array<{
|
||||
children?: Array<{
|
||||
marks?: Array<string>
|
||||
text?: string
|
||||
_type: 'span'
|
||||
_key: string
|
||||
}>
|
||||
style?: 'normal' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'blockquote'
|
||||
listItem?: 'bullet' | 'number'
|
||||
markDefs?: Array<{
|
||||
href?: string
|
||||
_type: 'link'
|
||||
_key: string
|
||||
}>
|
||||
level?: number
|
||||
_type: 'block'
|
||||
_key: string
|
||||
}>
|
||||
albumCover?: {
|
||||
asset?: SanityImageAssetReference
|
||||
media?: unknown
|
||||
hotspot?: SanityImageHotspot
|
||||
crop?: SanityImageCrop
|
||||
_type: 'image'
|
||||
}
|
||||
bookletPdf?: {
|
||||
asset?: SanityFileAssetReference
|
||||
media?: unknown
|
||||
_type: 'file'
|
||||
}
|
||||
tracks?: Array<{
|
||||
work?: WorkReference
|
||||
movement?: string
|
||||
displayTitle?: string
|
||||
artist?: string
|
||||
duration?: string
|
||||
previewMp3?: {
|
||||
asset?: SanityFileAssetReference
|
||||
media?: unknown
|
||||
_type: 'file'
|
||||
}
|
||||
_type: 'track'
|
||||
_key: string
|
||||
}>
|
||||
officialUrl?: string
|
||||
spotifyUrl?: string
|
||||
appleMusicUrl?: string
|
||||
deezerUrl?: string
|
||||
amazonMusicUrl?: string
|
||||
tidalUrl?: string
|
||||
qobuzUrl?: string
|
||||
nativeDsdUrl?: string
|
||||
credits?: Array<{
|
||||
role?: string
|
||||
name?: string
|
||||
_type: 'credit'
|
||||
_key: string
|
||||
}>
|
||||
recordingDate?: string
|
||||
recordingLocation?: string
|
||||
recordingFormat?: 'PCM 352.8 kHz 24 bit' | 'PCM 352.8 kHz 32 bit' | 'DSD 11.2 MHz 1 bit'
|
||||
masteringFormat?: 'PCM 352.8 kHz 32 bit' | 'PCM 352.8 kHz 64 bit' | 'DSD 11.2 MHz 1 bit'
|
||||
equipment?: Array<{
|
||||
type?: string
|
||||
name?: string
|
||||
_type: 'equipmentItem'
|
||||
_key: string
|
||||
}>
|
||||
genre?: Array<
|
||||
| 'earlyMusic'
|
||||
| 'baroque'
|
||||
| 'classical'
|
||||
| 'romantic'
|
||||
| 'contemporary'
|
||||
| 'worldMusic'
|
||||
| 'jazz'
|
||||
| 'crossover'
|
||||
| 'electronic'
|
||||
| 'minimal'
|
||||
| 'popRock'
|
||||
>
|
||||
instrumentation?: Array<'solo' | 'chamber' | 'ensemble' | 'orchestra' | 'vocalChoral'>
|
||||
artists?: Array<
|
||||
{
|
||||
_key: string
|
||||
} & ArtistReference
|
||||
>
|
||||
reviews?: Array<{
|
||||
quote?: string
|
||||
author?: string
|
||||
_type: 'review'
|
||||
_key: string
|
||||
}>
|
||||
availableVariants?: Array<string>
|
||||
}
|
||||
|
||||
export type SanityImageCrop = {
|
||||
_type: 'sanity.imageCrop'
|
||||
top?: number
|
||||
bottom?: number
|
||||
left?: number
|
||||
right?: number
|
||||
}
|
||||
|
||||
export type SanityImageHotspot = {
|
||||
_type: 'sanity.imageHotspot'
|
||||
x?: number
|
||||
y?: number
|
||||
height?: number
|
||||
width?: number
|
||||
}
|
||||
|
||||
export type Slug = {
|
||||
_type: 'slug'
|
||||
current?: string
|
||||
source?: string
|
||||
}
|
||||
|
||||
export type Work = {
|
||||
_id: string
|
||||
_type: 'work'
|
||||
_createdAt: string
|
||||
_updatedAt: string
|
||||
_rev: string
|
||||
title?: string
|
||||
composer?: ComposerReference
|
||||
arranger?: ComposerReference
|
||||
slug?: Slug
|
||||
description?: Array<{
|
||||
children?: Array<{
|
||||
marks?: Array<string>
|
||||
text?: string
|
||||
_type: 'span'
|
||||
_key: string
|
||||
}>
|
||||
style?: 'normal' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'blockquote'
|
||||
listItem?: 'bullet' | 'number'
|
||||
markDefs?: Array<{
|
||||
href?: string
|
||||
_type: 'link'
|
||||
_key: string
|
||||
}>
|
||||
level?: number
|
||||
_type: 'block'
|
||||
_key: string
|
||||
}>
|
||||
}
|
||||
|
||||
export type Concert = {
|
||||
_id: string
|
||||
_type: 'concert'
|
||||
_createdAt: string
|
||||
_updatedAt: string
|
||||
_rev: string
|
||||
title?: string
|
||||
subtitle?: string
|
||||
date?: string
|
||||
time?: string
|
||||
locationName?: string
|
||||
city?: string
|
||||
country?:
|
||||
| 'AF'
|
||||
| 'AL'
|
||||
| 'DZ'
|
||||
| 'AD'
|
||||
| 'AO'
|
||||
| 'AG'
|
||||
| 'AR'
|
||||
| 'AM'
|
||||
| 'AU'
|
||||
| 'AT'
|
||||
| 'AZ'
|
||||
| 'BS'
|
||||
| 'BH'
|
||||
| 'BD'
|
||||
| 'BB'
|
||||
| 'BY'
|
||||
| 'BE'
|
||||
| 'BZ'
|
||||
| 'BJ'
|
||||
| 'BT'
|
||||
| 'BO'
|
||||
| 'BA'
|
||||
| 'BW'
|
||||
| 'BR'
|
||||
| 'BN'
|
||||
| 'BG'
|
||||
| 'BF'
|
||||
| 'BI'
|
||||
| 'CV'
|
||||
| 'KH'
|
||||
| 'CM'
|
||||
| 'CA'
|
||||
| 'CF'
|
||||
| 'TD'
|
||||
| 'CL'
|
||||
| 'CN'
|
||||
| 'CO'
|
||||
| 'KM'
|
||||
| 'CD'
|
||||
| 'CG'
|
||||
| 'CR'
|
||||
| 'HR'
|
||||
| 'CU'
|
||||
| 'CY'
|
||||
| 'CZ'
|
||||
| 'DK'
|
||||
| 'DJ'
|
||||
| 'DM'
|
||||
| 'DO'
|
||||
| 'EC'
|
||||
| 'EG'
|
||||
| 'SV'
|
||||
| 'GQ'
|
||||
| 'ER'
|
||||
| 'EE'
|
||||
| 'SZ'
|
||||
| 'ET'
|
||||
| 'FJ'
|
||||
| 'FI'
|
||||
| 'FR'
|
||||
| 'GA'
|
||||
| 'GM'
|
||||
| 'GE'
|
||||
| 'DE'
|
||||
| 'GH'
|
||||
| 'GR'
|
||||
| 'GD'
|
||||
| 'GT'
|
||||
| 'GN'
|
||||
| 'GW'
|
||||
| 'GY'
|
||||
| 'HT'
|
||||
| 'HN'
|
||||
| 'HU'
|
||||
| 'IS'
|
||||
| 'IN'
|
||||
| 'ID'
|
||||
| 'IR'
|
||||
| 'IQ'
|
||||
| 'IE'
|
||||
| 'IL'
|
||||
| 'IT'
|
||||
| 'CI'
|
||||
| 'JM'
|
||||
| 'JP'
|
||||
| 'JO'
|
||||
| 'KZ'
|
||||
| 'KE'
|
||||
| 'KI'
|
||||
| 'XK'
|
||||
| 'KW'
|
||||
| 'KG'
|
||||
| 'LA'
|
||||
| 'LV'
|
||||
| 'LB'
|
||||
| 'LS'
|
||||
| 'LR'
|
||||
| 'LY'
|
||||
| 'LI'
|
||||
| 'LT'
|
||||
| 'LU'
|
||||
| 'MG'
|
||||
| 'MW'
|
||||
| 'MY'
|
||||
| 'MV'
|
||||
| 'ML'
|
||||
| 'MT'
|
||||
| 'MH'
|
||||
| 'MR'
|
||||
| 'MU'
|
||||
| 'MX'
|
||||
| 'FM'
|
||||
| 'MD'
|
||||
| 'MC'
|
||||
| 'MN'
|
||||
| 'ME'
|
||||
| 'MA'
|
||||
| 'MZ'
|
||||
| 'MM'
|
||||
| 'NA'
|
||||
| 'NR'
|
||||
| 'NP'
|
||||
| 'NL'
|
||||
| 'NZ'
|
||||
| 'NI'
|
||||
| 'NE'
|
||||
| 'NG'
|
||||
| 'KP'
|
||||
| 'MK'
|
||||
| 'NO'
|
||||
| 'OM'
|
||||
| 'PK'
|
||||
| 'PW'
|
||||
| 'PS'
|
||||
| 'PA'
|
||||
| 'PG'
|
||||
| 'PY'
|
||||
| 'PE'
|
||||
| 'PH'
|
||||
| 'PL'
|
||||
| 'PT'
|
||||
| 'QA'
|
||||
| 'RO'
|
||||
| 'RU'
|
||||
| 'RW'
|
||||
| 'KN'
|
||||
| 'LC'
|
||||
| 'VC'
|
||||
| 'WS'
|
||||
| 'SM'
|
||||
| 'ST'
|
||||
| 'SA'
|
||||
| 'SN'
|
||||
| 'RS'
|
||||
| 'SC'
|
||||
| 'SL'
|
||||
| 'SG'
|
||||
| 'SK'
|
||||
| 'SI'
|
||||
| 'SB'
|
||||
| 'SO'
|
||||
| 'ZA'
|
||||
| 'KR'
|
||||
| 'SS'
|
||||
| 'ES'
|
||||
| 'LK'
|
||||
| 'SD'
|
||||
| 'SR'
|
||||
| 'SE'
|
||||
| 'CH'
|
||||
| 'SY'
|
||||
| 'TW'
|
||||
| 'TJ'
|
||||
| 'TZ'
|
||||
| 'TH'
|
||||
| 'TL'
|
||||
| 'TG'
|
||||
| 'TO'
|
||||
| 'TT'
|
||||
| 'TN'
|
||||
| 'TR'
|
||||
| 'TM'
|
||||
| 'TV'
|
||||
| 'UG'
|
||||
| 'UA'
|
||||
| 'AE'
|
||||
| 'GB'
|
||||
| 'US'
|
||||
| 'UY'
|
||||
| 'UZ'
|
||||
| 'VU'
|
||||
| 'VA'
|
||||
| 'VE'
|
||||
| 'VN'
|
||||
| 'YE'
|
||||
| 'ZM'
|
||||
| 'ZW'
|
||||
artists?: Array<
|
||||
{
|
||||
_key: string
|
||||
} & ArtistReference
|
||||
>
|
||||
ticketUrl?: string
|
||||
}
|
||||
|
||||
export type Composer = {
|
||||
_id: string
|
||||
_type: 'composer'
|
||||
_createdAt: string
|
||||
_updatedAt: string
|
||||
_rev: string
|
||||
name?: string
|
||||
sortKey?: string
|
||||
birthYear?: number
|
||||
deathYear?: number
|
||||
slug?: Slug
|
||||
image?: {
|
||||
asset?: SanityImageAssetReference
|
||||
media?: unknown
|
||||
hotspot?: SanityImageHotspot
|
||||
crop?: SanityImageCrop
|
||||
_type: 'image'
|
||||
}
|
||||
bio?: Array<{
|
||||
children?: Array<{
|
||||
marks?: Array<string>
|
||||
text?: string
|
||||
_type: 'span'
|
||||
_key: string
|
||||
}>
|
||||
style?: 'normal' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'blockquote'
|
||||
listItem?: 'bullet' | 'number'
|
||||
markDefs?: Array<{
|
||||
href?: string
|
||||
_type: 'link'
|
||||
_key: string
|
||||
}>
|
||||
level?: number
|
||||
_type: 'block'
|
||||
_key: string
|
||||
}>
|
||||
}
|
||||
|
||||
export type Blog = {
|
||||
_id: string
|
||||
_type: 'blog'
|
||||
_createdAt: string
|
||||
_updatedAt: string
|
||||
_rev: string
|
||||
title?: string
|
||||
subtitle?: string
|
||||
featuredImage?: {
|
||||
asset?: SanityImageAssetReference
|
||||
media?: unknown
|
||||
hotspot?: SanityImageHotspot
|
||||
crop?: SanityImageCrop
|
||||
_type: 'image'
|
||||
}
|
||||
slug?: Slug
|
||||
author?: 'Brendon Heinst' | 'Maya Fridman'
|
||||
publishDate?: string
|
||||
category?: 'News' | 'Behind the Scenes' | 'Music History' | 'Tech Talk'
|
||||
content?: Array<
|
||||
| {
|
||||
children?: Array<{
|
||||
marks?: Array<string>
|
||||
text?: string
|
||||
_type: 'span'
|
||||
_key: string
|
||||
}>
|
||||
style?: 'normal' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'blockquote'
|
||||
listItem?: 'bullet' | 'number'
|
||||
markDefs?: Array<{
|
||||
href?: string
|
||||
_type: 'link'
|
||||
_key: string
|
||||
}>
|
||||
level?: number
|
||||
_type: 'block'
|
||||
_key: string
|
||||
}
|
||||
| {
|
||||
asset?: SanityImageAssetReference
|
||||
media?: unknown
|
||||
hotspot?: SanityImageHotspot
|
||||
crop?: SanityImageCrop
|
||||
alt?: string
|
||||
caption?: string
|
||||
_type: 'image'
|
||||
_key: string
|
||||
}
|
||||
| {
|
||||
url?: string
|
||||
_type: 'youtube'
|
||||
_key: string
|
||||
}
|
||||
>
|
||||
releases?: Array<
|
||||
{
|
||||
_key: string
|
||||
} & ReleaseReference
|
||||
>
|
||||
artists?: Array<
|
||||
{
|
||||
_key: string
|
||||
} & ArtistReference
|
||||
>
|
||||
composers?: Array<
|
||||
{
|
||||
_key: string
|
||||
} & ComposerReference
|
||||
>
|
||||
works?: Array<
|
||||
{
|
||||
_key: string
|
||||
} & WorkReference
|
||||
>
|
||||
}
|
||||
|
||||
export type Artist = {
|
||||
_id: string
|
||||
_type: 'artist'
|
||||
_createdAt: string
|
||||
_updatedAt: string
|
||||
_rev: string
|
||||
name?: string
|
||||
sortKey?: string
|
||||
slug?: Slug
|
||||
role?: string
|
||||
image?: {
|
||||
asset?: SanityImageAssetReference
|
||||
media?: unknown
|
||||
hotspot?: SanityImageHotspot
|
||||
crop?: SanityImageCrop
|
||||
_type: 'image'
|
||||
}
|
||||
bio?: Array<{
|
||||
children?: Array<{
|
||||
marks?: Array<string>
|
||||
text?: string
|
||||
_type: 'span'
|
||||
_key: string
|
||||
}>
|
||||
style?: 'normal' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'blockquote'
|
||||
listItem?: 'bullet' | 'number'
|
||||
markDefs?: Array<{
|
||||
href?: string
|
||||
_type: 'link'
|
||||
_key: string
|
||||
}>
|
||||
level?: number
|
||||
_type: 'block'
|
||||
_key: string
|
||||
}>
|
||||
}
|
||||
|
||||
export type MediaTag = {
|
||||
_id: string
|
||||
_type: 'media.tag'
|
||||
_createdAt: string
|
||||
_updatedAt: string
|
||||
_rev: string
|
||||
name?: Slug
|
||||
}
|
||||
|
||||
export type SanityImagePaletteSwatch = {
|
||||
_type: 'sanity.imagePaletteSwatch'
|
||||
background?: string
|
||||
foreground?: string
|
||||
population?: number
|
||||
title?: string
|
||||
}
|
||||
|
||||
export type SanityImagePalette = {
|
||||
_type: 'sanity.imagePalette'
|
||||
darkMuted?: SanityImagePaletteSwatch
|
||||
lightVibrant?: SanityImagePaletteSwatch
|
||||
darkVibrant?: SanityImagePaletteSwatch
|
||||
vibrant?: SanityImagePaletteSwatch
|
||||
dominant?: SanityImagePaletteSwatch
|
||||
lightMuted?: SanityImagePaletteSwatch
|
||||
muted?: SanityImagePaletteSwatch
|
||||
}
|
||||
|
||||
export type SanityImageDimensions = {
|
||||
_type: 'sanity.imageDimensions'
|
||||
height?: number
|
||||
width?: number
|
||||
aspectRatio?: number
|
||||
}
|
||||
|
||||
export type SanityImageMetadata = {
|
||||
_type: 'sanity.imageMetadata'
|
||||
location?: Geopoint
|
||||
dimensions?: SanityImageDimensions
|
||||
palette?: SanityImagePalette
|
||||
lqip?: string
|
||||
blurHash?: string
|
||||
thumbHash?: string
|
||||
hasAlpha?: boolean
|
||||
isOpaque?: boolean
|
||||
}
|
||||
|
||||
export type SanityFileAsset = {
|
||||
_id: string
|
||||
_type: 'sanity.fileAsset'
|
||||
_createdAt: string
|
||||
_updatedAt: string
|
||||
_rev: string
|
||||
originalFilename?: string
|
||||
label?: string
|
||||
title?: string
|
||||
description?: string
|
||||
altText?: string
|
||||
sha1hash?: string
|
||||
extension?: string
|
||||
mimeType?: string
|
||||
size?: number
|
||||
assetId?: string
|
||||
uploadId?: string
|
||||
path?: string
|
||||
url?: string
|
||||
source?: SanityAssetSourceData
|
||||
}
|
||||
|
||||
export type SanityAssetSourceData = {
|
||||
_type: 'sanity.assetSourceData'
|
||||
name?: string
|
||||
id?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
export type SanityImageAsset = {
|
||||
_id: string
|
||||
_type: 'sanity.imageAsset'
|
||||
_createdAt: string
|
||||
_updatedAt: string
|
||||
_rev: string
|
||||
originalFilename?: string
|
||||
label?: string
|
||||
title?: string
|
||||
description?: string
|
||||
altText?: string
|
||||
sha1hash?: string
|
||||
extension?: string
|
||||
mimeType?: string
|
||||
size?: number
|
||||
assetId?: string
|
||||
uploadId?: string
|
||||
path?: string
|
||||
url?: string
|
||||
metadata?: SanityImageMetadata
|
||||
source?: SanityAssetSourceData
|
||||
}
|
||||
|
||||
export type Geopoint = {
|
||||
_type: 'geopoint'
|
||||
lat?: number
|
||||
lng?: number
|
||||
alt?: number
|
||||
}
|
||||
|
||||
export type AllSanitySchemaTypes =
|
||||
| ReleaseReference
|
||||
| ArtistReference
|
||||
| ComposerReference
|
||||
| Settings
|
||||
| SanityImageAssetReference
|
||||
| SanityFileAssetReference
|
||||
| WorkReference
|
||||
| Release
|
||||
| SanityImageCrop
|
||||
| SanityImageHotspot
|
||||
| Slug
|
||||
| Work
|
||||
| Concert
|
||||
| Composer
|
||||
| Blog
|
||||
| Artist
|
||||
| MediaTag
|
||||
| SanityImagePaletteSwatch
|
||||
| SanityImagePalette
|
||||
| SanityImageDimensions
|
||||
| SanityImageMetadata
|
||||
| SanityFileAsset
|
||||
| SanityAssetSourceData
|
||||
| SanityImageAsset
|
||||
| Geopoint
|
||||
|
||||
export declare const internalGroqTypeReferenceTo: unique symbol
|
||||
4110
schema.json
Normal file
4110
schema.json
Normal file
File diff suppressed because it is too large
Load diff
68
schemaTypes/artist-type.ts
Normal file
68
schemaTypes/artist-type.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import {defineArrayMember, defineField, defineType} from 'sanity'
|
||||
import {UsersIcon} from '@sanity/icons'
|
||||
|
||||
export const artistType = defineType({
|
||||
name: 'artist',
|
||||
title: 'Artist',
|
||||
type: 'document',
|
||||
icon: UsersIcon,
|
||||
fieldsets: [{name: 'main', title: 'Main Info', options: {columns: 2}}],
|
||||
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
type: 'string',
|
||||
fieldset: 'main',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'sortKey',
|
||||
title: 'Sorting Key',
|
||||
type: 'string',
|
||||
fieldset: 'main',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'slug',
|
||||
title: 'Slug',
|
||||
type: 'slug',
|
||||
fieldset: 'main',
|
||||
options: {source: 'name', maxLength: 96},
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'role',
|
||||
title: 'Role',
|
||||
type: 'string',
|
||||
fieldset: 'main',
|
||||
}),
|
||||
defineField({
|
||||
name: 'image',
|
||||
title: 'Image',
|
||||
type: 'image',
|
||||
options: {hotspot: true},
|
||||
}),
|
||||
defineField({
|
||||
name: 'bio',
|
||||
title: 'Biography',
|
||||
type: 'array',
|
||||
of: [defineArrayMember({type: 'block'})],
|
||||
}),
|
||||
],
|
||||
orderings: [
|
||||
{
|
||||
title: 'First Name (A → Z)',
|
||||
name: 'nameAsc',
|
||||
by: [{field: 'name', direction: 'asc'}],
|
||||
},
|
||||
{
|
||||
title: 'Last Name (A → Z)',
|
||||
name: 'sortKeyAsc',
|
||||
by: [{field: 'sortKey', direction: 'asc'}],
|
||||
},
|
||||
],
|
||||
preview: {
|
||||
select: {title: 'name', subtitle: 'role', media: 'image'},
|
||||
},
|
||||
})
|
||||
11
schemaTypes/blog-options.ts
Normal file
11
schemaTypes/blog-options.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export const blogAuthors = [
|
||||
{title: 'Brendon Heinst', value: 'Brendon Heinst'},
|
||||
{title: 'Maya Fridman', value: 'Maya Fridman'},
|
||||
]
|
||||
|
||||
export const blogCategories = [
|
||||
{title: 'News', value: 'News'},
|
||||
{title: 'Behind the Scenes', value: 'Behind the Scenes'},
|
||||
{title: 'Music History', value: 'Music History'},
|
||||
{title: 'Tech Talk', value: 'Tech Talk'},
|
||||
]
|
||||
203
schemaTypes/blog-type.ts
Normal file
203
schemaTypes/blog-type.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import {defineArrayMember, defineField, defineType} from 'sanity'
|
||||
import {DocumentTextIcon} from '@sanity/icons'
|
||||
import {blogAuthors, blogCategories} from './blog-options'
|
||||
|
||||
export const blogType = defineType({
|
||||
name: 'blog',
|
||||
title: 'Blog',
|
||||
type: 'document',
|
||||
icon: DocumentTextIcon,
|
||||
|
||||
groups: [
|
||||
{name: 'main', title: 'Main', default: true},
|
||||
{name: 'content', title: 'Content'},
|
||||
{name: 'references', title: 'Related'},
|
||||
],
|
||||
|
||||
fieldsets: [{name: 'details', title: 'Details', options: {columns: 2}}],
|
||||
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'title',
|
||||
title: 'Title',
|
||||
type: 'string',
|
||||
group: 'main',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'subtitle',
|
||||
title: 'Subtitle',
|
||||
type: 'string',
|
||||
group: 'main',
|
||||
}),
|
||||
defineField({
|
||||
name: 'featuredImage',
|
||||
title: 'Featured Image',
|
||||
type: 'image',
|
||||
group: 'main',
|
||||
options: {hotspot: true},
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'slug',
|
||||
title: 'Slug',
|
||||
type: 'slug',
|
||||
group: 'main',
|
||||
fieldset: 'details',
|
||||
options: {
|
||||
source: 'title',
|
||||
maxLength: 200,
|
||||
},
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'author',
|
||||
title: 'Author',
|
||||
type: 'string',
|
||||
group: 'main',
|
||||
fieldset: 'details',
|
||||
options: {
|
||||
list: blogAuthors,
|
||||
},
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'publishDate',
|
||||
title: 'Publish Date',
|
||||
type: 'date',
|
||||
group: 'main',
|
||||
fieldset: 'details',
|
||||
description: 'Defaults to today. Override to backdate or schedule a post.',
|
||||
initialValue: () => new Date().toISOString().split('T')[0],
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'category',
|
||||
title: 'Category',
|
||||
type: 'string',
|
||||
group: 'main',
|
||||
fieldset: 'details',
|
||||
options: {
|
||||
list: blogCategories,
|
||||
},
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'content',
|
||||
title: 'Content',
|
||||
type: 'array',
|
||||
group: 'content',
|
||||
of: [
|
||||
defineArrayMember({type: 'block'}),
|
||||
defineArrayMember({
|
||||
type: 'image',
|
||||
options: {hotspot: true},
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'alt',
|
||||
title: 'Alt text',
|
||||
type: 'string',
|
||||
description: 'Describe the image for accessibility.',
|
||||
}),
|
||||
defineField({
|
||||
name: 'caption',
|
||||
title: 'Caption',
|
||||
type: 'string',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
defineArrayMember({
|
||||
type: 'object',
|
||||
name: 'youtube',
|
||||
title: 'YouTube Video',
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'url',
|
||||
title: 'YouTube URL',
|
||||
type: 'url',
|
||||
validation: (Rule) =>
|
||||
Rule.required()
|
||||
.uri({scheme: ['https']})
|
||||
.custom((url) => {
|
||||
if (typeof url !== 'string') return true
|
||||
if (
|
||||
url.includes('youtube.com/watch') ||
|
||||
url.includes('youtu.be/') ||
|
||||
url.includes('youtube.com/embed/')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return 'Must be a valid YouTube URL'
|
||||
}),
|
||||
}),
|
||||
],
|
||||
preview: {
|
||||
select: {url: 'url'},
|
||||
prepare({url}) {
|
||||
return {title: 'YouTube Video', subtitle: url}
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'releases',
|
||||
title: 'Related Release(s)',
|
||||
type: 'array',
|
||||
group: 'references',
|
||||
of: [defineArrayMember({type: 'reference', to: [{type: 'release'}]})],
|
||||
}),
|
||||
defineField({
|
||||
name: 'artists',
|
||||
title: 'Related Artist(s)',
|
||||
type: 'array',
|
||||
group: 'references',
|
||||
of: [defineArrayMember({type: 'reference', to: [{type: 'artist'}]})],
|
||||
}),
|
||||
defineField({
|
||||
name: 'composers',
|
||||
title: 'Related Composer(s)',
|
||||
type: 'array',
|
||||
group: 'references',
|
||||
of: [defineArrayMember({type: 'reference', to: [{type: 'composer'}]})],
|
||||
}),
|
||||
defineField({
|
||||
name: 'works',
|
||||
title: 'Related Work(s)',
|
||||
type: 'array',
|
||||
group: 'references',
|
||||
of: [defineArrayMember({type: 'reference', to: [{type: 'work'}]})],
|
||||
}),
|
||||
],
|
||||
|
||||
orderings: [
|
||||
{
|
||||
title: 'Publish Date (latest first)',
|
||||
name: 'publishDateDesc',
|
||||
by: [{field: 'publishDate', direction: 'desc'}],
|
||||
},
|
||||
{
|
||||
title: 'Publish Date (oldest first)',
|
||||
name: 'publishDateAsc',
|
||||
by: [{field: 'publishDate', direction: 'asc'}],
|
||||
},
|
||||
],
|
||||
|
||||
preview: {
|
||||
select: {
|
||||
title: 'title',
|
||||
author: 'author',
|
||||
media: 'featuredImage',
|
||||
category: 'category',
|
||||
},
|
||||
prepare({title, author, media, category}) {
|
||||
return {
|
||||
title: title || '(Untitled post)',
|
||||
subtitle: [author, category].filter(Boolean).join(' · '),
|
||||
media,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
94
schemaTypes/composer-type.ts
Normal file
94
schemaTypes/composer-type.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import {defineArrayMember, defineField, defineType} from 'sanity'
|
||||
import {ComposeIcon} from '@sanity/icons'
|
||||
|
||||
export const composerType = defineType({
|
||||
name: 'composer',
|
||||
title: 'Composer',
|
||||
type: 'document',
|
||||
icon: ComposeIcon,
|
||||
|
||||
fieldsets: [{name: 'main', title: 'Main Info', options: {columns: 2}}],
|
||||
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
type: 'string',
|
||||
fieldset: 'main',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'sortKey',
|
||||
title: 'Sorting Key',
|
||||
type: 'string',
|
||||
fieldset: 'main',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'birthYear',
|
||||
title: 'Year of Birth',
|
||||
type: 'number',
|
||||
fieldset: 'main',
|
||||
}),
|
||||
defineField({
|
||||
name: 'deathYear',
|
||||
title: 'Year of Death',
|
||||
type: 'number',
|
||||
fieldset: 'main',
|
||||
}),
|
||||
defineField({
|
||||
name: 'slug',
|
||||
title: 'Slug',
|
||||
type: 'slug',
|
||||
fieldset: 'main',
|
||||
options: {source: 'name', maxLength: 96},
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'image',
|
||||
title: 'Image',
|
||||
type: 'image',
|
||||
options: {hotspot: true},
|
||||
}),
|
||||
defineField({
|
||||
name: 'bio',
|
||||
title: 'Biography',
|
||||
type: 'array',
|
||||
of: [defineArrayMember({type: 'block'})],
|
||||
}),
|
||||
],
|
||||
orderings: [
|
||||
{
|
||||
title: 'First Name (A → Z)',
|
||||
name: 'nameAsc',
|
||||
by: [{field: 'name', direction: 'asc'}],
|
||||
},
|
||||
{
|
||||
title: 'Last Name (A → Z)',
|
||||
name: 'sortKeyAsc',
|
||||
by: [{field: 'sortKey', direction: 'asc'}],
|
||||
},
|
||||
{
|
||||
title: 'Year of Birth',
|
||||
name: 'yobAsc',
|
||||
by: [{field: 'birthYear', direction: 'asc'}],
|
||||
},
|
||||
],
|
||||
preview: {
|
||||
select: {
|
||||
title: 'name',
|
||||
birthYear: 'birthYear',
|
||||
deathYear: 'deathYear',
|
||||
media: 'image',
|
||||
},
|
||||
prepare({title, birthYear, deathYear, media}) {
|
||||
const subtitle = birthYear
|
||||
? deathYear
|
||||
? `${birthYear}–${deathYear}`
|
||||
: birthYear.toString()
|
||||
: ''
|
||||
|
||||
return {title, subtitle, media}
|
||||
},
|
||||
},
|
||||
})
|
||||
126
schemaTypes/concert-type.ts
Normal file
126
schemaTypes/concert-type.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import {defineArrayMember, defineField, defineType} from 'sanity'
|
||||
import {CalendarIcon} from '@sanity/icons'
|
||||
import {countryList} from './country-list'
|
||||
import {CountryInput} from '../components/CountryInput'
|
||||
|
||||
export const concertType = defineType({
|
||||
name: 'concert',
|
||||
title: 'Concert',
|
||||
type: 'document',
|
||||
icon: CalendarIcon,
|
||||
|
||||
fieldsets: [{name: 'details', title: 'Details', options: {columns: 2}}],
|
||||
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'title',
|
||||
title: 'Title',
|
||||
type: 'string',
|
||||
}),
|
||||
defineField({
|
||||
name: 'subtitle',
|
||||
title: 'Subtitle',
|
||||
type: 'string',
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'date',
|
||||
title: 'Date',
|
||||
type: 'date',
|
||||
fieldset: 'details',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'time',
|
||||
title: 'Time',
|
||||
type: 'string',
|
||||
fieldset: 'details',
|
||||
description: '24-hour format, e.g. 20:00',
|
||||
validation: (Rule) =>
|
||||
Rule.required().custom((value) => {
|
||||
if (typeof value !== 'string') return 'Time is required'
|
||||
return /^([01]\d|2[0-3]):[0-5]\d$/.test(value.trim())
|
||||
? true
|
||||
: 'Use HH:mm in 24-hour format (e.g. 14:30, 20:00)'
|
||||
}),
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'locationName',
|
||||
title: 'Location',
|
||||
type: 'string',
|
||||
fieldset: 'details',
|
||||
description: 'e.g. "Concertgebouw"',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'city',
|
||||
title: 'City',
|
||||
type: 'string',
|
||||
fieldset: 'details',
|
||||
description: 'e.g. "Amsterdam"',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'country',
|
||||
title: 'Country',
|
||||
type: 'string',
|
||||
fieldset: 'details',
|
||||
components: {input: CountryInput},
|
||||
options: {
|
||||
list: countryList,
|
||||
},
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'artists',
|
||||
title: 'Related Artist(s)',
|
||||
type: 'array',
|
||||
of: [defineArrayMember({type: 'reference', to: [{type: 'artist'}]})],
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'ticketUrl',
|
||||
title: 'Ticket URL',
|
||||
type: 'url',
|
||||
}),
|
||||
],
|
||||
|
||||
orderings: [
|
||||
{
|
||||
title: 'Date (Asc.)',
|
||||
name: 'dateAsc',
|
||||
by: [
|
||||
{field: 'date', direction: 'asc'},
|
||||
{field: 'time', direction: 'asc'},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Date (Desc.)',
|
||||
name: 'dateDesc',
|
||||
by: [
|
||||
{field: 'date', direction: 'desc'},
|
||||
{field: 'time', direction: 'desc'},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
preview: {
|
||||
select: {
|
||||
title: 'title',
|
||||
date: 'date',
|
||||
time: 'time',
|
||||
locationName: 'locationName',
|
||||
city: 'city',
|
||||
},
|
||||
prepare({title, date, time, locationName, city}) {
|
||||
const parts = [locationName, city].filter(Boolean).join(', ')
|
||||
const when = [date, time].filter(Boolean).join(' · ')
|
||||
return {
|
||||
title: title || parts || '(Untitled concert)',
|
||||
subtitle: [when, title ? parts : null].filter(Boolean).join(' • '),
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
199
schemaTypes/country-list.ts
Normal file
199
schemaTypes/country-list.ts
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
export const countryList = [
|
||||
{title: 'Afghanistan', value: 'AF'},
|
||||
{title: 'Albania', value: 'AL'},
|
||||
{title: 'Algeria', value: 'DZ'},
|
||||
{title: 'Andorra', value: 'AD'},
|
||||
{title: 'Angola', value: 'AO'},
|
||||
{title: 'Antigua and Barbuda', value: 'AG'},
|
||||
{title: 'Argentina', value: 'AR'},
|
||||
{title: 'Armenia', value: 'AM'},
|
||||
{title: 'Australia', value: 'AU'},
|
||||
{title: 'Austria', value: 'AT'},
|
||||
{title: 'Azerbaijan', value: 'AZ'},
|
||||
{title: 'Bahamas', value: 'BS'},
|
||||
{title: 'Bahrain', value: 'BH'},
|
||||
{title: 'Bangladesh', value: 'BD'},
|
||||
{title: 'Barbados', value: 'BB'},
|
||||
{title: 'Belarus', value: 'BY'},
|
||||
{title: 'Belgium', value: 'BE'},
|
||||
{title: 'Belize', value: 'BZ'},
|
||||
{title: 'Benin', value: 'BJ'},
|
||||
{title: 'Bhutan', value: 'BT'},
|
||||
{title: 'Bolivia', value: 'BO'},
|
||||
{title: 'Bosnia and Herzegovina', value: 'BA'},
|
||||
{title: 'Botswana', value: 'BW'},
|
||||
{title: 'Brazil', value: 'BR'},
|
||||
{title: 'Brunei', value: 'BN'},
|
||||
{title: 'Bulgaria', value: 'BG'},
|
||||
{title: 'Burkina Faso', value: 'BF'},
|
||||
{title: 'Burundi', value: 'BI'},
|
||||
{title: 'Cabo Verde', value: 'CV'},
|
||||
{title: 'Cambodia', value: 'KH'},
|
||||
{title: 'Cameroon', value: 'CM'},
|
||||
{title: 'Canada', value: 'CA'},
|
||||
{title: 'Central African Republic', value: 'CF'},
|
||||
{title: 'Chad', value: 'TD'},
|
||||
{title: 'Chile', value: 'CL'},
|
||||
{title: 'China', value: 'CN'},
|
||||
{title: 'Colombia', value: 'CO'},
|
||||
{title: 'Comoros', value: 'KM'},
|
||||
{title: 'Congo (Democratic Republic)', value: 'CD'},
|
||||
{title: 'Congo (Republic)', value: 'CG'},
|
||||
{title: 'Costa Rica', value: 'CR'},
|
||||
{title: 'Croatia', value: 'HR'},
|
||||
{title: 'Cuba', value: 'CU'},
|
||||
{title: 'Cyprus', value: 'CY'},
|
||||
{title: 'Czech Republic', value: 'CZ'},
|
||||
{title: 'Denmark', value: 'DK'},
|
||||
{title: 'Djibouti', value: 'DJ'},
|
||||
{title: 'Dominica', value: 'DM'},
|
||||
{title: 'Dominican Republic', value: 'DO'},
|
||||
{title: 'Ecuador', value: 'EC'},
|
||||
{title: 'Egypt', value: 'EG'},
|
||||
{title: 'El Salvador', value: 'SV'},
|
||||
{title: 'Equatorial Guinea', value: 'GQ'},
|
||||
{title: 'Eritrea', value: 'ER'},
|
||||
{title: 'Estonia', value: 'EE'},
|
||||
{title: 'Eswatini', value: 'SZ'},
|
||||
{title: 'Ethiopia', value: 'ET'},
|
||||
{title: 'Fiji', value: 'FJ'},
|
||||
{title: 'Finland', value: 'FI'},
|
||||
{title: 'France', value: 'FR'},
|
||||
{title: 'Gabon', value: 'GA'},
|
||||
{title: 'Gambia', value: 'GM'},
|
||||
{title: 'Georgia', value: 'GE'},
|
||||
{title: 'Germany', value: 'DE'},
|
||||
{title: 'Ghana', value: 'GH'},
|
||||
{title: 'Greece', value: 'GR'},
|
||||
{title: 'Grenada', value: 'GD'},
|
||||
{title: 'Guatemala', value: 'GT'},
|
||||
{title: 'Guinea', value: 'GN'},
|
||||
{title: 'Guinea-Bissau', value: 'GW'},
|
||||
{title: 'Guyana', value: 'GY'},
|
||||
{title: 'Haiti', value: 'HT'},
|
||||
{title: 'Honduras', value: 'HN'},
|
||||
{title: 'Hungary', value: 'HU'},
|
||||
{title: 'Iceland', value: 'IS'},
|
||||
{title: 'India', value: 'IN'},
|
||||
{title: 'Indonesia', value: 'ID'},
|
||||
{title: 'Iran', value: 'IR'},
|
||||
{title: 'Iraq', value: 'IQ'},
|
||||
{title: 'Ireland', value: 'IE'},
|
||||
{title: 'Israel', value: 'IL'},
|
||||
{title: 'Italy', value: 'IT'},
|
||||
{title: 'Ivory Coast', value: 'CI'},
|
||||
{title: 'Jamaica', value: 'JM'},
|
||||
{title: 'Japan', value: 'JP'},
|
||||
{title: 'Jordan', value: 'JO'},
|
||||
{title: 'Kazakhstan', value: 'KZ'},
|
||||
{title: 'Kenya', value: 'KE'},
|
||||
{title: 'Kiribati', value: 'KI'},
|
||||
{title: 'Kosovo', value: 'XK'},
|
||||
{title: 'Kuwait', value: 'KW'},
|
||||
{title: 'Kyrgyzstan', value: 'KG'},
|
||||
{title: 'Laos', value: 'LA'},
|
||||
{title: 'Latvia', value: 'LV'},
|
||||
{title: 'Lebanon', value: 'LB'},
|
||||
{title: 'Lesotho', value: 'LS'},
|
||||
{title: 'Liberia', value: 'LR'},
|
||||
{title: 'Libya', value: 'LY'},
|
||||
{title: 'Liechtenstein', value: 'LI'},
|
||||
{title: 'Lithuania', value: 'LT'},
|
||||
{title: 'Luxembourg', value: 'LU'},
|
||||
{title: 'Madagascar', value: 'MG'},
|
||||
{title: 'Malawi', value: 'MW'},
|
||||
{title: 'Malaysia', value: 'MY'},
|
||||
{title: 'Maldives', value: 'MV'},
|
||||
{title: 'Mali', value: 'ML'},
|
||||
{title: 'Malta', value: 'MT'},
|
||||
{title: 'Marshall Islands', value: 'MH'},
|
||||
{title: 'Mauritania', value: 'MR'},
|
||||
{title: 'Mauritius', value: 'MU'},
|
||||
{title: 'Mexico', value: 'MX'},
|
||||
{title: 'Micronesia', value: 'FM'},
|
||||
{title: 'Moldova', value: 'MD'},
|
||||
{title: 'Monaco', value: 'MC'},
|
||||
{title: 'Mongolia', value: 'MN'},
|
||||
{title: 'Montenegro', value: 'ME'},
|
||||
{title: 'Morocco', value: 'MA'},
|
||||
{title: 'Mozambique', value: 'MZ'},
|
||||
{title: 'Myanmar', value: 'MM'},
|
||||
{title: 'Namibia', value: 'NA'},
|
||||
{title: 'Nauru', value: 'NR'},
|
||||
{title: 'Nepal', value: 'NP'},
|
||||
{title: 'Netherlands', value: 'NL'},
|
||||
{title: 'New Zealand', value: 'NZ'},
|
||||
{title: 'Nicaragua', value: 'NI'},
|
||||
{title: 'Niger', value: 'NE'},
|
||||
{title: 'Nigeria', value: 'NG'},
|
||||
{title: 'North Korea', value: 'KP'},
|
||||
{title: 'North Macedonia', value: 'MK'},
|
||||
{title: 'Norway', value: 'NO'},
|
||||
{title: 'Oman', value: 'OM'},
|
||||
{title: 'Pakistan', value: 'PK'},
|
||||
{title: 'Palau', value: 'PW'},
|
||||
{title: 'Palestine', value: 'PS'},
|
||||
{title: 'Panama', value: 'PA'},
|
||||
{title: 'Papua New Guinea', value: 'PG'},
|
||||
{title: 'Paraguay', value: 'PY'},
|
||||
{title: 'Peru', value: 'PE'},
|
||||
{title: 'Philippines', value: 'PH'},
|
||||
{title: 'Poland', value: 'PL'},
|
||||
{title: 'Portugal', value: 'PT'},
|
||||
{title: 'Qatar', value: 'QA'},
|
||||
{title: 'Romania', value: 'RO'},
|
||||
{title: 'Russia', value: 'RU'},
|
||||
{title: 'Rwanda', value: 'RW'},
|
||||
{title: 'Saint Kitts and Nevis', value: 'KN'},
|
||||
{title: 'Saint Lucia', value: 'LC'},
|
||||
{title: 'Saint Vincent and the Grenadines', value: 'VC'},
|
||||
{title: 'Samoa', value: 'WS'},
|
||||
{title: 'San Marino', value: 'SM'},
|
||||
{title: 'São Tomé and Príncipe', value: 'ST'},
|
||||
{title: 'Saudi Arabia', value: 'SA'},
|
||||
{title: 'Senegal', value: 'SN'},
|
||||
{title: 'Serbia', value: 'RS'},
|
||||
{title: 'Seychelles', value: 'SC'},
|
||||
{title: 'Sierra Leone', value: 'SL'},
|
||||
{title: 'Singapore', value: 'SG'},
|
||||
{title: 'Slovakia', value: 'SK'},
|
||||
{title: 'Slovenia', value: 'SI'},
|
||||
{title: 'Solomon Islands', value: 'SB'},
|
||||
{title: 'Somalia', value: 'SO'},
|
||||
{title: 'South Africa', value: 'ZA'},
|
||||
{title: 'South Korea', value: 'KR'},
|
||||
{title: 'South Sudan', value: 'SS'},
|
||||
{title: 'Spain', value: 'ES'},
|
||||
{title: 'Sri Lanka', value: 'LK'},
|
||||
{title: 'Sudan', value: 'SD'},
|
||||
{title: 'Suriname', value: 'SR'},
|
||||
{title: 'Sweden', value: 'SE'},
|
||||
{title: 'Switzerland', value: 'CH'},
|
||||
{title: 'Syria', value: 'SY'},
|
||||
{title: 'Taiwan', value: 'TW'},
|
||||
{title: 'Tajikistan', value: 'TJ'},
|
||||
{title: 'Tanzania', value: 'TZ'},
|
||||
{title: 'Thailand', value: 'TH'},
|
||||
{title: 'Timor-Leste', value: 'TL'},
|
||||
{title: 'Togo', value: 'TG'},
|
||||
{title: 'Tonga', value: 'TO'},
|
||||
{title: 'Trinidad and Tobago', value: 'TT'},
|
||||
{title: 'Tunisia', value: 'TN'},
|
||||
{title: 'Turkey', value: 'TR'},
|
||||
{title: 'Turkmenistan', value: 'TM'},
|
||||
{title: 'Tuvalu', value: 'TV'},
|
||||
{title: 'Uganda', value: 'UG'},
|
||||
{title: 'Ukraine', value: 'UA'},
|
||||
{title: 'United Arab Emirates', value: 'AE'},
|
||||
{title: 'United Kingdom', value: 'GB'},
|
||||
{title: 'United States', value: 'US'},
|
||||
{title: 'Uruguay', value: 'UY'},
|
||||
{title: 'Uzbekistan', value: 'UZ'},
|
||||
{title: 'Vanuatu', value: 'VU'},
|
||||
{title: 'Vatican City', value: 'VA'},
|
||||
{title: 'Venezuela', value: 'VE'},
|
||||
{title: 'Vietnam', value: 'VN'},
|
||||
{title: 'Yemen', value: 'YE'},
|
||||
{title: 'Zambia', value: 'ZM'},
|
||||
{title: 'Zimbabwe', value: 'ZW'},
|
||||
]
|
||||
8
schemaTypes/index.ts
Normal file
8
schemaTypes/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import {artistType} from './artist-type'
|
||||
import {blogType} from './blog-type'
|
||||
import {composerType} from './composer-type'
|
||||
import {concertType} from './concert-type'
|
||||
import {workType} from './work-type'
|
||||
import {releaseType} from './release-type'
|
||||
import {settingsType} from './settings-type'
|
||||
export const schemaTypes = [artistType, blogType, composerType, concertType, workType, releaseType, settingsType]
|
||||
563
schemaTypes/release-type.ts
Normal file
563
schemaTypes/release-type.ts
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
import {defineArrayMember, defineField, defineType} from 'sanity'
|
||||
import {PlayIcon} from '@sanity/icons'
|
||||
import {TrackDisplayTitleInput} from '../components/TrackDisplayTitleInput'
|
||||
|
||||
export const releaseType = defineType({
|
||||
name: 'release',
|
||||
title: 'Release',
|
||||
type: 'document',
|
||||
icon: PlayIcon,
|
||||
|
||||
groups: [
|
||||
{name: 'main', title: 'Main', default: true},
|
||||
{name: 'text', title: 'Text'},
|
||||
{name: 'media', title: 'Media'},
|
||||
{name: 'tracklist', title: 'Tracklist'},
|
||||
{name: 'links', title: 'Streaming Links'},
|
||||
{name: 'references', title: 'Tags'},
|
||||
{name: 'reviews', title: 'Reviews'},
|
||||
{name: 'creditsSpecs', title: 'Credits & Specs'},
|
||||
{name: 'medusa', title: 'Medusa'},
|
||||
],
|
||||
|
||||
fieldsets: [
|
||||
{name: 'main', title: 'Release Information', options: {columns: 2}},
|
||||
{name: 'links', title: 'Streaming Links', options: {columns: 2}},
|
||||
],
|
||||
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'name',
|
||||
title: 'Title',
|
||||
type: 'string',
|
||||
group: 'main',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'albumArtist',
|
||||
title: 'Album Artist',
|
||||
type: 'string',
|
||||
group: 'main',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'catalogNo',
|
||||
title: 'Catalog #',
|
||||
type: 'string',
|
||||
group: 'main',
|
||||
fieldset: 'main',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'slug',
|
||||
title: 'Slug',
|
||||
type: 'slug',
|
||||
options: {
|
||||
source: (doc: Record<string, unknown>) =>
|
||||
[doc.albumArtist, doc.name].filter(Boolean).join(' '),
|
||||
maxLength: 200,
|
||||
},
|
||||
validation: (Rule) => Rule.required(),
|
||||
group: 'main',
|
||||
fieldset: 'main',
|
||||
}),
|
||||
defineField({name: 'upc', title: 'UPC/EAN', type: 'string', group: 'main', fieldset: 'main'}),
|
||||
defineField({
|
||||
name: 'releaseDate',
|
||||
title: 'Release Date',
|
||||
type: 'date',
|
||||
group: 'main',
|
||||
fieldset: 'main',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'format',
|
||||
title: 'Format',
|
||||
type: 'string',
|
||||
group: 'main',
|
||||
fieldset: 'main',
|
||||
options: {
|
||||
list: [
|
||||
{title: 'Single', value: 'single'},
|
||||
{title: 'EP', value: 'ep'},
|
||||
{title: 'Album', value: 'album'},
|
||||
{title: 'Boxset', value: 'boxset'},
|
||||
],
|
||||
},
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'label',
|
||||
title: 'Label',
|
||||
type: 'string',
|
||||
group: 'main',
|
||||
fieldset: 'main',
|
||||
options: {
|
||||
list: [
|
||||
{title: 'TRPTK', value: 'TRPTK'},
|
||||
{title: 'Other', value: 'other'},
|
||||
],
|
||||
},
|
||||
}),
|
||||
defineField({
|
||||
name: 'shortDescription',
|
||||
title: 'Short Description',
|
||||
type: 'text',
|
||||
rows: 4,
|
||||
group: 'text',
|
||||
}),
|
||||
defineField({
|
||||
name: 'description',
|
||||
title: 'Description',
|
||||
type: 'array',
|
||||
of: [defineArrayMember({type: 'block'})],
|
||||
group: 'text',
|
||||
}),
|
||||
defineField({
|
||||
name: 'albumCover',
|
||||
title: 'Album Cover',
|
||||
type: 'image',
|
||||
group: 'media',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'bookletPdf',
|
||||
title: 'Booklet PDF',
|
||||
type: 'file',
|
||||
options: {accept: 'application/pdf'},
|
||||
group: 'media',
|
||||
}),
|
||||
defineField({
|
||||
name: 'tracks',
|
||||
title: 'Tracklist',
|
||||
group: 'tracklist',
|
||||
type: 'array',
|
||||
of: [
|
||||
defineArrayMember({
|
||||
name: 'track',
|
||||
title: 'Track',
|
||||
type: 'object',
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'work',
|
||||
title: 'Work',
|
||||
type: 'reference',
|
||||
to: [{type: 'work'}],
|
||||
options: {disableNew: true},
|
||||
}),
|
||||
defineField({
|
||||
name: 'movement',
|
||||
title: 'Movement',
|
||||
type: 'string',
|
||||
}),
|
||||
defineField({
|
||||
name: 'displayTitle',
|
||||
title: 'Display title',
|
||||
type: 'string',
|
||||
description: 'Use only when Work and Movement are empty.',
|
||||
components: {
|
||||
input: TrackDisplayTitleInput,
|
||||
},
|
||||
}),
|
||||
defineField({
|
||||
name: 'artist',
|
||||
title: 'Artist',
|
||||
description: 'If left empty, Album Artist will be used.',
|
||||
type: 'string',
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'duration',
|
||||
title: 'Duration',
|
||||
description: 'm:ss, mm:ss, h:mm:ss, or hh:mm:ss',
|
||||
type: 'string',
|
||||
validation: (Rule) =>
|
||||
Rule.required().custom((value) => {
|
||||
if (typeof value !== 'string') return 'Duration is required'
|
||||
|
||||
const trimmed = value.trim()
|
||||
|
||||
const regex = /^(?:\d{1,2}:[0-5]\d:[0-5]\d|[0-5]?\d:[0-5]\d)$/
|
||||
|
||||
return regex.test(trimmed)
|
||||
? true
|
||||
: 'Use m:ss, mm:ss, h:mm:ss, or hh:mm:ss (e.g. 3:07, 03:07, 1:03:07, 12:03:07)'
|
||||
}),
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'previewMp3',
|
||||
title: '30s preview (MP3)',
|
||||
type: 'file',
|
||||
options: {accept: 'audio/mpeg'},
|
||||
}),
|
||||
],
|
||||
|
||||
preview: {
|
||||
select: {
|
||||
storedTitle: 'displayTitle',
|
||||
workTitle: 'work.title',
|
||||
movement: 'movement',
|
||||
mp3: 'previewMp3',
|
||||
composerName: 'work.composer.name',
|
||||
arrangerName: 'work.arranger.name',
|
||||
artist: 'artist',
|
||||
albumArtist: 'albumArtist',
|
||||
},
|
||||
prepare({
|
||||
storedTitle,
|
||||
workTitle,
|
||||
movement,
|
||||
mp3,
|
||||
composerName,
|
||||
arrangerName,
|
||||
artist,
|
||||
albumArtist,
|
||||
}) {
|
||||
const wt = (workTitle || '').trim()
|
||||
const mv = (movement || '').trim()
|
||||
|
||||
const computed = wt && mv ? `${wt}: ${mv}` : wt || mv
|
||||
const title = (storedTitle && storedTitle.trim()) || computed || '(Untitled track)'
|
||||
|
||||
const subtitleParts = [
|
||||
mp3 ? '♫' : null,
|
||||
composerName || null,
|
||||
arrangerName ? `(arr. ${arrangerName})` : null,
|
||||
].filter(Boolean)
|
||||
|
||||
return {title, subtitle: subtitleParts.join(' ')}
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
defineField({
|
||||
name: 'officialUrl',
|
||||
title: 'Official Link',
|
||||
type: 'url',
|
||||
group: 'links',
|
||||
fieldset: 'links',
|
||||
}),
|
||||
defineField({
|
||||
name: 'spotifyUrl',
|
||||
title: 'Spotify',
|
||||
type: 'url',
|
||||
group: 'links',
|
||||
fieldset: 'links',
|
||||
}),
|
||||
defineField({
|
||||
name: 'appleMusicUrl',
|
||||
title: 'Apple Music',
|
||||
type: 'url',
|
||||
group: 'links',
|
||||
fieldset: 'links',
|
||||
}),
|
||||
defineField({
|
||||
name: 'deezerUrl',
|
||||
title: 'Deezer',
|
||||
type: 'url',
|
||||
group: 'links',
|
||||
fieldset: 'links',
|
||||
}),
|
||||
defineField({
|
||||
name: 'amazonMusicUrl',
|
||||
title: 'Amazon Music',
|
||||
type: 'url',
|
||||
group: 'links',
|
||||
fieldset: 'links',
|
||||
}),
|
||||
defineField({name: 'tidalUrl', title: 'Tidal', type: 'url', group: 'links', fieldset: 'links'}),
|
||||
defineField({name: 'qobuzUrl', title: 'Qobuz', type: 'url', group: 'links', fieldset: 'links'}),
|
||||
defineField({
|
||||
name: 'nativeDsdUrl',
|
||||
title: 'NativeDSD',
|
||||
type: 'url',
|
||||
group: 'links',
|
||||
fieldset: 'links',
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'credits',
|
||||
title: 'Credits',
|
||||
group: 'creditsSpecs',
|
||||
type: 'array',
|
||||
of: [
|
||||
defineArrayMember({
|
||||
name: 'credit',
|
||||
title: 'Credit',
|
||||
type: 'object',
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'role',
|
||||
title: 'Role',
|
||||
type: 'string',
|
||||
description: 'e.g. “Recording engineer”',
|
||||
}),
|
||||
defineField({
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
type: 'text',
|
||||
rows: 2,
|
||||
description: 'You may use “ | ” to indicate multiple names.',
|
||||
}),
|
||||
],
|
||||
preview: {
|
||||
select: {role: 'role', name: 'name'},
|
||||
prepare({role, name}) {
|
||||
return {title: name || '(No role)', subtitle: role}
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'recordingDate',
|
||||
title: 'Recording date(s)',
|
||||
group: 'creditsSpecs',
|
||||
type: 'string',
|
||||
description: 'You may use “ | ” to indicate multiple dates.',
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'recordingLocation',
|
||||
title: 'Recording location(s)',
|
||||
group: 'creditsSpecs',
|
||||
type: 'string',
|
||||
description: 'You may use “ | ” to indicate multiple locations.',
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'recordingFormat',
|
||||
title: 'Recording format',
|
||||
group: 'creditsSpecs',
|
||||
type: 'string',
|
||||
options: {
|
||||
list: [
|
||||
{title: 'PCM 352.8 kHz 24 bit', value: 'PCM 352.8 kHz 24 bit'},
|
||||
{title: 'PCM 352.8 kHz 32 bit', value: 'PCM 352.8 kHz 32 bit'},
|
||||
{title: 'DSD 11.2 MHz 1 bit', value: 'DSD 11.2 MHz 1 bit'},
|
||||
],
|
||||
},
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'masteringFormat',
|
||||
title: 'Mastering format',
|
||||
group: 'creditsSpecs',
|
||||
type: 'string',
|
||||
options: {
|
||||
list: [
|
||||
{title: 'PCM 352.8 kHz 32 bit', value: 'PCM 352.8 kHz 32 bit'},
|
||||
{title: 'PCM 352.8 kHz 64 bit', value: 'PCM 352.8 kHz 64 bit'},
|
||||
{title: 'DSD 11.2 MHz 1 bit', value: 'DSD 11.2 MHz 1 bit'},
|
||||
],
|
||||
},
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'equipment',
|
||||
title: 'Equipment',
|
||||
group: 'creditsSpecs',
|
||||
type: 'array',
|
||||
of: [
|
||||
defineArrayMember({
|
||||
name: 'equipmentItem',
|
||||
title: 'Equipment item',
|
||||
type: 'object',
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'type',
|
||||
title: 'Type',
|
||||
type: 'string',
|
||||
description: 'e.g. “Microphones”',
|
||||
}),
|
||||
defineField({
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
type: 'text',
|
||||
rows: 2,
|
||||
description: 'You may use “ | ” to indicate multiple items.',
|
||||
}),
|
||||
],
|
||||
preview: {
|
||||
select: {type: 'type', name: 'name'},
|
||||
prepare({type, name}) {
|
||||
return {title: type || '(No type)', subtitle: name}
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'genre',
|
||||
title: 'Genre(s)',
|
||||
type: 'array',
|
||||
of: [
|
||||
defineArrayMember({
|
||||
type: 'string',
|
||||
options: {
|
||||
layout: 'tags' as const,
|
||||
list: [
|
||||
{title: 'Early Music', value: 'earlyMusic'},
|
||||
{title: 'Baroque', value: 'baroque'},
|
||||
{title: 'Classical', value: 'classical'},
|
||||
{title: 'Romantic', value: 'romantic'},
|
||||
{title: 'Contemporary', value: 'contemporary'},
|
||||
{title: 'World Music', value: 'worldMusic'},
|
||||
{title: 'Jazz', value: 'jazz'},
|
||||
{title: 'Crossover', value: 'crossover'},
|
||||
{title: 'Electronic', value: 'electronic'},
|
||||
{title: 'Minimal', value: 'minimal'},
|
||||
{title: 'Pop / Rock', value: 'popRock'},
|
||||
],
|
||||
},
|
||||
} as any),
|
||||
],
|
||||
group: 'references',
|
||||
}),
|
||||
defineField({
|
||||
name: 'instrumentation',
|
||||
title: 'Instrumentation(s)',
|
||||
type: 'array',
|
||||
of: [
|
||||
defineArrayMember({
|
||||
type: 'string',
|
||||
options: {
|
||||
layout: 'tags' as const,
|
||||
list: [
|
||||
{title: 'Solo', value: 'solo'},
|
||||
{title: 'Chamber', value: 'chamber'},
|
||||
{title: 'Ensemble', value: 'ensemble'},
|
||||
{title: 'Orchestral', value: 'orchestra'},
|
||||
{title: 'Vocal / Choral', value: 'vocalChoral'},
|
||||
],
|
||||
},
|
||||
} as any),
|
||||
],
|
||||
group: 'references',
|
||||
}),
|
||||
defineField({
|
||||
name: 'artists',
|
||||
title: 'Artist(s)',
|
||||
group: 'references',
|
||||
type: 'array',
|
||||
of: [
|
||||
defineArrayMember({
|
||||
type: 'reference',
|
||||
to: [{type: 'artist'}],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
defineField({
|
||||
name: 'reviews',
|
||||
title: 'Reviews',
|
||||
group: 'reviews',
|
||||
type: 'array',
|
||||
of: [
|
||||
defineArrayMember({
|
||||
type: 'object',
|
||||
name: 'review',
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'quote',
|
||||
title: 'Quote',
|
||||
type: 'text',
|
||||
rows: 4,
|
||||
}),
|
||||
defineField({
|
||||
name: 'author',
|
||||
title: 'Author',
|
||||
type: 'string',
|
||||
}),
|
||||
],
|
||||
preview: {
|
||||
select: {
|
||||
quote: 'quote',
|
||||
author: 'author',
|
||||
},
|
||||
prepare({quote, author}) {
|
||||
return {
|
||||
title: quote
|
||||
? quote.length > 60
|
||||
? quote.substring(0, 60) + '...'
|
||||
: quote
|
||||
: '(Empty review)',
|
||||
subtitle: author || '',
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
defineField({
|
||||
name: 'availableVariants',
|
||||
title: 'Available Variants',
|
||||
type: 'array',
|
||||
group: 'medusa',
|
||||
of: [defineArrayMember({type: 'string'})],
|
||||
options: {
|
||||
list: [
|
||||
{title: 'CD', value: 'cd'},
|
||||
{title: 'SACD', value: 'sacd'},
|
||||
{title: 'LP', value: 'lp'},
|
||||
{title: 'Stereo FLAC 88/24', value: '88k24b2ch'},
|
||||
{title: 'Stereo FLAC 176/24', value: '176k24b2ch'},
|
||||
{title: 'Stereo FLAC 352/24', value: '352k24b2ch'},
|
||||
{title: 'Stereo FLAC 352/32', value: '352k32b2ch'},
|
||||
{title: 'Surround FLAC 88/24', value: '88k24b5ch'},
|
||||
{title: 'Surround FLAC 176/24', value: '176k24b5ch'},
|
||||
{title: 'Surround FLAC 352/24', value: '352k24b5ch'},
|
||||
{title: 'Surround FLAC 352/32', value: '352k32b5ch'},
|
||||
{title: 'Dolby Atmos, DTS:X & Auro-3D in MKV', value: 'mkv'},
|
||||
{title: 'Auro-3D FLAC', value: 'a3d'},
|
||||
{title: 'Dolby Atmos ADM 48kHz', value: 'adm48'},
|
||||
{title: 'Dolby Atmos ADM 96kHz', value: 'adm96'},
|
||||
{title: 'HD Video', value: 'hd'},
|
||||
{title: '4K Video', value: '4k'},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
orderings: [
|
||||
{
|
||||
title: 'Release Date (latest first)',
|
||||
name: 'releaseDateDesc',
|
||||
by: [{field: 'releaseDate', direction: 'desc'}],
|
||||
},
|
||||
{
|
||||
title: 'Release Date (oldest first)',
|
||||
name: 'releaseDateAsc',
|
||||
by: [{field: 'releaseDate', direction: 'asc'}],
|
||||
},
|
||||
{
|
||||
title: 'Catalog # (Asc.)',
|
||||
name: 'catalogNoAsc',
|
||||
by: [{field: 'catalogNo', direction: 'asc'}],
|
||||
},
|
||||
{
|
||||
title: 'Catalog # (Desc.)',
|
||||
name: 'catalogNoDesc',
|
||||
by: [{field: 'catalogNo', direction: 'desc'}],
|
||||
},
|
||||
],
|
||||
|
||||
preview: {
|
||||
select: {
|
||||
title: 'name',
|
||||
artist: 'albumArtist',
|
||||
catNo: 'catalogNo',
|
||||
media: 'albumCover',
|
||||
},
|
||||
prepare({title, artist, catNo, media}) {
|
||||
return {
|
||||
title: title || '(Untitled release)',
|
||||
subtitle: artist ? artist : '',
|
||||
media,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
43
schemaTypes/settings-type.ts
Normal file
43
schemaTypes/settings-type.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import {defineField, defineType} from 'sanity'
|
||||
import {CogIcon} from '@sanity/icons'
|
||||
|
||||
export const settingsType = defineType({
|
||||
name: 'settings',
|
||||
title: 'Settings',
|
||||
type: 'document',
|
||||
icon: CogIcon,
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
hidden: true,
|
||||
initialValue: 'Settings',
|
||||
}),
|
||||
defineField({
|
||||
name: 'featuredAlbum',
|
||||
title: 'Featured Album',
|
||||
type: 'reference',
|
||||
to: [{type: 'release'}],
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'featuredArtist',
|
||||
title: 'Featured Artist',
|
||||
type: 'reference',
|
||||
to: [{type: 'artist'}],
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'featuredComposer',
|
||||
title: 'Featured Composer',
|
||||
type: 'reference',
|
||||
to: [{type: 'composer'}],
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
],
|
||||
preview: {
|
||||
prepare() {
|
||||
return {title: 'Settings'}
|
||||
},
|
||||
},
|
||||
})
|
||||
88
schemaTypes/work-type.ts
Normal file
88
schemaTypes/work-type.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import {defineArrayMember, defineField, defineType} from 'sanity'
|
||||
import {DocumentTextIcon} from '@sanity/icons'
|
||||
import {WorkSlugInput} from '../components/WorkSlugInput'
|
||||
|
||||
export const workType = defineType({
|
||||
name: 'work',
|
||||
title: 'Work',
|
||||
type: 'document',
|
||||
icon: DocumentTextIcon,
|
||||
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'title',
|
||||
title: 'Title',
|
||||
type: 'string',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'composer',
|
||||
title: 'Composer',
|
||||
type: 'reference',
|
||||
to: [{type: 'composer'}],
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'arranger',
|
||||
title: 'Arranger',
|
||||
type: 'reference',
|
||||
to: [{type: 'composer'}],
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'slug',
|
||||
title: 'Slug',
|
||||
type: 'slug',
|
||||
components: {
|
||||
input: WorkSlugInput,
|
||||
},
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
|
||||
defineField({
|
||||
name: 'description',
|
||||
title: 'Description',
|
||||
type: 'array',
|
||||
of: [defineArrayMember({type: 'block'})],
|
||||
}),
|
||||
],
|
||||
|
||||
orderings: [
|
||||
{
|
||||
title: 'Title (A → Z)',
|
||||
name: 'titleAsc',
|
||||
by: [{field: 'title', direction: 'asc'}],
|
||||
},
|
||||
{
|
||||
title: 'Title (Z → A)',
|
||||
name: 'titleDesc',
|
||||
by: [{field: 'title', direction: 'desc'}],
|
||||
},
|
||||
],
|
||||
|
||||
preview: {
|
||||
select: {
|
||||
title: 'title',
|
||||
composer: 'composer.name',
|
||||
arranger: 'arranger.name',
|
||||
},
|
||||
prepare({title, composer, arranger}) {
|
||||
let subtitle = ''
|
||||
|
||||
if (composer && arranger) {
|
||||
subtitle = `${composer} (arr. ${arranger})`
|
||||
} else if (composer) {
|
||||
subtitle = composer
|
||||
} else if (arranger) {
|
||||
subtitle = `arr. ${arranger}`
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
subtitle,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
50
scripts/regenerate-release-slugs.ts
Normal file
50
scripts/regenerate-release-slugs.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import {getCliClient} from 'sanity/cli'
|
||||
|
||||
const client = getCliClient().withConfig({apiVersion: '2024-01-01'})
|
||||
|
||||
function toSlug(input: string): string {
|
||||
return input
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '') // strip diacritics
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '') // remove non-alphanumeric
|
||||
.trim()
|
||||
.replace(/[\s-]+/g, '-') // spaces/hyphens → single hyphen
|
||||
}
|
||||
|
||||
async function regenerateSlugs() {
|
||||
const releases = await client.fetch<{_id: string; name?: string; albumArtist?: string}[]>(
|
||||
`*[_type == "release"]{_id, name, albumArtist}`,
|
||||
)
|
||||
|
||||
console.log(`Found ${releases.length} releases`)
|
||||
|
||||
const transaction = client.transaction()
|
||||
let count = 0
|
||||
|
||||
for (const release of releases) {
|
||||
const source = [release.albumArtist, release.name].filter(Boolean).join(' ')
|
||||
if (!source) {
|
||||
console.log(`Skipping ${release._id} — no albumArtist or name`)
|
||||
continue
|
||||
}
|
||||
|
||||
const slug = toSlug(source)
|
||||
console.log(`${release._id}: "${source}" → ${slug}`)
|
||||
transaction.patch(release._id, (p) => p.set({slug: {_type: 'slug', current: slug}}))
|
||||
count++
|
||||
}
|
||||
|
||||
if (count === 0) {
|
||||
console.log('No documents to update.')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await transaction.commit()
|
||||
console.log(`Done! Updated ${result.documentIds.length} documents.`)
|
||||
}
|
||||
|
||||
regenerateSlugs().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
1
static/.gitkeep
Normal file
1
static/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
Files placed here will be served by the Sanity server under the `/static`-prefix
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
Reference in a new issue