Compare commits

..

26 Commits

Author SHA1 Message Date
Ben Phelps
d94b3e829d update readme attributions 2022-09-03 13:15:17 +03:00
Ben Phelps
10c63939dc cleanup search widget 2022-09-03 13:12:09 +03:00
Ben Phelps
972ede9249 fix mismatched labels 2022-09-03 12:40:15 +03:00
Ben Phelps
8f001ad88a tweak mobile layouts for widgets 2022-09-03 12:40:04 +03:00
Ben Phelps
229c5dac59 Merge pull request #45 from aidenpwnz/main
Feature/Searchbar
2022-09-02 21:15:36 +03:00
aidenpwnz
0622395ec7 FIX: leftover 2022-09-02 16:54:42 +02:00
Luca Pellegrino
66073ed460 Merge pull request #1 from aidenpwnz/feature/npm
Feature/npm
2022-09-02 16:52:46 +02:00
aidenpwnz
533f40b536 Merge remote-tracking branch 'origin' into feature/npm 2022-09-02 16:49:46 +02:00
aidenpwnz
13afe82fa5 FEAT: NGINX Proxy Manager 2022-09-02 16:48:28 +02:00
Luca Pellegrino
10c27dfd84 Merge branch 'benphelps:main' into main 2022-09-02 12:16:25 +02:00
aidenpwnz
057d5eca8f FEAT: NGINX Proxy Manager 2022-09-02 12:13:15 +02:00
Ben Phelps
e89f3668a9 Merge pull request #46 from quod/main
Fix typos
2022-09-02 11:03:18 +03:00
Ben Phelps
c46306fc1d allow services to be display only 2022-09-02 10:55:19 +03:00
Benjamin Carson
76d534583b Fix typos 2022-09-01 17:24:08 -05:00
aidenpwnz
7b4f360a5e FIX: minor issue with abbr 2022-09-01 19:30:15 +02:00
aidenpwnz
992b18c9de FEAT: Searchbar 2022-09-01 19:21:44 +02:00
aidenpwnz
6291a5422a FIX: overflows 2022-09-01 19:13:51 +02:00
aidenpwnz
4581c4eeb0 FEAT: Searchbar || FIX: spacings, overflows 2022-09-01 19:11:45 +02:00
Ben Phelps
d6d93e3c03 add Jellyseerr attribution 2022-08-29 18:41:36 +03:00
Ben Phelps
f40ca1e25c Merge pull request #34 from ilusi0n/jellyseerr-integration
Implement Jellyseerr integration
2022-08-28 14:03:18 +03:00
ilusi0n
2d764ce59b update readme 2022-08-28 11:27:45 +01:00
ilusi0n
a1841f26bb merge with main 2022-08-28 11:26:29 +01:00
ilusi0n
c4ab3eb992 add jellyseerr integration 2022-08-28 11:22:43 +01:00
Ben Phelps
617cbcaee1 fix docker widget when network_mode is host 2022-08-28 10:15:25 +03:00
Ben Phelps
a9a28e14df switch to using the latest tag 2022-08-28 00:53:23 +03:00
Ben Phelps
169c64f687 attempt to add latest docker tag 2022-08-28 00:17:18 +03:00
15 changed files with 248 additions and 28 deletions

View File

@@ -79,6 +79,8 @@ jobs:
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=auto
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action

View File

@@ -9,11 +9,12 @@
- Docker Integration
- Status light + CPU, Memory & Network Reporting *(click on the status light)*
- Service Integration
- Currently supports Sonarr, Radarr, Ombi, Emby, Jellyfin, NZBGet, ruTorrent
- Portainer, Traefik, Speedtest Tracker, PiHole
- Currently supports Sonarr, Radarr, Ombi, Emby, Jellyfin, Jellyseerr ([by ilusi0n](https://github.com/benphelps/homepage/pull/34)), NZBGet, ruTorrent
- Portainer, Traefik, Speedtest Tracker, PiHole, Nginx Proxy Manager ([by aidenpwnz](https://github.com/benphelps/homepage/pull/45))
* Homepage Widgets
- System Stats (Disk, CPU, Memory)
- Weather via WeatherAPI.com or OpenWeatherMap ([thanks to AlexFullmoon](https://github.com/benphelps/homepage/pull/25))
- Weather via WeatherAPI.com or OpenWeatherMap ([by AlexFullmoon](https://github.com/benphelps/homepage/pull/25))
- Search Bar ([by aidenpwnz](https://github.com/benphelps/homepage/pull/45))
* Customizable
- 21 theme colors with light and dark mode support
@@ -35,7 +36,7 @@ Using docker compose:
version: '3.3'
services:
homepage:
image: ghcr.io/benphelps/homepage:main
image: ghcr.io/benphelps/homepage:latest
container_name: homepage
ports:
- 3000:3000
@@ -47,7 +48,7 @@ services:
or docker run:
```bash
docker run -p 3000:3000 -v /path/to/config:/app/config -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/benphelps/homepage:main
docker run -p 3000:3000 -v /path/to/config:/app/config -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/benphelps/homepage:latest
```
### With Node

View File

@@ -21,14 +21,19 @@ function resolveIcon(icon) {
export default function Item({ service }) {
return (
<li key={service.name} className="">
<li key={service.name}>
<Disclosure>
<div className="transition-all h-15 overflow-hidden mb-3 cursor-pointer p-1 rounded-md font-medium text-theme-700 hover:text-theme-800 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/40 bg-white/50 hover:bg-theme-300/10 dark:bg-white/5 dark:hover:bg-white/10">
<div className={
(service.href && service.href !== "#" ? 'cursor-pointer ' : 'cursor-default ') +
'transition-all h-15 overflow-hidden mb-3 p-1 rounded-md font-medium text-theme-700 hover:text-theme-800 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/40 bg-white/50 hover:bg-theme-300/10 dark:bg-white/5 dark:hover:bg-white/10'
}>
<div className="flex">
{service.icon && (
<div
onClick={() => {
window.open(service.href, "_blank").focus();
if (service.href && service.href !== "#") {
window.open(service.href, "_blank").focus();
}
}}
className="flex-shrink-0 flex items-center justify-center w-12 "
>
@@ -38,7 +43,9 @@ export default function Item({ service }) {
<div
onClick={() => {
window.open(service.href, "_blank").focus();
if (service.href && service.href !== "#") {
window.open(service.href, "_blank").focus();
}
}}
className="flex-1 flex items-center justify-between rounded-r-md "
>
@@ -48,7 +55,7 @@ export default function Item({ service }) {
</div>
</div>
{service.container && (
<Disclosure.Button as="div" className="flex-shrink-0 flex items-center justify-center w-12 ">
<Disclosure.Button as="div" className="flex-shrink-0 flex items-center justify-center w-12 cursor-pointer">
<Status service={service} />
</Disclosure.Button>
)}

View File

@@ -10,6 +10,8 @@ import Rutorrent from "./widgets/service/rutorrent";
import Jellyfin from "./widgets/service/jellyfin";
import Speedtest from "./widgets/service/speedtest";
import Traefik from "./widgets/service/traefik";
import Jellyseerr from "./widgets/service/jellyseerr";
import Npm from "./widgets/service/npm";
const widgetMappings = {
docker: Docker,
@@ -24,6 +26,8 @@ const widgetMappings = {
rutorrent: Rutorrent,
speedtest: Speedtest,
traefik: Traefik,
jellyseerr: Jellyseerr,
npm: Npm,
};
export default function Widget({ service }) {

View File

@@ -49,8 +49,12 @@ export default function Docker({ service }) {
<Widget>
<Block label="CPU" value={`${calculateCPUPercent(statsData.stats)}%`} />
<Block label="MEM" value={formatBytes(statsData.stats.memory_stats.usage, 0)} />
<Block label="RX" value={formatBytes(statsData.stats.networks.eth0.rx_bytes, 0)} />
<Block label="TX" value={formatBytes(statsData.stats.networks.eth0.tx_bytes, 0)} />
{statsData.stats.networks && (
<>
<Block label="RX" value={formatBytes(statsData.stats.networks.eth0.rx_bytes, 0)} />
<Block label="TX" value={formatBytes(statsData.stats.networks.eth0.tx_bytes, 0)} />
</>
)}
</Widget>
);
}

View File

@@ -0,0 +1,51 @@
import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
export default function Jellyseerr({ service }) {
const config = service.widget;
function buildApiUrl(endpoint) {
const { url } = config;
const reqUrl = new URL(`/api/v1/${endpoint}`, url);
return `/api/proxy?url=${encodeURIComponent(reqUrl)}`;
}
const fetcher = async (url) => {
const res = await fetch(url, {
method: "GET",
withCredentials: true,
credentials: "include",
headers: {
"X-Api-Key": `${config.key}`,
"Content-Type": "application/json"
}
});
return await res.json();
};
const { data: statsData, error: statsError } = useSWR(buildApiUrl(`request/count`), fetcher);
if (statsError) {
return <Widget error="Jellyseerr API Error" />;
}
if (!statsData) {
return (
<Widget>
<Block label="Pending" />
<Block label="Approved" />
<Block label="Available" />
</Widget>
);
}
return (
<Widget>
<Block label="Pending" value={statsData.pending} />
<Block label="Approved" value={statsData.approved} />
<Block label="Available" value={statsData.available} />
</Widget>
);
}

View File

@@ -0,0 +1,65 @@
import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
export default function Npm({ service }) {
const config = service.widget;
const { url } = config;
const fetcher = async (reqUrl) => {
const { url, username, password } = config;
const loginUrl = `${url}/api/tokens`;
const body = { identity: username, secret: password };
const res = await fetch(loginUrl, {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then(
async (data) =>
await fetch(reqUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + data.token,
},
})
);
return res.json();
};
const { data: infoData, error: infoError } = useSWR(`${url}/api/nginx/proxy-hosts`, fetcher);
console.log(infoData);
if (infoError) {
return <Widget error="NGINX Proxy Manager API Error" />;
}
if (!infoData) {
return (
<Widget>
<Block label="Enabled" />
<Block label="Disabled" />
<Block label="Total" />
</Widget>
);
}
const enabled = infoData.filter((c) => c.enabled === 1).length;
const disabled = infoData.filter((c) => c.enabled === 0).length;
const total = infoData.length;
return (
<Widget>
<Block label="Enabled" value={enabled} />
<Block label="Disabled" value={disabled} />
<Block label="Total" value={total} />
</Widget>
);
}

View File

@@ -1,12 +1,14 @@
import WeatherApi from "components/widgets/weather/weather";
import OpenWeatherMap from "components/widgets/openweathermap/weather";
import Resources from "components/widgets/resources/resources";
import Search from "components/widgets/search/search";
const widgetMappings = {
weather: WeatherApi, // This key will be deprecated in the future
weatherapi: WeatherApi,
openweathermap: OpenWeatherMap,
resources: Resources,
search: Search,
};
export default function Widget({ widget }) {

View File

@@ -33,7 +33,7 @@ export default function OpenWeatherMap({ options }) {
}
return (
<div className="flex flex-col">
<div className="flex flex-col justify-center">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<Icon

View File

@@ -5,11 +5,11 @@ import Memory from "./memory";
export default function Resources({ options }) {
return (
<>
<div className="pr-2 flex flex-col">
<div className="flex flex-row space-x-4">
{options.disk && <Disk options={options} />}
<div className="flex flex-col max-w:full basis-1/2 sm:basis-auto self-center">
<div className="flex flex-row space-x-4 self-center">
{options.cpu && <Cpu />}
{options.memory && <Memory />}
{options.disk && <Disk options={options} />}
</div>
{options.label && (
<div className="border-t-2 border-theme-800 dark:border-theme-200 mt-1 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">

View File

@@ -0,0 +1,69 @@
import { useState } from "react";
import { FiSearch } from "react-icons/fi";
import { SiDuckduckgo, SiMicrosoftbing, SiGoogle } from "react-icons/si";
const providers = {
google: {
name: "Google",
url: "https://www.google.com/search?q=",
icon: SiGoogle,
},
duckduckgo: {
name: "DuckDuckGo",
url: "https://duckduckgo.com/?q=",
icon: SiDuckduckgo,
},
bing: {
name: "Bing",
url: "https://www.bing.com/search?q=",
icon: SiMicrosoftbing,
},
custom: {
name: "Custom",
url: false,
icon: FiSearch,
},
};
export default function Search({ options }) {
const provider = providers[options.provider];
const [query, setQuery] = useState("");
if (!provider) {
return <></>;
}
function handleSubmit(event) {
const q = encodeURIComponent(query);
if (provider.url) {
window.open(`${provider.url}${q}`, options.target || "_blank");
} else {
window.open(`${options.url}${q}`, options.target || "_blank");
}
event.preventDefault();
event.target.reset();
setQuery("");
}
return (
<form className="flex-col relative h-8 my-4 min-w-full md:min-w-fit grow" onSubmit={handleSubmit}>
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-theme-200"></div>
<input
type="search"
autoFocus
className={`overflow-hidden w-full placeholder-theme-900 text-xs text-theme-900 bg-theme-50 rounded-md border border-theme-300 focus:ring-theme-500 focus:border-theme-500 dark:bg-theme-800 dark:border-theme-600 dark:placeholder-theme-400 dark:text-white dark:focus:ring-theme-500 dark:focus:border-theme-500 h-full`}
placeholder="Search..."
onChange={(s) => setQuery(s.currentTarget.value)}
required
/>
<button
type="submit"
className="text-white absolute right-0.5 bottom-0.5 bg-theme-700 hover:bg-theme-800 border-1 focus:ring-2 focus:ring-theme-300 font-medium rounded-r-md text-sm px-4 py-2 dark:bg-theme-600 dark:hover:bg-theme-700 dark:focus:ring-theme-500"
>
<provider.icon className="text-theme-800 dark:text-theme-200 w-3 h-3" />
</button>
</form>
);
}

View File

@@ -33,7 +33,7 @@ export default function WeatherApi({ options }) {
}
return (
<div className="flex flex-col">
<div className="flex flex-col justify-center">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-col items-center">
<Icon condition={data.current.condition.code} timeOfDay={data.current.is_day ? "day" : "night"} />

View File

@@ -8,6 +8,7 @@ import ServicesGroup from "components/services/group";
import BookmarksGroup from "components/bookmarks/group";
import Widget from "components/widget";
import { ColorProvider } from "utils/color-context";
import Search from "components/widgets/search/search";
const ThemeToggle = dynamic(() => import("components/theme-toggle"), {
ssr: false,
@@ -17,7 +18,8 @@ const ColorToggle = dynamic(() => import("components/color-toggle"), {
ssr: false,
});
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather"];
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "search"];
const expandedWidgets = ["search"];
export default function Home() {
const { data: services, error: servicesError } = useSWR("/api/services");
@@ -31,7 +33,7 @@ export default function Home() {
<title>Welcome</title>
</Head>
<div className="w-full container m-auto flex flex-col h-screen justify-between">
<div className="flex flex-wrap space-x-4 m-8 pb-4 mt-10 border-b-2 border-theme-800 dark:border-theme-200">
<div className="flex flex-row flex-wrap space-x-0 sm:space-x-4 m-8 pb-4 mt-10 border-b-2 border-theme-800 dark:border-theme-200 justify-between md:justify-start">
{widgets && (
<>
{widgets
@@ -39,12 +41,14 @@ export default function Home() {
.map((widget, i) => (
<Widget key={i} widget={widget} />
))}
<div className="grow"></div>
{widgets
.filter((widget) => rightAlignedWidgets.includes(widget.type))
.map((widget, i) => (
<Widget key={i} widget={widget} />
))}
<div className="flex flex-wrap basis-full space-x-0 sm:space-x-4 grow sm:basis-auto justify-between md:justify-end mt-2 md:mt-0">
{widgets
.filter((widget) => rightAlignedWidgets.includes(widget.type))
.map((widget, i) => (
<Widget key={i} widget={widget} />
))}
</div>
</>
)}
</div>

View File

@@ -3,15 +3,20 @@
- My First Group:
- My First Service:
href: http://localhosdt/
href: http://localhost/
description: Homepage is awesome
# widget:
# type: npm # npm for NGINX Proxy Manager
# url: http://localhost # no slash at the end
# username: email@example.com # your email
# password: secretpassword # your password
- My Second Group:
- My Second Service:
href: http://localhosdt/
href: http://localhost/
description: Homepage is the best
- My Third Group:
- My Third Service:
href: http://localhosdt/
href: http://localhost/
description: Homepage is 😎

View File

@@ -5,3 +5,9 @@
cpu: true
memory: true
disk: /
# - search: # Searchbar in widgets area
# provider: custom # Can be google, duckduckgo, bing or custom.
# target: _blank # Can be _blank, _top, _self or _parent.
# customdata:
# url: https://startpage.com/search?q= # Required for custom provider. Remember to add the q param as per your provider.
# abbr: G # Can be omitted. Only the first 2 characters will be considered.