first commit
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
|
||||
interface CTASectionProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
accent?: "blue" | "green";
|
||||
}
|
||||
|
||||
export default function CTASection({
|
||||
title = "Готовы обсудить проект?",
|
||||
subtitle = "Расскажите, что нужно — предложим решение и сроки в течение рабочего дня.",
|
||||
accent = "blue",
|
||||
}: CTASectionProps) {
|
||||
const isGreen = accent === "green";
|
||||
return (
|
||||
<section className="px-6 py-20">
|
||||
<div
|
||||
className={`mx-auto flex max-w-4xl flex-col items-start justify-between gap-6 rounded-3xl border p-10 sm:flex-row sm:items-center ${
|
||||
isGreen
|
||||
? "border-green-500/20 bg-green-500/[0.04]"
|
||||
: "border-blue-500/20 bg-blue-500/[0.04]"
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<h2 className="font-display text-2xl font-medium text-ink-100">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="mt-2 max-w-md text-ink-500">{subtitle}</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/contacts"
|
||||
className={`inline-flex shrink-0 items-center gap-1.5 rounded-full px-6 py-3 font-medium text-base-950 transition-transform hover:scale-[1.03] ${
|
||||
isGreen ? "bg-green-400" : "bg-blue-400"
|
||||
}`}
|
||||
>
|
||||
Связаться с нами
|
||||
<ArrowUpRight size={18} />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
"use client";
|
||||
|
||||
import { useState, FormEvent } from "react";
|
||||
import { Loader2, CheckCircle2, AlertCircle } from "lucide-react";
|
||||
|
||||
type Status = "idle" | "loading" | "success" | "error";
|
||||
|
||||
export default function ContactForm() {
|
||||
const [status, setStatus] = useState<Status>("idle");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setStatus("loading");
|
||||
setErrorMessage("");
|
||||
|
||||
const form = e.currentTarget;
|
||||
const data = new FormData(form);
|
||||
|
||||
// Honeypot: если поле заполнено — это бот, тихо считаем успехом.
|
||||
if (data.get("website")) {
|
||||
setStatus("success");
|
||||
form.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name: String(data.get("name") || ""),
|
||||
contact: String(data.get("contact") || ""),
|
||||
message: String(data.get("message") || ""),
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/contact", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (!res.ok || !result.ok) {
|
||||
throw new Error(result.error || "Не удалось отправить сообщение");
|
||||
}
|
||||
|
||||
setStatus("success");
|
||||
form.reset();
|
||||
} catch (err) {
|
||||
setStatus("error");
|
||||
setErrorMessage(
|
||||
err instanceof Error ? err.message : "Не удалось отправить сообщение"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (status === "success") {
|
||||
return (
|
||||
<div className="flex items-start gap-3 rounded-2xl border border-green-500/25 bg-green-500/[0.06] p-6">
|
||||
<CheckCircle2 className="mt-0.5 shrink-0 text-green-400" size={22} />
|
||||
<div>
|
||||
<p className="font-medium text-ink-100">Сообщение отправлено</p>
|
||||
<p className="mt-1 text-sm text-ink-500">
|
||||
Мы получили заявку и ответим в ближайшее время.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
{/* Honeypot — скрыто от людей, видно ботам */}
|
||||
<input
|
||||
type="text"
|
||||
name="website"
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
className="absolute left-[-9999px] h-0 w-0 opacity-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label htmlFor="name" className="mb-1.5 block text-sm text-ink-300">
|
||||
Имя
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
maxLength={100}
|
||||
placeholder="Как к вам обращаться"
|
||||
className="w-full rounded-xl border border-white/[0.08] bg-base-900/60 px-4 py-3 text-ink-100 placeholder:text-ink-700 focus:border-blue-500/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="contact" className="mb-1.5 block text-sm text-ink-300">
|
||||
Telegram, WhatsApp или email
|
||||
</label>
|
||||
<input
|
||||
id="contact"
|
||||
name="contact"
|
||||
type="text"
|
||||
required
|
||||
maxLength={150}
|
||||
placeholder="Как с вами связаться"
|
||||
className="w-full rounded-xl border border-white/[0.08] bg-base-900/60 px-4 py-3 text-ink-100 placeholder:text-ink-700 focus:border-blue-500/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="mb-1.5 block text-sm text-ink-300">
|
||||
Сообщение
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
required
|
||||
maxLength={2000}
|
||||
rows={5}
|
||||
placeholder="Расскажите о задаче — направление, сроки, пожелания"
|
||||
className="w-full resize-none rounded-xl border border-white/[0.08] bg-base-900/60 px-4 py-3 text-ink-100 placeholder:text-ink-700 focus:border-blue-500/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status === "error" && (
|
||||
<div className="flex items-start gap-2.5 rounded-xl border border-red-500/25 bg-red-500/[0.06] px-4 py-3 text-sm text-red-300">
|
||||
<AlertCircle size={18} className="mt-0.5 shrink-0" />
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === "loading"}
|
||||
className="inline-flex w-full items-center justify-center gap-2 rounded-full bg-blue-500 px-6 py-3.5 font-medium text-white transition-transform hover:scale-[1.01] disabled:opacity-60 sm:w-auto"
|
||||
>
|
||||
{status === "loading" && <Loader2 size={18} className="animate-spin" />}
|
||||
{status === "loading" ? "Отправляем…" : "Отправить сообщение"}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
interface FeatureBadgeProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export default function FeatureBadge({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
}: FeatureBadgeProps) {
|
||||
return (
|
||||
<div className="flex items-start gap-3.5">
|
||||
<div className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-white/[0.08] bg-white/[0.03] text-blue-400">
|
||||
<Icon size={18} strokeWidth={1.75} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-ink-100">{title}</p>
|
||||
<p className="text-sm text-ink-500">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import Link from "next/link";
|
||||
import { Send, Mail, MessageCircle } from "lucide-react";
|
||||
import Logo from "./Logo";
|
||||
import { navLinks } from "@/config/services";
|
||||
import { contacts } from "@/config/contacts";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-white/[0.06] px-6 py-14">
|
||||
<div className="mx-auto grid max-w-6xl gap-10 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<Logo />
|
||||
<p className="mt-4 max-w-xs text-sm leading-relaxed text-ink-500">
|
||||
Комплексные IT-решения для бизнеса: сайты, боты, CRM, серверы и
|
||||
техническая поддержка.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-4 font-mono text-xs uppercase tracking-[0.2em] text-ink-700">
|
||||
Разделы
|
||||
</p>
|
||||
<ul className="space-y-2.5">
|
||||
{navLinks.slice(1).map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-sm text-ink-500 transition-colors hover:text-ink-100"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-4 font-mono text-xs uppercase tracking-[0.2em] text-ink-700">
|
||||
Контакты
|
||||
</p>
|
||||
<ul className="space-y-2.5 text-sm text-ink-500">
|
||||
<li>{contacts.city}</li>
|
||||
<li>
|
||||
<a
|
||||
href={`mailto:${contacts.email}`}
|
||||
className="transition-colors hover:text-ink-100"
|
||||
>
|
||||
{contacts.email}
|
||||
</a>
|
||||
</li>
|
||||
<li>{contacts.workingHours}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-4 font-mono text-xs uppercase tracking-[0.2em] text-ink-700">
|
||||
Мы в сети
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<a
|
||||
href={contacts.telegramChannel}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Telegram"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full border border-white/[0.08] text-ink-300 transition-colors hover:border-blue-500/40 hover:text-blue-400"
|
||||
>
|
||||
<Send size={17} />
|
||||
</a>
|
||||
<a
|
||||
href={contacts.vk}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="VKontakte"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full border border-white/[0.08] text-ink-300 transition-colors hover:border-blue-500/40 hover:text-blue-400"
|
||||
>
|
||||
<MessageCircle size={17} />
|
||||
</a>
|
||||
<a
|
||||
href={`mailto:${contacts.email}`}
|
||||
aria-label="Email"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full border border-white/[0.08] text-ink-300 transition-colors hover:border-blue-500/40 hover:text-blue-400"
|
||||
>
|
||||
<Mail size={17} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-12 max-w-6xl border-t border-white/[0.06] pt-6 text-xs text-ink-700">
|
||||
© {new Date().getFullYear()} LabNetHub. Все права защищены.
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Menu, X, ArrowUpRight } from "lucide-react";
|
||||
import Logo from "./Logo";
|
||||
import { navLinks } from "@/config/services";
|
||||
|
||||
export default function Header() {
|
||||
const pathname = usePathname();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="fixed inset-x-0 top-0 z-50 border-b border-white/[0.06] bg-base-950/80 backdrop-blur-lg">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
|
||||
<Link href="/" onClick={() => setOpen(false)}>
|
||||
<Logo />
|
||||
</Link>
|
||||
|
||||
<nav className="hidden items-center gap-1 lg:flex">
|
||||
{navLinks.map((link) => {
|
||||
const active =
|
||||
link.href === "/"
|
||||
? pathname === "/"
|
||||
: pathname.startsWith(link.href);
|
||||
return (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`rounded-full px-4 py-2 text-sm transition-colors ${
|
||||
active
|
||||
? "bg-white/[0.06] text-ink-100"
|
||||
: "text-ink-500 hover:text-ink-100"
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<Link
|
||||
href="/contacts"
|
||||
className="hidden shrink-0 items-center gap-1.5 rounded-full bg-blue-500 px-5 py-2.5 text-sm font-medium text-white transition-transform hover:scale-[1.03] lg:inline-flex"
|
||||
>
|
||||
Связаться
|
||||
<ArrowUpRight size={16} />
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full border border-white/[0.08] text-ink-100 lg:hidden"
|
||||
aria-label={open ? "Закрыть меню" : "Открыть меню"}
|
||||
aria-expanded={open}
|
||||
>
|
||||
{open ? <X size={20} /> : <Menu size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{open && (
|
||||
<nav className="border-t border-white/[0.06] bg-base-950 px-6 py-4 lg:hidden">
|
||||
<ul className="flex flex-col gap-1">
|
||||
{navLinks.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
onClick={() => setOpen(false)}
|
||||
className="block rounded-lg px-3 py-2.5 text-ink-300 hover:bg-white/[0.05] hover:text-ink-100"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
interface LogoProps {
|
||||
className?: string;
|
||||
wordmarkClassName?: string;
|
||||
showWordmark?: boolean;
|
||||
}
|
||||
|
||||
export default function Logo({
|
||||
className = "h-8 w-8",
|
||||
wordmarkClassName = "text-lg",
|
||||
showWordmark = true,
|
||||
}: LogoProps) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-2.5 select-none">
|
||||
<svg
|
||||
viewBox="0 0 40 40"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="labnet-mark" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stopColor="#6FA8FF" />
|
||||
<stop offset="100%" stopColor="#2F63EB" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polygon points="9,3 19,3 15,34 5,34" fill="#F5F7FB" />
|
||||
<polygon
|
||||
points="14,34 36,34 30,24 18,24"
|
||||
fill="url(#labnet-mark)"
|
||||
/>
|
||||
</svg>
|
||||
{showWordmark && (
|
||||
<span
|
||||
className={`font-display font-medium tracking-tight ${wordmarkClassName}`}
|
||||
>
|
||||
<span className="text-ink-100">LabNet</span>
|
||||
<span className="text-gradient-blue">Hub</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
interface NetworkGraphicProps {
|
||||
accent?: "blue" | "green";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const nodes = [
|
||||
{ x: 40, y: 40, r: 3 },
|
||||
{ x: 140, y: 20, r: 2.5 },
|
||||
{ x: 220, y: 70, r: 4 },
|
||||
{ x: 300, y: 30, r: 2.5 },
|
||||
{ x: 90, y: 130, r: 2.5 },
|
||||
{ x: 190, y: 150, r: 3.5 },
|
||||
{ x: 280, y: 130, r: 2.5 },
|
||||
{ x: 20, y: 190, r: 2 },
|
||||
{ x: 340, y: 190, r: 3 },
|
||||
];
|
||||
|
||||
const edges: [number, number][] = [
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
[2, 3],
|
||||
[1, 4],
|
||||
[4, 5],
|
||||
[5, 6],
|
||||
[2, 5],
|
||||
[4, 7],
|
||||
[6, 8],
|
||||
[0, 4],
|
||||
];
|
||||
|
||||
export default function NetworkGraphic({
|
||||
accent = "blue",
|
||||
className = "",
|
||||
}: NetworkGraphicProps) {
|
||||
const stroke = accent === "green" ? "#12B76A" : "#3B7BFF";
|
||||
const dot = accent === "green" ? "#6EE7B7" : "#6FA8FF";
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 360 220"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
>
|
||||
<g opacity="0.55">
|
||||
{edges.map(([a, b], i) => (
|
||||
<line
|
||||
key={i}
|
||||
x1={nodes[a].x}
|
||||
y1={nodes[a].y}
|
||||
x2={nodes[b].x}
|
||||
y2={nodes[b].y}
|
||||
stroke={stroke}
|
||||
strokeWidth="1"
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
{nodes.map((n, i) => (
|
||||
<circle
|
||||
key={i}
|
||||
cx={n.x}
|
||||
cy={n.y}
|
||||
r={n.r}
|
||||
fill={dot}
|
||||
className="animate-pulse-slow"
|
||||
style={{ animationDelay: `${(i % 5) * 0.6}s` }}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import SectionEyebrow from "./SectionEyebrow";
|
||||
import NetworkGraphic from "./NetworkGraphic";
|
||||
|
||||
interface PageHeroProps {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
highlight?: string;
|
||||
subtitle: string;
|
||||
accent?: "blue" | "green";
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function PageHero({
|
||||
eyebrow,
|
||||
title,
|
||||
highlight,
|
||||
subtitle,
|
||||
accent = "blue",
|
||||
children,
|
||||
}: PageHeroProps) {
|
||||
return (
|
||||
<section className="relative overflow-hidden border-b border-white/[0.06] px-6 pb-16 pt-32 sm:pb-20 sm:pt-40">
|
||||
<div className="pointer-events-none absolute right-0 top-0 w-[420px] max-w-[60%] opacity-70 sm:opacity-100">
|
||||
<NetworkGraphic accent={accent} className="w-full" />
|
||||
</div>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<SectionEyebrow accent={accent}>{eyebrow}</SectionEyebrow>
|
||||
<h1 className="text-balance font-display text-4xl font-medium leading-[1.1] sm:text-5xl">
|
||||
{title}{" "}
|
||||
{highlight && (
|
||||
<span
|
||||
className={accent === "green" ? "text-gradient-green" : "text-gradient-blue"}
|
||||
>
|
||||
{highlight}
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
<p className="mt-5 max-w-xl text-lg leading-relaxed text-ink-500">
|
||||
{subtitle}
|
||||
</p>
|
||||
{children && <div className="mt-8 flex flex-wrap gap-3">{children}</div>}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
interface SectionEyebrowProps {
|
||||
children: React.ReactNode;
|
||||
accent?: "blue" | "green";
|
||||
}
|
||||
|
||||
export default function SectionEyebrow({
|
||||
children,
|
||||
accent = "blue",
|
||||
}: SectionEyebrowProps) {
|
||||
const dot = accent === "green" ? "bg-green-400" : "bg-blue-400";
|
||||
return (
|
||||
<div className="mb-4 inline-flex items-center gap-2 font-mono text-xs uppercase tracking-[0.2em] text-ink-500">
|
||||
<span className={`h-1.5 w-1.5 rounded-full ${dot}`} />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import Link from "next/link";
|
||||
import { Globe, Bot, Workflow, Server, LifeBuoy, ArrowUpRight } from "lucide-react";
|
||||
import type { ServiceMeta } from "@/config/services";
|
||||
|
||||
const iconMap = {
|
||||
Globe,
|
||||
Bot,
|
||||
Workflow,
|
||||
Server,
|
||||
LifeBuoy,
|
||||
};
|
||||
|
||||
interface ServiceCardProps {
|
||||
service: ServiceMeta;
|
||||
}
|
||||
|
||||
export default function ServiceCard({ service }: ServiceCardProps) {
|
||||
const Icon = iconMap[service.icon];
|
||||
const isGreen = service.accent === "green";
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={service.href}
|
||||
className={`group relative flex flex-col justify-between rounded-2xl border border-white/[0.06] bg-base-900/60 p-6 transition-all hover:-translate-y-1 clip-corner ${
|
||||
isGreen
|
||||
? "hover:border-green-500/30 hover:shadow-glow-green"
|
||||
: "hover:border-blue-500/30 hover:shadow-glow"
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={`mb-5 flex h-11 w-11 items-center justify-center rounded-xl border ${
|
||||
isGreen
|
||||
? "border-green-500/25 bg-green-500/10 text-green-400"
|
||||
: "border-blue-500/25 bg-blue-500/10 text-blue-400"
|
||||
}`}
|
||||
>
|
||||
<Icon size={20} strokeWidth={1.75} />
|
||||
</div>
|
||||
<h3 className="mb-2 font-display text-lg font-medium text-ink-100">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-ink-500">
|
||||
{service.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 flex items-center gap-1.5 text-sm font-medium text-ink-300 group-hover:text-ink-100">
|
||||
Подробнее
|
||||
<ArrowUpRight
|
||||
size={16}
|
||||
className="transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user