From 1138ee2fa06f81b4e30bb96209ae9a3f1dfa0639 Mon Sep 17 00:00:00 2001 From: rajun Date: Tue, 21 Jul 2026 18:11:33 +0530 Subject: [PATCH] feat: add login page and protect admin dashboard --- app/routes/dashboard.tsx | 277 ++++++++++++++++++++++++++++++++ app/routes/home.tsx | 335 --------------------------------------- app/routes/login.tsx | 139 ++++++++++++++++ 3 files changed, 416 insertions(+), 335 deletions(-) create mode 100644 app/routes/dashboard.tsx delete mode 100644 app/routes/home.tsx create mode 100644 app/routes/login.tsx diff --git a/app/routes/dashboard.tsx b/app/routes/dashboard.tsx new file mode 100644 index 0000000..4ba3549 --- /dev/null +++ b/app/routes/dashboard.tsx @@ -0,0 +1,277 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router'; +import QRCode from 'qrcode'; +import { pb } from '~/lib/pocketbase'; +import { ImageOff, LogOut, FileCode } from 'lucide-react'; +import { FileOrUrlField, type InputMode } from '~/components/FileOrUrlField'; + +export default function Dashboard() { + const navigate = useNavigate(); + const [authCheckComplete, setAuthCheckComplete] = useState(false); + const [name, setName] = useState(''); + + // GLB + const [glbMode, setGlbMode] = useState('file'); + const [glbUrl, setGlbUrl] = useState(''); + const [glbFile, setGlbFile] = useState(null); + const glbInputRef = useRef(null); + + // USDZ + const [usdzMode, setUsdzMode] = useState('file'); + const [usdzUrl, setUsdzUrl] = useState(''); + const [usdzFile, setUsdzFile] = useState(null); + const usdzInputRef = useRef(null); + + const [assets, setAssets] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchAssets = async () => { + try { + const records = await pb.collection('ar_assets').getFullList({ + sort: '-created', + }); + setAssets(records); + } catch (err) { + console.error('Failed to fetch assets:', err); + setAssets([]); + } + }; + + useEffect(() => { + if (!pb.authStore.isValid) { + navigate('/login', { replace: true }); + } else { + setAuthCheckComplete(true); + fetchAssets(); + } + }, [navigate]); + + const handleLogout = () => { + pb.authStore.clear(); + navigate('/login', { replace: true }); + }; + + const createAsset = async () => { + const hasGlb = glbMode === 'file' ? !!glbFile : !!glbUrl.trim(); + if (!name.trim() || !hasGlb) { + alert('Please provide a name and a GLB file or URL'); + return; + } + + setLoading(true); + + try { + const formData = new FormData(); + formData.append('name', name.trim()); + + if (glbMode === 'file' && glbFile) { + formData.append('glb_file', glbFile); + } else { + formData.append('glb_url', glbUrl.trim()); + } + + if (usdzMode === 'file' && usdzFile) { + formData.append('usdz_file', usdzFile); + } else if (usdzMode === 'url' && usdzUrl.trim()) { + formData.append('usdz_url', usdzUrl.trim()); + } + + const record = await pb.collection('ar_assets').create(formData); + + const qrUrl = `${import.meta.env.VITE_FRONTEND_URL}/ar/${record.id}`; + + const qrImage = await QRCode.toDataURL(qrUrl, { + width: 512, + margin: 2, + }); + + const qrBlob = await (await fetch(qrImage)).blob(); + + const qrFormData = new FormData(); + qrFormData.append('qr_url', qrUrl); + qrFormData.append('qr_image', new File([qrBlob], `${record.id}.png`, { type: 'image/png' })); + + await pb.collection('ar_assets').update(record.id, qrFormData); + + // Reset all fields + setName(''); + setGlbUrl(''); + setGlbFile(null); + setUsdzUrl(''); + setUsdzFile(null); + if (glbInputRef.current) glbInputRef.current.value = ''; + if (usdzInputRef.current) usdzInputRef.current.value = ''; + + fetchAssets(); + } catch (err: any) { + alert(err?.message || 'Failed to create asset'); + } finally { + setLoading(false); + } + }; + + if (!authCheckComplete) { + return ( +
+
+

Checking authorization…

+
+ ); + } + + return ( +
+
+ {/* Header */} +
+
+
+ t. +
+
+

+ AR Asset Dashboard +

+

+ Manage 3D assets and generate AR QR codes +

+
+
+
+ {pb.authStore.model?.email && ( + + {pb.authStore.model.email} + + )} + +
+
+ + {/* Form Card */} +
+

+ Create New Asset +

+ +
+ {/* Asset Name */} +
+ + setName(e.target.value)} + /> +
+ + {/* GLB — Android / Web */} + + + {/* USDZ — iOS */} + +
+ + +
+ + {/* Grid Header */} +
+

+ Generated Assets +

+ + {assets.length === 0 ? ( +
+ +

+ No assets created yet. Upload a model above to get started. +

+
+ ) : ( +
+ {assets.map((asset) => ( +
+

+ {asset?.name} +

+ +
+ {asset?.qr_image ? ( + {asset?.name} + ) : ( + asset?.qr_file ? ( + {asset?.name} + ) : ( +
+ + No QR Image +
+ ) + )} +
+
+ ))} +
+ )} +
+
+
+ ); +} + diff --git a/app/routes/home.tsx b/app/routes/home.tsx deleted file mode 100644 index cdf8e1b..0000000 --- a/app/routes/home.tsx +++ /dev/null @@ -1,335 +0,0 @@ -'use client'; - -import { useEffect, useRef, useState } from 'react'; -import QRCode from 'qrcode'; -import { pb } from '~/lib/pocketbase'; -import { ImageOff, Upload, Link, X } from 'lucide-react'; - -type InputMode = 'url' | 'file'; - -export default function Dashboard() { - const [name, setName] = useState(''); - - // GLB - const [glbMode, setGlbMode] = useState('file'); - const [glbUrl, setGlbUrl] = useState(''); - const [glbFile, setGlbFile] = useState(null); - const glbInputRef = useRef(null); - - // USDZ - const [usdzMode, setUsdzMode] = useState('file'); - const [usdzUrl, setUsdzUrl] = useState(''); - const [usdzFile, setUsdzFile] = useState(null); - const usdzInputRef = useRef(null); - - const [assets, setAssets] = useState([]); - const [loading, setLoading] = useState(false); - - const fetchAssets = async () => { - try { - const records = await pb.collection('ar_assets').getFullList({ - sort: '-created', - }); - setAssets(records); - } catch (err) { - console.error('Failed to fetch assets:', err); - setAssets([]); - } - }; - - useEffect(() => { - fetchAssets(); - }, []); - - const createAsset = async () => { - const hasGlb = glbMode === 'file' ? !!glbFile : !!glbUrl.trim(); - if (!name.trim() || !hasGlb) { - alert('Please provide a name and a GLB file or URL'); - return; - } - - setLoading(true); - - try { - // Build FormData so we can send files + fields in one request - const formData = new FormData(); - formData.append('name', name.trim()); - - if (glbMode === 'file' && glbFile) { - formData.append('glb_file', glbFile); - } else { - formData.append('glb_url', glbUrl.trim()); - } - - if (usdzMode === 'file' && usdzFile) { - formData.append('usdz_file', usdzFile); - } else if (usdzMode === 'url' && usdzUrl.trim()) { - formData.append('usdz_url', usdzUrl.trim()); - } - - const record = await pb.collection('ar_assets').create(formData); - - const qrUrl = `${import.meta.env.VITE_FRONTEND_URL}/ar/${record.id}`; - - const qrImage = await QRCode.toDataURL(qrUrl, { - width: 512, - margin: 2, - }); - - const qrBlob = await (await fetch(qrImage)).blob(); - - const qrFormData = new FormData(); - qrFormData.append('qr_url', qrUrl); - qrFormData.append('qr_image', new File([qrBlob], `${record.id}.png`, { type: 'image/png' })); - - await pb.collection('ar_assets').update(record.id, qrFormData); - - // Reset all fields - setName(''); - setGlbUrl(''); - setGlbFile(null); - setUsdzUrl(''); - setUsdzFile(null); - if (glbInputRef.current) glbInputRef.current.value = ''; - if (usdzInputRef.current) usdzInputRef.current.value = ''; - - fetchAssets(); - } catch (err: any) { - alert(err?.message || 'Failed to create asset'); - } finally { - setLoading(false); - } - }; - - return ( -
-
-
-

- AR Asset Dashboard -

-

- Manage 3D assets and generate AR QR codes -

-
- -
-

- Create New Asset -

- -
- {/* Asset Name */} -
- - setName(e.target.value)} - /> -
- - {/* GLB — Android / Web */} - - - {/* USDZ — iOS */} - -
- - -
- -
-

- Generated Assets -

- - {assets.length === 0 ? ( -

- No assets created yet. -

- ) : ( -
- {assets.map((asset) => ( -
-

- {asset?.name} -

- - {asset?.qr_image ? ( - {asset?.name} - ) : ( - asset?.qr_file ? ( - {asset?.name} - ) :
- - No QR Image -
- )} -
- ))} -
- )} -
-
-
- ); -} - -// ─── Reusable File-or-URL field ─────────────────────────────────────────────── -interface FileOrUrlFieldProps { - label: string; - required?: boolean; - mode: InputMode; - onModeChange: (m: InputMode) => void; - accept: string; - file: File | null; - onFileChange: (f: File | null) => void; - inputRef: React.RefObject; - url: string; - onUrlChange: (u: string) => void; - urlPlaceholder: string; -} - -function FileOrUrlField({ - label, required, mode, onModeChange, - accept, file, onFileChange, inputRef, - url, onUrlChange, urlPlaceholder, -}: FileOrUrlFieldProps) { - return ( -
- {/* Label + toggle */} -
- -
- - -
-
- - {mode === 'file' ? ( -
inputRef.current?.click()} - className={`flex cursor-pointer items-center gap-3 rounded-lg border-2 border-dashed px-4 py-3 transition-colors ${ - file ? 'border-black bg-gray-50' : 'border-gray-200 hover:border-gray-400' - }`} - > - onFileChange(e.target.files?.[0] ?? null)} - /> - {file ? ( - <> -
-
- -
- {file.name} - - {(file.size / 1024 / 1024).toFixed(1)} MB - -
- - - ) : ( -
- - Click to choose a file ({accept}) -
- )} -
- ) : ( - onUrlChange(e.target.value)} - /> - )} -
- ); -} diff --git a/app/routes/login.tsx b/app/routes/login.tsx new file mode 100644 index 0000000..e9c15b1 --- /dev/null +++ b/app/routes/login.tsx @@ -0,0 +1,139 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router'; +import { pb } from '~/lib/pocketbase'; +import { Lock, Mail, Eye, EyeOff, Loader2, ArrowRight } from 'lucide-react'; + +export default function Login() { + const navigate = useNavigate(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + // If already logged in, redirect to dashboard + useEffect(() => { + if (pb.authStore.isValid) { + navigate('/', { replace: true }); + } + }, [navigate]); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + if (!email || !password) { + setError('Please enter both email and password'); + return; + } + + setLoading(true); + setError(''); + + try { + await pb.collection('users').authWithPassword(email, password); + navigate('/', { replace: true }); + } catch (err: any) { + setError(err?.message || 'Invalid email or password'); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* Background Decorative Orange Glow */} +
+ +
+ {/* Logo & Header */} +
+ {/* Logo styled exactly like the AR Viewer top-right logo */} +
+ t. +
+

AR Management

+

+ Authorized Access Only +

+
+ + {/* Login Card */} +
+
+ {error && ( +
+ {error} +
+ )} + + {/* Email Input */} +
+ +
+ + + + setEmail(e.target.value)} + /> +
+
+ + {/* Password Input */} +
+ +
+ + + + setPassword(e.target.value)} + /> + +
+
+ + {/* Submit Button */} + +
+
+ +
+ SYSTEM IP MONITORED SECURE PORTAL +
+
+
+ ); +}