adding sanity components

This commit is contained in:
briannelson95
2025-09-30 19:40:25 -04:00
parent 3c0e27d7f2
commit f016b39e5c
32 changed files with 3062 additions and 8 deletions

1981
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"@types/node": "^22", "@types/node": "^24.6.0",
"eslint": "^9.22.0", "eslint": "^9.22.0",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0", "eslint-plugin-svelte": "^3.0.0",
@@ -36,5 +36,14 @@
"typescript": "^5.0.0", "typescript": "^5.0.0",
"typescript-eslint": "^8.20.0", "typescript-eslint": "^8.20.0",
"vite": "^7.0.4" "vite": "^7.0.4"
},
"dependencies": {
"@portabletext/svelte": "^3.0.1",
"@sanity/client": "^7.11.2",
"@sanity/image-url": "^1.2.0",
"@sanity/vision": "^4.10.2",
"groq": "^4.10.2",
"sanity": "^4.10.2",
"zod": "^4.1.11"
} }
} }

23
sanity.config.ts Normal file
View File

@@ -0,0 +1,23 @@
import { defineConfig } from "sanity";
import {structureTool} from 'sanity/structure';
import {visionTool} from '@sanity/vision';
import schemas from './schemas'
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!;
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!;
export default defineConfig({
name: "default",
title: 'briannelson.dev',
basePath: "/admin",
projectId,
dataset,
plugins: [
structureTool(),
visionTool()
],
schema: {types: schemas}
})

View File

@@ -0,0 +1,57 @@
import {ComposeIcon} from '@sanity/icons'
export default {
name: 'blogs',
title: 'Blogs',
type: 'document',
icon: ComposeIcon,
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96,
},
},
{
name: 'description',
title: 'Description',
type: 'text',
description: 'Enter a short snippit of the blog'
},
{
name: 'mainImage',
title: 'Main Image',
type: 'image',
options: [
{
hotspot: true
}
]
},
{
name: 'Categories',
title: 'categories',
type: 'array',
of: [{type: 'reference', to: {type: 'categories'}}]
},
{
name: 'publishedAt',
title: 'Published at',
type: 'datetime'
},
{
name: 'body',
title: 'Body',
type: 'blockContent'
}
],
}

View File

@@ -0,0 +1,20 @@
import { defineField, defineType } from 'sanity';
export const categories = defineType({
name: 'categories',
title: 'Categories',
type: 'document',
fields: [
defineField({
name: 'name',
title: 'Name',
type: 'string',
validation: Rule => Rule.required()
}),
defineField({
name: 'description',
title: 'Description',
type: 'text'
}),
],
})

View File

@@ -0,0 +1,60 @@
import { defineField, defineType } from "sanity";
export const frontend = defineType({
name: 'frontend',
title: 'Frontend Mentor',
type: 'document',
fieldsets: [
{
name: 'options',
options: {
collapsible: true,
collapsed: false
}
}
],
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96
},
}),
defineField({
name: 'qrCode',
title: 'QR Code Component',
type: 'qrCode',
fieldset: 'options'
}),
defineField({
name: 'resultsSummary',
title: 'Results Summary',
type: 'resultsSum',
fieldset: 'options'
}),
defineField({
name: 'productPrev',
title: 'Product Preview',
type: 'productInfo',
fieldset: 'options'
}),
defineField({
name: 'news',
type: 'newsData',
fieldset: 'options'
}),
defineField({
name: 'launch',
type: 'launchDate',
fieldset: 'options'
}),
],
})

View File

@@ -0,0 +1,68 @@
import { defineField, defineType } from "sanity";
export const homepage = defineType({
name: 'homepage',
title: 'Homepage',
type: 'document',
fieldsets: [
{
name: 'seo',
title: 'Metadata & SEO',
description: 'Use these fields to override the default metadata',
options: {
collapsible: true,
collapsed: true,
}
},
{
name: 'title',
title: 'Page Title'
}
],
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
fieldset: 'title'
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96
},
fieldset: 'title'
}),
defineField({
name: 'body',
title: 'Body',
type: 'blockContent'
}),
defineField({
name: 'cta',
title: 'Call to Action',
type: 'cta',
}),
defineField({
name: 'tech',
title: "Tech",
type: 'array',
of: [
{
type: 'reference',
to: [
{type: 'technologies'}
]
}
]
}),
defineField({
name: 'seo',
type: 'seo',
fieldset: 'seo'
})
],
})

View File

@@ -0,0 +1,34 @@
import { defineField, defineType, validateBasePaths } from 'sanity';
import { ImagesIcon } from '@sanity/icons'
export const mediaLibrary = defineType({
name: 'media',
title: 'Media Library',
type: 'document',
icon: ImagesIcon,
fields: [
defineField({
name: 'image',
title: 'Image',
type: 'image',
options: {
hotspot: true
}
}),
defineField({
name: 'alt',
title: 'Alternative Text',
type: 'string',
validation: (Rule) => Rule.required().max(250),
description: 'This is used for SEO and accessibility',
}),
],
preview: {
select: {
title: 'alt',
url: 'image.asset.url',
media: 'image', //adding this makes the image show up in standard preview components
},
},
})

View File

@@ -0,0 +1,12 @@
export default {
name: 'metadata',
title: 'Metadata',
type: 'document',
fields: [
{
name: 'customTitle',
title: 'Custom Title',
type: 'string',
},
],
}

View File

@@ -0,0 +1,21 @@
export default {
name: 'navigation',
title: 'Navigation',
type: 'document',
fields: [
{
name: 'navigation',
title: 'Navigation Menu',
type: 'array',
of: [
{
type: 'reference',
to: [
{type: 'pages'},
{type: 'homepage'}
]
}
]
},
],
}

View File

@@ -0,0 +1,43 @@
import MyPreviewComponent from '@/components/MyPreviewComponent';
import { defineField, defineType } from 'sanity';
export const newsPost = defineType({
name: 'newsPost',
title: 'News Posts',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string'
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96
}
}),
defineField({
name: 'image',
type: 'reference',
to: {
type: 'media',
components: {
preview: MyPreviewComponent
}
},
}),
defineField({
name: 'previewText',
title: 'Preview Text',
type: 'text'
}),
defineField({
name: 'category',
type: 'string'
})
],
})

View File

@@ -0,0 +1,50 @@
import { defineField, defineType } from "sanity";
export const pages = defineType({
name: 'pages',
title: 'Pages',
type: 'document',
fieldsets: [
{
name: 'seo',
title: 'Metadata & SEO',
description: 'Use these fields to override the default metadata',
options: {
collapsible: true,
collapsed: true,
}
},
{
name: 'title',
title: 'Page Title'
}
],
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
fieldset: 'title'
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96
},
fieldset: 'title'
}),
defineField({
name: 'body',
title: 'Body',
type: 'blockContent'
}),
defineField({
name: 'seo',
type: 'seo',
fieldset: 'seo'
})
],
})

View File

@@ -0,0 +1,128 @@
import { defineField, defineType } from "sanity";
import {Stack, Card, Flex } from '@sanity/ui'
import { RocketIcon } from '@sanity/icons'
function MyPreviewComponent(props: any) {
const {title, url} = props
return (
<Flex align="center" justify="center" height="fill">
<Card border padding={3}>
<Stack space={3} marginBottom={3}>
<img src={url} alt={title} style={{width: '100%'}} />
</Stack>
</Card>
</Flex>
)
}
export const projects = defineType({
name: 'projects',
title: 'Projects',
type: 'document',
icon: RocketIcon,
fieldsets: [
{
name: 'title',
title: 'Title'
},
{
name: 'projectLinks',
title: 'Project Links'
}
],
fields: [
defineField({
name: 'title',
title: 'Project Title',
type: 'string',
fieldset: 'title'
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96
},
fieldset: 'title'
}),
defineField({
name: 'order',
title: 'Order',
type: 'number',
fieldset: 'title',
}),
defineField({
name: 'description',
title: 'Description',
type: 'text',
rows: 5
}),
defineField({
name: 'featured',
title: 'Featured',
type: 'boolean',
initialValue: false,
fieldset: 'title'
}),
defineField({
name: 'body',
title: 'Body',
type: 'blockContent',
}),
defineField({
name: 'image',
title: 'Main Image',
type: 'reference',
to: {
type: 'media',
components: {
preview: MyPreviewComponent
}
},
}),
defineField({
name: 'link',
title: 'Link to Project',
type: 'url',
fieldset: 'projectLinks'
}),
defineField({
name: 'github',
title: 'GitHub Reop',
type: 'url',
fieldset: 'projectLinks'
}),
defineField({
name: 'tags',
title: 'Tags',
type: 'array',
of: [
{
type: 'reference',
to: [
{type: 'categories'}
]
}
]
}),
defineField({
name: 'stats',
title: 'Statistics',
type: 'array',
of: [
{
type: 'stats',
}
]
}),
],
preview: {
select: {
title: 'title',
media: 'image'
}
}
})

View File

@@ -0,0 +1,27 @@
export default {
name: 'siteSettings',
title: 'Site Settings',
type: 'document',
fields: [
{
name: 'siteTitle',
title: 'Site Title',
type: 'string',
},
{
name: 'contact',
type: 'contact'
},
{
name: 'logo',
title: 'Logo',
type: 'image'
},
{
name: 'favicon',
title: 'Favicon',
type: 'image',
description: 'This is used for the tab favicon as well as the mobile menu'
},
],
}

View File

@@ -0,0 +1,25 @@
import MyPreviewComponent from "@/components/MyPreviewComponent";
import { defineField, defineType } from "sanity";
export const technologies = defineType({
name: 'technologies',
title: 'Technologies',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
}),
defineField({
name: 'image',
type: 'reference',
to: {
type: 'media',
components: {
preview: MyPreviewComponent
}
},
}),
],
})

5
schemas/index.ts Normal file
View File

@@ -0,0 +1,5 @@
import { pages } from "./documents/pages";
export default [
pages
]

View File

@@ -0,0 +1,51 @@
export default {
name: 'blockContent',
title: 'Body',
type: 'array',
of: [
{
title: 'Block',
type: 'block',
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: 'Bullet', 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: 'Emphasis', value: 'em'},
],
// Annotations can be any object structure e.g. a link or a footnote.
annotations: [
{
title: 'URL',
name: 'link',
type: 'object',
fields: [
{
title: 'URL',
name: 'href',
type: 'url',
},
],
},
],
},
},
{
title: 'Image',
type: 'image'
},
{
type: 'code'
}
],
}

View File

@@ -0,0 +1,43 @@
export default {
name: 'contact',
title: 'Contact Info',
type: 'object',
fields: [
{
name: 'email',
title: 'Main Email',
type: 'string',
validation: (Rule: any) =>
Rule.regex(
/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/,
{
name: 'Email',
invert: false
}
)
},
{
name: 'phone',
title: 'Main Phone Number',
type: 'string',
validation: (Rule: any) =>
Rule.regex(
/^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/,
{
name: 'Phone',
invert: false
}
)
},
{
name: 'socials',
title: 'Socials',
type: 'array',
of: [
{name: 'facebook', title: 'Facebook', type: 'url'},
{name: 'twitter', title: 'Twitter', type: 'url'},
{name: 'instagram', title: 'Instagram', type: 'url'},
]
}
],
}

25
schemas/objects/cta.ts Normal file
View File

@@ -0,0 +1,25 @@
import { defineField, defineType } from 'sanity';
export const cta = defineType({
name: 'cta',
title: 'Call to Action',
type: 'object',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
validation: Rule => Rule.max(30).error('Keep this short')
}),
defineField({
name: 'link',
title: 'Link',
type: 'reference',
to: [
{type: 'pages'},
{type: 'projects'}
],
description: 'Keep your CTA to an internal page for SEO best practices'
})
],
})

View File

@@ -0,0 +1,17 @@
import { defineField, defineType } from 'sanity';
export const launchDate = defineType({
name: 'launchDate',
title: 'Launch Date',
type: 'object',
fields: [
defineField({
name: 'launchAt',
title: 'Launch At',
type: 'datetime'
})
],
options: {
collapsed: true
}
})

View File

@@ -0,0 +1,33 @@
import { defineField, defineType } from 'sanity';
export const newsData = defineType({
name: 'newsData',
title: 'News',
type: 'object',
fields: [
defineField({
name: 'nav',
title: 'Navigation',
type: 'array',
of: [
{type: 'string'}
]
}),
defineField({
name: 'posts',
title: 'Posts',
type: 'array',
of: [
{
type: 'reference',
to: [
{type: 'newsPost'}
]
}
]
})
],
options: {
collapsed: true
}
})

View File

@@ -0,0 +1,66 @@
import MyPreviewComponent from '@/components/MyPreviewComponent';
import { defineField, defineType } from 'sanity';
export const productInfo = defineType({
name: 'productInfo',
title: 'Product Info',
type: 'object',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string'
}),
defineField({
name: 'mobileImage',
type: 'reference',
to: {
type: 'media',
components: {
preview: MyPreviewComponent
}
},
}),
defineField({
name: 'deskImage',
type: 'reference',
to: {
type: 'media',
components: {
preview: MyPreviewComponent
}
},
}),
defineField({
name: 'price',
title: 'Price',
type: 'object',
fields: [
defineField({
name: 'ogPrice',
title: 'Original Price',
type: 'number'
}),
defineField({
name: 'newPrice',
title: 'New Price',
type: 'number'
}),
]
}),
defineField({
name: 'category',
title: 'Category',
type: 'string'
// I would make this a reference if there was more products
}),
defineField({
name: 'desc',
title: 'Description',
type: 'text'
})
],
options: {
collapsed: true
}
})

View File

@@ -0,0 +1,37 @@
import { defineField, defineType } from 'sanity';
import MyPreviewComponent from '../../../components/MyPreviewComponent'
export const qrCode = defineType({
name: 'qrCode',
title: 'QR Code',
type: 'object',
fields: [
defineField({
name: 'image',
type: 'reference',
to: {
type: 'media',
components: {
preview: MyPreviewComponent
}
},
}),
defineField({
name: 'text',
type: 'object',
fields: [
defineField({
name: 'title',
type: 'string',
}),
defineField({
name: 'subtitle',
type: 'string',
}),
]
})
],
options: {
collapsed: true
}
})

View File

@@ -0,0 +1,52 @@
import { defineField, defineType } from 'sanity';
export const resultsSum = defineType({
name: 'resultsSum',
title: 'Results Summary',
type: 'object',
fields: [
defineField({
name: 'allResults',
title: 'Results',
type: 'object',
fields: [
defineField({
name: 'resultArr',
type: 'array',
of: [
{
type: 'scores'
}
]
})
]
}),
defineField({
name: 'reaction',
title: 'Reaction',
type: 'number',
validation: Rule => Rule.max(100)
}),
defineField({
name: 'memory',
title: 'Memory',
type: 'number',
validation: Rule => Rule.max(100)
}),
defineField({
name: 'verbal',
title: 'Verbal',
type: 'number',
validation: Rule => Rule.max(100)
}),
defineField({
name: 'visual',
title: 'Visual',
type: 'number',
validation: Rule => Rule.max(100)
}),
],
options: {
collapsed: true
}
})

View File

@@ -0,0 +1,29 @@
import { defineField, defineType } from 'sanity';
export const scores = defineType({
name: 'scores',
title: 'Scores',
type: 'object',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string'
}),
defineField({
name: 'score',
title: 'Score',
type: 'number',
validation: Rule => Rule.max(100)
})
],
options: {
columns: 2
},
preview: {
select: {
title: 'score',
subtitle: 'title'
}
}
})

30
schemas/objects/seo.ts Normal file
View File

@@ -0,0 +1,30 @@
import { defineField, defineType } from "sanity";
export const seo = defineType({
name: 'seo',
title: 'SEO',
type: 'object',
fields: [
defineField({
name: 'title',
title: 'Meta Title',
type: 'string'
}),
defineField({
name: 'description',
title: 'Meta Description',
type: 'text'
}),
defineField({
name: 'keywords',
title: 'Keywords',
type: 'array',
of: [{type: 'string'}]
}),
defineField({
name: 'image',
title: 'Preview Image',
type: 'image'
})
]
})

28
schemas/objects/stats.ts Normal file
View File

@@ -0,0 +1,28 @@
import { defineField, defineType } from 'sanity';
export const stats = defineType({
name: 'stats',
title: 'Statistics',
type: 'object',
fields: [
defineField({
name: 'number',
title: 'Number',
type: 'number'
}),
defineField({
name: 'title',
title: 'Title',
type: 'string'
}),
],
options: {
columns: 2
},
preview: {
select: {
title: 'number',
subtitle: 'title'
}
}
})

View File

@@ -0,0 +1,5 @@
<script lang="ts">
import {PortableText} from '@portabletext/svelte'
export let value: any[] = []
</script>
<PortableText value={value} />

View File

@@ -0,0 +1,9 @@
<script lang="ts">
import {urlFor} from "$lib/sanity/image"
export let image: any
export const alt: string = ""
const base = urlFor(image).auto('format')
const src = base.width(1200).url()
const srcset = [400, 800, 1200, 1600].map(w => `${base.width(w).url()} ${w}w`).join(', ')
</script>
<img {alt} {src} {srcset} sizes="(min-width: 800px) 800px, 100vw" loading="lazy" />

8
src/lib/sanity/client.ts Normal file
View File

@@ -0,0 +1,8 @@
import {createClient} from '@sanity/client';
export const sanity = createClient({
projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID,
dataset: import.meta.env.PUBLIC_SANITY_DATASET,
apiVersion: '2025-09-01',
useCdn: true,
})

6
src/lib/sanity/image.ts Normal file
View File

@@ -0,0 +1,6 @@
import imageUrlBuilder from '@sanity/image-url';
import type {Image} from 'sanity';
import {sanity} from './client';
const builder = imageUrlBuilder(sanity);
export const urlFor = (src: Image) => builder.image(src);

66
src/lib/sanity/queries.ts Normal file
View File

@@ -0,0 +1,66 @@
import groq from "groq";
export const homepage = groq`{
"pageData": *[_type == 'homepage']{
...,
tech[]-> {
title,
image->{
alt,
image
}
},
cta{
link->{slug},
title
},
title,
},
"projects": *[_type == 'projects'] | order(order asc)[0...6]{
description,
featured,
github,
image->{
alt,
image
},
link,
slug,
tags[]->{
name
},
title
}
}`
export const projectList = groq`
*[_type == 'projects'] | order(order asc){
description,
featured,
github,
image->{
alt,
image
},
link,
slug,
tags[]->{
name
},
title
}
`
export const singleProject = groq`
*[_type == 'projects' && slug.current == $slug][0]{
...,
image->{
alt,
image
},
tags[]->{
name
},
}
`