Ready to go?
This commit is contained in:
parent
0712ea6a71
commit
ed1252a668
5
.gitignore
vendored
5
.gitignore
vendored
@ -19,7 +19,4 @@ dist-ssr
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Starbase 80
|
||||
config
|
||||
*.sw?
|
230
package-lock.json
generated
230
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -1,27 +1,25 @@
|
||||
{
|
||||
"name": "sb80",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/react-helmet": "^6.1.6",
|
||||
"js-yaml": "^4.1.0",
|
||||
"prettier": "^2.8.7",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-helmet": "^6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-helmet": "^6.1.6",
|
||||
"@types/react": "^18.0.28",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"js-yaml": "^4.1.0",
|
||||
"postcss": "^8.4.23",
|
||||
"prettier": "^2.8.7",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react": "^18.2.0",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.2.0"
|
||||
|
9
public/icons/home-assistant.svg
Normal file
9
public/icons/home-assistant.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.0 KiB |
BIN
public/icons/router.png
Normal file
BIN
public/icons/router.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
@ -1,146 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "Archivebox",
|
||||
"uri": "https://archivebox.starbase80.dev",
|
||||
"description": "Backup webpages",
|
||||
"icon": {
|
||||
"href": "/icons/archivebox.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authelia",
|
||||
"uri": "https://auth.starbase80.dev",
|
||||
"description": "Authentication",
|
||||
"icon": {
|
||||
"href": "/icons/authelia.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Calibre",
|
||||
"uri": "https://calibre.starbase80.dev",
|
||||
"description": "eBook library",
|
||||
"icon": {
|
||||
"href": "/icons/calibre.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Gitea",
|
||||
"uri": "https://git.starbase80.dev",
|
||||
"description": "Code hosting",
|
||||
"icon": {
|
||||
"href": "/icons/gitea.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Jellyfin",
|
||||
"uri": "https://jellyfin.starbase80.dev",
|
||||
"description": "Media server",
|
||||
"icon": {
|
||||
"href": "/icons/jellyfin.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Mastodon",
|
||||
"uri": "https://notclickable.social",
|
||||
"description": "NotClickable.social",
|
||||
"icon": {
|
||||
"href": "/icons/mastodon.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Mealie",
|
||||
"uri": "https://mealie.starbase80.dev",
|
||||
"description": "Recipe manager",
|
||||
"icon": {
|
||||
"href": "/icons/mealie.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "N8N",
|
||||
"uri": "https://n8n.starbase80.dev",
|
||||
"description": "Workflow automation",
|
||||
"icon": {
|
||||
"href": "/icons/n8n.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "MeTube",
|
||||
"uri": "https://metube.starbase80.dev",
|
||||
"description": "Archive YouTube",
|
||||
"icon": {
|
||||
"href": "/icons/metube.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Miniflux",
|
||||
"uri": "https://miniflux.starbase80.dev",
|
||||
"description": "RSS server",
|
||||
"icon": {
|
||||
"href": "/icons/miniflux.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Portainer",
|
||||
"uri": "https://portainer.starbase80.dev",
|
||||
"description": "Docker management",
|
||||
"icon": {
|
||||
"href": "/icons/portainer.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Roher Twins",
|
||||
"uri": "https://rohertwins.starbase80.dev",
|
||||
"description": "Weekly newsletter",
|
||||
"icon": {
|
||||
"href": "/icons/ghost.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Roher Wiki",
|
||||
"uri": "https://roherwiki.starbase80.dev",
|
||||
"description": "Family wiki",
|
||||
"icon": {
|
||||
"href": "/icons/wikijs.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Standard Notes",
|
||||
"uri": "https://standardnotes.starbase80.dev",
|
||||
"description": "Knowledge base",
|
||||
"icon": {
|
||||
"href": "/icons/standardnotes.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Vaultwarden",
|
||||
"uri": "https://vaultwarden.starbase80.dev",
|
||||
"description": "Password manager",
|
||||
"icon": {
|
||||
"href": "/icons/vaultwarden.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Visual Studio Code",
|
||||
"uri": "https://vscode.starbase80.dev",
|
||||
"description": "Code editor",
|
||||
"icon": {
|
||||
"href": "/icons/vscode.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Wallabag",
|
||||
"uri": "https://wallabag.starbase80.dev",
|
||||
"description": "Read later",
|
||||
"icon": {
|
||||
"href": "/icons/wallabag.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Woodpecker",
|
||||
"uri": "https://woodpecker.starbase80.dev",
|
||||
"description": "Continuous integration",
|
||||
"icon": {
|
||||
"href": "/icons/woodpecker.jpg"
|
||||
}
|
||||
}
|
||||
]
|
20
src/components/service-catalogs.tsx
Normal file
20
src/components/service-catalogs.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import { IServiceCatalog } from "../shared/types";
|
||||
import { Services } from "./services";
|
||||
|
||||
interface IProps {
|
||||
catalogs: IServiceCatalog[];
|
||||
}
|
||||
|
||||
export const ServiceCatalogs: React.FunctionComponent<IProps> = ({ catalogs }) => {
|
||||
return (
|
||||
<ul className="">
|
||||
{catalogs.map((catalog, index) => (
|
||||
<li key={index} className="mt-10 first:mt-0">
|
||||
<h2 className="text-2xl text-slate-800 inline-block py-2 px-4">{catalog.category}</h2>
|
||||
<Services services={catalog.services} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
177
src/config/services.json
Normal file
177
src/config/services.json
Normal file
@ -0,0 +1,177 @@
|
||||
[
|
||||
{
|
||||
"category": "Services",
|
||||
"services": [
|
||||
{
|
||||
"name": "Archivebox",
|
||||
"uri": "https://archivebox.starbase80.dev",
|
||||
"description": "Backup webpages",
|
||||
"icon": {
|
||||
"href": "/icons/archivebox.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authelia",
|
||||
"uri": "https://auth.starbase80.dev",
|
||||
"description": "Authentication",
|
||||
"icon": {
|
||||
"href": "/icons/authelia.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Calibre",
|
||||
"uri": "https://calibre.starbase80.dev",
|
||||
"description": "eBook library",
|
||||
"icon": {
|
||||
"href": "/icons/calibre.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Gitea",
|
||||
"uri": "https://git.starbase80.dev",
|
||||
"description": "Code hosting",
|
||||
"icon": {
|
||||
"href": "/icons/gitea.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Jellyfin",
|
||||
"uri": "https://jellyfin.starbase80.dev",
|
||||
"description": "Media server",
|
||||
"icon": {
|
||||
"href": "/icons/jellyfin.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Mastodon",
|
||||
"uri": "https://notclickable.social",
|
||||
"description": "NotClickable.social",
|
||||
"icon": {
|
||||
"href": "/icons/mastodon.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Mealie",
|
||||
"uri": "https://mealie.starbase80.dev",
|
||||
"description": "Recipe manager",
|
||||
"icon": {
|
||||
"href": "/icons/mealie.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "N8N",
|
||||
"uri": "https://n8n.starbase80.dev",
|
||||
"description": "Workflow automation",
|
||||
"icon": {
|
||||
"href": "/icons/n8n.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "MeTube",
|
||||
"uri": "https://metube.starbase80.dev",
|
||||
"description": "Archive YouTube",
|
||||
"icon": {
|
||||
"href": "/icons/metube.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Miniflux",
|
||||
"uri": "https://miniflux.starbase80.dev",
|
||||
"description": "RSS server",
|
||||
"icon": {
|
||||
"href": "/icons/miniflux.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Portainer",
|
||||
"uri": "https://portainer.starbase80.dev",
|
||||
"description": "Docker management",
|
||||
"icon": {
|
||||
"href": "/icons/portainer.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Roher Twins",
|
||||
"uri": "https://rohertwins.starbase80.dev",
|
||||
"description": "Weekly newsletter",
|
||||
"icon": {
|
||||
"href": "/icons/ghost.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Roher Wiki",
|
||||
"uri": "https://roherwiki.starbase80.dev",
|
||||
"description": "Family wiki",
|
||||
"icon": {
|
||||
"href": "/icons/wikijs.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Standard Notes",
|
||||
"uri": "https://standardnotes.starbase80.dev",
|
||||
"description": "Knowledge base",
|
||||
"icon": {
|
||||
"href": "/icons/standardnotes.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Vaultwarden",
|
||||
"uri": "https://vaultwarden.starbase80.dev",
|
||||
"description": "Password manager",
|
||||
"icon": {
|
||||
"href": "/icons/vaultwarden.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Visual Studio Code",
|
||||
"uri": "https://vscode.starbase80.dev",
|
||||
"description": "Code editor",
|
||||
"icon": {
|
||||
"href": "/icons/vscode.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Wallabag",
|
||||
"uri": "https://wallabag.starbase80.dev",
|
||||
"description": "Read later",
|
||||
"icon": {
|
||||
"href": "/icons/wallabag.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Woodpecker",
|
||||
"uri": "https://woodpecker.starbase80.dev",
|
||||
"description": "Continuous integration",
|
||||
"icon": {
|
||||
"href": "/icons/woodpecker.jpg"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Devices",
|
||||
"services": [
|
||||
{
|
||||
"name": "Router",
|
||||
"uri": "http://192.168.1.1/",
|
||||
"description": "Netgear Orbi",
|
||||
"icon": {
|
||||
"href": "/icons/router.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Home Assistant",
|
||||
"uri": "http://homeassistant.local:8123/",
|
||||
"description": "Home automation",
|
||||
"icon": {
|
||||
"href": "/icons/home-assistant.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Synology",
|
||||
"uri": "http://synology:5000",
|
||||
"description": "Network storage"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -1,8 +1,8 @@
|
||||
import React from "react";
|
||||
import userServices from "../../public/services.json";
|
||||
import { Header } from "../components/header";
|
||||
import { Services } from "../components/services";
|
||||
import { IService } from "../shared/types";
|
||||
import { ServiceCatalogs } from "../components/service-catalogs";
|
||||
import userServices from "../config/services.json";
|
||||
import { IServiceCatalog } from "../shared/types";
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
@ -10,7 +10,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
export const IndexPage: React.FunctionComponent<IProps> = ({ icon, title }) => {
|
||||
const mySerices = userServices as IService[];
|
||||
const mySerices = userServices as IServiceCatalog[];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col xl:flex-row">
|
||||
@ -18,7 +18,7 @@ export const IndexPage: React.FunctionComponent<IProps> = ({ icon, title }) => {
|
||||
<Header title={title} icon={icon} />
|
||||
</div>
|
||||
<div className="min-h-screen p-4 flex-grow">
|
||||
<Services services={mySerices} />
|
||||
<ServiceCatalogs catalogs={mySerices} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
141
src/shared/is.ts
141
src/shared/is.ts
@ -15,147 +15,6 @@ function IsNull(data: any): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function IsDate(data: any): boolean {
|
||||
return data instanceof Date;
|
||||
}
|
||||
|
||||
function IsDateString(data: any): boolean {
|
||||
return !IsNull(data) && data.indexOf("/Date(") === 0;
|
||||
}
|
||||
|
||||
function IsJsonString(data: any): boolean {
|
||||
try {
|
||||
JSON.parse(data);
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function IsNumeric(data: any): boolean {
|
||||
return (
|
||||
typeof data !== "undefined" &&
|
||||
data !== null &&
|
||||
data.toString().length > 0 &&
|
||||
data.toString().replace(/[\d.]+/gi, "").length === 0 &&
|
||||
!isNaN(data)
|
||||
);
|
||||
}
|
||||
|
||||
function IsString(data: any): boolean {
|
||||
return !IsNull(data) && typeof data === "string";
|
||||
}
|
||||
|
||||
function IsValidHttpUrl(data: any): boolean {
|
||||
// Copyright (c) 2010-2013 Diego Perini, MIT licensed
|
||||
// https://gist.github.com/dperini/729294
|
||||
// see also https://mathiasbynens.be/demo/url-regex
|
||||
// modified to allow protocol-relative URLs
|
||||
// and modified again to remove ftp (Jordan)
|
||||
if (IsNull(data)) {
|
||||
return false;
|
||||
}
|
||||
const validUrlRegex =
|
||||
/^(?:(?:(?:https?):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i;
|
||||
|
||||
return validUrlRegex.test(data);
|
||||
}
|
||||
|
||||
function IsFunction(data: any): boolean {
|
||||
return !IsNull(data) && data instanceof Function;
|
||||
}
|
||||
|
||||
const IsReactRoute = (uri: string): boolean => {
|
||||
const reactRouteRegex = /[a-z]{1,4}-[a-z]{1,4}\/r\/[ts]\//gi;
|
||||
|
||||
return !IsNull(uri) && reactRouteRegex.test(uri);
|
||||
};
|
||||
|
||||
const IsArrayDifferent = (array1: any[], array2: any[], useDeepComparison = false): boolean => {
|
||||
const isArray1Null = IsNull(array1);
|
||||
const isArray2Null = IsNull(array2);
|
||||
|
||||
if (isArray1Null && isArray2Null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const shallowResult =
|
||||
isArray1Null !== isArray2Null || (!isArray1Null && !isArray2Null && array1.length !== array2.length);
|
||||
|
||||
if (shallowResult) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!useDeepComparison) {
|
||||
return array1 !== array2;
|
||||
}
|
||||
|
||||
return array1.some((array1Item, index) => {
|
||||
return array2[index] !== array1Item;
|
||||
});
|
||||
};
|
||||
|
||||
const IsInArray = (array: any[], element: any): boolean => {
|
||||
return !IsNull(array) && array.indexOf(element) !== -1;
|
||||
};
|
||||
|
||||
const IsShallowEqual = (obj1: any, obj2: any, ignorePrivateProperties: boolean = false): boolean => {
|
||||
if (is.null(obj1) && is.null(obj2)) {
|
||||
return true;
|
||||
} else if (is.null(obj1) !== is.null(obj2)) {
|
||||
return false;
|
||||
} else if (typeof obj1 !== "object" && typeof obj2 !== "object") {
|
||||
return obj1 === obj2;
|
||||
}
|
||||
|
||||
for (const p in obj1) {
|
||||
if (ignorePrivateProperties && p.startsWith("_")) {
|
||||
continue;
|
||||
}
|
||||
if (obj1.hasOwnProperty(p)) {
|
||||
if (obj1[p] !== obj2[p]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const p in obj2) {
|
||||
if (ignorePrivateProperties && p.startsWith("_")) {
|
||||
continue;
|
||||
}
|
||||
if (obj2.hasOwnProperty(p)) {
|
||||
if (obj1[p] !== obj2[p]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const IsAnythingDifferent = (props: any, nextProps: any, ...keys: string[]): boolean => {
|
||||
return keys.some(key => {
|
||||
if (IsArray(props[key])) {
|
||||
return IsArrayDifferent(props[key], nextProps[key], true);
|
||||
}
|
||||
|
||||
return props[key] !== nextProps[key];
|
||||
});
|
||||
};
|
||||
|
||||
export const is = {
|
||||
array: IsArray,
|
||||
arrayDifferent: IsArrayDifferent,
|
||||
null: IsNull,
|
||||
date: IsDate,
|
||||
dateString: IsDateString,
|
||||
numeric: IsNumeric,
|
||||
string: IsString,
|
||||
validHttpUrl: IsValidHttpUrl,
|
||||
function: IsFunction,
|
||||
reactRoute: IsReactRoute,
|
||||
jsonString: IsJsonString,
|
||||
inArray: IsInArray,
|
||||
shallowEqual: IsShallowEqual,
|
||||
anythingDifferent: IsAnythingDifferent,
|
||||
};
|
||||
|
@ -1,3 +1,8 @@
|
||||
export interface IServiceCatalog {
|
||||
category: string;
|
||||
services: IService[];
|
||||
}
|
||||
|
||||
export interface IService {
|
||||
name: string;
|
||||
uri: string;
|
||||
|
Loading…
Reference in New Issue
Block a user