first commit
This commit is contained in:
commit
cbd63f0648
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
||||
.react-router
|
||||
build
|
||||
node_modules
|
||||
README.md
|
||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.DS_Store
|
||||
.env
|
||||
/node_modules/
|
||||
|
||||
# React Router
|
||||
/.react-router/
|
||||
/build/
|
||||
22
Dockerfile
Normal file
22
Dockerfile
Normal 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
87
README.md
Normal file
@ -0,0 +1,87 @@
|
||||
# Welcome to React Router!
|
||||
|
||||
A modern, production-ready template for building full-stack React applications using React Router.
|
||||
|
||||
[](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
60
app/app.css
Normal 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
6
app/lib/supabase.ts
Normal 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
133
app/qr-ar/ar-viewer.tsx
Normal 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
75
app/root.tsx
Normal 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
6
app/routes.ts
Normal 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
172
app/routes/home.tsx
Normal 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
4244
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
7
react-router.config.ts
Normal file
7
react-router.config.ts
Normal 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
27
tsconfig.json
Normal 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
8
vite.config.ts
Normal 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()],
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user