Ship Logs

Rough at Sea

A chronicle of building a ship while sailing it.

[NeetCode][Easy] Group Anagrams

## Group Anagrams This is [problem #4](https://neetcode.io/problems/anagram-groups/question?list=neetcode150) in NeetCode's `Arrays & Hashing` section of problems. Given an array of strings `strs`, group all anagrams together into sublists. You may return the output in any order. An anagram is a string that contains the exact same characters as another string, but the order of the characters can be different. Example 1: ```python Input: strs = ["act","pots","tops","cat","stop","hat"] Output: [["hat"],["act", "cat"],["stop", "pots", "tops"]] ``` Example 2: ```python Input: strs = ["x"] Output: [["x"]] ``` Example 3: ```python Input: strs = [""] Output: [[""]] ``` The code is stubbed out as follows: ```python class Solution: def twoSum(self, nums: List[int], target: int) -> List[int]: ``` Below is the solution that lives rent free in my head: ```python class Solution: def groupAnagrams(self, strs: List[str]) -> List[List[str]]: groups = {} for istr in strs: key = ''.join(sorted(istr)) if key in groups: groups[key].append(istr) else: groups[key] = [istr] return list(groups.values()) ``` As usual, the insight to solving this was not my own. We saw previously that if two strings are anagrams, then sorting them and checking if they are equal is a really fast way to determine if they are anagrams of each other. This is the reason why we say that `key = sorted(istr)`. The only reason we join it with an empty string is to cast its type from list to string. Hence why we have `key = ''.join(sorted(istr))`. Now we can use the sorted version of each string as the key, and then just add strings to the corresponding array as we encounter them. In the example where `strs = ["act","pots","tops","cat","stop","hat"]`, the loop will proceed as follows: ```python istr = "act" key = sorted(istr) = "act" groups[key] does not exist, so we short circuit to the `else` case. We create a new array with one element, i.e., ["act"] groups = { "act": ["act"] } istr = "pots" key = sorted(istr) = "opst" groups[key] does not exist, so we short circuit to the else case. We create a new array with one element, i.e., ["opst"] groups = { "act" : ["act"], "opst": ["pots"] } istr = "tops" key = sorted(istr) = "opst" groups[key] does exist from when istr was equal to "pots". In this case we append "tops" to the groups["opst"] array. groups = { "act" : ["act"], "opst": ["pots", "tops"] } istr = "cat" key = sorted(istr) = "act" groups[key] does exist from when istr was equal to "act". In this case we append "cat" to the groups["act"] array. groups = { "act" : ["act", "cat"], "opst": ["pots", "tops"] } istr = "stop" key = sorted(istr) = "opst" groups[key] does exist from when istr was equal to "pots". In this case we append "stop" to the groups["opst"] array. groups = { "act" : ["act", "cat"], "opst": ["pots", "tops", "stop"] } istr = "hat" key = sorted(istr) = "aht" groups[key] does not exist, so we short circuit to the else. We create a new array with one element, i.e., ["hat"]. groups = { "act" : ["act", "cat"], "opst": ["pots", "tops", "stop"], "aht": ["hat"] } ``` Finally, we simply return `groups`.

[NeetCode][Easy] Two Sum

# Two Sum This is [problem #3](https://neetcode.io/problems/two-integer-sum/question?list=neetcode150) in NeetCode's `Arrays & Hashing` section of problems. Given an array of integers `nums` and an integer `target`, return the indices `i` and `j` such that `nums[i] + nums[j] == target` and `i != j`. You may assume that every input has exactly one pair of indices `i` and `j` that satisfy the condition. Return the answer with the smaller index first. Example 1: ```python Input: nums = [3,4,5,6], target = 7 Output: [0,1] ``` Explanation: `nums[0] + nums[1] == 7`, so we return `[0, 1]`. Example 2: ```python Input: nums = [4,5,6], target = 10 Output: [0,2] ``` Example 3: ```python Input: nums = [5,5], target = 10 Output: [0,1] ``` The code is stubbed out as follows: ```python class Solution: def twoSum(self, nums: List[int], target: int) -> List[int]: ``` I didn't have a good idea of where to start with this one, so I started with a for-loop and just iterated over the nums array. I believe in my first attempt I came up with a solution that was O(n<sup>2</sup>). As usual, I could not come up with the actual insight on my own. ```python class Solution: def twoSum(self, nums: List[int], target: int) -> List[int]: seen = {} for i in range(len(nums)): complement = target - nums[i] if complement in seen: return [seen[complement], i] seen[nums[i]] = i return [] ``` The insight is that if we have a guarantee that two of the numbers add up to target, then it is necessarily the case the `target - nums[i]` exists in `nums`. The way we guarantee that `i != j` is by the fact that when we look for `complement`, we are only looking at values to the left of `num[i]`, so any place where we find `complement` within `nums` will necessarily have an index less than `i`.

[NeetCode][Easy] Valid Anagram

# Valid Anagram This is [problem #2](https://neetcode.io/problems/is-anagram/question?list=neetcode150) in NeetCode's `Arrays & Hashing` section of problems. Given two strings s and t, return true if the two strings are anagrams of each other, otherwise return false. An anagram is a string that contains the exact same characters as another string, but the order of the characters can be different. Example 1: `Input: s = "racecar", t = "carrace"` Output: `true` Example 2: `Input: s = "jar", t = "jam"` Output: `false` The code is stubbed out as follows: ```python class Solution: def isAnagram(self, s: str, t: str) -> bool: ``` Doing leetcode style problems as been, for me, a great way to learn that I am not brilliant. But it has revealed to me that I am pretty good at remembering abstract concepts and being able to map them to problems. I share this because I think there's a common misconception that you have to be brilliant to solve leet code problems. You don't. The truth is a lot more painful: you just have to practice them. Practice is slow and frustrating. But it is a good struggle. With that being said, the solution that I have memorized is fairly simple: ```python class Solution: def isAnagram(self, s: str, t: str) -> bool: return sorted(s) == sorted(t) ``` This was not my insight; I got this from the neetcode site itself. Let's look at why it works. If two words are anagrams of each other, then they have the same letters, but in a different order. `carrace` and `racecar` both contain the same letters. If we sort `carrace`, we get `aaccerr` Likewise, if we sort `racecar`, we also get `aaccerr` Thus, `aaccerr == aaccerr` evaluates to `True` only when the two words are anagrams. There is another interesting solution that is worth looking at at least once: ```python class Solution: def isAnagram(self, s: str, t: str) -> bool: if len(s) != len(t): return False letter_ticks = [0] * 26 a_ord = ord('a') for i in range(len(s)): letter_ticks[ord(s[i]) - a_ord] += 1 letter_ticks[ord(t[i]) - a_ord] -= 1 for num in letter_ticks: if num > 0: return False return True ``` I won't walk through the whole thing line by line. Rather, I'll focus on the key insight. We have an array called `letter_ticks` that consists of 26 zeros. Why? Well, there's 26 letters in the alphabet. We use a bit of code to map letters to numbers via `ord(s[i]) - a_ord`. All this does is it takes the ASCII encoding for a letter, such as the letter `a` with its ASCII encoding of `97`, and subtracts the ASCII encoding of `97` from it to give you `0`. This means we can map each letter in the alphabet to an index in our `letter_ticks` array. ```python a = 0 b = 1 c = 2 ... x = 23 y = 24 z = 25 ``` If two words are anagrams, then they will increment and decement the exact same elements in the `letter_ticks` array by the same amount such that the `letter_ticks` array returns to all zeros. In the case where `s = "racecar"` and `t = "carrace"` the array will change as follows: ```python [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] i = 0 s[0] = 'r' s[0] = 'c' [0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0] i = 1 s[0] = 'a' s[0] = 'a' [1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0] # Notice that the first element briefly became 1 and then went back down to 0 i = 2 s[0] = 'c' s[0] = 'r' [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] # Notice that the letter positions for 'c' and 'r' returned to zero. ``` As you proceed with this exercise, you realize that if the two words are anagrams, the elements in the array corresponding to the letters in the strings will be incremented and decremented by the same amount, briging every element back to zero by the end of the for-loop.

[NeetCode][Easy] Contains Duplicate

# Contains Duplicate This is [problem #1](https://neetcode.io/problems/duplicate-integer/question?list=neetcode150) in NeetCode's `Arrays & Hashing` section of problems. Given an integer array nums, return true if any value appears more than once in the array, otherwise return false. Example 1: ```python Input: nums = [1, 2, 3, 3] ``` Output: `true` Example 2: ```python Input: nums = [1, 2, 3, 4] ``` Output: `false` The code is stubbed out as follows: ```python class Solution: def hasDuplicate(self, nums: List[int]) -> bool: ``` I've solved this one a couple of times now, so here's my solution: ```python class Solution: def hasDuplicate(self, nums: List[int]) -> bool: hash_table = {} for num in nums: if num in hash_table: return True hash_table[num] = True return False ``` My original solution was probably something with a run time complexity of O(n<sup>2</sup>). I did end up looking up a better solution, which is what you see above. However, I've solved it enough times from memory that it's the only version I can think of whenever I see this problem. So let's talk about how it works. We use a hash table, which has a lookup speed of O(1). If `nums=[1,2,3,3]`, then as we iterate through nums, `hash_table` changes as follows: ```python num = 1 hash_table = { 1: True } num = 2 hash_table = { 1: True, 2: True } num = 3 hash_table = { 1: True, 2: True, 3: True } num = 3 ``` When num = 3 for the second time (due to the for-loop), when we do a hash table lookup via `if num in hash_table`, we do in fact find that there is already an instance of `3`, so we immediately return `True`. It's worth noting that the actual value we store in the key-value pairs in the dictionary is irrelevant. In other words, we can update the code as follows: ```python class Solution: def hasDuplicate(self, nums: List[int]) -> bool: hash_table = {} for num in nums: if num in hash_table: return True hash_table[num] = "some random value that is never used" return False ``` And the code will still work and pass the test cases. The only reason we care about the hash table is because it gives us O(1) lookups.

The Architecture of RoughAtSea.com

In this post I will break down the architecture of this site. We will also do a line-by-line explanation of some of the more important code files. This is an open source project, available [here](https://github.com/roughatsea/roughatseablog). The following technologies were used to build roughatsea.com: 1. Framework: Next.js (App Router) 2. Database: Neon (Serverless Postgres) 3. Object Relational Mapping (ORM): Drizzle ORM 4. Authentication: NextAuth.js 5. Content Edit: @uiw/react-md-editor 6. Storage: Vercel blob 7. UI & Styling: Tailwind CSS + shadcn/ui The file and folder structure below details the main files in the project and their purpose. ```text | .env.local - Stores local environment variables (DB URLs, API keys, NextAuth secrets). | components.json - Configuration file for shadcn/ui to manage component installations. | drizzle.config.ts - Configuration for Drizzle ORM specifying the schema path and database credentials. | eslint.config.mjs - Configuration for ESLint to maintain code quality and style rules. | next.config.ts - Configuration for Next.js, including compiler options and environment variables. | package-lock.json - Auto-generated file ensuring exact dependency versions for reproducible builds. | package.json - Defines project dependencies, scripts, and basic metadata. | postcss.config.mjs - Configuration for PostCSS, primarily used here to process Tailwind CSS. | tsconfig.json - Configuration for the TypeScript compiler. | \---src - The root folder for all application source code. +---app - Next.js App Router root. Contains all pages, layouts, and API routes. | | favicon.ico - The website's favicon. | | globals.css - Global CSS file containing Tailwind directives and base styles. | | layout.tsx - The root layout that wraps all pages (contains the HTML/Body tags and global Navbar). | | page.tsx - The landing/home page of the application. | | | +---api - Next.js serverless API routes. | | +---auth - Authentication API endpoints. | | | \---[...nextauth] - Catch-all route for NextAuth.js handling login, logout, and callbacks. | | | route.ts - The implementation of the NextAuth route handler. | | | | | \---upload - API route for handling file uploads. | | route.ts - Takes uploaded images and pushes them securely to Vercel Blob. | | | +---blog - Routes for the public blog feed and articles. | | | page.tsx - The main blog feed displaying a list of all published posts. | | | | | \---[slug] - Dynamic route for viewing a specific blog post by its URL slug. | | page.tsx - Renders the individual blog post content. | | | | | +---dashboard - Protected routes for authenticated authors. | | page.tsx - The main dashboard showing all posts (published and drafts) for the user. | | | +---edit - Routes for editing existing posts. | | \---[id] - Dynamic route for editing a specific post by its ID. | | page.tsx - Loads the post data and renders the PostForm for editing. | | | \---new - Route for creating a new post. | page.tsx - Renders an empty PostForm to create a new draft or published post. | +---components - Reusable UI components. | +---layout - Components defining page layout structure. | | Navbar.tsx - The global navigation bar containing links and auth state buttons. | | | \---ui - Raw UI components generated by shadcn/ui. | alert-dialog.tsx - Modal component for destructive actions (like deleting a post). | button.tsx - Standard button component. | checkbox.tsx - Checkbox component (used for the publish toggle). | form.tsx - Form wrapper components integrating react-hook-form. | input.tsx - Standard text input field. | label.tsx - Form label component. | sonner.tsx - Toast notification system provider. | textarea.tsx - Standard textarea component. | +---features - Domain-driven logic grouped by feature rather than type. | +---auth - Logic specifically related to authentication. | | schema.ts - Database schema for users, accounts, and sessions (NextAuth required tables). | | | \---posts - Logic specifically related to blog posts. | | actions.ts - Server Actions for mutating post data (create, update, delete). | | queries.ts - Database Queries for fetching post data (get by ID, get all, etc.). | | schema.ts - Database schema definition for the "posts" table. | | | \---components - UI Components specific to the posts feature. | DeletePostButton.tsx - A button that triggers a Server Action to delete a specific post. | PostForm.tsx - The main form (incorporating the Markdown editor) for creating and editing posts. | +---lib - Global utility functions and core configurations. | | auth.ts - NextAuth configuration object (providers, callbacks, session strategies). | | utils.ts - Generic utilities (like the `cn` function for merging Tailwind classes). | | | \---db - Database connection setup. | index.ts - Initializes the Neon HTTP client and binds it to Drizzle ORM. | \---types - Global TypeScript type declarations. next-auth.d.ts - Type overrides extending the default NextAuth session types (e.g. adding `id`). ``` I have some experience with traditional Single Page Applications (SPA) in React. In a traditional SPA, there is usually a `public/index.html` file with a `<div id="root"></div>` element, and a src/index.tsx file that calls `createRoot(document.getElementById('root')).render(<App />)`. The Next.js framework is different in that it is a Server-Side Rendered (SSR) framework, meaning the server renders the raw HTML and serves it back to the user. This means you don't get heavy DOM manipulation/calculation on the client side. Next.js uses a file-system based routing system where the entry points are defined by your folder structure in src/app/. ## `src/app/layout.tsx` This file is the equivalent of the index.html in a traditional React app. If we look inside layout.tsx, we'll see the actual `<html>` and `<body>` tags: ```typescript export default function RootLayout({ children }) { return ( <html lang="en"> <body> <Navbar /> {children} </body> </html> ); } ``` `<Navbar />` is a custom component that we'll talk about later. All it does is show a navigation bar at the top of every page. The `{children}` property is where the rest of the application is mounted into. Now let's dive into a line-by-line breakdown of exactly what layout.tsx does. This file acts as the "master template" for the entire Next.js application. Every single page in the app is injected into this layout. ### The Imports ```typescript 1: import type { Metadata } from "next"; ``` Imports the Metadata type from Next.js. This is strictly used by TypeScript to ensure that when we define our website's meta tags (like title and description), we are using the correct formatting. ```typescript 2: import { Geist, Geist_Mono } from "next/font/google"; ``` Imports two optimized fonts (Geist and Geist_Mono) directly from Google Fonts using Next.js's built-in font optimizer. This prevents layout shift and downloads the font automatically at build time. ```typescript 3: import "./globals.css"; ``` Imports the global CSS file. This is where Tailwind CSS is initialized and our global styling variables (like our vscDarkPlus overrides or custom colors) are applied to the whole app. ```typescript 4: import { Toaster } from "@/components/ui/sonner"; ``` Imports the Toaster component from the Sonner library. We use this component for showing small popup notifications, like "Upload failed" or "Post saved"). ### Font Configuration ``` typescript 6: const geistSans = Geist({ 7: variable: "--font-geist-sans", 8: subsets: ["latin"], 9: }); ``` Configures the Geist font for standard text. - `variable` assigns this font to a CSS variable (`--font-geist-sans`), allowing Tailwind CSS to use it easily. - `subsets: ["latin"]` tells Next.js to only download the English/Latin alphabet characters to save loading time. ```typescript 11: const geistMono = Geist_Mono({ 12: variable: "--font-geist-mono", 13: subsets: ["latin"], 14: }); ``` Does the exact same thing as above, but for the Geist_Mono font (which is typically used for code blocks or monospace text). ### SEO & Meta Tags ```typescript 16: export const metadata: Metadata = { 17: title: "Create Next App", 18: description: "Generated by create next app", 19: }; ``` This object defines the `<title>` and `<meta name="description">` that goes inside the `<head>` of the website's HTML. Normally you'll to change these from the default "Create Next App" to something like "Rough At Sea Blog" for better SEO. ### Layout Component ```typescript 21: import { Navbar } from "@/components/layout/Navbar"; ``` Imports our custom Navbar component. Because it's imported here in the root layout, the Navbar will persist across every single page without reloading. ```typescript 23: export default function RootLayout({ 24: children, 25: }: Readonly<{ 26: children: React.ReactNode; 27: }>) { ``` Defines the main RootLayout React component. It takes a prop called children (which represents whatever page the user is currently viewing). The `Readonly<{ children: React.ReactNode }>` is just TypeScript strictly saying "children is required and cannot be modified." ### The HTML Structure ```typescript 28: return ( 29: <html 30: lang="en" 31: className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} 32: > ``` Renders the outermost &lt;html&gt; tag of your website. It injects the two CSS variables we created earlier for our custom fonts. - `h-full` ensures the HTML takes up 100% of the screen height. - `antialiased` makes the font rendering smoother and sharper. ```typescript 33: <body className="min-h-full flex flex-col"> ``` Renders the `<body>` tag. `min-h-full` paired with `flex flex-col` creates a "sticky footer" setup, ensuring the body takes up at least the whole screen, and elements stack vertically. ```typescript 34: <Navbar /> ``` Renders our global navigation bar at the very top of the page. ```typescript 35: <div className="flex-1"> 36: {children} 37: </div> ``` This is the most important part of the file. `children` is whatever specific page the user navigated to (e.g., /dashboard, /blog, etc). The wrapping `div` with `flex-1` tells the main content area to expand and fill all the remaining vertical space left over by the Navbar. ```typescript 38: <Toaster /> ``` Places the Sonner notification system at the bottom of your app. Putting it here ensures that notification toasts can be triggered from anywhere in the app and will always float above the page content. ```typescript 39: </body> 40: </html> 41: ); 42: } ``` Closes out the tags and finishes the component. ## `src/app/page.tsx` Next.js is a directory-based framework for routing. In other words, we form our directory structure the same as our routes. `page.tsx` lives at the root of the application, so its corresponding route is `/`. This means page.tsx is what renders when we simply navigate to https://www.roughatsea.com/ ### The Imports ```tsx 1: import Link from "next/link"; ``` Imports Next.js's built-in Link component. In Next.js, you should always use &lt;Link&gt; instead of standard HTML &lt;a href="..."&gt; tags for internal links. It pre-fetches the destination page in the background and does client-side navigation without refreshing the browser, making your app feel lightning-fast. ```tsx 2: import { Button } from "@/components/ui/button"; ``` Imports the customized reusable Button component from our Shadcn UI library installation. This component handles all the Tailwind styling logic for buttons so we don't have to rewrite standard button styles every time. ### The Component Definition ```tsx 4: export default async function Home() { ``` Defines the main React component for this page. - `export default` makes this the primary component that Next.js will render when someone visits the route. - `async` means this is a Next.js Server Component that can perform server-side operations (like database queries or checking authentication) before the HTML is sent to the browser. - `Home` is just the name of the function. ```tsx 5: 6: return ( ``` Begins the return statement. Everything inside the parentheses is the JSX (HTML-like syntax) that will be rendered on the user's screen. ### The Layout Structure ```tsx 7: <main className="flex min-h-screen flex-col items-center justify-center p-24 text-center"> ``` The outermost container tag for the page content. Let's break down the Tailwind classes: - `flex flex-col`: Uses Flexbox to stack child elements vertically (in a column). - `min-h-screen`: Ensures the container is at least 100% of the viewport's height. - `items-center justify-center`: Centers all content perfectly in the middle of the screen both horizontally and vertically. - `p-24`: Adds 6rem (96px) of padding around the edges so content doesn't touch the sides of the browser window. - `text-center`: Ensures all text inside is centrally aligned. ### The Text Content ```tsx 8: <h1 className="text-5xl font-bold tracking-tight mb-4">Rough at Sea</h1> ``` Renders the massive main title of the homepage. - `text-5xl`: Makes the text very large. - `font-bold`: Makes the text heavy/thick. - `tracking-tight`: Pulls the letters slightly closer together (a popular modern typography design choice). - `mb-4`: Adds a bottom margin (spacing) below the title so it isn't crammed against the subtitle. ```tsx 9: <p className="text-xl text-muted-foreground mb-8 max-w-2xl"> 10: Building the ship as I sail it. 11: </p> ``` Renders the subtitle/slogan. - `text-xl`: Slightly larger than standard paragraph text. - `text-muted-foreground`: Uses a semantic Tailwind color variable (likely a subtle gray) so it doesn't distract from the main title. - `mb-8`: Adds a larger gap below the text before the buttons start. - `max-w-2xl`: Constrains the width of the text so that if you ever make this description much longer, it will wrap gracefully instead of stretching across the entire monitor. ### The Action Buttons ```tsx 13: <div className="flex flex-col sm:flex-row gap-4 justify-center items-center"> ``` A wrapper for our buttons. - `flex-col sm:flex-row`: On mobile phones (by default), the buttons will stack vertically. Once the screen hits the sm breakpoint (tablets and desktops), they switch to a horizontal row. - `gap-4`: Adds a consistent gap between buttons. ```tsx 14: <Link href="/blog"> ``` The Next.js router link that tells the browser to instantly navigate to /blog when clicked. ```tsx 15: <Button size="lg" className="w-full sm:w-auto shadow-lg hover:shadow-indigo-500/10"> ``` The button component itself. - `size="lg"`: Uses the predefined "large" variant from your Button component. - `w-full sm:w-auto`: On mobile phones, the button expands to be 100% width (filling the screen). On larger screens (sm), it shrinks back to auto-fit its text. - `shadow-lg hover:shadow-indigo-500/10`: Adds a nice shadow that gets a subtle indigo tint when you hover your mouse over it. ```tsx 16: Read the Blog 17: </Button> 18: </Link> 19: </div> 20: </main> 21: ); 22: } ``` Closes out the button, link wrapper, div wrapper, main tag, return statement, and function. ## `src/app/blog/page.tsx` This file serves the /blog route. ### The Imports ```tsx 1: import Link from "next/link"; ``` Imports Next.js's internal client-side navigation component, which ensures transitioning between pages is fast and doesn't require a full page refresh. ```tsx 2: import { getPublishedPosts } from "@/features/posts/queries"; ``` Imports our custom database query function that specifically fetches only posts marked as isPublished = true, sorting them so the newest posts show up first. ```tsx 3: import { Calendar, ArrowRight, BookOpen } from "lucide-react"; ``` Imports three icons from the Lucide icon library. ### The Component Definition ```tsx 5: export default async function BlogFeedPage() { 6: const posts = await getPublishedPosts(); ``` Defines our main Server Component for the /blog route. Line 6 tells Next.js to pause rendering, reach out to our Neon Postgres database, and grab all the published posts before sending any HTML to the browser. ```tsx 8: return ( 9: <main className="container mx-auto py-12 px-4 max-w-4xl min-h-screen"> ``` The outermost container for our blog feed. - `container mx-auto`: Centers the block horizontally on the screen. - `py-12 px-4`: Adds padding to the top/bottom (3rem) and sides (1rem). - `max-w-4xl`: Prevents the feed from becoming too wide on huge ultra-wide monitors, keeping it highly readable. - `min-h-screen`: Ensures the page background extends all the way down even if you only have one short post. ### The Header Section ```tsx 11: <div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-6 border-b border-border pb-8 mb-12"> ``` A flexbox container for the header. - `border-b border-border pb-8 mb-12`: Creates a nice subtle line at the bottom of the header, with padding before the line and margin after the line to separate it from the posts. ```tsx 13: <div className="flex items-center gap-2 text-indigo-500 mb-2"> 14: <BookOpen className="h-5 w-5" /> 15: <span className="text-sm font-semibold tracking-wider uppercase">Ship Logs</span> 16: </div> ``` Renders the little "Ship Logs" label above the title. - `text-indigo-500`: Gives the text and the BookOpen icon a purple/blue tint. - `tracking-wider uppercase`: Spaces out the letters and forces them to be all caps for a premium "kicker" aesthetic. ```tsx 17: <h1 className="text-4xl font-extrabold tracking-tight text-foreground sm:text-5xl"> 18: Rough at Sea 19: </h1> 20: <p className="text-muted-foreground text-base sm:text-lg mt-3 max-w-2xl leading-relaxed"> 21: A chronicle of build logs, hopeposts, and late-night architectural notes. Documenting the struggle sessions as we sail. 22: </p> ``` Renders the main title and the descriptive paragraph below it. ### The Empty State ```tsx 27: {posts.length === 0 ? ( 28: <div className="flex flex-col items-center justify-center p-12 text-center border rounded-2xl bg-card/30"> 29: <p className="text-muted-foreground text-lg">No log entries have been published yet.</p> 30: <p className="text-muted-foreground text-sm mt-1">Check back later once the captain logs the next journey.</p> 31: </div> ``` A classic React ternary operator (condition ? true : false). If the database query returns 0 posts, it renders this friendly placeholder box instead of an empty screen. - `rounded-2xl`: Gives it very soft, modern rounded corners. - `bg-card/30`: Uses our theme's card color but at 30% opacity, giving it a subtle transparent/glassy effect. ### The Posts Feed ```tsx 32: ) : ( 33: <div className="space-y-10"> ``` If there are posts, it renders this div. The `space-y-10` class automatically adds a 2.5rem vertical gap between every single post in the list so we don't have to manually add margins to them. ```tsx 34: {posts.map((post) => ( 35: <article 36: key={post.id} 37: className="group relative flex flex-col items-start gap-3 p-6 -mx-6 rounded-2xl hover:bg-accent/40 border border-transparent hover:border-border transition-all duration-300" 38: > ``` Loops (maps) through our array of posts from the database and renders an &lt;article&gt; block for each one. - `key={post.id}`: React strictly requires a unique key for items in a list so it can keep track of them. - `group`: This is a very powerful Tailwind class. It labels this entire article as a "group", allowing child elements to react when the parent article is hovered. - `-mx-6 p-6`: A clever CSS trick that pulls the article slightly off the margins and pads it internally so the hover background extends slightly beyond the text. - `hover:bg-accent/40 hover:border-border`: When the user hovers their mouse over the article, it fades into a subtle highlighted background with a border. - `transition-all duration-300`: Makes the hover effect fade in smoothly over 300 milliseconds. ```tsx 40: <div className="flex items-center gap-1.5 text-xs text-muted-foreground"> 41: <Calendar className="h-3.5 w-3.5" /> 42: <time dateTime={post.createdAt?.toISOString()}> 43: {new Date(post.createdAt!).toLocaleDateString("en-US", { ... })} 44: </time> 45: </div> ``` Renders the little calendar icon and formats the Javascript Date object into a readable string (e.g., "May 20, 2026"). ```tsx 51: <h2 className="text-2xl font-bold tracking-tight text-foreground group-hover:text-primary transition-colors"> 52: <Link href={`/blog/${post.slug}`}> 53: {post.title} 54: </Link> 55: </h2> ``` Renders the title of the post. - `group-hover:text-primary`: Because the parent &lt;article&gt; has the group class, hovering anywhere on the article changes the title's color to your primary accent color. ```tsx 58: <p className="text-muted-foreground text-sm sm:text-base leading-relaxed line-clamp-3"> 59: {post.content} 60: </p> ``` Renders a small preview snippet of the post's markdown content. - `line-clamp-3`: A Tailwind utility that cuts off the text with an ellipsis (...) after exactly 3 lines, preventing massive walls of markdown from ruining the feed layout. ```tsx 64: <div className="mt-2"> 65: <Link 66: href={`/blog/${post.slug}`} 67: className="inline-flex items-center gap-1 text-sm font-semibold text-indigo-500 hover:text-indigo-600 dark:hover:text-indigo-400 group/link" 68: > 69: Read full post 70: <ArrowRight className="h-4 w-4 transform group-hover/link:translate-x-1 transition-transform" /> 71: </Link> 72: </div> ``` Renders the final "Read full post" link. - `group/link`: Creates a nested subgroup specifically for this link. - `group-hover/link:translate-x-1`: When the user hovers directly over the link itself, the ArrowRight icon physically slides 4 pixels to the right. ```tsx 73: </article> 74: ))} 75: </div> 76: )} 77: </main> 78: ); 79: } ``` Closes the loop, the conditional statement, the main container, the return statement, and the function block. ## `src/app/dashboard/page.tsx` This file services the /dashboard route. This is the page that an authenticated user sees and displays a list of both published and unpublished posts, as well as buttons to view, edit, and delete existing posts, or create a new post. ### The Imports ```tsx 1: import { getServerSession } from "next-auth"; 2: import { redirect } from "next/navigation"; 3: import { authOptions } from "@/lib/auth"; ``` Imports the necessary tools to check if a user is logged in. getServerSession securely checks the user's cookies on the server, redirect allows you to force the browser to a different URL, and authOptions contains your specific NextAuth configuration (Google OAuth settings, etc). ```tsx 4: import { getPostsByAuthor } from "@/features/posts/queries"; ``` Imports our database query that fetches posts belonging only to a specific user ID, ensuring authors can only see and edit their own posts. ```tsx 5: import Link from "next/link"; 6: import { Button } from "@/components/ui/button"; 7: import { Plus, FileText, ExternalLink, Pencil } from "lucide-react"; 8: import { DeletePostButton } from "@/features/posts/components/DeletePostButton"; ``` Imports the internal router link, your customized UI button, a handful of Lucide icons, and a specialized Client Component (DeletePostButton) that handles the logic and popup confirmations for deleting a post. ### The Component & Authentication ```tsx 10: export default async function DashboardPage() { 11: // 1. Protect the route by checking the NextAuth session 12: const session = await getServerSession(authOptions); 13: if (!session?.user?.id) { 14: redirect("/api/auth/signin"); 15: } ``` Defines your asynchronous Server Component. Lines 12-15 act as a strict bouncer for your route. It checks if the user is authenticated. If they aren't (!session), it completely stops rendering this page and instantly kicks them to the sign-in screen. No unauthorized user can ever see the code past line 15. ```tsx 17: // 2. Fetch the user's posts using the query we created 18: const posts = await getPostsByAuthor(session.user.id); ``` Since we now know the user is securely logged in, we query the database for all posts authored by their unique Google user ID. ### The Layout & Header ```tsx 20: return ( 21: <main className="container mx-auto py-10 px-4 max-w-5xl"> ``` Starts the JSX rendering, wrapping the page in a container that tops out at max-w-5xl (slightly wider than your public blog feed, giving us more horizontal room for admin tables/lists). ```tsx 23: <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 border-b border-border pb-8 mb-8"> ``` A flex container for our dashboard header. On mobile (flex-col), the title and "Write New Post" button stack vertically. On desktops (sm:flex-row), they sit side-by-side on opposite ends of the screen (justify-between). ```tsx 25: <h1 className="text-4xl font-extrabold tracking-tight bg-gradient-to-r from-blue-600 via-indigo-500 to-sky-500 bg-clip-text text-transparent dark:from-blue-400 dark:to-sky-400"> 26: Captain's Log 27: </h1> ``` Renders the gradient text for our title. - `bg-gradient-to-r`: Creates a left-to-right gradient. - `bg-clip-text text-transparent`: Forces the gradient to only show up inside the letters themselves, making the actual text transparent so the background gradient shines through. ```tsx 28: <p className="text-muted-foreground text-sm mt-2"> 29: Welcome back, <span className="font-semibold text-foreground">{session.user.name || session.user.email}</span>. Manage your posts and drafts below. 30: </p> ``` A personalized welcome message that pulls the user's name (or email, as a fallback) directly from their Google session data. ```tsx 32: <div className="flex items-center gap-4"> 33: <Link href="/dashboard/new"> 34: <Button className="shadow-lg hover:shadow-indigo-500/20 transition-all flex items-center gap-2"> 35: <Plus className="h-4 w-4" /> 36: Write New Post 37: </Button> 38: </Link> 39: </div> ``` Renders the "Write New Post" button with a nice glowing drop-shadow on hover, linking directly to your composer page. ### The Empty State ```tsx 43: {posts.length === 0 ? ( 44: <div className="flex flex-col items-center justify-center border-2 border-dashed border-muted rounded-2xl p-16 text-center bg-card/30 backdrop-blur-sm"> 45: <div className="p-4 bg-muted/50 rounded-full mb-4"> 46: <FileText className="h-10 w-10 text-muted-foreground" /> 47: </div> ... 57: </Link> 58: </div> If the user has not written any posts yet, it renders this empty state. - `border-dashed border-2`: Creates a nice dotted line around the box instead of a solid border. - `backdrop-blur-sm`: Gives the box a frosted-glass effect if there happens to be anything colorful underneath it. Inside, it contains a large document icon, some text, and another button prompting you to create your first post. ### The Dashboard List ```tsx 59: ) : ( 60: <div className="grid gap-4"> ``` If we do have posts, it creates a CSS Grid layout with a 1rem gap between each item. ```tsx 61: {posts.map((post) => ( 62: <div 63: key={post.id} 64: className="group relative flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 p-6 border rounded-xl bg-card hover:bg-accent/40 hover:border-muted-foreground/30 hover:shadow-md transition-all duration-300" 65: > ``` Loops through our posts. Similar to the blog feed, this creates a card for each post that changes background color and gains a slight drop-shadow (hover:shadow-md) when hovered. ```tsx 68: <h2 className="text-xl font-bold tracking-tight text-foreground group-hover:text-primary transition-colors"> 69: {post.title} 70: </h2> ``` Renders the title of the post. ```tsx 71: {post.isPublished ? ( 72: <span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20"> 73: <span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> 74: Published 75: </span> 76: ) : ( 77: <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20"> 78: Draft 79: </span> 80: )} ``` Checks if the post is published or a draft, and renders a "badge" accordingly. The `Published` badge uses an emerald/green color scheme and features a tiny green dot `(h-1.5 w-1.5)` that pulses using Tailwind's built-in animate-pulse utility. The `Draft` badge uses a yellow/amber color scheme to indicate it's pending. ```tsx 82: <div className="text-sm text-muted-foreground flex items-center gap-2"> 83: <span> 84: {new Date(post.createdAt!).toLocaleDateString("en-US", { ... })} 85: </span> 86: {post.updatedAt && ( 87: <> 88: <span>•</span> 89: <span> 90: Updated {new Date(post.updatedAt).toLocaleDateString("en-US", { ... })} 91: </span> 92: </> 93: )} 94: </div> ``` Renders the post's creation date. If the post has been updated since creation (post.updatedAt), it also renders a small bullet point (•) followed by the last updated date. ```tsx 104: <div className="flex items-center gap-2 flex-wrap"> 105: <Link href={`/blog/${post.slug}`}> 106: <Button variant="outline" size="sm" className="flex items-center gap-1.5"> 107: View 108: <ExternalLink className="h-3.5 w-3.5" /> 109: </Button> 110: </Link> ``` Renders the "View" button, which takes you to the live public view of the post. It uses the outline variant (a transparent button with a border). ```tsx 111: <Link href={`/dashboard/edit/${post.id}`}> 112: <Button variant="secondary" size="sm" className="flex items-center gap-1.5"> 113: <Pencil className="h-3.5 w-3.5" /> 114: Edit 115: </Button> 116: </Link> ``` Renders the "Edit" button, pointing to our composer page, using the secondary variant (usually a muted gray background). ```tsx 117: <DeletePostButton postId={post.id} /> 118: </div> 119: </div> 120: ))} 121: </div> 122: )} 123: </main> 124: ); 125: } ``` Renders the interactive delete button (passing it the specific post.id so it knows which database record to destroy), and then closes out the loops, containers, return block, and function. ## `src\app\blog\[slug]\page.tsx` This file services the /blog/[slug] route. ### Why [slug] is in Square Brackets In Next.js (using the App Router), square brackets [] signify a Dynamic Route Segment. Instead of hard coding every single blog post URL (like /blog/hello-world, /blog/my-second-post, etc.), putting the folder name in brackets allows Next.js to treat that part of the URL as a variable. If a user visits /blog/my-blog-post, Next.js will route them to this page component and pass "my-blog-post" as the value for the slug parameter. You can name the variable anything (like [id] or [postName]), but [slug] is the standard convention for SEO-friendly string identifiers. Now let's look at a line-by-line explanation of how this file works. ### Imports Section ```tsx 1: import { notFound } from "next/navigation"; ``` Imports a Next.js utility that, when called, immediately halts rendering and shows the nearest 404 Not Found page. ```tsx 2: import { getPublishedPostBySlug } from "@/features/posts/queries"; ``` Imports a custom database query function that fetches a single blog post's data based on its URL slug. ```tsx 3: import ReactMarkdown from "react-markdown"; ``` Imports a library used to convert raw Markdown text into HTML elements. ```tsx 4: import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; ``` Imports a component that provides syntax highlighting for code blocks. ```tsx 5: import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"; ``` Imports the specific color theme (VS Code Dark+) for the syntax highlighter. ```tsx 6: import { Calendar, ChevronLeft } from "lucide-react"; ``` Imports two SVG icons (a calendar and a left-facing arrow) from the lucide-react icon library. ```tsx 7: import Link from "next/link"; ``` Imports the Next.js Link component, which enables fast client-side navigation between pages. ```tsx 8: import { Button } from "@/components/ui/button"; ``` Imports a reusable, pre-styled Button component (likely from Shadcn UI). ### Props Type Definition ```tsx 10: interface BlogPostPageProps { 11. params: Promise<{ slug: string }>; 12. } ``` Defines the TypeScript types for the page. In modern Next.js (v15+), dynamic route parameters (params) are passed to the page as a Promise containing the route variables (in this case, { slug: string }). ### Component Definition & Data Fetching ```tsx 14: export default async function BlogPostPage({ params }: BlogPostPageProps) { ``` Defines and exports the default React component for this route. It is an async Server Component, meaning it can fetch data directly on the server before sending HTML to the client. It accepts the params prop. ```tsx 15: const resolvedParams = await params; ``` Awaits the params promise to extract the actual object, so we can access the slug value from the URL. ```tsx 16: const post = await getPublishedPostBySlug(resolvedParams.slug); ``` Calls our database query with the URL's slug to fetch the corresponding blog post content. Error Handling ```tsx 18: // If the post does not exist or is not published, render 404 19: if (!post) { 20. notFound(); 21. } ``` Checks if the database returned a post. If the post doesn't exist (or isn't published), it triggers notFound(), which interrupts the component and renders a 404 page. ### Page Structure & Layout ```tsx 23: return ( 24. <main className="container mx-auto py-12 px-4 max-w-3xl min-h-screen"> ``` Begins the JSX structure. `main` acts as the semantic wrapper. The Tailwind classes (container mx-auto max-w-3xl) center the content and constrain the maximum width for readability. ```tsx 25: {/* Back Navigation */} 26: <div className="mb-8"> 27: <Link href="/blog"> 28: <Button variant="ghost" size="sm" className="flex items-center gap-1 -ml-3 text-muted-foreground hover:text-foreground"> 29: <ChevronLeft className="h-4 w-4" /> 30: Back to Blog 31: </Button> 32: </Link> 33: </div> ``` Renders the "Back to Blog" navigation button. It uses the &lt;Link&gt; component to navigate back to /blog and displays the ChevronLeft icon inside the custom &lt;Button&gt;. ### Article Header ```tsx 35: <article className="space-y-8"> ``` Wraps the main blog content in an &lt;article&gt; semantic tag and adds vertical spacing (space-y-8) between the header and the body. ```tsx 36: <header className="... border-b pb-6"> ``` Creates a header section with a bottom border to visually separate the title from the content. ```tsx 37: {/* Title */} 38: <h1 className="text-4xl font-extrabold tracking-tight text-foreground sm:text-5xl"> 39: {post.title} 40: </h1> ``` Renders the post's title (post.title) inside an &lt;h1&gt; tag with styling for large, bold text. ```tsx 42: {/* Meta info */} 43: <div className="flex items-center gap-2 text-sm text-muted-foreground"> 44: <Calendar className="h-4 w-4" /> 45: <time dateTime={post.createdAt?.toISOString()}> 46: {new Date(post.createdAt!).toLocaleDateString("en-US", { 47: month: "long", 48: day: "numeric", 49: year: "numeric", 50: })} 51: </time> 52: </div> 53: </header> ``` Renders the post's publication date. It shows the Calendar icon next to an HTML <time> element. It converts the post's createdAt timestamp into a readable format (e.g., "October 12, 2023") using standard JavaScript toLocaleDateString. ### Markdown Content Rendering ```tsx 56: <div className="prose dark:prose-invert max-w-none leading-relaxed"> ``` This wrapper uses the @tailwindcss/typography plugin (prose). This class automatically applies beautiful, standard styling (like margins, font sizes, and line heights) to all the raw HTML tags (like p, h2, ul) that the Markdown parser will generate. ```tsx 57: <ReactMarkdown ``` Starts the Markdown renderer. 58-78: components={{ code(props) { ... } }} ```tsx 58: components={{ 59: code(props) { 60: const { children, className, node, ref, ...rest } = props; 61: const match = /language-(\w+)/.exec(className || ""); 62: const isBlock = match || String(children).includes("\n"); 63: 64: return isBlock ? ( 65: <SyntaxHighlighter 66: {...rest} 67: PreTag="div" 68: children={String(children).replace(/\n$/, "")} 69: language={match ? match[1] : "text"} 70: style={vscDarkPlus} 71: /> 72: ) : ( 73: <code {...rest} className={className}> 74: {children} 75: </code> 76: ); 77: }, 78: }} ``` This block overrides how ReactMarkdown renders code. - It extracts the className to see if a language was specified (e.g., language-python generated from ```python``` in Markdown). - If it detects a language or multiple lines of code (isBlock), it renders the SyntaxHighlighter component to provide nicely colored code blocks (Lines 64-71). - If it's just a quick inline code snippet (e.g. `like this`), it falls back to rendering a standard HTML &lt;code&gt; tag (Lines 72-76). ```tsx 80: {post.content} ``` Feeds the raw markdown text stored in your database into the ReactMarkdown component to be processed. ```tsx 81: </ReactMarkdown> 82: </div> 83: </article> 84: </main> 85: ); 86: } ``` Closes all the open HTML tags and the component function. ## `src/features/posts/actions.ts` In modern Next.js architecture, `actions.ts` files are used to define Server Actions. Semantically, this file serves as the backend API layer for mutating blog post data. Instead of writing traditional REST API routes (like POST /api/posts), Server Actions allow you to write secure, server-side asynchronous functions that can be called directly from your React components (like a form submission or a button click). This specific file encapsulates the Write/Mutation operations (Create, Update, Delete) for your blog. For every operation, it enforces a strict security boundary by executing three critical steps: 1. Authentication & Authorization: Is the user logged in, and do they own this post? 2. Data Validation: Is the incoming form data formatted correctly? 3. Database Mutation & Cache Invalidation: Modify the database via Drizzle ORM, and then tell Next.js to purge its caches so the UI updates instantly. Below is a line-by-line breakdown of how the code works. ```tsx 1: "use server"; ``` This Next.js directive is crucial. It guarantees that all functions exported from this file will only execute on the server. The code (and any database credentials it uses) will never be bundled or sent to the browser. ```tsx 3: import { getServerSession } from "next-auth"; 4. import { authOptions } from "@/lib/auth"; ``` Imports the tools needed to securely verify the user's identity based on their cookies. ```tsx 5: import { db } from "@/lib/db"; ``` Imports the initialized Drizzle ORM database connection. ```tsx 6: import { posts, createPostSchema } from "./schema"; ``` Imports the posts table definition for database queries and the Zod schema (createPostSchema) for strict data validation. ```tsx 7: import { revalidatePath } from "next/cache"; ``` Imports the Next.js cache purging utility. ```tsx 8: import { eq } from "drizzle-orm"; ``` Imports the SQL equality operator (e.g., WHERE column = value) for Drizzle queries. ```tsx 10: export async function createPostAction(formData: unknown) { ``` Defines the action taking unknown/raw input data from the client. ```tsx 11: // 1. Authenticate the request 12: const session = await getServerSession(authOptions); 13: if (!session?.user?.id) { 14: throw new Error("Unauthorized: You must be logged in to post."); 15: } ``` This code checks authentication by fetcheing the session. If session.user.id is missing, it throws an error to abort the operation. ```tsx 17: // 2. Validate the incoming data against our strict Zod schema 18: const parsedData = createPostSchema.safeParse(formData); 19: 20: if (!parsedData.success) { 21: throw new Error("Invalid form data"); 22: } ``` This code performs validation by feeding the raw formData into the Zod schema using safeParse. If it fails validation (e.g., missing title), it aborts and throws an "Invalid form data" error. ```tsx 24: const { title, content, isPublished } = parsedData.data; ``` Extracts the now strongly-typed, validated data. ```tsx 26: // 3. Generate a simple slug 27: const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, ''); ``` Takes the title, converts it to lowercase, swaps spaces/symbols for hyphens, and removes trailing hyphens to generate a URL-friendly slug. ```tsx 29: // 4. Execute the database mutation 30: await db.insert(posts).values({ 31: title, 32: slug, 33: content, 34: isPublished, 35: authorId: session.user.id, 36: }); ``` This code performs Database Inserts. It uses Drizzle (db.insert().values()) to insert the new post into the Postgres database. Notice how it securely attaches session.user.id as the authorId, ensuring the user can't spoof creating a post for someone else. ```tsx 38: // 5. Tell Next.js to purge the cache for the blog feed so the new post appears instantly 39: revalidatePath("/"); 40: revalidatePath("/blog"); 41: revalidatePath("/dashboard"); ``` This code performs Cache Invalidation. It calls revalidatePath for the home page, /blog, and /dashboard. This tells Next.js: "Data changed! Delete your cached HTML for these pages and re-render them next time someone visits." ```tsx 43: return { success: true }; ``` Sends a success signal back to the frontend component that called it. ```tsx 46: export async function updatePostAction(id: number, formData: unknown) { ``` Similar to create, but requires the specific post id being updated. Lines 47-51: Validates the user is logged in. Same as above. Lines 53-59: Validates the new form data. Same as above. ```tsx 61: // 3. Verify ownership 62: const [existingPost] = await db 63: .select() 64: .from(posts) 65: .where(eq(posts.id, id)) 66: .limit(1); 67: 68: if (!existingPost) { 69: throw new Error("Post not found"); 70: } ``` This code queries the database for the existing post matching the id. If no post is found, it throws a "Post not found" error. ```tsx 72: if (existingPost.authorId !== session.user.id) { 73: throw new Error("Unauthorized: You do not own this post."); 74: } ``` This code performs authorization. It compares the database's existingPost.authorId against the active user's session.user.id. If they don't match, the user is trying to edit someone else's post, so it throws an "Unauthorized" error. ```tsx 77: // 4. Update the slug 78: const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, ''); ``` This code recalculates the slug just in case the title was changed. ```tsx 79: // 5. Update DB 80: await db 81: .update(posts) 82: .set({ 83: title, 84: slug, 85: content, 86: isPublished, 87: updatedAt: new Date(), 88: }) 89: .where(eq(posts.id, id)); ``` This code executes the SQL UPDATE statement via Drizzle (db.update().set().where()). It also updates the updatedAt timestamp. ```tsx 91: // 6. Revalidate caches 92: revalidatePath("/dashboard"); 93: revalidatePath("/blog"); 94: revalidatePath(`/blog/${slug}`); 95: revalidatePath(`/blog/${existingPost.slug}`); 96: revalidatePath("/"); ``` This code invalidates caches. Notice it revalidates both the new slug URL and the old existing slug URL (existingPost.slug), ensuring the old page gets wiped from the cache if the title changed. ```tsx 101: export async function deletePostAction(id: number) { ``` Defines the deletion action requiring only the post id. ```tsx 102: // 1. Authenticate request 103: const session = await getServerSession(authOptions); 104: if (!session?.user?.id) { 105: throw new Error("Unauthorized: You must be logged in."); 106: } ``` Checks if the user is logged in. ```tsx 108: // 2. Verify ownership 109: const [existingPost] = await db 110: .select() 111: .from(posts) 112: .where(eq(posts.id, id)) 113: .limit(1); 114: 115: if (!existingPost) { 116: throw new Error("Post not found"); 117: } ``` This code fetches the post to ensure it exists in the database. ```tsx if (existingPost.authorId !== session.user.id) { throw new Error("Unauthorized: You do not own this post."); } ``` This code performs authorization. As before, it strictly verifies that the user trying to delete the post is the original author. ```tsx 123: // 3. Delete from DB 124: await db.delete(posts).where(eq(posts.id, id)); ``` This code executes the SQL DELETE statement (db.delete().where()). ```tsx 126: // 4. Revalidate caches 127: revalidatePath("/dashboard"); 128: revalidatePath("/blog"); 129: revalidatePath(`/blog/${existingPost.slug}`); 130: revalidatePath("/"); ``` This code invalidates caches, specifically clearing the URL of the now-deleted post (/blog/${existingPost.slug}) so future visitors get a proper 404 instead of a cached ghost page. ```tsx 132: return { success: true }; ``` Returns success.