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
205 changes: 116 additions & 89 deletions app/routes/home.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
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 Navbar from "../../components/Navbar";
import {ArrowRight, ArrowUpRight, Clock, Layers} from "lucide-react";
import Button from "../../components/ui/Button";
import Upload from "../../components/Upload";
import {useNavigate} from "react-router";

import {useState} from "react";
import {createProject} from "../../lib/puter.action";

export function meta({}: Route.MetaArgs) {
return [
Expand All @@ -14,100 +15,126 @@ export function meta({}: Route.MetaArgs) {
}

export default function Home() {
const navigate = useNavigate();
const navigate = useNavigate();
const [projects, setProjects] = useState<DesignItem[]>([]);

const handleUploadComplete = async (base64Image: string) => {
const newId = Date.now().toString();
const name = `Residence ${newId}`;

const newItem = {
id: newId, name, sourceImage: base64Image,
renderedImage: undefined,
timestamp: Date.now()
}

const saved = await createProject({ item: newItem, visibility: 'private' });

if(!saved) {
console.error("Failed to create project");
return false;
}

navigate(`/visualizer/${newId}`);
setProjects((prev) => [newItem, ...prev]);

navigate(`/visualizer/${newId}`, {
state: {
initialImage: saved.sourceImage,
initialRendered: saved.renderedImage || null,
name
}
});

return true;
}

return (
<div className="home">
<Navbar/>

<section className="hero">
<div className="announce">
<div className="dot">
<div className="pulse"></div>
</div>
<p>Introducing Roomify 2.0</p>
</div>
<h1>
Build beautiful spaces at the speed of thought with Roomify
</h1>
<p className="subtitle">
Roomify is an AI-first design enviroment that helps ypu visualize,
render, and ship architectural projects faster than ever
</p>
<div className="actions">
<a href="#upload" className="cta">
Start Building <ArrowRight className="icon"/>
</a>
<Button variant="outline" size="lg" className="demo">
Watch Demo
</Button>
</div>
<div id="upload" className="upload-shell">
<div className="grid-overlay"/>
<div className="upload-card">
<div className="upload-head">
<div className="upload-icon">
<Layers className="icon"/>
</div>
<h3>Upload your floor plan</h3>
<p>Support JPG,PNG,formats up to 10MB</p>
</div>
<Upload onComplete={handleUploadComplete} />
{/* <Upload onComplete={(base64Data)=>{
console.log("upload complete",base64Data)
}}/> */}
</div>
<div className="home">
<Navbar />

</div>
<section className="hero">
<div className="announce">
<div className="dot">
<div className="pulse"></div>
</div>

<p>Introducing Roomify 2.0</p>
</div>

<h1>Build beautiful spaces at the speed of thought with Roomify</h1>

<p className="subtitle">
Roomify is an AI-first design environment that helps you visualize, render, and ship architectural projects faster than ever.
</p>

</section>

<section className="projects">
<div className="section-inner">
<div className="section-head">
<div className="copy">
<h2>Project</h2>
<p>Your latest work and shared community projects, all in one place</p>
</div>
</div>
<div className="projects-grid">
<div className="project-card group">
<div className="preview">
<img
src="https://roomify-mlhuk267-dfwu1i.puter.site/projects/1770803585402/rendered.png"
alt="Project"/>
<div className="badge">
<span>Community</span>
<div className="actions">
<a href="#upload" className="cta">
Start Building <ArrowRight className="icon" />
</a>

<Button variant="outline" size="lg" className="demo">
Watch Demo
</Button>
</div>
</div>
<div className="card-body">
<div>
<h3>Project Manhattan</h3>
<div className="meta">
<Clock size={12}/>
<span>
{new Date('01.01.2027').toLocaleDateString()}
</span>
<span>
By JS Mastery
</span>
</div>
</div>
<div className="arrow">
<ArrowUpRight size={18}/>
</div>
</div>
</div>
</div>

<div id="upload" className="upload-shell">
<div className="grid-overlay" />

<div className="upload-card">
<div className="upload-head">
<div className="upload-icon">
<Layers className="icon" />
</div>

<h3>Upload your floor plan</h3>
<p>Supports JPG, PNG, formats up to 10MB</p>
</div>

<Upload onComplete={handleUploadComplete} />
</div>
</div>
</section>

<section className="projects">
<div className="section-inner">
<div className="section-head">
<div className="copy">
<h2>Projects</h2>
<p>Your latest work and shared community projects, all in one place.</p>
</div>
</div>

<div className="projects-grid">
{projects.map(({id, name, renderedImage, sourceImage, timestamp}) => (
<div className="project-card group">
Comment on lines +108 to +109
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 key prop in list rendering.

The projects.map() renders items without a key prop, which will cause React reconciliation issues and trigger a console warning.

🔧 Proposed fix
-                     {projects.map(({id, name, renderedImage, sourceImage, timestamp}) => (
-                         <div className="project-card group">
+                     {projects.map(({id, name, renderedImage, sourceImage, timestamp}) => (
+                         <div key={id} className="project-card group">
📝 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
{projects.map(({id, name, renderedImage, sourceImage, timestamp}) => (
<div className="project-card group">
{projects.map(({id, name, renderedImage, sourceImage, timestamp}) => (
<div key={id} className="project-card group">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/home.tsx` around lines 108 - 109, The JSX returned by projects.map
in the Home route omits a React key, causing reconciliation warnings; update the
map callback that renders the <div className="project-card group"> (inside
projects.map) to include a stable key prop (e.g., key={id}) on the root element
for each item, falling back to a deterministic alternative only if id can be
absent, to ensure unique keys during rendering.

<div className="preview">
<img src={renderedImage || sourceImage} alt="Project"
/>

<div className="badge">
<span>Community</span>
</div>
</div>

<div className="card-body">
<div>
<h3>{name}</h3>

<div className="meta">
<Clock size={12} />
<span>{new Date(timestamp).toLocaleDateString()}</span>
<span>By JS Mastery</span>
</div>
</div>
<div className="arrow">
<ArrowUpRight size={18} />
</div>
</div>
</div>
))}
</div>
</div>
</section>
</div>
</section>
</div>
)
}
}
27 changes: 19 additions & 8 deletions app/routes/visualizer.$id.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@

import {useLocation} from "react-router";

const VisualizerId = () => {
return (
<div>
VisualizerId
</div>
)
}
const location = useLocation();
const { initialImage, name } = location.state || {};

export default VisualizerId
return (
<section>
<h1> {name || 'Untitled Project'}</h1>

<div className="visualizer">
{initialImage && (
<div className="image-container">
<h2>Source Image</h2>
<img src={initialImage} alt="source" />
</div>
)}
</div>
</section>
)
}
export default VisualizerId
60 changes: 56 additions & 4 deletions lib/puter.action.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,65 @@
import puter from "@heyputer/puter.js";
import {getOrCreateHostingConfig, uploadImageToHosting} from "./puter.hosting";
import {isHostedUrl} from "./utils";

export const signIn = async () => await puter.auth.signIn();

export const signOut = () => puter.auth.signOut();

export const getCurrentUser = async () =>{
try{
export const getCurrentUser = async () => {
try {
return await puter.auth.getUser();
}catch{
return null
} catch {
return null;
}
}

export const createProject = async ({ item }: CreateProjectParams): Promise<DesignItem | null | undefined> => {
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

The visibility parameter is declared in CreateProjectParams but never used.

The function destructures only { item }, silently dropping the visibility property that callers (e.g., home.tsx line 31) pass. Either implement visibility logic or remove it from the interface.

🔧 Option A: Use the parameter
-export const createProject = async ({ item }: CreateProjectParams): Promise<DesignItem | null | undefined> => {
+export const createProject = async ({ item, visibility = 'private' }: CreateProjectParams): Promise<DesignItem | null> => {
     const projectId = item.id;
+    // Use visibility when storing...
📝 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
export const createProject = async ({ item }: CreateProjectParams): Promise<DesignItem | null | undefined> => {
export const createProject = async ({ item, visibility = 'private' }: CreateProjectParams): Promise<DesignItem | null> => {
const projectId = item.id;
// Use visibility when storing...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/puter.action.ts` at line 17, The CreateProjectParams type declares a
visibility property but createProject currently only destructures { item },
dropping visibility; update createProject to also destructure visibility (e.g.,
change signature to accept { item, visibility }) and apply it to the created
DesignItem (or otherwise use it in the project-creation flow), or if visibility
is truly unused remove visibility from CreateProjectParams and all callers; key
symbols to modify are the CreateProjectParams type and the createProject
function to ensure visibility is either consumed (set on item / passed to
underlying persistence logic) or removed entirely.

const projectId = item.id;

const hosting = await getOrCreateHostingConfig();

const hostedSource = projectId ?
await uploadImageToHosting({ hosting, url: item.sourceImage, projectId, label: 'source', }) : null;

const hostedRender = projectId && item.renderedImage ?
await uploadImageToHosting({ hosting, url: item.renderedImage, projectId, label: 'rendered', }) : null;

const resolvedSource = hostedSource?.url || (isHostedUrl(item.sourceImage)
? item.sourceImage
: ''
);

if(!resolvedSource) {
console.warn('Failed to host source image, skipping save.')
return null;
}

const resolvedRender = hostedRender?.url
? hostedRender?.url
: item.renderedImage && isHostedUrl(item.renderedImage)
? item.renderedImage
: undefined;

const {
sourcePath: _sourcePath,
renderedPath: _renderedPath,
publicPath: _publicPath,
...rest
} = item;

const payload = {
...rest,
sourceImage: resolvedSource,
renderedImage: resolvedRender,
}

try {
// Call the Puter worker to store project in kv

return payload;
Comment on lines +57 to +60
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

Project persistence is not implemented.

The try block contains only a comment indicating storage should occur, but no actual persistence call exists. The function uploads images but never stores the project metadata, making createProject misleading — it hosts images but doesn't create a persistent project record.

Based on the pattern in lib/puter.hosting.ts (line 23), the implementation should call puter.kv.set() to persist the project.

🔧 Suggested implementation
     try {
         // Call the Puter worker to store project in kv
-
+        await puter.kv.set(`project:${projectId}`, payload);
         return payload;
     } catch (e) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/puter.action.ts` around lines 57 - 60, The try block in the createProject
flow returns payload but never persists project metadata; call puter.kv.set(...)
to store the project (use the same key pattern used in lib/puter.hosting.ts) and
await the result before returning payload, handling any errors in the catch;
reference the payload object and the puter.kv.set method so the project record
is created when createProject finishes.

} catch (e) {
console.log('Failed to save project', e)
return null;
}
}
65 changes: 65 additions & 0 deletions lib/puter.hosting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import puter from "@heyputer/puter.js";
import {
createHostingSlug,
fetchBlobFromUrl, getHostedUrl,
getImageExtension,
HOSTING_CONFIG_KEY,
imageUrlToPngBlob,
isHostedUrl
} from "./utils";

export const getOrCreateHostingConfig = async (): Promise<HostingConfig | null> => {
const existing = (await puter.kv.get(HOSTING_CONFIG_KEY)) as HostingConfig | null;

if(existing?.subdomain) return { subdomain: existing.subdomain };

const subdomain = createHostingSlug();

try {
const created = await puter.hosting.create(subdomain, '.');

const record = { subdomain: created.subdomain };

await puter.kv.set(HOSTING_CONFIG_KEY,record);

return record;


} catch (e) {
console.warn(`Could not find subdomain: ${e}`);
return null;
}
Comment on lines +11 to +31
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

Race condition in getOrCreateHostingConfig.

This function has a TOCTOU (time-of-check-time-of-use) vulnerability. Between checking puter.kv.get() and calling puter.kv.set(), another concurrent call could:

  1. Also see no existing config
  2. Create its own subdomain
  3. Overwrite the first call's stored config

This can occur when a user rapidly uploads multiple images before the first createProject completes.

🔒 Suggested mitigation using module-level promise caching
+let hostingConfigPromise: Promise<HostingConfig | null> | null = null;
+
-export const getOrCreateHostingConfig = async (): Promise<HostingConfig | null> => {
+export const getOrCreateHostingConfig = (): Promise<HostingConfig | null> => {
+    if (hostingConfigPromise) return hostingConfigPromise;
+
+    hostingConfigPromise = (async () => {
         const existing = (await puter.kv.get(HOSTING_CONFIG_KEY)) as HostingConfig | null;
-
         if(existing?.subdomain) return { subdomain: existing.subdomain };

         const subdomain = createHostingSlug();

         try {
             const created = await puter.hosting.create(subdomain, '.');
-
             const record = { subdomain: created.subdomain };
-
             await puter.kv.set(HOSTING_CONFIG_KEY, record);
-
             return record;
-
-        
         } catch (e) {
             console.warn(`Could not find subdomain: ${e}`);
+            hostingConfigPromise = null; // Allow retry on failure
             return null;
         }
-}
+    })();
+
+    return hostingConfigPromise;
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/puter.hosting.ts` around lines 11 - 31, getOrCreateHostingConfig has a
TOCTOU race: multiple concurrent calls can each create different subdomains
before puter.kv.set, causing overwrites; fix by introducing a module-level
promise cache (e.g., hostingConfigPromise) so concurrent callers share a single
in-flight creation. In getOrCreateHostingConfig, if hostingConfigPromise exists
return/await it; otherwise assign hostingConfigPromise = (async () => { check
puter.kv.get(HOSTING_CONFIG_KEY) again, if present return it, else call
createHostingSlug() and puter.hosting.create(...), then
puter.kv.set(HOSTING_CONFIG_KEY, record); return record })(); ensure you clear
or preserve hostingConfigPromise appropriately on failures so future calls can
retry.

}

export const uploadImageToHosting = async ({ hosting, url, projectId, label }: StoreHostedImageParams): Promise<HostedAsset | null> => {
if(!hosting || !url) return null;
if(isHostedUrl(url)) return { url };

try {
const resolved = label === "rendered"
? await imageUrlToPngBlob(url)
.then((blob) => blob ? { blob, contentType: 'image/png' }: null)
: await fetchBlobFromUrl(url);

if(!resolved) return null;

const contentType = resolved.contentType || resolved.blob.type || '';
const ext = getImageExtension(contentType, url);
const dir = `projects/${projectId}`;
const filePath = `${dir}/${label}.${ext}`;

const uploadFile = new File([resolved.blob], `${label}.${ext}`, {
type: contentType,
});

await puter.fs.mkdir(dir, { createMissingParents: true });
await puter.fs.write(filePath, uploadFile);

const hostedUrl = getHostedUrl({ subdomain: hosting.subdomain }, filePath);

return hostedUrl ? { url: hostedUrl } : null;
} catch (e) {
console.warn(`Failed to store hosted image: ${e}`);
return null;
}
}
Loading