diff --git a/pkg/appview/db/models.go b/pkg/appview/db/models.go
index 175a449..e5405ee 100644
--- a/pkg/appview/db/models.go
+++ b/pkg/appview/db/models.go
@@ -70,6 +70,7 @@ type Push struct {
IconURL string
StarCount int
PullCount int
+ IsStarred bool // Whether the current user has starred this repository
CreatedAt time.Time
HoldEndpoint string // Hold endpoint for health checking
Reachable bool // Whether the hold endpoint is reachable
@@ -114,6 +115,7 @@ type FeaturedRepository struct {
IconURL string
StarCount int
PullCount int
+ IsStarred bool // Whether the current user has starred this repository
}
// RepositoryWithStats combines repository data with statistics
@@ -131,6 +133,7 @@ type RepoCardData struct {
IconURL string
StarCount int
PullCount int
+ IsStarred bool // Whether the current user has starred this repository
}
// PlatformInfo represents platform information (OS/Architecture)
diff --git a/pkg/appview/db/queries.go b/pkg/appview/db/queries.go
index ef63217..3bededc 100644
--- a/pkg/appview/db/queries.go
+++ b/pkg/appview/db/queries.go
@@ -31,7 +31,7 @@ func escapeLikePattern(s string) string {
}
// GetRecentPushes fetches recent pushes with pagination
-func GetRecentPushes(db *sql.DB, limit, offset int, userFilter string) ([]Push, int, error) {
+func GetRecentPushes(db *sql.DB, limit, offset int, userFilter string, currentUserDID string) ([]Push, int, error) {
query := `
SELECT
u.did,
@@ -44,6 +44,7 @@ func GetRecentPushes(db *sql.DB, limit, offset int, userFilter string) ([]Push,
COALESCE((SELECT value FROM repository_annotations WHERE did = u.did AND repository = t.repository AND key = 'io.atcr.icon'), ''),
COALESCE(rs.pull_count, 0),
COALESCE((SELECT COUNT(*) FROM stars WHERE owner_did = u.did AND repository = t.repository), 0),
+ COALESCE((SELECT COUNT(*) FROM stars WHERE starrer_did = ? AND owner_did = u.did AND repository = t.repository), 0),
t.created_at,
m.hold_endpoint
FROM tags t
@@ -52,7 +53,7 @@ func GetRecentPushes(db *sql.DB, limit, offset int, userFilter string) ([]Push,
LEFT JOIN repository_stats rs ON t.did = rs.did AND t.repository = rs.repository
`
- args := []any{}
+ args := []any{currentUserDID}
if userFilter != "" {
query += " WHERE u.handle = ? OR u.did = ?"
@@ -71,9 +72,11 @@ func GetRecentPushes(db *sql.DB, limit, offset int, userFilter string) ([]Push,
var pushes []Push
for rows.Next() {
var p Push
- if err := rows.Scan(&p.DID, &p.Handle, &p.Repository, &p.Tag, &p.Digest, &p.Title, &p.Description, &p.IconURL, &p.PullCount, &p.StarCount, &p.CreatedAt, &p.HoldEndpoint); err != nil {
+ var isStarredInt int
+ if err := rows.Scan(&p.DID, &p.Handle, &p.Repository, &p.Tag, &p.Digest, &p.Title, &p.Description, &p.IconURL, &p.PullCount, &p.StarCount, &isStarredInt, &p.CreatedAt, &p.HoldEndpoint); err != nil {
return nil, 0, err
}
+ p.IsStarred = isStarredInt > 0
pushes = append(pushes, p)
}
@@ -95,7 +98,7 @@ func GetRecentPushes(db *sql.DB, limit, offset int, userFilter string) ([]Push,
}
// SearchPushes searches for pushes matching the query across handles, DIDs, repositories, and annotations
-func SearchPushes(db *sql.DB, query string, limit, offset int) ([]Push, int, error) {
+func SearchPushes(db *sql.DB, query string, limit, offset int, currentUserDID string) ([]Push, int, error) {
// Escape LIKE wildcards so they're treated literally
query = escapeLikePattern(query)
@@ -114,6 +117,7 @@ func SearchPushes(db *sql.DB, query string, limit, offset int) ([]Push, int, err
COALESCE((SELECT value FROM repository_annotations WHERE did = u.did AND repository = t.repository AND key = 'io.atcr.icon'), ''),
COALESCE(rs.pull_count, 0),
COALESCE((SELECT COUNT(*) FROM stars WHERE owner_did = u.did AND repository = t.repository), 0),
+ COALESCE((SELECT COUNT(*) FROM stars WHERE starrer_did = ? AND owner_did = u.did AND repository = t.repository), 0),
t.created_at,
m.hold_endpoint
FROM tags t
@@ -132,7 +136,7 @@ func SearchPushes(db *sql.DB, query string, limit, offset int) ([]Push, int, err
LIMIT ? OFFSET ?
`
- rows, err := db.Query(sqlQuery, searchPattern, query, searchPattern, searchPattern, limit, offset)
+ rows, err := db.Query(sqlQuery, currentUserDID, searchPattern, query, searchPattern, searchPattern, limit, offset)
if err != nil {
return nil, 0, err
}
@@ -141,9 +145,11 @@ func SearchPushes(db *sql.DB, query string, limit, offset int) ([]Push, int, err
var pushes []Push
for rows.Next() {
var p Push
- if err := rows.Scan(&p.DID, &p.Handle, &p.Repository, &p.Tag, &p.Digest, &p.Title, &p.Description, &p.IconURL, &p.PullCount, &p.StarCount, &p.CreatedAt, &p.HoldEndpoint); err != nil {
+ var isStarredInt int
+ if err := rows.Scan(&p.DID, &p.Handle, &p.Repository, &p.Tag, &p.Digest, &p.Title, &p.Description, &p.IconURL, &p.PullCount, &p.StarCount, &isStarredInt, &p.CreatedAt, &p.HoldEndpoint); err != nil {
return nil, 0, err
}
+ p.IsStarred = isStarredInt > 0
pushes = append(pushes, p)
}
@@ -1571,7 +1577,7 @@ func (m *MetricsDB) IncrementPushCount(did, repository string) error {
}
// GetFeaturedRepositories fetches top repositories sorted by stars and pulls
-func GetFeaturedRepositories(db *sql.DB, limit int) ([]FeaturedRepository, error) {
+func GetFeaturedRepositories(db *sql.DB, limit int, currentUserDID string) ([]FeaturedRepository, error) {
query := `
WITH latest_manifests AS (
SELECT did, repository, MAX(id) as latest_id
@@ -1596,7 +1602,8 @@ func GetFeaturedRepositories(db *sql.DB, limit int) ([]FeaturedRepository, error
COALESCE((SELECT value FROM repository_annotations WHERE did = m.did AND repository = m.repository AND key = 'org.opencontainers.image.description'), ''),
COALESCE((SELECT value FROM repository_annotations WHERE did = m.did AND repository = m.repository AND key = 'io.atcr.icon'), ''),
rs.pull_count,
- rs.star_count
+ rs.star_count,
+ COALESCE((SELECT COUNT(*) FROM stars WHERE starrer_did = ? AND owner_did = m.did AND repository = m.repository), 0)
FROM latest_manifests lm
JOIN manifests m ON lm.latest_id = m.id
JOIN users u ON m.did = u.did
@@ -1605,7 +1612,7 @@ func GetFeaturedRepositories(db *sql.DB, limit int) ([]FeaturedRepository, error
LIMIT ?
`
- rows, err := db.Query(query, limit)
+ rows, err := db.Query(query, currentUserDID, limit)
if err != nil {
return nil, err
}
@@ -1614,11 +1621,13 @@ func GetFeaturedRepositories(db *sql.DB, limit int) ([]FeaturedRepository, error
var featured []FeaturedRepository
for rows.Next() {
var f FeaturedRepository
+ var isStarredInt int
if err := rows.Scan(&f.OwnerDID, &f.OwnerHandle, &f.Repository,
- &f.Title, &f.Description, &f.IconURL, &f.PullCount, &f.StarCount); err != nil {
+ &f.Title, &f.Description, &f.IconURL, &f.PullCount, &f.StarCount, &isStarredInt); err != nil {
return nil, err
}
+ f.IsStarred = isStarredInt > 0
featured = append(featured, f)
}
diff --git a/pkg/appview/handlers/home.go b/pkg/appview/handlers/home.go
index 9394106..6feb6ab 100644
--- a/pkg/appview/handlers/home.go
+++ b/pkg/appview/handlers/home.go
@@ -11,6 +11,7 @@ import (
"atcr.io/pkg/appview/db"
"atcr.io/pkg/appview/holdhealth"
+ "atcr.io/pkg/appview/middleware"
)
// HomeHandler handles the home page
@@ -21,8 +22,14 @@ type HomeHandler struct {
}
func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ // Get current user DID (empty string if not logged in)
+ var currentUserDID string
+ if user := middleware.GetUser(r); user != nil {
+ currentUserDID = user.DID
+ }
+
// Fetch featured repositories (top 6)
- featured, err := db.GetFeaturedRepositories(h.DB, 6)
+ featured, err := db.GetFeaturedRepositories(h.DB, 6, currentUserDID)
if err != nil {
// Log error but continue - featured section will be empty
featured = []db.FeaturedRepository{}
@@ -39,6 +46,7 @@ func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
IconURL: repo.IconURL,
StarCount: repo.StarCount,
PullCount: repo.PullCount,
+ IsStarred: repo.IsStarred,
}
}
@@ -77,7 +85,13 @@ func (h *RecentPushesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
userFilter = r.URL.Query().Get("q")
}
- pushes, total, err := db.GetRecentPushes(h.DB, limit, offset, userFilter)
+ // Get current user DID (empty string if not logged in)
+ var currentUserDID string
+ if user := middleware.GetUser(r); user != nil {
+ currentUserDID = user.DID
+ }
+
+ pushes, total, err := db.GetRecentPushes(h.DB, limit, offset, userFilter, currentUserDID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
diff --git a/pkg/appview/handlers/search.go b/pkg/appview/handlers/search.go
index 06f37b9..d393e52 100644
--- a/pkg/appview/handlers/search.go
+++ b/pkg/appview/handlers/search.go
@@ -8,6 +8,7 @@ import (
"strings"
"atcr.io/pkg/appview/db"
+ "atcr.io/pkg/appview/middleware"
)
// SearchHandler handles the search page
@@ -77,7 +78,13 @@ func (h *SearchResultsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
offset, _ = strconv.Atoi(o)
}
- pushes, total, err := db.SearchPushes(h.DB, query, limit, offset)
+ // Get current user DID (empty string if not logged in)
+ var currentUserDID string
+ if user := middleware.GetUser(r); user != nil {
+ currentUserDID = user.DID
+ }
+
+ pushes, total, err := db.SearchPushes(h.DB, query, limit, offset, currentUserDID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
diff --git a/pkg/appview/handlers/settings.go b/pkg/appview/handlers/settings.go
index c7d9e48..5430281 100644
--- a/pkg/appview/handlers/settings.go
+++ b/pkg/appview/handlers/settings.go
@@ -129,5 +129,5 @@ func (h *UpdateDefaultHoldHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
}
w.Header().Set("Content-Type", "text/html")
- w.Write([]byte(`
✓ Default hold updated successfully!
`))
+ w.Write([]byte(` Default hold updated successfully!
`))
}
diff --git a/pkg/appview/static/css/style.css b/pkg/appview/static/css/style.css
index 1875a87..60c68ea 100644
--- a/pkg/appview/static/css/style.css
+++ b/pkg/appview/static/css/style.css
@@ -24,9 +24,6 @@
/* Button text color */
--btn-text: #ffffff;
- /* Theme toggle icon */
- --theme-icon: '🌙';
-
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05);
--shadow-md: 0 2px 4px rgba(0, 0, 0, 0.1);
@@ -61,7 +58,7 @@
--success: #34d399;
--success-bg: #064e3b;
--warning: #fbbf24;
- --warning-bg: #78350f;
+ --warning-bg: #422006;
--danger: #dc3545;
--danger-bg: #7f1d1d;
--bg: #2a2a2a;
@@ -79,9 +76,6 @@
/* Button text color */
--btn-text: #ffffff;
- /* Theme toggle icon */
- --theme-icon: '☀️';
-
/* Shadows - lighter for dark backgrounds */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 2px 4px rgba(0, 0, 0, 0.4);
@@ -347,23 +341,55 @@ button:hover, .btn:hover, .btn-primary:hover, .btn-secondary:hover {
text-decoration: none;
}
-.theme-toggle-btn::before {
- content: var(--theme-icon);
- font-size: 1.2rem;
- cursor: pointer;
+.theme-toggle-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.theme-toggle-btn .theme-icon {
+ width: 1.25rem;
+ height: 1.25rem;
}
.delete-btn {
- background: var(--danger);
+ background: transparent;
+ border: none;
+ color: var(--danger);
padding: 0.25rem 0.5rem;
font-size: 0.85rem;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ display: inline-flex;
+ align-items: center;
+}
+
+.delete-btn:hover {
+ color: var(--danger);
+}
+
+.delete-btn:hover .lucide {
+ transform: scale(1.2);
}
.copy-btn {
- padding: 0.25rem 0.75rem;
- background: var(--button-primary);
- color: var(--btn-text);
+ padding: 0.25rem 0.5rem;
+ background: transparent;
+ color: var(--secondary);
+ border: none;
font-size: 0.85rem;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ display: inline-flex;
+ align-items: center;
+}
+
+.copy-btn:hover {
+ color: var(--primary);
+}
+
+.copy-btn:hover .lucide {
+ transform: scale(1.2);
}
/* Cards */
@@ -414,6 +440,9 @@ button:hover, .btn:hover, .btn-primary:hover, .btn-secondary:hover {
}
.push-details {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
color: var(--border-dark);
font-size: 0.9rem;
margin-bottom: 0.75rem;
@@ -441,6 +470,54 @@ button:hover, .btn:hover, .btn-primary:hover, .btn-secondary:hover {
gap: 0.5rem;
}
+/* Docker command component */
+.docker-command {
+ display: inline-flex;
+ position: relative;
+ align-items: center;
+ gap: 0.5rem;
+ background: var(--code-bg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 0.75rem;
+ margin: 0.5rem 0;
+ max-width: 100%;
+}
+
+.docker-command-icon {
+ width: 1.25rem;
+ height: 1.25rem;
+ color: var(--secondary);
+ flex-shrink: 0;
+}
+
+.docker-command-text {
+ font-family: 'Monaco', 'Courier New', monospace;
+ font-size: 0.85rem;
+ color: var(--fg);
+ flex: 0 1 auto;
+ word-break: break-all;
+}
+
+.docker-command .copy-btn {
+ position: absolute;
+ right: 0.5rem;
+ top: 50%;
+ transform: translateY(-50%);
+ background: linear-gradient(to right, transparent, var(--code-bg) 30%);
+ padding: 0.5rem;
+ padding-left: 1.5rem;
+ border-radius: 4px;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.2s, visibility 0.2s;
+}
+
+.docker-command:hover .copy-btn {
+ opacity: 1;
+ visibility: visible;
+}
+
/* Digest tooltip on hover - using title attribute for native browser tooltip */
.digest {
cursor: default;
@@ -449,23 +526,41 @@ button:hover, .btn:hover, .btn-primary:hover, .btn-secondary:hover {
/* Digest copy button */
.digest-copy-btn {
background: transparent;
- border: 1px solid var(--border);
+ border: none;
color: var(--secondary);
padding: 0.1rem 0.4rem;
- font-size: 0.75rem;
- border-radius: 3px;
cursor: pointer;
- transition: all 0.2s;
+ transition: all 0.2s ease;
display: inline-flex;
align-items: center;
}
.digest-copy-btn:hover {
- background: var(--hover-bg);
- border-color: var(--primary);
color: var(--primary);
}
+.digest-copy-btn:hover .lucide {
+ transform: scale(1.2);
+}
+
+.digest-copy-btn .lucide {
+ width: 0.875rem;
+ height: 0.875rem;
+ transition: transform 0.2s ease;
+}
+
+.delete-btn .lucide {
+ width: 1rem;
+ height: 1rem;
+ transition: transform 0.2s ease;
+}
+
+.copy-btn .lucide {
+ width: 1rem;
+ height: 1rem;
+ transition: transform 0.2s ease;
+}
+
.separator {
color: var(--border);
}
@@ -538,11 +633,22 @@ button:hover, .btn:hover, .btn-primary:hover, .btn-secondary:hover {
.push-stat .star-icon {
color: var(--star);
font-size: 1rem;
+ width: 1rem;
+ height: 1rem;
+ stroke: var(--star);
+ fill: none;
+}
+
+.push-stat .star-icon.star-filled {
+ fill: var(--star);
}
.push-stat .pull-icon {
color: var(--primary);
font-size: 1rem;
+ width: 1rem;
+ height: 1rem;
+ stroke: var(--primary);
}
.push-stat .stat-count {
@@ -859,11 +965,36 @@ a.license-badge:hover {
margin: 1rem 0;
}
+.note a {
+ color: var(--warning);
+ text-decoration: underline;
+ font-weight: 500;
+}
+
+.note a:hover {
+ color: var(--primary);
+}
+
+.note a:visited {
+ color: var(--warning);
+}
+
.success {
background: var(--success-bg);
border-left: 4px solid var(--success);
padding: 1rem;
margin: 1rem 0;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.success .lucide {
+ width: 1.25rem;
+ height: 1.25rem;
+ color: var(--success);
+ stroke: var(--success);
+ flex-shrink: 0;
}
.error {
@@ -1075,11 +1206,30 @@ a.license-badge:hover {
background: var(--hover-bg);
}
+/* Lucide icon base styles */
+.lucide {
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ vertical-align: middle;
+ stroke-width: 2;
+ transition: transform 0.2s ease;
+}
+
+/* Star icon styles */
.star-icon {
font-size: 1.25rem;
line-height: 1;
transition: transform 0.2s ease;
- color:var(--star);
+ color: var(--star);
+ width: 1.25rem;
+ height: 1.25rem;
+ stroke: var(--star);
+ fill: none;
+}
+
+.star-icon.star-filled {
+ fill: var(--star);
}
.star-btn:hover:not(:disabled) .star-icon {
@@ -1193,7 +1343,9 @@ a.license-badge:hover {
/* Offline manifest badge */
.offline-badge {
- display: inline-block;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
padding: 0.25rem 0.5rem;
background: var(--warning-bg);
color: var(--warning);
@@ -1204,9 +1356,16 @@ a.license-badge:hover {
margin-left: 0.5rem;
}
+.offline-badge .lucide {
+ width: 0.875rem;
+ height: 0.875rem;
+}
+
/* Checking manifest badge (health check in progress) */
.checking-badge {
- display: inline-block;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
padding: 0.25rem 0.5rem;
background: #e3f2fd;
color: #1976d2;
@@ -1217,6 +1376,11 @@ a.license-badge:hover {
margin-left: 0.5rem;
}
+.checking-badge .lucide {
+ width: 0.875rem;
+ height: 0.875rem;
+}
+
/* Hide offline manifests by default */
.manifest-item[data-reachable="false"] {
display: none;
@@ -1295,6 +1459,11 @@ a.license-badge:hover {
color: var(--secondary);
}
+.manifest-type .lucide {
+ width: 0.95rem;
+ height: 0.95rem;
+}
+
.platform-count {
color: var(--border-dark);
font-size: 0.85rem;
@@ -1431,11 +1600,22 @@ a.license-badge:hover {
.featured-stat .star-icon {
color: var(--star);
font-size: 1.1rem;
+ width: 1.1rem;
+ height: 1.1rem;
+ stroke: var(--star);
+ fill: none;
+}
+
+.featured-stat .star-icon.star-filled {
+ fill: var(--star);
}
.featured-stat .pull-icon {
color: var(--primary);
font-size: 1.1rem;
+ width: 1.1rem;
+ height: 1.1rem;
+ stroke: var(--primary);
}
.featured-stat .stat-count {
@@ -1599,6 +1779,14 @@ a.license-badge:hover {
line-height: 1;
}
+.benefit-icon .lucide {
+ width: 3rem;
+ height: 3rem;
+ stroke-width: 1.5;
+ color: var(--primary);
+ stroke: var(--primary);
+}
+
.benefit-card h3 {
font-size: 1.2rem;
margin-bottom: 0.75rem;
@@ -1634,6 +1822,20 @@ a.license-badge:hover {
font-size: 1.1rem;
}
+.install-section a {
+ color: var(--primary);
+ text-decoration: underline;
+ font-weight: 500;
+}
+
+.install-section a:hover {
+ color: var(--primary-dark);
+}
+
+.install-section a:visited {
+ color: var(--primary);
+}
+
.code-block {
background: var(--code-bg);
border: 1px solid var(--border);
diff --git a/pkg/appview/static/js/app.js b/pkg/appview/static/js/app.js
index d95ecfe..fdebac1 100644
--- a/pkg/appview/static/js/app.js
+++ b/pkg/appview/static/js/app.js
@@ -19,6 +19,19 @@ function updateThemeIcon() {
if (!themeBtn) return;
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
+ const icon = themeBtn.querySelector('.theme-icon');
+
+ if (icon) {
+ // In dark mode, show sun icon (to switch to light)
+ // In light mode, show moon icon (to switch to dark)
+ icon.setAttribute('data-lucide', currentTheme === 'dark' ? 'sun' : 'moon');
+
+ // Re-initialize Lucide icons
+ if (typeof lucide !== 'undefined') {
+ lucide.createIcons();
+ }
+ }
+
themeBtn.setAttribute('aria-label', currentTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode');
}
@@ -26,11 +39,19 @@ function updateThemeIcon() {
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
// Show success feedback
- const btn = event.target;
- const originalText = btn.textContent;
- btn.textContent = '✓ Copied!';
+ const btn = event.target.closest('button');
+ const originalHTML = btn.innerHTML;
+ btn.innerHTML = ' Copied!';
+ // Re-initialize Lucide icons for the new icon
+ if (typeof lucide !== 'undefined') {
+ lucide.createIcons();
+ }
setTimeout(() => {
- btn.textContent = originalText;
+ btn.innerHTML = originalHTML;
+ // Re-initialize Lucide icons to restore original icon
+ if (typeof lucide !== 'undefined') {
+ lucide.createIcons();
+ }
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
@@ -75,7 +96,10 @@ function updateTimestamps() {
}
// Initial timestamp update
-document.addEventListener('DOMContentLoaded', updateTimestamps);
+document.addEventListener('DOMContentLoaded', () => {
+ updateTimestamps();
+ updateThemeIcon();
+});
// Update timestamps after HTMX swaps
document.addEventListener('htmx:afterSwap', updateTimestamps);
@@ -90,10 +114,15 @@ function toggleRepo(name) {
if (details.style.display === 'none') {
details.style.display = 'block';
- btn.textContent = '▲';
+ btn.innerHTML = '';
} else {
details.style.display = 'none';
- btn.textContent = '▼';
+ btn.innerHTML = '';
+ }
+
+ // Re-initialize Lucide icons
+ if (typeof lucide !== 'undefined') {
+ lucide.createIcons();
}
}
@@ -154,7 +183,7 @@ async function toggleStar(handle, repository) {
try {
// Check current state
- const isStarred = starIcon.textContent === '★';
+ const isStarred = starIcon.classList.contains('star-filled');
const method = isStarred ? 'DELETE' : 'POST';
const url = `/api/stars/${handle}/${repository}`;
@@ -180,13 +209,13 @@ async function toggleStar(handle, repository) {
// Update UI optimistically
if (data.starred) {
- starIcon.textContent = '★';
+ starIcon.classList.add('star-filled');
starBtn.classList.add('starred');
// Optimistically increment count
const currentCount = parseInt(starCountEl.textContent) || 0;
starCountEl.textContent = currentCount + 1;
} else {
- starIcon.textContent = '☆';
+ starIcon.classList.remove('star-filled');
starBtn.classList.remove('starred');
// Optimistically decrement count
const currentCount = parseInt(starCountEl.textContent) || 0;
@@ -229,7 +258,7 @@ async function loadStarStatus() {
const starData = await starResponse.json();
console.log('Star status data:', starData);
if (starData.starred) {
- starIcon.textContent = '★';
+ starIcon.classList.add('star-filled');
starBtn.classList.add('starred');
}
} else {
diff --git a/pkg/appview/templates/components/docker-command.html b/pkg/appview/templates/components/docker-command.html
new file mode 100644
index 0000000..38511db
--- /dev/null
+++ b/pkg/appview/templates/components/docker-command.html
@@ -0,0 +1,15 @@
+{{ define "docker-command" }}
+{{/*
+ Docker command component - displays a docker command with icon and copy button
+
+ Expects: string - the docker command to display
+ Usage: {{ template "docker-command" "docker pull atcr.io/alice/myapp:latest" }}
+*/}}
+
+
+ {{ . }}
+
+
+{{ end }}
diff --git a/pkg/appview/templates/components/head.html b/pkg/appview/templates/components/head.html
index dc1bfa4..2107bf0 100644
--- a/pkg/appview/templates/components/head.html
+++ b/pkg/appview/templates/components/head.html
@@ -15,6 +15,20 @@
+
+
+
+
{{ end }}
diff --git a/pkg/appview/templates/components/nav-theme-toggle.html b/pkg/appview/templates/components/nav-theme-toggle.html
index 64d47d6..cdb74c0 100644
--- a/pkg/appview/templates/components/nav-theme-toggle.html
+++ b/pkg/appview/templates/components/nav-theme-toggle.html
@@ -1,3 +1,5 @@
{{ define "nav-theme-toggle" }}
-
+
{{ end }}
diff --git a/pkg/appview/templates/components/repo-card.html b/pkg/appview/templates/components/repo-card.html
index f76ae21..dc66a95 100644
--- a/pkg/appview/templates/components/repo-card.html
+++ b/pkg/appview/templates/components/repo-card.html
@@ -31,11 +31,11 @@
- ★
+
{{ .StarCount }}
- ↓
+
{{ .PullCount }}
diff --git a/pkg/appview/templates/pages/home.html b/pkg/appview/templates/pages/home.html
index 2ba0e61..ca49e2a 100644
--- a/pkg/appview/templates/pages/home.html
+++ b/pkg/appview/templates/pages/home.html
@@ -39,17 +39,17 @@
-
🐳
+
Works with Docker
Use docker push & pull. No new tools to learn.
-
⚓
+
Your Data
Join shared holds or captain your own storage.
-
🧭
+
Discover Images
Browse and star public container registries.
diff --git a/pkg/appview/templates/pages/repository.html b/pkg/appview/templates/pages/repository.html
index 58c5e24..5c7a156 100644
--- a/pkg/appview/templates/pages/repository.html
+++ b/pkg/appview/templates/pages/repository.html
@@ -34,7 +34,7 @@
@@ -81,19 +81,9 @@
Pull this image
{{ if .Tags }}
{{ $firstTag := index .Tags 0 }}
-
- docker pull {{ $.RegistryURL }}/{{ $.Owner.Handle }}/{{ $.Repository.Name }}:{{ $firstTag.Tag.Tag }}
-
-
+ {{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" $firstTag.Tag.Tag) }}
{{ else }}
-
- docker pull {{ $.RegistryURL }}/{{ $.Owner.Handle }}/{{ $.Repository.Name }}:latest
-
-
+ {{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":latest") }}
{{ end }}
@@ -137,7 +127,7 @@
hx-confirm="Delete tag {{ .Tag.Tag }}?"
hx-target="#tag-{{ .Tag.Tag }}"
hx-swap="outerHTML">
- 🗑️
+
{{ end }}
@@ -146,7 +136,7 @@
{{ .Tag.Digest }}
-
+
{{ if .Platforms }}
@@ -157,12 +147,7 @@
{{ end }}
-
- docker pull {{ $.RegistryURL }}/{{ $.Owner.Handle }}/{{ $.Repository.Name }}:{{ .Tag.Tag }}
-
-
+ {{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" .Tag.Tag) }}
{{ end }}
@@ -187,23 +172,23 @@
- ★
+
{{ .StarCount }}
- ↓
+
{{ .PullCount }}
@@ -35,9 +35,8 @@
{{ .Digest }}
-
+
-
•