GitBrowser/Components/Pages/Home.razor
+35
-0
diff --git a/GitBrowser/Components/Pages/Home.razor b/GitBrowser/Components/Pages/Home.razor
new file mode 100644
index 0000000..66b7d84
@@ -0,0 +1,35 @@
@page "/"
<PageTitle>Repositories</PageTitle>
<div class="container">
<div class="header">
<h2 class="title">
<span>Repositories</span>
<span class="repo-count">@repositories.Count @(repositories.Count == 1 ? "repository" : "repositories")</span>
</h2>
</div>
@if (!repositories.Any())
{
<div class="empty-state">
<p>No repositories found.</p>
</div>
}
else
{
<div class="repo-list">
@foreach (var repo in repositories)
{
<a href="/@Uri.EscapeDataString(repo.Name)" class="repo-item">
<div class="repo-info">
<h3 class="repo-name">@repo.Name</h3>
<div class="repo-meta">
Updated @GetRelativeTime(repo.LastCommitDate)
</div>
</div>
</a>
}
</div>
}
</div>
GitBrowser/Components/Pages/Home.razor.cs
+79
-0
diff --git a/GitBrowser/Components/Pages/Home.razor.cs b/GitBrowser/Components/Pages/Home.razor.cs
new file mode 100644
index 0000000..77f0b5b
@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GitBrowser.Services;
using Microsoft.AspNetCore.Components;
namespace GitBrowser.Components.Pages;
public partial class Home : IDisposable
{
[Inject]
public required RepositoryService RepositoryService { get; set; }
private List<RepoListItem> repositories = [];
private class RepoListItem
{
public string Name { get; set; } = "";
public DateTimeOffset LastCommitDate { get; set; }
public RepositoryInformation RepositoryInfo { get; set; } = null!;
}
protected override void OnInitialized()
{
var allRepos = RepositoryService.GetAllRepositories();
foreach (var repoInfo in allRepos)
{
try
{
var lastCommit = repoInfo.Repository.Head?.Tip;
if (lastCommit != null)
{
repositories.Add(
new RepoListItem
{
Name = repoInfo.Name,
LastCommitDate = lastCommit.Author.When,
RepositoryInfo = repoInfo,
}
);
}
}
catch
{
// Skip repositories with errors
}
}
// Sort by latest commit (most recent first)
repositories = repositories.OrderByDescending(r => r.LastCommitDate).ToList();
}
private string GetRelativeTime(DateTimeOffset when)
{
var now = DateTimeOffset.Now;
var diff = now - when;
if (diff.TotalSeconds < 60)
return "just now";
if (diff.TotalMinutes < 60)
return $"{(int)diff.TotalMinutes} minute{((int)diff.TotalMinutes == 1 ? "" : "s")} ago";
if (diff.TotalHours < 24)
return $"{(int)diff.TotalHours} hour{((int)diff.TotalHours == 1 ? "" : "s")} ago";
if (diff.TotalDays < 30)
return $"{(int)diff.TotalDays} day{((int)diff.TotalDays == 1 ? "" : "s")} ago";
if (diff.TotalDays < 365)
return $"{(int)(diff.TotalDays / 30)} month{((int)(diff.TotalDays / 30) == 1 ? "" : "s")} ago";
return $"{(int)(diff.TotalDays / 365)} year{((int)(diff.TotalDays / 365) == 1 ? "" : "s")} ago";
}
public void Dispose()
{
foreach (var repo in repositories)
{
repo.RepositoryInfo.Repository.Dispose();
}
}
}
GitBrowser/Components/Pages/Home.razor.css
+76
-0
diff --git a/GitBrowser/Components/Pages/Home.razor.css b/GitBrowser/Components/Pages/Home.razor.css
new file mode 100644
index 0000000..bce33bd
@@ -0,0 +1,76 @@
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
.header {
margin-bottom: 1.5rem;
}
.title {
display: flex;
align-items: baseline;
justify-content: space-between;
font-size: 1.75rem;
font-weight: 600;
color: var(--gh-color-fg-default);
}
.repo-count {
font-size: 0.875rem;
color: var(--gh-color-fg-muted);
}
.empty-state {
padding: 3rem;
text-align: center;
color: var(--gh-color-fg-muted);
background: var(--gh-color-canvas-subtle);
border: 1px solid var(--gh-color-border-default);
border-radius: 6px;
}
.repo-list {
display: flex;
flex-direction: column;
gap: 0;
}
.repo-item {
display: block;
padding: 1.5rem;
border-bottom: 1px solid var(--gh-color-border-default);
text-decoration: none;
transition: background-color 0.2s;
}
.repo-item:hover {
background: var(--gh-color-canvas-subtle);
}
.repo-item:first-child {
border-top: 1px solid var(--gh-color-border-default);
}
.repo-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.repo-name {
font-size: 1.25rem;
font-weight: 600;
color: var(--gh-color-accent-fg);
margin: 0;
}
.repo-item:hover .repo-name {
text-decoration: underline;
}
.repo-meta {
font-size: 0.875rem;
color: var(--gh-color-fg-muted);
}
GitBrowser/Services/RepositoryService.cs
+13
-9
diff --git a/GitBrowser/Services/RepositoryService.cs b/GitBrowser/Services/RepositoryService.cs
index 7cd234b..38eff31 100644
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using LibGit2Sharp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@@ -30,24 +32,26 @@ public sealed partial class RepositoryService(IOptions<RepositoryConfiguration>
repositoryInformation = null;
return false;
}
if (!Repository.IsValid(repositoryDirectory))
if (!IsValidRepository(repositoryDirectory))
{
repositoryInformation = null;
return false;
}
if (!File.Exists(Path.Combine(repositoryDirectory, "git-daemon-export-ok")))
{
repositoryInformation = null;
return false;
}
var prettyName = StripGitEnding(Path.GetFileName(repositoryDirectory));
var repository = new Repository(repositoryDirectory);
repositoryInformation = new(prettyName, repository);
repositoryInformation = CreateRepositoryInformation(repositoryDirectory);
return true;
}
public IEnumerable<RepositoryInformation> GetAllRepositories() =>
Directory.GetDirectories(repositoriesPath).Where(IsValidRepository).Select(CreateRepositoryInformation);
private static string StripGitEnding(string name) =>
name.EndsWith(".git", StringComparison.OrdinalIgnoreCase) ? name[..^4] : name;
private static bool IsValidRepository(string path) =>
Repository.IsValid(path) && File.Exists(Path.Combine(path, "git-daemon-export-ok"));
private static RepositoryInformation CreateRepositoryInformation(string path) =>
new(StripGitEnding(Path.GetFileName(path)), new(path));
}
public sealed record RepositoryInformation(string Name, Repository Repository);