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": { "dependencies": {
"@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-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.4", "@radix-ui/react-dropdown-menu": "^2.0.4",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-navigation-menu": "^1.1.2", "@radix-ui/react-navigation-menu": "^1.1.2",
"@sanity/image-url": "^1.0.2", "@sanity/image-url": "^1.0.2",
"@sanity/vision": "^3.11.2", "@sanity/vision": "^3.11.2",
"@tailwindcss/typography": "^0.5.9",
"@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",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"class-variance-authority": "^0.6.0", "class-variance-authority": "^0.6.0",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"easymde": "2",
"eslint": "8.41.0", "eslint": "8.41.0",
"eslint-config-next": "13.4.3", "eslint-config-next": "13.4.3",
"groqd": "^0.15.6", "groqd": "^0.15.6",
@ -31,7 +34,12 @@
"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",
"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": "^3.11.2",
"sanity-plugin-markdown": "^4.1.0",
"styled-components": "^5.3.10", "styled-components": "^5.3.10",
"tailwind-merge": "^1.12.0", "tailwind-merge": "^1.12.0",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.2",
@ -41,6 +49,7 @@
"typescript": "5.0.4" "typescript": "5.0.4"
}, },
"devDependencies": { "devDependencies": {
"@types/react-syntax-highlighter": "^15.5.7",
"@types/twemoji": "^13.1.2" "@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 * 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 { visionTool } from "@sanity/vision";
import {defineConfig} from 'sanity' import { defineConfig } from "sanity";
import {deskTool} from 'sanity/desk' 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 // Go to https://www.sanity.io/docs/api-versioning to learn how API versioning works
import {apiVersion, dataset, projectId} from './sanity/env' import { apiVersion, dataset, projectId } from "./sanity/env";
import {schema} from './sanity/schema' import { schema } from "./sanity/schema";
export default defineConfig({ export default defineConfig({
basePath: '/internal/studio', basePath: "/internal/studio",
projectId, projectId,
dataset, dataset,
// Add and edit the content schema in the './sanity/schema' folder // 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 // Vision is a tool that lets you query your content with GROQ in the studio
// https://www.sanity.io/docs/the-vision-plugin // https://www.sanity.io/docs/the-vision-plugin
visionTool({ defaultApiVersion: apiVersion }), visionTool({ defaultApiVersion: apiVersion }),
markdownSchema(),
], ],
}) });

View File

@ -57,9 +57,9 @@ export default defineType({
type: "datetime", type: "datetime",
}), }),
defineField({ defineField({
name: "body", name: "content",
title: "Body", 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({ export default async function RootLayout({
children, children,
project,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
project?: React.ReactNode;
}) { }) {
const { query: projectQuery, schema: projectSchema } = q("*") const { query: projectQuery, schema: projectSchema } = q("*")
.filterByType("project") .filterByType("project")
@ -106,6 +108,8 @@ export default async function RootLayout({
</div> </div>
</div> </div>
</footer> </footer>
{project}
</body> </body>
</html> </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} width={width}
height={height} height={height}
alt={emoji} alt={emoji}
loading="lazy" loading="eager"
draggable={false} draggable={false}
className={className} className={className}
/> />

View File

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

1102
yarn.lock

File diff suppressed because it is too large Load Diff