Followed Sanity's best practices for a small overhaul.
This commit is contained in:
parent
a8aaf67022
commit
4370778b9c
21 changed files with 8929 additions and 2075 deletions
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
4129
package-lock.json
generated
4129
package-lock.json
generated
File diff suppressed because it is too large
Load diff
10
package.json
10
package.json
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "trptkio",
|
"name": "trptk-sanity",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
|
|
@ -9,16 +9,18 @@
|
||||||
"start": "sanity start",
|
"start": "sanity start",
|
||||||
"build": "sanity build",
|
"build": "sanity build",
|
||||||
"deploy": "sanity deploy",
|
"deploy": "sanity deploy",
|
||||||
"deploy-graphql": "sanity graphql deploy"
|
"deploy-graphql": "sanity graphql deploy",
|
||||||
|
"schema:extract": "sanity schema extract"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"sanity"
|
"sanity"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sanity/vision": "^5.2.0",
|
"@sanity/vision": "^5.11.0",
|
||||||
"react": "^19.1",
|
"react": "^19.1",
|
||||||
"react-dom": "^19.1",
|
"react-dom": "^19.1",
|
||||||
"sanity": "^5.2.0",
|
"sanity": "^5.11.0",
|
||||||
|
"sanity-plugin-media": "^4.1.1",
|
||||||
"styled-components": "^6.1.18"
|
"styled-components": "^6.1.18"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
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"
|
||||||
|
}
|
||||||
|
|
@ -2,15 +2,35 @@ import {defineConfig} from 'sanity'
|
||||||
import {structureTool} from 'sanity/structure'
|
import {structureTool} from 'sanity/structure'
|
||||||
import {visionTool} from '@sanity/vision'
|
import {visionTool} from '@sanity/vision'
|
||||||
import {schemaTypes} from './schemaTypes'
|
import {schemaTypes} from './schemaTypes'
|
||||||
|
import {media} from 'sanity-plugin-media'
|
||||||
|
import {CogIcon} from '@sanity/icons'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
name: 'default',
|
name: 'default',
|
||||||
title: 'TRPTK.io',
|
title: 'TRPTK',
|
||||||
|
|
||||||
projectId: 'e0x723bq',
|
projectId: 'e0x723bq',
|
||||||
dataset: 'production',
|
dataset: 'production',
|
||||||
|
|
||||||
plugins: [structureTool(), visionTool()],
|
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: {
|
schema: {
|
||||||
types: schemaTypes,
|
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
|
|
@ -1,29 +1,42 @@
|
||||||
import {defineField, defineType} from 'sanity'
|
import {defineArrayMember, defineField, defineType} from 'sanity'
|
||||||
|
import {UsersIcon} from '@sanity/icons'
|
||||||
|
|
||||||
export const artistType = defineType({
|
export const artistType = defineType({
|
||||||
name: 'artist',
|
name: 'artist',
|
||||||
title: 'Artist',
|
title: 'Artist',
|
||||||
type: 'document',
|
type: 'document',
|
||||||
|
icon: UsersIcon,
|
||||||
|
fieldsets: [{name: 'main', title: 'Main Info', options: {columns: 2}}],
|
||||||
|
|
||||||
fields: [
|
fields: [
|
||||||
defineField({
|
defineField({
|
||||||
name: 'name',
|
name: 'name',
|
||||||
title: 'Name',
|
title: 'Name',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
fieldset: 'main',
|
||||||
validation: (Rule) => Rule.required(),
|
validation: (Rule) => Rule.required(),
|
||||||
}),
|
}),
|
||||||
defineField({
|
defineField({
|
||||||
name: 'sortKey',
|
name: 'sortKey',
|
||||||
title: 'Sorting Key',
|
title: 'Sorting Key',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
fieldset: 'main',
|
||||||
validation: (Rule) => Rule.required(),
|
validation: (Rule) => Rule.required(),
|
||||||
}),
|
}),
|
||||||
defineField({
|
defineField({
|
||||||
name: 'slug',
|
name: 'slug',
|
||||||
title: 'Slug',
|
title: 'Slug',
|
||||||
type: 'slug',
|
type: 'slug',
|
||||||
|
fieldset: 'main',
|
||||||
options: {source: 'name', maxLength: 96},
|
options: {source: 'name', maxLength: 96},
|
||||||
validation: (Rule) => Rule.required(),
|
validation: (Rule) => Rule.required(),
|
||||||
}),
|
}),
|
||||||
|
defineField({
|
||||||
|
name: 'role',
|
||||||
|
title: 'Role',
|
||||||
|
type: 'string',
|
||||||
|
fieldset: 'main',
|
||||||
|
}),
|
||||||
defineField({
|
defineField({
|
||||||
name: 'image',
|
name: 'image',
|
||||||
title: 'Image',
|
title: 'Image',
|
||||||
|
|
@ -31,25 +44,25 @@ export const artistType = defineType({
|
||||||
options: {hotspot: true},
|
options: {hotspot: true},
|
||||||
}),
|
}),
|
||||||
defineField({
|
defineField({
|
||||||
name: 'description',
|
name: 'bio',
|
||||||
title: 'Description',
|
title: 'Biography',
|
||||||
type: 'array',
|
type: 'array',
|
||||||
of: [{type: 'block'}],
|
of: [defineArrayMember({type: 'block'})],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
orderings: [
|
orderings: [
|
||||||
{
|
{
|
||||||
title: 'Name (A → Z)',
|
title: 'First Name (A → Z)',
|
||||||
name: 'sortKeyAsc',
|
name: 'nameAsc',
|
||||||
by: [{field: 'name', direction: 'asc'}],
|
by: [{field: 'name', direction: 'asc'}],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Sorting key (A → Z)',
|
title: 'Last Name (A → Z)',
|
||||||
name: 'sortKeyAsc',
|
name: 'sortKeyAsc',
|
||||||
by: [{field: 'sortKey', direction: 'asc'}],
|
by: [{field: 'sortKey', direction: 'asc'}],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
preview: {
|
preview: {
|
||||||
select: {title: 'name', media: 'image'},
|
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'},
|
||||||
|
]
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
import {artistType} from './artistType'
|
import {artistType} from './artist-type'
|
||||||
import {releaseType} from './releaseType'
|
import {blogType} from './blog-type'
|
||||||
export const schemaTypes = [artistType, releaseType]
|
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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
import {defineField, defineType} from 'sanity'
|
|
||||||
|
|
||||||
export const releaseType = defineType({
|
|
||||||
name: 'release',
|
|
||||||
title: 'Release',
|
|
||||||
type: 'document',
|
|
||||||
|
|
||||||
groups: [
|
|
||||||
{name: 'main', title: 'Main', default: true},
|
|
||||||
{name: 'media', title: 'Media'},
|
|
||||||
{name: 'text', title: 'Text'},
|
|
||||||
{name: 'links', title: 'Streaming Links'},
|
|
||||||
{name: 'references', title: 'Artists / Composers'},
|
|
||||||
],
|
|
||||||
|
|
||||||
fieldsets: [
|
|
||||||
{name: 'albuminfo', title: 'Album'},
|
|
||||||
{name: 'main', title: 'Release Information', options: {columns: 2}},
|
|
||||||
{name: 'media', title: 'Media'},
|
|
||||||
{name: 'text', title: 'Descriptions'},
|
|
||||||
{name: 'links', title: 'Streaming Links', options: {columns: 2}},
|
|
||||||
{name: 'references', title: 'Artists / Composers'},
|
|
||||||
],
|
|
||||||
|
|
||||||
fields: [
|
|
||||||
defineField({
|
|
||||||
name: 'name',
|
|
||||||
title: 'Title',
|
|
||||||
type: 'string',
|
|
||||||
group: 'main',
|
|
||||||
fieldset: 'albuminfo',
|
|
||||||
}),
|
|
||||||
defineField({name: 'albumArtist', type: 'string', group: 'main', fieldset: 'albuminfo'}),
|
|
||||||
defineField({
|
|
||||||
name: 'catalogNo',
|
|
||||||
title: 'Catalog #',
|
|
||||||
type: 'string',
|
|
||||||
group: 'main',
|
|
||||||
fieldset: 'main',
|
|
||||||
}),
|
|
||||||
defineField({
|
|
||||||
name: 'slug',
|
|
||||||
title: 'Slug',
|
|
||||||
type: 'slug',
|
|
||||||
options: {source: 'catalogNo', maxLength: 10},
|
|
||||||
validation: (Rule) => Rule.required(),
|
|
||||||
group: 'main',
|
|
||||||
fieldset: 'main',
|
|
||||||
}),
|
|
||||||
defineField({
|
|
||||||
name: 'label',
|
|
||||||
title: 'Label',
|
|
||||||
type: 'string',
|
|
||||||
group: 'main',
|
|
||||||
fieldset: 'main',
|
|
||||||
options: {
|
|
||||||
list: [
|
|
||||||
{title: 'TRPTK', value: 'trptk'},
|
|
||||||
{title: 'Other', value: 'other'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
defineField({name: 'upc', title: 'UPC/EAN', type: 'string', group: 'main', fieldset: 'main'}),
|
|
||||||
defineField({
|
|
||||||
name: 'releaseDate',
|
|
||||||
title: 'Release Date',
|
|
||||||
type: 'date',
|
|
||||||
group: 'main',
|
|
||||||
fieldset: 'main',
|
|
||||||
options: {hotspot: true},
|
|
||||||
}),
|
|
||||||
defineField({name: 'albumCover', type: 'image', group: 'media', fieldset: 'media'}),
|
|
||||||
defineField({
|
|
||||||
name: 'bookletPdf',
|
|
||||||
title: 'Booklet PDF',
|
|
||||||
type: 'file',
|
|
||||||
options: {accept: 'application/pdf'},
|
|
||||||
group: 'media',
|
|
||||||
fieldset: 'media',
|
|
||||||
}),
|
|
||||||
defineField({
|
|
||||||
name: 'tracks',
|
|
||||||
title: 'Track previews',
|
|
||||||
group: 'media',
|
|
||||||
fieldset: 'media',
|
|
||||||
type: 'array',
|
|
||||||
of: [
|
|
||||||
defineField({
|
|
||||||
name: 'track',
|
|
||||||
title: 'Track',
|
|
||||||
type: 'object',
|
|
||||||
fields: [
|
|
||||||
defineField({
|
|
||||||
name: 'title',
|
|
||||||
title: 'Title',
|
|
||||||
type: 'string',
|
|
||||||
validation: (Rule) => Rule.required(),
|
|
||||||
}),
|
|
||||||
defineField({
|
|
||||||
name: 'artistText',
|
|
||||||
title: 'Artist / Composer',
|
|
||||||
type: 'string',
|
|
||||||
}),
|
|
||||||
defineField({
|
|
||||||
name: 'duration',
|
|
||||||
title: 'Duration',
|
|
||||||
type: 'string',
|
|
||||||
}),
|
|
||||||
defineField({
|
|
||||||
name: 'previewMp3',
|
|
||||||
title: '30s preview (MP3)',
|
|
||||||
type: 'file',
|
|
||||||
options: {accept: 'audio/mpeg'},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
preview: {
|
|
||||||
select: {
|
|
||||||
title: 'title',
|
|
||||||
trackNo: 'trackNo',
|
|
||||||
artistText: 'artistText',
|
|
||||||
duration: 'duration',
|
|
||||||
},
|
|
||||||
prepare({title, trackNo, artistText, duration}) {
|
|
||||||
const subtitleParts = [artistText, duration && `⏱ ${duration}`].filter(Boolean)
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: `${trackNo ? `${trackNo}. ` : ''}${title || '(Untitled track)'}`,
|
|
||||||
subtitle: subtitleParts.join(' · '),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
|
|
||||||
defineField({
|
|
||||||
name: 'shortDescription',
|
|
||||||
title: 'Short Description',
|
|
||||||
type: 'text',
|
|
||||||
rows: 2,
|
|
||||||
group: 'text',
|
|
||||||
fieldset: 'text',
|
|
||||||
}),
|
|
||||||
defineField({
|
|
||||||
name: 'description',
|
|
||||||
title: 'Description',
|
|
||||||
type: 'array',
|
|
||||||
of: [{type: 'block'}],
|
|
||||||
group: 'text',
|
|
||||||
fieldset: 'text',
|
|
||||||
}),
|
|
||||||
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: 'artists',
|
|
||||||
title: 'Artists',
|
|
||||||
group: 'references',
|
|
||||||
fieldset: 'references',
|
|
||||||
type: 'array',
|
|
||||||
of: [
|
|
||||||
{
|
|
||||||
type: 'reference',
|
|
||||||
to: [{type: 'artist'}],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
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} • ${catNo}` || '(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)
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue