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 ;
+ }
+
+ return (
+
+
+ ← Back to posts
+
+ {postImageUrl && (
+
+ )}
+ {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
+
+
+ );
+}
\ 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;