feat: add project intercept modal
This commit is contained in:
parent
d16dbde171
commit
22214f0d2d
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* 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 { 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
|
||||||
|
@ -20,6 +21,7 @@ export default defineConfig({
|
||||||
deskTool(),
|
deskTool(),
|
||||||
// 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(),
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
|
@ -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",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
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({
|
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>
|
||||||
);
|
);
|
||||||
|
|
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}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
alt={emoji}
|
alt={emoji}
|
||||||
loading="lazy"
|
loading="eager"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
className={className}
|
className={className}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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) => ({
|
||||||
|
|
Loading…
Reference in New Issue
Block a user