Ported sanity studio to Next js app

This commit is contained in:
Henrik Larsson
2023-08-14 12:06:46 +02:00
parent de85d266bd
commit d32baa7782
67 changed files with 3755 additions and 46 deletions

View File

@@ -0,0 +1,122 @@
import {CommentIcon} from '@sanity/icons'
import {defineField} from 'sanity'
import {languages} from '../../languages'
import {validateImage} from '../../utils/validation'
export default defineField({
name: 'blurb',
title: 'Blurb',
type: 'document',
icon: CommentIcon,
fields: [
defineField({
name: 'language',
type: 'string',
readOnly: true,
description: 'Language of this document.',
// hidden: true,
}),
// Title
defineField({
name: 'title',
title: 'Title',
type: 'string',
description: 'What do you want to convey?',
validation: (Rule) => Rule.required(),
}),
// Image
defineField({
name: 'image',
title: 'Image',
type: 'mainImage',
validation: (Rule) => validateImage(Rule, true),
}),
// Text
defineField({
name: 'text',
title: 'Text',
type: 'text',
description: 'Small text displayed below title.',
rows: 5,
}),
// Link
{
name: 'link',
title: 'Link',
type: 'object',
fields: [
{
name: 'linkType',
type: 'string',
title: 'Link type',
initialValue: 'internal',
description: 'Link to internal or external content.',
validation: (Rule) => Rule.required(),
options: {
list: ['internal', 'external'],
layout: 'radio',
},
},
{
name: 'internalLink',
type: 'linkInternal',
title: 'Internal link',
hidden: ({parent}) => parent?.linkType !== 'internal',
options: {
collapsible: false,
},
validation: (Rule) =>
Rule.custom((value: any, context: any) => {
if (context.parent.linkType == 'internal') {
const currentLink = value && value.reference
if (!currentLink) {
return 'Reference is required'
}
}
return true
}),
},
{
name: 'externalLink',
type: 'linkExternal',
title: 'External link',
hidden: ({parent}) => parent?.linkType !== 'external',
options: {
collapsible: false,
},
validation: (Rule) =>
Rule.custom((value: any, context: any) => {
if (context.parent.linkType == 'external') {
const currentTitle = value?.title
const currentUrl = value?.url
if (!currentTitle) {
return 'Title is required'
} else if (!currentUrl) {
return 'URL is required'
}
}
return true
}),
},
],
},
],
preview: {
select: {
title: 'title',
image: 'image',
language: 'language',
},
prepare(selection) {
const {image, title, language} = selection
const currentLang = languages.find((lang) => lang.id === language)
return {
media: image,
title,
subtitle: `${currentLang ? currentLang.title : ''}`,
}
},
},
})

View File

@@ -0,0 +1,126 @@
import {TagIcon} from '@sanity/icons'
import {defineField, defineType} from 'sanity'
import {slugWithLocalizedType} from './slugWithLocalizedType'
import {languages} from '../../languages'
import {validateImage} from '../../utils/validation'
const GROUPS = [
{
name: 'editorial',
title: 'Editorial',
},
{
name: 'seo',
title: 'SEO',
},
]
export default defineType({
name: 'category',
title: 'Category',
type: 'document',
icon: TagIcon,
groups: GROUPS,
fields: [
// Language
defineField({
name: 'language',
type: 'string',
readOnly: true,
description: 'Language of this document.',
// hidden: true,
}),
// Title
defineField({
name: 'title',
title: 'Title',
type: 'string',
description: 'Category title.',
}),
// Slug
slugWithLocalizedType('category', 'title'),
// // Show banner
// defineField({
// name: 'showBanner',
// title: 'Show banner',
// type: 'boolean',
// description: 'If disabled, category title will be displayed instead.',
// group: 'editorial',
// }),
// // Banner
// defineField({
// name: 'banner',
// title: 'Banner',
// type: 'banner',
// hidden: ({document}) => !document?.showBanner,
// group: 'editorial',
// }),
// Image
defineField({
name: 'image',
type: 'mainImage',
title: 'Image',
validation: (Rule) => validateImage(Rule, true),
}),
defineField({
name: 'description',
title: 'Description',
type: 'text',
rows: 5,
description: 'Description of this category.',
}),
defineField({
name: 'id',
title: 'ID',
type: 'number',
description: 'Unique ID.',
}),
defineField({
name: 'categoryId',
title: 'Category ID',
type: 'number',
description: 'Unique category ID.',
}),
defineField({
name: 'parentId',
title: 'Parent ID',
type: 'number',
description: 'Unique parent category ID.',
}),
// SEO
defineField({
name: 'seo',
title: 'SEO',
type: 'seo',
group: 'seo',
}),
],
orderings: [
{
name: 'titleAsc',
title: 'Title (A-Z)',
by: [{field: 'title', direction: 'asc'}],
},
{
name: 'titleDesc',
title: 'Title (Z-A)',
by: [{field: 'title', direction: 'desc'}],
},
],
preview: {
select: {
title: 'title',
language: 'language',
},
prepare(selection) {
const {title, language} = selection
const currentLang = languages.find((lang) => lang.id === language)
return {
title,
subtitle: `${currentLang ? currentLang.title : ''}`,
media: TagIcon,
}
},
},
})

View File

@@ -0,0 +1,48 @@
import {MenuIcon} from '@sanity/icons'
import {defineType, defineField} from 'sanity'
import {languages} from '../../languages'
export default defineType({
name: 'footerMenu',
title: 'Footer menu',
type: 'document',
icon: MenuIcon,
groups: [],
fields: [
defineField({
name: 'language',
type: 'string',
readOnly: true,
description: 'Language of this document.',
// hidden: true,
}),
defineField({
name: 'title',
type: 'string',
title: 'Title',
description: 'Menu title or designation for menu.',
}),
// Menu
defineField({
name: 'menu',
title: 'Menu',
type: 'menu',
}),
],
preview: {
select: {
title: 'title',
language: 'language',
},
prepare(selection) {
const {title, language} = selection
const currentLang = languages.find((lang) => lang.id === language)
return {
title: `${title}`,
subtitle: `${currentLang ? currentLang.title : ''}`,
}
},
},
})

View File

@@ -0,0 +1,75 @@
import {DocumentIcon} from '@sanity/icons'
import {defineField} from 'sanity'
import {languages} from '../../languages'
import {COMPONENT_REFERENCES} from '../../constants'
import {slugWithLocalizedType} from './slugWithLocalizedType'
export default defineField({
name: 'page',
title: 'Page',
type: 'document',
icon: DocumentIcon,
groups: [
{
name: 'editorial',
title: 'Editorial',
},
{
name: 'seo',
title: 'SEO',
},
],
fields: [
defineField({
name: 'language',
type: 'string',
readOnly: true,
description: 'Language of this document.',
// hidden: true,
}),
// Title
defineField({
name: 'title',
title: 'Title',
type: 'string',
description: 'Page title.',
validation: (Rule) => Rule.required(),
}),
// Slug
slugWithLocalizedType('page', 'title'),
// Content
defineField({
name: 'content',
title: 'Page sections',
type: 'array',
group: 'editorial',
description: 'Add, reorder, edit or delete page sections.',
of: COMPONENT_REFERENCES,
}),
// SEO
defineField({
name: 'seo',
title: 'SEO',
type: 'seo',
group: 'seo',
}),
],
preview: {
select: {
seoImage: 'seo.image',
title: 'title',
language: 'language',
},
prepare(selection) {
const {seoImage, title, language} = selection
const currentLang = languages.find((lang) => lang.id === language)
return {
media: seoImage,
title,
subtitle: `${currentLang ? currentLang.title : ''}`,
}
},
},
})

View File

@@ -0,0 +1,167 @@
import {PackageIcon} from '@sanity/icons'
import {defineField, defineType} from 'sanity'
import {slugWithLocalizedType} from './slugWithLocalizedType'
import {languages} from '../../languages'
import {validateImage} from '../../utils/validation'
const GROUPS = [
{
name: 'editorial',
title: 'Editorial',
},
{
name: 'seo',
title: 'SEO',
},
]
export default defineType({
name: 'product',
title: 'Product',
type: 'document',
icon: PackageIcon,
groups: GROUPS,
fields: [
// Language
defineField({
name: 'language',
type: 'string',
readOnly: true,
description: 'Language of this document.',
// hidden: true,
}),
// ID
defineField({
name: 'id',
title: 'ID',
type: 'number',
description: 'Unique product ID.',
}),
// Title
defineField({
name: 'title',
title: 'Title',
type: 'string',
description: 'Product title/name.',
validation: (Rule) => Rule.required(),
}),
// Slug
slugWithLocalizedType('product', 'title'),
defineField({
name: 'images',
title: 'Images',
description: 'Images of this product, the first image will be used as main image.',
type: 'array',
of: [
{
title: 'Image',
type: 'mainImage',
validation: (Rule) => validateImage(Rule, true),
},
],
validation: (Rule) => Rule.required().min(1).max(5),
}),
defineField({
name: 'description',
title: 'Description',
type: 'text',
description: 'Product description.',
}),
defineField({
name: 'price',
title: 'Price',
type: 'object',
description: 'Product price information.',
fields: [
defineField({
name: 'value',
title: 'Value',
type: 'number',
description: 'Product price.',
}),
defineField({
name: 'currencyCode',
title: 'Currency code',
type: 'string',
description: 'Product currency code.',
options: {
list: [
{title: 'SEK', value: 'SEK'},
{title: 'GBP', value: 'GBP'},
{title: 'EUR', value: 'EUR'},
],
layout: 'radio',
},
initialValue: 'SEK',
}),
defineField({
name: 'retailPrice',
title: 'Retail price',
type: 'number',
description: 'Product retail price.',
}),
],
}),
defineField({
name: 'options',
title: 'Product options',
type: 'array',
description: 'What product options are available?',
of: [{type: 'productOptions'}],
}),
defineField({
name: 'categories',
title: 'Categories',
type: 'array',
description: 'What category/categories does this product belong to?',
of: [{type: 'reference', to: {type: 'category'}}],
}),
defineField({
name: 'seo',
title: 'SEO',
type: 'seo',
group: 'seo',
}),
],
orderings: [
{
name: 'titleAsc',
title: 'Title (A-Z)',
by: [{field: 'title', direction: 'asc'}],
},
{
name: 'titleDesc',
title: 'Title (Z-A)',
by: [{field: 'title', direction: 'desc'}],
},
{
name: 'priceDesc',
title: 'Price (Highest first)',
by: [{field: 'price', direction: 'desc'}],
},
{
name: 'priceAsc',
title: 'Title (Lowest first)',
by: [{field: 'price', direction: 'asc'}],
},
],
preview: {
select: {
images: 'images',
title: 'title',
language: 'language',
},
prepare(selection) {
const {images, title, language} = selection
const currentLang = languages.find((lang) => lang.id === language)
const firstImage = images[0]
return {
title,
subtitle: `${currentLang ? currentLang.title : ''}`,
media: firstImage ? firstImage : PackageIcon,
}
},
},
})

View File

@@ -0,0 +1,30 @@
import {CopyIcon} from '@sanity/icons'
import {defineField, defineType} from 'sanity'
export default defineType({
name: 'productVariant',
title: 'Product variant',
type: 'document',
icon: CopyIcon,
fields: [
// Title
defineField({
title: 'Title',
name: 'title',
type: 'string',
description: 'Product variant title/name.'
}),
],
preview: {
select: {
title: 'title',
},
prepare(selection) {
const {title} = selection
return {
title,
}
},
},
})

View File

@@ -0,0 +1,64 @@
import {defineField} from 'sanity'
import {BlockElementIcon} from '@sanity/icons'
import {languages} from '../../languages'
export default defineField({
name: 'section',
type: 'document',
title: 'Reusable section',
icon: BlockElementIcon,
fields: [
defineField({
name: 'language',
type: 'string',
readOnly: true,
description: 'Language of this document.',
// hidden: true,
}),
// Title
defineField({
name: 'title',
title: 'Title',
type: 'string',
}),
defineField({
name: 'section',
title: 'Section',
type: 'object',
description: 'Reusable section to refer to from other pages.',
fields: [
defineField({
name: 'sectionType',
type: 'array',
title: 'Section type',
description: 'Select reusable component (only 1 allowed).',
of: [
{type: 'hero'},
{type: 'filteredProductList'},
{type: 'slider'},
{type: 'blurbSection'},
{type: 'uspSection'},
],
validation: (Rule) => Rule.length(1),
}),
],
}),
],
preview: {
select: {
title: 'title',
language: 'language',
},
prepare(selection) {
const {title, language} = selection
const currentLang = languages.find((lang) => lang.id === language)
return {
title: `${title}`,
media: BlockElementIcon,
subtitle: `${currentLang ? currentLang.title : ''}`,
}
},
},
})

View File

@@ -0,0 +1,56 @@
import {Rule, Slug} from 'sanity'
import slugify from "slugify";
import { i18n } from "../../languages";
import { localizedTypes } from "../../localizedTypes";
const MAX_LENGTH = 96
function formatSlug(input: string, docType: string, context: any, schemaType: object | any) {
const locale = schemaType?.parent?.language ? schemaType?.parent?.language : i18n.base;
let currentDocType: any;
currentDocType = localizedTypes.find(item => item.type === docType);
const currentDocTypeLocalized = currentDocType[locale];
const slugStart = currentDocTypeLocalized ? `/${currentDocTypeLocalized}/` : `/`;
const slug = slugify(input, { lower: true });
return slugStart + slug;
}
export function slugWithLocalizedType(documentType = '', source = `title`) {
const docType = documentType;
return {
name: `slug`,
type: `slug`,
options: {
source,
slugify: (value: any, context: any, schemaType: object | any) => formatSlug(value, docType, context, schemaType),
},
validation: (Rule: Rule) => {
return Rule.required().custom(async (value: Slug) => {
const currentSlug = value && value.current
if (!currentSlug) {
return true
}
if (currentSlug.length >= MAX_LENGTH) {
return `Must be less than ${MAX_LENGTH} characters`
}
if (currentSlug.length === 0) {
return 'Slug cannot be empty'
}
if (currentSlug.endsWith("/")) {
return 'Slug cannot end with "/"'
}
return true
})
}
};
}

View File

@@ -0,0 +1,62 @@
import {StarIcon} from '@sanity/icons'
import {defineField} from 'sanity'
import {languages} from '../../languages'
import {validateImage} from '../../utils/validation'
export default defineField({
name: 'usp',
title: 'USPs',
type: 'document',
icon: StarIcon,
fields: [
defineField({
name: 'language',
type: 'string',
readOnly: true,
description: 'Language of this document.',
// hidden: true,
}),
// Title
defineField({
name: 'title',
title: 'Title',
type: 'string',
description: 'USP title',
validation: (Rule) => Rule.required(),
}),
// Image
defineField({
name: 'image',
title: 'Image',
type: 'mainImage',
description: 'USP icon',
validation: (Rule) => validateImage(Rule, true),
}),
// Text
defineField({
name: 'text',
title: 'Text',
type: 'text',
description: 'Small text displayed below title.',
rows: 5,
}),
],
preview: {
select: {
title: 'title',
image: 'image',
language: 'language',
},
prepare(selection) {
const {image, title, language} = selection
const currentLang = languages.find((lang) => lang.id === language)
return {
media: image,
title,
subtitle: `${currentLang ? currentLang.title : ''}`,
}
},
},
})