diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 341384c..47ce52a 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -1,11 +1,11 @@ import type { Route } from "./+types/home"; import Navbar from "../../components/Navbar"; -import {ArrowRight, ArrowUpRight, Clock, Layers} from "lucide-react"; -import Button from "../../components/ui/Button"; +import { ArrowRight, ArrowUpRight, Clock, Layers } from "lucide-react"; import Upload from "../../components/Upload"; -import {useNavigate} from "react-router"; -import {useEffect, useRef, useState} from "react"; -import {createProject, getProjects} from "../../lib/puter.action"; +import { useNavigate } from "react-router"; +import { useEffect, useRef, useState } from "react"; +import { createProject, getProjects } from "../../lib/puter.action"; +import { Helmet } from "react-helmet"; export function meta({}: Route.MetaArgs) { return [ @@ -15,115 +15,118 @@ export function meta({}: Route.MetaArgs) { } export default function Home() { - const navigate = useNavigate(); - const [projects, setProjects] = useState([]); - const isCreatingProjectRef = useRef(false); - - const handleUploadComplete = async (base64Image: string) => { - try { - - if(isCreatingProjectRef.current) return false; - isCreatingProjectRef.current = true; - 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; - } - - setProjects((prev) => [saved, ...prev]); - - navigate(`/visualizer/${newId}`, { - state: { - initialImage: saved.sourceImage, - initialRendered: saved.renderedImage || null, - name - } - }); - - return true; - } finally { - isCreatingProjectRef.current = false; - } - } - const handleDelete = async (id: string) => { - try { - // TODO: ถ้ามี backend - // await deleteProject(id); - - setProjects((prev) => prev.filter((p) => p.id !== id)); - } catch (err) { - console.error("Delete failed", err); + const navigate = useNavigate(); + const [projects, setProjects] = useState([]); + const isCreatingProjectRef = useRef(false); + + const handleUploadComplete = async (base64Image: string) => { + try { + if (isCreatingProjectRef.current) return false; + isCreatingProjectRef.current = true; + 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; } - }; - - useEffect(() => { - const fetchProjects = async () => { - const items = await getProjects(); - setProjects(items) - } + setProjects((prev) => [saved, ...prev]); - fetchProjects(); - }, []); - - return ( -
- + navigate(`/visualizer/${newId}`, { + state: { + initialImage: saved.sourceImage, + initialRendered: saved.renderedImage || null, + name, + }, + }); -
-
-
-
-
+ return true; + } finally { + isCreatingProjectRef.current = false; + } + }; -

Introducing Roomify 2.0

-
+ useEffect(() => { + const fetchProjects = async () => { + const items = await getProjects(); -

Build beautiful spaces at the speed of thought with Roomify

+ setProjects(items); + }; -

- Roomify is an AI-first design environment that helps you visualize, render, and ship architectural projects faster than ever. -

+ fetchProjects(); + }, []); -
- - Start Building - + useEffect(() => { + document.title = "3DRoom"; + + }, []); - + return ( + <> + + {/* 3DRoom */} + + + +
+ + + +
+
+
+
+
+ +

Introducing 3DRoom 2.0

+
+ +

Build 2D floor plan to 3D floor plan

+ +

+ 3DRoom is an AI-first design environment that helps you visualize, + render, and ship architectural projects faster than ever. +

+ + + +
+
+ +
+
+
+
-
-
+

Upload your floor plan

+

Supports JPG, PNG, formats up to 10MB

+
-
-
-
- -
- -

Upload your floor plan

-

Supports JPG, PNG, formats up to 10MB

-
- - -
-
-
+ +
+
+
-
+ {/*
@@ -134,34 +137,36 @@ export default function Home() {
{projects.map(({id, name, renderedImage, sourceImage, timestamp}) => ( -
navigate(`/visualizer/${id}`)}> +
navigate(`/visualizer/${id}`)} + >
- Project + Project + +
Community
- {/* 🔴 ปุ่มลบ */} - + +
- {/*

{name}

*/} -
{new Date(timestamp).toLocaleDateString()} -
@@ -172,7 +177,8 @@ export default function Home() { ))}
-
-
- ) -} \ No newline at end of file + */} + + + ); +} diff --git a/app/routes/visualizer.$id.tsx b/app/routes/visualizer.$id.tsx index 9ac1f76..3dc02f4 100644 --- a/app/routes/visualizer.$id.tsx +++ b/app/routes/visualizer.$id.tsx @@ -1,213 +1,256 @@ -import { useNavigate, useOutletContext, useParams} from "react-router"; -import {useEffect, useRef, useState} from "react"; -import {generate3DView} from "../../lib/ai.action"; -import {Box, Download, RefreshCcw, Share2, X} from "lucide-react"; +import { useNavigate, useOutletContext, useParams } from "react-router"; +import { useEffect, useRef, useState } from "react"; +import { generate3DView } from "../../lib/ai.action"; +import { Box, Download, RefreshCcw, Share2, X } from "lucide-react"; import Button from "../../components/ui/Button"; -import {createProject, getProjectById} from "../../lib/puter.action"; -import { ReactCompareSlider, ReactCompareSliderImage } from "react-compare-slider"; +import { createProject, getProjectById } from "../../lib/puter.action"; +import { + ReactCompareSlider, + ReactCompareSliderImage, +} from "react-compare-slider"; +import { Helmet } from "react-helmet"; const VisualizerId = () => { - const { id } = useParams(); - const navigate = useNavigate(); - const { userId } = useOutletContext() + const { id } = useParams(); + const navigate = useNavigate(); + const { userId } = useOutletContext(); - const hasInitialGenerated = useRef(false); + const hasInitialGenerated = useRef(false); - const [project, setProject] = useState(null); - const [isProjectLoading, setIsProjectLoading] = useState(true); + const [project, setProject] = useState(null); + const [isProjectLoading, setIsProjectLoading] = useState(true); - const [isProcessing, setIsProcessing] = useState(false); - const [currentImage, setCurrentImage] = useState(null); + const [isProcessing, setIsProcessing] = useState(false); + const [currentImage, setCurrentImage] = useState(null); - const handleBack = () => navigate('/'); - const handleExport = () => { - if (!currentImage) return; + const handleBack = () => navigate("/"); - const link = document.createElement('a'); - link.href = currentImage; - link.download = `roomify-${id || 'design'}.png`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + const handleExport = async () => { + if (!currentImage) return; + + try { + const response = await fetch(currentImage); + const blob = await response.blob(); + + const url = window.URL.createObjectURL(blob); + + const link = document.createElement("a"); + link.href = url; + link.download = `roomify-${id || "design"}.png`; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + window.URL.revokeObjectURL(url); + } catch (err) { + console.error("Export failed:", err); } + }; - const runGeneration = async (item: DesignItem) => { - if(!id || !item.sourceImage) return; + const runGeneration = async (item: DesignItem) => { + if (!id || !item.sourceImage) return; - try { - setIsProcessing(true); - const result = await generate3DView({ sourceImage: item.sourceImage }); + try { + setIsProcessing(true); + const result = await generate3DView({ sourceImage: item.sourceImage }); - if(result.renderedImage) { - setCurrentImage(result.renderedImage); + if (result.renderedImage) { + setCurrentImage(result.renderedImage); - const updatedItem = { - ...item, - renderedImage: result.renderedImage, - renderedPath: result.renderedPath, - timestamp: Date.now(), - ownerId: item.ownerId ?? userId ?? null, - isPublic: item.isPublic ?? false, - } + const updatedItem = { + ...item, + renderedImage: result.renderedImage, + renderedPath: result.renderedPath, + timestamp: Date.now(), + ownerId: item.ownerId ?? userId ?? null, + isPublic: item.isPublic ?? false, + }; - const saved = await createProject({ item: updatedItem, visibility: "private" }) + const saved = await createProject({ + item: updatedItem, + visibility: "private", + }); - if(saved) { - setProject(saved); - setCurrentImage(saved.renderedImage || result.renderedImage); - } - } - } catch (error) { - console.error('Generation failed: ', error) - } finally { - setIsProcessing(false); + if (saved) { + setProject(saved); + setCurrentImage(saved.renderedImage || result.renderedImage); } + } + } catch (error) { + console.error("Generation failed: ", error); + } finally { + setIsProcessing(false); } + }; - useEffect(() => { - let isMounted = true; + useEffect(() => { + let isMounted = true; - const loadProject = async () => { - if (!id) { - setIsProjectLoading(false); - return; - } + const loadProject = async () => { + if (!id) { + setIsProjectLoading(false); + return; + } - setIsProjectLoading(true); + setIsProjectLoading(true); - const fetchedProject = await getProjectById({ id }); + const fetchedProject = await getProjectById({ id }); - if (!isMounted) return; + if (!isMounted) return; - setProject(fetchedProject); - setCurrentImage(fetchedProject?.renderedImage || null); - setIsProjectLoading(false); - hasInitialGenerated.current = false; - }; + setProject(fetchedProject); + setCurrentImage(fetchedProject?.renderedImage || null); + setIsProjectLoading(false); + hasInitialGenerated.current = false; + }; - loadProject(); + loadProject(); - return () => { - isMounted = false; - }; - }, [id]); - - useEffect(() => { - if ( - isProjectLoading || - hasInitialGenerated.current || - !project?.sourceImage - ) - return; - - if (project.renderedImage) { - setCurrentImage(project.renderedImage); - hasInitialGenerated.current = true; - return; - } + return () => { + isMounted = false; + }; + }, [id]); - hasInitialGenerated.current = true; - void runGeneration(project); - }, [project, isProjectLoading]); + useEffect(() => { + if ( + isProjectLoading || + hasInitialGenerated.current || + !project?.sourceImage + ) + return; - return ( -
- - -
-
-
-
-

Project

-

{project?.name || `Residence ${id}`}

-

Created by You

-
- -
- - + + +
+
+
+
+ {/*

Project

*/} + {/*

{project?.name || `Residence ${id}`}

*/} + {/*

Created by You

*/} +
+ +
+ + {/* -
-
- -
- {currentImage ? ( - AI Render - ) : ( -
- {project?.sourceImage && ( - Original - )} -
- )} - - {isProcessing && ( -
-
- - Rendering... - Generating your 3D visualization -
-
- )} -
- + */} +
+
+ +
+ {currentImage ? ( + AI Render + ) : ( +
+ {project?.sourceImage && ( + Original + )} +
+ )} + + {isProcessing && ( +
+
+ + Rendering... + + Generating your 3D visualization +
-
-
-
-

Comparison

-

Before and After

-
-
Drag to compare
-
-
- {project?.sourceImage && currentImage ? ( - - } - itemTwo={ - - } - /> - ): ( -
- {project?.sourceImage && ( - Before - )} -
- )} - -
- -
-
+
+ )} + - ) -} -export default VisualizerId \ No newline at end of file +
+
+
+

Comparison

+

Before and After

+
+
Drag to compare
+
+
+ {project?.sourceImage && currentImage ? ( + + } + itemTwo={ + + } + /> + ) : ( +
+ {project?.sourceImage && ( + Before + )} +
+ )} +
+
+ + + + ); +}; +export default VisualizerId; diff --git a/components/Navbar.tsx b/components/Navbar.tsx index d639b42..d1719b7 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -28,15 +28,15 @@ const Navbar = () => {
- Roomify + 3DRoom
- */}
{isSignedIn ? ( diff --git a/lib/puter.action.ts b/lib/puter.action.ts index c150ffe..809101f 100644 --- a/lib/puter.action.ts +++ b/lib/puter.action.ts @@ -57,6 +57,7 @@ export const createProject = async ({ item, visibility = "private" }: CreateProj ...rest, sourceImage: resolvedSource, renderedImage: resolvedRender, + } try { @@ -137,4 +138,10 @@ export const getProjectById = async ({ id }: { id: string }) => { console.error("Failed to fetch project:", error); return null; } -}; \ No newline at end of file +}; + + + + + + diff --git a/lib/puter.worker.js b/lib/puter.worker.js index 6031a6b..00d94f2 100644 --- a/lib/puter.worker.js +++ b/lib/puter.worker.js @@ -87,4 +87,8 @@ router.get('/api/projects/get', async ({ request, user }) => { } catch (e) { return jsonError(500, 'Failed to get project', { message: e.message || 'Unknown error' }); } -}) \ No newline at end of file +}) + + + + diff --git a/package-lock.json b/package-lock.json index 5ccfeaa..8cdd41d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "react": "^19.2.4", "react-compare-slider": "^4.0.0", "react-dom": "^19.2.4", + "react-helmet": "^6.1.0", "react-router": "7.12.0" }, "devDependencies": { @@ -22,6 +23,7 @@ "@types/node": "^22", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", + "@types/react-helmet": "^6.1.11", "tailwindcss": "^4.1.13", "typescript": "^5.9.2", "vite": "^7.1.7", @@ -1808,6 +1810,16 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-helmet": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.11.tgz", + "integrity": "sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2825,7 +2837,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/jsesc": { @@ -3122,6 +3133,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3313,6 +3336,15 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -3483,6 +3515,17 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3569,6 +3612,42 @@ "react": "^19.2.4" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, + "node_modules/react-helmet/node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", diff --git a/package.json b/package.json index 1665981..613ec21 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "react": "^19.2.4", "react-compare-slider": "^4.0.0", "react-dom": "^19.2.4", + "react-helmet": "^6.1.0", "react-router": "7.12.0" }, "devDependencies": { @@ -25,6 +26,7 @@ "@types/node": "^22", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", + "@types/react-helmet": "^6.1.11", "tailwindcss": "^4.1.13", "typescript": "^5.9.2", "vite": "^7.1.7", diff --git a/public/home.png b/public/home.png new file mode 100644 index 0000000..2660736 Binary files /dev/null and b/public/home.png differ