feat: UI component library (GlassCard, Button, Input, QRModal)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>aura-store</title>
|
<title>aura-store</title>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
variant?: "primary" | "secondary" | "danger";
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button({ children, variant = "primary", loading, className, ...props }: Props) {
|
||||||
|
const base = "px-5 py-2.5 rounded-xl font-semibold text-sm transition-all duration-200 disabled:opacity-50";
|
||||||
|
const variants = {
|
||||||
|
primary: "bg-gradient-to-r from-accent to-accent2 text-white hover:scale-[1.02] active:scale-[0.98]",
|
||||||
|
secondary: "border border-white/20 text-white hover:bg-white/10",
|
||||||
|
danger: "border border-red-500/40 text-red-400 hover:bg-red-500/10",
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
whileTap={{ scale: 0.97 }}
|
||||||
|
className={cn(base, variants[variant], className)}
|
||||||
|
disabled={loading || props.disabled}
|
||||||
|
{...(props as any)}
|
||||||
|
>
|
||||||
|
{loading ? "..." : children}
|
||||||
|
</motion.button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
animate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GlassCard({ children, className, onClick, animate = true }: Props) {
|
||||||
|
const Comp = animate ? motion.div : "div";
|
||||||
|
const animProps = animate ? {
|
||||||
|
initial: { opacity: 0, y: 20 },
|
||||||
|
whileInView: { opacity: 1, y: 0 },
|
||||||
|
viewport: { once: true },
|
||||||
|
transition: { duration: 0.4 },
|
||||||
|
} : {};
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
{...animProps}
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(
|
||||||
|
"rounded-2xl border p-6",
|
||||||
|
"bg-white/[0.07] border-white/[0.11] backdrop-blur-md",
|
||||||
|
onClick && "cursor-pointer hover:border-accent/40 transition-colors",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
interface Props extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Input({ label, error, className, ...props }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{label && <label className="text-sm text-white/60 font-medium">{label}</label>}
|
||||||
|
<input
|
||||||
|
className={cn(
|
||||||
|
"w-full px-4 py-3 rounded-xl bg-white/[0.07] border border-white/[0.11]",
|
||||||
|
"text-white placeholder-white/30 focus:outline-none focus:border-accent/60",
|
||||||
|
"transition-colors",
|
||||||
|
error && "border-red-500/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{error && <span className="text-xs text-red-400">{error}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
|
declare const QRCode: any;
|
||||||
|
|
||||||
|
interface Props { text: string; title?: string; onClose: () => void; }
|
||||||
|
|
||||||
|
export function QRModal({ text, title = "QR-код", onClose }: Props) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
ref.current.innerHTML = "";
|
||||||
|
new QRCode(ref.current, {
|
||||||
|
text, width: 220, height: 220,
|
||||||
|
colorDark: "#ffffff", colorLight: "#0d0b1e",
|
||||||
|
correctLevel: (QRCode as any).CorrectLevel?.M ?? 0,
|
||||||
|
});
|
||||||
|
}, [text]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||||||
|
<div className="bg-[#1a1535] rounded-2xl border border-white/10 p-6 w-full max-w-xs">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<span className="font-semibold">{title}</span>
|
||||||
|
<button onClick={onClose} className="text-white/50 hover:text-white"><X size={20} /></button>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center mb-3">
|
||||||
|
<div ref={ref} className="p-3 bg-[#0d0b1e] rounded-xl border border-white/10" />
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-white/40 text-center">Отсканируйте в VPN-приложении</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
interface Props { active: boolean; activeLabel?: string; inactiveLabel?: string; }
|
||||||
|
export function StatusBadge({ active, activeLabel = "Активна", inactiveLabel = "Неактивна" }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${active ? "bg-green-400 shadow-[0_0_6px_#34d399]" : "bg-red-400"}`} />
|
||||||
|
<span className={`text-xs font-bold tracking-wider ${active ? "text-green-400" : "text-red-400"}`}>
|
||||||
|
{active ? activeLabel : inactiveLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user