- Migrate from tailwindcss v3.3 to v4.1.18 - Replace @tailwind directives with @import 'tailwindcss' - Move custom colors to @theme block in globals.css - Convert custom utilities to @utility syntax - Update PostCSS config to use @tailwindcss/postcss - Remove autoprefixer (now built-in)
144 lines
6.1 KiB
TypeScript
144 lines
6.1 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
import { Camera, Zap } from 'lucide-react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { useSession } from '@/context/SessionContext';
|
|
import BulkScanSheet from './BulkScanSheet';
|
|
|
|
interface FloatingScannerButtonProps {
|
|
onImageSelected: (base64Image: string) => void;
|
|
}
|
|
|
|
export default function FloatingScannerButton({ onImageSelected }: FloatingScannerButtonProps) {
|
|
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
|
const { activeSession } = useSession();
|
|
const [isBulkOpen, setIsBulkOpen] = useState(false);
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
const base64String = reader.result as string;
|
|
onImageSelected(base64String);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
};
|
|
|
|
const handleMainClick = () => {
|
|
if (activeSession) {
|
|
setIsExpanded(!isExpanded);
|
|
} else {
|
|
fileInputRef.current?.click();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50">
|
|
<input
|
|
type="file"
|
|
ref={fileInputRef}
|
|
onChange={handleFileChange}
|
|
accept="image/*"
|
|
className="hidden"
|
|
/>
|
|
|
|
{/* Expanded Options */}
|
|
<AnimatePresence>
|
|
{isExpanded && activeSession && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20, scale: 0.8 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={{ opacity: 0, y: 20, scale: 0.8 }}
|
|
className="absolute bottom-20 left-1/2 -translate-x-1/2 flex gap-3"
|
|
>
|
|
{/* Single Scan */}
|
|
<motion.button
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
onClick={() => {
|
|
setIsExpanded(false);
|
|
fileInputRef.current?.click();
|
|
}}
|
|
className="flex flex-col items-center gap-1.5 px-4 py-3 bg-zinc-900 border border-zinc-700 rounded-2xl shadow-xl"
|
|
>
|
|
<Camera size={24} className="text-white" />
|
|
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest">Einzel</span>
|
|
</motion.button>
|
|
|
|
{/* Bulk Scan */}
|
|
<motion.button
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
onClick={() => {
|
|
setIsExpanded(false);
|
|
setIsBulkOpen(true);
|
|
}}
|
|
className="flex flex-col items-center gap-1.5 px-4 py-3 bg-orange-600 rounded-2xl shadow-xl shadow-orange-950/30"
|
|
>
|
|
<Zap size={24} className="text-white" />
|
|
<span className="text-[10px] font-bold text-white/80 uppercase tracking-widest">Bulk</span>
|
|
</motion.button>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* Main Button */}
|
|
<motion.button
|
|
onClick={handleMainClick}
|
|
whileHover={{ scale: 1.1, translateY: -4 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
initial={{ y: 100, opacity: 0 }}
|
|
animate={{ y: 0, opacity: 1, rotate: isExpanded ? 45 : 0 }}
|
|
className="relative group p-6 rounded-full bg-orange-600 text-black shadow-lg shadow-orange-950/40 hover:shadow-orange-950/60 transition-all overflow-hidden"
|
|
>
|
|
{/* Shine Animation */}
|
|
{!isExpanded && (
|
|
<motion.div
|
|
animate={{
|
|
x: ['-100%', '100%'],
|
|
}}
|
|
transition={{
|
|
duration: 2,
|
|
repeat: Infinity,
|
|
ease: "easeInOut",
|
|
repeatDelay: 3
|
|
}}
|
|
className="absolute inset-0 bg-linear-to-r from-transparent via-white/40 to-transparent skew-x-12 z-0"
|
|
/>
|
|
)}
|
|
|
|
<Camera size={32} strokeWidth={2.5} className="relative z-10" />
|
|
|
|
{/* Active Session Indicator */}
|
|
{activeSession && !isExpanded && (
|
|
<div className="absolute -top-1 -right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-orange-600 flex items-center justify-center">
|
|
<Zap size={10} className="text-white" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Pulse ring */}
|
|
{!isExpanded && (
|
|
<span className="absolute inset-0 rounded-full border-4 border-orange-600 animate-ping opacity-20" />
|
|
)}
|
|
</motion.button>
|
|
</div>
|
|
|
|
{/* Bulk Scan Sheet */}
|
|
{activeSession && (
|
|
<BulkScanSheet
|
|
isOpen={isBulkOpen}
|
|
onClose={() => setIsBulkOpen(false)}
|
|
sessionId={activeSession.id}
|
|
sessionName={activeSession.name}
|
|
onSuccess={() => setIsBulkOpen(false)}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|