feat: add sanity

This commit is contained in:
Jack 2023-05-24 23:49:48 -05:00
parent 949a904de7
commit 5ee230ba5f
No known key found for this signature in database
GPG Key ID: AFD9F14834097B8E
23 changed files with 17065 additions and 2684 deletions

9
.gitignore vendored
View File

@ -33,3 +33,12 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# sanity
/dist
.sanity/
# env
.env
.env.local
.env.development.local

10368
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,9 @@
"@heroicons/react": "^2.0.18",
"@radix-ui/react-context-menu": "^2.1.3",
"@radix-ui/react-dropdown-menu": "^2.0.4",
"@radix-ui/react-navigation-menu": "^1.1.2",
"@sanity/image-url": "^1.0.2",
"@sanity/vision": "^3.11.2",
"@types/node": "20.2.1",
"@types/react": "18.2.6",
"@types/react-dom": "18.2.4",
@ -22,13 +25,20 @@
"eslint-config-next": "13.4.3",
"lucide-react": "^0.220.0",
"next": "13.4.3",
"next-sanity": "^4.3.3",
"postcss": "8.4.23",
"react": "18.2.0",
"react-dom": "18.2.0",
"sanity": "^3.11.2",
"styled-components": "^5.3.10",
"tailwind-merge": "^1.12.0",
"tailwindcss": "3.3.2",
"tailwindcss-animate": "^1.0.5",
"tailwindcss-hero-patterns": "^0.1.2",
"twemoji": "^14.0.2",
"typescript": "5.0.4"
},
"devDependencies": {
"@types/twemoji": "^13.1.2"
}
}

10
sanity.cli.ts Normal file
View File

@ -0,0 +1,10 @@
/**
* This configuration file lets you run `$ sanity [command]` in this folder
* Go to https://www.sanity.io/docs/cli to learn more.
**/
import { defineCliConfig } from 'sanity/cli'
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET
export default defineCliConfig({ api: { projectId, dataset } })

25
sanity.config.ts Normal file
View File

@ -0,0 +1,25 @@
/**
* This configuration is used to for the Sanity Studio thats mounted on the `/app/internal/studio/[[...index]]/page.tsx` route
*/
import {visionTool} from '@sanity/vision'
import {defineConfig} from 'sanity'
import {deskTool} from 'sanity/desk'
// Go to https://www.sanity.io/docs/api-versioning to learn how API versioning works
import {apiVersion, dataset, projectId} from './sanity/env'
import {schema} from './sanity/schema'
export default defineConfig({
basePath: '/internal/studio',
projectId,
dataset,
// Add and edit the content schema in the './sanity/schema' folder
schema,
plugins: [
deskTool(),
// Vision is a tool that lets you query your content with GROQ in the studio
// https://www.sanity.io/docs/the-vision-plugin
visionTool({defaultApiVersion: apiVersion}),
],
})

22
sanity/env.ts Normal file
View File

@ -0,0 +1,22 @@
export const apiVersion =
process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2023-05-25'
export const dataset = assertValue(
process.env.NEXT_PUBLIC_SANITY_DATASET,
'Missing environment variable: NEXT_PUBLIC_SANITY_DATASET'
)
export const projectId = assertValue(
process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
'Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_ID'
)
export const useCdn = false
function assertValue<T>(v: T | undefined, errorMessage: string): T {
if (v === undefined) {
throw new Error(errorMessage)
}
return v
}

10
sanity/lib/client.ts Normal file
View File

@ -0,0 +1,10 @@
import { createClient } from 'next-sanity'
import { apiVersion, dataset, projectId, useCdn } from '../env'
export const client = createClient({
apiVersion,
dataset,
projectId,
useCdn,
})

13
sanity/lib/image.ts Normal file
View File

@ -0,0 +1,13 @@
import createImageUrlBuilder from '@sanity/image-url'
import type { Image } from 'sanity'
import { dataset, projectId } from '../env'
const imageBuilder = createImageUrlBuilder({
projectId: projectId || '',
dataset: dataset || '',
})
export const urlForImage = (source: Image) => {
return imageBuilder?.image(source).auto('format').fit('max')
}

11
sanity/schema.ts Normal file
View File

@ -0,0 +1,11 @@
import { type SchemaTypeDefinition } from "sanity";
import blockContent from "./schemas/blockContent";
import category from "./schemas/category";
import post from "./schemas/post";
import author from "./schemas/author";
import project from "./schemas/project";
export const schema: { types: SchemaTypeDefinition[] } = {
types: [post, author, category, blockContent, project],
};

57
sanity/schemas/author.ts Normal file
View File

@ -0,0 +1,57 @@
import {defineField, defineType} from 'sanity'
export default defineType({
name: 'author',
title: 'Author',
type: 'document',
fields: [
defineField({
name: 'name',
title: 'Name',
type: 'string',
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'name',
maxLength: 96,
},
}),
defineField({
name: 'image',
title: 'Image',
type: 'image',
options: {
hotspot: true,
},
fields: [
{
name: 'alt',
type: 'string',
title: 'Alternative Text',
}
]
}),
defineField({
name: 'bio',
title: 'Bio',
type: 'array',
of: [
{
title: 'Block',
type: 'block',
styles: [{title: 'Normal', value: 'normal'}],
lists: [],
},
],
}),
],
preview: {
select: {
title: 'name',
media: 'image',
},
},
})

View File

@ -0,0 +1,75 @@
import {defineType, defineArrayMember} from 'sanity'
/**
* This is the schema type for block content used in the post document type
* Importing this type into the studio configuration's `schema` property
* lets you reuse it in other document types 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 define what blocks can be marked up as. The default
// set corresponds 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: 'Bullet', value: 'bullet'}],
// Marks let you mark up inline text in the Portable Text Editor
marks: {
// Decorators usually describe a single property e.g. a typographic
// preference or highlighting
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',
},
],
},
],
},
}),
// 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({
type: 'image',
options: {hotspot: true},
fields: [
{
name: 'alt',
type: 'string',
title: 'Alternative Text',
}
]
}),
],
})

View File

@ -0,0 +1,19 @@
import {defineField, defineType} from 'sanity'
export default defineType({
name: 'category',
title: 'Category',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
}),
defineField({
name: 'description',
title: 'Description',
type: 'text',
}),
],
})

72
sanity/schemas/post.ts Normal file
View File

@ -0,0 +1,72 @@
import {defineField, defineType} from 'sanity'
export default defineType({
name: 'post',
title: 'Post',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96,
},
}),
defineField({
name: 'author',
title: 'Author',
type: 'reference',
to: {type: 'author'},
}),
defineField({
name: 'mainImage',
title: 'Main image',
type: 'image',
options: {
hotspot: true,
},
fields: [
{
name: 'alt',
type: 'string',
title: 'Alternative Text',
}
]
}),
defineField({
name: 'categories',
title: 'Categories',
type: 'array',
of: [{type: 'reference', to: {type: 'category'}}],
}),
defineField({
name: 'publishedAt',
title: 'Published at',
type: 'datetime',
}),
defineField({
name: 'body',
title: 'Body',
type: 'blockContent',
}),
],
preview: {
select: {
title: 'title',
author: 'author.name',
media: 'mainImage',
},
prepare(selection) {
const {author} = selection
return {...selection, subtitle: author && `by ${author}`}
},
},
})

77
sanity/schemas/project.ts Normal file
View File

@ -0,0 +1,77 @@
import { defineField, defineType } from "sanity";
export default defineType({
name: "project",
title: "Project",
type: "document",
fields: [
defineField({
name: "title",
title: "Title",
type: "string",
}),
defineField({
name: "subtitle",
title: "Subtitle",
type: "string",
}),
defineField({
name: "slug",
title: "Slug",
type: "slug",
options: {
source: "title",
maxLength: 96,
},
}),
defineField({
name: "author",
title: "Author",
type: "reference",
to: { type: "author" },
}),
defineField({
name: "mainImage",
title: "Main image",
type: "image",
options: {
hotspot: true,
},
fields: [
{
name: "alt",
type: "string",
title: "Alternative Text",
},
],
}),
defineField({
name: "categories",
title: "Categories",
type: "array",
of: [{ type: "reference", to: { type: "category" } }],
}),
defineField({
name: "publishedAt",
title: "Published at",
type: "datetime",
}),
defineField({
name: "body",
title: "Body",
type: "blockContent",
}),
],
preview: {
select: {
title: "title",
author: "author.name",
media: "mainImage",
},
prepare(selection) {
const { author } = selection;
return { ...selection, subtitle: author && `by ${author}` };
},
},
});

View File

@ -0,0 +1,17 @@
'use client'
/**
* This route is responsible for the built-in authoring environment using Sanity Studio.
* All routes under your studio path is handled by this file using Next.js' catch-all routes:
* https://nextjs.org/docs/routing/dynamic-routes#catch-all-routes
*
* You can learn more about the next-sanity package here:
* https://github.com/sanity-io/next-sanity
*/
import { NextStudio } from 'next-sanity/studio'
import config from '../../../../../sanity.config'
export default function StudioPage() {
return <NextStudio config={config} />
}

View File

@ -1,5 +1,8 @@
import "./globals.css";
import { Inter } from "next/font/google";
import Background from "@/components/Background";
import Navbar from "@/components/Navbar";
import { client } from "../../sanity/lib/client";
const inter = Inter({ subsets: ["latin"] });
@ -9,14 +12,26 @@ export const metadata = {
"Web designer and developer working to bring accessible designs to the masses.",
};
export default function RootLayout({
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const latestThreeProjects = await client.fetch(
`*[_type == "project"] | order(_createdAt desc) [0...3]`
);
console.log(latestThreeProjects);
return (
<html lang="en" className="h-full dark">
<body className={`${inter.className} h-full`}>{children}</body>
<html lang="en" className="dark">
<body
className={`${inter.className} min-h-[100vh] flex justify-between flex-col bg-white heropattern-wiggle-indigo-100 dark:bg-black dark:heropattern-wiggle-slate-900`}
>
<Navbar projects={latestThreeProjects} blogPosts={{}} />
<main className="w-full min-h-full">{children}</main>
<footer></footer>
</body>
</html>
);
}

View File

@ -1,13 +1,28 @@
import Background from "@/components/Background";
import Button from "@/components/Button";
import Navbar from "@/components/Navbar";
import Twemoji from "@/components/Twemoji";
import Image from "next/image";
export default function Home() {
return (
<main className="relative w-full h-full">
<Background />
<section className="flex items-center px-6 mx-auto h-2/3 max-w-7xl">
<div className="space-y-2 ">
<h1 className="flex items-center text-6xl font-bold gap-x-2">
<Twemoji emoji="👋" ext="svg" className="animate-hand-wave" /> Hey
hey!
</h1>
<h2 className="text-2xl font-semibold">
I&apos;m Jack Merrill, a web designer and developer working to bring
accessible designs to the masses.
</h2>
<Navbar />
</main>
<div className="flex">
<Button link href="mailto:contact@jackmerrill.com">
Contact me
</Button>
</div>
</div>
</section>
);
}

View File

@ -1,15 +1,43 @@
export default function Button({
rainbow,
rainbow = false,
link = false,
href,
children,
}: {
rainbow: boolean;
rainbow?: boolean;
link?: boolean;
href?: string;
children: React.ReactNode;
}) {
return (
<div className={`${rainbow ? "rainbow-btn" : ""}`}>
<button className="px-3 py-2 font-bold text-black rounded-md bg-slate-400 dark:text-white dark:bg-gray-800">
{children}
</button>
</div>
<>
{rainbow ? (
<div className="rainbow-btn">
{link ? (
<a
href={href}
className="px-3 py-3 font-bold text-black rounded-md bg-slate-400 dark:text-white dark:bg-gray-800"
>
{children}
</a>
) : (
<button className="px-3 py-2 font-bold text-black rounded-md bg-slate-400 dark:text-white dark:bg-gray-800">
{children}
</button>
)}
</div>
) : link ? (
<a
href={href}
className="px-3 py-3 font-bold text-black rounded-md bg-slate-400 dark:text-white dark:bg-gray-800"
>
{children}
</a>
) : (
<button className="px-3 py-2 font-bold text-black rounded-md bg-slate-400 dark:text-white dark:bg-gray-800">
{children}
</button>
)}
</>
);
}

View File

@ -1,3 +1,4 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useRef, useState, MouseEvent } from "react";
@ -10,10 +11,17 @@ import {
ContextMenuItem,
ContextMenuTrigger,
} from "./ui/context-menu";
import * as NavigationMenu from "@radix-ui/react-navigation-menu";
export default function Navbar() {
export default function Navbar({
projects,
blogPosts,
}: {
projects: any;
blogPosts: any;
}) {
return (
<nav className="flex items-center justify-between p-6">
<div className="flex items-center justify-between w-full p-6 mx-auto max-w-7xl">
<ContextMenu>
<ContextMenuTrigger>
<div className="p-4 bg-gray-800 rounded-md w-14 h-14 animate-rainbow-outline">
@ -27,27 +35,84 @@ export default function Navbar() {
</ContextMenuContent>
</ContextMenu>
<div className="flex items-center font-bold transition-all duration-150 rounded-full text-slate-900 dark:text-white bg-slate-300 dark:bg-gray-800 h-min hover:px-2">
<Link
href="/projects"
className="px-3 py-2 duration-150 rounded-full dark:hover:bg-gray-700 hover:bg-slate-400"
>
Projects
</Link>
<div className="flex items-center min-h-full font-bold transition-all duration-150 rounded-full text-slate-900 dark:text-white bg-slate-300 dark:bg-gray-800 hover:px-2">
<NavigationMenu.Root className="relative z-[1] flex w-full h-full justify-center">
<NavigationMenu.List className="flex m-0 list-none center">
<NavigationMenu.Item>
<NavigationMenu.Trigger className="hover:bg-gray-700 group flex select-none items-center justify-between gap-[2px] rounded-full h-full px-3 text-[15px] font-medium leading-none outline-none focus:shadow-[0_0_0_2px]">
Projects
</NavigationMenu.Trigger>
<NavigationMenu.Content className="data-[motion=from-start]:animate-enterFromLeft data-[motion=from-end]:animate-enterFromRight data-[motion=to-start]:animate-exitToLeft data-[motion=to-end]:animate-exitToRight absolute top-0 left-0 w-full sm:w-auto">
<ul className="m-0 grid list-none gap-x-[10px] p-[22px] sm:w-[500px] sm:grid-cols-[0.75fr_1fr]">
<li className="grid row-span-3">
<NavigationMenu.Link asChild>
<a
className="focus:shadow-violet7 from-purple9 to-indigo9 flex
h-full w-full select-none flex-col justify-end rounded-full bg-gradient-to-b p-[25px] no-underline outline-none focus:shadow-[0_0_0_2px]"
href="/"
>
<svg
aria-hidden
width="38"
height="38"
viewBox="0 0 25 25"
fill="white"
>
<path d="M12 25C7.58173 25 4 21.4183 4 17C4 12.5817 7.58173 9 12 9V25Z"></path>
<path d="M12 0H4V8H12V0Z"></path>
<path d="M17 8C19.2091 8 21 6.20914 21 4C21 1.79086 19.2091 0 17 0C14.7909 0 13 1.79086 13 4C13 6.20914 14.7909 8 17 8Z"></path>
</svg>
<div className="mt-4 mb-[7px] text-[18px] font-medium leading-[1.2] text-white">
Radix Primitives
</div>
<p className="text-mauve4 text-[14px] leading-[1.3]">
Unstyled, accessible components for React.
</p>
</a>
</NavigationMenu.Link>
</li>
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
<Link
href="/about"
className="px-3 py-2 duration-150 rounded-full dark:hover:bg-gray-700 hover:bg-slate-400"
>
About
</Link>
<NavigationMenu.Item>
<NavigationMenu.Trigger className="hover:bg-gray-700 h-full group flex select-none items-center justify-between gap-[2px] rounded-full px-3 py-2 text-[15px] font-medium leading-none outline-none focus:shadow-[0_0_0_2px]">
Blog
</NavigationMenu.Trigger>
<NavigationMenu.Content className="absolute top-0 left-0 w-full sm:w-auto">
<ul className="m-0 grid list-none gap-x-[10px] p-[22px] sm:w-[600px] sm:grid-flow-col sm:grid-rows-3"></ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
<ThemeSelector />
<NavigationMenu.Item>
<NavigationMenu.Link
className="hover:bg-gray-700 h-full items-center flex select-none rounded-full px-3 py-2 text-[15px] font-medium leading-none no-underline outline-none focus:shadow-[0_0_0_2px]"
href="https://github.com/radix-ui"
>
About
</NavigationMenu.Link>
</NavigationMenu.Item>
<NavigationMenu.Item>
<ThemeSelector />
</NavigationMenu.Item>
<NavigationMenu.Indicator className="data-[state=visible]:animate-fadeIn data-[state=hidden]:animate-fadeOut top-full z-[1] flex h-[10px] items-end justify-center overflow-hidden transition-[width,transform_250ms_ease]">
<div className="relative top-[70%] h-[10px] w-[10px] rotate-[45deg] rounded-tl-[2px] bg-white" />
</NavigationMenu.Indicator>
</NavigationMenu.List>
<div className="perspective-[2000px] absolute top-full left-0 flex w-full justify-center">
<NavigationMenu.Viewport className="data-[state=open]:animate-scaleIn data-[state=closed]:animate-scaleOut relative mt-[10px] h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] overflow-hidden rounded-[6px] bg-white dark:bg-gray-800 transition-[width,_height] duration-300 sm:w-[var(--radix-navigation-menu-viewport-width)]" />
</div>
</NavigationMenu.Root>
</div>
<div className="flex items-center">
<Button rainbow={true}>Contact</Button>
<Button link rainbow href="mailto:contact@jackmerrill.com">
Contact
</Button>
</div>
</nav>
</div>
);
}

View File

@ -0,0 +1,40 @@
import React from "react";
import NextImage from "next/image";
import twemoji from "twemoji";
const U200D = String.fromCharCode(0x200d);
const UFE0Fg = /\uFE0F/g;
function Twemoji({
emoji,
ext = "svg",
width = 72,
height = 72,
className,
}: {
emoji: string;
ext?: "svg" | "png";
width?: number;
height?: number;
className?: string;
}) {
const HEXCodePoint = twemoji.convert.toCodePoint(
emoji.indexOf(U200D) < 0 ? emoji.replace(UFE0Fg, "") : emoji
);
return (
<NextImage
src={`https://twemoji.maxcdn.com/v/latest/${
ext === "png" ? "72x72" : "svg"
}/${HEXCodePoint}.${ext}`}
width={width}
height={height}
alt={emoji}
loading="lazy"
draggable={false}
className={className}
/>
);
}
export default React.memo(Twemoji);

View File

@ -74,10 +74,16 @@ module.exports = {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
"hand-wave": {
"0%, 100%": { transform: "rotate(0deg)" },
"20%, 60%": { transform: "rotate(25deg)" },
"40%, 80%": { transform: "rotate(0deg)" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"hand-wave": "hand-wave 2s ease-in-out infinite",
},
},
},

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

8719
yarn.lock

File diff suppressed because it is too large Load Diff