refactored Authentication
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
import { Card } from '@/components/Card';
|
import { Card } from '@/components/Card';
|
||||||
import { useForm } from 'hooks/useForm';
|
import { useForm } from 'hooks/useForm';
|
||||||
|
|
||||||
@@ -42,11 +43,11 @@ export function Bootstrap() {
|
|||||||
<h2 className="text-2xl font-bold text-duck-dark mb-2">Welcome, Admin</h2>
|
<h2 className="text-2xl font-bold text-duck-dark mb-2">Welcome, Admin</h2>
|
||||||
<p className="text-sm text-duck-dark/60 mb-6">Enter your email to create your administrator account.</p>
|
<p className="text-sm text-duck-dark/60 mb-6">Enter your email to create your administrator account.</p>
|
||||||
<form ref={formRef} className="flex gap-2">
|
<form ref={formRef} className="flex gap-2">
|
||||||
<input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
placeholder="admin@example.com"
|
placeholder="admin@example.com"
|
||||||
className="flex-1 px-4 py-2.5 rounded-lg border-2 border-duck-dark/20 text-sm text-duck-dark placeholder:!text-duck-dark/30 focus:outline-none focus:border-duck-teal/50"
|
className="flex-1 px-4 py-2.5 rounded-lg border-2 border-duck-dark/20 text-sm text-duck-dark focus:outline-none focus:border-duck-teal/50"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleBootstrap}
|
onClick={handleBootstrap}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Background } from './Background';
|
||||||
|
type AuthenticationLayoutProps = {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
};
|
||||||
|
export function AuthenticationLayout({ children }: AuthenticationLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className="relative overflow-hidden h-dvh outline-none inset-0">
|
||||||
|
<section className="relative h-dvh snap-start overflow-hidden">
|
||||||
|
<Background />
|
||||||
|
<div className="absolute inset-0 z-520">
|
||||||
|
<div className="absolute inset-x-0 bottom-0 z-20 flex justify-center pb-8 md:pb-12">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { DuckAvatar } from "./DuckAvatar";
|
||||||
|
import { PixelGrid } from "@/components/PixelGrid";
|
||||||
|
import landscapebg from './landscape1.jpg';
|
||||||
|
|
||||||
|
export function Background() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-0"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${landscapebg})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PixelGrid />
|
||||||
|
<DuckAvatar showDebug={false} fullControlMode={false} currentSection={0} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
type DebugPanelProps = {
|
||||||
|
fullControlMode: boolean;
|
||||||
|
cameraInfo: {
|
||||||
|
cameraPosition: number[];
|
||||||
|
target: number[];
|
||||||
|
zoom: number;
|
||||||
|
};
|
||||||
|
modelInfo: {
|
||||||
|
position: number[];
|
||||||
|
rotation: number[];
|
||||||
|
scale: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DebugPanel({ fullControlMode, cameraInfo, modelInfo }: DebugPanelProps) {
|
||||||
|
return (
|
||||||
|
<div className="fixed top-4 left-4 bg-black/80 text-white p-4 rounded text-xs font-mono space-y-2 z-[100]">
|
||||||
|
<div className="font-bold text-blue-400">Controls:</div>
|
||||||
|
<div className={fullControlMode ? 'text-green-300' : 'text-yellow-300'}>
|
||||||
|
Mode: {fullControlMode ? 'FULL CONTROL' : 'ROTATION ONLY'}
|
||||||
|
</div>
|
||||||
|
<div>* Left-click/Touch + drag: Rotate</div>
|
||||||
|
{fullControlMode && (
|
||||||
|
<>
|
||||||
|
<div>* Right-click + drag: Pan</div>
|
||||||
|
<div>* Scroll: Zoom</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div>* Ctrl+Alt+Enter: Toggle full control</div>
|
||||||
|
<div>* Ctrl+Alt+D: Toggle debug panel</div>
|
||||||
|
|
||||||
|
<div className="font-bold text-green-400 pt-2">Camera:</div>
|
||||||
|
<div>Position: [{cameraInfo.cameraPosition.map((v) => v.toFixed(2)).join(', ')}]</div>
|
||||||
|
<div>Target: [{cameraInfo.target.map((v) => v.toFixed(2)).join(', ')}]</div>
|
||||||
|
<div>Zoom: {cameraInfo.zoom.toFixed(2)}</div>
|
||||||
|
|
||||||
|
<div className="font-bold text-yellow-400 pt-2">Model:</div>
|
||||||
|
<div>Position: [{modelInfo.position.map((v) => v.toFixed(2)).join(', ')}]</div>
|
||||||
|
<div>Rotation: [{modelInfo.rotation.map((v) => v.toFixed(2)).join(', ')}]</div>
|
||||||
|
<div>Scale: {modelInfo.scale.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { Canvas, useThree } from '@react-three/fiber';
|
||||||
|
import { OrbitControls } from '@react-three/drei';
|
||||||
|
import { useEffect, useState, useRef } from 'react';
|
||||||
|
import { DuckModel } from './DuckModel';
|
||||||
|
import { DebugPanel } from './DebugPanel';
|
||||||
|
|
||||||
|
type CameraInfoProps = {
|
||||||
|
onUpdate: (info: { cameraPosition: number[]; target: number[]; zoom: number }) => void;
|
||||||
|
initialTarget: [number, number, number];
|
||||||
|
fullControlMode: boolean;
|
||||||
|
initialPosition: [number, number, number];
|
||||||
|
};
|
||||||
|
|
||||||
|
function CameraInfo({ onUpdate, initialTarget, fullControlMode, initialPosition }: CameraInfoProps) {
|
||||||
|
const { camera } = useThree();
|
||||||
|
const controlsRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (controlsRef.current) {
|
||||||
|
controlsRef.current.target.set(...initialTarget);
|
||||||
|
camera.position.set(...initialPosition);
|
||||||
|
controlsRef.current.update();
|
||||||
|
}
|
||||||
|
}, [initialTarget, initialPosition, camera]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (controlsRef.current) {
|
||||||
|
const controls = controlsRef.current;
|
||||||
|
onUpdate({
|
||||||
|
cameraPosition: camera.position.toArray(),
|
||||||
|
target: controls.target.toArray(),
|
||||||
|
zoom: camera.zoom,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [camera, onUpdate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OrbitControls
|
||||||
|
ref={controlsRef}
|
||||||
|
enabled={fullControlMode}
|
||||||
|
enableRotate={fullControlMode}
|
||||||
|
enableZoom={fullControlMode}
|
||||||
|
enablePan={fullControlMode}
|
||||||
|
target={initialTarget}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type DuckAvatarProps = {
|
||||||
|
showDebug: boolean;
|
||||||
|
fullControlMode: boolean;
|
||||||
|
currentSection: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DuckAvatar({ showDebug, fullControlMode, currentSection }: DuckAvatarProps) {
|
||||||
|
const isHeroSection = currentSection === 0;
|
||||||
|
const [cameraInfo, setCameraInfo] = useState({
|
||||||
|
cameraPosition: [2.89, 5.24, 7.38],
|
||||||
|
target: [0.3, 3.29, -0.4],
|
||||||
|
zoom: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [modelInfo, setModelInfo] = useState({
|
||||||
|
position: [0, -0.2, 0],
|
||||||
|
rotation: [-0.1, -0.75, 0],
|
||||||
|
scale: 4.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
|
||||||
|
const heroClasses = 'fixed top-12 md:top-auto md:bottom-0 left-1/2 -translate-x-1/2 w-[60vh] h-[75vh]';
|
||||||
|
const miniClasses = 'fixed bottom-4 right-4 w-[20vh] h-[25vh]';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${isHeroSection ? heroClasses : miniClasses} overflow-visible z-1 pointer-events-none`}
|
||||||
|
style={{
|
||||||
|
background: 'transparent',
|
||||||
|
opacity: isLoaded ? 1 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="w-full h-full pointer-events-none overflow-visible" style={{ background: 'transparent' }}>
|
||||||
|
<Canvas
|
||||||
|
camera={{
|
||||||
|
position: [2.89, 5.24, 7.38],
|
||||||
|
fov: 65,
|
||||||
|
near: 0.1,
|
||||||
|
far: 1000,
|
||||||
|
}}
|
||||||
|
style={{ background: 'transparent' }}
|
||||||
|
>
|
||||||
|
<ambientLight intensity={1.5} />
|
||||||
|
<directionalLight position={[10, 10, 5]} intensity={2} />
|
||||||
|
<directionalLight position={[-10, -10, -5]} intensity={1} />
|
||||||
|
<pointLight position={[0, 5, 0]} intensity={1.5} />
|
||||||
|
<CameraInfo
|
||||||
|
onUpdate={setCameraInfo}
|
||||||
|
initialTarget={[0.3, 3.29, -0.4]}
|
||||||
|
fullControlMode={fullControlMode}
|
||||||
|
initialPosition={[2.89, 5.24, 7.38]}
|
||||||
|
/>
|
||||||
|
<DuckModel
|
||||||
|
onUpdate={setModelInfo}
|
||||||
|
onAssetsLoaded={() => {
|
||||||
|
setIsLoaded(true);
|
||||||
|
}}
|
||||||
|
currentSection={currentSection}
|
||||||
|
/>
|
||||||
|
</Canvas>
|
||||||
|
</div>
|
||||||
|
{showDebug && <DebugPanel fullControlMode={fullControlMode} cameraInfo={cameraInfo} modelInfo={modelInfo} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { useRef, useEffect } from 'react';
|
||||||
|
import { useGLTF, useAnimations } from '@react-three/drei';
|
||||||
|
|
||||||
|
type DuckModelProps = {
|
||||||
|
onUpdate: (info: { position: number[]; rotation: number[]; scale: number }) => void;
|
||||||
|
onAssetsLoaded: () => void;
|
||||||
|
currentSection: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DuckModel({ onUpdate, onAssetsLoaded, currentSection }: DuckModelProps) {
|
||||||
|
const character = useGLTF('/static/duck3D/Character_output.glb');
|
||||||
|
const animations = useGLTF('/static/duck3D/Meshy_Merged_Animations.glb');
|
||||||
|
const meshRef = useRef<any>(null);
|
||||||
|
const { actions } = useAnimations(animations.animations, meshRef);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onAssetsLoaded();
|
||||||
|
}, [character, animations, onAssetsLoaded]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!actions) return;
|
||||||
|
|
||||||
|
const isContactSection = currentSection === 6;
|
||||||
|
|
||||||
|
if (isContactSection) {
|
||||||
|
Object.values(actions).forEach((a) => a?.fadeOut(0.4));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = actions['Walking'];
|
||||||
|
if (!action) return;
|
||||||
|
|
||||||
|
Object.values(actions).forEach((a) => a?.fadeOut(0.4));
|
||||||
|
|
||||||
|
action.reset();
|
||||||
|
action.setLoop(2201, Infinity);
|
||||||
|
action.clampWhenFinished = false;
|
||||||
|
action.fadeIn(0.4);
|
||||||
|
action.play();
|
||||||
|
}, [actions, currentSection]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (meshRef.current) {
|
||||||
|
onUpdate({
|
||||||
|
position: meshRef.current.position.toArray(),
|
||||||
|
rotation: meshRef.current.rotation.toArray().slice(0, 3),
|
||||||
|
scale: meshRef.current.scale.x,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [onUpdate]);
|
||||||
|
|
||||||
|
const isContactSection = currentSection === 6;
|
||||||
|
const rotationY = isContactSection ? 0 : (10 * Math.PI) / 180;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<primitive
|
||||||
|
ref={meshRef}
|
||||||
|
object={character.scene}
|
||||||
|
position={[0, -0.2, 0]}
|
||||||
|
scale={4.5}
|
||||||
|
rotation={[-0.1, rotationY, 0]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './DuckAvatar';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './AuthenticationLayout';
|
||||||
Reference in New Issue
Block a user