fix(docs): restore homepage integration flow

This commit is contained in:
ajnart committed 2026-08-13 22:13:08 +02:00
1 parent c642f9a8d3
commit 673589f6c8
3 files changed
+291 -145

No files matched your search

@@ -1,147 +1,86 @@
import React, { useRef } from "react";
import styles from "../../../../pages/index.module.css";
import { supportedIntegrations } from "../../../../constants/supported-integrations";
import LeaderLine, { SocketType } from "leader-line-new";
const featuredIntegrations = [
{ name: "Radarr", position: styles.dataflowNodeTopLeft },
{ name: "Sonarr", position: styles.dataflowNodeMiddleLeft },
{ name: "Lidarr", position: styles.dataflowNodeBottomLeft },
{ name: "SABnzbd", position: styles.dataflowNodeTopRight },
{ name: "Jellyfin", position: styles.dataflowNodeMiddleRight },
].map((node) => ({
...node,
image: supportedIntegrations.find((integration) => integration.name === node.name)?.iconUrl ?? "",
}));
export const DataflowVisualizationComponent = () => {
const homarrRef = useRef<HTMLImageElement>(null);
const sonarrRef = useRef<HTMLImageElement>(null);
const lidarrRef = useRef<HTMLImageElement>(null);
const radarrRef = useRef<HTMLImageElement>(null);
const jellyfinRef = useRef<HTMLImageElement>(null);
const plexRef = useRef<HTMLImageElement>(null);
const sabnzbdRef = useRef<HTMLImageElement>(null);
const featuredNames = new Set(["Homarr", ...featuredIntegrations.map((integration) => integration.name)]);
const otherIntegrations = supportedIntegrations.filter((integration) => !featuredNames.has(integration.name));
const iconCycleStepInSeconds = 1.4;
return (
<div className={"bg-black/[.10] py-20"}>
<h2 className={"text-center lg:text-5xl text-3xl font-extrabold mb-24"}>
const paths = [
"M145 82 C330 82 330 235 480 260",
"M145 260 C300 260 360 260 480 260",
"M145 438 C330 438 330 285 480 260",
"M480 260 C630 235 630 82 815 82",
"M480 260 C600 260 660 260 815 260",
"M480 260 C630 285 630 438 815 438",
];
export const DataflowVisualizationComponent = () => (
<section className={styles.dataflowSection}>
<div className={styles.dataflowHeading}>
<h2>
No YAML configurations.
<br />
Easy and quick to manage integrations.
</h2>
<p>Homarr brings status, controls, and updates together in one dashboard.</p>
</div>
<div className="relative max-w-128 h-80 mx-auto animated-dataflow mx-5">
<img
ref={homarrRef}
className={"absolute left-1/2 top-1/2 -translate-y-1/2 -translate-x-1/2 aspect-square object-contain"}
src={"/img/logo.png"}
alt={"Homarr Logo"}
width={100}
height={50}
/>
<div
className={styles.dataflowDiagram}
role="img"
aria-label="Information flowing both ways between Homarr and six connected services"
>
<svg className={styles.dataflowLines} viewBox="0 0 960 520" preserveAspectRatio="none" aria-hidden="true">
{paths.map((path) => (
<g key={path}>
<path className={styles.dataflowLine} d={path} pathLength="1" />
<path className={styles.dataflowSignal} d={path} pathLength="1" />
<path className={styles.dataflowSignalReverse} d={path} pathLength="1" />
</g>
))}
</svg>
<img
ref={radarrRef}
className={"absolute left-0 top-0"}
src={"https://github.com/walkxcode/dashboard-icons/blob/main/png/radarr.png?raw=true"}
alt={"Radarr"}
width={50}
height={50}
/>
<img
ref={sonarrRef}
className={"absolute left-0 top-1/2 -translate-y-1/2"}
src={"https://github.com/walkxcode/dashboard-icons/blob/main/png/sonarr.png?raw=true"}
alt={"Sonarr"}
width={50}
height={50}
/>
<img
ref={lidarrRef}
className={"absolute left-0 bottom-0"}
src={"https://github.com/walkxcode/dashboard-icons/blob/main/png/lidarr.png?raw=true"}
alt={"Lidarr"}
width={50}
height={50}
/>
{featuredIntegrations.map((integration) => (
<div className={`${styles.dataflowNode} ${integration.position}`} key={integration.name}>
<img src={integration.image} alt="" width={54} height={54} />
<span>{integration.name}</span>
</div>
))}
<img
ref={jellyfinRef}
className={"absolute right-0 top-1/2 -translate-y-1/2"}
src={"https://github.com/walkxcode/dashboard-icons/blob/main/png/jellyfin.png?raw=true"}
alt={"Jellyfin"}
width={50}
height={50}
/>
<div className={`${styles.dataflowNode} ${styles.dataflowNodeBottomRight}`}>
<div className={styles.dataflowMoreIcons} aria-hidden="true">
{otherIntegrations.map((integration, index) => (
<img
src={integration.iconUrl}
alt=""
width={54}
height={54}
key={integration.name}
style={{
animationDelay: `${-index * iconCycleStepInSeconds}s`,
animationDuration: `${otherIntegrations.length * iconCycleStepInSeconds}s`,
}}
/>
))}
</div>
<span>+{otherIntegrations.length} more</span>
</div>
<img
ref={plexRef}
className={"absolute right-0 bottom-0"}
src={"https://github.com/walkxcode/dashboard-icons/blob/main/png/plex.png?raw=true"}
alt={"Plex"}
width={50}
height={50}
/>
<img
ref={sabnzbdRef}
className={"absolute right-0 top-0"}
src={"https://github.com/walkxcode/dashboard-icons/blob/main/png/sabnzbd.png?raw=true"}
alt={"Sabnzbd"}
width={50}
height={50}
/>
<LineTree start={sonarrRef} end={homarrRef} startSocket={"right"} endSocket={"left"} />
<LineTree start={radarrRef} end={homarrRef} startSocket={"right"} endSocket={"left"} />
<LineTree start={lidarrRef} end={homarrRef} startSocket={"right"} endSocket={"left"} />
<LineTree start={plexRef} end={homarrRef} startSocket={"left"} endSocket={"right"} x={100} />
<LineTree start={jellyfinRef} end={homarrRef} startSocket={"left"} endSocket={"right"} x={100} />
<LineTree start={sabnzbdRef} end={homarrRef} startSocket={"left"} endSocket={"right"} x={100} />
<div className={`${styles.dataflowNode} ${styles.dataflowHub}`}>
<img src="/img/logo.png" alt="" width={86} height={86} />
<span>Homarr</span>
</div>
</div>
);
};
export const LineTree = ({
start,
end,
startSocket,
endSocket,
x = 0,
}: {
start: any;
end: any;
startSocket: SocketType;
endSocket: SocketType;
x?: number;
}) => {
const line = useRef<LeaderLine | null>(null);
let leaderLine: LeaderLine;
React.useEffect(() => {
const drawLine = () => {
leaderLine = new LeaderLine(start.current, LeaderLine.pointAnchor(end.current, { x: `${x}%` }), {
startSocket: startSocket,
endSocket: endSocket,
color: "var(--ifm-color-primary)",
size: 3,
dash: {
animation: true,
},
});
};
const timer = setInterval(() => {
if (start.current) {
clearInterval(timer);
drawLine();
}
}, 5);
return () => {
timer && clearInterval(timer);
leaderLine.remove();
};
}, []);
React.useEffect(() => {
// scroll and resize listeners could be assigned here
setTimeout(() => {
// skip current even loop and wait
// the end of parent's render call
if (line.current && end?.current) {
(line.current as any).position();
}
}, 0);
});
return null;
};
</section>
);
+216 -3
View File
@@ -230,6 +230,188 @@ section {
font-weight: 700;
}
.dataflowSection {
margin-bottom: 0;
padding: clamp(4rem, 8vw, 7rem) clamp(1rem, 4vw, 3rem);
overflow: hidden;
background: color-mix(in srgb, var(--ifm-background-color) 88%, var(--ifm-color-primary));
}
.dataflowHeading {
max-width: 58rem;
margin: 0 auto clamp(2rem, 5vw, 4rem);
text-align: center;
}
.dataflowHeading h2 {
margin-bottom: 1rem;
font-size: clamp(2rem, 5vw, 4rem);
line-height: 1.08;
letter-spacing: -0.03em;
text-wrap: balance;
}
.dataflowHeading p {
margin: 0 auto;
max-width: 42rem;
color: color-mix(in srgb, var(--ifm-font-color-base) 76%, transparent);
font-size: clamp(1rem, 2vw, 1.25rem);
text-wrap: pretty;
}
.dataflowDiagram {
position: relative;
width: min(100%, 70rem);
aspect-ratio: 960 / 520;
margin-inline: auto;
}
.dataflowLines {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
}
.dataflowLine,
.dataflowSignal,
.dataflowSignalReverse {
fill: none;
stroke-linecap: round;
}
.dataflowLine {
stroke: color-mix(in srgb, var(--ifm-color-primary) 30%, transparent);
stroke-width: 3;
}
.dataflowSignal,
.dataflowSignalReverse {
stroke: var(--ifm-color-primary);
stroke-width: 7;
stroke-dasharray: 0.035 0.965;
animation: dataflow-signal 2.8s linear infinite;
}
.dataflowSignalReverse {
animation-direction: reverse;
animation-delay: -1.4s;
}
.dataflowNode {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
width: clamp(4.5rem, 10vw, 7.5rem);
padding: clamp(0.6rem, 1.5vw, 1rem);
border-radius: 0.875rem;
color: var(--ifm-font-color-base);
background: var(--ifm-background-surface-color);
box-shadow: 0 6px 12px rgb(31 41 55 / 14%);
transform: translate(-50%, -50%);
}
.dataflowNode img {
width: clamp(2rem, 4.5vw, 3.4rem);
height: clamp(2rem, 4.5vw, 3.4rem);
object-fit: contain;
}
.dataflowMoreIcons {
position: relative;
width: clamp(2rem, 4.5vw, 3.4rem);
aspect-ratio: 1;
}
.dataflowMoreIcons img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
opacity: 0;
animation: dataflow-icon-cycle linear infinite;
}
.dataflowNode span {
overflow: hidden;
width: 100%;
font-size: clamp(0.68rem, 1.4vw, 0.875rem);
font-weight: 700;
line-height: 1.2;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.dataflowNodeTopLeft,
.dataflowNodeMiddleLeft,
.dataflowNodeBottomLeft {
left: 15%;
}
.dataflowNodeTopRight,
.dataflowNodeMiddleRight,
.dataflowNodeBottomRight {
left: 85%;
}
.dataflowNodeTopLeft,
.dataflowNodeTopRight {
top: 16%;
}
.dataflowNodeMiddleLeft,
.dataflowNodeMiddleRight,
.dataflowHub {
top: 50%;
}
.dataflowNodeBottomLeft,
.dataflowNodeBottomRight {
top: 84%;
}
.dataflowHub {
left: 50%;
width: clamp(6rem, 13vw, 9rem);
background: var(--ifm-color-primary);
box-shadow: 0 8px 16px color-mix(in srgb, var(--ifm-color-primary-darkest) 35%, transparent);
}
.dataflowHub img {
width: clamp(3rem, 7vw, 5.4rem);
height: clamp(3rem, 7vw, 5.4rem);
filter: brightness(0) invert(1);
}
.dataflowHub span {
color: #171717;
font-size: clamp(0.8rem, 1.8vw, 1rem);
}
@keyframes dataflow-signal {
to {
stroke-dashoffset: -1;
}
}
@keyframes dataflow-icon-cycle {
0%,
4.8% {
opacity: 1;
transform: scale(1);
}
5.3%,
99% {
opacity: 0;
transform: scale(0.82);
}
}
:global([data-theme="dark"]) .resourceMeterCritical .resourceLabel,
:global([data-theme="dark"]) .containerRowFailed {
color: #ffe3e3;
@@ -280,8 +462,39 @@ section {
}
}
@media (prefers-reduced-motion: reduce) {
.statusDotFailed {
animation: none;
@media screen and (max-width: 640px) {
.dataflowSection {
padding-block: 4rem;
}
.dataflowHeading {
margin-bottom: 2rem;
}
.dataflowDiagram {
aspect-ratio: 4 / 5;
}
.dataflowNode {
width: 4.5rem;
padding: 0.6rem;
}
}
@media (prefers-reduced-motion: reduce) {
.statusDotFailed,
.dataflowSignal,
.dataflowSignalReverse,
.dataflowMoreIcons img {
animation: none;
}
.dataflowSignal,
.dataflowSignalReverse {
display: none;
}
.dataflowMoreIcons img:first-child {
opacity: 1;
}
}
+2 -8
View File
@@ -5,9 +5,9 @@ import Layout from "@theme/Layout";
import HomepageFeatures from "../components/pages/home/features/features";
import HomepageUserReviews from "../components/pages/home/review-list/review-list";
import HomeHero from "../components/pages/home/hero/hero";
import { DataflowVisualizationComponent } from "../components/pages/home/dataflow/dataflow";
import styles from "./index.module.css";
import BrowserOnly from "@docusaurus/BrowserOnly";
import { SectionContainer } from "@site/src/components/pages/home/container/section-container";
import { DragAndDropShowcase } from "@site/src/components/pages/home/drag-and-drop/drag-and-drop-showcase";
import { AvailableIntegrations } from "@site/src/components/pages/home/integrations/available-integrations";
@@ -32,13 +32,7 @@ export default function Home() {
<SectionContainer>
<HomepageFeatures />
</SectionContainer>
<BrowserOnly fallback={<span>loading...</span>}>
{() => {
const DataflowVisualizationComponent =
require("../components/pages/home/dataflow/dataflow").DataflowVisualizationComponent;
return <DataflowVisualizationComponent />;
}}
</BrowserOnly>
<DataflowVisualizationComponent />
<DragAndDropShowcase />
<AvailableIntegrations />
<SectionContainer>