feat: add sanity
This commit is contained in:
parent
949a904de7
commit
5ee230ba5f
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -33,3 +33,12 @@ yarn-error.log*
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# sanity
|
||||||
|
/dist
|
||||||
|
.sanity/
|
||||||
|
|
||||||
|
# env
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
10368
package-lock.json
generated
10368
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
|
@ -12,6 +12,9 @@
|
||||||
"@heroicons/react": "^2.0.18",
|
"@heroicons/react": "^2.0.18",
|
||||||
"@radix-ui/react-context-menu": "^2.1.3",
|
"@radix-ui/react-context-menu": "^2.1.3",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
"@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/node": "20.2.1",
|
||||||
"@types/react": "18.2.6",
|
"@types/react": "18.2.6",
|
||||||
"@types/react-dom": "18.2.4",
|
"@types/react-dom": "18.2.4",
|
||||||
|
@ -22,13 +25,20 @@
|
||||||
"eslint-config-next": "13.4.3",
|
"eslint-config-next": "13.4.3",
|
||||||
"lucide-react": "^0.220.0",
|
"lucide-react": "^0.220.0",
|
||||||
"next": "13.4.3",
|
"next": "13.4.3",
|
||||||
|
"next-sanity": "^4.3.3",
|
||||||
"postcss": "8.4.23",
|
"postcss": "8.4.23",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"sanity": "^3.11.2",
|
||||||
|
"styled-components": "^5.3.10",
|
||||||
"tailwind-merge": "^1.12.0",
|
"tailwind-merge": "^1.12.0",
|
||||||
"tailwindcss": "3.3.2",
|
"tailwindcss": "3.3.2",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
"tailwindcss-hero-patterns": "^0.1.2",
|
"tailwindcss-hero-patterns": "^0.1.2",
|
||||||
|
"twemoji": "^14.0.2",
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/twemoji": "^13.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10
sanity.cli.ts
Normal file
10
sanity.cli.ts
Normal 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
25
sanity.config.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* This configuration is used to for the Sanity Studio that’s 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
22
sanity/env.ts
Normal 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
10
sanity/lib/client.ts
Normal 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
13
sanity/lib/image.ts
Normal 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
11
sanity/schema.ts
Normal 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
57
sanity/schemas/author.ts
Normal 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
75
sanity/schemas/blockContent.ts
Normal file
75
sanity/schemas/blockContent.ts
Normal 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',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
19
sanity/schemas/category.ts
Normal file
19
sanity/schemas/category.ts
Normal 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
72
sanity/schemas/post.ts
Normal 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
77
sanity/schemas/project.ts
Normal 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}` };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
17
src/app/internal/studio/[[...index]]/page.tsx
Normal file
17
src/app/internal/studio/[[...index]]/page.tsx
Normal 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} />
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { Inter } from "next/font/google";
|
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"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
@ -9,14 +12,26 @@ export const metadata = {
|
||||||
"Web designer and developer working to bring accessible designs to the masses.",
|
"Web designer and developer working to bring accessible designs to the masses.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const latestThreeProjects = await client.fetch(
|
||||||
|
`*[_type == "project"] | order(_createdAt desc) [0...3]`
|
||||||
|
);
|
||||||
|
console.log(latestThreeProjects);
|
||||||
return (
|
return (
|
||||||
<html lang="en" className="h-full dark">
|
<html lang="en" className="dark">
|
||||||
<body className={`${inter.className} h-full`}>{children}</body>
|
<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>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,28 @@
|
||||||
import Background from "@/components/Background";
|
import Background from "@/components/Background";
|
||||||
|
import Button from "@/components/Button";
|
||||||
import Navbar from "@/components/Navbar";
|
import Navbar from "@/components/Navbar";
|
||||||
|
import Twemoji from "@/components/Twemoji";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<main className="relative w-full h-full">
|
<section className="flex items-center px-6 mx-auto h-2/3 max-w-7xl">
|
||||||
<Background />
|
<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'm Jack Merrill, a web designer and developer working to bring
|
||||||
|
accessible designs to the masses.
|
||||||
|
</h2>
|
||||||
|
|
||||||
<Navbar />
|
<div className="flex">
|
||||||
</main>
|
<Button link href="mailto:contact@jackmerrill.com">
|
||||||
|
Contact me
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,43 @@
|
||||||
export default function Button({
|
export default function Button({
|
||||||
rainbow,
|
rainbow = false,
|
||||||
|
link = false,
|
||||||
|
href,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
rainbow: boolean;
|
rainbow?: boolean;
|
||||||
|
link?: boolean;
|
||||||
|
href?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={`${rainbow ? "rainbow-btn" : ""}`}>
|
<>
|
||||||
|
{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">
|
<button className="px-3 py-2 font-bold text-black rounded-md bg-slate-400 dark:text-white dark:bg-gray-800">
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
"use client";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRef, useState, MouseEvent } from "react";
|
import { useRef, useState, MouseEvent } from "react";
|
||||||
|
@ -10,10 +11,17 @@ import {
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "./ui/context-menu";
|
} 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 (
|
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>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger>
|
<ContextMenuTrigger>
|
||||||
<div className="p-4 bg-gray-800 rounded-md w-14 h-14 animate-rainbow-outline">
|
<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>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</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">
|
<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">
|
||||||
<Link
|
<NavigationMenu.Root className="relative z-[1] flex w-full h-full justify-center">
|
||||||
href="/projects"
|
<NavigationMenu.List className="flex m-0 list-none center">
|
||||||
className="px-3 py-2 duration-150 rounded-full dark:hover:bg-gray-700 hover:bg-slate-400"
|
<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
|
Projects
|
||||||
</Link>
|
</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
|
<NavigationMenu.Item>
|
||||||
href="/about"
|
<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]">
|
||||||
className="px-3 py-2 duration-150 rounded-full dark:hover:bg-gray-700 hover:bg-slate-400"
|
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>
|
||||||
|
|
||||||
|
<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
|
About
|
||||||
</Link>
|
</NavigationMenu.Link>
|
||||||
|
</NavigationMenu.Item>
|
||||||
|
|
||||||
|
<NavigationMenu.Item>
|
||||||
<ThemeSelector />
|
<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>
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button rainbow={true}>Contact</Button>
|
<Button link rainbow href="mailto:contact@jackmerrill.com">
|
||||||
|
Contact
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
40
src/components/Twemoji.tsx
Normal file
40
src/components/Twemoji.tsx
Normal 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);
|
|
@ -74,10 +74,16 @@ module.exports = {
|
||||||
from: { height: "var(--radix-accordion-content-height)" },
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
to: { height: 0 },
|
to: { height: 0 },
|
||||||
},
|
},
|
||||||
|
"hand-wave": {
|
||||||
|
"0%, 100%": { transform: "rotate(0deg)" },
|
||||||
|
"20%, 60%": { transform: "rotate(25deg)" },
|
||||||
|
"40%, 80%": { transform: "rotate(0deg)" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
"hand-wave": "hand-wave 2s ease-in-out infinite",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "ES2017",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user