feat(docs): migrate documentation site into monorepo (#5912)
This commit is contained in:
1 parent
3e464a76f2
commit
9e421c67bd
605 files changed
+26764
-219
No files matched your search
@@ -0,0 +1,31 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Contributor = {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
};
|
||||
|
||||
// do not add any real users here
|
||||
const blacklistedUsernames = ["deepsource-autofix[bot]", "deepsource-io[bot]", "renovate[bot]", "dependabot[bot]"];
|
||||
|
||||
export const CodeContributorList = () => {
|
||||
const [contributors, setContributors] = useState<Contributor[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/data/contributions.json").then(async (response) => {
|
||||
const data = await response.json();
|
||||
setContributors(data.filter((contributor: Contributor) => !blacklistedUsernames.includes(contributor.login)));
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={"flex flex-wrap gap-3 argos-ignore"}>
|
||||
{contributors.map((contributor: Contributor) => (
|
||||
<div className={"flex flex-col items-center w-24"}>
|
||||
<img className={"w-24 h-24 aspect-square rounded mb-2"} src={contributor.avatar_url} alt={""} />
|
||||
<h6 className={"truncate text-nowrap max-w-full"}>{contributor.login}</h6>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Contributor = {
|
||||
username: string;
|
||||
avatarUrl: string;
|
||||
};
|
||||
|
||||
export const TranslationContributorList = () => {
|
||||
const [contributors, setContributors] = useState<Contributor[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/data/translation-contributions.json").then(async (response) => {
|
||||
const data = await response.json();
|
||||
setContributors(data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={"flex flex-wrap gap-3 argos-ignore"}>
|
||||
{contributors.map((contributor: Contributor) => (
|
||||
<div className={"flex flex-col items-center w-24"}>
|
||||
<img className={"w-24 h-24 aspect-square rounded mb-2"} src={contributor.avatarUrl} alt={""} />
|
||||
<h6 className={"truncate text-nowrap max-w-full"}>{contributor.username}</h6>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface SectionContainerProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SectionContainer = ({ children, className }: SectionContainerProps) => {
|
||||
return (
|
||||
<div className={"mx-auto w-full md:w-2/3 ps-10 pr-10 mb-5" + (className ? " " + className : "")}>{children}</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
import React, { useRef } from "react";
|
||||
|
||||
import LeaderLine, { SocketType } from "leader-line-new";
|
||||
|
||||
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);
|
||||
|
||||
return (
|
||||
<div className={"bg-black/[.10] py-20"}>
|
||||
<h2 className={"text-center lg:text-5xl text-3xl font-extrabold mb-24"}>
|
||||
No YAML configurations.
|
||||
<br />
|
||||
Easy and quick to manage integrations.
|
||||
</h2>
|
||||
|
||||
<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}
|
||||
/>
|
||||
|
||||
<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}
|
||||
/>
|
||||
|
||||
<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}
|
||||
/>
|
||||
|
||||
<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>
|
||||
</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;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import videoLight from "./showcase-light.mp4";
|
||||
import videoDark from "./showcase-dark.mp4";
|
||||
import { IconClick } from "@tabler/icons-react";
|
||||
import { SectionContainer } from "@site/src/components/pages/home/container/section-container";
|
||||
import { useColorMode } from "@docusaurus/theme-common";
|
||||
|
||||
export const DragAndDropShowcase = () => {
|
||||
const { colorMode } = useColorMode();
|
||||
const video = colorMode === "dark" ? videoDark : videoLight;
|
||||
return (
|
||||
<div className={"my-24"}>
|
||||
<SectionContainer className={"relative max-w-5xl"}>
|
||||
<h2 className={"lg:text-5xl text-3xl font-extrabold text-center mb-12"}>Easy setup using drag and drop</h2>
|
||||
<div className={"absolute top-0 right-0 translate-x-10 translate-y-8 hidden lg:block"}>
|
||||
<IconClick size={120} />
|
||||
</div>
|
||||
<div
|
||||
className={"rounded-3xl overflow-hidden border-8 border-red-500 border-solid"}
|
||||
style={{ aspectRatio: "17.6/9" }}
|
||||
>
|
||||
<video className={"w-full"} src={video} autoPlay loop muted />
|
||||
</div>
|
||||
</SectionContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,78 @@
|
||||
import React from "react";
|
||||
import {
|
||||
IconAdjustments,
|
||||
IconDragDrop,
|
||||
IconIcons,
|
||||
IconKey,
|
||||
IconLanguage,
|
||||
IconPlug,
|
||||
TablerIcon,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
interface Feature {
|
||||
icon: TablerIcon;
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const featureList: Feature[] = [
|
||||
{
|
||||
icon: IconDragDrop,
|
||||
title: "Easy to use drag and drop system",
|
||||
content:
|
||||
"Using the drag and drop system, you can simply move parts of your dashboard using your mouse or your finger on mobile devices. No YAML / JSON configurations are involved.",
|
||||
},
|
||||
{
|
||||
icon: IconIcons,
|
||||
title: "Over 10K icons available",
|
||||
content:
|
||||
"We integrate with many different icon repositories to provide you with high quality and easy to use images.",
|
||||
},
|
||||
{
|
||||
icon: IconPlug,
|
||||
title: "Seamless integrations",
|
||||
content:
|
||||
"Integrate with your favourite applications to display their status or control them. Scales well with hundreds of users. Robust background job system enables high performance & scalability.",
|
||||
},
|
||||
{
|
||||
icon: IconKey,
|
||||
title: "Authentication & Authorization built in",
|
||||
content: "Support for credentials authentication, OIDC and LDAP. Complex system to manage permissions for users",
|
||||
},
|
||||
{
|
||||
icon: IconLanguage,
|
||||
title: "26 languages available",
|
||||
content: "Accessible for users in many countries thanks to the community translation program",
|
||||
},
|
||||
{
|
||||
icon: IconAdjustments,
|
||||
title: "Detailed settings for customization",
|
||||
content: "Adjust apps and dashboards until you like them with helpful and easy to understand settings",
|
||||
},
|
||||
];
|
||||
|
||||
function FeatureComponent(props: Feature) {
|
||||
return (
|
||||
<div>
|
||||
<div className={"flex flex-nowrap gap-4 items-center mb-2"}>
|
||||
<div className={"rounded-xl bg-gray-100 dark:bg-zinc-800 p-3 pb-0.5 aspect-square"}>
|
||||
<props.icon size={40} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-extrabold text-gray-700 dark:text-gray-300 m-0">{props.title}</h3>
|
||||
<p className="mb-0 text-base text-gray-500 dark:text-gray-400">{props.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomepageFeatures() {
|
||||
return (
|
||||
<div className="mt-6 grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2 mb-32">
|
||||
{featureList.map((props, idx) => (
|
||||
<FeatureComponent key={idx} {...props} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { AppWidget } from "./widgets/app-widget";
|
||||
import { ClockWidget } from "./widgets/clock-widget";
|
||||
import { DownloadsWidget } from "./widgets/downloads-widget";
|
||||
import { WeatherWidget } from "./widgets/weather-widget";
|
||||
import { StockWidget } from "./widgets/stock-widget";
|
||||
import { EntityStateWidget } from "./widgets/entity-state-widget";
|
||||
|
||||
export const HeroCards = () => {
|
||||
return (
|
||||
<div className="argos-ignore hero-cards flex flex-wrap max-w-[504px] gap-y-4 gap-x-3 text-gray-700 dark:text-gray-300">
|
||||
<StockWidget />
|
||||
<AppWidget />
|
||||
<AppWidget className="hidden 3xl:block" />
|
||||
<WeatherWidget />
|
||||
<DownloadsWidget />
|
||||
<EntityStateWidget />
|
||||
<AppWidget />
|
||||
|
||||
<ClockWidget />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import Link from "@docusaurus/Link";
|
||||
import { IconDownload, IconExternalLink } from "@tabler/icons-react";
|
||||
import clsx from "clsx";
|
||||
import styles from "../../../../pages/index.module.css";
|
||||
import { HeroCards } from "./hero-cards";
|
||||
|
||||
export default function HomeHero() {
|
||||
return (
|
||||
<header className={clsx("hero hero--primary", styles.heroBanner, styles.hero)}>
|
||||
<div className="flex">
|
||||
<div className="ps-0 lg:ps-52">
|
||||
<h1 className="font-extrabold lg:text-7xl text-3xl">
|
||||
A simple, yet
|
||||
<br />
|
||||
powerful dashboard
|
||||
<br />
|
||||
for your server.
|
||||
</h1>
|
||||
<p className={"lg:text-2xl text-base"}>
|
||||
A sleek, modern dashboard that puts all of your apps and services at your fingertips. Control everything in
|
||||
one convenient location. Seamlessly integrates with the apps you've added, providing you with valuable
|
||||
information.
|
||||
</p>
|
||||
|
||||
<div className={"flex flex-nowrap gap-2"}>
|
||||
<Link
|
||||
data-attr="Install button"
|
||||
className={"button button--secondary button--lg rounded-3xl dark:border-zinc-600 dark:bg-zinc-800"}
|
||||
to="/docs/getting-started"
|
||||
>
|
||||
<div className={"flex items-center gap-3"}>
|
||||
<span className={"dark:text-gray-200"}>Install</span>
|
||||
<IconDownload className={"dark:text-gray-200"} size={20} />
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
data-attr="Try demo button"
|
||||
className={"button button--secondary button--lg rounded-3xl dark:border-zinc-600 dark:bg-zinc-800"}
|
||||
to="https://demo.homarr.dev/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className={"flex items-center gap-3"}>
|
||||
<span className={"dark:text-gray-200"}>Try Demo</span>
|
||||
<IconExternalLink className={"dark:text-gray-200"} size={20} />
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
data-attr="Redirect to PikaPods"
|
||||
className={`button bg-neutral-900 button--lg rounded-3xl dark:border-zinc-600 dark:bg-zinc-800`}
|
||||
to="https://www.pikapods.com/pods?run=homarr-v1"
|
||||
>
|
||||
<div className={"flex items-center gap-5"}>
|
||||
<img src={"/img/pictures/partner/pika-pods.svg"} alt={"Pika Pods logo"} height={20} />
|
||||
<span className={"dark:text-gray-200"}>Host from $2.3/month</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow-1 w-full lg:block hidden hover-animation">
|
||||
<HeroCards />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { supportedIntegrations } from "@site/src/constants/supported-integrations";
|
||||
import { CommonWidgetProps, WidgetCard } from "./card";
|
||||
import { getRndInteger } from "@site/src/tools/math";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const AppWidget = ({ className }: CommonWidgetProps) => {
|
||||
const randomApp = supportedIntegrations[getRndInteger(0, supportedIntegrations.length - 1)];
|
||||
|
||||
return (
|
||||
<WidgetCard width={1} className={clsx("text-center", className)}>
|
||||
<span className={"text-sm font-bold"}>{randomApp.name}</span>
|
||||
<img
|
||||
src={randomApp.iconUrl}
|
||||
className="aspect-square scale-[0.6] transition-transform"
|
||||
alt={`${randomApp.name} Icon`}
|
||||
/>
|
||||
<div className="absolute bottom-3 right-3 rounded-full bg-green-500 w-2 h-2"></div>
|
||||
</WidgetCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import clsx from "clsx";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export interface CommonWidgetProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface WidgetCardProps extends CommonWidgetProps {
|
||||
width: 1 | 2;
|
||||
children: ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
export const WidgetCard = ({ width, children, className, onClick }: WidgetCardProps) => {
|
||||
const Component = onClick ? "button" : "div";
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={clsx(
|
||||
"rounded-xl flex flex-col justify-center items-center p-2 relative border-none bg-[var(--ifm-navbar-background-color)]",
|
||||
className,
|
||||
onClick ? "cursor-pointer" : undefined,
|
||||
)}
|
||||
style={{
|
||||
width: (128 * width + (width === 2 ? 16 : 0)) * 0.9,
|
||||
height: 128,
|
||||
background: "rgb(from var(--ifm-navbar-background-color) r g b / 80%)",
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import dayjs from "dayjs";
|
||||
import { CommonWidgetProps, WidgetCard } from "./card";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const ClockWidget = ({ className }: CommonWidgetProps) => {
|
||||
return (
|
||||
<WidgetCard width={1} className={clsx("text-center gap-2", className)}>
|
||||
<span className={"font-bold text-xl"}>{dayjs().format("HH:mm")}</span>
|
||||
<span className="text-xs">{dayjs().format("dd, MMM. DD")}</span>
|
||||
</WidgetCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import { getRndInteger } from "@site/src/tools/math";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CommonWidgetProps, WidgetCard } from "./card";
|
||||
import clsx from "clsx";
|
||||
import { generateSlug } from "random-word-slugs";
|
||||
|
||||
export const DownloadsWidget = ({ className }: CommonWidgetProps) => {
|
||||
const [downloads, setDownloads] = useState<Download[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
setDownloads((prev: Download[]) => {
|
||||
const filteredPrev = prev.filter((download) => download.progress !== 100);
|
||||
|
||||
filteredPrev.forEach((download, index) => {
|
||||
download.progress = Math.min(100, download.progress + getRndInteger(0, index < 2 ? 16 : 8));
|
||||
});
|
||||
|
||||
if (filteredPrev.length < 3 && filteredPrev.length === prev.length) {
|
||||
filteredPrev.push(generateRandomDownload());
|
||||
}
|
||||
|
||||
return [...filteredPrev];
|
||||
});
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WidgetCard width={2} className={clsx("overflow-hidden !p-0", className)}>
|
||||
<table className="mb-0 w-full h-full text-xs">
|
||||
<thead className="inline-block w-full">
|
||||
<tr className="inline-block w-full">
|
||||
<th className="border-none px-2 py-1 text-start inline-block w-1/2">File</th>
|
||||
<th className="border-none px-2 py-1 text-start inline-block w-1/2">Progress</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{downloads.map((file: Download, index: number) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="border-none text-nowrap p-2">{file.filename}</td>
|
||||
<td className="border-none p-2 w-full">
|
||||
<div className="overflow-hidden rounded-xl h-2 w-full bg-zinc-600">
|
||||
<div className="bg-green-600 h-full animated-width" style={{ width: `${file.progress}%` }}></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</WidgetCard>
|
||||
);
|
||||
};
|
||||
|
||||
interface Download {
|
||||
filename: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
const formats = ["mkv", "mp4"];
|
||||
|
||||
const generateRandomDownload = (): Download => {
|
||||
const randomMovie = generateSlug(2);
|
||||
const randomFormat = formats[getRndInteger(0, formats.length - 1)];
|
||||
|
||||
return {
|
||||
filename: `${randomMovie}.${randomFormat}`,
|
||||
progress: 0,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useColorMode } from "@docusaurus/theme-common";
|
||||
import { CommonWidgetProps, WidgetCard } from "./card";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const EntityStateWidget = ({ className }: CommonWidgetProps) => {
|
||||
const colorMode = useColorMode();
|
||||
|
||||
return (
|
||||
<WidgetCard
|
||||
width={1}
|
||||
className={clsx("text-center", className)}
|
||||
onClick={() => colorMode.setColorMode(colorMode.isDarkTheme ? "light" : "dark")}
|
||||
>
|
||||
<span className={"text-sm font-bold"}>Lights</span>
|
||||
<span className="text-sm">{colorMode.isDarkTheme ? "OFF" : "ON"}</span>
|
||||
</WidgetCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Line } from "@nivo/line";
|
||||
import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CommonWidgetProps, WidgetCard } from "./card";
|
||||
|
||||
export const StockWidget = ({ className }: CommonWidgetProps) => {
|
||||
const [stockTrend, setStockTrend] = useState<{ x: number; y: number }[]>(generateStockTrend());
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setStockTrend(generateStockTrend());
|
||||
}, 15 * 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const upwardTrend = stockTrend[0].y < stockTrend[stockTrend.length - 1].y;
|
||||
|
||||
return (
|
||||
<WidgetCard width={2} className={clsx("!p-0", className)}>
|
||||
<div className="absolute top-2 left-2 flex gap-1 font-bold items-center">
|
||||
{upwardTrend && <IconTrendingUp size={20} color={"green"} />}
|
||||
{!upwardTrend && <IconTrendingDown size={20} color={"red"} />}
|
||||
HOMR
|
||||
</div>
|
||||
<Line
|
||||
height={128}
|
||||
width={244}
|
||||
colors={[upwardTrend ? "green" : "red"]}
|
||||
data={[
|
||||
{
|
||||
id: "stock",
|
||||
data: stockTrend,
|
||||
},
|
||||
]}
|
||||
curve={"natural"}
|
||||
enablePoints={false}
|
||||
enableGridX={false}
|
||||
enableGridY={false}
|
||||
yScale={{ type: "linear", min: "auto" }}
|
||||
enableArea={true}
|
||||
margin={{ bottom: 0, left: 4, right: 4, top: 40 }}
|
||||
axisBottom={null}
|
||||
axisLeft={null}
|
||||
axisRight={null}
|
||||
axisTop={null}
|
||||
/>
|
||||
<div className="absolute bottom-2 right-2 font-bold">{stockTrend[0].y}</div>
|
||||
</WidgetCard>
|
||||
);
|
||||
};
|
||||
|
||||
const generateStockTrend = () => {
|
||||
const data = [];
|
||||
let y = 100; // Starting price
|
||||
let trend = 1; // 1 for upward trend, -1 for downward trend
|
||||
let trendDuration = Math.floor(Math.random() * 10) + 5; // Random trend duration between 5 and 15
|
||||
|
||||
for (let x = 0; x < 100; x++) {
|
||||
if (trendDuration === 0) {
|
||||
trend = Math.random() > 0.5 ? 1 : -1; // Randomly switch trend
|
||||
trendDuration = Math.floor(Math.random() * 10) + 5;
|
||||
}
|
||||
|
||||
let change = (Math.random() - 0.5) * 2; // Smaller fluctuations
|
||||
y += (change + 0.5) * trend; // Apply trend bias
|
||||
trendDuration--;
|
||||
|
||||
data.push({ x, y: parseFloat(y.toFixed(2)) });
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IconCloud, IconWind, IconArrowUpRight, IconArrowDownRight } from "@tabler/icons-react";
|
||||
import { CommonWidgetProps, WidgetCard } from "./card";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const WeatherWidget = ({ className }: CommonWidgetProps) => {
|
||||
return (
|
||||
<WidgetCard width={1} className={clsx("gap-2", className)}>
|
||||
<div className="flex gap-2 justify-center items-center">
|
||||
<IconCloud size={20} />
|
||||
<span className="text-xl font-bold">10.8°C</span>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-center items-center">
|
||||
<IconWind size={14} />
|
||||
<span className="text-sm">5 km/h</span>
|
||||
</div>
|
||||
<div className="flex gap-1 justify-center items-center">
|
||||
<div className="flex justify-center items-center">
|
||||
<IconArrowUpRight size={14} />
|
||||
<span className="text-sm">13.5°C</span>
|
||||
</div>
|
||||
<div className="flex justify-center items-center">
|
||||
<IconArrowDownRight size={14} />
|
||||
<span className="text-sm">6.4°C</span>
|
||||
</div>
|
||||
</div>
|
||||
</WidgetCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
@keyframes scrolling {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-50.8%);
|
||||
}
|
||||
}
|
||||
|
||||
.scrollAnimationContainer {
|
||||
animation: scrolling;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.scrollAnimationContainer {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { splitToNChunks } from "@site/src/tools/array";
|
||||
import classes from "./available-integrations.module.css";
|
||||
import { SectionContainer } from "@site/src/components/pages/home/container/section-container";
|
||||
import { supportedIntegrations } from "@site/src/constants/supported-integrations";
|
||||
|
||||
const countIconGroups = 3;
|
||||
const animationDurationInSeconds = 12;
|
||||
|
||||
export const AvailableIntegrations = () => {
|
||||
const arrayInChunks = splitToNChunks(
|
||||
supportedIntegrations.map((integration) => integration.iconUrl),
|
||||
countIconGroups,
|
||||
);
|
||||
|
||||
return (
|
||||
<SectionContainer className={"my-24"}>
|
||||
<div className={"md:h-80 w-full dark:bg-neutral-900 p-10 rounded-3xl overflow-hidden relative"}>
|
||||
<div className={"flex gap-10 flex-nowrap"}>
|
||||
<div className={"md:w-1/2 w-full"}>
|
||||
<h2 className={"lg:text-5xl text-3xl font-extrabold "}>Many integrations built in</h2>
|
||||
<p className={"text-xl text-gray-500 dark:text-gray-400"}>
|
||||
Homarr has support for tons of your favourite applications, tools and websites. It integrates seamlessly
|
||||
and tests proper connectivity and configuration for you. Using the tasks system, it scales efficiently
|
||||
with tons of users, making Homarr reliable in big scale deployments too.
|
||||
</p>
|
||||
</div>
|
||||
<div className={"rotate-12 w-1/2 hidden md:block argos-ignore"}>
|
||||
<div className={"grid gap-12 grid-cols-3"}>
|
||||
{Array(countIconGroups)
|
||||
.fill(0)
|
||||
.map((_, columnIndex) => (
|
||||
<div key={`grid-column-${columnIndex}`} style={{ width: 50 }}>
|
||||
<div
|
||||
className={classes.scrollAnimationContainer}
|
||||
style={{
|
||||
animationDuration: `${animationDurationInSeconds - columnIndex}s`,
|
||||
}}
|
||||
>
|
||||
{arrayInChunks[columnIndex]?.map((icon, index) => (
|
||||
<img
|
||||
className={"rounded"}
|
||||
key={`grid-column-${columnIndex}-scroll-1-${index}`}
|
||||
src={icon}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* This is used for making the animation seem seamless */}
|
||||
{arrayInChunks[columnIndex]?.map((icon, index) => (
|
||||
<img
|
||||
className={"rounded"}
|
||||
key={`grid-column-${columnIndex}-scroll-2-${index}`}
|
||||
src={icon}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
import React from "react";
|
||||
|
||||
const reviews = [
|
||||
{
|
||||
username: "Maeglin",
|
||||
profilePicture: "/img/pictures/reviews/maeglin.webp",
|
||||
fullText:
|
||||
"Day 49/100 set up Homarr just for a test spin. It's a nice dashboard and the Arrs integration especialy the calendar is awesome.",
|
||||
link: "https://twitter.com/Maeglin931/status/1555725442744586240",
|
||||
},
|
||||
{
|
||||
username: "Noted",
|
||||
profilePicture: "https://noted.lol/content/images/size/w256h256/2023/08/Icon-1.2-1.png",
|
||||
fullText: "Homarr - A Simple, Self Hosted and Lightweight Server Homepage",
|
||||
link: "https://noted.lol/homarr-a-simple-self-hosted-and-lightweight-server-homepage/",
|
||||
},
|
||||
{
|
||||
username: "IBRACORP",
|
||||
profilePicture: "/img/pictures/reviews/ibracorp.webp",
|
||||
fullText: "Homarr Is Here To Stay. Here's Why | Selfhosted Homepage",
|
||||
link: "https://www.youtube.com/watch?v=Mk9ZZiH5qi0",
|
||||
},
|
||||
{
|
||||
username: "The Geek Freaks",
|
||||
profilePicture:
|
||||
"https://yt3.ggpht.com/6EWnfiBIE7Bus2MAS6zJsBARQTPWE7B3v7PXdwX1OfohaxylMqdQr-vt4j8gYZTJy2MBiOvu=s500-c-k-c0x00ffffff-no-rj",
|
||||
fullText: "MEHR Durchblick mit eigenem Dashboard! (Unraid Homarr Tutorial) ",
|
||||
link: "https://www.youtube.com/watch?v=DNGRL5QdBlg",
|
||||
},
|
||||
|
||||
{
|
||||
username: "u/uncmnsense",
|
||||
profilePicture: "/img/pictures/reviews/r_selfhosted.webp",
|
||||
fullText: "Anyone using homarr? check it out, its pretty fancy...",
|
||||
link: "https://www.reddit.com/r/selfhosted/comments/wqxsk3/anyone_using_homarr_check_it_out_its_pretty_fancy",
|
||||
},
|
||||
{
|
||||
username: "TechHut",
|
||||
profilePicture:
|
||||
"https://yt3.ggpht.com/TUoF-6QCUIKy6XgFtMG5FWi5FLVhtaUPtTOLvE7Ca3eJif1_RKBci07fKK-QvKxhC0HALEBH7Q=s500-c-k-c0x00ffffff-no-rj",
|
||||
fullText: "my FAVORITE Home Server Dashboard - Homarr Setup in Docker",
|
||||
link: "https://www.youtube.com/watch?v=A6vcTIzp_Ww",
|
||||
},
|
||||
{
|
||||
username: "u/RoachedCoach",
|
||||
profilePicture: "/img/pictures/reviews/r_unraid.webp",
|
||||
fullText: "I enjoy Homarr - mostly because it's very simple, straightforward, and attractive.",
|
||||
link: "https://www.reddit.com/r/unRAID/comments/wk3x2s/comment/ijn4vpg/",
|
||||
},
|
||||
{
|
||||
username: "Mariushosting",
|
||||
link: "https://mariushosting.com/synology-install-homarr-with-portainer/",
|
||||
fullText:
|
||||
"The thing I like most about Homarr is its speed and simplicity in adding links of your favorite docker apps. You can use it as a bookmark as well. Great integration with Plex, Sonarr, Radarr etc. Homarr supports multiple configs and persistent storage.",
|
||||
profilePicture: "https://mariushosting.com/wp-content/uploads/2021/05/cropped-mariushosting512-192x192.png",
|
||||
},
|
||||
{
|
||||
username: "u/YankeesIT",
|
||||
profilePicture: "/img/pictures/reviews/r_selfhosted.webp",
|
||||
fullText: "My Homarr setup...",
|
||||
link: "https://www.reddit.com/r/selfhosted/comments/x84c9v/my_homarr_setup/",
|
||||
},
|
||||
{
|
||||
username: "Selfhosted Corner",
|
||||
profilePicture: "https://pbs.twimg.com/profile_banners/1674813613650120705/1691340056/600x200",
|
||||
fullText:
|
||||
"So you have many different #Selfhosted apps and you are having trouble organizing them? Fear not cause homarr is here is to save you the hassle.",
|
||||
link: "https://twitter.com/SlhstdCorner/status/1698674826351685648",
|
||||
},
|
||||
];
|
||||
|
||||
export default function HomepageUserReviews() {
|
||||
return (
|
||||
<div className="my-24">
|
||||
<h2 className="lg:text-5xl text-3xl font-extrabold text-center mb-10">Community</h2>
|
||||
<p className={"text-xl text-gray-500 dark:text-gray-400 max-w-128 mx-auto mb-3"}>
|
||||
We have a big and active community, which regularly contributes new features & integrations to Homarr. Our
|
||||
community translation program and modularity end extendability in the code makes it easy to contribute and
|
||||
extend Homarr.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-2 xl:gap-x-8">
|
||||
{reviews.map((review) => (
|
||||
<article
|
||||
key={review.link}
|
||||
className="bg-gray-100 hover:bg-gray-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 rounded-2xl"
|
||||
>
|
||||
<a
|
||||
className="hover:no-underline h-full block p-5"
|
||||
href={review.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-attr="Open review"
|
||||
>
|
||||
<div className="flex space-x-4">
|
||||
<img className="w-24 h-24 rounded-full dark:bg-zinc-900" src={review.profilePicture} alt="profile" />
|
||||
<div className="space-y-1 font-medium dark:text-white">
|
||||
<p className="text--bold m-0">{review.username}</p>
|
||||
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm m-0">{review.fullText}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.swiperText {
|
||||
text-align: center;
|
||||
font-size: 2.7rem;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.communityFeedbackPlaceholderImage {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.communityFeedbackPlaceholderText {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type React from "react";
|
||||
import CodeBlock from "@theme/CodeBlock";
|
||||
import Admonition from "@theme/Admonition";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const generateRandomHex = (length = 64): string => {
|
||||
const array = new Uint8Array(length / 2);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
};
|
||||
|
||||
export const DockerInstallSnippet: React.FC = () => {
|
||||
const [randomHex, setRandomHex] = useState<string>("");
|
||||
const generateNewHex = () => {
|
||||
setRandomHex(generateRandomHex());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
generateNewHex();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
First, create a <code>docker-compose.yml</code> file with the following content:
|
||||
</p>
|
||||
<CodeBlock language="yml" title="docker-compose.yml" className={"argos-ignore"}>
|
||||
{`#---------------------------------------------------------------------#
|
||||
# Homarr - A simple, yet powerful dashboard for your server. #
|
||||
#---------------------------------------------------------------------#
|
||||
services:
|
||||
homarr:
|
||||
container_name: homarr
|
||||
image: ghcr.io/homarr-labs/homarr:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock # Optional, only if you want docker integration
|
||||
- ./homarr/appdata:/appdata
|
||||
environment:
|
||||
- SECRET_ENCRYPTION_KEY=${randomHex}
|
||||
ports:
|
||||
- '7575:7575'
|
||||
`}
|
||||
</CodeBlock>
|
||||
<Admonition type="info">
|
||||
Key provided above for the <code>SECRET_ENCRYPTION_KEY</code> environment variable is randomly generated using
|
||||
your browser cryotography API. It will be different every time You can generate one yourself using{" "}
|
||||
<code>openssl rand -hex 32</code>
|
||||
<button className="px-1 mx-4" onClick={generateNewHex}>
|
||||
Refresh key
|
||||
</button>
|
||||
</Admonition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user