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 type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import { ContextMenu } from "@/widgets/context-menu";
|
||||||
import { CookieConsent } from "@/widgets/cookie-consent";
|
import { CookieConsent } from "@/widgets/cookie-consent";
|
||||||
import { Cursor } from "@/widgets/cursor";
|
import { Cursor } from "@/widgets/cursor";
|
||||||
import { Preloader } from "@/widgets/preloader";
|
import { Preloader } from "@/widgets/preloader";
|
||||||
|
|
@ -32,6 +33,7 @@ const RootLayout = ({ children }: LayoutProps<"/">) => {
|
||||||
<Preloader />
|
<Preloader />
|
||||||
<Cursor />
|
<Cursor />
|
||||||
<ScrollCounter />
|
<ScrollCounter />
|
||||||
|
<ContextMenu />
|
||||||
{children}
|
{children}
|
||||||
<CookieConsent />
|
<CookieConsent />
|
||||||
<YandexMetrika />
|
<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