This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-website.git
The following commit(s) were added to refs/heads/master by this push:
new f04c0a1b8e2 feat: add Slack community nudge to homepage (#3967)
f04c0a1b8e2 is described below
commit f04c0a1b8e2daf3db5302229f4c9f24f7715f284
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Mon Jul 6 13:54:46 2026 +0800
feat: add Slack community nudge to homepage (#3967)
---
.../home-next/SlackCommunityNudge.logic.d.ts | 27 ++
.../home-next/SlackCommunityNudge.logic.js | 48 ++++
.../home-next/SlackCommunityNudge.logic.test.js | 119 ++++++++
src/components/home-next/SlackCommunityNudge.scss | 312 +++++++++++++++++++++
src/components/home-next/SlackCommunityNudge.tsx | 280 ++++++++++++++++++
.../home-next/sections/EcosystemSection.tsx | 3 +-
src/theme/Layout/Provider/index.tsx | 8 +-
7 files changed, 795 insertions(+), 2 deletions(-)
diff --git a/src/components/home-next/SlackCommunityNudge.logic.d.ts
b/src/components/home-next/SlackCommunityNudge.logic.d.ts
new file mode 100644
index 00000000000..c92e311e634
--- /dev/null
+++ b/src/components/home-next/SlackCommunityNudge.logic.d.ts
@@ -0,0 +1,27 @@
+export interface SlackNudgeTriggerState {
+ dismissed: boolean;
+ opened: boolean;
+ elapsedMs: number;
+ ecosystemVisible: boolean;
+}
+
+export interface MascotPupilOffsetInput {
+ eyeX: number;
+ eyeY: number;
+ mouseX: number;
+ mouseY: number;
+ eyeRadiusPx: number;
+ pupilRadiusRatio: number;
+ softDistancePx: number;
+}
+
+export interface MascotPupilOffset {
+ x: number;
+ y: number;
+}
+
+export function computeMascotPupilOffset(input: MascotPupilOffsetInput):
MascotPupilOffset;
+
+export function getSlackNudgeBenefits(): string[];
+
+export function shouldOpenSlackNudge(state: SlackNudgeTriggerState): boolean;
diff --git a/src/components/home-next/SlackCommunityNudge.logic.js
b/src/components/home-next/SlackCommunityNudge.logic.js
new file mode 100644
index 00000000000..3fd25063406
--- /dev/null
+++ b/src/components/home-next/SlackCommunityNudge.logic.js
@@ -0,0 +1,48 @@
+function shouldOpenSlackNudge({ dismissed, opened, elapsedMs, ecosystemVisible
}) {
+ if (dismissed || opened) {
+ return false;
+ }
+
+ return elapsedMs >= 5000 || ecosystemVisible;
+}
+
+function computeMascotPupilOffset({
+ eyeX,
+ eyeY,
+ mouseX,
+ mouseY,
+ eyeRadiusPx,
+ pupilRadiusRatio,
+ softDistancePx,
+}) {
+ const dx = mouseX - eyeX;
+ const dy = mouseY - eyeY;
+ const dist = Math.hypot(dx, dy);
+
+ if (dist === 0) {
+ return { x: 0, y: 0 };
+ }
+
+ const pupilRadiusPx = eyeRadiusPx * pupilRadiusRatio;
+ const maxOffset = Math.max(eyeRadiusPx - pupilRadiusPx, 0);
+ const scale = Math.min(dist / softDistancePx, 1);
+
+ return {
+ x: Number(((dx / dist) * maxOffset * scale).toFixed(2)),
+ y: Number(((dy / dist) * maxOffset * scale).toFixed(2)),
+ };
+}
+
+function getSlackNudgeBenefits() {
+ return [
+ 'Talk with Doris users and developers.',
+ 'Get the latest Doris community updates, learning resources, and event
information.',
+ 'Join community building, feature discussions, and PR reviews.',
+ ];
+}
+
+module.exports = {
+ computeMascotPupilOffset,
+ getSlackNudgeBenefits,
+ shouldOpenSlackNudge,
+};
diff --git a/src/components/home-next/SlackCommunityNudge.logic.test.js
b/src/components/home-next/SlackCommunityNudge.logic.test.js
new file mode 100644
index 00000000000..8514215f2b5
--- /dev/null
+++ b/src/components/home-next/SlackCommunityNudge.logic.test.js
@@ -0,0 +1,119 @@
+const assert = require('node:assert/strict');
+const test = require('node:test');
+
+const {
+ computeMascotPupilOffset,
+ getSlackNudgeBenefits,
+ shouldOpenSlackNudge,
+} = require('./SlackCommunityNudge.logic');
+
+test('opens after the visitor has stayed on the page for at least five
seconds', () => {
+ assert.equal(
+ shouldOpenSlackNudge({
+ dismissed: false,
+ opened: false,
+ elapsedMs: 5000,
+ ecosystemVisible: false,
+ }),
+ true,
+ );
+});
+
+test('opens when the ecosystem section becomes visible before five seconds',
() => {
+ assert.equal(
+ shouldOpenSlackNudge({
+ dismissed: false,
+ opened: false,
+ elapsedMs: 1200,
+ ecosystemVisible: true,
+ }),
+ true,
+ );
+});
+
+test('stays closed before five seconds on pages without the ecosystem
section', () => {
+ assert.equal(
+ shouldOpenSlackNudge({
+ dismissed: false,
+ opened: false,
+ elapsedMs: 1200,
+ ecosystemVisible: false,
+ }),
+ false,
+ );
+});
+
+test('stays closed after the visitor dismisses or has already seen it', () => {
+ assert.equal(
+ shouldOpenSlackNudge({
+ dismissed: true,
+ opened: false,
+ elapsedMs: 7000,
+ ecosystemVisible: true,
+ }),
+ false,
+ );
+
+ assert.equal(
+ shouldOpenSlackNudge({
+ dismissed: false,
+ opened: true,
+ elapsedMs: 7000,
+ ecosystemVisible: true,
+ }),
+ false,
+ );
+});
+
+test('keeps mascot pupils centered when the pointer is at the eye center', ()
=> {
+ assert.deepEqual(
+ computeMascotPupilOffset({
+ eyeX: 100,
+ eyeY: 100,
+ mouseX: 100,
+ mouseY: 100,
+ eyeRadiusPx: 10,
+ pupilRadiusRatio: 0.5,
+ softDistancePx: 80,
+ }),
+ { x: 0, y: 0 },
+ );
+});
+
+test('limits mascot pupil movement to the remaining eye radius', () => {
+ assert.deepEqual(
+ computeMascotPupilOffset({
+ eyeX: 100,
+ eyeY: 100,
+ mouseX: 260,
+ mouseY: 100,
+ eyeRadiusPx: 10,
+ pupilRadiusRatio: 0.5,
+ softDistancePx: 80,
+ }),
+ { x: 5, y: 0 },
+ );
+});
+
+test('softens mascot pupil movement for nearby pointers', () => {
+ assert.deepEqual(
+ computeMascotPupilOffset({
+ eyeX: 100,
+ eyeY: 100,
+ mouseX: 140,
+ mouseY: 100,
+ eyeRadiusPx: 10,
+ pupilRadiusRatio: 0.5,
+ softDistancePx: 80,
+ }),
+ { x: 2.5, y: 0 },
+ );
+});
+
+test('returns the Slack community benefits shown in the nudge', () => {
+ assert.deepEqual(getSlackNudgeBenefits(), [
+ 'Talk with Doris users and developers.',
+ 'Get the latest Doris community updates, learning resources, and event
information.',
+ 'Join community building, feature discussions, and PR reviews.',
+ ]);
+});
diff --git a/src/components/home-next/SlackCommunityNudge.scss
b/src/components/home-next/SlackCommunityNudge.scss
new file mode 100644
index 00000000000..22e0300cfe8
--- /dev/null
+++ b/src/components/home-next/SlackCommunityNudge.scss
@@ -0,0 +1,312 @@
+@use '../shared/typography' as type;
+
+.slack-community-nudge {
+ --sn-green: #06805F;
+ --sn-green-deep: #054C39;
+ --sn-green-glow: #2DDFA8;
+ --sn-cream: #FAF6EE;
+ --sn-cream-warm: #F5EFE4;
+ --sn-yellow: #FFD23F;
+ --sn-yellow-bright: #FFE066;
+ --sn-ink: #0F1A14;
+
+ position: fixed;
+ right: 24px;
+ bottom: 0;
+ z-index: 260;
+ width: 386px;
+ height: 306px;
+ pointer-events: none;
+ font-family: var(--font-family-base);
+
+ &__bubble {
+ position: absolute;
+ right: 22px;
+ bottom: 100px;
+ width: min(348px, calc(100vw - 32px));
+ padding: 18px 18px 16px;
+ border: 1px solid rgba(17, 166, 121, 0.22);
+ border-radius: 8px;
+ background:
+ radial-gradient(circle at 16% 0%, rgba(45, 223, 168, 0.16),
transparent 34%),
+ linear-gradient(180deg, #FFFCF5 0%, var(--sn-cream) 100%);
+ color: var(--sn-ink);
+ box-shadow:
+ 0 28px 70px -34px rgba(5, 76, 57, 0.52),
+ 0 14px 34px -24px rgba(15, 26, 20, 0.38),
+ inset 0 1px 0 rgba(255, 255, 255, 0.8);
+ opacity: 0;
+ pointer-events: none;
+ transform: translate3d(18px, 14px, 0) scale(0.94);
+ transform-origin: 88% 100%;
+ transition:
+ opacity 0.18s ease,
+ transform 0.22s cubic-bezier(0.18, 0.9, 0.25, 1.1);
+
+ &::after {
+ content: '';
+ position: absolute;
+ right: 54px;
+ bottom: -9px;
+ width: 18px;
+ height: 18px;
+ border-right: 1px solid rgba(17, 166, 121, 0.22);
+ border-bottom: 1px solid rgba(17, 166, 121, 0.22);
+ background: var(--sn-cream);
+ transform: rotate(45deg);
+ box-shadow: 10px 10px 24px -18px rgba(15, 26, 20, 0.38);
+ }
+ }
+
+ &--open &__bubble {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translate3d(0, 0, 0) scale(1);
+ }
+
+ &__close {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ width: 26px;
+ height: 26px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid rgba(15, 26, 20, 0.1);
+ border-radius: 6px;
+ background: rgba(255, 255, 255, 0.52);
+ color: rgba(15, 26, 20, 0.58);
+ cursor: pointer;
+ line-height: 1;
+ transition:
+ background 0.16s ease,
+ border-color 0.16s ease,
+ color 0.16s ease,
+ transform 0.16s ease;
+
+ span {
+ display: block;
+ transform: translateY(-1px);
+ font-size: 18px;
+ }
+
+ &:hover {
+ transform: translateY(-1px);
+ border-color: rgba(6, 128, 95, 0.22);
+ background: rgba(255, 255, 255, 0.86);
+ color: var(--sn-green-deep);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--sn-yellow);
+ outline-offset: 3px;
+ }
+ }
+
+ &__eyebrow {
+ margin: 0 34px 8px 0;
+ color: var(--sn-green-deep);
+ @include type.mono-text(10.5px, 700, 1.2, 0, uppercase);
+ }
+
+ &__title {
+ margin: 0 34px 7px 0;
+ color: var(--sn-ink);
+ @include type.mono-text(18px, 800, 1.12, 0);
+ }
+
+ &__copy {
+ margin: 0 0 10px;
+ max-width: 298px;
+ color: rgba(15, 26, 20, 0.68);
+ @include type.sans-text(14px, 400, 1.45);
+ }
+
+ &__benefits {
+ display: flex;
+ flex-direction: column;
+ gap: 7px;
+ margin: 0 0 15px;
+ padding: 0;
+ list-style: none;
+ }
+
+ &__benefit {
+ position: relative;
+ padding-left: 15px;
+ color: rgba(15, 26, 20, 0.74);
+ @include type.sans-text(13px, 450, 1.34);
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0.6em;
+ left: 0;
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: var(--sn-green);
+ box-shadow: 0 0 0 3px rgba(17, 166, 121, 0.11);
+ transform: translateY(-50%);
+ }
+ }
+
+ &__cta {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ min-height: 38px;
+ padding: 0 14px;
+ border: 1.5px solid var(--sn-yellow);
+ border-radius: 6px;
+ background: var(--sn-yellow);
+ color: var(--sn-ink) !important;
+ text-decoration: none !important;
+ @include type.mono-text(12px, 800, 1.2, 0, uppercase);
+ box-shadow: 0 14px 28px -20px rgba(15, 26, 20, 0.54);
+ transition:
+ background 0.16s ease,
+ border-color 0.16s ease,
+ box-shadow 0.16s ease,
+ transform 0.16s ease;
+
+ &:hover {
+ transform: translateY(-1px);
+ background: var(--sn-yellow-bright);
+ border-color: var(--sn-yellow-bright);
+ box-shadow: 0 18px 34px -22px rgba(15, 26, 20, 0.62);
+ text-decoration: none !important;
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--sn-green-deep);
+ outline-offset: 3px;
+ }
+ }
+
+ &__mascot {
+ position: absolute;
+ right: 0;
+ bottom: -22px;
+ width: 178px;
+ aspect-ratio: 1344 / 768;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ cursor: pointer;
+ pointer-events: auto;
+ filter: drop-shadow(0 18px 24px rgba(15, 26, 20, 0.22));
+ transform: translate3d(0, 0, 0);
+ transition:
+ filter 0.18s ease,
+ transform 0.18s ease;
+
+ &:hover {
+ filter: drop-shadow(0 20px 28px rgba(15, 26, 20, 0.28));
+ transform: translate3d(0, -3px, 0);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--sn-yellow);
+ outline-offset: 2px;
+ border-radius: 8px;
+ }
+ }
+
+ &--open &__mascot {
+ transform: translate3d(0, -2px, 0);
+ }
+
+ &__mascot-image {
+ display: block;
+ width: 100%;
+ height: auto;
+ user-select: none;
+ pointer-events: none;
+ }
+
+ &__mascot-pupil {
+ position: absolute;
+ width: 1.7%;
+ aspect-ratio: 1;
+ border-radius: 50%;
+ background: var(--sn-ink);
+ pointer-events: none;
+ transform: translate(-50%, -50%);
+ transition: transform 0.08s ease-out;
+ will-change: transform;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .slack-community-nudge {
+ &__bubble,
+ &__close,
+ &__cta,
+ &__mascot,
+ &__mascot-pupil {
+ transition: none;
+ }
+ }
+}
+
+@media (max-width: 640px) {
+ .slack-community-nudge {
+ left: 12px;
+ right: 12px;
+ bottom: max(12px, env(safe-area-inset-bottom));
+ width: auto;
+ height: auto;
+
+ &__bubble {
+ position: relative;
+ right: 0;
+ bottom: auto;
+ width: 100%;
+ max-height: calc(100vh - 96px);
+ overflow-y: auto;
+ padding: 16px;
+ transform: translate3d(0, 12px, 0) scale(0.98);
+ transform-origin: 50% 100%;
+ }
+
+ &__bubble::after {
+ display: none;
+ }
+
+ &__title {
+ font-size: 16px;
+ }
+
+ &__copy {
+ max-width: none;
+ font-size: 13px;
+ }
+
+ &__benefits {
+ gap: 6px;
+ margin-bottom: 13px;
+ }
+
+ &__benefit {
+ font-size: 12.5px;
+ }
+
+ &__mascot {
+ display: none;
+ }
+
+ &__mascot-pupil {
+ display: none;
+ }
+ }
+}
+
+@media print {
+ .slack-community-nudge {
+ display: none;
+ }
+}
diff --git a/src/components/home-next/SlackCommunityNudge.tsx
b/src/components/home-next/SlackCommunityNudge.tsx
new file mode 100644
index 00000000000..74a52b6dd62
--- /dev/null
+++ b/src/components/home-next/SlackCommunityNudge.tsx
@@ -0,0 +1,280 @@
+import React, { JSX, useCallback, useEffect, useRef, useState } from 'react';
+import {
+ computeMascotPupilOffset,
+ getSlackNudgeBenefits,
+ shouldOpenSlackNudge,
+} from './SlackCommunityNudge.logic';
+import './SlackCommunityNudge.scss';
+
+const SLACK_URL = 'https://doris.apache.org/slack';
+const STORAGE_KEY = 'doris-home-slack-nudge-dismissed';
+const ECOSYSTEM_TARGET_ID = 'home-next-ecosystem';
+const OPEN_DELAY_MS = 5000;
+const SMALL_SCREEN_QUERY = '(max-width: 640px)';
+const MASCOT_EYE_RADIUS_PCT = 1.7;
+const MASCOT_PUPIL_RADIUS_RATIO = 0.5;
+const MASCOT_SOFT_DISTANCE_PX = 80;
+const MASCOT_EYES: Array<{ cx: number; cy: number }> = [
+ { cx: 22.96, cy: 54.01 },
+ { cx: 28.59, cy: 54.01 },
+ { cx: 51.01, cy: 51.27 },
+ { cx: 56.39, cy: 51.95 },
+ { cx: 74.99, cy: 58.71 },
+ { cx: 80.30, cy: 59.91 },
+];
+
+function SlackGlyph(): JSX.Element {
+ return (
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
aria-hidden="true">
+ <path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0
0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0
0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834
24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0
1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521
2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0
1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 [...]
+ </svg>
+ );
+}
+
+function hasStoredDismissal(): boolean {
+ try {
+ return window.sessionStorage.getItem(STORAGE_KEY) === 'true';
+ } catch {
+ return false;
+ }
+}
+
+function storeDismissal(): void {
+ try {
+ window.sessionStorage.setItem(STORAGE_KEY, 'true');
+ } catch {
+ // Storage may be unavailable in private or restricted browsing modes.
+ }
+}
+
+export function SlackCommunityNudge(): JSX.Element {
+ const [isOpen, setIsOpen] = useState(false);
+ const [eyeTrackingEnabled, setEyeTrackingEnabled] = useState(false);
+ const autoDismissedRef = useRef(false);
+ const autoOpenedRef = useRef(false);
+ const mountedAtRef = useRef<number | null>(null);
+ const mascotRef = useRef<HTMLButtonElement>(null);
+ const pupilRefs = useRef<Array<HTMLSpanElement | null>>([]);
+ const lastPointerRef = useRef<{ x: number; y: number } | null>(null);
+
+ const openAutomatically = useCallback((ecosystemVisible: boolean) => {
+ const mountedAt = mountedAtRef.current ?? Date.now();
+ const elapsedMs = Date.now() - mountedAt;
+
+ if (
+ shouldOpenSlackNudge({
+ dismissed: autoDismissedRef.current,
+ opened: autoOpenedRef.current,
+ elapsedMs,
+ ecosystemVisible,
+ })
+ ) {
+ autoOpenedRef.current = true;
+ setIsOpen(true);
+ }
+ }, []);
+
+ const dismissAutoPrompt = useCallback(() => {
+ autoDismissedRef.current = true;
+ storeDismissal();
+ setIsOpen(false);
+ }, []);
+
+ useEffect(() => {
+ if (typeof window === 'undefined') return undefined;
+
+ mountedAtRef.current = Date.now();
+ autoDismissedRef.current = hasStoredDismissal();
+
+ const timer = window.setTimeout(() => {
+ openAutomatically(false);
+ }, OPEN_DELAY_MS);
+
+ return () => window.clearTimeout(timer);
+ }, [openAutomatically]);
+
+ useEffect(() => {
+ if (typeof window === 'undefined' || typeof document === 'undefined')
return undefined;
+
+ const target = document.getElementById(ECOSYSTEM_TARGET_ID);
+ if (!target || typeof window.IntersectionObserver === 'undefined') {
+ return undefined;
+ }
+
+ const observer = new window.IntersectionObserver(
+ entries => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting && entry.intersectionRatio >=
0.24) {
+ openAutomatically(true);
+ }
+ });
+ },
+ {
+ threshold: [0, 0.24, 0.5],
+ rootMargin: '0px 0px -12% 0px',
+ },
+ );
+
+ observer.observe(target);
+ return () => observer.disconnect();
+ }, [openAutomatically]);
+
+ useEffect(() => {
+ if (typeof window === 'undefined') return undefined;
+
+ const mediaQuery = window.matchMedia(SMALL_SCREEN_QUERY);
+ const updateEyeTracking = () =>
setEyeTrackingEnabled(!mediaQuery.matches);
+
+ updateEyeTracking();
+ if (typeof mediaQuery.addEventListener === 'function') {
+ mediaQuery.addEventListener('change', updateEyeTracking);
+ } else {
+ mediaQuery.addListener(updateEyeTracking);
+ }
+
+ return () => {
+ if (typeof mediaQuery.removeEventListener === 'function') {
+ mediaQuery.removeEventListener('change', updateEyeTracking);
+ } else {
+ mediaQuery.removeListener(updateEyeTracking);
+ }
+ };
+ }, []);
+
+ useEffect(() => {
+ if (typeof window === 'undefined' || !eyeTrackingEnabled) return
undefined;
+
+ const updateEyes = (mouseX: number, mouseY: number) => {
+ const mascot = mascotRef.current;
+ if (!mascot) return;
+
+ const rect = mascot.getBoundingClientRect();
+ if (rect.width === 0 || rect.height === 0) return;
+
+ const eyeRadiusPx = (MASCOT_EYE_RADIUS_PCT / 100) * rect.width;
+
+ MASCOT_EYES.forEach((eye, index) => {
+ const pupil = pupilRefs.current[index];
+ if (!pupil) return;
+
+ const eyeX = rect.left + (eye.cx / 100) * rect.width;
+ const eyeY = rect.top + (eye.cy / 100) * rect.height;
+ const offset = computeMascotPupilOffset({
+ eyeX,
+ eyeY,
+ mouseX,
+ mouseY,
+ eyeRadiusPx,
+ pupilRadiusRatio: MASCOT_PUPIL_RADIUS_RATIO,
+ softDistancePx: MASCOT_SOFT_DISTANCE_PX,
+ });
+
+ pupil.style.transform = `translate(calc(-50% + ${offset.x}px),
calc(-50% + ${offset.y}px))`;
+ });
+ };
+
+ const onMouseMove = (event: MouseEvent) => {
+ lastPointerRef.current = { x: event.clientX, y: event.clientY };
+ updateEyes(event.clientX, event.clientY);
+ };
+
+ const onTouchMove = (event: TouchEvent) => {
+ const touch = event.touches[0];
+ if (!touch) return;
+ lastPointerRef.current = { x: touch.clientX, y: touch.clientY };
+ updateEyes(touch.clientX, touch.clientY);
+ };
+
+ const onScroll = () => {
+ const pointer = lastPointerRef.current;
+ if (pointer) updateEyes(pointer.x, pointer.y);
+ };
+
+ window.addEventListener('mousemove', onMouseMove, { passive: true });
+ window.addEventListener('touchmove', onTouchMove, { passive: true });
+ window.addEventListener('scroll', onScroll, { passive: true });
+
+ return () => {
+ window.removeEventListener('mousemove', onMouseMove);
+ window.removeEventListener('touchmove', onTouchMove);
+ window.removeEventListener('scroll', onScroll);
+ };
+ }, [eyeTrackingEnabled]);
+
+ const handleJoin = () => {
+ dismissAutoPrompt();
+ };
+
+ return (
+ <aside
+ className={`slack-community-nudge${isOpen ? '
slack-community-nudge--open' : ''}`}
+ aria-label="Apache Doris Slack community invitation"
+ >
+ <div className="slack-community-nudge__bubble"
aria-hidden={!isOpen}>
+ <button
+ type="button"
+ className="slack-community-nudge__close"
+ aria-label="Dismiss Slack invitation"
+ onClick={dismissAutoPrompt}
+ tabIndex={isOpen ? undefined : -1}
+ >
+ <span aria-hidden="true">×</span>
+ </button>
+ <div className="slack-community-nudge__eyebrow">
+ Community help
+ </div>
+ <p className="slack-community-nudge__title">
+ Building with Doris?
+ </p>
+ <p className="slack-community-nudge__copy">
+ Join Slack for setup help, performance tips, and answers
from Apache Doris users and maintainers.
+ </p>
+ <ul className="slack-community-nudge__benefits">
+ {getSlackNudgeBenefits().map(benefit => (
+ <li className="slack-community-nudge__benefit"
key={benefit}>
+ {benefit}
+ </li>
+ ))}
+ </ul>
+ <a
+ className="slack-community-nudge__cta"
+ href={SLACK_URL}
+ target="_blank"
+ rel="noopener noreferrer"
+ onClick={handleJoin}
+ tabIndex={isOpen ? undefined : -1}
+ >
+ <SlackGlyph />
+ Join Slack
+ </a>
+ </div>
+
+ <button
+ type="button"
+ className="slack-community-nudge__mascot"
+ ref={mascotRef}
+ aria-expanded={isOpen}
+ aria-label={isOpen ? 'Hide Slack invitation' : 'Open Slack
invitation'}
+ onClick={() => setIsOpen(open => !open)}
+ >
+ <img
+ className="slack-community-nudge__mascot-image"
+ src="/images/next/home-page/doris-mascot.png"
+ width={1344}
+ height={768}
+ alt=""
+ draggable={false}
+ />
+ {MASCOT_EYES.map((eye, index) => (
+ <span
+ key={index}
+ ref={(el) => { pupilRefs.current[index] = el; }}
+ className="slack-community-nudge__mascot-pupil"
+ style={{ left: `${eye.cx}%`, top: `${eye.cy}%` }}
+ aria-hidden="true"
+ />
+ ))}
+ </button>
+ </aside>
+ );
+}
diff --git a/src/components/home-next/sections/EcosystemSection.tsx
b/src/components/home-next/sections/EcosystemSection.tsx
index 243eaa4d142..780f62c8e71 100644
--- a/src/components/home-next/sections/EcosystemSection.tsx
+++ b/src/components/home-next/sections/EcosystemSection.tsx
@@ -504,6 +504,7 @@ function CompactConnector(): JSX.Element {
function CompactEcosystem(): JSX.Element {
return (
<section
+ id="home-next-ecosystem"
className="ecosystem-next ecosystem-next--compact"
aria-labelledby="ecosystem-next-title"
>
@@ -544,7 +545,7 @@ export function EcosystemSection(): JSX.Element {
}
return (
- <section className="ecosystem-next"
aria-labelledby="ecosystem-next-title">
+ <section id="home-next-ecosystem" className="ecosystem-next"
aria-labelledby="ecosystem-next-title">
<div className="home-next-container">
<EcosystemHeader showCoverage />
diff --git a/src/theme/Layout/Provider/index.tsx
b/src/theme/Layout/Provider/index.tsx
index 4b765fba2c5..e681238e13b 100644
--- a/src/theme/Layout/Provider/index.tsx
+++ b/src/theme/Layout/Provider/index.tsx
@@ -9,6 +9,7 @@ import {
} from '@docusaurus/theme-common/internal';
import {DocsPreferredVersionContextProvider} from
'@docusaurus/plugin-content-docs/client';
import type {Props} from '@theme/Layout/Provider';
+import {SlackCommunityNudge} from
'@site/src/components/home-next/SlackCommunityNudge';
const Provider = composeProviders([
ColorModeProvider,
@@ -20,5 +21,10 @@ const Provider = composeProviders([
]);
export default function LayoutProvider({children}: Props): JSX.Element {
- return <Provider>{children}</Provider>;
+ return (
+ <Provider>
+ {children}
+ <SlackCommunityNudge />
+ </Provider>
+ );
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]