Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/morph/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build Commands

- Development: `pnpm dev`
- Build: `pnpm build`
- Lint: `pnpm lint`
- Format: `pnpm format`
- Format check: `pnpm format:check`
- Database migrations: `pnpm migrate`

## Code Style Guidelines

- TypeScript with strict typing (avoid `any` when possible)
- Use Next.js and React best practices with functional components
- Format: 100 char width, single quotes, no semicolons, trailing commas
- Naming: camelCase for variables/functions, PascalCase for components/classes, and snake-case for filenames.
- Imports: Follow order - React/Next imports first, then external libs, internal modules
- Error handling: Prefer early returns over deep nesting
- Component organization: Keep components focused on single responsibility
- Performance: Use memoization where appropriate, ensure proper cleanup in useEffect

## Architecture

- Next.js frontend with TypeScript
- DrizzleORM for database operations
- React Context for state management
- Follow existing patterns when adding new components or features
4 changes: 3 additions & 1 deletion packages/morph/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1973,7 +1973,9 @@ canvas {
max-width: 300px;
min-width: 260px;
width: auto;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 5px 10px -5px rgba(0, 0, 0, 0.05);
box-shadow:
0 10px 25px -5px rgba(0, 0, 0, 0.1),
0 5px 10px -5px rgba(0, 0, 0, 0.05);
}

/* Hide the default CodeMirror search panel */
Expand Down
4 changes: 2 additions & 2 deletions packages/morph/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PlausibleProvider from "next-plausible"
import Script from "next/script"
import type React from "react"

import { Toaster } from "@/components/ui/toaster"
import { Toaster } from "@/components/ui/sonner"

import ClientProvider from "@/context/providers"

Expand Down Expand Up @@ -49,7 +49,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<PlausibleProvider domain="morph-editor.app" trackOutboundLinks trackFileDownloads>
<ClientProvider>{children}</ClientProvider>
</PlausibleProvider>
<Toaster />
<Toaster position="bottom-center" />
{process.env.NODE_ENV === "production" && (
<Script
defer
Expand Down
11 changes: 0 additions & 11 deletions packages/morph/app/template.tsx

This file was deleted.

74 changes: 17 additions & 57 deletions packages/morph/app/vaults/[vault]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,42 @@
"use client"

import { motion } from "motion/react"
import { useParams } from "next/navigation"
import { useCallback, useEffect } from "react"
import { useEffect } from "react"

import Editor from "@/components/editor"

import { FilePreloader } from "@/context/providers"
import { useNotesContext } from "@/context/notes"
import { useVaultContext } from "@/context/vault"

// Define page transition variants
const pageVariants = {
initial: {
opacity: 0,
scale: 0.98,
},
animate: {
opacity: 1,
scale: 1,
transition: {
duration: 0.4,
ease: [0.25, 0.1, 0.25, 1],
staggerChildren: 0.1,
},
},
exit: {
opacity: 0,
scale: 0.98,
transition: {
duration: 0.3,
ease: [0.25, 0.1, 0.25, 1],
},
},
};

// Define child element variants for staggered animation
const childVariants = {
initial: { opacity: 0, y: 10 },
animate: {
opacity: 1,
y: 0,
transition: { duration: 0.3 }
},
};

export default function VaultPage() {
const params = useParams()
const vaultId = params.vault as string
const { vaults } = useVaultContext()
const { dispatch } = useNotesContext()

// Set active vault ID in localStorage whenever the vault ID changes
useEffect(() => {
if (vaultId) {
localStorage.setItem("morph:active-vault", vaultId)
// Set the current vault ID in the notes context
dispatch({ type: 'SET_CURRENT_VAULT_ID', vaultId })
}
}, [vaultId])

const MemoizedEditor = useCallback(
() => <Editor vaultId={vaultId} vaults={vaults} />,
[vaultId, vaults],
)
// Clean up when unmounting
return () => {
dispatch({ type: 'SET_CURRENT_VAULT_ID', vaultId: null })
dispatch({ type: 'SET_CURRENT_FILE_ID', fileId: null })
dispatch({ type: 'CLEAR_NOTES' })
}
}, [vaultId, dispatch])

return (
<motion.main
className="min-h-screen bg-background overflow-hidden"
variants={pageVariants}
initial="initial"
animate="animate"
exit="exit"
layoutId={`vault-page-${vaultId}`}
>
<motion.div
variants={childVariants}
className="w-full h-full"
>
<main className="min-h-screen bg-background overflow-hidden">
<div className="w-full h-full">
<FilePreloader />
<MemoizedEditor />
</motion.div>
</motion.main>
<Editor vaultId={vaultId} vaults={vaults} />
</div>
</main>
)
}
9 changes: 8 additions & 1 deletion packages/morph/app/vaults/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ const Notch = memo(function Notch({ onClickBanner }: { onClickBanner: () => void
}}
>
<defs>
<filter id="shadow" x="-10%" y="-10%" width="120%" height="150%" colorInterpolationFilters="sRGB">
<filter
id="shadow"
x="-10%"
y="-10%"
width="120%"
height="150%"
colorInterpolationFilters="sRGB"
>
{/* Combine multiple drop shadows into a single filter for better performance */}
<feDropShadow dx="0" dy="1.5" stdDeviation="1.5" floodColor="rgba(251, 191, 36, 0.5)" />
</filter>
Expand Down
83 changes: 53 additions & 30 deletions packages/morph/components/author-processor.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,78 @@
import { useAuthorTasks } from "@/context/authors"
import { useQueryAuthorStatus } from "@/services/authors"
import { useEffect } from "react"
import { useCallback, useEffect, memo } from "react"
import { PgliteDatabase } from "drizzle-orm/pglite"

import { useAuthorTasks } from "@/context/authors"

import * as schema from "@/db/schema"

export function AuthorProcessor() {
export const AuthorProcessor = memo(function AuthorProcessor({
db,
}: {
db: PgliteDatabase<typeof schema>
}) {
const { pendingTaskIds, getFileIdForTask, removeTask } = useAuthorTasks()

// Process each pending task in parallel with separate queries
return (
<>
{pendingTaskIds.map((taskId) => (
<AuthorTaskProcessor
key={taskId}
taskId={taskId}
fileId={getFileIdForTask(taskId)}
onComplete={removeTask}
/>
))}
</>
// Memoize the removeTask callback to prevent unnecessary rerenders
const handleTaskComplete = useCallback(
(taskId: string) => {
removeTask(taskId)
},
[removeTask]
)
}

// Create a memoized component factory function for task processors
const TaskProcessors = useCallback(() => {
if (pendingTaskIds.length === 0) return null

return pendingTaskIds.map((taskId) => (
<AuthorTaskProcessor
key={taskId}
taskId={taskId}
fileId={getFileIdForTask(taskId)}
db={db}
onComplete={() => handleTaskComplete(taskId)}
/>
))
}, [pendingTaskIds, getFileIdForTask, handleTaskComplete, db])

// Return the task processors
return <TaskProcessors />
})

function AuthorTaskProcessor({
taskId,
fileId,
db,
onComplete,
}: {
taskId: string
fileId: string
db: PgliteDatabase<typeof schema>
onComplete: (taskId: string) => void
}) {
// Use the query hook to poll the task status
const { data, isError } = useQueryAuthorStatus(taskId, fileId)
const { data, isSuccess, isError } = useQueryAuthorStatus(taskId, fileId, db)

// When the task completes (success or failure), remove it from pending tasks
// When task is successful or fails, call the onComplete callback
useEffect(() => {
if (!data) return
// Check for success status
const isSuccessful =
isSuccess && data && typeof data === "object" && "status" in data && data.status === "success"

const isComplete =
data.status === "success" || data.status === "failure" || data.status === "cancelled"
// Check for failure status
const isFailed =
(isSuccess &&
data &&
typeof data === "object" &&
"status" in data &&
(data.status === "failure" || data.status === "cancelled")) ||
isError

if (isComplete) {
onComplete(taskId)
}
}, [data, taskId, onComplete])

// If there's an error, also remove the task
useEffect(() => {
if (isError) {
if (isSuccessful || isFailed) {
onComplete(taskId)
}
}, [isError, taskId, onComplete])
}, [isSuccess, isError, data, taskId, onComplete])

// This component doesn't render anything visible
return null
Expand Down
Loading
Loading