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
7 changes: 5 additions & 2 deletions app/routes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { type RouteConfig, index } from "@react-router/dev/routes";
import { type RouteConfig, index,route } from "@react-router/dev/routes";

export default [index("routes/home.tsx")] satisfies RouteConfig;
export default [
index("routes/home.tsx"),
route('visualizer/:id','./routes/visualizer.$id.tsx')
] satisfies RouteConfig;
20 changes: 18 additions & 2 deletions app/routes/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Navbar from "components/Navbar";
import type { Route } from "./+types/home";
import { ArrowRight, ArrowUpRight, Clock, Layers } from "lucide-react";
import Button from "components/ui/Button";
import Upload from "components/Upload";
import {useNavigate} from "react-router";


export function meta({}: Route.MetaArgs) {
Expand All @@ -12,6 +14,15 @@ export function meta({}: Route.MetaArgs) {
}

export default function Home() {
const navigate = useNavigate();

const handleUploadComplete = async (base64Image: string) => {
const newId = Date.now().toString();

navigate(`/visualizer/${newId}`);

return true;
}
Comment on lines +19 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Uploaded image data is discarded.

handleUploadComplete receives base64Image containing the full floor plan data but never stores it. After navigating to /visualizer/${newId}, the image data is lost and the visualizer page has no way to retrieve it.

This breaks the core upload-to-visualize flow.

🔧 Option 1: Pass via route state
 const handleUploadComplete = async (base64Image: string) => {
     const newId = Date.now().toString();
-    navigate(`/visualizer/${newId}`);
-    return true;
+    navigate(`/visualizer/${newId}`, { state: { image: base64Image } });
 }

Then in visualizer.$id.tsx:

import { useLocation } from 'react-router';
const { state } = useLocation();
const image = state?.image;
🔧 Option 2: Store in context/global state before navigating
+import { useUploadStore } from 'stores/upload'; // or context
+
 const handleUploadComplete = async (base64Image: string) => {
     const newId = Date.now().toString();
+    useUploadStore.getState().setImage(newId, base64Image);
     navigate(`/visualizer/${newId}`);
-    return true;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleUploadComplete = async (base64Image: string) => {
const newId = Date.now().toString();
navigate(`/visualizer/${newId}`);
return true;
}
const handleUploadComplete = async (base64Image: string) => {
const newId = Date.now().toString();
navigate(`/visualizer/${newId}`, { state: { image: base64Image } });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/home.tsx` around lines 19 - 25, handleUploadComplete is discarding
the base64Image (the uploaded floor plan) before navigation, so the visualizer
page cannot access it; update handleUploadComplete to persist the image before
navigating—either pass it via navigate's state (call
navigate(`/visualizer/${newId}`, { state: { image: base64Image, id: newId } })
and then read state?.image in visualizer.$id.tsx via useLocation) or save it to
a shared store/context or localStorage under the newId (e.g.,
setImageForId(newId, base64Image)) and then navigate to `/visualizer/${newId}`
so visualizer.$id.tsx can retrieve the image by id; ensure the symbol
handleUploadComplete is modified and visualizer.$id.tsx reads the same storage
mechanism.

return (
<div className="home">
<Navbar/>
Expand Down Expand Up @@ -48,7 +59,10 @@ export default function Home() {
<h3>Upload your floor plan</h3>
<p>Support JPG,PNG,formats up to 10MB</p>
</div>
<p>Upload image</p>
<Upload onComplete={handleUploadComplete} />
{/* <Upload onComplete={(base64Data)=>{
console.log("upload complete",base64Data)
}}/> */}
</div>

</div>
Expand All @@ -66,7 +80,9 @@ export default function Home() {
<div className="projects-grid">
<div className="project-card group">
<div className="preview">
<img alt="Project"/>
<img
src="https://roomify-mlhuk267-dfwu1i.puter.site/projects/1770803585402/rendered.png"
alt="Project"/>
<div className="badge">
<span>Community</span>
</div>
Expand Down
11 changes: 11 additions & 0 deletions app/routes/visualizer.$id.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@


const VisualizerId = () => {
return (
<div>
VisualizerId
</div>
)
}
Comment on lines +3 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Placeholder component lacks access to uploaded image data.

This route receives an :id parameter but the uploaded image (base64 data) is never persisted before navigation. The handleUploadComplete in app/routes/home.tsx (lines 19-25) discards the base64Image argument, so this component has no mechanism to retrieve the actual floor plan image.

Consider one of:

  1. Store the image in a shared state manager (context, Zustand, etc.) before navigating
  2. Persist to backend/storage and fetch here using the id
  3. Pass via route state: navigate(\/visualizer/${newId}`, { state: { image: base64Image } })`

Also, the :id param is not being extracted — you'll need useParams from react-router to access it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/visualizer`.$id.tsx around lines 3 - 9, The VisualizerId component
is a placeholder and currently has no access to the uploaded base64 image
because handleUploadComplete in app/routes/home.tsx discards the base64Image and
you never extract the route :id; fix by persisting or passing the image before
navigation and reading it in VisualizerId: either (A) store base64Image in
shared state (Context/Zustand) in handleUploadComplete and have VisualizerId
read it, (B) upload/persist the image to backend in handleUploadComplete and
fetch it in VisualizerId using the id param, or (C) pass the image via
navigate(`/visualizer/${newId}`, { state: { image: base64Image } }) and then
read it from location.state in VisualizerId; additionally import and use
useParams (from react-router) inside VisualizerId to extract the id param when
you need to fetch by id.


export default VisualizerId
135 changes: 135 additions & 0 deletions components/Upload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React, {useCallback, useState} from 'react'
import {useOutletContext} from "react-router";
import {CheckCircle2, ImageIcon, UploadIcon} from "lucide-react";
import {PROGRESS_INCREMENT, REDIRECT_DELAY_MS, PROGRESS_INTERVAL_MS} from "../lib/constants";

interface UploadProps {
onComplete?: (base64Data: string) => void;
}

const Upload = ({ onComplete }: UploadProps) => {
const [file, setFile] = useState<File | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [progress, setProgress] = useState(0);

const { isSignedIn } = useOutletContext<AuthContext>();

const processFile = useCallback((file: File) => {
if (!isSignedIn) return;

setFile(file);
setProgress(0);

const reader = new FileReader();
reader.onerror = () =>{
setFile(null);
setProgress(0);
}
reader.onloadend = () => {
const base64Data = reader.result as string;

const interval = setInterval(() => {
setProgress((prev) => {
const next = prev + PROGRESS_INCREMENT;
if (next >= 100) {
clearInterval(interval);
setTimeout(() => {
onComplete?.(base64Data);
}, REDIRECT_DELAY_MS);
return 100;
}
return next;
});
}, PROGRESS_INTERVAL_MS);
Comment on lines +31 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Interval not cleaned up on unmount — potential memory leak.

If the component unmounts while the progress animation is running (e.g., user navigates away), the interval continues and attempts to update state on an unmounted component. This can cause memory leaks and React warnings.

🛠️ Suggested fix using useEffect cleanup or useRef
+import React, {useCallback, useState, useRef, useEffect} from 'react'
 ...
 const Upload = ({ onComplete }: UploadProps) => {
     const [file, setFile] = useState<File | null>(null);
     const [isDragging, setIsDragging] = useState(false);
     const [progress, setProgress] = useState(0);
+    const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
 
     const { isSignedIn } = useOutletContext<AuthContext>();
 
+    useEffect(() => {
+        return () => {
+            if (intervalRef.current) {
+                clearInterval(intervalRef.current);
+            }
+        };
+    }, []);
+
     const processFile = useCallback((file: File) => {
         ...
         reader.onloadend = () => {
             const base64Data = reader.result as string;
 
-            const interval = setInterval(() => {
+            intervalRef.current = setInterval(() => {
                 setProgress((prev) => {
                     const next = prev + PROGRESS_INCREMENT;
                     if (next >= 100) {
-                        clearInterval(interval);
+                        clearInterval(intervalRef.current!);
+                        intervalRef.current = null;
                         ...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Upload.tsx` around lines 27 - 39, The interval created in the
Upload progress logic (setInterval that calls setProgress using
PROGRESS_INCREMENT and PROGRESS_INTERVAL_MS and later triggers
onComplete(base64Data) after REDIRECT_DELAY_MS) isn't cleared on component
unmount; store the interval id (and any timeout id) in a ref or ensure the
interval is created inside a useEffect and return a cleanup that calls
clearInterval(intervalId) and clearTimeout(timeoutId) to prevent state updates
after unmount and eliminate the memory leak/warnings from setProgress/onComplete
being called on an unmounted component.

};
reader.readAsDataURL(file);
Comment on lines +23 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing FileReader error handling.

If FileReader fails (e.g., file is corrupted or read permission denied), there's no onerror handler. The component would silently fail, leaving the user with no feedback.

🔧 Proposed fix
 const reader = new FileReader();
+reader.onerror = () => {
+    setFile(null);
+    setProgress(0);
+    // Optionally show error state to user
+};
 reader.onloadend = () => {
     ...
 };
 reader.readAsDataURL(file);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const reader = new FileReader();
reader.onloadend = () => {
const base64Data = reader.result as string;
const interval = setInterval(() => {
setProgress((prev) => {
const next = prev + PROGRESS_INCREMENT;
if (next >= 100) {
clearInterval(interval);
setTimeout(() => {
onComplete?.(base64Data);
}, REDIRECT_DELAY_MS);
return 100;
}
return next;
});
}, PROGRESS_INTERVAL_MS);
};
reader.readAsDataURL(file);
const reader = new FileReader();
reader.onerror = () => {
setFile(null);
setProgress(0);
// Optionally show error state to user
};
reader.onloadend = () => {
const base64Data = reader.result as string;
const interval = setInterval(() => {
setProgress((prev) => {
const next = prev + PROGRESS_INCREMENT;
if (next >= 100) {
clearInterval(interval);
setTimeout(() => {
onComplete?.(base64Data);
}, REDIRECT_DELAY_MS);
return 100;
}
return next;
});
}, PROGRESS_INTERVAL_MS);
};
reader.readAsDataURL(file);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Upload.tsx` around lines 23 - 41, Add a FileReader error handler:
define reader.onerror alongside reader.onloadend to clear any running interval,
reset UI progress via setProgress(0) (or another safe fallback), and notify the
parent via an error callback (e.g., call onError?.(event) if you add that prop)
or at minimum log the error (console.error) so failures aren't silent; update
the code around FileReader, reader.onloadend, reader.readAsDataURL(file),
PROGRESS_INCREMENT, PROGRESS_INTERVAL_MS and REDIRECT_DELAY_MS to ensure the
interval is cleared and no onComplete is called when an error occurs.

}, [isSignedIn, onComplete]);

const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
if (!isSignedIn) return;
setIsDragging(true);
};

const handleDragLeave = () => {
setIsDragging(false);
};

const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);

if (!isSignedIn) return;

const droppedFile = e.dataTransfer.files[0];
const allowedTypes = ['image/jpeg','image/png'];
if (droppedFile && allowedTypes.includes(droppedFile.type)) {
processFile(droppedFile);
}
};

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!isSignedIn) return;

const selectedFile = e.target.files?.[0];
if (selectedFile) {
processFile(selectedFile);
}
};
Comment on lines +71 to +78
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent file type validation between handlers.

handleDrop validates droppedFile.type.startsWith('image/') (line 61), but handleChange accepts any file without type checking. This could allow non-image files via the file picker despite the accept attribute (which is only a hint and can be bypassed).

🔧 Proposed fix
 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     if (!isSignedIn) return;
 
     const selectedFile = e.target.files?.[0];
-    if (selectedFile) {
+    if (selectedFile && selectedFile.type.startsWith('image/')) {
         processFile(selectedFile);
     }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!isSignedIn) return;
const selectedFile = e.target.files?.[0];
if (selectedFile) {
processFile(selectedFile);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!isSignedIn) return;
const selectedFile = e.target.files?.[0];
if (selectedFile && selectedFile.type.startsWith('image/')) {
processFile(selectedFile);
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Upload.tsx` around lines 66 - 73, handleChange currently skips
file-type validation while handleDrop enforces image-only via
droppedFile.type.startsWith('image/'); update handleChange to mirror that check:
after obtaining selectedFile in handleChange, ensure isSignedIn and that
selectedFile.type.startsWith('image/') before calling processFile(selectedFile),
and reject or ignore non-image files (optionally trigger the same user
feedback/path used elsewhere). This keeps validation consistent between
handleDrop and handleChange and relies on the same processFile/isSignedIn flow.


return (
<div className="upload">
{!file ? (
<div
className={`dropzone ${isDragging ? 'is-dragging' : ''}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<input
type="file"
className="drop-input"
accept=".jpg,.jpeg,.png"
disabled={!isSignedIn}
onChange={handleChange}
/>

<div className="drop-content">
<div className="drop-icon">
<UploadIcon size={20} />
</div>
<p>
{isSignedIn ? (
"Click to upload or just drag and drop"
): ("Sign in or sign up with Puter to upload")}
</p>
<p className="help">Maximum file size 50 MB.</p>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

File size limit stated in UI is not enforced.

The UI states "Maximum file size 50 MB" but no validation exists in processFile, handleDrop, or handleChange. Large files could cause browser memory issues when converted to base64.

🔧 Proposed fix — add size check in processFile
+const MAX_FILE_SIZE_MB = 50;
+const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
+
 const processFile = useCallback((file: File) => {
     if (!isSignedIn) return;
+    if (file.size > MAX_FILE_SIZE_BYTES) {
+        // Show error to user
+        return;
+    }
     ...
 }, [isSignedIn, onComplete]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Upload.tsx` at line 101, The UI claims a 50 MB max but no
enforcement exists; update processFile (and callers handleDrop and handleChange)
to check file.size and reject files > 50 * 1024 * 1024 before any base64
conversion or heavy processing, returning/throwing or calling the existing error
handler to surface a user-friendly message and abort further processing; ensure
processFile is the single place that enforces the limit so
handleDrop/handleChange can quickly delegate and skip reads when the check
fails.

</div>
</div>
) : (
<div className="upload-status">
<div className="status-content">
<div className="status-icon">
{progress === 100 ? (
<CheckCircle2 className="check" />
): (
<ImageIcon className="image" />
)}
</div>

<h3>{file.name}</h3>

<div className='progress'>
<div className="bar" style={{ width: `${progress}%` }} />

<p className="status-text">
{progress < 100 ? 'Analyzing Floor Plan...' : 'Redirecting...'}
</p>
</div>
</div>
</div>
)}
</div>
)
}
export default Upload
56 changes: 56 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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;
export const PROGRESS_STEP = 5;

// 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**.

STRICT REQUIREMENTS (do not violate):
1) **REMOVE ALL TEXT**: Do not render any letters, numbers, labels, dimensions, or annotations. Floors must be continuous where text used to be.
2) **GEOMETRY MUST MATCH**: Walls, rooms, doors, and windows must follow the exact lines and positions in the plan. Do not shift or resize.
3) **TOP‑DOWN ONLY**: Orthographic top‑down view. No perspective tilt.
4) **CLEAN, REALISTIC OUTPUT**: Crisp edges, balanced lighting, and realistic materials. No sketch/hand‑drawn look.
5) **NO EXTRA CONTENT**: Do not add rooms, furniture, or objects that are not clearly indicated by the plan.

STRUCTURE & DETAILS:
- **Walls**: Extrude precisely from the plan lines. Consistent wall height and thickness.
- **Doors**: Convert door swing arcs into open doors, aligned to the plan.
- **Windows**: Convert thin perimeter lines into realistic glass windows.

FURNITURE & ROOM MAPPING (only where icons/fixtures are clearly shown):
- Bed icon → realistic bed with duvet and pillows.
- Sofa icon → modern sectional or sofa.
- Dining table icon → table with chairs.
- Kitchen icon → counters with sink and stove.
- Bathroom icon → toilet, sink, and tub/shower.
- Office/study icon → desk, chair, and minimal shelving.
- Porch/patio/balcony icon → outdoor seating or simple furniture (keep minimal).
- Utility/laundry icon → washer/dryer and minimal cabinetry.

STYLE & LIGHTING:
- Lighting: bright, neutral daylight. High clarity and balanced contrast.
- Materials: realistic wood/tile floors, clean walls, subtle shadows.
- Finish: professional architectural visualization; no text, no watermarks, no logos.
`.trim();