diff --git a/index.html b/index.html
index 83a9424..e51dab8 100644
--- a/index.html
+++ b/index.html
@@ -5,6 +5,7 @@
aura-store
+
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
new file mode 100644
index 0000000..a299632
--- /dev/null
+++ b/src/components/ui/Button.tsx
@@ -0,0 +1,26 @@
+import { motion } from "framer-motion";
+import { cn } from "../../lib/utils";
+
+interface Props extends React.ButtonHTMLAttributes {
+ 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 (
+
+ {loading ? "..." : children}
+
+ );
+}
diff --git a/src/components/ui/GlassCard.tsx b/src/components/ui/GlassCard.tsx
new file mode 100644
index 0000000..561d013
--- /dev/null
+++ b/src/components/ui/GlassCard.tsx
@@ -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 (
+
+ {children}
+
+ );
+}
diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx
new file mode 100644
index 0000000..9ca6df6
--- /dev/null
+++ b/src/components/ui/Input.tsx
@@ -0,0 +1,25 @@
+import { cn } from "../../lib/utils";
+
+interface Props extends React.InputHTMLAttributes {
+ label?: string;
+ error?: string;
+}
+
+export function Input({ label, error, className, ...props }: Props) {
+ return (
+
+ {label && }
+
+ {error && {error}}
+
+ );
+}
diff --git a/src/components/ui/QRModal.tsx b/src/components/ui/QRModal.tsx
new file mode 100644
index 0000000..cffa5ad
--- /dev/null
+++ b/src/components/ui/QRModal.tsx
@@ -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(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 (
+
+
+
+ {title}
+
+
+
+
Отсканируйте в VPN-приложении
+
+
+ );
+}
diff --git a/src/components/ui/StatusBadge.tsx b/src/components/ui/StatusBadge.tsx
new file mode 100644
index 0000000..67fd5df
--- /dev/null
+++ b/src/components/ui/StatusBadge.tsx
@@ -0,0 +1,11 @@
+interface Props { active: boolean; activeLabel?: string; inactiveLabel?: string; }
+export function StatusBadge({ active, activeLabel = "Активна", inactiveLabel = "Неактивна" }: Props) {
+ return (
+
+
+
+ {active ? activeLabel : inactiveLabel}
+
+
+ );
+}