feat: add project intercept modal
This commit is contained in:
parent
d16dbde171
commit
22214f0d2d
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,16 +2,17 @@
|
|||
* 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'
|
||||
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(),
|
||||
],
|
||||
})
|
||||
});
|
||||
|
|
|
@ -57,9 +57,9 @@ export default defineType({
|
|||
type: "datetime",
|
||||
}),
|
||||
defineField({
|
||||
name: "body",
|
||||
name: "content",
|
||||
title: "Body",
|
||||
type: "blockContent",
|
||||
type: "markdown",
|
||||
}),
|
||||
],
|
||||
|
||||
|
|
109
src/app/@project/(.)projects/[id]/page.tsx
Normal file
109
src/app/@project/(.)projects/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
3
src/app/@project/default.tsx
Normal file
3
src/app/@project/default.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Default() {
|
||||
return null;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
0
src/app/projects/[id]/page.tsx
Normal file
0
src/app/projects/[id]/page.tsx
Normal file
63
src/components/Codeblock.tsx
Normal file
63
src/components/Codeblock.tsx
Normal 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;
|
|
@ -30,7 +30,7 @@ function Twemoji({
|
|||
width={width}
|
||||
height={height}
|
||||
alt={emoji}
|
||||
loading="lazy"
|
||||
loading="eager"
|
||||
draggable={false}
|
||||
className={className}
|
||||
/>
|
||||
|
|
|
@ -131,6 +131,7 @@ module.exports = {
|
|||
plugins: [
|
||||
require("tailwindcss-hero-patterns"),
|
||||
require("tailwindcss-animate"),
|
||||
require("@tailwindcss/typography"),
|
||||
plugin(({ matchUtilities }) => {
|
||||
matchUtilities({
|
||||
perspective: (value) => ({
|
||||
|
|
Loading…
Reference in New Issue
Block a user