feat: add project intercept modal

This commit is contained in:
Jack Merrill 2023-06-07 01:04:52 -05:00
parent d16dbde171
commit 22214f0d2d
No known key found for this signature in database
GPG Key ID: B8E3CDF57DD80CA5
11 changed files with 1297 additions and 18 deletions

View File

@ -11,17 +11,20 @@
"dependencies": {
"@heroicons/react": "^2.0.18",
"@radix-ui/react-context-menu": "^2.1.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.4",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-navigation-menu": "^1.1.2",
"@sanity/image-url": "^1.0.2",
"@sanity/vision": "^3.11.2",
"@tailwindcss/typography": "^0.5.9",
"@types/node": "20.2.1",
"@types/react": "18.2.6",
"@types/react-dom": "18.2.4",
"autoprefixer": "10.4.14",
"class-variance-authority": "^0.6.0",
"clsx": "^1.2.1",
"easymde": "2",
"eslint": "8.41.0",
"eslint-config-next": "13.4.3",
"groqd": "^0.15.6",
@ -31,7 +34,12 @@
"postcss": "8.4.23",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-markdown": "^8.0.7",
"react-syntax-highlighter": "^15.5.0",
"rehype-raw": "^6.1.1",
"remark-gfm": "^3.0.1",
"sanity": "^3.11.2",
"sanity-plugin-markdown": "^4.1.0",
"styled-components": "^5.3.10",
"tailwind-merge": "^1.12.0",
"tailwindcss": "3.3.2",
@ -41,6 +49,7 @@
"typescript": "5.0.4"
},
"devDependencies": {
"@types/react-syntax-highlighter": "^15.5.7",
"@types/twemoji": "^13.1.2"
}
}

View File

@ -2,16 +2,17 @@
* 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'
import { visionTool } from "@sanity/vision";
import { defineConfig } from "sanity";
import { deskTool } from "sanity/desk";
import { markdownSchema } from "sanity-plugin-markdown";
// 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'
import { apiVersion, dataset, projectId } from "./sanity/env";
import { schema } from "./sanity/schema";
export default defineConfig({
basePath: '/internal/studio',
basePath: "/internal/studio",
projectId,
dataset,
// Add and edit the content schema in the './sanity/schema' folder
@ -21,5 +22,6 @@ export default defineConfig({
// 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 }),
markdownSchema(),
],
})
});

View File

@ -57,9 +57,9 @@ export default defineType({
type: "datetime",
}),
defineField({
name: "body",
name: "content",
title: "Body",
type: "blockContent",
type: "markdown",
}),
],

View File

@ -0,0 +1,109 @@
"use client";
import React from "react";
import * as Dialog from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import { useRouter } from "next/navigation";
import { q } from "groqd";
import { client } from "../../../../../sanity/lib/client";
import { cn } from "@/lib/utils";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import CodeBlock from "@/components/Codeblock";
type Project = {
title: string;
subtitle: string;
slug: string;
publishedAt: Date;
mainImage: string;
content: string;
};
export default function ProjectModal({
params: { id: slug },
}: {
params: {
id: string;
};
}) {
const router = useRouter();
const [project, setProject] = React.useState<Project | null>(null);
const handleOpenChange = (open: boolean) => {
if (!open) {
router.back();
}
};
React.useEffect(() => {
async function getProject() {
const { query: projectQuery, schema: projectSchema } = q("*")
.filterByType("project")
.filter(`slug.current == "${slug}"`)
.grab$({
title: q.string(),
subtitle: q.string(),
slug: q.slug("slug"),
publishedAt: q.date(),
mainImage: q("mainImage").grabOne$("asset->url", q.string()),
content: q.string(),
})
.slice(0, 1);
const project = projectSchema.parse(await client.fetch(projectQuery));
setProject(project[0]);
}
getProject();
}, [slug]);
return (
<Dialog.Root open onOpenChange={handleOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="bg-zinc-900 opacity-75 data-[state=open]:animate-overlayShow fixed inset-0" />
<Dialog.Content className="data-[state=open]:animate-contentShow overflow-y-scroll fixed top-[50%] left-[50%] w-[90vw] max-h-[85vh] max-w-[50vw] translate-x-[-50%] translate-y-[-50%] rounded-[6px] bg-white dark:bg-zinc-800 px-8 py-12 shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none">
<Dialog.Title
className={cn(
"dark:text-white text-indigo-600 m-0 text-6xl font-bold",
!project && "bg-gray-500 animate-pulse block w-52 h-5"
)}
>
{project?.title}
</Dialog.Title>
<Dialog.Description
className={cn(
"dark:text-white text-indigo-500 font-semibold mt-[10px] mb-5 text-2xl leading-normal",
!project && "bg-gray-500 animate-pulse block w-72 h-5"
)}
>
{project?.subtitle}
</Dialog.Description>
<article className="prose dark:prose-invert prose-zinc max-w-none lg:prose-xl">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
components={
{
code: CodeBlock,
} as any
}
>
{project?.content || ""}
</ReactMarkdown>
</article>
<Dialog.Close asChild>
<button
className="text-violet11 hover:bg-violet4 focus:shadow-violet7 absolute top-12 right-8 inline-flex h-[25px] w-[25px] appearance-none items-center justify-center rounded-full focus:shadow-[0_0_0_2px] focus:outline-none"
aria-label="Close"
>
<Cross2Icon />
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@ -0,0 +1,3 @@
export default function Default() {
return null;
}

View File

@ -23,8 +23,10 @@ export const metadata = {
export default async function RootLayout({
children,
project,
}: {
children: React.ReactNode;
project?: React.ReactNode;
}) {
const { query: projectQuery, schema: projectSchema } = q("*")
.filterByType("project")
@ -106,6 +108,8 @@ export default async function RootLayout({
</div>
</div>
</footer>
{project}
</body>
</html>
);

View File

View File

@ -0,0 +1,63 @@
import { CopyIcon } from "@radix-ui/react-icons";
import { CopyCheckIcon } from "lucide-react";
import React, { useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
const CodeBlock = ({
node,
inline,
className,
children,
...props
}: {
node: any;
inline: any;
className: any;
children: any;
props: any;
}) => {
const [isCopied, setIsCopied] = useState(false);
const handleCopyClick = async () => {
try {
await navigator.clipboard.writeText(children);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
} catch (err) {
console.error("Failed to copy: ", err);
}
};
const match = /language-(\w+)/.exec(className || "");
return !inline && match ? (
<div className="relative">
<SyntaxHighlighter
style={oneDark}
language={match[1]}
showLineNumbers
wrapLongLines
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
<button
className="absolute p-1 text-white bg-gray-700 rounded-md top-2 right-2 hover:bg-gray-600"
onClick={handleCopyClick}
>
{isCopied ? (
<CopyCheckIcon className="w-5 h-5" />
) : (
<CopyIcon className="w-5 h-5" />
)}
</button>
</div>
) : (
<code className={className} {...props}>
{children}
</code>
);
};
export default CodeBlock;

View File

@ -30,7 +30,7 @@ function Twemoji({
width={width}
height={height}
alt={emoji}
loading="lazy"
loading="eager"
draggable={false}
className={className}
/>

View File

@ -131,6 +131,7 @@ module.exports = {
plugins: [
require("tailwindcss-hero-patterns"),
require("tailwindcss-animate"),
require("@tailwindcss/typography"),
plugin(({ matchUtilities }) => {
matchUtilities({
perspective: (value) => ({

1102
yarn.lock

File diff suppressed because it is too large Load Diff