diff --git a/.gitignore b/.gitignore index 26b002a..c79f84d 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,8 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Turborepo +.turbo + +.vercel diff --git a/bun.lockb b/bun.lockb index 5aa12de..7cb7ea1 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components.json b/components.json new file mode 100644 index 0000000..fcf2f8a --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/styles/globals.scss", + "baseColor": "neutral", + "cssVariables": false, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} \ No newline at end of file diff --git a/debug.log b/debug.log new file mode 100644 index 0000000..dfdb404 --- /dev/null +++ b/debug.log @@ -0,0 +1,3 @@ +[1027/133620.512:ERROR:crashpad_client_win.cc(810)] not connected +[1027/150423.695:ERROR:crashpad_client_win.cc(810)] not connected +[1027/150423.912:ERROR:crashpad_client_win.cc(810)] not connected diff --git a/next.config.ts b/next.config.ts index e043361..342b2e5 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,7 +3,17 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { sassOptions: { silenceDeprecations: ['legacy-js-api'], - } + }, + images: { + remotePatterns: [ + { + hostname: "cdn.sanity.io", + pathname: "/images/**", + protocol: "https", + } + ] + }, + }; export default nextConfig; diff --git a/package.json b/package.json index 03861c7..21a8349 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,24 @@ "start": "next start", "lint": "next lint" }, + "packageManager": "bun@1.1.33", "dependencies": { + "@sanity/image-url": "^1.0.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "lucide-react": "^0.453.0", "next": "15.0.1", + "next-sanity": "^9.8.7", + "next-themes": "^0.3.0", "react": "19.0.0-rc-69d4b800-20241021", "react-dom": "19.0.0-rc-69d4b800-20241021", - "sass": "^1.80.4" + "react-error-boundary": "^4.1.2", + "sanity": "^3.62.2", + "sass": "^1.80.4", + "sonner": "^1.5.0", + "styled-components": "6", + "tailwind-merge": "^2.5.4", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "typescript": "^5", diff --git a/src/app/[slug]/page.tsx b/src/app/[slug]/page.tsx new file mode 100644 index 0000000..35340ba --- /dev/null +++ b/src/app/[slug]/page.tsx @@ -0,0 +1,88 @@ +import { defineQuery, PortableText } from "next-sanity"; +import imageUrlBuilder from "@sanity/image-url"; +import type { SanityImageSource } from "@sanity/image-url/lib/types/types"; +import { client, sanityFetch } from "../../sanity/client"; +import Link from "next/link"; +import Image from "next/image"; +import { Post, SanityImageAsset } from "@/sanity/sanity.types"; +import urlBuilder from "@sanity/image-url"; +import {getImageDimensions} from '@sanity/asset-utils' + +const POST_QUERY = defineQuery(`*[_type == "post" && slug.current == $slug][0]`); +const POSTS_QUERY = defineQuery(`*[_type == "post"]{slug}`) + +type PageParams = Promise<{ + slug: string; +}> + +const { projectId, dataset } = client.config(); +const urlFor = (source: SanityImageSource) => + projectId && dataset + ? imageUrlBuilder({ projectId, dataset }).image(source) + : null; + +export async function generateStaticParams() { + const posts: Post[] = (await sanityFetch({ query: POSTS_QUERY, stega: false, perspective: "published" })).data; + return posts.map((post) => ({ slug: post.slug?.current ?? null })).filter((post) => post.slug); +} + +export default async function PostPage(props: { params: PageParams }) { + const { slug } = await props.params; + + const post: Post = (await sanityFetch({ query: POST_QUERY, params: { slug } })).data; + + if (!post) { + return ( +
+

Post not found

+
+ ); + } + + const postImageUrl = post.mainImage ? urlFor(post.mainImage)?.width(550).height(310).url() : null; + + function PortableImage({ value, isInline }: { value: SanityImageAsset; isInline: boolean }) { + const {width, height} = getImageDimensions(value) + return {value.altText; + } + + return ( +
+ + ← Back to posts + + {postImageUrl && ( + {`Banner + )} +

{post.title}

+
+

Published: {new Date(post.publishedAt ?? "").toISOString().substring(0, 10)}

+ {Array.isArray(post.body) && } +
+
+ ); +} diff --git a/src/app/api/draft-mode/enable/route.ts b/src/app/api/draft-mode/enable/route.ts new file mode 100644 index 0000000..ab666f3 --- /dev/null +++ b/src/app/api/draft-mode/enable/route.ts @@ -0,0 +1,6 @@ +import { client } from '@/sanity/client'; +import { defineEnableDraftMode } from 'next-sanity/draft-mode'; + +export const { GET } = defineEnableDraftMode({ + client: client.withConfig({ token: process.env.SANITY_API_READ_TOKEN, stega: { studioUrl: 'https://vaporvee.sanity.studio', enabled: true } }), +}) \ No newline at end of file diff --git a/src/layouts/default.tsx b/src/app/layout.tsx similarity index 54% rename from src/layouts/default.tsx rename to src/app/layout.tsx index 53faa7d..33bdcb9 100644 --- a/src/layouts/default.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,12 @@ import localFont from "next/font/local"; import "../styles/globals.scss"; +import { draftMode } from "next/headers"; +import { VisualEditing } from 'next-sanity' +import { SanityLive } from '@/sanity/client' +import { LiveErrorBoundary } from "./live-error-boundary"; +import { Toaster } from "@/components/ui/sonner"; + const geistSans = localFont({ src: "../fonts/GeistVF.woff", variable: "--font-geist-sans", @@ -12,17 +18,24 @@ const geistMono = localFont({ weight: "100 900", }); -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const { isEnabled } = await draftMode(); + return ( {children} + + + + {isEnabled && } + ); diff --git a/src/app/live-error-boundary.tsx b/src/app/live-error-boundary.tsx new file mode 100644 index 0000000..6130278 --- /dev/null +++ b/src/app/live-error-boundary.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useEffect } from "react"; +import { + ErrorBoundary as ReactErrorBoundary, + type FallbackProps, +} from "react-error-boundary"; + +export function LiveErrorBoundary({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +function Fallback({ error }: FallbackProps) { + useEffect(() => { + const msg = "Couldn't connect to Live Content API"; + console.error(`${msg}: `, error); + }, [error]); + + return null; +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..8a86888 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,31 @@ +import Link from "next/link"; +import { type SanityDocument } from "next-sanity"; + +import { client } from "@/sanity/client"; + +const POSTS_QUERY = `*[ + _type == "post" + && defined(slug.current) +]|order(publishedAt desc)[0...12]{_id, title, slug, publishedAt}`; + +const options = { next: { revalidate: 30 } }; + +export default async function IndexPage() { + const posts = await client.fetch(POSTS_QUERY, {}, options); + + return ( +
+

Posts

+
    + {posts.map((post) => ( +
  • + +

    {post.title}

    +

    {new Date(post.publishedAt).toLocaleDateString()}

    + +
  • + ))} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..9c8e3a5 --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,31 @@ +"use client" + +import { useTheme } from "next-themes" +import { Toaster as Sonner } from "sonner" + +type ToasterProps = React.ComponentProps + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ) +} + +export { Toaster } diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/pages/blog.tsx b/src/pages/blog.tsx deleted file mode 100644 index f8521ab..0000000 --- a/src/pages/blog.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import '../layouts/default' - -export default function Home() { - return ( -
Blog
- ); -} diff --git a/src/pages/index.tsx b/src/pages/index.tsx deleted file mode 100644 index 95c2ba5..0000000 --- a/src/pages/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import '../layouts/default' - -export default function Home() { - return ( -
Home
- ); -} diff --git a/src/pages/projects.tsx b/src/pages/projects.tsx deleted file mode 100644 index 214c42c..0000000 --- a/src/pages/projects.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import '../layouts/default' - -export default function Home() { - return ( -
Projects
- ); -} diff --git a/src/sanity/client.ts b/src/sanity/client.ts new file mode 100644 index 0000000..c6ec77e --- /dev/null +++ b/src/sanity/client.ts @@ -0,0 +1,20 @@ +import { createClient, defineLive } from "next-sanity"; + +export const client = createClient({ + projectId: "zk5oebdb", + dataset: "production", + apiVersion: "2024-10-27", + useCdn: false, + stega: { studioUrl: 'https://vaporvee.sanity.studio' }, +}); + +const token = process.env.SANITY_API_READ_TOKEN +if (!token) { + throw new Error('Missing SANITY_API_READ_TOKEN') +} + +export const { sanityFetch, SanityLive } = defineLive({ + client, + serverToken: token, + browserToken: token, +}); diff --git a/src/sanity/sanity.types.ts b/src/sanity/sanity.types.ts new file mode 100644 index 0000000..83091d9 --- /dev/null +++ b/src/sanity/sanity.types.ts @@ -0,0 +1,278 @@ +/** + * --------------------------------------------------------------------------------- + * This file has been generated by Sanity TypeGen. + * Command: `sanity typegen generate` + * + * Any modifications made directly to this file will be overwritten the next time + * the TypeScript definitions are generated. Please make changes to the Sanity + * schema definitions and/or GROQ queries if you need to update these types. + * + * For more information on how to use Sanity TypeGen, visit the official documentation: + * https://www.sanity.io/docs/sanity-typegen + * --------------------------------------------------------------------------------- + */ + +// Source: schema.json +export type SanityImagePaletteSwatch = { + _type: 'sanity.imagePaletteSwatch' + background?: string + foreground?: string + population?: number + title?: string +} + +export type SanityImagePalette = { + _type: 'sanity.imagePalette' + darkMuted?: SanityImagePaletteSwatch + lightVibrant?: SanityImagePaletteSwatch + darkVibrant?: SanityImagePaletteSwatch + vibrant?: SanityImagePaletteSwatch + dominant?: SanityImagePaletteSwatch + lightMuted?: SanityImagePaletteSwatch + muted?: SanityImagePaletteSwatch +} + +export type SanityImageDimensions = { + _type: 'sanity.imageDimensions' + height?: number + width?: number + aspectRatio?: number +} + +export type SanityFileAsset = { + _id: string + _type: 'sanity.fileAsset' + _createdAt: string + _updatedAt: string + _rev: string + originalFilename?: string + label?: string + title?: string + description?: string + altText?: string + sha1hash?: string + extension?: string + mimeType?: string + size?: number + assetId?: string + uploadId?: string + path?: string + url?: string + source?: SanityAssetSourceData +} + +export type Geopoint = { + _type: 'geopoint' + lat?: number + lng?: number + alt?: number +} + +export type BlockContent = Array< + | { + children?: Array<{ + marks?: Array + text?: string + _type: 'span' + _key: string + }> + style?: 'normal' | 'h1' | 'h2' | 'h3' | 'h4' | 'blockquote' + listItem?: 'bullet' + markDefs?: Array<{ + href?: string + _type: 'link' + _key: string + }> + level?: number + _type: 'block' + _key: string + } + | { + asset?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' + } + hotspot?: SanityImageHotspot + crop?: SanityImageCrop + _type: 'image' + _key: string + } +> + +export type Category = { + _id: string + _type: 'category' + _createdAt: string + _updatedAt: string + _rev: string + title?: string + description?: string +} + +export type Post = { + _id: string + _type: 'post' + _createdAt: string + _updatedAt: string + _rev: string + title?: string + slug?: Slug + author?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'author' + } + mainImage?: { + asset?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' + } + hotspot?: SanityImageHotspot + crop?: SanityImageCrop + _type: 'image' + } + categories?: Array<{ + _ref: string + _type: 'reference' + _weak?: boolean + _key: string + [internalGroqTypeReferenceTo]?: 'category' + }> + publishedAt?: string + myCodeField?: Code + body?: BlockContent +} + +export type Author = { + _id: string + _type: 'author' + _createdAt: string + _updatedAt: string + _rev: string + name?: string + slug?: Slug + image?: { + asset?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' + } + hotspot?: SanityImageHotspot + crop?: SanityImageCrop + _type: 'image' + } + bio?: Array<{ + children?: Array<{ + marks?: Array + text?: string + _type: 'span' + _key: string + }> + style?: 'normal' + listItem?: never + markDefs?: Array<{ + href?: string + _type: 'link' + _key: string + }> + level?: number + _type: 'block' + _key: string + }> +} + +export type SanityImageCrop = { + _type: 'sanity.imageCrop' + top?: number + bottom?: number + left?: number + right?: number +} + +export type SanityImageHotspot = { + _type: 'sanity.imageHotspot' + x?: number + y?: number + height?: number + width?: number +} + +export type SanityImageAsset = { + _id: string + _type: 'sanity.imageAsset' + _createdAt: string + _updatedAt: string + _rev: string + originalFilename?: string + label?: string + title?: string + description?: string + altText?: string + sha1hash?: string + extension?: string + mimeType?: string + size?: number + assetId?: string + uploadId?: string + path?: string + url?: string + metadata?: SanityImageMetadata + source?: SanityAssetSourceData +} + +export type SanityAssetSourceData = { + _type: 'sanity.assetSourceData' + name?: string + id?: string + url?: string +} + +export type SanityImageMetadata = { + _type: 'sanity.imageMetadata' + location?: Geopoint + dimensions?: SanityImageDimensions + palette?: SanityImagePalette + lqip?: string + blurHash?: string + hasAlpha?: boolean + isOpaque?: boolean +} + +export type Slug = { + _type: 'slug' + current?: string + source?: string +} + +export type Code = { + _type: 'code' + language?: string + filename?: string + code?: string + highlightedLines?: Array +} + +export type AllSanitySchemaTypes = + | SanityImagePaletteSwatch + | SanityImagePalette + | SanityImageDimensions + | SanityFileAsset + | Geopoint + | BlockContent + | Category + | Post + | Author + | SanityImageCrop + | SanityImageHotspot + | SanityImageAsset + | SanityAssetSourceData + | SanityImageMetadata + | Slug + | Code +export declare const internalGroqTypeReferenceTo: unique symbol diff --git a/src/styles/globals.scss b/src/styles/globals.scss index 7d0ea7d..08356be 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -4,6 +4,7 @@ :root { --background: #ffffff; --foreground: #171717; + --radius: 0.75rem; } @media (prefers-color-scheme: dark) { diff --git a/tailwind.config.ts b/tailwind.config.ts index 021c393..864f33a 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,19 +1,25 @@ import type { Config } from "tailwindcss"; const config: Config = { - content: [ + darkMode: ["class"], + content: [ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", "./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/app/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { - extend: { - colors: { - background: "var(--background)", - foreground: "var(--foreground)", - }, - }, + extend: { + colors: { + background: 'var(--background)', + foreground: 'var(--foreground)' + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + } + } }, - plugins: [], + plugins: [require("tailwindcss-animate")], }; export default config;