diff --git a/.react-router/types/+future.ts b/.react-router/types/+future.ts index 21dbf5e..7f4533c 100644 --- a/.react-router/types/+future.ts +++ b/.react-router/types/+future.ts @@ -4,6 +4,6 @@ import "react-router"; declare module "react-router" { interface Future { - v8_middleware: false; + v8_middleware: false } -} +} \ No newline at end of file diff --git a/.react-router/types/+routes.ts b/.react-router/types/+routes.ts index 0334969..ef0dc1d 100644 --- a/.react-router/types/+routes.ts +++ b/.react-router/types/+routes.ts @@ -1,12 +1,12 @@ // Generated by React Router -import "react-router"; +import "react-router" declare module "react-router" { interface Register { - pages: Pages; - routeFiles: RouteFiles; - routeModules: RouteModules; + pages: Pages + routeFiles: RouteFiles + routeModules: RouteModules } } @@ -14,20 +14,30 @@ type Pages = { "/": { params: {}; }; + "/visualizer/:id": { + params: { + "id": string; + }; + }; }; type RouteFiles = { "root.tsx": { id: "root"; - page: "/"; + page: "/" | "/visualizer/:id"; }; "routes/home.tsx": { id: "routes/home"; page: "/"; }; + "./routes/visualizer.$id.tsx": { + id: "routes/visualizer.$id"; + page: "/visualizer/:id"; + }; }; type RouteModules = { - root: typeof import("./app/root.tsx"); + "root": typeof import("./app/root.tsx"); "routes/home": typeof import("./app/routes/home.tsx"); -}; + "routes/visualizer.$id": typeof import("./app/./routes/visualizer.$id.tsx"); +}; \ No newline at end of file diff --git a/.react-router/types/+server-build.d.ts b/.react-router/types/+server-build.d.ts index eba875f..13792c1 100644 --- a/.react-router/types/+server-build.d.ts +++ b/.react-router/types/+server-build.d.ts @@ -15,4 +15,4 @@ declare module "virtual:react-router/server-build" { export const ssr: ServerBuild["ssr"]; export const allowedActionOrigins: ServerBuild["allowedActionOrigins"]; export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"]; -} +} \ No newline at end of file diff --git a/.react-router/types/app/+types/root.ts b/.react-router/types/app/+types/root.ts index f9dae81..5bd414e 100644 --- a/.react-router/types/app/+types/root.ts +++ b/.react-router/types/app/+types/root.ts @@ -2,24 +2,19 @@ import type { GetInfo, GetAnnotations } from "react-router/internal"; -type Module = typeof import("../root.js"); +type Module = typeof import("../root.js") type Info = GetInfo<{ - file: "root.tsx"; - module: Module; -}>; - -type Matches = [ - { - id: "root"; - module: typeof import("../root.js"); - }, -]; - -type Annotations = GetAnnotations< - Info & { module: Module; matches: Matches }, - false ->; + file: "root.tsx", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../root.js"); +}]; + +type Annotations = GetAnnotations; export namespace Route { // links @@ -39,8 +34,7 @@ export namespace Route { export type MiddlewareFunction = Annotations["MiddlewareFunction"]; // clientMiddleware - export type ClientMiddlewareFunction = - Annotations["ClientMiddlewareFunction"]; + export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"]; // loader export type LoaderArgs = Annotations["LoaderArgs"]; @@ -62,4 +56,4 @@ export namespace Route { // ErrorBoundary export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"]; -} +} \ No newline at end of file diff --git a/.react-router/types/app/routes/+types/home.ts b/.react-router/types/app/routes/+types/home.ts index ac6346e..e49d62a 100644 --- a/.react-router/types/app/routes/+types/home.ts +++ b/.react-router/types/app/routes/+types/home.ts @@ -2,28 +2,22 @@ import type { GetInfo, GetAnnotations } from "react-router/internal"; -type Module = typeof import("../home.js"); +type Module = typeof import("../home.js") type Info = GetInfo<{ - file: "routes/home.tsx"; - module: Module; -}>; - -type Matches = [ - { - id: "root"; - module: typeof import("../../root.js"); - }, - { - id: "routes/home"; - module: typeof import("../home.js"); - }, -]; - -type Annotations = GetAnnotations< - Info & { module: Module; matches: Matches }, - false ->; + file: "routes/home.tsx", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/home"; + module: typeof import("../home.js"); +}]; + +type Annotations = GetAnnotations; export namespace Route { // links @@ -43,8 +37,7 @@ export namespace Route { export type MiddlewareFunction = Annotations["MiddlewareFunction"]; // clientMiddleware - export type ClientMiddlewareFunction = - Annotations["ClientMiddlewareFunction"]; + export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"]; // loader export type LoaderArgs = Annotations["LoaderArgs"]; @@ -66,4 +59,4 @@ export namespace Route { // ErrorBoundary export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"]; -} +} \ No newline at end of file diff --git a/.react-router/types/app/routes/+types/visualizer.$id.ts b/.react-router/types/app/routes/+types/visualizer.$id.ts new file mode 100644 index 0000000..8291497 --- /dev/null +++ b/.react-router/types/app/routes/+types/visualizer.$id.ts @@ -0,0 +1,62 @@ +// Generated by React Router + +import type { GetInfo, GetAnnotations } from "react-router/internal"; + +type Module = typeof import("../visualizer.$id.js") + +type Info = GetInfo<{ + file: "./routes/visualizer.$id.tsx", + module: Module +}> + +type Matches = [{ + id: "root"; + module: typeof import("../../root.js"); +}, { + id: "routes/visualizer.$id"; + module: typeof import("../visualizer.$id.js"); +}]; + +type Annotations = GetAnnotations; + +export namespace Route { + // links + export type LinkDescriptors = Annotations["LinkDescriptors"]; + export type LinksFunction = Annotations["LinksFunction"]; + + // meta + export type MetaArgs = Annotations["MetaArgs"]; + export type MetaDescriptors = Annotations["MetaDescriptors"]; + export type MetaFunction = Annotations["MetaFunction"]; + + // headers + export type HeadersArgs = Annotations["HeadersArgs"]; + export type HeadersFunction = Annotations["HeadersFunction"]; + + // middleware + export type MiddlewareFunction = Annotations["MiddlewareFunction"]; + + // clientMiddleware + export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"]; + + // loader + export type LoaderArgs = Annotations["LoaderArgs"]; + + // clientLoader + export type ClientLoaderArgs = Annotations["ClientLoaderArgs"]; + + // action + export type ActionArgs = Annotations["ActionArgs"]; + + // clientAction + export type ClientActionArgs = Annotations["ClientActionArgs"]; + + // HydrateFallback + export type HydrateFallbackProps = Annotations["HydrateFallbackProps"]; + + // Component + export type ComponentProps = Annotations["ComponentProps"]; + + // ErrorBoundary + export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"]; +} \ No newline at end of file diff --git a/app/app.css b/app/app.css index d6076aa..49cd9f4 100644 --- a/app/app.css +++ b/app/app.css @@ -1,599 +1,599 @@ @import "tailwindcss"; @theme { - --color-background: #fdfbf7; - --color-foreground: #1a1a1a; - --color-surface: #ffffff; - --color-surface-highlight: #f3f4f6; - --color-primary: #f97316; - --color-secondary: #3b82f6; - --color-accent: #8b5cf6; - --font-serif: "Instrument Serif", serif; - --font-sans: Inter, sans-serif; - --shadow-neobrutalism: 4px 4px 0px 0px rgba(0, 0, 0, 1); + --color-background: #fdfbf7; + --color-foreground: #1a1a1a; + --color-surface: #ffffff; + --color-surface-highlight: #f3f4f6; + --color-primary: #f97316; + --color-secondary: #3b82f6; + --color-accent: #8b5cf6; + --font-serif: "Instrument Serif", serif; + --font-sans: Inter, sans-serif; + --shadow-neobrutalism: 4px 4px 0px 0px rgba(0, 0, 0, 1); } @layer base { - html { - scroll-behavior: smooth; - } - - body { - background-color: #fdfbf7; - color: #1a1a1a; - font-family: Inter, sans-serif; - -webkit-font-smoothing: antialiased; - } - - h1, - h2, - h3, - h4, - h5, - h6 { - font-family: "Instrument Serif", serif; - } - - /* Custom Scrollbar */ - ::-webkit-scrollbar { - width: 8px; - height: 8px; - } - - ::-webkit-scrollbar-track { - background: #fdfbf7; - } - - ::-webkit-scrollbar-thumb { - background: #d1d5db; - border-radius: 4px; - } - - ::-webkit-scrollbar-thumb:hover { - background: #9ca3af; - } -} - -@layer components { - .btn { - @apply inline-flex items-center justify-center rounded-lg font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-background disabled:opacity-50 disabled:cursor-not-allowed; - - &.btn--primary { - @apply bg-primary text-white hover:bg-[#EA580C] shadow-sm; + html { + scroll-behavior: smooth; } - &.btn--secondary { - @apply bg-white text-black border border-zinc-200 hover:bg-zinc-50 shadow-sm; + body { + background-color: #fdfbf7; + color: #1a1a1a; + font-family: Inter, sans-serif; + -webkit-font-smoothing: antialiased; } - &.btn--ghost { - @apply text-zinc-600 hover:text-black hover:bg-black/5; + h1, + h2, + h3, + h4, + h5, + h6 { + font-family: "Instrument Serif", serif; } - &.btn--outline { - @apply bg-transparent border border-zinc-300 text-zinc-900 hover:bg-zinc-100; + /* Custom Scrollbar */ + ::-webkit-scrollbar { + width: 8px; + height: 8px; } - &.btn--sm { - @apply px-3 py-1.5 text-xs uppercase tracking-wide; + ::-webkit-scrollbar-track { + background: #fdfbf7; } - &.btn--md { - @apply px-5 py-2.5 text-sm; + ::-webkit-scrollbar-thumb { + background: #d1d5db; + border-radius: 4px; } - &.btn--lg { - @apply px-8 py-3 text-sm uppercase tracking-wide font-bold; + ::-webkit-scrollbar-thumb:hover { + background: #9ca3af; } +} + +@layer components { + .btn { + @apply inline-flex items-center justify-center rounded-lg font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-background disabled:opacity-50 disabled:cursor-not-allowed; + + &.btn--primary { + @apply bg-primary text-white hover:bg-[#EA580C] shadow-sm; + } + + &.btn--secondary { + @apply bg-white text-black border border-zinc-200 hover:bg-zinc-50 shadow-sm; + } + + &.btn--ghost { + @apply text-zinc-600 hover:text-black hover:bg-black/5; + } + + &.btn--outline { + @apply bg-transparent border border-zinc-300 text-zinc-900 hover:bg-zinc-100; + } + + &.btn--sm { + @apply px-3 py-1.5 text-xs uppercase tracking-wide; + } + + &.btn--md { + @apply px-5 py-2.5 text-sm; + } - &.btn--full { - @apply w-full; + &.btn--lg { + @apply px-8 py-3 text-sm uppercase tracking-wide font-bold; + } + + &.btn--full { + @apply w-full; + } } - } - .card { - @apply bg-surface border border-white/5 rounded-3xl p-6 shadow-xl; + .card { + @apply bg-surface border border-white/5 rounded-3xl p-6 shadow-xl; - .header { - @apply flex justify-between items-center mb-4; + .header { + @apply flex justify-between items-center mb-4; - h3 { - @apply text-lg font-semibold text-zinc-100; - } + h3 { + @apply text-lg font-semibold text-zinc-100; + } + } } - } - .navbar { - @apply fixed top-0 w-full z-50 bg-background/90 backdrop-blur-sm border-b border-black/5; + .navbar { + @apply fixed top-0 w-full z-50 bg-background/90 backdrop-blur-sm border-b border-black/5; - .inner { - @apply max-w-7xl mx-auto px-6 h-16 flex items-center justify-between; + .inner { + @apply max-w-7xl mx-auto px-6 h-16 flex items-center justify-between; - .left { - @apply flex items-center space-x-8; + .left { + @apply flex items-center space-x-8; - .brand { - @apply flex items-center space-x-2 cursor-pointer; + .brand { + @apply flex items-center space-x-2 cursor-pointer; - .logo { - @apply w-6 h-6 text-black; - } + .logo { + @apply w-6 h-6 text-black; + } - .name { - @apply text-xl font-serif font-bold text-black tracking-tight; - } - } + .name { + @apply text-xl font-serif font-bold text-black tracking-tight; + } + } - .links { - @apply hidden md:flex items-center space-x-6; + .links { + @apply hidden md:flex items-center space-x-6; - a { - @apply text-sm font-medium text-zinc-600 hover:text-black transition-colors; - } - } - } + a { + @apply text-sm font-medium text-zinc-600 hover:text-black transition-colors; + } + } + } - .actions { - @apply flex items-center space-x-4; + .actions { + @apply flex items-center space-x-4; - .greeting { - @apply text-xs font-semibold uppercase tracking-wide text-zinc-500; - } + .greeting { + @apply text-xs font-semibold uppercase tracking-wide text-zinc-500; + } - .btn { - @apply rounded-md; - } + .btn { + @apply rounded-md; + } - .login { - @apply text-xs font-bold uppercase tracking-wide text-zinc-900 hover:text-primary transition-colors; - } + .login { + @apply text-xs font-bold uppercase tracking-wide text-zinc-900 hover:text-primary transition-colors; + } - .cta { - @apply inline-flex items-center justify-center rounded-md font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-background disabled:opacity-50 disabled:cursor-not-allowed px-3 py-1.5 text-xs uppercase tracking-wide bg-primary text-white hover:bg-[#EA580C] shadow-sm; + .cta { + @apply inline-flex items-center justify-center rounded-md font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-background disabled:opacity-50 disabled:cursor-not-allowed px-3 py-1.5 text-xs uppercase tracking-wide bg-primary text-white hover:bg-[#EA580C] shadow-sm; + } + } } - } } - } - .auth-modal { - @apply absolute inset-0 z-60 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4; + .auth-modal { + @apply absolute inset-0 z-60 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4; - .panel { - @apply bg-white rounded-xl shadow-2xl max-w-md w-full p-6 text-center border border-zinc-200; + .panel { + @apply bg-white rounded-xl shadow-2xl max-w-md w-full p-6 text-center border border-zinc-200; - .icon { - @apply w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-4; + .icon { + @apply w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-4; - .alert { - @apply text-primary w-6 h-6; - } - } + .alert { + @apply text-primary w-6 h-6; + } + } - h3 { - @apply text-xl font-serif font-bold text-black mb-2; - } + h3 { + @apply text-xl font-serif font-bold text-black mb-2; + } - p { - @apply text-zinc-600 text-sm mb-6 leading-relaxed; - } + p { + @apply text-zinc-600 text-sm mb-6 leading-relaxed; + } - .actions { - @apply flex flex-col space-y-3; + .actions { + @apply flex flex-col space-y-3; - .confirm { - @apply bg-primary hover:bg-orange-600 text-white; - } + .confirm { + @apply bg-primary hover:bg-orange-600 text-white; + } - .cancel { - @apply text-xs text-zinc-400 hover:text-black mt-2; + .cancel { + @apply text-xs text-zinc-400 hover:text-black mt-2; + } + } } - } } - } - .upload { - @apply w-full; + .upload { + @apply w-full; - .dropzone { - @apply relative h-48 rounded-xl border-2 border-dashed transition-all duration-300 flex flex-col items-center justify-center cursor-pointer bg-zinc-50 border-zinc-200 hover:border-zinc-300 hover:bg-zinc-100; + .dropzone { + @apply relative h-48 rounded-xl border-2 border-dashed transition-all duration-300 flex flex-col items-center justify-center cursor-pointer bg-zinc-50 border-zinc-200 hover:border-zinc-300 hover:bg-zinc-100; - &.is-dragging { - @apply border-primary bg-orange-50; + &.is-dragging { + @apply border-primary bg-orange-50; - .drop-icon { - @apply bg-orange-100 text-primary; - } - } + .drop-icon { + @apply bg-orange-100 text-primary; + } + } - .drop-input { - @apply absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10; - } + .drop-input { + @apply absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10; + } - .drop-content { - @apply flex flex-col items-center pointer-events-none; + .drop-content { + @apply flex flex-col items-center pointer-events-none; - .drop-icon { - @apply w-12 h-12 rounded-full flex items-center justify-center mb-4 transition-colors bg-zinc-200 text-zinc-500; - } + .drop-icon { + @apply w-12 h-12 rounded-full flex items-center justify-center mb-4 transition-colors bg-zinc-200 text-zinc-500; + } - p { - @apply text-zinc-900 font-bold text-sm mb-1; - } + p { + @apply text-zinc-900 font-bold text-sm mb-1; + } - .help { - @apply text-zinc-500 text-xs; - } - } + .help { + @apply text-zinc-500 text-xs; + } + } - &:not(.is-dragging):hover .drop-icon { - @apply bg-zinc-300 text-black; - } - } + &:not(.is-dragging):hover .drop-icon { + @apply bg-zinc-300 text-black; + } + } - .upload-status { - @apply h-48 rounded-xl border border-zinc-200 bg-white p-6 flex flex-col justify-center relative overflow-hidden; + .upload-status { + @apply h-48 rounded-xl border border-zinc-200 bg-white p-6 flex flex-col justify-center relative overflow-hidden; - .status-content { - @apply relative z-10 flex flex-col items-center justify-center text-center; + .status-content { + @apply relative z-10 flex flex-col items-center justify-center text-center; - .status-icon { - @apply w-12 h-12 rounded-lg bg-zinc-100 flex items-center justify-center text-black mb-3 border border-zinc-200; + .status-icon { + @apply w-12 h-12 rounded-lg bg-zinc-100 flex items-center justify-center text-black mb-3 border border-zinc-200; - .check { - @apply text-green-500; - } - } + .check { + @apply text-green-500; + } + } - h3 { - @apply text-black font-bold text-sm truncate max-w-full px-4; - } + h3 { + @apply text-black font-bold text-sm truncate max-w-full px-4; + } - .progress { - @apply w-full max-w-50 h-1.5 bg-zinc-100 rounded-full mt-3 overflow-hidden; + .progress { + @apply w-full max-w-50 h-1.5 bg-zinc-100 rounded-full mt-3 overflow-hidden; - .bar { - @apply h-full bg-primary transition-all duration-300 ease-out; - } - } + .bar { + @apply h-full bg-primary transition-all duration-300 ease-out; + } + } - .status-text { - @apply text-zinc-500 text-xs font-mono uppercase mt-2; + .status-text { + @apply text-zinc-500 text-xs font-mono uppercase mt-2; + } + } } - } } - } - .visualizer { - @apply min-h-screen bg-background pt-6 pb-10 px-4 md:px-6 flex flex-col items-center font-sans relative; + .visualizer { + @apply min-h-screen bg-background pt-6 pb-10 px-4 md:px-6 flex flex-col items-center font-sans relative; - .topbar { - @apply w-full max-w-6xl flex items-center justify-between mb-6 px-2; + .topbar { + @apply w-full max-w-6xl flex items-center justify-between mb-6 px-2; - .brand { - @apply flex items-center space-x-2 cursor-pointer; + .brand { + @apply flex items-center space-x-2 cursor-pointer; - .logo { - @apply w-6 h-6 text-black; - } + .logo { + @apply w-6 h-6 text-black; + } - .name { - @apply text-xl font-serif font-bold text-black tracking-tight; - } - } + .name { + @apply text-xl font-serif font-bold text-black tracking-tight; + } + } - .exit { - @apply text-zinc-500 hover:text-black hover:bg-zinc-100; + .exit { + @apply text-zinc-500 hover:text-black hover:bg-zinc-100; - .icon { - @apply w-5 h-5 mr-2; + .icon { + @apply w-5 h-5 mr-2; + } + } } - } - } - .content { - @apply w-full max-w-6xl grid grid-cols-1 gap-6; - } + .content { + @apply w-full max-w-6xl grid grid-cols-1 gap-6; + } - .panel { - @apply bg-white rounded-xl border border-zinc-200 shadow-2xl overflow-hidden; + .panel { + @apply bg-white rounded-xl border border-zinc-200 shadow-2xl overflow-hidden; - .panel-header { - @apply flex flex-col md:flex-row md:items-center md:justify-between gap-4 p-5 border-b border-zinc-100; + .panel-header { + @apply flex flex-col md:flex-row md:items-center md:justify-between gap-4 p-5 border-b border-zinc-100; - .panel-meta { - p { - @apply text-xs font-mono uppercase tracking-widest text-zinc-400; - } + .panel-meta { + p { + @apply text-xs font-mono uppercase tracking-widest text-zinc-400; + } - h2 { - @apply text-2xl font-serif font-bold text-black; - } + h2 { + @apply text-2xl font-serif font-bold text-black; + } - h3 { - @apply text-lg font-serif font-bold text-black; - } + h3 { + @apply text-lg font-serif font-bold text-black; + } - .note { - @apply text-xs text-zinc-500 mt-1; - } - } + .note { + @apply text-xs text-zinc-500 mt-1; + } + } - .panel-actions { - @apply flex flex-wrap items-center gap-3; + .panel-actions { + @apply flex flex-wrap items-center gap-3; - .export { - @apply bg-primary text-white hover:bg-orange-600 h-9 border-none shadow-sm; - } + .export { + @apply bg-primary text-white hover:bg-orange-600 h-9 border-none shadow-sm; + } - .share { - @apply bg-black text-white h-9 shadow-sm hover:bg-zinc-800; - } - } - } + .share { + @apply bg-black text-white h-9 shadow-sm hover:bg-zinc-800; + } + } + } - .render-area { - @apply relative bg-zinc-100 min-h-105; + .render-area { + @apply relative bg-zinc-100 min-h-105; - .render-img { - @apply w-full h-full object-contain transition-all duration-700 opacity-100 scale-100; - } + .render-img { + @apply w-full h-full object-contain transition-all duration-700 opacity-100 scale-100; + } - &.is-processing { - .render-img { - @apply opacity-50 blur-sm scale-105; - } - } + &.is-processing { + .render-img { + @apply opacity-50 blur-sm scale-105; + } + } - .render-placeholder { - @apply w-full h-full flex items-center justify-center; - } + .render-placeholder { + @apply w-full h-full flex items-center justify-center; + } - .render-fallback { - @apply w-full h-full object-contain opacity-50; - } + .render-fallback { + @apply w-full h-full object-contain opacity-50; + } - .render-overlay { - @apply absolute inset-0 flex flex-col items-center justify-center z-30 bg-white/60 backdrop-blur-sm transition-opacity duration-300; + .render-overlay { + @apply absolute inset-0 flex flex-col items-center justify-center z-30 bg-white/60 backdrop-blur-sm transition-opacity duration-300; - .rendering-card { - @apply bg-white px-6 py-4 rounded-xl border border-zinc-200 flex flex-col items-center shadow-2xl; + .rendering-card { + @apply bg-white px-6 py-4 rounded-xl border border-zinc-200 flex flex-col items-center shadow-2xl; - .spinner { - @apply w-8 h-8 mb-3 animate-spin text-primary; - } + .spinner { + @apply w-8 h-8 mb-3 animate-spin text-primary; + } - .title { - @apply text-sm font-bold text-black; - } + .title { + @apply text-sm font-bold text-black; + } - .subtitle { - @apply text-xs text-zinc-500 mt-1; + .subtitle { + @apply text-xs text-zinc-500 mt-1; + } + } + } } - } - } - } - &.compare { - @apply shadow-xl; + &.compare { + @apply shadow-xl; - .panel-header { - @apply flex items-center justify-between; + .panel-header { + @apply flex items-center justify-between; - .hint { - @apply text-xs text-zinc-400; - } - } + .hint { + @apply text-xs text-zinc-400; + } + } - .compare-stage { - @apply relative bg-zinc-100 overflow-hidden; - } + .compare-stage { + @apply relative bg-zinc-100 overflow-hidden; + } - .compare-fallback { - @apply flex items-center justify-center; - } + .compare-fallback { + @apply flex items-center justify-center; + } - .compare-img { - @apply w-full h-auto object-contain; + .compare-img { + @apply w-full h-auto object-contain; + } + } } - } } - } - .home { - @apply min-h-screen bg-background relative overflow-x-hidden text-foreground; + .home { + @apply min-h-screen bg-background relative overflow-x-hidden text-foreground; - .hero { - @apply pt-32 pb-20 px-6 max-w-7xl mx-auto flex flex-col items-center text-center; + .hero { + @apply pt-32 pb-20 px-6 max-w-7xl mx-auto flex flex-col items-center text-center; - .announce { - @apply mb-8 inline-flex items-center px-3 py-1 rounded-md bg-white border border-zinc-200 shadow-sm; + .announce { + @apply mb-8 inline-flex items-center px-3 py-1 rounded-md bg-white border border-zinc-200 shadow-sm; - .dot { - @apply w-4 h-4 rounded bg-blue-100 flex items-center justify-center mr-2; + .dot { + @apply w-4 h-4 rounded bg-blue-100 flex items-center justify-center mr-2; - .pulse { - @apply w-2 h-2 rounded-full bg-blue-500 animate-pulse; - } - } + .pulse { + @apply w-2 h-2 rounded-full bg-blue-500 animate-pulse; + } + } - p { - @apply text-[10px] font-bold uppercase tracking-widest text-zinc-600; - } - } + p { + @apply text-[10px] font-bold uppercase tracking-widest text-zinc-600; + } + } - h1 { - @apply text-6xl md:text-8xl font-serif leading-[0.95] text-black mb-8 max-w-5xl mx-auto; - } + h1 { + @apply text-6xl md:text-8xl font-serif leading-[0.95] text-black mb-8 max-w-5xl mx-auto; + } - .subtitle { - @apply max-w-2xl mx-auto text-xs md:text-sm font-mono uppercase tracking-widest text-zinc-500 mb-10 leading-relaxed; - } + .subtitle { + @apply max-w-2xl mx-auto text-xs md:text-sm font-mono uppercase tracking-widest text-zinc-500 mb-10 leading-relaxed; + } - .actions { - @apply flex flex-col sm:flex-row gap-4 justify-center mb-16; + .actions { + @apply flex flex-col sm:flex-row gap-4 justify-center mb-16; - .cta { - @apply inline-flex items-center justify-center rounded-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-background px-8 py-3 text-sm uppercase tracking-wide font-bold bg-primary text-white hover:bg-[#ea580c] shadow-md; + .cta { + @apply inline-flex items-center justify-center rounded-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-background px-8 py-3 text-sm uppercase tracking-wide font-bold bg-primary text-white hover:bg-[#ea580c] shadow-md; - .icon { - @apply ml-2 w-4 h-4; - } - } + .icon { + @apply ml-2 w-4 h-4; + } + } - .demo { - @apply px-8 bg-white rounded-md hover:bg-zinc-50; - } - } + .demo { + @apply px-8 bg-white rounded-md hover:bg-zinc-50; + } + } - .upload-shell { - @apply w-full max-w-5xl mx-auto relative rounded-3xl overflow-hidden bg-linear-to-b from-blue-100/50 to-white/50 border border-blue-100 p-6 md:p-12 shadow-sm min-h-100 flex items-center justify-center; + .upload-shell { + @apply w-full max-w-5xl mx-auto relative rounded-3xl overflow-hidden bg-linear-to-b from-blue-100/50 to-white/50 border border-blue-100 p-6 md:p-12 shadow-sm min-h-100 flex items-center justify-center; - .grid-overlay { - @apply absolute inset-0 opacity-40 pointer-events-none; + .grid-overlay { + @apply absolute inset-0 opacity-40 pointer-events-none; - background-image: - linear-gradient(#3b82f6 1px, transparent 1px), - linear-gradient(90deg, #3b82f6 1px, transparent 1px); + background-image: + linear-gradient(#3b82f6 1px, transparent 1px), + linear-gradient(90deg, #3b82f6 1px, transparent 1px); - background-size: 60px 60px; - } + background-size: 60px 60px; + } - .upload-card { - @apply relative w-full max-w-xl bg-white rounded-2xl shadow-xl overflow-hidden border border-zinc-100 p-8 z-10 transition-transform hover:scale-[1.01] duration-500; + .upload-card { + @apply relative w-full max-w-xl bg-white rounded-2xl shadow-xl overflow-hidden border border-zinc-100 p-8 z-10 transition-transform hover:scale-[1.01] duration-500; - .upload-head { - @apply text-center mb-6; + .upload-head { + @apply text-center mb-6; - .upload-icon { - @apply w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-3; + .upload-icon { + @apply w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-3; - .icon { - @apply text-orange-500 w-6 h-6; - } - } + .icon { + @apply text-orange-500 w-6 h-6; + } + } - h3 { - @apply text-xl font-serif font-bold text-black; - } + h3 { + @apply text-xl font-serif font-bold text-black; + } - p { - @apply text-zinc-500 text-sm mt-1; + p { + @apply text-zinc-500 text-sm mt-1; + } + } + } } - } } - } - } - .projects { - @apply py-24 bg-white relative border-b border-zinc-100; + .projects { + @apply py-24 bg-white relative border-b border-zinc-100; - .section-inner { - @apply max-w-7xl mx-auto px-6; + .section-inner { + @apply max-w-7xl mx-auto px-6; - .section-head { - @apply flex flex-col md:flex-row md:items-end justify-between mb-12 gap-6; + .section-head { + @apply flex flex-col md:flex-row md:items-end justify-between mb-12 gap-6; - .copy { - @apply max-w-2xl; + .copy { + @apply max-w-2xl; - h2 { - @apply text-4xl font-serif text-black mb-4; - } + h2 { + @apply text-4xl font-serif text-black mb-4; + } - p { - @apply text-zinc-500 text-lg; - } - } - } + p { + @apply text-zinc-500 text-lg; + } + } + } - .loading { - @apply flex items-center justify-center text-sm text-zinc-500; - } + .loading { + @apply flex items-center justify-center text-sm text-zinc-500; + } - .projects-grid { - @apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8; + .projects-grid { + @apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8; - .project-card { - @apply relative bg-white rounded-xl overflow-hidden border border-zinc-200 shadow-sm hover:shadow-xl transition-all duration-300 cursor-pointer flex flex-col h-full; + .project-card { + @apply relative bg-white rounded-xl overflow-hidden border border-zinc-200 shadow-sm hover:shadow-xl transition-all duration-300 cursor-pointer flex flex-col h-full; - .preview { - @apply aspect-4/3 overflow-hidden bg-zinc-100 relative; + .preview { + @apply aspect-4/3 overflow-hidden bg-zinc-100 relative; - img { - @apply w-full h-full object-cover group-hover:scale-105 transition-transform duration-700; - } + img { + @apply w-full h-full object-cover group-hover:scale-105 transition-transform duration-700; + } - .badge { - @apply absolute top-3 left-3 bg-white/90 backdrop-blur-md px-2 py-1 rounded-md border border-zinc-200 shadow-sm; + .badge { + @apply absolute top-3 left-3 bg-white/90 backdrop-blur-md px-2 py-1 rounded-md border border-zinc-200 shadow-sm; - span { - @apply text-[10px] font-bold uppercase tracking-wider text-zinc-800; - } - } - } + span { + @apply text-[10px] font-bold uppercase tracking-wider text-zinc-800; + } + } + } - .card-body { - @apply p-5 flex justify-between items-center bg-white border-t border-zinc-100 grow; + .card-body { + @apply p-5 flex justify-between items-center bg-white border-t border-zinc-100 grow; - h3 { - @apply text-lg font-serif font-bold text-zinc-900 group-hover:text-primary transition-colors; - } + h3 { + @apply text-lg font-serif font-bold text-zinc-900 group-hover:text-primary transition-colors; + } - .meta { - @apply flex items-center text-zinc-400 text-xs mt-1 space-x-2; + .meta { + @apply flex items-center text-zinc-400 text-xs mt-1 space-x-2; - span { - @apply font-mono uppercase; - } + span { + @apply font-mono uppercase; + } - span:last-child { - @apply text-zinc-400 text-[10px] uppercase tracking-wide; - } - } + span:last-child { + @apply text-zinc-400 text-[10px] uppercase tracking-wide; + } + } - .arrow { - @apply w-10 h-10 rounded-full bg-zinc-50 border border-zinc-200 flex items-center justify-center text-zinc-400 group-hover:bg-primary group-hover:text-white group-hover:border-primary transition-all duration-300; - } - } - } + .arrow { + @apply w-10 h-10 rounded-full bg-zinc-50 border border-zinc-200 flex items-center justify-center text-zinc-400 group-hover:bg-primary group-hover:text-white group-hover:border-primary transition-all duration-300; + } + } + } - .empty { - @apply col-span-full rounded-xl border border-dashed border-zinc-200 bg-zinc-50 p-10 text-center text-sm text-zinc-500; - } + .empty { + @apply col-span-full rounded-xl border border-dashed border-zinc-200 bg-zinc-50 p-10 text-center text-sm text-zinc-500; + } + } + } } - } - } - .partners { - @apply border-t border-zinc-200 py-12 bg-white; + .partners { + @apply border-t border-zinc-200 py-12 bg-white; - .section-inner { - @apply max-w-7xl mx-auto px-6; + .section-inner { + @apply max-w-7xl mx-auto px-6; - .logos { - @apply grid grid-cols-2 md:grid-cols-5 gap-8 items-center opacity-60 grayscale hover:grayscale-0 transition-all duration-500; - } + .logos { + @apply grid grid-cols-2 md:grid-cols-5 gap-8 items-center opacity-60 grayscale hover:grayscale-0 transition-all duration-500; + } - .logo-item { - @apply flex items-center justify-center space-x-2 cursor-pointer; + .logo-item { + @apply flex items-center justify-center space-x-2 cursor-pointer; - div { - @apply text-zinc-800 group-hover:text-black transition-colors; - } + div { + @apply text-zinc-800 group-hover:text-black transition-colors; + } - span { - @apply font-bold text-xl text-zinc-800 group-hover:text-black transition-colors tracking-tight; - } + span { + @apply font-bold text-xl text-zinc-800 group-hover:text-black transition-colors tracking-tight; + } + } + } } - } } - } - .visualizer-route { - @apply min-h-screen flex items-center justify-center; + .visualizer-route { + @apply min-h-screen flex items-center justify-center; - &.loading { - @apply text-sm text-zinc-500; + &.loading { + @apply text-sm text-zinc-500; + } } - } -} +} \ No newline at end of file diff --git a/app/routes.ts b/app/routes.ts index 102b402..3049dc3 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -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; diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 0bfe7a0..7284978 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -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) { return [ @@ -11,6 +13,16 @@ 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; + } + return (
@@ -47,7 +59,7 @@ export default function Home() {

Upload your floor

Supports JPG, PNG formats up to 10MB

-

Upload Images

+ diff --git a/app/routes/visualizer.$id.tsx b/app/routes/visualizer.$id.tsx new file mode 100644 index 0000000..7090ee1 --- /dev/null +++ b/app/routes/visualizer.$id.tsx @@ -0,0 +1,7 @@ +const VisualizerId = () => { + return ( +
VisualizerId
+ ) +} + +export default VisualizerId \ No newline at end of file diff --git a/components/Upload.tsx b/components/Upload.tsx new file mode 100644 index 0000000..f37fb6e --- /dev/null +++ b/components/Upload.tsx @@ -0,0 +1,154 @@ +import React, {useCallback, useEffect, useRef, 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(null); + const [isDragging, setIsDragging] = useState(false); + const [progress, setProgress] = useState(0); + const intervalRef = useRef(null); + const timeoutRef = useRef(null); + + const { isSignedIn } = useOutletContext(); + + useEffect(() => { + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + }, []); + + 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; + + intervalRef.current = setInterval(() => { + setProgress((prev) => { + const next = prev + PROGRESS_INCREMENT; + if (next >= 100) { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + timeoutRef.current = setTimeout(() => { + onComplete?.(base64Data); + timeoutRef.current = null; + }, REDIRECT_DELAY_MS); + return 100; + } + return next; + }); + }, PROGRESS_INTERVAL_MS); + }; + reader.readAsDataURL(file); + }, [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) => { + if (!isSignedIn) return; + + const selectedFile = e.target.files?.[0]; + if (selectedFile) { + processFile(selectedFile); + } + }; + + return ( +
+ {!file ? ( +
+ + +
+
+ +
+

+ {isSignedIn ? ( + "Click to upload or just drag and drop" + ): ("Sign in or sign up with Puter to upload")} +

+

Maximum file size 50 MB.

+
+
+ ) : ( +
+
+
+ {progress === 100 ? ( + + ): ( + + )} +
+ +

{file.name}

+ +
+
+ +

+ {progress < 100 ? 'Analyzing Floor Plan...' : 'Redirecting...'} +

+
+
+
+ )} +
+ ) +} +export default Upload \ No newline at end of file diff --git a/lib/constants.ts b/lib/constants.ts new file mode 100644 index 0000000..cd740a7 --- /dev/null +++ b/lib/constants.ts @@ -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(); \ No newline at end of file