feat: add GitHub and Docker Hub links to app templates

- Introduced `github` and `hub-docker` fields in the app template presentation schema.
- Updated normalization logic to handle new fields and return appropriate values.
- Enhanced frontend components to display GitHub and Docker Hub links for each app.
- Added tests to validate the inclusion of new fields in app templates.
This commit is contained in:
KodekaTeam committed 2026-08-22 00:03:09 +07:00
1 parent ae2013740b
commit f38f8f16d3
6 files changed
+280 -5

No files matched your search

+127 -4
View File
@@ -3,6 +3,7 @@
import Link from "next/link"; import Link from "next/link";
import { import {
type FormEvent, type FormEvent,
type ReactNode,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
@@ -349,6 +350,104 @@ function getTemplateVisual(template: AppTemplate | null) {
}; };
} }
function normalizeExternalUrl(value?: string) {
const trimmed = value?.trim();
if (!trimmed) return "";
if (/^https?:\/\//i.test(trimmed)) {
return trimmed;
}
return `https://${trimmed}`;
}
function getTemplateLinks(template: AppTemplate) {
return {
github: normalizeExternalUrl(template.presentation?.github),
dockerHub: normalizeExternalUrl(template.presentation?.["hub-docker"]),
};
}
function SimpleIcon({
src,
label,
size,
}: {
src: string;
label: string;
size: number;
}) {
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={src}
alt={label}
width={size}
height={size}
style={{ display: "block" }}
/>
);
}
function MaskIcon({
src,
size,
}: {
src: string;
size: number;
}) {
return (
<span
aria-hidden="true"
style={{
width: size,
height: size,
display: "block",
background: "currentColor",
WebkitMask: `url("${src}") center / contain no-repeat`,
mask: `url("${src}") center / contain no-repeat`,
}}
/>
);
}
function TemplateSourceLink({
href,
label,
children,
}: {
href: string;
label: string;
children: ReactNode;
}) {
if (!href) return null;
return (
<a
href={href}
target="_blank"
rel="noreferrer"
aria-label={label}
title={label}
onClick={(e) => e.stopPropagation()}
style={{
width: 26,
height: 26,
borderRadius: 7,
border: "1px solid var(--border)",
background: "var(--bg-input)",
display: "inline-grid",
placeItems: "center",
color: "var(--text-muted)",
textDecoration: "none",
flexShrink: 0,
}}
>
{children}
</a>
);
}
function AppIconBadge({ function AppIconBadge({
template, template,
size, size,
@@ -2336,6 +2435,7 @@ export default function AppsPage() {
> >
{filtered.map((app) => { {filtered.map((app) => {
const visual = getTemplateVisual(app); const visual = getTemplateVisual(app);
const sourceLinks = getTemplateLinks(app);
const installCount = installCounts.get(app.id) ?? 0; const installCount = installCounts.get(app.id) ?? 0;
const visibleTags = app.tags.slice(0, 4); const visibleTags = app.tags.slice(0, 4);
const hiddenTagCount = Math.max( const hiddenTagCount = Math.max(
@@ -2512,11 +2612,34 @@ export default function AppsPage() {
gap: 8, gap: 8,
}} }}
> >
<span <div
style={{ fontSize: 10, color: "var(--text-muted)" }} style={{
display: "flex",
alignItems: "center",
gap: 6,
minHeight: 26,
}}
> >
{visual.installs.toLocaleString()} installs <TemplateSourceLink
</span> href={sourceLinks.github}
label={`${app.name} GitHub repository`}
>
<MaskIcon
src="https://cdn.simpleicons.org/github"
size={14}
/>
</TemplateSourceLink>
<TemplateSourceLink
href={sourceLinks.dockerHub}
label={`${app.name} Docker Hub page`}
>
<SimpleIcon
src="https://cdn.simpleicons.org/docker/2496ED"
label=""
size={14}
/>
</TemplateSourceLink>
</div>
<span <span
style={{ style={{
fontSize: 10, fontSize: 10,
+2
View File
@@ -2861,6 +2861,8 @@ export interface AppTemplate {
icon?: string; icon?: string;
color?: string; color?: string;
installs?: number; installs?: number;
github?: string;
"hub-docker"?: string;
}; };
image: string; image: string;
defaultPort: string; defaultPort: string;
+128
View File
@@ -14,6 +14,8 @@ export interface AppTemplatePresentation {
icon?: string; icon?: string;
color?: string; color?: string;
installs?: number; installs?: number;
github?: string;
"hub-docker"?: string;
} }
export interface AppTemplate { export interface AppTemplate {
@@ -51,6 +53,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/wordpress/21759B", icon: "https://cdn.simpleicons.org/wordpress/21759B",
installs: 1240, installs: 1240,
github: "https://github.com/WordPress/wordpress-develop",
"hub-docker": "https://hub.docker.com/_/wordpress",
}, },
}, },
{ {
@@ -68,6 +72,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/ghost/21759B", icon: "https://cdn.simpleicons.org/ghost/21759B",
installs: 870, installs: 870,
github: "https://github.com/TryGhost/Ghost",
"hub-docker": "https://hub.docker.com/_/ghost",
}, },
}, },
{ {
@@ -85,6 +91,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/strapi/4945FF", icon: "https://cdn.simpleicons.org/strapi/4945FF",
installs: 620, installs: 620,
github: "https://github.com/strapi/strapi",
"hub-docker": "https://hub.docker.com/r/strapi/strapi",
}, },
}, },
{ {
@@ -101,6 +109,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/drupal/0678BE", icon: "https://cdn.simpleicons.org/drupal/0678BE",
installs: 410, installs: 410,
github: "https://github.com/drupal/drupal",
"hub-docker": "https://hub.docker.com/_/drupal",
}, },
}, },
{ {
@@ -117,6 +127,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/nginx/009639", icon: "https://cdn.simpleicons.org/nginx/009639",
installs: 980, installs: 980,
github: "https://github.com/nginx/nginx",
"hub-docker": "https://hub.docker.com/_/nginx",
}, },
}, },
{ {
@@ -133,6 +145,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/apache/D22128", icon: "https://cdn.simpleicons.org/apache/D22128",
installs: 540, installs: 540,
github: "https://github.com/apache/httpd",
"hub-docker": "https://hub.docker.com/_/httpd",
}, },
}, },
{ {
@@ -150,6 +164,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/caddy/1F88C0", icon: "https://cdn.simpleicons.org/caddy/1F88C0",
installs: 760, installs: 760,
github: "https://github.com/caddyserver/caddy",
"hub-docker": "https://hub.docker.com/_/caddy",
}, },
}, },
{ {
@@ -169,6 +185,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/nodedotjs/5FA04E", icon: "https://cdn.simpleicons.org/nodedotjs/5FA04E",
installs: 650, installs: 650,
github: "https://github.com/nodejs/node",
"hub-docker": "https://hub.docker.com/_/node",
}, },
}, },
{ {
@@ -188,6 +206,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/bun/5FA04E", icon: "https://cdn.simpleicons.org/bun/5FA04E",
installs: 260, installs: 260,
github: "https://github.com/oven-sh/bun",
"hub-docker": "https://hub.docker.com/r/oven/bun",
}, },
}, },
{ {
@@ -205,6 +225,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/fastapi/009688", icon: "https://cdn.simpleicons.org/fastapi/009688",
installs: 330, installs: 330,
github: "https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker",
"hub-docker": "https://hub.docker.com/r/tiangolo/uvicorn-gunicorn-fastapi",
}, },
}, },
{ {
@@ -222,6 +244,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/php/777BB4", icon: "https://cdn.simpleicons.org/php/777BB4",
installs: 290, installs: 290,
github: "https://github.com/php/php-src",
"hub-docker": "https://hub.docker.com/_/php",
}, },
}, },
{ {
@@ -238,6 +262,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
tags: ["PHP", "Framework", "CodeIgniter"], tags: ["PHP", "Framework", "CodeIgniter"],
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/codeigniter/FF2D20", icon: "https://cdn.simpleicons.org/codeigniter/FF2D20",
github: "https://github.com/shinsenter/codeigniter4",
"hub-docker": "https://hub.docker.com/r/shinsenter/codeigniter4",
}, },
}, },
{ {
@@ -258,6 +284,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/laravel/5CD04E", icon: "https://cdn.simpleicons.org/laravel/5CD04E",
installs: 150, installs: 150,
github: "",
"hub-docker": "https://hub.docker.com/r/laravelfans/laravel",
}, },
presets: [ presets: [
{ {
@@ -301,6 +329,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/laravel/5CD04E", icon: "https://cdn.simpleicons.org/laravel/5CD04E",
installs: 230, installs: 230,
github: "https://github.com/bitnami/containers",
"hub-docker": "https://hub.docker.com/r/bitnami/laravel",
}, },
presets: [ presets: [
{ {
@@ -343,6 +373,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/django/092E20", icon: "https://cdn.simpleicons.org/django/092E20",
installs: 410, installs: 410,
github: "https://github.com/django/django",
"hub-docker": "https://hub.docker.com/_/python",
}, },
}, },
{ {
@@ -363,6 +395,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/flask/5FA04E", icon: "https://cdn.simpleicons.org/flask/5FA04E",
installs: 360, installs: 360,
github: "https://github.com/pallets/flask",
"hub-docker": "https://hub.docker.com/_/python",
}, },
}, },
{ {
@@ -382,6 +416,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/goland/00ADD8", icon: "https://cdn.simpleicons.org/goland/00ADD8",
installs: 290, installs: 290,
github: "https://github.com/golang/go",
"hub-docker": "https://hub.docker.com/_/golang",
}, },
}, },
{ {
@@ -401,6 +437,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/rust/5FA04E", icon: "https://cdn.simpleicons.org/rust/5FA04E",
installs: 220, installs: 220,
github: "https://github.com/rust-lang/rust",
"hub-docker": "https://hub.docker.com/_/rust",
}, },
}, },
{ {
@@ -418,6 +456,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
tags: ["DNS", "Adblock"], tags: ["DNS", "Adblock"],
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/adguard/68BC71", icon: "https://cdn.simpleicons.org/adguard/68BC71",
github: "https://github.com/AdguardTeam/AdGuardHome",
"hub-docker": "https://hub.docker.com/r/adguard/adguardhome",
}, },
}, },
{ {
@@ -435,6 +475,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
tags: ["DNS", "Adblock"], tags: ["DNS", "Adblock"],
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/pihole/96060C", icon: "https://cdn.simpleicons.org/pihole/96060C",
github: "https://github.com/pi-hole/pi-hole",
"hub-docker": "https://hub.docker.com/r/pihole/pihole",
}, },
}, },
{ {
@@ -451,6 +493,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
tags: ["DNS", "Dynamic DNS"], tags: ["DNS", "Dynamic DNS"],
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/cloudflare/F38020", icon: "https://cdn.simpleicons.org/cloudflare/F38020",
github: "https://github.com/oznu/docker-cloudflare-ddns",
"hub-docker": "https://hub.docker.com/r/oznu/cloudflare-ddns",
}, },
}, },
{ {
@@ -467,6 +511,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
tags: ["Network", "Tunnel"], tags: ["Network", "Tunnel"],
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/cloudflare/F38020", icon: "https://cdn.simpleicons.org/cloudflare/F38020",
github: "https://github.com/cloudflare/cloudflared",
"hub-docker": "https://hub.docker.com/r/cloudflare/cloudflared",
}, },
}, },
@@ -485,6 +531,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/excalidraw/6965DB", icon: "https://cdn.simpleicons.org/excalidraw/6965DB",
installs: 430, installs: 430,
github: "https://github.com/excalidraw/excalidraw",
"hub-docker": "https://hub.docker.com/r/excalidraw/excalidraw",
}, },
}, },
@@ -502,6 +550,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
tags: ["Database", "Admin"], tags: ["Database", "Admin"],
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/php/777BB4", icon: "https://cdn.simpleicons.org/php/777BB4",
github: "https://github.com/php/php-src",
"hub-docker": "https://hub.docker.com/_/php",
}, },
}, },
{ {
@@ -517,6 +567,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
tags: ["Database", "Admin"], tags: ["Database", "Admin"],
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/phpmyadmin/6C78AF", icon: "https://cdn.simpleicons.org/phpmyadmin/6C78AF",
github: "https://github.com/phpmyadmin/phpmyadmin",
"hub-docker": "https://hub.docker.com/r/phpmyadmin/phpmyadmin",
}, },
}, },
{ {
@@ -534,6 +586,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/phpmyadmin/F38020", icon: "https://cdn.simpleicons.org/phpmyadmin/F38020",
installs: 620, installs: 620,
github: "https://github.com/phpmyadmin/phpmyadmin",
"hub-docker": "https://hub.docker.com/r/phpmyadmin/phpmyadmin",
}, },
}, },
{ {
@@ -552,6 +606,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/postgresql/4169E1", icon: "https://cdn.simpleicons.org/postgresql/4169E1",
installs: 870, installs: 870,
github: "https://github.com/postgres/postgres",
"hub-docker": "https://hub.docker.com/_/postgres",
}, },
}, },
{ {
@@ -570,6 +626,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/mysql/4479A1", icon: "https://cdn.simpleicons.org/mysql/4479A1",
installs: 910, installs: 910,
github: "https://github.com/mysql/mysql-server",
"hub-docker": "https://hub.docker.com/_/mysql",
}, },
}, },
{ {
@@ -588,6 +646,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/mariadb/003545", icon: "https://cdn.simpleicons.org/mariadb/003545",
installs: 430, installs: 430,
github: "https://github.com/MariaDB/server",
"hub-docker": "https://hub.docker.com/_/mariadb",
}, },
}, },
{ {
@@ -606,6 +666,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/mongodb/47A248", icon: "https://cdn.simpleicons.org/mongodb/47A248",
installs: 480, installs: 480,
github: "https://github.com/mongodb/mongo",
"hub-docker": "https://hub.docker.com/_/mongo",
}, },
}, },
{ {
@@ -623,6 +685,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
tags: ["Database", "PostgreSQL", "Supabase"], tags: ["Database", "PostgreSQL", "Supabase"],
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/supabase/3ECF8E", icon: "https://cdn.simpleicons.org/supabase/3ECF8E",
github: "https://github.com/supabase/postgres",
"hub-docker": "https://hub.docker.com/r/supabase/postgres",
}, },
}, },
{ {
@@ -640,6 +704,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
tags: ["Database", "Supabase"], tags: ["Database", "Supabase"],
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/supabase/3ECF8E", icon: "https://cdn.simpleicons.org/supabase/3ECF8E",
github: "",
"hub-docker": "https://hub.docker.com/r/khulnasoft/supabase-all-in-one",
}, },
}, },
{ {
@@ -657,6 +723,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/redis/DC382D", icon: "https://cdn.simpleicons.org/redis/DC382D",
installs: 720, installs: 720,
github: "https://github.com/redis/redis",
"hub-docker": "https://hub.docker.com/_/redis",
}, },
}, },
{ {
@@ -672,6 +740,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
tags: ["Cache", "Memory"], tags: ["Cache", "Memory"],
presentation: { presentation: {
installs: 190, installs: 190,
github: "https://github.com/memcached/memcached",
"hub-docker": "https://hub.docker.com/_/memcached",
}, },
}, },
{ {
@@ -689,6 +759,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/rabbitmq/FF6600", icon: "https://cdn.simpleicons.org/rabbitmq/FF6600",
installs: 510, installs: 510,
github: "https://github.com/rabbitmq/rabbitmq-server",
"hub-docker": "https://hub.docker.com/_/rabbitmq",
}, },
}, },
{ {
@@ -706,6 +778,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/grafana/F46800", icon: "https://cdn.simpleicons.org/grafana/F46800",
installs: 430, installs: 430,
github: "https://github.com/grafana/grafana",
"hub-docker": "https://hub.docker.com/r/grafana/grafana",
}, },
}, },
{ {
@@ -723,6 +797,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/prometheus/E6522C", icon: "https://cdn.simpleicons.org/prometheus/E6522C",
installs: 390, installs: 390,
github: "https://github.com/prometheus/prometheus",
"hub-docker": "https://hub.docker.com/r/prom/prometheus",
}, },
}, },
{ {
@@ -739,6 +815,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/grafana/F46800", icon: "https://cdn.simpleicons.org/grafana/F46800",
installs: 180, installs: 180,
github: "https://github.com/grafana/loki",
"hub-docker": "https://hub.docker.com/r/grafana/loki",
}, },
}, },
{ {
@@ -756,6 +834,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/uptimekuma/5CDD8B", icon: "https://cdn.simpleicons.org/uptimekuma/5CDD8B",
installs: 520, installs: 520,
github: "https://github.com/louislam/uptime-kuma",
"hub-docker": "https://hub.docker.com/r/louislam/uptime-kuma",
}, },
}, },
{ {
@@ -789,6 +869,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/traefikproxy/24A1C1", icon: "https://cdn.simpleicons.org/traefikproxy/24A1C1",
installs: 560, installs: 560,
github: "https://github.com/traefik/traefik",
"hub-docker": "https://hub.docker.com/_/traefik",
}, },
}, },
{ {
@@ -806,6 +888,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/nginxproxymanager/F15833", icon: "https://cdn.simpleicons.org/nginxproxymanager/F15833",
installs: 610, installs: 610,
github: "https://github.com/NginxProxyManager/nginx-proxy-manager",
"hub-docker": "https://hub.docker.com/r/jc21/nginx-proxy-manager",
}, },
}, },
{ {
@@ -836,6 +920,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/minio/C72E49", icon: "https://cdn.simpleicons.org/minio/C72E49",
installs: 210, installs: 210,
github: "https://github.com/minio/minio",
"hub-docker": "https://hub.docker.com/r/minio/minio",
}, },
}, },
{ {
@@ -853,6 +939,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/nextcloud/0082C9", icon: "https://cdn.simpleicons.org/nextcloud/0082C9",
installs: 700, installs: 700,
github: "https://github.com/nextcloud/server",
"hub-docker": "https://hub.docker.com/_/nextcloud",
}, },
}, },
{ {
@@ -869,6 +957,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
tags: ["Files", "UI"], tags: ["Files", "UI"],
presentation: { presentation: {
installs: 280, installs: 280,
github: "https://github.com/filebrowser/filebrowser",
"hub-docker": "https://hub.docker.com/r/filebrowser/filebrowser",
}, },
}, },
{ {
@@ -887,6 +977,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/immich/4250AF", icon: "https://cdn.simpleicons.org/immich/4250AF",
installs: 460, installs: 460,
github: "https://github.com/immich-app/immich",
"hub-docker": "ghcr.io/imagegenius/immich:latest",
}, },
presets: [ presets: [
{ {
@@ -921,6 +1013,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/n8n/EA4B71", icon: "https://cdn.simpleicons.org/n8n/EA4B71",
installs: 680, installs: 680,
github: "https://github.com/n8n-io/n8n",
"hub-docker": "https://hub.docker.com/r/n8nio/n8n",
}, },
presets: [ presets: [
{ {
@@ -952,6 +1046,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
tags: ["AI", "LLM", "Chat"], tags: ["AI", "LLM", "Chat"],
presentation: { presentation: {
installs: 590, installs: 590,
github: "https://github.com/open-webui/open-webui",
"hub-docker": "",
}, },
presets: [ presets: [
{ {
@@ -986,6 +1082,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://openclaw.ai/favicon-32.png", icon: "https://openclaw.ai/favicon-32.png",
installs: 750, installs: 750,
github: "https://github.com/openclaw/openclaw",
"hub-docker": "",
}, },
}, },
{ {
@@ -1004,6 +1102,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://openclaw.ai/favicon-32.png", icon: "https://openclaw.ai/favicon-32.png",
installs: 750, installs: 750,
github: "https://github.com/openclaw/openclaw",
"hub-docker": "https://hub.docker.com/r/alpine/openclaw",
}, },
}, },
{ {
@@ -1021,6 +1121,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/openrouter/2496ED", icon: "https://cdn.simpleicons.org/openrouter/2496ED",
installs: 480, installs: 480,
github: "https://github.com/decolua/9router",
"hub-docker": "https://hub.docker.com/r/decolua/9router",
}, },
presets: [ presets: [
{ {
@@ -1052,6 +1154,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
tags: ["Container", "Docker"], tags: ["Container", "Docker"],
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/docker/2496ED", icon: "https://cdn.simpleicons.org/docker/2496ED",
github: "https://github.com/moby/moby",
"hub-docker": "https://hub.docker.com/_/docker",
}, },
}, },
{ {
@@ -1070,6 +1174,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/portainer/13BEF9", icon: "https://cdn.simpleicons.org/portainer/13BEF9",
installs: 340, installs: 340,
github: "https://github.com/portainer/portainer",
"hub-docker": "https://hub.docker.com/r/portainer/portainer-ce",
}, },
}, },
{ {
@@ -1087,6 +1193,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/gitea/609926", icon: "https://cdn.simpleicons.org/gitea/609926",
installs: 590, installs: 590,
github: "https://github.com/go-gitea/gitea",
"hub-docker": "https://hub.docker.com/r/gitea/gitea",
}, },
}, },
{ {
@@ -1104,6 +1212,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/jenkins/D24939", icon: "https://cdn.simpleicons.org/jenkins/D24939",
installs: 620, installs: 620,
github: "https://github.com/jenkinsci/jenkins",
"hub-docker": "https://hub.docker.com/r/jenkins/jenkins",
}, },
}, },
{ {
@@ -1121,6 +1231,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/docker/2496ED", icon: "https://cdn.simpleicons.org/docker/2496ED",
installs: 150, installs: 150,
github: "https://github.com/distribution/distribution",
"hub-docker": "https://hub.docker.com/_/registry",
}, },
}, },
@@ -1139,6 +1251,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/maildotru/005FF9", icon: "https://cdn.simpleicons.org/maildotru/005FF9",
installs: 220, installs: 220,
github: "https://github.com/mailhog/MailHog",
"hub-docker": "https://hub.docker.com/r/mailhog/mailhog",
}, },
}, },
{ {
@@ -1156,6 +1270,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/redis/DC382D", icon: "https://cdn.simpleicons.org/redis/DC382D",
installs: 200, installs: 200,
github: "https://github.com/RedisInsight/RedisInsight",
"hub-docker": "https://hub.docker.com/r/redislabs/redisinsight",
}, },
}, },
{ {
@@ -1173,6 +1289,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/redis/DC382D", icon: "https://cdn.simpleicons.org/redis/DC382D",
installs: 180, installs: 180,
github: "https://github.com/joeferner/redis-commander",
"hub-docker": "https://hub.docker.com/r/rediscommander/redis-commander",
}, },
}, },
@@ -1192,6 +1310,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/postgresql/4169E1", icon: "https://cdn.simpleicons.org/postgresql/4169E1",
installs: 270, installs: 270,
github: "https://github.com/pgadmin-org/pgadmin4",
"hub-docker": "https://hub.docker.com/r/dpage/pgadmin4",
}, },
}, },
{ {
@@ -1209,6 +1329,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/adminer/8C7EE0", icon: "https://cdn.simpleicons.org/adminer/8C7EE0",
installs: 320, installs: 320,
github: "https://github.com/vrana/adminer",
"hub-docker": "https://hub.docker.com/_/adminer",
}, },
}, },
@@ -1228,6 +1350,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/whatsapp/25D366", icon: "https://cdn.simpleicons.org/whatsapp/25D366",
installs: 80, installs: 80,
github: "https://github.com/asternic/wuzapi",
"hub-docker": "https://hub.docker.com/r/asternic/wuzapi",
}, },
}, },
{ {
@@ -1247,6 +1371,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/whatsapp/25D366", icon: "https://cdn.simpleicons.org/whatsapp/25D366",
installs: 60, installs: 60,
github: "https://github.com/devlikeapro/waha",
"hub-docker": "https://hub.docker.com/r/devlikeapro/waha",
}, },
}, },
{ {
@@ -1264,6 +1390,8 @@ const BASE_APP_TEMPLATES: AppTemplate[] = [
presentation: { presentation: {
icon: "https://cdn.simpleicons.org/umami/00BFFF", icon: "https://cdn.simpleicons.org/umami/00BFFF",
installs: 50, installs: 50,
github: "https://github.com/umami-software/umami",
"hub-docker": "https://hub.docker.com/r/umamisoftware/umami",
}, },
}, },
]; ];
+9
View File
@@ -598,12 +598,21 @@ const templatePresetSchema = z.object({
restartPolicy: z.string().optional(), restartPolicy: z.string().optional(),
}); });
const templatePresentationSchema = z.object({
icon: z.string().optional(),
color: z.string().optional(),
installs: z.number().optional(),
github: z.string().optional(),
"hub-docker": z.string().optional(),
});
const templateSnapshotSchema = z.object({ const templateSnapshotSchema = z.object({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
desc: z.string(), desc: z.string(),
category: z.string(), category: z.string(),
icon: z.string().url().optional(), icon: z.string().url().optional(),
presentation: templatePresentationSchema.optional(),
image: z.string().min(1), image: z.string().min(1),
defaultPort: z.string(), defaultPort: z.string(),
defaultEnv: z.string(), defaultEnv: z.string(),
+10 -1
View File
@@ -214,13 +214,22 @@ function normalizePresentation(
const icon = resolveRemoteAssetUrl(presentation.icon, baseUrl); const icon = resolveRemoteAssetUrl(presentation.icon, baseUrl);
const color = asString(presentation.color); const color = asString(presentation.color);
const installs = asNumber(presentation.installs); const installs = asNumber(presentation.installs);
const github = asString(presentation.github);
const dockerHub =
asString(presentation["hub-docker"]) ??
asString(presentation.hubDocker) ??
asString(presentation.dockerHub);
if (!icon && !color && installs === undefined) return undefined; if (!icon && !color && installs === undefined && !github && !dockerHub) {
return undefined;
}
return { return {
...(icon ? { icon } : {}), ...(icon ? { icon } : {}),
...(color ? { color } : {}), ...(color ? { color } : {}),
...(installs !== undefined ? { installs } : {}), ...(installs !== undefined ? { installs } : {}),
...(github ? { github } : {}),
...(dockerHub ? { "hub-docker": dockerHub } : {}),
}; };
} }
@@ -44,6 +44,8 @@ test("generic app template keeps its explicit category", () => {
icon: "icon.png", icon: "icon.png",
color: "#f59e0b", color: "#f59e0b",
installs: 42, installs: 42,
github: "https://github.com/henrygd/beszel",
"hub-docker": "https://hub.docker.com/r/henrygd/beszel",
}, },
}, },
"beszel", "beszel",
@@ -57,6 +59,8 @@ test("generic app template keeps its explicit category", () => {
icon: "https://raw.githubusercontent.com/DoktainerApp/templates/main/templates/beszel/icon.png", icon: "https://raw.githubusercontent.com/DoktainerApp/templates/main/templates/beszel/icon.png",
color: "#f59e0b", color: "#f59e0b",
installs: 42, installs: 42,
github: "https://github.com/henrygd/beszel",
"hub-docker": "https://hub.docker.com/r/henrygd/beszel",
}); });
}); });