Files

393 lines
19 KiB
HTML

{{ define "repository" }}
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ if .Repository.Title }}{{ .Repository.Title }}{{ else }}{{ .Owner.Handle }}/{{ .Repository.Name }}{{ end }} - ATCR</title>
<!-- Open Graph -->
<meta property="og:title" content="{{ .Owner.Handle }}/{{ .Repository.Name }} - ATCR">
<meta property="og:description" content="{{ if .Repository.Description }}{{ .Repository.Description }}{{ else }}Container image on ATCR{{ end }}">
<meta property="og:image" content="https://{{ .RegistryURL }}/og/r/{{ .Owner.Handle }}/{{ .Repository.Name }}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:type" content="website">
<meta property="og:url" content="https://{{ .RegistryURL }}/r/{{ .Owner.Handle }}/{{ .Repository.Name }}">
<meta property="og:site_name" content="ATCR">
<!-- Twitter Card (used by Discord) -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ .Owner.Handle }}/{{ .Repository.Name }} - ATCR">
<meta name="twitter:description" content="{{ if .Repository.Description }}{{ .Repository.Description }}{{ else }}Container image on ATCR{{ end }}">
<meta name="twitter:image" content="https://{{ .RegistryURL }}/og/r/{{ .Owner.Handle }}/{{ .Repository.Name }}">
{{ template "head" . }}
</head>
<body>
{{ template "nav" . }}
<main class="container">
<div class="repository-page">
<!-- Repository Header -->
<div class="repository-header">
<div class="repo-hero">
<div class="repo-hero-icon-wrapper">
{{ if .Repository.IconURL }}
<img src="{{ .Repository.IconURL }}" alt="{{ .Repository.Name }}" class="repo-hero-icon">
{{ else }}
<div class="repo-hero-icon-placeholder">{{ firstChar .Repository.Name }}</div>
{{ end }}
{{ if $.IsOwner }}
<label class="avatar-upload-overlay" for="avatar-upload">
<i data-lucide="plus"></i>
</label>
<input type="file" id="avatar-upload" accept="image/png,image/jpeg,image/webp"
onchange="uploadAvatar(this, '{{ .Repository.Name }}')" hidden>
{{ end }}
</div>
<div class="repo-hero-info">
<h1>
<a href="/u/{{ .Owner.Handle }}" class="owner-link">{{ .Owner.Handle }}</a>
<span class="repo-separator">/</span>
<span class="repo-name">{{ .Repository.Name }}</span>
</h1>
{{ if .Repository.Description }}
<p class="repo-hero-description">{{ .Repository.Description }}</p>
{{ end }}
</div>
</div>
<!-- Star Button and Metadata Row -->
<div class="repo-info-row">
<div class="repo-actions">
<button class="star-btn{{ if .IsStarred }} starred{{ end }}" id="star-btn" onclick="toggleStar('{{ .Owner.Handle }}', '{{ .Repository.Name }}')">
<i data-lucide="star" class="star-icon{{ if .IsStarred }} star-filled{{ end }}" id="star-icon"></i>
<span class="star-count" id="star-count">{{ .StarCount }}</span>
</button>
</div>
<!-- Metadata Section -->
{{ if or .Repository.Licenses .Repository.SourceURL .Repository.DocumentationURL .Repository.Version }}
<div class="repo-metadata">
{{ if .Repository.Version }}
<span class="metadata-badge version-badge" title="Version">
{{ .Repository.Version }}
</span>
{{ end }}
{{ if .Repository.Licenses }}
{{ range parseLicenses .Repository.Licenses }}
{{ if .IsValid }}
<a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" class="metadata-badge license-badge" title="{{ .Name }}">
{{ .SPDXID }}
</a>
{{ else }}
<span class="metadata-badge license-badge" title="Custom license: {{ .Name }}">
{{ .Name }}
</span>
{{ end }}
{{ end }}
{{ end }}
{{ if .Repository.SourceURL }}
<a href="{{ .Repository.SourceURL }}" target="_blank" class="metadata-link">
Source
</a>
{{ end }}
{{ if .Repository.DocumentationURL }}
<a href="{{ .Repository.DocumentationURL }}" target="_blank" class="metadata-link">
Documentation
</a>
{{ end }}
</div>
{{ else }}
<div class="repo-metadata"></div>
{{ end }}
</div>
<!-- Pull Command -->
<div class="pull-command-section">
<h3>Pull this image</h3>
{{ if .Tags }}
{{ $firstTag := index .Tags 0 }}
{{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" $firstTag.Tag.Tag) }}
{{ else }}
{{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":latest") }}
{{ end }}
</div>
</div>
<!-- README and Tags/Manifests Layout -->
{{ if .ReadmeHTML }}
<div class="repo-content-layout">
<!-- README Section (Left) -->
<div class="readme-section">
<h2>Overview</h2>
<div class="readme-content markdown-body">
{{ .ReadmeHTML }}
</div>
</div>
<!-- Tags and Manifests (Right) -->
<div class="repo-sidebar">
{{ end }}
<!-- Tags Section -->
<div class="repo-section">
<h2>Tags</h2>
{{ if .Tags }}
<div class="tags-list">
{{ range .Tags }}
<div class="tag-item" id="tag-{{ sanitizeID .Tag.Tag }}">
<div class="tag-item-header">
<div>
<span class="tag-name-large">{{ .Tag.Tag }}</span>
{{ if .IsMultiArch }}
<span class="badge-multi">Multi-arch</span>
{{ end }}
{{ if .HasAttestations }}
<span class="badge-attestation"><i data-lucide="shield-check"></i> Attestations</span>
{{ end }}
</div>
<div style="display: flex; gap: 1rem; align-items: center;">
<time class="tag-timestamp" datetime="{{ .Tag.CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}">
{{ timeAgo .Tag.CreatedAt }}
</time>
{{ if $.IsOwner }}
<button class="delete-btn"
hx-delete="/api/images/{{ $.Repository.Name }}/tags/{{ .Tag.Tag }}"
hx-confirm="Delete tag {{ .Tag.Tag }}?"
hx-target="#tag-{{ sanitizeID .Tag.Tag }}"
hx-swap="outerHTML">
<i data-lucide="trash-2"></i>
</button>
{{ end }}
</div>
</div>
<div class="tag-item-details">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div class="digest-container">
<code class="digest" title="{{ .Tag.Digest }}">{{ .Tag.Digest }}</code>
<button class="digest-copy-btn" onclick="copyToClipboard('{{ .Tag.Digest }}')"><i data-lucide="copy"></i></button>
</div>
{{ if .Platforms }}
<div class="platforms-inline">
{{ range .Platforms }}
<span class="platform-badge">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span>
{{ end }}
</div>
{{ end }}
</div>
</div>
{{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" .Tag.Tag) }}
</div>
{{ end }}
</div>
{{ else }}
<p class="empty-message">No tags available</p>
{{ end }}
</div>
<!-- Manifests Section -->
<div class="repo-section">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2>Manifests</h2>
<label class="show-offline-toggle">
<input type="checkbox" id="show-offline-toggle" onchange="toggleOfflineManifests()">
<span>Show offline images</span>
</label>
</div>
{{ if .Manifests }}
<div class="manifests-list">
{{ range .Manifests }}
<div class="manifest-item" id="manifest-{{ sanitizeID .Manifest.Digest }}" data-reachable="{{ .Reachable }}">
<div class="manifest-item-header">
<div>
{{ if .IsManifestList }}
<span class="manifest-type"><i data-lucide="package"></i> Multi-arch</span>
{{ else }}
<span class="manifest-type"><i data-lucide="file-text"></i> Image</span>
{{ end }}
{{ if .HasAttestations }}
<span class="badge-attestation"><i data-lucide="shield-check"></i> Attestations</span>
{{ end }}
{{ if .Pending }}
<span class="checking-badge"
hx-get="/api/manifest-health?endpoint={{ .Manifest.HoldEndpoint | urlquery }}"
hx-trigger="load delay:2s"
hx-swap="outerHTML">
<i data-lucide="refresh-ccw"></i> Checking...
</span>
{{ else if not .Reachable }}
<span class="offline-badge"><i data-lucide="alert-triangle"></i> Offline</span>
{{ end }}
<div class="digest-container">
<code class="digest manifest-digest" title="{{ .Manifest.Digest }}">{{ .Manifest.Digest }}</code>
<button class="digest-copy-btn" onclick="copyToClipboard('{{ .Manifest.Digest }}')"><i data-lucide="copy"></i></button>
</div>
</div>
<div style="display: flex; gap: 1rem; align-items: center;">
<time datetime="{{ .Manifest.CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}">
{{ timeAgo .Manifest.CreatedAt }}
</time>
{{ if $.IsOwner }}
<button class="delete-btn"
onclick="deleteManifest('{{ $.Repository.Name }}', '{{ .Manifest.Digest }}', '{{ sanitizeID .Manifest.Digest }}')">
<i data-lucide="trash-2"></i>
</button>
{{ end }}
</div>
</div>
<div class="manifest-item-details">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
{{ if .Tags }}
<span class="manifest-detail-label">Tags:</span>
{{ range $index, $tag := .Tags }}{{ if $index }}, {{ end }}{{ $tag }}{{ end }}
{{ else }}
<span class="text-muted">(untagged)</span>
{{ end }}
</div>
{{ if .IsManifestList }}
{{ if .Platforms }}
<div class="platforms-inline">
{{ range .Platforms }}
<span class="platform-badge">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span>
{{ end }}
</div>
{{ end }}
{{ end }}
</div>
</div>
</div>
{{ end }}
</div>
{{ else }}
<p class="empty-message">No manifests available</p>
{{ end }}
</div>
{{ if .ReadmeHTML }}
</div><!-- Close repo-sidebar -->
</div><!-- Close repo-content-layout -->
{{ end }}
</div>
</main>
<!-- Modal container for HTMX -->
<div id="modal"></div>
<!-- Manifest Delete Confirmation Modal -->
<div id="manifest-delete-modal" class="modal-overlay" style="display: none;">
<div class="modal-dialog">
<div class="modal-header">
<h3>Confirm Deletion</h3>
<button class="modal-close" onclick="closeManifestDeleteModal()">&times;</button>
</div>
<div class="modal-body">
<p id="manifest-delete-message">This manifest has associated tags that will also be deleted:</p>
<ul id="manifest-delete-tags" class="tag-list"></ul>
<p><strong>This action cannot be undone.</strong></p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeManifestDeleteModal()">Cancel</button>
<button class="btn btn-danger" id="confirm-manifest-delete-btn">Delete All</button>
</div>
</div>
</div>
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-dialog {
background: var(--bg-secondary, #1a1a1a);
border: 1px solid var(--border-color, #333);
border-radius: 8px;
max-width: 500px;
width: 90%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.modal-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color, #333);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 1.25rem;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-color, #fff);
padding: 0;
width: 2rem;
height: 2rem;
line-height: 1;
}
.modal-body {
padding: 1.5rem;
}
.modal-body .tag-list {
margin: 1rem 0;
padding-left: 1.5rem;
}
.modal-body .tag-list li {
margin: 0.5rem 0;
font-family: monospace;
}
.modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid var(--border-color, #333);
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn-secondary {
background: var(--bg-tertiary, #2a2a2a);
color: var(--text-color, #fff);
}
.btn-secondary:hover {
background: var(--bg-hover, #3a3a3a);
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
</style>
</body>
</html>
{{ end }}