diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..1a1cf25
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,370 @@
+# JavaScript Mastery Development Guidelines
+
+This document outlines the coding standards, principles, and processes that define how we build educational content at JavaScript Mastery.
+
+## Core Philosophy
+
+We are not just shipping code to production—we are using code as a medium of knowledge transfer. Every line of code must be:
+
+- **Clean**: Simple to understand, simple to write
+- **Educational**: Easy to explain line by line
+- **Optimized**: Not the shortest or hackiest, but the cleanest
+- **Non-duplicated**: Follow DRY principles strictly
+
+> "I want to confidently go through the code and explain every single line like it is the best line of code you have ever written."
+
+---
+
+## Code Quality Standards
+
+### 1. Eliminate Verbose and Repetitive Code
+
+**Bad Example:**
+
+```javascript
+// ❌ Verbose, repetitive, hard to explain
+const openSafari = () => set((state) => ({ ...state, safari: true }));
+const openFinder = () => set((state) => ({ ...state, finder: true }));
+const openTerminal = () => set((state) => ({ ...state, terminal: true }));
+const openContacts = () => set((state) => ({ ...state, contacts: true }));
+// ... repeated for every window
+```
+
+**Good Example:**
+
+```javascript
+// ✅ Clean, reusable, easy to understand
+const openWindow = (windowKey, data) => {
+ set((state) => {
+ const window = state.windows[windowKey];
+ window.isOpen = true;
+ window.zIndex = state.nextZIndex;
+ window.data = data;
+ state.nextZIndex++;
+ });
+};
+```
+
+### 2. Avoid Magic Numbers
+
+Extract constants with meaningful names:
+
+```javascript
+// ❌ Magic number - what does 1000 mean?
+zIndex: 1000;
+
+// ✅ Clear intent
+const INITIAL_Z_INDEX = 1000;
+zIndex: INITIAL_Z_INDEX;
+```
+
+### 3. Proper State Management
+
+When using Zustand, use selectors for referential stability:
+
+```javascript
+// ❌ Object recreation causes unnecessary re-renders
+const { openFinder, closeFinder } = useApplicationStore((state) => ({
+ openFinder: state.openFinder,
+ closeFinder: state.closeFinder,
+}));
+
+// ✅ Best practice: Use individual selectors for stable references
+const openFinder = useApplicationStore((state) => state.openFinder);
+const closeFinder = useApplicationStore((state) => state.closeFinder);
+
+// ✅ Alternative: Direct destructuring works when store actions are defined
+// outside the state and don't cause re-renders
+const { openFinder, closeFinder } = useApplicationStore();
+```
+
+### 4. Eliminate Switch Statement Anti-Patterns
+
+```javascript
+// ❌ Duplicated logic in switch cases
+const handleClick = (type) => {
+ switch (type) {
+ case 'finder':
+ openFinder();
+ break;
+ case 'safari':
+ openSafari();
+ break;
+ case 'contacts':
+ openContacts();
+ break;
+ // ... endless repetition
+ }
+};
+
+// ✅ Generic function
+const openApp = (windowKey) => openWindow(windowKey);
+```
+
+### 5. Use Libraries Over Custom Code
+
+Leverage well-established packages (millions of weekly downloads) instead of reinventing the wheel:
+
+| Instead of... | Use... |
+| ------------------------------ | -------------------- |
+| Custom date formatting | `dayjs` |
+| Manual tooltip positioning | `react-tooltip` |
+| Complex state spread operators | `immer` with Zustand |
+| Custom PDF generation | `react-pdf` |
+| Manual component styling | `shadcn/ui` |
+
+**Example:** dayjs replaces 50+ lines of custom date code with a single line.
+
+### 6. Higher-Order Components for Shared Logic
+
+When multiple components share the same functionality (dragging, focusing, opening/closing animations), use a wrapper component:
+
+```javascript
+// ✅ Window wrapper handles all shared logic
+const WindowWrapper = ({ children, windowKey }) => {
+ const { isOpen, zIndex } = useWindowStore(windowKey);
+ const { handleDragStart, handleFocus } = useWindowHandlers(windowKey);
+
+ if (!isOpen) return null;
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Individual windows only contain their unique JSX
+const TerminalWindow = () => (
+
+
+
+
+);
+```
+
+### 7. Separation of Concerns
+
+- Split stores by functionality (windows store, location store, etc.)
+- Keep components focused on single responsibilities
+- Extract constants to dedicated files
+
+---
+
+## Naming Conventions
+
+- Use clear, descriptive variable names
+- Avoid generic names like `test`, `data`, `temp`
+- Let AI tools (ChatGPT) suggest human-readable names based on context
+- Variable names should make the code self-documenting
+
+```javascript
+// ❌ Unclear
+const x = 1 + 0.25 * Math.exp(-d * d);
+
+// ✅ Self-documenting
+const intensity = 1 + MAGNIFICATION_FACTOR * Math.exp(-distance * distance);
+const scale = BASE_SCALE + intensity * SCALE_MULTIPLIER;
+```
+
+---
+
+## Refactoring Mindset
+
+Always ask yourself:
+
+1. How can I make this shorter?
+2. How can I make this easier to understand?
+3. How can I make this easier to manage?
+4. Is there a package that does this better?
+
+**Use AI tools for refactoring:**
+
+- Share working code with ChatGPT
+- Ask: "Make it cleaner, more concise, but also easier to understand"
+- Ask: "Improve naming"
+- Ask: "Are there too many spread operators? Is there a better way?"
+
+---
+
+## Documentation & Recording Guidelines
+
+### Recording Documentation Structure
+
+Documentation is NOT just code comments—it's the step-by-step guide for recreating the development process.
+
+**Key Principles:**
+
+1. **Introduce concepts when they're used**, not before
+
+ - Don't install 10 libraries at the start
+ - Introduce each library/concept right before it's needed
+
+2. **Explain the "why"**, not just the "what"
+
+ - Why did you choose this approach?
+ - What alternatives didn't work?
+ - What bugs did you encounter?
+
+3. **Document the struggle**
+
+ - Share failed attempts that led to the solution
+ - Note non-obvious gotchas and edge cases
+
+4. **Treat the reader as a beginner**
+
+ - Even if something seems obvious to you, explain it
+ - One missing line can cost hours of debugging
+
+5. **Provide constants/mock data at point of use**
+
+ ```javascript
+ // ❌ Provide all constants at the start
+ import { NAV_LINKS, APP_DATA, WINDOW_CONFIG } from './constants';
+
+ // ✅ Provide constants right before mapping over them
+ // (now introduce NAV_LINKS)
+ {NAV_LINKS.map(link => ...)}
+ ```
+
+---
+
+## Project Development Order
+
+Structure video builds to maximize early visual impact:
+
+1. **Start with visible, exciting features** - Give viewers immediate dopamine
+2. **Use mock data first** - Display realistic content before implementing CRUD
+3. **Delay boring setup** - Don't start with auth if it takes 30 minutes
+4. **80/20 Rule** - 20% of features for 80% of the wow factor
+
+**Example Build Order for Mac OS Portfolio:**
+
+1. ✅ Navbar (simple, visual)
+2. ✅ Welcome screen with background animation (immediate wow)
+3. ✅ Dock with hover effects (core interaction)
+4. ❌ Authentication (later, if needed)
+
+---
+
+## Visual & Content Standards
+
+### No Placeholder Content
+
+Never use:
+
+- Lorem ipsum
+- Broken or placeholder images
+- Generic names like "test", "user1"
+- Random gibberish data
+
+**Always use:**
+
+- Real (but fake) data that makes sense
+- Properly formatted images
+- Pop culture references or funny examples
+- Professional-looking mock content
+
+```javascript
+// ❌ Boring, meaningless
+const user = { name: 'Test User', role: 'test' };
+
+// ✅ Memorable, engaging
+const user = { name: 'Jon Snow', car: 'Aston Martin' };
+```
+
+### Demo Examples Should Be Award-Worthy
+
+Even simple tutorials should use professional visuals:
+
+- Reference Awwwards-winning websites for design inspiration
+- Combine simple concepts with beautiful execution
+- A scroll animation demo should look like it belongs on a real site
+
+---
+
+## Feature Scope Management
+
+### The Balance
+
+Every project must balance:
+
+- **Wow Factor**: Visual appeal and impressiveness
+- **Time to Build**: Respect viewer's time investment
+- **Educational Value**: What they actually learn
+- **Complexity**: Code should remain understandable
+
+### When to Remove Features
+
+Remove features that:
+
+- Take 80% of time but add only 20% value
+- Require complex logic for minimal visual impact
+- Are difficult to explain clearly
+- Break the project's time budget
+
+### Speaking Up
+
+**You are empowered to push back on feature requests.**
+
+If a feature doesn't make sense:
+
+- Explain why it lowers quality
+- Suggest alternatives
+- Propose what could be done instead
+
+> "Don't just listen and implement. Tell me 'I don't think it's the best way to do it. Here are the reasons why, and here's what we can do instead.'"
+
+---
+
+## Ownership Mentality
+
+Don't just write code because you're asked to. Own your work by:
+
+1. **Thinking critically** about project direction
+2. **Suggesting improvements** to features and approach
+3. **Contributing to positioning** (thumbnails, titles, hooks)
+4. **Understanding the viewer** and what excites them
+5. **Balancing education and entertainment**
+
+Ask yourself:
+
+- Would I click on this video?
+- Would I be excited to build this project?
+- What would make this more engaging?
+
+---
+
+## Quality Checklist
+
+Before submitting code for review:
+
+- [ ] No duplicate code blocks
+- [ ] No magic numbers without named constants
+- [ ] No unnecessary object recreation in hooks
+- [ ] Libraries used where appropriate
+- [ ] Higher-order components for shared logic
+- [ ] Clear, descriptive naming
+- [ ] No Lorem ipsum or placeholder content
+- [ ] Features introduced at point of use
+- [ ] Documentation explains the "why"
+- [ ] Failed attempts and gotchas documented
+- [ ] Build order prioritizes visual impact
+
+---
+
+## Remember: Impact
+
+We've helped millions of developers:
+
+- Land their first jobs
+- Escape poverty through new skills
+- Build projects they're proud of
+- Understand concepts that seemed impossible
+
+Every line of code, every explanation, every tutorial has the potential to change someone's life. That's why quality matters.
diff --git a/app/root.tsx b/app/root.tsx
index 7de1272..1fd7930 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from "react";
+import { useEffect, useState } from "react";
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
import "../index.css";
import {
@@ -7,22 +7,32 @@ import {
signOut as puterSignOut,
} from "../lib/puter.action";
+interface AuthState {
+ isSignedIn: boolean;
+ userName: string | null;
+ userId: string | null;
+}
+
+const DEFAULT_AUTH_STATE: AuthState = {
+ isSignedIn: false,
+ userName: null,
+ userId: null,
+};
+
export default function Root() {
- const [isSignedIn, setIsSignedIn] = useState(false);
- const [userName, setUserName] = useState(null);
- const [userId, setUserId] = useState(null);
+ const [authState, setAuthState] = useState(DEFAULT_AUTH_STATE);
const refreshAuth = async () => {
try {
const user = await getCurrentUser();
- setIsSignedIn(!!user);
- setUserName(user?.username || null);
- setUserId(user?.uuid || null);
+ setAuthState({
+ isSignedIn: !!user,
+ userName: user?.username || null,
+ userId: user?.uuid || null,
+ });
return !!user;
} catch {
- setIsSignedIn(false);
- setUserName(null);
- setUserId(null);
+ setAuthState(DEFAULT_AUTH_STATE);
return false;
}
};
@@ -65,9 +75,7 @@ export default function Root() {
diff --git a/app/routes/visualizer.$id.tsx b/app/routes/visualizer.$id.tsx
index 80a025a..c107a82 100644
--- a/app/routes/visualizer.$id.tsx
+++ b/app/routes/visualizer.$id.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from "react";
+import { useEffect, useState } from "react";
import {
Navigate,
useLocation,
@@ -16,6 +16,22 @@ import {
import Visualizer from "@/components/Visualizer";
+const createDesignHistoryItem = (
+ id: string,
+ base: Partial,
+ overrides: Partial = {}
+): DesignHistoryItem => ({
+ id,
+ name: base.name || `Residence ${id}`,
+ sourceImage: base.sourceImage || "",
+ renderedImage: base.renderedImage,
+ renderedPath: base.renderedPath,
+ timestamp: Date.now(),
+ ownerId: base.ownerId || null,
+ isPublic: base.isPublic || false,
+ ...overrides,
+});
+
export default function VisualizerRoute() {
const { id } = useParams();
const navigate = useNavigate();
@@ -57,16 +73,15 @@ export default function VisualizerRoute() {
renderedPath?: string;
}) => {
if (!id) return;
- const updatedItem = {
- id,
- name: resolvedItem?.name || `Residence ${id}`,
+ const updatedItem = createDesignHistoryItem(id, {
+ name: resolvedItem?.name,
sourceImage: uploadedImage || "",
+ ownerId: resolvedItem?.ownerId,
+ isPublic: resolvedItem?.isPublic,
+ }, {
renderedImage: payload.renderedImage,
renderedPath: payload.renderedPath,
- timestamp: Date.now(),
- ownerId: resolvedItem?.ownerId || null,
- isPublic: resolvedItem?.isPublic || false,
- };
+ });
setResolvedItem(updatedItem);
await saveProject(updatedItem, updatedItem.isPublic ? "public" : "private");
};
@@ -77,19 +92,19 @@ export default function VisualizerRoute() {
) => {
if (!id) return;
const visibility = opts?.visibility || "public";
- const updatedItem = {
- id,
- name: resolvedItem?.name || `Residence ${id}`,
+ const ownerId = visibility === "public"
+ ? resolvedItem?.ownerId || currentUserId || null
+ : resolvedItem?.ownerId || null;
+
+ const updatedItem = createDesignHistoryItem(id, {
+ name: resolvedItem?.name,
sourceImage: uploadedImage || "",
- renderedImage: image,
renderedPath: resolvedItem?.renderedPath,
- timestamp: Date.now(),
- ownerId:
- visibility === "public"
- ? resolvedItem?.ownerId || currentUserId || null
- : resolvedItem?.ownerId || null,
+ }, {
+ renderedImage: image,
+ ownerId,
isPublic: visibility === "public",
- };
+ });
setResolvedItem(updatedItem);
if (visibility === "public") {
await shareProject(updatedItem);
@@ -104,15 +119,14 @@ export default function VisualizerRoute() {
const state = (location.state || {}) as VisualizerLocationState;
if (state.initialImage) {
- const item: DesignHistoryItem = {
- id,
- name: state.name || null,
+ const item = createDesignHistoryItem(id, {
sourceImage: state.initialImage,
+ }, {
+ name: state.name || null,
renderedImage: state.initialRender || undefined,
- timestamp: Date.now(),
ownerId: state.ownerId || queryOwnerId || null,
isPublic: isPublicProject,
- };
+ });
setResolvedItem(item);
setUploadedImage(state.initialImage);
setSelectedInitialRender(state.initialRender || null);
diff --git a/components/Upload.tsx b/components/Upload.tsx
index e87c49a..81d913b 100644
--- a/components/Upload.tsx
+++ b/components/Upload.tsx
@@ -5,6 +5,11 @@ import {
Image as ImageIcon,
} from "lucide-react";
import { useOutletContext } from "react-router";
+import {
+ PROGRESS_INCREMENT,
+ REDIRECT_DELAY_MS,
+ PROGRESS_INTERVAL_MS,
+} from "@/lib/constants";
const Upload = ({ onComplete, className = "" }: UploadProps) => {
const [isDragging, setIsDragging] = useState(false);
@@ -52,19 +57,19 @@ const Upload = ({ onComplete, className = "" }: UploadProps) => {
let completed = false;
const interval = setInterval(() => {
setProgress((prev) => {
- const next = Math.min(prev + 15, 100);
+ const next = Math.min(prev + PROGRESS_INCREMENT, 100);
if (next === 100 && !completed) {
completed = true;
clearInterval(interval);
setTimeout(() => {
onComplete(result);
- }, 600);
+ }, REDIRECT_DELAY_MS);
}
return next;
});
- }, 100);
+ }, PROGRESS_INTERVAL_MS);
};
reader.readAsDataURL(selectedFile);
diff --git a/components/Visualizer.tsx b/components/Visualizer.tsx
index 26d23e1..20f03b6 100644
--- a/components/Visualizer.tsx
+++ b/components/Visualizer.tsx
@@ -10,6 +10,10 @@ import { Button } from "./ui/Button";
import AuthRequiredModal from "./AuthRequiredModal";
import { generate3DView } from "@/lib/ai.action";
+import {
+ SHARE_STATUS_RESET_DELAY_MS,
+ UNAUTHORIZED_STATUSES,
+} from "@/lib/constants";
const Visualizer = ({
onBack,
@@ -70,10 +74,10 @@ const Visualizer = ({
}
setShareStatus("done");
- window.setTimeout(() => {
+ setTimeout(() => {
setShareStatus("idle");
setShareAction(null);
- }, 1500);
+ }, SHARE_STATUS_RESET_DELAY_MS);
} catch (error) {
console.error(`${nextAction} failed:`, error);
setShareStatus("idle");
@@ -110,7 +114,7 @@ const Visualizer = ({
}
} catch (error: any) {
console.error("Generation failed:", error);
- if (error?.status === 401 || error?.status === 403) {
+ if (UNAUTHORIZED_STATUSES.includes(error?.status)) {
setAuthRequired(true);
}
} finally {
diff --git a/lib/ai.action.ts b/lib/ai.action.ts
index b17520c..c8e55e4 100644
--- a/lib/ai.action.ts
+++ b/lib/ai.action.ts
@@ -1,5 +1,5 @@
import { puter } from "@heyputer/puter.js";
-import { ROOMIFY_RENDER_PROMPT } from "@/lib/constants";
+import { ROOMIFY_RENDER_PROMPT, STORAGE_PATHS } from "@/lib/constants";
export const generate3DView = async ({
sourceImage,
@@ -31,14 +31,14 @@ export const generate3DView = async ({
try {
const blob = await (await fetch(rawImageUrl)).blob();
try {
- await puter.fs.mkdir("roomify/renders", { recursive: true });
+ await puter.fs.mkdir(STORAGE_PATHS.RENDERS, { recursive: true });
} catch (error) {
console.warn("Failed to ensure render directory:", error);
}
const fileName = projectId
- ? `roomify/renders/${projectId}.png`
- : `roomify/renders/${Date.now()}.png`;
+ ? `${STORAGE_PATHS.RENDERS}/${projectId}.png`
+ : `${STORAGE_PATHS.RENDERS}/${Date.now()}.png`;
await puter.fs.write(fileName, blob);
const storedUrl = await puter.fs.getReadURL(fileName);
diff --git a/lib/constants.ts b/lib/constants.ts
index 24c2453..c7c4cfe 100644
--- a/lib/constants.ts
+++ b/lib/constants.ts
@@ -1,5 +1,28 @@
export const PUTER_WORKER_URL = import.meta.env.VITE_PUTER_WORKER_URL || "";
+// Storage Paths
+export const STORAGE_PATHS = {
+ ROOT: "roomify",
+ SOURCES: "roomify/sources",
+ RENDERS: "roomify/renders",
+} as const;
+
+// Timing Constants (in milliseconds)
+export const SHARE_STATUS_RESET_DELAY_MS = 1500;
+export const PROGRESS_INCREMENT = 15;
+export const REDIRECT_DELAY_MS = 600;
+export const PROGRESS_INTERVAL_MS = 100;
+
+// UI Constants
+export const GRID_OVERLAY_SIZE = "60px 60px";
+export const GRID_COLOR = "#3B82F6";
+
+// HTTP Status Codes
+export const UNAUTHORIZED_STATUSES = [401, 403];
+
+// Image Dimensions
+export const IMAGE_RENDER_DIMENSION = 1024;
+
export const ROOMIFY_RENDER_PROMPT = `
TASK: Convert the input 2D floor plan into a **photorealistic, top‑down 3D architectural render**.
diff --git a/lib/puter.action.ts b/lib/puter.action.ts
index 7da8e23..79c977d 100644
--- a/lib/puter.action.ts
+++ b/lib/puter.action.ts
@@ -1,5 +1,5 @@
import { puter } from "@heyputer/puter.js";
-import { PUTER_WORKER_URL } from "./constants";
+import { PUTER_WORKER_URL, STORAGE_PATHS } from "./constants";
export const signIn = async () => await puter.auth.signIn();
@@ -88,10 +88,10 @@ export const saveProject = async (
if (sourceImage?.startsWith("data:") && item.id) {
try {
- await puter.fs.mkdir("roomify/sources", { recursive: true });
+ await puter.fs.mkdir(STORAGE_PATHS.SOURCES, { recursive: true });
const sourceBlob = await (await fetch(sourceImage)).blob();
- sourcePath = sourcePath || `roomify/sources/${item.id}.png`;
+ sourcePath = sourcePath || `${STORAGE_PATHS.SOURCES}/${item.id}.png`;
await puter.fs.write(sourcePath, sourceBlob);
sourceImage = await puter.fs.getReadURL(sourcePath);
diff --git a/type.d.ts b/type.d.ts
index fc5d3bb..b6d11ff 100644
--- a/type.d.ts
+++ b/type.d.ts
@@ -1,11 +1,3 @@
-interface Material {
- id: string;
- name: string;
- thumbnail: string;
- type: "color" | "texture";
- category: "floor" | "wall" | "furniture";
-}
-
interface DesignHistoryItem {
id: string;
name?: string | null;
@@ -21,19 +13,6 @@ interface DesignHistoryItem {
isPublic?: boolean;
}
-interface DesignConfig {
- floor: string;
- walls: string;
- style: string;
-}
-
-enum AppStatus {
- IDLE = "IDLE",
- UPLOADING = "UPLOADING",
- PROCESSING = "PROCESSING",
- READY = "READY",
-}
-
type RenderCompletePayload = {
renderedImage: string;
renderedPath?: string;