feat: add AR illustration and armchair components with animations

This commit is contained in:
Dikshantpatil2077 2026-10-09 15:41:36 +05:30
parent a7836f0c4b
commit d085cbca26
6 changed files with 229 additions and 2 deletions

View File

@ -58,3 +58,13 @@ model-viewer {
color: rgb(156 163 175); /* gray-400 */ color: rgb(156 163 175); /* gray-400 */
opacity: 1; opacity: 1;
} }
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
@keyframes fade-up {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}

View File

@ -0,0 +1,129 @@
import { Armchair } from '~/components/auth/armchair';
export function ArIllustration() {
return (
<svg
viewBox="0 0 400 460"
className="h-auto w-full max-w-[380px] animate-[float_7s_ease-in-out_infinite] drop-shadow-[0_30px_60px_rgba(0,0,0,0.6)]"
role="img"
aria-label="An armchair placed in a room through a phone camera in augmented reality"
>
<defs>
<linearGradient id="chair-back" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stopColor="#fb923c" />
<stop offset="55%" stopColor="#c2410c" />
<stop offset="100%" stopColor="#7c2d12" />
</linearGradient>
<linearGradient id="chair-back-inner" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#f97316" />
<stop offset="100%" stopColor="#9a3412" />
</linearGradient>
<linearGradient id="chair-cushion" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#fdba74" />
<stop offset="100%" stopColor="#ea580c" />
</linearGradient>
<linearGradient id="chair-base" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#c2410c" />
<stop offset="100%" stopColor="#7c2d12" />
</linearGradient>
<linearGradient id="chair-arm-left" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="#9a3412" />
<stop offset="100%" stopColor="#ea580c" />
</linearGradient>
<linearGradient id="chair-arm-right" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="#f97316" />
<stop offset="100%" stopColor="#9a3412" />
</linearGradient>
<linearGradient id="chair-wood" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#92582f" />
<stop offset="100%" stopColor="#3f2412" />
</linearGradient>
<linearGradient id="ar-screen" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#161616" />
<stop offset="60%" stopColor="#1a1511" />
<stop offset="100%" stopColor="#221810" />
</linearGradient>
<linearGradient id="ar-grid-fade" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="white" stopOpacity="0" />
<stop offset="100%" stopColor="white" stopOpacity="1" />
</linearGradient>
<mask id="ar-grid-mask">
<rect x="0" y="200" width="400" height="200" fill="url(#ar-grid-fade)" />
</mask>
<clipPath id="ar-screen-clip">
<rect x="118" y="38" width="164" height="364" rx="24" />
</clipPath>
<radialGradient id="ar-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stopColor="#f97316" stopOpacity="0.3" />
<stop offset="100%" stopColor="#f97316" stopOpacity="0" />
</radialGradient>
<filter id="ar-blur" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="5" />
</filter>
</defs>
{/* Phone body */}
<rect x="108" y="28" width="184" height="384" rx="32" fill="#0b0b0b" stroke="#2a2a2a" strokeWidth="2" />
<rect x="118" y="38" width="164" height="364" rx="24" fill="url(#ar-screen)" />
<g clipPath="url(#ar-screen-clip)">
{/* Wall / floor horizon */}
<rect x="118" y="38" width="164" height="176" fill="white" opacity="0.015" />
{/* Perspective floor seen through the camera */}
<g stroke="#f97316" strokeOpacity="0.3" strokeWidth="1" mask="url(#ar-grid-mask)">
{[240, 262, 290, 326, 372].map((y) => (
<line key={y} x1="100" y1={y} x2="300" y2={y} />
))}
{[-260, -170, -90, -30, 30, 90, 170, 260].map((dx) => (
<line key={dx} x1="200" y1="214" x2={200 + dx} y2="420" />
))}
</g>
<ellipse cx="200" cy="320" rx="96" ry="40" fill="url(#ar-glow)" />
{/* Placement reticle */}
<ellipse cx="200" cy="320" rx="76" ry="19" fill="none" stroke="#f97316" strokeWidth="1.5" strokeDasharray="5 5">
<animate attributeName="stroke-dashoffset" from="0" to="-20" dur="2s" repeatCount="indefinite" />
</ellipse>
<ellipse cx="200" cy="320" rx="60" ry="15" fill="#f97316" fillOpacity="0.12">
<animate attributeName="fill-opacity" values="0.18;0.05;0.18" dur="2.4s" repeatCount="indefinite" />
</ellipse>
<Armchair />
{/* Camera scan brackets */}
<g fill="none" stroke="white" strokeOpacity="0.55" strokeWidth="2" strokeLinecap="round">
<path d="M136 152 V138 H150" />
<path d="M264 152 V138 H250" />
<path d="M136 342 V356 H150" />
<path d="M264 342 V356 H250" />
</g>
{/* AR badge */}
<rect x="180" y="62" width="40" height="18" rx="9" fill="#f97316" fillOpacity="0.15" stroke="#f97316" strokeOpacity="0.6" />
<text x="200" y="74.5" textAnchor="middle" fontSize="9" fontWeight="700" letterSpacing="1" fill="#fb923c" fontFamily="Inter, sans-serif">AR</text>
{/* Shutter */}
<circle cx="200" cy="380" r="12" fill="none" stroke="white" strokeOpacity="0.4" strokeWidth="2" />
<circle cx="200" cy="380" r="8" fill="#f97316" />
</g>
{/* Notch */}
<rect x="172" y="46" width="56" height="8" rx="4" fill="#0b0b0b" />
{/* Floating particles */}
{[
{ cx: 70, cy: 120, r: 3, d: '3s' },
{ cx: 330, cy: 90, r: 2, d: '4s' },
{ cx: 345, cy: 260, r: 3, d: '3.6s' },
{ cx: 55, cy: 300, r: 2, d: '4.4s' },
{ cx: 95, cy: 210, r: 1.5, d: '2.8s' },
].map((p) => (
<circle key={`${p.cx}-${p.cy}`} cx={p.cx} cy={p.cy} r={p.r} fill="#f97316">
<animate attributeName="opacity" values="0.15;0.8;0.15" dur={p.d} repeatCount="indefinite" />
</circle>
))}
</svg>
);
}

View File

@ -0,0 +1,43 @@
// SVG armchair drawn in the coordinate space of <ArIllustration>; relies on its gradient/filter defs.
export function Armchair() {
return (
<g>
{/* Contact shadow */}
<ellipse cx="200" cy="324" rx="68" ry="11" fill="black" opacity="0.55" filter="url(#ar-blur)" />
{/* Back legs */}
<path d="M168 286 L173 286 L171 308 L168 308 Z" fill="url(#chair-wood)" />
<path d="M227 286 L232 286 L232 308 L229 308 Z" fill="url(#chair-wood)" />
{/* Backrest */}
<path d="M156 264 C150 222 158 192 176 182 C190 175 210 175 224 182 C242 192 250 222 244 264 Z" fill="url(#chair-back)" />
<path d="M166 256 C162 222 168 199 182 191 C194 186 206 186 218 191 C232 199 238 222 234 256 Z" fill="url(#chair-back-inner)" />
<path d="M176 186 C190 179 210 179 224 186" fill="none" stroke="#fed7aa" strokeOpacity="0.45" strokeWidth="1.5" strokeLinecap="round" />
{/* Tufting */}
<g fill="#7c2d12">
{[[184, 210], [200, 206], [216, 210], [192, 230], [208, 230]].map(([cx, cy]) => (
<circle key={`${cx}-${cy}`} cx={cx} cy={cy} r="1.8" />
))}
</g>
<g fill="none" stroke="#7c2d12" strokeOpacity="0.35" strokeWidth="0.8">
<path d="M184 210 L192 230 L200 206 L208 230 L216 210" />
</g>
{/* Seat base */}
<path d="M152 280 C175 290 225 290 248 280 L248 298 C225 308 175 308 152 298 Z" fill="url(#chair-base)" />
{/* Seat cushion */}
<path d="M154 276 C160 262 240 262 246 276 C246 286 224 292 200 292 C176 292 154 286 154 276 Z" fill="url(#chair-cushion)" />
<ellipse cx="200" cy="270" rx="30" ry="3.5" fill="white" opacity="0.2" />
{/* Armrests */}
<path d="M140 300 C136 270 138 248 150 238 C158 232 168 236 168 246 L168 296 Z" fill="url(#chair-arm-left)" />
<path d="M260 300 C264 270 262 248 250 238 C242 232 232 236 232 246 L232 296 Z" fill="url(#chair-arm-right)" />
<path d="M150 238 C158 232 168 236 168 246" fill="none" stroke="#fed7aa" strokeOpacity="0.5" strokeWidth="1.5" strokeLinecap="round" />
<path d="M250 238 C242 232 232 236 232 246" fill="none" stroke="#fed7aa" strokeOpacity="0.35" strokeWidth="1.5" strokeLinecap="round" />
{/* Front legs */}
<path d="M159 299 L166 299 L163 326 L160 326 Z" fill="url(#chair-wood)" />
<path d="M234 299 L241 299 L241 326 L238 326 Z" fill="url(#chair-wood)" />
</g>
);
}

View File

@ -0,0 +1,36 @@
import { LogoMark } from '~/components/logo-mark';
import { ArIllustration } from '~/components/auth/ar-illustration';
export function AuthBrandPanel() {
return (
<aside className="relative hidden overflow-hidden border-r border-neutral-900 bg-[#0A0A0A] p-12 lg:flex lg:flex-col lg:justify-between">
<div className="pointer-events-none absolute top-1/3 left-1/2 h-[520px] w-[520px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-orange-500/10 blur-[140px]" />
{/* Real-world floor grid */}
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-1/2 [perspective:600px]">
<div
className="absolute inset-0 origin-bottom [transform:rotateX(60deg)] opacity-30"
style={{
backgroundImage:
'linear-gradient(to right, rgba(249,115,22,0.25) 1px, transparent 1px), linear-gradient(to bottom, rgba(249,115,22,0.25) 1px, transparent 1px)',
backgroundSize: '48px 48px',
maskImage: 'linear-gradient(to top, black 10%, transparent 90%)',
WebkitMaskImage: 'linear-gradient(to top, black 10%, transparent 90%)',
}}
/>
</div>
<div className="relative z-10">
<LogoMark className="h-10 w-10 text-2xl" />
</div>
<div className="relative z-10 flex flex-1 items-center justify-center py-8">
<ArIllustration />
</div>
<p className="relative z-10 text-2xl font-semibold tracking-tight text-white">
Step into <span className="text-orange-500">augmented reality.</span>
</p>
</aside>
);
}

View File

@ -0,0 +1,9 @@
import { cn } from '~/lib/utils';
export function LogoMark({ className }: { className?: string }) {
return (
<div className={cn('flex items-center justify-center rounded-xl border border-neutral-800 bg-black', className)}>
<span className="select-none font-extrabold leading-none text-orange-500">t.</span>
</div>
);
}

View File

@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import { useSubmit, useNavigation, useActionData, useNavigate, redirect } from 'react-router'; import { useSubmit, useNavigation, useActionData, useNavigate, redirect } from 'react-router';
import type { Route } from './+types/login'; import type { Route } from './+types/login';
import { pb } from '~/lib/pocketbase'; import { pb } from '~/lib/pocketbase';
`import { Lock, Mail, Eye, EyeOff, Loader2, ArrowRight, AlertCircle } from 'lucide-react'; import { Lock, Mail, Eye, EyeOff, Loader2, ArrowRight, AlertCircle } from 'lucide-react';
import { Input } from '~/components/ui/input'; import { Input } from '~/components/ui/input';
import { Button } from '~/components/ui/button'; import { Button } from '~/components/ui/button';
import { Label } from '~/components/ui/label'; import { Label } from '~/components/ui/label';
@ -176,7 +176,7 @@ export default function Login() {
name="password" name="password"
autoComplete="current-password" autoComplete="current-password"
required required
className={`${fieldInput} pr-12`} className={fieldInput + ' pr-12'}
placeholder="Enter your password" placeholder="Enter your password"
value={password} value={password}
onChange={(e) => { onChange={(e) => {