Skip to content

Commit 6e71618

Browse files
committed
feat(contact-launcher): add contact launcher component with email and social links
1 parent f856a2e commit 6e71618

File tree

3 files changed

+309
-1
lines changed

3 files changed

+309
-1
lines changed
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
---
2+
type Lang = "pl" | "en";
3+
4+
const {
5+
lang = "en",
6+
} = Astro.props as {
7+
lang?: Lang;
8+
};
9+
10+
const email = "hello@rocketdeploy.dev";
11+
const freelancerUrl = "https://www.freelancer.com/u/D4m1an0101";
12+
const linkedinUrl = "https://www.linkedin.com/in/profile_name/";
13+
14+
const t = {
15+
pl: {
16+
fab: "Kontakt",
17+
title: "Wybierz formę kontaktu",
18+
copy: "Kopiuj",
19+
copied: "Skopiowano ✓",
20+
emailHint: "Najlepsze do rozmów technicznych.",
21+
freelancerLabel: "Napisz na Freelancer",
22+
freelancerHint: "Formalna współpraca przez platformę.",
23+
linkedinLabel: "Napisz na LinkedIn",
24+
linkedinHint: "Pierwszy kontakt i networking.",
25+
foot: "Nie prowadzimy czatu na żywo — odpowiadamy asynchronicznie.",
26+
},
27+
en: {
28+
fab: "Contact",
29+
title: "Choose a contact option",
30+
copy: "Copy",
31+
copied: "Copied ✓",
32+
emailHint: "Best for technical discussions.",
33+
freelancerLabel: "Message on Freelancer",
34+
freelancerHint: "If you want to work via the platform.",
35+
linkedinLabel: "Message on LinkedIn",
36+
linkedinHint: "Great for introductions and quick follow-ups.",
37+
foot: "We don’t use live chat — we reply asynchronously.",
38+
},
39+
} as const;
40+
41+
const L = t[lang];
42+
---
43+
44+
<div class="rd-contact" data-open="false">
45+
<button
46+
class="rd-contact__fab"
47+
type="button"
48+
aria-haspopup="dialog"
49+
aria-expanded="false"
50+
>
51+
<span class="rd-contact__fab-dot" aria-hidden="true"></span>
52+
<span class="rd-contact__fab-text">{L.fab}</span>
53+
</button>
54+
55+
<div class="rd-contact__pop" role="dialog">
56+
<div class="rd-contact__pop-head">
57+
<div class="rd-contact__title">{L.title}</div>
58+
<button class="rd-contact__close" type="button" aria-label="Close">✕</button>
59+
</div>
60+
61+
<div class="rd-contact__items">
62+
<!-- EMAIL -->
63+
<div class="rd-contact__item">
64+
<div class="rd-contact__kicker">email</div>
65+
<div class="rd-contact__row">
66+
<a class="rd-contact__link" href={`mailto:${email}`}>{email}</a>
67+
<button
68+
class="rd-contact__copy"
69+
type="button"
70+
data-copy={email}
71+
data-copy-label={L.copy}
72+
data-copied-label={L.copied}
73+
>
74+
{L.copy}
75+
</button>
76+
</div>
77+
<div class="rd-contact__hint">{L.emailHint}</div>
78+
</div>
79+
80+
<!-- FREELANCER -->
81+
<a
82+
class="rd-contact__item rd-contact__item--link"
83+
href={freelancerUrl}
84+
target="_blank"
85+
rel="noreferrer"
86+
>
87+
<div class="rd-contact__kicker">freelancer</div>
88+
<div class="rd-contact__row">
89+
<div class="rd-contact__label">{L.freelancerLabel}</div>
90+
<div class="rd-contact__go">↗</div>
91+
</div>
92+
<div class="rd-contact__hint">{L.freelancerHint}</div>
93+
</a>
94+
95+
<!-- LINKEDIN -->
96+
<a
97+
class="rd-contact__item rd-contact__item--link"
98+
href={linkedinUrl}
99+
target="_blank"
100+
rel="noreferrer"
101+
>
102+
<div class="rd-contact__kicker">linkedin</div>
103+
<div class="rd-contact__row">
104+
<div class="rd-contact__label">{L.linkedinLabel}</div>
105+
<div class="rd-contact__go">↗</div>
106+
</div>
107+
<div class="rd-contact__hint">{L.linkedinHint}</div>
108+
</a>
109+
</div>
110+
111+
<div class="rd-contact__foot">{L.foot}</div>
112+
</div>
113+
114+
<div class="rd-contact__backdrop"></div>
115+
</div>
116+
117+
<script>
118+
(() => {
119+
const root = document.querySelector<HTMLElement>(".rd-contact");
120+
if (!root) return;
121+
122+
const fab = root.querySelector<HTMLButtonElement>(".rd-contact__fab");
123+
const closeBtn = root.querySelector<HTMLButtonElement>(".rd-contact__close");
124+
const backdrop = root.querySelector<HTMLElement>(".rd-contact__backdrop");
125+
126+
if (!fab || !closeBtn || !backdrop) return;
127+
128+
const setOpen = (open: boolean): void => {
129+
root.dataset.open = open ? "true" : "false";
130+
fab.setAttribute("aria-expanded", open ? "true" : "false");
131+
};
132+
133+
fab.addEventListener("click", () => setOpen(root.dataset.open !== "true"));
134+
closeBtn.addEventListener("click", () => setOpen(false));
135+
backdrop.addEventListener("click", () => setOpen(false));
136+
137+
document.addEventListener("keydown", (e: KeyboardEvent) => {
138+
if (e.key === "Escape") setOpen(false);
139+
});
140+
141+
root.querySelectorAll<HTMLButtonElement>("[data-copy]").forEach((btn) => {
142+
btn.addEventListener("click", async (e) => {
143+
e.preventDefault();
144+
const text = btn.dataset.copy;
145+
if (!text) return;
146+
147+
const copyLabel = btn.dataset.copyLabel || btn.textContent || "Copy";
148+
const copiedLabel = btn.dataset.copiedLabel || "Copied ✓";
149+
150+
try {
151+
await navigator.clipboard.writeText(text);
152+
} catch {
153+
const ta = document.createElement("textarea");
154+
ta.value = text;
155+
document.body.appendChild(ta);
156+
ta.select();
157+
document.execCommand("copy");
158+
document.body.removeChild(ta);
159+
}
160+
161+
btn.textContent = copiedLabel;
162+
setTimeout(() => {
163+
btn.textContent = copyLabel;
164+
}, 1200);
165+
});
166+
});
167+
})();
168+
</script>

src/layouts/BaseLayout.astro

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ const { title, lang = "en" } = Astro.props;
33
import "../styles/global.css";
44
import Header from "../components/Header.astro";
55
import Footer from "../components/Footer.astro";
6+
import ContactLauncher from "../components/ContactLauncher.astro";
67
---
8+
79
<!doctype html>
810
<html lang={lang}>
911
<head>
@@ -12,6 +14,7 @@ import Footer from "../components/Footer.astro";
1214
<meta name="color-scheme" content="dark" />
1315
<title>{title ? `${title} · rocketdeploy` : "rocketdeploy"}</title>
1416
</head>
17+
1518
<body>
1619
<div class="header">
1720
<div class="container">
@@ -28,5 +31,7 @@ import Footer from "../components/Footer.astro";
2831
<Footer lang={lang} />
2932
</div>
3033
</div>
34+
35+
<ContactLauncher lang={lang} />
3136
</body>
32-
</html>
37+
</html>

src/styles/global.css

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,4 +500,139 @@ h1{
500500
}
501501
.grid.problem-snap::-webkit-scrollbar{
502502
display: none; /* Chromium/Safari */
503+
}
504+
505+
/* === Contact Launcher === */
506+
507+
.rd-contact {
508+
position: fixed;
509+
right: 18px;
510+
bottom: 18px;
511+
z-index: 9999;
512+
}
513+
514+
.rd-contact__fab {
515+
display: inline-flex;
516+
align-items: center;
517+
gap: 10px;
518+
padding: 12px 14px;
519+
border-radius: 999px;
520+
border: 1px solid rgba(255,255,255,.15);
521+
background: rgba(12,14,18,.85);
522+
color: #fff;
523+
cursor: pointer;
524+
backdrop-filter: blur(10px);
525+
}
526+
527+
.rd-contact__fab-dot {
528+
width: 10px;
529+
height: 10px;
530+
border-radius: 50%;
531+
background: var(--accent, #7aa2ff);
532+
box-shadow: 0 0 0 4px rgba(122,162,255,.2);
533+
}
534+
535+
.rd-contact__pop {
536+
position: absolute;
537+
right: 0;
538+
bottom: 58px;
539+
width: min(360px, calc(100vw - 36px));
540+
border-radius: 16px;
541+
background: rgba(12,14,18,.95);
542+
border: 1px solid rgba(255,255,255,.12);
543+
box-shadow: 0 20px 60px rgba(0,0,0,.6);
544+
opacity: 0;
545+
pointer-events: none;
546+
transform: translateY(8px);
547+
transition: .2s ease;
548+
z-index: 2;
549+
}
550+
551+
.rd-contact[data-open="true"] .rd-contact__pop {
552+
opacity: 1;
553+
pointer-events: auto;
554+
transform: translateY(0);
555+
}
556+
557+
.rd-contact__backdrop {
558+
position: fixed;
559+
inset: 0;
560+
display: none;
561+
z-index: 1;
562+
}
563+
564+
.rd-contact[data-open="true"] .rd-contact__backdrop {
565+
display: block;
566+
}
567+
568+
.rd-contact__pop-head {
569+
position: relative;
570+
}
571+
572+
.rd-contact__close {
573+
position: absolute;
574+
top: 10px;
575+
right: 10px;
576+
}
577+
578+
.rd-contact__pop-head,
579+
.rd-contact__foot {
580+
padding: 12px 14px;
581+
border-bottom: 1px solid rgba(255,255,255,.08);
582+
}
583+
584+
.rd-contact__foot {
585+
border-top: 1px solid rgba(255,255,255,.08);
586+
border-bottom: none;
587+
font-size: 12px;
588+
color: rgba(255,255,255,.6);
589+
}
590+
591+
.rd-contact__items {
592+
padding: 10px;
593+
display: grid;
594+
gap: 10px;
595+
}
596+
597+
.rd-contact__item {
598+
padding: 12px;
599+
border-radius: 12px;
600+
border: 1px solid rgba(255,255,255,.1);
601+
background: rgba(255,255,255,.03);
602+
text-decoration: none;
603+
color: #fff;
604+
}
605+
606+
.rd-contact__item:hover {
607+
background: rgba(255,255,255,.05);
608+
}
609+
610+
.rd-contact__kicker {
611+
font-size: 11px;
612+
text-transform: uppercase;
613+
letter-spacing: .12em;
614+
opacity: .6;
615+
margin-bottom: 6px;
616+
}
617+
618+
.rd-contact__row {
619+
display: flex;
620+
justify-content: space-between;
621+
align-items: center;
622+
gap: 10px;
623+
}
624+
625+
.rd-contact__copy {
626+
padding: 6px 10px;
627+
border-radius: 10px;
628+
border: 1px solid rgba(255,255,255,.15);
629+
background: transparent;
630+
color: #fff;
631+
cursor: pointer;
632+
}
633+
634+
@media (max-width: 520px) {
635+
.rd-contact__fab-text {
636+
display: none;
637+
}
503638
}

0 commit comments

Comments
 (0)