first commit

This commit is contained in:
NalagamdinniRaju 2026-02-11 12:35:23 +05:30
commit cbd63f0648
17 changed files with 7116 additions and 0 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
.react-router
build
node_modules
README.md

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.DS_Store
.env
/node_modules/
# React Router
/.react-router/
/build/

22
Dockerfile Normal file
View File

@ -0,0 +1,22 @@
FROM node:20-alpine AS development-dependencies-env
COPY . /app
WORKDIR /app
RUN npm ci
FROM node:20-alpine AS production-dependencies-env
COPY ./package.json package-lock.json /app/
WORKDIR /app
RUN npm ci --omit=dev
FROM node:20-alpine AS build-env
COPY . /app/
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
WORKDIR /app
RUN npm run build
FROM node:20-alpine
COPY ./package.json package-lock.json /app/
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
COPY --from=build-env /app/build /app/build
WORKDIR /app
CMD ["npm", "run", "start"]

87
README.md Normal file
View File

@ -0,0 +1,87 @@
# Welcome to React Router!
A modern, production-ready template for building full-stack React applications using React Router.
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
## Features
- 🚀 Server-side rendering
- ⚡️ Hot Module Replacement (HMR)
- 📦 Asset bundling and optimization
- 🔄 Data loading and mutations
- 🔒 TypeScript by default
- 🎉 TailwindCSS for styling
- 📖 [React Router docs](https://reactrouter.com/)
## Getting Started
### Installation
Install the dependencies:
```bash
npm install
```
### Development
Start the development server with HMR:
```bash
npm run dev
```
Your application will be available at `http://localhost:5173`.
## Building for Production
Create a production build:
```bash
npm run build
```
## Deployment
### Docker Deployment
To build and run using Docker:
```bash
docker build -t my-app .
# Run the container
docker run -p 3000:3000 my-app
```
The containerized application can be deployed to any platform that supports Docker, including:
- AWS ECS
- Google Cloud Run
- Azure Container Apps
- Digital Ocean App Platform
- Fly.io
- Railway
### DIY Deployment
If you're familiar with deploying Node applications, the built-in app server is production-ready.
Make sure to deploy the output of `npm run build`
```
├── package.json
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
├── build/
│ ├── client/ # Static assets
│ └── server/ # Server-side code
```
## Styling
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
---
Built with ❤️ using React Router.

60
app/app.css Normal file
View File

@ -0,0 +1,60 @@
@import 'tailwindcss';
@theme {
--font-sans:
'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
html,
body {
@apply bg-white dark:bg-gray-950;
@media (prefers-color-scheme: dark) {
color-scheme: dark;
}
}
* {
box-sizing: border-box;
}
body {
font-family:
'Inter',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
'Roboto',
sans-serif;
background: #050509;
margin: 0;
padding: 0;
height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
model-viewer {
width: 100%;
height: 500px;
background: linear-gradient(to bottom, #e0e7ff, #f3f4f6);
border-radius: 16px;
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
position: relative;
}
@media (max-width: 768px) {
model-viewer {
height: 400px;
}
}
::placeholder {
color: rgb(156 163 175); /* gray-400 */
opacity: 1;
}

6
app/lib/supabase.ts Normal file
View File

@ -0,0 +1,6 @@
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY,
);

133
app/qr-ar/ar-viewer.tsx Normal file
View File

@ -0,0 +1,133 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router';
import { supabase } from '~/lib/supabase';
// --- A-FRAME/AR.JS CUSTOM ELEMENTS TYPING FIX ---
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'a-scene': any;
'a-assets': any;
'a-asset-item': any;
'a-marker': any;
'a-entity': any;
'a-text': any;
'model-viewer': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
> & {
src?: string;
'ios-src'?: string;
ar?: boolean;
'ar-modes'?: string;
'ar-scale'?: string;
'ar-placement'?: string;
'camera-controls'?: boolean;
'touch-action'?: string;
alt?: string;
'shadow-intensity'?: string | number;
'shadow-softness'?: string | number;
exposure?: string | number;
'interaction-prompt'?: string;
'min-camera-orbit'?: string;
'max-camera-orbit'?: string;
'camera-orbit'?: string;
'field-of-view'?: string;
scale?: string;
'auto-rotate'?: boolean;
'rotation-per-second'?: string;
onLoad?: (e: any) => void;
'onAr-status'?: (e: any) => void;
'onModel-visibility'?: (e: any) => void;
'onCamera-change'?: (e: any) => void;
};
}
}
}
export default function ARViewer() {
const { id } = useParams();
const modelViewerRef = useRef<any>(null);
const [asset, setAsset] = useState<any>(null);
const [scriptLoaded, setScriptLoaded] = useState(false);
useEffect(() => {
if (!id) return;
const loadAsset = async () => {
const { data, error } = await supabase
.from('ar_assets')
.select('*')
.eq('id', id)
.single();
console.log('Supabase response:', { data, error });
if (!error && data) {
setAsset(data);
}
};
loadAsset();
}, [id]);
useEffect(() => {
if (document.getElementById('model-viewer-script')) {
setScriptLoaded(true);
return;
}
const script = document.createElement('script');
script.id = 'model-viewer-script';
script.type = 'module';
script.src =
'https://ajax.googleapis.com/ajax/libs/model-viewer/3.4.0/model-viewer.min.js';
script.onload = () => {
console.log('model-viewer loaded');
setScriptLoaded(true);
};
document.head.appendChild(script);
}, []);
if (!scriptLoaded) {
return (
<pre style={{ padding: 20 }}>
Loading AR Viewer
{'\n\n'}
asset: {JSON.stringify(asset, null, 2)}
{'\n'}
scriptLoaded: {String(scriptLoaded)}
{'\n'}
id: {String(id)}
</pre>
);
}
return (
<model-viewer
ref={modelViewerRef}
src={asset?.glb_url}
ios-src={asset?.usdz_url}
ar
ar-modes='webxr scene-viewer quick-look'
camera-controls
auto-rotate
style={{ width: '100vw', height: '80vh' }}
onError={(e) => {
console.error('Model viewer error:', e);
console.error('Failed to load:', asset?.glb_url);
}}
onLoad={() => console.log('Model loaded successfully')}
/>
// <div className='bg-white text-2xl text-red-400'>
// Id : {asset?.id}
// AR Viewer for asset: {asset?.name}
// glb URL: {asset?.glb_url}
// usdz URL: {asset?.usdz_url}
// </div>
);
}

75
app/root.tsx Normal file
View File

@ -0,0 +1,75 @@
import {
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "react-router";
import type { Route } from "./+types/root";
import "./app.css";
export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
},
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = "Oops!";
let details = "An unexpected error occurred.";
let stack: string | undefined;
if (isRouteErrorResponse(error)) {
message = error.status === 404 ? "404" : "Error";
details =
error.status === 404
? "The requested page could not be found."
: error.statusText || details;
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message;
stack = error.stack;
}
return (
<main className="pt-16 p-4 container mx-auto">
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre className="w-full p-4 overflow-x-auto">
<code>{stack}</code>
</pre>
)}
</main>
);
}

6
app/routes.ts Normal file
View File

@ -0,0 +1,6 @@
import { type RouteConfig, index, route } from '@react-router/dev/routes';
export default [
index('routes/home.tsx'),
route('ar/:id', './qr-ar/ar-viewer.tsx'),
] satisfies RouteConfig;

172
app/routes/home.tsx Normal file
View File

@ -0,0 +1,172 @@
'use client';
import { useEffect, useState } from 'react';
import QRCode from 'qrcode';
import { supabase } from '~/lib/supabase';
export default function Dashboard() {
const [name, setName] = useState('');
const [glbUrl, setGlbUrl] = useState('');
const [usdzUrl, setUsdzUrl] = useState('');
const [assets, setAssets] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const fetchAssets = async () => {
const { data } = await supabase
.from('ar_assets')
.select('*')
.order('created_at', { ascending: false });
setAssets(data || []);
};
useEffect(() => {
fetchAssets();
}, []);
const createAsset = async () => {
if (!name || !glbUrl || !usdzUrl) {
alert('Fill all fields');
return;
}
setLoading(true);
const { data, error } = await supabase
.from('ar_assets')
.insert({
name,
glb_url: glbUrl,
usdz_url: usdzUrl,
})
.select()
.single();
if (error || !data) {
alert(error?.message);
setLoading(false);
return;
}
const qrUrl = `http://10.20.2.107:5173/ar/${data.id}`;
// const qrUrl = `${window.location.origin}/v/${data.id}`;
const qrImage = await QRCode.toDataURL(qrUrl, {
width: 512,
margin: 2,
});
const qrBlob = await (await fetch(qrImage)).blob();
const filePath = `${data.id}.png`;
await supabase.storage.from('qr-codes').upload(filePath, qrBlob, {
upsert: true,
contentType: 'image/png',
});
const { data: publicUrl } = supabase.storage
.from('qr-codes')
.getPublicUrl(filePath);
await supabase
.from('ar_assets')
.update({
qr_url: qrUrl,
qr_image_url: publicUrl.publicUrl,
})
.eq('id', data.id);
setName('');
setGlbUrl('');
setUsdzUrl('');
setLoading(false);
fetchAssets();
};
return (
<div className='min-h-screen bg-gray-50 px-6 py-10'>
<div className='mx-auto max-w-5xl space-y-10'>
<div>
<h1 className='text-3xl font-bold text-gray-900'>
AR Asset Dashboard
</h1>
<p className='mt-1 text-gray-500'>
Manage 3D assets and generate AR QR codes
</p>
</div>
<div className='rounded-2xl bg-white p-6 shadow-sm'>
<h2 className='mb-4 text-lg font-semibold text-gray-800'>
Create New Asset
</h2>
<div className='grid gap-4 md:grid-cols-3 text-black'>
<input
className='rounded-lg border border-gray-300 px-4 py-2 text-sm focus:border-black focus:outline-none'
placeholder='Asset name'
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
className='rounded-lg border border-gray-300 px-4 py-2 text-sm focus:border-black focus:outline-none'
placeholder='GLB URL'
value={glbUrl}
onChange={(e) => setGlbUrl(e.target.value)}
/>
<input
className='rounded-lg border border-gray-300 px-4 py-2 text-sm focus:border-black focus:outline-none'
placeholder='USDZ URL'
value={usdzUrl}
onChange={(e) => setUsdzUrl(e.target.value)}
/>
</div>
<button
onClick={createAsset}
disabled={loading}
className='mt-6 inline-flex items-center justify-center rounded-xl bg-black px-6 py-2.5 text-sm font-medium text-white transition hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-60'
>
{loading ? 'Creating…' : 'Create Asset'}
</button>
</div>
<div>
<h2 className='mb-4 text-lg font-semibold text-gray-800'>
Generated Assets
</h2>
{assets.length === 0 ? (
<p className='text-sm text-gray-500'>
No assets created yet.
</p>
) : (
<div className='grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3'>
{assets.map((asset) => (
<div
key={asset.id}
className='rounded-2xl bg-white p-5 shadow-sm transition hover:shadow-md'
>
<h3 className='mb-3 text-base font-semibold text-gray-900'>
{asset?.name}
</h3>
{asset?.qr_image_url ? (
<img
src={asset?.qr_image_url}
alt={asset?.name}
className='mx-auto w-40 rounded-lg'
/>
) : (
<div className='flex h-40 items-center justify-center rounded-lg bg-gray-100 text-sm text-gray-400'>
No QR
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
);
}

4244
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "ar-test",
"private": true,
"type": "module",
"scripts": {
"build": "react-router build",
"dev": "react-router dev",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc"
},
"dependencies": {
"@react-router/node": "^7.9.2",
"@react-router/serve": "^7.9.2",
"@supabase/supabase-js": "^2.95.3",
"isbot": "^5.1.31",
"lucide-react": "^0.555.0",
"qrcode": "^1.5.4",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router": "^7.9.2"
},
"devDependencies": {
"@react-router/dev": "^7.9.2",
"@tailwindcss/vite": "^4.1.13",
"@types/node": "^22",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"@types/three": "^0.181.0",
"tailwindcss": "^4.1.13",
"typescript": "^5.9.2",
"vite": "^7.1.7",
"vite-tsconfig-paths": "^5.1.4"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

7
react-router.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { Config } from "@react-router/dev/config";
export default {
// Config options...
// Server-side render by default, to enable SPA mode set this to `false`
ssr: true,
} satisfies Config;

27
tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"include": [
"**/*",
"**/.server/**/*",
"**/.client/**/*",
".react-router/types/**/*"
],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["node", "vite/client"],
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"rootDirs": [".", "./.react-router/types"],
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
},
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true
}
}

8
vite.config.ts Normal file
View File

@ -0,0 +1,8 @@
import { reactRouter } from "@react-router/dev/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
});

2223
yarn.lock Normal file

File diff suppressed because it is too large Load Diff