turned template into create cli
This commit is contained in:
29
template/apps/studio/.gitignore
vendored
Normal file
29
template/apps/studio/.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# Dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# Compiled Sanity Studio
|
||||
/dist
|
||||
|
||||
# Temporary Sanity runtime, generated by the CLI on every dev server start
|
||||
/.sanity
|
||||
|
||||
# Logs
|
||||
/logs
|
||||
*.log
|
||||
|
||||
# Coverage directory used by testing tools
|
||||
/coverage
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# Typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Dotenv and similar local-only files
|
||||
*.local
|
||||
11
template/apps/studio/README.md
Normal file
11
template/apps/studio/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Sanity Blogging Content Studio
|
||||
|
||||
Congratulations, you have now installed the Sanity Content Studio, an open-source real-time content editing environment connected to the Sanity backend.
|
||||
|
||||
Now you can do the following things:
|
||||
|
||||
- [Read “getting started” in the docs](https://www.sanity.io/docs/introduction/getting-started?utm_source=readme)
|
||||
- Check out the example frontend: [React/Next.js](https://github.com/sanity-io/tutorial-sanity-blog-react-next)
|
||||
- [Read the blog post about this template](https://www.sanity.io/blog/build-your-own-blog-with-sanity-and-next-js?utm_source=readme)
|
||||
- [Join the Sanity community](https://www.sanity.io/community/join?utm_source=readme)
|
||||
- [Extend and build plugins](https://www.sanity.io/docs/content-studio/extending?utm_source=readme)
|
||||
21
template/apps/studio/components/TextAlignComponent.tsx
Normal file
21
template/apps/studio/components/TextAlignComponent.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import {BlockDecoratorProps} from 'sanity'
|
||||
|
||||
type TextAlignValue = 'left' | 'center' | 'right'
|
||||
interface TextAlignBlockDecoratorProps extends BlockDecoratorProps {
|
||||
value: TextAlignValue
|
||||
}
|
||||
|
||||
export const TextAlign = (props: TextAlignBlockDecoratorProps) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
// props.value exists and is of type TextAlignValue
|
||||
textAlign: props.value ? props.value : 'left',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
template/apps/studio/components/icons/alignment-center.tsx
Normal file
11
template/apps/studio/components/icons/alignment-center.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function AlignmentCenterIcon() {
|
||||
return (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.875 1H10.125M3.875 4.625H8.125M1.875 8.25H10.125"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
11
template/apps/studio/components/icons/alignment-left.tsx
Normal file
11
template/apps/studio/components/icons/alignment-left.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function AlignmentLeftIcon() {
|
||||
return (
|
||||
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.375 1H10.625M2.375 4.625H6.625M2.375 8.25H10.625"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
11
template/apps/studio/components/icons/alignment-right.tsx
Normal file
11
template/apps/studio/components/icons/alignment-right.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function AlignmentRightIcon() {
|
||||
return (
|
||||
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.375 1H10.625M6.375 4.625H10.625M2.375 8.25H10.625"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
3
template/apps/studio/eslint.config.mjs
Normal file
3
template/apps/studio/eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import studio from '@sanity/eslint-config-studio'
|
||||
|
||||
export default [...studio]
|
||||
1
template/apps/studio/example.env
Normal file
1
template/apps/studio/example.env
Normal file
@@ -0,0 +1 @@
|
||||
SANITY_STUDIO_PREVIEW_ORIGIN=http://localhost:3000
|
||||
18
template/apps/studio/lib/colorUtils.ts
Normal file
18
template/apps/studio/lib/colorUtils.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { brandColors, primitives, variables } from '@repo/ui'
|
||||
|
||||
export const createColorList = () => {
|
||||
// Flatten primitives into a single object
|
||||
const flatPrimitives = Object.entries(primitives).reduce((acc, [category, shades]) => {
|
||||
Object.entries(shades).forEach(([shade, value]) => {
|
||||
acc[`${category}-${shade}`] = value;
|
||||
});
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
const allColors = { ...flatPrimitives, ...variables, ...brandColors };
|
||||
|
||||
return Object.entries(allColors).map(([key, value]) => ({
|
||||
label: key.replace(/[-_]/g, ' ').replace(/^\w/, c => c.toUpperCase()),
|
||||
value: value as string
|
||||
}));
|
||||
};
|
||||
45
template/apps/studio/package.json
Normal file
45
template/apps/studio/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "studio",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"main": "package.json",
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"dev": "sanity dev",
|
||||
"start": "sanity start",
|
||||
"build": "sanity build",
|
||||
"deploy": "sanity deploy",
|
||||
"generate": "sanity schema extract && sanity typegen generate",
|
||||
"deploy-graphql": "sanity graphql deploy"
|
||||
},
|
||||
"keywords": [
|
||||
"sanity"
|
||||
],
|
||||
"dependencies": {
|
||||
"@repo/sanity-connection": "*",
|
||||
"@repo/ui": "*",
|
||||
"@sanity/document-internationalization": "^3.3.3",
|
||||
"@sanity/vision": "^4.1.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"sanity": "^4.1.1",
|
||||
"sanity-plugin-link-field": "^1.4.0",
|
||||
"sanity-plugin-media": "^3.0.4",
|
||||
"sanity-plugin-seo": "^1.3.1",
|
||||
"sanity-plugin-simpler-color-input": "^3.1.1",
|
||||
"styled-components": "^6.1.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sanity/eslint-config-studio": "^5.0.2",
|
||||
"@types/react": "^19.1.8",
|
||||
"eslint": "^9.31.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"printWidth": 100,
|
||||
"bracketSpacing": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
}
|
||||
4
template/apps/studio/sanity-typegen.json
Normal file
4
template/apps/studio/sanity-typegen.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"generates": "../client/src/sanity/sanity.types.ts",
|
||||
"path": "./schemaTypes/*.ts"
|
||||
}
|
||||
15
template/apps/studio/sanity.cli.ts
Normal file
15
template/apps/studio/sanity.cli.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { sanityConnection } from '@repo/sanity-connection'
|
||||
import {defineCliConfig} from 'sanity/cli'
|
||||
|
||||
export default defineCliConfig({
|
||||
api: {
|
||||
projectId: sanityConnection.projectId,
|
||||
dataset: sanityConnection.dataset
|
||||
},
|
||||
studioHost: sanityConnection.studioHost,
|
||||
/**
|
||||
* Enable auto-updates for studios.
|
||||
* Learn more at https://www.sanity.io/docs/cli#auto-updates
|
||||
*/
|
||||
autoUpdates: true,
|
||||
})
|
||||
65
template/apps/studio/sanity.config.ts
Normal file
65
template/apps/studio/sanity.config.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {defineConfig} from 'sanity'
|
||||
import {structureTool} from 'sanity/structure'
|
||||
import {ClipboardIcon, HomeIcon, WrenchIcon} from '@sanity/icons'
|
||||
import {schemaTypes} from './schemaTypes'
|
||||
import {presentationTool} from 'sanity/presentation'
|
||||
import {linkField} from 'sanity-plugin-link-field'
|
||||
import {seoMetaFields} from 'sanity-plugin-seo'
|
||||
import {simplerColorInput} from 'sanity-plugin-simpler-color-input'
|
||||
import {createColorList} from './lib/colorUtils'
|
||||
import { sanityConnection } from '@repo/sanity-connection'
|
||||
import Logo from '@repo/ui/components/logo'
|
||||
|
||||
export default defineConfig({
|
||||
name: 'default',
|
||||
title: sanityConnection.pageTitle,
|
||||
projectId: sanityConnection.projectId,
|
||||
dataset: sanityConnection.dataset,
|
||||
icon: Logo,
|
||||
plugins: [
|
||||
structureTool({
|
||||
title: 'Content',
|
||||
structure: (S) =>
|
||||
S.list()
|
||||
.title('Content')
|
||||
.items([
|
||||
S.listItem()
|
||||
.title('Landing Page')
|
||||
.icon(HomeIcon)
|
||||
.child(S.document().schemaType('home').documentId('home').title('Home')),
|
||||
S.divider(),
|
||||
S.listItem()
|
||||
.title('Custom pages')
|
||||
.icon(ClipboardIcon)
|
||||
.child(S.documentTypeList('custom').title('Content')),
|
||||
S.divider(),
|
||||
S.listItem()
|
||||
.title('Settings')
|
||||
.icon(WrenchIcon)
|
||||
.child(S.document().schemaType('settings').documentId('settings').title('Settings')),
|
||||
]),
|
||||
}),
|
||||
linkField({
|
||||
linkableSchemaTypes: ['custom'],
|
||||
}),
|
||||
presentationTool({
|
||||
previewUrl: {
|
||||
origin: sanityConnection.previewUrl ?? 'http://localhost:3000',
|
||||
preview: '/',
|
||||
previewMode: {
|
||||
enable: '/api/draft-mode/enable',
|
||||
},
|
||||
},
|
||||
}),
|
||||
seoMetaFields(),
|
||||
simplerColorInput({
|
||||
defaultColorFormat: 'hex',
|
||||
defaultColorList: createColorList(),
|
||||
enableSearch: true,
|
||||
showColorValue: true,
|
||||
}),
|
||||
],
|
||||
schema: {
|
||||
types: schemaTypes,
|
||||
},
|
||||
})
|
||||
3133
template/apps/studio/schema.json
Normal file
3133
template/apps/studio/schema.json
Normal file
File diff suppressed because it is too large
Load Diff
89
template/apps/studio/schemaTypes/blockContent.ts
Normal file
89
template/apps/studio/schemaTypes/blockContent.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { defineType, defineArrayMember } from 'sanity'
|
||||
import AlignmentLeftIcon from '../components/icons/alignment-left'
|
||||
import AlignmentCenterIcon from '../components/icons/alignment-center'
|
||||
import AlignmentRightIcon from '../components/icons/alignment-right'
|
||||
import { TextAlign } from '../components/TextAlignComponent'
|
||||
|
||||
/**
|
||||
* This is the schema definition for the rich text fields used for
|
||||
* for this blog studio. When you import it in schemas.js it can be
|
||||
* reused in other parts of the studio with:
|
||||
* {
|
||||
* name: 'someName',
|
||||
* title: 'Some title',
|
||||
* type: 'blockContent'
|
||||
* }
|
||||
*/
|
||||
export default defineType({
|
||||
title: 'Block Content',
|
||||
name: 'blockContent',
|
||||
type: 'array',
|
||||
of: [
|
||||
defineArrayMember({
|
||||
title: 'Block',
|
||||
type: 'block',
|
||||
// Styles let you set what your user can mark up blocks with. These
|
||||
// correspond with HTML tags, but you can set any title or value
|
||||
// you want and decide how you want to deal with it where you want to
|
||||
// use your content.
|
||||
styles: [
|
||||
{ title: 'Normal', value: 'normal' },
|
||||
{ title: 'H1', value: 'h1' },
|
||||
{ title: 'H2', value: 'h2' },
|
||||
{ title: 'H3', value: 'h3' },
|
||||
{ title: 'H4', value: 'h4' },
|
||||
{ title: 'Quote', value: 'blockquote' },
|
||||
],
|
||||
lists: [{ title: 'Bulletpoint', value: 'bullet' }],
|
||||
// Marks let you mark up inline text in the block editor.
|
||||
marks: {
|
||||
// Decorators usually describe a single property – e.g. a typographic
|
||||
// preference or highlighting by editors.
|
||||
decorators: [
|
||||
{ title: 'Strong', value: 'strong' },
|
||||
{ title: 'Italic', value: 'em' },
|
||||
{ title: 'Left', value: 'left', icon: AlignmentLeftIcon, component: (props) => TextAlign(props) },
|
||||
{ title: 'Center', value: 'center', icon: AlignmentCenterIcon, component: (props) => TextAlign(props) },
|
||||
{ title: 'Right', value: 'right', icon: AlignmentRightIcon, component: (props) => TextAlign(props) },
|
||||
],
|
||||
// Annotations can be any object structure – e.g. a link or a footnote.
|
||||
annotations: [
|
||||
{
|
||||
title: 'Link',
|
||||
name: 'link',
|
||||
type: 'object',
|
||||
fields: [
|
||||
{
|
||||
title: 'Link',
|
||||
name: 'href',
|
||||
type: 'link',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "textColor"
|
||||
},
|
||||
{
|
||||
type: "highlightColor"
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
// You can add additional types here. Note that you can't use
|
||||
// primitive types such as 'string' and 'number' in the same array
|
||||
// as a block type.
|
||||
defineArrayMember({
|
||||
title: 'Button',
|
||||
type: 'button',
|
||||
}),
|
||||
defineArrayMember({
|
||||
title: 'Image',
|
||||
type: 'image',
|
||||
options: { hotspot: true },
|
||||
}),
|
||||
defineArrayMember({
|
||||
title: 'File',
|
||||
type: 'file',
|
||||
}),
|
||||
],
|
||||
})
|
||||
15
template/apps/studio/schemaTypes/index.ts
Normal file
15
template/apps/studio/schemaTypes/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import blockContent from "./blockContent";
|
||||
import settings from "./settings";
|
||||
|
||||
import * as objects from './objects'
|
||||
import * as sections from './sections'
|
||||
import * as pages from './pages'
|
||||
|
||||
export const schemaTypes = [
|
||||
blockContent,
|
||||
settings,
|
||||
|
||||
...Object.values(objects),
|
||||
...Object.values(sections),
|
||||
...Object.values(pages),
|
||||
]
|
||||
33
template/apps/studio/schemaTypes/objects/button.ts
Normal file
33
template/apps/studio/schemaTypes/objects/button.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ComponentIcon } from '@sanity/icons'
|
||||
import { defineField, defineType } from 'sanity'
|
||||
|
||||
export default defineType({
|
||||
name: 'button',
|
||||
title: 'CTA Button',
|
||||
type: 'object',
|
||||
icon: ComponentIcon,
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'text',
|
||||
title: 'Button Label',
|
||||
type: 'string',
|
||||
validation: (Rule) => Rule.required().min(2).max(50),
|
||||
}),
|
||||
defineField({
|
||||
name: 'link',
|
||||
title: 'Link',
|
||||
type: 'link',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
],
|
||||
preview: {
|
||||
select: {
|
||||
title: 'text',
|
||||
},
|
||||
prepare({ title }) {
|
||||
return {
|
||||
title: title || 'Untitled Button',
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
34
template/apps/studio/schemaTypes/objects/faq.ts
Normal file
34
template/apps/studio/schemaTypes/objects/faq.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { HelpCircleIcon } from '@sanity/icons'
|
||||
import { defineField, defineType } from 'sanity'
|
||||
|
||||
export default defineType({
|
||||
name: 'faq',
|
||||
title: 'FAQ Item',
|
||||
type: 'object',
|
||||
icon: HelpCircleIcon,
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'question',
|
||||
title: 'Question',
|
||||
type: 'string',
|
||||
validation: (Rule) => Rule.required().min(1).max(200),
|
||||
}),
|
||||
defineField({
|
||||
name: 'answer',
|
||||
title: 'Answer',
|
||||
type: 'blockContent',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
],
|
||||
preview: {
|
||||
select: {
|
||||
title: 'question',
|
||||
subtitle: 'answer',
|
||||
},
|
||||
prepare({ title }) {
|
||||
return {
|
||||
title: title || 'Untitled FAQ',
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
20
template/apps/studio/schemaTypes/objects/imageWithAlt.ts
Normal file
20
template/apps/studio/schemaTypes/objects/imageWithAlt.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {defineField, defineType} from 'sanity'
|
||||
|
||||
export default defineField({
|
||||
name: 'imageWithAlt',
|
||||
title: 'Image',
|
||||
type: 'image',
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'alt',
|
||||
title: 'Alt Text',
|
||||
type: 'string',
|
||||
description: 'Important for SEO and accessibility',
|
||||
validation: (Rule) => Rule.required().min(1).max(100),
|
||||
}),
|
||||
],
|
||||
options: {
|
||||
hotspot: true,
|
||||
},
|
||||
validation: (Rule) => Rule.required()
|
||||
})
|
||||
3
template/apps/studio/schemaTypes/objects/index.ts
Normal file
3
template/apps/studio/schemaTypes/objects/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as faq } from './faq'
|
||||
export { default as imageWithAlt } from './imageWithAlt'
|
||||
export { default as button } from './button'
|
||||
36
template/apps/studio/schemaTypes/pages/custom.ts
Normal file
36
template/apps/studio/schemaTypes/pages/custom.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineField, defineType, type SlugRule, type StringRule } from 'sanity'
|
||||
|
||||
export default defineType({
|
||||
name: 'custom',
|
||||
title: 'Custom page',
|
||||
type: 'document',
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'title',
|
||||
title: 'Title',
|
||||
type: 'string',
|
||||
validation: (Rule: StringRule) => Rule.required().error('Title is required')
|
||||
}),
|
||||
defineField({
|
||||
name: 'slug',
|
||||
title: 'Slug',
|
||||
type: 'slug',
|
||||
options: {
|
||||
source: 'title',
|
||||
maxLength: 96,
|
||||
},
|
||||
validation: (Rule: SlugRule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
title: 'Content',
|
||||
name: 'body',
|
||||
type: 'blockContent',
|
||||
}),
|
||||
],
|
||||
|
||||
preview: {
|
||||
select: {
|
||||
title: 'title'
|
||||
},
|
||||
},
|
||||
})
|
||||
33
template/apps/studio/schemaTypes/pages/home.ts
Normal file
33
template/apps/studio/schemaTypes/pages/home.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {HomeIcon} from '@sanity/icons'
|
||||
import {defineField, defineType} from 'sanity'
|
||||
|
||||
export default defineType({
|
||||
name: 'home',
|
||||
title: 'Landing Page',
|
||||
type: 'document',
|
||||
icon: HomeIcon,
|
||||
groups: [
|
||||
{
|
||||
name: 'header',
|
||||
title: 'Header',
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'title',
|
||||
title: 'Page Title',
|
||||
type: 'string',
|
||||
validation: (Rule) => Rule.required(),
|
||||
group: 'header',
|
||||
}),
|
||||
|
||||
// Page sections
|
||||
defineField({
|
||||
name: 'headerSection',
|
||||
title: 'Header Section',
|
||||
type: 'ctaSection',
|
||||
group: 'header',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
],
|
||||
})
|
||||
3
template/apps/studio/schemaTypes/pages/index.ts
Normal file
3
template/apps/studio/schemaTypes/pages/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Page types
|
||||
export { default as homePage } from './home'
|
||||
export { default as custom } from "./custom";
|
||||
71
template/apps/studio/schemaTypes/sections/contact.ts
Normal file
71
template/apps/studio/schemaTypes/sections/contact.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { EnvelopeIcon } from '@sanity/icons'
|
||||
import { defineField, defineType } from 'sanity'
|
||||
|
||||
export default defineType({
|
||||
name: 'contactSection',
|
||||
title: 'Contact Section',
|
||||
type: 'object',
|
||||
icon: EnvelopeIcon,
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'title',
|
||||
title: 'Section Title',
|
||||
type: 'string',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'contactMethods',
|
||||
title: 'Contact Methods',
|
||||
type: 'array',
|
||||
of: [
|
||||
{
|
||||
type: 'object',
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'type',
|
||||
title: 'Contact Type',
|
||||
type: 'string',
|
||||
options: {
|
||||
list: [
|
||||
{title: 'Email', value: 'email'},
|
||||
{title: 'Phone', value: 'phone'},
|
||||
{title: 'Address', value: 'address'},
|
||||
{title: 'Social Media', value: 'social'},
|
||||
],
|
||||
},
|
||||
}),
|
||||
defineField({
|
||||
name: 'label',
|
||||
title: 'Label',
|
||||
type: 'string',
|
||||
}),
|
||||
defineField({
|
||||
name: 'value',
|
||||
title: 'Contact Information',
|
||||
type: 'string',
|
||||
}),
|
||||
defineField({
|
||||
name: 'link',
|
||||
title: 'Link',
|
||||
type: 'link',
|
||||
description: 'Optional link (mailto:, tel:, etc.)',
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
preview: {
|
||||
select: {
|
||||
title: 'title',
|
||||
contactMethods: 'contactMethods',
|
||||
},
|
||||
prepare({ title, contactMethods }) {
|
||||
const methodCount = contactMethods?.length || 0
|
||||
return {
|
||||
title: title || 'Contact Section',
|
||||
subtitle: `${methodCount} contact method${methodCount !== 1 ? 's' : ''}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
49
template/apps/studio/schemaTypes/sections/cta.ts
Normal file
49
template/apps/studio/schemaTypes/sections/cta.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { RocketIcon } from '@sanity/icons'
|
||||
import { defineField, defineType } from 'sanity'
|
||||
|
||||
export default defineType({
|
||||
name: 'ctaSection',
|
||||
title: 'Call to Action Section',
|
||||
type: 'object',
|
||||
icon: RocketIcon,
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'backgroundImage',
|
||||
title: 'Background Image',
|
||||
type: 'imageWithAlt',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'title',
|
||||
title: 'Title',
|
||||
type: 'string',
|
||||
validation: (Rule) => Rule.required().min(1).max(100),
|
||||
}),
|
||||
defineField({
|
||||
name: 'description',
|
||||
title: 'Description',
|
||||
type: 'blockContent',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'button',
|
||||
title: 'Button',
|
||||
type: 'button',
|
||||
validation: (Rule) => Rule.required(),
|
||||
}),
|
||||
],
|
||||
preview: {
|
||||
select: {
|
||||
title: 'title',
|
||||
media: 'backgroundImage.image',
|
||||
buttonText: 'primaryButton.text',
|
||||
},
|
||||
prepare({ title, media, buttonText }) {
|
||||
return {
|
||||
title: title || 'CTA Section',
|
||||
subtitle: buttonText ? `Primary: ${buttonText}` : 'No button text',
|
||||
media,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
45
template/apps/studio/schemaTypes/sections/faq.ts
Normal file
45
template/apps/studio/schemaTypes/sections/faq.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { BulbOutlineIcon } from '@sanity/icons'
|
||||
import { defineField, defineType } from 'sanity'
|
||||
|
||||
export default defineType({
|
||||
name: 'faqSection',
|
||||
title: 'FAQ Section',
|
||||
type: 'object',
|
||||
icon: BulbOutlineIcon,
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'sectionTitle',
|
||||
title: 'Section Title',
|
||||
type: 'string',
|
||||
description: 'Small title above the main heading',
|
||||
}),
|
||||
defineField({
|
||||
name: 'title',
|
||||
title: 'Title',
|
||||
type: 'string',
|
||||
validation: (Rule) => Rule.required().min(1).max(100),
|
||||
}),
|
||||
defineField({
|
||||
name: 'faqs',
|
||||
title: 'FAQ Items',
|
||||
type: 'array',
|
||||
of: [{ type: 'faq' }],
|
||||
validation: (Rule) => Rule.required().min(1).max(20),
|
||||
options: {
|
||||
sortable: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
preview: {
|
||||
select: {
|
||||
title: 'title',
|
||||
faqCount: 'faqs.length',
|
||||
},
|
||||
prepare({ title, faqCount }) {
|
||||
return {
|
||||
title: title || 'FAQ Section',
|
||||
subtitle: `${faqCount || 0} FAQs`,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
3
template/apps/studio/schemaTypes/sections/index.ts
Normal file
3
template/apps/studio/schemaTypes/sections/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as ctaSection } from './cta'
|
||||
export { default as faqSection } from './faq'
|
||||
export { default as contactSection } from './contact'
|
||||
44
template/apps/studio/schemaTypes/settings.ts
Normal file
44
template/apps/studio/schemaTypes/settings.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {WrenchIcon} from '@sanity/icons'
|
||||
import {defineField, defineType} from 'sanity'
|
||||
|
||||
export default defineType({
|
||||
name: 'settings',
|
||||
title: 'Einstellungen',
|
||||
type: 'document',
|
||||
icon: WrenchIcon,
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'title',
|
||||
title: 'Title',
|
||||
type: 'string',
|
||||
}),
|
||||
defineField({
|
||||
name: 'longTitle',
|
||||
title: 'Long Title',
|
||||
description: 'Used for SEO and social media',
|
||||
type: 'string',
|
||||
}),
|
||||
defineField({
|
||||
name: 'description',
|
||||
title: 'Description',
|
||||
description: 'Important for SEO and social media',
|
||||
type: 'text',
|
||||
}),
|
||||
defineField({
|
||||
name: 'logo',
|
||||
title: 'Logo',
|
||||
type: 'image',
|
||||
}),
|
||||
defineField({
|
||||
name: 'favicon',
|
||||
title: 'Favicon',
|
||||
type: 'image',
|
||||
}),
|
||||
defineField({
|
||||
name: 'footer',
|
||||
title: 'Copyright',
|
||||
initialValue: '© {YEAR} . All rights reserved.',
|
||||
type: 'string',
|
||||
}),
|
||||
],
|
||||
})
|
||||
14
template/apps/studio/static/manifest.webmanifest
Normal file
14
template/apps/studio/static/manifest.webmanifest
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "/favicon-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
template/apps/studio/tsconfig.json
Normal file
17
template/apps/studio/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user