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
|
||||
*.tsbuildinfo
|
||||
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",
|
||||
"@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
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 { 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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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'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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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" : ""}`}>
|
||||
<>
|
||||
{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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
<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
|
||||
</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
|
||||
href="/about"
|
||||
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 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>
|
||||
|
||||
<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
|
||||
</Link>
|
||||
</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>
|
||||
</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)" },
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
|
Loading…
Reference in New Issue
Block a user