Skip to content

Commit b639ea8

Browse files
committed
Add Case Study document type and pages
1 parent 595a96f commit b639ea8

File tree

10 files changed

+435
-4
lines changed

10 files changed

+435
-4
lines changed

studio/sanity.config.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import { codeInput } from '@sanity/code-input'
2-
import { BlockquoteIcon, TiersIcon } from '@sanity/icons'
2+
import { BlockquoteIcon, MasterDetailIcon, TiersIcon } from '@sanity/icons'
33
import { visionTool } from '@sanity/vision'
44
import {
55
defineConfig,
66
defineField,
77
defineType,
8-
useDocumentOperation,
98
type DocumentActionComponent,
109
type DocumentActionsContext,
10+
useDocumentOperation,
1111
} from 'sanity'
1212
import { structureTool } from 'sanity/structure'
1313

14-
import gallery from './schemas/gallery'
14+
import caseStudy from './schemas/case-study'
1515
import { inlineOnlyBlock } from './schemas/fields/inline'
16+
import gallery from './schemas/gallery'
1617
import note from './schemas/note'
1718
import page from './schemas/page'
1819
import photo from './schemas/photo'
@@ -33,6 +34,7 @@ const config = defineConfig({
3334

3435
schema: {
3536
types: [
37+
caseStudy,
3638
gallery,
3739
note,
3840
page,
@@ -64,7 +66,7 @@ const config = defineConfig({
6466
name: 'aside',
6567
title: 'Aside',
6668
type: 'object',
67-
// Icon: BlockquoteIcon,
69+
icon: MasterDetailIcon,
6870
fields: [
6971
defineField({
7072
name: 'content',

studio/schemas/case-study.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import type { PortableTextBlock } from '@portabletext/types'
2+
import { HighlightIcon, ImageIcon } from '@sanity/icons'
3+
import { defineArrayMember, defineField, defineType } from 'sanity'
4+
5+
import { serializePortableText } from '../utils/serialize-portable-text'
6+
import { decorators } from './fields/inline'
7+
import { ledeField } from './fields/lede'
8+
9+
interface PreviewArguments {
10+
lede: PortableTextBlock[]
11+
title: string
12+
image?: string
13+
}
14+
15+
export default defineType({
16+
name: 'caseStudy',
17+
title: 'Case study',
18+
icon: HighlightIcon,
19+
type: 'document',
20+
preview: {
21+
select: {
22+
image: 'images[0].image',
23+
title: 'title',
24+
lede: 'lede',
25+
},
26+
prepare({ image, title, lede }: PreviewArguments) {
27+
return {
28+
media: image,
29+
title,
30+
subtitle: serializePortableText(lede),
31+
}
32+
},
33+
},
34+
fields: [
35+
defineField({
36+
name: 'title',
37+
title: 'Title',
38+
type: 'string',
39+
validation: (rule) => rule.required(),
40+
}),
41+
defineField({
42+
name: 'slug',
43+
title: 'Slug',
44+
type: 'slug',
45+
options: {
46+
source: (document, context) => [document.title].join('-'),
47+
maxLength: 96,
48+
isUnique: async (value, context) => context.defaultIsUnique(value, context),
49+
},
50+
validation: (rule) => rule.required(),
51+
}),
52+
defineField({
53+
name: 'startDate',
54+
title: 'Start date',
55+
type: 'date',
56+
}),
57+
defineField({
58+
name: 'endDate',
59+
title: 'End date',
60+
type: 'date',
61+
}),
62+
defineField({
63+
name: 'discipline',
64+
title: 'Discipline',
65+
type: 'array',
66+
of: [defineArrayMember({ type: 'string' })],
67+
options: {
68+
layout: 'tags',
69+
},
70+
}),
71+
ledeField,
72+
defineField({
73+
name: 'content',
74+
title: 'Content',
75+
type: 'array',
76+
of: [
77+
defineArrayMember({ type: 'block', marks: { decorators } }),
78+
defineArrayMember({
79+
type: 'image',
80+
icon: ImageIcon,
81+
options: {
82+
storeOriginalFilename: true,
83+
metadata: ['blurhash', 'lqip', 'palette', 'exif'],
84+
hotspot: true,
85+
},
86+
fields: [
87+
defineField({
88+
name: 'caption',
89+
title: 'Caption',
90+
type: 'string',
91+
}),
92+
defineField({
93+
name: 'alt',
94+
title: 'Alt description',
95+
type: 'text',
96+
validation: (Rule) => Rule.required(),
97+
}),
98+
],
99+
}),
100+
defineArrayMember({ type: 'code' }),
101+
defineArrayMember({ type: 'blockquote' }),
102+
defineArrayMember({ type: 'aside' }),
103+
],
104+
}),
105+
],
106+
})

studio/schemas/page.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default defineType({
2121
title: 'Slug',
2222
type: 'slug',
2323
options: {
24+
source: (document, context) => [document.title].join('-'),
2425
maxLength: 96,
2526
isUnique: async (value, context) => context.defaultIsUnique(value, context),
2627
},

www/src/lib/sanity/case-studies.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import groq from 'groq'
2+
3+
import { client } from './client.js'
4+
5+
/**
6+
* @returns {Promise<import('./types.js').CaseStudiesQueryResult>}
7+
*/
8+
export async function getCaseStudies() {
9+
let caseStudiesQuery = groq`*[_type == "caseStudy" && defined(slug.current)] | order(_createdAt desc)`
10+
11+
return await client.fetch(caseStudiesQuery)
12+
}
13+
14+
/**
15+
* @param {string} slug
16+
* @returns {Promise<import('./types.js').CaseStudyQueryResult>}
17+
*/
18+
export async function getCaseStudy(slug) {
19+
let caseStudyQuery = groq`
20+
*[_type == "caseStudy" && slug.current == $slug]{
21+
...,
22+
'ledeClean': pt::text(lede),
23+
// 'coverImage': cover->image.asset,
24+
}[0]`
25+
26+
return await client.fetch(caseStudyQuery, { slug })
27+
}

www/src/routes/about/+page.svelte

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ export let data
3535
</FadeUp>
3636
</svelte:fragment>
3737
</PageHero>
38+
<!-- <PageSection>
39+
<div class="prose">
40+
<p>
41+
You can read more about some of the work I’ve done as <a href="/work/"
42+
>case studies</a
43+
>
44+
(or in <a href="/resume/">resume form</a> if you’re really interested). There’s
45+
also a page for side projects I’ve put out into the world and one for photos
46+
I’ve taken (in real life and video games).
47+
</p>
48+
<p>Thank you for visiting!</p>
49+
</div>
50+
</PageSection> -->
3851
<PageSection>
3952
<div class="prose flex flex-col gap-10">
4053
<Heading level={2}>Watching</Heading>

www/src/routes/work/+page.server.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { format } from 'date-fns'
2+
3+
import { getCaseStudies } from '$lib/sanity/case-studies.js'
4+
import { getPage } from '$lib/sanity/pages.js'
5+
6+
/** @type {import('./$types').PageServerLoad} */
7+
export const load = async () => {
8+
const page = await getPage('work')
9+
const cases = await getCaseStudies()
10+
11+
for (const item of cases) {
12+
item.startDateNice = format(new Date(item.startDate), 'MMMM yyyy')
13+
item.endDateNice = format(new Date(item.endDate), 'MMMM yyyy')
14+
}
15+
16+
return {
17+
...page,
18+
cases,
19+
}
20+
}

www/src/routes/work/+page.svelte

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script>
2+
import FadeUp from '$lib/components/helpers/fade-up.svelte'
3+
import PageIntro from '$lib/components/page-intro.svelte'
4+
import PageSection from '$lib/components/page-section.svelte'
5+
import PortableText from '$lib/components/portable-text.svelte'
6+
import TextLede from '$lib/components/text-lede.svelte'
7+
8+
import WorkBlock from './work-block.svelte'
9+
10+
/** @type {import('./$types').PageData} */
11+
export let data
12+
13+
$: ({ title, lede, content, cases } = data)
14+
</script>
15+
16+
<svelte:head>
17+
<title>{title} — Tidal Theory</title>
18+
<meta name="twitter:card" content="summary" />
19+
<meta property="og:title" content="{title} — Tidal Theory" />
20+
</svelte:head>
21+
22+
<article>
23+
<PageIntro>
24+
{title}
25+
<FadeUp slot="intro" let:intersecting showing={intersecting} delay={100}>
26+
<TextLede><PortableText value={lede} /></TextLede>
27+
</FadeUp>
28+
</PageIntro>
29+
{#if content}
30+
<PageSection>
31+
<div class="prose prose-invert">
32+
<PortableText value={content} />
33+
</div>
34+
</PageSection>
35+
{/if}
36+
<PageSection>
37+
<div class="flex flex-col gap-16 md:gap-20">
38+
{#if cases.length > 0}
39+
{#each cases as work}
40+
<WorkBlock {work} />
41+
{/each}
42+
{:else}
43+
<div class="prose">
44+
<p>No case studies entered yet.</p>
45+
</div>
46+
{/if}
47+
</div>
48+
</PageSection>
49+
</article>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { format } from 'date-fns'
2+
import Prism from 'prismjs'
3+
4+
import { getCaseStudy } from '$lib/sanity/case-studies.js'
5+
6+
/** @type {import('./$types').PageServerLoad} */
7+
export const load = async ({ params }) => {
8+
const post = await getCaseStudy(params.slug)
9+
10+
post.startDateNice = format(new Date(post.startDate), 'MMMM yyyy')
11+
post.endDateNice = format(new Date(post.endDate), 'MMMM yyyy')
12+
13+
for (const block of post.content) {
14+
if (block._type === 'code') {
15+
/**
16+
* @todo Maybe look into highlightjs for "server-side" line
17+
* highlighting (Prism plugin only works on DOM).
18+
*/
19+
block.code = Prism.highlight(
20+
block.code,
21+
Prism.languages[block.language],
22+
block.language,
23+
)
24+
}
25+
}
26+
27+
return {
28+
...post,
29+
createdAt: format(new Date(post._createdAt), 'dd MMMM yyyy'),
30+
}
31+
}

0 commit comments

Comments
 (0)