Merge pull request #20 from notclickable-jordan/static
Switching to an entirely static site
This commit is contained in:
commit
86f53bc546
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,4 +1,8 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
src/*.js
|
||||
src/**/*.js
|
||||
public/index.html
|
||||
public/main.css
|
||||
public/css
|
||||
|
16
default.conf
Normal file
16
default.conf
Normal file
@ -0,0 +1,16 @@
|
||||
server {
|
||||
listen 4173;
|
||||
|
||||
root /app/public;
|
||||
|
||||
location /app {
|
||||
try_files $uri $uri/ =404;
|
||||
|
||||
# Cache settings for static content
|
||||
expires 7d; # Cache static content for 7 days
|
||||
add_header Cache-Control "public, max-age=604800, immutable";
|
||||
}
|
||||
|
||||
error_page 404 /index.html;
|
||||
error_page 500 502 503 504 /index.html;
|
||||
}
|
24
dockerfile
24
dockerfile
@ -1,23 +1,8 @@
|
||||
# Build site using Node JS
|
||||
FROM node:21-slim
|
||||
|
||||
# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
|
||||
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
|
||||
# installs, work.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y wget gnupg \
|
||||
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
|
||||
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y chromium fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
|
||||
--no-install-recommends \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD true
|
||||
|
||||
# Install puppeteer so it's available in the container.
|
||||
RUN npm init -y && \
|
||||
npm i -g puppeteer
|
||||
# Install nginx
|
||||
RUN apt-get update && apt-get install -y nginx
|
||||
|
||||
ARG BUILD_DATE
|
||||
|
||||
@ -35,6 +20,9 @@ RUN npm i
|
||||
|
||||
COPY . .
|
||||
|
||||
# Copy the nginx config to the correct folder
|
||||
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
ENV TITLE "My Website"
|
||||
@ -53,4 +41,4 @@ EXPOSE 4173
|
||||
RUN chmod +x /app/docker-entrypoint.sh
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
CMD ["sh", "-c", "npm run build && nginx -g 'daemon off;'"]
|
@ -1,13 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
<title>My Website</title>
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<link rel="stylesheet" href="./main.css" crossorigin="" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
2568
package-lock.json
generated
2568
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@ -5,35 +5,21 @@
|
||||
"repository": {
|
||||
"url": "https://github.com/notclickable-jordan/starbase-80/"
|
||||
},
|
||||
"version": "1.3.0",
|
||||
"type": "module",
|
||||
"version": "1.4.0",
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"serve": "vite preview",
|
||||
"start": "node start.js",
|
||||
"html": "html-minifier --remove-comments --collapse-whitespace --input-dir ./dist --output-dir ./dist --file-ext html",
|
||||
"replace": "npm run replace:script",
|
||||
"replace:script": "replace-in-file --configFile replace-script.cjs"
|
||||
"build": "tsc && node ./dist/index.js && npm run html && npm run tailwind && npm run css-cache-break",
|
||||
"html": "html-minifier --remove-comments --collapse-whitespace --input-dir ./public --output-dir ./public --file-ext html",
|
||||
"tailwind": "npx tailwindcss -i ./src/tailwind.css -o ./public/main.css",
|
||||
"css-cache-break": "node ./dist/css-cache-break.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/react": "^18.2.45",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"@types/node": "^20.10.5",
|
||||
"clean-css": "^5.3.3",
|
||||
"html-minifier": "^4.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"postcss": "^8.4.32",
|
||||
"prettier": "^3.1.1",
|
||||
"puppeteer": "^21.6.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"replace-in-file": "^7.0.2",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.8"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"prettier": {
|
||||
"arrowParens": "avoid",
|
||||
@ -43,5 +29,8 @@
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": true
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/clean-css": "^4.2.11"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
@ -32,6 +32,11 @@ Inspired by [Ben Phelps' Homepage](https://gethomepage.dev/) and [Umbrel](https:
|
||||
|
||||
# Change history
|
||||
|
||||
## 1.4.0
|
||||
|
||||
- Rewrote the entire application to not use React. Now it's just a Node application that emits static HTML.
|
||||
- Removed lots of packages
|
||||
|
||||
## 1.3.0
|
||||
|
||||
- Removed all JavaScript as part of the build step. The image will be slightly larger and take longer to start up and shut down, but the page will be even lighter.
|
||||
|
@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
files: "./dist/index.html",
|
||||
from: /<script[^>]+><\/script>/gi,
|
||||
to: "",
|
||||
};
|
34
src/components/anchor.ts
Normal file
34
src/components/anchor.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { is } from "../shared/is";
|
||||
import { NEWWINDOW } from "../variables";
|
||||
|
||||
interface IProps {
|
||||
uri: string;
|
||||
title?: string;
|
||||
className?: string;
|
||||
children?: any;
|
||||
newWindow?: boolean;
|
||||
}
|
||||
|
||||
export const Anchor = function (props: IProps) {
|
||||
const { uri, children, title, className, newWindow } = props;
|
||||
|
||||
let newWindowLocal = NEWWINDOW;
|
||||
|
||||
if (!is.null(newWindow)) {
|
||||
newWindowLocal = newWindow as boolean;
|
||||
}
|
||||
|
||||
if (newWindowLocal) {
|
||||
return `
|
||||
<a href="${uri}" target="_blank" rel="noreffer" title="${title || ""}" class="${className || ""}">
|
||||
${children}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<a href="${uri}" title="${title || ""}" class="${className || ""}">
|
||||
${children}
|
||||
</a>
|
||||
`;
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
import React from "react";
|
||||
import { is } from "../shared/is";
|
||||
import { NEWWINDOW } from "../variables";
|
||||
|
||||
interface IProps {
|
||||
uri: string;
|
||||
title?: string;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
newWindow?: boolean;
|
||||
}
|
||||
|
||||
export const Anchor: React.FunctionComponent<IProps> = ({ uri, children, title, className, newWindow }) => {
|
||||
let newWindowLocal = NEWWINDOW;
|
||||
|
||||
if (!is.null(newWindow)) {
|
||||
newWindowLocal = newWindow as boolean;
|
||||
}
|
||||
|
||||
if (newWindowLocal) {
|
||||
return (
|
||||
<a href={uri} target="_blank" rel="noreffer" title={title} className={className}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={uri} title={title} className={className}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
16
src/components/header.ts
Normal file
16
src/components/header.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { is } from "../shared/is";
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export const Header = function (props: IProps) {
|
||||
const { icon, title } = props;
|
||||
return `
|
||||
<div class="p-2 xl:p-4 flex flex-nowrap justify-center items-center gap-2 xl:flex-wrap">
|
||||
${!is.null(icon) && `<img src="${icon}" alt="${title || ""}" class="inline-block w-16 h-16" />`}
|
||||
<h1>${title}</h1>
|
||||
</div>
|
||||
`;
|
||||
};
|
@ -1,16 +0,0 @@
|
||||
import React from "react";
|
||||
import { is } from "../shared/is";
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export const Header: React.FunctionComponent<IProps> = ({ icon, title }) => {
|
||||
return (
|
||||
<div className="p-2 xl:p-4 flex flex-nowrap justify-center items-center gap-2 xl:flex-wrap">
|
||||
{!is.null(icon) && <img src={icon} alt={title} className="inline-block w-16 h-16" />}
|
||||
<h1>{title}</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { is } from "../shared/is";
|
||||
import { IconAspect } from "../shared/types";
|
||||
import { Anchor } from "./anchor";
|
||||
@ -36,67 +35,54 @@ interface IProps {
|
||||
newWindow?: boolean;
|
||||
}
|
||||
|
||||
export const Icon: React.FunctionComponent<IProps> = ({
|
||||
name,
|
||||
uri,
|
||||
icon,
|
||||
index,
|
||||
iconBG,
|
||||
iconBubble,
|
||||
iconColor,
|
||||
iconAspect,
|
||||
newWindow,
|
||||
}) => {
|
||||
export const Icon = function (props: IProps): string {
|
||||
const { name, uri, icon, index, iconBG, iconBubble, iconColor, iconAspect, newWindow } = props;
|
||||
|
||||
if (is.null(icon)) {
|
||||
if (!is.null(uri)) {
|
||||
return (
|
||||
<Anchor uri={uri as string} title={name} newWindow={newWindow}>
|
||||
<IconBlank index={index} />
|
||||
</Anchor>
|
||||
);
|
||||
return Anchor({ uri: uri as string, title: name, newWindow, children: IconBlank({ index }) });
|
||||
}
|
||||
|
||||
return <IconBlank index={index} />;
|
||||
return IconBlank({ index });
|
||||
}
|
||||
|
||||
if (!is.null(uri)) {
|
||||
return (
|
||||
<Anchor uri={uri as string} title={name} newWindow={newWindow} className="self-center">
|
||||
<IconBase
|
||||
icon={icon as string}
|
||||
iconBG={iconBG}
|
||||
iconColor={iconColor}
|
||||
iconBubble={iconBubble}
|
||||
iconAspect={iconAspect}
|
||||
/>
|
||||
</Anchor>
|
||||
);
|
||||
return Anchor({
|
||||
uri: uri as string,
|
||||
title: name,
|
||||
newWindow,
|
||||
className: "self-center",
|
||||
children: IconBase({
|
||||
icon: icon as string,
|
||||
iconBG,
|
||||
iconColor,
|
||||
iconBubble,
|
||||
iconAspect,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<IconBase
|
||||
icon={icon as string}
|
||||
iconBG={iconBG}
|
||||
iconColor={iconColor}
|
||||
iconBubble={iconBubble}
|
||||
iconAspect={iconAspect}
|
||||
/>
|
||||
);
|
||||
return IconBase({
|
||||
icon: icon as string,
|
||||
iconBG,
|
||||
iconColor,
|
||||
iconBubble,
|
||||
iconAspect,
|
||||
});
|
||||
};
|
||||
|
||||
interface IIconBlankProps {
|
||||
index: number;
|
||||
}
|
||||
|
||||
const IconBlank: React.FunctionComponent<IIconBlankProps> = ({ index }) => {
|
||||
return (
|
||||
function IconBlank(props: IIconBlankProps) {
|
||||
const { index } = props;
|
||||
return `
|
||||
<span
|
||||
className={`block w-16 h-16 rounded-2xl border border-black/5 shadow-sm ${getIconColor(
|
||||
index
|
||||
)} overflow-hidden`}
|
||||
class="${`block w-16 h-16 rounded-2xl border border-black/5 shadow-sm ${getIconColor(index)} overflow-hidden`}"
|
||||
/>
|
||||
);
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
enum IconType {
|
||||
uri,
|
||||
@ -112,13 +98,9 @@ interface IIconBaseProps {
|
||||
iconAspect?: IconAspect;
|
||||
}
|
||||
|
||||
const IconBase: React.FunctionComponent<IIconBaseProps> = ({
|
||||
icon,
|
||||
iconBG,
|
||||
iconBubble,
|
||||
iconColor,
|
||||
iconAspect = "square",
|
||||
}) => {
|
||||
function IconBase(props: IIconBaseProps) {
|
||||
let { icon, iconBG, iconBubble, iconColor, iconAspect = "square" } = props;
|
||||
|
||||
let iconType: IconType = IconType.uri;
|
||||
|
||||
if (icon.startsWith("http") || icon.startsWith("/")) {
|
||||
@ -152,7 +134,7 @@ const IconBase: React.FunctionComponent<IIconBaseProps> = ({
|
||||
iconClassName += " rounded-2xl border border-black/5 shadow-sm";
|
||||
}
|
||||
|
||||
const iconStyle: React.CSSProperties = {};
|
||||
const iconStyle: string[] = [];
|
||||
|
||||
switch (iconType) {
|
||||
case IconType.uri:
|
||||
@ -160,7 +142,7 @@ const IconBase: React.FunctionComponent<IIconBaseProps> = ({
|
||||
// Default to bubble and no background for URI and Dashboard icons
|
||||
if (!is.null(iconBG)) {
|
||||
if (iconBG?.startsWith("#")) {
|
||||
iconStyle.backgroundColor = iconBG;
|
||||
iconStyle.push(`background-color: ${iconBG}`);
|
||||
} else {
|
||||
iconClassName += ` bg-${iconBG}`;
|
||||
}
|
||||
@ -172,7 +154,7 @@ const IconBase: React.FunctionComponent<IIconBaseProps> = ({
|
||||
iconClassName += ` bg-slate-200 dark:bg-gray-900`;
|
||||
} else {
|
||||
if (iconBG?.startsWith("#")) {
|
||||
iconStyle.backgroundColor = iconBG;
|
||||
iconStyle.push(`background-color: ${iconBG}`);
|
||||
} else {
|
||||
iconClassName += ` bg-${iconBG}`;
|
||||
}
|
||||
@ -184,41 +166,50 @@ const IconBase: React.FunctionComponent<IIconBaseProps> = ({
|
||||
break;
|
||||
}
|
||||
|
||||
const mdiIconStyle: React.CSSProperties = {};
|
||||
const mdiIconStyle: string[] = [];
|
||||
let mdiIconColorFull = "bg-" + iconColor;
|
||||
|
||||
if (!is.null(iconColor) && iconColor?.startsWith("#")) {
|
||||
mdiIconColorFull = "";
|
||||
mdiIconStyle.backgroundColor = iconColor;
|
||||
mdiIconStyle.push(`background-color: ${iconColor}`);
|
||||
}
|
||||
|
||||
switch (iconType) {
|
||||
case IconType.uri:
|
||||
return <img src={icon} alt="" className={iconClassName} style={{ ...iconStyle }} />;
|
||||
return `<img src="${icon}" alt="" class="${iconClassName || ""}" style="${unwrapStyles(iconStyle)}" />`;
|
||||
case IconType.dashboard:
|
||||
icon = icon.replace(".png", "").replace(".jpg", "").replace(".svg", "");
|
||||
return (
|
||||
return `
|
||||
<img
|
||||
src={`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}.png`}
|
||||
src=${`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}.png`}
|
||||
alt=""
|
||||
className={iconClassName}
|
||||
style={{ ...iconStyle }}
|
||||
class="${iconClassName || ""}"
|
||||
style="${unwrapStyles(iconStyle)}"
|
||||
/>
|
||||
);
|
||||
`;
|
||||
case IconType.material:
|
||||
icon = icon.replace("mdi-", "").replace(".svg", "");
|
||||
|
||||
return (
|
||||
<div className={iconClassName} style={{ ...iconStyle }}>
|
||||
return `
|
||||
<div class="${iconClassName || ""}" style="${{ ...iconStyle }}">
|
||||
<div
|
||||
className={`block ${iconWidthHeightClassName} ${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`,
|
||||
}}
|
||||
className=${`block ${iconWidthHeightClassName} ${mdiIconColorFull} overflow-hidden`}
|
||||
style="${unwrapStyles(
|
||||
mdiIconStyle.concat(
|
||||
`mask: url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${icon}.svg) no-repeat center / contain`,
|
||||
`webkit-mask: url(https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/${icon}.svg) no-repeat center / contain`
|
||||
)
|
||||
)}"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function unwrapStyles(styles: string[]): string {
|
||||
if (is.null(styles)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return styles.join(";");
|
||||
}
|
||||
};
|
@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { IServiceCatalog } from "../shared/types";
|
||||
import { CATEGORIES } from "../variables";
|
||||
import { Services } from "./services";
|
||||
@ -7,14 +6,14 @@ interface IProps {
|
||||
catalogs: IServiceCatalog[];
|
||||
}
|
||||
|
||||
export const ServiceCatalogList: React.FunctionComponent<IProps> = ({ catalogs }) => {
|
||||
return (
|
||||
export const ServiceCatalogList = function (props: IProps) {
|
||||
const { catalogs } = props;
|
||||
|
||||
return `
|
||||
<ul>
|
||||
{catalogs.map((catalog, index) => (
|
||||
<ServiceCatalog key={index} catalog={catalog} index={index} />
|
||||
))}
|
||||
${catalogs.map((catalog, index) => ServiceCatalog({ catalog, index })).join("")}
|
||||
</ul>
|
||||
);
|
||||
`;
|
||||
};
|
||||
|
||||
interface ICatalogProps {
|
||||
@ -22,7 +21,9 @@ interface ICatalogProps {
|
||||
index: number;
|
||||
}
|
||||
|
||||
const ServiceCatalog: React.FunctionComponent<ICatalogProps> = ({ catalog, index }) => {
|
||||
const ServiceCatalog = function (props: ICatalogProps) {
|
||||
const { catalog, index } = props;
|
||||
|
||||
let categoryClassName = "dark:text-slate-200";
|
||||
|
||||
switch (CATEGORIES as string) {
|
||||
@ -41,10 +42,10 @@ const ServiceCatalog: React.FunctionComponent<ICatalogProps> = ({ catalog, index
|
||||
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} />
|
||||
return `
|
||||
<li class="${liClassName}">
|
||||
<h2 class="${categoryClassName}">${catalog.category}</h2>
|
||||
${Services({ services: catalog.services })}
|
||||
</li>
|
||||
);
|
||||
`;
|
||||
};
|
55
src/components/services.ts
Normal file
55
src/components/services.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { is } from "../shared/is";
|
||||
import { IService } from "../shared/types";
|
||||
import { Anchor } from "./anchor";
|
||||
import { Icon } from "./icon";
|
||||
|
||||
interface IServicesProps {
|
||||
services: IService[];
|
||||
}
|
||||
|
||||
export const Services = function (props: IServicesProps) {
|
||||
const { services } = props;
|
||||
|
||||
return `
|
||||
<ul class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-1 lg:gap-2 lg:gap-y-4">
|
||||
${services.map((service, index) => Service({ index, service })).join("")}
|
||||
</ul>
|
||||
`;
|
||||
};
|
||||
|
||||
interface IServiceProps {
|
||||
service: IService;
|
||||
index: number;
|
||||
}
|
||||
|
||||
function Service(props: IServiceProps) {
|
||||
const { service, index } = props;
|
||||
|
||||
const { name, uri, description, icon, iconBG, iconBubble, iconColor, iconAspect, newWindow } = service;
|
||||
|
||||
return `
|
||||
<li class="p-4 flex gap-4">
|
||||
${
|
||||
!is.null(icon) &&
|
||||
`
|
||||
<span class="flex-shrink-0 flex">
|
||||
${Icon({ name, icon, uri, index, iconColor, iconBG, iconBubble, iconAspect, newWindow })}
|
||||
</span>
|
||||
`
|
||||
}
|
||||
<div>
|
||||
<h3 class="text-lg mt-1 font-semibold line-clamp-1">
|
||||
${Anchor({ uri, newWindow, children: name })}
|
||||
</h3>
|
||||
${
|
||||
!is.null(description) &&
|
||||
`
|
||||
<p class="text-sm text-black/50 dark:text-white/50 line-clamp-1">
|
||||
${Anchor({ uri, newWindow, children: description })}
|
||||
</p>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
import React from "react";
|
||||
import { is } from "../shared/is";
|
||||
import { IService } from "../shared/types";
|
||||
import { Anchor } from "./anchor";
|
||||
import { Icon } from "./icon";
|
||||
|
||||
interface IServicesProps {
|
||||
services: IService[];
|
||||
}
|
||||
|
||||
export const Services: React.FunctionComponent<IServicesProps> = ({ services }) => {
|
||||
return (
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-1 lg:gap-2 lg:gap-y-4">
|
||||
{services.map((service, index) => (
|
||||
<Service key={index} service={service} index={index} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
interface IServiceProps {
|
||||
service: IService;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const Service: React.FunctionComponent<IServiceProps> = ({ service, index }) => {
|
||||
const { name, uri, description, icon, iconBG, iconBubble, iconColor, iconAspect, newWindow } = service;
|
||||
|
||||
return (
|
||||
<li className="p-4 flex gap-4">
|
||||
{!is.null(icon) && (
|
||||
<span className="flex-shrink-0 flex">
|
||||
<Icon
|
||||
name={name}
|
||||
icon={icon}
|
||||
uri={uri}
|
||||
index={index}
|
||||
iconColor={iconColor}
|
||||
iconBG={iconBG}
|
||||
iconBubble={iconBubble}
|
||||
iconAspect={iconAspect}
|
||||
newWindow={newWindow}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="text-lg mt-1 font-semibold line-clamp-1">
|
||||
<Anchor uri={uri} newWindow={newWindow}>
|
||||
{name}
|
||||
</Anchor>
|
||||
</h3>
|
||||
{!is.null(description) && (
|
||||
<p className="text-sm text-black/50 dark:text-white/50 line-clamp-1">
|
||||
<Anchor uri={uri} newWindow={newWindow}>
|
||||
{description}
|
||||
</Anchor>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
26
src/css-cache-break.ts
Normal file
26
src/css-cache-break.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import CleanCSS from "clean-css";
|
||||
import * as path from "path";
|
||||
import { sbMakeFolder, sbReadFile, sbRemoveAllCSS, sbWriteFile } from "./shared/files";
|
||||
|
||||
const mainCSSFilename = `main.${new Date().getTime()}.css`;
|
||||
|
||||
const cssFilePath = path.join(__dirname, "../public/css");
|
||||
const cssFileInPath = path.join(__dirname, "../public", "main.css");
|
||||
const cssFileOutPath = path.join(cssFilePath, mainCSSFilename);
|
||||
const indexFileInOutPath = path.join(__dirname, "../public", "index.html");
|
||||
|
||||
async function start(): Promise<void> {
|
||||
await sbRemoveAllCSS(cssFilePath);
|
||||
await sbMakeFolder(cssFilePath);
|
||||
|
||||
const css = await sbReadFile(cssFileInPath);
|
||||
await sbWriteFile(cssFileOutPath, new CleanCSS().minify(css).styles);
|
||||
|
||||
const index = await sbReadFile(indexFileInOutPath);
|
||||
const cssLink = `<link rel="stylesheet" href="./main.css" crossorigin="">`;
|
||||
const cssLinkReplacement = `<link rel="stylesheet" href="./css/${mainCSSFilename}" crossorigin="" />`;
|
||||
|
||||
await sbWriteFile(indexFileInOutPath, index.replace(cssLink, cssLinkReplacement));
|
||||
}
|
||||
|
||||
start();
|
21
src/index.ts
Normal file
21
src/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import * as path from "path";
|
||||
import { IndexPage } from "./pages/index";
|
||||
import { sbReadFile, sbWriteFile } from "./shared/files";
|
||||
import { PAGEICON, PAGETITLE } from "./variables";
|
||||
|
||||
const indexFileInPath = path.join(__dirname, "../", "index.html");
|
||||
const indexFileOutPath = path.join(__dirname, "../", "./public", "index.html");
|
||||
|
||||
async function start(): Promise<void> {
|
||||
const index = await sbReadFile(indexFileInPath);
|
||||
|
||||
const newText = IndexPage({ icon: PAGEICON, title: PAGETITLE });
|
||||
const rootDiv = '<div id="root"></div>';
|
||||
const rootDivReplacement = '<div id="root">$1</div>';
|
||||
|
||||
const newIndex = index.replace(rootDiv, rootDivReplacement.replace("$1", newText));
|
||||
|
||||
await sbWriteFile(indexFileOutPath, newIndex);
|
||||
}
|
||||
|
||||
start();
|
11
src/main.tsx
11
src/main.tsx
@ -1,11 +0,0 @@
|
||||
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={PAGETITLE} icon={PAGEICON} />
|
||||
</React.StrictMode>
|
||||
);
|
@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Header } from "../components/header";
|
||||
import { ServiceCatalogList } from "../components/service-catalogs";
|
||||
import userServicesOld from "../config.json";
|
||||
@ -12,8 +11,9 @@ interface IProps {
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export const IndexPage: React.FunctionComponent<IProps> = ({ icon, title }) => {
|
||||
const mySerices = (is.null(userServicesOld) ? userServices : userServicesOld) as IServiceCatalog[];
|
||||
export const IndexPage = function (props: IProps): string {
|
||||
const { icon, title } = props;
|
||||
const myServices = (is.null(userServicesOld) ? userServices : userServicesOld) as IServiceCatalog[];
|
||||
|
||||
let headerClassName = "p-4";
|
||||
|
||||
@ -45,18 +45,21 @@ export const IndexPage: React.FunctionComponent<IProps> = ({ icon, title }) => {
|
||||
serviceCatalogListWrapperClassName += " min-h-screen";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className={pageWrapperClassName}>
|
||||
{SHOWHEADER && (
|
||||
<div className={headerClassName}>
|
||||
<Header title={title} icon={icon} />
|
||||
return `
|
||||
<div class="min-h-screen">
|
||||
<div class="${pageWrapperClassName}">
|
||||
${
|
||||
SHOWHEADER &&
|
||||
`
|
||||
<div class="${headerClassName}">
|
||||
${Header({ icon, title })}
|
||||
</div>
|
||||
)}
|
||||
<div className={serviceCatalogListWrapperClassName}>
|
||||
<ServiceCatalogList catalogs={mySerices} />
|
||||
`
|
||||
}
|
||||
<div class="${serviceCatalogListWrapperClassName}">
|
||||
${ServiceCatalogList({ catalogs: myServices })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
`;
|
||||
};
|
65
src/shared/files.ts
Normal file
65
src/shared/files.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import * as fs from "fs/promises";
|
||||
|
||||
export async function sbReadFile(fileName: string): Promise<string> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const index = await fs.readFile(fileName);
|
||||
|
||||
return resolve(index.toString());
|
||||
} catch (exception) {
|
||||
console.error(`Could not read file: ${fileName}`);
|
||||
reject(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function sbWriteFile(fileName: string, contents: string): Promise<boolean> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
await fs.writeFile(fileName, contents);
|
||||
return resolve(true);
|
||||
} catch (exception) {
|
||||
console.error(`Could not write file: ${fileName}`);
|
||||
reject(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function sbRename(fileNameOld: string, fileNameNew: string): Promise<boolean> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
await fs.rename(fileNameOld, fileNameNew);
|
||||
return resolve(true);
|
||||
} catch (exception) {
|
||||
console.error(`Could not rename file: ${fileNameOld} to ${fileNameNew}`);
|
||||
reject(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function sbMakeFolder(path: string): Promise<boolean> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
await fs.mkdir(path, { recursive: true });
|
||||
return resolve(true);
|
||||
} catch (exception) {
|
||||
console.error(`Could not make folder: ${path}`, exception);
|
||||
reject(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function sbRemoveAllCSS(path: string): Promise<boolean> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
await fs.rm(path, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
return resolve(true);
|
||||
} catch (exception) {
|
||||
console.error(`Could not remove all CSS from path: ${path}`);
|
||||
reject(exception);
|
||||
}
|
||||
});
|
||||
}
|
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
61
start.js
61
start.js
@ -1,61 +0,0 @@
|
||||
import { exec } from "child_process";
|
||||
import fs from "fs/promises";
|
||||
import puppeteer from "puppeteer";
|
||||
|
||||
function runNpmCommand(command) {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`npm ${command}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Error executing command: ${error}`);
|
||||
reject(error);
|
||||
} else {
|
||||
console.log(`Command output: ${stdout}`);
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function renderHomePage() {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||
executablePath: "chromium", // Remove this when running locally
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.goto("http://localhost:4173", { waitUntil: "networkidle0" });
|
||||
|
||||
// Wait for any additional content to load
|
||||
await page.waitForNetworkIdle();
|
||||
|
||||
// Get the rendered HTML content
|
||||
const renderedHTML = await page.content();
|
||||
|
||||
// Save the rendered HTML to a file
|
||||
fs.writeFile("./dist/index.html", renderedHTML);
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
async function runCommands() {
|
||||
try {
|
||||
console.log("Building React site");
|
||||
await runNpmCommand("run build");
|
||||
|
||||
setTimeout(async () => {
|
||||
console.log("Taking snapshot");
|
||||
await renderHomePage();
|
||||
await runNpmCommand("run html");
|
||||
await runNpmCommand("run replace");
|
||||
}, 2000);
|
||||
|
||||
console.log("Starting web server");
|
||||
await runNpmCommand("run serve");
|
||||
} catch (err) {
|
||||
// Handle errors
|
||||
console.error("An error occurred:", err);
|
||||
}
|
||||
}
|
||||
|
||||
runCommands();
|
@ -1,6 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
content: ["./public/index.html"],
|
||||
safelist: [
|
||||
/* Built from icon.tsx */
|
||||
{
|
||||
|
@ -1,21 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
"module": "CommonJS"
|
||||
},
|
||||
"include": ["src", "public"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"include": ["src/index.ts", "src/css-cache-break.ts"]
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
watch: {
|
||||
usePolling: true,
|
||||
},
|
||||
host: true,
|
||||
strictPort: true,
|
||||
port: 4173,
|
||||
},
|
||||
});
|
Loading…
Reference in New Issue
Block a user