mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
feat: public template catalog + Vercel git integration
Backend: service_templates.is_public lets owners publish user templates to the anonymous catalog (GET /api/v1/templates), mirroring Railway's marketplace publish flow. Migration, sqlc queries, visibility rules, and openapi.yaml updated; types regenerated. App: template editor gains a "List in the public catalog" checkbox; Public badge on catalog cards and detail view. Site: new templates.html catalog page - search, category filters, Official vs Community badges, detail modal with variables and copyable import JSON. Fetches the live catalog from a reachable instance (?api= or localStorage override) and falls back to templates.json, generated from SeedTemplates via cmd/dumptemplates. Shared styles extracted to style.css. Vercel: project connected to GitHub repo with rootDirectory=site; .vercelignore limits CLI uploads to site/ so `vercel deploy --prod` from repo root matches git builds. Authored By: TDvorak <[email protected]> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
2b38406a63
commit
117b469fae
18 files changed
+1589
-492
No files matched your search
@@ -48,6 +48,7 @@ const demoTemplates: TemplateEntity[] = [
|
||||
variablesRaw: '[]',
|
||||
isOfficial: true,
|
||||
ownerId: null,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: 'tpl-go',
|
||||
@@ -59,6 +60,7 @@ const demoTemplates: TemplateEntity[] = [
|
||||
variablesRaw: '[]',
|
||||
isOfficial: true,
|
||||
ownerId: null,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: 'tpl-postgres',
|
||||
@@ -70,6 +72,7 @@ const demoTemplates: TemplateEntity[] = [
|
||||
variablesRaw: '[]',
|
||||
isOfficial: true,
|
||||
ownerId: null,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: 'tpl-mysql',
|
||||
@@ -81,6 +84,7 @@ const demoTemplates: TemplateEntity[] = [
|
||||
variablesRaw: '[]',
|
||||
isOfficial: true,
|
||||
ownerId: null,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: 'tpl-mariadb',
|
||||
@@ -92,6 +96,7 @@ const demoTemplates: TemplateEntity[] = [
|
||||
variablesRaw: '[]',
|
||||
isOfficial: true,
|
||||
ownerId: null,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: 'tpl-clickhouse',
|
||||
@@ -103,6 +108,7 @@ const demoTemplates: TemplateEntity[] = [
|
||||
variablesRaw: '[]',
|
||||
isOfficial: true,
|
||||
ownerId: null,
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
id: 'tpl-dragonfly',
|
||||
@@ -114,6 +120,7 @@ const demoTemplates: TemplateEntity[] = [
|
||||
variablesRaw: '[]',
|
||||
isOfficial: true,
|
||||
ownerId: null,
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -445,6 +452,7 @@ export function TemplatesPage() {
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
const [editorJson, setEditorJson] = useState(EMPTY_TEMPLATE_JSON);
|
||||
const [editingTemplate, setEditingTemplate] = useState<TemplateEntity | null>(null);
|
||||
const [editorIsPublic, setEditorIsPublic] = useState(false);
|
||||
const [editorError, setEditorError] = useState<string | null>(null);
|
||||
|
||||
const [categoryFilter, setCategoryFilter] = useState('');
|
||||
@@ -655,6 +663,7 @@ export function TemplatesPage() {
|
||||
const openCreateEditor = () => {
|
||||
setEditingTemplate(null);
|
||||
setEditorJson(EMPTY_TEMPLATE_JSON);
|
||||
setEditorIsPublic(false);
|
||||
setEditorError(null);
|
||||
setEditorOpen(true);
|
||||
};
|
||||
@@ -687,6 +696,7 @@ export function TemplatesPage() {
|
||||
2,
|
||||
),
|
||||
);
|
||||
setEditorIsPublic(template.isPublic);
|
||||
setEditorError(null);
|
||||
setEditorOpen(true);
|
||||
};
|
||||
@@ -711,6 +721,7 @@ export function TemplatesPage() {
|
||||
logo: body.logo ?? '',
|
||||
config: body.config as Record<string, unknown>,
|
||||
variables: Array.isArray(body.variables) ? body.variables : [],
|
||||
isPublic: editorIsPublic,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -880,6 +891,11 @@ export function TemplatesPage() {
|
||||
Mine
|
||||
</span>
|
||||
) : null}
|
||||
{!template.isOfficial && template.isPublic ? (
|
||||
<span className="px-1.5 py-0.5 rounded bg-[var(--info-soft)] text-[var(--info)] text-[10px] font-semibold">
|
||||
Public
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span
|
||||
@@ -955,6 +971,11 @@ export function TemplatesPage() {
|
||||
Official
|
||||
</span>
|
||||
)}
|
||||
{!selectedDetail.template.isOfficial && selectedDetail.template.isPublic && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-[var(--info-soft)] text-[var(--info)] text-xs font-medium">
|
||||
Public
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-1">{selectedDetail.template.description}</p>
|
||||
</div>
|
||||
@@ -1230,9 +1251,15 @@ export function TemplatesPage() {
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center justify-between border-t border-[var(--border-subtle)] px-5 py-3.5">
|
||||
<p className="text-[11px] text-[var(--text-tertiary)]">
|
||||
Saved as a user template — only you (and admins) can edit or delete it.
|
||||
</p>
|
||||
<label className="inline-flex cursor-pointer items-center gap-2 text-[11px] text-[var(--text-secondary)]">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editorIsPublic}
|
||||
onChange={(e) => setEditorIsPublic(e.target.checked)}
|
||||
className="h-3.5 w-3.5 accent-[var(--accent-primary)]"
|
||||
/>
|
||||
List in the public catalog - anyone browsing this instance can see and deploy it.
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -7806,6 +7806,8 @@ export interface components {
|
||||
is_official?: boolean;
|
||||
/** @description Owning user ID for user-created templates; null for official presets */
|
||||
owner_id?: string | null;
|
||||
/** @description Whether a user-created template is published to the public catalog */
|
||||
is_public?: boolean;
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description Creation timestamp
|
||||
@@ -7830,6 +7832,8 @@ export interface components {
|
||||
config: Record<string, never>;
|
||||
/** @description Configurable variables exposed at deploy time */
|
||||
variables?: components["schemas"]["TemplateVariable"][];
|
||||
/** @description Publish this template to the public catalog (visible anonymously) */
|
||||
is_public?: boolean;
|
||||
};
|
||||
TemplateConfig: {
|
||||
/** @description Service type for created service */
|
||||
|
||||
@@ -124,6 +124,7 @@ export type TemplateEntity = {
|
||||
variablesRaw: string;
|
||||
isOfficial: boolean;
|
||||
ownerId: string | null;
|
||||
isPublic: boolean;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
@@ -615,6 +616,7 @@ function normalizeTemplate(template: RawServiceTemplate): TemplateEntity | null
|
||||
variablesRaw: template.variables ?? '',
|
||||
isOfficial: Boolean(template.is_official),
|
||||
ownerId: template.owner_id ?? null,
|
||||
isPublic: Boolean(template.is_public),
|
||||
createdAt: template.created_at,
|
||||
updatedAt: template.updated_at,
|
||||
};
|
||||
@@ -1358,6 +1360,7 @@ export type TemplateWriteInput = {
|
||||
logo?: string;
|
||||
config: Record<string, unknown>;
|
||||
variables?: TemplateVariableEntity[];
|
||||
isPublic?: boolean;
|
||||
};
|
||||
|
||||
function templateWriteBody(input: TemplateWriteInput): string {
|
||||
@@ -1368,6 +1371,7 @@ function templateWriteBody(input: TemplateWriteInput): string {
|
||||
logo: input.logo ?? '',
|
||||
config: input.config,
|
||||
variables: input.variables ?? [],
|
||||
is_public: input.isPublic ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user