Merge pull request #2 from notclickable-jordan/v1.1

Version 1.1
This commit is contained in:
Jordan Roher 2023-04-21 16:48:26 -07:00 committed by GitHub
commit e1f348e2bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 377 additions and 76 deletions

3
.gitignore vendored
View File

@ -2,4 +2,5 @@ node_modules
.DS_Store
dist
dist-ssr
src/config.json
src/config.json
public

View File

@ -1,15 +1,21 @@
#!/bin/sh
echo "Replacing HTMLTITLE with ${title} in /app/index.html"
sed -i -e 's/HTMLTITLE/'"${TITLE}"'/g' /app/index.html
echo "Replacing HTMLTITLE with ${title} in /app/src/main.tsx"
sed -i -e 's/HTMLTITLE/'"${TITLE}"'/g' /app/src/main.tsx
# Escape slashes... apparently
# Escape slashes
LOGO=${LOGO//\//\\/}
echo "Replacing LOGO with ${LOGO} in /app/src/main.tsx"
sed -i -e 's/LOGO/'"${LOGO}"'/g' /app/src/main.tsx
# HTML replacement
sed -i -e 's/HTMLTITLE/'"${TITLE}"'/g' /app/index.html
# TypeScript replacement
sed -i -e 's/PAGETITLE = "My Website"/PAGETITLE = "'"${TITLE}"'"/g' /app/src/variables.ts
sed -i -e 's/PAGEICON = "\/logo\.png"/PAGEICON = "'"${LOGO}"'"/g' /app/src/variables.ts
sed -i -e 's/SHOWHEADER = true/SHOWHEADER = '"${HEADER}"'/g' /app/src/variables.ts
sed -i -e 's/SHOWHEADERLINE = true/SHOWHEADERLINE = '"${HEADERLINE}"'/g' /app/src/variables.ts
sed -i -e 's/SHOWHEADERTOP = false/SHOWHEADERTOP = '"${HEADERTOP}"'/g' /app/src/variables.ts
sed -i -e 's/CATEGORIES = "normal"/CATEGORIES = "'"${CATEGORIES}"'"/g' /app/src/variables.ts
# CSS replacement
sed -i -e 's/background-color: theme(\(colors\.slate\.50\))/background-color: '"${BGCOLOR}"'/g' /app/src/tailwind.css
sed -i -e 's/background-color: theme(\(colors\.gray\.950\))/background-color: '"${BGCOLORDARK}"'/g' /app/src/tailwind.css
exec "$@"

View File

@ -11,6 +11,12 @@ ENV NODE_ENV production
ENV TITLE "My Website"
ENV LOGO "/logo.png"
ENV HEADER "true"
ENV HEADERLINE "true"
ENV HEADERTOP "false"
ENV CATEGORIES "normal"
ENV BGCOLOR "theme(colors.slate.50)"
ENV BGCOLORDARK "theme(colors.gray.950)"
EXPOSE 4173

8
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "sb80",
"version": "1.0.0",
"name": "starbase-80",
"version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sb80",
"version": "1.0.0",
"name": "starbase-80",
"version": "1.0.1",
"dependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",

120
readme.md
View File

@ -6,15 +6,71 @@
# About
<img src="./preview.jpg" alt="" />
A nice looking homepage for Docker containers or any services and links.
No actual integration with Docker. Loads instantly. No dark theme.
No actual integration with Docker. Loads instantly. Dark mode follows your OS.
If you make a change to the config JSON, restart this container and refresh.
Provide your own icons.
Inspired by [Ben Phelps' Homepage](https://gethomepage.dev/) and [Umbrel](https://umbrel.com/).
<img src="./preview.jpg" alt="" />
# Icons
## Use your own
Create a volume or bind mount to a subfolder of `/app/public` and specify a relative path.
```bash
# Your folder
compose.yml
- icons
- jellyfin.jpg
- ghost.jpg
- etc
# Bind mount
./icons:/app/public/icons
# Specify an icon in config.json
"icon": "/icons/jellyfin.jpg"
```
## Dashboard icons
Use [Dashboard icons](https://github.com/walkxcode/dashboard-icons) by specifying a name without any prefix.
```bash
# Specify an icon in config.json
"icon": "jellyfin"
```
## Material design
Use any [Material Design icon](https://icon-sets.iconify.design/mdi/) by prefixing the name with `mdi-`.
Fill the icon by providing an "iconColor" from the [list of Tailwind colors](https://tailwindcss.com/docs/background-color). Do not prefix with "bg-".
Use "black" or "white" for those colors.
```bash
# Specify an icon in config.json
"icon": "mdi-cloud",
"iconColor": "black"
```
## Options
```bash
# Specify an icon in config.json
"icon": "/icons/jellyfin.jpg", # mostly required, but if set to "" it removes the icon
"iconColor": "blue-500", # optional, defaults to a contrasting color
"iconBG": "gray-200", # optional, defaults to a complementary color
"iconBubble": false, # optional, defaults to true, turns off bubble and shadow when false
```
For `iconColor` and `iconBG`, use a [Tailwind color](https://tailwindcss.com/docs/background-color). Turn off background color with a value of `"transparent"`. Do not prefix with `"bg-"`.
# Docker compose
@ -25,8 +81,14 @@ services:
ports:
- 4173:4173
environment:
- TITLE=My Homepage
- LOGO=/logo.png
- TITLE=Starbase 80 # defaults to "My Website", set to TITLE= to hide the title
- LOGO=/starbase80.jpg # defaults to /logo.png, set to LOGO= to hide the logo
- HEADER=true # defaults to true, set to false to hide the title and logo
- HEADERLINE=true # defaults to true, set to false to turn off the header border line
- HEADERTOP=true # defaults to false, set to true to force the header to always stay on top
- CATEGORIES=small # defaults to normal, set to small for smaller, uppercase category labels
- BGCOLOR=#fff # defaults to theme(colors.slate.50), set to any hex color or Tailwind color using the theme syntax
- BGCOLORDARK=#000 # defaults to theme(colors.gray.950), set to any hex color or Tailwind color using the theme syntax
volumes:
- ./config.json:/app/src/config.json # required
- ./public/favicon.ico:/app/public/favicon.ico # optional
@ -36,6 +98,48 @@ services:
# config.json format
## Categories
Can have as many categories as you like.
- **category**: Title, optional, displays above services
- **bubble**: boolean, optional, defaults to false, shows a bubble around category
- **services**: Array of services
## Service
- **name**: Name, required
- **uri**: Hyperlink, required
- **description**: 2-3 words, optional
- **icon**: optional, relative URI, absolute URI, service name ([Dashboard icon](https://github.com/walkxcode/dashboard-icons)) or `mdi-`service name ([Material Design icon](https://icon-sets.iconify.design/mdi/))
- **iconBG**: optional, hex code or [Tailwind color](https://tailwindcss.com/docs/background-color) (do not prefix with `bg-`). Background color for icons.
- **iconColor**: optional, hex code or [Tailwind color](https://tailwindcss.com/docs/background-color) (do not prefix with `bg-`). Only used as the fill color for Material Design icons.
- **iconBubble**: option, `true` or `false`, when `false` the bubble and shadow are removed from the icon
## Template
```json
[
{
"category": "Category name",
"bubble": false,
"services": [
{
"name": "My Cloud App",
"uri": "https://website.com",
"description": "Fun site",
"icon": "mdi-cloud",
"iconBG": "#fff",
"iconColor": "#000",
"iconBubble": false
}
]
}
]
```
## Example
```json
[
{
@ -63,6 +167,7 @@ services:
},
{
"category": "Devices",
"bubble": true,
"services": [
{
"name": "Router",
@ -74,7 +179,8 @@ services:
"name": "Home Assistant",
"uri": "http://homeassistant.local:8123/",
"description": "Home automation",
"icon": "/icons/home-assistant.svg"
"icon": "home-assistant",
"iconBubble": false
},
{
"name": "Synology",

View File

@ -23,16 +23,20 @@ const iconLevel = 300;
const getIconColor = (index: number) => `bg-${iconColors[iconColors.length % index]}-${iconLevel}`;
interface IProps {
name: string;
index: number;
icon?: string;
iconColor?: string;
iconBG?: string;
iconBubble?: boolean;
uri?: string;
}
export const Icon: React.FunctionComponent<IProps> = ({ uri, icon, index }) => {
export const Icon: React.FunctionComponent<IProps> = ({ name, uri, icon, index, iconBG, iconBubble, iconColor }) => {
if (is.null(icon)) {
if (!is.null(uri)) {
return (
<a href={uri} target="_blank">
<a href={uri} target="_blank" rel="noreferrer" title={name}>
<IconBlank index={index} />
</a>
);
@ -43,13 +47,13 @@ export const Icon: React.FunctionComponent<IProps> = ({ uri, icon, index }) => {
if (!is.null(uri)) {
return (
<a href={uri} target="_blank">
<IconBase icon={icon as string} />
<a href={uri} target="_blank" rel="noreferrer" title={name}>
<IconBase icon={icon as string} iconBG={iconBG} iconColor={iconColor} iconBubble={iconBubble} />
</a>
);
}
return <IconBase icon={icon as string} />;
return <IconBase icon={icon as string} iconBG={iconBG} iconColor={iconColor} iconBubble={iconBubble} />;
};
interface IIconBlankProps {
@ -66,16 +70,108 @@ const IconBlank: React.FunctionComponent<IIconBlankProps> = ({ index }) => {
);
};
interface IIconBaseProps {
icon: string;
enum IconType {
uri,
material,
dashboard,
}
const IconBase: React.FunctionComponent<IIconBaseProps> = ({ icon }) => {
return (
<img
src={icon}
alt=""
className=" block w-16 h-16 rounded-2xl border border-black/5 shadow-sm overflow-hidden"
/>
);
interface IIconBaseProps {
icon: string;
iconColor?: string;
iconBG?: string;
iconBubble?: boolean;
}
const IconBase: React.FunctionComponent<IIconBaseProps> = ({ icon, iconBG, iconBubble, iconColor }) => {
let iconType: IconType = IconType.uri;
if (icon.startsWith("http") || icon.startsWith("/")) {
iconType = IconType.uri;
} else if (icon.startsWith("mdi-")) {
iconType = IconType.material;
} else {
iconType = IconType.dashboard;
}
// Everyone starts the same size
let iconClassName = "block w-16 h-16 overflow-hidden bg-contain";
if (is.null(iconBubble) || iconBubble !== false) {
iconClassName += " rounded-2xl border border-black/5 shadow-sm";
}
const iconStyle: React.CSSProperties = {};
switch (iconType) {
case IconType.uri:
case IconType.dashboard:
// Default to bubble and no background for URI and Dashboard icons
if (!is.null(iconBG)) {
if (iconBG?.startsWith("#")) {
iconStyle.backgroundColor = iconBG;
} else {
iconClassName += ` bg-${iconBG}`;
}
}
break;
case IconType.material:
// Material icons get a color and a background by default, then an optional bubble
if (is.null(iconBG)) {
iconClassName += ` bg-slate-200 dark:bg-gray-900`;
} else {
if (iconBG?.startsWith("#")) {
iconStyle.backgroundColor = iconBG;
} else {
iconClassName += ` bg-${iconBG}`;
}
}
if (is.null(iconBubble) || iconBubble !== false) {
iconClassName += " rounded-2xl border border-black/5 shadow-sm";
}
if (is.null(iconColor)) {
iconColor = "black dark:bg-white";
}
break;
}
const mdiIconStyle: React.CSSProperties = {};
let mdiIconColorFull = "bg-" + iconColor;
if (!is.null(iconColor) && iconColor?.startsWith("#")) {
mdiIconColorFull = "";
mdiIconStyle.backgroundColor = iconColor;
}
switch (iconType) {
case IconType.uri:
return <img src={icon} alt="" className={iconClassName} style={{ ...iconStyle }} />;
case IconType.dashboard:
icon = icon.replace(".png", "").replace(".jpg", "").replace(".svg", "");
return (
<img
src={`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}.png`}
alt=""
className={iconClassName}
style={{ ...iconStyle }}
/>
);
case IconType.material:
icon = icon.replace("mdi-", "").replace(".svg", "");
return (
<div className={iconClassName} style={{ ...iconStyle }}>
<div
className={`block w-16 h-16 ${mdiIconColorFull} overflow-hidden`}
style={{
...mdiIconStyle,
mask: `url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${icon}.svg) no-repeat center / contain`,
WebkitMask: `url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${icon}.svg) no-repeat center / contain`,
}}
/>
</div>
);
}
};

View File

@ -1,20 +1,50 @@
import React from "react";
import { IServiceCatalog } from "../shared/types";
import { CATEGORIES } from "../variables";
import { Services } from "./services";
interface IProps {
catalogs: IServiceCatalog[];
}
export const ServiceCatalogs: React.FunctionComponent<IProps> = ({ catalogs }) => {
export const ServiceCatalogList: React.FunctionComponent<IProps> = ({ catalogs }) => {
return (
<ul>
{catalogs.map((catalog, index) => (
<li key={index} className="mt-12 first:mt-0 xl:first:mt-6">
<h2 className="text-2xl text-slate-600 font-light py-2 px-4">{catalog.category}</h2>
<Services services={catalog.services} />
</li>
<ServiceCatalog key={index} catalog={catalog} index={index} />
))}
</ul>
);
};
interface ICatalogProps {
catalog: IServiceCatalog;
index: number;
}
const ServiceCatalog: React.FunctionComponent<ICatalogProps> = ({ catalog, index }) => {
let categoryClassName = "dark:text-slate-200";
switch (CATEGORIES as string) {
case "small":
categoryClassName += " text-sm text-slate-800 font-semibold py px-4 uppercase";
break;
case "normal":
default:
categoryClassName += " text-2xl text-slate-600 font-light py-2 px-4";
break;
}
let liClassName = "mt-12 first:mt-0 xl:first:mt-6";
if (catalog.bubble) {
liClassName += " bg-white dark:bg-black rounded-2xl px-6 py-6 ring-1 ring-slate-900/5 shadow-xl";
}
return (
<li key={index} className={liClassName}>
<h2 className={categoryClassName}>{catalog.category}</h2>
<Services services={catalog.services} />
</li>
);
};

View File

@ -23,13 +23,23 @@ interface IServiceProps {
}
const Service: React.FunctionComponent<IServiceProps> = ({ service, index }) => {
const { name, uri, description, icon } = service;
const { name, uri, description, icon, iconBG, iconBubble, iconColor } = service;
return (
<li className="p-4 flex gap-4">
<span className="flex-shrink-0 block ">
<Icon icon={icon} uri={uri} index={index} />
</span>
{!is.null(icon) && (
<span className="flex-shrink-0 block">
<Icon
name={name}
icon={icon}
uri={uri}
index={index}
iconColor={iconColor}
iconBG={iconBG}
iconBubble={iconBubble}
/>
</span>
)}
<div>
<h3 className="text-lg mt-1 font-semibold line-clamp-1">
<a href={uri} target="_blank">
@ -37,7 +47,7 @@ const Service: React.FunctionComponent<IServiceProps> = ({ service, index }) =>
</a>
</h3>
{!is.null(description) && (
<p className="text-sm text-black/50 line-clamp-1">
<p className="text-sm text-black/50 dark:text-white/50 line-clamp-1">
<a href={uri} target="_blank">
{description}
</a>

View File

@ -2,9 +2,10 @@ import React from "react";
import ReactDOM from "react-dom/client";
import { IndexPage } from "./pages";
import "./tailwind.css";
import { PAGEICON, PAGETITLE } from "./variables";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<IndexPage title="HTMLTITLE" icon="LOGO" />
<IndexPage title={PAGETITLE} icon={PAGEICON} />
</React.StrictMode>
);

View File

@ -1,8 +1,9 @@
import React from "react";
import { Header } from "../components/header";
import { ServiceCatalogs } from "../components/service-catalogs";
import { ServiceCatalogList } from "../components/service-catalogs";
import userServices from "../config.json";
import { IServiceCatalog } from "../shared/types";
import { SHOWHEADER, SHOWHEADERLINE, SHOWHEADERTOP } from "../variables";
interface IProps {
title?: string;
@ -12,13 +13,47 @@ interface IProps {
export const IndexPage: React.FunctionComponent<IProps> = ({ icon, title }) => {
const mySerices = userServices as IServiceCatalog[];
let headerClassName = "p-4";
if (SHOWHEADERTOP) {
headerClassName += " w-full";
} else {
headerClassName += " w-full xl:w-auto xl:max-w-xs xl:min-h-screen";
}
if (SHOWHEADERLINE) {
headerClassName += "border-0 border-solid border-gray-300 dark:border-gray-700";
if (SHOWHEADERTOP) {
headerClassName += " border-b";
} else {
headerClassName += " border-b xl:border-r xl:border-b-0";
}
}
let pageWrapperClassName = "min-h-screen flex flex-col max-w-screen-2xl mx-auto";
if (!SHOWHEADERTOP) {
pageWrapperClassName += " xl:flex-row";
}
let serviceCatalogListWrapperClassName = "p-4 flex-grow";
if (!SHOWHEADERTOP) {
serviceCatalogListWrapperClassName += " min-h-screen";
}
return (
<div className="min-h-screen flex flex-col xl:flex-row">
<div className="w-full xl:w-auto xl:max-w-xs xl:min-h-screen border-0 border-solid border-b xl:border-r xl:border-b-0 border-gray-300 p-4">
<Header title={title} icon={icon} />
</div>
<div className="min-h-screen p-4 flex-grow">
<ServiceCatalogs catalogs={mySerices} />
<div className="min-h-screen">
<div className={pageWrapperClassName}>
{SHOWHEADER && (
<div className={headerClassName}>
<Header title={title} icon={icon} />
</div>
)}
<div className={serviceCatalogListWrapperClassName}>
<ServiceCatalogList catalogs={mySerices} />
</div>
</div>
</div>
);

View File

@ -1,5 +1,6 @@
export interface IServiceCatalog {
category: string;
bubble?: boolean;
services: IService[];
}
@ -9,4 +10,7 @@ export interface IService {
description?: string;
icon?: string;
iconColor?: string;
iconBG?: string;
iconBubble?: boolean;
}

View File

@ -3,13 +3,25 @@
@tailwind utilities;
html {
background-color: theme(colors.bg);
background-color: theme(colors.slate.50);
}
@media (prefers-color-scheme: dark) {
html {
background-color: theme(colors.gray.950);
}
}
body {
min-height: 100vh;
}
@media (prefers-color-scheme: dark) {
body {
color: theme(colors.slate.200);
}
}
h1 {
font-size: theme(fontSize.3xl);
font-weight: theme(fontWeight.semibold);

7
src/variables.ts Normal file
View File

@ -0,0 +1,7 @@
export const PAGETITLE = "My Website";
export const PAGEICON = "/logo.png";
export const SHOWHEADER = true;
export const SHOWHEADERLINE = true;
export const SHOWHEADERTOP = false;
export const CATEGORIES = "normal";
export const THEME = "light";

View File

@ -3,29 +3,16 @@ export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
safelist: [
/* Built from icon.tsx */
"bg-blue-300",
"bg-rose-300",
"bg-green-300",
"bg-red-300",
"bg-yellow-300",
"bg-cyan-300",
"bg-pink-300",
"bg-orange-300",
"bg-sky-300",
"bg-slate-300",
"bg-emerald-300",
"bg-zinc-300",
"bg-neutral-300",
"bg-amber-300",
"bg-violet-300",
{
pattern:
/bg-(black|white|slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(50|100|200|300|400|500|600|700|800|900|950)/,
},
"bg-transparent",
"bg-black",
"bg-white",
],
theme: {
extend: {
colors: {
bg: "#f8fafc",
border: "#336699",
},
},
extend: {},
},
plugins: [],
};