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;