add custom right click
This commit is contained in:
parent
d52e6e59de
commit
71c8dce144
10 changed files with 705 additions and 0 deletions
BIN
public/Nikita_Egorov_Resume.pdf
Normal file
BIN
public/Nikita_Egorov_Resume.pdf
Normal file
Binary file not shown.
BIN
public/Nikita_Egorov_Resume_EN.pdf
Normal file
BIN
public/Nikita_Egorov_Resume_EN.pdf
Normal file
Binary file not shown.
BIN
public/Nikita_Egorov_Resume_RU.pdf
Normal file
BIN
public/Nikita_Egorov_Resume_RU.pdf
Normal file
Binary file not shown.
BIN
public/resume-en.pdf
Normal file
BIN
public/resume-en.pdf
Normal file
Binary file not shown.
BIN
public/resume-ru.pdf
Normal file
BIN
public/resume-ru.pdf
Normal file
Binary file not shown.
BIN
public/resume.pdf
Normal file
BIN
public/resume.pdf
Normal file
Binary file not shown.
|
|
@ -1,5 +1,6 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { ContextMenu } from "@/widgets/context-menu";
|
||||
import { CookieConsent } from "@/widgets/cookie-consent";
|
||||
import { Cursor } from "@/widgets/cursor";
|
||||
import { Preloader } from "@/widgets/preloader";
|
||||
|
|
@ -32,6 +33,7 @@ const RootLayout = ({ children }: LayoutProps<"/">) => {
|
|||
<Preloader />
|
||||
<Cursor />
|
||||
<ScrollCounter />
|
||||
<ContextMenu />
|
||||
{children}
|
||||
<CookieConsent />
|
||||
<YandexMetrika />
|
||||
|
|
|
|||
221
src/static/context-menu/context-menu.module.css
Normal file
221
src/static/context-menu/context-menu.module.css
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
.menu {
|
||||
position: fixed;
|
||||
z-index: 9990;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 265px;
|
||||
max-width: calc(100vw - 24px);
|
||||
padding: 8px;
|
||||
|
||||
background: rgba(14, 14, 16, 0.92);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 20px 48px rgba(0, 0, 0, 0.7),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.04);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
|
||||
color: #fff;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
animation: menuIn 0.16s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
@keyframes menuIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96) translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 10px 10px;
|
||||
}
|
||||
|
||||
.headerInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.headerTitleRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
color: #fff;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.headerSubtitle {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.langToggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px;
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 8px;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.langBtn {
|
||||
padding: 3px 7px;
|
||||
font-family: inherit;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.langBtn:hover {
|
||||
color: #000;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.langBtnActive,
|
||||
.langBtnActive:hover {
|
||||
color: #000 !important;
|
||||
background: #fff !important;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.statusDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #4ade80;
|
||||
box-shadow: 0 0 6px #4ade80;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
margin: 4px 2px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
color 0.15s ease,
|
||||
transform 0.12s ease;
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background: rgba(255, 255, 255, 0.09);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.item:active {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.itemPrimary {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.itemPrimary:hover {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
|
||||
.itemIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.item:hover .itemIcon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.itemLabel {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.itemBadge {
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.footerHint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 10px 4px;
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.kbd {
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
font-family: inherit;
|
||||
font-size: 9px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
1
src/widgets/context-menu/index.ts
Normal file
1
src/widgets/context-menu/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { ContextMenu } from "./ui/ContextMenu";
|
||||
481
src/widgets/context-menu/ui/ContextMenu.tsx
Normal file
481
src/widgets/context-menu/ui/ContextMenu.tsx
Normal file
|
|
@ -0,0 +1,481 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import styles from "@/static/context-menu/context-menu.module.css";
|
||||
|
||||
const PADDING = 12;
|
||||
const ESTIMATED_WIDTH = 275;
|
||||
const ESTIMATED_HEIGHT = 345;
|
||||
|
||||
type Lang = "ru" | "en";
|
||||
|
||||
const content = {
|
||||
ru: {
|
||||
name: "Никита Егоров",
|
||||
role: "Frontend & Web Developer",
|
||||
statusTitle: "Открыт к предложениям",
|
||||
primaryPdf: "/Nikita_Egorov_Resume_RU.pdf",
|
||||
primaryFilename: "Nikita_Egorov_Resume_RU.pdf",
|
||||
primaryLabel: "Скачать резюме (RU)",
|
||||
primaryBadge: "RU",
|
||||
secondaryPdf: "/Nikita_Egorov_Resume_EN.pdf",
|
||||
secondaryFilename: "Nikita_Egorov_Resume_EN.pdf",
|
||||
secondaryLabel: "Скачать резюме (EN)",
|
||||
secondaryBadge: "EN",
|
||||
previewLabel: "Открыть в новой вкладке",
|
||||
copyLabel: "Скопировать ссылку",
|
||||
copiedLabel: "Ссылка скопирована!",
|
||||
telegramLabel: "Написать в Telegram",
|
||||
footerMenu: "Меню сайта",
|
||||
esc: "Esc",
|
||||
},
|
||||
en: {
|
||||
name: "Nikita Egorov",
|
||||
role: "Frontend & Web Developer",
|
||||
statusTitle: "Available for hire",
|
||||
primaryPdf: "/Nikita_Egorov_Resume_EN.pdf",
|
||||
primaryFilename: "Nikita_Egorov_Resume_EN.pdf",
|
||||
primaryLabel: "Download Resume (EN)",
|
||||
primaryBadge: "EN",
|
||||
secondaryPdf: "/Nikita_Egorov_Resume_RU.pdf",
|
||||
secondaryFilename: "Nikita_Egorov_Resume_RU.pdf",
|
||||
secondaryLabel: "Download Resume (RU)",
|
||||
secondaryBadge: "RU",
|
||||
previewLabel: "Open in new tab",
|
||||
copyLabel: "Copy website link",
|
||||
copiedLabel: "Link copied!",
|
||||
telegramLabel: "Message on Telegram",
|
||||
footerMenu: "Site menu",
|
||||
esc: "Esc",
|
||||
},
|
||||
};
|
||||
|
||||
export const ContextMenu = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [lang, setLang] = useState<Lang>("ru");
|
||||
const [coords, setCoords] = useState({ x: 0, y: 0 });
|
||||
const [renderCoords, setRenderCoords] = useState({ x: 0, y: 0 });
|
||||
const [copied, setCopied] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
const handleSetLang = (newLang: Lang) => {
|
||||
setLang(newLang);
|
||||
try {
|
||||
localStorage.setItem("portfolio_lang", newLang);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
// Synchronous layout adjustment with actual DOM dimensions
|
||||
useLayoutEffect(() => {
|
||||
if (!isOpen || !menuRef.current) return;
|
||||
|
||||
const rect = menuRef.current.getBoundingClientRect();
|
||||
const width = rect.width || ESTIMATED_WIDTH;
|
||||
const height = rect.height || ESTIMATED_HEIGHT;
|
||||
|
||||
let nextX = Math.max(PADDING, coords.x);
|
||||
let nextY = Math.max(PADDING, coords.y);
|
||||
|
||||
if (nextX + width > window.innerWidth - PADDING) {
|
||||
nextX = Math.max(PADDING, window.innerWidth - width - PADDING);
|
||||
}
|
||||
if (nextY + height > window.innerHeight - PADDING) {
|
||||
nextY = Math.max(PADDING, window.innerHeight - height - PADDING);
|
||||
}
|
||||
|
||||
setRenderCoords({ x: Math.round(nextX), y: Math.round(nextY) });
|
||||
}, [isOpen, coords, lang]);
|
||||
|
||||
// Contextmenu listener
|
||||
useEffect(() => {
|
||||
const onContextMenu = (e: MouseEvent) => {
|
||||
// 1. Allow native menu if Shift key is pressed (developer/power user override)
|
||||
if (e.shiftKey) {
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. If user right clicks on open menu, prevent default and keep it
|
||||
if (
|
||||
menuRef.current &&
|
||||
e.target instanceof Node &&
|
||||
menuRef.current.contains(e.target)
|
||||
) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Allow native menu on text inputs, textareas and editable elements
|
||||
const target = e.target;
|
||||
if (
|
||||
target instanceof Element &&
|
||||
target.closest("input, textarea, [contenteditable='true'], select")
|
||||
) {
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Allow native menu if user has selected text on the page (for native Copy)
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString().trim().length > 0) {
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept browser context menu
|
||||
e.preventDefault();
|
||||
|
||||
let targetX = Math.max(PADDING, e.clientX);
|
||||
let targetY = Math.max(PADDING, e.clientY);
|
||||
|
||||
// Initial clamp with estimated dimensions to avoid frame jump
|
||||
if (targetX + ESTIMATED_WIDTH > window.innerWidth - PADDING) {
|
||||
targetX = Math.max(PADDING, window.innerWidth - ESTIMATED_WIDTH - PADDING);
|
||||
}
|
||||
if (targetY + ESTIMATED_HEIGHT > window.innerHeight - PADDING) {
|
||||
targetY = Math.max(PADDING, window.innerHeight - ESTIMATED_HEIGHT - PADDING);
|
||||
}
|
||||
|
||||
try {
|
||||
const saved = localStorage.getItem("portfolio_lang") as Lang | null;
|
||||
if (saved === "ru" || saved === "en") {
|
||||
setLang(saved);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
setCoords({ x: e.clientX, y: e.clientY });
|
||||
setRenderCoords({ x: Math.round(targetX), y: Math.round(targetY) });
|
||||
setCopied(false);
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
window.addEventListener("contextmenu", onContextMenu);
|
||||
return () => {
|
||||
window.removeEventListener("contextmenu", onContextMenu);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Close handlers: Escape, outside click, scroll, resize, window blur
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
if (
|
||||
menuRef.current &&
|
||||
e.target instanceof Node &&
|
||||
!menuRef.current.contains(e.target)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onScroll = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const onResize = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("pointerdown", onPointerDown);
|
||||
window.addEventListener("scroll", onScroll, { capture: true, passive: true });
|
||||
window.addEventListener("resize", onResize, { passive: true });
|
||||
window.addEventListener("blur", onBlur);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
window.removeEventListener("pointerdown", onPointerDown);
|
||||
window.removeEventListener("scroll", onScroll, { capture: true });
|
||||
window.removeEventListener("resize", onResize);
|
||||
window.removeEventListener("blur", onBlur);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const handleCopyLink = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await navigator.clipboard.writeText(window.location.href);
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
setIsOpen(false);
|
||||
}, 700);
|
||||
} catch {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActionClick = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const current = content[lang];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
role="menu"
|
||||
aria-label="Контекстное меню"
|
||||
className={styles.menu}
|
||||
style={{
|
||||
left: `${renderCoords.x}px`,
|
||||
top: `${renderCoords.y}px`,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
>
|
||||
{/* Header with name and language switcher */}
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerInfo}>
|
||||
<div className={styles.headerTitleRow}>
|
||||
<span className={styles.headerTitle}>{current.name}</span>
|
||||
<span
|
||||
className={styles.statusDot}
|
||||
title={current.statusTitle}
|
||||
aria-label={current.statusTitle}
|
||||
/>
|
||||
</div>
|
||||
<span className={styles.headerSubtitle}>{current.role}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.langToggle} role="group" aria-label="Язык / Language">
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.langBtn} ${lang === "ru" ? styles.langBtnActive : ""}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSetLang("ru");
|
||||
}}
|
||||
data-cursor-hover
|
||||
aria-pressed={lang === "ru"}
|
||||
>
|
||||
RU
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.langBtn} ${lang === "en" ? styles.langBtnActive : ""}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSetLang("en");
|
||||
}}
|
||||
data-cursor-hover
|
||||
aria-pressed={lang === "en"}
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.divider} />
|
||||
|
||||
{/* Main Download: Active language resume */}
|
||||
<a
|
||||
href={current.primaryPdf}
|
||||
download={current.primaryFilename}
|
||||
className={`${styles.item} ${styles.itemPrimary}`}
|
||||
role="menuitem"
|
||||
data-cursor-hover
|
||||
onClick={handleActionClick}
|
||||
>
|
||||
<span className={styles.itemIcon} aria-hidden="true">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
</span>
|
||||
<span className={styles.itemLabel}>{current.primaryLabel}</span>
|
||||
<span className={styles.itemBadge}>{current.primaryBadge}</span>
|
||||
</a>
|
||||
|
||||
{/* Secondary Download: Alternate language resume */}
|
||||
<a
|
||||
href={current.secondaryPdf}
|
||||
download={current.secondaryFilename}
|
||||
className={styles.item}
|
||||
role="menuitem"
|
||||
data-cursor-hover
|
||||
onClick={handleActionClick}
|
||||
>
|
||||
<span className={styles.itemIcon} aria-hidden="true">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
</span>
|
||||
<span className={styles.itemLabel}>{current.secondaryLabel}</span>
|
||||
<span className={styles.itemBadge}>{current.secondaryBadge}</span>
|
||||
</a>
|
||||
|
||||
{/* Preview PDF */}
|
||||
<a
|
||||
href={current.primaryPdf}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.item}
|
||||
role="menuitem"
|
||||
data-cursor-hover
|
||||
onClick={handleActionClick}
|
||||
>
|
||||
<span className={styles.itemIcon} aria-hidden="true">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
<polyline points="10 9 9 9 8 9" />
|
||||
</svg>
|
||||
</span>
|
||||
<span className={styles.itemLabel}>{current.previewLabel}</span>
|
||||
<span className={styles.itemIcon} aria-hidden="true" style={{ width: 14, height: 14 }}>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||
<polyline points="15 3 21 3 21 9" />
|
||||
<line x1="10" y1="14" x2="21" y2="3" />
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div className={styles.divider} />
|
||||
|
||||
{/* Copy link */}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.item}
|
||||
role="menuitem"
|
||||
data-cursor-hover
|
||||
onClick={handleCopyLink}
|
||||
>
|
||||
<span className={styles.itemIcon} aria-hidden="true">
|
||||
{copied ? (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
stroke="#4ade80"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
<span className={styles.itemLabel}>
|
||||
{copied ? current.copiedLabel : current.copyLabel}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Telegram */}
|
||||
<a
|
||||
href="https://t.me/autli"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.item}
|
||||
role="menuitem"
|
||||
data-cursor-hover
|
||||
onClick={handleActionClick}
|
||||
>
|
||||
<span className={styles.itemIcon} aria-hidden="true">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span className={styles.itemLabel}>{current.telegramLabel}</span>
|
||||
<span className={styles.itemIcon} aria-hidden="true" style={{ width: 14, height: 14 }}>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="7" y1="17" x2="17" y2="7" />
|
||||
<polyline points="7 7 17 7 17 17" />
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div className={styles.divider} />
|
||||
|
||||
<div className={styles.footerHint}>
|
||||
<span>{current.footerMenu}</span>
|
||||
<kbd className={styles.kbd}>{current.esc}</kbd>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue